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]) }