diff --git a/cmd/mesh-builder/main.go b/cmd/mesh-builder/main.go index 32ec20cb..fc418367 100644 --- a/cmd/mesh-builder/main.go +++ b/cmd/mesh-builder/main.go @@ -331,7 +331,7 @@ func answer(ctx context.Context, publisher builder.Publisher, on, workspace stri result.Check = &link.CheckOutcome{Verdict: v.Verdict, Summary: v.Summary, Report: v.Report, Took: v.Took.Round(time.Second).String(), Gate: layerOf(v.Gate), RepoCheck: layerOf(v.Repo), Head: v.Head, Trunk: v.Trunk, Beside: v.Beside, Built: checkBuiltOf(v.Built), - Release: v.Release, JudgedBy: v.JudgedBy, BesideAt: v.BesideAt} + Release: v.Release, JudgedBy: v.JudgedBy, BesideAt: v.BesideAt, Validator: v.Validator} // What each of a group's heads was judged as (novox/hq ADR 0323 §6). for _, m := range v.Members { result.Check.Members = append(result.Check.Members, link.CheckedMember{Owner: m.Owner, Repo: m.Repo, diff --git a/cmd/mesh-controller/checks.go b/cmd/mesh-controller/checks.go index 3fa4a882..e51c97c5 100644 --- a/cmd/mesh-controller/checks.go +++ b/cmd/mesh-controller/checks.go @@ -576,6 +576,7 @@ func checkedOf(result link.BuildResult) link.Checked { c.Trunk, c.Beside, c.Took = result.Check.Trunk, result.Check.Beside, result.Check.Took c.Built = result.Check.Built c.Release, c.JudgedBy, c.BesideAt = result.Check.Release, result.Check.JudgedBy, result.Check.BesideAt + c.Validator = result.Check.Validator if c.Gate == nil { // A build seat from before the layers: its verdict is the gate's. c.Gate = &link.CheckLayer{Verdict: c.Verdict, Summary: c.Summary} diff --git a/cmd/mesh-controller/release_branch_test.go b/cmd/mesh-controller/release_branch_test.go index 3147b3d5..e6ddf276 100644 --- a/cmd/mesh-controller/release_branch_test.go +++ b/cmd/mesh-controller/release_branch_test.go @@ -156,12 +156,40 @@ func TestAReleaseBranchVerdictNamesTheBranchTheJudgeAndWhatItRead(t *testing.T) c := checkedOf(link.BuildResult{ID: "b", Ref: "aaaaaaaaaaaa0000", Checked: asked, Check: &link.CheckOutcome{ Verdict: "pass", Head: "aaaaaaaaaaaa", Trunk: "111111111111", Gate: &link.CheckLayer{Verdict: "pass"}, RepoCheck: &link.CheckLayer{Verdict: "pass"}, Release: aRelease, JudgedBy: "fcdecb436117", - Beside: map[string]string{"mesh-host": "222222222222", "mesh-lab": "333333333333"}, - BesideAt: map[string]string{"mesh-host": aRelease, "mesh-lab": "main"}}}) + Beside: map[string]string{"mesh-host": "222222222222", "mesh-lab": "333333333333"}, + BesideAt: map[string]string{"mesh-host": aRelease, "mesh-lab": "main"}, + Validator: "mesh-host's validator of " + aRelease + " at 222222222222"}}) if c.Release != aRelease || c.JudgedBy != "fcdecb436117" { t.Fatalf("the verdict names release %q judged by %q", c.Release, c.JudgedBy) } - if c.BesideAt["mesh-host"] != aRelease || c.BesideAt["mesh-lab"] != "main" || c.Beside["mesh-host"] != "222222222222" { + if c.BesideAt["mesh-host"] != aRelease || c.BesideAt["mesh-lab"] != "main" || c.Beside["mesh-host"] != "222222222222" || + c.Validator != "mesh-host's validator of "+aRelease+" at 222222222222" { t.Fatalf("the verdict names what it read beside it as %v at %v", c.Beside, c.BesideAt) } } + +// **Clause 5** (ADR 0358 §5): a release branch's own pull request into its trunk is no release-branch check: the forge +// is asked nothing, the request names no release branch, and the repositories beside it are read as for any trunk +// pull request. Its delivery group's composed check is asked as for any other (askGroupCheck, unchanged). +func TestAReleaseBranchsPullRequestIntoItsTrunkIsCheckedAsAnyTrunkPullRequest(t *testing.T) { + entries := theCore() + asked := aForgeWithBranches(t, map[string][]string{"novox/mesh-controller": {"main", aRelease}}, nil) + for _, repo := range []string{"mesh-catalog", "mesh-controller", "mesh-host"} { + p := link.PullUpdated{Owner: "novox", Repo: repo, Number: 400, Head: aRelease, Base: "main", Commit: "abc", + Paths: []string{"modules/gitea/index.ts"}} + got, err := releaseBranchOf(t.Context(), p, pullScope(p, entries, nil, nil), entries) + if err != nil || got != "" || *asked != 0 { + t.Fatalf("%s's release branch into main read as release %q (%v) after %d question(s)", repo, got, err, *asked) + } + } + clone := func(s inventory.Source) (string, error) { return "http://git/" + s.Repository + ".git", nil } + beside, err := besideOf(entries, func(module string) string { return "running-" + module }, clone, "") + if err != nil { + t.Fatal(err) + } + for dir, b := range beside { + if b.Ref == aRelease || b.Else != "" { + t.Fatalf("beside a trunk pull request %s is read at %+v", dir, b) + } + } +} diff --git a/internal/builder/check.go b/internal/builder/check.go index 1a9d32bb..ce95febc 100644 --- a/internal/builder/check.go +++ b/internal/builder/check.go @@ -198,6 +198,10 @@ type CheckVerdict struct { Release string JudgedBy string BesideAt map[string]string + // Validator says which node-engine validator the judge of a release-branch check was built with (ADR 0358 §2): + // mesh-host's of the release branch with its commit, the one the release branch's controller vendors, the + // change's own, or the one the change's own controller vendors. Empty for a check of any other base. + Validator string } // Layer is one layer of a check, judged. @@ -384,7 +388,7 @@ func Check(ctx context.Context, run Runner, spec CheckSpec, workspace, registry } named := func(v CheckVerdict) CheckVerdict { v.Head, v.Trunk, v.Merge, v.Beside, v.Members = judged.Head, judged.Trunk, judged.Merge, judged.Beside, judged.Members - v.Release, v.JudgedBy, v.BesideAt = spec.Release, judged.JudgedBy, judged.BesideAt + v.Release, v.JudgedBy, v.BesideAt, v.Validator = spec.Release, judged.JudgedBy, judged.BesideAt, judged.Validator return v } // **What the merge would move is built from a checkout nothing of the check touches** (novox/hq issue 703): the @@ -594,8 +598,10 @@ func Check(ctx context.Context, run Runner, spec CheckSpec, workspace, registry judgedBy = tree } judged.JudgedBy = headAt(ctx, run, judgedBy) - fmt.Fprintf(&out, "--- %s\n", releaseWords(spec.Release, judged.JudgedBy, judged.BesideAt, judged.Beside)) - say("check", "%s", releaseWords(spec.Release, judged.JudgedBy, judged.BesideAt, judged.Beside)) + judged.Validator = releaseValidator(spec, judged.BesideAt, judged.Beside) + said := releaseWords(spec.Release, judged.JudgedBy, judged.Validator, judged.BesideAt, judged.Beside) + fmt.Fprintf(&out, "--- %s\n", said) + say("check", "%s", said) } verdictFile := filepath.Join(root, "verdict.json") env := []string{EnvFacts + "=" + factsFile, EnvGate + "=" + gate, EnvGateStore + "=" + storeURL, @@ -1209,13 +1215,27 @@ func fullHeadAt(ctx context.Context, run Runner, tree string) string { return strings.TrimSpace(out) } +// releaseValidator says which node-engine validator a release-branch check's judge is built with (novox/hq ADR 0358 +// §2), by the rule judgeFor builds it by. +func releaseValidator(spec CheckSpec, at, commits map[string]string) string { + switch { + case spec.Judge == "self": + return "the validator the change's own controller vendors" + case spec.Judge == "validator": + return "the change's own validator" + case at["mesh-host"] == spec.Release: + return fmt.Sprintf("mesh-host's validator of %s at %s", spec.Release, commits["mesh-host"]) + } + return fmt.Sprintf("the validator %s's controller vendors (mesh-host has no branch %s)", spec.Release, spec.Release) +} + // releaseWords is how a release-branch check says what judged it and what it read (novox/hq ADR 0358 §4). -func releaseWords(release, judgedBy string, at, commits map[string]string) string { +func releaseWords(release, judgedBy, validator string, at, commits map[string]string) string { var read []string for _, dir := range sortedKeys(at) { read = append(read, fmt.Sprintf("%s %s at %s", dir, refWords(at[dir]), commits[dir])) } - said := fmt.Sprintf("judged for the release branch %s by its controller at %s", release, judgedBy) + said := fmt.Sprintf("judged for the release branch %s by its controller at %s, with %s", release, judgedBy, validator) if len(read) > 0 { said += "; read beside it: " + strings.Join(read, ", ") } diff --git a/internal/builder/release_branch_test.go b/internal/builder/release_branch_test.go index a30406cd..14e97a4f 100644 --- a/internal/builder/release_branch_test.go +++ b/internal/builder/release_branch_test.go @@ -239,6 +239,19 @@ func TestACataloguePullRequestIntoAReleaseBranchIsJudgedByThatBranchsController( if !strings.Contains(v.Report, "read beside it: ") || !strings.Contains(v.Report, "mesh-host "+releaseBranch+" at "+hostRelease[:12]) { t.Fatalf("the report does not say what was read beside it:\n%s", v.Report) } + // It says which validator judged: mesh-host's of the release branch, with its commit. + if want := "mesh-host's validator of " + releaseBranch + " at " + hostRelease[:12]; v.Validator != want || + !strings.Contains(v.Report, want) { + t.Fatalf("the verdict says the validator was %q, not %q:\n%s", v.Validator, want, v.Report) + } + // Each repository read beside it is named with the branch and the commit read. + for dir := range beside { + if v.BesideAt[dir] == "" || v.Beside[dir] == "" || + !strings.Contains(v.Report, dir+" "+v.BesideAt[dir]+" at "+v.Beside[dir]) { + t.Fatalf("%s is not named with its branch and commit: at %q, commit %q\n%s", dir, v.BesideAt[dir], + v.Beside[dir], v.Report) + } + } if n := builds(t, built); n != 1 { t.Fatalf("the judge was built %d times for one check", n) } @@ -306,3 +319,65 @@ func TestAPullRequestIntoTheTrunkIsJudgedAsBefore(t *testing.T) { t.Fatalf("mesh-host was read at %s, not the commit asked %s", v.Beside["mesh-host"], hostMain[:12]) } } + +// **Without mesh-host's release branch, the release branch's controller keeps the validator it vendors** (ADR 0358 +// §2), and the verdict says so. +func TestWithoutTheNodeEnginesReleaseBranchTheJudgeKeepsItsVendoredValidator(t *testing.T) { + aRuntimeThatBuildsTheJudge(t) + registry := aFactsRegistry(t) + beside, controllerRelease, _, _ := theReleasedMesh(t) + host, hostMain, _ := aRepositoryWithARelease(t, aHostSaying("main's validator"), nil) + beside["mesh-host"] = Beside{Repository: host, Ref: releaseBranch, Else: "main"} + catalogue, head := aPullRequestInto(t, map[string]string{"modules/gitea/module.json": `{"module":"gitea"}`, + CheckScript: "true\n"}, releaseBranch, map[string]string{"modules/gitea/index.ts": "x"}) + v, err := Check(t.Context(), Command, CheckSpec{ID: "check-704-vendored", Repository: catalogue, Ref: head, + Owner: "novox", Repo: "mesh-catalog", Number: 286, Base: releaseBranch, Release: releaseBranch, + JudgeCache: t.TempDir(), Paths: []string{"modules/gitea/index.ts"}, Modules: []string{"gitea"}, Beside: beside, + Toolchain: "golang"}, t.TempDir(), registry, GitCredential{}, nil) + if err != nil { + t.Fatal(err) + } + if v.Gate == nil || !strings.HasSuffix(v.Gate.Summary, "the release branch's controller with the vendored validator") { + t.Fatalf("without mesh-host's release branch the judge did not keep its vendored validator: %+v\n%s", v.Gate, v.Report) + } + if v.JudgedBy != controllerRelease[:12] || v.BesideAt["mesh-host"] != "main" || v.Beside["mesh-host"] != hostMain[:12] { + t.Fatalf("judged by %q, mesh-host read at %q (%s)", v.JudgedBy, v.BesideAt["mesh-host"], v.Beside["mesh-host"]) + } + if !strings.Contains(v.Validator, "controller vendors") || !strings.Contains(v.Report, v.Validator) { + t.Fatalf("the verdict does not say the vendored validator judged: %q\n%s", v.Validator, v.Report) + } +} + +// **Clause 5** (ADR 0358 §5): a release branch's own pull request into its trunk is checked as any pull request into a +// trunk — by the controller beside it as the mesh runs it, the repositories at what a trunk check reads — and its +// verdict names no release branch. +func TestAReleaseBranchsPullRequestIntoItsTrunkIsJudgedByTheControllerTheMeshRuns(t *testing.T) { + aRuntimeThatBuildsTheJudge(t) + registry := aFactsRegistry(t) + controller, controllerMain, _ := aRepositoryWithARelease(t, aJudgeOf("the controller the mesh runs"), + aJudgeOf("the release branch's controller")) + host, hostMain, _ := aRepositoryWithARelease(t, aHostSaying("main's validator"), + aHostSaying("the release branch's validator")) + catalogue, _, releaseHead := aRepositoryWithARelease(t, map[string]string{ + "modules/gitea/module.json": `{"module":"gitea"}`, CheckScript: "true\n"}, + map[string]string{"modules/gitea/index.ts": "released"}) + v, err := Check(t.Context(), Command, CheckSpec{ID: "check-704-release-into-trunk", Repository: catalogue, + Ref: releaseHead, Owner: "novox", Repo: "mesh-catalog", Number: 300, Base: "main", JudgeCache: t.TempDir(), + Paths: []string{"modules/gitea/index.ts"}, Modules: []string{"gitea"}, Toolchain: "golang", + Beside: map[string]Beside{"mesh-controller": {Repository: controller, Ref: controllerMain}, + "mesh-host": {Repository: host, Ref: hostMain}}}, t.TempDir(), registry, GitCredential{}, nil) + if err != nil { + t.Fatal(err) + } + if v.Gate == nil || v.Gate.Summary != "the controller the mesh runs with the vendored validator" { + t.Fatalf("the release branch's pull request into its trunk was not judged by the controller the mesh runs: %+v\n%s", + v.Gate, v.Report) + } + if v.Release != "" || v.JudgedBy != "" || v.BesideAt != nil || v.Validator != "" || + strings.Contains(v.Report, "release branch") || strings.Contains(v.Gate.Summary, "[release") { + t.Fatalf("its verdict names a release branch: %q %q %v %q", v.Release, v.JudgedBy, v.BesideAt, v.Validator) + } + if v.Beside["mesh-host"] != hostMain[:12] || v.Beside["mesh-controller"] != controllerMain[:12] { + t.Fatalf("beside it were read %v, not the trunk's %s and %s", v.Beside, controllerMain[:12], hostMain[:12]) + } +} diff --git a/internal/link/build.go b/internal/link/build.go index e5ddafc4..fa837d3f 100644 --- a/internal/link/build.go +++ b/internal/link/build.go @@ -297,6 +297,8 @@ type CheckOutcome struct { Release string `json:"release,omitempty"` JudgedBy string `json:"judged-by,omitempty"` BesideAt map[string]string `json:"beside-at,omitempty"` + // Validator says which node-engine validator the judge of a release-branch check was built with (ADR 0358 §2). + Validator string `json:"validator,omitempty"` } // SourceOnSeat names a repository by the seat whose holder serves it and its path there. diff --git a/internal/link/events.go b/internal/link/events.go index 30265231..83ba26c6 100644 --- a/internal/link/events.go +++ b/internal/link/events.go @@ -299,6 +299,8 @@ type Checked struct { Release string `json:"release,omitempty"` JudgedBy string `json:"judged-by,omitempty"` BesideAt map[string]string `json:"beside-at,omitempty"` + // Validator says which node-engine validator the judge of a release-branch check was built with (ADR 0358 §2). + Validator string `json:"validator,omitempty"` // Built are the builds the check made for its merge, each with its reuse key and digests, or why not // (novox/hq ADR 0350 §1): what the walk of the merge takes instead of building. Built []CheckBuilt `json:"built,omitempty"`