mesh-build-go takes a zero-version requirement from the ask or the trunk, never from the pull request heads (ADR 0573 sections 2 and 4, issue 1966)
mesh/check failed: integration: mesh-vault/cmd/mesh-vault.Tes… | 1feda1f533b8 on 559939aa8ce1 | judged by the candidate's mesh-build-go at 87d30b709d68
mesh/delivery closed: a newer head of the same pull request
mesh/check failed: integration: mesh-vault/cmd/mesh-vault.Tes… | 1feda1f533b8 on 559939aa8ce1 | judged by the candidate's mesh-build-go at 87d30b709d68
mesh/delivery closed: a newer head of the same pull request
A go.mod requirement of a forge module at v0.0.0-00010101000000-000000000000 names no commit; the fetch looked for the commit 000000000000 among the pull request heads and failed. It is now taken from the commit the ask names for the package, else from the head of its repository among the asked versions, else from the latest published version. Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
@@ -19,6 +19,7 @@ import (
|
||||
|
||||
"git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep"
|
||||
sdkstep "git.novox.be/novox/mesh-sdk/go/buildstep"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
||||
)
|
||||
|
||||
@@ -177,7 +178,9 @@ func (m *Module) fetchInto(ctx context.Context, req sdkstep.FetchRequest, id str
|
||||
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)
|
||||
var more []TakenVersion
|
||||
more, errs[i] = m.downloadModules(ctx, root, filepath.Join(tree, d), d, &pins, req.Packages)
|
||||
taken[i] = append(taken[i], more...)
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
@@ -194,21 +197,29 @@ func (m *Module) fetchInto(ctx context.Context, req sdkstep.FetchRequest, id str
|
||||
|
||||
// downloadModules downloads and verifies the Go modules of the directory dir (d in the tree). A pin of a forge module
|
||||
// that Go cannot fetch is looked for among the forge's pull request heads and handed to Go by a modfile (pins.go); a
|
||||
// failure that names no such pin is the first failure.
|
||||
func (m *Module) downloadModules(ctx context.Context, root, dir, d string, pins *sync.Mutex) error {
|
||||
// failure that names no such pin is the first failure. A requirement at the zero version is taken from the asked
|
||||
// versions (pins.go takeZeroPins).
|
||||
func (m *Module) downloadModules(ctx context.Context, root, dir, d string, pins *sync.Mutex, asked delivery.PackageVersions) ([]TakenVersion, error) {
|
||||
var taken []TakenVersion
|
||||
for {
|
||||
failure := m.downloadAndVerify(ctx, dir, d)
|
||||
if failure == nil {
|
||||
return nil
|
||||
return taken, nil
|
||||
}
|
||||
// A requirement at the zero version names no commit: it is taken from the asked versions or the trunk's latest.
|
||||
took, err := m.takeZeroPins(ctx, root, dir, d, failure.Error(), asked, pins)
|
||||
taken = append(taken, took...)
|
||||
if err != nil {
|
||||
return taken, err
|
||||
}
|
||||
pins.Lock()
|
||||
progress, err := m.resolvePins(ctx, root, dir, failure.Error())
|
||||
pins.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
return taken, err
|
||||
}
|
||||
if !progress {
|
||||
return failure
|
||||
if !progress && len(took) == 0 {
|
||||
return taken, failure
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,16 +12,23 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
||||
)
|
||||
|
||||
const forgePrefix = "git.novox.be/novox/"
|
||||
|
||||
// zeroShort is the 12 digits a requirement at the zero version v0.0.0-00010101000000-000000000000 ends in: no commit.
|
||||
const zeroShort = "000000000000"
|
||||
|
||||
var pseudoVersion = regexp.MustCompile(`[-.][0-9]{14}-([0-9a-f]{12})$`)
|
||||
|
||||
// pin is a requirement of a go.mod on a commit of a repository of the forge, by a pseudo-version.
|
||||
@@ -107,6 +114,9 @@ func (m *Module) resolvePins(ctx context.Context, root, dir, failure string) (bo
|
||||
}
|
||||
progress := false
|
||||
for _, p := range forgePins(string(raw)) {
|
||||
if p.short == zeroShort {
|
||||
continue // takeZeroPins': a requirement at the zero version names no commit to look for
|
||||
}
|
||||
if !strings.Contains(failure, p.short) || strings.Contains(done, "replace "+p.module+" =>") {
|
||||
continue
|
||||
}
|
||||
@@ -241,3 +251,66 @@ func absoluteReplaces(body []byte, dir string) []byte {
|
||||
}
|
||||
return []byte(strings.Join(lines, "\n"))
|
||||
}
|
||||
|
||||
// takeZeroPins takes, for each requirement of dir's go.mod at the zero version that a go failure names, the version it is
|
||||
// built against (ADR 0573 §2, §4: whatever go.mod names): the commit the ask names for the package; else the head of
|
||||
// the package's repository among the asked versions (the check's or try's members: another package of the same
|
||||
// repository names it); else the latest published version, the trunk's head. It never looks for the commit
|
||||
// 000000000000 among the pull request heads. It answers what it took; none when no zero requirement is named.
|
||||
func (m *Module) takeZeroPins(ctx context.Context, root, dir, d, failure string, asked delivery.PackageVersions, pins *sync.Mutex) ([]TakenVersion, error) {
|
||||
raw, err := os.ReadFile(filepath.Join(dir, "go.mod"))
|
||||
if err != nil || !strings.Contains(failure, zeroShort) {
|
||||
return nil, nil
|
||||
}
|
||||
resolved := map[string]string{}
|
||||
if modfile, ok := m.pinModfile(dir); ok {
|
||||
if b, err := os.ReadFile(modfile); err == nil {
|
||||
resolved, _ = goModRequirements(string(b))
|
||||
for module := range resolved {
|
||||
if strings.Contains(string(b), "replace "+module+" =>") {
|
||||
resolved[module] = "replaced"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var taken []TakenVersion
|
||||
for _, p := range forgePins(string(raw)) {
|
||||
if p.short != zeroShort {
|
||||
continue
|
||||
}
|
||||
if v := resolved[p.module]; v != "" && !strings.HasSuffix(v, zeroShort) {
|
||||
continue // taken before
|
||||
}
|
||||
named := "v0.0.0-00010101000000-" + zeroShort
|
||||
tv := TakenVersion{Dir: d, Package: p.module, Asked: askedFor(asked, p), Named: named}
|
||||
why := "the ask names it"
|
||||
if tv.Asked == "" {
|
||||
tv.Asked, why = "latest", "the ask names no commit of its repository, so the trunk's latest published version"
|
||||
} else if _, ok := asked[p.module]; !ok {
|
||||
why = "the head of its repository among the asked versions"
|
||||
}
|
||||
if err := m.takeVersion(ctx, root, dir, &tv, pins); err != nil {
|
||||
return taken, fmt.Errorf("the requirement of %s at the zero version: %w", p.module, err)
|
||||
}
|
||||
tv.Said = strings.TrimPrefix(tv.Said+"; "+why, "; ")
|
||||
if m.Say != nil {
|
||||
m.Say("%s: %s names no commit; built at %s (%s)", d, p.module, tv.Built, why)
|
||||
}
|
||||
taken = append(taken, tv)
|
||||
}
|
||||
return taken, nil
|
||||
}
|
||||
|
||||
// askedFor is the commit the ask names for the package p: its own, else that of the first package of the same repository
|
||||
// in order (the members' heads are by repository), else none.
|
||||
func askedFor(asked delivery.PackageVersions, p pin) string {
|
||||
if c := asked[p.module]; c != "" {
|
||||
return c
|
||||
}
|
||||
for _, pkg := range slices.Sorted(maps.Keys(asked)) {
|
||||
if pkg == forgePrefix+p.repo || strings.HasPrefix(pkg, forgePrefix+p.repo+"/") {
|
||||
return asked[pkg]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -290,3 +290,71 @@ func TestARelativeReplaceStillResolvesInTheTemporaryModfile(t *testing.T) {
|
||||
t.Fatalf("go mod verify in the pins directory: %v\n%s", err, out)
|
||||
}
|
||||
}
|
||||
|
||||
// A go.mod requirement at the zero version v0.0.0-00010101000000-000000000000 names no commit (ADR 0573 §2, §4: whatever
|
||||
// go.mod names): it is taken from the commit the ask names for the package, else from the head of its repository among
|
||||
// the asked versions, else from the trunk's latest published version, and never looked for as the commit 000000000000
|
||||
// among the pull request heads.
|
||||
func TestARequirementAtTheZeroVersionIsTakenFromTheAskOrTheTrunk(t *testing.T) {
|
||||
const sdk = "git.novox.be/novox/mesh-sdk/go"
|
||||
zeroMod := "module fixture/member\n\ngo 1.26\n\nrequire (\n\t" + sdk + " v0.0.0-00010101000000-" + zeroShort + "\n)\n"
|
||||
trunk := strings.Repeat("a", 40)
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
packages map[string]any
|
||||
download string // what go mod download -json answers
|
||||
want string // the version asked of go
|
||||
head bool // the asked commit is on no trunk: only a pull request head carries it
|
||||
}{
|
||||
{name: "the ask names the package", packages: map[string]any{sdk: trunk}, want: sdk + "@" + trunk,
|
||||
download: `{"Version":"v0.1.15-0.20261101000000-aaaaaaaaaaaa","Sum":"h1:zip="}`},
|
||||
{name: "the ask names another package of its repository", packages: map[string]any{"git.novox.be/novox/mesh-sdk/other": trunk}, want: sdk + "@" + trunk,
|
||||
download: `{"Version":"v0.1.15-0.20261101000000-aaaaaaaaaaaa","Sum":"h1:zip="}`},
|
||||
{name: "the ask names the head of a pull request", packages: map[string]any{sdk: commitSHA}, want: sdk + "@" + commitSHA, head: true},
|
||||
{name: "the ask names nothing of its repository", packages: map[string]any{"git.novox.be/novox/mesh-tools/go": trunk}, want: sdk + "@latest",
|
||||
download: `{"Version":"v0.1.20-0.20261104000000-bbbbbbbbbbbb","Sum":"h1:zip="}`},
|
||||
{name: "no ask", want: sdk + "@latest", download: `{"Version":"v0.1.20-0.20261104000000-bbbbbbbbbbbb","Sum":"h1:zip="}`},
|
||||
} {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
h, states := newPinHarness(t, zeroMod, false, pullList)
|
||||
base := h.m.Run
|
||||
h.m.Run = func(ctx context.Context, env []string, name string, args ...string) forgekey.Ran {
|
||||
line := name + " " + strings.Join(args, " ")
|
||||
switch {
|
||||
case name == "go" && strings.Contains(line, " mod download -json "):
|
||||
h.mu.Lock()
|
||||
h.cmds = append(h.cmds, cmdRecord{env: env, line: line})
|
||||
h.mu.Unlock()
|
||||
if c.head {
|
||||
return forgekey.Ran{Status: 1, Stdout: `{"Error":"invalid version: unknown revision ` + commitSHA[:12] + `"}`}
|
||||
}
|
||||
return forgekey.Ran{Stdout: c.download}
|
||||
case name == "go" && strings.Contains(line, " mod download") && !strings.Contains(strings.Join(env, " "), "-modfile="):
|
||||
return forgekey.Ran{Status: 1, Stderr: "go: " + sdk + "@v0.0.0-00010101000000-" + zeroShort + ": invalid version: unknown revision " + zeroShort + "\n"}
|
||||
}
|
||||
return base(ctx, env, name, args...)
|
||||
}
|
||||
args := map[string]any{"repository": "novox/mesh-delivery", "commit": commitSHA}
|
||||
if c.packages != nil {
|
||||
args["packages"] = c.packages
|
||||
}
|
||||
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)
|
||||
}
|
||||
if !h.ran(" mod download -json " + c.want) {
|
||||
t.Errorf("go was not asked for %s:\n%s", c.want, strings.Join(h.f.lines(), "\n"))
|
||||
}
|
||||
// The pull request heads are listed only to place an asked head, never to look for the commit of zeros.
|
||||
if listed := h.ran("ls-remote"); listed != c.head {
|
||||
t.Errorf("the pull request heads were listed: %v, want %v", listed, c.head)
|
||||
}
|
||||
if h.ran("pinned/mesh-sdk@" + zeroShort) {
|
||||
t.Error("a repository was placed at the commit of zeros")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +105,10 @@ func (m *Module) takeVersion(ctx context.Context, root, dir string, tv *TakenVer
|
||||
// Not a commit Go can fetch: the head of a group's pull request (ADR 0573 §4), placed from the forge's heads.
|
||||
// Whether it is one is the forge's fact (placePin), whatever go said.
|
||||
failure := strings.TrimSpace(info.Error + "\n" + r.Err + "\n" + r.Stderr)
|
||||
if !commitOK.MatchString(tv.Asked) {
|
||||
// "latest" (the trunk's head) is no commit of a pull request: there is nothing to place.
|
||||
return fmt.Errorf("the %s version of %s could not be taken (a download failure that may be retaken): %s", tv.Asked, tv.Package, causeLine(failure))
|
||||
}
|
||||
repo, sub, _ := strings.Cut(strings.TrimPrefix(tv.Package, forgePrefix), "/")
|
||||
p := pin{module: tv.Package, repo: repo, sub: sub, short: tv.Asked[:12]}
|
||||
pins.Lock()
|
||||
|
||||
Reference in New Issue
Block a user