Judge every pull request against the mesh that runs, before it merges (hq ADR 0237, to-be 45 §9)

Every check the mesh had ran after a merge, on a machine: a manifest the node-engine refused
(236), an identity a real machine's name made too long (263). merge-gate raises the mesh as the
facts snapshot says it is and the mesh with the change, each in a throwaway store through the
controller's own records, composes every machine twice and validates it with the node-engine's
validator, and fails what the change breaks, naming the machine's roles and the module - plus a
manifest the judging controller cannot read, a consumer left out of its grant, a module removed
while a machine runs it, a new module the node-engine would refuse; it warns on a wide rebuild.

The forge's new head of a pull request becomes a check the controller asks of the build seat:
the head and, beside it, the controller the mesh runs, the catalogue, the host and the lab; a
throwaway store and bus of the versions the mesh runs; the repository's merge-check.sh in the
mesh's Go toolchain with no container runtime socket; then mesh-lab's replays. The verdict is
said as checked, an error never a pass, and nothing is recorded or registered.
This commit is contained in:
jochen
2026-10-06 21:11:26 +02:00
parent 068283137b
commit 9c714f00d6
22 changed files with 2320 additions and 6 deletions
+29 -1
View File
@@ -237,7 +237,21 @@ func answer(ctx context.Context, publisher builder.Publisher, on, workspace stri
npmrc, err := packagesFrom() npmrc, err := packagesFrom()
var built builder.Result var built builder.Result
if err == nil { if request.Check != nil {
// **A pull request's merge check, not a build** (novox/hq to-be 45 §9): nothing is built,
// published or registered; the verdict is the outcome.
result.Checked = request.Check
registry := ""
if r, ok := publisher.(builder.Registry); ok {
registry = r.Address
}
var v builder.CheckVerdict
v, err = builder.Check(building, builder.Command, checkSpecOf(request), workspace, registry, forgeFrom(), say)
if err == nil {
result.Check = &link.CheckOutcome{Verdict: v.Verdict, Summary: v.Summary, Report: v.Report,
Took: v.Took.Round(time.Second).String()}
}
} else if err == nil {
// The package-registry credential is a build input, so it is resolved before the clone: a // The package-registry credential is a build input, so it is resolved before the clone: a
// build that could not have resolved its dependencies is refused in front of the reason, not // build that could not have resolved its dependencies is refused in front of the reason, not
// after a clone that then fails at npm ci. // after a clone that then fails at npm ci.
@@ -263,6 +277,8 @@ func answer(ctx context.Context, publisher builder.Publisher, on, workspace stri
// builder that is not running, and those want completely different responses. // builder that is not running, and those want completely different responses.
result.Failed = err.Error() result.Failed = err.Error()
say("failed", err.Error()) say("failed", err.Error())
} else if request.Check != nil {
say("checked", result.Check.Verdict+": "+result.Check.Summary)
} else { } else {
manifest, marshalErr := json.Marshal(built.Manifest) manifest, marshalErr := json.Marshal(built.Manifest)
if marshalErr != nil { if marshalErr != nil {
@@ -312,6 +328,18 @@ func answer(ctx context.Context, publisher builder.Publisher, on, workspace stri
} }
} }
// checkSpecOf is a check request as the builder runs it.
func checkSpecOf(request link.BuildRequest) builder.CheckSpec {
c := request.Check
spec := builder.CheckSpec{ID: request.ID, Repository: request.Repository, Ref: request.Ref,
Owner: c.Owner, Repo: c.Repo, Number: c.Number, Paths: c.Paths, Beside: map[string]builder.Beside{},
Toolchain: builder.ToolchainOf(request.Held)}
for dir, b := range c.Beside {
spec.Beside[dir] = builder.Beside{Repository: b.Repository, Ref: b.Ref}
}
return spec
}
// packagesFrom is where a build resolves the mesh's own published packages — the SDK above all // packagesFrom is where a build resolves the mesh's own published packages — the SDK above all
// (novox/hq ADR 0076, issue 053). // (novox/hq ADR 0076, issue 053).
// //
+198
View File
@@ -0,0 +1,198 @@
package main
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/novox/mesh-controller/internal/catalogue"
"github.com/novox/mesh-controller/internal/inventory"
"github.com/novox/mesh-controller/internal/link"
)
// A pull request's merge check (novox/hq to-be 45 §9): the forge announces a pull request's new head,
// the controller asks the build seat to check it, and says the verdict as `checked`, which the forge's
// holder sets as the pull request's status. **Before merge, never after**: every check the mesh had ran
// after a merge, on a machine.
//
// What is checked is decided here and run there. Here: whether the mesh builds anything from the
// repository into that branch — a repository it builds nothing from is not its to judge — and what the
// check reads beside it: the controller the mesh runs (its judge, for a catalogue change: a manifest
// that controller cannot read fails, which is version skew caught), the catalogue the mesh holds, the
// host it runs. There: the repository's own merge-check.sh, or the merge gate alone for a repository
// that declares none (internal/builder/check.go).
// checkTimeout is how long one check may run on the build seat. Said here so the ask's watchdog (S6)
// and the builder agree on what late means.
const checkTimeout = 45 * time.Minute
// PullUpdated asks for a pull request's merge check.
func (f following) PullUpdated(ctx context.Context, p link.PullUpdated) error {
inv := f.open.inventory
entries, err := inv.Catalogued(ctx)
if err != nil {
return err
}
moved := link.SourceMoved{Owner: p.Owner, Repo: p.Repo, Base: p.Base, CloneURL: p.CloneURL}
var from *inventory.Entry
for i, e := range entries {
if !e.Provided && sourceIs(e.Source, moved) {
from = &entries[i]
break
}
}
if from == nil {
fmt.Printf("%s/%s#%d (%.8s): the mesh builds nothing from it into %s, so it is not the mesh's to check\n",
p.Owner, p.Repo, p.Number, p.Commit, p.Base)
return nil
}
request, err := checkRequestFor(ctx, f.open, p, *from, entries)
if err != nil {
return err
}
seat := buildSeatHeld(ctx)
ask, err := askOverOn(seat)
if err != nil {
return err
}
defer ask.Close()
if err := ask.Ask(ctx, request); err != nil {
return err
}
fmt.Printf("%s/%s#%d (%.8s): asked %s to check it before it merges, as %s\n", p.Owner, p.Repo, p.Number,
p.Commit, seat, request.ID)
return nil
}
// checkRequestFor is the ask for one pull request's head: the repository as the mesh clones it, the head,
// and what is read beside it.
func checkRequestFor(ctx context.Context, open *stores, p link.PullUpdated, from inventory.Entry,
entries []inventory.Entry) (link.BuildRequest, error) {
shelf := map[string]catalogue.Manifest{}
for _, e := range entries {
shelf[e.Manifest.Module] = e.Manifest
}
world, err := theRestOfTheMesh(ctx, open.inventory, shelf, "")
if err != nil {
return link.BuildRequest{}, err
}
clone := func(s inventory.Source) (string, error) {
if s.Seat == "" {
return s.Repository, nil
}
return clonedFromSeat(world, s.Seat, s.Repository)
}
repository, err := clone(from.Source)
if err != nil {
return link.BuildRequest{}, err
}
current, err := open.inventory.CurrentBuilds(ctx)
if err != nil {
return link.BuildRequest{}, err
}
// Beside it, at what the mesh runs: each core repository by the module the mesh builds from it.
beside := map[string]link.CheckedOut{}
byModule := map[string]string{"mesh-controller": "mesh-controller", "mesh-host": "mesh-host",
"node-tools": "mesh-tools", "nats": "mesh-catalog"}
for _, e := range entries {
dir, core := byModule[e.Manifest.Module]
if !core || e.Provided || e.Source.Repository == "" {
continue
}
url, err := clone(e.Source)
if err != nil {
return link.BuildRequest{}, err
}
ref := current[e.Manifest.Module].Commit
if dir == "mesh-catalog" {
// The catalogue the mesh runs is in the snapshot, every manifest as it holds it; its checkout
// beside is what tests read its files from, so its main — what the next merge builds from.
ref = "main"
}
beside[dir] = link.CheckedOut{Repository: url, Ref: ref}
if dir == "mesh-controller" {
// And its main, for a judge the running controller predates (Phase 5 rolling out).
beside["mesh-controller-main"] = link.CheckedOut{Repository: url, Ref: "main"}
// And the lab, whose replays of what the mesh runs every check runs; on the same forge.
if e.Source.Seat != "" {
if lab, err := clone(inventory.Source{Seat: e.Source.Seat, Repository: siblingOf(e.Source.Repository,
"mesh-lab")}); err == nil {
beside["mesh-lab"] = link.CheckedOut{Repository: lab, Ref: "main"}
}
}
}
}
return link.BuildRequest{
ID: link.NewBuildID(time.Now()),
Repository: repository,
Ref: p.Commit,
Held: heldBy(ctx),
Seats: seatBases(ctx),
Source: sourceOnSeat(from.Source),
Check: &link.CheckRequest{Owner: p.Owner, Repo: p.Repo, Number: p.Number, Base: p.Base,
Paths: p.Paths, Beside: beside},
}, nil
}
// siblingOf is another repository of the same owner: novox/mesh-controller → novox/mesh-lab.
func siblingOf(repository, name string) string {
if cut := strings.LastIndex(repository, "/"); cut >= 0 {
return repository[:cut+1] + name
}
return name
}
// sourceOnSeat is a source's seat form, nil for one on no seat.
func sourceOnSeat(s inventory.Source) *link.SourceOnSeat {
if s.Seat == "" {
return nil
}
return &link.SourceOnSeat{Seat: s.Seat, Repository: s.Repository}
}
// checkEvents is where the serving controller says a check's verdict; nil in a command.
var checkEvents link.Bus
// maxCheckReport is how much of a check's report travels in its verdict: enough for the failures and
// the machines, never a log.
const maxCheckReport = 60 << 10
// checked says a merge check's verdict as the controller's `checked`. Nothing is recorded or
// registered: a check builds nothing (issue 240's rule for a dry run, kept for a check).
func checked(ctx context.Context, result link.BuildResult) {
c := link.Checked{ID: result.ID, On: result.On, Commit: result.Ref}
if result.Checked != nil {
c.Owner, c.Repo, c.Number = result.Checked.Owner, result.Checked.Repo, result.Checked.Number
}
switch {
case result.Check != nil:
c.Verdict, c.Summary, c.Report = result.Check.Verdict, result.Check.Summary, result.Check.Report
case result.Failed != "":
// The check could not run: an error, never read as a pass.
c.Verdict, c.Summary = "error", "the check could not run: "+firstLine(result.Failed)
default:
c.Verdict, c.Summary = "error", "the build seat answered the check with no verdict"
}
if c.Verdict == "" {
c.Verdict = "error"
}
if len(c.Report) > maxCheckReport {
c.Report = "…" + c.Report[len(c.Report)-maxCheckReport:]
}
fmt.Printf("%s: %s/%s#%d at %.8s checked on %s: %s — %s\n", result.ID, c.Owner, c.Repo, c.Number, c.Commit,
orSomewhere(c.On), strings.ToUpper(c.Verdict), c.Summary)
if checkEvents == nil {
return
}
body, err := json.Marshal(c)
if err != nil {
return
}
stating, stop := context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second)
defer stop()
if err := checkEvents.PublishSeatEvent(stating, link.MeshControllerSeat, link.KeyChecked, body); err != nil {
fmt.Printf("%s: the verdict could not be said, so the pull request is not told it: %v\n", result.ID, err)
}
}
+9
View File
@@ -117,6 +117,9 @@ func run() error {
// The facts snapshot a merge check is fed (novox/hq to-be 45 §9). // The facts snapshot a merge check is fed (novox/hq to-be 45 §9).
case "facts": case "facts":
return factsCommand(ctx, args[1:]) return factsCommand(ctx, args[1:])
// The merge gate: every machine of the snapshot composed with a change (novox/hq to-be 45 §9).
case "merge-gate":
return mergeGateCommand(ctx, args[1:])
case "upgrade": case "upgrade":
return upgradeCommand(ctx, args[1:]) return upgradeCommand(ctx, args[1:])
// The bus as a planned step (novox/hq to-be 45 §8, ADR 0236). // The bus as a planned step (novox/hq to-be 45 §8, ADR 0236).
@@ -335,6 +338,12 @@ func parseAround(set *flag.FlagSet, args []string) ([]string, error) {
// registered, the same as the waiting command does. Said either way, so the daemon's log tells what // registered, the same as the waiting command does. Said either way, so the daemon's log tells what
// became of a build nobody was watching. // became of a build nobody was watching.
func (b builds) Built(ctx context.Context, result link.BuildResult) error { func (b builds) Built(ctx context.Context, result link.BuildResult) error {
// **A merge check builds nothing** (novox/hq to-be 45 §9): its verdict is said, and nothing of it is
// recorded or registered.
if result.Check != nil || result.Checked != nil {
checked(ctx, result)
return nil
}
// **A dry run is looked at, never taken in** (novox/hq issue 240). On 2026-10-04 a dry run of an // **A dry run is looked at, never taken in** (novox/hq issue 240). On 2026-10-04 a dry run of an
// unmerged branch was heard here like any build, registered, and its definition reached a machine // unmerged branch was heard here like any build, registered, and its definition reached a machine
// before anyone had reviewed it. // before anyone had reviewed it.
File diff suppressed because it is too large Load Diff
+208
View File
@@ -0,0 +1,208 @@
package main
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/novox/mesh-controller/internal/catalogue"
snapshot "github.com/novox/mesh-controller/internal/facts"
"github.com/novox/mesh-controller/internal/inventory"
)
// The merge gate (novox/hq to-be 45 §9): a change judged against every machine of the snapshot, by the
// incidents it is written from. Each case raises a mesh, takes its snapshot, and judges a pull request's
// tree against it in throwaway stores of its own.
// catalogueMesh is two machines and a catalogue repository: a resolver and an object store on the
// anchor, and on the laptop a network manager requiring the resolver, an album requiring the object
// store, and a login manager. Every module is registered from novox/mesh-catalog, as the mesh's are.
func catalogueMesh(t *testing.T) (snapshot.Facts, map[string]string) {
t.Helper()
open := aMesh(t)
ctx := t.Context()
manifests := map[string]string{
"objects": `{"module":"objects","version":"1",
"provides":[{"name":"s3-bucket","scope":"mesh","identity":{"max":20,"in":"an S3 access key"}}],
"receives":{"s3-bucket":"/var/lib/mesh/objects/mesh.json"}}`,
"resolver": `{"module":"resolver","version":"1",
"provides":[{"name":"wildcard-resolution","scope":"mesh","identity":false}]}`,
"networkmanager": `{"module":"networkmanager","version":"1","requires":["wildcard-resolution"]}`,
"album": `{"module":"album","version":"1","requires":["s3-bucket"]}`,
"lemurs": `{"module":"lemurs","version":"1","capabilities":["systemd"],
"resources":[{"id":"service","type":"service","unit":"lemurs.service","state":"running","boot":"enabled"}]}`,
}
for name, raw := range manifests {
m, err := catalogue.ParseManifest([]byte(raw))
if err != nil {
t.Fatalf("%s: %v", name, err)
}
if err := open.inventory.RegisterModule(ctx, m, inventory.Source{Repository: "novox/mesh-catalog",
Path: "modules/" + name, BuiltFrom: "c0ffee"}); err != nil {
t.Fatal(err)
}
}
for _, a := range [][2]string{{"anchor", "objects"}, {"anchor", "resolver"}, {"laptop", "networkmanager"},
{"laptop", "album"}, {"laptop", "lemurs"}} {
if _, err := assign(ctx, open, a[0], a[1]); err != nil {
t.Fatalf("assign %s %s: %v", a[0], a[1], err)
}
}
f, err := gatherFacts(ctx, open, "2.11.17")
if err != nil {
t.Fatal(err)
}
return f, manifests
}
// aTree is a checkout of the catalogue repository holding these manifests.
func aTree(t *testing.T, manifests map[string]string) string {
t.Helper()
dir := t.TempDir()
for name, raw := range manifests {
at := filepath.Join(dir, "modules", name)
if err := os.MkdirAll(at, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(at, "module.json"), []byte(raw), 0o644); err != nil {
t.Fatal(err)
}
}
return dir
}
func gateJudged(t *testing.T, f snapshot.Facts, tree string, changed ...string) mergeVerdict {
t.Helper()
admin := os.Getenv("MESH_TEST_POSTGRES")
in := mergeCheckInput{facts: f, admin: admin, changed: changed}
if tree != "" {
in.repository, in.tree = "novox/mesh-catalog", tree
}
v, err := judgeChange(t.Context(), in)
if err != nil {
t.Fatal(err)
}
return v
}
func withEdit(manifests map[string]string, name, raw string) map[string]string {
out := map[string]string{}
for k, v := range manifests {
out[k] = v
}
if raw == "" {
delete(out, name)
} else {
out[name] = raw
}
return out
}
// The mesh as it is passes: every machine composes in the gate's store as the controller composed it.
func TestTheMeshAsItIsPassesTheGate(t *testing.T) {
f, manifests := catalogueMesh(t)
for _, m := range f.Machines {
if !m.Declaration.Composes {
t.Fatalf("the mesh itself does not compose: %+v", m.Declaration)
}
}
v := gateJudged(t, f, aTree(t, manifests))
if v.Verdict != "pass" {
t.Fatalf("an unchanged catalogue does not pass:\n%s", v.Report())
}
for _, m := range v.Machines {
if !m.Base.Composes || !m.Change.Composes {
t.Errorf("%s does not compose in the gate's store as it does on the mesh: %+v / %+v", m.Described, m.Base, m.Change)
}
}
}
// **Issue 263**: a change makes the network manager require the object store's provision; on a machine
// whose name is six characters its identity is 26 against a bound of 20. Refused in the pull request,
// naming the module, and not on the anchor after it merged.
func TestIssue263AnIdentityARealMachineNameOverflowsFailsThePullRequest(t *testing.T) {
f, manifests := catalogueMesh(t)
tree := aTree(t, withEdit(manifests, "networkmanager",
`{"module":"networkmanager","version":"1","requires":["wildcard-resolution","s3-bucket"]}`))
v := gateJudged(t, f, tree)
if v.Verdict != "fail" {
t.Fatalf("the overflow passed the gate:\n%s", v.Report())
}
report := v.Report()
if !strings.Contains(report, "networkmanager") || !strings.Contains(report, "s3-bucket") {
t.Errorf("the refusal does not name the module and the provision:\n%s", report)
}
}
// **Issue 236**: a login manager's service that omits its state passed the catalogue check and was
// refused whole by the node-engine on the first machine. A change to a module a machine runs, and a
// module nobody runs yet, are both refused before merge, naming the machine and the module.
func TestIssue236AManifestTheNodeEngineRefusesFailsThePullRequest(t *testing.T) {
f, manifests := catalogueMesh(t)
broken := `{"module":"lemurs","version":"1","capabilities":["systemd"],
"resources":[{"id":"service","type":"service","unit":"lemurs.service","boot":"enabled"}]}`
v := gateJudged(t, f, aTree(t, withEdit(manifests, "lemurs", broken)))
if v.Verdict != "fail" || !strings.Contains(v.Report(), "lemurs") {
t.Fatalf("a service the node-engine refuses passed the gate:\n%s", v.Report())
}
laptop := snapshot.Pseudonym("machine", "laptop")
if !strings.Contains(v.Report(), laptop) {
t.Errorf("the refusal does not name the machine it would be refused on:\n%s", v.Report())
}
// And as a new module, which no machine runs: tried on one that could.
newcomer := strings.ReplaceAll(broken, `"lemurs"`, `"greeter"`)
newcomer = strings.ReplaceAll(newcomer, "lemurs.service", "greeter.service")
v = gateJudged(t, f, aTree(t, withEdit(manifests, "greeter", newcomer)))
if v.Verdict != "fail" || !strings.Contains(v.Report(), "greeter, assigned to") {
t.Fatalf("a new module the node-engine would refuse passed the gate:\n%s", v.Report())
}
}
// **Version skew**: a manifest the controller judging it cannot read — the one the mesh runs, for a
// catalogue change — fails here, not at registration after the merge.
func TestAManifestTheRunningControllerCannotReadFailsThePullRequest(t *testing.T) {
f, manifests := catalogueMesh(t)
v := gateJudged(t, f, aTree(t, withEdit(manifests, "album",
`{"module":"album","version":"1","requires":["s3-bucket"],"a-field-of-a-newer-controller":true}`)))
if v.Verdict != "fail" || !strings.Contains(v.Report(), "refuses it") {
t.Fatalf("a manifest this controller cannot read passed:\n%s", v.Report())
}
}
// A module a machine runs, removed from its source, fails until it is unassigned (ADR 0236).
func TestAModuleAMachineRunsRemovedFromItsSourceFails(t *testing.T) {
f, manifests := catalogueMesh(t)
v := gateJudged(t, f, aTree(t, withEdit(manifests, "album", "")))
if v.Verdict != "fail" || !strings.Contains(v.Report(), "album is removed") {
t.Fatalf("removing a module a machine runs passed:\n%s", v.Report())
}
}
// **Issue 278**: a file of a module nobody holds read as shared code, and the merge rebuilt the
// catalogue. The gate says how wide a merge's rebuild is, and warns when shared code makes it wide.
func TestIssue278AWideRebuildIsSaidBeforeTheMerge(t *testing.T) {
f, manifests := catalogueMesh(t)
was := wideRebuild
wideRebuild = 2
t.Cleanup(func() { wideRebuild = was })
v := gateJudged(t, f, aTree(t, manifests), "modules/showcase/index.ts")
if v.Width == nil || len(v.Width.Modules) < 5 || len(v.Width.Shared) != 1 {
t.Fatalf("the width reads %+v", v.Width)
}
if v.Verdict != "warning" || !strings.Contains(v.Report(), "shared") {
t.Fatalf("a rebuild of everything for one shared file is not said:\n%s", v.Report())
}
// The same file, with the reference module's definition in the tree: its directory is a module, held
// or not, and the file is its business alone (the fix of 278, read from the tree as the announcer does).
withShowcase := withEdit(manifests, "showcase", `{"module":"showcase","version":"1"}`)
v = gateJudged(t, f, aTree(t, withShowcase), "modules/showcase/index.ts")
if v.Width == nil || len(v.Width.Modules) != 0 || len(v.Width.Shared) != 0 {
t.Fatalf("a file of a module nobody holds still reads as shared: %+v", v.Width)
}
v = gateJudged(t, f, aTree(t, manifests), "modules/album/module.json")
if v.Width == nil || strings.Join(v.Width.Modules, ",") != "album" || v.Verdict != "pass" {
t.Fatalf("a change to one module's directory reads %+v, %s", v.Width, v.Verdict)
}
}
+6
View File
@@ -210,6 +210,12 @@ func serve(ctx context.Context) (err error) {
handActConn = bus.Conn handActConn = bus.Conn
// And says when it replaced a value given by hand (novox/hq ADR 0228). // And says when it replaced a value given by hand (novox/hq ADR 0228).
givenEvents = bus givenEvents = bus
// And a pull request's merge check, asked when the forge announces its head and said when judged
// (novox/hq to-be 45 §9).
checkEvents = bus
if err := server.Checks(following{open}); err != nil {
return err
}
// Composed now and kept current, before the verb that answers from it is served. // Composed now and kept current, before the verb that answers from it is served.
go statusFrom.keep(ctx) go statusFrom.keep(ctx)
// The facts snapshot a merge check is fed (novox/hq to-be 45 §9): kept current while this // The facts snapshot a merge check is fed (novox/hq to-be 45 §9): kept current while this
+2
View File
@@ -30,6 +30,8 @@ func TestTheFactsTheGrantPermitsAreTheFactsTheMeshStates(t *testing.T) {
states = append(states, link.KeyHealerActed) states = append(states, link.KeyHealerActed)
// And a build put back after its gate failed (novox/hq ADR 0236). // And a build put back after its gate failed (novox/hq ADR 0236).
states = append(states, link.KeyRolledBack) states = append(states, link.KeyRolledBack)
// And a pull request's merge check, judged (novox/hq to-be 45 §9).
states = append(states, link.KeyChecked)
for _, event := range states { for _, event := range states {
if !slices.Contains(broker.ControllerStates, event) { if !slices.Contains(broker.ControllerStates, event) {
t.Errorf("the mesh states %q and its account may not publish it", event) t.Errorf("the mesh states %q and its account may not publish it", event)
+8 -1
View File
@@ -215,7 +215,10 @@ var ControllerStates = []string{"applied", "refused", "built-before",
// made by itself is said like one a person made, never quietly. // made by itself is said like one a person made, never quietly.
"healer-acted", "healer-acted",
// And a build put back after its gate failed on its first machine (novox/hq ADR 0236, to-be 45 §8). // And a build put back after its gate failed on its first machine (novox/hq ADR 0236, to-be 45 §8).
"rolled-back"} "rolled-back",
// And a pull request's merge check, judged (novox/hq to-be 45 §9): what the forge's holder sets as
// the pull request's status, and anybody else may read.
"checked"}
// BusAdvisories are what the bus server says about the mesh's own account that the controller // BusAdvisories are what the bus server says about the mesh's own account that the controller
// reads (novox/hq to-be 45 §3, S9): a durable consumer that handed a message over as often as it // reads (novox/hq to-be 45 §3, S9): a durable consumer that handed a message over as often as it
@@ -261,6 +264,10 @@ var ControllerFollows = []string{
// for a person, re-enabled, deleted — a provider's third word, from whichever module provides. // for a person, re-enabled, deleted — a provider's third word, from whichever module provides.
// Appended, because the index is a name. // Appended, because the index is a name.
moduleEventSubject("*", ProvisionerRetirement), moduleEventSubject("*", ProvisionerRetirement),
// **A pull request's head, announced** (novox/hq to-be 45 §9): what the controller asks the build
// seat to check before it merges — every machine of the facts snapshot composed with the change.
// Appended, because the index is a name.
moduleEventSubject("gitea", "pull.updated"),
} }
// The provider standing events, by their local names. Written here as well as in the catalogue // The provider standing events, by their local names. Written here as well as in the catalogue
+2 -2
View File
@@ -24,8 +24,8 @@ accounts {
jetstream: enabled jetstream: enabled
users = [ users = [
{ user: "controller", password: "$2a$11$cccccccccccccccccccccc", permissions: { { user: "controller", password: "$2a$11$cccccccccccccccccccccc", permissions: {
publish: { allow: ["$JS.ACK.CONTROL.controller.>", "$JS.ACK.EVENTS.controller.>", "$JS.API.>", "$KV.SEAT_MESH_BUILD_MACHINE_cancelled.>", "$KV.SEAT_NODE_BUILD_AGENT_cancelled.>", "$KV.mesh-controller_calls.>", "$KV.mesh-controller_condition-history.>", "$KV.mesh-controller_conditions.>", "$KV.mesh-controller_hand-acts.>", "$KV.mesh-controller_lease.>", "$SRV.INFO", "_INBOX.enrol.>", "mesh.assignment.>", "mesh.mod.*.tool.>", "mesh.node.>", "mesh.seat.mesh-build-machine.accept.>", "mesh.seat.mesh-build-machine.tool.>", "mesh.seat.mesh-controller.event.applied", "mesh.seat.mesh-controller.event.built-before", "mesh.seat.mesh-controller.event.condition-changed", "mesh.seat.mesh-controller.event.condition-cleared", "mesh.seat.mesh-controller.event.condition-raised", "mesh.seat.mesh-controller.event.doctor-heartbeat", "mesh.seat.mesh-controller.event.healer-acted", "mesh.seat.mesh-controller.event.refused", "mesh.seat.mesh-controller.event.rolled-back", "mesh.seat.mesh-controller.event.secret-replaced", "mesh.seat.node-backup.tool.backed-up.*", "mesh.seat.node-backup.tool.now.*", "mesh.seat.node-build-agent.accept.>", "mesh.seat.node-build-agent.tool.>", "mesh.seat.node-intrusion-prevention.tool.banned.*"] } publish: { allow: ["$JS.ACK.CONTROL.controller.>", "$JS.ACK.EVENTS.controller.>", "$JS.API.>", "$KV.SEAT_MESH_BUILD_MACHINE_cancelled.>", "$KV.SEAT_NODE_BUILD_AGENT_cancelled.>", "$KV.mesh-controller_calls.>", "$KV.mesh-controller_condition-history.>", "$KV.mesh-controller_conditions.>", "$KV.mesh-controller_hand-acts.>", "$KV.mesh-controller_lease.>", "$SRV.INFO", "_INBOX.enrol.>", "mesh.assignment.>", "mesh.mod.*.tool.>", "mesh.node.>", "mesh.seat.mesh-build-machine.accept.>", "mesh.seat.mesh-build-machine.tool.>", "mesh.seat.mesh-controller.event.applied", "mesh.seat.mesh-controller.event.built-before", "mesh.seat.mesh-controller.event.checked", "mesh.seat.mesh-controller.event.condition-changed", "mesh.seat.mesh-controller.event.condition-cleared", "mesh.seat.mesh-controller.event.condition-raised", "mesh.seat.mesh-controller.event.doctor-heartbeat", "mesh.seat.mesh-controller.event.healer-acted", "mesh.seat.mesh-controller.event.refused", "mesh.seat.mesh-controller.event.rolled-back", "mesh.seat.mesh-controller.event.secret-replaced", "mesh.seat.node-backup.tool.backed-up.*", "mesh.seat.node-backup.tool.now.*", "mesh.seat.node-build-agent.accept.>", "mesh.seat.node-build-agent.tool.>", "mesh.seat.node-intrusion-prevention.tool.banned.*"] }
subscribe: { allow: ["$JS.API.>", "$JS.EVENT.ADVISORY.CONSUMER.DELETED.>", "$JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES.>", "$SRV.INFO", "$SRV.INFO.mesh-controller", "$SRV.INFO.mesh-controller.>", "$SRV.PING", "$SRV.PING.mesh-controller", "$SRV.PING.mesh-controller.>", "$SRV.STATS", "$SRV.STATS.mesh-controller", "$SRV.STATS.mesh-controller.>", "_DELIVER.controller", "_DELIVER.controller.>", "_INBOX.controller.>", "mesh.control.>", "mesh.mod.*.event.provisioner.failing", "mesh.mod.*.event.provisioner.recovered", "mesh.mod.*.event.provisioner.retirement", "mesh.mod.gitea.event.pull.merged", "mesh.mod.mesh-catalog.event.catching-up", "mesh.mod.mesh-catalog.event.upgraded", "mesh.seat.mesh-build-machine.event.built", "mesh.seat.mesh-controller.tool.>", "mesh.seat.node-build-agent.event.built"] } subscribe: { allow: ["$JS.API.>", "$JS.EVENT.ADVISORY.CONSUMER.DELETED.>", "$JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES.>", "$SRV.INFO", "$SRV.INFO.mesh-controller", "$SRV.INFO.mesh-controller.>", "$SRV.PING", "$SRV.PING.mesh-controller", "$SRV.PING.mesh-controller.>", "$SRV.STATS", "$SRV.STATS.mesh-controller", "$SRV.STATS.mesh-controller.>", "_DELIVER.controller", "_DELIVER.controller.>", "_INBOX.controller.>", "mesh.control.>", "mesh.mod.*.event.provisioner.failing", "mesh.mod.*.event.provisioner.recovered", "mesh.mod.*.event.provisioner.retirement", "mesh.mod.gitea.event.pull.merged", "mesh.mod.gitea.event.pull.updated", "mesh.mod.mesh-catalog.event.catching-up", "mesh.mod.mesh-catalog.event.upgraded", "mesh.seat.mesh-build-machine.event.built", "mesh.seat.mesh-controller.tool.>", "mesh.seat.node-build-agent.event.built"] }
allow_responses: { max: 1, ttl: "1m" } allow_responses: { max: 1, ttl: "1m" }
} } } }
{ user: "enrol.one", password: "$2a$11$eeeeeeeeeeeeeeeeeeeeee", permissions: { { user: "enrol.one", password: "$2a$11$eeeeeeeeeeeeeeeeeeeeee", permissions: {
+6
View File
@@ -116,6 +116,12 @@ var WritersTable = []WriterRow{
Shared: "every machine holding the build seat answers the asks it took; each outcome names its ask"}, Shared: "every machine holding the build seat answers the asks it took; each outcome names its ask"},
{State: "a merge announced", Writer: "one announcer per forge (the hook, or the poll when the hook is absent — never both)", {State: "a merge announced", Writer: "one announcer per forge (the hook, or the poll when the hook is absent — never both)",
KeptIn: "the bus", Others: "—", Subjects: []string{"mesh.mod.*.event.pull.merged"}, Writes: ownModule}, KeptIn: "the bus", Others: "—", Subjects: []string{"mesh.mod.*.event.pull.merged"}, Writes: ownModule},
// A pull request's head, before it merges (novox/hq to-be 45 §9): the same one announcer.
{State: "a pull request's head announced", Writer: "the forge's announcer, the one that announces its merges",
KeptIn: "the bus", Others: "the controller asks the build seat to check it", Subjects: []string{"mesh.mod.*.event.pull.updated"},
Writes: ownModule},
{State: "a pull request's merge check", Writer: "the build seat's holder that ran it, said as the controller's `checked`",
KeptIn: "the bus", Others: "the forge's holder sets it as the pull request's status"},
{State: "a provider's standing", Writer: "the provider", KeptIn: "the provider's events", {State: "a provider's standing", Writer: "the provider", KeptIn: "the provider's events",
Others: "the controller keeps the newest word as a condition", Others: "the controller keeps the newest word as a condition",
Subjects: []string{"mesh.mod.*.event.provisioner.failing", "mesh.mod.*.event.provisioner.recovered", Subjects: []string{"mesh.mod.*.event.provisioner.failing", "mesh.mod.*.event.provisioner.recovered",
+2
View File
@@ -25,6 +25,8 @@ var designRows = []string{
"stream definitions and bus permissions", "stream definitions and bus permissions",
"builds and their outcomes", "builds and their outcomes",
"a merge announced", "a merge announced",
"a pull request's head announced",
"a pull request's merge check",
"a provider's standing", "a provider's standing",
"the operator-channel's open messages", "the operator-channel's open messages",
"the facts snapshot", "the facts snapshot",
+437
View File
@@ -0,0 +1,437 @@
package builder
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"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).
//
// **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.
//
// 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 — 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. builds the judge — the controller the mesh runs, or its main while the running one predates the
// merge gate — and runs the repository's own merge-check.sh, or the merge gate alone for a
// repository that declares none. **In the mesh's Go toolchain, in a container of its own**, never in
// the build machine's: 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;
// 5. answers 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
// Toolchain is the image a check's Go runs in: the mesh's own Go toolchain, as it holds it.
Toolchain string
}
// ToolchainOf is the Go toolchain image among what the mesh holds, empty when it holds none.
func ToolchainOf(held map[string]string) string {
for _, chain := range toolchains {
if chain.Language == "go" {
return held[chain.Base+"/"+chain.Artifact]
}
}
return ""
}
// Beside is one repository cloned next to the one checked.
type Beside struct {
Repository string
Ref string
}
// CheckVerdict is what came of one check.
type CheckVerdict struct {
Verdict string
Summary string
Report string
Took time.Duration
}
// CheckScript is what a repository declares its 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"
)
// 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
// 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 == "" {
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
}
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))
}
// 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")
}
inToolchain := func(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, spec.Toolchain), command...)
}
// The judge: the controller the mesh runs, or its main while the running one has no merge gate.
gate := ""
if name != "mesh-controller" {
gate, err = judge(ctx, labelled, run, root, inToolchain, say)
if err != nil {
return CheckVerdict{}, err
}
}
verdictFile := filepath.Join(root, "verdict.json")
env := append([]string{},
EnvFacts+"="+factsFile, EnvGate+"="+gate, EnvGateStore+"="+storeURL, EnvTestStore+"="+storeURL,
EnvTestBus+"=nats://"+bus, EnvRepository+"="+spec.Owner+"/"+spec.Repo,
EnvChanged+"="+strings.Join(spec.Paths, ","), EnvVerdict+"="+verdictFile, EnvBeside+"="+root,
"GOCACHE="+filepath.Join(workspace, "go-cache"), "GOMODCACHE="+filepath.Join(workspace, "go-modules"))
tree := filepath.Join(root, name)
var command []string
if _, err := os.Stat(filepath.Join(tree, CheckScript)); err == nil {
say("check", "running its %s in the mesh's Go toolchain", CheckScript)
command = []string{"sh", CheckScript}
} else {
if gate == "" {
return CheckVerdict{}, fmt.Errorf("%s declares no %s and there is no judge to run", name, CheckScript)
}
say("check", "it declares no %s: the merge gate alone", CheckScript)
command = []string{"sh", "-c", `"$MESH_GATE" merge-gate --facts "$MESH_FACTS" --store "$MESH_GATE_POSTGRES" ` +
`--repository "$MESH_CHECK_REPOSITORY" --tree . --changed "$MESH_CHECK_CHANGED" --json > "$MESH_CHECK_VERDICT"`}
}
cmd := exec.CommandContext(ctx, "docker", LabelledArgs("docker", inToolchain(tree, env, command...), spec.ID)...)
inItsOwnGroup(cmd)
var out tail
cmd.Stdout, cmd.Stderr = &out, &out
runErr := cmd.Run()
// **And 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.
var replayErr error
if lab := filepath.Join(root, "mesh-lab", "replays"); runErr == nil && 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 script above.
args = append([]string{args[0], "--volume", "/var/run/docker.sock:/var/run/docker.sock"}, args[1:]...)
replays := exec.CommandContext(ctx, "docker", LabelledArgs("docker", args, spec.ID)...)
inItsOwnGroup(replays)
replays.Stdout, replays.Stderr = &out, &out
replayErr = replays.Run()
}
}
v := CheckVerdict{Report: out.String(), Took: time.Since(began)}
var gateSaid struct {
Verdict string `json:"verdict"`
Summary string `json:"summary"`
}
if raw, err := os.ReadFile(verdictFile); err == nil {
_ = json.Unmarshal(raw, &gateSaid)
}
switch {
case errors.Is(ctx.Err(), context.DeadlineExceeded):
v.Verdict, v.Summary = "error", fmt.Sprintf("the check ran past %s and was ended", CheckTimeout)
case ctx.Err() != nil:
return v, ctx.Err()
case runErr != nil:
v.Verdict = "fail"
v.Summary = gateSaid.Summary
if v.Summary == "" || gateSaid.Verdict != "fail" {
v.Summary = "the merge check failed: " + lastLine(out.String())
}
case replayErr != nil:
v.Verdict, v.Summary = "fail", "a replay of a core incident fails with this change: "+lastLine(out.String())
default:
v.Verdict, v.Summary = "pass", "the merge check passed"
if gateSaid.Verdict == "warning" || gateSaid.Verdict == "pass" {
v.Verdict, v.Summary = gateSaid.Verdict, gateSaid.Summary
}
}
say("check", "%s — %s (%s)", strings.ToUpper(v.Verdict), v.Summary, v.Took.Round(time.Second))
return v, nil
}
// 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.
func judge(ctx context.Context, run, plain Runner, root string, inToolchain func(string, []string, ...string) []string,
say func(step, format string, args ...any)) (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, 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, 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])
}
+152
View File
@@ -0,0 +1,152 @@
package builder
import (
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
"github.com/novox/mesh-controller/internal/artifacts"
"github.com/novox/mesh-controller/internal/facts"
)
// A merge check on the build seat (novox/hq to-be 45 §9), against a real container runtime and a real
// registry: the repository's own merge-check.sh runs with the facts the controller keeps, beside a
// throwaway store and bus of **the versions the mesh runs**, and everything raised is removed.
//
// MESH_TEST_DOCKER=1 MESH_TEST_REGISTRY=127.0.0.1:15000 go test ./internal/builder -run Check
func TestTheStoreAndBusAChecksStandsOnAreTheOnesTheMeshRuns(t *testing.T) {
if got := StoreImage("17.11"); got != "postgres:17-alpine" {
t.Errorf("a store at 17.11 is checked against %s", got)
}
if got := StoreImage("16.4 (Debian 16.4-1.pgdg120+1)"); got != "postgres:16-alpine" {
t.Errorf("a store at 16.4 is checked against %s", got)
}
if got := BusImage("2.11.17"); got != "nats:2.11.17-alpine" {
t.Errorf("a bus at 2.11.17 is checked against %s", got)
}
}
// aRepository is a git repository holding these files, committed, and its head.
func aCheckedRepository(t *testing.T, files map[string]string) (string, string) {
t.Helper()
dir := t.TempDir()
git := func(args ...string) string {
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))
}
git("init", "--quiet", "-b", "main")
for name, body := range files {
if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0o755); err != nil {
t.Fatal(err)
}
}
git("add", "-A")
git("commit", "--quiet", "-m", "x")
return dir, git("rev-parse", "HEAD")
}
func checkEnvironment(t *testing.T) string {
t.Helper()
if os.Getenv("MESH_TEST_DOCKER") != "1" || os.Getenv("MESH_TEST_REGISTRY") == "" {
t.Skip("MESH_TEST_DOCKER=1 and MESH_TEST_REGISTRY: a check raises containers and reads the registry")
}
registry := os.Getenv("MESH_TEST_REGISTRY")
body, err := json.Marshal(facts.Facts{Format: facts.Format, Taken: time.Now().UTC(),
Versions: facts.Versions{Bus: "2.11.17", Store: "17.11"},
Machines: []facts.Machine{{Name: "abcdef", Length: 6}}})
if err != nil {
t.Fatal(err)
}
if _, err := (artifacts.Store{Address: registry}).PutTagged(t.Context(), facts.Repository, facts.Tag,
facts.MediaType, body); err != nil {
t.Fatal(err)
}
return registry
}
// goToolchain is the Go image a check's script runs in here: the base the controller's own image is built on.
const goToolchain = "golang@sha256:8ac98ca534ac3f51e1f420a1dd2c15e74c75cfa0f23f3ad27eb5d7236c349a0c"
func labelled(id string) []string {
out, _ := exec.Command("docker", "ps", "-aq", "--filter", "label="+BuildLabel+"="+id).Output()
return strings.Fields(string(out))
}
func TestACheckRunsTheRepositorysOwnScriptBesideTheMeshsVersionsAndLeavesNothing(t *testing.T) {
registry := checkEnvironment(t)
// The script proves what it was given: the facts, a store that answers, a bus that answers, and
// writes the gate's verdict where it is told to.
script := `set -e
test -s "$MESH_FACTS"
grep -q '"bus": "2.11.17"' "$MESH_FACTS" || grep -q '"bus":"2.11.17"' "$MESH_FACTS"
case "$MESH_TEST_POSTGRES" in postgres://*127.0.0.1:*) ;; *) echo "no store: $MESH_TEST_POSTGRES"; exit 1;; esac
case "$MESH_TEST_NATS" in nats://127.0.0.1:*) ;; *) echo "no bus: $MESH_TEST_NATS"; exit 1;; esac
test "$MESH_CHECK_REPOSITORY" = "novox/mesh-controller"
test "$MESH_CHECK_CHANGED" = "a.go,b.go"
test ! -S /var/run/docker.sock || { echo "the check holds the container runtime's socket"; exit 1; }
echo '{"verdict":"warning","summary":"a merge rebuilds 14 module(s)"}' > "$MESH_CHECK_VERDICT"
echo checked
`
repo, head := aCheckedRepository(t, map[string]string{CheckScript: script})
id := fmt.Sprintf("check-test-%d", time.Now().UnixNano())
v, err := Check(t.Context(), Command, CheckSpec{ID: id, Repository: repo, Ref: head, Owner: "novox",
Repo: "mesh-controller", Number: 7, Paths: []string{"a.go", "b.go"}, Toolchain: goToolchain}, t.TempDir(), registry, GitCredential{}, nil)
if err != nil {
t.Fatal(err)
}
if v.Verdict != "warning" || v.Summary != "a merge rebuilds 14 module(s)" || !strings.Contains(v.Report, "checked") {
t.Fatalf("the check answered %+v", v)
}
if left := labelled(id); len(left) > 0 {
t.Errorf("the check left %d container(s) behind", len(left))
}
}
func TestAFailingCheckFailsAndOneThatCannotRunIsNeverAPass(t *testing.T) {
registry := checkEnvironment(t)
repo, head := aCheckedRepository(t, map[string]string{CheckScript: "echo 'resource \"x.service\": refused'; exit 3\n"})
v, err := Check(t.Context(), Command, CheckSpec{ID: fmt.Sprintf("check-fail-%d", time.Now().UnixNano()),
Repository: repo, Ref: head, Owner: "novox", Repo: "mesh-controller", Toolchain: goToolchain}, t.TempDir(), registry, GitCredential{}, nil)
if err != nil {
t.Fatal(err)
}
if v.Verdict != "fail" || !strings.Contains(v.Summary, "refused") {
t.Fatalf("a failing script answered %+v", v)
}
// Past its bound: an error, not a pass.
was := CheckTimeout
CheckTimeout = 25 * time.Second
t.Cleanup(func() { CheckTimeout = was })
repo, head = aCheckedRepository(t, map[string]string{CheckScript: "sleep 120\n"})
v, err = Check(context.Background(), Command, CheckSpec{ID: fmt.Sprintf("check-slow-%d", time.Now().UnixNano()),
Repository: repo, Ref: head, Owner: "novox", Repo: "mesh-controller", Toolchain: goToolchain}, t.TempDir(), registry, GitCredential{}, nil)
if err == nil && v.Verdict == "pass" {
t.Fatalf("a check past its bound passed: %+v", v)
}
if err == nil && v.Verdict != "error" {
t.Fatalf("a check past its bound answered %+v", v)
}
// No facts: it cannot run, and says so.
repo, head = aCheckedRepository(t, map[string]string{CheckScript: "exit 0\n"})
_, err = Check(t.Context(), Command, CheckSpec{ID: "check-nofacts", Repository: repo, Ref: head, Owner: "novox",
Repo: "mesh-controller", Toolchain: goToolchain}, t.TempDir(), "127.0.0.1:1", GitCredential{}, nil)
if err == nil || !strings.Contains(err.Error(), "facts snapshot") {
t.Fatalf("a check with no facts said %v", err)
}
}
+3 -1
View File
@@ -91,7 +91,9 @@ var defaultSeats = append([]Seat{
// Every act a healer takes (novox/hq to-be 45 §7). // Every act a healer takes (novox/hq to-be 45 §7).
"healer-acted", "healer-acted",
// A build put back after its gate failed (novox/hq ADR 0236, to-be 45 §8). // A build put back after its gate failed (novox/hq ADR 0236, to-be 45 §8).
"rolled-back"}, "rolled-back",
// A pull request's merge check, judged (novox/hq to-be 45 §9).
"checked"},
Serves: ControllerVerbs}, Serves: ControllerVerbs},
// The store's first verbs (novox/hq ADR 0159): the smallest set that makes the store askable, // The store's first verbs (novox/hq ADR 0159): the smallest set that makes the store askable,
// served by whichever module holds the seat with tools of these names. // served by whichever module holds the seat with tools of these names.
+41
View File
@@ -87,6 +87,42 @@ type BuildRequest struct {
// builder echoes it, and whoever hears the outcome takes nothing in — no record, no registration, // builder echoes it, and whoever hears the outcome takes nothing in — no record, no registration,
// no plan, nothing a push could send. // no plan, nothing a push could send.
DryRun bool `json:"dry-run,omitempty"` DryRun bool `json:"dry-run,omitempty"`
// Check makes this ask a pull request's merge check rather than a build (novox/hq to-be 45 §9): the
// builder checks the repository out at Ref with the repositories it is checked beside, reads the
// facts snapshot, raises the throwaway stores the check needs, runs the repository's own
// merge-check.sh and answers its verdict. Nothing is built, published or registered.
Check *CheckRequest `json:"check,omitempty"`
}
// CheckRequest is what a merge check needs beyond the repository and its head.
type CheckRequest struct {
Owner string `json:"owner"`
Repo string `json:"repo"`
Number int `json:"number,omitempty"`
Base string `json:"base,omitempty"`
// Paths are the files the pull request changes, for the width of its rebuild.
Paths []string `json:"paths,omitempty"`
// Beside are the repositories the check reads next to this one, each cloned at the ref given — the
// controller the mesh runs, the catalogue it holds, the host it runs — keyed by the directory name the
// check finds it under.
Beside map[string]CheckedOut `json:"beside,omitempty"`
}
// CheckedOut is a repository cloned beside a check, at a ref.
type CheckedOut struct {
Repository string `json:"repository"`
Ref string `json:"ref,omitempty"`
}
// CheckOutcome is a merge check's verdict.
type CheckOutcome struct {
// Verdict is pass, warning, fail, or error: the check could not run, which is never a pass.
Verdict string `json:"verdict"`
Summary string `json:"summary"`
// Report is the check's own account, its last lines, bounded.
Report string `json:"report,omitempty"`
// Took is how long it ran.
Took string `json:"took,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.
@@ -151,6 +187,11 @@ type BuildResult struct {
// DryRun is the request's, echoed: an outcome nobody may take in (novox/hq issue 240). // DryRun is the request's, echoed: an outcome nobody may take in (novox/hq issue 240).
DryRun bool `json:"dry-run,omitempty"` DryRun bool `json:"dry-run,omitempty"`
// Check is a merge check's verdict, for an ask that was one; the request's Check is echoed in
// Checked so whoever hears it knows which pull request it judged.
Check *CheckOutcome `json:"check-outcome,omitempty"`
Checked *CheckRequest `json:"check,omitempty"`
} }
// ReadRepository is a repository a build read source from besides the module's own, at the branch, // ReadRepository is a repository a build read source from besides the module's own, at the branch,
+3
View File
@@ -40,6 +40,9 @@ var Contracts = map[string]Contract{
Tests: []string{"TestAnEnrolmentMetByAHeldTokenIsAskedToTryAgain"}}, Tests: []string{"TestAnEnrolmentMetByAHeldTokenIsAskedToTryAgain"}},
KindModuleMoved: {Unordered: "the catalogue saying a module's current build moved: acted on by reading the " + KindModuleMoved: {Unordered: "the catalogue saying a module's current build moved: acted on by reading the " +
"catalogue's record, which is the order, so a late one reads the same record"}, "catalogue's record, which is the order, so a late one reads the same record"},
KindPullUpdated: {Unordered: "a pull request's head, asked to be checked: each head is its own commit, and its " +
"verdict is set on that commit alone, so a head heard late is checked and judged as itself and never " +
"stands for a newer one (novox/hq to-be 45 §9)"},
KindCatchUp: {Unordered: "a catalogue asking what it missed: answered from the record, whenever asked"}, KindCatchUp: {Unordered: "a catalogue asking what it missed: answered from the record, whenever asked"},
KindProvisioner: {Unordered: "a provider's newest word about a consumer, said again every fifteen minutes " + KindProvisioner: {Unordered: "a provider's newest word about a consumer, said again every fifteen minutes " +
"while it holds (ADR 0224): the condition keeps the last observed, and S8 says when the words stop. " + "while it holds (ADR 0224): the condition keeps the last observed, and S8 says when the words stop. " +
+37
View File
@@ -73,6 +73,9 @@ const (
// KeyRolledBack: a build failed its gate on its first machine and was put back there, or could not // KeyRolledBack: a build failed its gate on its first machine and was put back there, or could not
// be (novox/hq ADR 0236, to-be 45 §8); or a witness on a machine put a core component back. // be (novox/hq ADR 0236, to-be 45 §8); or a witness on a machine put a core component back.
KeyRolledBack = "rolled-back" KeyRolledBack = "rolled-back"
// KeyChecked: a pull request's merge check was judged (novox/hq to-be 45 §9) — the verdict, its
// summary and its report, for the forge's holder to set as the pull request's status.
KeyChecked = "checked"
) )
// Applied is what a machine now runs, as the mesh states it. // Applied is what a machine now runs, as the mesh states it.
@@ -203,6 +206,40 @@ type SourceMoved struct {
ModuleDirsSaid bool `json:"module_dirs_said,omitempty"` ModuleDirsSaid bool `json:"module_dirs_said,omitempty"`
} }
// PullUpdated is what the forge announces when an open pull request's head moves — opened, or pushed
// to (novox/hq to-be 45 §9): what the controller asks the build seat to check before it merges.
type PullUpdated struct {
Owner string `json:"owner"`
Repo string `json:"repo"`
Number int `json:"number"`
Title string `json:"title,omitempty"`
Base string `json:"base"`
Head string `json:"head"`
Commit string `json:"head_sha"`
CloneURL string `json:"clone_url"`
HTMLURL string `json:"html_url,omitempty"`
// Paths are the files the pull request changes; PathsTruncated says there were more.
Paths []string `json:"paths,omitempty"`
PathsTruncated bool `json:"paths_truncated,omitempty"`
}
// Checked is a pull request's merge check, judged: what the controller says as `checked`.
type Checked struct {
Owner string `json:"owner"`
Repo string `json:"repo"`
Number int `json:"number,omitempty"`
Commit string `json:"commit"`
// Verdict is pass, warning, fail, or error — the check could not be run, which is not the change's
// fault and is never read as a pass.
Verdict string `json:"verdict"`
Summary string `json:"summary"`
// Report is the check's own account, bounded.
Report string `json:"report,omitempty"`
// ID is the ask, and On the machine that ran it.
ID string `json:"id"`
On string `json:"on,omitempty"`
}
type Upgraded struct { type Upgraded struct {
Module string `json:"module"` Module string `json:"module"`
Commit string `json:"commit"` Commit string `json:"commit"`
+3
View File
@@ -39,6 +39,9 @@ const (
// KindProvisioner is a provider saying a consumer has failed for minutes, or recovered // KindProvisioner is a provider saying a consumer has failed for minutes, or recovered
// (novox/hq ADR 0224). // (novox/hq ADR 0224).
KindProvisioner = "provisioner" KindProvisioner = "provisioner"
// KindPullUpdated is the forge announcing a pull request's new head: checked before it merges
// (novox/hq to-be 45 §9).
KindPullUpdated = "pull-updated"
) )
// Control is one thing a node or a module said, as the controller must act on it. // Control is one thing a node or a module said, as the controller must act on it.
+3 -1
View File
@@ -53,7 +53,7 @@ func Nats(js *broker.JetStream) Inbound {
// whatever was asked for — and not at all when nothing was. // whatever was asked for — and not at all when nothing was.
func (n *natsInbound) Also(kind string) error { func (n *natsInbound) Also(kind string) error {
switch kind { switch kind {
case KindModuleMoved, KindCatchUp, KindSourceMoved, KindProvisioner: case KindModuleMoved, KindCatchUp, KindSourceMoved, KindProvisioner, KindPullUpdated:
n.follows[kind] = true n.follows[kind] = true
return nil return nil
default: default:
@@ -248,6 +248,8 @@ func kindOfSubject(subject string) (string, bool) {
return KindCatchUp, true return KindCatchUp, true
case broker.ControllerFollows[3]: case broker.ControllerFollows[3]:
return KindSourceMoved, true return KindSourceMoved, true
case PullUpdatedSubject:
return KindPullUpdated, true
case BuildOutcome(), BuildOutcomeOf(TheBuildMachineBefore): case BuildOutcome(), BuildOutcomeOf(TheBuildMachineBefore):
// A build's outcome is the role's event now, so it arrives on the events stream rather than // A build's outcome is the role's event now, so it arrives on the events stream rather than
// the control branch — and is acted on by the same handler, because what the controller does // the control branch — and is acted on by the same handler, because what the controller does
+41
View File
@@ -63,6 +63,15 @@ type Upgrader interface {
SourceMoved(ctx context.Context, m SourceMoved) error SourceMoved(ctx context.Context, m SourceMoved) error
} }
// Checker is what the controller does when the forge says a pull request's head moved: ask for it to be
// checked before it merges (novox/hq to-be 45 §9).
type Checker interface {
PullUpdated(ctx context.Context, p PullUpdated) error
}
// PullUpdatedSubject is where the forge's pull requests land: the controller's own follow of them.
var PullUpdatedSubject = broker.ControllerFollows[len(broker.ControllerFollows)-1]
// Server acts on what nodes and modules say. // Server acts on what nodes and modules say.
// //
// **It holds no transport.** What arrives comes through Inbound and what it publishes goes through // **It holds no transport.** What arrives comes through Inbound and what it publishes goes through
@@ -83,6 +92,8 @@ type Server struct {
standings Standings standings Standings
// retirements keeps what providers say about consumers the mesh stopped asking for (ADR 0230). // retirements keeps what providers say about consumers the mesh stopped asking for (ADR 0230).
retirements Retirements retirements Retirements
// checker asks for a pull request's merge check (novox/hq to-be 45 §9).
checker Checker
log *log.Logger log *log.Logger
// giveUp is how long one message is held for the store; zero means GiveUpAfter. // giveUp is how long one message is held for the store; zero means GiveUpAfter.
@@ -116,6 +127,15 @@ func (s *Server) Follows(u Upgrader) error {
return nil return nil
} }
// Checks says what to do about a pull request's new head, and asks for them to be delivered.
func (s *Server) Checks(c Checker) error {
if err := s.inbound.Also(KindPullUpdated); err != nil {
return err
}
s.checker = c
return nil
}
// Answers says what to do about a catalogue's catch-up request, and asks for them to be delivered. // Answers says what to do about a catalogue's catch-up request, and asks for them to be delivered.
func (s *Server) Answers(r Replayer) error { func (s *Server) Answers(r Replayer) error {
if err := s.inbound.Also(KindCatchUp); err != nil { if err := s.inbound.Also(KindCatchUp); err != nil {
@@ -184,6 +204,8 @@ func (s *Server) act(ctx context.Context, m Control) {
s.catchingUp(ctx, m) s.catchingUp(ctx, m)
case KindProvisioner: case KindProvisioner:
s.provisioner(ctx, m) s.provisioner(ctx, m)
case KindPullUpdated:
s.pullUpdated(ctx, m)
default: default:
// Dropped: a message nothing understands will not be understood on the next attempt // Dropped: a message nothing understands will not be understood on the next attempt
// either, and asking for it again would spin. // either, and asking for it again would spin.
@@ -611,6 +633,25 @@ func (s *Server) sourceMoved(ctx context.Context, m Control) {
_ = m.Took() _ = m.Took()
} }
// pullUpdated asks for a pull request's merge check. Taken whatever happens: a check that could not be
// asked is said here, and the next push to the pull request asks again.
func (s *Server) pullUpdated(ctx context.Context, m Control) {
var p PullUpdated
if err := json.Unmarshal(m.Body(), &p); err != nil || p.Commit == "" || p.Repo == "" {
s.log.Printf("a pull request's announcement could not be read or named no repository or head; ignored")
_ = m.Took()
return
}
if s.checker == nil {
_ = m.Took()
return
}
if err := s.checker.PullUpdated(ctx, p); err != nil {
s.log.Printf("%s/%s#%d at %.8s could not be asked to be checked: %v", p.Owner, p.Repo, p.Number, p.Commit, err)
}
_ = m.Took()
}
// saysWhatItDid states what a machine now runs, or what it would not take, as a fact on the bus // saysWhatItDid states what a machine now runs, or what it would not take, as a fact on the bus
// (novox/hq ADR 0134). // (novox/hq ADR 0134).
// //
Executable
+35
View File
@@ -0,0 +1,35 @@
#!/bin/sh
# The merge check of the controller (novox/hq to-be 45 §9), run by the build seat on every pull request
# before it merges — and by hand: `MESH_FACTS=facts.json MESH_GATE_POSTGRES=… MESH_TEST_POSTGRES=…
# MESH_TEST_NATS=… sh merge-check.sh`.
#
# The build seat clones this repository with the catalogue and the host beside it (the tests read them
# there), reads the facts snapshot the controller keeps, and raises a throwaway store and bus of the
# versions the mesh runs; this says what is judged with them:
#
# 1. formatted and vetted;
# 2. the merge gate, judged by THIS change's controller: every machine of the snapshot composed with
# it and validated by the node-engine's own validator, against the mesh as it is;
# 3. the whole suite, the replays among it, against that store and that bus, one package at a time
# because the live tests share one bus's fixed names.
#
# Fails on the first that fails. The gate's verdict is written where MESH_CHECK_VERDICT says, so the
# pull request is told the gate's own words.
set -eu
export GOFLAGS=-mod=vendor GOPROXY=off CGO_ENABLED=0
unformatted=$(gofmt -l cmd internal examples)
if [ -n "$unformatted" ]; then
echo "not gofmt'd:"
echo "$unformatted"
exit 1
fi
go vet ./...
judge="${MESH_CHECK_BESIDE:-${TMPDIR:-/tmp}}/bin/judge"
go build -o "$judge" ./cmd/mesh-controller
"$judge" merge-gate --facts "$MESH_FACTS" --store "$MESH_GATE_POSTGRES" \
--repository "${MESH_CHECK_REPOSITORY:-novox/mesh-controller}" --tree . \
--changed "${MESH_CHECK_CHANGED:-}" --json > "${MESH_CHECK_VERDICT:-/dev/null}"
go test -p 1 -timeout 25m ./...
BIN
View File
Binary file not shown.