From 21abd948aa356c64dfe4db0809f8628b9b4eb73e Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 13 Sep 2026 04:56:00 +0200 Subject: [PATCH] Say that a module is unbuilt, rather than letting a machine call it malformed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A container naming an artifact is a module saying the mesh builds this. Until a build publishes one there is nothing to run — and what reached the machine was an unresolved field, which its language has no room for, so it refused the whole declaration and reported that a container does not use "artifact". That reads as a broken manifest. It is not broken, it is unbuilt, and only the mesh can tell those apart. Found by the four-machine bed, which assigns modules the mesh has not built. --- internal/catalogue/declaration.go | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/internal/catalogue/declaration.go b/internal/catalogue/declaration.go index a8f30bf..3ead392 100644 --- a/internal/catalogue/declaration.go +++ b/internal/catalogue/declaration.go @@ -456,6 +456,11 @@ func (r Resolution) Declaration(with Rendering) ([]map[string]any, error) { if err := pinned(copied, m.Module); err != nil { return nil, err } + // And the other half of the same question: an image nobody has published yet is named + // by an artifact rather than by a placeholder digest, and is just as unrunnable. + if err := built(copied, m.Module); err != nil { + return nil, err + } publishedOn(copied, m.Module, with) copied["id"] = m.Module + "." + fmt.Sprint(resource["id"]) // A service saying what it reflects names resources within its own module, so those @@ -887,6 +892,30 @@ func pinned(resource map[string]any, module string) error { module, resource["id"]) } +// built refuses a container that still names an artifact nobody has made. +// +// **Said here, by the thing that knows what "artifact" means.** A container naming an artifact is +// a module saying "the mesh builds this"; the field is resolved into an image when a build +// publishes one, and until then there is nothing to run. A host receiving it refuses the whole +// declaration — correctly, since its language has no such field — but what it can say is that a +// container does not use "artifact", which tells a reader the manifest is malformed. It is not: +// it is unbuilt, which is a different problem with a different fix, and only the mesh is in a +// position to tell them apart (novox/hq ADR 0073). +func built(resource map[string]any, module string) error { + if fmt.Sprint(resource["type"]) != "container" { + return nil + } + artifact, ok := resource["artifact"].(string) + if !ok || artifact == "" { + return nil + } + return fmt.Errorf( + "%s has not been built for this mesh: %v is its %q artifact, and no build has published "+ + "one. Build it — `build --path ` — and the module will name what "+ + "came out instead", + module, resource["id"], artifact) +} + // publishedOn puts the machine's own port on the outside of a container's mapping. // // **A module writes the port the software uses; the mesh says where the machine puts it**