Order a branch's plans by its merges, and build the newest commit (hq issue 349)

A merge acted on late by the catch-up made its plan after the plan of the
merge that followed it, superseded it by creation time, and folded its
unbuilt modules into a plan at the older commit: on 2026-10-09 the
forge's security fix (a082615b) was superseded by 8ff8197a. A plan now
keeps its merge time (migration 0086), supersession follows it, and a
merge older than an open plan of its branch is planned at that plan's
commit, which contains it.
This commit is contained in:
2026-10-09 15:00:56 +02:00
committed by jochen
parent 077ddf0eb8
commit 997a4925b0
5 changed files with 184 additions and 8 deletions
+93
View File
@@ -0,0 +1,93 @@
package main
import (
"strings"
"testing"
"time"
"github.com/novox/mesh-controller/internal/catalogue"
"github.com/novox/mesh-controller/internal/inventory"
"github.com/novox/mesh-controller/internal/link"
)
// novox/hq issue 349: on 2026-10-09 the plan of mesh-catalog at a082615b (the merge of a security fix to the
// forge's module) was "superseded at tier 0 by" the plan at 8ff8197a, the merge before it, which the
// catch-up acted on late; what the later plan had not built was folded into a plan at the commit before the
// fix. Plans of one branch are ordered by when the forge made their merges, and a merge older than an open
// plan of its branch is planned at that plan's commit.
// TestTwoMergesActedOnInReverseOrderBuildTheNewerCommit replays it: the later merge acted on first, the
// earlier one second (the catch-up). One plan is left open, at the later commit, and it builds both.
func TestTwoMergesActedOnInReverseOrderBuildTheNewerCommit(t *testing.T) {
open := aCatalogueMesh(t)
ctx := t.Context()
asksWithPaths(t)
for _, m := range []string{"gitea", "notes"} {
if err := open.inventory.RegisterModule(ctx, catalogue.Manifest{Module: m, Version: "1"},
inventory.Source{Repository: "novox/mesh-catalog", Seat: "git", Path: "modules/" + m, Ref: "main",
BuiltFrom: "c0", Head: "c0"}); err != nil {
t.Fatal(err)
}
}
at := time.Now().UTC().Add(-20 * time.Minute).Truncate(time.Second)
fix := link.SourceMoved{Owner: "novox", Repo: "mesh-catalog", Base: "main", Commit: "a082615bfix",
MergedAt: at.Add(2 * time.Minute).Format(time.RFC3339),
Paths: []string{"modules/gitea/module.json"}, ModuleDirs: []string{"modules/gitea"}, ModuleDirsSaid: true}
before := link.SourceMoved{Owner: "novox", Repo: "mesh-catalog", Base: "main", Commit: "8ff8197abefore",
MergedAt: at.Format(time.RFC3339),
Paths: []string{"modules/notes/module.json"}, ModuleDirs: []string{"modules/notes"}, ModuleDirsSaid: true}
for _, m := range []link.SourceMoved{fix, before} {
if err := (following{open: open}).SourceMoved(ctx, m); err != nil {
t.Fatal(err)
}
}
plans, err := open.inventory.OpenPlans(ctx)
if err != nil {
t.Fatal(err)
}
if len(plans) != 1 {
var said []string
for _, p := range plans {
said = append(said, p.ID+" "+p.Commit+" "+p.Note)
}
t.Fatalf("open plans: %s", strings.Join(said, "; "))
}
p := plans[0]
if p.Commit != fix.Commit {
t.Fatalf("the open plan builds %s, not the newer commit %s", p.Commit, fix.Commit)
}
for _, m := range []string{"gitea", "notes"} {
if _, has := p.Modules[m]; !has {
t.Fatalf("the open plan at the newer commit does not build %s: %v", m, p.Modules)
}
}
}
// Pure: the branch's order is the merges', where both plans know it; and a merge older than an open plan
// of its branch finds it.
func TestTheBranchOrderIsTheMerges(t *testing.T) {
t0 := time.Date(2026, 10, 9, 10, 0, 0, 0, time.UTC)
newerMerge := inventory.Plan{ID: "plan-1", Repository: "novox/mesh-catalog", Branch: "main", Commit: "a082615b",
Merged: t0.Add(time.Minute), Created: t0.Add(2 * time.Minute), State: inventory.PlanRolling}
olderMerge := inventory.Plan{ID: "plan-2", Repository: "novox/mesh-catalog", Branch: "main", Commit: "8ff8197a",
Merged: t0, Created: t0.Add(10 * time.Minute), State: inventory.PlanBuilding}
if earlierOnTheBranch(newerMerge, olderMerge) || !earlierOnTheBranch(olderMerge, newerMerge) {
t.Fatal("ordered by when the plans were made, not by when the merges were")
}
if _, closed := supersededBy(olderMerge, []inventory.Plan{newerMerge}, func(string) bool { return true }); len(closed) != 0 {
t.Fatalf("the plan of an older merge superseded a newer one: %s", closed[0].Note)
}
unknown := newerMerge
unknown.Merged = time.Time{}
if !earlierOnTheBranch(unknown, olderMerge) {
t.Fatal("without a merge time the plans' own order does not stand")
}
m := link.SourceMoved{Owner: "novox", Repo: "mesh-catalog", Base: "main", Commit: "8ff8197a", MergedAt: t0.Format(time.RFC3339)}
if p, ok := newestOnTheBranch(m, []inventory.Plan{newerMerge}); !ok || p.ID != "plan-1" {
t.Fatalf("the newer open plan of the branch was not found: %v %+v", ok, p)
}
m.Base = "release"
if _, ok := newestOnTheBranch(m, []inventory.Plan{newerMerge}); ok {
t.Fatal("another branch's plan was taken")
}
}
+44 -1
View File
@@ -188,11 +188,13 @@ func planOfMerge(m link.SourceMoved, moved []string, edges []inventory.Edge) inv
for _, name := range set {
modules[name] = &inventory.PlanModule{}
}
merged, _ := time.Parse(time.RFC3339, m.MergedAt)
return inventory.Plan{
ID: fmt.Sprintf("plan-%d", time.Now().UnixNano()),
Repository: m.Owner + "/" + m.Repo,
Branch: m.Base,
Commit: m.Commit,
Merged: merged.UTC(),
Created: time.Now().UTC(),
State: inventory.PlanBuilding,
Tiers: tiers,
@@ -220,12 +222,20 @@ func planOfMerge(m link.SourceMoved, moved []string, edges []inventory.Edge) inv
//
// A plan with no branch recorded is from before branches were kept, and is superseded by the next
// plan of its repository: what it had not built is folded in, so nothing is lost by it.
//
// **Newer is the branch's order, not the plans'** (novox/hq issue 349). A merge the bus did not hand
// over is acted on late, by the catch-up, so its plan is made after the plan of a merge that came after
// it — and that later-made plan of the earlier commit superseded the later merge's, and built what it
// folded in from the commit before the later merge: twice on 2026-10-09, once a security fix. So where
// both plans know when their merge was made, that decides; only where one does not do the plans' own
// times. A merge older than an open plan of its branch never reaches here at its own commit: it is
// planned at the newest open plan's commit, which contains it (newestOnTheBranch).
func supersededBy(newer inventory.Plan, open []inventory.Plan, rollsOut func(string) bool) ([]string, []inventory.Plan) {
folded := map[string]bool{}
var closed []inventory.Plan
for _, old := range open {
if old.ID == newer.ID || !old.Open() || !strings.EqualFold(old.Repository, newer.Repository) ||
(old.Branch != "" && old.Branch != newer.Branch) || !old.Created.Before(newer.Created) {
(old.Branch != "" && old.Branch != newer.Branch) || !earlierOnTheBranch(old, newer) {
continue
}
var took []string
@@ -254,6 +264,39 @@ func supersededBy(newer inventory.Plan, open []inventory.Plan, rollsOut func(str
return out, closed
}
// earlierOnTheBranch says plan a answers a merge made before b's: by when the forge made each merge where
// both are known, else by when each plan was made. A merge's own plan made again at the same commit
// (the same merge time) is ordered by when it was made. Pure.
func earlierOnTheBranch(a, b inventory.Plan) bool {
if !a.Merged.IsZero() && !b.Merged.IsZero() && !a.Merged.Equal(b.Merged) {
return a.Merged.Before(b.Merged)
}
return a.Created.Before(b.Created)
}
// newestOnTheBranch is the open plan of m's repository and branch whose merge was made after m's, the
// newest of them; false when none was. Merges into one branch are made one on the other, so that plan's
// commit contains m's change: m is planned there, and the newest commit of the branch is what is built
// (novox/hq issue 349). Pure.
func newestOnTheBranch(m link.SourceMoved, open []inventory.Plan) (inventory.Plan, bool) {
merged, err := time.Parse(time.RFC3339, m.MergedAt)
if err != nil {
return inventory.Plan{}, false
}
var newest inventory.Plan
found := false
for _, p := range open {
if !p.Open() || p.Release != nil || p.Merged.IsZero() || !strings.EqualFold(p.Repository, m.Owner+"/"+m.Repo) ||
p.Branch != m.Base || !p.Merged.After(merged) {
continue
}
if !found || p.Merged.After(newest.Merged) {
newest, found = p, true
}
}
return newest, found
}
// gates is what the next tier needs running from this one: a module of the tier that a later
// tier is built by — the runtime dependency — and whose policy rolls it out, must be applied by
// the machines running it before the next tier is asked. A base an image stands on need only be
+14
View File
@@ -345,6 +345,20 @@ func (f following) SourceMoved(ctx context.Context, m link.SourceMoved) error {
e.Manifest.Module, m.Owner, m.Repo, e.Source.Ref, m.Base)
}
}
// **A merge older than an open plan of its branch is planned at that plan's commit** (novox/hq issue
// 349): the catch-up acts on a merge the bus did not hand over after the merges that came after it,
// and planned at its own commit it built what the later merge had fixed from the commit before the fix.
// The later commit contains this merge's change, so what this merge moved is built from there, and the
// plan made here supersedes that one as any newer plan does.
if open, err := inv.OpenPlans(ctx); err == nil {
if newest, later := newestOnTheBranch(m, open); later {
fmt.Printf(" %s/%s %.8s was merged before %.8s, which %s is planning: what it moved is built "+
"from %.8s, which contains it\n", m.Owner, m.Repo, m.Commit, newest.Commit, newest.ID, newest.Commit)
m.Commit, m.MergedAt = newest.Commit, newest.Merged.Format(time.RFC3339)
}
} else {
return notNow(err)
}
touched, added, _ := touchedBy(from, entries, m)
// **A module the merge deleted is not built** (novox/hq ADR 0236): its manifest is gone, so the build
// seat finds nothing saying what it is, and the plan failed on it (`has no module.json at …`) with