The gate holds no merge for a check: the hold, the turns, their stalled line and tools, and check-gates-merge go; the merge judges the operator's Approve alone (milestone 47/G, W11.14)
ADR 0661: the check gates the ask, in one place (askWaits); once the Approve covers the heads (approvedWaits) no verdict, carry, composed check or required status is judged, an Approve given before a trunk move merges onto the new trunk, and a failure after the Approve is said once on the pull request and as a stalled line naming the merge it follows. A check failing before the answer withdraws the ask. Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ (Joined from 43696644.) Builds: design 47 G §278, test 52 (GATE10) Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
@@ -36,15 +36,6 @@ func TestTheBoundsAreSettingsWithTheirDefaults(t *testing.T) {
|
||||
if h.refusalFloor() != 10*time.Minute || h.authorBound() != 2*time.Hour {
|
||||
t.Fatalf("empty file: floor %s, author %s", h.refusalFloor(), h.authorBound())
|
||||
}
|
||||
if !h.checkGates() {
|
||||
t.Fatalf("check-gates-merge unset reads false; the default is true")
|
||||
}
|
||||
if s, err = read(`{"check-gates-merge": false}`); err != nil || s.CheckGatesMerge == nil || *s.CheckGatesMerge || h.checkGates() {
|
||||
t.Fatalf("check-gates-merge false: %+v %v, gates %v", s, err, h.checkGates())
|
||||
}
|
||||
if _, err := read(`{"check-gates-merge": "no"}`); err == nil {
|
||||
t.Fatalf("a check-gates-merge that is not true or false was taken")
|
||||
}
|
||||
if _, err := read(`{"author-new-head-minutes": 0}`); err == nil {
|
||||
t.Fatalf("a bound of zero minutes was taken")
|
||||
}
|
||||
@@ -139,9 +130,9 @@ func TestAnyOtherRefusalIsStalledAtOnce(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// rejectedAfterItsApprove is a delivery approved and then rejected: its check failed after the ask (as in events_test.go),
|
||||
// rejectedBeforeItsAnswer is a delivery approved and then rejected: its check failed after the ask (as in events_test.go),
|
||||
// its pull request's description naming who pushed it when pushedBy is given.
|
||||
func rejectedAfterItsApprove(t *testing.T, pushedBy string) (*world, string) {
|
||||
func rejectedBeforeItsAnswer(t *testing.T, pushedBy string) (*world, string) {
|
||||
t.Helper()
|
||||
w := newWorld(t)
|
||||
w.op.auto = false
|
||||
@@ -163,7 +154,11 @@ func rejectedAfterItsApprove(t *testing.T, pushedBy string) (*world, string) {
|
||||
d.Check = nil
|
||||
w.h.mu.Unlock()
|
||||
hear(t, w, shimCheckedKey, judgedAt("novox/app", 7, readyHead, "fail", "t1t1t1t1t1t1"))
|
||||
approve(w)
|
||||
w.settleAll()
|
||||
// A check that fails before the operator answers withdraws the ask (novox/hq ADR 0661 §5, ADR 0441 §1).
|
||||
if open := w.op.open(); len(open) != 0 {
|
||||
t.Fatalf("the ask stands after its check failed: %v", open)
|
||||
}
|
||||
w.h.React()
|
||||
if w.state(id) != Rejected {
|
||||
t.Fatalf("not rejected: %s", w.state(id))
|
||||
@@ -175,7 +170,7 @@ func rejectedAfterItsApprove(t *testing.T, pushedBy string) (*world, string) {
|
||||
// with no new head, in words naming its author (its branch, and the session that pushed it) and needing a new head —
|
||||
// never "stop it", never "the operator's".
|
||||
func TestARejectedDeliveryIsItsAuthorsForTwoHours(t *testing.T) {
|
||||
w, id := rejectedAfterItsApprove(t, "quiet-deliveries@shanks")
|
||||
w, id := rejectedBeforeItsAnswer(t, "quiet-deliveries@shanks")
|
||||
said := false
|
||||
for _, c := range w.forge.comments {
|
||||
said = said || strings.Contains(c, "not merged")
|
||||
@@ -208,7 +203,7 @@ func TestARejectedDeliveryIsItsAuthorsForTwoHours(t *testing.T) {
|
||||
|
||||
// ADR 0505 §5: with no Pushed-by line, the branch alone names its author.
|
||||
func TestARejectedDeliveryWithNoPushedByLineIsNamedByItsBranch(t *testing.T) {
|
||||
w, id := rejectedAfterItsApprove(t, "")
|
||||
w, id := rejectedBeforeItsAnswer(t, "")
|
||||
w.later(2*time.Hour + time.Minute)
|
||||
if got := linesFor(w, id); len(got) != 1 || got[0].Author != "the author of feat/x" {
|
||||
t.Fatalf("not named by its branch: %+v", got)
|
||||
@@ -217,7 +212,7 @@ func TestARejectedDeliveryWithNoPushedByLineIsNamedByItsBranch(t *testing.T) {
|
||||
|
||||
// ADR 0505 §5: a new head within the two hours ends it: the rejected head is never the operator's.
|
||||
func TestANewHeadWithinTwoHoursKeepsTheRejectedHeadFromTheOperator(t *testing.T) {
|
||||
w, id := rejectedAfterItsApprove(t, "quiet-deliveries@shanks")
|
||||
w, id := rejectedBeforeItsAnswer(t, "quiet-deliveries@shanks")
|
||||
w.later(time.Hour)
|
||||
w.h.PullUpdated(pr("novox/app", 7, pushed, "feat/x", "src/a.go"))
|
||||
w.settleAll()
|
||||
|
||||
@@ -1,244 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// One check per delivery (novox/hq ADR 0548), the gate's part: nothing merges into a repository while a check of other heads
|
||||
// judges it (§2); an approved merge holds a turn on its repositories (§3); a held merge past its bound is a stalled line, read
|
||||
// and handed by two tools (§4); an approved delivery whose trunk moved merges on its carried verdict with no second ask (§8).
|
||||
|
||||
const otherHead = "c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3"
|
||||
|
||||
// approvedLone is a lone delivery into oneRepo whose check passed, approved by the operator (the world approves at once).
|
||||
func approvedLone(w *world) string {
|
||||
w.t.Helper()
|
||||
p := pr(oneRepo, 1, oneHead, "feat/one")
|
||||
requiresCheck(w, p)
|
||||
w.h.PullUpdated(p)
|
||||
putCheckerVerdict(w, "pass", headOf(oneRepo, 1, oneHead))
|
||||
w.h.CommitStatus(checkStatus(oneRepo, oneHead, "success", oneTrunk))
|
||||
return IDOf(oneRepo, oneHead)
|
||||
}
|
||||
|
||||
// memTurns is the bucket turns in memory; it logs each write with the merges the fake bridge had made by then.
|
||||
type memTurns struct {
|
||||
mu sync.Mutex
|
||||
m map[string]Turn
|
||||
log []string
|
||||
merges func() int
|
||||
}
|
||||
|
||||
func newMemTurns(w *world) *memTurns {
|
||||
return &memTurns{m: map[string]Turn{}, merges: func() int { return len(w.forge.merges) }}
|
||||
}
|
||||
|
||||
func (f *memTurns) All() (map[string]Turn, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
out := map[string]Turn{}
|
||||
for k, v := range f.m {
|
||||
out[k] = v
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (f *memTurns) Put(t Turn) error {
|
||||
if _, err := EncodeTurn(t); err != nil {
|
||||
return err
|
||||
}
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.m[t.Repository] = t
|
||||
f.log = append(f.log, fmt.Sprintf("put %s after %d merges", t.Repository, f.merges()))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *memTurns) Delete(repository string) error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
delete(f.m, repository)
|
||||
f.log = append(f.log, fmt.Sprintf("delete %s after %d merges", repository, f.merges()))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *memTurns) said() string { f.mu.Lock(); defer f.mu.Unlock(); return strings.Join(f.log, "; ") }
|
||||
|
||||
func TestOneCheckPerDeliveryAtTheGate(t *testing.T) {
|
||||
for name, run := range map[string]func(t *testing.T){
|
||||
// §2: a merge into a repository a running verdict names is not written to merges, and is written on that verdict's pass.
|
||||
"an approved merge waits for a running check on its repository and merges on its end": func(t *testing.T) {
|
||||
w := newWorld(t)
|
||||
putCheckerVerdict(w, "running", headOf(oneRepo, 9, otherHead))
|
||||
id := approvedLone(w)
|
||||
w.settleAll()
|
||||
if w.op.count() != 1 || len(w.forge.merges) != 0 {
|
||||
t.Fatalf("asked %d times, merges %v: want the Approve asked and the merge held", w.op.count(), w.forge.merges)
|
||||
}
|
||||
d := w.delivery(id)
|
||||
if d.Ready == nil || !strings.Contains(d.Ready.Waits, "waits for the check of g") || !strings.Contains(d.Ready.Waits, oneRepo) ||
|
||||
d.Ready.HeldSince == nil {
|
||||
t.Fatalf("the merge does not say what it waits for: %+v", d.Ready)
|
||||
}
|
||||
// §9: deliveries says what it waits for.
|
||||
for _, l := range w.h.Deliveries("", "", "", false) {
|
||||
if l.ID == id && !strings.Contains(l.Waits, "approved; waits for the check of g") {
|
||||
t.Errorf("deliveries says it waits for %q", l.Waits)
|
||||
}
|
||||
}
|
||||
// §4: past 30 minutes the held merge is a stalled line naming the check it waits for and since when.
|
||||
for _, l := range w.h.Stalled() {
|
||||
if l.ID == id {
|
||||
t.Fatalf("a stalled line within the bound: %+v", l)
|
||||
}
|
||||
}
|
||||
w.later(turnBound + time.Minute)
|
||||
w.settleAll()
|
||||
var line *StalledLine
|
||||
for _, l := range w.h.Stalled() {
|
||||
if l.ID == id {
|
||||
line = &l
|
||||
}
|
||||
}
|
||||
if line == nil || line.Bound != "30m0s" || !strings.Contains(line.Says, "waits for the check of g") || !strings.Contains(line.Says, "held since") ||
|
||||
!strings.Contains(line.Says, "checker_status") || !strings.Contains(line.H2, "delivery_hand_now") {
|
||||
t.Fatalf("the stalled line of the held merge = %+v", line)
|
||||
}
|
||||
putCheckerVerdict(w, "pass", headOf(oneRepo, 9, otherHead))
|
||||
w.settleAll()
|
||||
if len(w.forge.merges) != 1 || w.op.count() != 1 {
|
||||
t.Fatalf("after the check's end: merges %v, asked %d times; want merged with no second ask", w.forge.merges, w.op.count())
|
||||
}
|
||||
},
|
||||
"a running check on another repository holds nothing": func(t *testing.T) {
|
||||
w := newWorld(t)
|
||||
putCheckerVerdict(w, "running", headOf(oneRepo2, 9, otherHead))
|
||||
approvedLone(w)
|
||||
w.settleAll()
|
||||
if len(w.forge.merges) != 1 {
|
||||
t.Fatalf("merges %v: a check of another repository held the merge (%s)", w.forge.merges, w.logs())
|
||||
}
|
||||
},
|
||||
"the delivery's own running verdict does not hold it": func(t *testing.T) {
|
||||
w := newWorld(t)
|
||||
w.op.auto = false
|
||||
id := approvedLone(w)
|
||||
w.settleAll()
|
||||
putCheckerVerdict(w, "running", headOf(oneRepo, 1, oneHead)) // its own heads judged again (a carry)
|
||||
for _, k := range w.op.open() {
|
||||
w.op.answer(k, OptionApprove)
|
||||
}
|
||||
w.settleAll()
|
||||
if d := w.delivery(id); len(w.forge.merges) != 1 || d.Ready == nil || d.Ready.HeldSince != nil {
|
||||
t.Fatalf("merges %v, run %+v: its own check held it", w.forge.merges, d.Ready)
|
||||
}
|
||||
},
|
||||
// §3: the turn is written before the verdicts are read for the merge, and deleted after the last merge.
|
||||
"a lone merge's turn is written before it is handed and deleted once it merged": func(t *testing.T) {
|
||||
w := newWorld(t)
|
||||
turns := newMemTurns(w)
|
||||
w.h.Turns = turns
|
||||
approvedLone(w)
|
||||
w.settleAll()
|
||||
if len(w.forge.merges) != 1 || turns.said() != "put "+oneRepo+" after 0 merges; delete "+oneRepo+" after 1 merges" {
|
||||
t.Fatalf("merges %v; the turns were %q", w.forge.merges, turns.said())
|
||||
}
|
||||
},
|
||||
"a group's turns stand on every member's repository until its last member merged": func(t *testing.T) {
|
||||
w := newWorld(t)
|
||||
turns := newMemTurns(w)
|
||||
w.h.Turns = turns
|
||||
a, b := pr(oneRepo, 1, oneHead, "feat/two"), pr(oneRepo2, 2, oneHead2, "feat/two")
|
||||
requiresCheck(w, a, b)
|
||||
w.h.PullUpdated(a)
|
||||
w.h.PullUpdated(b)
|
||||
w.h.wantForge()
|
||||
putCheckerVerdict(w, "pass", headOf(oneRepo, 1, oneHead), headOf(oneRepo2, 2, oneHead2))
|
||||
w.h.CommitStatus(checkStatus(oneRepo, oneHead, "success", oneTrunk))
|
||||
w.h.CommitStatus(checkStatus(oneRepo2, oneHead2, "success", oneTrunk))
|
||||
for range 3 {
|
||||
w.settleAll()
|
||||
}
|
||||
want := "put " + oneRepo2 + " after 0 merges; put " + oneRepo + " after 0 merges; delete " + oneRepo2 + " after 2 merges; delete " + oneRepo + " after 2 merges"
|
||||
if len(w.forge.merges) != 2 || turns.said() != want {
|
||||
t.Fatalf("merges %v; the turns were %q, want %q", w.forge.merges, turns.said(), want)
|
||||
}
|
||||
},
|
||||
// §8: an approved delivery whose trunk moved goes back to proposed, waits for its carried verdict rather than being
|
||||
// handed on the old one, and merges on it with no second ask.
|
||||
"an approved merge whose trunk moved merges on its carried verdict with no second ask": func(t *testing.T) {
|
||||
w := newWorld(t)
|
||||
putCheckerVerdict(w, "running", headOf(oneRepo, 9, otherHead)) // holds the approved merge (§2)
|
||||
id := approvedLone(w)
|
||||
w.settleAll()
|
||||
if w.op.count() != 1 || len(w.forge.merges) != 0 {
|
||||
t.Fatalf("asked %d times, merges %v; want approved and held", w.op.count(), w.forge.merges)
|
||||
}
|
||||
w.h.PullMerged(merged(oneRepo, 5, otherHead, oneTrunk2)) // a merge at the forge moves the trunk
|
||||
putCheckerVerdict(w, "pass", headOf(oneRepo, 9, otherHead))
|
||||
w.settleAll()
|
||||
if len(w.forge.merges) != 0 {
|
||||
t.Fatalf("merges %v: handed on a verdict judged on the old trunk", w.forge.merges)
|
||||
}
|
||||
d := w.delivery(id)
|
||||
if d.Ready == nil || !strings.Contains(d.Ready.Waits, "being carried") {
|
||||
t.Fatalf("after the trunk moved it waits for %q, want its verdict carried", d.Ready.Waits)
|
||||
}
|
||||
// The checker's carried line: the head on the new trunk, carried from the old one.
|
||||
carried := checkStatus(oneRepo, oneHead, "success", oneTrunk2)
|
||||
carried.Description = "pass " + oneHead[:12] + " on " + oneTrunk2[:12] + " | carried from " + oneTrunk[:12] + " | unit 0 run, 4 kept"
|
||||
if head, trunk := checkLine(carried.Description); head != oneHead[:12] || trunk != oneTrunk2[:12] {
|
||||
t.Fatalf("the carried line reads as %s on %s", head, trunk)
|
||||
}
|
||||
if head, trunk := checkLine("failed: a test"); head != "" || trunk != "" {
|
||||
t.Fatalf("a line that names no commits gave %q %q", head, trunk)
|
||||
}
|
||||
putCheckerVerdict(w, "pass", headOf(oneRepo, 1, oneHead))
|
||||
w.h.CommitStatus(carried)
|
||||
w.settleAll()
|
||||
if w.op.count() != 1 {
|
||||
t.Errorf("the Approve was asked %d times, want once: it stands through the carry", w.op.count())
|
||||
}
|
||||
if d := w.delivery(id); d.Check == nil || d.Check.Trunk != oneTrunk2[:12] || len(w.forge.merges) != 1 {
|
||||
t.Errorf("after the carry: verdict %+v, merges %v; want merged on the carried verdict", d.Check, w.forge.merges)
|
||||
}
|
||||
},
|
||||
// §4: delivery_turns reads every turn with the check it waits for; delivery_hand_now hands a held merge at once on the
|
||||
// operator's word, and refuses one no check holds.
|
||||
"the turns are read, and a held merge is handed now on the operator's word": func(t *testing.T) {
|
||||
w := newWorld(t)
|
||||
w.h.Turns = newMemTurns(w)
|
||||
putCheckerVerdict(w, "running", headOf(oneRepo, 9, otherHead))
|
||||
id := approvedLone(w)
|
||||
w.settleAll()
|
||||
read := w.h.TurnsRead()
|
||||
lines := read["turns"].([]TurnLine)
|
||||
if len(lines) != 1 || lines[0].Repository != oneRepo || len(lines[0].Turn.Waiting) != 1 || lines[0].Turn.Waiting[0].WaitsFor == "" ||
|
||||
!strings.Contains(lines[0].Waits["feat/one"].(string), "the check g.") || read["written"] != true {
|
||||
t.Fatalf("delivery_turns = %+v", read)
|
||||
}
|
||||
if _, err := w.h.HandNow(id, ""); err == nil || !strings.Contains(err.Error(), "why") {
|
||||
t.Errorf("a hand without the operator's why: %v", err)
|
||||
}
|
||||
if _, err := w.h.HandNow(IDOf(oneRepo2, oneHead2), "x"); err == nil {
|
||||
t.Error("an unknown delivery was handed")
|
||||
}
|
||||
if _, err := w.h.HandNow(id, "the operator, on telegram: merge it now"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
w.settleAll()
|
||||
if len(w.forge.merges) != 1 || w.delivery(id).Ready.HandNow == "" {
|
||||
t.Fatalf("merges %v after the hand", w.forge.merges)
|
||||
}
|
||||
if _, err := w.h.HandNow(id, "again"); err == nil {
|
||||
t.Error("a merged delivery was handed again")
|
||||
}
|
||||
},
|
||||
} {
|
||||
t.Run(name, run)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// No check gates a merge (novox/hq ADR 0661), the gate's part: no approved merge waits for another delivery's running check,
|
||||
// and no turn or stalled line holds it (§1); the merge judges the operator's Approve alone, so an Approve given before a trunk
|
||||
// move merges onto the new trunk with no wait for its carry (§3); a failure after the Approve is said once on the pull
|
||||
// request and as a stalled line naming the merge it follows, and refuses nothing (§5).
|
||||
|
||||
const otherHead = "c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3"
|
||||
|
||||
// approvedLone is a lone delivery into oneRepo whose check passed, approved by the operator (the world approves at once).
|
||||
func approvedLone(w *world) string {
|
||||
w.t.Helper()
|
||||
p := pr(oneRepo, 1, oneHead, "feat/one")
|
||||
requiresCheck(w, p)
|
||||
w.h.PullUpdated(p)
|
||||
putCheckerVerdict(w, "pass", headOf(oneRepo, 1, oneHead))
|
||||
w.h.CommitStatus(checkStatus(oneRepo, oneHead, "success", oneTrunk))
|
||||
return IDOf(oneRepo, oneHead)
|
||||
}
|
||||
|
||||
// sayingFailed is the forge's word that oneHead's check failed on the trunk commit given (a carry's line).
|
||||
func sayingFailed(trunk string) ForgeStatusEvent {
|
||||
e := checkStatus(oneRepo, oneHead, "failure", trunk)
|
||||
e.Description = "failed " + oneHead[:12] + " on " + trunk[:12] + " | a test failed"
|
||||
return e
|
||||
}
|
||||
|
||||
func lineOf(w *world, id string) *StalledLine {
|
||||
for _, l := range w.h.Stalled() {
|
||||
if l.ID == id {
|
||||
return &l
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestNoCheckGatesAMergeAtTheGate(t *testing.T) {
|
||||
for name, run := range map[string]func(t *testing.T){
|
||||
// §1: a running check of other heads on the same repository holds nothing, and no stalled line is raised for it.
|
||||
"an approved merge into a repository under a running check merges at once": func(t *testing.T) {
|
||||
w := newWorld(t)
|
||||
putCheckerVerdict(w, "running", headOf(oneRepo, 9, otherHead))
|
||||
id := approvedLone(w)
|
||||
w.settleAll()
|
||||
if w.op.count() != 1 || len(w.forge.merges) != 1 {
|
||||
t.Fatalf("asked %d times, merges %v: want asked once and merged at once (%s)", w.op.count(), w.forge.merges, w.logs())
|
||||
}
|
||||
w.h.PullMerged(merged(oneRepo, 1, oneHead, strings.Repeat("c", 40))) // the seat git says the merge too
|
||||
w.later(time.Hour)
|
||||
w.settleAll()
|
||||
if l := lineOf(w, id); l != nil {
|
||||
t.Fatalf("a stalled line for a merge a running check once held: %+v", l)
|
||||
}
|
||||
},
|
||||
"the delivery's own running verdict does not hold it": func(t *testing.T) {
|
||||
w := newWorld(t)
|
||||
w.op.auto = false
|
||||
id := approvedLone(w)
|
||||
w.settleAll()
|
||||
putCheckerVerdict(w, "running", headOf(oneRepo, 1, oneHead)) // its own heads judged again (a carry)
|
||||
for _, k := range w.op.open() {
|
||||
w.op.answer(k, OptionApprove)
|
||||
}
|
||||
w.settleAll()
|
||||
if d := w.delivery(id); len(w.forge.merges) != 1 || d.Ready == nil {
|
||||
t.Fatalf("merges %v, run %+v: its own check held it", w.forge.merges, d.Ready)
|
||||
}
|
||||
},
|
||||
// §3 (and ADR 0548 §8): the ask stands through a trunk move; the Approve given then merges onto the new trunk on the
|
||||
// verdict judged against the old one, with no wait for its carry and no second ask.
|
||||
"an Approve given after a trunk move merges at once, its carry after": func(t *testing.T) {
|
||||
w := newWorld(t)
|
||||
w.op.auto = false
|
||||
id := approvedLone(w)
|
||||
w.settleAll()
|
||||
if w.op.count() != 1 || len(w.forge.merges) != 0 {
|
||||
t.Fatalf("asked %d times, merges %v: want asked and not merged yet", w.op.count(), w.forge.merges)
|
||||
}
|
||||
w.h.PullMerged(merged(oneRepo, 5, otherHead, oneTrunk2)) // a merge at the forge moves the trunk
|
||||
w.settleAll()
|
||||
for _, k := range w.op.open() {
|
||||
w.op.answer(k, OptionApprove)
|
||||
}
|
||||
w.settleAll()
|
||||
d := w.delivery(id)
|
||||
if len(w.forge.merges) != 1 || w.op.count() != 1 || d.State != Ready && d.State != Merged {
|
||||
t.Fatalf("merges %v, asked %d times, %s (waits %q): want merged at once on the old verdict (%s)", w.forge.merges,
|
||||
w.op.count(), d.State, d.Ready.Waits, w.logs())
|
||||
}
|
||||
},
|
||||
// §5: a failure after the Approve, a carry's, is said once on the pull request and as one stalled line naming the merge;
|
||||
// it refuses nothing, and a later pass clears the line.
|
||||
"a failure after the Approve is said once and refuses nothing": func(t *testing.T) {
|
||||
w := newWorld(t)
|
||||
id := approvedLone(w)
|
||||
w.settleAll()
|
||||
if len(w.forge.merges) != 1 {
|
||||
t.Fatalf("merges %v: want merged at once", w.forge.merges)
|
||||
}
|
||||
before := len(w.forge.comments)
|
||||
w.h.CommitStatus(sayingFailed(oneTrunk2))
|
||||
w.settleAll()
|
||||
w.h.CommitStatus(sayingFailed(oneTrunk2))
|
||||
w.settleAll()
|
||||
said := 0
|
||||
for _, c := range w.forge.comments[before:] {
|
||||
if strings.Contains(c, "failed after the Approve") {
|
||||
said++
|
||||
}
|
||||
}
|
||||
if said != 1 {
|
||||
t.Fatalf("said %d times on the pull request, want once: %v", said, w.forge.comments[before:])
|
||||
}
|
||||
l := lineOf(w, id)
|
||||
if l == nil || !strings.Contains(l.Says, "after the operator's Approve") || !strings.Contains(l.Says, oneHead[:12]) {
|
||||
t.Fatalf("the stalled line = %+v", l)
|
||||
}
|
||||
if d := w.delivery(id); d.State == Rejected || len(w.forge.merges) != 1 {
|
||||
t.Fatalf("%s, merges %v: the failure refused the approved merge", d.State, w.forge.merges)
|
||||
}
|
||||
w.h.CommitStatus(checkStatus(oneRepo, oneHead, "success", oneTrunk2))
|
||||
w.settleAll()
|
||||
if l := lineOf(w, id); l != nil {
|
||||
t.Fatalf("a later pass left the stalled line: %+v", l)
|
||||
}
|
||||
},
|
||||
} {
|
||||
t.Run(name, run)
|
||||
}
|
||||
}
|
||||
@@ -338,9 +338,9 @@ func (h *Holder) endStaleAsks() {
|
||||
h.cancels = append(h.cancels, h.dropAsk(r.AskID, "its head "+shortOf(d.Commit, 12)+" is no longer the pull request's head, or the pull request closed"))
|
||||
case !h.headsHold(d, r):
|
||||
h.cancels = append(h.cancels, h.dropAsk(r.AskID, "a head it covers moved on: the heads it covers are no longer the group's heads"))
|
||||
case r.Approved == nil && r.AskID != "" && r.AskCheck != "" && !h.checkGates() && h.checkMoved(d, r.AskCheck):
|
||||
// The check gates no merge, so the ask was made before its verdict: once the check passes or fails (or a pass it
|
||||
// named is gone), the ask is asked again in words that say so (issue 1733). Running and not started say the same.
|
||||
case r.Approved == nil && r.AskID != "" && r.AskCheck != "" && h.checkMoved(d, r.AskCheck):
|
||||
// The check gates the ask, so it was made on a pass: once that pass is gone (a failure, or a check of these heads
|
||||
// running again), the ask not yet answered is withdrawn in words that say so (novox/hq ADR 0661 §5, ADR 0441 §1).
|
||||
h.cancels = append(h.cancels, h.withdrawAsk(r.AskID, "the check at its heads is now: "+h.checkAt(h.askMembers(d)).Words))
|
||||
case r.Approved == nil && r.AskID != "" && !r.AskedAt.IsZero() && !h.Now().Before(r.AskedAt.Add(AskLasts)):
|
||||
// The ask lapsed at the router with nobody's answer, and nothing in the gate's inputs says so: it is taken away and
|
||||
|
||||
@@ -225,9 +225,6 @@ func (h *Holder) nextStall(now time.Time) time.Time {
|
||||
consider(r.RefusedSince.Add(h.answeredRefusalBound(d.Repository)))
|
||||
consider(r.RefusedSince.Add(outOfDateFor))
|
||||
}
|
||||
if r.HeldSince != nil {
|
||||
consider(r.HeldSince.Add(turnBound))
|
||||
}
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
@@ -67,9 +67,7 @@ func (h *Holder) CommitStatus(e ForgeStatusEvent) {
|
||||
h.takeCheckStatus(e.Owner+"/"+e.Repo, e.SHA, e.State, e.Description)
|
||||
}
|
||||
if e.State != "success" {
|
||||
if h.checkGates() {
|
||||
h.withdrawForStatus(e) // while the check gates no merge, its status is shown and takes back no ask
|
||||
}
|
||||
h.withdrawForStatus(e) // a status not a success takes back an ask not yet answered (novox/hq ADR 0661 §5)
|
||||
return
|
||||
}
|
||||
repository := strings.ToLower(e.Owner + "/" + e.Repo)
|
||||
|
||||
@@ -196,7 +196,7 @@ func TestNoWaitReadsADescription(t *testing.T) {
|
||||
w.h.mu.Lock()
|
||||
got := []string{state, why}
|
||||
for _, d := range ds {
|
||||
got = append(got, w.h.readyWaits(d), w.h.ungatedWaits(d))
|
||||
got = append(got, w.h.askWaits(d), w.h.approvedWaits(d))
|
||||
}
|
||||
w.h.mu.Unlock()
|
||||
if i == 0 {
|
||||
|
||||
@@ -43,14 +43,24 @@ const (
|
||||
// conflict); empty when all hold. forge says the reason is the forge's merge answer rather than a status. An error is a
|
||||
// read that could not be made: the member then counts as not mergeable.
|
||||
func (h *Holder) notASuccess(m memberRead) (says string, forge bool, err error) {
|
||||
return h.memberRead(m, true)
|
||||
}
|
||||
|
||||
// notMergeableNow is notASuccess at the merge: the head the Approve covers and the forge's merge answer, and no required
|
||||
// status read again, for the merge judges the operator's Approve alone (novox/hq ADR 0661 §4).
|
||||
func (h *Holder) notMergeableNow(m memberRead) (says string, forge bool, err error) {
|
||||
return h.memberRead(m, false)
|
||||
}
|
||||
|
||||
func (h *Holder) memberRead(m memberRead, statuses bool) (says string, forge bool, err error) {
|
||||
fc, err := h.Forge.Statuses(m.owner, m.repo, m.number, m.head, m.base)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
if fc.Merge.Mergeable == nil {
|
||||
if statuses && fc.Merge.Mergeable == nil {
|
||||
return "", false, fmt.Errorf("the forge could not say whether %s may merge: %s", m.id, fc.Merge.Says)
|
||||
}
|
||||
if !*fc.Merge.Mergeable {
|
||||
if statuses && !*fc.Merge.Mergeable {
|
||||
var parts []string
|
||||
for _, b := range fc.Merge.Blocking {
|
||||
parts = append(parts, fmt.Sprintf("%s (%s)", b.Context, b.State))
|
||||
@@ -82,8 +92,8 @@ func (h *Holder) unmergedLive(g *Group) []*Delivery {
|
||||
return out
|
||||
}
|
||||
|
||||
// groupMergeable reads every member's required statuses, its head and the forge's merge answer at the head the Approve
|
||||
// covers, and says whether all hold: no member is merged unless every member merges now (issue 1275, 2026-10-26: two of
|
||||
// groupMergeable reads every member's head and the forge's merge answer at the head the Approve covers, no required status
|
||||
// (novox/hq ADR 0661 §4), and says whether all hold: no member is merged unless every member merges now (issue 1275, 2026-10-26: two of
|
||||
// three merged and the third refused). When one does not, or cannot be read, nothing is handed to the bridge: the group is
|
||||
// held, said urgent (ADR 0441 §2, §4), and the target's turn is given back so the next event that decides whether it may
|
||||
// merge asks it again. A read that could not be made is a failed ask, tried again after askAgainAfter; the forge saying a
|
||||
@@ -97,7 +107,7 @@ func (h *Holder) groupMergeable(gid, target string, reads []memberRead) bool {
|
||||
}
|
||||
}
|
||||
for _, m := range reads {
|
||||
says, forge, err := h.notASuccess(m)
|
||||
says, forge, err := h.notMergeableNow(m)
|
||||
if err == nil && says == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -21,12 +21,6 @@ func verdictMoving(repo string, number int, sha string, _ ...string) CheckedEven
|
||||
return verdict(repo, number, sha, "pass")
|
||||
}
|
||||
|
||||
// ungated is the setting check-gates-merge false: mesh/check gates no merge (the operator, 2026-10-25 22:56).
|
||||
func ungated(w *world) {
|
||||
off := false
|
||||
w.h.Settings = func() Settings { return Settings{CheckGatesMerge: &off} }
|
||||
}
|
||||
|
||||
func statusOn(w *world, repo, commit string) string {
|
||||
w.forge.mu.Lock()
|
||||
defer w.forge.mu.Unlock()
|
||||
|
||||
@@ -85,7 +85,7 @@ type Holder struct {
|
||||
// unchecked are the open pull requests' heads seen without a merge check (unchecked.go).
|
||||
unchecked map[string]unchecked
|
||||
// checker are the checker's verdicts by the hash of the heads they judged, as its verdicts bucket holds them
|
||||
// (onestatus.go, issue 1302); runningSince when this owner first saw each running (ADR 0548 §2, turns.go).
|
||||
// (onestatus.go, issue 1302); runningSince when this owner first saw each running, said in the check's words.
|
||||
checker map[string]checkerVerdict
|
||||
runningSince map[string]time.Time
|
||||
// unmergeableSince is when the forge first said a group's member does not merge now, by group and member (issue 1275,
|
||||
@@ -93,11 +93,6 @@ type Holder struct {
|
||||
unmergeableSince map[string]time.Time
|
||||
// writeReads is whether the forge's merging account writes each repository, as last read (writes.go, issue 1945).
|
||||
writeReads map[string]*writeRead
|
||||
// Turns is the gate's bucket turns (turns.go, ADR 0548 §3); nil keeps none. turnsHeld are the turns as written there,
|
||||
// by repository, read once at the first look (turnsLoaded).
|
||||
Turns TurnStore
|
||||
turnsHeld map[string]Turn
|
||||
turnsLoaded bool
|
||||
// reach is the delivery view's reach of each repository, by its key (reached.go, novox/hq ADR 0556): what `deliveries`
|
||||
// and `show` derive whether a merge is on the machines from. Nothing here records it.
|
||||
reach map[string]viewstate.Reach
|
||||
@@ -152,7 +147,7 @@ func (h *Holder) init() {
|
||||
h.lostRead = map[string]time.Time{}
|
||||
h.unchecked = map[string]unchecked{}
|
||||
h.checker = map[string]checkerVerdict{}
|
||||
h.runningSince, h.turnsHeld = map[string]time.Time{}, map[string]Turn{}
|
||||
h.runningSince = map[string]time.Time{}
|
||||
h.reach = map[string]viewstate.Reach{}
|
||||
h.started = h.Now()
|
||||
}
|
||||
|
||||
@@ -56,29 +56,16 @@ func urgentLines(w *world) []StalledLine {
|
||||
return out
|
||||
}
|
||||
|
||||
// 1275/AC2: an approved group whose member has a required status that is not a success at its head when the merge starts
|
||||
// is merged not at all, though the first member in order is fine; the group says which member and which status.
|
||||
func TestAnApprovedGroupWithAMemberWhoseRequiredStatusIsNotASuccessMergesNothing(t *testing.T) {
|
||||
// novox/hq ADR 0661 §4 (changing 1275/AC2 and ADR 0441 §2): an approved group whose member's required status turned
|
||||
// failed after the Approve merges whole, with no read of its statuses at the merge, and raises no line.
|
||||
func TestAnApprovedGroupWhoseMemberStatusTurnedAfterTheApproveMergesWhole(t *testing.T) {
|
||||
w := approvedGroup(t)
|
||||
allGreen(w)
|
||||
w.op.answer(w.op.open()[0], OptionApprove)
|
||||
setChecks(w, gCtl, gHc, "success", "") // the second member's repository check went missing after the ask
|
||||
w.settleAll()
|
||||
if len(w.forge.merges) != 0 {
|
||||
t.Fatalf("merged with a required status missing: %v", w.forge.merges)
|
||||
}
|
||||
lines := urgentLines(w)
|
||||
if len(lines) != 1 || !strings.Contains(lines[0].Says, "feat/g") || !strings.Contains(lines[0].Says, IDOf(gCtl, gHc)) ||
|
||||
!strings.Contains(lines[0].Says, "mesh/repo-check (missing)") || !strings.Contains(lines[0].Says, "Needed") {
|
||||
t.Fatalf("the held group is not said as one urgent line naming group, member, status and need: %+v", lines)
|
||||
}
|
||||
// The status turns success: the whole group merges.
|
||||
setChecks(w, gCtl, gHc, "success", "success")
|
||||
w.h.CommitStatus(ForgeStatusEvent{Owner: "novox", Repo: "mesh-controller", SHA: gHc, Context: repoCheck, State: "success"})
|
||||
w.later(time30s)
|
||||
w.settleAll()
|
||||
if len(w.forge.merges) != 2 || len(urgentLines(w)) != 0 {
|
||||
t.Fatalf("merges %v, lines %+v; wanted both merged and no line", w.forge.merges, urgentLines(w))
|
||||
t.Fatalf("merges %v, lines %+v; wanted both merged on the Approve and no line", w.forge.merges, urgentLines(w))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,33 +129,6 @@ func TestAStatusThatTurnsWhileTheAskWaitsWithdrawsItAndItsAnswerMergesNothing(t
|
||||
}
|
||||
}
|
||||
|
||||
// 1275/AC3 (review round 1): a member that is both refused and held for a required status is said by one stalled line,
|
||||
// not two with the same id.
|
||||
func TestAMemberBothRefusedAndHeldForAStatusIsOneStalledLine(t *testing.T) {
|
||||
w := approvedGroup(t)
|
||||
allGreen(w)
|
||||
w.forge.refuseRepo = map[string]error{gCat: refusedWords("novox/mesh-catalog#2")}
|
||||
w.op.answer(w.op.open()[0], OptionApprove)
|
||||
w.settleAll()
|
||||
setChecks(w, gCat, gHk, "success", "") // now its status is missing too; the next pass is held
|
||||
w.h.CommitStatus(ForgeStatusEvent{Owner: "novox", Repo: "mesh-catalog", SHA: gHk, Context: repoCheck, State: "success"})
|
||||
w.h.mergeAgainNow(w.delivery(IDOf(gCat, gHk)))
|
||||
w.later(time30s)
|
||||
w.settleAll()
|
||||
if g := w.h.groups["feat/g"]; g.Hold == nil {
|
||||
t.Fatalf("the group was not held for the missing status")
|
||||
}
|
||||
n := 0
|
||||
for _, l := range w.h.Stalled() {
|
||||
if l.ID == IDOf(gCat, gHk) {
|
||||
n++
|
||||
}
|
||||
}
|
||||
if n != 1 {
|
||||
t.Fatalf("%d stalled lines for one member: %+v", n, w.h.Stalled())
|
||||
}
|
||||
}
|
||||
|
||||
// 1275/AC6, as novox/hq ADR 0505 §4 changed it: a member refused because its verdict is out of date (the trunk moved; its
|
||||
// fresh check runs) is no stalled line while the gate answers it, and urgent once ten minutes passed with the group not
|
||||
// merged; the pass stops at the refusal all the same.
|
||||
@@ -505,7 +465,7 @@ func TestAnApprovedGroupIsHandedWholeInItsOrderAndAStopIsSaid(t *testing.T) {
|
||||
w.settleAll()
|
||||
w.h.PullMerged(merged(gCat, 2, gHk, gT1))
|
||||
bridgeMerged(w, gCat, 2, gHk, gT1)
|
||||
setChecks(w, gCtl, gHc, "success", "") // the second member's status went missing: the pass stops at it
|
||||
moveForgeHead(w, gCtl, 1, otherHead) // the second member's head moved at the forge: the pass stops at it
|
||||
w.settleAll()
|
||||
for range 3 {
|
||||
w.later(time30s)
|
||||
@@ -522,7 +482,7 @@ func TestAnApprovedGroupIsHandedWholeInItsOrderAndAStopIsSaid(t *testing.T) {
|
||||
!strings.Contains(c.Summary, "half merged (merged 1 of 2)") {
|
||||
t.Fatalf("raised %d times, last %+v; wanted once, urgent, naming the group half merged", raised, c)
|
||||
}
|
||||
setChecks(w, gCtl, gHc, "success", "success")
|
||||
moveForgeHead(w, gCtl, 1, gHc)
|
||||
w.h.CommitStatus(ForgeStatusEvent{Owner: "novox", Repo: "mesh-controller", SHA: gHc, Context: repoCheck, State: "success"})
|
||||
w.settleAll() // the status is an event: the pass goes on with no timer
|
||||
handed := false
|
||||
@@ -770,3 +730,13 @@ func bridgeMerged(w *world, repository string, number int, head, merge string) {
|
||||
w.merges.say(forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: repository, Number: number, Head: head, Merge: merge, Root: "root",
|
||||
Trees: map[string]string{"app": "tree"}})
|
||||
}
|
||||
|
||||
// moveForgeHead sets the head the forge says a pull request has, as a push the gate has not heard yet does.
|
||||
func moveForgeHead(w *world, repo string, number int, head string) {
|
||||
w.forge.mu.Lock()
|
||||
defer w.forge.mu.Unlock()
|
||||
key := fmt.Sprintf("%s#%d", repo, number)
|
||||
p := w.forge.pulls[key]
|
||||
p.HeadSHA = head
|
||||
w.forge.pulls[key] = p
|
||||
}
|
||||
|
||||
@@ -135,62 +135,47 @@ func handedWords(ps []MergePull) string {
|
||||
|
||||
// Two approved merges into one repository: the one whose pass started has the repository until every member merged, and
|
||||
// the other is handed only then (2026-10-26: mesh-delivery#72 of feat/890-assignments merged between fix/734-lost-ask's
|
||||
// members). This holds while mesh/check gates no merge too (ADR 0551): it waits for a merge, never for a check.
|
||||
// members). It waits for a merge, never for a check: once the group's pass ended #72 is handed on its Approve, its verdict
|
||||
// gone out of date with #73's merge or not (novox/hq ADR 0661 §3).
|
||||
func TestTwoApprovedMergesIntoOneRepositoryDoNotInterleave(t *testing.T) {
|
||||
for _, gates := range []bool{true, false} {
|
||||
t.Run(fmt.Sprintf("check-gates-merge %v", gates), func(t *testing.T) {
|
||||
w := approvedGroupOf(t, lostAsk...)
|
||||
if !gates {
|
||||
ungated(w)
|
||||
}
|
||||
w.merges.manual = true
|
||||
allGreenFor(w, lostAsk...)
|
||||
w.op.answer(w.op.open()[0], OptionApprove)
|
||||
w.settleAll()
|
||||
if got := handedWords(handing(w)); got != "novox/mesh-sdk#92" {
|
||||
t.Fatalf("handed %q, wanted the group's first member", got)
|
||||
}
|
||||
// feat/890-assignments: a lone pull request into mesh-delivery, approved while the group's pass is under way.
|
||||
w.h.PullUpdated(pr(wDel, 72, wH72, "feat/890-assignments", "modules/mesh-delivery-checker/go.mod"))
|
||||
w.h.Checked(judged(wDel, 72, wH72, "pass", gTrunk0))
|
||||
setChecks(w, wDel, wH72, "success", "success")
|
||||
w.settleAll()
|
||||
if open := w.op.open(); len(open) != 1 {
|
||||
t.Fatalf("the lone pull request is asked once: %v", open)
|
||||
} else {
|
||||
w.op.answer(open[0], OptionApprove)
|
||||
}
|
||||
w.settleAll()
|
||||
var order []string
|
||||
steps := []struct {
|
||||
repo string
|
||||
number int
|
||||
head string
|
||||
}{{wSDK, 92, wH92}, {wCtl, 472, wH472}, {wDel, 73, wH73}}
|
||||
if !gates {
|
||||
// While the check gates the merge, #72's verdict went out of date with #73's merge and it waits for its
|
||||
// check against the new trunk; while it gates none, #72 is handed once the group's pass ended.
|
||||
steps = append(steps, struct {
|
||||
repo string
|
||||
number int
|
||||
head string
|
||||
}{wDel, 72, wH72})
|
||||
}
|
||||
for _, next := range steps {
|
||||
handed := handing(w)
|
||||
if len(handed) != 1 || handed[0].Repository != next.repo || handed[0].Number != next.number {
|
||||
t.Fatalf("after %v: handed %q, wanted only %s#%d", order, handedWords(handed), next.repo, next.number)
|
||||
}
|
||||
merge := strings.Repeat(string(rune('a'+next.number%20)), 40)
|
||||
bridgeMerged(w, next.repo, next.number, next.head, merge)
|
||||
w.h.PullMerged(merged(next.repo, next.number, next.head, merge))
|
||||
w.settleAll()
|
||||
order = append(order, fmt.Sprintf("%s#%d", next.repo, next.number))
|
||||
}
|
||||
if left := handing(w); len(left) != 0 {
|
||||
t.Fatalf("still handed: %q", handedWords(left))
|
||||
}
|
||||
})
|
||||
w := approvedGroupOf(t, lostAsk...)
|
||||
w.merges.manual = true
|
||||
allGreenFor(w, lostAsk...)
|
||||
w.op.answer(w.op.open()[0], OptionApprove)
|
||||
w.settleAll()
|
||||
if got := handedWords(handing(w)); got != "novox/mesh-sdk#92" {
|
||||
t.Fatalf("handed %q, wanted the group's first member", got)
|
||||
}
|
||||
// feat/890-assignments: a lone pull request into mesh-delivery, approved while the group's pass is under way.
|
||||
w.h.PullUpdated(pr(wDel, 72, wH72, "feat/890-assignments", "modules/mesh-delivery-checker/go.mod"))
|
||||
w.h.Checked(judged(wDel, 72, wH72, "pass", gTrunk0))
|
||||
setChecks(w, wDel, wH72, "success", "success")
|
||||
w.settleAll()
|
||||
if open := w.op.open(); len(open) != 1 {
|
||||
t.Fatalf("the lone pull request is asked once: %v", open)
|
||||
} else {
|
||||
w.op.answer(open[0], OptionApprove)
|
||||
}
|
||||
w.settleAll()
|
||||
var order []string
|
||||
steps := []struct {
|
||||
repo string
|
||||
number int
|
||||
head string
|
||||
}{{wSDK, 92, wH92}, {wCtl, 472, wH472}, {wDel, 73, wH73}, {wDel, 72, wH72}}
|
||||
for _, next := range steps {
|
||||
handed := handing(w)
|
||||
if len(handed) != 1 || handed[0].Repository != next.repo || handed[0].Number != next.number {
|
||||
t.Fatalf("after %v: handed %q, wanted only %s#%d", order, handedWords(handed), next.repo, next.number)
|
||||
}
|
||||
merge := strings.Repeat(string(rune('a'+next.number%20)), 40)
|
||||
bridgeMerged(w, next.repo, next.number, next.head, merge)
|
||||
w.h.PullMerged(merged(next.repo, next.number, next.head, merge))
|
||||
w.settleAll()
|
||||
order = append(order, fmt.Sprintf("%s#%d", next.repo, next.number))
|
||||
}
|
||||
if left := handing(w); len(left) != 0 {
|
||||
t.Fatalf("still handed: %q", handedWords(left))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -135,18 +135,15 @@ func TestTheBridgesRefusalHandsNoMemberAndIsSaidUrgent(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// ADR 0441 §2: before a member is handed, every member's required statuses are read again; one that is not a success holds
|
||||
// the group, even after the first member merged.
|
||||
func TestTheNextMemberIsNotHandedWhileARequiredStatusOfTheGroupIsNotASuccess(t *testing.T) {
|
||||
// novox/hq ADR 0661 §4 (changing ADR 0441 §2): the next member is handed on the Approve covering its head, with no read of
|
||||
// the group's required statuses, after the first member merged.
|
||||
func TestTheNextMemberIsHandedWithNoReadOfTheGroupsRequiredStatuses(t *testing.T) {
|
||||
w := answeredFirst(t)
|
||||
w.merges.say(forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: gCat, Number: 2, Head: gHk, Merge: strings.Repeat("a", 40), Root: "r", Trees: map[string]string{"x": "t"}})
|
||||
setChecks(w, gCtl, gHc, "success", "") // the second member's repository check went missing meanwhile
|
||||
w.settleAll()
|
||||
if len(w.merges.puts) != 1 {
|
||||
t.Fatalf("the second member was handed with a required status missing: %d merges written", len(w.merges.puts))
|
||||
}
|
||||
if lines := urgentLines(w); len(lines) != 1 || !strings.Contains(lines[0].Says, "mesh/repo-check (missing)") {
|
||||
t.Fatalf("the held group is not said urgent: %+v", lines)
|
||||
if len(w.merges.puts) != 2 || len(urgentLines(w)) != 0 {
|
||||
t.Fatalf("%d merges written, lines %+v: wanted the second member handed on its Approve", len(w.merges.puts), urgentLines(w))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import "git.novox.be/novox/mesh-sdk/go/announce"
|
||||
// issue 1790): the module says it itself, from its code, before it serves, and a reader is granted what it said
|
||||
// (clause 3). A state is readable when another module's manifest reads it (keeps_test.go names the readers). keeps_test.go holds the list to the
|
||||
// manifest's `state`, and keeps_integration_test.go that the module announces it on a bus.
|
||||
var Kept = []announce.State{{Name: "asks", Readable: true}, {Name: "conditions"}, {Name: "deliveries"}, {Name: "groups"}, {Name: "merges", Readable: true}, {Name: "owed", Readable: true}, {Name: "pulls", Readable: true}, {Name: "source", Readable: true}, {Name: "turns", Readable: true}}
|
||||
var Kept = []announce.State{{Name: "asks", Readable: true}, {Name: "conditions"}, {Name: "deliveries"}, {Name: "groups"}, {Name: "merges", Readable: true}, {Name: "owed", Readable: true}, {Name: "pulls", Readable: true}, {Name: "source", Readable: true}}
|
||||
|
||||
// announceKept says Kept through keeps, which is stdio.Keeps.
|
||||
func announceKept(keeps func(...announce.State) error) error { return keeps(Kept...) }
|
||||
|
||||
@@ -66,7 +66,6 @@ func main() {
|
||||
Operator: kvOperator{},
|
||||
Merges: kvMerges{},
|
||||
Source: kvSource{},
|
||||
Turns: kvTurns{},
|
||||
Owed: seatOwed{say: stdio.SeatPublish},
|
||||
Pulls: kvPulls{},
|
||||
// The gate says what stalled to the operator itself (issue 1275, ADR 0437 §9).
|
||||
@@ -435,24 +434,6 @@ func tools(h *Holder, l *listening) []stdio.Tool {
|
||||
}
|
||||
return h.Settle(ids, strArg(a, "why"), aPerson)
|
||||
}},
|
||||
{Name: "delivery_turns",
|
||||
Description: "Every merge turn (novox/hq ADR 0548 §3): per repository, the approved merges that wait to merge into it in the order of " +
|
||||
"their Approve, the check of another delivery each waits for and since when, whether the gate wrote the turn to its bucket " +
|
||||
"turns yet (the checker claims no check of a repository a turn names), and the bound past which a held merge is a stalled " +
|
||||
"line. Read-only.",
|
||||
Run: func(map[string]any) (any, error) { return h.TurnsRead(), nil }},
|
||||
{Name: "delivery_hand_now",
|
||||
Description: "On the operator's word, hand an approved merge held for another delivery's running check to the merge at once " +
|
||||
"(novox/hq ADR 0548 §4): the check it waited for runs on and is carried to the new trunk after the merge. Refused for a " +
|
||||
"delivery that is not an approved merge held by a check, naming why. The why is kept on the delivery.",
|
||||
Input: map[string]any{"type": "object", "properties": map[string]any{"id": str("the delivery's id (owner/repository@commit)"),
|
||||
"why": str("the operator's word, kept on the delivery")}, "required": []string{"id", "why"}},
|
||||
Run: func(a map[string]any) (any, error) {
|
||||
if err := need(a, "id", "why"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return h.HandNow(strArg(a, "id"), strArg(a, "why"))
|
||||
}},
|
||||
{Name: "delivery_merge_again",
|
||||
Description: "On the operator's word, ask again at once the merge of an approved delivery the forge refused, or of an approved " +
|
||||
"group held or half merged, at the heads the Approve named (novox/hq issue 1275): every member is read first and " +
|
||||
|
||||
@@ -120,12 +120,20 @@ func TestItClaimsTheDeliverySeatAndFollowsWhatItHandles(t *testing.T) {
|
||||
if !reflect.DeepEqual(m.Emits, []string{"transition", "group", "merged"}) {
|
||||
t.Fatalf("emits %v", m.Emits)
|
||||
}
|
||||
// Nine buckets, and no `asked` (the teller asks, ADR 0437 §6): ADR 0450 §3, the conditions it raised (issue 1275) and the
|
||||
// merge turns (ADR 0548 §3).
|
||||
// Eight buckets, and no `asked` (the teller asks, ADR 0437 §6): ADR 0450 §3 and the conditions it raised (issue 1275). No
|
||||
// `turns`, no turn tool and no `check-gates-merge`: no check gates a merge (novox/hq ADR 0661 §1, §2, §6).
|
||||
buckets := m.buckets()
|
||||
if !reflect.DeepEqual(buckets, []string{"deliveries", "groups", "pulls", "asks", "merges", "owed", "source", "conditions", "turns"}) {
|
||||
if !reflect.DeepEqual(buckets, []string{"deliveries", "groups", "pulls", "asks", "merges", "owed", "source", "conditions"}) {
|
||||
t.Fatalf("state %v", buckets)
|
||||
}
|
||||
for _, tool := range m.Tools {
|
||||
if strings.Contains(tool, "turn") || strings.Contains(tool, "hand_now") {
|
||||
t.Fatalf("the gate lists the turn tool %s", tool)
|
||||
}
|
||||
}
|
||||
if raw, err := os.ReadFile("../../module.json"); err != nil || strings.Contains(string(raw), "check-gates-merge") {
|
||||
t.Fatalf("the manifest still names check-gates-merge (%v)", err)
|
||||
}
|
||||
// What it reads: the teller's answers and the checker's verdicts (issue 1302). The carry from today's module's buckets, the
|
||||
// seat's holder and the machines' haves went with the seat's hand-over (ADR 0539 §17). The delivery view's reach, which
|
||||
// `deliveries` and `show` derive whether a merge is on the machines from (ADR 0556 clause 4); no forge tool is asked for it.
|
||||
|
||||
@@ -164,6 +164,13 @@ func (v *Verdict) Judged() string {
|
||||
// outOfDate is why a delivery's verdict no longer counts: it names a trunk commit that is not its trunk's head
|
||||
// as this owner heard it (novox/hq ADR 0323 §7). Empty when it counts — or names no trunk commit, as a verdict
|
||||
// of a build seat from before the rule does, which counts as it did.
|
||||
// approvedAtHead is whether the operator's Approve covers this delivery at its head: once it does, the merge judges that
|
||||
// Approve alone, not the check (novox/hq ADR 0661 §3, §4).
|
||||
func (d *Delivery) approvedAtHead() bool {
|
||||
r := d.Ready
|
||||
return r != nil && r.Approved != nil && !r.declined() && r.Heads[d.ID] == d.Commit
|
||||
}
|
||||
|
||||
func (d *Delivery) outOfDate() string {
|
||||
if d.Check == nil || d.Check.Trunk == "" || d.TrunkNow == "" || sameCommit(d.Check.Trunk, d.TrunkNow) {
|
||||
return ""
|
||||
|
||||
@@ -97,7 +97,7 @@ func (h *Holder) CheckerVerdictChanged(key string, value []byte) {
|
||||
v.Heads = hash
|
||||
}
|
||||
if prev, ok := h.checker[v.Heads]; v.State == checkerRunning && (!ok || prev.State != checkerRunning) {
|
||||
h.runningSince[v.Heads] = h.Now() // a check starts: a merge it holds says since when (ADR 0548 §2)
|
||||
h.runningSince[v.Heads] = h.Now() // a check starts: its words say since when
|
||||
} else if v.State != checkerRunning {
|
||||
delete(h.runningSince, v.Heads)
|
||||
}
|
||||
@@ -171,10 +171,12 @@ func (h *Holder) takeCheckStatus(repository, sha, state, desc string) {
|
||||
}
|
||||
h.moved(d, []Transition{t})
|
||||
}
|
||||
if d.State == Ready && !h.checkGates() {
|
||||
// The check gates no merge (check-gates-merge false): its verdict is shown on the delivery, and moves nothing.
|
||||
if d.approvedAtHead() && (d.State == Ready || d.State == Merged) {
|
||||
// The merge judges the operator's Approve alone (novox/hq ADR 0661 §3): a verdict after it, a carry's, is shown and
|
||||
// moves nothing; a failure is said (failedAfterApprove).
|
||||
d.Check = v
|
||||
h.Logf("[mesh-delivery] %s: its mesh/check is %s — %s (shown; the check gates no merge)", d.ID, state, oneLine(clip(desc, 120)))
|
||||
h.Logf("[mesh-delivery] %s: its mesh/check is %s — %s (after the operator's Approve)", d.ID, state, oneLine(clip(desc, 120)))
|
||||
h.failedAfterApprove(d, v)
|
||||
h.keep(d)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -128,12 +128,11 @@ type ReadyRun struct {
|
||||
Warrant string `json:"warrant,omitempty"`
|
||||
Refused string `json:"refused,omitempty"`
|
||||
|
||||
// One check per delivery (novox/hq ADR 0548 §2, §4, turns.go): HeldSince is when the approved merge began to wait for the
|
||||
// check of other heads HeldFor names (its verdict key), running on a repository it goes into; HandNow the operator's word
|
||||
// that handed it at once past that check.
|
||||
HeldSince *time.Time `json:"held_since,omitempty"`
|
||||
HeldFor string `json:"held_for,omitempty"`
|
||||
HandNow string `json:"hand_now,omitempty"`
|
||||
// FailedAfter is a failure heard after the operator's Approve covered the head, a carry's (novox/hq ADR 0661 §5): said
|
||||
// once on the pull request and as a stalled line naming the merge it follows, never a refusal of the merge; cleared by a
|
||||
// later pass. FailedAfterSince is when it was heard.
|
||||
FailedAfter string `json:"failed_after,omitempty"`
|
||||
FailedAfterSince *time.Time `json:"failed_after_since,omitempty"`
|
||||
|
||||
// CannotWrite is why the operator is not asked: the forge's merging account cannot write a repository the ask covers
|
||||
// (writes.go, issue 1945); CannotWriteSince since when. Said at once as a condition.
|
||||
@@ -194,7 +193,6 @@ func (h *Holder) runOf(d *Delivery, now time.Time) *ReadyRun {
|
||||
func (h *Holder) readyAsks(now time.Time) []func() {
|
||||
var asks []func()
|
||||
h.endStaleAsks()
|
||||
gates := h.checkGates()
|
||||
ids := make([]string, 0, len(h.deliveries))
|
||||
for id := range h.deliveries {
|
||||
ids = append(ids, id)
|
||||
@@ -205,9 +203,6 @@ func (h *Holder) readyAsks(now time.Time) []func() {
|
||||
if d.Number == 0 || d.State.Final() || d.MergedAs != "" {
|
||||
continue // not of a pull request, or past its merge
|
||||
}
|
||||
if !gates {
|
||||
h.ungate(d, now)
|
||||
}
|
||||
if d.Ready == nil && d.State == Ready {
|
||||
h.runOf(d, now)
|
||||
h.Poke() // its check, if none is asked, is worth asking now (askCheck)
|
||||
@@ -276,9 +271,6 @@ func (h *Holder) readyAsks(now time.Time) []func() {
|
||||
if waits == "" {
|
||||
waits, approvalAsk = h.approval(d, now)
|
||||
}
|
||||
if waits == "" && gates {
|
||||
waits = h.turnWaits(d, now) // nothing merges into a repository while a check of other heads judges it (ADR 0548 §2)
|
||||
}
|
||||
if waits == "" {
|
||||
waits = h.passWaits(d) // no merge lands in a repository between another approved group's members (issue 1275)
|
||||
}
|
||||
@@ -310,7 +302,6 @@ func (h *Holder) readyAsks(now time.Time) []func() {
|
||||
h.noteNotHanded(d, h.notHanded(g, d), now) // a merge never handed says why (2026-10-29: silent for an hour)
|
||||
}
|
||||
}
|
||||
asks = append(asks, h.turnWrites()...) // the merge turns, after the holds of this look (ADR 0548 §3)
|
||||
asks = append(asks, h.settleMerges()...)
|
||||
asks = append(asks, h.settleForgeMerges()...)
|
||||
asks = append(asks, h.recordForgeMade()...)
|
||||
@@ -321,30 +312,6 @@ func (h *Holder) readyAsks(now time.Time) []func() {
|
||||
return asks
|
||||
}
|
||||
|
||||
// ungate stands a delivery where it is while mesh/check gates no merge (check-gates-merge false): a proposed one is ready
|
||||
// with no verdict, and a rejected one, refused on a verdict before the setting changed, is proposed again and so ready. Its
|
||||
// verdict stays on it, shown. Under the lock.
|
||||
func (h *Holder) ungate(d *Delivery, now time.Time) {
|
||||
f := h.factsAt(now)
|
||||
if d.State == Rejected {
|
||||
f.By, f.Why = byOwner, "the check gates no merge (check-gates-merge false): its verdict is shown, not waited on"
|
||||
t, err := Apply(d, EvRecheck, true, f)
|
||||
if err != nil {
|
||||
h.refuse(err)
|
||||
return
|
||||
}
|
||||
if d.Ready != nil {
|
||||
d.Ready.Failed = ""
|
||||
}
|
||||
h.moved(d, []Transition{t})
|
||||
f.By, f.Why = "", ""
|
||||
}
|
||||
if d.State == Proposed {
|
||||
h.moved(d, settle(d, f))
|
||||
h.closeGroupOf(d)
|
||||
}
|
||||
}
|
||||
|
||||
// sayAfterLine tells a pull request, once, that its description holds an `after: <repository>` line, which is not read
|
||||
// (novox/hq ADR 0419 §2): the order of a delivery comes from the module graph alone, and no refusal asks for the line.
|
||||
// Once per pull request, not per head. Under the lock; the comment is said outside it.
|
||||
@@ -406,14 +373,21 @@ func (r *ReadyRun) noteRefusal(d *Delivery, answer string) {
|
||||
r.Repeats, r.RepeatTrunk = 1, d.TrunkNow
|
||||
}
|
||||
|
||||
// readyWaits is what a delivery's merge waits on; empty when the Approve may be asked and the merge asked. A ready
|
||||
// delivery is asked. So is a proposed one whose verdict went out of date, when an Approve was asked for it already (it
|
||||
// stays through a trunk move, ADR 0421 §3) and its check against the trunk's head is not on its way; one without an
|
||||
// Approve waits for that check first.
|
||||
// readyWaits is what a delivery's merge waits on; empty when the Approve may be asked and the merge asked. Once the
|
||||
// operator's Approve covers its heads the merge judges that Approve alone (approvedWaits); until then the check gates the
|
||||
// ask (askWaits), the one place a check gates anything here (novox/hq ADR 0661 §3, §4). Under the lock.
|
||||
func (h *Holder) readyWaits(d *Delivery) string {
|
||||
if !h.checkGates() {
|
||||
return h.ungatedWaits(d)
|
||||
if c := h.covering(d); c != nil && c.Approved != nil {
|
||||
return h.approvedWaits(d)
|
||||
}
|
||||
return h.askWaits(d)
|
||||
}
|
||||
|
||||
// askWaits is why the operator's Approve is not asked yet: the check gates the ask (novox/hq ADR 0661 §3, ADR 0421 §1). A
|
||||
// ready delivery is asked once the checker's verdict at exactly the heads the ask covers passed; a proposed one whose
|
||||
// verdict went out of date while its ask is pending waits for its check against the trunk's head. A group is asked only
|
||||
// once its composed check passed at its members' heads (ADR 0661 §4).
|
||||
func (h *Holder) askWaits(d *Delivery) string {
|
||||
if d.judgedByCheck() && d.State == Proposed && d.outOfDate() != "" && h.covering(d) == nil {
|
||||
return "its mesh/check is " + d.outOfDate() + ": the checker checks it again against the trunk's head"
|
||||
}
|
||||
@@ -421,10 +395,6 @@ func (h *Holder) readyWaits(d *Delivery) string {
|
||||
case d.State == Ready:
|
||||
case d.State == Proposed && d.outOfDate() != "" && d.AskedAgain != nil:
|
||||
return "its check against the trunk's head is asked and has not answered"
|
||||
case d.State == Proposed && d.outOfDate() != "" && d.Check.Passes() && h.covering(d) != nil:
|
||||
// Approved, and its trunk moved: the checker carries its verdict to the trunk's head by its package digests (ADR 0548
|
||||
// §5, §8). The Approve stands and is not asked again; the merge waits for the carried verdict, not a fresh check.
|
||||
return "approved; its verdict is being carried to the trunk's head " + shortOf(d.TrunkNow, 12) + " (" + d.outOfDate() + ")"
|
||||
case d.State == Proposed && d.outOfDate() != "":
|
||||
return "its verdict is out of date: its check against the trunk's head is asked before the operator is"
|
||||
case d.State == Proposed && d.AskedAgain != nil:
|
||||
@@ -442,10 +412,16 @@ func (h *Holder) readyWaits(d *Delivery) string {
|
||||
return h.groupTurnToMerge(d)
|
||||
}
|
||||
|
||||
// ungatedWaits is readyWaits while mesh/check gates no merge (check-gates-merge false): a ready delivery waits for nothing but
|
||||
// a group member for every member to be ready; no verdict, composed check or trunk is waited on,
|
||||
// so the Approve is asked at once.
|
||||
func (h *Holder) ungatedWaits(d *Delivery) string {
|
||||
// approvedWaits is what a merge the operator's Approve covers waits on: nothing of the check (novox/hq ADR 0661 §3, §4).
|
||||
// No verdict, carry, composed check or trunk move is waited on, and no status is read again; a group member waits only
|
||||
// for every member to be ready, at the heads the Approve covers, so the group merges in one pass. A proposed delivery
|
||||
// the Approve covers (a trunk moved before the Approve took its run) is ready on it. Under the lock.
|
||||
func (h *Holder) approvedWaits(d *Delivery) string {
|
||||
if c := h.covering(d); d.State == Proposed && d.Ready != nil && c != nil && c.Approved != nil {
|
||||
now := h.Now()
|
||||
h.approval(d, now) // the covering Approve taken onto its own run, so the table reads it
|
||||
h.moved(d, settle(d, h.factsAt(now)))
|
||||
}
|
||||
if d.State != Ready {
|
||||
return "it is " + string(d.State) + ", not ready yet"
|
||||
}
|
||||
@@ -456,9 +432,6 @@ func (h *Holder) ungatedWaits(d *Delivery) string {
|
||||
}
|
||||
}
|
||||
}
|
||||
// No verdict is waited on (the operator, 2026-11-07: "we aren't running any checks anymore, we're delivering"): while
|
||||
// the check gates no merge, the Approve is asked once the delivery is ready; a verdict that comes later
|
||||
// is shown on the pull request and the delivery, as before.
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -517,12 +490,50 @@ func (h *Holder) sayOn(d *Delivery, body string) func() {
|
||||
return func() {}
|
||||
}
|
||||
|
||||
// failedAfterApprove says a failure heard after the operator's Approve covered the head: once on the pull request, and
|
||||
// as the stalled line readyLines lists; the merge is not refused for it, and the operator decides the remedy, a fix or a
|
||||
// revert (novox/hq ADR 0661 §5). A later pass clears it. Under the lock.
|
||||
func (h *Holder) failedAfterApprove(d *Delivery, v *Verdict) {
|
||||
r := d.Ready
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
if v.Passes() {
|
||||
r.FailedAfter, r.FailedAfterSince = "", nil
|
||||
return
|
||||
}
|
||||
if r.FailedAfter != "" {
|
||||
return
|
||||
}
|
||||
now := h.Now()
|
||||
r.FailedAfter, r.FailedAfterSince = "its check failed after the operator's Approve: "+oneLine(clip(v.Summary, 120)), &now
|
||||
h.Logf("[mesh-delivery] %s (%s): %s", d.ID, h.mergeFollowed(d), r.FailedAfter)
|
||||
h.sayOn(d, fmt.Sprintf("mesh-delivery: **failed after the Approve.** This pull request (%s) %s. The merge is not "+
|
||||
"refused for it: the operator decides the remedy, a fix or a revert.", h.mergeFollowed(d), r.FailedAfter))
|
||||
}
|
||||
|
||||
// mergeFollowed names the merge a failure after the Approve follows: the trunk commit it merged as, or its approved head
|
||||
// not merged yet.
|
||||
func (h *Holder) mergeFollowed(d *Delivery) string {
|
||||
if d.MergedAs != "" {
|
||||
return "merged as " + shortOf(d.MergedAs, 12) + " at its head " + shortOf(d.Commit, 12)
|
||||
}
|
||||
return "approved at its head " + shortOf(d.Commit, 12) + ", not merged yet"
|
||||
}
|
||||
|
||||
// readyLines are the approved heads that will not merge: a check that failed at once, a merge refused past its bound;
|
||||
// as stalled lines, which the gate says as conditions (conditions.go). Under the lock.
|
||||
func (h *Holder) readyLines(now time.Time) []StalledLine {
|
||||
var out []StalledLine
|
||||
for _, d := range h.deliveries {
|
||||
r := d.Ready
|
||||
if r != nil && r.FailedAfter != "" && r.FailedAfterSince != nil && d.State != Closed {
|
||||
out = append(out, StalledLine{ID: d.ID, Number: d.Number, State: d.State,
|
||||
For: now.Sub(*r.FailedAfterSince).Round(time.Second).String(), Bound: "0s",
|
||||
H2: "none: the operator decides the remedy, a fix or a revert; a later pass clears it",
|
||||
Says: fmt.Sprintf("%s#%d (%s): %s", d.Repository, d.Number, h.mergeFollowed(d), r.FailedAfter)})
|
||||
continue
|
||||
}
|
||||
if r == nil || r.Merged != nil || d.MergedAs != "" || d.State.Final() {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"time"
|
||||
|
||||
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
|
||||
mdc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
|
||||
)
|
||||
|
||||
// **A pull request whose checks passed and whose merge the operator approved is merged by this owner** (novox/hq issues
|
||||
@@ -85,22 +84,9 @@ func TestAnApprovedPullRequestIsMergedThroughTheMergeTool(t *testing.T) {
|
||||
{name: "gated, passed", gated: true, state: "pass", asks: 1, head: "Merge into main: app#7 Fix it",
|
||||
want: []string{"app#7 Fix it passed its check. Approve merges it into main at " + readyHead[:12] + ";"},
|
||||
not: []string{"before its check ends", "no verdict yet"}},
|
||||
// The operator, 2026-11-07: "we aren't running any checks anymore, we're delivering": while the check gates no
|
||||
// merge the Approve is asked at once, with no verdict or a running check, never waiting for one.
|
||||
{name: "ungated, no verdict", asks: 1, not: []string{waitsCheckBeforeAsking}},
|
||||
{name: "ungated, running", state: "running", asks: 1, not: []string{waitsCheckBeforeAsking}},
|
||||
// Asked at once, then asked again in its words once the verdict comes (a later verdict asks a pending Approve again).
|
||||
{name: "ungated, passed", state: "pass", asks: 2,
|
||||
want: []string{"passed its check. Approve merges it into main at " + readyHead[:12] + ";"}, not: []string{"before its check ends"}},
|
||||
{name: "ungated, failed", state: "fail", failed: "TestThing: wanted 1, got 2", asks: 2,
|
||||
head: "Check failed \u2014 merge into main: app#7 Fix it",
|
||||
want: []string{"its check failed: TestThing: wanted 1, got 2", "although its check failed"}, not: []string{"passed", "anyway"}},
|
||||
} {
|
||||
t.Run(row.name, func(t *testing.T) {
|
||||
v := newWorld(t)
|
||||
if !row.gated {
|
||||
ungated(v)
|
||||
}
|
||||
v.op.auto = false
|
||||
p := pr("novox/app", 7, readyHead, "feat/x", "src/a.go")
|
||||
p.Title = "Fix it"
|
||||
@@ -158,47 +144,6 @@ func TestAnApprovedPullRequestIsMergedThroughTheMergeTool(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// check-gates-merge false (the operator, 2026-10-25 22:56): a reviewed pull request is asked with no verdict, another
|
||||
// merge moving its trunk puts it out of nothing, a failed verdict is shown on it and holds nothing, and the gate's
|
||||
// status and show say the mode.
|
||||
u := newWorld(t)
|
||||
ungated(u)
|
||||
u.op.auto = false
|
||||
const other = "3333333333333333333333333333333333333333"
|
||||
u.h.PullUpdated(pr("novox/app", 7, readyHead, "feat/x", "src/a.go"))
|
||||
u.h.PullUpdated(pr("novox/app", 8, other, "feat/y", "src/b.go"))
|
||||
u.settleAll()
|
||||
if u.op.count() == 0 || u.state(IDOf("novox/app", readyHead)) != Ready {
|
||||
t.Fatalf("ungated, two reviewed pull requests with no verdict: %d asks, %s; wanted ready and asked at once", u.op.count(), u.state(IDOf("novox/app", readyHead)))
|
||||
}
|
||||
if gates, _ := u.h.Status("listening")["check-gates-merge"].(bool); gates {
|
||||
t.Fatalf("delivery_status does not say the check gates no merge: %v", u.h.Status("listening"))
|
||||
}
|
||||
shown, _ := u.h.Show(IDOf("novox/app", other))
|
||||
if gates, _ := shown.(map[string]any)["check-gates-merge"].(bool); gates {
|
||||
t.Fatalf("show does not say the check gates no merge: %v", shown)
|
||||
}
|
||||
u.h.Checked(judgedAt("novox/app", 8, other, "fail", "t1t1t1t1t1t1"))
|
||||
checkerSays(u, "feat/y", "fail", "TestB", headOf("novox/app", 8, other))
|
||||
u.h.PullMerged(merged("novox/app", 7, readyHead, "m7m7m7m7m7m7"))
|
||||
u.settleAll()
|
||||
if u.op.count() == 0 {
|
||||
t.Fatalf("ungated, a failed verdict holds the ask (the check gates no merge): %d asks", u.op.count())
|
||||
}
|
||||
d = u.delivery(IDOf("novox/app", other))
|
||||
if d.State != Ready || d.Check == nil || d.Check.Passes() {
|
||||
t.Fatalf("ungated, after a failed verdict and a trunk's move: %s, verdict %+v", d.State, d.Check)
|
||||
}
|
||||
for _, a := range u.op.open() {
|
||||
if strings.Contains(a, "feat/y") {
|
||||
u.op.answer(a, OptionApprove)
|
||||
}
|
||||
}
|
||||
u.settleAll()
|
||||
if len(u.forge.merges) != 1 || u.forge.merges[0] != "novox/app#8@"+other {
|
||||
t.Fatalf("ungated, the approved head is merged %v; wanted once, with no verdict waited on", u.forge.merges)
|
||||
}
|
||||
}
|
||||
|
||||
// 1144/AC3: a refused merge is not asked again by any timer; only an event (here, mergeAgainNow called directly, as a
|
||||
@@ -342,57 +287,6 @@ func TestAGroupMergesEveryMemberInOnePassOnlyOnceComposedAndApproved(t *testing.
|
||||
if w.op.count() != 1 {
|
||||
t.Fatalf("a second Approve was asked: %d", w.op.count())
|
||||
}
|
||||
|
||||
// check-gates-merge false: the group is asked once with no verdict and no composed check, and merges whole in one pass.
|
||||
u := newWorld(t)
|
||||
ungated(u)
|
||||
u.op.auto = false
|
||||
u.h.PullUpdated(pr("novox/mesh-controller", 1, ctlHead, "feat/x", "internal/a.go"))
|
||||
u.h.PullUpdated(pr("novox/mesh-catalog", 2, catHead, "feat/x", "modules/app/module.json"))
|
||||
u.settleAll()
|
||||
group := []mdc.Head{headOf("novox/mesh-controller", 1, ctlHead), headOf("novox/mesh-catalog", 2, catHead)}
|
||||
checkerSays(u, "feat/x", "running", "", group...)
|
||||
u.settleAll()
|
||||
// Issue 1966 (the operator, 2026-11-07): the group is asked at once, its check running or not; nothing waits on it.
|
||||
if d := u.delivery(IDOf("novox/mesh-catalog", catHead)); u.op.count() != 1 || d.Ready == nil ||
|
||||
strings.Contains(d.Ready.Waits, waitsCheckBeforeAsking) {
|
||||
t.Fatalf("ungated, the group is asked %d times while its check runs; wanted once at once, and the wait said: %+v", u.op.count(), d.Ready)
|
||||
}
|
||||
checkerSays(u, "feat/x", "pass", "", group...)
|
||||
u.settleAll()
|
||||
if u.op.count() != 2 {
|
||||
t.Fatalf("ungated, the group is asked %d times once its check passed; wanted once at once and once again in its words", u.op.count())
|
||||
}
|
||||
if e := u.op.ask(u.op.open()[0]).Explanation; !strings.Contains(e, "2 pull requests, passed its check. Approve merges all of them into main in one pass, at the heads named;") {
|
||||
t.Fatalf("ungated, the group's ask does not say its check passed:\n%s", e)
|
||||
}
|
||||
// A member re-pushed hours after its sibling became ready: with no verdict at the new heads nothing is asked, however
|
||||
// long the sibling has waited.
|
||||
r := newWorld(t)
|
||||
ungated(r)
|
||||
r.op.auto = false
|
||||
const ctlHead2 = "c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3"
|
||||
r.h.PullUpdated(pr("novox/mesh-controller", 1, ctlHead, "feat/x", "internal/a.go"))
|
||||
r.h.PullUpdated(pr("novox/mesh-catalog", 2, catHead, "feat/x", "modules/app/module.json"))
|
||||
r.settleAll()
|
||||
r.later(5 * time.Hour)
|
||||
r.h.PullUpdated(pr("novox/mesh-controller", 1, ctlHead2, "feat/x", "internal/a.go"))
|
||||
// Made ready by a handler, before the look gives it a run: no verdict and no status at the new heads.
|
||||
r.h.mu.Lock()
|
||||
r.h.ungate(r.h.deliveries[IDOf("novox/mesh-controller", ctlHead2)], r.h.Now())
|
||||
r.h.mu.Unlock()
|
||||
r.settleAll()
|
||||
// Issue 1966: no verdict is waited on, so the new heads are asked again at once, never held for a check.
|
||||
if d := r.delivery(IDOf("novox/mesh-catalog", catHead)); r.op.count() == 0 || d.Ready == nil ||
|
||||
strings.Contains(d.Ready.Waits, waitsCheckBeforeAsking) {
|
||||
t.Fatalf("a member re-pushed hours later: asked %d times, wait %+v; wanted asked again at once", r.op.count(), d.Ready)
|
||||
}
|
||||
|
||||
u.op.answer(u.op.open()[0], OptionApprove)
|
||||
u.settleAll()
|
||||
if len(u.forge.merges) != 2 {
|
||||
t.Fatalf("ungated, both members merge in one pass: %v", u.forge.merges)
|
||||
}
|
||||
}
|
||||
|
||||
// **A merge into another branch than main is its author's** (novox/hq ADR 0398 §4, issue 948): a green pull request
|
||||
|
||||
@@ -25,10 +25,6 @@ const (
|
||||
type Settings struct {
|
||||
RefusalFloorMinutes *int `json:"refusal-stall-floor-minutes"`
|
||||
AuthorBoundMinutes *int `json:"author-new-head-minutes"`
|
||||
// CheckGatesMerge is whether mesh/check gates the merge (the default, true). False (the operator, 2026-10-25 22:56:
|
||||
// "run the check locally only, but not as a gate, it loses us too much time"): a reviewed pull request is ready and its
|
||||
// Approve asked without a verdict, a trunk's move puts nothing out of date, and a verdict is shown, never waited on.
|
||||
CheckGatesMerge *bool `json:"check-gates-merge"`
|
||||
// RecordRetentionDays is how many days a delivery record is kept from when it opened (design 47 *The delivery
|
||||
// record*); nil keeps DefaultRecordRetention.
|
||||
RecordRetentionDays *int `json:"record-retention"`
|
||||
@@ -73,28 +69,13 @@ func readSettings(path string) (Settings, error) {
|
||||
default:
|
||||
return Settings{}, errors.New("record-retention is not a number")
|
||||
}
|
||||
switch v := loose["check-gates-merge"].(type) {
|
||||
case nil:
|
||||
case bool:
|
||||
s.CheckGatesMerge = &v
|
||||
default:
|
||||
return Settings{}, errors.New("check-gates-merge is not true or false")
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// checkGates is whether mesh/check gates the merge: true unless the setting check-gates-merge is false.
|
||||
func (h *Holder) checkGates() bool {
|
||||
if h.Settings == nil {
|
||||
return true
|
||||
}
|
||||
v := h.Settings().CheckGatesMerge
|
||||
return v == nil || *v
|
||||
}
|
||||
|
||||
// factsAt are the facts a settle reads at now: the moment, and whether the check gates the merge.
|
||||
// factsAt are the facts a settle reads at now: the moment. No setting switches whether a check gates a merge: the check
|
||||
// gates the ask, and the merge judges the operator's Approve alone (novox/hq ADR 0661 §2, §3).
|
||||
func (h *Holder) factsAt(now time.Time) Facts {
|
||||
return Facts{Now: now, Ungated: !h.checkGates()}
|
||||
return Facts{Now: now}
|
||||
}
|
||||
|
||||
// minutesOr is a setting's minutes, or the default when it gives none.
|
||||
|
||||
@@ -94,9 +94,6 @@ type Facts struct {
|
||||
Now time.Time
|
||||
By string
|
||||
Why string
|
||||
// Ungated is the setting check-gates-merge false: mesh/check gates no merge, so a proposed delivery is accepted with no
|
||||
// verdict, none is refused for one, and a trunk's move puts none back to proposed.
|
||||
Ungated bool
|
||||
}
|
||||
|
||||
// Row is one transition the table holds.
|
||||
@@ -145,10 +142,10 @@ var Table = []Row{
|
||||
// cannot loop with `trunk moved`; a recheck or the merge tool's ask judges it again.
|
||||
{From: []State{Proposed}, Event: EvAccepted, To: Ready,
|
||||
Guard: "a verdict names this commit and the trunk's head as its trunk commit, the gate passed or warned, and " +
|
||||
"the repository's own check did not fail; or the check gates no merge (check-gates-merge false)",
|
||||
"the repository's own check did not fail; or the operator's Approve covers this head (the merge judges it alone)",
|
||||
Holds: func(d *Delivery, f Facts) error {
|
||||
if f.Ungated {
|
||||
return nil
|
||||
if d.approvedAtHead() {
|
||||
return nil // novox/hq ADR 0661 §3: an Approve given stands across a trunk move, and the merge judges it alone
|
||||
}
|
||||
if d.Check == nil {
|
||||
return errors.New("no verdict")
|
||||
@@ -160,10 +157,11 @@ var Table = []Row{
|
||||
}},
|
||||
{From: []State{Proposed}, Event: EvRefused, To: Rejected,
|
||||
Guard: "a verdict names this commit and the trunk's head as its trunk commit, and the gate failed or could not run, " +
|
||||
"or the repository's own check failed; never while the check gates no merge",
|
||||
"or the repository's own check failed; never once the operator's Approve covers this head",
|
||||
Holds: func(d *Delivery, f Facts) error {
|
||||
if f.Ungated {
|
||||
return errors.New("the check gates no merge (check-gates-merge false)")
|
||||
if d.approvedAtHead() {
|
||||
// novox/hq ADR 0661 §5: a failure after the Approve is said, and does not undo it.
|
||||
return errors.New("the operator's Approve covers this head: a failure is said, the merge is not refused")
|
||||
}
|
||||
if d.Check == nil {
|
||||
return errors.New("no verdict")
|
||||
@@ -176,10 +174,11 @@ var Table = []Row{
|
||||
// **A ready delivery whose verdict went out of date goes back to proposed** (novox/hq ADR 0323 §7): another
|
||||
// merge moved its trunk. A rejected one stays rejected, its status on the forge saying the verdict is out of date.
|
||||
{From: []State{Ready}, Event: EvTrunkMoved, To: Proposed,
|
||||
Guard: "its verdict names a trunk commit that is not its trunk's head, and the check gates the merge",
|
||||
Guard: "its verdict names a trunk commit that is not its trunk's head, and no Approve covers this head yet",
|
||||
Holds: func(d *Delivery, f Facts) error {
|
||||
if f.Ungated {
|
||||
return errors.New("the check gates no merge (check-gates-merge false)")
|
||||
if d.approvedAtHead() {
|
||||
// novox/hq ADR 0661 §3: an approved merge waits for no carry; its carry runs after the merge.
|
||||
return errors.New("the operator's Approve covers this head: it merges onto the new trunk, its carry after")
|
||||
}
|
||||
return unless(d.MergedAs == "" && d.outOfDate() != "", "not merged, and its verdict is not out of date")
|
||||
},
|
||||
|
||||
@@ -161,21 +161,34 @@ func TestTheTableKeepsItsRules(t *testing.T) {
|
||||
if _, err := Apply(d0, EvReask, true, Facts{Now: time.Now(), By: byH2, Why: "stalled"}); err == nil {
|
||||
t.Fatal("a check re-asked once was re-asked again")
|
||||
}
|
||||
// While the check gates no merge (check-gates-merge false): a proposed delivery is accepted with no verdict or a failed
|
||||
// one, none is refused, and a trunk's move puts no ready one back to proposed.
|
||||
off := Facts{Now: time.Now(), Ungated: true}
|
||||
// Once the operator's Approve covers the head (novox/hq ADR 0661 §3, §5): a proposed delivery is accepted with no
|
||||
// verdict or a failed one, none is refused, and a trunk's move puts no ready one back to proposed.
|
||||
at := time.Now()
|
||||
approved := func(d *Delivery) *Delivery {
|
||||
d.Ready = &ReadyRun{Approved: &at, Heads: map[string]string{d.ID: d.Commit}}
|
||||
return d
|
||||
}
|
||||
f := Facts{Now: at}
|
||||
for _, check := range []*Verdict{nil, {Gate: "fail"}} {
|
||||
p := &Delivery{ID: "novox/app@c0", State: Proposed, Number: 1, Check: check}
|
||||
if _, err := Apply(p, EvRefused, false, off); err == nil {
|
||||
t.Fatalf("ungated, a verdict %+v refused the delivery", check)
|
||||
p := approved(&Delivery{ID: "novox/app@c0", Commit: "c0", State: Proposed, Number: 1, Check: check})
|
||||
if _, err := Apply(p, EvRefused, false, f); err == nil {
|
||||
t.Fatalf("approved, a verdict %+v refused the delivery", check)
|
||||
}
|
||||
if _, err := Apply(p, EvAccepted, false, off); err != nil || p.State != Ready {
|
||||
t.Fatalf("ungated, a delivery with the verdict %+v is %s: %v", check, p.State, err)
|
||||
if _, err := Apply(p, EvAccepted, false, f); err != nil || p.State != Ready {
|
||||
t.Fatalf("approved, a delivery with the verdict %+v is %s: %v", check, p.State, err)
|
||||
}
|
||||
unapproved := &Delivery{ID: "novox/app@c0", Commit: "c0", State: Proposed, Number: 1, Check: check}
|
||||
if _, err := Apply(unapproved, EvAccepted, false, f); err == nil {
|
||||
t.Fatalf("with no Approve, a delivery with the verdict %+v was accepted: the check gates the ask", check)
|
||||
}
|
||||
}
|
||||
stale := &Delivery{ID: "novox/app@c0", State: Ready, Check: &Verdict{Gate: "pass", Trunk: "d28ca95bbbbb"}, TrunkNow: "690966dccccc"}
|
||||
if _, err := Apply(stale, EvTrunkMoved, false, off); err == nil {
|
||||
t.Fatal("ungated, a trunk's move put a ready delivery back to proposed")
|
||||
stale := approved(&Delivery{ID: "novox/app@c0", Commit: "c0", State: Ready, Check: &Verdict{Gate: "pass", Trunk: "d28ca95bbbbb"}, TrunkNow: "690966dccccc"})
|
||||
if _, err := Apply(stale, EvTrunkMoved, false, f); err == nil {
|
||||
t.Fatal("approved, a trunk's move put a ready delivery back to proposed")
|
||||
}
|
||||
stale.Ready = nil
|
||||
if _, err := Apply(stale, EvTrunkMoved, false, f); err != nil || stale.State != Proposed {
|
||||
t.Fatalf("with no Approve, a trunk's move left a ready delivery %s: %v", stale.State, err)
|
||||
}
|
||||
// A ready delivery off the trunk is refused its merge, whatever else is true of it.
|
||||
d := &Delivery{ID: "novox/app@c0", State: Ready}
|
||||
|
||||
@@ -1,461 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
checker "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
|
||||
mdc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
)
|
||||
|
||||
// **One check per delivery** (novox/hq ADR 0548 §2 to §4, issue 568). While a check runs, nothing merges into a repository it
|
||||
// judges, and an approved merge holds a turn on its repositories so no new check starts there:
|
||||
//
|
||||
// - the hold (§2): an approved merge is handed to the forge bridge only when no verdict of the checker's verdicts bucket in
|
||||
// the state running names, among its trunk heads, a repository the merge goes into; the verdict of the merge's own heads
|
||||
// excepted. The gate watches that bucket already (onestatus.go), so the verdict leaving running gives the waiting merge its
|
||||
// turn at once, with no timer. Siblings a check reads beside are not held (§2). Only while mesh/check gates the merge;
|
||||
// - the turn (§3): before it reads the verdicts for a merge, the gate writes one key per repository the merge goes into in its
|
||||
// bucket turns (`<owner>.<name>`, mdc.TurnsKey): the deliveries that wait there in the order of their Approve, the check
|
||||
// each waits for and since when; for a group on every member's repository until its last member merged. The checker reads
|
||||
// the bucket and claims no check of a repository a turn names. The key goes when the last delivery in it merged or left;
|
||||
// - the bound and the tools (§4): a merge held past turnBound is a stalled line; delivery_turns reads every turn and
|
||||
// delivery_hand_now hands a waiting merge at once on the operator's word, the check it waited for then carried.
|
||||
|
||||
// turnBound is how long an approved merge may wait for another delivery's check before it is a stalled line (ADR 0548 §4).
|
||||
const turnBound = 30 * time.Minute
|
||||
|
||||
// TurnStore is the gate's bucket turns: one key per repository, written by the gate alone.
|
||||
type TurnStore interface {
|
||||
// All reads every turn, by repository (lowercase).
|
||||
All() (map[string]Turn, error)
|
||||
Put(t Turn) error
|
||||
Delete(repository string) error
|
||||
}
|
||||
|
||||
// kvTurns is the bucket turns over the runtime.
|
||||
type kvTurns struct{}
|
||||
|
||||
func (kvTurns) All() (map[string]Turn, error) {
|
||||
keys, err := stdio.State("turns").Keys()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := map[string]Turn{}
|
||||
for _, k := range keys {
|
||||
e, err := stdio.State("turns").Get(k)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if e == nil {
|
||||
continue
|
||||
}
|
||||
repository, err := mdc.ParseTurnsKey(k)
|
||||
if err != nil {
|
||||
continue // not a turn's key: never the gate's, left
|
||||
}
|
||||
t, err := DecodeTurn(e.Value)
|
||||
if err != nil {
|
||||
t = Turn{Repository: repository} // unreadable: rewritten or deleted at the next look
|
||||
}
|
||||
out[repository] = t
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (kvTurns) Put(t Turn) error {
|
||||
key, err := mdc.TurnsKey(t.Repository)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
raw, err := EncodeTurn(t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = stdio.State("turns").Put(key, json.RawMessage(raw))
|
||||
return err
|
||||
}
|
||||
|
||||
func (kvTurns) Delete(repository string) error {
|
||||
key, err := mdc.TurnsKey(repository)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return stdio.State("turns").Delete(key)
|
||||
}
|
||||
|
||||
// approvedMerge is an approved merge not yet done: the run holding its Approve, the deliveries it covers, the hash of their
|
||||
// heads as the checker keys their verdict (lowercase, onestatus.go headsOf), and the repositories it goes into (lowercase).
|
||||
type approvedMerge struct {
|
||||
owner *Delivery
|
||||
run *ReadyRun
|
||||
group string
|
||||
heads string
|
||||
repos []string
|
||||
}
|
||||
|
||||
// approvedMerges are the merges the operator approved that are not done, in the order of their Approve: each ask once, its
|
||||
// run on the delivery that asked it. A declined, refused, merged, rejected or final one has left. Under the lock.
|
||||
func (h *Holder) approvedMerges() []approvedMerge {
|
||||
var out []approvedMerge
|
||||
seen := map[string]bool{}
|
||||
ids := make([]string, 0, len(h.deliveries))
|
||||
for id := range h.deliveries {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
sort.Strings(ids) // the same owner of an ask at every look, whichever member's run holds it
|
||||
for _, id := range ids {
|
||||
d := h.deliveries[id]
|
||||
r := d.Ready
|
||||
if r == nil || r.Approved == nil || r.declined() || r.AskID == "" || len(r.Heads) == 0 || seen[r.AskID] ||
|
||||
d.State.Final() || d.State == Rejected || d.Base != MergeBranch || r.RefusedSince != nil || h.allMerged(r.Heads) {
|
||||
continue
|
||||
}
|
||||
seen[r.AskID] = true
|
||||
var heads []mdc.Head
|
||||
repos := map[string]bool{}
|
||||
for id, sha := range r.Heads {
|
||||
m := h.deliveries[id]
|
||||
if m == nil {
|
||||
continue
|
||||
}
|
||||
heads = append(heads, mdc.Head{Repository: strings.ToLower(m.Repository), Number: m.Number, Head: strings.ToLower(sha)})
|
||||
repos[strings.ToLower(m.Repository)] = true
|
||||
}
|
||||
hash, err := mdc.HeadsHash(heads)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
am := approvedMerge{owner: d, run: r, group: askGroup(d), heads: hash}
|
||||
for repo := range repos {
|
||||
am.repos = append(am.repos, repo)
|
||||
}
|
||||
sort.Strings(am.repos)
|
||||
out = append(out, am)
|
||||
}
|
||||
sort.SliceStable(out, func(i, j int) bool {
|
||||
if !out[i].run.Approved.Equal(*out[j].run.Approved) {
|
||||
return out[i].run.Approved.Before(*out[j].run.Approved)
|
||||
}
|
||||
return out[i].heads < out[j].heads
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
// mergeOfDelivery is the approved merge covering d, if one is approved.
|
||||
func (h *Holder) mergeOfDelivery(d *Delivery) (approvedMerge, bool) {
|
||||
c := h.covering(d)
|
||||
if c == nil || c.Approved == nil {
|
||||
return approvedMerge{}, false
|
||||
}
|
||||
for _, am := range h.approvedMerges() {
|
||||
// By the ask, not the run: a group's members may each hold a run of the same ask, and the merge is the ask's.
|
||||
if am.run.AskID == c.AskID {
|
||||
return am, true
|
||||
}
|
||||
}
|
||||
return approvedMerge{}, false
|
||||
}
|
||||
|
||||
// runningCheck is a check of the checker in the state running that names repo among its trunk heads, its heads not own: its
|
||||
// verdict key and since when the gate has seen it run. False when none runs there. Under the lock.
|
||||
func (h *Holder) runningCheck(repo, own string) (key string, v checkerVerdict, since time.Time, ok bool) {
|
||||
hashes := make([]string, 0, len(h.checker))
|
||||
for hash := range h.checker {
|
||||
hashes = append(hashes, hash)
|
||||
}
|
||||
sort.Strings(hashes)
|
||||
for _, hash := range hashes {
|
||||
v := h.checker[hash]
|
||||
if v.State != checkerRunning || v.Heads == own {
|
||||
continue
|
||||
}
|
||||
for r := range v.Trunk {
|
||||
if strings.EqualFold(r, repo) {
|
||||
key, err := checker.VerdictKey(v.Group, v.Heads)
|
||||
if err != nil {
|
||||
key = v.Group + "." + v.Heads
|
||||
}
|
||||
return key, v, h.runningSince[v.Heads], true
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", checkerVerdict{}, time.Time{}, false
|
||||
}
|
||||
|
||||
// turnWaits is what an approved merge waits for before it is handed (ADR 0548 §2, §3): its turns written, then the end of
|
||||
// every check of other heads running on a repository it goes into. Empty when it may be handed. It keeps on the run since
|
||||
// when it is held and for which check, for the stalled line and the tools. Under the lock.
|
||||
func (h *Holder) turnWaits(d *Delivery, now time.Time) string {
|
||||
am, ok := h.mergeOfDelivery(d)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
r := am.run
|
||||
if h.Turns != nil {
|
||||
if !h.turnsLoaded {
|
||||
return "approved; its merge turn waits for the turns the gate wrote before to be read"
|
||||
}
|
||||
for _, repo := range am.repos {
|
||||
if !turnNames(h.turnsHeld[repo], am.heads) {
|
||||
return "approved; its merge turn on " + repo + " is being written, so no check starts there before it merges"
|
||||
}
|
||||
}
|
||||
}
|
||||
if r.HandNow != "" {
|
||||
if r.HeldSince != nil {
|
||||
r.HeldSince, r.HeldFor = nil, ""
|
||||
h.keep(am.owner)
|
||||
}
|
||||
return "" // handed now on the operator's word: the check it waited for is carried after the merge
|
||||
}
|
||||
for _, repo := range am.repos {
|
||||
key, v, since, running := h.runningCheck(repo, am.heads)
|
||||
if !running {
|
||||
continue
|
||||
}
|
||||
if r.HeldSince == nil || r.HeldFor != key {
|
||||
at := now
|
||||
r.HeldSince, r.HeldFor = &at, key
|
||||
h.keep(am.owner)
|
||||
h.Logf("[mesh-delivery] %s is approved and held: the check %s runs on %s, and nothing merges there while it does (ADR 0548 §2)", am.owner.ID, key, repo)
|
||||
}
|
||||
words := fmt.Sprintf("approved; waits for the check of %s (heads %s) on %s", v.Group, v.Heads, repo)
|
||||
if !since.IsZero() {
|
||||
words += ", running since " + since.UTC().Format(time.RFC3339)
|
||||
}
|
||||
return words + " (novox/hq ADR 0548 §2)"
|
||||
}
|
||||
if r.HeldSince != nil {
|
||||
r.HeldSince, r.HeldFor = nil, ""
|
||||
h.keep(am.owner)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// turnNames says whether a turn names the heads.
|
||||
func turnNames(t Turn, heads string) bool {
|
||||
for _, w := range t.Waiting {
|
||||
if w.Heads == heads {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// wantedTurns are the turns the approved merges hold now, by repository: each merge on every repository it goes into, in
|
||||
// the order of their Approve, with the check of other heads it waits for there. Under the lock.
|
||||
func (h *Holder) wantedTurns() map[string]Turn {
|
||||
out := map[string]Turn{}
|
||||
for _, am := range h.approvedMerges() {
|
||||
for _, repo := range am.repos {
|
||||
t := out[repo]
|
||||
t.Repository = repo
|
||||
w := TurnWait{Group: am.group, Heads: am.heads, Ask: am.run.AskID, Approved: am.run.Approved.UTC(), Since: am.run.Approved.UTC()}
|
||||
if key, _, since, ok := h.runningCheck(repo, am.heads); ok {
|
||||
w.WaitsFor = key
|
||||
if !since.IsZero() {
|
||||
w.WaitsSince = since.UTC()
|
||||
}
|
||||
}
|
||||
t.Waiting = append(t.Waiting, w)
|
||||
out[repo] = t
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// turnWrites are the writes that bring the bucket turns to what the approved merges hold: a turn written where it changed,
|
||||
// a key deleted where its last delivery merged or left. The first look reads what a gate before this one wrote. Under the
|
||||
// lock; the writes run outside it, and each that lands is kept and asks for a look.
|
||||
func (h *Holder) turnWrites() []func() {
|
||||
if h.Turns == nil {
|
||||
return nil
|
||||
}
|
||||
if !h.turnsLoaded {
|
||||
return []func(){func() {
|
||||
held, err := h.Turns.All()
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
if err != nil {
|
||||
h.Logf("[mesh-delivery] the merge turns written before cannot be read, so no merge is handed yet: %v; asked again in %s", err, askAgainAfter)
|
||||
h.again("the merge turns written before, read")
|
||||
return
|
||||
}
|
||||
h.turnsHeld, h.turnsLoaded = held, true
|
||||
h.Poke()
|
||||
}}
|
||||
}
|
||||
want := map[string]Turn{}
|
||||
if h.checkGates() {
|
||||
want = h.wantedTurns() // while the check gates no merge, no merge waits for one, and no turn holds a check
|
||||
}
|
||||
var out []func()
|
||||
repos := make([]string, 0, len(want)+len(h.turnsHeld))
|
||||
for repo := range want {
|
||||
repos = append(repos, repo)
|
||||
}
|
||||
for repo := range h.turnsHeld {
|
||||
if _, ok := want[repo]; !ok {
|
||||
repos = append(repos, repo)
|
||||
}
|
||||
}
|
||||
sort.Strings(repos)
|
||||
for _, repo := range repos {
|
||||
t, wanted := want[repo]
|
||||
held, had := h.turnsHeld[repo]
|
||||
switch {
|
||||
case wanted && had && sameTurnValue(held, t):
|
||||
case wanted:
|
||||
out = append(out, func() {
|
||||
err := h.Turns.Put(t)
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
if err != nil {
|
||||
h.Logf("[mesh-delivery] the merge turn on %s cannot be written: %v; asked again in %s", repo, err, askAgainAfter)
|
||||
h.again("a merge turn, written")
|
||||
return
|
||||
}
|
||||
h.turnsHeld[repo] = t
|
||||
h.Poke()
|
||||
})
|
||||
default:
|
||||
out = append(out, func() {
|
||||
err := h.Turns.Delete(repo)
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
if err != nil {
|
||||
h.Logf("[mesh-delivery] the merge turn on %s cannot be deleted: %v; asked again in %s", repo, err, askAgainAfter)
|
||||
h.again("a merge turn, deleted")
|
||||
return
|
||||
}
|
||||
delete(h.turnsHeld, repo)
|
||||
h.Logf("[mesh-delivery] the merge turn on %s is deleted: no approved merge waits on it, so the checker may start a check there", repo)
|
||||
h.Poke()
|
||||
})
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// sameTurnValue says whether two turns hold the same value, the version aside.
|
||||
func sameTurnValue(a, b Turn) bool {
|
||||
a.V, b.V = 0, 0
|
||||
return SameTurn(a, b)
|
||||
}
|
||||
|
||||
// turnLines are the stalled lines of approved merges held past turnBound by another delivery's check (ADR 0548 §4). Under the
|
||||
// lock.
|
||||
func (h *Holder) turnLines(now time.Time) []StalledLine {
|
||||
var out []StalledLine
|
||||
for _, d := range h.deliveries {
|
||||
r := d.Ready
|
||||
if r == nil || r.HeldSince == nil || r.Merged != nil || d.MergedAs != "" || d.State.Final() || now.Sub(*r.HeldSince) < turnBound {
|
||||
continue
|
||||
}
|
||||
words := r.Waits
|
||||
if words == "" {
|
||||
words = "approved; waits for the check " + r.HeldFor
|
||||
}
|
||||
out = append(out, StalledLine{ID: d.ID, Number: d.Number, State: d.State, For: now.Sub(*r.HeldSince).Round(time.Second).String(),
|
||||
Bound: turnBound.String(),
|
||||
H2: "none: the check it waits for ends, or the operator hands the merge now with delivery_hand_now — the operator's",
|
||||
Says: fmt.Sprintf("%s#%d at %s is %s, held since %s; the step that check is at is read with the checker's checker_status",
|
||||
d.Repository, d.Number, shortOf(r.Head, 12), words, r.HeldSince.UTC().Format(time.RFC3339))})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// TurnLine is one repository's turn as delivery_turns reads it: the turn as the gate holds it and, for each delivery waiting,
|
||||
// the check it waits for in words.
|
||||
type TurnLine struct {
|
||||
Repository string `json:"repository"`
|
||||
Turn Turn `json:"turn"`
|
||||
Waits map[string]any `json:"waits,omitempty"`
|
||||
}
|
||||
|
||||
// TurnsRead is the tool delivery_turns: every turn the gate holds, with the check each waiting delivery waits for, since when,
|
||||
// and whether the merge was handed now on the operator's word.
|
||||
func (h *Holder) TurnsRead() map[string]any {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
want := map[string]Turn{}
|
||||
if h.checkGates() {
|
||||
want = h.wantedTurns() // as turnWrites: while the check gates no merge, no turn is written by design
|
||||
}
|
||||
repos := make([]string, 0, len(want))
|
||||
for repo := range want {
|
||||
repos = append(repos, repo)
|
||||
}
|
||||
sort.Strings(repos)
|
||||
lines := []TurnLine{}
|
||||
for _, repo := range repos {
|
||||
l := TurnLine{Repository: repo, Turn: want[repo], Waits: map[string]any{}}
|
||||
for _, w := range want[repo].Waiting {
|
||||
says := "nothing: no check of other heads runs on " + repo
|
||||
if w.WaitsFor != "" {
|
||||
says = "the check " + w.WaitsFor
|
||||
if !w.WaitsSince.IsZero() {
|
||||
says += ", running since " + w.WaitsSince.Format(time.RFC3339)
|
||||
}
|
||||
}
|
||||
l.Waits[w.Group] = says
|
||||
}
|
||||
lines = append(lines, l)
|
||||
}
|
||||
out := map[string]any{"turns": lines, "check-gates-merge": h.checkGates(), "bound": turnBound.String()}
|
||||
if !h.checkGates() {
|
||||
out["why-none"] = "the check gates no merge (check-gates-merge false): no merge waits for a check, so no turn is written"
|
||||
}
|
||||
if h.Turns != nil {
|
||||
out["written"] = h.turnsLoaded
|
||||
var unwritten []string
|
||||
for _, repo := range repos {
|
||||
if held, ok := h.turnsHeld[repo]; !ok || !sameTurnValue(held, want[repo]) {
|
||||
unwritten = append(unwritten, repo)
|
||||
}
|
||||
}
|
||||
for repo := range h.turnsHeld {
|
||||
if _, ok := want[repo]; !ok {
|
||||
unwritten = append(unwritten, repo+" (to delete)")
|
||||
}
|
||||
}
|
||||
sort.Strings(unwritten)
|
||||
if len(unwritten) > 0 {
|
||||
out["not-yet-written"] = unwritten
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// HandNow is the tool delivery_hand_now (ADR 0548 §4): on the operator's word an approved merge held for another delivery's
|
||||
// check is handed at once; that check runs on and is carried to the new trunk after the merge. Refused for a delivery that is
|
||||
// not an approved merge waiting, naming why.
|
||||
func (h *Holder) HandNow(id, why string) (string, error) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
d := h.deliveries[id]
|
||||
if d == nil {
|
||||
return "", fmt.Errorf("no delivery %q", id)
|
||||
}
|
||||
if strings.TrimSpace(why) == "" {
|
||||
return "", fmt.Errorf("say why: the hand is the operator's word, kept on the delivery")
|
||||
}
|
||||
am, ok := h.mergeOfDelivery(d)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("%s is not an approved merge waiting: nothing to hand (its state is %s)", id, d.State)
|
||||
}
|
||||
if am.run.HeldSince == nil {
|
||||
return "", fmt.Errorf("%s waits for no check of another delivery: %s", id, firstOf(am.run.Waits, "it is handed at its turn"))
|
||||
}
|
||||
am.run.HandNow = why
|
||||
h.keep(am.owner)
|
||||
h.mergeAgainNow(d)
|
||||
h.Logf("[mesh-delivery] %s is handed now on the operator's word, past the check %s it waited for, which is carried after the merge: %s",
|
||||
id, am.run.HeldFor, why)
|
||||
return fmt.Sprintf("%s is handed to the merge at the next look; the check %s it waited for runs on and is carried to the new trunk", id, am.run.HeldFor), nil
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package main
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
mdc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
|
||||
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
|
||||
@@ -137,63 +136,3 @@ func SameAsk(a, b Ask) bool {
|
||||
a.Ask, b.Ask = nil, nil
|
||||
return reflect.DeepEqual(a, b) && reflect.DeepEqual(x, y)
|
||||
}
|
||||
|
||||
// Turn is the state turns' value: the approved deliveries waiting to merge in one repository, in the order of the Approve.
|
||||
type Turn struct {
|
||||
V int
|
||||
Repository string
|
||||
Waiting []TurnWait
|
||||
}
|
||||
|
||||
// TurnWait is one approved delivery in a turn; WaitsFor, WaitsSince and Step say the check of another delivery it waits on.
|
||||
type TurnWait struct {
|
||||
Group string
|
||||
Heads string
|
||||
Ask string
|
||||
Approved time.Time
|
||||
Since time.Time
|
||||
WaitsFor string
|
||||
WaitsSince time.Time
|
||||
Step string
|
||||
}
|
||||
|
||||
func (t Turn) stored() seat.TurnsByRepositoryValue {
|
||||
out := seat.TurnsByRepositoryValue{Repository: t.Repository, V: int64(t.V), Waiting: []seat.TurnsByRepositoryValueWaitingItem{}}
|
||||
for _, w := range t.Waiting {
|
||||
item := seat.TurnsByRepositoryValueWaitingItem{Approved: w.Approved, Ask: w.Ask, Group: w.Group, Heads: w.Heads,
|
||||
Since: w.Since, Step: w.Step, WaitsFor: w.WaitsFor}
|
||||
if !w.WaitsSince.IsZero() {
|
||||
since := w.WaitsSince
|
||||
item.WaitsSince = &since
|
||||
}
|
||||
out.Waiting = append(out.Waiting, item)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func turnFromSeat(s seat.TurnsByRepositoryValue) Turn {
|
||||
out := Turn{V: int(s.V), Repository: s.Repository}
|
||||
for _, w := range s.Waiting {
|
||||
item := TurnWait{Group: w.Group, Heads: w.Heads, Ask: w.Ask, Approved: w.Approved, Since: w.Since, WaitsFor: w.WaitsFor, Step: w.Step}
|
||||
if w.WaitsSince != nil {
|
||||
item.WaitsSince = *w.WaitsSince
|
||||
}
|
||||
out.Waiting = append(out.Waiting, item)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// EncodeTurn is t as the value of a turns key.
|
||||
func EncodeTurn(t Turn) ([]byte, error) { return mdc.EncodeTurn(t.stored()) }
|
||||
|
||||
// DecodeTurn reads the value of a turns key.
|
||||
func DecodeTurn(data []byte) (Turn, error) {
|
||||
s, err := mdc.DecodeTurn(data)
|
||||
if err != nil {
|
||||
return Turn{}, err
|
||||
}
|
||||
return turnFromSeat(s), nil
|
||||
}
|
||||
|
||||
// SameTurn says whether two turns are the same value.
|
||||
func SameTurn(a, b Turn) bool { return mdc.SameTurn(a.stored(), b.stored()) }
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
mdc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
|
||||
)
|
||||
@@ -12,8 +11,6 @@ import (
|
||||
// What go/delivery wrote into the seat's states, key order and all, must decode as it did and encode to the same
|
||||
// content (ADR 0634 §2): the gate's working values are converted to the seat's types at the edge.
|
||||
func TestStoredValuesOfTheEarlierLayoutDecodeUnchanged(t *testing.T) {
|
||||
approved := time.Date(2026, 11, 5, 10, 0, 0, 0, time.UTC)
|
||||
since := time.Date(2026, 11, 5, 10, 5, 0, 0, time.UTC)
|
||||
head := mdc.Head{Repository: "novox/app", Number: 7, Head: "0123456789abcdef0123456789abcdef01234567"}
|
||||
rows := []struct {
|
||||
name string
|
||||
@@ -32,13 +29,6 @@ func TestStoredValuesOfTheEarlierLayoutDecodeUnchanged(t *testing.T) {
|
||||
{"ask keeps the router's ask as bytes", `{"v":1,"group":"feat/x","heads":[{"repository":"novox/app","number":7,"head":"` + head.Head + `"}],"ask":{"id":"a","options":["Approve"]},"asking":2}`,
|
||||
func(s string) (any, error) { return DecodeAsk([]byte(s)) }, func(v any) ([]byte, error) { return EncodeAsk(v.(Ask)) },
|
||||
Ask{V: 1, Group: "feat/x", Heads: []mdc.Head{head}, Ask: json.RawMessage(`{"id":"a","options":["Approve"]}`), Asking: 2}},
|
||||
{"turn without and with a check waited for", `{"v":1,"repository":"novox/app","waiting":[` +
|
||||
`{"group":"feat/x","heads":"0123456789ab","ask":"merge.x","approved":"2026-11-05T10:00:00Z","since":"2026-11-05T10:00:00Z"},` +
|
||||
`{"group":"feat/y","heads":"ba9876543210","approved":"2026-11-05T10:00:00Z","since":"2026-11-05T10:00:00Z","waits-for":"feat/x.0123456789ab","waits-since":"2026-11-05T10:05:00Z","step":"unit"}]}`,
|
||||
func(s string) (any, error) { return DecodeTurn([]byte(s)) }, func(v any) ([]byte, error) { return EncodeTurn(v.(Turn)) },
|
||||
Turn{V: 1, Repository: "novox/app", Waiting: []TurnWait{
|
||||
{Group: "feat/x", Heads: "0123456789ab", Ask: "merge.x", Approved: approved, Since: approved},
|
||||
{Group: "feat/y", Heads: "ba9876543210", Approved: approved, Since: approved, WaitsFor: "feat/x.0123456789ab", WaitsSince: since, Step: "unit"}}}},
|
||||
{"source", `{"v":1,"group":"feat/x","heads":{"novox/app":"c0ffee"}}`,
|
||||
func(s string) (any, error) { return mdc.DecodeSource([]byte(s)) }, func(v any) ([]byte, error) { return mdc.EncodeSource(v.(Source)) },
|
||||
Source{V: 1, Group: "feat/x", Heads: map[string]string{"novox/app": "c0ffee"}}},
|
||||
@@ -74,8 +64,5 @@ func sameValue(a, b any) bool {
|
||||
if x, ok := a.(Ask); ok {
|
||||
return SameAsk(x, b.(Ask))
|
||||
}
|
||||
if x, ok := a.(Turn); ok {
|
||||
return SameTurn(x, b.(Turn)) && reflect.DeepEqual(len(x.Waiting), len(b.(Turn).Waiting)) && x.Waiting[len(x.Waiting)-1].WaitsSince.Equal(b.(Turn).Waiting[len(x.Waiting)-1].WaitsSince)
|
||||
}
|
||||
return reflect.DeepEqual(a, b)
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ func (h *Holder) Show(id string) (any, error) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
if d := h.deliveries[id]; d != nil {
|
||||
out := map[string]any{"delivery": d, "waits": h.waitsFor(d), "check-gates-merge": h.checkGates()}
|
||||
out := map[string]any{"delivery": d, "waits": h.waitsFor(d)}
|
||||
if m := h.machines(d); m != "" {
|
||||
out["machines"] = m
|
||||
}
|
||||
@@ -97,7 +97,7 @@ func (h *Holder) Show(id string) (any, error) {
|
||||
if g := h.groups[id]; g != nil {
|
||||
members := h.membersOf(g)
|
||||
state, why := GroupState(g, members)
|
||||
return map[string]any{"group": g, "state": state, "why": why, "members": members, "check-gates-merge": h.checkGates()}, nil
|
||||
return map[string]any{"group": g, "state": state, "why": why, "members": members}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("no delivery or group %q: `deliveries` lists them", id)
|
||||
}
|
||||
@@ -246,7 +246,6 @@ func (h *Holder) Stalled() []StalledLine {
|
||||
}
|
||||
out = append(out, h.withChecks(h.uncheckedLines(now))...)
|
||||
out = append(out, h.readyLines(now)...)
|
||||
out = append(out, h.turnLines(now)...) // an approved merge held past its bound by another delivery's check (ADR 0548 §4)
|
||||
out = append(out, h.groupLines(now)...)
|
||||
out = append(out, h.owedLines(now)...) // an act owed the forge with no outcome past its bound (W11.17)
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID })
|
||||
@@ -316,7 +315,7 @@ func (h *Holder) Status(listening string) map[string]any {
|
||||
byState[string(s)] = n
|
||||
}
|
||||
out := map[string]any{"deliveries": byState, "groups": len(h.groups), "owed": owed, "unkept": len(h.dirty),
|
||||
"events": listening, "started": h.started.Format(time.RFC3339), "check-gates-merge": h.checkGates()}
|
||||
"events": listening, "started": h.started.Format(time.RFC3339)}
|
||||
// The owed effects tried since this owner started (novox/hq issue 407): done, failed and to be tried again,
|
||||
// or given up, and when the last was tried. A count since the start, said with that start: two readings in
|
||||
// one run give a window's tries.
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"refusal-stall-floor-minutes": 10,
|
||||
"author-new-head-minutes": 120,
|
||||
"check-gates-merge": true,
|
||||
"record-retention": 90
|
||||
}
|
||||
|
||||
@@ -316,11 +316,7 @@
|
||||
"name": "source",
|
||||
"history": 64
|
||||
},
|
||||
"conditions",
|
||||
{
|
||||
"name": "turns",
|
||||
"history": 5
|
||||
}
|
||||
"conditions"
|
||||
],
|
||||
"reads": [
|
||||
"mesh-delivery-teller.answers",
|
||||
@@ -328,10 +324,8 @@
|
||||
"mesh-delivery-view.reach"
|
||||
],
|
||||
"tools": [
|
||||
"delivery_hand_now",
|
||||
"delivery_merge_again",
|
||||
"delivery_status",
|
||||
"delivery_turns",
|
||||
"gate_consumer",
|
||||
"gate_record",
|
||||
"gate_record_end",
|
||||
@@ -354,12 +348,6 @@
|
||||
"trusted": false,
|
||||
"default": 120
|
||||
},
|
||||
"check-gates-merge": {
|
||||
"type": "boolean",
|
||||
"description": "Whether mesh/check gates the merge. True (the default): the operator's Approve is asked, and a merge handed, only on a passing verdict judged against the trunk's head, and a trunk's move puts a verdict out of date (ADR 0447, ADR 0505 §4). False (the operator, 2026-10-25 22:56: \"run the check locally only, but not as a gate, it loses us too much time\"; developers run the unit and integration levels locally before a pull request): a ready pull request (no review line is waited for, ADR 0659) has its Approve is asked once the check at its heads (a group: of all its heads together) has a verdict, a pass or a failure said in the ask, or after 60 minutes with none, said so (the operator, 2026-10-27 23:54: \"if you don't want me to approve it, don't send the approval request\"); an approved merge waits for no verdict; no merge is refused for a check judged on an older trunk; a later verdict is shown on the pull request and the delivery, and asks a pending Approve again in its words; a group still merges all its members in one pass. Set it false together with the gitea module's check-gates-merge, which lets its merge tool merge an approved head without a passing mesh/check.",
|
||||
"trusted": false,
|
||||
"default": true
|
||||
},
|
||||
"record-retention": {
|
||||
"type": "number",
|
||||
"description": "days a delivery record is kept from when it opened (design 47 *The delivery record*): ninety covers every trace asked of a past delivery so far; when a record is removed at its retention, the gate says freed for each graph revision it pinned. Nothing else is removed to make room.",
|
||||
@@ -385,7 +373,6 @@
|
||||
"takes-config": [
|
||||
"refusal-stall-floor-minutes",
|
||||
"author-new-head-minutes",
|
||||
"check-gates-merge",
|
||||
"record-retention"
|
||||
],
|
||||
"merge": "json",
|
||||
|
||||
Reference in New Issue
Block a user