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.
This commit is contained in:
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/novox/mesh-controller/internal/catalogue"
|
"github.com/novox/mesh-controller/internal/catalogue"
|
||||||
"github.com/novox/mesh-controller/internal/inventory"
|
"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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -240,6 +240,13 @@ func (f following) SourceMoved(ctx context.Context, m link.SourceMoved) error {
|
|||||||
if e.Source.BuiltFrom == m.Commit {
|
if e.Source.BuiltFrom == m.Commit {
|
||||||
continue
|
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 {
|
if err := inv.SourceMoved(ctx, e.Manifest.Module, m.Commit); err != nil {
|
||||||
return notNow(err)
|
return notNow(err)
|
||||||
}
|
}
|
||||||
@@ -362,3 +369,16 @@ func standsOnModule(e inventory.Entry, module string, against map[string][]strin
|
|||||||
}
|
}
|
||||||
return false
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -86,9 +86,11 @@ func TestAnAssignedModuleBecomesAUserWithWhatItDeclared(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
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") ||
|
if !granted(perms.Publish, "mesh.mod.shop.event.order.placed") ||
|
||||||
!granted(perms.Publish, "mesh.seat.telegram-sender.accept.send") ||
|
!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)
|
t.Fatalf("one.shop's authority is not what it declared: %+v", perms)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
"github.com/novox/mesh-controller/internal/catalogue"
|
"github.com/novox/mesh-controller/internal/catalogue"
|
||||||
@@ -38,6 +39,9 @@ type Source struct {
|
|||||||
BuiltFrom string
|
BuiltFrom string
|
||||||
// Head is the newest commit the source is known to have.
|
// Head is the newest commit the source is known to have.
|
||||||
Head string
|
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.
|
// 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,
|
`select m.name, m.manifest,
|
||||||
coalesce(m.source, ''), m.source_path, m.source_seat, coalesce(m.ref, ''),
|
coalesce(m.source, ''), m.source_path, m.source_seat, coalesce(m.ref, ''),
|
||||||
coalesce(m.built_from, ''), coalesce(m.source_head, ''),
|
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), '{}')
|
coalesce(array_agg(n.name order by n.name) filter (where n.name is not null), '{}')
|
||||||
from module m
|
from module m
|
||||||
left join assignment a on a.module = m.name
|
left join assignment a on a.module = m.name
|
||||||
left join node n on n.id = a.node
|
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,
|
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`)
|
order by m.name`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -955,9 +960,12 @@ func (i *Inventory) Catalogued(ctx context.Context) ([]Entry, error) {
|
|||||||
var source Source
|
var source Source
|
||||||
var on []string
|
var on []string
|
||||||
if err := rows.Scan(&name, &raw, &source.Repository, &source.Path, &source.Seat, &source.Ref,
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if source.Seen.Unix() == 0 {
|
||||||
|
source.Seen = time.Time{}
|
||||||
|
}
|
||||||
var m catalogue.Manifest
|
var m catalogue.Manifest
|
||||||
if err := json.Unmarshal(raw, &m); err != nil {
|
if err := json.Unmarshal(raw, &m); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -128,6 +128,8 @@ type SourceMoved struct {
|
|||||||
Commit string `json:"merge_commit_sha"`
|
Commit string `json:"merge_commit_sha"`
|
||||||
CloneURL string `json:"clone_url"`
|
CloneURL string `json:"clone_url"`
|
||||||
HTMLURL string `json:"html_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 {
|
type Upgraded struct {
|
||||||
|
|||||||
Reference in New Issue
Block a user