Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af4e4e60a3 |
+19
-2
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"maps"
|
||||||
"path"
|
"path"
|
||||||
"slices"
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
@@ -42,14 +43,30 @@ type AskSteps struct {
|
|||||||
// RunAsk runs ask's job with steps and answers a delivery.BuildAnswer or a delivery.CheckAnswer.
|
// RunAsk runs ask's job with steps and answers a delivery.BuildAnswer or a delivery.CheckAnswer.
|
||||||
func RunAsk(ctx context.Context, steps AskSteps, ask delivery.StepAsk) (any, error) {
|
func RunAsk(ctx context.Context, steps AskSteps, ask delivery.StepAsk) (any, error) {
|
||||||
if ask.Build != nil {
|
if ask.Build != nil {
|
||||||
return steps.build(ctx, *ask.Build)
|
job := *ask.Build
|
||||||
|
job.Fetch = withPackages(job.Fetch, ask.Packages)
|
||||||
|
return steps.build(ctx, job)
|
||||||
}
|
}
|
||||||
if ask.Check != nil {
|
if ask.Check != nil {
|
||||||
return steps.check(ctx, *ask.Check)
|
job := *ask.Check
|
||||||
|
job.Fetch = withPackages(job.Fetch, ask.Packages)
|
||||||
|
return steps.check(ctx, job)
|
||||||
}
|
}
|
||||||
return nil, Refuse("the ask %s holds no job", ask.ID)
|
return nil, Refuse("the ask %s holds no job", ask.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// withPackages is the fetch's arguments with the ask's package versions (novox/hq ADR 0573 §2), which the build module
|
||||||
|
// writes into the workspace it fetches; a copy, so the ask is left as it was. An ask with none, or a copy alone that
|
||||||
|
// fetches nothing, is left as it is.
|
||||||
|
func withPackages(fetch map[string]any, packages delivery.PackageVersions) map[string]any {
|
||||||
|
if len(packages) == 0 || fetch == nil {
|
||||||
|
return fetch
|
||||||
|
}
|
||||||
|
out := maps.Clone(fetch)
|
||||||
|
out["packages"] = maps.Clone(packages)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
func raw(v any) (json.RawMessage, error) {
|
func raw(v any) (json.RawMessage, error) {
|
||||||
if r, ok := v.(json.RawMessage); ok {
|
if r, ok := v.(json.RawMessage); ok {
|
||||||
return r, nil
|
return r, nil
|
||||||
|
|||||||
@@ -74,15 +74,21 @@ func TestRunAskBuilds(t *testing.T) {
|
|||||||
steps := r.steps()
|
steps := r.steps()
|
||||||
var released []string
|
var released []string
|
||||||
steps.Release = func(ws string) error { released = append(released, ws); return nil }
|
steps.Release = func(ws string) error { released = append(released, ws); return nil }
|
||||||
out, err := RunAsk(context.Background(), steps, ask(delivery.BuildJob{Module: "m", Path: "modules/m", Publish: true,
|
// Issue 1585: the ask's package versions reach the fetch, and the ask's own fetch arguments stay as they were.
|
||||||
|
a := ask(delivery.BuildJob{Module: "m", Path: "modules/m", Publish: true,
|
||||||
Fetch: map[string]any{"repository": "novox/x", "commit": runCommit},
|
Fetch: map[string]any{"repository": "novox/x", "commit": runCommit},
|
||||||
Artifacts: []delivery.AskArtifact{{Artifact: "base", From: "registry/b@sha256:00"}, {Artifact: "bundle"}, {Artifact: "lib"}}}))
|
Artifacts: []delivery.AskArtifact{{Artifact: "base", From: "registry/b@sha256:00"}, {Artifact: "bundle"}, {Artifact: "lib"}}})
|
||||||
|
a.Packages = delivery.PackageVersions{"git.novox.be/novox/mesh-sdk/go": runCommit}
|
||||||
|
out, err := RunAsk(context.Background(), steps, a)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
if _, put := a.Build.Fetch["packages"]; put {
|
||||||
|
t.Error("the ask's fetch arguments were changed")
|
||||||
|
}
|
||||||
got := strings.Join(r.calls, "\n")
|
got := strings.Join(r.calls, "\n")
|
||||||
want := `copy {"artifact":"base","from":"registry/b@sha256:00","module":"m"}
|
want := `copy {"artifact":"base","from":"registry/b@sha256:00","module":"m"}
|
||||||
fetch {"commit":"` + runCommit + `","repository":"novox/x"}
|
fetch {"commit":"` + runCommit + `","packages":{"git.novox.be/novox/mesh-sdk/go":"` + runCommit + `"},"repository":"novox/x"}
|
||||||
build {"artifact":"bundle","module":"modules/m","publish":true,"workspace":"/w/1"}
|
build {"artifact":"bundle","module":"modules/m","publish":true,"workspace":"/w/1"}
|
||||||
build {"artifact":"lib","module":"modules/m","publish":true,"workspace":"/w/1"}`
|
build {"artifact":"lib","module":"modules/m","publish":true,"workspace":"/w/1"}`
|
||||||
if got != want {
|
if got != want {
|
||||||
@@ -91,9 +97,9 @@ build {"artifact":"lib","module":"modules/m","publish":true,"workspace":"/w/1"}`
|
|||||||
if len(released) != 1 || released[0] != "/w/1" {
|
if len(released) != 1 || released[0] != "/w/1" {
|
||||||
t.Errorf("released %v, want the one workspace", released)
|
t.Errorf("released %v, want the one workspace", released)
|
||||||
}
|
}
|
||||||
a := out.(delivery.BuildAnswer)
|
answer := out.(delivery.BuildAnswer)
|
||||||
if len(a.Artifacts) != 3 || a.Artifacts[1].Reference != "artifact-store://bundle" {
|
if len(answer.Artifacts) != 3 || answer.Artifacts[1].Reference != "artifact-store://bundle" {
|
||||||
t.Fatalf("answer %+v", a)
|
t.Fatalf("answer %+v", answer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,12 @@ package buildstep
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"maps"
|
||||||
"path"
|
"path"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -27,6 +30,10 @@ type FetchRequest struct {
|
|||||||
Base string `json:"base,omitempty"`
|
Base string `json:"base,omitempty"`
|
||||||
Dir string `json:"dir,omitempty"`
|
Dir string `json:"dir,omitempty"`
|
||||||
Paths []string `json:"paths,omitempty"`
|
Paths []string `json:"paths,omitempty"`
|
||||||
|
// Packages are the version of each mesh package the build or the check builds against (delivery.StepAsk.Packages,
|
||||||
|
// novox/hq ADR 0573 §2, §4): the build module writes each into the workspace's go.mod and go.sum, whatever version
|
||||||
|
// the repository names, checksum-verified, and names them in its log.
|
||||||
|
Packages delivery.PackageVersions `json:"packages,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// FetchRequestProperties are the JSON schema properties of FetchRequest's dir and paths, for a fetch tool's Input.
|
// FetchRequestProperties are the JSON schema properties of FetchRequest's dir and paths, for a fetch tool's Input.
|
||||||
@@ -35,6 +42,8 @@ func FetchRequestProperties() map[string]any {
|
|||||||
"dir": map[string]any{"type": "string", "description": "the module's directory in the repository (\".\" for its root), with paths"},
|
"dir": map[string]any{"type": "string", "description": "the module's directory in the repository (\".\" for its root), with paths"},
|
||||||
"paths": map[string]any{"type": "array", "items": map[string]any{"type": "string"},
|
"paths": map[string]any{"type": "array", "items": map[string]any{"type": "string"},
|
||||||
"description": "the paths the module names, relative to dir, from the indexer's kept inputs: the build sees only these, the manifest and its lock files"},
|
"description": "the paths the module names, relative to dir, from the indexer's kept inputs: the build sees only these, the manifest and its lock files"},
|
||||||
|
"packages": map[string]any{"type": "object", "additionalProperties": map[string]any{"type": "string"},
|
||||||
|
"description": "optional: the version (a full commit) of each mesh package to build against, by its module path, whatever version go.mod names (novox/hq ADR 0573 §2); the builder and the checker fill it"},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,6 +58,12 @@ func (r FetchRequest) Args() map[string]any {
|
|||||||
if len(r.Paths) > 0 {
|
if len(r.Paths) > 0 {
|
||||||
args["paths"] = append([]string(nil), r.Paths...)
|
args["paths"] = append([]string(nil), r.Paths...)
|
||||||
}
|
}
|
||||||
|
if len(r.Packages) > 0 {
|
||||||
|
args["packages"] = map[string]any{}
|
||||||
|
for p, v := range r.Packages {
|
||||||
|
args["packages"].(map[string]any)[p] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
return args
|
return args
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,9 +93,42 @@ func ParseFetchRequest(args map[string]any) (FetchRequest, error) {
|
|||||||
default:
|
default:
|
||||||
return FetchRequest{}, fmt.Errorf("paths is a list of strings")
|
return FetchRequest{}, fmt.Errorf("paths is a list of strings")
|
||||||
}
|
}
|
||||||
|
packages, err := PackagesArg(args["packages"])
|
||||||
|
if err != nil {
|
||||||
|
return FetchRequest{}, err
|
||||||
|
}
|
||||||
|
r.Packages = packages
|
||||||
return r, r.Check()
|
return r, r.Check()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PackagesArg reads a tool call's packages argument, an object of strings (as JSON gives it) or the map an ask gives;
|
||||||
|
// nil for none.
|
||||||
|
func PackagesArg(v any) (delivery.PackageVersions, error) {
|
||||||
|
out := delivery.PackageVersions{}
|
||||||
|
switch v := v.(type) {
|
||||||
|
case nil:
|
||||||
|
return nil, nil
|
||||||
|
case delivery.PackageVersions:
|
||||||
|
maps.Copy(out, v)
|
||||||
|
case map[string]string:
|
||||||
|
maps.Copy(out, v)
|
||||||
|
case map[string]any:
|
||||||
|
for p, version := range v {
|
||||||
|
s, ok := version.(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("packages is an object of versions (strings), by package")
|
||||||
|
}
|
||||||
|
out[p] = s
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("packages is an object of versions (strings), by package")
|
||||||
|
}
|
||||||
|
if len(out) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return out, out.Check()
|
||||||
|
}
|
||||||
|
|
||||||
// Check is what ParseFetchRequest refuses.
|
// Check is what ParseFetchRequest refuses.
|
||||||
func (r FetchRequest) Check() error {
|
func (r FetchRequest) Check() error {
|
||||||
if !requestRepositoryOK.MatchString(r.Repository) || strings.Contains(r.Repository, "..") {
|
if !requestRepositoryOK.MatchString(r.Repository) || strings.Contains(r.Repository, "..") {
|
||||||
|
|||||||
@@ -10,11 +10,15 @@ import (
|
|||||||
// from the indexer's kept inputs and the build module reads them from the call, never from the manifest (ADR 0495
|
// from the indexer's kept inputs and the build module reads them from the call, never from the manifest (ADR 0495
|
||||||
// clause 1). The request survives the trip through a tool call's arguments.
|
// clause 1). The request survives the trip through a tool call's arguments.
|
||||||
func TestAFetchRequestCarriesTheNamedPathsThroughTheToolCall(t *testing.T) {
|
func TestAFetchRequestCarriesTheNamedPathsThroughTheToolCall(t *testing.T) {
|
||||||
|
// And the versions of the mesh packages to build against (novox/hq ADR 0573 §2; issue 1585 S4).
|
||||||
req := FetchRequest{Repository: "novox/mesh-sdk", Commit: strings.Repeat("a", 40), Ref: "refs/pull/7/head",
|
req := FetchRequest{Repository: "novox/mesh-sdk", Commit: strings.Repeat("a", 40), Ref: "refs/pull/7/head",
|
||||||
Dir: ".", Paths: []string{"package.json", "src"}}
|
Dir: ".", Paths: []string{"package.json", "src"}, Packages: map[string]string{"git.novox.be/novox/mesh-sdk/go": strings.Repeat("d", 40)}}
|
||||||
args := req.Args()
|
args := req.Args()
|
||||||
// A tool call's arguments arrive as JSON: lists of any.
|
// A tool call's arguments arrive as JSON: lists of any, objects of any.
|
||||||
args["paths"] = []any{"package.json", "src"}
|
args["paths"] = []any{"package.json", "src"}
|
||||||
|
if p, ok := args["packages"].(map[string]any); !ok || p["git.novox.be/novox/mesh-sdk/go"] != strings.Repeat("d", 40) {
|
||||||
|
t.Fatalf("the packages are not in the call's arguments: %v", args["packages"])
|
||||||
|
}
|
||||||
got, err := ParseFetchRequest(args)
|
got, err := ParseFetchRequest(args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -42,6 +46,9 @@ func TestAFetchRequestCarriesTheNamedPathsThroughTheToolCall(t *testing.T) {
|
|||||||
{map[string]any{"dir": "../a", "paths": []any{"src"}}, "leaves the repository"},
|
{map[string]any{"dir": "../a", "paths": []any{"src"}}, "leaves the repository"},
|
||||||
{map[string]any{"dir": "modules/a", "paths": []any{3}}, "paths is a list of strings"},
|
{map[string]any{"dir": "modules/a", "paths": []any{3}}, "paths is a list of strings"},
|
||||||
{map[string]any{"dir": "modules/a"}, "names a module directory and no path"},
|
{map[string]any{"dir": "modules/a"}, "names a module directory and no path"},
|
||||||
|
{map[string]any{"packages": map[string]any{"git.novox.be/novox/mesh-sdk/go": "v0.1.7"}}, "no full commit"},
|
||||||
|
{map[string]any{"packages": map[string]any{"a b": strings.Repeat("d", 40)}}, "no module path"},
|
||||||
|
{map[string]any{"packages": []any{"x"}}, "packages is an object"},
|
||||||
} {
|
} {
|
||||||
args := map[string]any{}
|
args := map[string]any{}
|
||||||
for k, v := range ok {
|
for k, v := range ok {
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ type ModuleTree struct {
|
|||||||
// Paths are, of a module whose every artifact names its paths, each named path's git object id at the commit; Tree
|
// Paths are, of a module whose every artifact names its paths, each named path's git object id at the commit; Tree
|
||||||
// is then their delivery.NamedTree with Manifest (novox/hq ADR 0499 clause 2). Absent otherwise.
|
// is then their delivery.NamedTree with Manifest (novox/hq ADR 0499 clause 2). Absent otherwise.
|
||||||
Paths map[string]string `json:"paths,omitempty"`
|
Paths map[string]string `json:"paths,omitempty"`
|
||||||
|
// Packages are the mesh packages the directory's go.sum names (delivery.MeshPackagesOf, novox/hq ADR 0573 §2).
|
||||||
|
Packages []string `json:"packages,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var lockFiles = []string{"go.sum", "package-lock.json"}
|
var lockFiles = []string{"go.sum", "package-lock.json"}
|
||||||
@@ -67,6 +69,9 @@ func ModuleTrees(ctx context.Context, run forgekey.Runner, env []string, dir, co
|
|||||||
mt.Locks = map[string]string{}
|
mt.Locks = map[string]string{}
|
||||||
}
|
}
|
||||||
mt.Locks[lock] = sha256Hex(raw)
|
mt.Locks[lock] = sha256Hex(raw)
|
||||||
|
if lock == "go.sum" {
|
||||||
|
mt.Packages = delivery.MeshPackagesOf(raw) // the mesh packages it uses (novox/hq ADR 0573 §2)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
trees[mdir] = mt
|
trees[mdir] = mt
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ func TestModuleTreesAnswersTreeManifestAndLocksPerModuleDirectory(t *testing.T)
|
|||||||
writeTreeFile(t, root, "module.json", `{"root":1}`)
|
writeTreeFile(t, root, "module.json", `{"root":1}`)
|
||||||
writeTreeFile(t, root, "go.sum", "rootsum")
|
writeTreeFile(t, root, "go.sum", "rootsum")
|
||||||
writeTreeFile(t, root, "modules/a/module.json", `{"a":1}`)
|
writeTreeFile(t, root, "modules/a/module.json", `{"a":1}`)
|
||||||
writeTreeFile(t, root, "modules/a/go.sum", "asum")
|
asum := "git.novox.be/novox/mesh-sdk/go v0.1.7 h1:x=\ngit.novox.be/novox/mesh-sdk/go v0.1.7/go.mod h1:y=\ngithub.com/nats-io/nats.go v1.0.0 h1:z=\n"
|
||||||
|
writeTreeFile(t, root, "modules/a/go.sum", asum)
|
||||||
writeTreeFile(t, root, "modules/a/package-lock.json", "alock")
|
writeTreeFile(t, root, "modules/a/package-lock.json", "alock")
|
||||||
writeTreeFile(t, root, "modules/b/module.json", `{"b":1}`)
|
writeTreeFile(t, root, "modules/b/module.json", `{"b":1}`)
|
||||||
merge := strings.Repeat("c", 40)
|
merge := strings.Repeat("c", 40)
|
||||||
@@ -55,8 +56,9 @@ func TestModuleTreesAnswersTreeManifestAndLocksPerModuleDirectory(t *testing.T)
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
want := map[string]ModuleTree{
|
want := map[string]ModuleTree{
|
||||||
".": {Tree: "tree-of-" + merge + "^{tree}", Manifest: hexOf(`{"root":1}`), Locks: map[string]string{"go.sum": hexOf("rootsum")}},
|
".": {Tree: "tree-of-" + merge + "^{tree}", Manifest: hexOf(`{"root":1}`), Locks: map[string]string{"go.sum": hexOf("rootsum")}},
|
||||||
"modules/a": {Tree: "tree-of-" + merge + ":modules/a", Manifest: hexOf(`{"a":1}`), Locks: map[string]string{"go.sum": hexOf("asum"), "package-lock.json": hexOf("alock")}},
|
"modules/a": {Tree: "tree-of-" + merge + ":modules/a", Manifest: hexOf(`{"a":1}`), Locks: map[string]string{"go.sum": hexOf(asum), "package-lock.json": hexOf("alock")},
|
||||||
|
Packages: []string{"git.novox.be/novox/mesh-sdk/go"}}, // issue 1585: the mesh packages its go.sum names
|
||||||
"modules/b": {Tree: "tree-of-" + merge + ":modules/b", Manifest: hexOf(`{"b":1}`)},
|
"modules/b": {Tree: "tree-of-" + merge + ":modules/b", Manifest: hexOf(`{"b":1}`)},
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(got, want) {
|
if !reflect.DeepEqual(got, want) {
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ import (
|
|||||||
// SourcePrefix2 opens every source fingerprint of the new form.
|
// SourcePrefix2 opens every source fingerprint of the new form.
|
||||||
const SourcePrefix2 = "src2:"
|
const SourcePrefix2 = "src2:"
|
||||||
|
|
||||||
|
// NoPackageVersion opens the reason a module has no fingerprint while a mesh package it uses has no published version
|
||||||
|
// the indexer holds (novox/hq ADR 0573 §2); the packages follow, by module path.
|
||||||
|
const NoPackageVersion = "no published version of the package "
|
||||||
|
|
||||||
// Toolchain2 is what an artifact's toolchain enters the new fingerprint as (novox/hq ADR 0405 clause 7): the
|
// Toolchain2 is what an artifact's toolchain enters the new fingerprint as (novox/hq ADR 0405 clause 7): the
|
||||||
// artifact's language, the installed toolchain's version and the build module's recipe version, in place of the
|
// artifact's language, the installed toolchain's version and the build module's recipe version, in place of the
|
||||||
// toolchain image's digest. A toolchain update so changes the fingerprints of that language's modules only.
|
// toolchain image's digest. A toolchain update so changes the fingerprints of that language's modules only.
|
||||||
@@ -37,15 +41,19 @@ type SourceInputs2 struct {
|
|||||||
Contexts map[string]string
|
Contexts map[string]string
|
||||||
Locks map[string]string
|
Locks map[string]string
|
||||||
Toolchains map[string]Toolchain2
|
Toolchains map[string]Toolchain2
|
||||||
Unpinned string
|
// Packages are the version of each mesh package the module uses, by package (novox/hq ADR 0573 §2): a package with
|
||||||
|
// no version gives no fingerprint. None leaves the fingerprint as it was before packages entered it.
|
||||||
|
Packages map[string]string
|
||||||
|
Unpinned string
|
||||||
}
|
}
|
||||||
|
|
||||||
// SourceFingerprint2 is the module's source fingerprint of the new form, `src2:` and the hex sha256 of its inputs'
|
// SourceFingerprint2 is the module's source fingerprint of the new form, `src2:` and the hex sha256 of its inputs'
|
||||||
// lines, or none and the reason when the source does not pin the build: Unpinned when set, "no tree" when Tree is
|
// lines, or none and the reason when the source does not pin the build: Unpinned when set, "no tree" when Tree is
|
||||||
// empty, "no lock" when an artifact resolves packages and has no lock. It is a pure function of its inputs: the order
|
// empty, "no lock" when an artifact resolves packages and has no lock. It is a pure function of its inputs: the order
|
||||||
// of maps and a base given twice change nothing. The lines are "module <m>", "tree <t>" and "builder <fp>", then
|
// of maps and a base given twice change nothing. The lines are "module <m>", "tree <t>" and "builder <fp>", then
|
||||||
// "base <fp>" (once each), "context <artifact> <tree>", "lock <artifact> <hash>" and "toolchain <artifact> <language>
|
// "base <fp>" (once each), "context <artifact> <tree>", "lock <artifact> <hash>", "toolchain <artifact> <language>
|
||||||
// <version> <recipe>", sorted, all joined by a newline.
|
// <version> <recipe>" and "package <package> <version>", sorted, all joined by a newline. A module that uses no mesh
|
||||||
|
// package has no package line, so its fingerprint is the one it had before packages entered (novox/hq ADR 0573 §2).
|
||||||
func SourceFingerprint2(in SourceInputs2) (fp, none string) {
|
func SourceFingerprint2(in SourceInputs2) (fp, none string) {
|
||||||
if in.Unpinned != "" {
|
if in.Unpinned != "" {
|
||||||
return "", in.Unpinned
|
return "", in.Unpinned
|
||||||
@@ -58,6 +66,16 @@ func SourceFingerprint2(in SourceInputs2) (fp, none string) {
|
|||||||
return "", "no lock"
|
return "", "no lock"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var unpublished []string
|
||||||
|
for p, v := range in.Packages {
|
||||||
|
if v == "" {
|
||||||
|
unpublished = append(unpublished, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(unpublished) > 0 {
|
||||||
|
sort.Strings(unpublished)
|
||||||
|
return "", NoPackageVersion + strings.Join(unpublished, ", ")
|
||||||
|
}
|
||||||
lines := []string{"module " + in.Module, "tree " + in.Tree, "builder " + in.Builder}
|
lines := []string{"module " + in.Module, "tree " + in.Tree, "builder " + in.Builder}
|
||||||
bases := map[string]bool{}
|
bases := map[string]bool{}
|
||||||
for _, b := range in.Bases {
|
for _, b := range in.Bases {
|
||||||
@@ -75,6 +93,9 @@ func SourceFingerprint2(in SourceInputs2) (fp, none string) {
|
|||||||
for a, t := range in.Toolchains {
|
for a, t := range in.Toolchains {
|
||||||
lines = append(lines, "toolchain "+a+" "+t.Language+" "+t.Version+" "+t.Recipe)
|
lines = append(lines, "toolchain "+a+" "+t.Language+" "+t.Version+" "+t.Recipe)
|
||||||
}
|
}
|
||||||
|
for p, v := range in.Packages {
|
||||||
|
lines = append(lines, "package "+p+" "+v)
|
||||||
|
}
|
||||||
sort.Strings(lines[3:])
|
sort.Strings(lines[3:])
|
||||||
sum := sha256.Sum256([]byte(strings.Join(lines, "\n")))
|
sum := sha256.Sum256([]byte(strings.Join(lines, "\n")))
|
||||||
return SourcePrefix2 + hex.EncodeToString(sum[:]), ""
|
return SourcePrefix2 + hex.EncodeToString(sum[:]), ""
|
||||||
|
|||||||
@@ -62,6 +62,9 @@ func TestFingerprint2Changes(t *testing.T) {
|
|||||||
"a context": func(in *SourceInputs2) { in.Contexts["mail"] = "t-changed" },
|
"a context": func(in *SourceInputs2) { in.Contexts["mail"] = "t-changed" },
|
||||||
"a context added": func(in *SourceInputs2) { in.Contexts["extra"] = "t-root" },
|
"a context added": func(in *SourceInputs2) { in.Contexts["extra"] = "t-root" },
|
||||||
"a toolchain added": func(in *SourceInputs2) { in.Toolchains["extra"] = Toolchain2{"go", "1.26.0", "3"} },
|
"a toolchain added": func(in *SourceInputs2) { in.Toolchains["extra"] = Toolchain2{"go", "1.26.0", "3"} },
|
||||||
|
// Issue 1585 (ADR 0573 §2): a package's new published version, and a package used at all.
|
||||||
|
"a package added": func(in *SourceInputs2) { in.Packages = map[string]string{"git.novox.be/novox/mesh-sdk/go": "c1"} },
|
||||||
|
"a package version": func(in *SourceInputs2) { in.Packages = map[string]string{"git.novox.be/novox/mesh-sdk/go": "c2"} },
|
||||||
} {
|
} {
|
||||||
in := inputs2()
|
in := inputs2()
|
||||||
f(&in)
|
f(&in)
|
||||||
@@ -111,6 +114,17 @@ func TestFingerprint2None(t *testing.T) {
|
|||||||
if fp, none := SourceFingerprint2(in); fp != "" || none != "no tree" {
|
if fp, none := SourceFingerprint2(in); fp != "" || none != "no tree" {
|
||||||
t.Errorf("no tree: fp = %q, none = %q", fp, none)
|
t.Errorf("no tree: fp = %q, none = %q", fp, none)
|
||||||
}
|
}
|
||||||
|
// Issue 1585: a package with no published version gives none, naming it; an empty set of packages changes nothing.
|
||||||
|
in = inputs2()
|
||||||
|
in.Packages = map[string]string{"git.novox.be/novox/b/go": "", "git.novox.be/novox/a/go": "", "git.novox.be/novox/c/go": "c1"}
|
||||||
|
if fp, none := SourceFingerprint2(in); fp != "" || none != NoPackageVersion+"git.novox.be/novox/a/go, git.novox.be/novox/b/go" {
|
||||||
|
t.Errorf("no package version: fp = %q, none = %q", fp, none)
|
||||||
|
}
|
||||||
|
in = inputs2()
|
||||||
|
in.Packages = map[string]string{}
|
||||||
|
if fp, _ := SourceFingerprint2(in); fp != mustFP2(t, inputs2()) {
|
||||||
|
t.Errorf("no package changes the fingerprint: %s", fp)
|
||||||
|
}
|
||||||
in = inputs2()
|
in = inputs2()
|
||||||
in.Tree, in.Unpinned = "", "a context is a Go program's build source"
|
in.Tree, in.Unpinned = "", "a context is a Go program's build source"
|
||||||
if _, none := SourceFingerprint2(in); none != in.Unpinned {
|
if _, none := SourceFingerprint2(in); none != in.Unpinned {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package delivery
|
package delivery
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@@ -163,4 +164,35 @@ func TestModuleKeepsInputs2(t *testing.T) {
|
|||||||
if _, err := EncodeModule(m); err == nil {
|
if _, err := EncodeModule(m); err == nil {
|
||||||
t.Error("Inputs2 without a manifest digest was encoded")
|
t.Error("Inputs2 without a manifest digest was encoded")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Issue 1585 (ADR 0573 §2): the mesh packages a go.sum names, those of other repositories, and the latest published
|
||||||
|
// version of each: the trunk commit of the deepest module of its repository whose directory holds it.
|
||||||
|
sum := "git.novox.be/novox/mesh-sdk/go v0.1.7 h1:a=\ngit.novox.be/novox/mesh-sdk/go v0.1.7/go.mod h1:b=\n" +
|
||||||
|
"git.novox.be/novox/mesh-controller/modules/route-proxy/contract v0.1.0 h1:c=\n" +
|
||||||
|
"github.com/nats-io/nats.go v1.0.0 h1:d=\ngit.novox.be/novox/mesh-host v0.0.0-1 h1:e=\n"
|
||||||
|
pkgs := MeshPackagesOf([]byte(sum))
|
||||||
|
if want := []string{"git.novox.be/novox/mesh-controller/modules/route-proxy/contract", "git.novox.be/novox/mesh-host", "git.novox.be/novox/mesh-sdk/go"}; !reflect.DeepEqual(pkgs, want) {
|
||||||
|
t.Errorf("the packages of a go.sum: %v, want %v", pkgs, want)
|
||||||
|
}
|
||||||
|
if got := OtherPackages(pkgs, "novox/mesh-controller"); len(got) != 2 || got[0] != "git.novox.be/novox/mesh-host" {
|
||||||
|
t.Errorf("the packages of other repositories: %v", got)
|
||||||
|
}
|
||||||
|
held := map[string]Module{
|
||||||
|
"mesh-sdk": {Repository: "novox/mesh-sdk", Path: RootPath, Commit: "c-sdk"},
|
||||||
|
"route-proxy": {Repository: "novox/mesh-controller", Path: "modules/route-proxy", Commit: "c-rp"},
|
||||||
|
"controller": {Repository: "novox/mesh-controller", Path: RootPath, Commit: "c-ctl"},
|
||||||
|
"route-proxy2": {Repository: "novox/mesh-controller", Path: "modules/route", Commit: "c-wrong"},
|
||||||
|
"no-path": {Repository: "novox/mesh-host", Commit: "c-host"},
|
||||||
|
}
|
||||||
|
versions, missing := PackageVersionsOf(pkgs, held)
|
||||||
|
if want := (PackageVersions{"git.novox.be/novox/mesh-sdk/go": "c-sdk", "git.novox.be/novox/mesh-controller/modules/route-proxy/contract": "c-rp"}); !reflect.DeepEqual(versions, want) {
|
||||||
|
t.Errorf("versions %v, want %v", versions, want)
|
||||||
|
}
|
||||||
|
if len(missing) != 1 || missing[0] != "git.novox.be/novox/mesh-host" {
|
||||||
|
t.Errorf("missing %v: a module that names no directory holds no package", missing)
|
||||||
|
}
|
||||||
|
kept.Packages = []string{"git.novox.be/novox/mesh-sdk/go", "git.novox.be/novox/mesh-host"}
|
||||||
|
if fp, none := SourceFingerprint2(kept.WithPackages("m", tc, versions)); fp != "" || !strings.HasPrefix(none, NoPackageVersion) {
|
||||||
|
t.Errorf("a package with no version: %q %q", fp, none)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package delivery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"maps"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// **Every module builds against the latest published version of each mesh package it uses** (novox/hq ADR 0573 §2;
|
||||||
|
// issue 1585). A mesh package is a Go module of a repository of the mesh's forge (ForgeModulePrefix): mesh-sdk's Go
|
||||||
|
// module, and a seat's contract its holder's repository publishes (ADR 0573 §1). Its version is the trunk merge the
|
||||||
|
// indexer holds for the module whose directory holds the package (PackageVersionsOf): a merge that changes it changes
|
||||||
|
// the fingerprint of every module that uses it, and the build writes that version into its own go.mod and go.sum
|
||||||
|
// (StepAsk.Packages). Which packages a module uses is read from its lock file by MeshPackagesOf, the same way by the
|
||||||
|
// forge bridge and by the build modules (buildstep.ModuleTrees), so nobody names a version by hand.
|
||||||
|
|
||||||
|
// ForgeModulePrefix is the module path prefix of the Go modules the mesh's forge serves: a Go module whose path starts
|
||||||
|
// with it is a mesh package, git.novox.be/novox/<repository>[/<directory of the module in it>].
|
||||||
|
const ForgeModulePrefix = "git.novox.be/novox/"
|
||||||
|
|
||||||
|
// MeshPackagesOf are the mesh packages a Go module's go.sum names (the module path of each line that starts with
|
||||||
|
// ForgeModulePrefix), sorted and each once. go.sum names every module of the build's graph, so a package used only
|
||||||
|
// through another is named too. Nil for none. A package of the module's own repository comes with its own tree, and
|
||||||
|
// its reader leaves it out (OtherPackages).
|
||||||
|
func MeshPackagesOf(goSum []byte) []string {
|
||||||
|
seen := map[string]bool{}
|
||||||
|
for _, line := range strings.Split(string(goSum), "\n") {
|
||||||
|
f := strings.Fields(line)
|
||||||
|
if len(f) < 2 || !strings.HasPrefix(f[0], ForgeModulePrefix) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, _, ok := PackageRepository(f[0]); !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[f[0]] = true
|
||||||
|
}
|
||||||
|
if len(seen) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return slices.Sorted(maps.Keys(seen))
|
||||||
|
}
|
||||||
|
|
||||||
|
// OtherPackages are the packages not published from repository, the module's own, in their order; nil for none.
|
||||||
|
func OtherPackages(packages []string, repository string) []string {
|
||||||
|
var out []string
|
||||||
|
for _, p := range packages {
|
||||||
|
if repo, _, ok := PackageRepository(p); ok && !SameRepository(repo, repository) {
|
||||||
|
out = append(out, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// PackageRepository is the repository (<owner>/<name>) a mesh package is published from and the package's directory in
|
||||||
|
// it ("" for its root); false for a path that is not a forge module's.
|
||||||
|
func PackageRepository(pkg string) (repository, dir string, ok bool) {
|
||||||
|
rest, found := strings.CutPrefix(pkg, ForgeModulePrefix)
|
||||||
|
if !found || rest == "" {
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
name, dir, _ := strings.Cut(rest, "/")
|
||||||
|
if name == "" {
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
return "novox/" + name, dir, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// PackageVersionsOf is the latest published version of each package from the modules the indexer holds (by key): the
|
||||||
|
// trunk merge commit of the module of the package's repository whose directory holds the package's directory (the
|
||||||
|
// deepest, a module at the repository's root holding every directory). A package no held module holds, or whose
|
||||||
|
// module names no commit or no directory, is in missing, sorted; versions is nil when packages is empty.
|
||||||
|
func PackageVersionsOf(packages []string, modules map[string]Module) (versions PackageVersions, missing []string) {
|
||||||
|
for _, pkg := range packages {
|
||||||
|
repository, dir, ok := PackageRepository(pkg)
|
||||||
|
commit, depth := "", -1
|
||||||
|
if ok {
|
||||||
|
for _, key := range slices.Sorted(maps.Keys(modules)) {
|
||||||
|
m := modules[key]
|
||||||
|
if !SameRepository(m.Repository, repository) || m.Commit == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
at := strings.Trim(m.Path, "/")
|
||||||
|
switch {
|
||||||
|
case at == "":
|
||||||
|
continue // a value that names no directory says nothing of where the package lies
|
||||||
|
case at == RootPath:
|
||||||
|
at = ""
|
||||||
|
case dir != at && !strings.HasPrefix(dir, at+"/"):
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if d := len(at); d > depth {
|
||||||
|
commit, depth = m.Commit, d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if commit == "" {
|
||||||
|
missing = append(missing, pkg)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if versions == nil {
|
||||||
|
versions = PackageVersions{}
|
||||||
|
}
|
||||||
|
versions[pkg] = commit
|
||||||
|
}
|
||||||
|
slices.Sort(missing)
|
||||||
|
return versions, missing
|
||||||
|
}
|
||||||
@@ -107,6 +107,8 @@ type PreviewModule struct {
|
|||||||
// Paths are, of a module whose every artifact names its paths, each named path's git object id at the merge; Tree
|
// Paths are, of a module whose every artifact names its paths, each named path's git object id at the merge; Tree
|
||||||
// is then their NamedTree (novox/hq ADR 0499 clause 2). Absent otherwise.
|
// is then their NamedTree (novox/hq ADR 0499 clause 2). Absent otherwise.
|
||||||
Paths map[string]string `json:"paths,omitempty"`
|
Paths map[string]string `json:"paths,omitempty"`
|
||||||
|
// Packages are the mesh packages the module's lock file names at the merge (MeshPackagesOf, novox/hq ADR 0573 §2).
|
||||||
|
Packages []string `json:"packages,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preview is the value of a preview key: the repository, the head and the trunk commit it was merged onto, the merge
|
// Preview is the value of a preview key: the repository, the head and the trunk commit it was merged onto, the merge
|
||||||
|
|||||||
+38
-3
@@ -284,6 +284,10 @@ type CandidateModule struct {
|
|||||||
// Paths are, of a module whose every artifact names its paths, each named path's git object id at the preview
|
// Paths are, of a module whose every artifact names its paths, each named path's git object id at the preview
|
||||||
// (novox/hq ADR 0499 clause 2), so the builder's fetch request for the candidate names them. Absent otherwise.
|
// (novox/hq ADR 0499 clause 2), so the builder's fetch request for the candidate names them. Absent otherwise.
|
||||||
Paths map[string]string `json:"paths,omitempty"`
|
Paths map[string]string `json:"paths,omitempty"`
|
||||||
|
// Packages are the versions of the mesh packages FP2 was computed with (novox/hq ADR 0573 §2, §4): the latest
|
||||||
|
// published version of each, or the head of the candidate's pull request that publishes it, so a group's candidate
|
||||||
|
// builds against the group's own package. Absent for none.
|
||||||
|
Packages PackageVersions `json:"packages,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m CandidateModule) check() error {
|
func (m CandidateModule) check() error {
|
||||||
@@ -296,6 +300,9 @@ func (m CandidateModule) check() error {
|
|||||||
if err := checkPathObjects(m.Paths); err != nil {
|
if err := checkPathObjects(m.Paths); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := m.Packages.Check(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return checkNewForm(m.FP2, m.Manifest)
|
return checkNewForm(m.FP2, m.Manifest)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -415,6 +422,10 @@ type Module struct {
|
|||||||
// so the checker tells whether a change touches an artifact's build source without reading a manifest (the lead's
|
// so the checker tells whether a change touches an artifact's build source without reading a manifest (the lead's
|
||||||
// decision L8 of R4e). Absent for an artifact that names none, and in a value written before.
|
// decision L8 of R4e). Absent for an artifact that names none, and in a value written before.
|
||||||
ArtifactPaths map[string][]string `json:"paths,omitempty"`
|
ArtifactPaths map[string][]string `json:"paths,omitempty"`
|
||||||
|
// Packages are the versions of the mesh packages FP2 was computed with (novox/hq ADR 0573 §2): the trunk merge the
|
||||||
|
// indexer held for each package of Inputs2.Packages. A builder builds the fingerprint against exactly these
|
||||||
|
// (StepAsk.Packages). Empty when FP2 is empty or the module uses no mesh package.
|
||||||
|
Packages PackageVersions `json:"packages,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RootPath is Module.Path of a module whose manifest is its repository's root.
|
// RootPath is Module.Path of a module whose manifest is its repository's root.
|
||||||
@@ -433,6 +444,10 @@ type KeptInputs2 struct {
|
|||||||
// computation from kept inputs gives the merge's value (novox/hq ADR 0499 clause 2). Absent for every other module,
|
// computation from kept inputs gives the merge's value (novox/hq ADR 0499 clause 2). Absent for every other module,
|
||||||
// whose Tree is its directory's tree, and in a value written before.
|
// whose Tree is its directory's tree, and in a value written before.
|
||||||
Paths map[string]string `json:"paths,omitempty"`
|
Paths map[string]string `json:"paths,omitempty"`
|
||||||
|
// Packages are the mesh packages the module's lock file names (MeshPackagesOf, novox/hq ADR 0573 §2): the
|
||||||
|
// fingerprint takes the latest published version of each (Module.Packages). Absent for none, and in a value written
|
||||||
|
// before.
|
||||||
|
Packages []string `json:"packages,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkPathObjects refuses a named path that is not clean and relative to the module's directory, or names no object.
|
// checkPathObjects refuses a named path that is not clean and relative to the module's directory, or names no object.
|
||||||
@@ -469,6 +484,19 @@ func (k KeptInputs2) With(module string, toolchains map[string]Toolchain2) Sourc
|
|||||||
Locks: k.Locks, Toolchains: toolchains}
|
Locks: k.Locks, Toolchains: toolchains}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithPackages is With and the version of each package the module uses (Packages), as PackageVersionsOf read them: a
|
||||||
|
// package with no version enters with none, and the fingerprint is then none (SourceFingerprint2).
|
||||||
|
func (k KeptInputs2) WithPackages(module string, toolchains map[string]Toolchain2, versions PackageVersions) SourceInputs2 {
|
||||||
|
in := k.With(module, toolchains)
|
||||||
|
if len(k.Packages) > 0 {
|
||||||
|
in.Packages = map[string]string{}
|
||||||
|
for _, p := range k.Packages {
|
||||||
|
in.Packages[p] = versions[p]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return in
|
||||||
|
}
|
||||||
|
|
||||||
func (m Module) check() error {
|
func (m Module) check() error {
|
||||||
switch {
|
switch {
|
||||||
case m.FP != "" && m.None != "":
|
case m.FP != "" && m.None != "":
|
||||||
@@ -485,6 +513,9 @@ func (m Module) check() error {
|
|||||||
return fmt.Errorf("delivery: the module's toolchain of %q needs its version and its recipe version", language)
|
return fmt.Errorf("delivery: the module's toolchain of %q needs its version and its recipe version", language)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err := m.Packages.Check(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if m.Inputs2 != nil && m.Manifest == "" {
|
if m.Inputs2 != nil && m.Manifest == "" {
|
||||||
return fmt.Errorf("delivery: a module keeps new-form inputs but has no manifest digest")
|
return fmt.Errorf("delivery: a module keeps new-form inputs but has no manifest digest")
|
||||||
}
|
}
|
||||||
@@ -534,9 +565,13 @@ type Forge struct {
|
|||||||
// id at the same commit as its manifest, by path relative to the directory (novox/hq ADR 0499 clause 3). The
|
// id at the same commit as its manifest, by path relative to the directory (novox/hq ADR 0499 clause 3). The
|
||||||
// indexer names the paths; a message without them gives such a module none. Older writers send none.
|
// indexer names the paths; a message without them gives such a module none. Older writers send none.
|
||||||
Paths map[string]map[string]string `json:"paths,omitempty"`
|
Paths map[string]map[string]string `json:"paths,omitempty"`
|
||||||
Act string `json:"act,omitempty"`
|
// Packages are, of each module directory, the mesh packages its lock file names (MeshPackagesOf; novox/hq ADR 0573
|
||||||
Words string `json:"words,omitempty"`
|
// §2): the indexer keeps them in the module's inputs and takes each one's latest published version into its
|
||||||
Open []OpenPull `json:"open,omitempty"`
|
// fingerprint. Absent for a directory that uses none, and from an older writer.
|
||||||
|
Packages map[string][]string `json:"packages,omitempty"`
|
||||||
|
Act string `json:"act,omitempty"`
|
||||||
|
Words string `json:"words,omitempty"`
|
||||||
|
Open []OpenPull `json:"open,omitempty"`
|
||||||
// RetiredPlacements is the placement files a bridge before ADR 0567 carried (ADR 0549 §2, ADR 0559 §2): placement is
|
// RetiredPlacements is the placement files a bridge before ADR 0567 carried (ADR 0549 §2, ADR 0559 §2): placement is
|
||||||
// the seat mesh-placement's state now and no repository holds it (ADR 0567 §11), so a message carrying them is
|
// the seat mesh-placement's state now and no repository holds it (ADR 0567 §11), so a message carrying them is
|
||||||
// refused, loudly, rather than read with them dropped.
|
// refused, loudly, rather than read with them dropped.
|
||||||
|
|||||||
@@ -253,6 +253,30 @@ type StepAsk struct {
|
|||||||
Compose *ComposeJob `json:"compose,omitempty"`
|
Compose *ComposeJob `json:"compose,omitempty"`
|
||||||
// Needs are what the node that runs the ask must have (AskNeeds); a check ask's alone. Empty: any node.
|
// Needs are what the node that runs the ask must have (AskNeeds); a check ask's alone. Empty: any node.
|
||||||
Needs []string `json:"needs,omitempty"`
|
Needs []string `json:"needs,omitempty"`
|
||||||
|
// Packages are the version of each mesh package the build or the check builds against, by its module path (a Go
|
||||||
|
// module's path, git.novox.be/novox/mesh-sdk/go; the shape takes a seat's package too), whatever version the
|
||||||
|
// repository's go.mod names (novox/hq ADR 0573 §2, §4; issue 1585): a Go package's version is a full commit, the
|
||||||
|
// trunk merge the indexer holds as the package's latest published version or, in a delivery group's check, the
|
||||||
|
// head of the group's member that publishes it. The build module writes each into its workspace, checksum-verified,
|
||||||
|
// and names them in its log (PackageVersions). A compose takes none: it builds the mesh-sdk its reader names.
|
||||||
|
Packages PackageVersions `json:"packages,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PackageVersions are the versions of the mesh packages a build or a check builds against, by package (a Go module's
|
||||||
|
// path), each a full commit (novox/hq ADR 0573 §2).
|
||||||
|
type PackageVersions map[string]string
|
||||||
|
|
||||||
|
// Check refuses a package that is no module path or a version that is no full commit, naming it.
|
||||||
|
func (p PackageVersions) Check() error {
|
||||||
|
for pkg, version := range p {
|
||||||
|
if pkg == "" || strings.ContainsAny(pkg, " \t\n@") || strings.HasPrefix(pkg, "/") {
|
||||||
|
return fmt.Errorf("delivery: the package %q is no module path", pkg)
|
||||||
|
}
|
||||||
|
if !askCommitOK.MatchString(version) {
|
||||||
|
return fmt.Errorf("delivery: the package %s names the version %q, which is no full commit", pkg, version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ComposeJob is a compose ask's job (novox/hq ADR 0482 clause 4, issue 1622): fetch the commit, build the composer
|
// ComposeJob is a compose ask's job (novox/hq ADR 0482 clause 4, issue 1622): fetch the commit, build the composer
|
||||||
@@ -389,6 +413,11 @@ func (a StepAsk) check() error {
|
|||||||
return fmt.Errorf("delivery: the ask %s is a check of an image, which has no tests", a.ID)
|
return fmt.Errorf("delivery: the ask %s is a check of an image, which has no tests", a.ID)
|
||||||
case len(a.Needs) > 0 && a.Check == nil:
|
case len(a.Needs) > 0 && a.Check == nil:
|
||||||
return fmt.Errorf("delivery: the ask %s names needs and is no check, whose tests alone need a node", a.ID)
|
return fmt.Errorf("delivery: the ask %s names needs and is no check, whose tests alone need a node", a.ID)
|
||||||
|
case len(a.Packages) > 0 && a.Compose != nil:
|
||||||
|
return fmt.Errorf("delivery: the ask %s is a compose and names package versions, which a compose does not take", a.ID)
|
||||||
|
}
|
||||||
|
if err := a.Packages.Check(); err != nil {
|
||||||
|
return fmt.Errorf("the ask %s: %w", a.ID, err)
|
||||||
}
|
}
|
||||||
for _, n := range a.Needs {
|
for _, n := range a.Needs {
|
||||||
if err := CheckNeed(n); err != nil {
|
if err := CheckNeed(n); err != nil {
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ func buildAsk() StepAsk {
|
|||||||
Build: &BuildJob{Module: "mesh-delivery-view", Path: "modules/mesh-delivery-view", FP: "ab12",
|
Build: &BuildJob{Module: "mesh-delivery-view", Path: "modules/mesh-delivery-view", FP: "ab12",
|
||||||
Fetch: map[string]any{"repository": "novox/mesh-delivery", "commit": askCommit, "dir": "modules/mesh-delivery-view", "paths": []any{"cmd"}},
|
Fetch: map[string]any{"repository": "novox/mesh-delivery", "commit": askCommit, "dir": "modules/mesh-delivery-view", "paths": []any{"cmd"}},
|
||||||
Artifacts: []AskArtifact{{Artifact: "bundle"}, {Artifact: "base", From: "registry/x@sha256:00"}},
|
Artifacts: []AskArtifact{{Artifact: "bundle"}, {Artifact: "base", From: "registry/x@sha256:00"}},
|
||||||
Publish: true, Toolchain: &ToolchainUsed{Version: "go1.26.0", Recipe: "3"}}}
|
Publish: true, Toolchain: &ToolchainUsed{Version: "go1.26.0", Recipe: "3"}},
|
||||||
|
Packages: PackageVersions{"git.novox.be/novox/mesh-sdk/go": askCommit}}
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkAsk() StepAsk {
|
func checkAsk() StepAsk {
|
||||||
@@ -25,10 +26,11 @@ func checkAsk() StepAsk {
|
|||||||
At: time.Date(2026, 10, 23, 13, 0, 0, 0, time.UTC),
|
At: time.Date(2026, 10, 23, 13, 0, 0, 0, time.UTC),
|
||||||
Check: &CheckJob{Fetch: map[string]any{"repository": "novox/mesh-catalog", "commit": askCommit, "ref": "refs/pull/3/head", "base": askCommit},
|
Check: &CheckJob{Fetch: map[string]any{"repository": "novox/mesh-catalog", "commit": askCommit, "ref": "refs/pull/3/head", "base": askCommit},
|
||||||
Levels: []string{LevelUnit, LevelIntegration}, Kept: true, Runner: "workspace", RunnerWhen: "modules/mesh-build-go/", Run: "",
|
Levels: []string{LevelUnit, LevelIntegration}, Kept: true, Runner: "workspace", RunnerWhen: "modules/mesh-build-go/", Run: "",
|
||||||
NodeNames: []string{"anchor", "laptop"}}}
|
NodeNames: []string{"anchor", "laptop"}},
|
||||||
|
Packages: PackageVersions{"git.novox.be/novox/mesh-sdk/go": askCommit, "git.novox.be/novox/mesh-controller/modules/route-proxy/contract": askCommit}}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1887/A1a: an ask encodes and decodes with every field.
|
// 1887/A1a: an ask encodes and decodes with every field; 1585/S4: the package versions too.
|
||||||
func TestAskRoundTrips(t *testing.T) {
|
func TestAskRoundTrips(t *testing.T) {
|
||||||
for _, a := range []StepAsk{buildAsk(), checkAsk()} {
|
for _, a := range []StepAsk{buildAsk(), checkAsk()} {
|
||||||
raw, err := EncodeStepAsk(a)
|
raw, err := EncodeStepAsk(a)
|
||||||
@@ -58,6 +60,13 @@ func TestAskRefusesUnknownKindAndLanguage(t *testing.T) {
|
|||||||
"commit": func(a *StepAsk) { a.Build.Fetch = map[string]any{"repository": "novox/x", "commit": "main"} },
|
"commit": func(a *StepAsk) { a.Build.Fetch = map[string]any{"repository": "novox/x", "commit": "main"} },
|
||||||
"path": func(a *StepAsk) { a.Build.Path = "" },
|
"path": func(a *StepAsk) { a.Build.Path = "" },
|
||||||
"needs": func(a *StepAsk) { a.Needs = []string{NeedNodeBuildNode} }, // issue 890: a build needs no node
|
"needs": func(a *StepAsk) { a.Needs = []string{NeedNodeBuildNode} }, // issue 890: a build needs no node
|
||||||
|
// Issue 1585: a package's version is a full commit, of a module path.
|
||||||
|
"version": func(a *StepAsk) { a.Packages["git.novox.be/novox/mesh-sdk/go"] = "v0.1.7" },
|
||||||
|
"package": func(a *StepAsk) { a.Packages["git.novox.be/novox/x y"] = askCommit },
|
||||||
|
"compose": func(a *StepAsk) {
|
||||||
|
a.Kind, a.Build = AskCheck, nil
|
||||||
|
a.Compose = &ComposeJob{Fetch: map[string]any{"repository": "novox/mesh-sdk", "commit": askCommit}, Dir: "go", Package: "x", Request: json.RawMessage(`{}`)}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for name, change := range cases {
|
for name, change := range cases {
|
||||||
a := buildAsk()
|
a := buildAsk()
|
||||||
|
|||||||
Reference in New Issue
Block a user