Give a group's TypeScript members the mesh-sdk member's package when its merge changes the package's paths, and pin the SDK's R4e head (ADR 0500 §4-§5, issue 1309's checker half)
A TypeScript member of a group that changes mesh-sdk's package resolved the old version from the registry. Its Node fetch now names the mesh-sdk member's head, which the Node build module packs and serves, publishing nothing. The paths come from the indexer's modules value (lead decision L8), so a change of Go files or of the manifest alone resolves the registry as before. A test holds that no step of a check carries publish.
This commit is contained in:
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
@@ -31,7 +32,7 @@ type judgeDeps struct {
|
||||
|
||||
func newJudge(d judgeDeps) check.Judge {
|
||||
return &check.Examiner{Steps: d.runner, Toolchains: d.tools, Presence: d.presence, Health: d.have, Places: d.places,
|
||||
Kept: check.Kept{Store: d.kept}, Previews: d.previews, Modules: d.repos, Forge: d.forge, Runs: d.repos, Builds: d.builds, Announce: d.announce, Trunks: d.trunks, Logf: d.logf}
|
||||
Kept: check.Kept{Store: d.kept}, Previews: d.previews, Modules: d.repos, Packages: d.repos, Forge: d.forge, Runs: d.repos, Builds: d.builds, Announce: d.announce, Trunks: d.trunks, Logf: d.logf}
|
||||
}
|
||||
|
||||
// manifestKV reads a manifest by its digest.
|
||||
@@ -47,10 +48,13 @@ type repoModules struct {
|
||||
logf func(format string, args ...any)
|
||||
|
||||
mu sync.Mutex
|
||||
langs map[string]map[string]bool // by manifest digest: the languages of its artifacts
|
||||
repoOf map[string]string // by module: its repository
|
||||
commit map[string]string // by module: the commit its record was computed at (the commit the mesh runs)
|
||||
typed map[string]string // by module: its manifest digest
|
||||
langs map[string]map[string]bool // by manifest digest: the languages of its artifacts
|
||||
tsPkgs map[string][]string // by manifest digest: the names of its TypeScript package artifacts
|
||||
path map[string]string // by module: its directory in its repository
|
||||
named map[string]map[string][]string // by module: each artifact's named paths, as the indexer keeps them
|
||||
repoOf map[string]string // by module: its repository
|
||||
commit map[string]string // by module: the commit its record was computed at (the commit the mesh runs)
|
||||
typed map[string]string // by module: its manifest digest
|
||||
}
|
||||
|
||||
// Put takes a change of the indexer's modules bucket in: a module's repository and the languages its manifest names.
|
||||
@@ -58,11 +62,14 @@ func (r *repoModules) Put(ctx context.Context, key string, value []byte, deleted
|
||||
r.mu.Lock()
|
||||
if r.repoOf == nil {
|
||||
r.repoOf, r.typed, r.langs, r.commit = map[string]string{}, map[string]string{}, map[string]map[string]bool{}, map[string]string{}
|
||||
r.tsPkgs, r.path, r.named = map[string][]string{}, map[string]string{}, map[string]map[string][]string{}
|
||||
}
|
||||
if deleted {
|
||||
delete(r.repoOf, key)
|
||||
delete(r.typed, key)
|
||||
delete(r.commit, key)
|
||||
delete(r.path, key)
|
||||
delete(r.named, key)
|
||||
r.mu.Unlock()
|
||||
return
|
||||
}
|
||||
@@ -75,12 +82,15 @@ func (r *repoModules) Put(ctx context.Context, key string, value []byte, deleted
|
||||
r.repoOf[key] = m.Repository
|
||||
r.typed[key] = m.Manifest
|
||||
r.commit[key] = m.Commit
|
||||
r.path[key] = m.Path
|
||||
r.named[key] = m.ArtifactPaths
|
||||
_, known := r.langs[m.Manifest]
|
||||
r.mu.Unlock()
|
||||
if m.Manifest == "" || known {
|
||||
return
|
||||
}
|
||||
set := map[string]bool{}
|
||||
var pkgs []string
|
||||
if mk, err := delivery.ManifestsKey(m.Manifest); err == nil {
|
||||
if raw, _, found, err := r.manifests.Get(ctx, mk); err == nil && found {
|
||||
if kept, err := delivery.DecodeManifest(raw); err == nil {
|
||||
@@ -89,6 +99,9 @@ func (r *repoModules) Put(ctx context.Context, key string, value []byte, deleted
|
||||
if a.Language != "" {
|
||||
set[a.Language] = true
|
||||
}
|
||||
if a.Kind == "package" && a.Language == "typescript" {
|
||||
pkgs = append(pkgs, a.Name)
|
||||
}
|
||||
}
|
||||
} else if err != nil && r.logf != nil {
|
||||
r.logf("the manifest %s cannot be read, so its module counts as no TypeScript: %v", m.Manifest, err)
|
||||
@@ -98,9 +111,37 @@ func (r *repoModules) Put(ctx context.Context, key string, value []byte, deleted
|
||||
}
|
||||
r.mu.Lock()
|
||||
r.langs[m.Manifest] = set
|
||||
r.tsPkgs[m.Manifest] = pkgs
|
||||
r.mu.Unlock()
|
||||
}
|
||||
|
||||
// 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
|
||||
// holds no paths.
|
||||
func (r *repoModules) PackagePaths(repository string) []string {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
var out []string
|
||||
for module, repo := range r.repoOf {
|
||||
if repo != repository {
|
||||
continue
|
||||
}
|
||||
dir := strings.Trim(r.path[module], "/")
|
||||
for _, name := range r.tsPkgs[r.typed[module]] {
|
||||
for _, p := range r.named[module][name] {
|
||||
p = strings.Trim(strings.TrimPrefix(p, "./"), "/")
|
||||
if dir != "" && dir != delivery.RootPath {
|
||||
p = dir + "/" + p
|
||||
}
|
||||
out = append(out, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// BuildModules are the build modules that judge a repository, sorted.
|
||||
func (r *repoModules) BuildModules(repository string) []string {
|
||||
out := []string{buildrun.GoBuildModule}
|
||||
|
||||
@@ -73,3 +73,40 @@ func TestARepositoryWithATypeScriptModuleIsJudgedByTheNodeBuildModuleToo(t *test
|
||||
t.Errorf("a repository with no module: %v, want the Go build module", got)
|
||||
}
|
||||
}
|
||||
|
||||
// sdkManifest is mesh-sdk's own manifest (its module.json at feat/r4e-sdk): one TypeScript package naming its paths.
|
||||
const sdkManifest = `{"module":"mesh-sdk","version":"1","slug":"sdk","build":{"artifacts":[{"name":"lib","kind":"package","language":"typescript",` +
|
||||
`"paths":["src","test","package.json","package-lock.json","tsconfig.json"]}]},"resources":[]}`
|
||||
|
||||
func TestTheTypeScriptPackagesPathsComeFromTheIndexersModulesNotTheManifest(t *testing.T) { // ADR 0500 §5 with lead decision L8
|
||||
store := memManifests{}
|
||||
data, err := delivery.EncodeManifest(delivery.Manifest{Module: "mesh-sdk", JSON: sdkManifest})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
store[delivery.ManifestDigest([]byte(sdkManifest))] = data
|
||||
r := &repoModules{manifests: store}
|
||||
put := func(path string, paths map[string][]string) {
|
||||
v, err := delivery.EncodeModule(delivery.Module{Repository: "novox/mesh-sdk", Commit: strings.Repeat("a", 40), FP: "src1:" + strings.Repeat("a", 64),
|
||||
Manifest: delivery.ManifestDigest([]byte(sdkManifest)), Path: path, ArtifactPaths: paths})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r.Put(context.Background(), "mesh-sdk", v, false)
|
||||
}
|
||||
put(".", map[string][]string{"lib": {"src", "package.json"}}) // the indexer's word, not the manifest's five paths
|
||||
if got := strings.Join(r.PackagePaths("novox/mesh-sdk"), ","); got != "package.json,src" {
|
||||
t.Errorf("paths = %q, want the indexer's, at the repository's root", got)
|
||||
}
|
||||
put("ts", map[string][]string{"lib": {"src"}})
|
||||
if got := strings.Join(r.PackagePaths("novox/mesh-sdk"), ","); got != "ts/src" {
|
||||
t.Errorf("paths = %q, want them under the module's directory", got)
|
||||
}
|
||||
put(".", nil) // a value written before the indexer kept paths: nothing to serve by
|
||||
if got := r.PackagePaths("novox/mesh-sdk"); len(got) != 0 {
|
||||
t.Errorf("paths = %v, want none", got)
|
||||
}
|
||||
if got := r.PackagePaths("novox/mesh-delivery"); len(got) != 0 {
|
||||
t.Errorf("another repository: %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ module mesh-delivery-checker
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021192148-78364e318bdc
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261022160024-9cb4d59093a7
|
||||
github.com/nats-io/nats-server/v2 v2.11.17
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021192148-78364e318bdc h1:ZJScJSnF6LgFl55/VJ9RT/lNroVWun469sXtx0nlVYw=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021192148-78364e318bdc/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261022160024-9cb4d59093a7 h1:wK9O1CgNk7mo3IdJqW/CCx/hebg+1F0rkvW8CvJfnRM=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261022160024-9cb4d59093a7/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -649,3 +650,76 @@ func TestADigestThatEchoesOtherSiblingsKeepsNothing(t *testing.T) { // 1287 with
|
||||
t.Errorf("kept %v under a digest that does not cover every sibling", keys)
|
||||
}
|
||||
}
|
||||
|
||||
type pkgPaths map[string][]string
|
||||
|
||||
func (p pkgPaths) PackagePaths(repository string) []string { return p[repository] }
|
||||
|
||||
// groupPackageBench is a group of a TypeScript member (mesh-delivery) and the mesh-sdk member, whose merge changes sdkChanged.
|
||||
func groupPackageBench(t *testing.T, sdkChanged []string) *bench {
|
||||
b := newBench(t)
|
||||
b.ex.Modules = langs{repoA: {buildrun.GoBuildModule, buildrun.NodeBuildModule}, repoSDK: {buildrun.GoBuildModule, buildrun.NodeBuildModule}}
|
||||
b.ex.Packages = pkgPaths{repoSDK: {"src", "test", "package.json", "package-lock.json", "tsconfig.json"}}
|
||||
fetch := func(c stepCall) (any, error) {
|
||||
f := b.fetch
|
||||
if c.args["repository"] == repoSDK {
|
||||
f.Changed = sdkChanged
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
b.steps.handlers["build_go_fetch"], b.steps.handlers["build_node_fetch"] = fetch, fetch
|
||||
return b
|
||||
}
|
||||
|
||||
func nodeFetchOf(b *bench, repo string) stepCall {
|
||||
for _, c := range b.steps.tool("fetch") {
|
||||
if c.module == buildrun.NodeBuildModule && c.args["repository"] == repo {
|
||||
return c
|
||||
}
|
||||
}
|
||||
return stepCall{}
|
||||
}
|
||||
|
||||
func TestATypeScriptMemberTakesTheGroupsPackageWhenTheSDKMemberChangesItsPaths(t *testing.T) { // ADR 0500 §5, C11b
|
||||
b := groupPackageBench(t, []string{"go/delivery/x.go", "src/state.ts"})
|
||||
b.judge(t, caseOf(t, pullOf(repoA, 7, head1), pullOf(repoSDK, 12, head2)))
|
||||
want := map[string]any{"repository": repoSDK, "commit": head2, "ref": "refs/pull/12/head"}
|
||||
if got := nodeFetchOf(b, repoA).args["package"]; !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("mesh-delivery's Node fetch was given the package %v, want %v", got, want)
|
||||
}
|
||||
if got := nodeFetchOf(b, repoSDK).args["package"]; got != nil {
|
||||
t.Errorf("the mesh-sdk member's own fetch was given %v: it builds its package itself", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestATypeScriptMemberResolvesTheRegistryWhenTheSDKMemberLeavesItsPaths(t *testing.T) { // ADR 0500 §5: the manifest alone, or Go alone
|
||||
for _, changed := range [][]string{{"go/delivery/x.go"}, {"module.json"}, {"srcx/a.ts"}} {
|
||||
b := groupPackageBench(t, changed)
|
||||
b.judge(t, caseOf(t, pullOf(repoA, 7, head1), pullOf(repoSDK, 12, head2)))
|
||||
if got := nodeFetchOf(b, repoA).args["package"]; got != nil {
|
||||
t.Errorf("%v: the Node fetch was given %v, want the registry", changed, got)
|
||||
}
|
||||
}
|
||||
// A lone TypeScript pull request: no group package either.
|
||||
b := groupPackageBench(t, []string{"src/state.ts"})
|
||||
b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
|
||||
if got := nodeFetchOf(b, repoA).args["package"]; got != nil {
|
||||
t.Errorf("a lone pull request's Node fetch was given %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoStepOfACheckCarriesPublish(t *testing.T) { // ADR 0500 §4, 1309 (the checker's half): only the builder's trunk build publishes
|
||||
b := groupPackageBench(t, []string{"src/state.ts"})
|
||||
b.judge(t, caseOf(t, pullOf(repoA, 7, head1), pullOf(repoSDK, 12, head2)))
|
||||
if len(b.steps.calls) == 0 {
|
||||
t.Fatal("no step ran")
|
||||
}
|
||||
for _, c := range b.steps.calls {
|
||||
if _, ok := c.args["publish"]; ok {
|
||||
t.Errorf("%s was given publish: %v", c.tool, c.args)
|
||||
}
|
||||
if strings.HasSuffix(c.tool, "_build") || strings.HasSuffix(c.tool, "_pack") {
|
||||
t.Errorf("the checker asked %s: a check builds no artifact of its own", c.tool)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +69,9 @@ type Examiner struct {
|
||||
Builds BuildStates
|
||||
// Announce says a preview was written under the key (EventPreviewWritten); nil says nothing.
|
||||
Announce Announcer
|
||||
// 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{ PackagePaths(repository string) []string }
|
||||
// Trunks are the trunk heads of every repository, for the siblings a repository's tests read.
|
||||
Trunks interface{ Heads() map[string]string }
|
||||
Now func() time.Time
|
||||
@@ -269,7 +272,20 @@ func (st *state) choose(bm string) (node, toolchain string, err error) {
|
||||
func (st *state) fetchAll(ctx context.Context) *delivery.Verdict {
|
||||
began := st.j.now()
|
||||
defer func() { st.clone = st.j.now().Sub(began) }()
|
||||
for _, p := range st.c.Pulls {
|
||||
// The mesh-sdk member first: whether its merge changes the TypeScript package's paths decides whether the other members'
|
||||
// Node fetches take the group's package (ADR 0500 §5).
|
||||
pulls := slices.Clone(st.c.Pulls)
|
||||
slices.SortStableFunc(pulls, func(a, b Pull) int {
|
||||
if (a.Repository == SDKRepository) != (b.Repository == SDKRepository) {
|
||||
if a.Repository == SDKRepository {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
var groupPackage map[string]any
|
||||
for _, p := range pulls {
|
||||
var merged []Fetched
|
||||
for _, bm := range st.j.Modules.BuildModules(p.Repository) {
|
||||
node, tc, err := st.choose(bm)
|
||||
@@ -280,6 +296,9 @@ func (st *state) fetchAll(ctx context.Context) *delivery.Verdict {
|
||||
st.nodes[node] = true
|
||||
args := map[string]any{"repository": p.Repository, "commit": p.Head, "ref": refOf(p.Number), "base": p.Trunk}
|
||||
t.fetchArgs = args
|
||||
if bm == buildrun.NodeBuildModule && groupPackage != nil && p.Repository != SDKRepository {
|
||||
args["package"] = groupPackage
|
||||
}
|
||||
if bm == buildrun.GoBuildModule {
|
||||
beside, err := st.j.besideFor(ctx, st.c, p)
|
||||
if err != nil {
|
||||
@@ -311,10 +330,33 @@ func (st *state) fetchAll(ctx context.Context) *delivery.Verdict {
|
||||
if st.c.Try == nil { // a try owes the indexer no preview: no pull request holds its head
|
||||
st.j.writePreview(ctx, p, merged)
|
||||
}
|
||||
if p.Repository == SDKRepository && len(st.c.Pulls) > 1 && st.j.changesPackage(p, merged) {
|
||||
groupPackage = map[string]any{"repository": p.Repository, "commit": p.Head, "ref": refOf(p.Number)}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// changesPackage says whether the merge of a pull request changes a file inside the named paths of one of its repository's
|
||||
// TypeScript packages: a change of the manifest alone, or of files outside those paths, does not (ADR 0500 §5).
|
||||
func (j *Examiner) changesPackage(p Pull, merged []Fetched) bool {
|
||||
if j.Packages == nil {
|
||||
return false
|
||||
}
|
||||
paths := j.Packages.PackagePaths(p.Repository)
|
||||
for _, f := range merged {
|
||||
for _, changed := range f.Changed {
|
||||
for _, named := range paths {
|
||||
named = strings.TrimSuffix(strings.TrimPrefix(named, "./"), "/")
|
||||
if changed == named || strings.HasPrefix(changed, named+"/") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isTestFile says whether a repository-relative path is a test file of the languages the checker judges: Go's _test.go and
|
||||
// the TypeScript module's .test. and .spec. files and test directories.
|
||||
func isTestFile(p string) bool {
|
||||
|
||||
Reference in New Issue
Block a user