Write a failed publish of a merged image to the key; read mesh-build-image's captured steps (review r2)
mesh/repo-check head db62e82e26d7 merged into trunk 47ab8cd3cf45 beside mesh-catalog#435@7c5d11e5 — pass: its merge-check.sh passed
mesh/merge-gate head db62e82e26d7 merged into trunk 47ab8cd3cf45 — pass: group feat/r4c9-image-build composed: every machine composes with the change as it…
mesh/check pass db62e82e26d7 on 2b635e2138ae | unit 366 run 0 kept | int 405 run 0 kept | clone 9.2s unit 191ms int 6.7s+44s | composition not yet run
mesh/delivery delivered
mesh/delivery-group group feat/r4c9-image-build delivered: every member is delivered

This commit is contained in:
2026-10-22 10:37:21 +02:00
parent 12f61998c5
commit db62e82e26
7 changed files with 133 additions and 10 deletions
@@ -0,0 +1,41 @@
package build
import (
"encoding/json"
"os"
"testing"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// result reads a captured step value of mesh-build-image (testdata/captured/README) and returns its result.
func captured(t *testing.T, name string) json.RawMessage {
t.Helper()
raw, err := os.ReadFile("testdata/captured/" + name)
if err != nil {
t.Fatal(err)
}
var step delivery.Step
if err := json.Unmarshal(raw, &step); err != nil {
t.Fatalf("%s: %v", name, err)
}
if step.State != delivery.StepPassed || len(step.Result) == 0 {
t.Fatalf("%s: state %q, result %s, want a passed step with a result", name, step.State, step.Result)
}
return step.Result
}
func TestCapturedImageStepsDecodeWithTheBuildersOwnTypes(t *testing.T) { // review r2, mesh-catalog 2; ADR 0487 clause 1
var f fetchResult
if err := json.Unmarshal(captured(t, "fetch.json"), &f); err != nil || f.Workspace == "" {
t.Errorf("fetch = %+v, %v, want a workspace (the field the builder reads)", f, err)
}
var trunk buildResult
if err := json.Unmarshal(captured(t, "build-trunk.json"), &trunk); err != nil || trunk.Digest == "" || trunk.Reference == "" {
t.Errorf("trunk build = %+v, %v, want a digest and a reference", trunk, err)
}
var cand buildResult
if err := json.Unmarshal(captured(t, "build-candidate.json"), &cand); err != nil || cand.Digest == "" || cand.Reference != "" {
t.Errorf("candidate build = %+v, %v, want a digest and no reference", cand, err)
}
}
@@ -310,6 +310,7 @@ func (b *Builder) try(ctx context.Context, module string, m delivery.Module) {
// built as a candidate's; the other artifacts stay as the done key has them (ADR 0487 clauses 5 and 8).
var prior *delivery.BuilderBuild
var only map[string]bool
publishing := false
if cl.Build.State == delivery.BuildDone {
prior, only = &cl.Build, map[string]bool{}
for _, bm := range OneNodeModules(cl.Build) {
@@ -317,6 +318,7 @@ func (b *Builder) try(ctx context.Context, module string, m delivery.Module) {
}
if trunk && Unpublished(cl.Build) {
only[ImageBuildModule] = true
publishing = true
}
}
result := b.build(ctx, module, m, plan, planErr, prior, only)
@@ -328,7 +330,11 @@ func (b *Builder) try(ctx context.Context, module string, m delivery.Module) {
return
}
result.By, result.Manifest = b.Claimer.By, m.Manifest
if cl.Build.State == delivery.BuildDone && result.State == delivery.BuildFailed && !strings.HasPrefix(result.Error, buildrun.DigestsDifferPrefix) {
if publishing && result.State == delivery.BuildFailed {
// The trunk's image was built only as a candidate's and its push failed: the key says so, so the builds view, the
// checker and the failed-key retake see it, and a look does not build the image again for nothing.
result.Error = "publishing the image of the merged commit failed: " + result.Error
} else if cl.Build.State == delivery.BuildDone && result.State == delivery.BuildFailed && !strings.HasPrefix(result.Error, buildrun.DigestsDifferPrefix) {
// A done key built again on a second node keeps its done value when the new build fails for another reason than
// the digests: the first build stands, and a later change of the nodes looks again.
b.logf("%s: built again on a second node and failed, the done build stands: %s", cl.Key, result.Error)
@@ -597,6 +603,18 @@ func (b *Builder) step(ctx context.Context, bm, node, tool string, args map[stri
return b.Runner.Run(ctx, bm, node, toolPrefix[bm]+tool, args)
}
// fetchResult and buildResult are the results of a build module's fetch and build steps as the builder reads them.
type fetchResult struct {
Workspace string `json:"workspace"`
Merge string `json:"merge"`
Conflicts []string `json:"conflicts"`
}
type buildResult struct {
Digest string `json:"digest"`
Reference string `json:"reference"`
}
// runLane takes a place on the node, fetches the commit there and builds each of the lane's artifacts, one step at a time.
func (b *Builder) runLane(ctx context.Context, l *lane, repository string, m delivery.Module, module string) {
release, err := b.Places.Acquire(ctx, l.node)
@@ -619,11 +637,7 @@ func (b *Builder) runLane(ctx context.Context, l *lane, repository string, m del
l.err = err
return
}
var fetched struct {
Workspace string `json:"workspace"`
Merge string `json:"merge"`
Conflicts []string `json:"conflicts"`
}
var fetched fetchResult
if err := json.Unmarshal(raw, &fetched); err != nil || fetched.Workspace == "" {
l.err = fmt.Errorf("the fetch on %s answered no workspace: %s", l.node, raw)
return
@@ -652,10 +666,7 @@ func (b *Builder) runLane(ctx context.Context, l *lane, repository string, m del
l.err = err
return
}
var built struct {
Digest string `json:"digest"`
Reference string `json:"reference"`
}
var built buildResult
if err := json.Unmarshal(raw, &built); err != nil || built.Digest == "" {
l.err = fmt.Errorf("the build of %s of %s on %s answered no digest: %s", t.Artifact, module, l.node, raw)
return
@@ -606,3 +606,28 @@ func TestASecondGoNodeDoesNotBuildOrPushTheImageAgain(t *testing.T) { // review
t.Errorf("%d go builds, want 3", n)
}
}
func TestAFailedPublishOfACandidatesImageIsWrittenToTheKey(t *testing.T) { // review r2, mesh-delivery 1; ADR 0487 clause 5
r := newRig(t, imageArt)
r.b.Health = imageHave(t, "ace")
trunkModule(t, r)
r.b.Candidate(context.Background(), "feat%2Fx", candidateValue(t, "feat/x", 7, headX))
r.b.Wait()
if v := valueOf(t, r.st, r.key); v.State != delivery.BuildDone {
t.Fatalf("candidate build = %+v", v)
}
r.bm.fails["ace/build_image_build"] = "push to the registry: connection refused"
r.b.Module(context.Background(), "m", moduleValue(t, "modules/m")) // the merge: the trunk names the fingerprint
r.b.Wait()
v := valueOf(t, r.st, r.key)
if v.State != delivery.BuildFailed || !strings.Contains(v.Error, "connection refused") || !strings.Contains(v.Error, "publish") {
t.Fatalf("build = %+v, want failed naming the publish and its reason", v)
}
// The failed key is retaken as any failed key is, and ends published once the registry answers.
delete(r.bm.fails, "ace/build_image_build")
r.b.Module(context.Background(), "m", moduleValue(t, "modules/m"))
r.b.Wait()
if v := valueOf(t, r.st, r.key); v.State != delivery.BuildDone || v.Artifacts[0].Reference == "" {
t.Errorf("build = %+v, want done with a reference", v)
}
}
@@ -0,0 +1,4 @@
Captured step values of mesh-build-image (fetch, a candidate's build, a trunk's build), copied from
novox/mesh-catalog modules/mesh-build-image/cmd/mesh-build-image/testdata/captured/. Both copies must change together: the
module's capture test writes them, and the builder's TestCapturedImageStepsDecodeWithTheBuildersOwnTypes (internal/build,
fixtures_test.go) decodes these with the types the builder reads results with and fails on a missing field.
@@ -0,0 +1,14 @@
{
"ended": "2026-10-22T08:00:01Z",
"id": "STEPID",
"node": "ace",
"result": {
"artifact": "server",
"digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111",
"pushed": false
},
"started": "2026-10-22T08:00:00Z",
"state": "passed",
"tool": "build_image_build",
"v": 1
}
@@ -0,0 +1,15 @@
{
"ended": "2026-10-22T08:00:01Z",
"id": "STEPID",
"node": "ace",
"result": {
"artifact": "server",
"digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111",
"pushed": true,
"reference": "REGISTRY/fixture-image/server@sha256:1111111111111111111111111111111111111111111111111111111111111111"
},
"started": "2026-10-22T08:00:00Z",
"state": "passed",
"tool": "build_image_build",
"v": 1
}
@@ -0,0 +1,13 @@
{
"ended": "2026-10-22T08:00:01Z",
"id": "STEPID",
"node": "ace",
"result": {
"commit": "COMMIT",
"workspace": "WORKSPACE"
},
"started": "2026-10-22T08:00:00Z",
"state": "passed",
"tool": "build_image_fetch",
"v": 1
}