Compare commits

..
Author SHA1 Message Date
jochen 39419b1892 Pin the build modules to mesh-sdk db1007ff, which deletes a gone process's ask key so its redelivery still answers (issue 1890)
mesh/delivery ready: it is merged on the Approve
mesh/check pass 39419b1892c9 on 64530a8dcb08 | judged by the candidate's mesh-build-go at 21af6af6fc76
mesh/delivery-group group fix/1890-step-owner ready: every member ready, and composed together they pass
2026-10-27 01:30:31 +01:00
jochen 3d42988afa Hold every build module to ending what a previous process left running (issue 1890)
A replaced mesh-build-go left a check ask's step running and its start
sweep spared it (issue 1890, note 21). mesh-build-go, mesh-build-node and
mesh-build-image pin mesh-sdk 981a35b5, whose steps name their owner and
whose sweep ends another owner's at once; each runs the sweep through one
method that main calls and that its test runs through the conformance
kit's left-running case. The failurekit check now refuses a build module
whose tests do not run that case, test step or not.
2026-10-27 01:22:13 +01:00
31 changed files with 191 additions and 525 deletions
+21 -8
View File
@@ -2,7 +2,9 @@
// fix): a module that claims a node-build-* seat and serves a test step must hold a test that calls conformance.Run, so
// its parse of the test runner's output is held to the failure contract (a failing test named with its cause, never a
// passing line in its place). A build module with no test step (mesh-build-image builds images and runs no tests) is
// exempt, and said so.
// exempt from that, and said so. Every build module, test step or not, must also hold a test that calls
// conformance.LeftRunning with its own start sweep, so a step a previous process left running is ended at its start
// (issue 1890).
package failurekit
import (
@@ -57,20 +59,31 @@ func Judge(root string) ([]Judged, error) {
}
dir := filepath.Dir(p)
j := Judged{Module: filepath.Base(dir)}
var refused []string
test := testTool(m.Tools)
switch {
case test == "":
j.Exempt = fmt.Sprintf("it claims %s and serves no test step", seat)
j.Exempt = fmt.Sprintf("it claims %s and serves no test step, so the failure contract's cases do not apply", seat)
default:
runs, err := runsTheKit(dir)
runs, err := runsTheKit(dir, "conformance.Run(")
if err != nil {
return nil, err
}
if !runs {
j.Refused = fmt.Sprintf("it claims %s and serves the test step %s, and no test of it calls conformance.Run "+
"(mesh-sdk go/buildstep/conformance): run the kit with the module's own parse path (issue 1901)", seat, test)
refused = append(refused, fmt.Sprintf("it claims %s and serves the test step %s, and no test of it calls conformance.Run "+
"(mesh-sdk go/buildstep/conformance): run the kit with the module's own parse path (issue 1901)", seat, test))
}
}
// Every build module keeps steps, so every one runs the left-running case, test step or not (issue 1890).
sweeps, err := runsTheKit(dir, "conformance.LeftRunning(")
if err != nil {
return nil, err
}
if !sweeps {
refused = append(refused, fmt.Sprintf("it claims %s, and no test of it calls conformance.LeftRunning "+
"(mesh-sdk go/buildstep/conformance): run the kit's left-running case with the module's own start sweep (issue 1890)", seat))
}
j.Refused = strings.Join(refused, "; ")
out = append(out, j)
}
return out, nil
@@ -96,8 +109,8 @@ func testTool(tools []json.RawMessage) string {
return ""
}
// runsTheKit says a _test.go file under dir calls conformance.Run.
func runsTheKit(dir string) (bool, error) {
// runsTheKit says a _test.go file under dir calls the kit's function call (such as "conformance.Run(").
func runsTheKit(dir, call string) (bool, error) {
found := false
err := filepath.WalkDir(dir, func(p string, d fs.DirEntry, err error) error {
if err != nil || found || d.IsDir() || !strings.HasSuffix(p, "_test.go") {
@@ -107,7 +120,7 @@ func runsTheKit(dir string) (bool, error) {
if err != nil {
return err
}
found = strings.Contains(string(raw), "conformance.Run(") &&
found = strings.Contains(string(raw), call) &&
strings.Contains(string(raw), `"git.novox.be/novox/mesh-sdk/go/buildstep/conformance"`)
return nil
})
+12 -5
View File
@@ -6,17 +6,24 @@ import (
"testing"
)
// 1901/AC7: a build module with a test step and no kit is refused, one that runs the kit passes, one with no test step is
// exempt, and a module that claims no node-build-* seat is not judged.
// 1901/AC7 and 1890/AC8: a build module with a test step and no kit is refused, one that runs the kit passes, one with no
// test step is exempt from the failure cases but not from the left-running case, a module that runs only the failure
// cases is refused, and a module that claims no node-build-* seat is not judged.
func TestABuildModuleWithATestStepMustRunTheKit(t *testing.T) {
root := t.TempDir()
kit := "package main\n\nimport \"git.novox.be/novox/mesh-sdk/go/buildstep/conformance\"\n\nfunc TestKit(t *testing.T) { conformance.Run(t, conformance.GoTest, nil) }\n"
const imp = "package main\n\nimport \"git.novox.be/novox/mesh-sdk/go/buildstep/conformance\"\n\n"
const sweep = "func TestSweep(t *testing.T) { conformance.LeftRunning(t, nil) }\n"
kit := imp + "func TestKit(t *testing.T) { conformance.Run(t, conformance.GoTest, nil) }\n" + sweep
for path, body := range map[string]string{
"modules/a/module.json": `{"claims":[{"name":"node-build-a"}],"tools":["build_a_fetch","build_a_test"]}`,
"modules/a/cmd/a/check_test.go": "package main\n",
"modules/b/module.json": `{"claims":[{"name":"node-build-b"}],"tools":[{"name":"build_b_test"}]}`,
"modules/b/cmd/b/kit_test.go": kit,
"modules/c/module.json": `{"claims":[{"name":"node-build-c"}],"tools":["build_c_build"]}`,
"modules/c/cmd/c/kit_test.go": imp + sweep,
"modules/f/module.json": `{"claims":[{"name":"node-build-f"}],"tools":["build_f_build"]}`,
"modules/g/module.json": `{"claims":[{"name":"node-build-g"}],"tools":["build_g_test"]}`,
"modules/g/cmd/g/kit_test.go": imp + "func TestKit(t *testing.T) { conformance.Run(t, conformance.GoTest, nil) }\n",
"modules/d/module.json": `{"claims":[{"name":"other"}],"tools":["d_test"]}`,
"modules/e/module.json": `{"claims":[{"name":"node-build-e"}],"tools":["build_e_test"]}`,
"modules/e/cmd/e/kit.go": kit, // not a test file
@@ -33,9 +40,9 @@ func TestABuildModuleWithATestStepMustRunTheKit(t *testing.T) {
if err != nil {
t.Fatal(err)
}
want := map[string]string{"a": "refused", "b": "passes", "c": "exempt", "e": "refused"}
want := map[string]string{"a": "refused", "b": "passes", "c": "exempt", "e": "refused", "f": "refused", "g": "refused"}
if len(got) != len(want) {
t.Fatalf("judged %+v, want a, b, c and e", got)
t.Fatalf("judged %+v, want a, b, c, e, f and g", got)
}
for _, j := range got {
is := "passes"
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/buildstep"
@@ -128,6 +129,13 @@ func (m *Module) SeatTools(p *buildstep.Puller) []stdio.Tool {
// StepEndTool ends one step left running (issue 1890); the module's steps state is the one it reads and writes.
const StepEndTool = "build_go_step_end"
// endLeftRunning is the module's start sweep (issue 1890): it ends the steps of its node that a previous process left
// running (buildstep.EndStaleAtStart; every step names its owner). main runs it once at the start, and the module's test
// runs it through the conformance kit's left-running case.
func (m *Module) endLeftRunning(ctx context.Context, scan buildstep.Scan, booted time.Time, logf func(string, ...any)) {
buildstep.EndStaleAtStart(ctx, scan, m.Node, booted, logf)
}
// stepScan is the module's steps state as buildstep reads it.
func (m *Module) stepScan() buildstep.Scan {
return buildstep.ScanOf(stdio.State(delivery.StepsState))
@@ -1,7 +1,9 @@
package main
import (
"context"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/buildstep"
"git.novox.be/novox/mesh-sdk/go/buildstep/conformance"
@@ -20,3 +22,12 @@ func TestTheTestStepKeepsTheFailureContract(t *testing.T) {
return res.Failed
})
}
// 1890/AC7: the module's own start sweep (endLeftRunning, as main runs it) keeps the conformance kit's left-running
// case: a step a previous process left running is ended at once and said, whatever its age, and a step of this process
// is kept.
func TestTheStartSweepEndsWhatAPreviousProcessLeft(t *testing.T) {
conformance.LeftRunning(t, func(ctx context.Context, scan buildstep.Scan, node string, before time.Time, logf func(string, ...any)) {
(&Module{Node: node}).endLeftRunning(ctx, scan, before, logf)
})
}
@@ -8,7 +8,6 @@ package main
import (
"context"
"fmt"
"git.novox.be/novox/mesh-sdk/go/buildstep"
"os"
"os/user"
"strings"
@@ -76,8 +75,9 @@ func main() {
puller := m.Puller(steps, nil)
m.Held = puller
if m.Node != "" {
// A step a previous process left running is ended (issue 1890); this process's own steps start after booted.
go buildstep.EndStaleAtStart(ctx, m.stepScan(), m.Node, booted, logf)
// A step a previous process left running is ended (issue 1890): one that names another owner, or none and
// started before booted.
go m.endLeftRunning(ctx, m.stepScan(), booted, logf)
}
if m.Node != "" {
// The seat's asks, pulled once Serve has the runtime's channel; a pull before it is said and tried again.
+1 -1
View File
@@ -3,7 +3,7 @@ module meshbuildgo
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026233910-a2759ec92ec9
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027002948-db1007ffdda1
github.com/distribution/distribution/v3 v3.1.2
github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0
+2 -2
View File
@@ -1,5 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026233910-a2759ec92ec9 h1:MS7i4NVNQHMAMqRRFMdzbto31BHZY+BuEZY1ql3p4Rs=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026233910-a2759ec92ec9/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027002948-db1007ffdda1 h1:9qyUcrvlklxDRBUCBqGH1L3oximOk554IfaPrTMgaTA=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027002948-db1007ffdda1/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI=
github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
@@ -2,6 +2,7 @@ package main
import (
"context"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/buildstep"
@@ -78,6 +79,13 @@ func (m *Module) SeatTools(p *buildstep.Puller) []stdio.Tool {
// StepEndTool ends one step left running (issue 1890); the module's steps state is the one it reads and writes.
const StepEndTool = "build_image_step_end"
// endLeftRunning is the module's start sweep (issue 1890): it ends the steps of its node that a previous process left
// running (buildstep.EndStaleAtStart; every step names its owner). main runs it once at the start, and the module's test
// runs it through the conformance kit's left-running case.
func (m *Module) endLeftRunning(ctx context.Context, scan buildstep.Scan, booted time.Time, logf func(string, ...any)) {
buildstep.EndStaleAtStart(ctx, scan, m.Node, booted, logf)
}
// stepScan is the module's steps state as buildstep reads it.
func (m *Module) stepScan() buildstep.Scan {
return buildstep.ScanOf(stdio.State(delivery.StepsState))
@@ -36,6 +36,9 @@ func normalized(t *testing.T, st delivery.Step, workspace, commit, registry stri
t.Fatal(err)
}
v["id"] = "STEPID"
if _, ok := v["owner"]; ok {
v["owner"] = "OWNER" // the process that ran it (issue 1890), differs per run
}
v["started"], v["ended"] = "2026-10-22T08:00:00Z", "2026-10-22T08:00:01Z"
out, _ := json.MarshalIndent(v, "", " ")
s := string(out)
@@ -0,0 +1,19 @@
package main
import (
"context"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/buildstep"
"git.novox.be/novox/mesh-sdk/go/buildstep/conformance"
)
// 1890/AC7: the module's own start sweep (endLeftRunning, as main runs it) keeps the conformance kit's left-running
// case: a step a previous process left running is ended at once and said, whatever its age, and a step of this process
// is kept.
func TestTheStartSweepEndsWhatAPreviousProcessLeft(t *testing.T) {
conformance.LeftRunning(t, func(ctx context.Context, scan buildstep.Scan, node string, before time.Time, logf func(string, ...any)) {
(&Module{Node: node}).endLeftRunning(ctx, scan, before, logf)
})
}
@@ -9,7 +9,6 @@ package main
import (
"context"
"fmt"
"git.novox.be/novox/mesh-sdk/go/buildstep"
"os"
"os/user"
"strings"
@@ -63,8 +62,9 @@ func main() {
puller := m.Puller(steps, nil)
m.Held = puller
if m.Node != "" {
// A step a previous process left running is ended (issue 1890); this process's own steps start after booted.
go buildstep.EndStaleAtStart(ctx, m.stepScan(), m.Node, booted, logf)
// A step a previous process left running is ended (issue 1890): one that names another owner, or none and
// started before booted.
go m.endLeftRunning(ctx, m.stepScan(), booted, logf)
}
if m.Node != "" {
// The seat's asks, pulled once Serve has the runtime's channel; a pull before it is said and tried again.
@@ -2,6 +2,7 @@
"ended": "2026-10-22T08:00:01Z",
"id": "STEPID",
"node": "ace",
"owner": "OWNER",
"result": {
"artifact": "server",
"digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111",
@@ -2,6 +2,7 @@
"ended": "2026-10-22T08:00:01Z",
"id": "STEPID",
"node": "ace",
"owner": "OWNER",
"result": {
"artifact": "server",
"digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111",
@@ -2,6 +2,7 @@
"ended": "2026-10-22T08:00:01Z",
"id": "STEPID",
"node": "ace",
"owner": "OWNER",
"result": {
"commit": "COMMIT",
"workspace": "WORKSPACE"
+1 -1
View File
@@ -3,7 +3,7 @@ module meshbuildimage
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026202658-805b4d9a3536
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027002948-db1007ffdda1
github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0
)
+2 -2
View File
@@ -1,5 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026202658-805b4d9a3536 h1:+SoDdDXF7Tgj38WP4kRpUSps6DJVl6P+w5u/KfdtwnQ=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026202658-805b4d9a3536/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027002948-db1007ffdda1 h1:9qyUcrvlklxDRBUCBqGH1L3oximOk554IfaPrTMgaTA=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027002948-db1007ffdda1/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=
@@ -2,6 +2,7 @@ package main
import (
"context"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/buildstep"
@@ -86,6 +87,13 @@ func (m *Module) SeatTools(p *buildstep.Puller) []stdio.Tool {
// StepEndTool ends one step left running (issue 1890); the module's steps state is the one it reads and writes.
const StepEndTool = "build_node_step_end"
// endLeftRunning is the module's start sweep (issue 1890): it ends the steps of its node that a previous process left
// running (buildstep.EndStaleAtStart; every step names its owner). main runs it once at the start, and the module's test
// runs it through the conformance kit's left-running case.
func (m *Module) endLeftRunning(ctx context.Context, scan buildstep.Scan, booted time.Time, logf func(string, ...any)) {
buildstep.EndStaleAtStart(ctx, scan, m.Node, booted, logf)
}
// stepScan is the module's steps state as buildstep reads it.
func (m *Module) stepScan() buildstep.Scan {
return buildstep.ScanOf(stdio.State(delivery.StepsState))
@@ -1,7 +1,9 @@
package main
import (
"context"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/buildstep"
"git.novox.be/novox/mesh-sdk/go/buildstep/conformance"
@@ -18,3 +20,12 @@ func TestTheTestStepKeepsTheFailureContract(t *testing.T) {
return testFailures(".", "type-check", o.Dir, forgekey.Ran{Status: 2, Stdout: o.Stdout, Stderr: o.Stderr}, "")
})
}
// 1890/AC7: the module's own start sweep (endLeftRunning, as main runs it) keeps the conformance kit's left-running
// case: a step a previous process left running is ended at once and said, whatever its age, and a step of this process
// is kept.
func TestTheStartSweepEndsWhatAPreviousProcessLeft(t *testing.T) {
conformance.LeftRunning(t, func(ctx context.Context, scan buildstep.Scan, node string, before time.Time, logf func(string, ...any)) {
(&Module{Node: node}).endLeftRunning(ctx, scan, before, logf)
})
}
@@ -9,7 +9,6 @@ package main
import (
"context"
"fmt"
"git.novox.be/novox/mesh-sdk/go/buildstep"
"os"
"os/user"
"path/filepath"
@@ -75,8 +74,9 @@ func main() {
puller := m.Puller(steps, nil)
m.Held = puller
if m.Node != "" {
// A step a previous process left running is ended (issue 1890); this process's own steps start after booted.
go buildstep.EndStaleAtStart(ctx, m.stepScan(), m.Node, booted, logf)
// A step a previous process left running is ended (issue 1890): one that names another owner, or none and
// started before booted.
go m.endLeftRunning(ctx, m.stepScan(), booted, logf)
}
if m.Node != "" {
// The seat's asks, pulled once Serve has the runtime's channel; a pull before it is said and tried again.
+1 -1
View File
@@ -3,7 +3,7 @@ module meshbuildnode
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026233910-a2759ec92ec9
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027002948-db1007ffdda1
github.com/distribution/distribution/v3 v3.1.2
github.com/nats-io/nats.go v1.54.0
)
+2 -2
View File
@@ -1,5 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026233910-a2759ec92ec9 h1:MS7i4NVNQHMAMqRRFMdzbto31BHZY+BuEZY1ql3p4Rs=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026233910-a2759ec92ec9/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027002948-db1007ffdda1 h1:9qyUcrvlklxDRBUCBqGH1L3oximOk554IfaPrTMgaTA=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027002948-db1007ffdda1/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI=
github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
-1
View File
@@ -32,7 +32,6 @@ what it refuses.
| `tracking` | optionally `conditions` (condition keys) | for every condition key an issue links (link kind `condition`), the issues that link it, those not closed first: status, severity, needs, worker with since, active and stale, and where the fix is (`fix`); the operator's channel says it in a condition's message (hq ADR 0285) |
| `reserve` | `kind` (`decision` or `research`), optionally `for` (one line: what the record is about) | the next number of that kind, spent forever; answers `kind` and `number` (and `prefix`, the number as the record's name starts with it); see below |
| `fix-times` | optionally `days` (1 to 90, default 7) | each class's fix budget, the median and worst fix time of the issues whose last fix was delivered in the window, within and over, and every issue over its budget with its longest phase; see *A fix's time* |
| `learn-fixes` | `number` | asks again for the fixes of one issue the tracker keeps no record of (a pull request merged before it heard deliveries): the delivery, then the forge, say whether each is merged (and the commit it landed as) or closed without merging; answers what each is kept as and the delivery line |
**The cycle's rules, at the write** (hq playbook 03, which `00-META/checks/cycle.py` enforced on the
folders until the import, and hq ADR 0284): a status is one of `open`,
+31 -57
View File
@@ -18,7 +18,6 @@ package main
import (
"fmt"
"sort"
"strings"
"time"
)
@@ -113,60 +112,46 @@ func (t *Tracker) clearNotResolved(is *Issue) error {
// askUnheard asks mesh-delivery for a few fixes of issues not yet resolved that the tracker never heard of: a pull
// request linked after its delivery moved hears no transition again, and OnMoved asks only at a move.
func (t *Tracker) askUnheard() error {
if t.Deliveries == nil && t.Pull == nil {
if t.Deliveries == nil {
return nil
}
all, err := t.allIssues()
if err != nil {
return err
}
// Issues not resolved first, then the others: a resolved issue's fix merged before the tracker heard deliveries is learned
// too, so its delivery line stops saying nothing was heard (issue 1950). The asks are bounded, and the pull request asked
// longest ago goes first, so ones that cannot be learned (still open, gone) never starve the rest.
type cand struct {
n uint64
pr string
open bool
at time.Time
}
var cs []cand
t.askedMu.Lock()
asked := 0
for n, is := range all {
for _, pr := range unheardOf(is) {
cs = append(cs, cand{n, pr, unresolved(is), t.askedAt[pr]})
}
}
t.askedMu.Unlock()
sort.Slice(cs, func(i, j int) bool {
a, b := cs[i], cs[j]
if a.open != b.open {
return a.open
}
if !a.at.Equal(b.at) {
return a.at.Before(b.at)
}
if a.n != b.n {
return a.n < b.n
}
return a.pr < b.pr
})
for i, c := range cs {
if i == MostSweepAsks {
break
}
t.askedMu.Lock()
if t.askedAt == nil {
t.askedAt = map[string]time.Time{}
}
t.askedAt[c.pr] = t.now()
t.askedMu.Unlock()
kept, err := t.learnFix(c.n, c.pr)
if err != nil {
t.Logf("[mesh-issues] the delivery of %s (issue %d) could not be asked (the next sweep asks the others first): %v", c.pr, c.n, err)
if !unresolved(is) {
continue
}
if kept {
if err := t.afterFix(c.n); err != nil {
heard := false
for _, pr := range fixPullRequests(is) {
if fixOf(is, pr) != nil {
continue
}
if asked == MostSweepAsks {
return nil
}
asked++
repo, number, _ := strings.Cut(pr, "#")
d, err := t.Deliveries(repo, number)
if err != nil {
return fmt.Errorf("the delivery of %s (issue %d): %v", pr, n, err)
}
if d.State == "" {
continue
}
since := d.Since
if since.IsZero() {
since = t.now()
}
if _, err := t.recordFix(pr, d.heard(since), n); err != nil {
return err
}
heard = true
}
if heard {
if err := t.afterFix(n); err != nil {
return err
}
}
@@ -174,17 +159,6 @@ func (t *Tracker) askUnheard() error {
return nil
}
// unheardOf is the pull requests an issue names that the tracker keeps no record for.
func unheardOf(is *Issue) []string {
var out []string
for _, pr := range fixPullRequests(is) {
if fixOf(is, pr) == nil {
out = append(out, pr)
}
}
return out
}
// openPullRequest says whether an issue names a pull request not merged yet, or one whose delivery the tracker
// never heard of: work in review is work, and a fix unheard of is not taken for none. One closed without merging
// is not open.
+34 -31
View File
@@ -454,22 +454,9 @@ func (t *Tracker) OnDelivered(e stdio.Envelope) error {
at = t.now()
}
var changed []uint64
learned := 0
for n, is := range all {
for _, pr := range liveFixes(is) {
f := fixOf(is, pr)
if f == nil && learned < MostSweepAsks {
// A pull request named and kept no record for (merged before the tracker heard deliveries): it learns its
// merge now, so this very event can cover it (issue 1950).
learned++
if ok, err := t.learnFix(n, pr); err != nil {
t.Logf("[mesh-issues] the merge of %s (issue %d) could not be learned (asked again at the next event): %v", pr, n, err)
} else if ok {
if fresh, _, err := read(t.Store, n); err == nil {
is, f = fresh, fixOf(fresh, pr)
}
}
}
if f != nil && f.Merged && !contains(is.Delivered, pr) && f.MergedAs == "" {
t.fillMerge(n, f, pr)
}
@@ -637,10 +624,6 @@ type Delivery struct {
Repository string
Group string
MergedAs string
// Pulled says the answer carried the forge's pull request (merged, closed, the commit its merge landed as); Closed that it
// was closed without merging.
Pulled bool
Closed bool
}
// heard is what a delivery answered, as a fix keeps it.
@@ -679,18 +662,16 @@ func busDeliveryState(owner string) DeliveryState {
Group string `json:"group"`
MergedAs string `json:"merged_as"`
} `json:"delivery"`
Pull *pullFacts `json:"pull"`
Pull struct {
Merged bool `json:"merged"`
} `json:"pull"`
}
if err := json.Unmarshal(raw, &out); err != nil {
return Delivery{}, fmt.Errorf("mesh-delivery's checks answered no delivery: %.200s", raw)
}
d := Delivery{State: out.Delivery.State, Why: out.Delivery.Waits, Since: out.Delivery.Since.UTC(),
return Delivery{State: out.Delivery.State, Why: out.Delivery.Waits, Since: out.Delivery.Since.UTC(), Merged: out.Pull.Merged,
Class: out.Delivery.Class, Opened: nonZero(out.Delivery.Opened), MergedAt: nonZero(out.Delivery.MergedAt),
Repository: strings.ToLower(owner + "/" + repository), Group: out.Delivery.Group, MergedAs: out.Delivery.MergedAs}
if out.Pull != nil {
d.fromPull(*out.Pull, owner+"/"+repository)
}
return d, nil
Repository: strings.ToLower(owner + "/" + repository), Group: out.Delivery.Group, MergedAs: out.Delivery.MergedAs}, nil
}
}
@@ -714,12 +695,34 @@ func (t *Tracker) OnMoved(e stdio.Envelope) error {
if t.Deliveries != nil {
// Each ask up to the bus's 30 seconds, under the runtime's two minutes; one that fails, or more left to
// ask, has the event offered again, so a delivery never asked ends in max-deliveries, not in silence.
asked, _, err := t.learnUnheard(body.Number, is, MostDeliveryAsks)
if err != nil {
return err
}
if asked == MostDeliveryAsks && len(unheardOf(is)) > asked {
return fmt.Errorf("issue %d names more pull requests than are asked at once; the rest at the next offer", body.Number)
asked := 0
for _, pr := range fixPullRequests(is) {
if fixOf(is, pr) != nil {
continue
}
if asked == MostDeliveryAsks {
return fmt.Errorf("issue %d names more pull requests than are asked at once; the rest at the next offer", body.Number)
}
asked++
repo, number, _ := strings.Cut(pr, "#")
d, err := t.Deliveries(repo, number)
if err != nil {
return fmt.Errorf("the delivery of %s (issue %d) could not be asked: %v", pr, body.Number, err)
}
state := d.State
if state == "" {
// The delivery knows nothing of it yet: not kept, so it is asked again at the next move, and its own
// events say it when they come.
continue
}
// The delivery's own time, so a transition stamped the same moment is not taken as older.
since := d.Since
if since.IsZero() {
since = t.now()
}
if _, err := t.recordFix(pr, d.heard(since), body.Number); err != nil {
return err
}
}
}
return t.afterFix(body.Number)
@@ -791,7 +794,7 @@ func (t *Tracker) Sweep() (int, error) {
named := liveFixes(is)
for _, f := range is.Fixes {
if !contains(named, f.PullRequest) || contains(is.Delivered, f.PullRequest) || !f.Merged || f.MergedAt == nil ||
reached(f.State) || f.StuckSaid || now.Sub(*f.MergedAt) < bound || now.Sub(f.Since) < bound {
reached(f.State) || f.StuckSaid || now.Sub(*f.MergedAt) < bound {
continue
}
state := f.State
@@ -320,11 +320,6 @@ func (t *Tracker) timeFix(n uint64) error {
if err != nil {
return err
}
if closedStatus(is.Status) && is.FixTime == nil {
// An issue closed before its fix was timed keeps none: measuring now would end its fix time at a delivery learned
// late ("fixed in N weeks") and could raise a false over-budget warning (issue 1950).
return nil
}
ft, _ := t.fixTimeOf(is)
if ft == nil || sameFixTime(ft, is.FixTime) {
return nil
@@ -1,179 +0,0 @@
package main
// A fix whose pull request the tracker keeps no record for (merged before the tracker heard the delivery, or linked after it
// moved) learns its merge here, so the view's `delivered` event can cover it like any recorded fix (issue 1950). The delivery
// is asked first (its `checks` carries the forge's own pull request beside its delivery); the forge is asked when the delivery
// cannot say. One that neither can say is left for the next time and logged; a pull request closed without merging becomes
// the withdrawn state the tracker already knows.
import (
"encoding/json"
"errors"
"fmt"
"strings"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
)
// pullFacts reads the forge's pull request block, as gitea_get_pull_request and mesh-delivery's checks both carry it.
type pullFacts struct {
Merged bool `json:"merged"`
State string `json:"state"`
MergeCommitSHA string `json:"merge_commit_sha"`
MergedAt *time.Time `json:"merged_at"`
}
// fromPull completes a delivery with what the forge's pull request says: whether it is merged or closed, the commit its
// merge landed as and when.
func (d *Delivery) fromPull(p pullFacts, repository string) {
d.Pulled = true
d.Merged = p.Merged
d.Closed = p.State == "closed" && !p.Merged
if d.Repository == "" {
d.Repository = strings.ToLower(repository)
}
if p.Merged {
if d.MergedAs == "" {
d.MergedAs = p.MergeCommitSHA
}
if d.MergedAt == nil {
d.MergedAt = nonZero(p.MergedAt)
}
}
}
// busPull asks the forge for one pull request (gitea_get_pull_request).
func busPull(owner string) DeliveryState {
return func(repository, number string) (Delivery, error) {
var n int
if _, err := fmt.Sscanf(number, "%d", &n); err != nil || n <= 0 {
return Delivery{}, fmt.Errorf("%q is not a pull request number", number)
}
raw, err := stdio.Ask("gitea.gitea_get_pull_request", map[string]any{"owner": owner, "repo": repository, "number": n})
if err != nil {
return Delivery{}, err
}
var out struct {
Pull *pullFacts `json:"pull"`
Answer struct {
Pull *pullFacts `json:"pull"`
} `json:"answer"`
}
if json.Unmarshal(raw, &out) != nil {
return Delivery{}, errors.New("the forge's gitea_get_pull_request is not readable")
}
p := out.Pull
if p == nil {
p = out.Answer.Pull
}
if p == nil {
return Delivery{}, errors.New("the forge's gitea_get_pull_request answered no pull request")
}
var d Delivery
d.fromPull(*p, owner+"/"+repository)
return d, nil
}
}
// learnFix gives a pull request an issue names and keeps no Fix record for its record: from the delivery's own state when it has
// one, else from the pull request itself (the delivery's, then the forge's). It answers whether a record was kept. A pull request
// still open is not kept (its own events say it when they come); one neither can say anything of is left for next time.
func (t *Tracker) learnFix(n uint64, pr string) (bool, error) {
repo, number, _ := strings.Cut(pr, "#")
var d Delivery
var derr error
if t.Deliveries != nil {
d, derr = t.Deliveries(repo, number)
}
if derr == nil && d.State != "" {
since := d.Since
if since.IsZero() {
since = t.now()
}
ch, err := t.recordFix(pr, d.heard(since), n)
return len(ch) > 0, err
}
if derr != nil || !d.Pulled {
if t.Pull == nil {
return false, derr
}
f, ferr := t.Pull(repo, number)
if ferr != nil {
if derr != nil {
return false, derr
}
t.Logf("[mesh-issues] the pull request %s (issue %d) could not be read from the forge (asked again later): %v", pr, n, ferr)
return false, nil
}
d = f
}
var h heardFix
switch {
case d.Merged:
// Kept as of now, the moment it was learned: the fix's state did not change days ago for the tracker, and the stuck
// sweep measures from it. MergedAt stays the forge's own moment.
h = heardFix{State: "merged", Merged: true, At: t.now(), MergedAt: d.MergedAt, Repository: d.Repository, MergedAs: d.MergedAs}
case d.Closed:
h = heardFix{State: "closed", Why: "closed without merging", At: t.now()}
default:
return false, nil
}
ch, err := t.recordFix(pr, h, n)
return len(ch) > 0, err
}
// learnUnheard learns, for at most most pull requests, each fix of issue n with no record; it answers whether any was kept.
func (t *Tracker) learnUnheard(n uint64, is *Issue, most int) (int, bool, error) {
asked, kept := 0, false
for _, pr := range fixPullRequests(is) {
if fixOf(is, pr) != nil {
continue
}
if asked == most {
break
}
asked++
ok, err := t.learnFix(n, pr)
if err != nil {
return asked, kept, fmt.Errorf("the delivery of %s (issue %d) could not be asked: %v", pr, n, err)
}
kept = kept || ok
}
return asked, kept, nil
}
// LearnFixes is the tool that asks again for one issue: every pull request its fixed-by names with no record is asked of the
// delivery and the forge now. It answers what each is kept as.
func (t *Tracker) LearnFixes(n uint64) (map[string]any, error) {
is, _, err := read(t.Store, n)
if err != nil {
return nil, err
}
if _, _, err := t.learnUnheard(n, is, MostSweepAsks*2); err != nil {
return nil, err
}
if err := t.afterFix(n); err != nil {
return nil, err
}
is, _, err = read(t.Store, n)
if err != nil {
return nil, err
}
out := map[string]string{}
for _, pr := range fixPullRequests(is) {
switch f := fixOf(is, pr); {
case f == nil:
out[pr] = "nothing heard: neither the delivery nor the forge said"
case withdrawn(f):
out[pr] = "withdrawn: closed without merging"
case contains(is.Delivered, pr):
out[pr] = "delivered"
case f.Merged:
out[pr] = "merged as " + shortCommit(f.MergedAs) + ", not delivered"
default:
out[pr] = f.State
}
}
return map[string]any{"number": n, "fixes": out, "delivery": DeliveryLine(is)}, nil
}
@@ -1,188 +0,0 @@
package main
import (
"encoding/json"
"errors"
"strings"
"testing"
"time"
)
// The forge's answer for novox/mesh-delivery#5, read from gitea.gitea_get_pull_request on 2026-10-27 (shortened): merged, closed,
// its merge commit and moment. mesh-delivery's checks carries the same `pull` block and no `delivery` block for it.
const forgePullMerged = `{"pull":{"base":"main","head":"feat/delivery-parts","merged":true,"merged_at":"2026-10-19T18:05:16Z",` +
`"merge_commit_sha":"1325c8b452c8fa65c478af6815887870a1f42e7b","number":5,"state":"closed"}}`
func TestAPullRequestWithNoFixRecordLearnsItsMerge(t *testing.T) {
mergedAt := time.Date(2026, 10, 19, 18, 5, 16, 0, time.UTC)
merged := Delivery{Pulled: true, Merged: true, MergedAs: "1325c8b452c8fa65c478af6815887870a1f42e7b", MergedAt: &mergedAt, Repository: "novox/mesh-catalog"}
closed := Delivery{Pulled: true, Closed: true}
open := Delivery{Pulled: true}
for _, c := range []struct {
name string
delivery Delivery
deliveryEr error
forge *Delivery
forgeEr error
want string // the delivery line after learning
wantErr bool
}{
{name: "mesh-delivery's pull says merged", delivery: merged, want: "fix merged, not delivered: mesh-catalog#50"},
{name: "mesh-delivery cannot, the forge says merged", deliveryEr: errors.New("down"), forge: &merged,
want: "fix merged, not delivered: mesh-catalog#50"},
{name: "mesh-delivery says nothing of the pull, the forge says merged", forge: &merged, want: "fix merged, not delivered: mesh-catalog#50"},
{name: "closed without merging is withdrawn", delivery: closed, want: "fix withdrawn: mesh-catalog#50 closed without merging"},
{name: "still open stays unheard", delivery: open, want: "nothing heard from the delivery of mesh-catalog#50"},
{name: "a forge that cannot say leaves it", forgeEr: errors.New("forge down"), want: "nothing heard from the delivery of mesh-catalog#50"},
{name: "neither can say", deliveryEr: errors.New("down"), forgeEr: errors.New("forge down"), want: "nothing heard from the delivery of mesh-catalog#50", wantErr: true},
} {
t.Run(c.name, func(t *testing.T) {
h := newHarness(t)
n := fixIssue(t, h, "mesh-catalog#50")
h.Deliveries = func(string, string) (Delivery, error) { return c.delivery, c.deliveryEr }
h.Pull = func(string, string) (Delivery, error) {
if c.forge != nil {
return *c.forge, nil
}
return Delivery{}, errOr(c.forgeEr)
}
_, err := h.LearnFixes(n)
if (err != nil) != c.wantErr {
t.Fatalf("error %v, wanted error %v", err, c.wantErr)
}
if v, _ := h.View(n); v.Delivery != c.want {
t.Fatalf("%q, wanted %q", v.Delivery, c.want)
}
})
}
}
func errOr(e error) error {
if e == nil {
return errors.New("the forge has no such pull request")
}
return e
}
// A fix merged before the tracker heard deliveries: the delivered event itself learns its merge and covers it, a resolved issue's
// included; and the sweep learns it with no event.
func TestAFixMergedBeforeTheTrackerHeardDeliveriesIsDeliveredByTheNextEventOfItsRepository(t *testing.T) {
mergedAs := "1325c8b452c8fa65c478af6815887870a1f42e7b"
forge := func(string, string) (Delivery, error) {
return Delivery{Pulled: true, Merged: true, MergedAs: mergedAs, Repository: "novox/mesh-catalog"}, nil
}
contains := func(_, _, commit, in string) (bool, error) {
return commit == mergedAs && in == "ffffffffffff0000", nil
}
heads := map[string]string{"novox/mesh-catalog": "ffffffffffff0000"}
delivered := "delivered to every machine it should reach"
h := newHarness(t)
n := fixIssue(t, h, "mesh-catalog#51")
h.Pull, h.Contains = forge, contains
viewSays(t, h, 30, "", heads)
if v, _ := h.View(n); v.Delivery != delivered {
t.Fatalf("the event did not cover the old fix: %q", v.Delivery)
}
if f := fixOf(get(t, h, n), "mesh-catalog#51"); f == nil || f.MergedAs != mergedAs || f.Repository != "novox/mesh-catalog" || f.MergedAt != nil && f.MergedAt.IsZero() {
t.Fatalf("fix %+v", f)
}
// A resolved issue is learned by the sweep, which then waits for the next event, not for ever.
h2 := newHarness(t)
m := fixIssue(t, h2, "mesh-catalog#52")
if _, err := h2.Move(MoveAsk{Number: m, Status: StatusResolved, FixedBy: []string{"mesh-catalog#52"}}); err != nil {
t.Fatal(err)
}
if v, _ := h2.View(m); !strings.HasPrefix(v.Delivery, "nothing heard") {
t.Fatalf("before: %q", v.Delivery)
}
h2.Pull, h2.Contains = forge, contains
if _, err := h2.Sweep(); err != nil {
t.Fatal(err)
}
if v, _ := h2.View(m); v.Delivery != "fix merged, not delivered: mesh-catalog#52" {
t.Fatalf("after the sweep: %q", v.Delivery)
}
viewSays(t, h2, 31, "", heads)
if v, _ := h2.View(m); v.Delivery != delivered {
t.Fatalf("after the event: %q", v.Delivery)
}
}
func TestTheForgesRealPullAnswerIsReadAsMerged(t *testing.T) {
var out struct {
Pull pullFacts `json:"pull"`
}
if err := json.Unmarshal([]byte(forgePullMerged), &out); err != nil {
t.Fatal(err)
}
var d Delivery
d.fromPull(out.Pull, "novox/mesh-delivery")
if !d.Pulled || !d.Merged || d.Closed || d.MergedAs != "1325c8b452c8fa65c478af6815887870a1f42e7b" ||
d.MergedAt == nil || d.MergedAt.Format(time.RFC3339) != "2026-10-19T18:05:16Z" || d.Repository != "novox/mesh-delivery" {
t.Fatalf("%+v", d)
}
}
// A merge learned late is kept as of the moment it was learned: the stuck bound runs from then, so an old merge raises no
// false warning in the sweep that learned it; and an issue closed before its fix was timed keeps no fix time when a late
// delivery covers it.
func TestALearnedOldMergeRaisesNoStuckWarningAndTimesNoClosedIssue(t *testing.T) {
h := newHarness(t)
now := clocked(h)
note := &notified{}
h.Notify = note.notify
old := now.Add(-10 * 24 * time.Hour)
mergedAs := "1325c8b452c8fa65c478af6815887870a1f42e7b"
h.Pull = func(string, string) (Delivery, error) {
return Delivery{Pulled: true, Merged: true, MergedAs: mergedAs, MergedAt: &old, Repository: "novox/mesh-catalog"}, nil
}
h.Contains = func(_, _, commit, _ string) (bool, error) { return commit == mergedAs, nil }
open := fixIssue(t, h, "mesh-catalog#60")
closed := fixIssue(t, h, "mesh-catalog#61")
if _, err := h.Move(MoveAsk{Number: closed, Status: StatusResolved, FixedBy: []string{"mesh-catalog#61"}}); err != nil {
t.Fatal(err)
}
if _, err := h.Sweep(); err != nil {
t.Fatal(err)
}
if len(note.said) != 0 || noted(get(t, h, open), "Stuck: the fix") {
t.Fatalf("a learned old merge was said stuck in the sweep that learned it: %q", note.said)
}
if f := fixOf(get(t, h, open), "mesh-catalog#60"); f == nil || f.MergedAt == nil || !f.MergedAt.Equal(old) {
t.Fatalf("the forge's merge moment is kept: %+v", f)
}
*now = now.Add(3 * time.Hour)
if _, err := h.Sweep(); err != nil || len(note.said) != 1 {
t.Fatalf("not delivered 3 hours after it was learned: %q %v", note.said, err)
}
viewSays(t, h, 40, "", map[string]string{"novox/mesh-catalog": "ffffffffffff0000"})
if v, _ := h.View(closed); v.Delivery != "delivered to every machine it should reach" || get(t, h, closed).FixTime != nil {
t.Fatalf("closed issue: %q, fix time %+v", v.Delivery, get(t, h, closed).FixTime)
}
}
// Pull requests that cannot be learned (still open) do not starve the others: the one asked longest ago goes first.
func TestTheSweepAsksTheLeastRecentlyAskedPullRequestFirst(t *testing.T) {
h := newHarness(t)
var stuck []uint64
for i := 0; i < MostSweepAsks+2; i++ {
stuck = append(stuck, fixIssue(t, h, "mesh-catalog#"+itoa(70+i)))
}
last := fixIssue(t, h, "mesh-catalog#99")
h.Pull = func(_, number string) (Delivery, error) {
if number == "99" {
return Delivery{Pulled: true, Merged: true, MergedAs: "1325c8b452c8fa65c478af6815887870a1f42e7b"}, nil
}
return Delivery{Pulled: true}, nil // still open
}
for i := 0; i < 4; i++ {
if _, err := h.Sweep(); err != nil {
t.Fatal(err)
}
}
if v, _ := h.View(last); !strings.HasPrefix(v.Delivery, "fix merged") {
t.Fatalf("starved by pull requests that cannot be learned: %q (%d of them)", v.Delivery, len(stuck))
}
}
+1 -22
View File
@@ -23,7 +23,7 @@ const Seat = "issue-tracker"
// Verbs are the seat's verbs, as the manifest declares and claims them.
var Verbs = []string{"open", "get", "list", "move", "note", "link", "milestones", "milestone-set", "milestone-move", "import",
"groom", "set", "tracking", "reserve", "fix-times", "learn-fixes"}
"groom", "set", "tracking", "reserve", "fix-times"}
func logf(format string, a ...any) { fmt.Fprintf(os.Stderr, format+"\n", a...) }
@@ -105,9 +105,6 @@ func main() {
t.Deliveries = func(repository, number string) (Delivery, error) {
return busDeliveryState(forgeOwner())(repository, number)
}
t.Pull = func(repository, number string) (Delivery, error) {
return busPull(forgeOwner())(repository, number)
}
t.Approval = busApproval(forgeOwner)
t.Contains = busContains
if len(os.Args) > 1 && os.Args[1] == "dry-run" {
@@ -924,23 +921,5 @@ func Tools(t *Tracker) []stdio.Tool {
}
return t.FixTimes(int(days))
}},
{Name: Seat + ".learn-fixes",
Description: "Ask again for the fixes of one issue the tracker keeps no record of: each pull request its fixed-by names that " +
"the delivery was never heard of (merged before the tracker heard deliveries, or linked after) is asked of mesh-delivery " +
"and, when it cannot say, of the forge, and kept as merged (with the commit its merge landed as) or as withdrawn when it " +
"was closed without merging; a merge is then matched to the next delivered revision of its repository. Answers what each " +
"pull request is kept as and the issue's delivery line. A pull request still open, or one neither can say, stays unheard.",
Input: schema([]string{"number"}, map[string]any{"number": number("the issue's number")}),
Run: func(m map[string]any) (any, error) {
a, err := readArgs("learn-fixes", m, "number")
if err != nil {
return nil, err
}
n, err := a.number("number", true)
if err != nil {
return nil, err
}
return t.LearnFixes(n)
}},
}
}
@@ -112,7 +112,7 @@ func TestItDeclaresAndHoldsTheIssueTrackerSeatOnceForTheMesh(t *testing.T) {
// the import and a reservation read the record through the records module (issue 424).
if !reflect.DeepEqual(m.Consumes, []string{"mesh-issues." + EventOpened, "mesh-issues." + EventMoved, "mesh-delivery-gate.transition", DeliveredEvent}) ||
!reflect.DeepEqual(m.Invokes, []string{"claude-code.claude_code_judge", "seat:mesh-delivery.checks", "seat:operator-channel.notify",
"gitea.gitea_pull_reviews", "gitea.gitea_contains", "gitea.gitea_get_pull_request", "records.records_status", "records.records_list", "records.records_read", "mesh-knowledge.knowledge_highest"}) {
"gitea.gitea_pull_reviews", "gitea.gitea_contains", "records.records_status", "records.records_list", "records.records_read", "mesh-knowledge.knowledge_highest"}) {
t.Fatalf("consumes %v, invokes %v", m.Consumes, m.Invokes)
}
// The tools bundle, and the archive the build packs files/ into for the templates (novox/hq ADR 0349 §2).
@@ -37,11 +37,6 @@ type Tracker struct {
Judge Judgement
// Deliveries answers a pull request's delivery (ADR 0279 §3, ADR 0281); nil asks nothing.
Deliveries DeliveryState
// Pull reads one pull request from the forge (gitea_get_pull_request) for a fix the delivery cannot say anything of; nil asks nothing.
Pull DeliveryState
// askedAt is when each pull request without a record was last asked by the sweep, so the least recently asked goes first.
askedMu sync.Mutex
askedAt map[string]time.Time
// Contains asks the forge whether a commit is contained in another (gitea_contains); nil asks nothing, and a delivered
// revision then marks only the fixes it names (OnDelivered).
Contains func(owner, repo, commit, in string) (bool, error)
+2 -5
View File
@@ -21,8 +21,7 @@
"set",
"tracking",
"reserve",
"fix-times",
"learn-fixes"
"fix-times"
]
}
],
@@ -45,8 +44,7 @@
"set",
"tracking",
"reserve",
"fix-times",
"learn-fixes"
"fix-times"
]
}
],
@@ -76,7 +74,6 @@
"seat:operator-channel.notify",
"gitea.gitea_pull_reviews",
"gitea.gitea_contains",
"gitea.gitea_get_pull_request",
"records.records_status",
"records.records_list",
"records.records_read",