BG7, BN7: a fetch naming builds reads each package member from the artifact store

Each member's build is read at /v2/<module>/<artifact>/blobs/<digest> through the required provision artifact-store
(bound to a file), checked against its digest and laid as the package it names: a Go module zip replaced in the temporary
modfile, an npm tarball in node_modules. A mismatch or a store that does not answer fails the step naming the member.

Builds: design 47 G §465, §373, §466 (lead answer A-151)

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-12 05:00:48 +01:00
parent 247c422fbf
commit 904b8aedd4
16 changed files with 489 additions and 13 deletions
@@ -0,0 +1,70 @@
package main
// **A group run's package members, laid from their builds** (design 47 G §465, §373): a fetch naming `builds` reads each
// member's build (the npm package's packed tarball, package.go's packed) from the artifact store by its digest, checks
// it and places it as the local package it names in each TypeScript module's node_modules: nothing is fetched by a
// version, and none is reserved. A member that does not match its digest, or a store that does not answer, fails the
// step naming the member.
import (
"context"
"fmt"
"os"
"path/filepath"
"git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep"
sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep"
)
// storedBuild is one member's build, read and checked.
type storedBuild struct {
member sdkstep.FetchBuild
tgz []byte
}
// readBuilds reads every member of builds from the artifact store.
func (m *Module) readBuilds(ctx context.Context, builds []sdkstep.FetchBuild) ([]storedBuild, error) {
if len(builds) == 0 {
return nil, nil
}
base, err := buildstep.StoreURL(m.ArtifactStoreFile)
if err != nil {
return nil, fmt.Errorf("a group run's members are read from the artifact store: %w", err)
}
var out []storedBuild
for _, b := range builds {
data, err := buildstep.ReadStoredBuild(ctx, m.httpClient(), base, b)
if err != nil {
return nil, err
}
out = append(out, storedBuild{b, data})
}
return out, nil
}
// names says whether a member of the builds is the package pkg.
func namesPackage(builds []storedBuild, pkg string) bool {
for _, b := range builds {
if b.member.Package == pkg {
return true
}
}
return false
}
// layBuilds places each member in dir's node_modules as the package it names, in place of any other.
func (m *Module) layBuilds(dir string, builds []storedBuild) error {
for _, b := range builds {
into := filepath.Join(dir, "node_modules", filepath.FromSlash(b.member.Package))
if err := os.RemoveAll(into); err != nil {
return err
}
if err := unpackPackage(b.tgz, into); err != nil {
return fmt.Errorf("the build of %s (%s %s at %s): %w", b.member.Package, b.member.Module, b.member.Artifact, b.member.Digest, err)
}
if m.Say != nil {
m.Say("laid %s from its build %s (%s %s)", b.member.Package, b.member.Digest, b.member.Module, b.member.Artifact)
}
}
return nil
}
@@ -0,0 +1,57 @@
package main
import (
"context"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep"
)
// Design 47 G §465, §373 (BN7): a fetch naming builds reads each member from the artifact store by its digest, checks the
// bytes and lays it in node_modules as the package named; a mismatch, or a store that does not answer, fails naming it.
func TestAFetchLaysAGroupMembersBuildFromTheArtifactStore(t *testing.T) {
tgz := tgzOf(t, "@novox/lib", "0.0.0")
digest := digestOf(tgz)
store := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v2/lib/pkg/blobs/"+digest {
http.NotFound(w, r)
return
}
w.Write(tgz)
}))
defer store.Close()
m := &Module{ArtifactStoreFile: filepath.Join(t.TempDir(), "artifact-store.json")}
os.WriteFile(m.ArtifactStoreFile, []byte(`{"at":"`+strings.TrimPrefix(store.URL, "http://")+`"}`), 0o600)
member := sdkstep.FetchBuild{Package: "@novox/lib", Module: "lib", Artifact: "pkg", Digest: digest}
got, err := m.readBuilds(context.Background(), []sdkstep.FetchBuild{member})
if err != nil {
t.Fatal(err)
}
dir := t.TempDir()
if err := m.layBuilds(dir, got); err != nil {
t.Fatal(err)
}
if b, err := os.ReadFile(filepath.Join(dir, "node_modules", "@novox", "lib", "dist", "index.js")); err != nil || !strings.Contains(string(b), "0.0.0") {
t.Fatalf("the member was not laid: %q, %v", b, err)
}
if !namesPackage(got, "@novox/lib") || namesPackage(got, "@novox/other") {
t.Error("namesPackage is wrong")
}
bad := member
bad.Digest = "sha256:" + strings.Repeat("0", 64)
for _, c := range []sdkstep.FetchBuild{bad, {Package: "@novox/lib", Module: "lib", Artifact: "missing", Digest: digest}} {
if _, err := m.readBuilds(context.Background(), []sdkstep.FetchBuild{c}); err == nil || !strings.Contains(err.Error(), "@novox/lib") {
t.Errorf("member %+v: error %v", c, err)
}
}
store.Close()
if _, err := m.readBuilds(context.Background(), []sdkstep.FetchBuild{member}); err == nil || !strings.Contains(err.Error(), "@novox/lib") {
t.Errorf("a store that does not answer: %v", err)
}
}
@@ -102,6 +102,11 @@ func (m *Module) fetchTool() StepTool {
"package": map[string]any{"type": "object", "description": "optional: the group's tagged version of the SDK, {name, version}, placed from the registry in each TypeScript member whatever its lock names (ADR 0607 §3); or the group's package member, {repository, commit, ref} (ADR 0500 §5), packed here, publishing nothing, and served to each TypeScript member whose lock names its version, in place of the registry's",
"properties": map[string]any{"name": map[string]any{"type": "string"}, "version": map[string]any{"type": "string"},
"repository": map[string]any{"type": "string"}, "commit": map[string]any{"type": "string"}, "ref": map[string]any{"type": "string"}}},
"builds": map[string]any{"type": "array", "description": "optional: a group run's package members, each by its build's digest, read from the artifact store and laid as the package it names in each module's node_modules (design 47 G §465)",
"items": map[string]any{"type": "object", "properties": map[string]any{
"package": map[string]any{"type": "string"}, "module": map[string]any{"type": "string"},
"artifact": map[string]any{"type": "string"}, "digest": map[string]any{"type": "string"},
}, "required": []string{"package", "module", "artifact", "digest"}}},
"base": map[string]any{"type": "string", "description": "optional trunk commit, 40 hex digits: the head is merged onto it as the forge's merge would, and the answer carries the merge commit, the files in conflict (an answer, the workspace is then left at the trunk commit), the changed paths and the tree of each module directory"},
},
"required": []string{"repository", "commit"},
@@ -124,6 +129,10 @@ func (m *Module) fetch(ctx context.Context, args map[string]any) (res any, err e
if err != nil {
return nil, err
}
buildsAsked, err := buildstep.BuildsArg(args["builds"])
if err != nil {
return nil, err
}
m.CleanWorkspaces(m.now())
id := sdkstep.NewID()
done := m.spaces().Hold(id)
@@ -135,10 +144,10 @@ func (m *Module) fetch(ctx context.Context, args map[string]any) (res any, err e
}
}
}()
return m.fetchInto(ctx, req, group, id)
return m.fetchInto(ctx, req, group, id, buildsAsked)
}
func (m *Module) fetchInto(ctx context.Context, req sdkstep.FetchRequest, group *groupPackage, id string) (any, error) {
func (m *Module) fetchInto(ctx context.Context, req sdkstep.FetchRequest, group *groupPackage, id string, buildsAsked []sdkstep.FetchBuild) (any, error) {
repository, commit, ref, base := req.Repository, req.Commit, req.Ref, req.Base
var err error
root := filepath.Join(m.Work, id)
@@ -181,6 +190,11 @@ func (m *Module) fetchInto(ctx context.Context, req sdkstep.FetchRequest, group
}
served, res.Group = &p, p.Name+"@"+p.Version+" "+p.Integrity
}
// A group run's package members, each read from the artifact store by its build's digest (builds.go, §465).
members, err := m.readBuilds(ctx, buildsAsked)
if err != nil {
return nil, err
}
dirs, packages, err := m.typeScriptModules(ctx, tree)
if err != nil {
return nil, err
@@ -193,6 +207,9 @@ func (m *Module) fetchInto(ctx context.Context, req sdkstep.FetchRequest, group
if err := m.installDev(ctx, root, dir, d); err != nil {
return nil, err
}
if err := m.layBuilds(dir, members); err != nil {
return nil, err
}
res.Installed = append(res.Installed, d)
continue
}
@@ -201,8 +218,13 @@ func (m *Module) fetchInto(ctx context.Context, req sdkstep.FetchRequest, group
return nil, err
}
if refused == "" {
// The latest published SDK, or the group's (ADR 0573 §2, ADR 0607 §3).
if err := m.placeSDK(ctx, dir, d, served, group); err != nil {
// The latest published SDK, or the group's (ADR 0573 §2, ADR 0607 §3); the SDK as a member's build is laid from it.
if !namesPackage(members, sdkPackage) {
if err := m.placeSDK(ctx, dir, d, served, group); err != nil {
return nil, err
}
}
if err := m.layBuilds(dir, members); err != nil {
return nil, err
}
if res.SDK == nil {
@@ -65,6 +65,7 @@ func main() {
}
m.Floor = floorOf(m.Node)
m.Settings = filepath.Join(home, "settings.json")
m.ArtifactStoreFile = filepath.Join(home, "artifact-store.json")
// Outside a launch by the runtime — a build, a check — there is no node and no state to write.
if m.Node != "" {
go m.Start(ctx)
@@ -238,8 +238,9 @@ func TestTheModuleInvokesTheRegistrySeatsAndHoldsNoWriteCredential(t *testing.T)
}
}
}
if slices.Contains(m.Requires, "artifact-store") {
t.Errorf("requires %v still name artifact-store", m.Requires)
// It requires the provision to read a group run's package members' builds (design 47 G §465), never to write.
if !slices.Contains(m.Requires, "artifact-store") {
t.Errorf("requires %v lack artifact-store, which a group run's members are read from", m.Requires)
}
for _, r := range m.Resources {
if r["id"] == "registry" {
@@ -51,6 +51,8 @@ type Module struct {
// Detected and ReadDetected are the Put and the read of the state detected-steps (design 47 G §326, §414).
Detected buildstep.Put
ReadDetected func(key string) (json.RawMessage, error)
// ArtifactStoreFile is the binding of the provision artifact-store, read for a group run's package members (§465).
ArtifactStoreFile string
// ForgetDetected deletes one key of detected-steps; RecordAct writes an act in the hand-act log of the seat
// mesh-hand-acts (§463, §480): a drop writes it first.
ForgetDetected func(key string) error
+2
View File
@@ -66,12 +66,14 @@
],
"requires": [
{"name": "detected-steps", "storage": "key-value", "template": "state", "options": {"history": 1}},
"artifact-store",
"forge-account",
"npm-package-registry",
"node-toolchain",
"node-tools"
],
"binds": {
"artifact-store": "${dir:state}/artifact-store.json",
"npm-package-registry": "${dir:state}/package-registry.json"
},
"invokes": [