Raise the mesh as it is in the gate, call a baseline that does not compose an error, and let a check run by hand as the seat runs it
The gate composed 0 of 4 machines with the change and without, and passed every change: the store it raised held each module's bus credential but no account for it (issue 203's refusal), no outward links (so no filter could be composed), and refused settings the mesh holds. Now the account is minted with its credential, the facts carry each machine's outward links (a stand-in for an older snapshot), the mesh's layers are kept as held, and a withheld path keeps a path's shape. A machine the mesh composes that the gate cannot raise makes the verdict an error, never a pass; the verdict alone is on stdout. A merge-check.sh that passed on an agent's machine failed on the build seat: a newer gofmt, siblings at a feature branch, another user. `mesh-controller check-here` runs builder.Check with the ask the controller would make, from facts that now name the toolchains and the refs cloned beside; a failed script is said by what failed. (novox/hq issues 282, 283)
This commit is contained in:
@@ -81,6 +81,9 @@ type CheckSpec struct {
|
||||
// Toolchains is every toolchain the mesh holds, by language, for a script that declares another.
|
||||
Toolchain string
|
||||
Toolchains map[string]string
|
||||
// User is who a check's containers run as, uid:gid: the builder's own when empty — on the build seat,
|
||||
// the user its service runs as. A check run by hand (`mesh-controller check-here`) says the seat's.
|
||||
User 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.
|
||||
@@ -326,10 +329,18 @@ func Check(ctx context.Context, run Runner, spec CheckSpec, workspace, registry
|
||||
if spec.Toolchain == "" {
|
||||
return CheckVerdict{}, errors.New("the mesh holds no Go toolchain to run a check in")
|
||||
}
|
||||
user := spec.User
|
||||
if user == "" {
|
||||
user = fmt.Sprintf("%d:%d", os.Getuid(), os.Getgid())
|
||||
}
|
||||
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}
|
||||
"--user", user, "--env", "HOME=" + workspace,
|
||||
// The check's own checkouts, whoever cloned them: git in a container of another user than the
|
||||
// one that cloned refuses a repository it does not own ("dubious ownership"), and Go's build
|
||||
// stamps the version from git — a judge that would not build for want of it.
|
||||
"--env", "GIT_CONFIG_COUNT=1", "--env", "GIT_CONFIG_KEY_0=safe.directory", "--env", "GIT_CONFIG_VALUE_0=*"}
|
||||
for _, e := range env {
|
||||
args = append(args, "--env", e)
|
||||
}
|
||||
@@ -421,7 +432,7 @@ func Check(ctx context.Context, run Runner, spec CheckSpec, workspace, registry
|
||||
case ctx.Err() != nil:
|
||||
return v, ctx.Err()
|
||||
case err != nil:
|
||||
v.Repo = &Layer{Verdict: "fail", Summary: "its " + CheckScript + " failed: " + lastLine(own.String())}
|
||||
v.Repo = &Layer{Verdict: "fail", Summary: "its " + CheckScript + " failed: " + whatFailed(own.String())}
|
||||
default:
|
||||
v.Repo = &Layer{Verdict: "pass", Summary: "its " + CheckScript + " passed"}
|
||||
}
|
||||
@@ -503,8 +514,14 @@ func gateLayer(ctx context.Context, spec CheckSpec, tree, root, gate, verdictFil
|
||||
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
|
||||
if raw, err := os.ReadFile(verdictFile); err == nil && json.Unmarshal(raw, &said) == nil {
|
||||
switch said.Verdict {
|
||||
case "fail":
|
||||
return "fail", said.Summary
|
||||
case "error":
|
||||
// The gate could not raise the mesh as it is (novox/hq issue 282): said in its own words.
|
||||
return "error", 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())
|
||||
@@ -816,6 +833,33 @@ func (t *tail) String() string {
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
// whatFailed is the line of a failed script's output that says what failed, for the status a pull request
|
||||
// shows: the first failing test, the first failing package, the files not formatted — a bare "FAIL" or a
|
||||
// file's name said nothing a reader could act on (novox/hq issue 283) — and the last line otherwise.
|
||||
func whatFailed(s string) string {
|
||||
lines := strings.Split(strings.TrimSpace(s), "\n")
|
||||
for i, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "not gofmt'd:") {
|
||||
var files []string
|
||||
for _, f := range lines[i+1:] {
|
||||
if f = strings.TrimSpace(f); f != "" {
|
||||
files = append(files, f)
|
||||
}
|
||||
}
|
||||
return "not gofmt'd by the toolchain's gofmt: " + strings.Join(files, ", ")
|
||||
}
|
||||
}
|
||||
for _, prefix := range []string{"--- FAIL:", "FAIL\t", "panic:"} {
|
||||
for _, line := range lines {
|
||||
if line = strings.TrimSpace(line); strings.HasPrefix(line, prefix) {
|
||||
return line
|
||||
}
|
||||
}
|
||||
}
|
||||
return lastLine(s)
|
||||
}
|
||||
|
||||
func lastLine(s string) string {
|
||||
lines := strings.Split(strings.TrimSpace(s), "\n")
|
||||
return strings.TrimSpace(lines[len(lines)-1])
|
||||
|
||||
@@ -414,3 +414,18 @@ func TestABuildSaysWhetherItsCommitIsOnTheTrunk(t *testing.T) {
|
||||
t.Errorf("a repository with no origin reads as trunk %q, on %v", trunk, on)
|
||||
}
|
||||
}
|
||||
|
||||
// **Issue 283**: a failed merge-check.sh was said by its last line — a bare "FAIL", or the name of a file
|
||||
// gofmt listed — which named nothing a reader could act on. The status says what failed.
|
||||
func TestAFailedScriptIsSaidByWhatFailed(t *testing.T) {
|
||||
for out, want := range map[string]string{
|
||||
"not gofmt'd:\ninternal/bootstrap/publish_test.go\n": "not gofmt'd by the toolchain's gofmt: internal/bootstrap/publish_test.go",
|
||||
"ok \tx/a\t1s\n--- FAIL: TestTheInstallersFirstUserList (0.59s)\n t.go:37: refused\nFAIL\nFAIL\tx/b\t1s\nFAIL\n": "--- FAIL: TestTheInstallersFirstUserList (0.59s)",
|
||||
"ok \tx/a\t1s\nFAIL\tx/b [build failed]\nFAIL\n": "FAIL\tx/b [build failed]",
|
||||
"npm ERR! missing script: typecheck\n": "npm ERR! missing script: typecheck",
|
||||
} {
|
||||
if got := whatFailed(out); got != want {
|
||||
t.Errorf("%q is said as %q, not %q", out, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,10 @@ type Facts struct {
|
||||
// Edges are the build dependencies between modules, as the mesh recorded them — what a merge's
|
||||
// rebuild width is computed from.
|
||||
Edges []Edge `json:"edges,omitempty"`
|
||||
// Beside is the ref each repository is cloned at beside a merge check, by the directory it is found
|
||||
// under — the commits the mesh runs, the catalogue's main — so a check run by hand reads the siblings
|
||||
// the build seat reads (novox/hq issue 283).
|
||||
Beside map[string]string `json:"beside,omitempty"`
|
||||
}
|
||||
|
||||
// Build names one build of a core component.
|
||||
@@ -78,6 +82,11 @@ type Versions struct {
|
||||
Store string `json:"store,omitempty"`
|
||||
// NodeEngines are the node-engine builds the machines report, each once.
|
||||
NodeEngines []string `json:"node-engines,omitempty"`
|
||||
// Toolchains are the toolchain images a merge check runs in, by language, as the mesh holds them —
|
||||
// with no address: the artifact store the snapshot is read from is where they are pulled from. What
|
||||
// a repository's merge-check.sh runs in on the build seat, and so what it must run in anywhere else
|
||||
// (novox/hq issue 283): two releases of one compiler disagree, down to how gofmt lays out a file.
|
||||
Toolchains map[string]string `json:"toolchains,omitempty"`
|
||||
}
|
||||
|
||||
// Source is a repository the mesh builds from, and the newest commit it built a module of.
|
||||
@@ -112,6 +121,10 @@ type Machine struct {
|
||||
Public bool `json:"public,omitempty"`
|
||||
// OnNetwork is whether it has a place on the private network at all.
|
||||
OnNetwork bool `json:"on-network,omitempty"`
|
||||
// OutwardLinks are the links it reported as facing outside it, scrubbed: a machine that reported none
|
||||
// is sent no filter, so a check that raised it without them would compose a machine the mesh does
|
||||
// not (novox/hq ADR 0140).
|
||||
OutwardLinks []string `json:"outward-links,omitempty"`
|
||||
// Adopted is whether the mesh adopted it rather than converged it.
|
||||
Adopted bool `json:"adopted,omitempty"`
|
||||
// Account is the operator's login there (a pseudonym of the same length), and AccountHome where its
|
||||
|
||||
@@ -157,3 +157,18 @@ func TestANewerSnapshotIsRefusedNotHalfRead(t *testing.T) {
|
||||
t.Errorf("read %+v, %v", f, err)
|
||||
}
|
||||
}
|
||||
|
||||
// A path that reads as a key is withheld, and keeps a path's shape: an access or a place must be an
|
||||
// absolute path, and a bare word left a machine the mesh composes refused where a check raised it.
|
||||
func TestAWithheldPathStaysAPath(t *testing.T) {
|
||||
s := NewScrubber()
|
||||
if got := s.Text("/storage/media/Formula1-Season-2026"); got != "/"+Withheld {
|
||||
t.Errorf("a path reading as a key became %q", got)
|
||||
}
|
||||
if got := s.Text("/storage/media/movies"); got != "/storage/media/movies" {
|
||||
t.Errorf("a plain path became %q", got)
|
||||
}
|
||||
if got := s.Text("Hunter2Hunter2Hunter2Hunter2xx"); got != Withheld {
|
||||
t.Errorf("a key became %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,7 +228,13 @@ func secretRuns(text string) string {
|
||||
b.WriteString(run)
|
||||
continue
|
||||
}
|
||||
b.WriteString(judgeRun(run))
|
||||
judged := judgeRun(run)
|
||||
// A path withheld keeps a path's shape — an absolute one stays absolute — so a check composing a
|
||||
// place or an access still finds a path where one must be.
|
||||
if judged == Withheld && strings.HasPrefix(run, "/") {
|
||||
judged = "/" + Withheld
|
||||
}
|
||||
b.WriteString(judged)
|
||||
}
|
||||
b.WriteString(text[last:])
|
||||
return b.String()
|
||||
|
||||
@@ -755,6 +755,18 @@ func profileFrom(raw []byte) ([]Capability, error) {
|
||||
// this is a statement of the whole layer, so removing a key is done by leaving it out, which is
|
||||
// the only way removing one could work at all.
|
||||
func (i *Inventory) SetSettings(ctx context.Context, nodeName, module string, values map[string]any) error {
|
||||
return i.setSettings(ctx, nodeName, module, values, true)
|
||||
}
|
||||
|
||||
// KeepSettings records a layer the mesh already holds, as it holds it, without judging it alone: for a
|
||||
// store raised from the facts snapshot (the merge gate), where the layers arrive one at a time and a
|
||||
// mesh-wide layer that needs a machine's own value to compose would be refused before that machine's
|
||||
// layer is there — though the mesh keeps both and composes. Composition still judges every layer.
|
||||
func (i *Inventory) KeepSettings(ctx context.Context, nodeName, module string, values map[string]any) error {
|
||||
return i.setSettings(ctx, nodeName, module, values, false)
|
||||
}
|
||||
|
||||
func (i *Inventory) setSettings(ctx context.Context, nodeName, module string, values map[string]any, judge bool) error {
|
||||
raw, err := json.Marshal(values)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -762,8 +774,12 @@ func (i *Inventory) SetSettings(ctx context.Context, nodeName, module string, va
|
||||
// Judged here, against the module's current definition, before it is kept (novox/hq ADR 0163,
|
||||
// rule 6): a setting that cannot compose is refused where it is set, naming the node, the
|
||||
// module, the layer and the key — never stored to refuse the whole machine where it is read.
|
||||
if err := i.judgeSettings(ctx, nodeName, module, values); err != nil {
|
||||
return err
|
||||
if judge {
|
||||
if err := i.judgeSettings(ctx, nodeName, module, values); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if _, err := i.declared(ctx, module); err != nil {
|
||||
return fmt.Errorf("%w: %s", ErrNoSuchModule, module)
|
||||
}
|
||||
if nodeName == "" {
|
||||
// A port is a fact about one machine (novox/hq ADR 0100). Refused here, in composition's
|
||||
|
||||
Reference in New Issue
Block a user