Merge pull request 'mesh-delivery: a closed pull request's check builds go, and a walk says what each build did (ADR 0350, issue 649)' (#282) from feat/350-one-build-per-change into main

This commit was merged in pull request #282.
This commit is contained in:
2026-10-13 21:30:30 +00:00
7 changed files with 98 additions and 2 deletions
@@ -0,0 +1,50 @@
package main
import (
"reflect"
"strings"
"testing"
"time"
)
// One build per change (novox/hq ADR 0350): a pull request closed unmerged has its check builds let go of, and a
// walk's record says per module whether its build reused a check's build or built, and why.
// A pull request closed unmerged asks the controller to let go of what its checks built for its merge (§5); one
// the controller cannot answer is said, never fatal.
func TestAPullRequestClosedUnmergedLetsGoOfItsCheckBuilds(t *testing.T) {
w := newWorldOver(t, newMemStore(), newFakeController(), newFakeForge(), atCC)
cc := pr(cat, 101, headCC, "feat/claude-code", "modules/claude-code/module.json")
w.h.PullUpdated(cc)
w.h.PullClosed(cc)
if want := []string{"novox/mesh-catalog#101"}; !reflect.DeepEqual(w.ctl.forgot, want) {
t.Fatalf("closed unmerged, the controller was asked to let go of %v", w.ctl.forgot)
}
if s := w.state(IDOf(cat, headCC)); s != Stopped {
t.Fatalf("closed unmerged it is %s", s)
}
w.ctl.down = true
du := pr(cat, 102, headDu, "feat/dunst", "modules/dunst/module.json")
w.h.PullUpdated(du)
w.h.PullClosed(du)
if s := w.state(IDOf(cat, headDu)); s != Stopped {
t.Fatalf("with the controller down, a closed delivery is %s", s)
}
}
// The walk's record says per module what its build did, and the delivery keeps it beside its walk (§4).
func TestAWalksRecordSaysWhetherEachModuleWasReusedOrBuilt(t *testing.T) {
walk := catWalk("plan-1", mergeCC, atCC.Add(time.Minute), "claude-code", "dunst")
walk.Modules["claude-code"].Made = "reused from the check of novox/mesh-catalog#101 at head cccccccccccc " +
"(merged into trunk 553b71910000): tools sha256:aa"
walk.Modules["dunst"].Made = "built: no check of novox/mesh-catalog#101 built it"
seen := walk.Seen()
if !strings.HasPrefix(seen.Built["claude-code"], "reused from the check of novox/mesh-catalog#101") ||
seen.Built["dunst"] != "built: no check of novox/mesh-catalog#101 built it" {
t.Fatalf("the delivery keeps %v", seen.Built)
}
if catWalk("plan-2", mergeCC, atCC, "dunst").Seen().Built != nil {
t.Fatal("a walk from a controller that says nothing of its builds reads as if it said something")
}
}
@@ -110,6 +110,18 @@ type fakeController struct {
onCheck func() onCheck func()
// planned is what Plan answers instead of "builds app", when set. // planned is what Plan answers instead of "builds app", when set.
planned *DeliveryPlan planned *DeliveryPlan
// forgot are the pull requests whose check builds were let go of (novox/hq ADR 0350 §5), "owner/repo#n".
forgot []string
}
func (c *fakeController) ForgetChecks(owner, repo string, number int) error {
c.mu.Lock()
defer c.mu.Unlock()
if c.down {
return errors.New("down")
}
c.forgot = append(c.forgot, fmt.Sprintf("%s/%s#%d", owner, repo, number))
return nil
} }
func newFakeController() *fakeController { func newFakeController() *fakeController {
@@ -593,9 +593,12 @@ func verdictOfState(s string) string {
} }
// PullClosed takes a pull request closed unmerged: its open deliveries are stopped. // PullClosed takes a pull request closed unmerged: its open deliveries are stopped.
//
// **And what its checks built for its merge goes** (novox/hq ADR 0350 §5): nothing will take it now. Asked of the
// controller once the deliveries are settled, outside the lock; a refusal is said, and the build seat's holders let
// go of it within the week anyway.
func (h *Holder) PullClosed(p PullEvent) { func (h *Holder) PullClosed(p PullEvent) {
h.mu.Lock() h.mu.Lock()
defer h.mu.Unlock()
for _, d := range h.deliveries { for _, d := range h.deliveries {
if d.Repository == p.repository() && d.Number == p.Number && !d.State.Final() && d.MergedAs == "" { if d.Repository == p.repository() && d.Number == p.Number && !d.State.Final() && d.MergedAs == "" {
d.ClosedUnmerged = true d.ClosedUnmerged = true
@@ -605,6 +608,16 @@ func (h *Holder) PullClosed(p PullEvent) {
} }
} }
} }
h.mu.Unlock()
if h.Controller == nil || p.Owner == "" || p.Repo == "" || p.Number <= 0 {
return
}
if err := h.Controller.ForgetChecks(p.Owner, p.Repo, p.Number); err != nil {
h.mu.Lock()
h.refuse(fmt.Errorf("%s#%d closed, and its check builds stay until the week's sweep: %w", p.repository(),
p.Number, err))
h.mu.Unlock()
}
} }
// CheckedEvent is the controller's `checked`. // CheckedEvent is the controller's `checked`.
@@ -61,7 +61,7 @@ func TestItClaimsTheDeliverySeatAndFollowsWhatItHandles(t *testing.T) {
} }
// It calls exactly what its ports ask: the controller's six verbs and the forge's four tools. // It calls exactly what its ports ask: the controller's six verbs and the forge's four tools.
want := []string{} want := []string{}
for _, v := range []string{"delivery-plan", "delivery-order", "delivery-check", "deliver", "delivery-stop", "delivery-walks"} { for _, v := range []string{"delivery-plan", "delivery-order", "delivery-check", "deliver", "delivery-stop", "delivery-walks", "forget-checks"} {
want = append(want, "seat:"+ControllerSeat+"."+v) want = append(want, "seat:"+ControllerSeat+"."+v)
} }
want = append(want, "gitea.gitea_note_append", "gitea.gitea_delivery_view", "gitea.gitea_commit_status", want = append(want, "gitea.gitea_note_append", "gitea.gitea_delivery_view", "gitea.gitea_commit_status",
@@ -402,6 +402,9 @@ type WalkModule struct {
Build string `json:"build,omitempty"` Build string `json:"build,omitempty"`
Gate *WalkGate `json:"gate,omitempty"` Gate *WalkGate `json:"gate,omitempty"`
GatedBy string `json:"gated_by,omitempty"` GatedBy string `json:"gated_by,omitempty"`
// Made is what the walk's build of it did (novox/hq ADR 0350 §4): "reused from <check build>", naming the pull
// request's head, the trunk commit and the digests, or "built: <why>". Empty from a controller before it.
Made string `json:"made,omitempty"`
} }
// WalkGate is a module's judging on its first machine. // WalkGate is a module's judging on its first machine.
@@ -592,6 +595,9 @@ type WalkSeen struct {
// Unchanged are the modules the walk said matched what runs, with why (novox/hq issue 611): not built, and // Unchanged are the modules the walk said matched what runs, with why (novox/hq issue 611): not built, and
// nothing for it to deliver. // nothing for it to deliver.
Unchanged map[string]string `json:"unchanged,omitempty"` Unchanged map[string]string `json:"unchanged,omitempty"`
// Built is, per module, what the walk's build of it did (novox/hq ADR 0350 §4): "reused from <check build>" or
// "built: <why>" — what `show` says beside the build phase.
Built map[string]string `json:"built,omitempty"`
} }
// Seen is a walk as a delivery keeps it. // Seen is a walk as a delivery keeps it.
@@ -616,6 +622,12 @@ func (w Walk) Seen() *WalkSeen {
if m != nil && (m.SentAt != nil || m.Gate != nil && m.Gate.Verdict == "passed") { if m != nil && (m.SentAt != nil || m.Gate != nil && m.Gate.Verdict == "passed") {
s.Moved = append(s.Moved, name) s.Moved = append(s.Moved, name)
} }
if m != nil && m.Made != "" {
if s.Built == nil {
s.Built = map[string]string{}
}
s.Built[name] = m.Made
}
} }
sort.Strings(s.Moved) sort.Strings(s.Moved)
s.MovedSaid = true s.MovedSaid = true
@@ -5,6 +5,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"sort" "sort"
"strconv"
"strings" "strings"
stdio "git.novox.be/novox/mesh-sdk/go" stdio "git.novox.be/novox/mesh-sdk/go"
@@ -70,6 +71,8 @@ type Controller interface {
StopWithout(walk, why, by string, without []string) (StopAnswer, error) StopWithout(walk, why, by string, without []string) (StopAnswer, error)
// Walks are the walks the controller keeps — one, given its id — and whether this seat has a holder on record. // Walks are the walks the controller keeps — one, given its id — and whether this seat has a holder on record.
Walks(walk string) (bool, []Walk, error) Walks(walk string) (bool, []Walk, error)
// ForgetChecks lets go of the builds a pull request's merge checks made for its merge (novox/hq ADR 0350 §5).
ForgetChecks(owner, repo string, number int) error
} }
// Forge is what the forge's holder does for a delivery. // Forge is what the forge's holder does for a delivery.
@@ -271,6 +274,11 @@ func (c seatController) Deliver(walk, why string) error {
return err return err
} }
func (c seatController) ForgetChecks(owner, repo string, number int) error {
_, err := c.verb("forget-checks", map[string]any{"owner": owner, "repo": repo, "number": strconv.Itoa(number)})
return err
}
func (c seatController) Stop(walk, why, by string) error { func (c seatController) Stop(walk, why, by string) error {
_, err := c.verb("delivery-stop", map[string]any{"plan": walk, "why": why, "by": by}) _, err := c.verb("delivery-stop", map[string]any{"plan": walk, "why": why, "by": by})
return err return err
+1
View File
@@ -41,6 +41,7 @@
"seat:mesh-controller.deliver", "seat:mesh-controller.deliver",
"seat:mesh-controller.delivery-stop", "seat:mesh-controller.delivery-stop",
"seat:mesh-controller.delivery-walks", "seat:mesh-controller.delivery-walks",
"seat:mesh-controller.forget-checks",
"gitea.gitea_note_append", "gitea.gitea_note_append",
"gitea.gitea_delivery_view", "gitea.gitea_delivery_view",
"gitea.gitea_commit_status", "gitea.gitea_commit_status",