Judge a pull request into a release branch by that branch's controller and repositories
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate head 4a75cc51dda6 merged into trunk 535c7641ac41 — pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every …
mesh/repo-check head 4a75cc51dda6 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 4a75cc51dda6 merged into trunk 535c7641ac41 — pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every …
mesh/repo-check head 4a75cc51dda6 merged into trunk 535c7641ac41 — pass: its merge-check.sh passed
A release branch could never carry a change that needs another repository's release-branch change: its pull requests were judged by the running controller and against the other repositories' main (hq issue 704, ADR 0358). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
@@ -313,6 +314,9 @@ func answer(ctx context.Context, publisher builder.Publisher, on, workspace stri
|
|||||||
registry = r.Address
|
registry = r.Address
|
||||||
}
|
}
|
||||||
spec := checkSpecOf(request)
|
spec := checkSpecOf(request)
|
||||||
|
// A release branch's judge is kept beside the workspace, never in it: the check's containers mount the
|
||||||
|
// workspace, and a pull request's script must not plant the judge of the next check (novox/hq ADR 0358 §2).
|
||||||
|
spec.JudgeCache = judgeCacheOf(workspace)
|
||||||
// What the merge would move, built by the check for the walk to take (novox/hq ADR 0350 §1): into the
|
// What the merge would move, built by the check for the walk to take (novox/hq ADR 0350 §1): into the
|
||||||
// scratch namespace. **Never with the package registry's credential**: this is code nobody has approved
|
// scratch namespace. **Never with the package registry's credential**: this is code nobody has approved
|
||||||
// yet, and a recipe could read the credential it is handed (issue 462). A module that resolves packages
|
// yet, and a recipe could read the credential it is handed (issue 462). A module that resolves packages
|
||||||
@@ -326,7 +330,8 @@ func answer(ctx context.Context, publisher builder.Publisher, on, workspace stri
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
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}
|
||||||
// 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,
|
||||||
@@ -456,9 +461,9 @@ func checkSpecOf(request link.BuildRequest) builder.CheckSpec {
|
|||||||
Owner: c.Owner, Repo: c.Repo, Number: c.Number, Paths: c.Paths, Beside: map[string]builder.Beside{},
|
Owner: c.Owner, Repo: c.Repo, Number: c.Number, Paths: c.Paths, Beside: map[string]builder.Beside{},
|
||||||
Modules: c.Modules, New: c.New, Manifests: c.Manifests, Judge: c.Judge, Base: c.Base,
|
Modules: c.Modules, New: c.New, Manifests: c.Manifests, Judge: c.Judge, Base: c.Base,
|
||||||
Toolchain: builder.ToolchainOf(request.Held), Toolchains: builder.ToolchainsOf(request.Held),
|
Toolchain: builder.ToolchainOf(request.Held), Toolchains: builder.ToolchainsOf(request.Held),
|
||||||
Tests: c.Tests, StoreOnly: c.StoreOnly}
|
Tests: c.Tests, StoreOnly: c.StoreOnly, Release: c.Release}
|
||||||
for dir, b := range c.Beside {
|
for dir, b := range c.Beside {
|
||||||
spec.Beside[dir] = builder.Beside{Repository: b.Repository, Ref: b.Ref}
|
spec.Beside[dir] = builder.Beside{Repository: b.Repository, Ref: b.Ref, Else: b.Else}
|
||||||
}
|
}
|
||||||
for _, b := range c.Builds {
|
for _, b := range c.Builds {
|
||||||
spec.Builds = append(spec.Builds, builder.CheckBuildSpec{Module: b.Module, Repository: b.Repository, Path: b.Path,
|
spec.Builds = append(spec.Builds, builder.CheckBuildSpec{Module: b.Module, Repository: b.Repository, Path: b.Path,
|
||||||
@@ -471,6 +476,12 @@ func checkSpecOf(request link.BuildRequest) builder.CheckSpec {
|
|||||||
return spec
|
return spec
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// judgeCacheOf is where a release branch's judge is kept (novox/hq ADR 0358 §2): beside the workspace, outside what
|
||||||
|
// a check's containers mount.
|
||||||
|
func judgeCacheOf(workspace string) string {
|
||||||
|
return filepath.Clean(workspace) + "-judges"
|
||||||
|
}
|
||||||
|
|
||||||
// triedOf is a try's outcome as its log line says it (novox/hq issue 686): the repository's own check's verdict,
|
// triedOf is a try's outcome as its log line says it (novox/hq issue 686): the repository's own check's verdict,
|
||||||
// or the check's where that did not run — a head that does not merge — and the lines naming what failed.
|
// or the check's where that did not run — a head that does not merge — and the lines naming what failed.
|
||||||
func triedOf(v builder.CheckVerdict) link.Tried {
|
func triedOf(v builder.CheckVerdict) link.Tried {
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/novox/mesh-controller/internal/link"
|
||||||
|
)
|
||||||
|
|
||||||
|
// **A pull request into a release branch reaches the check as one** (novox/hq ADR 0358): the build seat hands the
|
||||||
|
// check the release branch and, beside it, the trunk each repository is read at when it has no branch of that name;
|
||||||
|
// and it keeps the release branch's judge outside the workspace a check's containers mount.
|
||||||
|
func TestTheBuildSeatHandsTheCheckItsReleaseBranch(t *testing.T) {
|
||||||
|
spec := checkSpecOf(link.BuildRequest{ID: "b", Repository: "r", Ref: "abc", Check: &link.CheckRequest{
|
||||||
|
Owner: "novox", Repo: "mesh-catalog", Base: "feat/57", Release: "feat/57",
|
||||||
|
Beside: map[string]link.CheckedOut{"mesh-host": {Repository: "h", Ref: "feat/57", Else: "main"}}}})
|
||||||
|
if spec.Release != "feat/57" || spec.Beside["mesh-host"].Ref != "feat/57" || spec.Beside["mesh-host"].Else != "main" {
|
||||||
|
t.Fatalf("the check was handed release %q and mesh-host %+v", spec.Release, spec.Beside["mesh-host"])
|
||||||
|
}
|
||||||
|
workspace := filepath.Join(t.TempDir(), "mesh-builder")
|
||||||
|
cache := judgeCacheOf(workspace)
|
||||||
|
if rel, err := filepath.Rel(workspace, cache); err != nil || !strings.HasPrefix(rel, "..") {
|
||||||
|
t.Fatalf("the judge cache %s lies inside the workspace %s, which a check's containers mount", cache, workspace)
|
||||||
|
}
|
||||||
|
}
|
||||||
+140
-22
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path"
|
"path"
|
||||||
"regexp"
|
"regexp"
|
||||||
@@ -248,30 +249,14 @@ func checkRequestFor(ctx context.Context, open *stores, p link.PullUpdated, scop
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return link.BuildRequest{}, err
|
return link.BuildRequest{}, err
|
||||||
}
|
}
|
||||||
// Beside it, at what the mesh runs: each core repository by the module the mesh builds from it.
|
// **A pull request into a release branch is judged by that branch** (novox/hq ADR 0358): asked of the forge once.
|
||||||
beside := map[string]link.CheckedOut{}
|
release, err := releaseBranchOf(ctx, p, scope, entries)
|
||||||
for _, e := range entries {
|
|
||||||
dir, core := coreModules[e.Manifest.Module]
|
|
||||||
if !core || e.Provided || e.Source.Repository == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
url, err := clone(e.Source)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return link.BuildRequest{}, err
|
return link.BuildRequest{}, err
|
||||||
}
|
}
|
||||||
refs := besideRefs(dir, current[e.Manifest.Module].Commit)
|
beside, err := besideOf(entries, func(module string) string { return current[module].Commit }, clone, release)
|
||||||
beside[dir] = link.CheckedOut{Repository: url, Ref: refs[dir]}
|
if err != nil {
|
||||||
if dir == "mesh-controller" {
|
return link.BuildRequest{}, err
|
||||||
beside["mesh-controller-main"] = link.CheckedOut{Repository: url, Ref: refs["mesh-controller-main"]}
|
|
||||||
if e.Source.Seat != "" {
|
|
||||||
for _, sibling := range []string{"mesh-lab", "mesh-sdk"} {
|
|
||||||
if url, err := clone(inventory.Source{Seat: e.Source.Seat, Repository: siblingOf(e.Source.Repository,
|
|
||||||
sibling)}); err == nil {
|
|
||||||
beside[sibling] = link.CheckedOut{Repository: url, Ref: refs[sibling]}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
builds := checkBuildsOf(p, scope, entries, clone)
|
builds := checkBuildsOf(p, scope, entries, clone)
|
||||||
return link.BuildRequest{
|
return link.BuildRequest{
|
||||||
@@ -283,10 +268,142 @@ func checkRequestFor(ctx context.Context, open *stores, p link.PullUpdated, scop
|
|||||||
Source: sourceOnSeat(source),
|
Source: sourceOnSeat(source),
|
||||||
Check: &link.CheckRequest{Owner: p.Owner, Repo: p.Repo, Number: p.Number, Base: p.Base,
|
Check: &link.CheckRequest{Owner: p.Owner, Repo: p.Repo, Number: p.Number, Base: p.Base,
|
||||||
Paths: p.Paths, Beside: beside, Modules: scope.Modules, Dependents: scope.Dependents, New: scope.New,
|
Paths: p.Paths, Beside: beside, Modules: scope.Modules, Dependents: scope.Dependents, New: scope.New,
|
||||||
Manifests: scope.Manifests, Judge: scope.Judge, Builds: builds, Named: namedForTheCheck(ctx, p, builds)},
|
Manifests: scope.Manifests, Judge: scope.Judge, Builds: builds, Named: namedForTheCheck(ctx, p, builds),
|
||||||
|
Release: release},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// besideOf is what a check reads beside its own repository, by the directory it is found under: each core repository
|
||||||
|
// by the module the mesh builds from it, at besideRefs — or, for a pull request into a release branch (novox/hq ADR
|
||||||
|
// 0358 §3), at its branch of that name where it has one and at its trunk otherwise, the controller's main kept for
|
||||||
|
// its judge. running is the commit the mesh runs of a module.
|
||||||
|
func besideOf(entries []inventory.Entry, running func(module string) string,
|
||||||
|
clone func(inventory.Source) (string, error), release string) (map[string]link.CheckedOut, error) {
|
||||||
|
beside := map[string]link.CheckedOut{}
|
||||||
|
for _, e := range entries {
|
||||||
|
dir, core := coreModules[e.Manifest.Module]
|
||||||
|
if !core || e.Provided || e.Source.Repository == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
url, err := clone(e.Source)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
refs := besideRefs(dir, running(e.Manifest.Module))
|
||||||
|
at := func(dir, trunk string) link.CheckedOut {
|
||||||
|
if release == "" {
|
||||||
|
return link.CheckedOut{Repository: url, Ref: refs[dir]}
|
||||||
|
}
|
||||||
|
if trunk == "" {
|
||||||
|
trunk = "main"
|
||||||
|
}
|
||||||
|
return link.CheckedOut{Repository: url, Ref: release, Else: trunk}
|
||||||
|
}
|
||||||
|
beside[dir] = at(dir, followedBranch(e.Source.Ref))
|
||||||
|
if dir == "mesh-controller" {
|
||||||
|
// The controller's main stays its main: it judges only a check the running controller predates.
|
||||||
|
beside["mesh-controller-main"] = link.CheckedOut{Repository: url, Ref: refs["mesh-controller-main"]}
|
||||||
|
if release != "" {
|
||||||
|
// mesh-controller has the release branch: that is what makes it one (ADR 0358 §1).
|
||||||
|
beside[dir] = link.CheckedOut{Repository: url, Ref: release}
|
||||||
|
}
|
||||||
|
if e.Source.Seat != "" {
|
||||||
|
for _, sibling := range []string{"mesh-lab", "mesh-sdk"} {
|
||||||
|
if url, err := clone(inventory.Source{Seat: e.Source.Seat, Repository: siblingOf(e.Source.Repository,
|
||||||
|
sibling)}); err == nil {
|
||||||
|
b := link.CheckedOut{Repository: url, Ref: refs[sibling]}
|
||||||
|
if release != "" {
|
||||||
|
b = link.CheckedOut{Repository: url, Ref: release, Else: "main"}
|
||||||
|
}
|
||||||
|
beside[sibling] = b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return beside, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// releaseBranchOf is the release branch a pull request merges into (novox/hq ADR 0358 §1): its base, when that is not
|
||||||
|
// its repository's trunk and mesh-controller has a branch of that name; empty otherwise, and the pull request is
|
||||||
|
// checked as before it. **A base that is a trunk asks nothing**: main, the branch a module built from the repository
|
||||||
|
// follows, or mesh-controller's own trunk. Any other base is asked of the forge once, and an answer it cannot give is
|
||||||
|
// an error, never read as "no release branch".
|
||||||
|
func releaseBranchOf(ctx context.Context, p link.PullUpdated, scope checkScope, entries []inventory.Entry) (string, error) {
|
||||||
|
base := p.Base
|
||||||
|
if base == "" || base == "main" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
if scope.From != nil && base == followedBranch(scope.From.Source.Ref) {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
var controller *inventory.Entry
|
||||||
|
for i, e := range entries {
|
||||||
|
if e.Manifest.Module == "mesh-controller" && !e.Provided && e.Source.Repository != "" {
|
||||||
|
controller = &entries[i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if controller == nil || base == followedBranch(controller.Source.Ref) {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
if !tryRef.MatchString(base) || strings.Contains(base, "..") {
|
||||||
|
return "", nil // no branch the forge could hold: checked as before
|
||||||
|
}
|
||||||
|
owner, repo := sourceOwner(controller.Source.Repository), sourceName(controller.Source.Repository)
|
||||||
|
if owner == "" || repo == "" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
has, err := forgeHasBranch(ctx, owner, repo, base)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("whether %s/%s#%d merges into a release branch cannot be told — the forge did not say "+
|
||||||
|
"whether %s/%s has a branch %s (novox/hq ADR 0358): %w", p.Owner, p.Repo, p.Number, owner, repo, base, err)
|
||||||
|
}
|
||||||
|
if !has {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return base, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sourceName is the name of a recorded repository, a path on the git seat or a URL: novox/mesh-host → mesh-host.
|
||||||
|
func sourceName(repository string) string {
|
||||||
|
parts := strings.Split(strings.Trim(strings.TrimSuffix(repository, ".git"), "/"), "/")
|
||||||
|
if len(parts) < 2 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return parts[len(parts)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// forgeHasBranch asks the forge, through its module's tools on the bus, whether a repository has a branch. A
|
||||||
|
// variable so a test needs no forge.
|
||||||
|
var forgeHasBranch = func(ctx context.Context, owner, repo, branch string) (bool, error) {
|
||||||
|
js, err := aBus()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
defer js.Close()
|
||||||
|
raw, _ := json.Marshal(map[string]any{"path": "/repos/" + owner + "/" + repo + "/branches/" + branch})
|
||||||
|
answer, err := link.Ask(ctx, link.OverNATS{Conn: js.Conn()}, "gitea", "gitea_api", raw, 30*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if answer.Error != "" {
|
||||||
|
if strings.Contains(answer.Error, " 404 ") {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return false, errors.New(answer.Error)
|
||||||
|
}
|
||||||
|
var found struct {
|
||||||
|
Result struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
} `json:"result"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(answer.Result, &found); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return found.Result.Name == branch, nil
|
||||||
|
}
|
||||||
|
|
||||||
// namedForTheCheck are the builds earlier checks of the same pull request at the same head named, read once for the
|
// namedForTheCheck are the builds earlier checks of the same pull request at the same head named, read once for the
|
||||||
// check (novox/hq issue 703): asked again because only the trunk moved, the check keeps what they built of a key
|
// check (novox/hq issue 703): asked again because only the trunk moved, the check keeps what they built of a key
|
||||||
// still the same. Another pull request's build, or one of an earlier head, is never kept. None when they cannot be
|
// still the same. Another pull request's build, or one of an earlier head, is never kept. None when they cannot be
|
||||||
@@ -458,6 +575,7 @@ func checkedOf(result link.BuildResult) link.Checked {
|
|||||||
// repositories read beside it.
|
// repositories read beside it.
|
||||||
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
|
||||||
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}
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/novox/mesh-controller/internal/inventory"
|
||||||
|
"github.com/novox/mesh-controller/internal/link"
|
||||||
|
)
|
||||||
|
|
||||||
|
// **A pull request into a release branch is judged by that branch** (novox/hq ADR 0358, issue 704): a base that is
|
||||||
|
// not its repository's trunk, where mesh-controller has a branch of that name, is a release branch; the check then
|
||||||
|
// reads every repository beside it at its branch of that name where it has one, else at its trunk. Any other base is
|
||||||
|
// checked exactly as before.
|
||||||
|
|
||||||
|
const aRelease = "feat/57-config-and-provisioning"
|
||||||
|
|
||||||
|
// theCore is the mesh's core as a check reads it beside: the controller, the node-engine, the tool runner and the
|
||||||
|
// catalogue's bus, on the git seat.
|
||||||
|
func theCore() []inventory.Entry {
|
||||||
|
onSeat := func(module, repository string) inventory.Entry {
|
||||||
|
e := fromRepo(module, repository, "")
|
||||||
|
e.Source.Seat = "git"
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
nats := onSeat("nats", "novox/mesh-catalog")
|
||||||
|
nats.Source.Path = "modules/nats"
|
||||||
|
return []inventory.Entry{onSeat("mesh-controller", "novox/mesh-controller"), onSeat("mesh-host", "novox/mesh-host"),
|
||||||
|
onSeat("node-tools", "novox/mesh-tools"), nats, onSeat("gitea", "novox/mesh-catalog")}
|
||||||
|
}
|
||||||
|
|
||||||
|
// aForgeWithBranches stands in for the forge: which branches each repository has, and how often it was asked.
|
||||||
|
func aForgeWithBranches(t *testing.T, branches map[string][]string, failing error) *int {
|
||||||
|
t.Helper()
|
||||||
|
asked := 0
|
||||||
|
was := forgeHasBranch
|
||||||
|
t.Cleanup(func() { forgeHasBranch = was })
|
||||||
|
forgeHasBranch = func(_ context.Context, owner, repo, branch string) (bool, error) {
|
||||||
|
asked++
|
||||||
|
if failing != nil {
|
||||||
|
return false, failing
|
||||||
|
}
|
||||||
|
for _, b := range branches[owner+"/"+repo] {
|
||||||
|
if b == branch {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return &asked
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestABaseIsAReleaseBranchWhenTheControllerHasABranchOfThatName(t *testing.T) {
|
||||||
|
entries := theCore()
|
||||||
|
scopeOf := func(p link.PullUpdated) checkScope { return pullScope(p, entries, nil, nil) }
|
||||||
|
pull := func(repo, base string) link.PullUpdated {
|
||||||
|
return link.PullUpdated{Owner: "novox", Repo: repo, Number: 286, Base: base, Commit: "abc",
|
||||||
|
Paths: []string{"modules/gitea/index.ts"}}
|
||||||
|
}
|
||||||
|
|
||||||
|
asked := aForgeWithBranches(t, map[string][]string{"novox/mesh-controller": {"main", aRelease}}, nil)
|
||||||
|
for _, c := range []struct {
|
||||||
|
what string
|
||||||
|
p link.PullUpdated
|
||||||
|
release string
|
||||||
|
asks int
|
||||||
|
}{
|
||||||
|
{"a pull request into the trunk asks nothing and is no release", pull("mesh-catalog", "main"), "", 0},
|
||||||
|
{"a catalogue pull request into the release branch", pull("mesh-catalog", aRelease), aRelease, 1},
|
||||||
|
{"a controller pull request into the release branch", pull("mesh-controller", aRelease), aRelease, 1},
|
||||||
|
{"a base mesh-controller has no branch of is checked as today", pull("mesh-catalog", "feat/other"), "", 1},
|
||||||
|
{"no base: as today", pull("mesh-catalog", ""), "", 0},
|
||||||
|
} {
|
||||||
|
*asked = 0
|
||||||
|
got, err := releaseBranchOf(t.Context(), c.p, scopeOf(c.p), entries)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s: %v", c.what, err)
|
||||||
|
}
|
||||||
|
if got != c.release || *asked != c.asks {
|
||||||
|
t.Errorf("%s: release %q after %d question(s) to the forge, not %q after %d", c.what, got, *asked,
|
||||||
|
c.release, c.asks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The branch a module built from the repository follows is its trunk: never asked about.
|
||||||
|
photos := fromRepo("photos", "novox/photos", "")
|
||||||
|
photos.Source.Ref = "nox-mesh"
|
||||||
|
withPhotos := append(theCore(), photos)
|
||||||
|
p := link.PullUpdated{Owner: "novox", Repo: "photos", Base: "nox-mesh", Commit: "abc", Paths: []string{"x.js"}}
|
||||||
|
*asked = 0
|
||||||
|
if got, err := releaseBranchOf(t.Context(), p, pullScope(p, withPhotos, nil, nil), withPhotos); err != nil ||
|
||||||
|
got != "" || *asked != 0 {
|
||||||
|
t.Fatalf("a module's own trunk read as release %q (%v) after %d question(s)", got, err, *asked)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A forge that cannot answer is an error, never "no release branch".
|
||||||
|
aForgeWithBranches(t, nil, errors.New("nothing serves gitea.gitea_api"))
|
||||||
|
q := pull("mesh-catalog", aRelease)
|
||||||
|
if _, err := releaseBranchOf(t.Context(), q, scopeOf(q), entries); err == nil ||
|
||||||
|
!strings.Contains(err.Error(), "cannot be told") {
|
||||||
|
t.Fatalf("an unanswered forge was read as an answer: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBesideAReleaseBranchCheckEachRepositoryIsReadAtThatBranchElseItsTrunk(t *testing.T) {
|
||||||
|
entries := theCore()
|
||||||
|
clone := func(s inventory.Source) (string, error) { return "http://git/" + s.Repository + ".git", nil }
|
||||||
|
running := func(module string) string { return "running-" + module }
|
||||||
|
|
||||||
|
beside, err := besideOf(entries, running, clone, aRelease)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
at := func(repo string, ref, orElse string) link.CheckedOut {
|
||||||
|
return link.CheckedOut{Repository: "http://git/novox/" + repo + ".git", Ref: ref, Else: orElse}
|
||||||
|
}
|
||||||
|
want := map[string]link.CheckedOut{
|
||||||
|
// The judge: mesh-controller's release branch, which it has.
|
||||||
|
"mesh-controller": at("mesh-controller", aRelease, ""),
|
||||||
|
"mesh-controller-main": at("mesh-controller", "main", ""),
|
||||||
|
// A controller pull request into the release branch reads mesh-host at that branch where it has one.
|
||||||
|
"mesh-host": at("mesh-host", aRelease, "main"),
|
||||||
|
"mesh-tools": at("mesh-tools", aRelease, "main"),
|
||||||
|
"mesh-catalog": at("mesh-catalog", aRelease, "main"),
|
||||||
|
"mesh-lab": at("mesh-lab", aRelease, "main"),
|
||||||
|
"mesh-sdk": at("mesh-sdk", aRelease, "main"),
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(beside, want) {
|
||||||
|
t.Fatalf("beside a release-branch check:\n got %v\nwant %v", beside, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any other base: exactly as before — what the mesh runs, the catalogue's and the lab's main.
|
||||||
|
beside, err = besideOf(entries, running, clone, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want = map[string]link.CheckedOut{
|
||||||
|
"mesh-controller": at("mesh-controller", "running-mesh-controller", ""),
|
||||||
|
"mesh-controller-main": at("mesh-controller", "main", ""),
|
||||||
|
"mesh-host": at("mesh-host", "running-mesh-host", ""),
|
||||||
|
"mesh-tools": at("mesh-tools", "running-node-tools", ""),
|
||||||
|
"mesh-catalog": at("mesh-catalog", "main", ""),
|
||||||
|
"mesh-lab": at("mesh-lab", "main", ""),
|
||||||
|
"mesh-sdk": at("mesh-sdk", sdkPinned(), ""),
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(beside, want) {
|
||||||
|
t.Fatalf("beside a trunk check:\n got %v\nwant %v", beside, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAReleaseBranchVerdictNamesTheBranchTheJudgeAndWhatItRead(t *testing.T) {
|
||||||
|
asked := &link.CheckRequest{Owner: "novox", Repo: "mesh-catalog", Number: 286, Base: aRelease, Release: aRelease}
|
||||||
|
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"}}})
|
||||||
|
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" {
|
||||||
|
t.Fatalf("the verdict names what it read beside it as %v at %v", c.Beside, c.BesideAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
+244
-7
@@ -13,6 +13,7 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -87,6 +88,16 @@ type CheckSpec struct {
|
|||||||
// Judge is who judges the gate: "" the controller the mesh runs, "self" the change's own, "validator"
|
// Judge is who judges the gate: "" the controller the mesh runs, "self" the change's own, "validator"
|
||||||
// the running one with the change's validator.
|
// the running one with the change's validator.
|
||||||
Judge string
|
Judge string
|
||||||
|
// Release is the release branch the pull request merges into (novox/hq ADR 0358), empty for any other base. The
|
||||||
|
// gate is then judged by the controller beside it as mesh-controller — mesh-controller's release branch, never
|
||||||
|
// the controller the mesh runs nor its main — with mesh-host's validator from beside it when mesh-host was read at
|
||||||
|
// the release branch too; a change to the controller still judges itself and a change to the node-engine brings
|
||||||
|
// its own validator. The verdict names the branch, the judge's commit and the branch each beside was read at.
|
||||||
|
Release string
|
||||||
|
// JudgeCache is a directory outside the workspace where a release branch's judge is kept by the commits it is
|
||||||
|
// built from, so it is built once per release-branch head (ADR 0358 §2). Outside: the check's containers mount
|
||||||
|
// the workspace and a pull request's own script must not plant a judge for the next check. Empty keeps none.
|
||||||
|
JudgeCache string
|
||||||
// Toolchain is the image a check's Go runs in: the mesh's own Go toolchain, as it holds it.
|
// Toolchain is the image a check's Go runs in: the mesh's own Go toolchain, as it holds it.
|
||||||
// Toolchains is every toolchain the mesh holds, by language, for a script that declares another.
|
// Toolchains is every toolchain the mesh holds, by language, for a script that declares another.
|
||||||
Toolchain string
|
Toolchain string
|
||||||
@@ -153,10 +164,12 @@ func ToolchainsOf(held map[string]string) map[string]string {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// Beside is one repository cloned next to the one checked.
|
// Beside is one repository cloned next to the one checked: at Ref, or at Else when Else is said and the repository
|
||||||
|
// has no branch Ref (novox/hq ADR 0358 §3).
|
||||||
type Beside struct {
|
type Beside struct {
|
||||||
Repository string
|
Repository string
|
||||||
Ref string
|
Ref string
|
||||||
|
Else string
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckVerdict is what came of one check. Verdict and Summary are the gate's; Gate and Repo each layer.
|
// CheckVerdict is what came of one check. Verdict and Summary are the gate's; Gate and Repo each layer.
|
||||||
@@ -179,6 +192,12 @@ type CheckVerdict struct {
|
|||||||
Members []JudgedHead
|
Members []JudgedHead
|
||||||
// Built are the builds it made for its merge (novox/hq ADR 0350 §1), each with its key, or why not.
|
// Built are the builds it made for its merge (novox/hq ADR 0350 §1), each with its key, or why not.
|
||||||
Built []CheckBuilt
|
Built []CheckBuilt
|
||||||
|
// Release is the release branch it was judged for (novox/hq ADR 0358 §4), JudgedBy the commit of the controller
|
||||||
|
// that judged its gate (twelve characters), and BesideAt the branch each repository beside it was read at, by
|
||||||
|
// directory. Empty for a check of any other base.
|
||||||
|
Release string
|
||||||
|
JudgedBy string
|
||||||
|
BesideAt map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Layer is one layer of a check, judged.
|
// Layer is one layer of a check, judged.
|
||||||
@@ -303,13 +322,23 @@ func Check(ctx context.Context, run Runner, spec CheckSpec, workspace, registry
|
|||||||
if err := forgetLeftCredentials(workspace); err != nil {
|
if err := forgetLeftCredentials(workspace); err != nil {
|
||||||
return CheckVerdict{}, err
|
return CheckVerdict{}, err
|
||||||
}
|
}
|
||||||
clone := func(repository, ref, dir string) error {
|
// cloneAt clones a repository and checks out ref — or, when the repository has no branch ref and orElse is said,
|
||||||
|
// orElse (novox/hq ADR 0358 §3); readAt keeps which, by directory.
|
||||||
|
readAt := map[string]string{}
|
||||||
|
cloneAt := func(repository, ref, orElse, dir string) error {
|
||||||
if _, err := run(ctx, root, "git", cloneWith(credentials, "clone", "--quiet", repository, dir)...); err != nil {
|
if _, err := run(ctx, root, "git", cloneWith(credentials, "clone", "--quiet", repository, dir)...); err != nil {
|
||||||
return fmt.Errorf("cannot clone %s: %s", redact.redact(repository), redact.redact(err.Error()))
|
return fmt.Errorf("cannot clone %s: %s", redact.redact(repository), redact.redact(err.Error()))
|
||||||
}
|
}
|
||||||
if err := recordedWithoutUserinfo(ctx, run, filepath.Join(root, dir), repository); err != nil {
|
if err := recordedWithoutUserinfo(ctx, run, filepath.Join(root, dir), repository); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if orElse != "" && ref != "" {
|
||||||
|
if _, err := run(ctx, filepath.Join(root, dir), "git", "rev-parse", "--verify", "--quiet",
|
||||||
|
"refs/remotes/origin/"+ref); err != nil {
|
||||||
|
ref = orElse
|
||||||
|
}
|
||||||
|
}
|
||||||
|
readAt[dir] = ref
|
||||||
if ref != "" {
|
if ref != "" {
|
||||||
if _, err := run(ctx, filepath.Join(root, dir), "git", "checkout", "--quiet", ref); err != nil {
|
if _, err := run(ctx, filepath.Join(root, dir), "git", "checkout", "--quiet", ref); err != nil {
|
||||||
return fmt.Errorf("%s has no %s: %w", redact.redact(repository), ref, err)
|
return fmt.Errorf("%s has no %s: %w", redact.redact(repository), ref, err)
|
||||||
@@ -317,6 +346,9 @@ func Check(ctx context.Context, run Runner, spec CheckSpec, workspace, registry
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
clone := func(repository, ref, dir string) error {
|
||||||
|
return cloneAt(repository, ref, "", dir)
|
||||||
|
}
|
||||||
name := spec.Repo
|
name := spec.Repo
|
||||||
if name == "" || !safeName.MatchString(name) {
|
if name == "" || !safeName.MatchString(name) {
|
||||||
name = "checked"
|
name = "checked"
|
||||||
@@ -352,6 +384,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
|
||||||
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
|
||||||
@@ -392,9 +425,16 @@ func Check(ctx context.Context, run Runner, spec CheckSpec, workspace, registry
|
|||||||
if dir == name || !safeName.MatchString(dir) {
|
if dir == name || !safeName.MatchString(dir) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := clone(b.Repository, b.Ref, dir); err != nil {
|
if err := cloneAt(b.Repository, b.Ref, b.Else, dir); err != nil {
|
||||||
return CheckVerdict{}, fmt.Errorf("beside it, %w", err)
|
return CheckVerdict{}, fmt.Errorf("beside it, %w", err)
|
||||||
}
|
}
|
||||||
|
if spec.Release != "" {
|
||||||
|
// The branch each was read at is named with its commit (ADR 0358 §4).
|
||||||
|
if judged.BesideAt == nil {
|
||||||
|
judged.BesideAt = map[string]string{}
|
||||||
|
}
|
||||||
|
judged.BesideAt[dir] = readAt[dir]
|
||||||
|
}
|
||||||
// The commit each one read is named in the verdict (ADR 0323 §3): its move is issue 568's question.
|
// The commit each one read is named in the verdict (ADR 0323 §3): its move is issue 568's question.
|
||||||
if at := headAt(ctx, run, filepath.Join(root, dir)); at != "" {
|
if at := headAt(ctx, run, filepath.Join(root, dir)); at != "" {
|
||||||
if judged.Beside == nil {
|
if judged.Beside == nil {
|
||||||
@@ -402,7 +442,7 @@ func Check(ctx context.Context, run Runner, spec CheckSpec, workspace, registry
|
|||||||
}
|
}
|
||||||
judged.Beside[dir] = at
|
judged.Beside[dir] = at
|
||||||
}
|
}
|
||||||
say("check", "beside it %s at %s", dir, short(b.Ref))
|
say("check", "beside it %s at %s", dir, refWords(readAt[dir]))
|
||||||
}
|
}
|
||||||
|
|
||||||
// A delivery group's other heads (novox/hq ADR 0239), each at its head, for the gate to compose with this.
|
// A delivery group's other heads (novox/hq ADR 0239), each at its head, for the gate to compose with this.
|
||||||
@@ -542,10 +582,21 @@ func Check(ctx context.Context, run Runner, spec CheckSpec, workspace, registry
|
|||||||
|
|
||||||
// The judge. Its failing to build is the change's fault when the change is the judge or its validator,
|
// The judge. Its failing to build is the change's fault when the change is the judge or its validator,
|
||||||
// and the check's when it is the controller the mesh runs.
|
// and the check's when it is the controller the mesh runs.
|
||||||
gate, judgeFault, err := judgeFor(ctx, labelled, run, spec, root, tree, inToolchain, say)
|
gate, judgeFault, err := judgeFor(ctx, labelled, run, spec, root, tree, judged.BesideAt, inToolchain, say)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return CheckVerdict{}, err
|
return CheckVerdict{}, err
|
||||||
}
|
}
|
||||||
|
if spec.Release != "" {
|
||||||
|
// **A release-branch judgement says so** (novox/hq ADR 0358 §4): the branch, the controller's commit that
|
||||||
|
// judged, and each repository read beside it at its branch and commit.
|
||||||
|
judgedBy := filepath.Join(root, "mesh-controller")
|
||||||
|
if spec.Judge == "self" {
|
||||||
|
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))
|
||||||
|
}
|
||||||
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,
|
||||||
EnvTestStore + "=" + storeURL, EnvTestBus + "=nats://" + bus, EnvRepository + "=" + spec.Owner + "/" + spec.Repo,
|
EnvTestStore + "=" + storeURL, EnvTestBus + "=nats://" + bus, EnvRepository + "=" + spec.Owner + "/" + spec.Repo,
|
||||||
@@ -611,6 +662,11 @@ func Check(ctx context.Context, run Runner, spec CheckSpec, workspace, registry
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if spec.Release != "" {
|
||||||
|
// The statuses name the release branch and the judge first, so the forge's short description keeps them.
|
||||||
|
mark := releaseMark(spec.Release, judged.JudgedBy)
|
||||||
|
v.Gate.Summary, v.Repo.Summary = mark+v.Gate.Summary, mark+v.Repo.Summary
|
||||||
|
}
|
||||||
// What the check printed travels in the verdict to the forge and the controller: redacted as the log is.
|
// What the check printed travels in the verdict to the forge and the controller: redacted as the log is.
|
||||||
v.Gate.Summary, v.Repo.Summary, v.Repo.Failed = redact.redact(v.Gate.Summary), redact.redact(v.Repo.Summary),
|
v.Gate.Summary, v.Repo.Summary, v.Repo.Failed = redact.redact(v.Gate.Summary), redact.redact(v.Repo.Summary),
|
||||||
redact.redact(v.Repo.Failed)
|
redact.redact(v.Repo.Failed)
|
||||||
@@ -968,7 +1024,12 @@ func gateLayer(ctx context.Context, spec CheckSpec, tree, root, gate, verdictFil
|
|||||||
// the mesh runs. It answers the judge's path; or, when the change makes its own judge unbuildable, why —
|
// the mesh runs. It answers the judge's path; or, when the change makes its own judge unbuildable, why —
|
||||||
// the change's fault, a failing gate; or an error when the check cannot build a judge at all. A check
|
// the change's fault, a failing gate; or an error when the check cannot build a judge at all. A check
|
||||||
// whose gate does not run builds a judge only to hand its scripts one, and goes on without when it cannot.
|
// whose gate does not run builds a judge only to hand its scripts one, and goes on without when it cannot.
|
||||||
func judgeFor(ctx context.Context, labelled, run Runner, spec CheckSpec, root, tree string,
|
//
|
||||||
|
// **A pull request into a release branch is judged by that branch's controller** (novox/hq ADR 0358 §2): the one
|
||||||
|
// beside it as mesh-controller, read at the release branch — with mesh-host's validator from beside it when mesh-host
|
||||||
|
// has that branch too, or with the change's own validator for a change to the node-engine — never the controller the
|
||||||
|
// mesh runs. besideAt is the branch each repository beside it was read at.
|
||||||
|
func judgeFor(ctx context.Context, labelled, run Runner, spec CheckSpec, root, tree string, besideAt map[string]string,
|
||||||
inToolchain func(string, []string, ...string) []string, say func(step, format string, args ...any)) (string, string, error) {
|
inToolchain func(string, []string, ...string) []string, say func(step, format string, args ...any)) (string, string, error) {
|
||||||
gated := spec.Gated()
|
gated := spec.Gated()
|
||||||
switch spec.Judge {
|
switch spec.Judge {
|
||||||
@@ -982,7 +1043,14 @@ func judgeFor(ctx context.Context, labelled, run Runner, spec CheckSpec, root, t
|
|||||||
say("check", "judged by the change's own controller")
|
say("check", "judged by the change's own controller")
|
||||||
return bin, "", nil
|
return bin, "", nil
|
||||||
}
|
}
|
||||||
bin, judgeTree, err := judge(ctx, labelled, run, root, inToolchain, say)
|
var bin, judgeTree string
|
||||||
|
var err error
|
||||||
|
if spec.Release != "" {
|
||||||
|
withHost := spec.Judge != "validator" && besideAt["mesh-host"] == spec.Release
|
||||||
|
bin, judgeTree, err = releaseJudge(ctx, labelled, run, spec, root, withHost, inToolchain, say)
|
||||||
|
} else {
|
||||||
|
bin, judgeTree, err = judge(ctx, labelled, run, root, inToolchain, say)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !gated {
|
if !gated {
|
||||||
say("check", "no judge for its script: %v", err)
|
say("check", "no judge for its script: %v", err)
|
||||||
@@ -1004,12 +1072,181 @@ func judgeFor(ctx context.Context, labelled, run Runner, spec CheckSpec, root, t
|
|||||||
if _, err := labelled(ctx, root, "docker", inToolchain(filepath.Join(root, judgeTree),
|
if _, err := labelled(ctx, root, "docker", inToolchain(filepath.Join(root, judgeTree),
|
||||||
[]string{"CGO_ENABLED=0", "GOFLAGS=-mod=vendor", "GOPROXY=off", "GOCACHE=" + filepath.Join(filepath.Dir(root), "go-cache")},
|
[]string{"CGO_ENABLED=0", "GOFLAGS=-mod=vendor", "GOPROXY=off", "GOCACHE=" + filepath.Join(filepath.Dir(root), "go-cache")},
|
||||||
"go", "build", "-o", withValidator, "./cmd/mesh-controller")...); err != nil {
|
"go", "build", "-o", withValidator, "./cmd/mesh-controller")...); err != nil {
|
||||||
|
if spec.Release != "" {
|
||||||
|
return "", spec.Release + "'s controller does not build with this change's validator: " +
|
||||||
|
firstLine(err.Error()), nil
|
||||||
|
}
|
||||||
return "", "the controller the mesh runs does not build with this change's validator: " + firstLine(err.Error()), nil
|
return "", "the controller the mesh runs does not build with this change's validator: " + firstLine(err.Error()), nil
|
||||||
}
|
}
|
||||||
|
if spec.Release != "" {
|
||||||
|
say("check", "judged by %s's controller, with this change's validator", spec.Release)
|
||||||
|
return withValidator, "", nil
|
||||||
|
}
|
||||||
say("check", "judged by the controller the mesh runs, with this change's validator")
|
say("check", "judged by the controller the mesh runs, with this change's validator")
|
||||||
return withValidator, "", nil
|
return withValidator, "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// releaseJudge builds the controller that judges a pull request into a release branch (novox/hq ADR 0358 §2): the
|
||||||
|
// one beside the check as mesh-controller, read at that branch — with mesh-host's validator from beside it in place
|
||||||
|
// of the one it vendors when withHost. **Built once per release-branch head**: kept in the spec's judge cache by the
|
||||||
|
// commits it is built from and taken from there while they stand. It answers the binary and the directory it was
|
||||||
|
// built from.
|
||||||
|
func releaseJudge(ctx context.Context, run, plain Runner, spec CheckSpec, root string, withHost bool,
|
||||||
|
inToolchain func(string, []string, ...string) []string, say func(step, format string, args ...any)) (string, string, error) {
|
||||||
|
const dir = "mesh-controller"
|
||||||
|
if _, err := os.Stat(filepath.Join(root, dir)); err != nil {
|
||||||
|
return "", "", fmt.Errorf("no controller of %s beside the check to judge it with", spec.Release)
|
||||||
|
}
|
||||||
|
key := fullHeadAt(ctx, plain, filepath.Join(root, dir))
|
||||||
|
if key == "" {
|
||||||
|
return "", "", fmt.Errorf("the controller of %s beside the check names no commit", spec.Release)
|
||||||
|
}
|
||||||
|
with := ""
|
||||||
|
if withHost {
|
||||||
|
host := fullHeadAt(ctx, plain, filepath.Join(root, "mesh-host"))
|
||||||
|
if host == "" {
|
||||||
|
return "", "", fmt.Errorf("mesh-host of %s beside the check names no commit", spec.Release)
|
||||||
|
}
|
||||||
|
key += "-with-mesh-host-" + host
|
||||||
|
with = ", with mesh-host's validator of " + spec.Release + " at " + twelve(host)
|
||||||
|
}
|
||||||
|
bin := filepath.Join(root, "bin", "mesh-controller")
|
||||||
|
if took := judgeKept(spec.JudgeCache, key, bin); took {
|
||||||
|
say("check", "judged by %s's controller at %s%s, built for an earlier check of that head", spec.Release,
|
||||||
|
twelve(key), with)
|
||||||
|
return bin, dir, nil
|
||||||
|
}
|
||||||
|
if withHost {
|
||||||
|
vendored := filepath.Join(root, dir, "vendor", "github.com", "novox", "mesh-host")
|
||||||
|
for _, pkg := range []string{"validate", filepath.Join("internal", "declaration")} {
|
||||||
|
if err := replaceGoFiles(filepath.Join(root, "mesh-host", pkg), filepath.Join(vendored, pkg)); err != nil {
|
||||||
|
return "", "", fmt.Errorf("mesh-host's validator of %s could not be put in its controller: %w", spec.Release, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := run(ctx, root, "docker", inToolchain(filepath.Join(root, dir),
|
||||||
|
[]string{"CGO_ENABLED=0", "GOFLAGS=-mod=vendor", "GOPROXY=off", "GOCACHE=" + filepath.Join(filepath.Dir(root), "go-cache")},
|
||||||
|
"go", "build", "-o", bin, "./cmd/mesh-controller")...); err != nil {
|
||||||
|
return "", "", fmt.Errorf("the controller of %s does not build%s: %w", spec.Release, with, err)
|
||||||
|
}
|
||||||
|
// Kept before anything of the change runs: nothing a pull request's script writes can reach the cache.
|
||||||
|
keepJudge(spec.JudgeCache, key, bin, say)
|
||||||
|
say("check", "judged by %s's controller at %s%s", spec.Release, twelve(key), with)
|
||||||
|
return bin, dir, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// judgesKept is how many release-branch judges a judge cache keeps: a head moved past is not built from again.
|
||||||
|
const judgesKept = 4
|
||||||
|
|
||||||
|
// judgeKept puts the judge kept under key in the cache at bin, and says whether there was one.
|
||||||
|
func judgeKept(cache, key, bin string) bool {
|
||||||
|
if cache == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
body, err := os.ReadFile(filepath.Join(cache, key, "mesh-controller"))
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(filepath.Dir(bin), 0o755); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(bin, body, 0o755); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
_ = os.Chtimes(filepath.Join(cache, key), now, now)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// keepJudge keeps a judge just built under key, and lets go of all but the judgesKept most recently used. A judge
|
||||||
|
// that cannot be kept is said and built again next time: the cache only saves time.
|
||||||
|
func keepJudge(cache, key, bin string, say func(step, format string, args ...any)) {
|
||||||
|
if cache == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
body, err := os.ReadFile(bin)
|
||||||
|
if err == nil {
|
||||||
|
err = os.MkdirAll(filepath.Join(cache, key), 0o755)
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
kept := filepath.Join(cache, key, "mesh-controller")
|
||||||
|
if err = os.WriteFile(kept+".part", body, 0o755); err == nil {
|
||||||
|
err = os.Rename(kept+".part", kept)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
say("check", "the judge could not be kept for the next check, which builds it again: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
entries, err := os.ReadDir(cache)
|
||||||
|
if err != nil || len(entries) <= judgesKept {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
type used struct {
|
||||||
|
name string
|
||||||
|
at time.Time
|
||||||
|
}
|
||||||
|
var all []used
|
||||||
|
for _, e := range entries {
|
||||||
|
if info, err := e.Info(); err == nil && e.IsDir() {
|
||||||
|
all = append(all, used{e.Name(), info.ModTime()})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Slice(all, func(i, j int) bool { return all[i].at.After(all[j].at) })
|
||||||
|
for _, u := range all[min(judgesKept, len(all)):] {
|
||||||
|
if u.name != key {
|
||||||
|
_ = os.RemoveAll(filepath.Join(cache, u.name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fullHeadAt is the whole commit a checkout stands at, empty when it names none.
|
||||||
|
func fullHeadAt(ctx context.Context, run Runner, tree string) string {
|
||||||
|
out, err := run(ctx, tree, "git", "rev-parse", "--verify", "--quiet", "HEAD^{commit}")
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
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)
|
||||||
|
if len(read) > 0 {
|
||||||
|
said += "; read beside it: " + strings.Join(read, ", ")
|
||||||
|
}
|
||||||
|
return said
|
||||||
|
}
|
||||||
|
|
||||||
|
// releaseMark opens each layer's summary of a release-branch check, so a status says it first.
|
||||||
|
func releaseMark(release, judgedBy string) string {
|
||||||
|
return fmt.Sprintf("[release %s, judged by its controller %s] ", release, judgedBy)
|
||||||
|
}
|
||||||
|
|
||||||
|
// refWords is a ref as a line says it: a commit by its first eight characters, a branch whole.
|
||||||
|
func refWords(ref string) string {
|
||||||
|
if commitLike.MatchString(ref) {
|
||||||
|
return short(ref)
|
||||||
|
}
|
||||||
|
return ref
|
||||||
|
}
|
||||||
|
|
||||||
|
// commitLike is a ref that names a commit.
|
||||||
|
var commitLike = regexp.MustCompile(`^[0-9a-f]{7,40}$`)
|
||||||
|
|
||||||
|
func sortedKeys(m map[string]string) []string {
|
||||||
|
keys := make([]string, 0, len(m))
|
||||||
|
for k := range m {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
// replaceGoFiles puts a package's Go files — never its tests — in place of another copy's.
|
// replaceGoFiles puts a package's Go files — never its tests — in place of another copy's.
|
||||||
func replaceGoFiles(from, into string) error {
|
func replaceGoFiles(from, into string) error {
|
||||||
old, err := filepath.Glob(filepath.Join(into, "*.go"))
|
old, err := filepath.Glob(filepath.Join(into, "*.go"))
|
||||||
|
|||||||
@@ -0,0 +1,308 @@
|
|||||||
|
package builder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// **A pull request into a release branch is judged by the controller of that branch** (novox/hq ADR 0358, issue
|
||||||
|
// 704): the build seat's check builds the judge from mesh-controller read at the release branch — with mesh-host's
|
||||||
|
// validator of that branch when mesh-host has one — reads every repository beside it at that branch where it has one
|
||||||
|
// and at its trunk otherwise, and names the branch, the judge's commit and each repository read with its commit.
|
||||||
|
|
||||||
|
const releaseBranch = "feat/57-config-and-provisioning"
|
||||||
|
|
||||||
|
// gitInRepo runs git in a repository made for a test.
|
||||||
|
func gitInRepo(t *testing.T, dir string, args ...string) string {
|
||||||
|
t.Helper()
|
||||||
|
cmd := exec.Command("git", args...)
|
||||||
|
cmd.Dir = dir
|
||||||
|
cmd.Env = append(os.Environ(), "GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@example.org",
|
||||||
|
"GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@example.org")
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("git %v: %v\n%s", args, err, out)
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(string(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
// commitFiles writes files into a repository and commits them on the branch checked out.
|
||||||
|
func commitFiles(t *testing.T, dir string, files map[string]string) string {
|
||||||
|
t.Helper()
|
||||||
|
for name, body := range files {
|
||||||
|
if err := os.MkdirAll(filepath.Dir(filepath.Join(dir, name)), 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gitInRepo(t, dir, "add", "-A")
|
||||||
|
gitInRepo(t, dir, "commit", "--quiet", "-m", "x")
|
||||||
|
return gitInRepo(t, dir, "rev-parse", "HEAD")
|
||||||
|
}
|
||||||
|
|
||||||
|
// aRepositoryWithARelease is a repository whose main holds mainFiles and, when releaseFiles is not nil, a release
|
||||||
|
// branch over them holding releaseFiles. It answers the repository, main's head and the release branch's head.
|
||||||
|
func aRepositoryWithARelease(t *testing.T, mainFiles, releaseFiles map[string]string) (string, string, string) {
|
||||||
|
t.Helper()
|
||||||
|
dir := t.TempDir()
|
||||||
|
gitInRepo(t, dir, "init", "--quiet", "-b", "main")
|
||||||
|
main := commitFiles(t, dir, mainFiles)
|
||||||
|
if releaseFiles == nil {
|
||||||
|
return dir, main, ""
|
||||||
|
}
|
||||||
|
gitInRepo(t, dir, "checkout", "--quiet", "-b", releaseBranch)
|
||||||
|
head := commitFiles(t, dir, releaseFiles)
|
||||||
|
gitInRepo(t, dir, "checkout", "--quiet", "main")
|
||||||
|
return dir, main, head
|
||||||
|
}
|
||||||
|
|
||||||
|
// aJudgeOf is a controller that judges as told: its gate passes, saying who it is and the word of the node-engine
|
||||||
|
// validator it vendors — the one it was built with.
|
||||||
|
func aJudgeOf(who string) map[string]string {
|
||||||
|
return map[string]string{
|
||||||
|
"go.mod": "module example.org/judge\n\ngo 1.22\n\nrequire github.com/novox/mesh-host v0.0.0\n",
|
||||||
|
"vendor/modules.txt": "# github.com/novox/mesh-host v0.0.0\n## explicit; go 1.22\ngithub.com/novox/mesh-host/validate\n",
|
||||||
|
"vendor/github.com/novox/mesh-host/validate/v.go": "package validate\n\nconst Word = \"the vendored validator\"\n",
|
||||||
|
"vendor/github.com/novox/mesh-host/internal/declaration/d.go": "package declaration\n",
|
||||||
|
"cmd/mesh-controller/main.go": `package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/novox/mesh-host/validate"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) == 2 && os.Args[1] == "merge-gate" {
|
||||||
|
fmt.Println("usage: merge-gate --facts <file> --store <postgres>")
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
fmt.Printf("{\"verdict\":\"pass\",\"summary\":\"%s with %s\"}\n", "` + who + `", validate.Word)
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// aHostSaying is the node-engine's validator, saying a word.
|
||||||
|
func aHostSaying(word string) map[string]string {
|
||||||
|
return map[string]string{
|
||||||
|
"validate/v.go": "package validate\n\nconst Word = \"" + word + "\"\n",
|
||||||
|
"internal/declaration/d.go": "package declaration\n",
|
||||||
|
"validate/v_test.go": "package validate\n",
|
||||||
|
"internal/declaration/x_test.go": "package declaration\n",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// aRuntimeThatBuildsTheJudge puts a container runtime on PATH that runs every container's command on this machine,
|
||||||
|
// in the directory and with the environment the check gives it — the judge's build too — and a store and a bus that
|
||||||
|
// answer. It answers the file each `go` the check runs is written to.
|
||||||
|
func aRuntimeThatBuildsTheJudge(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
if _, err := exec.LookPath("go"); err != nil {
|
||||||
|
t.Skip("no Go toolchain on this machine to build a judge with")
|
||||||
|
}
|
||||||
|
bus, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { bus.Close() })
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
c, err := bus.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
dir := t.TempDir()
|
||||||
|
built := filepath.Join(dir, "built")
|
||||||
|
runtime := `#!/bin/sh
|
||||||
|
cmd="$1"; shift
|
||||||
|
case "$cmd" in
|
||||||
|
ps|rm|exec) exit 0 ;;
|
||||||
|
port) echo "` + bus.Addr().String() + `"; exit 0 ;;
|
||||||
|
run)
|
||||||
|
workdir=""; detached=""
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
-d) detached=1; shift ;;
|
||||||
|
--rm) shift ;;
|
||||||
|
--workdir) workdir="$2"; shift 2 ;;
|
||||||
|
--env) export "$2"; shift 2 ;;
|
||||||
|
--network|--volume|--user|--label|--name|-p|-e) shift 2 ;;
|
||||||
|
*) break ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
if [ -n "$detached" ]; then echo a-container; exit 0; fi
|
||||||
|
shift
|
||||||
|
if [ "$1" = go ]; then echo "$*" >> "` + built + `"; fi
|
||||||
|
cd "$workdir" && exec "$@" ;;
|
||||||
|
esac
|
||||||
|
exit 1
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, "docker"), []byte(runtime), 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||||
|
t.Setenv("GOTOOLCHAIN", "local")
|
||||||
|
return built
|
||||||
|
}
|
||||||
|
|
||||||
|
// builds is how many times the check ran go to build a judge.
|
||||||
|
func builds(t *testing.T, file string) int {
|
||||||
|
t.Helper()
|
||||||
|
body, err := os.ReadFile(file)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return strings.Count(string(body), "go build")
|
||||||
|
}
|
||||||
|
|
||||||
|
// theReleasedMesh is the core beside a release-branch check: a controller whose main and release branch judge
|
||||||
|
// differently, a node-engine with a validator of each, and a lab with no release branch.
|
||||||
|
func theReleasedMesh(t *testing.T) (map[string]Beside, string, string, string) {
|
||||||
|
t.Helper()
|
||||||
|
controller, _, controllerRelease := aRepositoryWithARelease(t, aJudgeOf("main's controller"),
|
||||||
|
aJudgeOf("the release branch's controller"))
|
||||||
|
host, _, hostRelease := aRepositoryWithARelease(t, aHostSaying("main's validator"),
|
||||||
|
aHostSaying("the release branch's validator"))
|
||||||
|
lab, labMain, _ := aRepositoryWithARelease(t, map[string]string{"README": "the lab"}, nil)
|
||||||
|
return map[string]Beside{
|
||||||
|
"mesh-controller": {Repository: controller, Ref: releaseBranch},
|
||||||
|
"mesh-controller-main": {Repository: controller, Ref: "main"},
|
||||||
|
"mesh-host": {Repository: host, Ref: releaseBranch, Else: "main"},
|
||||||
|
"mesh-lab": {Repository: lab, Ref: releaseBranch, Else: "main"},
|
||||||
|
}, controllerRelease, hostRelease, labMain
|
||||||
|
}
|
||||||
|
|
||||||
|
// aPullRequestInto is a repository whose pull request's head is cut from base, with a change.
|
||||||
|
func aPullRequestInto(t *testing.T, files map[string]string, base string, change map[string]string) (string, string) {
|
||||||
|
t.Helper()
|
||||||
|
repo, _, _ := aRepositoryWithARelease(t, files, map[string]string{"released": "r"})
|
||||||
|
gitInRepo(t, repo, "checkout", "--quiet", "-b", "change", base)
|
||||||
|
head := commitFiles(t, repo, change)
|
||||||
|
gitInRepo(t, repo, "checkout", "--quiet", "main")
|
||||||
|
return repo, head
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestACataloguePullRequestIntoAReleaseBranchIsJudgedByThatBranchsController(t *testing.T) {
|
||||||
|
built := aRuntimeThatBuildsTheJudge(t)
|
||||||
|
registry := aFactsRegistry(t)
|
||||||
|
beside, controllerRelease, hostRelease, labMain := theReleasedMesh(t)
|
||||||
|
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"})
|
||||||
|
cache := t.TempDir()
|
||||||
|
check := func(id string) CheckVerdict {
|
||||||
|
t.Helper()
|
||||||
|
v, err := Check(t.Context(), Command, CheckSpec{ID: id, Repository: catalogue, Ref: head, Owner: "novox",
|
||||||
|
Repo: "mesh-catalog", Number: 286, Base: releaseBranch, Release: releaseBranch, JudgeCache: cache,
|
||||||
|
Paths: []string{"modules/gitea/index.ts"}, Modules: []string{"gitea"}, Beside: beside, Toolchain: "golang"},
|
||||||
|
t.TempDir(), registry, GitCredential{}, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
v := check("check-704-catalogue")
|
||||||
|
// The release branch's controller judged, with mesh-host's validator of the release branch.
|
||||||
|
if v.Gate == nil || v.Gate.Verdict != "pass" ||
|
||||||
|
!strings.HasSuffix(v.Gate.Summary, "the release branch's controller with the release branch's validator") {
|
||||||
|
t.Fatalf("the gate was not judged by the release branch's controller with its node-engine's validator: %+v\n%s",
|
||||||
|
v.Gate, v.Report)
|
||||||
|
}
|
||||||
|
// It names the release branch, the judge's commit, and each repository read beside it at its branch and commit.
|
||||||
|
mark := "[release " + releaseBranch + ", judged by its controller " + controllerRelease[:12] + "] "
|
||||||
|
if !strings.HasPrefix(v.Gate.Summary, mark) || !strings.HasPrefix(v.Repo.Summary, mark) {
|
||||||
|
t.Fatalf("the statuses do not name the release branch and its judge first:\n%s\n%s", v.Gate.Summary, v.Repo.Summary)
|
||||||
|
}
|
||||||
|
if v.Release != releaseBranch || v.JudgedBy != controllerRelease[:12] {
|
||||||
|
t.Fatalf("the verdict names release %q judged by %q, not %s by %s", v.Release, v.JudgedBy, releaseBranch,
|
||||||
|
controllerRelease[:12])
|
||||||
|
}
|
||||||
|
if v.BesideAt["mesh-host"] != releaseBranch || v.Beside["mesh-host"] != hostRelease[:12] {
|
||||||
|
t.Fatalf("mesh-host was read at %q (%s), not its release branch at %s", v.BesideAt["mesh-host"],
|
||||||
|
v.Beside["mesh-host"], hostRelease[:12])
|
||||||
|
}
|
||||||
|
// A repository with no release branch is read at its trunk, and says so.
|
||||||
|
if v.BesideAt["mesh-lab"] != "main" || v.Beside["mesh-lab"] != labMain[:12] {
|
||||||
|
t.Fatalf("the lab, with no release branch, was read at %q (%s), not main at %s", v.BesideAt["mesh-lab"],
|
||||||
|
v.Beside["mesh-lab"], labMain[: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)
|
||||||
|
}
|
||||||
|
if n := builds(t, built); n != 1 {
|
||||||
|
t.Fatalf("the judge was built %d times for one check", n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// **Built once per release-branch head**: a second check while that head stands takes the judge kept.
|
||||||
|
v = check("check-704-catalogue-again")
|
||||||
|
if !strings.HasSuffix(v.Gate.Summary, "the release branch's controller with the release branch's validator") {
|
||||||
|
t.Fatalf("the kept judge did not judge: %+v\n%s", v.Gate, v.Report)
|
||||||
|
}
|
||||||
|
if n := builds(t, built); n != 1 {
|
||||||
|
t.Fatalf("the judge of the same release-branch head was built again: %d builds", n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAControllerPullRequestIntoAReleaseBranchReadsTheNodeEngineAtThatBranch(t *testing.T) {
|
||||||
|
aRuntimeThatBuildsTheJudge(t)
|
||||||
|
registry := aFactsRegistry(t)
|
||||||
|
beside, _, hostRelease, _ := theReleasedMesh(t)
|
||||||
|
delete(beside, "mesh-controller")
|
||||||
|
controller, head := aPullRequestInto(t, aJudgeOf("the change's controller"), releaseBranch,
|
||||||
|
map[string]string{"cmd/x.txt": "x"})
|
||||||
|
v, err := Check(t.Context(), Command, CheckSpec{ID: "check-704-controller", Repository: controller, Ref: head,
|
||||||
|
Owner: "novox", Repo: "mesh-controller", Number: 315, Base: releaseBranch, Release: releaseBranch,
|
||||||
|
JudgeCache: t.TempDir(), Paths: []string{"cmd/x.txt"}, Modules: []string{"mesh-controller"}, Judge: "self",
|
||||||
|
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 change's controller with the vendored validator") {
|
||||||
|
t.Fatalf("a controller change into a release branch did not judge itself: %+v\n%s", v.Gate, v.Report)
|
||||||
|
}
|
||||||
|
if v.BesideAt["mesh-host"] != releaseBranch || v.Beside["mesh-host"] != hostRelease[:12] {
|
||||||
|
t.Fatalf("mesh-host was read beside it at %q (%s), not at the release branch's %s", v.BesideAt["mesh-host"],
|
||||||
|
v.Beside["mesh-host"], hostRelease[:12])
|
||||||
|
}
|
||||||
|
if v.Release != releaseBranch || v.JudgedBy == "" || v.JudgedBy != v.Merge[:12] {
|
||||||
|
t.Fatalf("the verdict names release %q judged by %q, not its own merge %s", v.Release, v.JudgedBy, v.Merge)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPullRequestIntoTheTrunkIsJudgedAsBefore(t *testing.T) {
|
||||||
|
aRuntimeThatBuildsTheJudge(t)
|
||||||
|
registry := aFactsRegistry(t)
|
||||||
|
controller, controllerMain, _ := aRepositoryWithARelease(t, aJudgeOf("main's controller"),
|
||||||
|
aJudgeOf("the release branch's controller"))
|
||||||
|
host, hostMain, _ := aRepositoryWithARelease(t, aHostSaying("main's validator"),
|
||||||
|
aHostSaying("the release branch's validator"))
|
||||||
|
catalogue, head := aPullRequestInto(t, map[string]string{"modules/gitea/module.json": `{"module":"gitea"}`,
|
||||||
|
CheckScript: "true\n"}, "main", map[string]string{"modules/gitea/index.ts": "x"})
|
||||||
|
v, err := Check(t.Context(), Command, CheckSpec{ID: "check-704-trunk", Repository: catalogue, Ref: head,
|
||||||
|
Owner: "novox", Repo: "mesh-catalog", Number: 1, 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 != "main's controller with the vendored validator" {
|
||||||
|
t.Fatalf("a trunk pull request was not judged by the controller beside it as before: %+v\n%s", v.Gate, v.Report)
|
||||||
|
}
|
||||||
|
if v.Release != "" || v.JudgedBy != "" || v.BesideAt != nil || strings.Contains(v.Report, "release branch") {
|
||||||
|
t.Fatalf("a trunk pull request's verdict speaks of a release branch: %q %q %v", v.Release, v.JudgedBy, v.BesideAt)
|
||||||
|
}
|
||||||
|
if 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])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -167,6 +167,12 @@ type CheckRequest struct {
|
|||||||
// the controller, judged by itself; JudgeValidator for a change to the node-engine, judged by the
|
// the controller, judged by itself; JudgeValidator for a change to the node-engine, judged by the
|
||||||
// running controller built with the change's validator in place of the one it vendors.
|
// running controller built with the change's validator in place of the one it vendors.
|
||||||
Judge string `json:"judge,omitempty"`
|
Judge string `json:"judge,omitempty"`
|
||||||
|
// Release is the release branch the pull request merges into (novox/hq ADR 0358): its base, when that is not its
|
||||||
|
// repository's trunk and mesh-controller has a branch of that name. The gate is then judged by the controller
|
||||||
|
// built from mesh-controller's release branch — a change to the controller still by itself, a change to the
|
||||||
|
// node-engine with its own validator — and each repository beside it is read at its branch of that name where it
|
||||||
|
// has one (CheckedOut.Else otherwise). Empty for every other base, checked as before it.
|
||||||
|
Release string `json:"release,omitempty"`
|
||||||
// Group names a delivery group whose members' heads are composed together with this one (novox/hq ADR
|
// Group names a delivery group whose members' heads are composed together with this one (novox/hq ADR
|
||||||
// 0239), and Members are those other heads, each cloned beside it and laid over the mesh in turn. The
|
// 0239), and Members are those other heads, each cloned beside it and laid over the mesh in turn. The
|
||||||
// repository's own check is not run for a group: each member's pull request runs its own.
|
// repository's own check is not run for a group: each member's pull request runs its own.
|
||||||
@@ -257,6 +263,10 @@ const (
|
|||||||
type CheckedOut struct {
|
type CheckedOut struct {
|
||||||
Repository string `json:"repository"`
|
Repository string `json:"repository"`
|
||||||
Ref string `json:"ref,omitempty"`
|
Ref string `json:"ref,omitempty"`
|
||||||
|
// Else is the ref read when the repository has no branch Ref (novox/hq ADR 0358 §3): beside a check of a pull
|
||||||
|
// request into a release branch, Ref is that branch and Else the repository's trunk. Empty: Ref is read, or the
|
||||||
|
// check cannot run.
|
||||||
|
Else string `json:"else,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckOutcome is a merge check's verdict.
|
// CheckOutcome is a merge check's verdict.
|
||||||
@@ -281,6 +291,12 @@ type CheckOutcome struct {
|
|||||||
Members []CheckedMember `json:"members,omitempty"`
|
Members []CheckedMember `json:"members,omitempty"`
|
||||||
// Built are the builds it made for its merge (novox/hq ADR 0350 §1).
|
// Built are the builds it made for its merge (novox/hq ADR 0350 §1).
|
||||||
Built []CheckBuilt `json:"built,omitempty"`
|
Built []CheckBuilt `json:"built,omitempty"`
|
||||||
|
// Release is the release branch the check was judged for (novox/hq ADR 0358 §4), JudgedBy the commit of the
|
||||||
|
// controller that judged its gate, as twelve characters, and BesideAt the branch each repository beside it was
|
||||||
|
// read at, by directory. Empty from a check of any other base, and from a build seat from before it.
|
||||||
|
Release string `json:"release,omitempty"`
|
||||||
|
JudgedBy string `json:"judged-by,omitempty"`
|
||||||
|
BesideAt map[string]string `json:"beside-at,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.
|
||||||
|
|||||||
@@ -293,6 +293,12 @@ type Checked struct {
|
|||||||
Trunk string `json:"trunk,omitempty"`
|
Trunk string `json:"trunk,omitempty"`
|
||||||
Beside map[string]string `json:"beside,omitempty"`
|
Beside map[string]string `json:"beside,omitempty"`
|
||||||
Took string `json:"took,omitempty"`
|
Took string `json:"took,omitempty"`
|
||||||
|
// Release is the release branch the check judged for (novox/hq ADR 0358 §4), JudgedBy the commit of the
|
||||||
|
// controller that judged its gate (twelve characters), and BesideAt the branch each repository beside it was
|
||||||
|
// read at, by directory: so a reader sees a release-branch judgement apart from a trunk one. Empty otherwise.
|
||||||
|
Release string `json:"release,omitempty"`
|
||||||
|
JudgedBy string `json:"judged-by,omitempty"`
|
||||||
|
BesideAt map[string]string `json:"beside-at,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