package build import ( "context" "fmt" "strings" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" ) // **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: } } }