DLV-C3r3, C3r4: a group's check builds its members against the group's tagged versions

Design 51 *Tagged versions* and ADR 0607 §3, §5: once each package's published is heard, a Go member's ask names the
group's tagged version of each package another member publishes (Go's form, v0.1.15-feat-x.2), a package new in the
group included, and a TypeScript member's Node fetch takes {name, version} from the registry instead of a packed head.
The published is matched by seat, head and name, so one head with several Go packages (mesh-contracts) gets each its
own version. With no preview of the merge, a member's manifest is read at its head on the forge, so a package new in
the group is asked for on the first check too (the lead's ruling 4 on A2c's finding 4).

Needs mesh-sdk's PackageVersions.Check to take a Go semantic version (a2d-sdk-tagged-version.patch, for A1).

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
2026-11-04 13:23:10 +01:00
parent e20eb05a9b
commit c04fa5ff62
6 changed files with 306 additions and 57 deletions
@@ -159,7 +159,7 @@ func newJudge(d judgeDeps) check.Judge {
nodes = d.nodes
}
ex := &check.Examiner{Nodes: nodes, Compositions: d.compositions, Asks: d.asks, Toolchains: d.tools,
Kept: check.Kept{Store: d.kept}, Previews: d.previews, Modules: d.repos, Packages: d.repos, Published: d.published, Forge: d.forge, Runs: d.repos, Builds: d.builds, BuildEnds: d.ends, BuildWait: d.buildWait, Announce: d.announce, Trunks: d.trunks, Manifests: d.repos, Read: d.repos, Predefined: d.predefined, Tags: d.tags, Logf: d.logf}
Kept: check.Kept{Store: d.kept}, Previews: d.previews, Modules: d.repos, Packages: d.repos, Published: d.published, Forge: d.forge, Runs: d.repos, Builds: d.builds, BuildEnds: d.ends, BuildWait: d.buildWait, Announce: d.announce, Trunks: d.trunks, Manifests: d.repos, Read: d.repos, Dirs: d.repos, Predefined: d.predefined, Tags: d.tags, Logf: d.logf}
if d.nodes != nil && d.builds != nil && d.repos != nil {
ex.Serving = runningServes{have: d.nodes, builds: d.builds, read: d.repos, logf: d.logf}
}
@@ -290,6 +290,15 @@ func (r *repoModules) Put(ctx context.Context, key string, value []byte, deleted
r.mu.Unlock()
}
// ModuleDir is a module's directory in its repository as the indexer's modules hold it (check.Examiner.Dirs); false for a
// module the indexer holds no directory of.
func (r *repoModules) ModuleDir(module string) (string, bool) {
r.mu.Lock()
defer r.mu.Unlock()
d, ok := r.path[module]
return d, ok && d != ""
}
// PackagePaths are the paths, relative to the repository, that the TypeScript package artifacts of a repository's modules
// are built from, sorted: the named paths the indexer keeps in each module's value (delivery.Module.ArtifactPaths; only the
// indexer reads them from a manifest, the lead's decision L8), under the module's directory. None for a module whose value
@@ -52,8 +52,8 @@ func (t tagAsks) Want(ctx context.Context, w check.TagWanted) error {
return err
}
func (t tagAsks) Await(ctx context.Context, seat, head string) (check.TaggedPublished, bool) {
return t.heard.Await(ctx, seat, head)
func (t tagAsks) Await(ctx context.Context, seat, head string, names []string) (check.TaggedPublished, bool) {
return t.heard.Await(ctx, seat, head, names)
}
// heardPublished takes a message of the events consumer in when it is a package registry seat's published, and answers
@@ -113,6 +113,11 @@ type Examiner struct {
Tags TagAsks
// PublishBound is how long a group's check waits for each package's published; DefaultPublishBound when 0.
PublishBound time.Duration
// Dirs names a module's directory in its repository, from the indexer's modules, so a manifest is read at a member's
// head (tagwanted.go); nil, or a module it does not know, tries modules/<module> and the repository's root.
Dirs interface {
ModuleDir(module string) (string, bool)
}
// Packages names, of a repository, the paths (repository-relative) its TypeScript package artifacts are built from, from
// the indexer's modules (delivery.Module.ArtifactPaths, lead decision L8); nil serves no group package.
Packages interface {
@@ -250,6 +255,8 @@ type state struct {
// unpreviewed are the members (pullKey) with no preview of a merge: a head that does not merge, or a fetch that did not
// answer. The indexer holds no manifest of them, so the assigner's composition would only wait: it is not awaited.
unpreviewed []string
// tags are the group's tagged versions its members are built against (ADR 0607 §3), once the check heard them.
tags groupVersions
}
// Judge runs the case (it is the Judge of a Checker). A failure of the change, of a merge or of a step is a verdict; an error is only a case that cannot
@@ -415,8 +422,12 @@ func (st *state) judgeAll(ctx context.Context) *delivery.Verdict {
t.agreed = &delivery.ToolchainUsed{Version: tc.Version, Recipe: tc.Recipe}
}
args := map[string]any{"repository": p.Repository, "commit": p.Head, "ref": refOf(p.Number), "base": p.Trunk}
if bm == buildrun.NodeBuildModule && groupPackage != nil && p.Repository != SDKRepository {
args["package"] = groupPackage
if bm == buildrun.NodeBuildModule {
if np, ok := st.tags.nodePackageFor(p.Repository); ok {
args["package"] = map[string]any{"name": np.Name, "version": np.Version} // the group's tagged version (ADR 0607 §3)
} else if groupPackage != nil && p.Repository != SDKRepository {
args["package"] = groupPackage
}
}
if bm == buildrun.GoBuildModule {
beside, err := st.j.besideFor(ctx, st.c, p)
@@ -449,7 +460,7 @@ func (st *state) judgeAll(ctx context.Context) *delivery.Verdict {
st.unpreviewed = append(st.unpreviewed, pullKey(p))
}
st.judgedByMember(p, merged)
if p.Repository == SDKRepository && len(st.c.Pulls) > 1 && st.j.changesPackage(p, merged) {
if p.Repository == SDKRepository && len(st.c.Pulls) > 1 && len(st.tags.node) == 0 && st.j.changesPackage(p, merged) {
groupPackage = map[string]any{"repository": p.Repository, "commit": p.Head, "ref": refOf(p.Number)}
}
}
@@ -31,10 +31,12 @@ type Forge interface {
File(ctx context.Context, repository, path, ref string) (string, error)
}
// packagesOf is the version of each mesh package a Go check of repository builds against (ADR 0573 §2, §4): the latest
// published version of each package the mesh's modules use (Published), and for a package one of the case's pull
// requests publishes, that pull request's head (mesh-sdk's Go module whenever mesh-sdk is a member). A package of
// repository itself comes with the tree the check fetched, and is left out. Nil for none.
// packagesOf is the version of each mesh package a Go check of repository builds against (ADR 0573 §2, §4, as ADR 0607
// §5 changes §4): the latest published version of each package the mesh's modules use (Published); for a package a
// member of the case publishes, the group's tagged version of it once the check heard it (design 51 *Tagged versions*,
// also a package new in the group, which has no published version yet), else that member's head (mesh-sdk's Go module
// whenever mesh-sdk is a member). A package of repository itself comes with the tree the check fetched, and is left out.
// Nil for none.
func (st *state) packagesOf(repository string) delivery.PackageVersions {
versions := delivery.PackageVersions{}
if st.j.Published != nil {
@@ -45,13 +47,18 @@ func (st *state) packagesOf(repository string) delivery.PackageVersions {
versions[SDKModulePath] = ""
}
}
for pkg := range st.tags.goPackages {
versions[pkg] = ""
}
for pkg := range versions {
repo, _, ok := delivery.PackageRepository(pkg)
if !ok || delivery.SameRepository(repo, repository) {
delete(versions, pkg)
continue
}
if m := st.c.member(repo); m != nil && len(m.Head) == 40 {
if tagged := st.tags.goPackages[pkg]; tagged != "" {
versions[pkg] = tagged
} else if m := st.c.member(repo); m != nil && len(m.Head) == 40 {
versions[pkg] = m.Head
}
if versions[pkg] == "" {
@@ -2,6 +2,8 @@ package check
import (
"context"
"encoding/json"
"reflect"
"maps"
"slices"
"strings"
@@ -9,6 +11,7 @@ import (
"testing"
"time"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
"git.novox.be/novox/mesh-sdk/go/buildstep"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
@@ -68,23 +71,48 @@ func TestAGroupMemberIsBuiltAgainstTheSDKMembersHeadWhateverItsGoModNames(t *tes
// Design 51 *Tagged versions* (DLV-C3): a group's check says tag-wanted for each package a member declares and builds no
// member before its published; a lone pull request asks nothing; no published within the bound fails naming the package.
sdk := catalogue.Manifest{Module: "mesh-sdk", Build: &catalogue.Build{Artifacts: []catalogue.Artifact{
{Name: "go", Kind: catalogue.ArtifactPackage, Language: "go"}, {Name: "lib", Kind: catalogue.ArtifactPackage, Language: "typescript"}}}}
digest := strings.Repeat("c", 64)
{Name: "go", Kind: catalogue.ArtifactPackage, Language: "go", Paths: []string{"go"}}, {Name: "lib", Kind: catalogue.ArtifactPackage, Language: "typescript"}}}}
contracts := catalogue.Manifest{Module: "mesh-contracts", Build: &catalogue.Build{Artifacts: []catalogue.Artifact{
{Name: "go", Kind: catalogue.ArtifactPackage, Language: "go", Paths: []string{"go"}},
{Name: "go-gopackageregistry", Kind: catalogue.ArtifactPackage, Language: "go", Paths: []string{"go/seats/gopackageregistry"}}}}}
digest, contractsDigest := strings.Repeat("c", 64), strings.Repeat("d", 64)
sdkPull := func() delivery.CandidatePull {
p := pullOf(repoSDK, 12, head2)
p.Modules = []string{"mesh-sdk"}
p.Fps = map[string]delivery.CandidateModule{"mesh-sdk": {FP2: "src2:" + strings.Repeat("f", 64), Manifest: digest}}
return p
}
unpreviewed := func() delivery.CandidatePull { // a package new in the group: no preview carries its manifest yet
p := sdkPull()
p.Fps = map[string]delivery.CandidateModule{"mesh-sdk": {FP2: "src2:" + strings.Repeat("f", 64)}}
return p
}
contractsPull := func() delivery.CandidatePull {
p := pullOf("novox/mesh-catalog", 4, strings.Repeat("3", 40))
p.Modules = []string{"mesh-contracts"}
p.Fps = map[string]delivery.CandidateModule{"mesh-contracts": {FP2: "src2:" + strings.Repeat("e", 64), Manifest: contractsDigest}}
return p
}
const sdkGo, contractsGo, seatGo = SDKModulePath, "git.novox.be/novox/mesh-catalog/go", "git.novox.be/novox/mesh-catalog/go/seats/gopackageregistry"
for _, row := range []struct {
name string
pulls []delivery.CandidatePull
published []string // the seats that publish the SDK member's head
published []string // the seats that publish the members' heads
wants int
fails string
// built is the Go packages mesh-delivery's member is built against (ADR 0607 §3, §5), node the package its Node
// fetch takes from the registry; nil checks none.
built map[string]string
node map[string]any
}{
{name: "a group", pulls: []delivery.CandidatePull{pullOf(repoA, 7, head1), sdkPull()},
published: []string{"go-package-registry", "npm-package-registry"}, wants: 2},
published: []string{"go-package-registry", "npm-package-registry"}, wants: 2,
built: map[string]string{sdkGo: "v0.1.15-feat-x.1"}, node: map[string]any{"name": "lib", "version": "0.1.15-feat-x.1"}},
{name: "a package new in the group, read at its head", pulls: []delivery.CandidatePull{pullOf(repoA, 7, head1), unpreviewed()},
published: []string{"go-package-registry", "npm-package-registry"}, wants: 2, built: map[string]string{sdkGo: "v0.1.15-feat-x.1"}},
{name: "one head, two Go packages (as mesh-contracts' head)", pulls: []delivery.CandidatePull{pullOf(repoA, 7, head1), contractsPull()},
published: []string{"go-package-registry"}, wants: 2,
built: map[string]string{contractsGo: "v0.1.15-feat-x.1", seatGo: "v0.1.3-feat-x.1"}},
{name: "a lone pull request", pulls: []delivery.CandidatePull{sdkPull()}},
{name: "a package never published", pulls: []delivery.CandidatePull{pullOf(repoA, 7, head1), sdkPull()},
published: []string{"go-package-registry"}, wants: 2,
@@ -92,7 +120,10 @@ func TestAGroupMemberIsBuiltAgainstTheSDKMembersHeadWhateverItsGoModNames(t *tes
} {
b := newBench(t)
b.fetch.Modules = []string{"modules/m"}
b.ex.Read = fixedManifests{digest: sdk}
b.ex.Read = fixedManifests{digest: sdk, contractsDigest: contracts}
b.ex.Modules = langs{repoA: {buildrun.GoBuildModule, buildrun.NodeBuildModule}}
raw, _ := json.Marshal(sdk)
b.ex.Forge = headFiles{repoSDK + " module.json " + head2: string(raw)}
tags := &recordedTags{heard: &PublishedHeard{}, publish: row.published}
b.ex.Tags, b.ex.PublishBound = tags, 50*time.Millisecond
b.asks.Answer = func(a delivery.StepAsk) delivery.Step {
@@ -106,7 +137,7 @@ func TestAGroupMemberIsBuiltAgainstTheSDKMembersHeadWhateverItsGoModNames(t *tes
t.Errorf("%s: tag-wanted said %d times, want %d: %+v", row.name, len(tags.wanted), row.wants, tags.wanted)
}
for _, w := range tags.wanted {
if w.Repository != repoSDK || w.Branch != "feat/x" || w.Head != head2 || w.Module != "mesh-sdk" || w.Pull != 12 || w.Tag != "" {
if !slices.Contains([]string{repoSDK, "novox/mesh-catalog"}, w.Repository) || w.Branch != "feat/x" || w.Head == head1 || w.Module == "" || w.Pull == 7 || w.Tag != "" {
t.Errorf("%s: tag-wanted = %+v", row.name, w)
}
}
@@ -118,9 +149,32 @@ func TestAGroupMemberIsBuiltAgainstTheSDKMembersHeadWhateverItsGoModNames(t *tes
case row.fails != "" && len(b.steps.tool("fetch")) != 0:
t.Errorf("%s: a member was built though a package was not published", row.name)
}
if row.built != nil {
got := packagesOfFetch(t, b, repoA)
for pkg, version := range row.built {
if got[pkg] != version {
t.Errorf("%s: mesh-delivery is built against %s %q, want the group's tagged version %s (all: %v)", row.name, pkg, got[pkg], version, got)
}
}
}
if row.node != nil {
if got := nodeFetchOf(b, repoA).args["package"]; !reflect.DeepEqual(got, row.node) {
t.Errorf("%s: mesh-delivery's Node fetch takes %v, want the group's tagged version %v from the registry", row.name, got, row.node)
}
}
}
}
// headFiles are the forge's files by "<repository> <path> <ref>"; any other is not found.
type headFiles map[string]string
func (h headFiles) File(_ context.Context, repository, path, ref string) (string, error) {
if text, ok := h[repository+" "+path+" "+ref]; ok {
return text, nil
}
return "", ErrNotFound
}
// recordedTags are the checker's tag asks as a test hears them: each tag-wanted kept, and the seats in publish answering it
// at once with a tagged version, as the builder and a registry seat would.
type recordedTags struct {
@@ -135,13 +189,17 @@ func (r *recordedTags) Want(_ context.Context, w TagWanted) error {
r.wanted = append(r.wanted, w)
r.mu.Unlock()
for _, seat := range r.publish {
r.heard.Heard(seat, TaggedPublished{Name: w.Package, Kind: "package", Commit: w.Head, Tag: "feat-x", Version: "0.1.15-feat-x.1"})
version := "0.1.15-feat-x.1"
if w.Package == "go-gopackageregistry" {
version = "0.1.3-feat-x.1"
}
r.heard.Heard(seat, TaggedPublished{Name: w.Package, Kind: "package", Commit: w.Head, Tag: "feat-x", Version: version})
}
return nil
}
func (r *recordedTags) Await(ctx context.Context, seat, head string) (TaggedPublished, bool) {
return r.heard.Await(ctx, seat, head)
func (r *recordedTags) Await(ctx context.Context, seat, head string, names []string) (TaggedPublished, bool) {
return r.heard.Await(ctx, seat, head, names)
}
type runsAt map[string]string
@@ -2,7 +2,10 @@ package check
import (
"context"
"errors"
"fmt"
"maps"
"path"
"slices"
"strings"
"sync"
@@ -47,76 +50,165 @@ type TaggedPublished struct {
type TagAsks interface {
// Want says one tag-wanted on the checker's family asks.
Want(ctx context.Context, t TagWanted) error
// Await answers the tagged version a seat published from a head, once heard; false when ctx ends first.
Await(ctx context.Context, seat, head string) (TaggedPublished, bool)
// Await answers the tagged version a seat published from a head under one of names, once heard; false when ctx ends
// first. A head declaring several packages on one seat (mesh-contracts' Go modules) is told apart by the name.
Await(ctx context.Context, seat, head string, names []string) (TaggedPublished, bool)
}
// PublishedHeard is the registry seats' published of tagged versions as the checker heard them on its durable consumer
// of the shared events stream, by seat and source commit: a waiter wakes on the event, never on a timer.
// of the shared events stream, by seat and source commit, each name kept: a waiter wakes on the event, never on a timer.
type PublishedHeard struct {
mu sync.Mutex
heard map[string]TaggedPublished
waiters map[string][]chan TaggedPublished
heard map[string][]TaggedPublished
waiters map[string][]publishedWaiter
}
type publishedWaiter struct {
names []string
c chan TaggedPublished
}
func publishedKey(seat, head string) string { return seat + " " + head }
func (h *PublishedHeard) init() {
if h.heard == nil {
h.heard, h.waiters = map[string][]TaggedPublished{}, map[string][]publishedWaiter{}
}
}
// Heard takes one published of a seat in; one of no tagged version (no tag, or no commit) is left out and answers false.
// A later published of the same name and head replaces the earlier.
func (h *PublishedHeard) Heard(seat string, p TaggedPublished) bool {
if p.Tag == "" || p.Commit == "" {
return false
}
h.mu.Lock()
defer h.mu.Unlock()
if h.heard == nil {
h.heard, h.waiters = map[string]TaggedPublished{}, map[string][]chan TaggedPublished{}
}
h.init()
k := publishedKey(seat, p.Commit)
h.heard[k] = p
for _, w := range h.waiters[k] {
w <- p
}
delete(h.waiters, k)
h.heard[k] = append(slices.DeleteFunc(h.heard[k], func(o TaggedPublished) bool { return o.Name == p.Name }), p)
h.waiters[k] = slices.DeleteFunc(h.waiters[k], func(w publishedWaiter) bool {
if !slices.Contains(w.names, p.Name) {
return false
}
w.c <- p
return true
})
return true
}
// Await is TagAsks.Await over what was heard.
func (h *PublishedHeard) Await(ctx context.Context, seat, head string) (TaggedPublished, bool) {
func (h *PublishedHeard) Await(ctx context.Context, seat, head string, names []string) (TaggedPublished, bool) {
k := publishedKey(seat, head)
h.mu.Lock()
if h.heard == nil {
h.heard, h.waiters = map[string]TaggedPublished{}, map[string][]chan TaggedPublished{}
h.init()
for _, p := range h.heard[k] {
if slices.Contains(names, p.Name) {
h.mu.Unlock()
return p, true
}
}
if p, ok := h.heard[k]; ok {
h.mu.Unlock()
return p, true
}
w := make(chan TaggedPublished, 1)
w := publishedWaiter{names: names, c: make(chan TaggedPublished, 1)}
h.waiters[k] = append(h.waiters[k], w)
h.mu.Unlock()
select {
case p := <-w:
case p := <-w.c:
return p, true
case <-ctx.Done():
h.mu.Lock()
h.waiters[k] = slices.DeleteFunc(h.waiters[k], func(c chan TaggedPublished) bool { return c == w })
h.waiters[k] = slices.DeleteFunc(h.waiters[k], func(o publishedWaiter) bool { return o.c == w.c })
h.mu.Unlock()
select {
case p := <-w.c: // heard as the wait ended
return p, true
default:
}
return TaggedPublished{}, false
}
}
// declaredPackage is one package a group member declares: the module, the artifact's name and the seat it is published
// through.
// declaredPackage is one package a group member declares: the module, its directory in the repository (RootPath for its
// root, empty when not known), the artifact's name and paths, and the seat it is published through.
type declaredPackage struct {
pull Pull
module, name string
module, dir string
name string
paths []string
language, seat string
}
// publishedNames are the names a registry seat's published may carry for a declared package: the builder's
// `<module>/<artifact>` (its versions' name), the artifact's name, and a Go package's module path.
func (d declaredPackage) publishedNames() []string {
names := []string{d.module + "/" + d.name, d.name}
if d.language == "go" {
names = append(names, d.goModulePath(""))
}
return names
}
// goModulePath is the Go module path of a declared Go package (design 51 *Go packages*): the forge's module prefix, the
// repository's name, the module's directory and the artifact's first named path; a published name that is already a
// module path of the forge's is taken as it is.
func (d declaredPackage) goModulePath(published string) string {
if strings.HasPrefix(published, delivery.ForgeModulePrefix) {
return published
}
_, name, _ := strings.Cut(d.pull.Repository, "/")
parts := []string{strings.TrimSuffix(delivery.ForgeModulePrefix, "/") + "/" + name}
if dir := strings.Trim(d.dir, "/"); dir != "" && dir != delivery.RootPath {
parts = append(parts, dir)
}
if len(d.paths) > 0 {
if p := strings.Trim(strings.TrimPrefix(d.paths[0], "./"), "/"); p != "" && p != "." {
parts = append(parts, p)
}
}
return path.Join(parts...)
}
// manifestAtHead is a touched module's manifest read from the forge at its member's head (the lead's ruling 4 on A2c's
// finding 4): with no preview of the merge, a package new in the group is declared only there, never at the trunk. The
// module's directory is the indexer's when it knows the module, else modules/<module> or the repository's root, the one
// whose manifest names the module. Found is false when no such file names it.
func (st *state) manifestAtHead(ctx context.Context, p Pull, module string) (m catalogue.Manifest, dir string, found bool, err error) {
var dirs []string
if st.j.Dirs != nil {
if d, ok := st.j.Dirs.ModuleDir(module); ok {
dirs = []string{d}
}
}
if dirs == nil {
dirs = []string{"modules/" + module, delivery.RootPath}
}
for _, d := range dirs {
file := "module.json"
if t := strings.Trim(d, "/"); t != "" && t != delivery.RootPath {
file = t + "/module.json"
}
text, err := st.j.Forge.File(ctx, p.Repository, file, p.Head)
if errors.Is(err, ErrNotFound) {
continue
}
if err != nil {
return catalogue.Manifest{}, "", false, fmt.Errorf("the manifest of %s at the head of %s cannot be read%s: %w", module, pullKey(p), unheardWords(err), err)
}
m, err := catalogue.Decode([]byte(text))
if err != nil {
return catalogue.Manifest{}, "", false, fmt.Errorf("the manifest %s of %s at the head of %s is no manifest: %w", file, module, pullKey(p), err)
}
if m.Module == module {
return m, d, true, nil
}
}
return catalogue.Manifest{}, "", false, nil
}
// declaredPackages are the packages the case's members declare, read from each touched module's manifest at the merge
// preview (the candidate's), else at the trunk (the indexer's manifests): a member that declares none is not asked for.
// preview (the candidate's); with no preview, at the member's head on the forge, so a package new in the group is asked
// for on the first check too (trunk's manifest when no forge is read): a member that declares none is not asked for.
func (st *state) declaredPackages(ctx context.Context) ([]declaredPackage, error) {
if st.j.Read == nil {
if st.j.Read == nil && st.j.Forge == nil {
return nil, nil
}
var trunk map[string]string
@@ -126,16 +218,34 @@ func (st *state) declaredPackages(ctx context.Context) ([]declaredPackage, error
var out []declaredPackage
for _, p := range st.c.Pulls {
for _, module := range p.Modules {
var m catalogue.Manifest
var dir string
digest := p.Fps[module].Manifest
if digest == "" {
digest = trunk[module]
}
if digest == "" {
switch {
case digest != "" && st.j.Read != nil:
var err error
if m, err = st.j.Read.Manifest(ctx, digest); err != nil {
return nil, fmt.Errorf("the manifest of %s (%s) cannot be read, so the packages it declares are not known: %w", module, pullKey(p), err)
}
case st.j.Forge != nil:
var found bool
var err error
if m, dir, found, err = st.manifestAtHead(ctx, p, module); err != nil {
return nil, fmt.Errorf("%v, so the packages it declares are not known", err)
}
if !found {
continue
}
case trunk[module] != "" && st.j.Read != nil:
var err error
if m, err = st.j.Read.Manifest(ctx, trunk[module]); err != nil {
return nil, fmt.Errorf("the manifest of %s (%s) cannot be read, so the packages it declares are not known: %w", module, pullKey(p), err)
}
default:
continue
}
m, err := st.j.Read.Manifest(ctx, digest)
if err != nil {
return nil, fmt.Errorf("the manifest of %s (%s) cannot be read, so the packages it declares are not known: %w", module, pullKey(p), err)
if dir == "" && st.j.Dirs != nil {
dir, _ = st.j.Dirs.ModuleDir(module)
}
if m.Build == nil {
continue
@@ -148,13 +258,66 @@ func (st *state) declaredPackages(ctx context.Context) ([]declaredPackage, error
if !ok {
return nil, fmt.Errorf("the package %s of %s (%s) is in %q, which no registry seat publishes", a.Name, module, pullKey(p), a.Language)
}
out = append(out, declaredPackage{pull: p, module: module, name: a.Name, language: a.Language, seat: seat})
out = append(out, declaredPackage{pull: p, module: module, dir: dir, name: a.Name, paths: a.Paths, language: a.Language, seat: seat})
}
}
}
return out, nil
}
// groupVersions are a group's tagged versions as its check builds its members against them (ADR 0607 §3, §5): the Go
// packages by module path in Go's form (StepAsk.Packages), and the TypeScript packages a member's Node fetch takes from the
// registry, by the repository that publishes them.
type groupVersions struct {
goPackages delivery.PackageVersions
node map[string]NodePackage
}
// NodePackage is a TypeScript package a group's check hands the Node build module's fetch (its `package`): the name and the
// tagged version it takes from the registry, its integrity checked against the member's lock (ADR 0607 §3).
type NodePackage struct {
Name string `json:"name"`
Version string `json:"version"`
}
// nodePackageFor is the TypeScript package a member of repository takes as the group's tagged version: another member's
// (the Node build module places mesh-sdk's alone, so mesh-sdk's first); false when the group published none.
func (g *groupVersions) nodePackageFor(repository string) (NodePackage, bool) {
if np, ok := g.node[SDKRepository]; ok && repository != SDKRepository {
return np, true
}
for _, repo := range slices.Sorted(maps.Keys(g.node)) {
if repo != repository {
return g.node[repo], true
}
}
return NodePackage{}, false
}
// goVersion is a published version in Go's form: a leading v.
func goVersion(v string) string {
if strings.HasPrefix(v, "v") {
return v
}
return "v" + v
}
// took keeps a declared package's tagged version as the group's.
func (g *groupVersions) took(d declaredPackage, p TaggedPublished) {
switch d.language {
case "go":
if g.goPackages == nil {
g.goPackages = delivery.PackageVersions{}
}
g.goPackages[d.goModulePath(p.Name)] = goVersion(p.Version)
case "typescript":
if g.node == nil {
g.node = map[string]NodePackage{}
}
g.node[d.pull.Repository] = NodePackage{Name: p.Name, Version: p.Version}
}
}
// tagged asks a group's tagged versions before any member is built (design 51 *Tagged versions*): one tag-wanted per
// package a member declares, then each package's published awaited within the publish bound. A lone pull request and a
// try ask nothing. A package not published within the bound fails the check naming it.
@@ -189,11 +352,12 @@ func (st *state) tagged(ctx context.Context) *delivery.Verdict {
defer cancel()
var late []string
for _, d := range decl {
p, ok := st.j.Tags.Await(at, d.seat, d.pull.Head)
p, ok := st.j.Tags.Await(at, d.seat, d.pull.Head, d.publishedNames())
if !ok {
late = append(late, fmt.Sprintf("%s of %s (%s) on %s", d.name, d.module, pullKey(d.pull), d.seat))
continue
}
st.tags.took(d, p)
st.c.note(fmt.Sprintf("%s of %s is built as %s %s (tag %s)", d.name, d.module, p.Name, p.Version, p.Tag))
}
if len(late) > 0 {