From 0cd95c69f0f43314aacde6d529c952d973b3dfbf Mon Sep 17 00:00:00 2001 From: mesh-admin Date: Wed, 14 Oct 2026 19:07:41 +0000 Subject: [PATCH] Bring F3 up to K2 on its release branch (part 1 of 3) The release branch's head is fcdecb43, with K2 (#323); 920ba34b was brought up to an older head. Co-Authored-By: Claude Opus 5.5 --- cmd/mesh-builder/main.go | 5 + cmd/mesh-controller/build.go | 38 ++++- cmd/mesh-controller/check.go | 26 +++- cmd/mesh-controller/merge_gate.go | 15 +- cmd/mesh-controller/predefined.go | 93 ++++++++++++ cmd/mesh-controller/predefined_test.go | 197 +++++++++++++++++++++++++ cmd/mesh-controller/stores.go | 6 + internal/builder/builder.go | 53 ++++++- internal/builder/builder_test.go | 2 + 9 files changed, 426 insertions(+), 9 deletions(-) create mode 100644 cmd/mesh-controller/predefined.go create mode 100644 cmd/mesh-controller/predefined_test.go diff --git a/cmd/mesh-builder/main.go b/cmd/mesh-builder/main.go index be469d09..985e8652 100644 --- a/cmd/mesh-builder/main.go +++ b/cmd/mesh-builder/main.go @@ -424,6 +424,11 @@ func answer(ctx context.Context, publisher builder.Publisher, on, workspace stri for _, s := range built.Sources { result.Sources = append(result.Sources, link.BuildSource{Repository: s.Repository, Ref: s.Ref, Paths: s.Paths}) } + // The list of predefined keys the manifest was judged against, for the controller to publish + // (novox/hq ADR 0348 §7). + if p := built.Predefined; p != nil { + result.Predefined = &link.PredefinedList{Keys: p.Keys, Generation: p.Generation} + } say("built", built.Manifest.Module+" from "+short(built.Commit)) } } diff --git a/cmd/mesh-controller/build.go b/cmd/mesh-controller/build.go index d7eb9c2d..ae337692 100644 --- a/cmd/mesh-controller/build.go +++ b/cmd/mesh-controller/build.go @@ -183,8 +183,10 @@ func buildFrom(result link.BuildResult) inventory.Build { kept.Manifest = recordedManifest(result.Manifest, announced) // The module name comes from the manifest, which only exists when the build got that far. if len(kept.Manifest) > 0 { - if m, err := catalogue.ParseManifest(kept.Manifest); err == nil { - kept.Module = m.Module + if list, err := judgedWith(result); err == nil { + if m, err := catalogue.ParseManifestWith(kept.Manifest, list); err == nil { + kept.Module = m.Module + } } } return kept @@ -603,6 +605,11 @@ func saysWhenThePolicyActs(ctx context.Context, inv *inventory.Inventory, module // written with the same values. func takeIn(ctx context.Context, inv *inventory.Inventory, result link.BuildResult) ( catalogue.Manifest, inventory.Build, error) { + // The list of predefined keys as the store keeps it now: another controller process may have kept a newer one. + if err := loadPredefined(ctx, inv); err != nil { + return catalogue.Manifest{}, inventory.Build{}, fmt.Errorf("the list of predefined keys kept cannot be "+ + "read: %w", err) + } kept := buildFrom(result) if err := inv.RecordBuild(ctx, kept); err != nil { return catalogue.Manifest{}, kept, err @@ -613,7 +620,15 @@ func takeIn(ctx context.Context, inv *inventory.Inventory, result link.BuildResu return catalogue.Manifest{}, kept, fmt.Errorf("%s could not build %s:\n%s", result.On, result.Repository, result.Failed) } - manifest, err := catalogue.ParseManifest(kept.Manifest) + // Judged first against the list the build was judged against: the catalogue's at the commit built (novox/hq + // ADR 0348 §7), so a module naming a key its own commit adds is read; and again below against the list the + // mesh publishes, before it is registered. + list, err := judgedWith(result) + if err != nil { + return catalogue.Manifest{}, kept, fmt.Errorf("%s built %s (%s), recorded and not registered: %w", result.On, + result.Repository, short(result.Commit), err) + } + manifest, err := catalogue.ParseManifestWith(kept.Manifest, list) if err != nil { return catalogue.Manifest{}, kept, fmt.Errorf("%s built %s and what came back is not a manifest: %w", result.On, result.Repository, err) @@ -687,6 +702,23 @@ func takeIn(ctx context.Context, inv *inventory.Inventory, result link.BuildResu "back: it is recorded and not registered again — a newer build is", result.On, manifest.Module, short(result.Commit)) } + // **The newest trunk build's list of predefined keys is published** (novox/hq ADR 0348 §7): a build of the + // catalogue that may be registered keeps its list when its commit is newer than the one kept. Then the module + // is judged against the list the mesh publishes, as every manifest read again is: one naming a key that list + // does not hold, or of another type, is refused here and never enters the catalogue. + if carried, err := carriedList(result); err != nil { + return manifest, kept, err + } else if carried != nil { + if err := publishPredefined(ctx, inv, result, carried); err != nil { + fmt.Printf("%s: the list of predefined keys it carried is not published: %v\n", result.ID, err) + } + } + if manifest.NamesPredefined() { + if _, err := catalogue.ParseManifest(kept.Manifest); err != nil { + return manifest, kept, fmt.Errorf("%s built %s (%s), recorded and not registered: judged against the "+ + "list of predefined keys the mesh publishes, %w", result.On, manifest.Module, short(result.Commit), err) + } + } // **A build whose source is unchanged is never a move** (novox/hq issue 280): a rebuild made from // what the build the mesh stands on was made from registers that build's artifacts at the new // commit, so no machine is sent a new digest for a source nobody changed — an image is not diff --git a/cmd/mesh-controller/check.go b/cmd/mesh-controller/check.go index 64b04429..cd7d57bc 100644 --- a/cmd/mesh-controller/check.go +++ b/cmd/mesh-controller/check.go @@ -47,6 +47,24 @@ func moduleCheckFor(paths []string, longestMachine int, out io.Writer) error { shelf := catalogue.Shelf{} faulted := map[string]bool{} failed := 0 + // The list of predefined keys at the root of the repository above each manifest (novox/hq ADR 0348 §7), read + // once per repository: a manifest naming a key is judged against the list its own tree holds. + lists := map[string]catalogue.PredefinedList{} + listOf := func(path string) (catalogue.PredefinedList, error) { + root := catalogue.RepositoryRoot(filepath.Dir(path)) + if root == "" { + return nil, nil + } + if list, read := lists[root]; read { + return list, nil + } + list, err := catalogue.ReadPredefined(root) + if err != nil { + return nil, err + } + lists[root] = list + return list, nil + } for _, path := range paths { raw, err := os.ReadFile(path) if err != nil { @@ -54,7 +72,13 @@ func moduleCheckFor(paths []string, longestMachine int, out io.Writer) error { failed++ continue } - m, err := catalogue.ParseManifest(raw) + list, err := listOf(path) + if err != nil { + fmt.Fprintf(out, "%s: %v\n", path, err) + failed++ + continue + } + m, err := catalogue.ParseManifestWith(raw, list) if err == nil { // Each template's text read from the tree beside the manifest, and judged as inline content is // (novox/hq ADR 0349 §2): a missing template is refused by name. diff --git a/cmd/mesh-controller/merge_gate.go b/cmd/mesh-controller/merge_gate.go index 78b5332a..b555e4e5 100644 --- a/cmd/mesh-controller/merge_gate.go +++ b/cmd/mesh-controller/merge_gate.go @@ -550,8 +550,17 @@ func shelfWithChange(base map[string]catalogue.Manifest, sources map[string]inve out[k] = m } var failures []string + // The list of predefined keys at the tree's root (novox/hq ADR 0348 §7): what the build of this tree judges each + // manifest against and registration publishes, so what the gate judges each against too. A list that is + // wrong is refused by name, and every manifest is then judged against none. + predefined, err := catalogue.ReadPredefined(tree) + if err != nil { + failures = append(failures, fmt.Sprintf("%s: the controller judging this (%s) refuses it — %s", + catalogue.PredefinedFile, version, oneLine(err.Error()))) + predefined = nil + } found := map[string]string{} // module → its directory in the tree - err := filepath.WalkDir(tree, func(path string, d fs.DirEntry, err error) error { + err = filepath.WalkDir(tree, func(path string, d fs.DirEntry, err error) error { if err != nil { return err } @@ -575,7 +584,7 @@ func shelfWithChange(base map[string]catalogue.Manifest, sources map[string]inve if err != nil { return err } - m, err := catalogue.ParseManifest(raw) + m, err := catalogue.ParseManifestWith(raw, predefined) 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. @@ -610,7 +619,7 @@ func shelfWithChange(base map[string]catalogue.Manifest, sources map[string]inve // As registration reads a built manifest: strictly, again (takeIn). var raw []byte if raw, err = json.Marshal(resolved); err == nil { - resolved, err = catalogue.ParseManifest(raw) + resolved, err = catalogue.ParseManifestWith(raw, predefined) } } if err == nil { diff --git a/cmd/mesh-controller/predefined.go b/cmd/mesh-controller/predefined.go new file mode 100644 index 00000000..7b93b748 --- /dev/null +++ b/cmd/mesh-controller/predefined.go @@ -0,0 +1,93 @@ +package main + +import ( + "context" + "fmt" + "path" + "strings" + + "github.com/novox/mesh-controller/internal/catalogue" + "github.com/novox/mesh-controller/internal/inventory" + "github.com/novox/mesh-controller/internal/link" +) + +// The list of predefined keys the mesh publishes (novox/hq ADR 0348 §7, design 57 package K2, issue 661). +// +// **The newest trunk build of the catalogue wins.** A build of the catalogue reads predefined.json at the commit +// it builds and carries it with the commit's generation (link.PredefinedList). Registration keeps it in the +// controller's store when that generation is larger than the one kept (inventory.KeepPredefined), so an older +// build's list heard later never replaces a newer one, and every manifest the controller reads again is judged +// against the list kept (catalogue.UsePredefined). mesh-config publishes it on the bus from there (design 57 +// package K3); the controller keeps no config value, only the keys. + +// fromTheCatalogue is whether a build is of the catalogue's repository, whose list alone the mesh publishes: by +// its name, in the URL form or the seat form. +func fromTheCatalogue(result link.BuildResult) bool { + for _, repository := range []string{result.Repository, sourceRepository(result)} { + name := strings.TrimSuffix(path.Base(strings.TrimRight(repository, "/")), ".git") + if name == catalogue.CatalogueRepository { + return true + } + } + return false +} + +func sourceRepository(result link.BuildResult) string { + if result.Source == nil { + return "" + } + return result.Source.Repository +} + +// carriedList is the list a build of the catalogue carried, read strictly; nil for a build of another +// repository, or one that carried none. +func carriedList(result link.BuildResult) (catalogue.PredefinedList, error) { + if result.Predefined == nil || !fromTheCatalogue(result) { + return nil, nil + } + list, err := catalogue.ParsePredefined(result.Predefined.Keys) + if err != nil { + return nil, fmt.Errorf("%s carried a list of predefined keys that is refused: %w", result.ID, err) + } + return list, nil +} + +// judgedWith is the list a build's manifest is first judged against at registration: the catalogue's own at the +// commit built, which its build judged it against, where it carried one; the list kept otherwise. +func judgedWith(result link.BuildResult) (catalogue.PredefinedList, error) { + list, err := carriedList(result) + if err != nil || list != nil { + return list, err + } + return catalogue.PublishedPredefined(), nil +} + +// loadPredefined makes the list kept in the store the one this controller judges manifests against. A store +// keeping none leaves the list it has. +func loadPredefined(ctx context.Context, inv *inventory.Inventory) error { + kept, has, err := inv.PredefinedKept(ctx) + if err != nil || !has { + return err + } + catalogue.UsePredefined(kept.Keys) + return nil +} + +// publishPredefined keeps a trunk build's list when it is newer than the one kept, says so, and loads what the +// store keeps then — this build's, or a newer one another controller process kept. +func publishPredefined(ctx context.Context, inv *inventory.Inventory, result link.BuildResult, + list catalogue.PredefinedList) error { + kept, err := inv.KeepPredefined(ctx, inventory.PredefinedRecord{Keys: list, Repository: result.Repository, + Commit: result.Commit, Generation: result.Predefined.Generation, Build: result.ID}) + if err != nil { + return err + } + if kept { + fmt.Printf("%s: the list of predefined keys at %s (generation %d) is published: %s\n", result.ID, + short(result.Commit), result.Predefined.Generation, strings.Join(list.Keys(), ", ")) + } else { + fmt.Printf("%s: the list of predefined keys at %s (generation %d) is not newer than the one kept, which "+ + "stands\n", result.ID, short(result.Commit), result.Predefined.Generation) + } + return loadPredefined(ctx, inv) +} diff --git a/cmd/mesh-controller/predefined_test.go b/cmd/mesh-controller/predefined_test.go new file mode 100644 index 00000000..25f09c50 --- /dev/null +++ b/cmd/mesh-controller/predefined_test.go @@ -0,0 +1,197 @@ +package main + +import ( + "bytes" + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/novox/mesh-controller/internal/catalogue" + "github.com/novox/mesh-controller/internal/link" +) + +// The list of predefined keys (novox/hq ADR 0348 §7, design 57 package K2, issue 661): `module check` and the +// merge gate judge each manifest against the list at the root of its tree; registration publishes the newest +// trunk build's list of the catalogue and judges every module against the list the mesh publishes. + +const theFirstKeys = `{ + "font-size": {"type": "number", "description": "the size text is shown in, in points", "minimum": 6, "maximum": 72}, + "agent-managed-settings": {"type": "object", "description": "the managed settings of every agent session, a JSON object", "trusted": true} +}` + +// notifierFallingBackTo is the notifier in the config form, its font size falling back to the key named. +func notifierFallingBackTo(key, kind string) string { + def := "10" + if kind == "string" { + def = `"10"` + } + return `{"module":"notifier","version":"1", + "config":{"font-size":{"type":"` + kind + `","description":"the notification's font size, in points", + "default":` + def + `,"predefined":"` + key + `"}}, + "resources":[{"id":"conf","type":"file","path":"/etc/notifier/notifier.conf","mode":"0644", + "content":"font = ${config:font-size}\n"}]}` +} + +// withoutAPublishedList judges the test against no list the mesh published, and puts the process's back after. +func withoutAPublishedList(t *testing.T) { + t.Helper() + was := catalogue.ReplacePublished(nil) + t.Cleanup(func() { catalogue.ReplacePublished(was) }) +} + +// aRepositoryTree is a repository's checkout holding one module at modules/notifier, and the list when given. +func aRepositoryTree(t *testing.T, manifest string, list *string) string { + t.Helper() + root := t.TempDir() + if err := os.Mkdir(filepath.Join(root, ".git"), 0o755); err != nil { + t.Fatal(err) + } + if list != nil { + if err := os.WriteFile(filepath.Join(root, catalogue.PredefinedFile), []byte(*list), 0o644); err != nil { + t.Fatal(err) + } + } + return aTemplatedModule(t, filepath.Join(root, "modules", "notifier"), manifest, nil) +} + +func TestModuleCheckJudgesAManifestAgainstTheListAtItsRepositoryRoot(t *testing.T) { + withoutAPublishedList(t) + list := theFirstKeys + check := func(path string) (string, error) { + var out bytes.Buffer + err := moduleCheck([]string{path}, &out) + return out.String(), err + } + if out, err := check(aRepositoryTree(t, notifierFallingBackTo("font-size", "number"), &list)); err != nil || + !strings.Contains(out, "notifier: ok") { + t.Fatalf("a module naming font-size was refused: %v\n%s", err, out) + } + for _, c := range []struct { + manifest string + list *string + says string + }{ + {notifierFallingBackTo("font-scale", "number"), &list, `"font-scale", which is not on the list`}, + {notifierFallingBackTo("font-size", "string"), &list, "which is a number, and this value is a string"}, + {notifierFallingBackTo("font-size", "number"), nil, "is not present"}, + {notifierFallingBackTo("font-size", "number"), ptr(`{"font-size": {"type": "number"}}`), "no description"}, + } { + if out, err := check(aRepositoryTree(t, c.manifest, c.list)); err == nil || !strings.Contains(out, c.says) { + t.Errorf("wanted the check refused saying %q, got %v:\n%s", c.says, err, out) + } + } +} + +func ptr(s string) *string { return &s } + +// writeTheList puts the list at the root of a tree the gate judges. +func writeTheList(t *testing.T, tree, list string) string { + t.Helper() + if err := os.WriteFile(filepath.Join(tree, catalogue.PredefinedFile), []byte(list), 0o644); err != nil { + t.Fatal(err) + } + return tree +} + +func TestTheMergeGateJudgesEachManifestAgainstTheListOfTheTree(t *testing.T) { + withoutAPublishedList(t) + f, manifests := templatedMesh(t) + + v := gateJudged(t, f, writeTheList(t, aTree(t, withEdit(manifests, "notifier", + notifierFallingBackTo("font-size", "number"))), theFirstKeys), "modules/notifier/module.json") + if v.Verdict == "fail" || v.Modules["notifier"] != "changed" { + t.Fatalf("a module naming font-size on the tree's list failed the gate (%v):\n%s", v.Modules, v.Report()) + } + for _, c := range []struct{ manifest, list, says string }{ + {notifierFallingBackTo("font-scale", "number"), theFirstKeys, `"font-scale", which is not on the list`}, + {notifierFallingBackTo("font-size", "string"), theFirstKeys, "which is a number"}, + {inlineNotifier, `{"font-size": {"type": "number", "description": "points", "unit": "pt"}}`, `unknown field "unit"`}, + } { + v := gateJudged(t, f, writeTheList(t, aTree(t, withEdit(manifests, "notifier", c.manifest)), c.list), + "modules/notifier/module.json", catalogue.PredefinedFile) + if v.Verdict != "fail" || !strings.Contains(v.Report(), c.says) { + t.Errorf("wanted the gate to fail saying %q:\n%s", c.says, v.Report()) + } + } +} + +// catalogueBuild is a trunk build of the catalogue carrying the list at a generation. +func catalogueBuild(id, manifest, list string, generation int) link.BuildResult { + r := link.BuildResult{ID: id, Repository: "http://forge.internal:20000/novox/mesh-catalog.git", + Path: "modules/notifier", Ref: "main", On: "anchor", Commit: strings.Repeat(id[len(id)-1:], 12), + Manifest: json.RawMessage(manifest), Source: &link.SourceOnSeat{Seat: "git", Repository: "novox/mesh-catalog"}} + if list != "" { + r.Predefined = &link.PredefinedList{Keys: json.RawMessage(list), Generation: generation} + } + return r +} + +func TestRegistrationPublishesTheNewestTrunkListAndJudgesEveryModuleAgainstIt(t *testing.T) { + withoutAPublishedList(t) + open := aMesh(t) + ctx := t.Context() + + // A manifest naming font-size registers, and its build's list is published. + if _, _, err := takeIn(ctx, open.inventory, asTheOperator(t, open.inventory, + catalogueBuild("b-1", notifierFallingBackTo("font-size", "number"), theFirstKeys, 10))); err != nil { + t.Fatalf("a module naming font-size was not registered: %v", err) + } + kept, has, err := open.inventory.PredefinedKept(ctx) + if err != nil || !has || kept.Generation != 10 || kept.Build != "b-1" { + t.Fatalf("the list was not published: %+v %v %v", kept, has, err) + } + if got := catalogue.PublishedPredefined(); len(got) != 2 { + t.Fatalf("the controller judges against %v, not the list published", got) + } + + // One naming a key not on the list, or of another type, is refused and never enters the catalogue. + for i, c := range []struct{ manifest, says string }{ + {notifierFallingBackTo("font-scale", "number"), "not on the list"}, + {notifierFallingBackTo("font-size", "string"), "which is a number"}, + } { + id := []string{"b-2", "b-3"}[i] + if _, _, err := takeIn(ctx, open.inventory, catalogueBuild(id, c.manifest, theFirstKeys, 10)); err == nil || + !strings.Contains(err.Error(), c.says) { + t.Errorf("wanted the registration refused saying %q, got %v", c.says, err) + } + } + src, err := open.inventory.SourceOf(ctx, "notifier") + if err != nil || src.BuiltFrom != strings.Repeat("1", 12) { + t.Fatalf("a refused build replaced the registered one: %+v %v", src, err) + } + + // A newer build's list, which drops agent-managed-settings, replaces the published one. + smaller := `{"font-size": {"type": "number", "description": "points", "minimum": 6, "maximum": 72}}` + if _, _, err := takeIn(ctx, open.inventory, catalogueBuild("b-4", + notifierFallingBackTo("font-size", "number"), smaller, 12)); err != nil { + t.Fatal(err) + } + // The older build's outcome heard later registers nothing new about the list: the newer stands. + if _, _, err := takeIn(ctx, open.inventory, catalogueBuild("b-5", + notifierFallingBackTo("font-size", "number"), theFirstKeys, 11)); err != nil { + t.Fatal(err) + } + kept, _, _ = open.inventory.PredefinedKept(ctx) + if kept.Build != "b-4" || len(kept.Keys) != 1 || len(catalogue.PublishedPredefined()) != 1 { + t.Fatalf("an older build's list heard later replaced the newer one: %+v", kept) + } + + // A build of another repository publishes nothing, and its manifest is judged against the published list. + other := catalogueBuild("b-6", notifierFallingBackTo("font-size", "number"), theFirstKeys, 99) + other.Repository, other.Source.Repository = "http://forge.internal:20000/novox/notifier.git", "novox/notifier" + other.Manifest = json.RawMessage(strings.Replace(notifierFallingBackTo("font-size", "number"), `"notifier"`, + `"chimes"`, 1)) + if _, _, err := takeIn(ctx, open.inventory, asTheOperator(t, open.inventory, other)); err != nil { + t.Fatalf("a module of another repository naming a published key: %v", err) + } + if kept, _, _ = open.inventory.PredefinedKept(ctx); kept.Build != "b-4" { + t.Fatalf("a build of another repository published its list: %+v", kept) + } + other.ID, other.Manifest = "b-7", json.RawMessage(strings.Replace(notifierFallingBackTo("agent-managed-settings", + "number"), `"notifier"`, `"chimes"`, 1)) + if _, _, err := takeIn(ctx, open.inventory, other); err == nil || !strings.Contains(err.Error(), "not on the list") { + t.Fatalf("a key only another repository's list holds was registered: %v", err) + } +} diff --git a/cmd/mesh-controller/stores.go b/cmd/mesh-controller/stores.go index aeea12c1..f2db129a 100644 --- a/cmd/mesh-controller/stores.go +++ b/cmd/mesh-controller/stores.go @@ -3,6 +3,7 @@ package main import ( "context" "fmt" + "os" "time" "github.com/novox/mesh-controller/internal/catalogue" @@ -124,6 +125,11 @@ func openInventory(ctx context.Context) (*inventory.Inventory, error) { if aliases, err := inv.Aliases(ctx); err == nil { catalogue.UseAliases(aliases) } + // And the list of predefined keys the mesh publishes (novox/hq ADR 0348 §7), which every manifest read again + // is judged against. A store a migration has not reached yet keeps none, and nothing names a key then. + if err := loadPredefined(ctx, inv); err != nil { + fmt.Fprintf(os.Stderr, "the list of predefined keys kept cannot be read: %v\n", err) + } return inv, nil } diff --git a/internal/builder/builder.go b/internal/builder/builder.go index 728ce83e..7b9b5f58 100644 --- a/internal/builder/builder.go +++ b/internal/builder/builder.go @@ -108,6 +108,17 @@ type Result struct { // recipe and the package it compiles; for any other, nothing is said and the whole of what the build // sees stays its source, as before. Sources []BuildSource + + // Predefined is the list of predefined keys at the root of the repository at the commit built (novox/hq ADR + // 0348 §7), which the manifest was judged against; nil for a repository that holds none. + Predefined *Predefined +} + +// Predefined is the list of predefined keys a build read: its text, canonical (catalogue.PredefinedList), and +// the generation of the commit built — how many commits it stands on, which orders two builds of one trunk. +type Predefined struct { + Keys []byte + Generation int } // BuildSource is the build source a build read in one repository (novox/hq ADR 0267). @@ -261,11 +272,28 @@ func build(ctx context.Context, run Runner, publish Publisher, "%s has no %s at %s, so there is nothing saying what it is: %w", repository, ManifestName, describe(path), err) } - manifest, err := catalogue.ParseManifest(raw) + // **The list of predefined keys at the commit built** (novox/hq ADR 0348 §7): predefined.json at the + // repository's root, read strictly; a list that is wrong fails the build, and the manifest is judged against + // the list of its own tree, never against one the mesh published from another commit. + predefined, err := catalogue.ReadPredefined(tree) + if err != nil { + say("predefined", "INVALID: %v", err) + return Result{}, err + } + manifest, err := catalogue.ParseManifestWith(raw, predefined) if err != nil { say("manifest", "INVALID: %v", err) return Result{}, err } + var listed *Predefined + if predefined != nil { + text, err := predefined.Canonical() + if err != nil { + return Result{}, err + } + listed = &Predefined{Keys: text, Generation: generationOf(ctx, run, tree, commit)} + say("predefined", "%d key(s) at %s, generation %d", len(predefined), short(commit), listed.Generation) + } // What is judged only where a definition is written (novox/hq ADR 0326 §3, issue 574): a build is // where a changed definition is first read, so it is refused here before anything is made of it. // Judged with each template's text in its file, as written in the tree (novox/hq ADR 0349 §2): a template the @@ -291,6 +319,11 @@ func build(ctx context.Context, run Runner, publish Publisher, if src.tree, err = gitTree(ctx, run, tree, path); err != nil { src.notPinned("its tree could not be named: " + err.Error()) } + // A module falling back to a predefined key was judged against the list, so a change to the list is a change + // to it: the planner rebuilds it, and its build publishes the list (ADR 0348 §7). + if predefined != nil && manifest.NamesPredefined() { + src.rootHas(catalogue.PredefinedFile) + } // A build-time credential, written into the build context as .npmrc, but ONLY for a module that // asks for it: a `package` artifact (which publishes), or an image whose Dockerfile COPYs .npmrc. @@ -451,7 +484,23 @@ func build(ctx context.Context, run Runner, publish Publisher, return Result{Manifest: resolved, Commit: commit, Built: built, Against: against(within, manifest, stoodOn), Read: readBy(manifest), Source: fingerprint, Trunk: trunk, OnTrunk: onTrunk, Branches: branches, Sources: sources, - ReuseKey: key.Key, ReuseParts: key.Parts, NoKey: key.Why, Reused: reused, Why: builtWhy}, nil + ReuseKey: key.Key, ReuseParts: key.Parts, NoKey: key.Why, Reused: reused, Why: builtWhy, + Predefined: listed}, nil +} + +// generationOf is how many commits the commit built stands on, itself included: of two commits on one trunk the +// later has more, so the controller keeps the list of the newer one whatever order it hears them in (ADR 0348 +// §7). 0 when git could not say. +func generationOf(ctx context.Context, run Runner, tree, commit string) int { + out, err := run(ctx, tree, "git", "rev-list", "--count", commit) + if err != nil { + return 0 + } + n, err := strconv.Atoi(strings.TrimSpace(out)) + if err != nil || n < 0 { + return 0 + } + return n } // takeACheckBuild is the walk's look for a check build of its key (novox/hq ADR 0350 §2, §3): the key taken by diff --git a/internal/builder/builder_test.go b/internal/builder/builder_test.go index afe57572..4bfe447f 100644 --- a/internal/builder/builder_test.go +++ b/internal/builder/builder_test.go @@ -83,6 +83,8 @@ func (r *recorded) run(_ context.Context, dir, name string, args ...string) (str return r.tree + "\n", nil case name == "git" && len(args) > 0 && args[0] == "rev-parse": return "c0ffeec0ffeec0ffeec0ffeec0ffeec0ffeec0ff\n", nil + case name == "git" && len(args) > 1 && args[0] == "rev-list" && args[1] == "--count": + return "42\n", nil } return "", nil }