diff --git a/contracts/go/seats/meshwants/contract.go b/contracts/go/seats/meshwants/contract.go index ad44c028..f4cb11ff 100644 --- a/contracts/go/seats/meshwants/contract.go +++ b/contracts/go/seats/meshwants/contract.go @@ -100,9 +100,6 @@ const StreamVersionDelivered = "SEAT_mesh-wants_version-delivered" // SubjectVersionDelivered is where version-delivered is published. const SubjectVersionDelivered = "mesh.seat.mesh-wants.event.version-delivered" -// StreamStepProgress is the stream step-progress is kept in. -const StreamStepProgress = "SEAT_mesh-wants_step-progress" - // SubjectStepProgress is where step-progress is published, on mesh.seat.mesh-wants.event.step-progress.. func SubjectStepProgress(node string) string { return "mesh.seat.mesh-wants.event.step-progress." + node diff --git a/contracts/go/seats/nodebuildgo/contract.go b/contracts/go/seats/nodebuildgo/contract.go index d1ae0f25..7db4425a 100644 --- a/contracts/go/seats/nodebuildgo/contract.go +++ b/contracts/go/seats/nodebuildgo/contract.go @@ -36,6 +36,7 @@ const ( EventStepEnded = "step-ended" EventToolchainReported = "toolchain-reported" EventStepProgress = "step-progress" + EventAskTaken = "ask-taken" ) // Verbs is every verb the seat serves, in its definition's order: a new copy each call. @@ -45,7 +46,7 @@ func Verbs() []string { // Events is every event published on the seat's subject: a new copy each call. func Events() []string { - return []string{EventStepEnded, EventToolchainReported, EventStepProgress} + return []string{EventStepEnded, EventToolchainReported, EventStepProgress, EventAskTaken} } // Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects. @@ -74,9 +75,6 @@ func SubjectToolchainReported(node string) string { // SubjectToolchainReportedAll hears every toolchain-reported. const SubjectToolchainReportedAll = "mesh.seat.node-build-go.event.toolchain-reported.*" -// StreamStepProgress is the stream step-progress is kept in. -const StreamStepProgress = "SEAT_node-build-go_step-progress" - // SubjectStepProgress is where step-progress is published, on mesh.seat.node-build-go.event.step-progress.. func SubjectStepProgress(node string) string { return "mesh.seat.node-build-go.event.step-progress." + node @@ -85,6 +83,12 @@ func SubjectStepProgress(node string) string { // SubjectStepProgressAll hears every step-progress. const SubjectStepProgressAll = "mesh.seat.node-build-go.event.step-progress.*" +// SubjectAskTaken is where ask-taken is published, on mesh.seat.node-build-go.event.ask-taken.. +func SubjectAskTaken(node string) string { return "mesh.seat.node-build-go.event.ask-taken." + node } + +// SubjectAskTakenAll hears every ask-taken. +const SubjectAskTakenAll = "mesh.seat.node-build-go.event.ask-taken.*" + // AcceptSubjectDelivery is where delivery is sent. const AcceptSubjectDelivery = "mesh.seat.node-build-go.accept.delivery" @@ -372,6 +376,16 @@ type AskArtifact struct { Version string `json:"version,omitempty"` } +// AskTaken is an ask a build seat's holder took, said when it takes any ask, whatever its asker (design 47 G section 602). +type AskTaken struct { + // the ask's id + Ask string `json:"ask"` + Asker string `json:"asker"` + At time.Time `json:"at"` + // the node that took it, as in the subject + Node string `json:"node"` +} + // BuildAnswer is a type of the contract. type BuildAnswer struct { Artifacts []BuiltArtifact `json:"artifacts"` @@ -761,23 +775,31 @@ type StepAsk struct { Step *StepRun `json:"step,omitempty"` // the layout version V int64 `json:"v,omitempty"` + // the version MAJOR.MINOR.PATCH the builder reserved for the build, passed to it as MESH_VERSION; empty for a build that reserves none, which stamps none (design 47 G section 573) + Version string `json:"version,omitempty"` } -// StepEnded is a pipeline step's end, said by the build seat's holder on the node that ran it (ADR 0666 sections 7, 21): the runner takes it only from that holder's account, naming the ask, the commit and the fingerprint it was asked at. +// StepEnded is an ask's end, said by the build seat's holder on the node that ran it, whatever its asker (design 47 G section 602): a pipeline step's end names the pipeline, the commit and the fingerprint it was asked at, which the runner takes only from that holder's account (ADR 0666 sections 7, 21); the end of any other ask names its asker, the ask, the node, the outcome and when, and neither commit nor fingerprint (design 47 G section 548). type StepEnded struct { - Artifact string `json:"artifact"` + Artifact string `json:"artifact,omitempty"` + // the ask's id, by which a reader takes only its own asks + Ask string `json:"ask"` + // who asked it + Asker string `json:"asker"` + // when it ended (RFC 3339 time) + At time.Time `json:"at"` // for a rerun, the caller the runtime proved By string `json:"by,omitempty"` // the packages not run because the ask's kept results held them Carried []KeptResult `json:"carried,omitempty"` - Commit string `json:"commit"` + Commit string `json:"commit,omitempty"` Ended *time.Time `json:"ended,omitempty"` - Fingerprint string `json:"fingerprint"` + Fingerprint string `json:"fingerprint,omitempty"` // the ask's id ID string `json:"id"` // the packages that passed at their digest, for the runner to keep Kept []KeptResult `json:"kept,omitempty"` - Module string `json:"module"` + Module string `json:"module,omitempty"` // the node that ran it, as in the subject Node string `json:"node"` // never passed for a step that ran nothing @@ -786,12 +808,12 @@ type StepEnded struct { Output string `json:"output,omitempty"` // the output's object in the sender's sent store, when over the per-message limit (ADR 0666 section 16) OutputObject string `json:"output-object,omitempty"` - Pipeline string `json:"pipeline"` + Pipeline string `json:"pipeline,omitempty"` Rerun bool `json:"rerun,omitempty"` // why it failed or could not run, naming the part not heard Said string `json:"said,omitempty"` Started *time.Time `json:"started,omitempty"` - Step string `json:"step"` + Step string `json:"step,omitempty"` Tests *TestCounts `json:"tests,omitempty"` // the layout version V int64 `json:"v,omitempty"` @@ -912,6 +934,8 @@ type TestCounts struct { type Toolchain struct { // when (RFC 3339 time) At *time.Time `json:"at,omitempty"` + // the sha256: digest of the language's toolchain program as the build module runs it: the program of the module that provides that language's toolchain, never the build module's own (design 47 G section 472) + Digest string `json:"digest,omitempty"` // go or typescript Language string `json:"language"` Recipe string `json:"recipe"` @@ -1067,6 +1091,9 @@ type ToolchainReportedEvent = ToolchainReported // StepProgressEvent is the body of the event step-progress. type StepProgressEvent = StepProgress +// AskTakenEvent is the body of the event ask-taken. +type AskTakenEvent = AskTaken + // StepsByAskerNodeIDValue is one value of the state steps under the key ... type StepsByAskerNodeIDValue = Step diff --git a/contracts/go/seats/nodebuildgo/contract_test.go b/contracts/go/seats/nodebuildgo/contract_test.go index 1948bb46..50eb29a1 100644 --- a/contracts/go/seats/nodebuildgo/contract_test.go +++ b/contracts/go/seats/nodebuildgo/contract_test.go @@ -17,14 +17,18 @@ func TestFixtures(t *testing.T) { decode func([]byte) (any, error) subjectIs *regexp.Regexp }{ - {name: "a unit step ended passed on its node, with its counts and what it carried", subject: "mesh.seat.node-build-go.event.step-ended.ace", given: "{\"artifact\":\"nats:program\",\"carried\":[{\"digest\":\"sha256:bbbb\",\"level\":\"unit\",\"package\":\"./modules/nats/a\"}],\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"ended\":\"2026-11-10T10:02:00Z\",\"fingerprint\":\"fp-0123\",\"id\":\"m2kx9a1b3c4d5e\",\"kept\":[{\"digest\":\"sha256:cccc\",\"level\":\"unit\",\"package\":\"./modules/nats/b\"}],\"module\":\"nats\",\"node\":\"ace\",\"outcome\":\"passed\",\"pipeline\":\"main.12.nats\",\"started\":\"2026-11-10T10:00:00Z\",\"step\":\"unit\",\"tests\":{\"unit\":14},\"v\":1}", wire: "{\"artifact\":\"nats:program\",\"carried\":[{\"digest\":\"sha256:bbbb\",\"level\":\"unit\",\"package\":\"./modules/nats/a\"}],\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"ended\":\"2026-11-10T10:02:00Z\",\"fingerprint\":\"fp-0123\",\"id\":\"m2kx9a1b3c4d5e\",\"kept\":[{\"digest\":\"sha256:cccc\",\"level\":\"unit\",\"package\":\"./modules/nats/b\"}],\"module\":\"nats\",\"node\":\"ace\",\"outcome\":\"passed\",\"pipeline\":\"main.12.nats\",\"started\":\"2026-11-10T10:00:00Z\",\"step\":\"unit\",\"tests\":{\"unit\":14},\"v\":1}", + {name: "a unit step ended passed on its node, with its counts and what it carried", subject: "mesh.seat.node-build-go.event.step-ended.ace", given: "{\"artifact\":\"nats:program\",\"ask\":\"m2kx9a1b3c4d5e\",\"asker\":\"mesh-delivery-pipeline\",\"at\":\"2026-11-10T10:02:00Z\",\"carried\":[{\"digest\":\"sha256:bbbb\",\"level\":\"unit\",\"package\":\"./modules/nats/a\"}],\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"ended\":\"2026-11-10T10:02:00Z\",\"fingerprint\":\"fp-0123\",\"id\":\"m2kx9a1b3c4d5e\",\"kept\":[{\"digest\":\"sha256:cccc\",\"level\":\"unit\",\"package\":\"./modules/nats/b\"}],\"module\":\"nats\",\"node\":\"ace\",\"outcome\":\"passed\",\"pipeline\":\"main.12.nats\",\"started\":\"2026-11-10T10:00:00Z\",\"step\":\"unit\",\"tests\":{\"unit\":14},\"v\":1}", wire: "{\"artifact\":\"nats:program\",\"ask\":\"m2kx9a1b3c4d5e\",\"asker\":\"mesh-delivery-pipeline\",\"at\":\"2026-11-10T10:02:00Z\",\"carried\":[{\"digest\":\"sha256:bbbb\",\"level\":\"unit\",\"package\":\"./modules/nats/a\"}],\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"ended\":\"2026-11-10T10:02:00Z\",\"fingerprint\":\"fp-0123\",\"id\":\"m2kx9a1b3c4d5e\",\"kept\":[{\"digest\":\"sha256:cccc\",\"level\":\"unit\",\"package\":\"./modules/nats/b\"}],\"module\":\"nats\",\"node\":\"ace\",\"outcome\":\"passed\",\"pipeline\":\"main.12.nats\",\"started\":\"2026-11-10T10:00:00Z\",\"step\":\"unit\",\"tests\":{\"unit\":14},\"v\":1}", decode: func(b []byte) (any, error) { var v StepEndedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-go\\.event\\.step-ended\\.[^.*> ]+$")}, - {name: "an integration step that could not run, naming the part not heard", subject: "mesh.seat.node-build-go.event.step-ended.g14", given: "{\"artifact\":\"nats:program\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"fingerprint\":\"fp-0123\",\"id\":\"m2kx9a1b3c4d5f\",\"module\":\"nats\",\"node\":\"g14\",\"outcome\":\"could not run\",\"output-object\":\"mesh-build-go_sent-objects/m2kx9a1b3c4d5f\",\"pipeline\":\"main.12.nats\",\"said\":\"the sandbox could not fetch novox/mesh-sdk\",\"step\":\"integration\",\"v\":1}", wire: "{\"artifact\":\"nats:program\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"fingerprint\":\"fp-0123\",\"id\":\"m2kx9a1b3c4d5f\",\"module\":\"nats\",\"node\":\"g14\",\"outcome\":\"could not run\",\"output-object\":\"mesh-build-go_sent-objects/m2kx9a1b3c4d5f\",\"pipeline\":\"main.12.nats\",\"said\":\"the sandbox could not fetch novox/mesh-sdk\",\"step\":\"integration\",\"v\":1}", + {name: "an integration step that could not run, naming the part not heard", subject: "mesh.seat.node-build-go.event.step-ended.g14", given: "{\"artifact\":\"nats:program\",\"ask\":\"m2kx9a1b3c4d5f\",\"asker\":\"mesh-delivery-pipeline\",\"at\":\"2026-11-10T10:02:00Z\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"fingerprint\":\"fp-0123\",\"id\":\"m2kx9a1b3c4d5f\",\"module\":\"nats\",\"node\":\"g14\",\"outcome\":\"could not run\",\"output-object\":\"mesh-build-go_sent-objects/m2kx9a1b3c4d5f\",\"pipeline\":\"main.12.nats\",\"said\":\"the sandbox could not fetch novox/mesh-sdk\",\"step\":\"integration\",\"v\":1}", wire: "{\"artifact\":\"nats:program\",\"ask\":\"m2kx9a1b3c4d5f\",\"asker\":\"mesh-delivery-pipeline\",\"at\":\"2026-11-10T10:02:00Z\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"fingerprint\":\"fp-0123\",\"id\":\"m2kx9a1b3c4d5f\",\"module\":\"nats\",\"node\":\"g14\",\"outcome\":\"could not run\",\"output-object\":\"mesh-build-go_sent-objects/m2kx9a1b3c4d5f\",\"pipeline\":\"main.12.nats\",\"said\":\"the sandbox could not fetch novox/mesh-sdk\",\"step\":\"integration\",\"v\":1}", decode: func(b []byte) (any, error) { var v StepEndedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-go\\.event\\.step-ended\\.[^.*> ]+$")}, {name: "a node's toolchain reported", subject: "mesh.seat.node-build-go.event.toolchain-reported.ace", given: "{\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"language\":\"go\",\"node\":\"ace\",\"recipe\":\"1\",\"version\":\"1.26.0\"}", wire: "{\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"language\":\"go\",\"node\":\"ace\",\"recipe\":\"1\",\"version\":\"1.26.0\"}", decode: func(b []byte) (any, error) { var v ToolchainReportedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-go\\.event\\.toolchain-reported\\.[^.*> ]+$")}, {name: "a running ask says its progress", subject: "mesh.seat.node-build-go.event.step-progress.ace", given: "{\"ask\":\"a1b2\",\"asker\":\"runner\",\"at\":\"2026-11-11T10:01:00Z\",\"phase\":\"fetch\"}", wire: "{\"ask\":\"a1b2\",\"asker\":\"runner\",\"at\":\"2026-11-11T10:01:00Z\",\"phase\":\"fetch\"}", decode: func(b []byte) (any, error) { var v StepProgressEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-go\\.event\\.step-progress\\.[^.*> ]+$")}, + {name: "a builder's build ended, naming its asker and no commit or fingerprint (G 548)", subject: "mesh.seat.node-build-go.event.step-ended.ace", given: "{\"ask\":\"m2kx9a1b3c4d6a\",\"asker\":\"mesh-delivery-builder\",\"at\":\"2026-11-11T10:03:00Z\",\"id\":\"m2kx9a1b3c4d6a\",\"node\":\"ace\",\"outcome\":\"failed\",\"said\":\"the build failed\",\"v\":1}", wire: "{\"ask\":\"m2kx9a1b3c4d6a\",\"asker\":\"mesh-delivery-builder\",\"at\":\"2026-11-11T10:03:00Z\",\"id\":\"m2kx9a1b3c4d6a\",\"node\":\"ace\",\"outcome\":\"failed\",\"said\":\"the build failed\",\"v\":1}", + decode: func(b []byte) (any, error) { var v StepEndedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-go\\.event\\.step-ended\\.[^.*> ]+$")}, + {name: "an ask taken, whatever its asker (G 602)", subject: "mesh.seat.node-build-go.event.ask-taken.ace", given: "{\"ask\":\"m2kx9a1b3c4d6a\",\"asker\":\"mesh-delivery-builder\",\"at\":\"2026-11-11T10:00:00Z\",\"node\":\"ace\"}", wire: "{\"ask\":\"m2kx9a1b3c4d6a\",\"asker\":\"mesh-delivery-builder\",\"at\":\"2026-11-11T10:00:00Z\",\"node\":\"ace\"}", + decode: func(b []byte) (any, error) { var v AskTakenEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-go\\.event\\.ask-taken\\.[^.*> ]+$")}, {name: "a running step", subject: "", given: "{\"id\":\"a1b2\",\"node\":\"ace\",\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"running\",\"tool\":\"build\",\"v\":1}", wire: "{\"id\":\"a1b2\",\"node\":\"ace\",\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"running\",\"tool\":\"build\",\"v\":1}", decode: func(b []byte) (any, error) { var v StepsByNodeIDValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "a paused node", subject: "", given: "{\"at\":\"2026-11-05T10:00:00Z\",\"by\":\"operator@shanks\",\"paused\":true,\"v\":1}", wire: "{\"at\":\"2026-11-05T10:00:00Z\",\"by\":\"operator@shanks\",\"paused\":true,\"v\":1}", @@ -45,8 +49,16 @@ func TestFixtures(t *testing.T) { err := json.Unmarshal(b, &v) return v, err }, subjectIs: nil}, + {name: "a node's toolchain with its program's digest (G 472)", subject: "", given: "{\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"language\":\"go\",\"recipe\":\"1\",\"v\":1,\"version\":\"1.26.0\"}", wire: "{\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"language\":\"go\",\"recipe\":\"1\",\"v\":1,\"version\":\"1.26.0\"}", + decode: func(b []byte) (any, error) { + var v ToolchainsByNodeLanguageValue + err := json.Unmarshal(b, &v) + return v, err + }, 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 build ask with the version the builder reserved (G 573)", subject: "mesh.seat.node-build-go.accept.delivery", given: "{\"asker\":\"mesh-delivery-builder\",\"build\":{\"artifacts\":[{\"artifact\":\"go\"}],\"module\":\"nats\"},\"id\":\"a1b3\",\"kind\":\"build\",\"language\":\"go\",\"version\":\"1.4.2\"}", wire: "{\"asker\":\"mesh-delivery-builder\",\"build\":{\"artifacts\":[{\"artifact\":\"go\"}],\"module\":\"nats\"},\"id\":\"a1b3\",\"kind\":\"build\",\"language\":\"go\",\"version\":\"1.4.2\"}", + 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\"],\"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}", diff --git a/contracts/go/seats/nodebuildimage/contract.go b/contracts/go/seats/nodebuildimage/contract.go index bde198f4..367353bb 100644 --- a/contracts/go/seats/nodebuildimage/contract.go +++ b/contracts/go/seats/nodebuildimage/contract.go @@ -35,6 +35,7 @@ const ( EventStepEnded = "step-ended" EventToolchainReported = "toolchain-reported" EventStepProgress = "step-progress" + EventAskTaken = "ask-taken" ) // Verbs is every verb the seat serves, in its definition's order: a new copy each call. @@ -44,7 +45,7 @@ func Verbs() []string { // Events is every event published on the seat's subject: a new copy each call. func Events() []string { - return []string{EventStepEnded, EventToolchainReported, EventStepProgress} + return []string{EventStepEnded, EventToolchainReported, EventStepProgress, EventAskTaken} } // Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects. @@ -75,9 +76,6 @@ func SubjectToolchainReported(node string) string { // SubjectToolchainReportedAll hears every toolchain-reported. const SubjectToolchainReportedAll = "mesh.seat.node-build-image.event.toolchain-reported.*" -// StreamStepProgress is the stream step-progress is kept in. -const StreamStepProgress = "SEAT_node-build-image_step-progress" - // SubjectStepProgress is where step-progress is published, on mesh.seat.node-build-image.event.step-progress.. func SubjectStepProgress(node string) string { return "mesh.seat.node-build-image.event.step-progress." + node @@ -86,6 +84,12 @@ func SubjectStepProgress(node string) string { // SubjectStepProgressAll hears every step-progress. const SubjectStepProgressAll = "mesh.seat.node-build-image.event.step-progress.*" +// SubjectAskTaken is where ask-taken is published, on mesh.seat.node-build-image.event.ask-taken.. +func SubjectAskTaken(node string) string { return "mesh.seat.node-build-image.event.ask-taken." + node } + +// SubjectAskTakenAll hears every ask-taken. +const SubjectAskTakenAll = "mesh.seat.node-build-image.event.ask-taken.*" + // AcceptSubjectDelivery is where delivery is sent. const AcceptSubjectDelivery = "mesh.seat.node-build-image.accept.delivery" @@ -370,6 +374,16 @@ type AskArtifact struct { Version string `json:"version,omitempty"` } +// AskTaken is an ask a build seat's holder took, said when it takes any ask, whatever its asker (design 47 G section 602). +type AskTaken struct { + // the ask's id + Ask string `json:"ask"` + Asker string `json:"asker"` + At time.Time `json:"at"` + // the node that took it, as in the subject + Node string `json:"node"` +} + // BuildAnswer is a type of the contract. type BuildAnswer struct { Artifacts []BuiltArtifact `json:"artifacts"` @@ -759,23 +773,31 @@ type StepAsk struct { Step *StepRun `json:"step,omitempty"` // the layout version V int64 `json:"v,omitempty"` + // the version MAJOR.MINOR.PATCH the builder reserved for the build, passed to it as MESH_VERSION; empty for a build that reserves none, which stamps none (design 47 G section 573) + Version string `json:"version,omitempty"` } -// StepEnded is a pipeline step's end, said by the build seat's holder on the node that ran it (ADR 0666 sections 7, 21): the runner takes it only from that holder's account, naming the ask, the commit and the fingerprint it was asked at. +// StepEnded is an ask's end, said by the build seat's holder on the node that ran it, whatever its asker (design 47 G section 602): a pipeline step's end names the pipeline, the commit and the fingerprint it was asked at, which the runner takes only from that holder's account (ADR 0666 sections 7, 21); the end of any other ask names its asker, the ask, the node, the outcome and when, and neither commit nor fingerprint (design 47 G section 548). type StepEnded struct { - Artifact string `json:"artifact"` + Artifact string `json:"artifact,omitempty"` + // the ask's id, by which a reader takes only its own asks + Ask string `json:"ask"` + // who asked it + Asker string `json:"asker"` + // when it ended (RFC 3339 time) + At time.Time `json:"at"` // for a rerun, the caller the runtime proved By string `json:"by,omitempty"` // the packages not run because the ask's kept results held them Carried []KeptResult `json:"carried,omitempty"` - Commit string `json:"commit"` + Commit string `json:"commit,omitempty"` Ended *time.Time `json:"ended,omitempty"` - Fingerprint string `json:"fingerprint"` + Fingerprint string `json:"fingerprint,omitempty"` // the ask's id ID string `json:"id"` // the packages that passed at their digest, for the runner to keep Kept []KeptResult `json:"kept,omitempty"` - Module string `json:"module"` + Module string `json:"module,omitempty"` // the node that ran it, as in the subject Node string `json:"node"` // never passed for a step that ran nothing @@ -784,12 +806,12 @@ type StepEnded struct { Output string `json:"output,omitempty"` // the output's object in the sender's sent store, when over the per-message limit (ADR 0666 section 16) OutputObject string `json:"output-object,omitempty"` - Pipeline string `json:"pipeline"` + Pipeline string `json:"pipeline,omitempty"` Rerun bool `json:"rerun,omitempty"` // why it failed or could not run, naming the part not heard Said string `json:"said,omitempty"` Started *time.Time `json:"started,omitempty"` - Step string `json:"step"` + Step string `json:"step,omitempty"` Tests *TestCounts `json:"tests,omitempty"` // the layout version V int64 `json:"v,omitempty"` @@ -910,6 +932,8 @@ type TestCounts struct { type Toolchain struct { // when (RFC 3339 time) At *time.Time `json:"at,omitempty"` + // the sha256: digest of the language's toolchain program as the build module runs it: the program of the module that provides that language's toolchain, never the build module's own (design 47 G section 472) + Digest string `json:"digest,omitempty"` // go or typescript Language string `json:"language"` Recipe string `json:"recipe"` @@ -1062,6 +1086,9 @@ type ToolchainReportedEvent = ToolchainReported // StepProgressEvent is the body of the event step-progress. type StepProgressEvent = StepProgress +// AskTakenEvent is the body of the event ask-taken. +type AskTakenEvent = AskTaken + // StepsByAskerNodeIDValue is one value of the state steps under the key ... type StepsByAskerNodeIDValue = Step diff --git a/contracts/go/seats/nodebuildimage/contract_test.go b/contracts/go/seats/nodebuildimage/contract_test.go index 21300d5e..5ebe76a8 100644 --- a/contracts/go/seats/nodebuildimage/contract_test.go +++ b/contracts/go/seats/nodebuildimage/contract_test.go @@ -17,14 +17,18 @@ func TestFixtures(t *testing.T) { decode func([]byte) (any, error) subjectIs *regexp.Regexp }{ - {name: "a lint step ended passed on its node", subject: "mesh.seat.node-build-image.event.step-ended.ace", given: "{\"artifact\":\"nats:program\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"ended\":\"2026-11-10T10:02:00Z\",\"fingerprint\":\"fp-0123\",\"id\":\"m2kx9a1b3c4d5e\",\"module\":\"nats\",\"node\":\"ace\",\"outcome\":\"passed\",\"pipeline\":\"main.12.nats\",\"started\":\"2026-11-10T10:00:00Z\",\"step\":\"lint\",\"v\":1}", wire: "{\"artifact\":\"nats:program\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"ended\":\"2026-11-10T10:02:00Z\",\"fingerprint\":\"fp-0123\",\"id\":\"m2kx9a1b3c4d5e\",\"module\":\"nats\",\"node\":\"ace\",\"outcome\":\"passed\",\"pipeline\":\"main.12.nats\",\"started\":\"2026-11-10T10:00:00Z\",\"step\":\"lint\",\"v\":1}", + {name: "a lint step ended passed on its node", subject: "mesh.seat.node-build-image.event.step-ended.ace", given: "{\"artifact\":\"nats:program\",\"ask\":\"m2kx9a1b3c4d5e\",\"asker\":\"mesh-delivery-pipeline\",\"at\":\"2026-11-10T10:02:00Z\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"ended\":\"2026-11-10T10:02:00Z\",\"fingerprint\":\"fp-0123\",\"id\":\"m2kx9a1b3c4d5e\",\"module\":\"nats\",\"node\":\"ace\",\"outcome\":\"passed\",\"pipeline\":\"main.12.nats\",\"started\":\"2026-11-10T10:00:00Z\",\"step\":\"lint\",\"v\":1}", wire: "{\"artifact\":\"nats:program\",\"ask\":\"m2kx9a1b3c4d5e\",\"asker\":\"mesh-delivery-pipeline\",\"at\":\"2026-11-10T10:02:00Z\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"ended\":\"2026-11-10T10:02:00Z\",\"fingerprint\":\"fp-0123\",\"id\":\"m2kx9a1b3c4d5e\",\"module\":\"nats\",\"node\":\"ace\",\"outcome\":\"passed\",\"pipeline\":\"main.12.nats\",\"started\":\"2026-11-10T10:00:00Z\",\"step\":\"lint\",\"v\":1}", decode: func(b []byte) (any, error) { var v StepEndedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-image\\.event\\.step-ended\\.[^.*> ]+$")}, - {name: "a lint step that could not run, naming the part not heard", subject: "mesh.seat.node-build-image.event.step-ended.g14", given: "{\"artifact\":\"nats:program\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"fingerprint\":\"fp-0123\",\"id\":\"m2kx9a1b3c4d5f\",\"module\":\"nats\",\"node\":\"g14\",\"outcome\":\"could not run\",\"output-object\":\"mesh-build-go_sent-objects/m2kx9a1b3c4d5f\",\"pipeline\":\"main.12.nats\",\"said\":\"the sandbox could not fetch novox/mesh-sdk\",\"step\":\"lint\",\"v\":1}", wire: "{\"artifact\":\"nats:program\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"fingerprint\":\"fp-0123\",\"id\":\"m2kx9a1b3c4d5f\",\"module\":\"nats\",\"node\":\"g14\",\"outcome\":\"could not run\",\"output-object\":\"mesh-build-go_sent-objects/m2kx9a1b3c4d5f\",\"pipeline\":\"main.12.nats\",\"said\":\"the sandbox could not fetch novox/mesh-sdk\",\"step\":\"lint\",\"v\":1}", + {name: "a lint step that could not run, naming the part not heard", subject: "mesh.seat.node-build-image.event.step-ended.g14", given: "{\"artifact\":\"nats:program\",\"ask\":\"m2kx9a1b3c4d5f\",\"asker\":\"mesh-delivery-pipeline\",\"at\":\"2026-11-10T10:02:00Z\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"fingerprint\":\"fp-0123\",\"id\":\"m2kx9a1b3c4d5f\",\"module\":\"nats\",\"node\":\"g14\",\"outcome\":\"could not run\",\"output-object\":\"mesh-build-go_sent-objects/m2kx9a1b3c4d5f\",\"pipeline\":\"main.12.nats\",\"said\":\"the sandbox could not fetch novox/mesh-sdk\",\"step\":\"lint\",\"v\":1}", wire: "{\"artifact\":\"nats:program\",\"ask\":\"m2kx9a1b3c4d5f\",\"asker\":\"mesh-delivery-pipeline\",\"at\":\"2026-11-10T10:02:00Z\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"fingerprint\":\"fp-0123\",\"id\":\"m2kx9a1b3c4d5f\",\"module\":\"nats\",\"node\":\"g14\",\"outcome\":\"could not run\",\"output-object\":\"mesh-build-go_sent-objects/m2kx9a1b3c4d5f\",\"pipeline\":\"main.12.nats\",\"said\":\"the sandbox could not fetch novox/mesh-sdk\",\"step\":\"lint\",\"v\":1}", decode: func(b []byte) (any, error) { var v StepEndedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-image\\.event\\.step-ended\\.[^.*> ]+$")}, {name: "a node's toolchain reported", subject: "mesh.seat.node-build-image.event.toolchain-reported.ace", given: "{\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"language\":\"go\",\"node\":\"ace\",\"recipe\":\"1\",\"version\":\"1.26.0\"}", wire: "{\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"language\":\"go\",\"node\":\"ace\",\"recipe\":\"1\",\"version\":\"1.26.0\"}", decode: func(b []byte) (any, error) { var v ToolchainReportedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-image\\.event\\.toolchain-reported\\.[^.*> ]+$")}, {name: "a running ask says its progress", subject: "mesh.seat.node-build-image.event.step-progress.ace", given: "{\"ask\":\"a1b2\",\"asker\":\"runner\",\"at\":\"2026-11-11T10:01:00Z\",\"phase\":\"fetch\"}", wire: "{\"ask\":\"a1b2\",\"asker\":\"runner\",\"at\":\"2026-11-11T10:01:00Z\",\"phase\":\"fetch\"}", decode: func(b []byte) (any, error) { var v StepProgressEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-image\\.event\\.step-progress\\.[^.*> ]+$")}, + {name: "a builder's build ended, naming its asker and no commit or fingerprint (G 548)", subject: "mesh.seat.node-build-image.event.step-ended.ace", given: "{\"ask\":\"m2kx9a1b3c4d6a\",\"asker\":\"mesh-delivery-builder\",\"at\":\"2026-11-11T10:03:00Z\",\"id\":\"m2kx9a1b3c4d6a\",\"node\":\"ace\",\"outcome\":\"failed\",\"said\":\"the build failed\",\"v\":1}", wire: "{\"ask\":\"m2kx9a1b3c4d6a\",\"asker\":\"mesh-delivery-builder\",\"at\":\"2026-11-11T10:03:00Z\",\"id\":\"m2kx9a1b3c4d6a\",\"node\":\"ace\",\"outcome\":\"failed\",\"said\":\"the build failed\",\"v\":1}", + decode: func(b []byte) (any, error) { var v StepEndedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-image\\.event\\.step-ended\\.[^.*> ]+$")}, + {name: "an ask taken, whatever its asker (G 602)", subject: "mesh.seat.node-build-image.event.ask-taken.ace", given: "{\"ask\":\"m2kx9a1b3c4d6a\",\"asker\":\"mesh-delivery-builder\",\"at\":\"2026-11-11T10:00:00Z\",\"node\":\"ace\"}", wire: "{\"ask\":\"m2kx9a1b3c4d6a\",\"asker\":\"mesh-delivery-builder\",\"at\":\"2026-11-11T10:00:00Z\",\"node\":\"ace\"}", + decode: func(b []byte) (any, error) { var v AskTakenEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-image\\.event\\.ask-taken\\.[^.*> ]+$")}, {name: "a running step", subject: "", given: "{\"id\":\"a1b2\",\"node\":\"ace\",\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"running\",\"tool\":\"build\",\"v\":1}", wire: "{\"id\":\"a1b2\",\"node\":\"ace\",\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"running\",\"tool\":\"build\",\"v\":1}", decode: func(b []byte) (any, error) { var v StepsByNodeIDValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "a paused node", subject: "", given: "{\"at\":\"2026-11-05T10:00:00Z\",\"by\":\"operator@shanks\",\"paused\":true,\"v\":1}", wire: "{\"at\":\"2026-11-05T10:00:00Z\",\"by\":\"operator@shanks\",\"paused\":true,\"v\":1}", @@ -45,8 +49,16 @@ func TestFixtures(t *testing.T) { err := json.Unmarshal(b, &v) return v, err }, subjectIs: nil}, + {name: "a node's toolchain with its program's digest (G 472)", subject: "", given: "{\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"language\":\"go\",\"recipe\":\"1\",\"v\":1,\"version\":\"1.26.0\"}", wire: "{\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"language\":\"go\",\"recipe\":\"1\",\"v\":1,\"version\":\"1.26.0\"}", + decode: func(b []byte) (any, error) { + var v ToolchainsByNodeLanguageValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, {name: "a build ask", subject: "mesh.seat.node-build-image.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-image\\.accept\\.delivery$")}, + {name: "a build ask with the version the builder reserved (G 573)", subject: "mesh.seat.node-build-image.accept.delivery", given: "{\"asker\":\"mesh-delivery-builder\",\"build\":{\"artifacts\":[{\"artifact\":\"go\"}],\"module\":\"nats\"},\"id\":\"a1b3\",\"kind\":\"build\",\"language\":\"go\",\"version\":\"1.4.2\"}", wire: "{\"asker\":\"mesh-delivery-builder\",\"build\":{\"artifacts\":[{\"artifact\":\"go\"}],\"module\":\"nats\"},\"id\":\"a1b3\",\"kind\":\"build\",\"language\":\"go\",\"version\":\"1.4.2\"}", + decode: func(b []byte) (any, error) { var v DeliveryAccept; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-image\\.accept\\.delivery$")}, {name: "a step ended on a transport fault, given again by the bus", subject: "", given: "{\"again-at\":\"2026-11-05T10:01:30Z\",\"ended\":\"2026-11-05T10:00:30Z\",\"id\":\"a1b2\",\"node\":\"ace\",\"passing\":true,\"reason\":\"fetching the commit: kex_exchange_identification: read: Connection reset by peer\",\"run\":1,\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"again\",\"tool\":\"build\",\"v\":1}", wire: "{\"again-at\":\"2026-11-05T10:01:30Z\",\"ended\":\"2026-11-05T10:00:30Z\",\"id\":\"a1b2\",\"node\":\"ace\",\"passing\":true,\"reason\":\"fetching the commit: kex_exchange_identification: read: Connection reset by peer\",\"run\":1,\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"again\",\"tool\":\"build\",\"v\":1}", decode: func(b []byte) (any, error) { var v StepsByNodeIDValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "an ask dropped by the caller that no longer waits", subject: "", given: "{\"at\":\"2026-11-05T10:00:00Z\",\"by\":\"mesh-delivery-builder\",\"v\":1,\"why\":\"a newer fingerprint supersedes it\"}", wire: "{\"at\":\"2026-11-05T10:00:00Z\",\"by\":\"mesh-delivery-builder\",\"v\":1,\"why\":\"a newer fingerprint supersedes it\"}", diff --git a/contracts/go/seats/nodebuildnode/contract.go b/contracts/go/seats/nodebuildnode/contract.go index edd7230d..6014fe4a 100644 --- a/contracts/go/seats/nodebuildnode/contract.go +++ b/contracts/go/seats/nodebuildnode/contract.go @@ -35,6 +35,7 @@ const ( EventStepEnded = "step-ended" EventToolchainReported = "toolchain-reported" EventStepProgress = "step-progress" + EventAskTaken = "ask-taken" ) // Verbs is every verb the seat serves, in its definition's order: a new copy each call. @@ -44,7 +45,7 @@ func Verbs() []string { // Events is every event published on the seat's subject: a new copy each call. func Events() []string { - return []string{EventStepEnded, EventToolchainReported, EventStepProgress} + return []string{EventStepEnded, EventToolchainReported, EventStepProgress, EventAskTaken} } // Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects. @@ -75,9 +76,6 @@ func SubjectToolchainReported(node string) string { // SubjectToolchainReportedAll hears every toolchain-reported. const SubjectToolchainReportedAll = "mesh.seat.node-build-node.event.toolchain-reported.*" -// StreamStepProgress is the stream step-progress is kept in. -const StreamStepProgress = "SEAT_node-build-node_step-progress" - // SubjectStepProgress is where step-progress is published, on mesh.seat.node-build-node.event.step-progress.. func SubjectStepProgress(node string) string { return "mesh.seat.node-build-node.event.step-progress." + node @@ -86,6 +84,12 @@ func SubjectStepProgress(node string) string { // SubjectStepProgressAll hears every step-progress. const SubjectStepProgressAll = "mesh.seat.node-build-node.event.step-progress.*" +// SubjectAskTaken is where ask-taken is published, on mesh.seat.node-build-node.event.ask-taken.. +func SubjectAskTaken(node string) string { return "mesh.seat.node-build-node.event.ask-taken." + node } + +// SubjectAskTakenAll hears every ask-taken. +const SubjectAskTakenAll = "mesh.seat.node-build-node.event.ask-taken.*" + // AcceptSubjectDelivery is where delivery is sent. const AcceptSubjectDelivery = "mesh.seat.node-build-node.accept.delivery" @@ -370,6 +374,16 @@ type AskArtifact struct { Version string `json:"version,omitempty"` } +// AskTaken is an ask a build seat's holder took, said when it takes any ask, whatever its asker (design 47 G section 602). +type AskTaken struct { + // the ask's id + Ask string `json:"ask"` + Asker string `json:"asker"` + At time.Time `json:"at"` + // the node that took it, as in the subject + Node string `json:"node"` +} + // BuildAnswer is a type of the contract. type BuildAnswer struct { Artifacts []BuiltArtifact `json:"artifacts"` @@ -759,23 +773,31 @@ type StepAsk struct { Step *StepRun `json:"step,omitempty"` // the layout version V int64 `json:"v,omitempty"` + // the version MAJOR.MINOR.PATCH the builder reserved for the build, passed to it as MESH_VERSION; empty for a build that reserves none, which stamps none (design 47 G section 573) + Version string `json:"version,omitempty"` } -// StepEnded is a pipeline step's end, said by the build seat's holder on the node that ran it (ADR 0666 sections 7, 21): the runner takes it only from that holder's account, naming the ask, the commit and the fingerprint it was asked at. +// StepEnded is an ask's end, said by the build seat's holder on the node that ran it, whatever its asker (design 47 G section 602): a pipeline step's end names the pipeline, the commit and the fingerprint it was asked at, which the runner takes only from that holder's account (ADR 0666 sections 7, 21); the end of any other ask names its asker, the ask, the node, the outcome and when, and neither commit nor fingerprint (design 47 G section 548). type StepEnded struct { - Artifact string `json:"artifact"` + Artifact string `json:"artifact,omitempty"` + // the ask's id, by which a reader takes only its own asks + Ask string `json:"ask"` + // who asked it + Asker string `json:"asker"` + // when it ended (RFC 3339 time) + At time.Time `json:"at"` // for a rerun, the caller the runtime proved By string `json:"by,omitempty"` // the packages not run because the ask's kept results held them Carried []KeptResult `json:"carried,omitempty"` - Commit string `json:"commit"` + Commit string `json:"commit,omitempty"` Ended *time.Time `json:"ended,omitempty"` - Fingerprint string `json:"fingerprint"` + Fingerprint string `json:"fingerprint,omitempty"` // the ask's id ID string `json:"id"` // the packages that passed at their digest, for the runner to keep Kept []KeptResult `json:"kept,omitempty"` - Module string `json:"module"` + Module string `json:"module,omitempty"` // the node that ran it, as in the subject Node string `json:"node"` // never passed for a step that ran nothing @@ -784,12 +806,12 @@ type StepEnded struct { Output string `json:"output,omitempty"` // the output's object in the sender's sent store, when over the per-message limit (ADR 0666 section 16) OutputObject string `json:"output-object,omitempty"` - Pipeline string `json:"pipeline"` + Pipeline string `json:"pipeline,omitempty"` Rerun bool `json:"rerun,omitempty"` // why it failed or could not run, naming the part not heard Said string `json:"said,omitempty"` Started *time.Time `json:"started,omitempty"` - Step string `json:"step"` + Step string `json:"step,omitempty"` Tests *TestCounts `json:"tests,omitempty"` // the layout version V int64 `json:"v,omitempty"` @@ -910,6 +932,8 @@ type TestCounts struct { type Toolchain struct { // when (RFC 3339 time) At *time.Time `json:"at,omitempty"` + // the sha256: digest of the language's toolchain program as the build module runs it: the program of the module that provides that language's toolchain, never the build module's own (design 47 G section 472) + Digest string `json:"digest,omitempty"` // go or typescript Language string `json:"language"` Recipe string `json:"recipe"` @@ -1062,6 +1086,9 @@ type ToolchainReportedEvent = ToolchainReported // StepProgressEvent is the body of the event step-progress. type StepProgressEvent = StepProgress +// AskTakenEvent is the body of the event ask-taken. +type AskTakenEvent = AskTaken + // StepsByAskerNodeIDValue is one value of the state steps under the key ... type StepsByAskerNodeIDValue = Step diff --git a/contracts/go/seats/nodebuildnode/contract_test.go b/contracts/go/seats/nodebuildnode/contract_test.go index 3eb5ebe0..78aff7ee 100644 --- a/contracts/go/seats/nodebuildnode/contract_test.go +++ b/contracts/go/seats/nodebuildnode/contract_test.go @@ -17,14 +17,18 @@ func TestFixtures(t *testing.T) { decode func([]byte) (any, error) subjectIs *regexp.Regexp }{ - {name: "a unit step ended passed on its node, with its counts and what it carried", subject: "mesh.seat.node-build-node.event.step-ended.ace", given: "{\"artifact\":\"nats:program\",\"carried\":[{\"digest\":\"sha256:bbbb\",\"level\":\"unit\",\"package\":\"./modules/nats/a\"}],\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"ended\":\"2026-11-10T10:02:00Z\",\"fingerprint\":\"fp-0123\",\"id\":\"m2kx9a1b3c4d5e\",\"kept\":[{\"digest\":\"sha256:cccc\",\"level\":\"unit\",\"package\":\"./modules/nats/b\"}],\"module\":\"nats\",\"node\":\"ace\",\"outcome\":\"passed\",\"pipeline\":\"main.12.nats\",\"started\":\"2026-11-10T10:00:00Z\",\"step\":\"unit\",\"tests\":{\"unit\":14},\"v\":1}", wire: "{\"artifact\":\"nats:program\",\"carried\":[{\"digest\":\"sha256:bbbb\",\"level\":\"unit\",\"package\":\"./modules/nats/a\"}],\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"ended\":\"2026-11-10T10:02:00Z\",\"fingerprint\":\"fp-0123\",\"id\":\"m2kx9a1b3c4d5e\",\"kept\":[{\"digest\":\"sha256:cccc\",\"level\":\"unit\",\"package\":\"./modules/nats/b\"}],\"module\":\"nats\",\"node\":\"ace\",\"outcome\":\"passed\",\"pipeline\":\"main.12.nats\",\"started\":\"2026-11-10T10:00:00Z\",\"step\":\"unit\",\"tests\":{\"unit\":14},\"v\":1}", + {name: "a unit step ended passed on its node, with its counts and what it carried", subject: "mesh.seat.node-build-node.event.step-ended.ace", given: "{\"artifact\":\"nats:program\",\"ask\":\"m2kx9a1b3c4d5e\",\"asker\":\"mesh-delivery-pipeline\",\"at\":\"2026-11-10T10:02:00Z\",\"carried\":[{\"digest\":\"sha256:bbbb\",\"level\":\"unit\",\"package\":\"./modules/nats/a\"}],\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"ended\":\"2026-11-10T10:02:00Z\",\"fingerprint\":\"fp-0123\",\"id\":\"m2kx9a1b3c4d5e\",\"kept\":[{\"digest\":\"sha256:cccc\",\"level\":\"unit\",\"package\":\"./modules/nats/b\"}],\"module\":\"nats\",\"node\":\"ace\",\"outcome\":\"passed\",\"pipeline\":\"main.12.nats\",\"started\":\"2026-11-10T10:00:00Z\",\"step\":\"unit\",\"tests\":{\"unit\":14},\"v\":1}", wire: "{\"artifact\":\"nats:program\",\"ask\":\"m2kx9a1b3c4d5e\",\"asker\":\"mesh-delivery-pipeline\",\"at\":\"2026-11-10T10:02:00Z\",\"carried\":[{\"digest\":\"sha256:bbbb\",\"level\":\"unit\",\"package\":\"./modules/nats/a\"}],\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"ended\":\"2026-11-10T10:02:00Z\",\"fingerprint\":\"fp-0123\",\"id\":\"m2kx9a1b3c4d5e\",\"kept\":[{\"digest\":\"sha256:cccc\",\"level\":\"unit\",\"package\":\"./modules/nats/b\"}],\"module\":\"nats\",\"node\":\"ace\",\"outcome\":\"passed\",\"pipeline\":\"main.12.nats\",\"started\":\"2026-11-10T10:00:00Z\",\"step\":\"unit\",\"tests\":{\"unit\":14},\"v\":1}", decode: func(b []byte) (any, error) { var v StepEndedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-node\\.event\\.step-ended\\.[^.*> ]+$")}, - {name: "an integration step that could not run, naming the part not heard", subject: "mesh.seat.node-build-node.event.step-ended.g14", given: "{\"artifact\":\"nats:program\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"fingerprint\":\"fp-0123\",\"id\":\"m2kx9a1b3c4d5f\",\"module\":\"nats\",\"node\":\"g14\",\"outcome\":\"could not run\",\"output-object\":\"mesh-build-go_sent-objects/m2kx9a1b3c4d5f\",\"pipeline\":\"main.12.nats\",\"said\":\"the sandbox could not fetch novox/mesh-sdk\",\"step\":\"integration\",\"v\":1}", wire: "{\"artifact\":\"nats:program\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"fingerprint\":\"fp-0123\",\"id\":\"m2kx9a1b3c4d5f\",\"module\":\"nats\",\"node\":\"g14\",\"outcome\":\"could not run\",\"output-object\":\"mesh-build-go_sent-objects/m2kx9a1b3c4d5f\",\"pipeline\":\"main.12.nats\",\"said\":\"the sandbox could not fetch novox/mesh-sdk\",\"step\":\"integration\",\"v\":1}", + {name: "an integration step that could not run, naming the part not heard", subject: "mesh.seat.node-build-node.event.step-ended.g14", given: "{\"artifact\":\"nats:program\",\"ask\":\"m2kx9a1b3c4d5f\",\"asker\":\"mesh-delivery-pipeline\",\"at\":\"2026-11-10T10:02:00Z\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"fingerprint\":\"fp-0123\",\"id\":\"m2kx9a1b3c4d5f\",\"module\":\"nats\",\"node\":\"g14\",\"outcome\":\"could not run\",\"output-object\":\"mesh-build-go_sent-objects/m2kx9a1b3c4d5f\",\"pipeline\":\"main.12.nats\",\"said\":\"the sandbox could not fetch novox/mesh-sdk\",\"step\":\"integration\",\"v\":1}", wire: "{\"artifact\":\"nats:program\",\"ask\":\"m2kx9a1b3c4d5f\",\"asker\":\"mesh-delivery-pipeline\",\"at\":\"2026-11-10T10:02:00Z\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"fingerprint\":\"fp-0123\",\"id\":\"m2kx9a1b3c4d5f\",\"module\":\"nats\",\"node\":\"g14\",\"outcome\":\"could not run\",\"output-object\":\"mesh-build-go_sent-objects/m2kx9a1b3c4d5f\",\"pipeline\":\"main.12.nats\",\"said\":\"the sandbox could not fetch novox/mesh-sdk\",\"step\":\"integration\",\"v\":1}", decode: func(b []byte) (any, error) { var v StepEndedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-node\\.event\\.step-ended\\.[^.*> ]+$")}, {name: "a node's toolchain reported", subject: "mesh.seat.node-build-node.event.toolchain-reported.ace", given: "{\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"language\":\"go\",\"node\":\"ace\",\"recipe\":\"1\",\"version\":\"1.26.0\"}", wire: "{\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"language\":\"go\",\"node\":\"ace\",\"recipe\":\"1\",\"version\":\"1.26.0\"}", decode: func(b []byte) (any, error) { var v ToolchainReportedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-node\\.event\\.toolchain-reported\\.[^.*> ]+$")}, {name: "a running ask says its progress", subject: "mesh.seat.node-build-node.event.step-progress.ace", given: "{\"ask\":\"a1b2\",\"asker\":\"runner\",\"at\":\"2026-11-11T10:01:00Z\",\"phase\":\"fetch\"}", wire: "{\"ask\":\"a1b2\",\"asker\":\"runner\",\"at\":\"2026-11-11T10:01:00Z\",\"phase\":\"fetch\"}", decode: func(b []byte) (any, error) { var v StepProgressEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-node\\.event\\.step-progress\\.[^.*> ]+$")}, + {name: "a builder's build ended, naming its asker and no commit or fingerprint (G 548)", subject: "mesh.seat.node-build-node.event.step-ended.ace", given: "{\"ask\":\"m2kx9a1b3c4d6a\",\"asker\":\"mesh-delivery-builder\",\"at\":\"2026-11-11T10:03:00Z\",\"id\":\"m2kx9a1b3c4d6a\",\"node\":\"ace\",\"outcome\":\"failed\",\"said\":\"the build failed\",\"v\":1}", wire: "{\"ask\":\"m2kx9a1b3c4d6a\",\"asker\":\"mesh-delivery-builder\",\"at\":\"2026-11-11T10:03:00Z\",\"id\":\"m2kx9a1b3c4d6a\",\"node\":\"ace\",\"outcome\":\"failed\",\"said\":\"the build failed\",\"v\":1}", + decode: func(b []byte) (any, error) { var v StepEndedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-node\\.event\\.step-ended\\.[^.*> ]+$")}, + {name: "an ask taken, whatever its asker (G 602)", subject: "mesh.seat.node-build-node.event.ask-taken.ace", given: "{\"ask\":\"m2kx9a1b3c4d6a\",\"asker\":\"mesh-delivery-builder\",\"at\":\"2026-11-11T10:00:00Z\",\"node\":\"ace\"}", wire: "{\"ask\":\"m2kx9a1b3c4d6a\",\"asker\":\"mesh-delivery-builder\",\"at\":\"2026-11-11T10:00:00Z\",\"node\":\"ace\"}", + decode: func(b []byte) (any, error) { var v AskTakenEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-node\\.event\\.ask-taken\\.[^.*> ]+$")}, {name: "a running step", subject: "", given: "{\"id\":\"a1b2\",\"node\":\"ace\",\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"running\",\"tool\":\"build\",\"v\":1}", wire: "{\"id\":\"a1b2\",\"node\":\"ace\",\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"running\",\"tool\":\"build\",\"v\":1}", decode: func(b []byte) (any, error) { var v StepsByNodeIDValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "a paused node", subject: "", given: "{\"at\":\"2026-11-05T10:00:00Z\",\"by\":\"operator@shanks\",\"paused\":true,\"v\":1}", wire: "{\"at\":\"2026-11-05T10:00:00Z\",\"by\":\"operator@shanks\",\"paused\":true,\"v\":1}", @@ -45,8 +49,16 @@ func TestFixtures(t *testing.T) { err := json.Unmarshal(b, &v) return v, err }, subjectIs: nil}, + {name: "a node's toolchain with its program's digest (G 472)", subject: "", given: "{\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"language\":\"go\",\"recipe\":\"1\",\"v\":1,\"version\":\"1.26.0\"}", wire: "{\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"language\":\"go\",\"recipe\":\"1\",\"v\":1,\"version\":\"1.26.0\"}", + decode: func(b []byte) (any, error) { + var v ToolchainsByNodeLanguageValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, {name: "a build ask", subject: "mesh.seat.node-build-node.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-node\\.accept\\.delivery$")}, + {name: "a build ask with the version the builder reserved (G 573)", subject: "mesh.seat.node-build-node.accept.delivery", given: "{\"asker\":\"mesh-delivery-builder\",\"build\":{\"artifacts\":[{\"artifact\":\"go\"}],\"module\":\"nats\"},\"id\":\"a1b3\",\"kind\":\"build\",\"language\":\"go\",\"version\":\"1.4.2\"}", wire: "{\"asker\":\"mesh-delivery-builder\",\"build\":{\"artifacts\":[{\"artifact\":\"go\"}],\"module\":\"nats\"},\"id\":\"a1b3\",\"kind\":\"build\",\"language\":\"go\",\"version\":\"1.4.2\"}", + decode: func(b []byte) (any, error) { var v DeliveryAccept; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-node\\.accept\\.delivery$")}, {name: "a check ask with the group's tagged package", subject: "mesh.seat.node-build-node.accept.check", given: "{\"asker\":\"mesh-delivery-checker\",\"at\":\"2026-11-05T10:00:00Z\",\"check\":{\"fetch\":{\"base\":\"89abcdef0123456789abcdef0123456789abcdef\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"package\":{\"name\":\"@novox/mesh-sdk\",\"version\":\"0.1.15-feat-x.1\"},\"ref\":\"refs/pull/7/head\",\"repository\":\"novox/mesh-delivery\"},\"kept\":true,\"levels\":[\"unit\",\"integration\"],\"node-names\":[\"ace\",\"g14\"],\"toolchain\":{\"language\":\"typescript\",\"recipe\":\"1\",\"version\":\"26.12.0\"}},\"id\":\"c3d5\",\"kind\":\"check\",\"language\":\"typescript\",\"v\":1}", wire: "{\"asker\":\"mesh-delivery-checker\",\"at\":\"2026-11-05T10:00:00Z\",\"check\":{\"fetch\":{\"base\":\"89abcdef0123456789abcdef0123456789abcdef\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"package\":{\"name\":\"@novox/mesh-sdk\",\"version\":\"0.1.15-feat-x.1\"},\"ref\":\"refs/pull/7/head\",\"repository\":\"novox/mesh-delivery\"},\"kept\":true,\"levels\":[\"unit\",\"integration\"],\"node-names\":[\"ace\",\"g14\"],\"toolchain\":{\"language\":\"typescript\",\"recipe\":\"1\",\"version\":\"26.12.0\"}},\"id\":\"c3d5\",\"kind\":\"check\",\"language\":\"typescript\",\"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-node\\.accept\\.check$")}, {name: "a finished check step of a TypeScript merge preview", subject: "", given: "{\"ended\":\"2026-11-05T10:01:00Z\",\"id\":\"c3d6\",\"node\":\"ace\",\"result\":{\"fetch\":{\"base\":\"abcdef0123456789abcdef0123456789abcdef01\",\"changed\":[\"modules/m/package.json\"],\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"merge\":\"0123456789abcdef0123456789abcdef01234567\",\"modules\":[\"modules/m\"],\"trees\":{\"modules/m\":{\"manifest\":\"0581b699d9d05517e69b21f5cedbeb2eee8aba2b205280b561be731594d7feef\",\"tree\":\"0123456789abcdef0123456789abcdef01234567\"}},\"workspace\":\"mvii1sxt979f67\"}},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"passed\",\"tool\":\"check\",\"v\":1}", wire: "{\"ended\":\"2026-11-05T10:01:00Z\",\"id\":\"c3d6\",\"node\":\"ace\",\"result\":{\"fetch\":{\"base\":\"abcdef0123456789abcdef0123456789abcdef01\",\"changed\":[\"modules/m/package.json\"],\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"merge\":\"0123456789abcdef0123456789abcdef01234567\",\"modules\":[\"modules/m\"],\"trees\":{\"modules/m\":{\"manifest\":\"0581b699d9d05517e69b21f5cedbeb2eee8aba2b205280b561be731594d7feef\",\"tree\":\"0123456789abcdef0123456789abcdef01234567\"}},\"workspace\":\"mvii1sxt979f67\"}},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"passed\",\"tool\":\"check\",\"v\":1}", diff --git a/contracts/parts/build-step.json b/contracts/parts/build-step.json index 6b522354..8c0b00b6 100644 --- a/contracts/parts/build-step.json +++ b/contracts/parts/build-step.json @@ -44,7 +44,8 @@ "description": "End an ask this node holds for a caller that no longer waits for it: a step not yet running is ended dropped when the bus gives it, one running is cancelled; both end the step dropped, naming who dropped it and why. Writes dropped. in the pauses state; the build module removes the key when it has acted.", "callers": [ "seat:mesh-pipeline-runner", - "seat:mesh-builds" + "seat:mesh-builds", + "seat:mesh-delivery-checker" ], "input": { "type": "object", @@ -261,6 +262,10 @@ "description": "when (RFC 3339 time)", "format": "date-time" }, + "version": { + "type": "string", + "description": "the version MAJOR.MINOR.PATCH the builder reserved for the build, passed to it as MESH_VERSION; empty for a build that reserves none, which stamps none (design 47 G section 573)" + }, "build": { "type": "object", "properties": { @@ -682,6 +687,10 @@ "type": "string", "description": "when (RFC 3339 time)", "format": "date-time" + }, + "digest": { + "type": "string", + "description": "the sha256: digest of the language's toolchain program as the build module runs it: the program of the module that provides that language's toolchain, never the build module's own (design 47 G section 472)" } }, "required": [ @@ -1508,7 +1517,7 @@ }, "StepEnded": { "type": "object", - "description": "a pipeline step's end, said by the build seat's holder on the node that ran it (ADR 0666 sections 7, 21): the runner takes it only from that holder's account, naming the ask, the commit and the fingerprint it was asked at", + "description": "an ask's end, said by the build seat's holder on the node that ran it, whatever its asker (design 47 G section 602): a pipeline step's end names the pipeline, the commit and the fingerprint it was asked at, which the runner takes only from that holder's account (ADR 0666 sections 7, 21); the end of any other ask names its asker, the ask, the node, the outcome and when, and neither commit nor fingerprint (design 47 G section 548)", "properties": { "v": { "type": "integer", @@ -1518,6 +1527,14 @@ "type": "string", "description": "the ask's id" }, + "asker": { + "type": "string", + "description": "who asked it" + }, + "ask": { + "type": "string", + "description": "the ask's id, by which a reader takes only its own asks" + }, "pipeline": { "type": "string" }, @@ -1598,18 +1615,20 @@ "ended": { "type": "string", "format": "date-time" + }, + "at": { + "type": "string", + "description": "when it ended (RFC 3339 time)", + "format": "date-time" } }, "required": [ "id", - "pipeline", - "step", - "module", - "artifact", - "commit", - "fingerprint", + "asker", + "ask", "node", - "outcome" + "outcome", + "at" ] }, "DetectedStep": { @@ -1721,6 +1740,33 @@ "phase", "at" ] + }, + "AskTaken": { + "type": "object", + "description": "an ask a build seat's holder took, said when it takes any ask, whatever its asker (design 47 G section 602)", + "properties": { + "asker": { + "type": "string" + }, + "ask": { + "type": "string", + "description": "the ask's id" + }, + "node": { + "type": "string", + "description": "the node that took it, as in the subject" + }, + "at": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "asker", + "ask", + "node", + "at" + ] } }, "events": [ @@ -1751,10 +1797,13 @@ "subject": "step-progress.", "body": { "$ref": "#/defs/StepProgress" - }, - "keep": { - "max-age-days": 1, - "max-mib": 16 + } + }, + { + "name": "ask-taken", + "subject": "ask-taken.", + "body": { + "$ref": "#/defs/AskTaken" } } ] diff --git a/contracts/seats/mesh-wants/seat.json b/contracts/seats/mesh-wants/seat.json index 6e41ad9f..b2ce7f21 100644 --- a/contracts/seats/mesh-wants/seat.json +++ b/contracts/seats/mesh-wants/seat.json @@ -431,10 +431,6 @@ { "name": "step-progress", "subject": "step-progress.", - "keep": { - "max-age-days": 1, - "max-mib": 16 - }, "body": { "type": "object", "description": "a composition step's progress, said once a minute while the assigner runs it (design 47 G section 496)", diff --git a/contracts/seats/node-build-go/fixtures/events.json b/contracts/seats/node-build-go/fixtures/events.json index 1d3351e3..61773fa3 100644 --- a/contracts/seats/node-build-go/fixtures/events.json +++ b/contracts/seats/node-build-go/fixtures/events.json @@ -6,6 +6,8 @@ "given": { "v": 1, "id": "m2kx9a1b3c4d5e", + "asker": "mesh-delivery-pipeline", + "ask": "m2kx9a1b3c4d5e", "pipeline": "main.12.nats", "step": "unit", "module": "nats", @@ -32,7 +34,8 @@ } ], "started": "2026-11-10T10:00:00Z", - "ended": "2026-11-10T10:02:00Z" + "ended": "2026-11-10T10:02:00Z", + "at": "2026-11-10T10:02:00Z" } }, { @@ -42,6 +45,8 @@ "given": { "v": 1, "id": "m2kx9a1b3c4d5f", + "asker": "mesh-delivery-pipeline", + "ask": "m2kx9a1b3c4d5f", "pipeline": "main.12.nats", "step": "integration", "module": "nats", @@ -51,7 +56,8 @@ "node": "g14", "outcome": "could not run", "said": "the sandbox could not fetch novox/mesh-sdk", - "output-object": "mesh-build-go_sent-objects/m2kx9a1b3c4d5f" + "output-object": "mesh-build-go_sent-objects/m2kx9a1b3c4d5f", + "at": "2026-11-10T10:02:00Z" } }, { @@ -76,5 +82,31 @@ "phase": "fetch", "at": "2026-11-11T10:01:00Z" } + }, + { + "name": "a builder's build ended, naming its asker and no commit or fingerprint (G 548)", + "of": "event:step-ended", + "subject": "mesh.seat.node-build-go.event.step-ended.ace", + "given": { + "v": 1, + "id": "m2kx9a1b3c4d6a", + "asker": "mesh-delivery-builder", + "ask": "m2kx9a1b3c4d6a", + "node": "ace", + "outcome": "failed", + "said": "the build failed", + "at": "2026-11-11T10:03:00Z" + } + }, + { + "name": "an ask taken, whatever its asker (G 602)", + "of": "event:ask-taken", + "subject": "mesh.seat.node-build-go.event.ask-taken.ace", + "given": { + "asker": "mesh-delivery-builder", + "ask": "m2kx9a1b3c4d6a", + "node": "ace", + "at": "2026-11-11T10:00:00Z" + } } ] diff --git a/contracts/seats/node-build-go/fixtures/states.json b/contracts/seats/node-build-go/fixtures/states.json index 9794d3fa..98e3890d 100644 --- a/contracts/seats/node-build-go/fixtures/states.json +++ b/contracts/seats/node-build-go/fixtures/states.json @@ -41,6 +41,17 @@ "recipe": "1" } }, + { + "name": "a node's toolchain with its program's digest (G 472)", + "of": "state:toolchains/.", + "given": { + "v": 1, + "language": "go", + "version": "1.26.0", + "recipe": "1", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + }, { "name": "a build ask", "of": "accept:delivery", @@ -60,6 +71,26 @@ } } }, + { + "name": "a build ask with the version the builder reserved (G 573)", + "of": "accept:delivery", + "subject": "mesh.seat.node-build-go.accept.delivery", + "given": { + "id": "a1b3", + "kind": "build", + "language": "go", + "asker": "mesh-delivery-builder", + "version": "1.4.2", + "build": { + "module": "nats", + "artifacts": [ + { + "artifact": "go" + } + ] + } + } + }, { "name": "a check ask with its fetch, siblings and levels", "of": "accept:check", diff --git a/contracts/seats/node-build-image/fixtures/events.json b/contracts/seats/node-build-image/fixtures/events.json index e8dca58f..b4e9402d 100644 --- a/contracts/seats/node-build-image/fixtures/events.json +++ b/contracts/seats/node-build-image/fixtures/events.json @@ -6,6 +6,8 @@ "given": { "v": 1, "id": "m2kx9a1b3c4d5e", + "asker": "mesh-delivery-pipeline", + "ask": "m2kx9a1b3c4d5e", "pipeline": "main.12.nats", "step": "lint", "module": "nats", @@ -15,7 +17,8 @@ "node": "ace", "outcome": "passed", "started": "2026-11-10T10:00:00Z", - "ended": "2026-11-10T10:02:00Z" + "ended": "2026-11-10T10:02:00Z", + "at": "2026-11-10T10:02:00Z" } }, { @@ -25,6 +28,8 @@ "given": { "v": 1, "id": "m2kx9a1b3c4d5f", + "asker": "mesh-delivery-pipeline", + "ask": "m2kx9a1b3c4d5f", "pipeline": "main.12.nats", "step": "lint", "module": "nats", @@ -34,7 +39,8 @@ "node": "g14", "outcome": "could not run", "said": "the sandbox could not fetch novox/mesh-sdk", - "output-object": "mesh-build-go_sent-objects/m2kx9a1b3c4d5f" + "output-object": "mesh-build-go_sent-objects/m2kx9a1b3c4d5f", + "at": "2026-11-10T10:02:00Z" } }, { @@ -59,5 +65,31 @@ "phase": "fetch", "at": "2026-11-11T10:01:00Z" } + }, + { + "name": "a builder's build ended, naming its asker and no commit or fingerprint (G 548)", + "of": "event:step-ended", + "subject": "mesh.seat.node-build-image.event.step-ended.ace", + "given": { + "v": 1, + "id": "m2kx9a1b3c4d6a", + "asker": "mesh-delivery-builder", + "ask": "m2kx9a1b3c4d6a", + "node": "ace", + "outcome": "failed", + "said": "the build failed", + "at": "2026-11-11T10:03:00Z" + } + }, + { + "name": "an ask taken, whatever its asker (G 602)", + "of": "event:ask-taken", + "subject": "mesh.seat.node-build-image.event.ask-taken.ace", + "given": { + "asker": "mesh-delivery-builder", + "ask": "m2kx9a1b3c4d6a", + "node": "ace", + "at": "2026-11-11T10:00:00Z" + } } ] diff --git a/contracts/seats/node-build-image/fixtures/states.json b/contracts/seats/node-build-image/fixtures/states.json index 9e60272a..f585ebf3 100644 --- a/contracts/seats/node-build-image/fixtures/states.json +++ b/contracts/seats/node-build-image/fixtures/states.json @@ -41,6 +41,17 @@ "recipe": "1" } }, + { + "name": "a node's toolchain with its program's digest (G 472)", + "of": "state:toolchains/.", + "given": { + "v": 1, + "language": "go", + "version": "1.26.0", + "recipe": "1", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + }, { "name": "a build ask", "of": "accept:delivery", @@ -60,6 +71,26 @@ } } }, + { + "name": "a build ask with the version the builder reserved (G 573)", + "of": "accept:delivery", + "subject": "mesh.seat.node-build-image.accept.delivery", + "given": { + "id": "a1b3", + "kind": "build", + "language": "go", + "asker": "mesh-delivery-builder", + "version": "1.4.2", + "build": { + "module": "nats", + "artifacts": [ + { + "artifact": "go" + } + ] + } + } + }, { "name": "a step ended on a transport fault, given again by the bus", "of": "state:steps/.", diff --git a/contracts/seats/node-build-node/fixtures/events.json b/contracts/seats/node-build-node/fixtures/events.json index 1f1924a0..aa0e8f98 100644 --- a/contracts/seats/node-build-node/fixtures/events.json +++ b/contracts/seats/node-build-node/fixtures/events.json @@ -6,6 +6,8 @@ "given": { "v": 1, "id": "m2kx9a1b3c4d5e", + "asker": "mesh-delivery-pipeline", + "ask": "m2kx9a1b3c4d5e", "pipeline": "main.12.nats", "step": "unit", "module": "nats", @@ -32,7 +34,8 @@ } ], "started": "2026-11-10T10:00:00Z", - "ended": "2026-11-10T10:02:00Z" + "ended": "2026-11-10T10:02:00Z", + "at": "2026-11-10T10:02:00Z" } }, { @@ -42,6 +45,8 @@ "given": { "v": 1, "id": "m2kx9a1b3c4d5f", + "asker": "mesh-delivery-pipeline", + "ask": "m2kx9a1b3c4d5f", "pipeline": "main.12.nats", "step": "integration", "module": "nats", @@ -51,7 +56,8 @@ "node": "g14", "outcome": "could not run", "said": "the sandbox could not fetch novox/mesh-sdk", - "output-object": "mesh-build-go_sent-objects/m2kx9a1b3c4d5f" + "output-object": "mesh-build-go_sent-objects/m2kx9a1b3c4d5f", + "at": "2026-11-10T10:02:00Z" } }, { @@ -76,5 +82,31 @@ "phase": "fetch", "at": "2026-11-11T10:01:00Z" } + }, + { + "name": "a builder's build ended, naming its asker and no commit or fingerprint (G 548)", + "of": "event:step-ended", + "subject": "mesh.seat.node-build-node.event.step-ended.ace", + "given": { + "v": 1, + "id": "m2kx9a1b3c4d6a", + "asker": "mesh-delivery-builder", + "ask": "m2kx9a1b3c4d6a", + "node": "ace", + "outcome": "failed", + "said": "the build failed", + "at": "2026-11-11T10:03:00Z" + } + }, + { + "name": "an ask taken, whatever its asker (G 602)", + "of": "event:ask-taken", + "subject": "mesh.seat.node-build-node.event.ask-taken.ace", + "given": { + "asker": "mesh-delivery-builder", + "ask": "m2kx9a1b3c4d6a", + "node": "ace", + "at": "2026-11-11T10:00:00Z" + } } ] diff --git a/contracts/seats/node-build-node/fixtures/states.json b/contracts/seats/node-build-node/fixtures/states.json index af631c10..d57e12a1 100644 --- a/contracts/seats/node-build-node/fixtures/states.json +++ b/contracts/seats/node-build-node/fixtures/states.json @@ -41,6 +41,17 @@ "recipe": "1" } }, + { + "name": "a node's toolchain with its program's digest (G 472)", + "of": "state:toolchains/.", + "given": { + "v": 1, + "language": "go", + "version": "1.26.0", + "recipe": "1", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + }, { "name": "a build ask", "of": "accept:delivery", @@ -60,6 +71,26 @@ } } }, + { + "name": "a build ask with the version the builder reserved (G 573)", + "of": "accept:delivery", + "subject": "mesh.seat.node-build-node.accept.delivery", + "given": { + "id": "a1b3", + "kind": "build", + "language": "go", + "asker": "mesh-delivery-builder", + "version": "1.4.2", + "build": { + "module": "nats", + "artifacts": [ + { + "artifact": "go" + } + ] + } + } + }, { "name": "a check ask with the group's tagged package", "of": "accept:check", diff --git a/contracts/ts/seats/meshwants/index.d.ts b/contracts/ts/seats/meshwants/index.d.ts index 91a28584..b2b00598 100644 --- a/contracts/ts/seats/meshwants/index.d.ts +++ b/contracts/ts/seats/meshwants/index.d.ts @@ -62,8 +62,6 @@ export declare const versionDeliveredStream: "SEAT_mesh-wants_version-delivered" export declare const versionDeliveredSubject: "mesh.seat.mesh-wants.event.version-delivered"; -export declare const stepProgressStream: "SEAT_mesh-wants_step-progress"; - export declare function stepProgressSubject(node: string): string; export declare const stepProgressSubjectAll: "mesh.seat.mesh-wants.event.step-progress.*"; diff --git a/contracts/ts/seats/meshwants/index.js b/contracts/ts/seats/meshwants/index.js index 488f1228..df1d0f62 100644 --- a/contracts/ts/seats/meshwants/index.js +++ b/contracts/ts/seats/meshwants/index.js @@ -64,8 +64,6 @@ export const versionDeliveredStream = "SEAT_mesh-wants_version-delivered"; export const versionDeliveredSubject = "mesh.seat.mesh-wants.event.version-delivered"; -export const stepProgressStream = "SEAT_mesh-wants_step-progress"; - export function stepProgressSubject(node) { return "mesh.seat.mesh-wants.event.step-progress." + node; } diff --git a/contracts/ts/seats/nodebuildgo/index.d.ts b/contracts/ts/seats/nodebuildgo/index.d.ts index 11d2e419..1fe4b734 100644 --- a/contracts/ts/seats/nodebuildgo/index.d.ts +++ b/contracts/ts/seats/nodebuildgo/index.d.ts @@ -28,6 +28,7 @@ export declare const events: { readonly stepEnded: "step-ended"; readonly toolchainReported: "toolchain-reported"; readonly stepProgress: "step-progress"; + readonly askTaken: "ask-taken"; }; export declare const family: { readonly name: "node-build-go.events"; readonly maxAgeDays: 7 }; @@ -44,12 +45,14 @@ export declare function toolchainReportedSubject(node: string): string; export declare const toolchainReportedSubjectAll: "mesh.seat.node-build-go.event.toolchain-reported.*"; -export declare const stepProgressStream: "SEAT_node-build-go_step-progress"; - export declare function stepProgressSubject(node: string): string; export declare const stepProgressSubjectAll: "mesh.seat.node-build-go.event.step-progress.*"; +export declare function askTakenSubject(node: string): string; + +export declare const askTakenSubjectAll: "mesh.seat.node-build-go.event.ask-taken.*"; + export declare const deliverySubject: "mesh.seat.node-build-go.accept.delivery"; export declare const checkSubject: "mesh.seat.node-build-go.accept.check"; @@ -112,6 +115,8 @@ export declare function detectedStepsByModuleArtifactKey(module: string, artifac export declare function parseDetectedStepsByModuleArtifactKey(_key: string): { module: string; artifact: string } | undefined; export type AskArtifact = buildStep.AskArtifact; +export type AskTaken = buildStep.AskTaken; +export type AskTakenEvent = buildStep.AskTakenEvent; export type BuildAnswer = buildStep.BuildAnswer; export type BuiltArtifact = buildStep.BuiltArtifact; export type BuiltArtifactResolvedItem = buildStep.BuiltArtifactResolvedItem; diff --git a/contracts/ts/seats/nodebuildgo/index.js b/contracts/ts/seats/nodebuildgo/index.js index cf4a3f99..f2566f56 100644 --- a/contracts/ts/seats/nodebuildgo/index.js +++ b/contracts/ts/seats/nodebuildgo/index.js @@ -26,6 +26,7 @@ export const events = { stepEnded: "step-ended", toolchainReported: "toolchain-reported", stepProgress: "step-progress", + askTaken: "ask-taken", }; export const family = { name: "node-build-go.events", maxAgeDays: 7 }; @@ -46,14 +47,18 @@ export function toolchainReportedSubject(node) { export const toolchainReportedSubjectAll = "mesh.seat.node-build-go.event.toolchain-reported.*"; -export const stepProgressStream = "SEAT_node-build-go_step-progress"; - export function stepProgressSubject(node) { return "mesh.seat.node-build-go.event.step-progress." + node; } export const stepProgressSubjectAll = "mesh.seat.node-build-go.event.step-progress.*"; +export function askTakenSubject(node) { + return "mesh.seat.node-build-go.event.ask-taken." + node; +} + +export const askTakenSubjectAll = "mesh.seat.node-build-go.event.ask-taken.*"; + export const deliverySubject = "mesh.seat.node-build-go.accept.delivery"; export const checkSubject = "mesh.seat.node-build-go.accept.check"; diff --git a/contracts/ts/seats/nodebuildgo/parts/build-step.d.ts b/contracts/ts/seats/nodebuildgo/parts/build-step.d.ts index c8907fdb..2addf5c4 100644 --- a/contracts/ts/seats/nodebuildgo/parts/build-step.d.ts +++ b/contracts/ts/seats/nodebuildgo/parts/build-step.d.ts @@ -15,6 +15,7 @@ export declare const events: { readonly stepEnded: "step-ended"; readonly toolchainReported: "toolchain-reported"; readonly stepProgress: "step-progress"; + readonly askTaken: "ask-taken"; }; /** one artifact of a build ask: built from the fetched tree, or copied from an upstream pin */ @@ -25,6 +26,16 @@ export interface AskArtifact { "version"?: string; } +/** an ask a build seat's holder took, said when it takes any ask, whatever its asker (design 47 G section 602) */ +export interface AskTaken { + /** the ask's id */ + "ask": string; + "asker": string; + "at": string; + /** the node that took it, as in the subject */ + "node": string; +} + export interface BuildAnswer { "artifacts": BuiltArtifact[]; "imports"?: string[]; @@ -392,23 +403,31 @@ export interface StepAsk { "step"?: StepRun; /** the layout version */ "v"?: number; + /** the version MAJOR.MINOR.PATCH the builder reserved for the build, passed to it as MESH_VERSION; empty for a build that reserves none, which stamps none (design 47 G section 573) */ + "version"?: string; } -/** a pipeline step's end, said by the build seat's holder on the node that ran it (ADR 0666 sections 7, 21): the runner takes it only from that holder's account, naming the ask, the commit and the fingerprint it was asked at */ +/** an ask's end, said by the build seat's holder on the node that ran it, whatever its asker (design 47 G section 602): a pipeline step's end names the pipeline, the commit and the fingerprint it was asked at, which the runner takes only from that holder's account (ADR 0666 sections 7, 21); the end of any other ask names its asker, the ask, the node, the outcome and when, and neither commit nor fingerprint (design 47 G section 548) */ export interface StepEnded { - "artifact": string; + "artifact"?: string; + /** the ask's id, by which a reader takes only its own asks */ + "ask": string; + /** who asked it */ + "asker": string; + /** when it ended (RFC 3339 time) */ + "at": string; /** for a rerun, the caller the runtime proved */ "by"?: string; /** the packages not run because the ask's kept results held them */ "carried"?: KeptResult[]; - "commit": string; + "commit"?: string; "ended"?: string; - "fingerprint": string; + "fingerprint"?: string; /** the ask's id */ "id": string; /** the packages that passed at their digest, for the runner to keep */ "kept"?: KeptResult[]; - "module": string; + "module"?: string; /** the node that ran it, as in the subject */ "node": string; /** never passed for a step that ran nothing */ @@ -417,12 +436,12 @@ export interface StepEnded { "output"?: string; /** the output's object in the sender's sent store, when over the per-message limit (ADR 0666 section 16) */ "output-object"?: string; - "pipeline": string; + "pipeline"?: string; "rerun"?: boolean; /** why it failed or could not run, naming the part not heard */ "said"?: string; "started"?: string; - "step": "lint" | "unit" | "integration"; + "step"?: "lint" | "unit" | "integration"; "tests"?: TestCounts; /** the layout version */ "v"?: number; @@ -513,6 +532,8 @@ export interface TestCounts { export interface Toolchain { /** when (RFC 3339 time) */ "at"?: string; + /** the sha256: digest of the language's toolchain program as the build module runs it: the program of the module that provides that language's toolchain, never the build module's own (design 47 G section 472) */ + "digest"?: string; /** go or typescript */ "language": string; "recipe": string; @@ -643,3 +664,6 @@ export type ToolchainReportedEvent = ToolchainReported; /** is the body of the event step-progress. */ export type StepProgressEvent = StepProgress; +/** is the body of the event ask-taken. */ +export type AskTakenEvent = AskTaken; + diff --git a/contracts/ts/seats/nodebuildgo/parts/build-step.js b/contracts/ts/seats/nodebuildgo/parts/build-step.js index 155a68be..400cdf1c 100644 --- a/contracts/ts/seats/nodebuildgo/parts/build-step.js +++ b/contracts/ts/seats/nodebuildgo/parts/build-step.js @@ -15,6 +15,7 @@ export const events = { stepEnded: "step-ended", toolchainReported: "toolchain-reported", stepProgress: "step-progress", + askTaken: "ask-taken", }; export {}; diff --git a/contracts/ts/seats/nodebuildimage/index.d.ts b/contracts/ts/seats/nodebuildimage/index.d.ts index 67b12889..dff95615 100644 --- a/contracts/ts/seats/nodebuildimage/index.d.ts +++ b/contracts/ts/seats/nodebuildimage/index.d.ts @@ -27,6 +27,7 @@ export declare const events: { readonly stepEnded: "step-ended"; readonly toolchainReported: "toolchain-reported"; readonly stepProgress: "step-progress"; + readonly askTaken: "ask-taken"; }; export declare const family: { readonly name: "node-build-image.events"; readonly maxAgeDays: 7 }; @@ -43,12 +44,14 @@ export declare function toolchainReportedSubject(node: string): string; export declare const toolchainReportedSubjectAll: "mesh.seat.node-build-image.event.toolchain-reported.*"; -export declare const stepProgressStream: "SEAT_node-build-image_step-progress"; - export declare function stepProgressSubject(node: string): string; export declare const stepProgressSubjectAll: "mesh.seat.node-build-image.event.step-progress.*"; +export declare function askTakenSubject(node: string): string; + +export declare const askTakenSubjectAll: "mesh.seat.node-build-image.event.ask-taken.*"; + export declare const deliverySubject: "mesh.seat.node-build-image.accept.delivery"; export declare const checkSubject: "mesh.seat.node-build-image.accept.check"; @@ -109,6 +112,8 @@ export declare function detectedStepsByModuleArtifactKey(module: string, artifac export declare function parseDetectedStepsByModuleArtifactKey(_key: string): { module: string; artifact: string } | undefined; export type AskArtifact = buildStep.AskArtifact; +export type AskTaken = buildStep.AskTaken; +export type AskTakenEvent = buildStep.AskTakenEvent; export type BuildAnswer = buildStep.BuildAnswer; export type BuiltArtifact = buildStep.BuiltArtifact; export type BuiltArtifactResolvedItem = buildStep.BuiltArtifactResolvedItem; diff --git a/contracts/ts/seats/nodebuildimage/index.js b/contracts/ts/seats/nodebuildimage/index.js index 8a23cc70..bf0ad43c 100644 --- a/contracts/ts/seats/nodebuildimage/index.js +++ b/contracts/ts/seats/nodebuildimage/index.js @@ -25,6 +25,7 @@ export const events = { stepEnded: "step-ended", toolchainReported: "toolchain-reported", stepProgress: "step-progress", + askTaken: "ask-taken", }; export const family = { name: "node-build-image.events", maxAgeDays: 7 }; @@ -45,14 +46,18 @@ export function toolchainReportedSubject(node) { export const toolchainReportedSubjectAll = "mesh.seat.node-build-image.event.toolchain-reported.*"; -export const stepProgressStream = "SEAT_node-build-image_step-progress"; - export function stepProgressSubject(node) { return "mesh.seat.node-build-image.event.step-progress." + node; } export const stepProgressSubjectAll = "mesh.seat.node-build-image.event.step-progress.*"; +export function askTakenSubject(node) { + return "mesh.seat.node-build-image.event.ask-taken." + node; +} + +export const askTakenSubjectAll = "mesh.seat.node-build-image.event.ask-taken.*"; + export const deliverySubject = "mesh.seat.node-build-image.accept.delivery"; export const checkSubject = "mesh.seat.node-build-image.accept.check"; diff --git a/contracts/ts/seats/nodebuildimage/parts/build-step.d.ts b/contracts/ts/seats/nodebuildimage/parts/build-step.d.ts index c8907fdb..2addf5c4 100644 --- a/contracts/ts/seats/nodebuildimage/parts/build-step.d.ts +++ b/contracts/ts/seats/nodebuildimage/parts/build-step.d.ts @@ -15,6 +15,7 @@ export declare const events: { readonly stepEnded: "step-ended"; readonly toolchainReported: "toolchain-reported"; readonly stepProgress: "step-progress"; + readonly askTaken: "ask-taken"; }; /** one artifact of a build ask: built from the fetched tree, or copied from an upstream pin */ @@ -25,6 +26,16 @@ export interface AskArtifact { "version"?: string; } +/** an ask a build seat's holder took, said when it takes any ask, whatever its asker (design 47 G section 602) */ +export interface AskTaken { + /** the ask's id */ + "ask": string; + "asker": string; + "at": string; + /** the node that took it, as in the subject */ + "node": string; +} + export interface BuildAnswer { "artifacts": BuiltArtifact[]; "imports"?: string[]; @@ -392,23 +403,31 @@ export interface StepAsk { "step"?: StepRun; /** the layout version */ "v"?: number; + /** the version MAJOR.MINOR.PATCH the builder reserved for the build, passed to it as MESH_VERSION; empty for a build that reserves none, which stamps none (design 47 G section 573) */ + "version"?: string; } -/** a pipeline step's end, said by the build seat's holder on the node that ran it (ADR 0666 sections 7, 21): the runner takes it only from that holder's account, naming the ask, the commit and the fingerprint it was asked at */ +/** an ask's end, said by the build seat's holder on the node that ran it, whatever its asker (design 47 G section 602): a pipeline step's end names the pipeline, the commit and the fingerprint it was asked at, which the runner takes only from that holder's account (ADR 0666 sections 7, 21); the end of any other ask names its asker, the ask, the node, the outcome and when, and neither commit nor fingerprint (design 47 G section 548) */ export interface StepEnded { - "artifact": string; + "artifact"?: string; + /** the ask's id, by which a reader takes only its own asks */ + "ask": string; + /** who asked it */ + "asker": string; + /** when it ended (RFC 3339 time) */ + "at": string; /** for a rerun, the caller the runtime proved */ "by"?: string; /** the packages not run because the ask's kept results held them */ "carried"?: KeptResult[]; - "commit": string; + "commit"?: string; "ended"?: string; - "fingerprint": string; + "fingerprint"?: string; /** the ask's id */ "id": string; /** the packages that passed at their digest, for the runner to keep */ "kept"?: KeptResult[]; - "module": string; + "module"?: string; /** the node that ran it, as in the subject */ "node": string; /** never passed for a step that ran nothing */ @@ -417,12 +436,12 @@ export interface StepEnded { "output"?: string; /** the output's object in the sender's sent store, when over the per-message limit (ADR 0666 section 16) */ "output-object"?: string; - "pipeline": string; + "pipeline"?: string; "rerun"?: boolean; /** why it failed or could not run, naming the part not heard */ "said"?: string; "started"?: string; - "step": "lint" | "unit" | "integration"; + "step"?: "lint" | "unit" | "integration"; "tests"?: TestCounts; /** the layout version */ "v"?: number; @@ -513,6 +532,8 @@ export interface TestCounts { export interface Toolchain { /** when (RFC 3339 time) */ "at"?: string; + /** the sha256: digest of the language's toolchain program as the build module runs it: the program of the module that provides that language's toolchain, never the build module's own (design 47 G section 472) */ + "digest"?: string; /** go or typescript */ "language": string; "recipe": string; @@ -643,3 +664,6 @@ export type ToolchainReportedEvent = ToolchainReported; /** is the body of the event step-progress. */ export type StepProgressEvent = StepProgress; +/** is the body of the event ask-taken. */ +export type AskTakenEvent = AskTaken; + diff --git a/contracts/ts/seats/nodebuildimage/parts/build-step.js b/contracts/ts/seats/nodebuildimage/parts/build-step.js index 155a68be..400cdf1c 100644 --- a/contracts/ts/seats/nodebuildimage/parts/build-step.js +++ b/contracts/ts/seats/nodebuildimage/parts/build-step.js @@ -15,6 +15,7 @@ export const events = { stepEnded: "step-ended", toolchainReported: "toolchain-reported", stepProgress: "step-progress", + askTaken: "ask-taken", }; export {}; diff --git a/contracts/ts/seats/nodebuildnode/index.d.ts b/contracts/ts/seats/nodebuildnode/index.d.ts index 69ed8aee..7d0eaaf3 100644 --- a/contracts/ts/seats/nodebuildnode/index.d.ts +++ b/contracts/ts/seats/nodebuildnode/index.d.ts @@ -27,6 +27,7 @@ export declare const events: { readonly stepEnded: "step-ended"; readonly toolchainReported: "toolchain-reported"; readonly stepProgress: "step-progress"; + readonly askTaken: "ask-taken"; }; export declare const family: { readonly name: "node-build-node.events"; readonly maxAgeDays: 7 }; @@ -43,12 +44,14 @@ export declare function toolchainReportedSubject(node: string): string; export declare const toolchainReportedSubjectAll: "mesh.seat.node-build-node.event.toolchain-reported.*"; -export declare const stepProgressStream: "SEAT_node-build-node_step-progress"; - export declare function stepProgressSubject(node: string): string; export declare const stepProgressSubjectAll: "mesh.seat.node-build-node.event.step-progress.*"; +export declare function askTakenSubject(node: string): string; + +export declare const askTakenSubjectAll: "mesh.seat.node-build-node.event.ask-taken.*"; + export declare const deliverySubject: "mesh.seat.node-build-node.accept.delivery"; export declare const checkSubject: "mesh.seat.node-build-node.accept.check"; @@ -109,6 +112,8 @@ export declare function detectedStepsByModuleArtifactKey(module: string, artifac export declare function parseDetectedStepsByModuleArtifactKey(_key: string): { module: string; artifact: string } | undefined; export type AskArtifact = buildStep.AskArtifact; +export type AskTaken = buildStep.AskTaken; +export type AskTakenEvent = buildStep.AskTakenEvent; export type BuildAnswer = buildStep.BuildAnswer; export type BuiltArtifact = buildStep.BuiltArtifact; export type BuiltArtifactResolvedItem = buildStep.BuiltArtifactResolvedItem; diff --git a/contracts/ts/seats/nodebuildnode/index.js b/contracts/ts/seats/nodebuildnode/index.js index 6cda8ea2..90d22dca 100644 --- a/contracts/ts/seats/nodebuildnode/index.js +++ b/contracts/ts/seats/nodebuildnode/index.js @@ -25,6 +25,7 @@ export const events = { stepEnded: "step-ended", toolchainReported: "toolchain-reported", stepProgress: "step-progress", + askTaken: "ask-taken", }; export const family = { name: "node-build-node.events", maxAgeDays: 7 }; @@ -45,14 +46,18 @@ export function toolchainReportedSubject(node) { export const toolchainReportedSubjectAll = "mesh.seat.node-build-node.event.toolchain-reported.*"; -export const stepProgressStream = "SEAT_node-build-node_step-progress"; - export function stepProgressSubject(node) { return "mesh.seat.node-build-node.event.step-progress." + node; } export const stepProgressSubjectAll = "mesh.seat.node-build-node.event.step-progress.*"; +export function askTakenSubject(node) { + return "mesh.seat.node-build-node.event.ask-taken." + node; +} + +export const askTakenSubjectAll = "mesh.seat.node-build-node.event.ask-taken.*"; + export const deliverySubject = "mesh.seat.node-build-node.accept.delivery"; export const checkSubject = "mesh.seat.node-build-node.accept.check"; diff --git a/contracts/ts/seats/nodebuildnode/parts/build-step.d.ts b/contracts/ts/seats/nodebuildnode/parts/build-step.d.ts index c8907fdb..2addf5c4 100644 --- a/contracts/ts/seats/nodebuildnode/parts/build-step.d.ts +++ b/contracts/ts/seats/nodebuildnode/parts/build-step.d.ts @@ -15,6 +15,7 @@ export declare const events: { readonly stepEnded: "step-ended"; readonly toolchainReported: "toolchain-reported"; readonly stepProgress: "step-progress"; + readonly askTaken: "ask-taken"; }; /** one artifact of a build ask: built from the fetched tree, or copied from an upstream pin */ @@ -25,6 +26,16 @@ export interface AskArtifact { "version"?: string; } +/** an ask a build seat's holder took, said when it takes any ask, whatever its asker (design 47 G section 602) */ +export interface AskTaken { + /** the ask's id */ + "ask": string; + "asker": string; + "at": string; + /** the node that took it, as in the subject */ + "node": string; +} + export interface BuildAnswer { "artifacts": BuiltArtifact[]; "imports"?: string[]; @@ -392,23 +403,31 @@ export interface StepAsk { "step"?: StepRun; /** the layout version */ "v"?: number; + /** the version MAJOR.MINOR.PATCH the builder reserved for the build, passed to it as MESH_VERSION; empty for a build that reserves none, which stamps none (design 47 G section 573) */ + "version"?: string; } -/** a pipeline step's end, said by the build seat's holder on the node that ran it (ADR 0666 sections 7, 21): the runner takes it only from that holder's account, naming the ask, the commit and the fingerprint it was asked at */ +/** an ask's end, said by the build seat's holder on the node that ran it, whatever its asker (design 47 G section 602): a pipeline step's end names the pipeline, the commit and the fingerprint it was asked at, which the runner takes only from that holder's account (ADR 0666 sections 7, 21); the end of any other ask names its asker, the ask, the node, the outcome and when, and neither commit nor fingerprint (design 47 G section 548) */ export interface StepEnded { - "artifact": string; + "artifact"?: string; + /** the ask's id, by which a reader takes only its own asks */ + "ask": string; + /** who asked it */ + "asker": string; + /** when it ended (RFC 3339 time) */ + "at": string; /** for a rerun, the caller the runtime proved */ "by"?: string; /** the packages not run because the ask's kept results held them */ "carried"?: KeptResult[]; - "commit": string; + "commit"?: string; "ended"?: string; - "fingerprint": string; + "fingerprint"?: string; /** the ask's id */ "id": string; /** the packages that passed at their digest, for the runner to keep */ "kept"?: KeptResult[]; - "module": string; + "module"?: string; /** the node that ran it, as in the subject */ "node": string; /** never passed for a step that ran nothing */ @@ -417,12 +436,12 @@ export interface StepEnded { "output"?: string; /** the output's object in the sender's sent store, when over the per-message limit (ADR 0666 section 16) */ "output-object"?: string; - "pipeline": string; + "pipeline"?: string; "rerun"?: boolean; /** why it failed or could not run, naming the part not heard */ "said"?: string; "started"?: string; - "step": "lint" | "unit" | "integration"; + "step"?: "lint" | "unit" | "integration"; "tests"?: TestCounts; /** the layout version */ "v"?: number; @@ -513,6 +532,8 @@ export interface TestCounts { export interface Toolchain { /** when (RFC 3339 time) */ "at"?: string; + /** the sha256: digest of the language's toolchain program as the build module runs it: the program of the module that provides that language's toolchain, never the build module's own (design 47 G section 472) */ + "digest"?: string; /** go or typescript */ "language": string; "recipe": string; @@ -643,3 +664,6 @@ export type ToolchainReportedEvent = ToolchainReported; /** is the body of the event step-progress. */ export type StepProgressEvent = StepProgress; +/** is the body of the event ask-taken. */ +export type AskTakenEvent = AskTaken; + diff --git a/contracts/ts/seats/nodebuildnode/parts/build-step.js b/contracts/ts/seats/nodebuildnode/parts/build-step.js index 155a68be..400cdf1c 100644 --- a/contracts/ts/seats/nodebuildnode/parts/build-step.js +++ b/contracts/ts/seats/nodebuildnode/parts/build-step.js @@ -15,6 +15,7 @@ export const events = { stepEnded: "step-ended", toolchainReported: "toolchain-reported", stepProgress: "step-progress", + askTaken: "ask-taken", }; export {}; diff --git a/sdk/buildstep/ask.go b/sdk/buildstep/ask.go index 8a065726..a739fe8a 100644 --- a/sdk/buildstep/ask.go +++ b/sdk/buildstep/ask.go @@ -204,15 +204,18 @@ func AskStepWatch(askID string) (string, error) { // StepAsk is one ask of a build module: the part's `StepAsk`. type StepAsk struct { - V int `json:"v"` - ID string `json:"id"` - Kind string `json:"kind"` - Language string `json:"language"` - Asker string `json:"asker"` - At time.Time `json:"at,omitzero"` - Build *BuildJob `json:"build,omitempty"` - Check *CheckJob `json:"check,omitempty"` - Compose *ComposeJob `json:"compose,omitempty"` + V int `json:"v"` + ID string `json:"id"` + Kind string `json:"kind"` + Language string `json:"language"` + Asker string `json:"asker"` + At time.Time `json:"at,omitzero"` + // Version is the version MAJOR.MINOR.PATCH the builder reserved for the build, which the build module passes as + // MESH_VERSION; empty for a build that reserves none, which stamps none (design 47 G §573). + Version string `json:"version,omitempty"` + Build *BuildJob `json:"build,omitempty"` + Check *CheckJob `json:"check,omitempty"` + Compose *ComposeJob `json:"compose,omitempty"` // Run is a run ask's job (run.go): a program built from a change and run over the heads, on the Go seat's check // stream alone, as a compose. Run *RunJob `json:"run,omitempty"` diff --git a/sdk/buildstep/ask_test.go b/sdk/buildstep/ask_test.go index 1ad37ce0..41a83ca7 100644 --- a/sdk/buildstep/ask_test.go +++ b/sdk/buildstep/ask_test.go @@ -300,9 +300,30 @@ func TestAskSubjectsStreamsAndKeys(t *testing.T) { t.Fatal(err) } goSubj, _ := StepEndedSubject(AskGo, "ace") - if back, err := DecodeStepEnded(goSubj, raw); err != nil || back.Fingerprint != "fp-0123" || back.Tests.Unit != 14 { + if back, err := DecodeStepEnded(goSubj, raw); err != nil || back.Fingerprint != "fp-0123" || back.Tests.Unit != 14 || + back.Asker != stepAsk().Asker || back.Ask != testAskID { t.Errorf("round trip: %+v %v", back, err) } + // 1966/CT12 (G §548, §602): the end of an ask outside a pipeline names its asker, the ask and when, never a commit. + at := time.Date(2026, 11, 11, 10, 3, 0, 0, time.UTC) + build := StepEnded{ID: testAskID, Asker: "mesh-delivery-builder", Ask: testAskID, Node: "ace", Outcome: OutcomePassed, At: at} + if raw, err := EncodeStepEnded(build); err != nil || strings.Contains(string(raw), "commit") { + t.Errorf("a builder's build end: %s %v", raw, err) + } else if back, err := DecodeStepEnded(goSubj, raw); err != nil || back.Asker != "mesh-delivery-builder" || !back.At.Equal(at) { + t.Errorf("a builder's build end reads back as %+v %v", back, err) + } + for name, bad := range map[string]func(*StepEnded){ + "no asker": func(e *StepEnded) { e.Asker = "" }, + "no time": func(e *StepEnded) { e.At = time.Time{} }, + "a commit": func(e *StepEnded) { e.Commit = "0123456789abcdef0123456789abcdef01234567" }, + "a fingerprint": func(e *StepEnded) { e.Fingerprint = "fp-0123" }, + } { + e := build + bad(&e) + if _, err := EncodeStepEnded(e); err == nil { + t.Errorf("an end outside a pipeline with %s is said", name) + } + } g14, _ := StepEndedSubject(AskGo, "g14") if _, err := DecodeStepEnded(g14, raw); err == nil || !strings.Contains(err.Error(), "said on g14's subject") { t.Errorf("an end said on another node's subject: %v", err) diff --git a/sdk/buildstep/pipelinestep.go b/sdk/buildstep/pipelinestep.go index b5157cc0..40e51f74 100644 --- a/sdk/buildstep/pipelinestep.go +++ b/sdk/buildstep/pipelinestep.go @@ -106,16 +106,20 @@ func (r StepRun) check(id string) error { return nil } -// StepEnded is a pipeline step's end, said by the build seat's holder on the node that ran it. +// StepEnded is an ask's end, said by the build seat's holder on the node that ran it, whatever its asker (design 47 G +// §602): a pipeline step's end names its pipeline, step, module, artifact, commit and fingerprint; the end of any other +// ask names its asker, the ask, the node, the outcome and when, and neither commit nor fingerprint (G §548). type StepEnded struct { V int `json:"v"` ID string `json:"id"` - Pipeline string `json:"pipeline"` - Step string `json:"step"` - Module string `json:"module"` - Artifact string `json:"artifact"` - Commit string `json:"commit"` - Fingerprint string `json:"fingerprint"` + Asker string `json:"asker"` + Ask string `json:"ask"` + Pipeline string `json:"pipeline,omitempty"` + Step string `json:"step,omitempty"` + Module string `json:"module,omitempty"` + Artifact string `json:"artifact,omitempty"` + Commit string `json:"commit,omitempty"` + Fingerprint string `json:"fingerprint,omitempty"` Node string `json:"node"` Outcome string `json:"outcome"` Tests *TestCounts `json:"tests,omitempty"` @@ -128,6 +132,7 @@ type StepEnded struct { By string `json:"by,omitempty"` Started time.Time `json:"started,omitzero"` Ended time.Time `json:"ended,omitzero"` + At time.Time `json:"at,omitzero"` } // EndOf is the end of the step a holds, on node, with outcome: the ask's names copied, so the runner can match the ask, @@ -137,7 +142,7 @@ func EndOf(a StepAsk, node, outcome string) (StepEnded, error) { return StepEnded{}, fmt.Errorf("buildstep: the ask %s is no pipeline step", a.ID) } s := a.Step - e := StepEnded{V: StepEndedVersion, ID: a.ID, Pipeline: s.Pipeline, Step: s.Step, Module: s.Module, Artifact: s.Artifact, + e := StepEnded{V: StepEndedVersion, ID: a.ID, Asker: a.Asker, Ask: a.ID, Pipeline: s.Pipeline, Step: s.Step, Module: s.Module, Artifact: s.Artifact, Commit: s.Commit, Fingerprint: s.Fingerprint, Node: node, Outcome: outcome, Rerun: s.Rerun, By: s.By} return e, e.check() } @@ -149,6 +154,20 @@ func (e StepEnded) check() error { if err := checkNode(e.Node); err != nil { return err } + if e.Pipeline == "" { + // G §548, §602: the end of an ask outside a pipeline names its asker, the ask and when, and no commit or + // fingerprint. + if !slices.Contains(StepOutcomes, e.Outcome) { + return fmt.Errorf("buildstep: the end of %s says %q, not one of %s", e.ID, e.Outcome, strings.Join(StepOutcomes, ", ")) + } + if e.Asker == "" || e.Ask == "" || e.At.IsZero() { + return fmt.Errorf("buildstep: the end of %s, an ask outside a pipeline, names no asker, ask or time", e.ID) + } + if e.Commit != "" || e.Fingerprint != "" { + return fmt.Errorf("buildstep: the end of %s names a commit or a fingerprint and no pipeline", e.ID) + } + return nil + } if !slices.Contains(PipelineSteps, e.Step) { return fmt.Errorf("buildstep: the end of %s names the step %q", e.ID, e.Step) } diff --git a/sdk/buildstep/toolchain.go b/sdk/buildstep/toolchain.go index 9b655926..61d13d70 100644 --- a/sdk/buildstep/toolchain.go +++ b/sdk/buildstep/toolchain.go @@ -16,11 +16,14 @@ const ToolchainVersion = 1 // the part build-step's `Toolchain`, the layout every node-build seat gives that state; the three build modules share // this neutral form (a seat's generated type is one package per seat). A test reads each seat's fixture strictly into it. type Toolchain struct { - V int `json:"v,omitempty"` - Language string `json:"language"` - Version string `json:"version"` - Recipe string `json:"recipe"` - At time.Time `json:"at,omitzero"` + V int `json:"v,omitempty"` + Language string `json:"language"` + Version string `json:"version"` + Recipe string `json:"recipe"` + // Digest is the sha256: digest of the language's toolchain program as the build module runs it, the program of the + // module that provides that language's toolchain, never the build module's own (design 47 G §472). + Digest string `json:"digest,omitempty"` + At time.Time `json:"at,omitzero"` } func checkToolchainLanguage(language string) error { diff --git a/sdk/buildstep/toolchain_test.go b/sdk/buildstep/toolchain_test.go index 3cff6eea..01afe441 100644 --- a/sdk/buildstep/toolchain_test.go +++ b/sdk/buildstep/toolchain_test.go @@ -44,8 +44,8 @@ func TestTheToolchainsValueAndKeyAreTheSeatsLayout(t *testing.T) { read++ } } - if read != 3 { - t.Errorf("%d toolchain fixtures read, want 3", read) + if read != 6 { // 1966/CT12 (G §472): each seat's fixture with its program's digest too + t.Errorf("%d toolchain fixtures read, want 6", read) } raw, err := EncodeToolchain(Toolchain{Language: "go", Version: "go1.27.1", Recipe: "3", At: time.Date(2026, 11, 5, 10, 0, 0, 0, time.UTC)}) if err != nil {