Files
mesh-catalog/modules/mesh-build-go/buildstep/publish.go
T
jschoubben 37a1f2be53 CAT-NR1: every build narrates its asks of a seat in one place
asked, waiting on the seat every five minutes, stored or refused, as the ask's
phase on its step key, so a publish or a copy that stands still is readable on
the bus. PublishOne and PublishFiles narrate for every build module; the image
copy and the late publish use the same narration.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 16:05:27 +01:00

87 lines
3.5 KiB
Go

package buildstep
// The publish of a done build's file through a registry seat (novox/hq design 51 *Publishing*, ADR 0611; CAT-BG1,
// CAT-BN3): from the build module's own process, after the build step ended, through the seat's verbs publish and
// publish-part, which each build module's manifest `invokes`, in chunks, only what the holder lacks. The holder checks
// the build with the builder and says published or refused on the seat's events. One place for every build module, so
// the move to the registry clients is made here once.
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"os"
"strings"
reg "git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry"
"git.novox.be/novox/mesh-sdk/go/publish"
)
// The registry seats a build module publishes on.
const (
ArtifactSeat = "oci-artifact-registry"
ImageSeat = reg.Seat
GoSeat = "go-package-registry"
NpmSeat = "npm-package-registry"
)
// SeatCall asks one verb of a seat, `seat:<seat>.<verb>`, with a body, and answers its JSON (the bundle's stdio.Ask).
type SeatCall = func(key string, body any) (json.RawMessage, error)
// TransferName is a transfer's name, the build module's own: the name, a qualifier and the digest's head, safe as a
// transfer id.
func TransferName(name, qualifier, digest string) string {
head := strings.TrimPrefix(digest, "sha256:")
if len(head) > 16 {
head = head[:16]
}
return strings.NewReplacer("/", "-", ".", "-", " ", "-", "*", "-", ">", "-", "@", "-").Replace(name + "-" + qualifier + "-" + head)
}
// PublishOne publishes body, of digest, as name (at version, when it has one) of kind on seat, in one part.
func PublishOne(ctx context.Context, call SeatCall, seat, name, kind, version, digest, transfer string, body []byte) error {
in := reg.PublishInput{Name: name, Kind: kind, Version: version, Digest: digest, Transfer: transfer}
_, err := OnSeat(ctx, seat, publishWhat(in), func() (publish.Sent, error) {
return publish.Publish(ctx, publish.Call(call), seat, in, []publish.Part{{Digest: digest, Size: int64(len(body)),
Open: func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(body)), nil }}})
}, SentHow(1))
return err
}
// publishWhat names a publish in a narration: its name, its version when it has one, and its digest.
func publishWhat(in reg.PublishInput) string {
what := "publish of " + in.Name
if in.Version != "" {
what += " " + in.Version
}
return what + " at " + in.Digest
}
// SentHow says what of a publish of parts travelled, for its stored line.
func SentHow(parts int) func(publish.Sent) string {
return func(s publish.Sent) string {
return fmt.Sprintf("%d of its %d parts sent, the rest held", len(s.Parts), parts)
}
}
// File is one part of a publish kept on disk: an image's config, layer or manifest, by its digest and size.
type File struct {
Digest string
Size int64
Path string
}
// PublishFiles publishes in with files as its parts (an image's config, layers and manifest, CAT-BI1): the holder
// answers the parts it lacks, and only those are read and sent.
func PublishFiles(ctx context.Context, call SeatCall, seat string, in reg.PublishInput, files []File) (publish.Sent, error) {
ps := make([]publish.Part, 0, len(files))
for _, f := range files {
ps = append(ps, publish.Part{Digest: f.Digest, Size: f.Size, Open: func() (io.ReadCloser, error) { return os.Open(f.Path) }})
}
return OnSeat(ctx, seat, publishWhat(in), func() (publish.Sent, error) {
return publish.Publish(ctx, publish.Call(call), seat, in, ps)
}, SentHow(len(files)))
}