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
@@ -0,0 +1,10 @@
-- A plan keeps when the forge made the merge it answers (novox/hq issue 349).
--
-- A newer plan of a repository's branch supersedes the older open ones, and "newer" was read from when each
-- plan was made. 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 superseded it, folding its unbuilt modules into a plan
-- at the older commit. On 2026-10-09 a security fix to the forge's module would have been built from the
-- commit before it. Merges into one branch are made one after another, each on the one before, so the
-- forge's merge time is the branch's order. Null for a plan kept before this column, and for a plan no merge
-- made (a release); then the plans' own order stands, as before.
alter table release_plan add column merged_at timestamptz;
+23 -7
View File
@@ -19,8 +19,12 @@ type Plan struct {
Repository string `json:"repository"`
// Branch is the branch the merge went into (novox/hq issue 254): a newer plan supersedes the open
// ones of the same repository and branch. Empty for a plan from before it was kept.
Branch string `json:"branch,omitempty"`
Commit string `json:"commit"`
Branch string `json:"branch,omitempty"`
Commit string `json:"commit"`
// Merged is when the forge made the merge this plan answers (novox/hq issue 349): the order of a
// branch's merges, which is not the order their plans were made in when one was acted on late. Zero
// for a release, and for a plan kept before it was.
Merged time.Time `json:"merged,omitzero"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
State string `json:"state"`
@@ -249,8 +253,8 @@ func (i *Inventory) SavePlan(ctx context.Context, p *Plan) error {
var revision int64
err = tx.QueryRow(ctx,
`insert into release_plan (id, repository, commit_hash, created, updated, state, tier, tiers, modules, note,
branch, tier_entered, revision, epoch, release, delivery)
values ($1, $2, $3, $4, now(), $5, $6, $7, $8, $9, $10, $11, 1, $13, $14, $15)
branch, tier_entered, revision, epoch, release, delivery, merged_at)
values ($1, $2, $3, $4, now(), $5, $6, $7, $8, $9, $10, $11, 1, $13, $14, $15, $16)
on conflict (id) do update set updated = now(), state = excluded.state, tier = excluded.tier,
tiers = excluded.tiers, modules = excluded.modules, note = excluded.note, branch = excluded.branch,
tier_entered = excluded.tier_entered, revision = release_plan.revision + 1, epoch = excluded.epoch,
@@ -258,7 +262,7 @@ func (i *Inventory) SavePlan(ctx context.Context, p *Plan) error {
where release_plan.revision = $12
returning revision`,
p.ID, p.Repository, p.Commit, p.Created, p.State, p.Tier, tiers, modules, p.Note, p.Branch, entered,
p.Revision, epoch, release, delivery).Scan(&revision)
p.Revision, epoch, release, delivery, mergedAt(p.Merged)).Scan(&revision)
if errors.Is(err, pgx.ErrNoRows) {
// The row is there and at another revision — moved since this was read, or there already
// when this one is new: either way not this writer's to overwrite. (A plan saved before plans
@@ -291,6 +295,14 @@ func short(commit string) string {
return commit
}
// mergedAt is a plan's merge time as the store keeps it: null when not known.
func mergedAt(t time.Time) *time.Time {
if t.IsZero() {
return nil
}
return &t
}
// OpenPlans is every plan still being worked, oldest first.
func (i *Inventory) OpenPlans(ctx context.Context) ([]Plan, error) {
return i.plans(ctx, `where state in ('building', 'rolling') order by created`)
@@ -316,7 +328,7 @@ func (i *Inventory) PlanByID(ctx context.Context, id string) (Plan, error) {
func (i *Inventory) plans(ctx context.Context, tail string) ([]Plan, error) {
rows, err := i.store.Pool().Query(ctx,
`select id, repository, commit_hash, created, updated, state, tier, tiers, modules, note, branch,
coalesce(tier_entered, created), revision, coalesce(epoch, 0), release, delivery
coalesce(tier_entered, created), revision, coalesce(epoch, 0), release, delivery, merged_at
from release_plan `+tail)
if err != nil {
return nil, err
@@ -327,11 +339,15 @@ func (i *Inventory) plans(ctx context.Context, tail string) ([]Plan, error) {
var p Plan
var tiers, modules, release, delivery []byte
var epoch int64
var merged *time.Time
if err := rows.Scan(&p.ID, &p.Repository, &p.Commit, &p.Created, &p.Updated, &p.State,
&p.Tier, &tiers, &modules, &p.Note, &p.Branch, &p.TierEntered, &p.Revision, &epoch, &release,
&delivery); err != nil {
&delivery, &merged); err != nil {
return nil, err
}
if merged != nil {
p.Merged = merged.UTC()
}
if len(release) > 0 {
if err := json.Unmarshal(release, &p.Release); err != nil {
return nil, err