Commit the build modules' publish code a bare ignore pattern hid; drop committed go.work.sum

The patterns mesh-build-node and mesh-build-image matched the command
directory, so publish.go and its test were never committed and the
branch did not compile. Anchored to the binaries now.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
2026-11-04 16:24:06 +01:00
parent b88c6dac5b
commit eb50bf1f48
7 changed files with 310 additions and 68 deletions
@@ -0,0 +1,151 @@
package main
// The publish of a trunk build (novox/hq design 51 *Publishing*, ADR 0611; CAT-BI1): after the build has ended, from
// the module's own process and not from a Dockerfile step, the image is written as an OCI layout in the step's
// workspace and published through the seat container-image-registry's verbs publish and publish-part, which the
// manifest `invokes`, with the build modules' one sender (buildstep.PublishFiles): its config, its layers and its
// manifest, by their digests, of which only the parts the holder lacks travel. This module holds no registry address and
// no credential; the holder checks the build with the builder and says published or refused on the seat's events. The
// step answers the seat's reference, container-image-registry/<module>/<artifact>@sha256:<digest>.
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
reg "git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep"
)
// KindImage is the kind an image is published as on its seat.
const KindImage = "image"
// call is the seat's request: Call when a test set it, else the runtime's request over the bus (stdio.Ask).
func (m *Module) call() buildstep.SeatCall {
if m.Call != nil {
return m.Call
}
return stdio.Ask
}
// layout is an OCI image layout one build wrote: its one manifest's digest and the files of its parts.
type layout struct {
Digest string
Files []buildstep.File
}
type descriptor struct {
MediaType string `json:"mediaType"`
Digest string `json:"digest"`
Size int64 `json:"size"`
}
// readLayout reads the layout in dir: index.json names exactly one manifest, which names its config and layers; every
// part is a blob of the layout, of the size its descriptor says. The manifest comes last, so a holder stores it after
// what it names.
func readLayout(dir string) (layout, error) {
var index struct {
Manifests []descriptor `json:"manifests"`
}
raw, err := os.ReadFile(filepath.Join(dir, "index.json"))
if err != nil {
return layout{}, fmt.Errorf("the image's layout has no index: %w", err)
}
if err := json.Unmarshal(raw, &index); err != nil || len(index.Manifests) != 1 {
return layout{}, fmt.Errorf("the image's layout names %d manifests, not one (%v)", len(index.Manifests), err)
}
top := index.Manifests[0]
blob := func(d descriptor) (buildstep.File, error) {
if !digestOK.MatchString(d.Digest) {
return buildstep.File{}, fmt.Errorf("%q is not a digest", d.Digest)
}
p := filepath.Join(dir, "blobs", "sha256", strings.TrimPrefix(d.Digest, "sha256:"))
info, err := os.Stat(p)
if err != nil {
return buildstep.File{}, fmt.Errorf("the layout lacks the blob %s: %w", d.Digest, err)
}
if info.Size() != d.Size {
return buildstep.File{}, fmt.Errorf("the blob %s holds %d bytes, not the %d its descriptor says", d.Digest, info.Size(), d.Size)
}
return buildstep.File{Digest: d.Digest, Size: d.Size, Path: p}, nil
}
manifest, err := blob(top)
if err != nil {
return layout{}, err
}
var body struct {
Config descriptor `json:"config"`
Layers []descriptor `json:"layers"`
}
raw, err = os.ReadFile(manifest.Path)
if err != nil || json.Unmarshal(raw, &body) != nil || body.Config.Digest == "" {
return layout{}, fmt.Errorf("the manifest %s names no config (%v)", top.Digest, err)
}
out := layout{Digest: top.Digest}
seen := map[string]bool{}
for _, d := range append([]descriptor{body.Config}, body.Layers...) {
if seen[d.Digest] {
continue
}
seen[d.Digest] = true
f, err := blob(d)
if err != nil {
return layout{}, err
}
out.Files = append(out.Files, f)
}
out.Files = append(out.Files, manifest)
return out, nil
}
// publish writes local as an OCI layout and publishes it on the image seat as <module>/<artifact>, at the version the
// builder gave it (and the tag the call names): the step answers the layout's manifest digest, checked against the
// digest buildah wrote, and the seat's reference; a publish the holder does not take fails `not stored:`.
func (m *Module) publish(ctx context.Context, workspace, module, local string, res BuildResult, version, tag string) (any, error) {
dir := filepath.Join(m.Work, workspace, "layout."+res.Artifact)
digestFile := filepath.Join(m.Work, workspace, "pushed."+res.Artifact)
_ = os.RemoveAll(dir)
defer os.RemoveAll(dir)
argv := append(m.storeFlags(), "push", "--digestfile", digestFile, local, "oci:"+dir)
if r := m.Run(ctx, m.env(), "buildah", argv...); r.Status != 0 || r.Err != "" {
m.sayFailure("buildah push of "+res.Artifact+" to its layout", r)
return nil, fmt.Errorf("%s%s: writing its layout: %s", delivery.NotStored, res.Artifact, cause(r))
}
img, err := readLayout(dir)
if err != nil {
return nil, fmt.Errorf("%s%s: %w", delivery.NotStored, res.Artifact, err)
}
// What buildah wrote is checked against the layout: a digest never goes out unchecked.
raw, err := os.ReadFile(digestFile)
if pushed := strings.TrimSpace(string(raw)); err != nil || pushed != img.Digest {
return nil, fmt.Errorf("%s%s: buildah wrote the digest %q (%v), but its layout's manifest is %s", delivery.NotStored, res.Artifact, pushed, err, img.Digest)
}
name := module + "/" + res.Artifact
q := KindImage
if version != "" {
q = "v" + version
}
in := reg.PublishInput{Name: name, Kind: KindImage, Version: version, Tag: tag, Digest: img.Digest,
Transfer: buildstep.TransferName(name, q, img.Digest)}
sent, err := buildstep.PublishFiles(ctx, m.call(), buildstep.ImageSeat, in, img.Files)
if err != nil {
return nil, fmt.Errorf("%s%s: %v", delivery.NotStored, res.Artifact, err)
}
m.say("published %s on %s: %d of its %d parts sent, the rest held", name, buildstep.ImageSeat, len(sent.Parts), len(img.Files))
res.Digest, res.Pushed = img.Digest, true
res.Reference = catalogue.SeatReference(buildstep.ImageSeat, module, res.Artifact, img.Digest)
return res, nil
}
func (m *Module) say(format string, a ...any) {
if m.Say != nil {
m.Say(format, a...)
}
}
@@ -0,0 +1,124 @@
package main
import (
"errors"
"os"
"path/filepath"
"slices"
"strings"
"testing"
reg "git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// 1703d/AC (ADR 0487 §5): a candidate's build leaves the image seat unchanged: nothing is written, nothing is asked.
func TestACandidatesBuildLeavesTheRegistryUnchanged(t *testing.T) {
m, f, states := setup(t, same)
res, st := runBuild(t, m, states, nil) // no publish
if st.State != delivery.StepPassed || res.Pushed {
t.Fatalf("step = %+v result = %+v", st, res)
}
if len(f.called("push")) != 0 || len(f.published) != 0 {
t.Errorf("a candidate's build reached the seat: published %v, calls %v", f.published, f.lines())
}
}
// CAT-BI1 (design 51 *Publishing*): a trunk build writes its image as an OCI layout after the build has ended and
// publishes it once through container-image-registry, as <module>/<artifact> at the ask's version, its config, layers
// and manifest by digest, sending only the parts the holder lacks; the key carries the layout's manifest digest and the
// seat's reference. A holder refusing, a digest file that disagrees and a seat other than the image seat each fail.
func TestATrunkBuildPublishesThroughTheImageSeatOnlyWhatTheHolderLacks(t *testing.T) {
for name, c := range map[string]struct {
extra map[string]any
heldLayer bool
refuse error
digestFile string
reason string
}{
"every part sent": {extra: map[string]any{"version": "1.4.0"}},
"the held layer is not sent": {extra: map[string]any{"version": "1.4.0"}, heldLayer: true},
"a tag of the call's own": {extra: map[string]any{"tag": "fp-abc123"}},
"the holder refuses": {refuse: errors.New("refused: the builder did not number it"), reason: "not stored: server: "},
"buildah wrote another digest": {digestFile: "other", reason: "not stored: server: buildah wrote the digest"},
"buildah wrote no digest": {digestFile: "none", reason: "not stored: server: buildah wrote the digest \"\""},
"another seat": {extra: map[string]any{"seat": "oci-artifact-registry"}, reason: "is published through container-image-registry"},
} {
t.Run(name, func(t *testing.T) {
m, f, states := setup(t, same)
f.refuse, f.digestFile = c.refuse, c.digestFile
args := map[string]any{"publish": true}
for k, v := range c.extra {
args[k] = v
}
if c.heldLayer {
// The layout's layer is the fake's one fixed layer: held under any name.
f.held[sha([]byte("the layer's bytes"))] = true
}
res, st := runBuild(t, m, states, args)
if c.reason != "" {
if (st.State != delivery.StepFailed && st.State != delivery.StepRefused) || !strings.Contains(st.Reason, c.reason) {
t.Fatalf("step = %+v, want failed with %q", st, c.reason)
}
return
}
if st.State != delivery.StepPassed {
t.Fatalf("step = %+v", st)
}
if !res.Pushed || res.Digest != f.pushed || res.Reference != reg.Seat+"/fixture-image/server@"+f.pushed {
t.Errorf("result = %+v, want the layout's %s", res, f.pushed)
}
if len(f.published) != 1 {
t.Fatalf("published = %+v", f.published)
}
p := f.published[0]
version, _ := c.extra["version"].(string)
tag, _ := c.extra["tag"].(string)
if p.Name != "fixture-image/server" || p.Kind != "image" || p.Digest != f.pushed || p.Version != version || p.Tag != tag || p.Transfer == "" {
t.Errorf("publish = %+v", p)
}
var parts []string
for _, x := range p.Parts {
parts = append(parts, x.Digest)
}
if !slices.Equal(parts, f.parts) {
t.Errorf("parts = %v, want config, layer, manifest %v", parts, f.parts)
}
want := f.parts
if c.heldLayer {
want = []string{f.parts[0], f.parts[2]}
}
if !slices.Equal(f.sent, want) {
t.Errorf("sent = %v, want %v", f.sent, want)
}
push := f.called("push")
if len(push) != 1 || !strings.Contains(push[0].line, " oci:"+filepath.Join(m.Work, "ws1", "layout.server")) || strings.Contains(push[0].line, "docker://") {
t.Fatalf("push = %v", push)
}
for _, bad := range []string{"--creds", "--authfile", "--sign", "--tls-verify"} {
if strings.Contains(push[0].line, bad) {
t.Errorf("the push carries %s", bad)
}
}
if _, err := os.Stat(filepath.Join(m.Work, "ws1", "layout.server")); !os.IsNotExist(err) {
t.Errorf("the layout outlived the step: %v", err)
}
// The layout is written after the build has ended: bud, then images, then push.
order := []string{}
for _, l := range f.lines() {
for _, v := range []string{" bud ", " images ", " push ", " rmi "} {
if strings.Contains(l, "buildah") && strings.Contains(l, v) {
order = append(order, strings.TrimSpace(v))
}
}
}
if strings.Join(order, ",") != "bud,images,push,rmi" {
t.Errorf("order = %v", order)
}
})
}
m2, f2, states2 := setup(t, same)
if _, st := runBuild(t, m2, states2, map[string]any{"publish": true, "tag": "a/b:c"}); st.State != delivery.StepFailed || len(f2.called("bud")) != 0 {
t.Errorf("a tag that is not one: step = %+v", st)
}
}