Merge pull request 'The merge gate judges a moved module as a real build hands it its templates (issue 876)' (#353) from fix/876-gate-judges-moved-modules into feat/57-config-and-provisioning
mesh/merge-gate head 6a6628a76961 merged into trunk d70bb1f38867 — fail: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus…
mesh/repo-check head 6a6628a76961 merged into trunk d70bb1f38867 — fail: head 6a6628a76961 does not merge cleanly into trunk d70bb1f38867: in conflict cmd/…
mesh/delivery-group group feat/57-config-and-provisioning rejected: a member's own check failed
mesh/delivery stopped: the pull request closed unmerged

This commit was merged in pull request #353.
This commit is contained in:
2026-10-16 08:14:05 +00:00
3 changed files with 52 additions and 5 deletions
+18 -2
View File
@@ -585,11 +585,18 @@ func shelfWithChange(base map[string]catalogue.Manifest, sources map[string]inve
return err
}
m, err := catalogue.ParseManifestWith(raw, predefined)
// The manifest as written, still naming its templates: what the stand-in build resolves, as a real build
// does, so a moved module is handed its templates (issue 876).
written := m
var texts map[string]string
if err == nil {
// Each template's text from the tree beside the manifest, as the build reads it back from the archive it
// packs (novox/hq ADR 0349 §2): so a change to a template alone is a change to its module.
m, err = catalogue.WithTemplatesFrom(m, filepath.Dir(path))
}
if err == nil && written.Templated() {
texts, err = catalogue.ReadTemplates(written, filepath.Dir(path))
}
if err != nil {
if inScope(dir) {
failures = append(failures, fmt.Sprintf("%s: the controller judging this (%s) refuses it — %s",
@@ -614,7 +621,7 @@ func shelfWithChange(base map[string]catalogue.Manifest, sources map[string]inve
"of one module name (issue 239)", orRoot(dir), m.Module, s.Repository))
return nil
}
resolved, err := standInBuild(m)
resolved, err := standInBuild(written, texts)
if err == nil {
// As registration reads a built manifest: strictly, again (takeIn).
var raw []byte
@@ -737,15 +744,24 @@ func builtAlike(m catalogue.Manifest) string {
// standInBuild resolves a manifest as a build would, with artifacts nobody built: a digest made from the
// module's and the artifact's names, in the store's own vocabulary. Composition and validation read the
// shape of a reference, never its bytes.
func standInBuild(m catalogue.Manifest) (catalogue.Manifest, error) {
func standInBuild(m catalogue.Manifest, texts map[string]string) (catalogue.Manifest, error) {
if m.Build == nil {
return m.Resolve(nil)
}
// The templates archive carries each template's text by its path inside the archive, as a build reads it back.
archive, inside := catalogue.TemplatesArtifact(m), m.TemplatesDir()+"/"
read := map[string]string{}
for template, text := range texts {
read[strings.TrimPrefix(template, inside)] = text
}
var built []catalogue.Built
for _, a := range m.Build.Artifacts {
sum := sha256.Sum256([]byte("gate\x00" + m.Module + "\x00" + a.Name))
digest := fmt.Sprintf("sha256:%x", sum)
b := catalogue.Built{Name: a.Name, Kind: a.Kind, Digest: digest}
if a.Name == archive && archive != "" {
b.Texts = read
}
switch a.Kind {
case catalogue.ArtifactImage, catalogue.ArtifactUpstream:
b.Reference = catalogue.ArtifactStoreScheme + m.Module + "/" + a.Name + "@" + digest
+33 -2
View File
@@ -185,11 +185,11 @@ func TestTheMergeGateResolvesATemplatedModuleToItsInlineTwin(t *testing.T) {
if err != nil {
t.Fatal(err)
}
m, err = catalogue.WithTemplatesFrom(m, dir)
texts, err := catalogue.ReadTemplates(m, dir)
if err != nil {
t.Fatal(err)
}
resolved, err := standInBuild(m)
resolved, err := standInBuild(m, texts)
if err != nil {
t.Fatal(err)
}
@@ -239,3 +239,34 @@ func TestModuleCheckReadsTemplatesFromTheTemplatesDirectoryTheManifestNames(t *t
t.Fatalf("a template outside the templates directory the manifest names passed:\n%s", got.String())
}
}
// Issue 876: the gate judges a moved module that names its templates as registration reads it after a real build,
// handed its templates; it refused every such module as "marked moved and names no template". This calls the gate's
// own shelf (shelfWithChange), which needs no database.
func TestReplay876(t *testing.T) {
moved := strings.Replace(templatedNotifier, `"version":"1",`, `"version":"1","moved":true,`, 1)
judge := func(manifest string, files map[string]string) (map[string]catalogue.Manifest, []string) {
tree := t.TempDir()
aTemplatedModule(t, filepath.Join(tree, "modules", "notifier"), manifest, files)
out, failures, err := shelfWithChange(map[string]catalogue.Manifest{}, nil, snapshot.Facts{},
"novox/mesh-catalog", tree, map[string]string{}, nil)
if err != nil {
t.Fatal(err)
}
return out, failures
}
out, failures := judge(moved, map[string]string{"files/notifier.conf": "volume = 3\n"})
if len(failures) > 0 {
t.Fatalf("a moved module naming its templates was refused: %v", failures)
}
if h := out["notifier"].Handed; h == nil || h.Templates["conf"] != "notifier.conf" {
t.Fatalf("the gate did not hand the moved module its templates: %+v", h)
}
// A moved module naming no template is still refused.
none := strings.Replace(strings.Replace(moved, `"template":"files/notifier.conf"`,
`"content":"volume = 3\n"`, 1), `"build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]},`, "", 1)
if _, failures := judge(none, nil); len(failures) != 1 || !strings.Contains(failures[0], "names no template") {
t.Fatalf("a moved module naming no template was not refused: %v", failures)
}
}
+1 -1
View File
@@ -61,7 +61,7 @@ func consumersPlaceholderProblems(m Manifest, r map[string]any) []string {
// client alone fills it, so on a machine that does not hand the module it is **left out** of the declaration, never
// written as that text or as an empty list the provider would read as no consumers, and never the reason the module is
// refused: a provider not handed there keeps working as it did, from its grant files (novox/hq ADR 0373, issue 780).
// A moved module judged from its repository's tree (WithTemplatesFrom: `module check`, the merge gate), on a machine
// A moved module judged from its repository's tree (WithTemplatesFrom: `module check`), on a machine
// that would hand it, has no templates archive to hand and is judged as it composes; there the placeholder is judged
// and taken out of the text, since what is composed so is never sent (judged answers whether the file is so judged).
func consumersHandedOnly(m Manifest, r map[string]any, with Rendering) (judged, leftOut bool) {