package builder import ( "context" "errors" "fmt" "os" "path/filepath" "time" "github.com/novox/mesh-controller/internal/catalogue" ) // The builds a merge check makes for its merge (novox/hq ADR 0350 §1): every artifact of every module the delivery // plan moves, built from the merged tree the check judged into the scratch namespace, kept under its reuse key, so // the walk of the merge commit takes it instead of building again. Built after the check has judged, and only when // it did not fail: a change that fails is not merged, and nothing it built would be taken. // CheckBuildSpec is one module a check builds for its merge, in the walk's order (Tier). type CheckBuildSpec struct { Module string // Repository, Path and Ref are where it is built from; Merged says from the merge the check made instead — the // module is built from the repository checked, at the branch merged into. Repository string Path string Ref string Merged bool Tier int // Skip says why it cannot be built as the walk would build it: not built, and nothing after it. Skip string } // CheckBuilding is what a check builds with: where it publishes and keeps its builds, and what a build is handed. type CheckBuilding struct { Publish Publisher Scratch Scratch Held map[string]string Npmrc Npmrc Seats map[string]string // Named are the builds the build seat's verdicts of earlier checks of this pull request at this head name // (novox/hq issue 703): a build kept under a module's key is kept again, instead of built, only when one of them // names its key and every digest, and the build is this pull request's at this head. The record alone is never // enough. Named []NamedBuild } // CheckBuilt is one module a check built for its merge, as its verdict lists it (§1): its key and its artifacts, or // why it was not built. type CheckBuilt struct { Module string Key string Made []catalogue.Built // NotBuilt says why it was not: no key, a build that failed, or a module after one of those. NotBuilt string Took time.Duration // Kept is the check whose build this check kept instead of building, its key unchanged (novox/hq issue 703): // the verdict names that build, by key and digest, as that check's. Empty when this check built it. Kept string } // skipsTheBuilds is why a check builds nothing for its merge; empty when it builds. func skipsTheBuilds(v CheckVerdict) string { for _, l := range []*Layer{v.Gate, v.Repo} { if l != nil && (l.Verdict == "fail" || l.Verdict == "error") { return "the check did not pass, so nothing is built for its merge" } } return "" } // buildForTheMerge builds each module the plan moves, tier by tier, into the scratch namespace, and keeps what it // built under its keys. A module standing after one not built is not built either: what it stands on in the walk // is not what it would stand on here. tree is the check's clone and merge the merge commit it made there. func buildForTheMerge(ctx context.Context, run Runner, spec CheckSpec, with CheckBuilding, workspace, tree, merge string, judged CheckVerdict, forge GitCredential, log Log, say func(step, format string, args ...any)) []CheckBuilt { if len(spec.Builds) == 0 || with.Publish == nil || with.Scratch == nil { return nil } ref := merge if ref == "" { ref = spec.Ref } held := map[string]string{} for k, v := range with.Held { held[k] = v } var out []CheckBuilt var kept []CheckBuild stopAt, stopWhy := -1, "" for _, b := range spec.Builds { began := time.Now() if stopAt >= 0 && b.Tier > stopAt { out = append(out, CheckBuilt{Module: b.Module, NotBuilt: stopWhy}) continue } if b.Skip != "" { say("build", "%s not built for the merge: %s", b.Module, b.Skip) out = append(out, CheckBuilt{Module: b.Module, NotBuilt: b.Skip}) if stopAt < 0 || b.Tier < stopAt { stopAt, stopWhy = b.Tier, "it stands after "+b.Module+", which was not built for the merge" } continue } repository, at := b.Repository, b.Ref if b.Merged { repository, at = tree, ref } say("build", "for the merge: %s (tier %d)", b.Module, b.Tier) var mirrored []string result, err := build(ctx, run, scratchPublisher{to: with.Publish}, repository, b.Path, at, filepath.Join(workspace, "for-the-merge"), held, with.Npmrc, forge, log, &mirrored, &Reuse{keyFirst: true, Scratch: with.Scratch, Named: with.Named, pull: PullRef{Owner: spec.Owner, Repo: spec.Repo, Number: spec.Number}, head: judged.Head}, with.Seats) built := CheckBuilt{Module: b.Module, Key: result.ReuseKey, Took: time.Since(began)} switch { case errors.Is(err, errNoKey): built.NotBuilt = err.Error() case err != nil: built.NotBuilt = "its build failed: " + err.Error() case result.ReuseKey == "": built.NotBuilt = "it has no reuse key: " + result.NoKey } if built.NotBuilt != "" { say("build", "%s not built for the merge: %s", b.Module, built.NotBuilt) out = append(out, built) if stopAt < 0 || b.Tier < stopAt { stopAt, stopWhy = b.Tier, "it stands after "+b.Module+", which was not built for the merge" } continue } built.Made = result.Built for _, m := range result.Built { held[b.Module+"/"+m.Name] = m.Reference } if result.Reused != nil { // **Kept, not built** (novox/hq issue 703): the earlier check's build, named as that check's, so the // walk takes it as the verdict names it; this check's document names it too, so it stays while this // check does (ADR 0350 §5). built.Kept = result.Reused.From.Check say("build", "%s kept for the merge under %s: %s", b.Module, twelve(result.ReuseKey[3:]), result.Reused.From.Said()) out = append(out, built) kept = append(kept, result.Reused.From) continue } say("build", "%s built for the merge under %s (%s)", b.Module, twelve(result.ReuseKey[3:]), built.Took.Round(time.Second)) out = append(out, built) if len(result.Built) > 0 { kept = append(kept, CheckBuild{Key: result.ReuseKey, Module: b.Module, Parts: result.ReuseParts, Made: result.Built, Pull: PullRef{Owner: spec.Owner, Repo: spec.Repo, Number: spec.Number}, Head: judged.Head, Trunk: judged.Trunk, Check: spec.ID, At: time.Now().UTC()}) } } _ = os.RemoveAll(filepath.Join(workspace, "for-the-merge")) if len(kept) > 0 { if err := with.Scratch.Keep(ctx, PullRef{Owner: spec.Owner, Repo: spec.Repo, Number: spec.Number}, spec.ID, time.Now().UTC(), kept); err != nil { // Built and not findable: the walk builds, and says no check built it. Said, never the check's verdict. say("build", "the builds for the merge could not be kept, and the walk will build: %v", err) if err := with.Scratch.Discard(ctx, kept); err != nil { say("build", "and what they made could not all be let go of: %v", err) } for i := range out { if out[i].NotBuilt == "" { out[i].NotBuilt = fmt.Sprintf("built, and could not be kept: %v", err) } } } } return out }