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; workspace // is where each build's working copy is made, a directory the host's container engine sees at the same path — the // builder's own workspace, never a temporary directory of its container (novox/hq issue 713). 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 } // What an earlier check left, ended before it could remove it, is not built on. _ = os.RemoveAll(filepath.Join(workspace, "for-the-merge")) 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 }