Files
mesh-controller/internal/builder/check.go
T
jochen 487aa040de
mesh/merge-gate pass: every machine composes with the change as it did without (0 of 4 compose)
mesh/delivery delivered
mesh/delivery-group group feat/mesh-delivery delivered: every member is delivered
Let a walk wait for its delivery's word, and serve the delivery's owner (hq ADR 0239)
While the mesh-delivery seat has a holder on record, a merge that moves no
core module opens its walk and asks nothing until mesh-delivery or a person
says go; nothing of it is registered before its turn, so no other send
carries it. The controller keeps the planner, the gate, sending and the
walk, and gains the verbs the owner asks with: delivery-plan, -order,
-check (a group composed as one future state), deliver, delivery-stop,
delivery-walks; every walk kept is said as plan-moved.
2026-10-07 00:01:42 +02:00

823 lines
32 KiB
Go

package builder
import (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/novox/mesh-controller/internal/artifacts"
"github.com/novox/mesh-controller/internal/facts"
)
// A pull request's merge check, run on the build seat (novox/hq to-be 45 §9, ADR 0237).
//
// **The build machine already has what a check needs**: the repositories, a container runtime, the
// artifact store where the controller keeps the facts snapshot, and a Go toolchain. So a check is one
// more kind of work on the build seat's queue rather than a CI the mesh would have to run beside itself.
//
// **Two layers, each its own status on the pull request** (ADR 0237 as amended, 2026-10-06):
//
// - **the gate** (`mesh/merge-gate`) runs when the change touches a module of the mesh's graph — the
// controller, which holds the graph, says which (Modules) and which directories it adds a module in
// (New). The touched manifests through `module check`; every machine of the facts snapshot composed
// with the change and validated by the node-engine's own validator; then mesh-lab's replays. The
// judge is the controller the mesh runs — or, for a change to the controller, the change's own
// controller, and for a change to the node-engine, the running controller with the change's validator
// in place of the one it vendors. The graph decides whether this runs, never the repository.
// - **the repository's own check** (`mesh/repo-check`): its merge-check.sh, its unit tests and code
// quality, run when present in the toolchain it declares (`# mesh-check-toolchain: go|typescript`
// among its first lines; go when it declares none). A repository that reaches the build seat with
// none is said as a warning: it is the mesh's, and nothing of its own is tested before it merges.
//
// One check:
//
// 1. clones the repository at the pull request's head, and beside it the repositories its check
// reads — the controller the mesh runs, the catalogue, the host, the lab — each at the ref asked;
// 2. reads the facts snapshot the controller keeps, and with it **the versions the mesh runs**: the
// store a check's tests stand on is the store's own release, and the bus the bus's;
// 3. raises a throwaway PostgreSQL and a throwaway bus of those versions, labelled with the ask so a
// kill or a crash leaves nothing behind;
// 4. runs the gate, then the repository's script — **each in a toolchain container of its own**, never
// in the build machine: a pull request is code nobody has approved yet, and the build machine holds
// the container runtime's socket; the check's container holds none, and reaches only the throwaway
// store and bus on loopback. Only the replays — mesh-lab's main, reviewed code — get the socket;
// 5. answers each layer pass, warning or fail from what ran, and error — never pass — when it could
// not run.
// CheckSpec is one check, as the controller asks it.
type CheckSpec struct {
ID string
Repository string
Ref string
Owner string
Repo string
Number int
Paths []string
// Beside are the repositories cloned next to it, by the directory they are found under.
Beside map[string]Beside
// Modules are the modules of the mesh's graph the change touches, New the directories it adds a
// module in, and Manifests the manifests among them in the change's tree: the gate runs when Modules
// or New is not empty.
Modules []string
New []string
Manifests []string
// Base is the branch the pull request merges into: what the gate compares the change against.
Base string
// 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.
Judge string
// 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.
Toolchain string
Toolchains map[string]string
// Group are a delivery group's other heads (novox/hq ADR 0239), each cloned beside this one at its head
// and composed with it by the gate as one future state. The repository's own check is not run for a
// group: each member's pull request runs its own.
Group []GroupHead
}
// GroupHead is one other head of a delivery group's composed check.
type GroupHead struct {
Owner string
Repo string
Repository string
Ref string
Paths []string
}
// Gated is whether the change touches the mesh's graph, and so whether the gate runs.
func (s CheckSpec) Gated() bool { return len(s.Modules)+len(s.New) > 0 }
// ToolchainOf is the Go toolchain image among what the mesh holds, empty when it holds none.
func ToolchainOf(held map[string]string) string {
return ToolchainsOf(held)["go"]
}
// ToolchainsOf is every toolchain image the mesh holds, by language.
func ToolchainsOf(held map[string]string) map[string]string {
out := map[string]string{}
for _, chain := range toolchains {
if image := held[chain.Base+"/"+chain.Artifact]; image != "" {
out[chain.Language] = image
}
}
return out
}
// Beside is one repository cloned next to the one checked.
type Beside struct {
Repository string
Ref string
}
// CheckVerdict is what came of one check. Verdict and Summary are the gate's; Gate and Repo each layer.
type CheckVerdict struct {
Verdict string
Summary string
Report string
Took time.Duration
Gate *Layer
Repo *Layer
}
// Layer is one layer of a check, judged.
type Layer struct {
Verdict string
Summary string
Modules []string
}
// CheckScript is what a repository declares its own merge check as: run from its root, with the
// environment below.
const CheckScript = "merge-check.sh"
// Where a check finds what the builder raised and read for it.
const (
EnvFacts = "MESH_FACTS"
EnvGate = "MESH_GATE"
EnvGateStore = "MESH_GATE_POSTGRES"
EnvTestStore = "MESH_TEST_POSTGRES"
EnvTestBus = "MESH_TEST_NATS"
EnvRepository = "MESH_CHECK_REPOSITORY"
EnvChanged = "MESH_CHECK_CHANGED"
EnvVerdict = "MESH_CHECK_VERDICT"
EnvBeside = "MESH_CHECK_BESIDE"
EnvModules = "MESH_CHECK_MODULES"
// EnvGroup is a delivery group's other heads, as JSON, for the gate (novox/hq ADR 0239); empty for a
// pull request checked alone.
EnvGroup = "MESH_CHECK_GROUP"
)
// CheckTimeout bounds one check; a check that runs past it is an error, not a pass.
var CheckTimeout = 45 * time.Minute
// reportLines is how much of what a check printed travels in its verdict.
const reportLines = 200
// noModule is the gate's word for a change that touches nothing of the mesh's graph: a fact, not a
// missing check, so a pass.
const noModule = "the change touches no module of the mesh's graph"
// noScript is the repository layer's word for a repository with no merge-check.sh of its own.
const noScript = "the repository declares no " + CheckScript + ": none of its own tests run before it merges"
// toolchainLine is how a merge-check.sh declares the toolchain it runs in.
var toolchainLine = regexp.MustCompile(`^#\s*mesh-check-toolchain:\s*([a-z0-9-]+)\s*$`)
// ScriptToolchain is the language a merge-check.sh declares it runs in, among its first twenty lines;
// go when it declares none.
func ScriptToolchain(script []byte) string {
lines := bufio.NewScanner(bytes.NewReader(script))
for i := 0; i < 20 && lines.Scan(); i++ {
if m := toolchainLine.FindStringSubmatch(strings.TrimSpace(lines.Text())); m != nil {
return m[1]
}
}
return "go"
}
// Check runs one merge check. An error is that it could not run; the verdict is then "error".
func Check(ctx context.Context, run Runner, spec CheckSpec, workspace, registry string, forge GitCredential,
log Log) (CheckVerdict, error) {
say := logging(log)
began := time.Now()
ctx, stop := context.WithTimeout(ctx, CheckTimeout)
defer stop()
root := filepath.Join(workspace, "check")
if err := os.RemoveAll(root); err != nil {
return CheckVerdict{}, err
}
if err := os.MkdirAll(root, 0o755); err != nil {
return CheckVerdict{}, err
}
defer os.RemoveAll(root)
credentials := ""
if forge.URL != "" {
credentials = filepath.Join(workspace, "git-credentials")
if err := os.WriteFile(credentials, []byte(forge.URL+"\n"), 0o600); err != nil {
return CheckVerdict{}, err
}
}
clone := func(repository, ref, dir string) error {
if _, err := run(ctx, root, "git", cloneWith(credentials, "clone", "--quiet", repository, dir)...); err != nil {
return fmt.Errorf("cannot clone %s: %w", repository, err)
}
if ref != "" {
if _, err := run(ctx, filepath.Join(root, dir), "git", "checkout", "--quiet", ref); err != nil {
return fmt.Errorf("%s has no %s: %w", repository, ref, err)
}
}
return nil
}
name := spec.Repo
if name == "" || !safeName.MatchString(name) {
name = "checked"
}
say("check", "%s/%s#%d at %s", spec.Owner, spec.Repo, spec.Number, short(spec.Ref))
if err := clone(spec.Repository, spec.Ref, name); err != nil {
return CheckVerdict{}, err
}
tree := filepath.Join(root, name)
script, scriptErr := os.ReadFile(filepath.Join(tree, CheckScript))
hasScript := scriptErr == nil
gated := spec.Gated()
if !gated && !hasScript {
// Nothing to run: said, never passed silently.
v := CheckVerdict{Verdict: "pass", Summary: noModule, Took: time.Since(began),
Gate: &Layer{Verdict: "pass", Summary: noModule}, Repo: &Layer{Verdict: "warning", Summary: noScript}}
say("check", "%s; %s", noModule, noScript)
return v, nil
}
for dir, b := range spec.Beside {
if dir == name || !safeName.MatchString(dir) {
continue
}
if err := clone(b.Repository, b.Ref, dir); err != nil {
return CheckVerdict{}, fmt.Errorf("beside it, %w", err)
}
say("check", "beside it %s at %s", dir, short(b.Ref))
}
// A delivery group's other heads (novox/hq ADR 0239), each at its head, for the gate to compose with this.
groupFile := ""
if len(spec.Group) > 0 {
var heads []map[string]any
for i, g := range spec.Group {
dir := fmt.Sprintf("group-%d", i)
if g.Repo != "" && safeName.MatchString(g.Repo) {
dir = "group-" + g.Repo
}
if err := clone(g.Repository, g.Ref, dir); err != nil {
return CheckVerdict{}, fmt.Errorf("a head of the group, %w", err)
}
say("check", "with it, of its group, %s/%s at %s", g.Owner, g.Repo, short(g.Ref))
heads = append(heads, map[string]any{"repository": g.Owner + "/" + g.Repo,
"tree": filepath.Join(root, dir), "changed": g.Paths})
}
raw, err := json.Marshal(heads)
if err != nil {
return CheckVerdict{}, err
}
groupFile = filepath.Join(root, "group.json")
if err := os.WriteFile(groupFile, raw, 0o644); err != nil {
return CheckVerdict{}, err
}
}
// The facts, and the versions they say the mesh runs.
if registry == "" {
return CheckVerdict{}, errors.New("no artifact store to read the facts snapshot from")
}
body, digest, err := (artifacts.Store{Address: registry}).GetTagged(ctx, facts.Repository, facts.Tag)
if err != nil {
return CheckVerdict{}, fmt.Errorf("the facts snapshot cannot be read, and a check without it judges nothing: %w", err)
}
f, err := facts.Decode(body)
if err != nil {
return CheckVerdict{}, err
}
factsFile := filepath.Join(root, "facts.json")
if err := os.WriteFile(factsFile, body, 0o644); err != nil {
return CheckVerdict{}, err
}
say("check", "the facts of %s (%s): %d machine(s), the bus at %s, the store at %s", f.Taken.Format(time.RFC3339),
short(strings.TrimPrefix(digest, "sha256:")), len(f.Machines), f.Versions.Bus, f.Versions.Store)
// The throwaway store and bus, of the versions the mesh runs, removed whatever happens.
defer func() {
removing, done := context.WithTimeout(context.Background(), time.Minute)
defer done()
if n, err := RemoveContainersOf(removing, run, spec.ID); err == nil && n > 0 {
say("check", "removed %d throwaway container(s)", n)
}
}()
labelled := Labelled(run, spec.ID)
store, err := throwaway(ctx, labelled, run, spec.ID+"-store", StoreImage(f.Versions.Store), 5432,
[]string{"-e", "POSTGRES_PASSWORD=check"}, nil)
if err != nil {
return CheckVerdict{}, err
}
storeURL := "postgres://postgres:check@" + store + "/postgres?sslmode=disable"
if err := waitFor(ctx, run, spec.ID+"-store", []string{"pg_isready", "-U", "postgres"}); err != nil {
return CheckVerdict{}, err
}
bus, err := throwaway(ctx, labelled, run, spec.ID+"-bus", BusImage(f.Versions.Bus), 4222, nil, []string{"-js"})
if err != nil {
return CheckVerdict{}, err
}
if err := dialable(ctx, bus); err != nil {
return CheckVerdict{}, err
}
say("check", "a throwaway store (%s) and bus (%s) of the versions the mesh runs", StoreImage(f.Versions.Store),
BusImage(f.Versions.Bus))
if spec.Toolchain == "" {
return CheckVerdict{}, errors.New("the mesh holds no Go toolchain to run a check in")
}
in := func(image, dir string, env []string, command ...string) []string {
// As the builder itself: what a check writes into the workspace is the builder's to remove.
args := []string{"run", "--rm", "--network", "host", "--volume", workspace + ":" + workspace, "--workdir", dir,
"--user", fmt.Sprintf("%d:%d", os.Getuid(), os.Getgid()), "--env", "HOME=" + workspace}
for _, e := range env {
args = append(args, "--env", e)
}
return append(append(args, image), command...)
}
inToolchain := func(dir string, env []string, command ...string) []string {
return in(spec.Toolchain, dir, env, command...)
}
var out tail
// running runs one container of the check, its output into the report, in a process group of its own.
running := func(args []string) error {
cmd := exec.CommandContext(ctx, "docker", LabelledArgs("docker", args, spec.ID)...)
inItsOwnGroup(cmd)
cmd.Stdout, cmd.Stderr = &out, &out
return cmd.Run()
}
// 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.
gate, judgeFault, err := judgeFor(ctx, labelled, run, spec, root, tree, inToolchain, say)
if err != nil {
return CheckVerdict{}, err
}
verdictFile := filepath.Join(root, "verdict.json")
env := []string{EnvFacts + "=" + factsFile, EnvGate + "=" + gate, EnvGateStore + "=" + storeURL,
EnvTestStore + "=" + storeURL, EnvTestBus + "=nats://" + bus, EnvRepository + "=" + spec.Owner + "/" + spec.Repo,
EnvChanged + "=" + strings.Join(spec.Paths, ","), EnvBeside + "=" + root,
EnvModules + "=" + strings.Join(append(append([]string{}, spec.Modules...), spec.New...), ","),
EnvGroup + "=" + groupFile,
"GOCACHE=" + filepath.Join(workspace, "go-cache"), "GOMODCACHE=" + filepath.Join(workspace, "go-modules")}
v := CheckVerdict{}
modules := append(append([]string{}, spec.Modules...), prefixed("new:", spec.New)...)
timedOut := func() bool { return errors.Is(ctx.Err(), context.DeadlineExceeded) }
// **The gate**, when the graph says the change touches it.
if !gated {
v.Gate = &Layer{Verdict: "pass", Summary: noModule}
} else {
v.Gate = &Layer{Modules: modules}
switch {
case judgeFault != "":
v.Gate.Verdict, v.Gate.Summary = "fail", judgeFault
default:
say("check", "the gate: %d module(s) of the graph touched — %s", len(modules), strings.Join(modules, ", "))
fmt.Fprintf(&out, "--- the gate: %s\n", strings.Join(modules, ", "))
v.Gate.Verdict, v.Gate.Summary = gateLayer(ctx, spec, tree, root, gate, verdictFile, env, inToolchain,
running, &out, bus, workspace, name, say)
}
if timedOut() {
v.Gate.Verdict, v.Gate.Summary = "error", fmt.Sprintf("the check ran past %s and was ended", CheckTimeout)
}
}
if ctx.Err() != nil && !timedOut() {
return v, ctx.Err()
}
// **The repository's own check**, in the toolchain it declares.
switch {
case len(spec.Group) > 0:
// A group's composed check judges the heads together; each member's own tests are its pull request's.
v.Repo = &Layer{Verdict: "pass", Summary: "a group's composed check: each member's own " + CheckScript +
" runs on its pull request"}
case !hasScript:
v.Repo = &Layer{Verdict: "warning", Summary: noScript}
case timedOut():
v.Repo = &Layer{Verdict: "error", Summary: fmt.Sprintf("the check ran past %s before its %s ran", CheckTimeout, CheckScript)}
default:
language := ScriptToolchain(script)
image := spec.Toolchains[language]
if language == "go" && image == "" {
image = spec.Toolchain
}
if image == "" {
v.Repo = &Layer{Verdict: "error", Summary: fmt.Sprintf("its %s runs in the %s toolchain, which the mesh does "+
"not hold", CheckScript, language)}
break
}
say("check", "running its %s in the mesh's %s toolchain", CheckScript, language)
fmt.Fprintf(&out, "--- its %s (%s toolchain)\n", CheckScript, language)
var own tail
cmd := exec.CommandContext(ctx, "docker", LabelledArgs("docker", in(image, tree, env, "sh", CheckScript), spec.ID)...)
inItsOwnGroup(cmd)
w := io.MultiWriter(&out, &own)
cmd.Stdout, cmd.Stderr = w, w
switch err := cmd.Run(); {
case timedOut():
v.Repo = &Layer{Verdict: "error", Summary: fmt.Sprintf("its %s ran past %s and was ended", CheckScript, CheckTimeout)}
case ctx.Err() != nil:
return v, ctx.Err()
case err != nil:
v.Repo = &Layer{Verdict: "fail", Summary: "its " + CheckScript + " failed: " + lastLine(own.String())}
default:
v.Repo = &Layer{Verdict: "pass", Summary: "its " + CheckScript + " passed"}
}
}
v.Verdict, v.Summary = v.Gate.Verdict, v.Gate.Summary
v.Report, v.Took = out.String(), time.Since(began)
say("check", "gate %s — %s; repository %s — %s (%s)", strings.ToUpper(v.Gate.Verdict), v.Gate.Summary,
strings.ToUpper(v.Repo.Verdict), v.Repo.Summary, v.Took.Round(time.Second))
return v, nil
}
// gateLayer runs the gate: the touched manifests through `module check`, every machine composed with the
// change, and the replays of what the mesh runs. It answers the gate's verdict and summary.
func gateLayer(ctx context.Context, spec CheckSpec, tree, root, gate, verdictFile string, env []string,
inToolchain func(string, []string, ...string) []string, running func([]string) error, out *tail, bus, workspace,
name string, say func(step, format string, args ...any)) (string, string) {
// 1. The manifests the change touches, as the judge reads them: a manifest it cannot read, or one with a
// problem the change brings, fails here. **What was already so on the base branch is said and fails
// nothing** — the gate's own rule: a module whose manifest the running controller already finds fault
// with would otherwise fail every pull request that touches it, for a fault none of them made.
var manifests []string
for _, m := range spec.Manifests {
if _, err := os.Stat(filepath.Join(tree, m)); err == nil {
manifests = append(manifests, m)
}
}
if len(manifests) > 0 {
checked := func(dir string) (string, error) {
var own tail
cmd := exec.CommandContext(ctx, "docker", LabelledArgs("docker",
inToolchain(dir, env, append([]string{gate, "module", "check"}, manifests...)...), spec.ID)...)
inItsOwnGroup(cmd)
w := io.MultiWriter(out, &own)
cmd.Stdout, cmd.Stderr = w, w
err := cmd.Run()
return own.String(), err
}
said, err := checked(tree)
if err != nil {
if ctx.Err() != nil {
return "error", "the check was ended during the module check"
}
// The same manifests as the base branch has them, beside the change.
was := ""
if spec.Base != "" {
base := filepath.Join(root, "base-manifests")
for _, m := range manifests {
body, err := gitShow(ctx, tree, "origin/"+spec.Base, m)
if err != nil {
continue // new on this branch: nothing was so before it
}
if err := os.MkdirAll(filepath.Dir(filepath.Join(base, m)), 0o755); err == nil {
_ = os.WriteFile(filepath.Join(base, m), body, 0o644)
}
}
fmt.Fprintf(out, "--- the same manifests on %s\n", spec.Base)
if _, err := os.Stat(base); err == nil {
was, _ = checked(base)
}
}
brought := newProblems(said, was)
if len(brought) > 0 {
return "fail", "a manifest the change touches fails the module check: " + brought[0]
}
fmt.Fprintf(out, "the module check's problems were all so on %s already: said, not the change's\n", spec.Base)
}
}
// 2. Every machine composed with the change.
if err := running(inToolchain(tree, append(env, EnvVerdict+"="+verdictFile), "sh", "-c",
`"$MESH_GATE" merge-gate --facts "$MESH_FACTS" --store "$MESH_GATE_POSTGRES" `+
`--repository "$MESH_CHECK_REPOSITORY" --tree . --changed "$MESH_CHECK_CHANGED" `+
`${MESH_CHECK_GROUP:+--group "$MESH_CHECK_GROUP"} --json > "$MESH_CHECK_VERDICT"`)); err != nil {
if ctx.Err() != nil {
return "error", "the check was ended during the merge gate"
}
var said struct {
Verdict string `json:"verdict"`
Summary string `json:"summary"`
}
if raw, err := os.ReadFile(verdictFile); err == nil && json.Unmarshal(raw, &said) == nil && said.Verdict == "fail" {
return "fail", said.Summary
}
// The gate could not judge: not the change's fault, and never a pass.
return "error", "the merge gate could not judge the change: " + lastLine(out.String())
}
var said struct {
Verdict string `json:"verdict"`
Summary string `json:"summary"`
}
raw, err := os.ReadFile(verdictFile)
if err != nil || json.Unmarshal(raw, &said) != nil || said.Verdict == "" {
return "error", "the merge gate said no verdict"
}
// 3. **The replays of what the mesh runs** (to-be 45 §9, M9): mesh-lab's, from its main — reviewed code,
// so given the container runtime the resolver replay raises containers with — against the bus of the
// release the mesh runs and the change's own catalogue when the change is to the catalogue.
if lab := filepath.Join(root, "mesh-lab", "replays"); ctx.Err() == nil {
if _, err := os.Stat(lab); err == nil {
catalogue := filepath.Join(root, "mesh-catalog")
if name == "mesh-catalog" {
catalogue = tree
}
say("check", "the replays of what the mesh runs, from mesh-lab")
fmt.Fprintln(out, "--- the replays (mesh-lab replays/)")
args := inToolchain(lab, []string{EnvTestBus + "=nats://" + bus, "MESH_REPLAY_CATALOGUE=" + catalogue,
"GOCACHE=" + filepath.Join(workspace, "go-cache"), "GOMODCACHE=" + filepath.Join(workspace, "go-modules")},
"go", "test", "-count=1", "./...")
// The socket goes to the replays alone, never to the change's own code above.
args = append([]string{args[0], "--volume", "/var/run/docker.sock:/var/run/docker.sock"}, args[1:]...)
if err := running(args); err != nil {
if ctx.Err() != nil {
return "error", "the check was ended during the replays"
}
return "fail", "a replay of a core incident fails with this change: " + lastLine(out.String())
}
}
}
if said.Verdict == "pass" || said.Verdict == "warning" || said.Verdict == "fail" {
return said.Verdict, said.Summary
}
return "error", "the merge gate said " + said.Verdict
}
// judgeFor builds the judge of a check: the change's own controller (a change to the controller), the
// running controller built with the change's validator (a change to the node-engine), or the controller
// 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
// 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,
inToolchain func(string, []string, ...string) []string, say func(step, format string, args ...any)) (string, string, error) {
gated := spec.Gated()
switch spec.Judge {
case "self":
bin := filepath.Join(root, "bin", "judge-of-itself")
if _, err := labelled(ctx, root, "docker", inToolchain(tree,
[]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 "", "the change's controller, which judges itself, does not build: " + firstLine(err.Error()), nil
}
say("check", "judged by the change's own controller")
return bin, "", nil
}
bin, judgeTree, err := judge(ctx, labelled, run, root, inToolchain, say)
if err != nil {
if !gated {
say("check", "no judge for its script: %v", err)
return "", "", nil
}
return "", "", err
}
if spec.Judge != "validator" {
return bin, "", nil
}
// The node-engine's change: its validator in place of the one the judge vendors.
vendored := filepath.Join(root, judgeTree, "vendor", "github.com", "novox", "mesh-host")
for _, pkg := range []string{"validate", filepath.Join("internal", "declaration")} {
if err := replaceGoFiles(filepath.Join(tree, pkg), filepath.Join(vendored, pkg)); err != nil {
return "", "", fmt.Errorf("the change's validator could not be put in the judge: %w", err)
}
}
withValidator := filepath.Join(root, "bin", "judge-with-this-validator")
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")},
"go", "build", "-o", withValidator, "./cmd/mesh-controller")...); err != nil {
return "", "the controller the mesh runs does not build with this change's validator: " + firstLine(err.Error()), nil
}
say("check", "judged by the controller the mesh runs, with this change's validator")
return withValidator, "", nil
}
// replaceGoFiles puts a package's Go files — never its tests — in place of another copy's.
func replaceGoFiles(from, into string) error {
old, err := filepath.Glob(filepath.Join(into, "*.go"))
if err != nil {
return err
}
for _, f := range old {
if err := os.Remove(f); err != nil {
return err
}
}
if err := os.MkdirAll(into, 0o755); err != nil {
return err
}
files, err := filepath.Glob(filepath.Join(from, "*.go"))
if err != nil {
return err
}
if len(files) == 0 {
return fmt.Errorf("%s holds no Go files", from)
}
for _, f := range files {
if strings.HasSuffix(f, "_test.go") {
continue
}
body, err := os.ReadFile(f)
if err != nil {
return err
}
if err := os.WriteFile(filepath.Join(into, filepath.Base(f)), body, 0o644); err != nil {
return err
}
}
return nil
}
// problemLines are the lines of a module check's output that name a problem: not a manifest found ok, not
// the count, not the closing note.
func problemLines(s string) []string {
var out []string
for _, line := range strings.Split(s, "\n") {
line = strings.TrimSpace(line)
switch {
case line == "", strings.Contains(line, ": ok"), strings.Contains(line, "problem(s) in"),
strings.Contains(line, "manifest(s) checked"):
continue
}
out = append(out, line)
}
return out
}
// newProblems are the problems a module check said of the change that it did not say of the base.
func newProblems(change, base string) []string {
was := map[string]bool{}
for _, p := range problemLines(base) {
was[p] = true
}
var out []string
for _, p := range problemLines(change) {
if !was[p] {
out = append(out, p)
}
}
if len(out) == 0 && len(problemLines(change)) == 0 {
// It failed and named nothing: not readable as already so.
out = append(out, lastLine(change))
}
return out
}
// gitShow is a file as a ref has it.
func gitShow(ctx context.Context, dir, ref, file string) ([]byte, error) {
cmd := exec.CommandContext(ctx, "git", "show", ref+":"+filepath.ToSlash(file))
cmd.Dir = dir
return cmd.Output()
}
func prefixed(prefix string, items []string) []string {
out := make([]string, 0, len(items))
for _, i := range items {
out = append(out, prefix+i)
}
return out
}
func firstLine(s string) string {
line, _, _ := strings.Cut(strings.TrimSpace(s), "\n")
return line
}
// safeName is a directory a repository beside a check may be cloned under.
var safeName = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*$`)
// StoreImage is the store a check stands on: the major release the mesh's store runs (`17.11` → 17),
// on Alpine as the store module runs it.
func StoreImage(version string) string {
major, _, _ := strings.Cut(strings.TrimSpace(version), ".")
if major == "" || strings.ContainsAny(major, " (") {
major = "17"
}
return "postgres:" + major + "-alpine"
}
// BusImage is the bus a check stands on: the release the mesh's bus server runs.
func BusImage(version string) string {
v := strings.TrimPrefix(strings.TrimSpace(version), "v")
if v == "" {
v = "2.11"
}
return "nats:" + v + "-alpine"
}
// throwaway starts a container publishing one port on loopback, and answers where it is reached.
func throwaway(ctx context.Context, run, plain Runner, name, image string, port int, opts, args []string) (string, error) {
invocation := []string{"run", "-d", "--rm", "--name", name, "-p", fmt.Sprintf("127.0.0.1::%d", port)}
invocation = append(invocation, opts...)
invocation = append(invocation, image)
invocation = append(invocation, args...)
if _, err := run(ctx, "", "docker", invocation...); err != nil {
return "", fmt.Errorf("a throwaway %s could not be raised: %w", image, err)
}
out, err := plain(ctx, "", "docker", "port", name, fmt.Sprintf("%d/tcp", port))
if err != nil {
return "", err
}
for _, line := range strings.Split(strings.TrimSpace(out), "\n") {
if strings.HasPrefix(line, "127.0.0.1:") {
return strings.TrimSpace(line), nil
}
}
return "", fmt.Errorf("%s published %d nowhere on loopback: %q", name, port, out)
}
// waitFor runs a readiness command in a container until it answers, for a minute.
func waitFor(ctx context.Context, run Runner, name string, ready []string) error {
for i := 0; i < 60; i++ {
if _, err := run(ctx, "", "docker", append([]string{"exec", name}, ready...)...); err == nil {
return nil
}
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(time.Second):
}
}
return fmt.Errorf("%s never became ready", name)
}
// dialable waits for an address to take a connection, for a minute.
func dialable(ctx context.Context, address string) error {
for i := 0; i < 60; i++ {
if c, err := net.DialTimeout("tcp", address, time.Second); err == nil {
c.Close()
return nil
}
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(time.Second):
}
}
return fmt.Errorf("nothing took a connection at %s", address)
}
// judge builds the controller that judges a change: the one the mesh runs, beside the check as
// mesh-controller — or, when that one predates the merge gate, the controller's main, said. It answers
// the binary and the directory it was built from.
func judge(ctx context.Context, run, plain Runner, root string, inToolchain func(string, []string, ...string) []string,
say func(step, format string, args ...any)) (string, string, error) {
bin := filepath.Join(root, "bin", "mesh-controller")
build := func(dir string) error {
_, 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")...)
return err
}
// Static, so it runs where the builder does.
hasGate := func() bool {
out, _ := plain(ctx, root, bin, "merge-gate")
return strings.Contains(out, "merge-gate --facts")
}
if _, err := os.Stat(filepath.Join(root, "mesh-controller")); err == nil {
if err := build("mesh-controller"); err != nil {
return "", "", fmt.Errorf("the controller the mesh runs does not build: %w", err)
}
if hasGate() {
say("check", "judged by the controller the mesh runs")
return bin, "mesh-controller", nil
}
}
if _, err := os.Stat(filepath.Join(root, "mesh-controller-main")); err != nil {
return "", "", errors.New("no controller beside the check to judge it with")
}
if err := build("mesh-controller-main"); err != nil {
return "", "", fmt.Errorf("the controller's main does not build: %w", err)
}
say("check", "judged by the controller's main: the one the mesh runs predates the merge gate")
return bin, "mesh-controller-main", nil
}
// tail keeps the last lines written to it.
type tail struct{ buf bytes.Buffer }
func (t *tail) Write(p []byte) (int, error) {
t.buf.Write(p)
if t.buf.Len() > 1<<20 {
keep := t.buf.Bytes()[t.buf.Len()-(512<<10):]
t.buf = *bytes.NewBuffer(append([]byte(nil), keep...))
}
return len(p), nil
}
func (t *tail) String() string {
lines := strings.Split(strings.TrimRight(t.buf.String(), "\n"), "\n")
if len(lines) > reportLines {
lines = lines[len(lines)-reportLines:]
}
return strings.Join(lines, "\n")
}
func lastLine(s string) string {
lines := strings.Split(strings.TrimSpace(s), "\n")
return strings.TrimSpace(lines[len(lines)-1])
}