From 21414307c8f17e07c336b08a804b7a74c83f9bad Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 17 Oct 2026 15:41:35 +0200 Subject: [PATCH] =?UTF-8?q?issue=201025:=20a=20context=20naming=20the=20mo?= =?UTF-8?q?dule's=20own=20repository=20is=20its=20own=20build=20source=20(?= =?UTF-8?q?ADR=200420=20=C2=A74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/mesh-controller/merge_gate.go | 2 + cmd/mesh-controller/planner_rules_test.go | 32 ++++++++++++ internal/inventory/builds.go | 60 ++++++++++++++++++++++- 3 files changed, 92 insertions(+), 2 deletions(-) diff --git a/cmd/mesh-controller/merge_gate.go b/cmd/mesh-controller/merge_gate.go index a199c4f4..6e6d0fb9 100644 --- a/cmd/mesh-controller/merge_gate.go +++ b/cmd/mesh-controller/merge_gate.go @@ -1267,6 +1267,8 @@ func graphOfFacts(f snapshot.Facts) ([]inventory.Entry, map[string][]inventory.R read[mod.Name] = append(read[mod.Name], inventory.ReadRepository{Own: true, Paths: s.Paths}) } } + // A context naming the module's own repository is its own build source (novox/hq ADR 0420 §4). + read[mod.Name] = inventory.OwnRepositoryAsOwn(read[mod.Name], mod.Repository) } var edges []inventory.Edge for _, e := range f.Edges { diff --git a/cmd/mesh-controller/planner_rules_test.go b/cmd/mesh-controller/planner_rules_test.go index 9e422c3a..71d2b71f 100644 --- a/cmd/mesh-controller/planner_rules_test.go +++ b/cmd/mesh-controller/planner_rules_test.go @@ -680,3 +680,35 @@ func TestAPlanSaysWhyEachModuleIsInIt(t *testing.T) { } } } + +// **A context naming the module's own repository is the module's own build source** (novox/hq ADR 0420 §4, +// issue 1025). A module registered from the controller's repository whose image compiles cmd/x through a +// context naming that same repository is moved by a change to the program, and by none outside its closure. +func TestAContextNamingTheModulesOwnRepositoryIsItsOwnBuildSource(t *testing.T) { + const controllerRepo = "http://forge.internal:20000/novox/mesh-controller.git" + entries := []inventory.Entry{fromRepo("mover", controllerRepo, "modules/mover")} + said := []inventory.ReadRepository{ + {Repository: "novox/mesh-controller", Ref: "main", Paths: []string{"go.mod", "cmd/x/", "internal/y/"}}, + {Own: true, Paths: []string{"modules/mover/module.json"}}, + } + read := map[string][]inventory.ReadRepository{"mover": inventory.OwnRepositoryAsOwn(said, controllerRepo)} + for _, c := range []struct { + what string + paths []string + want string + }{ + {"the program its image compiles", []string{"cmd/x/main.go"}, "mover"}, + {"a package the program imports", []string{"internal/y/y.go"}, "mover"}, + {"its own manifest", []string{"modules/mover/module.json"}, "mover"}, + {"a file outside its closure", []string{"cmd/other/main.go"}, ""}, + } { + if _, got := planMerge(t, "mesh-controller", c.paths, entries, read, nil); got != c.want { + t.Errorf("%s: planned %q, wanted %q", c.what, got, c.want) + } + } + // A context of another repository stays a context read, and a context of this one that said no paths too. + other := []inventory.ReadRepository{{Repository: "novox/mesh-catalog", Paths: []string{"a/"}}, {Repository: "novox/mesh-controller"}} + if got := inventory.OwnRepositoryAsOwn(other, controllerRepo); len(got) != 2 || got[0].Own || got[1].Own { + t.Errorf("a context that is not this repository's with paths became its own: %+v", got) + } +} diff --git a/internal/inventory/builds.go b/internal/inventory/builds.go index 8a11e13c..a6b6c955 100644 --- a/internal/inventory/builds.go +++ b/internal/inventory/builds.go @@ -428,7 +428,8 @@ func (i *Inventory) ReadRepositories(ctx context.Context) (map[string][]ReadRepo } rows, err := i.store.Pool().Query(ctx, - `select distinct on (module) module, built_contexts, build_sources, coalesce(asked, at) + `select distinct on (module) module, built_contexts, build_sources, coalesce(asked, at), + (select source from module where module.name = build.module) from build where module is not null and module <> '' and failed = '' order by module, `+newestRequestFirst) @@ -442,7 +443,8 @@ func (i *Inventory) ReadRepositories(ctx context.Context) (map[string][]ReadRepo var module string var raw, rawSources []byte var built time.Time - if err := rows.Scan(&module, &raw, &rawSources, &built); err != nil { + var own *string + if err := rows.Scan(&module, &raw, &rawSources, &built, &own); err != nil { return nil, err } var of []ReadRepository @@ -462,6 +464,9 @@ func (i *Inventory) ReadRepositories(ctx context.Context) (map[string][]ReadRepo sources, whole = nil, "a newer build of it failed" } of = WithBuildSources(of, sources) + if own != nil { + of = OwnRepositoryAsOwn(of, *own) + } if whole != "" { of = append(of, ReadRepository{Own: true, Whole: whole}) } @@ -498,6 +503,57 @@ func WithBuildSources(read []ReadRepository, sources []BuildSource) []ReadReposi return out } +// OwnRepositoryAsOwn reads a context that names the module's own registered repository, with the paths its +// build read there, as the module's own build source (novox/hq ADR 0420 §4): a change to the program it +// compiles moves the module, as one in its own directory does. A context of that repository that said +// no paths stays a context read, whole. +func OwnRepositoryAsOwn(read []ReadRepository, repository string) []ReadRepository { + if repository == "" { + return read + } + out := make([]ReadRepository, 0, len(read)) + var moved []ReadRepository + for _, r := range read { + if !r.Own && len(r.Paths) > 0 && r.Repository != "" && namesRepository(repository, r.Repository) { + moved = append(moved, r) + continue + } + out = append(out, r) + } + if len(moved) == 0 { + return read + } + // One entry of its own: ownSource reads the first that has paths. + at := -1 + for k := range out { + if out[k].Own && len(out[k].Paths) > 0 { + at = k + break + } + } + if at < 0 { + out = append(out, ReadRepository{Own: true}) + at = len(out) - 1 + } + for _, r := range moved { + out[at].Paths = append(append([]string(nil), out[at].Paths...), r.Paths...) + if out[at].Ref == "" { + out[at].Ref = r.Ref + } + } + return out +} + +// namesRepository is whether a context's repository, as owner/name, is the registered one, which may be +// spelled as the URL it was cloned from. +func namesRepository(registered, named string) bool { + trim := func(s string) string { + return strings.TrimSuffix(strings.TrimRight(strings.ToLower(strings.TrimSpace(s)), "/"), ".git") + } + reg, name := trim(registered), trim(named) + return reg == name || strings.HasSuffix(reg, "/"+name) +} + // manifestOrNil keeps the difference between "declared nothing" and "predates this being kept". // // A build recorded before the mesh kept manifests has no manifest, and that is not the same as one