Files
mesh-delivery/modules/mesh-delivery-builder/internal/build/step.go
T
jschoubben 71a35a6376 MV1: move contracts and sdk under modules/, importers follow
The delivery repository's contracts and SDK live under modules/ as
mesh-delivery-contracts and mesh-delivery-sdk, their Go module paths
renamed to match the folders (the TypeScript names already did), and
every importer in mesh-delivery, mesh-catalog, mesh-host, mesh-tools
and mesh-sdk follows in the same group: a design as it should be, one
path per module, checked by the group's check building every importer.

Builds: design 47 G §427, §88

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-12 05:09:56 +01:00

152 lines
5.7 KiB
Go

package build
import (
"context"
"fmt"
"strings"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshbuilds"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog"
)
// **A candidate's build is the runner's step ask** (design 47 G A-97, A-84, A-99): the runner's asking half asks the seat
// mesh-builds a `build` step naming each entry by fingerprint, and the builder, its one entry point for a candidate, reads each
// entry through the seat mesh-catalog's `entry` (the repository and the commit are the member's head), builds it as it builds any
// fingerprint (the existing claim, plan and lanes) and answers the step's end. A build is per fingerprint, so no pull request
// number is needed, and the builder follows no candidate.
// BuildStep builds the entries of one step ask and answers its end: passed when every entry's build is done, failed when one
// failed (said), could not run when an entry could not be read, nothing to run for an ask naming none. The ask's id and the node
// are the caller's to set.
func (b *Builder) BuildStep(ctx context.Context, run meshbuilds.StepRun) meshbuilds.StepEnded {
b.init()
started := b.now()
end := meshbuilds.StepEnded{V: 1, Pipeline: run.Pipeline, Step: meshbuilds.StepEndedStepBuild, Rerun: run.Rerun, By: run.By, Entries: run.Entries, Node: b.Node, Started: &started}
finish := func(outcome, said string) meshbuilds.StepEnded {
at := b.now()
end.Ended, end.Outcome, end.Said = &at, outcome, said
return end
}
if len(run.Entries) == 0 {
return finish(meshbuilds.StepEndedOutcomeNothingToRun, "the ask names no entry")
}
if b.StepEntry == nil {
return finish(meshbuilds.StepEndedOutcomeCouldNotRun, "the builder reads no entry of the seat mesh-catalog")
}
var failed []string
for _, e := range run.Entries {
outcome, said := b.buildEntry(ctx, e)
switch outcome {
case meshbuilds.StepEndedOutcomeCouldNotRun:
return finish(outcome, said)
case meshbuilds.StepEndedOutcomeFailed:
failed = append(failed, said)
}
}
if len(failed) > 0 {
return finish(meshbuilds.StepEndedOutcomeFailed, strings.Join(failed, "; "))
}
return finish(meshbuilds.StepEndedOutcomePassed, "")
}
// buildEntry builds one entry at its fingerprint and waits for its end.
func (b *Builder) buildEntry(ctx context.Context, e meshbuilds.BuildEntry) (outcome, said string) {
entry, err := b.StepEntry(ctx, e.Fingerprint)
if err != nil {
return meshbuilds.StepEndedOutcomeCouldNotRun, fmt.Sprintf("the entry %s at %s cannot be read: %v", e.Module, e.Fingerprint, err)
}
if entry.Module != e.Module || (e.Commit != "" && entry.Commit != e.Commit) {
return meshbuilds.StepEndedOutcomeCouldNotRun, fmt.Sprintf("the entry of %s at %s is of %s at %s", e.Module, e.Fingerprint, entry.Module, entry.Commit)
}
key, err := builds.BuilderBuildKey(entry.Module, entry.Fp)
if err != nil {
return meshbuilds.StepEndedOutcomeCouldNotRun, err.Error()
}
ended := b.awaitEnd(key)
defer b.dropEnd(key, ended)
if done, failedWords, found := b.keyEnd(ctx, key); found {
if done {
return meshbuilds.StepEndedOutcomePassed, ""
}
return meshbuilds.StepEndedOutcomeFailed, failedWords
}
m := meshcatalog.ModulesByModuleValue{Repository: entry.Repository, Commit: entry.Commit, Fp2: entry.Fp, Manifest: entry.ManifestDigest, Path: entry.Path,
Toolchains: CatalogToolchains(b.agreedFor(ctx, entry.ManifestDigest)), Packages: entry.Packages}
b.consider(ctx, entry.Module, m)
select {
case log := <-ended:
if log.Outcome == builds.BuildDone {
return meshbuilds.StepEndedOutcomePassed, ""
}
return meshbuilds.StepEndedOutcomeFailed, fmt.Sprintf("%s at %s: %s %s", log.Module, log.FP, log.Outcome, log.Error)
case <-ctx.Done():
return meshbuilds.StepEndedOutcomeCouldNotRun, fmt.Sprintf("%s at %s: the builder stopped before its build ended", entry.Module, entry.Fp)
}
}
// keyEnd reads the build key: whether it is done, and its error when it failed for good; found only for a key that has ended.
func (b *Builder) keyEnd(ctx context.Context, key string) (done bool, failed string, found bool) {
raw, _, ok, err := b.Claimer.Store.Get(ctx, key)
if err != nil || !ok {
return false, "", false
}
v, err := builds.DecodeBuilderBuild(raw)
if err != nil {
return false, "", false
}
switch {
case v.State == builds.BuildDone && !Unpublished(v, b.Seats):
return true, "", true
case v.State == builds.BuildFailed && builds.FailedUntilChanged(v.Error):
return false, key + ": " + v.Error, true
}
return false, "", false
}
// awaitEnd registers a wait for the end of the build of key, heard at its log entry (logBuild).
func (b *Builder) awaitEnd(key string) chan builds.BuildLogEntry {
ch := make(chan builds.BuildLogEntry, 1)
b.mu.Lock()
if b.stepWaits == nil {
b.stepWaits = map[string][]chan builds.BuildLogEntry{}
}
b.stepWaits[key] = append(b.stepWaits[key], ch)
b.mu.Unlock()
return ch
}
func (b *Builder) dropEnd(key string, ch chan builds.BuildLogEntry) {
b.mu.Lock()
defer b.mu.Unlock()
for i, c := range b.stepWaits[key] {
if c == ch {
b.stepWaits[key] = append(b.stepWaits[key][:i], b.stepWaits[key][i+1:]...)
break
}
}
if len(b.stepWaits[key]) == 0 {
delete(b.stepWaits, key)
}
}
// stepEnded tells the step asks waiting for a build that its end was logged.
func (b *Builder) stepEnded(e builds.BuildLogEntry) {
if e.Event != builds.BuildEndedEvent {
return
}
key, err := builds.BuilderBuildKey(e.Module, e.FP)
if err != nil {
return
}
b.mu.Lock()
chans := append([]chan builds.BuildLogEntry(nil), b.stepWaits[key]...)
b.mu.Unlock()
for _, ch := range chans {
select {
case ch <- e:
default:
}
}
}