From 001498411610f6a29f5357e1e73251c7b53f3a35 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 28 Sep 2026 05:12:37 +0200 Subject: [PATCH] An older merge does not move a source The forge announces what it finds merged, and an old merge surfacing late moved the recorded head backwards and rebuilt everything built from that repository, once per old merge. A merge made before the source was last seen is history; one that says nothing about when is taken as news. The catalogue now carries when each source was last seen. The bus-records test follows #116: a module's tools are every one under its own name. --- cmd/mesh-controller/order_test.go | 16 ++++++++++++++++ cmd/mesh-controller/upgrades.go | 20 ++++++++++++++++++++ internal/inventory/busrecords_test.go | 4 +++- internal/inventory/catalogue.go | 12 ++++++++++-- internal/link/events.go | 2 ++ 5 files changed, 51 insertions(+), 3 deletions(-) diff --git a/cmd/mesh-controller/order_test.go b/cmd/mesh-controller/order_test.go index 9a78a07..72d4373 100644 --- a/cmd/mesh-controller/order_test.go +++ b/cmd/mesh-controller/order_test.go @@ -3,6 +3,7 @@ package main import ( "strings" "testing" + "time" "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/inventory" @@ -90,3 +91,18 @@ func TestAMergeMatchesTheSourcesBuiltFromIt(t *testing.T) { } } } + +// A merge made before the source was last seen is history: it does not move the source, and a +// merge that says nothing about when it was made is taken as news. +func TestAMergeOlderThanTheLastLookIsHistory(t *testing.T) { + seen := time.Date(2026, 9, 28, 3, 0, 0, 0, time.UTC) + if !isHistory("2026-09-28T02:00:00Z", seen) { + t.Fatal("an older merge was taken as news") + } + if isHistory("2026-09-28T04:00:00Z", seen) { + t.Fatal("a newer merge was taken as history") + } + if isHistory("", seen) || isHistory("2026-09-28T02:00:00Z", time.Time{}) { + t.Fatal("a merge or a source with no time on it was refused") + } +} diff --git a/cmd/mesh-controller/upgrades.go b/cmd/mesh-controller/upgrades.go index 5b78207..56368ec 100644 --- a/cmd/mesh-controller/upgrades.go +++ b/cmd/mesh-controller/upgrades.go @@ -240,6 +240,13 @@ func (f following) SourceMoved(ctx context.Context, m link.SourceMoved) error { if e.Source.BuiltFrom == m.Commit { continue } + // **A merge older than the last look at the source is history, not a move.** The forge + // announces what it finds merged, and an old merge surfacing late would otherwise move the + // recorded head backwards and rebuild everything built from that repository, once per old + // merge (2026-09-28). + if isHistory(m.MergedAt, e.Source.Seen) { + continue + } if err := inv.SourceMoved(ctx, e.Manifest.Module, m.Commit); err != nil { return notNow(err) } @@ -362,3 +369,16 @@ func standsOnModule(e inventory.Entry, module string, against map[string][]strin } return false } + +// isHistory is whether a merge made at mergedAt predates the last time the source was seen. A merge +// with no time on it is taken as news: refusing it would silence a forge that says less. +func isHistory(mergedAt string, seen time.Time) bool { + if mergedAt == "" || seen.IsZero() { + return false + } + at, err := time.Parse(time.RFC3339, mergedAt) + if err != nil { + return false + } + return at.Before(seen) +} diff --git a/internal/inventory/busrecords_test.go b/internal/inventory/busrecords_test.go index 24185e3..7f61cfe 100644 --- a/internal/inventory/busrecords_test.go +++ b/internal/inventory/busrecords_test.go @@ -86,9 +86,11 @@ func TestAnAssignedModuleBecomesAUserWithWhatItDeclared(t *testing.T) { if err != nil { t.Fatal(err) } + // Its tools are every one under its own name — the list in the manifest is a person's + // vocabulary for asking, not the module's permission to answer. if !granted(perms.Publish, "mesh.mod.shop.event.order.placed") || !granted(perms.Publish, "mesh.seat.telegram-sender.accept.send") || - !granted(perms.Subscribe, "mesh.mod.shop.tool.price") { + !granted(perms.Subscribe, "mesh.mod.shop.tool.>") { t.Fatalf("one.shop's authority is not what it declared: %+v", perms) } } diff --git a/internal/inventory/catalogue.go b/internal/inventory/catalogue.go index 51eaa02..f63597b 100644 --- a/internal/inventory/catalogue.go +++ b/internal/inventory/catalogue.go @@ -8,6 +8,7 @@ import ( "sort" "strconv" "strings" + "time" "github.com/jackc/pgx/v5" "github.com/novox/mesh-controller/internal/catalogue" @@ -38,6 +39,9 @@ type Source struct { BuiltFrom string // Head is the newest commit the source is known to have. Head string + // Seen is when the source was last looked at — by a build, by hand, or by the forge saying it + // moved. What a late report of an older move is judged against. + Seen time.Time } // Current reports whether what the mesh holds is what the source last had. @@ -936,12 +940,13 @@ func (i *Inventory) Catalogued(ctx context.Context) ([]Entry, error) { `select m.name, m.manifest, coalesce(m.source, ''), m.source_path, m.source_seat, coalesce(m.ref, ''), coalesce(m.built_from, ''), coalesce(m.source_head, ''), + coalesce(m.source_seen, to_timestamp(0)), coalesce(array_agg(n.name order by n.name) filter (where n.name is not null), '{}') from module m left join assignment a on a.module = m.name left join node n on n.id = a.node group by m.name, m.manifest, m.source, m.source_path, m.source_seat, m.ref, m.built_from, - m.source_head + m.source_head, m.source_seen order by m.name`) if err != nil { return nil, err @@ -955,9 +960,12 @@ func (i *Inventory) Catalogued(ctx context.Context) ([]Entry, error) { var source Source var on []string if err := rows.Scan(&name, &raw, &source.Repository, &source.Path, &source.Seat, &source.Ref, - &source.BuiltFrom, &source.Head, &on); err != nil { + &source.BuiltFrom, &source.Head, &source.Seen, &on); err != nil { return nil, err } + if source.Seen.Unix() == 0 { + source.Seen = time.Time{} + } var m catalogue.Manifest if err := json.Unmarshal(raw, &m); err != nil { return nil, err diff --git a/internal/link/events.go b/internal/link/events.go index 408b0fd..716bc2a 100644 --- a/internal/link/events.go +++ b/internal/link/events.go @@ -128,6 +128,8 @@ type SourceMoved struct { Commit string `json:"merge_commit_sha"` CloneURL string `json:"clone_url"` HTMLURL string `json:"html_url"` + // MergedAt is when the forge merged it, RFC 3339. What decides whether this is news. + MergedAt string `json:"merged_at"` } type Upgraded struct { -- 2.54.0