From c84298508d608a9c0ee448131fcec4378f3fa307 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 1 Nov 2026 02:12:33 +0100 Subject: [PATCH] Build Go modules against the package versions an ask names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADR 0573 §2 and §4 have a build take the latest published version of each mesh package, or a group's own head, whatever go.mod names (issue 1585, S15). The fetch writes each asked version into the directory's temporary modfile before downloading: a trunk commit is required at its Go version with its checksums for go mod verify, a pull request's head is placed as a pin on a head is, a newer go.mod version is kept and said, and the answer and the log name each. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL --- modules/mesh-build-go/README.md | 7 + .../mesh-build-go/cmd/mesh-build-go/fetch.go | 24 +- .../mesh-build-go/cmd/mesh-build-go/pins.go | 44 ++- .../mesh-build-go/pins_integration_test.go | 17 +- .../cmd/mesh-build-go/versions.go | 265 ++++++++++++++++++ .../cmd/mesh-build-go/versions_test.go | 122 ++++++++ modules/mesh-build-go/go.mod | 2 +- modules/mesh-build-go/go.sum | 2 + 8 files changed, 456 insertions(+), 27 deletions(-) create mode 100644 modules/mesh-build-go/cmd/mesh-build-go/versions.go create mode 100644 modules/mesh-build-go/cmd/mesh-build-go/versions_test.go diff --git a/modules/mesh-build-go/README.md b/modules/mesh-build-go/README.md index 7b2c14f3..51619357 100644 --- a/modules/mesh-build-go/README.md +++ b/modules/mesh-build-go/README.md @@ -55,6 +55,13 @@ fetch looks for that commit among the forge's `refs/pull/*/head`, places the rep temporary `-modfile` in the workspace's `pins/` that replaces the module by that directory. A pin Go can fetch is left as it is, a pin on no ref fails naming the module and the commit, and a directory that vendors is never resolved. +The asked versions of the mesh's packages (novox/hq ADR 0573 §2, §4; issue 1585): a fetch given `packages` (a full commit +by Go module path, which the builder and the checker fill from the indexer's trunk values or a group's heads) writes each +into the temporary `-modfile` of every directory whose `go.mod` requires the package, before anything is downloaded. A +commit Go can fetch is required at its Go version with its checksums beside the modfile, which `go mod verify` checks; a +head only a pull request carries is placed as a pin on a head is; a newer version `go.mod` names is built instead, and +said. The fetch's answer names each in `packages`, and the module's log says each. + The account's ssh files (`files/forge`, `files/known_hosts`, `files/gitconfig`) are filled on the node by the provisioner's client from the module's entry of `forge-account` (the manifest says `moved`, ADR 0466 §4, ADR 0400 §5). The forge writes that entry only once the module has run and reported its key (ADR 0400 §2), so until then the client diff --git a/modules/mesh-build-go/cmd/mesh-build-go/fetch.go b/modules/mesh-build-go/cmd/mesh-build-go/fetch.go index e9d2f57f..808c0662 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/fetch.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/fetch.go @@ -52,6 +52,9 @@ type FetchResult struct { // Beside are the sibling repositories placed as plain directories under the workspace's beside directory, which // build_go_test hands to the tests as MESH_CHECK_BESIDE (ADR 0484 clause 4). Beside []Sibling `json:"beside,omitempty"` + // Packages are, of each directory whose go.mod requires an asked mesh package, the version it is built against + // (versions.go; novox/hq ADR 0573 §2): the asked, or go.mod's when it names a newer one, said. + Packages []TakenVersion `json:"packages,omitempty"` } // fetchTool is the fetch step tool. @@ -70,6 +73,7 @@ func (m *Module) fetchTool() StepTool { "base": map[string]any{"type": "string", "description": "optional trunk commit, 40 hex digits: the head is merged onto it as the forge's merge would, and the answer carries the merge commit, the files in conflict (an answer, the workspace is then left at the trunk commit), the changed paths and the tree of each module directory"}, "dir": buildstep.FetchRequestProperties()["dir"], "paths": buildstep.FetchRequestProperties()["paths"], + "packages": buildstep.FetchRequestProperties()["packages"], "beside": map[string]any{"type": "array", "description": "optional sibling repositories the tests read, each placed as a plain directory (no symlink) under the workspace's beside/, which build_go_test gives the tests as MESH_CHECK_BESIDE", "items": map[string]any{"type": "object", "properties": map[string]any{ "repository": map[string]any{"type": "string", "description": "/ on the forge"}, @@ -156,6 +160,7 @@ func (m *Module) fetchInto(ctx context.Context, req buildstep.FetchRequest, id s // step's. Resolving a pin places a repository beside the workspace, which two directories may share: one at a time. var pins sync.Mutex errs := make([]error, len(dirs)) + taken := make([][]TakenVersion, len(dirs)) slots := make(chan struct{}, m.parallel()) var wg sync.WaitGroup for i, d := range dirs { @@ -167,6 +172,10 @@ func (m *Module) fetchInto(ctx context.Context, req buildstep.FetchRequest, id s go func() { defer wg.Done() defer func() { <-slots }() + // The asked versions of the mesh's packages go into the modfile before anything is downloaded (versions.go). + if taken[i], errs[i] = m.takeVersions(ctx, root, filepath.Join(tree, d), d, req.Packages, &pins); errs[i] != nil { + return + } errs[i] = m.downloadModules(ctx, root, filepath.Join(tree, d), d, &pins) }() } @@ -176,6 +185,9 @@ func (m *Module) fetchInto(ctx context.Context, req buildstep.FetchRequest, id s return nil, err } } + for _, t := range taken { + res.Packages = append(res.Packages, t...) + } return res, nil } @@ -204,18 +216,20 @@ func (m *Module) downloadModules(ctx context.Context, root, dir, d string, pins var verifyLocks sync.Map // verifiedMark is the file naming a pass of go mod verify for a go.mod and go.sum pair, "" when a pass is not kept: -// the module cache is only read by steps that are shut off with it read-only (ADR 0448 §3), so the pass holds only then, -// and a directory with a pin modfile verifies another file. +// the module cache is only read by steps that are shut off with it read-only (ADR 0448 §3), so the pass holds only then. +// A directory with a temporary modfile (an asked version, a pin on a head) verifies the modfile and the sums beside it, +// which a placed repository's path makes the workspace's own. func (m *Module) verifiedMark(dir string) string { if m.Cache == "" || m.Bubblewrap == nil { return "" } - if _, ok := m.pinModfile(dir); ok { - return "" + from := dir + if modfile, ok := m.pinModfile(dir); ok { + from = filepath.Dir(modfile) } h := sha256.New() for _, name := range []string{"go.mod", "go.sum"} { - raw, _ := os.ReadFile(filepath.Join(dir, name)) + raw, _ := os.ReadFile(filepath.Join(from, name)) fmt.Fprintf(h, "%s %d\n", name, len(raw)) h.Write(raw) } diff --git a/modules/mesh-build-go/cmd/mesh-build-go/pins.go b/modules/mesh-build-go/cmd/mesh-build-go/pins.go index 52532d6f..63294545 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/pins.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/pins.go @@ -151,27 +151,41 @@ func (m *Module) placePin(ctx context.Context, root string, p pin, failure strin return where, nil } -// replaceInModfile adds `replace module => where` to the temporary modfile of dir, made from the directory's go.mod and -// go.sum the first time; Go reads the sums beside the modfile. +// replaceInModfile adds `replace module => where` to the temporary modfile of dir (ensureModfile). func (m *Module) replaceInModfile(dir, module, where string) error { - pins := m.pinsDir(dir) - if pins == "" { - return fmt.Errorf("the directory %s lies outside a workspace", dir) - } - if err := os.MkdirAll(pins, 0o700); err != nil { + modfile, err := m.ensureModfile(dir) + if err != nil { return err } - modfile := filepath.Join(pins, "go.mod") body, err := os.ReadFile(modfile) if err != nil { - if body, err = os.ReadFile(filepath.Join(dir, "go.mod")); err != nil { - return err - } - sum, _ := os.ReadFile(filepath.Join(dir, "go.sum")) - if err := os.WriteFile(filepath.Join(pins, "go.sum"), sum, 0o600); err != nil { - return err - } + return err } body = append(body, []byte("\nreplace "+module+" => "+where+"\n")...) return os.WriteFile(modfile, body, 0o600) } + +// ensureModfile is the temporary modfile of dir, made from the directory's go.mod and go.sum the first time (an asked +// version, versions.go, or a pin on a head writes into it); Go reads the sums beside the modfile. +func (m *Module) ensureModfile(dir string) (string, error) { + pins := m.pinsDir(dir) + if pins == "" { + return "", fmt.Errorf("the directory %s lies outside a workspace", dir) + } + modfile := filepath.Join(pins, "go.mod") + if _, err := os.Stat(modfile); err == nil { + return modfile, nil + } + if err := os.MkdirAll(pins, 0o700); err != nil { + return "", err + } + body, err := os.ReadFile(filepath.Join(dir, "go.mod")) + if err != nil { + return "", err + } + sum, _ := os.ReadFile(filepath.Join(dir, "go.sum")) + if err := os.WriteFile(filepath.Join(pins, "go.sum"), sum, 0o600); err != nil { + return "", err + } + return modfile, os.WriteFile(modfile, body, 0o600) +} diff --git a/modules/mesh-build-go/cmd/mesh-build-go/pins_integration_test.go b/modules/mesh-build-go/cmd/mesh-build-go/pins_integration_test.go index bfff720c..9060b950 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/pins_integration_test.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/pins_integration_test.go @@ -16,10 +16,11 @@ import ( "git.novox.be/novox/mesh-sdk/go/delivery" ) -// 1148/AC2: given a forge with a mesh-sdk-like pull request whose branch is deleted (only refs/pull/7/head carries its -// commit) and a Go member pinning that head by a pseudo-version, when build_go_fetch then build_go_build run on the -// member, then both pass, the build with GOPROXY=off. -func TestRealFetchAndBuildOfAMemberPinningAPullHead(t *testing.T) { +// 1148/AC2 as issue 1585 S15 has it (novox/hq ADR 0573 §4): given a forge with a mesh-sdk-like pull request whose branch +// is deleted (only refs/pull/7/head carries its commit) and a Go member whose go.mod names an older commit of it, when +// build_go_fetch is asked that head as the package's version and build_go_build runs on the member, then both pass, the +// build with GOPROXY=off against the head: the older commit holds no Go package at all. +func TestRealFetchAndBuildOfAMemberAskedAPullHeadItsGoModDoesNotName(t *testing.T) { // Not skipped when a tool is missing: a pass of the check must mean this test ran (the check prints no test log). for _, tool := range []string{"go", "git"} { if _, err := exec.LookPath(tool); err != nil { @@ -34,7 +35,7 @@ func TestRealFetchAndBuildOfAMemberPinningAPullHead(t *testing.T) { }, "refs/pull/7/head") git(t, work, "checkout", "-q", "main") // no branch of the repository carries the head member := map[string]string{ - "go.mod": "module fixture/member\n\ngo 1.25.0\n\nrequire git.novox.be/novox/dep/go v0.0.0-20261022120000-" + head[:12] + "\n", + "go.mod": "module fixture/member\n\ngo 1.25.0\n\nrequire git.novox.be/novox/dep/go v0.0.0-20261021120000-" + fork[:12] + "\n", "cmd/prog/main.go": "package main\n\nimport (\n\t\"fmt\"\n\n\tdep \"git.novox.be/novox/dep/go\"\n)\n\n" + "func main() { fmt.Println(dep.Name) }\n", "module.json": `{"module":"fixture-member","version":"1","build":{"artifacts":[{"name":"tools","kind":"bundle","language":"go","system":"arch","from":"cmd/prog","binary":"prog-bin","loads":["prog-bin"]}]}}`, @@ -43,10 +44,14 @@ func TestRealFetchAndBuildOfAMemberPinningAPullHead(t *testing.T) { m, states := f.module(t) withStore(t, m) // the build pushes its archive (ADR 0498) - res, st := fetched(t, m, states, map[string]any{"repository": "novox/member", "commit": memberHead}) + res, st := fetched(t, m, states, map[string]any{"repository": "novox/member", "commit": memberHead, + "packages": map[string]any{"git.novox.be/novox/dep/go": head}}) if st.State != delivery.StepPassed { t.Fatalf("the fetch ended %s: %s", st.State, st.Reason) } + if len(res.Packages) != 1 || res.Packages[0].Built != head || res.Packages[0].Named != "v0.0.0-20261021120000-"+fork[:12] { + t.Errorf("the fetch names what it built against: %+v, want the asked head %s", res.Packages, head) + } if _, err := os.Stat(filepath.Join(m.Work, res.Workspace, "pinned", "dep@"+head, "beside", "dep", "go", "dep.go")); err != nil { t.Errorf("the pull request's repository was not placed beside the workspace: %v", err) } diff --git a/modules/mesh-build-go/cmd/mesh-build-go/versions.go b/modules/mesh-build-go/cmd/mesh-build-go/versions.go new file mode 100644 index 00000000..a3d42471 --- /dev/null +++ b/modules/mesh-build-go/cmd/mesh-build-go/versions.go @@ -0,0 +1,265 @@ +package main + +// The asked versions of the mesh's packages (novox/hq ADR 0573 §2, §4; issue 1585): every module builds against the latest +// published version of each mesh package it uses, and a delivery group's check against the group's own heads, whatever +// version its go.mod names. The builder and the checker name each version in the ask (delivery.StepAsk.Packages, a full +// commit by package); before Go downloads anything, the fetch writes them into the temporary -modfile of each directory +// whose go.mod requires the package (pins.go), so the fetch's go commands, the build and the tests all take them: +// +// - a commit Go can fetch (a trunk merge) is resolved to its Go version, required in the modfile, and its checksums +// written beside it, which go mod verify then checks the downloaded module against; +// - a commit Go cannot fetch (the head of a group's pull request) is found among the forge's pull request heads and +// placed beside the workspace, and the modfile replaces the package by it, as a pin on a head is (pins.go); +// - a version go.mod names is the oldest the module accepts: one newer than the asked is built, and said loudly. +// +// Each package's outcome is named in the module's log and in the fetch's answer (FetchResult.Packages). + +import ( + "context" + "encoding/json" + "fmt" + "maps" + "os" + "path/filepath" + "slices" + "strconv" + "strings" + "sync" + + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +// TakenVersion is what the fetch built a directory against for one asked package: the asked commit, the version its go.mod +// names, the version built (a Go version, or the asked commit when it was placed from a pull request's head) and its +// checksum, and why the built one is not the asked one when it is not. +type TakenVersion struct { + Dir string `json:"dir"` + Package string `json:"package"` + Asked string `json:"asked"` + Named string `json:"named"` + Built string `json:"built"` + Sum string `json:"sum,omitempty"` + Said string `json:"said,omitempty"` +} + +// downloadInfo is what go mod download -json answers of one module. +type downloadInfo struct { + Version string + Sum string + GoModSum string + Error string +} + +// takeVersions writes the asked versions into the modfile of dir (d in the tree) and answers what it took, one entry per +// asked package its go.mod requires. A package go.mod replaces is built as go.mod says, and said. A version that can be +// neither fetched nor found among the forge's pull request heads fails the fetch, naming it. pins serialises the +// placements a workspace's directories share. +func (m *Module) takeVersions(ctx context.Context, root, dir, d string, asked delivery.PackageVersions, pins *sync.Mutex) ([]TakenVersion, error) { + raw, err := os.ReadFile(filepath.Join(dir, "go.mod")) + if err != nil { + return nil, nil + } + required, replaced := goModRequirements(string(raw)) + var taken []TakenVersion + for _, pkg := range slices.Sorted(maps.Keys(asked)) { + named, ok := required[pkg] + if !ok { + continue // the module does not use it + } + tv := TakenVersion{Dir: d, Package: pkg, Asked: asked[pkg], Named: named} + switch { + case replaced[pkg]: + tv.Built, tv.Said = named, "go.mod replaces it, so it is built as go.mod says" + default: + if err := m.takeVersion(ctx, root, dir, &tv, pins); err != nil { + return taken, err + } + } + if m.Say != nil { + m.Say("%s: %s is built at %s (asked %.12s, go.mod names %s)%s", d, pkg, tv.Built, tv.Asked, tv.Named, saidSuffix(tv.Said)) + } + taken = append(taken, tv) + } + return taken, nil +} + +func saidSuffix(said string) string { + if said == "" { + return "" + } + return ": " + said +} + +// takeVersion takes one asked version into dir's modfile (takeVersions). +func (m *Module) takeVersion(ctx context.Context, root, dir string, tv *TakenVersion, pins *sync.Mutex) error { + modfile, err := m.ensureModfile(dir) + if err != nil { + return err + } + pinsDir := filepath.Dir(modfile) + r := m.Run(ctx, m.goEnv(), "go", "-C", pinsDir, "mod", "download", "-json", tv.Package+"@"+tv.Asked) + var info downloadInfo + _ = json.Unmarshal([]byte(r.Stdout), &info) + if r.Status != 0 || r.Err != "" || info.Version == "" || info.Error != "" { + // Not a commit Go can fetch: the head of a group's pull request (ADR 0573 §4), placed from the forge's heads. + failure := strings.TrimSpace(info.Error + "\n" + r.Err + "\n" + r.Stderr) + repo, sub, _ := strings.Cut(strings.TrimPrefix(tv.Package, forgePrefix), "/") + p := pin{module: tv.Package, repo: repo, sub: sub, short: tv.Asked[:12]} + pins.Lock() + where, err := m.placePin(ctx, root, p, failure) + if err == nil { + err = m.replaceInModfile(dir, tv.Package, where) + } + pins.Unlock() + if err != nil { + return fmt.Errorf("the asked version %s of %s: %w", tv.Asked, tv.Package, err) + } + tv.Built, tv.Said = tv.Asked, "Go cannot fetch it: placed from the pull request's head, which has no checksum" + return nil + } + if compareVersions(tv.Named, info.Version) > 0 { + // A version go.mod names is the oldest the module accepts (ADR 0573 §2): said loudly, and go.mod's is built. + tv.Built = tv.Named + tv.Said = fmt.Sprintf("go.mod names %s, newer than the asked %s, so go.mod's is built (a version a repository names is the oldest it accepts, ADR 0573 §2)", tv.Named, info.Version) + return nil + } + tv.Built, tv.Sum = info.Version, info.Sum + if tv.Named == info.Version { + return nil + } + if r := m.Run(ctx, m.goEnv(), "go", "-C", pinsDir, "mod", "edit", "-require="+tv.Package+"@"+info.Version); r.Status != 0 || r.Err != "" { + return fmt.Errorf("the asked version %s of %s could not be required: %s", info.Version, tv.Package, causeLine(r.Err+"\n"+r.Stderr)) + } + // The checksums Go computed of the download, which go mod verify checks the module cache against. + sums := "" + if info.Sum != "" { + sums += tv.Package + " " + info.Version + " " + info.Sum + "\n" + } + if info.GoModSum != "" { + sums += tv.Package + " " + info.Version + "/go.mod " + info.GoModSum + "\n" + } + f, err := os.OpenFile(filepath.Join(pinsDir, "go.sum"), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o600) + if err != nil { + return err + } + _, err = f.WriteString(sums) + if cerr := f.Close(); err == nil { + err = cerr + } + return err +} + +// goModRequirements are the module paths a go.mod requires, with the version each names, and those it replaces. +func goModRequirements(gomod string) (required map[string]string, replaced map[string]bool) { + required, replaced = map[string]string{}, map[string]bool{} + block := "" + for _, line := range strings.Split(gomod, "\n") { + if i := strings.Index(line, "//"); i >= 0 { + line = line[:i] + } + f := strings.Fields(line) + switch { + case len(f) == 0: + continue + case f[0] == ")": + block = "" + continue + case len(f) == 2 && f[1] == "(": + block = f[0] + continue + } + verb := block + if f[0] == "require" || f[0] == "replace" { + verb, f = f[0], f[1:] + } + switch { + case verb == "require" && len(f) >= 2: + required[f[0]] = f[1] + case verb == "replace" && len(f) >= 1: + replaced[f[0]] = true + } + } + return required, replaced +} + +// compareVersions orders two Go module versions (semantic versions, pseudo-versions among them, by their pre-release +// identifiers): negative when a is older than b, 0 when equal, positive when newer. A version that is not a semantic +// version is older than any that is. +func compareVersions(a, b string) int { + pa, oka := parseVersion(a) + pb, okb := parseVersion(b) + switch { + case !oka && !okb: + return strings.Compare(a, b) + case !oka: + return -1 + case !okb: + return 1 + } + for i := 0; i < 3; i++ { + if c := pa.core[i] - pb.core[i]; c != 0 { + if c < 0 { + return -1 + } + return 1 + } + } + switch { + case len(pa.pre) == 0 && len(pb.pre) == 0: + return 0 + case len(pa.pre) == 0: + return 1 // a release is newer than its pre-releases + case len(pb.pre) == 0: + return -1 + } + for i := 0; i < len(pa.pre) && i < len(pb.pre); i++ { + if c := comparePre(pa.pre[i], pb.pre[i]); c != 0 { + return c + } + } + return len(pa.pre) - len(pb.pre) +} + +type version struct { + core [3]int + pre []string +} + +func parseVersion(v string) (version, bool) { + s, ok := strings.CutPrefix(v, "v") + if !ok { + return version{}, false + } + s, _, _ = strings.Cut(s, "+") + core, pre, hasPre := strings.Cut(s, "-") + parts := strings.Split(core, ".") + if len(parts) != 3 { + return version{}, false + } + var out version + for i, p := range parts { + n, err := strconv.Atoi(p) + if err != nil || n < 0 { + return version{}, false + } + out.core[i] = n + } + if hasPre { + out.pre = strings.Split(pre, ".") + } + return out, true +} + +func comparePre(a, b string) int { + na, ea := strconv.Atoi(a) + nb, eb := strconv.Atoi(b) + switch { + case ea == nil && eb == nil: + return na - nb + case ea == nil: + return -1 // numeric identifiers are older than alphanumeric ones + case eb == nil: + return 1 + } + return strings.Compare(a, b) +} diff --git a/modules/mesh-build-go/cmd/mesh-build-go/versions_test.go b/modules/mesh-build-go/cmd/mesh-build-go/versions_test.go new file mode 100644 index 00000000..e9fea937 --- /dev/null +++ b/modules/mesh-build-go/cmd/mesh-build-go/versions_test.go @@ -0,0 +1,122 @@ +package main + +import ( + "context" + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" + + "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-sdk/go/forgekey" +) + +// Issue 1585 S15 (novox/hq ADR 0573 §2, §4): the asked version of a mesh package is written into the directory's modfile +// before anything is downloaded, whatever go.mod names, with its checksums beside it, and named in the fetch's answer: a +// trunk commit Go resolves is required at its Go version; a version go.mod names that is newer is built instead, said; a +// group's head Go cannot fetch is placed from the forge's pull request heads; a go.mod that does not require the package +// takes nothing. +func TestAnAskedVersionIsWrittenIntoTheModfileWhateverGoModNames(t *testing.T) { + const sdk = "git.novox.be/novox/mesh-sdk/go" + trunk := strings.Repeat("a", 40) + for _, c := range []struct { + name, asked, answer string + failing bool + built, said, edit string + }{ + {name: "a trunk commit newer than go.mod's", asked: trunk, + answer: `{"Path":"` + sdk + `","Version":"v0.1.15-0.20261101000000-aaaaaaaaaaaa","Sum":"h1:zip=","GoModSum":"h1:mod="}`, + built: "v0.1.15-0.20261101000000-aaaaaaaaaaaa", edit: "-require=" + sdk + "@v0.1.15-0.20261101000000-aaaaaaaaaaaa"}, + {name: "go.mod names a newer one", asked: trunk, + answer: `{"Path":"` + sdk + `","Version":"v0.1.15-0.20261001000000-aaaaaaaaaaaa","Sum":"h1:zip="}`, + built: "v0.1.15-0.20261022120000-" + pinShort, said: "newer than the asked"}, + {name: "a group's head Go cannot fetch", asked: commitSHA, failing: true, + answer: `{"Path":"` + sdk + `","Error":"invalid version: unknown revision ` + pinShort + `"}`, + built: commitSHA, said: "placed from the pull request's head"}, + } { + t.Run(c.name, func(t *testing.T) { + h, states := newPinHarness(t, pinnedGoMod, false, pullList) + h.f.gomods = "go.mod\nother/go.mod\n" + base := h.m.Run + h.m.Run = func(ctx context.Context, env []string, name string, args ...string) forgekey.Ran { + line := name + " " + strings.Join(args, " ") + if name == "git" && strings.Contains(line, " checkout ") && !strings.Contains(line, "pinned") { + r := base(ctx, env, name, args...) + for i, a := range args { + if a == "-C" { // other/ uses no mesh package + os.WriteFile(filepath.Join(args[i+1], "other", "go.mod"), []byte("module fixture/other\n\ngo 1.26\n"), 0o644) + } + } + return r + } + if name == "go" && strings.Contains(line, "/other mod ") { + return forgekey.Ran{} // other/ requires nothing Go cannot fetch + } + if name == "go" && strings.Contains(line, " mod download -json "+sdk+"@") { + h.mu.Lock() + h.cmds = append(h.cmds, cmdRecord{env: env, line: line}) + h.mu.Unlock() + if c.failing { + return forgekey.Ran{Status: 1, Stdout: c.answer} + } + return forgekey.Ran{Stdout: c.answer} + } + return base(ctx, env, name, args...) + } + args := map[string]any{"repository": "novox/mesh-delivery", "commit": commitSHA, "packages": map[string]any{sdk: c.asked}} + if _, err := fetchTool(t, h.m)(args); err != nil { + t.Fatal(err) + } + st := states.waitEnd(t) + if st.State != delivery.StepPassed { + t.Fatalf("the step ended %s: %s", st.State, st.Reason) + } + var res FetchResult + if err := json.Unmarshal(st.Result, &res); err != nil { + t.Fatal(err) + } + if len(res.Packages) != 1 { + t.Fatalf("packages = %+v, want one: the directory requiring mesh-sdk", res.Packages) + } + got := res.Packages[0] + if got.Dir != "." || got.Package != sdk || got.Asked != c.asked || got.Built != c.built || !strings.Contains(got.Said, c.said) { + t.Errorf("taken = %+v, want built %s saying %q", got, c.built, c.said) + } + if h.ran(" mod edit ") != (c.edit != "") || (c.edit != "" && !h.ran(c.edit)) { + t.Errorf("go mod edit %q ran: %v", c.edit, h.ran(" mod edit ")) + } + modfile, ok := h.m.pinModfile(filepath.Join(h.m.Work, res.Workspace, "tree")) + if !ok { + t.Fatal("the directory was handed no modfile") + } + sums, _ := os.ReadFile(filepath.Join(filepath.Dir(modfile), "go.sum")) + if wantSum := c.edit != ""; strings.Contains(string(sums), "h1:zip=") != wantSum || strings.Contains(string(sums), "/go.mod h1:mod=") != wantSum { + t.Errorf("the checksums beside the modfile:\n%s", sums) + } + body, _ := os.ReadFile(modfile) + if placed := strings.Contains(string(body), "replace "+sdk+" => "); placed != c.failing { + t.Errorf("the modfile replaces mesh-sdk by a placed head: %v, want %v:\n%s", placed, c.failing, body) + } + if _, other := h.m.pinModfile(filepath.Join(h.m.Work, res.Workspace, "tree", "other")); other { + t.Error("a go.mod that requires no asked package was handed a modfile") + } + }) + } + // The order of Go versions: a pseudo-version by its time, a release after its pre-releases, a non-version first. + for _, v := range []struct { + a, b string + want int + }{ + {"v0.1.15-0.20261101000000-aaaaaaaaaaaa", "v0.1.15-0.20261022120000-0123456789ab", 1}, + {"v0.1.14-0.20261031084708-acbf80ad910a", "v0.1.14-0.20261101005726-af4e4e60a3a0", -1}, + {"v0.1.7", "v0.1.14-0.20261101005726-af4e4e60a3a0", -1}, + {"v0.1.14", "v0.1.14-0.20261101005726-af4e4e60a3a0", 1}, + {"v0.2.0", "v0.2.0", 0}, + {"main", "v0.0.1", -1}, + } { + if got := compareVersions(v.a, v.b); (got > 0) != (v.want > 0) || (got < 0) != (v.want < 0) { + t.Errorf("compareVersions(%s, %s) = %d, want the sign of %d", v.a, v.b, got, v.want) + } + } +} diff --git a/modules/mesh-build-go/go.mod b/modules/mesh-build-go/go.mod index 18b896e0..1fd3aede 100644 --- a/modules/mesh-build-go/go.mod +++ b/modules/mesh-build-go/go.mod @@ -3,7 +3,7 @@ module meshbuildgo go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101005726-af4e4e60a3a0 github.com/distribution/distribution/v3 v3.1.2 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 diff --git a/modules/mesh-build-go/go.sum b/modules/mesh-build-go/go.sum index fc784332..afeb09b3 100644 --- a/modules/mesh-build-go/go.sum +++ b/modules/mesh-build-go/go.sum @@ -1,5 +1,7 @@ git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a h1:DcSvfEmdPBKqCg5OmXRNb269EkkPe7BsEp+RykEhkgE= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101005726-af4e4e60a3a0 h1:q6ywchpTM9fwaJ1UNwDSuOjb5FYDFcsaeiTrKoB4scc= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101005726-af4e4e60a3a0/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI= github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=