From 0596f444141b78976f552eb662942f095cb68e8d Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 19 Oct 2026 14:40:19 +0200 Subject: [PATCH 1/2] Say a build's directory and a pull request's module directories in the SDK (issue 1233) --- go/delivery/buildrecord.go | 5 +- go/delivery/buildrecord_test.go | 37 +++++++++++++ go/delivery/source.go | 15 +++++- go/delivery/source_test.go | 52 +++++++++++++++++++ .../testdata/build-record-build-agent.json | 2 +- 5 files changed, 108 insertions(+), 3 deletions(-) diff --git a/go/delivery/buildrecord.go b/go/delivery/buildrecord.go index cb48ec5..8802f6f 100644 --- a/go/delivery/buildrecord.go +++ b/go/delivery/buildrecord.go @@ -4,7 +4,9 @@ import "time" // BuildRecord is the layout mesh-controller internal/chain/records.go Build writes (line 91), the fields a reader outside // the controller reads (novox/hq ADR 0437 §4 §5, issue 1223). The controller's record carries more; a decode ignores -// what is not here. SourceInputs has no json keys of its own: it is written under its Go field names, as the controller +// what is not here. Path is the module's directory in its repository, as the controller writes it (Build.Path, records.go:100, +// set from the build request trimmed of slashes at internal/inventory/chain_requests.go:19); empty for a module whose +// manifest is the repository's root. SourceInputs has no json keys of its own: it is written under its Go field names, as the controller // writes it. type BuildRecord struct { ID string `json:"id"` @@ -12,6 +14,7 @@ type BuildRecord struct { Repository string `json:"repository"` Ref string `json:"ref,omitempty"` Module string `json:"module,omitempty"` + Path string `json:"path,omitempty"` Commit string `json:"commit,omitempty"` At time.Time `json:"at,omitzero"` SourceFingerprint string `json:"source-fingerprint,omitempty"` diff --git a/go/delivery/buildrecord_test.go b/go/delivery/buildrecord_test.go index 4374234..f67b24d 100644 --- a/go/delivery/buildrecord_test.go +++ b/go/delivery/buildrecord_test.go @@ -7,6 +7,8 @@ import ( "time" ) +// The fixture's path is what the controller writes for a build request: strings.Trim(path, "/") at mesh-controller +// internal/inventory/chain_requests.go:19, the module's directory in its repository (Build.Path, internal/chain/records.go:100). // The fixture is a record the controller's own encoder wrote (json.Marshal of a chain.Build, mesh-controller // internal/chain, a throwaway test run once and deleted) with build-agent's build-1792395447088936520 values, plus a // manifest-digest and one made artifact, which a BuildRecord does not carry. @@ -49,6 +51,41 @@ func TestBuildRecordDecodes(t *testing.T) { // 1223/AC1 } } +func TestBuildRecordPath(t *testing.T) { // 1233/AC1 + var r BuildRecord + if err := json.Unmarshal(loadBuildFixture(t), &r); err != nil { + t.Fatal(err) + } + if r.Path != "modules/mesh-delivery-watchman" { + t.Errorf("path = %q, want modules/mesh-delivery-watchman", r.Path) + } + // mesh-controller's manifest is its repository's root: the controller writes no path (omitempty), so the path is "". + var m map[string]json.RawMessage + if err := json.Unmarshal(loadBuildFixture(t), &m); err != nil { + t.Fatal(err) + } + delete(m, "path") + raw, _ := json.Marshal(m) + var root BuildRecord + if err := json.Unmarshal(raw, &root); err != nil { + t.Fatal(err) + } + if root.Path != "" { + t.Errorf("path of a root module = %q, want empty", root.Path) + } + // Every field 1223/AC1 checks is still equal in both. + if root.ID != r.ID || root.Module != r.Module || root.Repository != r.Repository || root.Ref != r.Ref || + root.Commit != r.Commit || root.State != r.State || root.SourceFingerprint != r.SourceFingerprint || !root.At.Equal(r.At) { + t.Errorf("fields differ: %+v vs %+v", root, r) + } + // And the path survives a round trip through the SDK's own type. + back, _ := json.Marshal(r) + var again BuildRecord + if err := json.Unmarshal(back, &again); err != nil || again.Path != r.Path { + t.Errorf("round trip path = %q (%v)", again.Path, err) + } +} + func TestBuildRecordWithoutInputs(t *testing.T) { // 1223/AC2 var m map[string]json.RawMessage if err := json.Unmarshal(loadBuildFixture(t), &m); err != nil { diff --git a/go/delivery/source.go b/go/delivery/source.go index 533fb32..f4328ff 100644 --- a/go/delivery/source.go +++ b/go/delivery/source.go @@ -241,7 +241,9 @@ func (m Module) check() error { // Forge is a message of the forge bridge's stream, trimmed to repository, branch, pull request and head. A pull // message says Action; a merged message carries the merge commit, the tree of every directory holding a // module.json and the repository's root tree; a refused message names the owed act it answers and the forge's own -// words. +// words. A pull message may also carry the module directories its head changes (Dirs, in the order the forge said) and +// DirsKnown, true when the forge could say them: known and empty is DirsKnown with no Dirs, not known is neither +// (the forge's module_dirs and module_dirs_said, mesh-catalog modules/gitea/trunk.ts). Other messages carry neither. type Forge struct { V int `json:"v"` Kind string `json:"kind"` @@ -250,6 +252,8 @@ type Forge struct { Number int `json:"number,omitempty"` Head string `json:"head,omitempty"` Action string `json:"action,omitempty"` + Dirs []string `json:"dirs,omitempty"` + DirsKnown bool `json:"dirs-known,omitempty"` Merge string `json:"merge,omitempty"` Trees map[string]string `json:"trees,omitempty"` Root string `json:"root,omitempty"` @@ -277,7 +281,13 @@ func (f Forge) check() error { default: return fmt.Errorf("delivery: the pull action %q is not opened, updated or closed", f.Action) } + if len(f.Dirs) > 0 && !f.DirsKnown { + return fmt.Errorf("delivery: a pull message with module directories must say it knows them (dirs-known)") + } case ForgeMerged: + if len(f.Dirs) > 0 || f.DirsKnown { + return fmt.Errorf("delivery: a merged message carries no module directories") + } if f.Number <= 0 { return fmt.Errorf("delivery: a merged message needs its pull request number") } @@ -299,6 +309,9 @@ func (f Forge) check() error { } } case ForgeRefused: + if len(f.Dirs) > 0 || f.DirsKnown { + return fmt.Errorf("delivery: a refused message carries no module directories") + } if f.Act == "" { return fmt.Errorf("delivery: a refused message needs the act it answers") } diff --git a/go/delivery/source_test.go b/go/delivery/source_test.go index 2abe8da..029793b 100644 --- a/go/delivery/source_test.go +++ b/go/delivery/source_test.go @@ -3,6 +3,7 @@ package delivery import ( "encoding/json" "errors" + "reflect" "strings" "testing" ) @@ -286,3 +287,54 @@ func TestSourceBuckets(t *testing.T) { t.Error("a bucket or history differs from the decided layout") } } + +func TestForgeDirs(t *testing.T) { + pull := func(dirs []string, known bool) Forge { + return Forge{Kind: ForgePull, Repository: "novox/mesh-sdk", Number: 36, Head: "aaaa", Action: "updated", Dirs: dirs, DirsKnown: known} + } + round := func(f Forge) Forge { + t.Helper() + data, err := EncodeForge(f) + if err != nil { + t.Fatal(err) + } + got, err := DecodeForge(data) + if err != nil { + t.Fatal(err) + } + return got + } + // 1233/AC2: the directories come back equal and in order. + got := round(pull([]string{"modules/a", "modules/b"}, true)) + if !reflect.DeepEqual(got.Dirs, []string{"modules/a", "modules/b"}) || !got.DirsKnown { + t.Errorf("dirs = %v known %v", got.Dirs, got.DirsKnown) + } + // 1233/AC3: a head changing no module directory is told apart from one whose directories could not be said. + none, unknown := round(pull(nil, true)), round(pull(nil, false)) + if !none.DirsKnown || len(none.Dirs) != 0 { + t.Errorf("known and empty: dirs %v known %v", none.Dirs, none.DirsKnown) + } + if unknown.DirsKnown || len(unknown.Dirs) != 0 { + t.Errorf("not known: dirs %v known %v", unknown.Dirs, unknown.DirsKnown) + } + // 1233/AC4: a merged or a refused message carrying directories is refused. + m := sampleForges()[1] + m.Dirs, m.DirsKnown = []string{"modules/a"}, true + if _, err := EncodeForge(m); err == nil { + t.Error("merged message with dirs accepted") + } + r := sampleForges()[2] + r.Dirs, r.DirsKnown = []string{"modules/a"}, true + if _, err := EncodeForge(r); err == nil { + t.Error("refused message with dirs accepted") + } + r = sampleForges()[2] + r.DirsKnown = true + if _, err := EncodeForge(r); err == nil { + t.Error("refused message with dirs-known accepted") + } + // directories on a pull message that does not say it knows them are refused too. + if _, err := EncodeForge(pull([]string{"modules/a"}, false)); err == nil { + t.Error("dirs without dirs-known accepted") + } +} diff --git a/go/delivery/testdata/build-record-build-agent.json b/go/delivery/testdata/build-record-build-agent.json index 028ad4b..2084a3b 100644 --- a/go/delivery/testdata/build-record-build-agent.json +++ b/go/delivery/testdata/build-record-build-agent.json @@ -1 +1 @@ -{"id":"build-1792395447088936520","state":"built","repository":"novox/mesh-controller","ref":"main","module":"build-agent","commit":"7a74cee83f88e6585a5511c3cc53bdf0f5303d20","manifest-digest":"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef","source-fingerprint":"src1:e846efe73716b78e80860baaac96153de2cb76551325753838ca245d2160a3d7","source-inputs":{"Module":"build-agent","Tree":"11c0b4f90cae0d6e4ef9cc48cbfdf9f6dbe52bca","Bases":["sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc","sha256:8ac98ca534ac3f51e1f420a1dd2c15e74c75cfa0f23f3ad27eb5d7236c349a0c"],"Contexts":{"server":"narrow:d51ca744fd11af59d616c858f76c27b3e58e16e11fbdc0c720eb9d7d19d55fac"},"Toolchains":null,"Unpinned":""},"made":[{"name":"server","kind":"image","reference":"registry.example/build-agent@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc"}],"at":"2026-10-19T09:38:00Z"} +{"id":"build-1792395447088936520","state":"built","repository":"novox/mesh-controller","ref":"main","module":"build-agent","path":"modules/mesh-delivery-watchman","commit":"7a74cee83f88e6585a5511c3cc53bdf0f5303d20","manifest-digest":"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef","source-fingerprint":"src1:e846efe73716b78e80860baaac96153de2cb76551325753838ca245d2160a3d7","source-inputs":{"Module":"build-agent","Tree":"11c0b4f90cae0d6e4ef9cc48cbfdf9f6dbe52bca","Bases":["sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc","sha256:8ac98ca534ac3f51e1f420a1dd2c15e74c75cfa0f23f3ad27eb5d7236c349a0c"],"Contexts":{"server":"narrow:d51ca744fd11af59d616c858f76c27b3e58e16e11fbdc0c720eb9d7d19d55fac"},"Toolchains":null,"Unpinned":""},"made":[{"name":"server","kind":"image","reference":"registry.example/build-agent@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc"}],"at":"2026-10-19T09:38:00Z"} -- 2.54.0 From 9ade199740e38e84c1d806ac04a6e6fa9afecced Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 19 Oct 2026 14:42:04 +0200 Subject: [PATCH 2/2] Give the build fixture the path the controller writes for build-agent (issue 1233) --- go/delivery/buildrecord_test.go | 4 ++-- go/delivery/testdata/build-record-build-agent.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go/delivery/buildrecord_test.go b/go/delivery/buildrecord_test.go index f67b24d..c056d38 100644 --- a/go/delivery/buildrecord_test.go +++ b/go/delivery/buildrecord_test.go @@ -56,8 +56,8 @@ func TestBuildRecordPath(t *testing.T) { // 1233/AC1 if err := json.Unmarshal(loadBuildFixture(t), &r); err != nil { t.Fatal(err) } - if r.Path != "modules/mesh-delivery-watchman" { - t.Errorf("path = %q, want modules/mesh-delivery-watchman", r.Path) + if r.Path != "modules/build-agent" { + t.Errorf("path = %q, want modules/build-agent", r.Path) } // mesh-controller's manifest is its repository's root: the controller writes no path (omitempty), so the path is "". var m map[string]json.RawMessage diff --git a/go/delivery/testdata/build-record-build-agent.json b/go/delivery/testdata/build-record-build-agent.json index 2084a3b..458b9e3 100644 --- a/go/delivery/testdata/build-record-build-agent.json +++ b/go/delivery/testdata/build-record-build-agent.json @@ -1 +1 @@ -{"id":"build-1792395447088936520","state":"built","repository":"novox/mesh-controller","ref":"main","module":"build-agent","path":"modules/mesh-delivery-watchman","commit":"7a74cee83f88e6585a5511c3cc53bdf0f5303d20","manifest-digest":"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef","source-fingerprint":"src1:e846efe73716b78e80860baaac96153de2cb76551325753838ca245d2160a3d7","source-inputs":{"Module":"build-agent","Tree":"11c0b4f90cae0d6e4ef9cc48cbfdf9f6dbe52bca","Bases":["sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc","sha256:8ac98ca534ac3f51e1f420a1dd2c15e74c75cfa0f23f3ad27eb5d7236c349a0c"],"Contexts":{"server":"narrow:d51ca744fd11af59d616c858f76c27b3e58e16e11fbdc0c720eb9d7d19d55fac"},"Toolchains":null,"Unpinned":""},"made":[{"name":"server","kind":"image","reference":"registry.example/build-agent@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc"}],"at":"2026-10-19T09:38:00Z"} +{"id":"build-1792395447088936520","state":"built","repository":"novox/mesh-controller","ref":"main","module":"build-agent","commit":"7a74cee83f88e6585a5511c3cc53bdf0f5303d20","path":"modules/build-agent","manifest-digest":"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef","source-fingerprint":"src1:e846efe73716b78e80860baaac96153de2cb76551325753838ca245d2160a3d7","source-inputs":{"Module":"build-agent","Tree":"11c0b4f90cae0d6e4ef9cc48cbfdf9f6dbe52bca","Bases":["sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc","sha256:8ac98ca534ac3f51e1f420a1dd2c15e74c75cfa0f23f3ad27eb5d7236c349a0c"],"Contexts":{"server":"narrow:d51ca744fd11af59d616c858f76c27b3e58e16e11fbdc0c720eb9d7d19d55fac"},"Toolchains":null,"Unpinned":""},"made":[{"name":"server","kind":"image","reference":"registry.example/build-agent@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc"}],"at":"2026-10-19T09:38:00Z"} -- 2.54.0