Compare commits

...
Author SHA1 Message Date
jschoubben 06cdac97e3 Pin the group's mesh-sdk head 0d97c09c for the retake class (issue 1748)
mesh/delivery-group group fix/1748-retake-and-resolver checking: 2 of 2 member(s) ready
mesh/delivery closed: the pull request closed unmerged
mesh/check pass 06cdac97e32c on fd4931a75261 | judged by the candidate's mesh-build-go at 3200cb79deed
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 22:45:25 +01:00
jschoubben 82aaae846e mesh-build-go opens a network failure of the fetch with the retake prefix (issue 1748)
The builder takes a retakeable failure again by itself after a backoff.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 22:41:57 +01:00
mesh-tools fd4931a752 Merge pull request 'mesh-build-go: a merge preview names a path and a path inside it (mesh-contracts' go), and a step's cause keeps the module Go could not find (issue 1966)' (#613) from fix/buildstep-preview into main 2026-11-04 19:44:20 +00:00
jschoubben da2aa91832 Keep Go's indented continuation in a step's cause: the composer's reason ended at "requires" and hid the module that could not be found (issue 1966)
mesh/delivery merged: source.main moved to novox/mesh-catalog@fd4931a75261
mesh/check being carried to da2aa9183221 on fd4931a75261 | from 8acad77d33f9
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 20:19:16 +01:00
jschoubben 1332f8b523 Ask git ls-tree for nested named paths in rounds: given go and go/seats/x together it lists go's children, so mesh-contracts' preview failed (issue 1966)
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 20:19:16 +01:00
12 changed files with 173 additions and 21 deletions
+43 -10
View File
@@ -82,18 +82,20 @@ func narrow(ctx context.Context, run forgekey.Runner, env []string, dir, commit,
for i, p := range paths {
full[i] = path.Join(mdir, p)
}
r := run(ctx, env, "git", append([]string{"-C", dir, "ls-tree", "-z", commit, "--"}, full...)...)
if r.Status != 0 || r.Err != "" {
return fmt.Errorf("git ls-tree %s of the module %s: %s", commit, mdir, forgekey.Said(r.Err+"\n"+r.Stderr))
}
objects := map[string]string{}
for _, l := range strings.Split(r.Stdout, "\x00") {
meta, name, ok := strings.Cut(l, "\t")
fields := strings.Fields(meta)
if !ok || len(fields) != 3 {
continue
for _, round := range unnested(full) {
r := run(ctx, env, "git", append([]string{"-C", dir, "ls-tree", "-z", commit, "--"}, round...)...)
if r.Status != 0 || r.Err != "" {
return fmt.Errorf("git ls-tree %s of the module %s: %s", commit, mdir, forgekey.Said(r.Err+"\n"+r.Stderr))
}
for _, l := range strings.Split(r.Stdout, "\x00") {
meta, name, ok := strings.Cut(l, "\t")
fields := strings.Fields(meta)
if !ok || len(fields) != 3 {
continue
}
objects[name] = fields[2]
}
objects[name] = fields[2]
}
mt.Paths = map[string]string{}
for i, p := range paths {
@@ -106,4 +108,35 @@ func narrow(ctx context.Context, run forgekey.Runner, env []string, dir, commit,
return nil
}
// unnested splits paths into rounds of one git ls-tree each, none of whose paths lies inside another of its round: given
// "go" and "go/seats/x" together, git ls-tree descends into go and lists its children instead of go itself, so the
// object of go is missing from its answer (issue 1966: mesh-contracts' manifest names both). Usually one round; the
// order of paths is kept within a round.
func unnested(paths []string) [][]string {
var rounds [][]string
for _, p := range paths {
placed := false
for i, round := range rounds {
if !nestsWithAny(p, round) {
rounds[i] = append(round, p)
placed = true
break
}
}
if !placed {
rounds = append(rounds, []string{p})
}
}
return rounds
}
func nestsWithAny(p string, round []string) bool {
for _, q := range round {
if p == q || strings.HasPrefix(p, q+"/") || strings.HasPrefix(q, p+"/") || p == "." || q == "." {
return true
}
}
return false
}
func sha256Hex(b []byte) string { h := sha256.Sum256(b); return hex.EncodeToString(h[:]) }
@@ -62,4 +62,16 @@ func TestAGoOnlyCommitLeavesTheNamedObjectsEqualOnRealGit(t *testing.T) {
if src["."].Paths["src"] != realGit(t, repo, "rev-parse", "HEAD:src") {
t.Fatalf("the object of src is %s, git says %s", src["."].Paths["src"], realGit(t, repo, "rev-parse", "HEAD:src"))
}
// A manifest naming a path and a path inside it (mesh-contracts' "go" and "go/seats/meshvault"; issue 1966): each
// gets its own object, not git ls-tree's listing of the outer one's children.
writeFile(t, repo, "nested/module.json", `{"module":"nested","version":"1","build":{"artifacts":[`+
`{"name":"go","kind":"package","language":"go","paths":["go"]},`+
`{"name":"go-seat","kind":"package","language":"go","paths":["go/seats/a"]}]}}`)
writeFile(t, repo, "nested/go/go.mod", "module x\n")
writeFile(t, repo, "nested/go/seats/a/a.go", "package a\n")
nested := trees(commit("nested"))["nested"]
if nested.Paths["go"] != realGit(t, repo, "rev-parse", "HEAD:nested/go") || nested.Paths["go/seats/a"] != realGit(t, repo, "rev-parse", "HEAD:nested/go/seats/a") {
t.Fatalf("a path and a path inside it: %v", nested.Paths)
}
}
@@ -97,9 +97,10 @@ func (m *Module) candidate(ctx context.Context, workspace string) (string, error
// compileError is the first line of the compiler's output that says an error: not the `# package` heading go build
// prints above a package's errors, nor Go's progress.
func compileError(out string) string {
for _, l := range strings.Split(out, "\n") {
lines := strings.Split(out, "\n")
for i, l := range lines {
if l = strings.TrimSpace(l); l != "" && !strings.HasPrefix(l, "#") && !strings.HasPrefix(l, "go: downloading ") {
return l
return withContinuation(l, lines[i+1:])
}
}
return firstLine(out)
@@ -113,7 +113,8 @@ func (m *Module) fetch(ctx context.Context, args map[string]any) (res any, err e
}
}
}()
return m.fetchInto(ctx, req, id, siblings)
res, err = m.fetchInto(ctx, req, id, siblings)
return res, retakeable(err) // a network failure is a retakeable one (issue 1748)
}
func (m *Module) fetchInto(ctx context.Context, req sdkstep.FetchRequest, id string, siblings []siblingArg) (any, error) {
@@ -351,15 +352,33 @@ func (m *Module) now() time.Time { return m.clock() }
// causeLine is the first line of a go command's output that says something other than Go's own progress: `go:
// downloading ...` comes first when Go fetches a module, and `# <package>` heads a compile error of go build or go vet
// (issue 1703: the reason said only the package), and the cause of a failure on a later line.
//
// A cause Go writes over several lines keeps its indented continuation lines, joined by a space: a module graph error
// is "X requires" with the module that fails, and why, on the next line (issue 1966: the composer's reason ended at
// "requires" and hid "golang.org/x/tools@v0.42.0: module lookup disabled").
func causeLine(s string) string {
for _, l := range strings.Split(s, "\n") {
lines := strings.Split(s, "\n")
for i, l := range lines {
if l = strings.TrimSpace(l); l != "" && !strings.HasPrefix(l, "go: downloading ") && !strings.HasPrefix(l, "# ") {
return l
return withContinuation(l, lines[i+1:])
}
}
return firstLine(s)
}
// withContinuation is first with each following line Go indented under it, trimmed and joined by a space.
func withContinuation(first string, rest []string) string {
for _, l := range rest {
if !strings.HasPrefix(l, "\t") && !strings.HasPrefix(l, " ") {
break
}
if l = strings.TrimSpace(l); l != "" {
first += " " + l
}
}
return first
}
func firstLine(s string) string {
for _, l := range strings.Split(s, "\n") {
if l = strings.TrimSpace(l); l != "" {
@@ -309,9 +309,14 @@ func TestCauseLineSkipsProgressAndThePackageHeading(t *testing.T) {
"go: downloading x v1\n# git.example/m/cmd\n./main.go:3:2: undefined: y\n": "./main.go:3:2: undefined: y",
"\ngo: example.com/x@v1: reading: 404\n": "go: example.com/x@v1: reading: 404",
"go: downloading x v1\n": "go: downloading x v1",
// issue 1966: a module graph error goes on, indented, on the next line; a later error is not taken in.
"a.go:3:8: x@v1 requires\n\ty@v2: module lookup disabled by GOPROXY=off\nb.go:1:1: other\n": "a.go:3:8: x@v1 requires y@v2: module lookup disabled by GOPROXY=off",
} {
if got := causeLine(in); got != want {
t.Errorf("causeLine(%q) = %q, want %q", in, got, want)
}
if got := compileError(in); got != want {
t.Errorf("compileError(%q) = %q, want %q", in, got, want)
}
}
}
@@ -17,6 +17,7 @@ import (
"regexp"
"strings"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/forgekey"
)
@@ -140,7 +141,7 @@ func (m *Module) placePin(ctx context.Context, root string, p pin, failure strin
r := m.Run(ctx, []string{"GIT_TERMINAL_PROMPT=0"}, "git", "-c", "credential.helper=", "ls-remote", m.remote(repository), "refs/pull/*/head")
if r.Status != 0 || r.Err != "" {
// The forge did not answer: retaken, with its own cause, never held as a refusal until the module changes (issue 1748).
return "", fmt.Errorf("the pin of %s at commit %s: listing the pull requests of %s failed (a download failure that may be retaken): %s", p.module, p.short, repository, forgekey.Said(r.Err+"\n"+r.Stderr))
return "", fmt.Errorf(delivery.Retake+"the pin of %s at commit %s: listing the pull requests of %s failed (a download failure, taken again by the builder): %s", p.module, p.short, repository, forgekey.Said(r.Err+"\n"+r.Stderr))
}
commit, ref := "", ""
for _, l := range strings.Split(r.Stdout, "\n") {
@@ -0,0 +1,39 @@
package main
import (
"fmt"
"strings"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// networkWords are what a failed lookup, connection or transfer says in Go's, git's and the resolver's words: a failure of
// the network or of the forge that passes by itself (issue 1748), not of the source built.
var networkWords = []string{
"i/o timeout", "no such host", "could not resolve host", "temporary failure in name resolution", "server misbehaving",
"connection refused", "connection reset", "tls handshake timeout", "network is unreachable", "unexpected eof", "http2: server sent goaway",
}
// networkFailure says whether a failure of a download or a fetch is the network's: its words are one of networkWords and
// it is not go not knowing the revision (unknownRevision, a pin on no head), which waits for another fingerprint.
func networkFailure(failure string) bool {
if unknownRevision(failure) {
return false
}
low := strings.ToLower(failure)
for _, w := range networkWords {
if strings.Contains(low, w) {
return true
}
}
return false
}
// retakeable opens a network failure of the fetch step with delivery.Retake, so the builder takes the key again after a
// backoff (delivery.Retakeable, issue 1748); any other error, one that opens with a prefix already included, is as it was.
func retakeable(err error) error {
if err == nil || strings.HasPrefix(err.Error(), delivery.Retake) || !networkFailure(err.Error()) {
return err
}
return fmt.Errorf("%s%w", delivery.Retake, err)
}
@@ -0,0 +1,42 @@
package main
import (
"errors"
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// 1748/AC2: a download or a fetch that fails on the network ends with an error that opens with the retake prefix; a
// compile error, a failing test and an unknown revision (a pin on no head) do not.
func TestANetworkFailureOfTheFetchOpensWithTheRetakePrefix(t *testing.T) {
for _, c := range []struct {
name, failure string
retake bool
}{
{"a lookup that times out", "go mod download in .: git.novox.be/novox/mesh-sdk/go@v1: dial tcp: lookup git.novox.be on 10.10.0.3:53: read udp 10.10.0.3:42292->10.10.0.3:53: i/o timeout", true},
{"a refused connection", "git ls-remote -q origin: dial tcp 10.10.0.1:443: connect: connection refused", true},
{"a forge that cannot be resolved", "fatal: unable to access the forge: Could not resolve host: git.novox.be", true},
{"an unknown revision", "invalid version: unknown revision abcdef123456", false},
{"an unknown revision on a cut network is still one go knows", "unknown revision abcdef123456: i/o timeout", false},
{"a compile error", "go build of tool: ./main.go:3:1: undefined: x", false},
{"a failing test", "go test ./...: FAIL: TestX", false},
} {
t.Run(c.name, func(t *testing.T) {
got := retakeable(errors.New(c.failure))
if has := strings.HasPrefix(got.Error(), delivery.Retake); has != c.retake {
t.Fatalf("retakeable(%q) = %q", c.failure, got)
}
if c.retake && (!delivery.Retakeable(got.Error()) || !strings.Contains(got.Error(), c.failure)) {
t.Errorf("%q does not keep the cause or is not Retakeable", got)
}
})
}
if retakeable(nil) != nil {
t.Error("retakeable(nil) is an error")
}
if once := retakeable(errors.New(delivery.Retake + "connection refused")); strings.Count(once.Error(), delivery.Retake) != 1 {
t.Errorf("the prefix was added twice: %q", once)
}
}
@@ -113,7 +113,7 @@ func (m *Module) takeVersion(ctx context.Context, root, dir string, tv *TakenVer
pins.Unlock()
// On no head, and go failed for another cause (the forge or the resolver did not answer): it fails with go's own
// words, and not as a kind the builder holds until the module changes (issue 1748).
return fmt.Errorf("downloading the asked version %s of %s failed (a download failure that may be retaken): %s", tv.Asked, tv.Package, causeLine(failure))
return fmt.Errorf(delivery.Retake+"downloading the asked version %s of %s failed (a download failure, taken again by the builder): %s", tv.Asked, tv.Package, causeLine(failure))
}
if err == nil {
err = m.replaceInModfile(dir, tv.Package, where)
@@ -92,7 +92,7 @@ func TestAnAskedVersionIsWrittenIntoTheModfileWhateverGoModNames(t *testing.T) {
st := states.waitEnd(t)
if c.failsWith != "" {
if st.State != delivery.StepFailed || !strings.Contains(st.Reason, c.failsWith) || strings.Contains(st.Reason, "no pull request head") ||
!strings.Contains(st.Reason, "may be retaken") || delivery.FailedUntilChanged(st.Reason) {
!strings.HasPrefix(st.Reason, delivery.Retake) || delivery.FailedUntilChanged(st.Reason) {
t.Errorf("the download failure ended %s: %q", st.State, st.Reason)
}
return
+1 -1
View File
@@ -4,7 +4,7 @@ go 1.26.0
require (
git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104214149-0d97c09c49b4
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
+2 -2
View File
@@ -1,5 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAuqVBXbybSqhLNOGX5rUOrn9T0cHNCDUQF7IQ=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104214149-0d97c09c49b4 h1:K2niszrlnJ7JMiXI6xg4gG90YCd/tqNLGBvujLInI48=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104214149-0d97c09c49b4/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=