diff --git a/go/delivery/builder.go b/go/delivery/builder.go index 42bee50..06abbb5 100644 --- a/go/delivery/builder.go +++ b/go/delivery/builder.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "reflect" + "slices" "strings" contractseats "git.novox.be/novox/mesh-contracts/go/seats" @@ -89,6 +90,10 @@ type BuiltArtifact struct { Kind string `json:"kind,omitempty"` Digest string `json:"digest"` Reference string `json:"reference,omitempty"` + // References are every form this artifact is named in, Reference among them (novox/hq ADR 0618, ADR 0624): a build + // keeps each form that a counted node-engine states, away nodes included, and Reference stays the form every one + // of them reads. A reader that does not know the field reads Reference alone. + References []string `json:"references,omitempty"` } const artifactStore = "artifact-store://" @@ -211,6 +216,10 @@ type BuilderBuild struct { // Imports are, on a done key, the mesh packages the built programs import, as the build module answered them // (BuildAnswer.Imports; novox/hq ADR 0593 §2): the builder carries them onto the build's end (BuildLogEntry.Imports). Imports []string `json:"imports,omitempty"` + // Needs are, on a done key only, the reader capabilities (`form:`) the build's reference forms need of a + // node-engine, beside the ones its manifest needs (novox/hq ADR 0618 §1). A reader that does not know the field + // reads the build as needing none. + Needs []string `json:"needs,omitempty"` } func (b BuilderBuild) check() error { @@ -245,6 +254,22 @@ func (b BuilderBuild) check() error { if p := a.referenceProblem(); p != "" { return fmt.Errorf("delivery: the reference %q of the %s %q %s", a.Reference, a.Kind, a.Name, p) } + if len(a.References) > 0 && !slices.Contains(a.References, a.Reference) { + return fmt.Errorf("delivery: the references of the %s %q do not name its reference %q", a.Kind, a.Name, a.Reference) + } + for _, r := range a.References { + if p := (BuiltArtifact{Name: a.Name, Kind: a.Kind, Digest: a.Digest, Reference: r}).referenceProblem(); p != "" { + return fmt.Errorf("delivery: the reference %q of the %s %q %s", r, a.Kind, a.Name, p) + } + } + } + for _, n := range b.Needs { + if !strings.HasPrefix(n, "form:") || len(n) == len("form:") { + return fmt.Errorf("delivery: the need %q of the build is not form:", n) + } + } + if len(b.Needs) > 0 && b.State != BuildDone { + return fmt.Errorf("delivery: a %s build names needs; only a done key does", b.State) } for language, id := range b.Asks { if b.State != BuildBuilding { diff --git a/go/delivery/builder_test.go b/go/delivery/builder_test.go index 8e29474..82038ca 100644 --- a/go/delivery/builder_test.go +++ b/go/delivery/builder_test.go @@ -26,11 +26,15 @@ func doneBuild() BuilderBuild { func builderSamples() map[string]BuilderBuild { failed := BuilderBuild{State: BuildFailed, By: "builder.novox.1", Step: "test", Error: "go test: 2 failed"} + both := doneBuild() + both.Needs = []string{"form:artifact-registry"} + both.Artifacts[1].References = []string{both.Artifacts[1].Reference, "artifact-store://mail/server@" + both.Artifacts[1].Digest} return map[string]BuilderBuild{ - "queued": {State: BuildQueued, Manifest: digestOf('4')}, - "building": {State: BuildBuilding, By: "builder.novox.1", Step: "fetch", Manifest: digestOf('4')}, - "done": doneBuild(), - "failed": failed, + "done with needs and both forms": both, + "queued": {State: BuildQueued, Manifest: digestOf('4')}, + "building": {State: BuildBuilding, By: "builder.novox.1", Step: "fetch", Manifest: digestOf('4')}, + "done": doneBuild(), + "failed": failed, } } @@ -90,10 +94,17 @@ func TestBuilderBuildChecked(t *testing.T) { edit func(*BuilderBuild) words []string }{ - "done without a manifest": {func(b *BuilderBuild) { b.Manifest = "" }, []string{"manifest"}}, - "a malformed manifest": {func(b *BuilderBuild) { b.Manifest = "abc" }, []string{"abc"}}, - "an artifact without a digest": {func(b *BuilderBuild) { b.Artifacts[1].Digest = "" }, []string{"digest", "server"}}, - "an artifact without a name": {func(b *BuilderBuild) { b.Artifacts[0].Name = "" }, []string{"name"}}, + "done without a manifest": {func(b *BuilderBuild) { b.Manifest = "" }, []string{"manifest"}}, + "a malformed manifest": {func(b *BuilderBuild) { b.Manifest = "abc" }, []string{"abc"}}, + "an artifact without a digest": {func(b *BuilderBuild) { b.Artifacts[1].Digest = "" }, []string{"digest", "server"}}, + "an artifact without a name": {func(b *BuilderBuild) { b.Artifacts[0].Name = "" }, []string{"name"}}, + "a need that is not a form": {func(b *BuilderBuild) { b.Needs = []string{"word:x"} }, []string{"word:x", "form:"}}, + "a reference not among the references": {func(b *BuilderBuild) { + b.Artifacts[1].References = []string{"artifact-store://mail/server@" + b.Artifacts[1].Digest} + }, []string{"server", "references"}}, + "a reference of another artifact": {func(b *BuilderBuild) { + b.Artifacts[1].References = []string{b.Artifacts[1].Reference, "registry.example/mail/other@" + b.Artifacts[1].Digest} + }, []string{"server", "other"}}, "an unknown state": {func(b *BuilderBuild) { b.State = "paused" }, []string{"paused"}}, "a toolchain without a version": {func(b *BuilderBuild) { b.Toolchains["go"] = ToolchainUsed{Recipe: "3"} }, []string{"go", "version"}}, "a toolchain without a recipe": {func(b *BuilderBuild) { b.Toolchains["go"] = ToolchainUsed{Version: "1.26.0"} }, []string{"go", "recipe"}}, @@ -119,6 +130,11 @@ func TestBuilderBuildChecked(t *testing.T) { } // a queued or building value may not already carry an outcome q := builderSamples()["queued"] + q.Needs = []string{"form:x"} + if _, err := EncodeBuilderBuild(q); err == nil { + t.Error("a queued build with needs accepted") + } + q.Needs = nil q.Artifacts = doneBuild().Artifacts if _, err := EncodeBuilderBuild(q); err == nil { t.Error("a queued build with artifacts accepted")