Keep nothing for a workspace given siblings, place the siblings where the tests state them, decide the hold from the listed plan keys (review D1-D3)
mesh/repo-check head b519c8dde2da merged into trunk a5aa69a4e815 beside mesh-catalog#423@12ff1cfb +1 more — pass: its merge-check.sh passed
mesh/merge-gate head b519c8dde2da merged into trunk a5aa69a4e815 — pass: group feat/the-checker composed: every machine composes with the change as it did …
mesh/delivery delivered
mesh/delivery-group group feat/the-checker delivered: every member is delivered

This commit is contained in:
2026-10-21 21:52:41 +02:00
parent 7214bf4368
commit b519c8dde2
17 changed files with 322 additions and 107 deletions
@@ -13,6 +13,7 @@ import (
const ( const (
getFileTool = "gitea.gitea_get_file" getFileTool = "gitea.gitea_get_file"
compareTool = "gitea.gitea_compare" compareTool = "gitea.gitea_compare"
commitsTool = "gitea.gitea_commits"
) )
// forgeReads reads the forge through call (mesh-sdk process.Conn.Call as the checker's own account). // forgeReads reads the forge through call (mesh-sdk process.Conn.Call as the checker's own account).
@@ -59,3 +60,21 @@ func (f forgeReads) OnTrunk(_ context.Context, repository, commit string) (bool,
} }
return *a.Total == 0, nil return *a.Total == 0, nil
} }
// FullSHA is the whole sha of a commit named by its first digits: the newest commit of the history at that ref.
func (f forgeReads) FullSHA(_ context.Context, repository, commit string) (string, error) {
owner, repo := ownerRepo(repository)
raw, err := f.call(commitsTool, map[string]any{"owner": owner, "repo": repo, "ref": commit, "limit": 1})
if err != nil {
return "", err
}
var a struct {
Commits []struct {
SHA string `json:"sha"`
} `json:"commits"`
}
if err := json.Unmarshal(raw, &a); err != nil || len(a.Commits) == 0 || len(a.Commits[0].SHA) != 40 || !strings.HasPrefix(a.Commits[0].SHA, commit) {
return "", fmt.Errorf("the answer of %s for %s is not a commit that starts with it: %v", commitsTool, commit, err)
}
return a.Commits[0].SHA, nil
}
@@ -53,3 +53,18 @@ func TestTheForgeReadsParseWhatTheForgeAnswered(t *testing.T) { // 1624/AC4
t.Error("an answer with no total was taken") t.Error("an answer with no total was taken")
} }
} }
func TestTheForgeNamesTheWholeShaOfAShortCommit(t *testing.T) { // review D2
f := forgeReads{call: func(key string, body any) (json.RawMessage, error) {
if key != commitsTool {
t.Errorf("called %s", key)
}
return capturedAnswer(t, "forge-commits"), nil
}}
if sha, err := f.FullSHA(context.Background(), "novox/mesh-sdk", "cb54756c3865"); err != nil || sha != "cb54756c3865df5ac9c0dc0481ad9ebe2bf013a1" {
t.Errorf("sha = %q, %v", sha, err)
}
if _, err := f.FullSHA(context.Background(), "novox/mesh-sdk", "deadbeefdead"); err == nil {
t.Error("a commit that does not start with the pin was taken")
}
}
@@ -11,15 +11,18 @@ import (
"mesh-delivery-checker/internal/check" "mesh-delivery-checker/internal/check"
) )
// holdWatch holds the checker back while a delivery builds (ADR 0484 clause 12): it watches the controller's bucket of the // holdWatch holds the checker back while a delivery builds (ADR 0484 clause 12): whether one does is the list of the keys of
// delivery plans in their build phase and reads it as empty where it is absent (issue 1652 makes it) or cannot be read, // the controller's bucket of the delivery plans in their build phase, read each time a check is about to start. Its keys age
// saying so once. The bucket may come into being after the checker started and the checker holds no timer (ADR 0437 §8), so // out (a time to live, and no delete marker), which a watch never hears, so a watch cannot decide it (mesh-controller
// each time a check is about to start and the bucket is not watched yet, the checker looks for it. // internal/link/holds.go lists the keys too); the watch is only the wake-up, for a plan that ended by a delete. The bucket
// is read as empty where it is absent (issue 1652 makes it) or cannot be read, saying so once. It may come into being after
// the checker started and the checker holds no timer (ADR 0437 §8), so each time a check is about to start and the bucket is
// not known yet, the checker looks for it.
type holdWatch struct { type holdWatch struct {
ctx context.Context ctx context.Context
js bucketOpener js bucketOpener
hold check.PlanHold keys func(ctx context.Context) ([]string, error)
changed func(ctx context.Context) // the last plan ended: the checks that waited may start changed func(ctx context.Context) // a plan key changed: the checks that waited may start
logger *log.Logger logger *log.Logger
errs chan error errs chan error
mu sync.Mutex mu sync.Mutex
@@ -34,23 +37,37 @@ func newHoldWatch(ctx context.Context, js bucketOpener, changed func(context.Con
// Held says whether a delivery builds now. // Held says whether a delivery builds now.
func (w *holdWatch) Held() bool { func (w *holdWatch) Held() bool {
w.ensure() w.ensure()
return w.hold.Held() w.mu.Lock()
keys := w.keys
w.mu.Unlock()
if keys == nil {
return false
}
list, err := keys(w.ctx)
if err != nil {
w.say("the keys of %s cannot be listed, so it is read as empty: %v", check.PlansBuildingBucket, err)
return false
}
return len(list) > 0
} }
func (w *holdWatch) say(format string, args ...any) { func (w *holdWatch) say(format string, args ...any) {
w.mu.Lock()
defer w.mu.Unlock()
if !w.said { if !w.said {
w.said = true w.said = true
w.logger.Printf(format, args...) w.logger.Printf(format, args...)
} }
} }
// ensure starts the watch if the bucket exists and is not watched. // ensure finds the bucket if it is not known and exists, and starts the wake-up watch.
func (w *holdWatch) ensure() { func (w *holdWatch) ensure() {
w.mu.Lock() w.mu.Lock()
defer w.mu.Unlock() if w.watched || w.js == nil {
if w.watched { w.mu.Unlock()
return return
} }
w.mu.Unlock()
kv, err := w.js.KeyValue(w.ctx, check.PlansBuildingBucket) kv, err := w.js.KeyValue(w.ctx, check.PlansBuildingBucket)
switch { switch {
case errors.Is(err, jetstream.ErrBucketNotFound): case errors.Is(err, jetstream.ErrBucketNotFound):
@@ -60,17 +77,25 @@ func (w *holdWatch) ensure() {
w.say("the bucket %s cannot be read, so it is read as empty: %v", check.PlansBuildingBucket, err) w.say("the bucket %s cannot be read, so it is read as empty: %v", check.PlansBuildingBucket, err)
return return
} }
watcher, err := settle(w.ctx, kv, func(e jetstream.KeyValueEntry) { w.hold.Put(e.Key(), !opOf(e)) }) w.mu.Lock()
if err != nil { defer w.mu.Unlock()
w.say("the bucket %s cannot be watched, so it is read as empty: %v", check.PlansBuildingBucket, err) if w.watched {
return return
} }
w.watched, w.said = true, false w.watched, w.said = true, false
w.keys = func(ctx context.Context) ([]string, error) {
list, err := kv.Keys(ctx)
if errors.Is(err, jetstream.ErrNoKeysFound) {
return nil, nil
}
return list, err
}
go func() { go func() {
w.errs <- follow(w.ctx, watcher, func(e jetstream.KeyValueEntry) { wake, err := kv.WatchAll(w.ctx)
if w.hold.Put(e.Key(), !opOf(e)) && !w.hold.Held() { if err != nil {
w.changed(w.ctx) w.errs <- err
} return
}) }
w.errs <- follow(w.ctx, wake, func(jetstream.KeyValueEntry) { w.changed(w.ctx) })
}() }()
} }
@@ -0,0 +1,35 @@
package main
import (
"context"
"errors"
"io"
"log"
"testing"
)
// The controller's plans-building keys age out of the bucket (a time to live, no delete marker), which a watch never hears:
// whether a delivery builds is the list of the keys now, and the watch is only a wake-up (review D3).
func TestADeliveryHoldsWhileItsKeyIsListedAndNotWhenItAgedOutWithNoEvent(t *testing.T) {
listed := []string{"plan-1"}
w := &holdWatch{watched: true, logger: log.New(io.Discard, "", 0), keys: func(context.Context) ([]string, error) { return listed, nil }}
if !w.Held() {
t.Fatal("a plan key is listed and nothing holds")
}
listed = nil // the key aged out: no delete reached the watch
if w.Held() {
t.Error("the key is gone from the bucket and the checker is still held")
}
}
func TestABucketThatCannotBeListedIsReadAsEmpty(t *testing.T) {
w := &holdWatch{watched: true, logger: log.New(io.Discard, "", 0), keys: func(context.Context) ([]string, error) { return []string{"plan-1"}, errors.New("the bus is down") }}
if w.Held() {
t.Error("an unreadable bucket held the checker")
}
var absent holdWatch
absent.logger = log.New(io.Discard, "", 0)
if absent.Held() {
t.Error("a watch with no bucket held the checker")
}
}
@@ -31,7 +31,7 @@ type judgeDeps struct {
func newJudge(d judgeDeps) check.Judge { func newJudge(d judgeDeps) check.Judge {
return &check.Examiner{Steps: d.runner, Toolchains: d.tools, Presence: d.presence, Health: d.have, Places: d.places, 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, Builds: d.builds, Announce: d.announce, Trunks: d.trunks, Logf: d.logf} 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}
} }
// manifestKV reads a manifest by its digest. // manifestKV reads a manifest by its digest.
@@ -49,6 +49,7 @@ type repoModules struct {
mu sync.Mutex mu sync.Mutex
langs map[string]map[string]bool // by manifest digest: the languages of its artifacts langs map[string]map[string]bool // by manifest digest: the languages of its artifacts
repoOf map[string]string // by module: its repository 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 typed map[string]string // by module: its manifest digest
} }
@@ -56,11 +57,12 @@ type repoModules struct {
func (r *repoModules) Put(ctx context.Context, key string, value []byte, deleted bool) { func (r *repoModules) Put(ctx context.Context, key string, value []byte, deleted bool) {
r.mu.Lock() r.mu.Lock()
if r.repoOf == nil { if r.repoOf == nil {
r.repoOf, r.typed, r.langs = map[string]string{}, map[string]string{}, map[string]map[string]bool{} r.repoOf, r.typed, r.langs, r.commit = map[string]string{}, map[string]string{}, map[string]map[string]bool{}, map[string]string{}
} }
if deleted { if deleted {
delete(r.repoOf, key) delete(r.repoOf, key)
delete(r.typed, key) delete(r.typed, key)
delete(r.commit, key)
r.mu.Unlock() r.mu.Unlock()
return return
} }
@@ -72,6 +74,7 @@ func (r *repoModules) Put(ctx context.Context, key string, value []byte, deleted
r.mu.Lock() r.mu.Lock()
r.repoOf[key] = m.Repository r.repoOf[key] = m.Repository
r.typed[key] = m.Manifest r.typed[key] = m.Manifest
r.commit[key] = m.Commit
_, known := r.langs[m.Manifest] _, known := r.langs[m.Manifest]
r.mu.Unlock() r.mu.Unlock()
if m.Manifest == "" || known { if m.Manifest == "" || known {
@@ -128,3 +131,10 @@ func (r buildReader) Build(ctx context.Context, module, fp2 string) (delivery.Bu
b, err := delivery.DecodeBuilderBuild(value) b, err := delivery.DecodeBuilderBuild(value)
return b, err == nil, err return b, err == nil, err
} }
// CommitOf is the commit the indexer's record of the module was computed at: the commit of its repository the mesh runs.
func (r *repoModules) CommitOf(module string) string {
r.mu.Lock()
defer r.mu.Unlock()
return r.commit[module]
}
@@ -66,6 +66,9 @@ func TestARepositoryWithATypeScriptModuleIsJudgedByTheNodeBuildModuleToo(t *test
if got := r.BuildModules("novox/mesh-catalog"); strings.Join(got, ",") != buildrun.GoBuildModule+","+buildrun.NodeBuildModule { if got := r.BuildModules("novox/mesh-catalog"); strings.Join(got, ",") != buildrun.GoBuildModule+","+buildrun.NodeBuildModule {
t.Errorf("mesh-catalog: %v, want both", got) t.Errorf("mesh-catalog: %v, want both", got)
} }
if got := r.CommitOf("mesh-delivery-checker"); got != strings.Repeat("a", 40) || r.CommitOf("gone") != "" {
t.Errorf("the commit the mesh runs = %q", got)
}
if got := r.BuildModules("novox/mesh-sdk"); strings.Join(got, ",") != buildrun.GoBuildModule { if got := r.BuildModules("novox/mesh-sdk"); strings.Join(got, ",") != buildrun.GoBuildModule {
t.Errorf("a repository with no module: %v, want the Go build module", got) t.Errorf("a repository with no module: %v, want the Go build module", got)
} }
@@ -1,6 +1,10 @@
package check package check
import "strconv" import (
"context"
"fmt"
"strconv"
)
// BesideRef is a sibling repository a repository's tests read, placed beside the fetched tree as a plain directory (ADR // BesideRef is a sibling repository a repository's tests read, placed beside the fetched tree as a plain directory (ADR
// 0484 clause 4). Ref names the pull request's head when the commit is only there. // 0484 clause 4). Ref names the pull request's head when the commit is only there.
@@ -10,37 +14,74 @@ type BesideRef struct {
Ref string `json:"ref,omitempty"` Ref string `json:"ref,omitempty"`
} }
const (
catalogueRepository = "novox/mesh-catalog"
nodeEngineRepo = "novox/mesh-host"
nodeEngineModule = "mesh-host"
controllerRepo = "novox/mesh-controller"
)
// besideRules are the repositories each core repository's tests read, as the tests state it (mesh-sdk go/internal/beside // besideRules are the repositories each core repository's tests read, as the tests state it (mesh-sdk go/internal/beside
// beside.go): the catalogue at its trunk, the node-engine, the SDK as the controller pins it, and the delivery's pinned // beside.go): the catalogue, the node-engine, the SDK, and the delivery's pinned forge dependencies (mesh-catalog
// forge dependencies (mesh-catalog checks/deps). The checker chooses which sibling at which commit; the build module only // checks/deps). Which commit of each is besideCommit's rule.
// places what it is told.
var besideRules = map[string][]string{ var besideRules = map[string][]string{
"novox/mesh-controller": {"novox/mesh-catalog", "novox/mesh-host", "novox/mesh-sdk"}, controllerRepo: {catalogueRepository, nodeEngineRepo, SDKRepository},
"novox/mesh-host": {"novox/mesh-catalog", "novox/mesh-sdk"}, nodeEngineRepo: {catalogueRepository, SDKRepository},
"novox/mesh-sdk": {"novox/mesh-catalog", "novox/mesh-host"}, SDKRepository: {catalogueRepository, nodeEngineRepo},
"novox/mesh-catalog": {"novox/mesh-sdk", "novox/mesh-controller"}, catalogueRepository: {SDKRepository, controllerRepo},
"novox/mesh-delivery": {"novox/mesh-sdk", "novox/mesh-catalog"}, "novox/mesh-delivery": {SDKRepository, catalogueRepository},
} }
// Beside are the siblings a pull request's tests read: each at its trunk head, and at the head of the group's pull request // besideFor are the siblings a pull request's tests read (ADR 0484 clause 4): the catalogue at its trunk, always, even in a
// when the group holds one of that repository (a group's members are checked together, ADR 0482 clause 2) — except the // group holding a catalogue pull request; the node-engine at the commit the mesh runs (the commit of the indexer's module
// catalogue, which the tests read at its trunk even when the group holds a catalogue pull request (beside.go). // mesh-host, its trunk head when none is known); mesh-sdk, for the controller's tests, at the commit the controller's go.mod
func (j *Examiner) Beside(c *Case, p Pull) []BesideRef { // pins; any other sibling at the head of the group's pull request for it when the group holds one, else at its trunk.
func (j *Examiner) besideFor(ctx context.Context, c *Case, p Pull) ([]BesideRef, error) {
var heads map[string]string var heads map[string]string
if j.Trunks != nil { if j.Trunks != nil {
heads = j.Trunks.Heads() heads = j.Trunks.Heads()
} }
var out []BesideRef var out []BesideRef
for _, repo := range besideRules[p.Repository] { for _, repo := range besideRules[p.Repository] {
if member := c.member(repo); member != nil && repo != "novox/mesh-catalog" { switch {
out = append(out, BesideRef{Repository: repo, Commit: member.Head, Ref: refOf(member.Number)}) case repo == catalogueRepository:
continue if head := heads[repo]; head != "" {
} out = append(out, BesideRef{Repository: repo, Commit: head})
if head := heads[repo]; head != "" { }
out = append(out, BesideRef{Repository: repo, Commit: head}) case repo == nodeEngineRepo:
commit := ""
if j.Runs != nil {
commit = j.Runs.CommitOf(nodeEngineModule)
}
if commit == "" {
commit = heads[repo]
}
if commit != "" {
out = append(out, BesideRef{Repository: repo, Commit: commit})
}
case repo == SDKRepository && p.Repository == controllerRepo && j.Forge != nil:
text, err := j.Forge.File(ctx, p.Repository, "go.mod", p.Head)
if err != nil {
return nil, fmt.Errorf("go.mod of %s#%d cannot be read to place the mesh-sdk its tests read: %w", p.Repository, p.Number, err)
}
pin, ok := SDKPin(text)
if !ok {
return nil, fmt.Errorf("go.mod of %s#%d names no mesh-sdk commit, so the mesh-sdk its tests read cannot be placed", p.Repository, p.Number)
}
full, err := j.Forge.FullSHA(ctx, SDKRepository, pin)
if err != nil {
return nil, fmt.Errorf("mesh-sdk %s, which %s#%d pins, cannot be found: %w", pin, p.Repository, p.Number, err)
}
out = append(out, BesideRef{Repository: repo, Commit: full})
default:
if member := c.member(repo); member != nil {
out = append(out, BesideRef{Repository: repo, Commit: member.Head, Ref: refOf(member.Number)})
} else if head := heads[repo]; head != "" {
out = append(out, BesideRef{Repository: repo, Commit: head})
}
} }
} }
return out return out, nil
} }
func (c *Case) member(repository string) *Pull { func (c *Case) member(repository string) *Pull {
@@ -56,7 +97,5 @@ func refOf(number int) string {
if number <= 0 { if number <= 0 {
return "" return ""
} }
return "refs/pull/" + itoa(number) + "/head" return "refs/pull/" + strconv.Itoa(number) + "/head"
} }
func itoa(n int) string { return strconv.Itoa(n) }
@@ -150,7 +150,7 @@ func caseOf(t testing.TB, pulls ...delivery.CandidatePull) *Case {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
cs := CaseOf(c, map[string]string{repoA: trunkA, repoSDK: trunkS}) cs := CaseOf(c, map[string]string{repoA: trunkA, repoSDK: trunkS, "novox/mesh-controller": trunkA, "novox/mesh-catalog": trunkA})
if cs == nil { if cs == nil {
t.Fatal("no case") t.Fatal("no case")
} }
@@ -552,3 +552,36 @@ func TestTheOutcomeSaysIfEachCandidatesModuleIsBuiltFailedOrWaiting(t *testing.T
t.Errorf("builds = %v, want one note for each module that has a fingerprint, sorted", got) t.Errorf("builds = %v, want one note for each module that has a fingerprint, sorted", got)
} }
} }
// trunksOf are trunk heads of every repository, as the gate's source names them.
func trunksOf(t *testing.T, heads map[string]string) *Trunks {
t.Helper()
data, err := delivery.EncodeSource(delivery.Source{Group: "g", Heads: heads})
if err != nil {
t.Fatal(err)
}
tr := &Trunks{}
tr.Put(data, false)
return tr
}
func TestNothingIsKeptForAWorkspaceGivenSiblingsBeside(t *testing.T) { // review D1, ADR 0444, issue 1287
b := newBench(t)
b.ex.Trunks = trunksOf(t, map[string]string{repoSDK: trunkS, "novox/mesh-catalog": trunkA})
b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
if got := len(b.steps.tool("fetch")); got != 1 || b.steps.tool("fetch")[0].args["beside"] == nil {
t.Fatalf("the fetch was not given siblings: %+v", b.steps.tool("fetch"))
}
if keys, _ := b.kept.Keys(context.Background()); len(keys) != 0 {
t.Errorf("kept %v for a package whose tests may read a sibling the digest does not cover", keys)
}
// And a kept verdict of the same digest from elsewhere is not taken for it: everything runs.
if _, err := (Kept{Store: b.kept}).Keep(context.Background(), "unit", digA, keptValue("fixture/slow", 2)); err != nil {
t.Fatal(err)
}
b.steps.calls = nil
b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
if n := len(b.steps.tool("test")); n != 2 {
t.Errorf("%d test steps, want both levels run whole", n)
}
}
@@ -1,40 +1,7 @@
package check package check
import "sync"
// PlansBuildingBucket is the controller's bucket of the delivery plans in their build phase, one key for each plan, written // PlansBuildingBucket is the controller's bucket of the delivery plans in their build phase, one key for each plan, written
// by the controller alone (issue 1652): a plan key there means a delivery builds on the build nodes, and no new check starts // by the controller alone (issue 1652): a plan key there means a delivery builds on the build nodes, and no new check starts
// meanwhile (ADR 0484 clause 12, ADR 0483 clause 6). The bucket may be absent: the controller's issue 1652 is delivered // meanwhile (ADR 0484 clause 12, ADR 0483 clause 6). The keys age out with no delete marker, so the keys are listed, not watched. The bucket may be absent: the controller's issue 1652 is delivered
// before R4c-5b, and if it is not, the bucket is read as empty. // before R4c-5b, and if it is not, the bucket is read as empty.
const PlansBuildingBucket = "mesh-controller_plans-building" const PlansBuildingBucket = "mesh-controller_plans-building"
// PlanHold holds which delivery plans build now, from the controller's bucket (a watch puts the changes in). Safe for use
// from the watch and from a check at once.
type PlanHold struct {
mu sync.Mutex
plans map[string]bool
}
// Put takes a change of the bucket in: a put of a plan's key, or its delete. It answers whether a delivery builds now
// differently than before, so a watch wakes the checker when the last plan ended.
func (h *PlanHold) Put(key string, deleted bool) (changed bool) {
h.mu.Lock()
defer h.mu.Unlock()
before := len(h.plans) > 0
if deleted {
delete(h.plans, key)
} else {
if h.plans == nil {
h.plans = map[string]bool{}
}
h.plans[key] = true
}
return before != (len(h.plans) > 0)
}
// Held says whether any delivery plan builds now.
func (h *PlanHold) Held() bool {
h.mu.Lock()
defer h.mu.Unlock()
return len(h.plans) > 0
}
@@ -7,39 +7,17 @@ import (
"git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/delivery"
) )
func TestADeliveryThatBuildsHoldsTheChecksAndTheEndOfTheLastPlanSaysSo(t *testing.T) { // N8/AC1
var h PlanHold
if h.Held() {
t.Fatal("an empty bucket holds")
}
if !h.Put("plan-1", false) || !h.Held() {
t.Error("a first plan did not hold")
}
if h.Put("plan-2", false) {
t.Error("a second plan changed whether a delivery builds")
}
if h.Put("plan-1", true) || !h.Held() {
t.Error("one of two plans ending released the hold")
}
if !h.Put("plan-2", true) || h.Held() {
t.Error("the last plan ending did not release the hold")
}
if h.Put("plan-3", true) {
t.Error("the delete of a plan never put changed the hold")
}
}
func TestARunningCheckIsNotStoppedWhenADeliveryStartsToBuild(t *testing.T) { // ADR 0484 clause 12, ADR 0483 clause 4 func TestARunningCheckIsNotStoppedWhenADeliveryStartsToBuild(t *testing.T) { // ADR 0484 clause 12, ADR 0483 clause 4
ctx := context.Background() ctx := context.Background()
var h PlanHold held := false
j := &judgeFunc{f: func(_ context.Context, _ *Case, call int) (Outcome, error) { j := &judgeFunc{f: func(_ context.Context, _ *Case, call int) (Outcome, error) {
if call == 1 { if call == 1 {
h.Put("plan-1", false) // a delivery starts while this check runs held = true // a delivery starts while this check runs
} }
return Outcome{Verdict: passing()}, nil return Outcome{Verdict: passing()}, nil
}} }}
r := newRig(t, j) r := newRig(t, j)
r.ch.Hold = h.Held r.ch.Hold = func() bool { return held }
r.ch.Candidate(ctx, "feat/x", candidateOf(t, "feat/x", pullOf(repoA, 7, head1))) r.ch.Candidate(ctx, "feat/x", candidateOf(t, "feat/x", pullOf(repoA, 7, head1)))
r.ch.Wait() r.ch.Wait()
if v := r.verdict(t, keyOf(t, "feat/x", candidateOf(t, "feat/x", pullOf(repoA, 7, head1)))); v.State != delivery.VerdictPass { if v := r.verdict(t, keyOf(t, "feat/x", candidateOf(t, "feat/x", pullOf(repoA, 7, head1)))); v.State != delivery.VerdictPass {
@@ -51,7 +29,7 @@ func TestARunningCheckIsNotStoppedWhenADeliveryStartsToBuild(t *testing.T) { //
if j.count() != 1 { if j.count() != 1 {
t.Errorf("judged %d times while a delivery builds, want only the one already running", j.count()) t.Errorf("judged %d times while a delivery builds, want only the one already running", j.count())
} }
h.Put("plan-1", true) held = false
r.ch.Reconsider(ctx) r.ch.Reconsider(ctx)
r.ch.Wait() r.ch.Wait()
if j.count() != 2 { if j.count() != 2 {
@@ -58,6 +58,8 @@ type Examiner struct {
// Previews is the previews bucket; nil writes none. // Previews is the previews bucket; nil writes none.
Previews KV Previews KV
Modules BuildModules Modules BuildModules
// Runs says which commit the mesh runs of a module (the indexer's modules); nil knows none.
Runs interface{ CommitOf(module string) string }
// Forge reads the go.mod files of the Go members and says whether a mesh-sdk commit is on its trunk (ADR 0484 clause 9); // Forge reads the go.mod files of the Go members and says whether a mesh-sdk commit is on its trunk (ADR 0484 clause 9);
// nil checks no pin. // nil checks no pin.
Forge Forge Forge Forge
@@ -94,7 +96,9 @@ type target struct {
fetched Fetched fetched Fetched
// priority are the Go packages (import paths) or the Node module directories whose test files the merge changes. // priority are the Go packages (import paths) or the Node module directories whose test files the merge changes.
changed []string changed []string
plans map[string]*goPlan // by level // beside says the workspace was given siblings: its tests may read what the digest does not cover, so nothing is kept for it.
beside bool
plans map[string]*goPlan // by level
} }
// goPlan is what a Go target has to run at one level: the digests, the packages without a kept verdict and the tests taken. // goPlan is what a Go target has to run at one level: the digests, the packages without a kept verdict and the tests taken.
@@ -238,8 +242,13 @@ func (st *state) fetchAll(ctx context.Context) *delivery.Verdict {
st.nodes[node] = true st.nodes[node] = true
args := map[string]any{"repository": p.Repository, "commit": p.Head, "ref": refOf(p.Number), "base": p.Trunk} args := map[string]any{"repository": p.Repository, "commit": p.Head, "ref": refOf(p.Number), "base": p.Trunk}
if bm == buildrun.GoBuildModule { if bm == buildrun.GoBuildModule {
if beside := st.j.Beside(st.c, p); len(beside) > 0 { beside, err := st.j.besideFor(ctx, st.c, p)
if err != nil {
return fail(node, err.Error())
}
if len(beside) > 0 {
args["beside"] = beside args["beside"] = beside
t.beside = true
} }
} }
raw, err := st.step(ctx, t, "fetch", fmt.Sprintf("fetch %s#%d on %s", p.Repository, p.Number, node), args) raw, err := st.step(ctx, t, "fetch", fmt.Sprintf("fetch %s#%d on %s", p.Repository, p.Number, node), args)
@@ -364,6 +373,12 @@ func (st *state) planGo(ctx context.Context, t *target, level string) *delivery.
} }
plan := &goPlan{digests: map[string]string{}} plan := &goPlan{digests: map[string]string{}}
t.plans[level] = plan t.plans[level] = plan
if t.beside {
// The digest covers the package's own files, the toolchain and the header, not the siblings placed beside the tree: a
// package whose tests read one must never be kept, nor taken from a verdict kept for other inputs (ADR 0444).
plan.all = true
return nil
}
raw, err := st.step(ctx, t, "digest", fmt.Sprintf("digest the %s level of %s#%d on %s", level, t.pull.Repository, t.pull.Number, t.node), raw, err := st.step(ctx, t, "digest", fmt.Sprintf("digest the %s level of %s#%d on %s", level, t.pull.Repository, t.pull.Number, t.node),
map[string]any{"workspace": t.fetched.Workspace, "level": level}) map[string]any{"workspace": t.fetched.Workspace, "level": level})
var d Digested var d Digested
@@ -29,6 +29,8 @@ type Forge interface {
// OnTrunk says whether commit (a full or a 12-digit sha) is already merged into the trunk of the repository: it holds no // OnTrunk says whether commit (a full or a 12-digit sha) is already merged into the trunk of the repository: it holds no
// commit the trunk does not. // commit the trunk does not.
OnTrunk(ctx context.Context, repository, commit string) (bool, error) OnTrunk(ctx context.Context, repository, commit string) (bool, error)
// FullSHA is the 40-digit sha of a commit named by its first 12 digits.
FullSHA(ctx context.Context, repository, commit string) (string, error)
} }
var pseudoVersion = regexp.MustCompile(`-([0-9a-f]{12})$`) var pseudoVersion = regexp.MustCompile(`-([0-9a-f]{12})$`)
@@ -29,6 +29,13 @@ func (f *fakeForge) File(_ context.Context, repository, path, ref string) (strin
return "", fmt.Errorf("no file %s of %s at %s", path, repository, ref) return "", fmt.Errorf("no file %s of %s at %s", path, repository, ref)
} }
func (f *fakeForge) FullSHA(_ context.Context, _ string, commit string) (string, error) {
if f.err != nil {
return "", f.err
}
return commit + strings.Repeat("0", 40-len(commit)), nil
}
func (f *fakeForge) OnTrunk(_ context.Context, _ string, commit string) (bool, error) { func (f *fakeForge) OnTrunk(_ context.Context, _ string, commit string) (bool, error) {
if f.err != nil { if f.err != nil {
return false, f.err return false, f.err
@@ -145,3 +152,53 @@ func TestAPinThatCannotBeCheckedFailsAndNeverPasses(t *testing.T) { // ADR 0453
t.Errorf("verdict = %+v", v) t.Errorf("verdict = %+v", v)
} }
} }
type runsAt map[string]string
func (r runsAt) CommitOf(module string) string { return r[module] }
func besideOf(t *testing.T, b *bench, repository string) map[string]BesideRef {
t.Helper()
out := map[string]BesideRef{}
for _, f := range b.steps.tool("fetch") {
if f.args["repository"] == repository {
refs, _ := f.args["beside"].([]BesideRef)
for _, r := range refs {
out[r.Repository] = r
}
}
}
return out
}
func TestTheSiblingsAreWhereTheTestsStateThem(t *testing.T) { // review D2, ADR 0484 clause 4
const controller, catalogue, host = "novox/mesh-controller", "novox/mesh-catalog", "novox/mesh-host"
hostRuns := strings.Repeat("7", 40)
b := newBench(t)
b.ex.Trunks = trunksOf(t, map[string]string{controller: trunkA, catalogue: trunkS, host: trunkA2, SDKRepository: "9999999999999999999999999999999999999999"})
b.ex.Runs = runsAt{"mesh-host": hostRuns}
forge := &fakeForge{goMods: map[string]string{controller + " go.mod " + head1: capturedGoMod(t, "cb54756c3865")}, onTrunk: map[string]bool{"cb54756c3865": true}}
b.ex.Forge = forge
// A group of the controller and the catalogue: the controller's tests read the catalogue's trunk, not the group's head.
b.judge(t, caseOf(t, pullOf(controller, 7, head1), pullOf(catalogue, 12, head2)))
got := besideOf(t, b, controller)
if c := got[catalogue]; c.Commit != trunkS || c.Ref != "" {
t.Errorf("the catalogue beside the controller = %+v, want its trunk %s", c, trunkS)
}
if h := got[host]; h.Commit != hostRuns {
t.Errorf("the node-engine beside the controller = %+v, want the commit the mesh runs %s", h, hostRuns)
}
if s := got[SDKRepository]; s.Commit != "cb54756c3865"+strings.Repeat("0", 28) {
t.Errorf("mesh-sdk beside the controller = %+v, want the commit its go.mod pins, whole", s)
}
}
func TestThePinOfTheControllerThatCannotBeReadFailsTheCheck(t *testing.T) { // review D2
b := newBench(t)
b.ex.Trunks = trunksOf(t, map[string]string{"novox/mesh-controller": trunkA})
b.ex.Forge = &fakeForge{goMods: map[string]string{}, onTrunk: map[string]bool{}}
v := b.judge(t, caseOf(t, pullOf("novox/mesh-controller", 7, head1))).Verdict
if v.State != delivery.VerdictFail || !strings.Contains(v.Failed.Words, "go.mod") {
t.Errorf("verdict = %+v", v)
}
}
@@ -17,3 +17,5 @@ novox/mesh-delivery at main, and forge-compare-trunk.json the answer of gitea_co
trunk: no commit main does not hold); both called through the mesh on 2026-10-21 for issue 1624. forge-compare-branch.json is the answer of the same call for a trunk: no commit main does not hold); both called through the mesh on 2026-10-21 for issue 1624. forge-compare-branch.json is the answer of the same call for a
head that is not on the trunk (novox/mesh-sdk base main, head afbe371003a5, the branch feat/r4c5-sdk), shortened to two of its seven commits and one of its head that is not on the trunk (novox/mesh-sdk base main, head afbe371003a5, the branch feat/r4c5-sdk), shortened to two of its seven commits and one of its
twenty-six files, with the head and the totals set to match. twenty-six files, with the head and the totals set to match.
forge-commits.json is the answer of the forge's gitea_commits for novox/mesh-sdk, ref cb54756c3865, limit 1, called through the mesh on 2026-10-21 (review D2).
@@ -0,0 +1,14 @@
{
"commits": [
{
"author": "mesh-tools",
"date": "2026-10-21T10:02:09Z",
"sha": "cb54756c3865df5ac9c0dc0481ad9ebe2bf013a1",
"subject": "Merge pull request 'Give delivery.Module the toolchains its fingerprint was computed with, and '.' as a root module's path, so the builder builds a key only with its own toolchain (issue 1615)' (#55) from fix/1615-module-toolchains-root into main"
}
],
"more": true,
"page": 1,
"ref": "cb54756c3865",
"repository": "novox/mesh-sdk"
}
@@ -100,7 +100,7 @@ func TestManifestDeclarations(t *testing.T) { // 1678/AC1
"mesh-delivery-indexer.manifests", "mesh-delivery-indexer.modules", "mesh-delivery-watchman.presence", "node-engine.have"}; !reflect.DeepEqual(sorted(m.Reads), want) { "mesh-delivery-indexer.manifests", "mesh-delivery-indexer.modules", "mesh-delivery-watchman.presence", "node-engine.have"}; !reflect.DeepEqual(sorted(m.Reads), want) {
t.Errorf("reads = %v, want %v", m.Reads, want) t.Errorf("reads = %v, want %v", m.Reads, want)
} }
if want := []string{"gitea.gitea_compare", "gitea.gitea_get_file", "mesh-build-go.build_go_digest", "mesh-build-go.build_go_fetch", "mesh-build-go.build_go_test", "mesh-build-node.build_node_fetch", "mesh-build-node.build_node_test"}; !reflect.DeepEqual(sorted(m.Invokes), want) { if want := []string{"gitea.gitea_commits", "gitea.gitea_compare", "gitea.gitea_get_file", "mesh-build-go.build_go_digest", "mesh-build-go.build_go_fetch", "mesh-build-go.build_go_test", "mesh-build-node.build_node_fetch", "mesh-build-node.build_node_test"}; !reflect.DeepEqual(sorted(m.Invokes), want) {
t.Errorf("invokes = %v, want only the build modules' fetch, digest and test (no run step before issue 1309, ADR 0484 clause 5) and the forge's two reads for a mesh-sdk pin (clause 9)", m.Invokes) t.Errorf("invokes = %v, want only the build modules' fetch, digest and test (no run step before issue 1309, ADR 0484 clause 5) and the forge's two reads for a mesh-sdk pin (clause 9)", m.Invokes)
} }
// The checker is woken by a build module's report of a toolchain, as the indexer is: the bucket may not exist when it starts. // The checker is woken by a build module's report of a toolchain, as the indexer is: the bucket may not exist when it starts.
@@ -35,6 +35,7 @@
"checker_verdicts" "checker_verdicts"
], ],
"invokes": [ "invokes": [
"gitea.gitea_commits",
"gitea.gitea_compare", "gitea.gitea_compare",
"gitea.gitea_get_file", "gitea.gitea_get_file",
"mesh-build-go.build_go_digest", "mesh-build-go.build_go_digest",