diff --git a/contracts/go/seats/nodebuildgo/contract.go b/contracts/go/seats/nodebuildgo/contract.go index 3a31416a..7eec9ed5 100644 --- a/contracts/go/seats/nodebuildgo/contract.go +++ b/contracts/go/seats/nodebuildgo/contract.go @@ -361,13 +361,17 @@ type StepAskBuild struct { // StepAskCheck is a type of the contract. type StepAskCheck struct { - Fetch Fetch `json:"fetch"` - Kept bool `json:"kept,omitempty"` - Levels []string `json:"levels"` + Fetch Fetch `json:"fetch"` + Kept bool `json:"kept,omitempty"` + Levels []string `json:"levels"` + // the node names the check's tests run against + NodeNames []string `json:"node-names,omitempty"` Packages []string `json:"packages,omitempty"` Run string `json:"run,omitempty"` Runner string `json:"runner,omitempty"` RunnerWhen string `json:"runner_when,omitempty"` + // the toolchain the checker agreed for the language (ADR 0584 clause 2): a node on another declines the check + Toolchain *ToolchainUsed `json:"toolchain,omitempty"` } // StepAskCompose is a type of the contract. diff --git a/contracts/go/seats/nodebuildgo/contract_test.go b/contracts/go/seats/nodebuildgo/contract_test.go index 4aabcc73..abc309d5 100644 --- a/contracts/go/seats/nodebuildgo/contract_test.go +++ b/contracts/go/seats/nodebuildgo/contract_test.go @@ -39,7 +39,7 @@ func TestFixtures(t *testing.T) { }, subjectIs: nil}, {name: "a build ask", subject: "mesh.seat.node-build-go.accept.delivery", given: "{\"asker\":\"mesh-delivery-builder\",\"build\":{\"artifacts\":[{\"artifact\":\"go\"}],\"module\":\"nats\"},\"id\":\"a1b2\",\"kind\":\"build\",\"language\":\"go\"}", wire: "{\"asker\":\"mesh-delivery-builder\",\"build\":{\"artifacts\":[{\"artifact\":\"go\"}],\"module\":\"nats\"},\"id\":\"a1b2\",\"kind\":\"build\",\"language\":\"go\"}", decode: func(b []byte) (any, error) { var v DeliveryAccept; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-go\\.accept\\.delivery$")}, - {name: "a check ask with its fetch, siblings and levels", subject: "mesh.seat.node-build-go.accept.check", given: "{\"asker\":\"mesh-delivery-checker\",\"at\":\"2026-11-05T10:00:00Z\",\"check\":{\"fetch\":{\"base\":\"89abcdef0123456789abcdef0123456789abcdef\",\"beside\":[{\"commit\":\"fedcba9876543210fedcba9876543210fedcba98\",\"repository\":\"novox/mesh-sdk\"}],\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"packages\":{\"git.novox.be/novox/mesh-sdk/go\":\"fedcba9876543210fedcba9876543210fedcba98\"},\"ref\":\"refs/pull/7/head\",\"repository\":\"novox/mesh-catalog\"},\"kept\":true,\"levels\":[\"unit\",\"integration\"]},\"id\":\"c3d4\",\"kind\":\"check\",\"language\":\"go\",\"v\":1}", wire: "{\"asker\":\"mesh-delivery-checker\",\"at\":\"2026-11-05T10:00:00Z\",\"check\":{\"fetch\":{\"base\":\"89abcdef0123456789abcdef0123456789abcdef\",\"beside\":[{\"commit\":\"fedcba9876543210fedcba9876543210fedcba98\",\"repository\":\"novox/mesh-sdk\"}],\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"packages\":{\"git.novox.be/novox/mesh-sdk/go\":\"fedcba9876543210fedcba9876543210fedcba98\"},\"ref\":\"refs/pull/7/head\",\"repository\":\"novox/mesh-catalog\"},\"kept\":true,\"levels\":[\"unit\",\"integration\"]},\"id\":\"c3d4\",\"kind\":\"check\",\"language\":\"go\",\"v\":1}", + {name: "a check ask with its fetch, siblings and levels", subject: "mesh.seat.node-build-go.accept.check", given: "{\"asker\":\"mesh-delivery-checker\",\"at\":\"2026-11-05T10:00:00Z\",\"check\":{\"fetch\":{\"base\":\"89abcdef0123456789abcdef0123456789abcdef\",\"beside\":[{\"commit\":\"fedcba9876543210fedcba9876543210fedcba98\",\"repository\":\"novox/mesh-sdk\"}],\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"packages\":{\"git.novox.be/novox/mesh-sdk/go\":\"fedcba9876543210fedcba9876543210fedcba98\"},\"ref\":\"refs/pull/7/head\",\"repository\":\"novox/mesh-catalog\"},\"kept\":true,\"levels\":[\"unit\",\"integration\"],\"node-names\":[\"ace\",\"g14\"],\"toolchain\":{\"language\":\"go\",\"recipe\":\"2\",\"version\":\"1.26.0\"}},\"id\":\"c3d4\",\"kind\":\"check\",\"language\":\"go\",\"v\":1}", wire: "{\"asker\":\"mesh-delivery-checker\",\"at\":\"2026-11-05T10:00:00Z\",\"check\":{\"fetch\":{\"base\":\"89abcdef0123456789abcdef0123456789abcdef\",\"beside\":[{\"commit\":\"fedcba9876543210fedcba9876543210fedcba98\",\"repository\":\"novox/mesh-sdk\"}],\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"packages\":{\"git.novox.be/novox/mesh-sdk/go\":\"fedcba9876543210fedcba9876543210fedcba98\"},\"ref\":\"refs/pull/7/head\",\"repository\":\"novox/mesh-catalog\"},\"kept\":true,\"levels\":[\"unit\",\"integration\"],\"node-names\":[\"ace\",\"g14\"],\"toolchain\":{\"language\":\"go\",\"recipe\":\"2\",\"version\":\"1.26.0\"}},\"id\":\"c3d4\",\"kind\":\"check\",\"language\":\"go\",\"v\":1}", decode: func(b []byte) (any, error) { var v CheckAccept; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-go\\.accept\\.check$")}, {name: "a finished build step with its artifacts", subject: "", given: "{\"ended\":\"2026-11-05T10:03:00Z\",\"id\":\"a1b2\",\"node\":\"ace\",\"result\":{\"artifacts\":[{\"artifact\":\"go\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"reference\":\"container-image-registry/nats/go@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}],\"imports\":[\"git.novox.be/novox/mesh-sdk/go\"],\"toolchain\":{\"language\":\"go\",\"recipe\":\"1\",\"version\":\"1.26.0\"}},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"passed\",\"tool\":\"build\",\"v\":1}", wire: "{\"ended\":\"2026-11-05T10:03:00Z\",\"id\":\"a1b2\",\"node\":\"ace\",\"result\":{\"artifacts\":[{\"artifact\":\"go\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"reference\":\"container-image-registry/nats/go@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}],\"imports\":[\"git.novox.be/novox/mesh-sdk/go\"],\"toolchain\":{\"language\":\"go\",\"recipe\":\"1\",\"version\":\"1.26.0\"}},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"passed\",\"tool\":\"build\",\"v\":1}", decode: func(b []byte) (any, error) { var v StepsByNodeIDValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, diff --git a/contracts/go/seats/nodebuildimage/contract.go b/contracts/go/seats/nodebuildimage/contract.go index e5445d8a..c9675571 100644 --- a/contracts/go/seats/nodebuildimage/contract.go +++ b/contracts/go/seats/nodebuildimage/contract.go @@ -361,13 +361,17 @@ type StepAskBuild struct { // StepAskCheck is a type of the contract. type StepAskCheck struct { - Fetch Fetch `json:"fetch"` - Kept bool `json:"kept,omitempty"` - Levels []string `json:"levels"` + Fetch Fetch `json:"fetch"` + Kept bool `json:"kept,omitempty"` + Levels []string `json:"levels"` + // the node names the check's tests run against + NodeNames []string `json:"node-names,omitempty"` Packages []string `json:"packages,omitempty"` Run string `json:"run,omitempty"` Runner string `json:"runner,omitempty"` RunnerWhen string `json:"runner_when,omitempty"` + // the toolchain the checker agreed for the language (ADR 0584 clause 2): a node on another declines the check + Toolchain *ToolchainUsed `json:"toolchain,omitempty"` } // StepAskCompose is a type of the contract. diff --git a/contracts/go/seats/nodebuildnode/contract.go b/contracts/go/seats/nodebuildnode/contract.go index 9f2abc00..966a4add 100644 --- a/contracts/go/seats/nodebuildnode/contract.go +++ b/contracts/go/seats/nodebuildnode/contract.go @@ -361,13 +361,17 @@ type StepAskBuild struct { // StepAskCheck is a type of the contract. type StepAskCheck struct { - Fetch Fetch `json:"fetch"` - Kept bool `json:"kept,omitempty"` - Levels []string `json:"levels"` + Fetch Fetch `json:"fetch"` + Kept bool `json:"kept,omitempty"` + Levels []string `json:"levels"` + // the node names the check's tests run against + NodeNames []string `json:"node-names,omitempty"` Packages []string `json:"packages,omitempty"` Run string `json:"run,omitempty"` Runner string `json:"runner,omitempty"` RunnerWhen string `json:"runner_when,omitempty"` + // the toolchain the checker agreed for the language (ADR 0584 clause 2): a node on another declines the check + Toolchain *ToolchainUsed `json:"toolchain,omitempty"` } // StepAskCompose is a type of the contract. diff --git a/contracts/parts/build-step.json b/contracts/parts/build-step.json index 55920376..9ee1d5d0 100644 --- a/contracts/parts/build-step.json +++ b/contracts/parts/build-step.json @@ -130,6 +130,17 @@ "items": { "type": "string" } + }, + "toolchain": { + "$ref": "#/defs/ToolchainUsed", + "description": "the toolchain the checker agreed for the language (ADR 0584 clause 2): a node on another declines the check" + }, + "node-names": { + "type": "array", + "items": { + "type": "string" + }, + "description": "the node names the check's tests run against" } }, "required": [ diff --git a/contracts/seats/node-build-go/fixtures/states.json b/contracts/seats/node-build-go/fixtures/states.json index 5193a6af..0898036e 100644 --- a/contracts/seats/node-build-go/fixtures/states.json +++ b/contracts/seats/node-build-go/fixtures/states.json @@ -91,7 +91,16 @@ "unit", "integration" ], - "kept": true + "kept": true, + "toolchain": { + "language": "go", + "version": "1.26.0", + "recipe": "2" + }, + "node-names": [ + "ace", + "g14" + ] } } }, diff --git a/contracts/ts/seats/nodebuildgo/src/parts/build-step.ts b/contracts/ts/seats/nodebuildgo/src/parts/build-step.ts index 4ca1eb95..2ce76a13 100644 --- a/contracts/ts/seats/nodebuildgo/src/parts/build-step.ts +++ b/contracts/ts/seats/nodebuildgo/src/parts/build-step.ts @@ -173,10 +173,14 @@ export interface StepAskCheck { "fetch": Fetch; "kept"?: boolean; "levels": string[]; + /** the node names the check's tests run against */ + "node-names"?: string[]; "packages"?: string[]; "run"?: string; "runner"?: string; "runner_when"?: string; + /** the toolchain the checker agreed for the language (ADR 0584 clause 2): a node on another declines the check */ + "toolchain"?: ToolchainUsed; } export interface StepAskCompose { diff --git a/contracts/ts/seats/nodebuildimage/src/parts/build-step.ts b/contracts/ts/seats/nodebuildimage/src/parts/build-step.ts index 4ca1eb95..2ce76a13 100644 --- a/contracts/ts/seats/nodebuildimage/src/parts/build-step.ts +++ b/contracts/ts/seats/nodebuildimage/src/parts/build-step.ts @@ -173,10 +173,14 @@ export interface StepAskCheck { "fetch": Fetch; "kept"?: boolean; "levels": string[]; + /** the node names the check's tests run against */ + "node-names"?: string[]; "packages"?: string[]; "run"?: string; "runner"?: string; "runner_when"?: string; + /** the toolchain the checker agreed for the language (ADR 0584 clause 2): a node on another declines the check */ + "toolchain"?: ToolchainUsed; } export interface StepAskCompose { diff --git a/contracts/ts/seats/nodebuildnode/src/parts/build-step.ts b/contracts/ts/seats/nodebuildnode/src/parts/build-step.ts index 4ca1eb95..2ce76a13 100644 --- a/contracts/ts/seats/nodebuildnode/src/parts/build-step.ts +++ b/contracts/ts/seats/nodebuildnode/src/parts/build-step.ts @@ -173,10 +173,14 @@ export interface StepAskCheck { "fetch": Fetch; "kept"?: boolean; "levels": string[]; + /** the node names the check's tests run against */ + "node-names"?: string[]; "packages"?: string[]; "run"?: string; "runner"?: string; "runner_when"?: string; + /** the toolchain the checker agreed for the language (ADR 0584 clause 2): a node on another declines the check */ + "toolchain"?: ToolchainUsed; } export interface StepAskCompose { diff --git a/sdk/buildstep/ask.go b/sdk/buildstep/ask.go index be85a8af..1701cab3 100644 --- a/sdk/buildstep/ask.go +++ b/sdk/buildstep/ask.go @@ -258,6 +258,9 @@ type CheckJob struct { Run string `json:"run,omitempty"` Packages []string `json:"packages,omitempty"` NodeNames []string `json:"node-names,omitempty"` + // Toolchain is the toolchain the checker agreed for the language (ADR 0584 clause 2): a holder whose node reports + // another version declines the check. + Toolchain *ToolchainUsed `json:"toolchain,omitempty"` } // ComposeJob asks one compose-candidate build. @@ -409,6 +412,9 @@ func (c CheckJob) check(id string) error { return fmt.Errorf("buildstep: the check ask %s names a package %q that is no import path", id, p) } } + if t := c.Toolchain; t != nil && (t.Language == "" || t.Version == "" || t.Recipe == "") { + return fmt.Errorf("buildstep: the check ask %s names a toolchain without its language, version and recipe", id) + } seen := map[string]bool{} for _, n := range c.NodeNames { if err := meshkeys.CheckName("node", n); err != nil || strings.Contains(n, ",") || seen[n] { diff --git a/sdk/buildstep/ask_test.go b/sdk/buildstep/ask_test.go index 07b2d67a..b452a1cb 100644 --- a/sdk/buildstep/ask_test.go +++ b/sdk/buildstep/ask_test.go @@ -72,6 +72,19 @@ func TestAskCheckAndRoundTrip(t *testing.T) { t.Errorf("a level: %v", err) } chk = goodCheck() + chk.Check.Toolchain = &ToolchainUsed{Language: "go", Version: "1.26.0", Recipe: "2"} + raw, err := EncodeStepAsk(chk) + if err != nil { + t.Fatal(err) + } + if back, err := DecodeStepAsk(raw); err != nil || back.Check.Toolchain == nil || back.Check.Toolchain.Version != "1.26.0" { + t.Errorf("the check's toolchain is lost in a round trip: %+v, %v", back.Check, err) + } + chk.Check.Toolchain.Recipe = "" + if _, err := EncodeStepAsk(chk); err == nil || !strings.Contains(err.Error(), "toolchain") { + t.Errorf("a toolchain without a recipe: %v", err) + } + chk = goodCheck() chk.Language = AskImage if _, err := EncodeStepAsk(chk); err == nil || !strings.Contains(err.Error(), "no tests") { t.Errorf("an image check: %v", err)