Say which validator judged a release-branch check, and prove clause 5
mesh/merge-gate head ea7d91bff190 merged into trunk 535c7641ac41 — pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every …
mesh/repo-check head ea7d91bff190 merged into trunk 535c7641ac41 — pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate head ea7d91bff190 merged into trunk 535c7641ac41 — pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every …
mesh/repo-check head ea7d91bff190 merged into trunk 535c7641ac41 — pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
ADR 0358 version 4 asks the verdict to say whether mesh-host's release-branch validator or the vendored one judged, and checks that a release branch's own pull request into its trunk is judged as any trunk pull request (issue 704). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
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),
|
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),
|
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).
|
// What each of a group's heads was judged as (novox/hq ADR 0323 §6).
|
||||||
for _, m := range v.Members {
|
for _, m := range v.Members {
|
||||||
result.Check.Members = append(result.Check.Members, link.CheckedMember{Owner: m.Owner, Repo: m.Repo,
|
result.Check.Members = append(result.Check.Members, link.CheckedMember{Owner: m.Owner, Repo: m.Repo,
|
||||||
|
|||||||
@@ -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.Trunk, c.Beside, c.Took = result.Check.Trunk, result.Check.Beside, result.Check.Took
|
||||||
c.Built = result.Check.Built
|
c.Built = result.Check.Built
|
||||||
c.Release, c.JudgedBy, c.BesideAt = result.Check.Release, result.Check.JudgedBy, result.Check.BesideAt
|
c.Release, c.JudgedBy, c.BesideAt = result.Check.Release, result.Check.JudgedBy, result.Check.BesideAt
|
||||||
|
c.Validator = result.Check.Validator
|
||||||
if c.Gate == nil {
|
if c.Gate == nil {
|
||||||
// A build seat from before the layers: its verdict is the gate's.
|
// A build seat from before the layers: its verdict is the gate's.
|
||||||
c.Gate = &link.CheckLayer{Verdict: c.Verdict, Summary: c.Summary}
|
c.Gate = &link.CheckLayer{Verdict: c.Verdict, Summary: c.Summary}
|
||||||
|
|||||||
@@ -156,12 +156,40 @@ func TestAReleaseBranchVerdictNamesTheBranchTheJudgeAndWhatItRead(t *testing.T)
|
|||||||
c := checkedOf(link.BuildResult{ID: "b", Ref: "aaaaaaaaaaaa0000", Checked: asked, Check: &link.CheckOutcome{
|
c := checkedOf(link.BuildResult{ID: "b", Ref: "aaaaaaaaaaaa0000", Checked: asked, Check: &link.CheckOutcome{
|
||||||
Verdict: "pass", Head: "aaaaaaaaaaaa", Trunk: "111111111111", Gate: &link.CheckLayer{Verdict: "pass"},
|
Verdict: "pass", Head: "aaaaaaaaaaaa", Trunk: "111111111111", Gate: &link.CheckLayer{Verdict: "pass"},
|
||||||
RepoCheck: &link.CheckLayer{Verdict: "pass"}, Release: aRelease, JudgedBy: "fcdecb436117",
|
RepoCheck: &link.CheckLayer{Verdict: "pass"}, Release: aRelease, JudgedBy: "fcdecb436117",
|
||||||
Beside: map[string]string{"mesh-host": "222222222222", "mesh-lab": "333333333333"},
|
Beside: map[string]string{"mesh-host": "222222222222", "mesh-lab": "333333333333"},
|
||||||
BesideAt: map[string]string{"mesh-host": aRelease, "mesh-lab": "main"}}})
|
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" {
|
if c.Release != aRelease || c.JudgedBy != "fcdecb436117" {
|
||||||
t.Fatalf("the verdict names release %q judged by %q", c.Release, c.JudgedBy)
|
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)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -198,6 +198,10 @@ type CheckVerdict struct {
|
|||||||
Release string
|
Release string
|
||||||
JudgedBy string
|
JudgedBy string
|
||||||
BesideAt map[string]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.
|
// 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 {
|
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.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
|
return v
|
||||||
}
|
}
|
||||||
// **What the merge would move is built from a checkout nothing of the check touches** (novox/hq issue 703): the
|
// **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
|
judgedBy = tree
|
||||||
}
|
}
|
||||||
judged.JudgedBy = headAt(ctx, run, judgedBy)
|
judged.JudgedBy = headAt(ctx, run, judgedBy)
|
||||||
fmt.Fprintf(&out, "--- %s\n", releaseWords(spec.Release, judged.JudgedBy, judged.BesideAt, judged.Beside))
|
judged.Validator = releaseValidator(spec, judged.BesideAt, judged.Beside)
|
||||||
say("check", "%s", releaseWords(spec.Release, judged.JudgedBy, 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")
|
verdictFile := filepath.Join(root, "verdict.json")
|
||||||
env := []string{EnvFacts + "=" + factsFile, EnvGate + "=" + gate, EnvGateStore + "=" + storeURL,
|
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)
|
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).
|
// 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
|
var read []string
|
||||||
for _, dir := range sortedKeys(at) {
|
for _, dir := range sortedKeys(at) {
|
||||||
read = append(read, fmt.Sprintf("%s %s at %s", dir, refWords(at[dir]), commits[dir]))
|
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 {
|
if len(read) > 0 {
|
||||||
said += "; read beside it: " + strings.Join(read, ", ")
|
said += "; read beside it: " + strings.Join(read, ", ")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -239,6 +239,19 @@ func TestACataloguePullRequestIntoAReleaseBranchIsJudgedByThatBranchsController(
|
|||||||
if !strings.Contains(v.Report, "read beside it: ") || !strings.Contains(v.Report, "mesh-host "+releaseBranch+" at "+hostRelease[:12]) {
|
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)
|
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 {
|
if n := builds(t, built); n != 1 {
|
||||||
t.Fatalf("the judge was built %d times for one check", n)
|
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])
|
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])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -297,6 +297,8 @@ type CheckOutcome struct {
|
|||||||
Release string `json:"release,omitempty"`
|
Release string `json:"release,omitempty"`
|
||||||
JudgedBy string `json:"judged-by,omitempty"`
|
JudgedBy string `json:"judged-by,omitempty"`
|
||||||
BesideAt map[string]string `json:"beside-at,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.
|
// SourceOnSeat names a repository by the seat whose holder serves it and its path there.
|
||||||
|
|||||||
@@ -299,6 +299,8 @@ type Checked struct {
|
|||||||
Release string `json:"release,omitempty"`
|
Release string `json:"release,omitempty"`
|
||||||
JudgedBy string `json:"judged-by,omitempty"`
|
JudgedBy string `json:"judged-by,omitempty"`
|
||||||
BesideAt map[string]string `json:"beside-at,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
|
// 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.
|
// (novox/hq ADR 0350 §1): what the walk of the merge takes instead of building.
|
||||||
Built []CheckBuilt `json:"built,omitempty"`
|
Built []CheckBuilt `json:"built,omitempty"`
|
||||||
|
|||||||
Reference in New Issue
Block a user