diff --git a/internal/catalogue/build.go b/internal/catalogue/build.go index 1bc31c6..930c69a 100644 --- a/internal/catalogue/build.go +++ b/internal/catalogue/build.go @@ -436,3 +436,33 @@ func BinaryOf(a Artifact) string { } return a.Name } + +// undeliveredBundles says which of a module's bundles nothing would ever put on a machine (novox/hq +// 04-ISSUES/216). A bundle reaches a machine three ways: the node's runtime serves it (it says +// `loads`, or its module declares `tools`), a resource names it (a process, a step, an archive), or +// it is the runtime itself. One reached by none of them was built, recorded and pushed as success, +// and was simply absent — seven modules' tools went missing that way on 2026-10-03. Refused here, +// naming the field that would deliver it. +func undeliveredBundles(m Manifest) []string { + if m.Build == nil || m.Module == RuntimeModule { + return nil + } + named := map[string]bool{} + for _, r := range m.Resources { + if a, ok := r["artifact"].(string); ok && a != "" { + named[a] = true + } + } + var problems []string + for _, a := range m.Build.Artifacts { + if a.Kind != ArtifactBundle || named[a.Name] || len(a.Loads) > 0 || len(m.Tools) > 0 { + continue + } + problems = append(problems, fmt.Sprintf( + "%s: the bundle %q would be built and never reach a machine: nothing loads it, runs it or "+ + "unpacks it. A tools bundle says `loads` (the entrypoints the node's runtime serves) or its "+ + "module lists its `tools`; a daemon or a step is a resource naming it (novox/hq 04-ISSUES/216)", + m.Module, a.Name)) + } + return problems +} diff --git a/internal/catalogue/manifest.go b/internal/catalogue/manifest.go index 01686d5..aef8497 100644 --- a/internal/catalogue/manifest.go +++ b/internal/catalogue/manifest.go @@ -1373,6 +1373,7 @@ func ParseManifest(raw []byte) (Manifest, error) { } } problems = append(problems, m.Build.problems(m.Module)...) + problems = append(problems, undeliveredBundles(m)...) // **What provides the artifact store cannot be delivered through it** (novox/hq 04-ISSUES/029). // // Building publishes to the store, and the builder will not start without one. So a module diff --git a/internal/catalogue/runtime_test.go b/internal/catalogue/runtime_test.go index fa4dc43..b7f2173 100644 --- a/internal/catalogue/runtime_test.go +++ b/internal/catalogue/runtime_test.go @@ -389,3 +389,28 @@ func TestARuntimeCompiledToABinaryRunsItself(t *testing.T) { t.Errorf("the Go runtime is not told what to serve or whose it is: %v %v", env, process["user"]) } } + +// novox/hq 04-ISSUES/216: a bundle nothing loads, runs or unpacks is refused at registration; saying +// `loads`, listing `tools`, or a resource naming it admits it. +func TestABundleNothingDeliversIsRefused(t *testing.T) { + base := func() Manifest { + return Manifest{Module: "baserow", Version: "1", Build: &Build{Artifacts: []Artifact{ + {Name: "tools", Kind: ArtifactBundle, Language: "typescript", Entrypoints: []string{"tools/index.js"}}}}} + } + if p := undeliveredBundles(base()); len(p) != 1 || !strings.Contains(p[0], "never reach a machine") { + t.Fatalf("a bundle nothing delivers was admitted: %v", p) + } + loads := base() + loads.Build.Artifacts[0].Loads = []string{"tools/index.js"} + tools := base() + tools.Tools = []string{"baserow_list_rows"} + run := base() + run.Resources = []map[string]any{{"id": "daemon", "type": "process", "artifact": "tools", "run": []any{"node", "tools/index.js"}}} + runtime := base() + runtime.Module = RuntimeModule + for name, m := range map[string]Manifest{"loads": loads, "tools": tools, "a process": run, "the runtime": runtime} { + if p := undeliveredBundles(m); len(p) != 0 { + t.Errorf("a bundle delivered by %s was refused: %v", name, p) + } + } +}