Gate: seed source.main from the latest announcement's base head, not the newest delivery's (issue 1422)

A delivery made earlier can be announced again after a newer one, with main's head moved on: its base_sha is the freshest.
The gate now keeps when each base_sha was announced and seeds from the latest.
This commit is contained in:
2026-10-28 11:05:38 +01:00
parent 0c6b7b5d64
commit e057670bc4
4 changed files with 44 additions and 10 deletions
@@ -345,8 +345,8 @@ func (h *Holder) pullUpdated(p PullEvent) (isNew bool) {
// Announced again: nothing new — but for a delivery whose verdict is out of date, the announcement is its
// check asked again against the trunk's head (the merge tool's ask, novox/hq ADR 0323 §5), and its bound
// runs from now (§7). The base head it was announced with is kept, for a check this owner asks after it.
if p.BaseSHA != "" && d.BaseSHA != p.BaseSHA {
d.BaseSHA = p.BaseSHA
if p.BaseSHA != "" {
d.BaseSHA, d.BaseSHAAt = p.BaseSHA, now
h.keep(d)
}
// A delivery made from its verdict before its head was heard (issue 1726) holds none of what an announcement
@@ -441,7 +441,7 @@ func (h *Holder) fillFromAnnouncement(d *Delivery, p PullEvent) {
d.ReviewedBy, d.ReviewedAt = ReviewIn(p.Body)
d.PushedBy = PushedIn(p.Body)
if p.BaseSHA != "" {
d.BaseSHA = p.BaseSHA
d.BaseSHA, d.BaseSHAAt = p.BaseSHA, h.Now()
}
d.Number = p.Number
h.Logf("[mesh-delivery] %s: its pull request's announcement filled its base (%s), branch and title", d.ID, p.Base)
@@ -560,6 +560,9 @@ func (h *Holder) freshAnnouncement(p PullEvent, now time.Time, startedOver strin
Title: p.Title, Base: p.Base, Branch: p.Head, HTMLURL: p.HTMLURL, CloneURL: p.CloneURL, Paths: p.Paths,
PathsTruncated: p.PathsTruncated, Removed: p.Removed, ModuleDirs: p.ModuleDirs, ModuleDirsSaid: p.ModuleDirsSaid,
After: AfterIn(p.Body), Issues: IssuesIn(p.Title + "\n" + p.Body), Created: now, BaseSHA: p.BaseSHA}
if p.BaseSHA != "" {
d.BaseSHAAt = now
}
d.ReviewedBy, d.ReviewedAt = ReviewIn(p.Body)
d.PushedBy = PushedIn(p.Body)
why := fmt.Sprintf("%s#%d's head announced", d.Repository, d.Number)
@@ -55,12 +55,12 @@ func TestStartingWritesTheNewestMergeOfEachRepositoryNotYetInSourceMain(t *testi
const never, trunkN, trunkM = "novox/never-merged", "eeee000000000000000000000000000000000001", "eeee000000000000000000000000000000000002"
s := newWorld(t)
s.source.main = &sdk.Source{V: sdk.Version, Group: "before", Heads: map[string]string{i1726Repo: i1726Merge1}}
other := pr("novox/other-base", 6, "ffff000000000000000000000000000000000001", "fix/c", "a.go")
other := pr("novox/other-base", 6, "f1f1000000000000000000000000000000000001", "fix/c", "a.go")
other.Base, other.BaseSHA = "nox-mesh", trunkM
s.h.PullUpdated(other)
unread := pr("novox/unread", 2, "ffff000000000000000000000000000000000002", "fix/d", "a.go")
unread := pr("novox/unread", 2, "f2f2000000000000000000000000000000000002", "fix/d", "a.go")
s.h.PullUpdated(unread)
seed := pr(never, 4, "ffff000000000000000000000000000000000003", "fix/e", "a.go")
seed := pr(never, 4, "f3f3000000000000000000000000000000000003", "fix/e", "a.go")
seed.BaseSHA = trunkN
s.h.PullUpdated(seed)
s.settleAll()
@@ -70,7 +70,7 @@ func TestStartingWritesTheNewestMergeOfEachRepositoryNotYetInSourceMain(t *testi
if !strings.Contains(s.logs(), "seeded with "+trunkN[:12]) {
t.Errorf("the seed was not said: %s", s.logs())
}
again := pr(never, 5, "ffff000000000000000000000000000000000004", "fix/f", "a.go")
again := pr(never, 5, "f4f4000000000000000000000000000000000004", "fix/f", "a.go")
again.BaseSHA = trunkM
s.h.PullUpdated(again)
s.h.SourceCatchUp()
@@ -79,6 +79,25 @@ func TestStartingWritesTheNewestMergeOfEachRepositoryNotYetInSourceMain(t *testi
t.Fatalf("a seeded head was replaced by %s (%d moves)", got, len(s.source.moves))
}
// The freshest announcement's base head is the seed, not the newest delivery's: #4, made first, is announced again after #5
// with main's head moved on.
const trunkF = "eeee000000000000000000000000000000000003"
f := newWorld(t)
first := pr(never, 4, "f5f5000000000000000000000000000000000005", "fix/g", "a.go")
first.BaseSHA = trunkN
f.h.PullUpdated(first)
f.later(time.Minute)
second := pr(never, 5, "f6f6000000000000000000000000000000000006", "fix/h", "a.go")
second.BaseSHA = trunkM
f.h.PullUpdated(second)
f.later(time.Minute)
first.BaseSHA = trunkF
f.h.PullUpdated(first)
f.settleAll()
if got := f.source.main.Heads[never]; got != trunkF {
t.Fatalf("seeded with %s; wanted %s, the base head of the latest announcement", got, trunkF)
}
o := newWorld(t)
o.source.main = &sdk.Source{V: sdk.Version, Group: "newer", Heads: map[string]string{i1726Repo: i1726Merge2}}
o.h.PullUpdated(pr(i1726Repo, 471, head2, "fix/b", "modules/app/b.go"))
@@ -529,8 +529,8 @@ func (h *Holder) settleForgeMerges() []func() {
var trunkHead = regexp.MustCompile(`^[0-9a-f]{40}$`)
// seedTrunks gives source.main the trunk head of a repository it names none of (issue 1422): one never merged through the
// gate, whose open pull requests the checker could otherwise never judge. The head is the base_sha the forge announced the
// newest open pull request into main with (gitea.pull.updated; ADR 0323), the trunk's head when it was announced. It is
// gate, whose open pull requests the checker could otherwise never judge. The head is the base_sha of the latest announcement of
// any open pull request into main (gitea.pull.updated; ADR 0323; lastAnnounced), the trunk's freshest head the gate heard. It is
// added in one compare-and-set write that never replaces a head source.main holds, so a merge always wins; the first merge
// through the gate moves it as any other. source.main is per repository and names main's head only (ADR 0484 §11, MergeBranch):
// a pull request into another base seeds nothing. A repository is weighed once per start after it was found or seeded.
@@ -541,7 +541,7 @@ func (h *Holder) seedTrunks() []func() {
if h.seeded[d.Repository] || d.Base != MergeBranch || !trunkHead.MatchString(d.BaseSHA) || d.State.Final() || d.MergedAs != "" {
continue
}
if o := newest[d.Repository]; o == nil || d.Created.After(o.Created) {
if o := newest[d.Repository]; o == nil || lastAnnounced(d).After(lastAnnounced(o)) {
newest[d.Repository] = d
}
}
@@ -585,6 +585,15 @@ func (h *Holder) seedTrunks() []func() {
return out
}
// lastAnnounced is when the forge last announced a delivery's head with its base head: BaseSHAAt, or when the gate made it
// for one kept before that was written.
func lastAnnounced(d *Delivery) time.Time {
if !d.BaseSHAAt.IsZero() {
return d.BaseSHAAt
}
return d.Created
}
// replayLate is how long after its merge a merge heard only from the forge's word is a replay: the forge announces a merge
// within seconds, so one heard this late was replayed from the bus's history, and the source already holds what came after.
const replayLate = 15 * time.Minute
@@ -66,6 +66,9 @@ type Delivery struct {
// BaseSHA is the base branch's head the forge last announced its head with (ADR 0323): sent with a check this
// owner asks, for a verdict no build judges to name its trunk commit.
BaseSHA string `json:"base_sha,omitempty"`
// BaseSHAAt is when the forge last announced its head with BaseSHA: the freshest of a repository's announcements gives
// source.main its trunk head when it names none (seedTrunks, issue 1422). Zero for one kept before it was written.
BaseSHAAt time.Time `json:"base_sha_at,omitzero"`
// AskedAgain is when its head was announced again while its verdict was out of date — the merge tool's ask of
// its check against the trunk's head (ADR 0323 §5) — and no verdict has come since: its one-hour bound runs from
// then. Nil while nothing is asked, and cleared by a verdict or another move of its trunk.