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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user