A module's dependencies are one relation in the catalogue — stands-on, packages, built-by, declared — answered by one call. A merge takes what moved and everything reachable from it, sorts the set into tiers (a code dependency in the same tier, a build dependency after its base is built, a runtime dependency after the build machine is built and running; the build machine's own base comes first, built by the one that runs), writes the plan to the store, asks the first tier and returns. Every outcome advances the plan; a ticker advances what outcomes cannot; a controller replaced mid-plan resumes it. status lists open plans and names one that has waited too long.
124 lines
4.0 KiB
Go
124 lines
4.0 KiB
Go
package inventory
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
)
|
|
|
|
// A Plan is what a merge produces (novox/hq ADR 0162): the modules it changed and everything
|
|
// standing on them, sorted into tiers, each module's state, and the tier the plan is at. Kept in
|
|
// the store so a controller replaced mid-plan resumes it, and so `status` can say what a merge
|
|
// still waits for.
|
|
type Plan struct {
|
|
ID string `json:"id"`
|
|
Repository string `json:"repository"`
|
|
Commit string `json:"commit"`
|
|
Created time.Time `json:"created"`
|
|
Updated time.Time `json:"updated"`
|
|
State string `json:"state"`
|
|
Tier int `json:"tier"`
|
|
Tiers [][]string `json:"tiers"`
|
|
Modules map[string]*PlanModule `json:"modules"`
|
|
Note string `json:"note,omitempty"`
|
|
}
|
|
|
|
// PlanModule is one module's state within a plan.
|
|
type PlanModule struct {
|
|
// State: asked, built, failed; empty for a module whose tier has not been asked yet.
|
|
State string `json:"state,omitempty"`
|
|
AskedAt *time.Time `json:"asked_at,omitempty"`
|
|
BuiltAt *time.Time `json:"built_at,omitempty"`
|
|
Commit string `json:"commit,omitempty"`
|
|
Why string `json:"why,omitempty"`
|
|
}
|
|
|
|
// The states a plan passes through.
|
|
const (
|
|
PlanBuilding = "building"
|
|
PlanRolling = "rolling"
|
|
PlanDone = "done"
|
|
PlanFailed = "failed"
|
|
)
|
|
|
|
// Open says whether the plan is still being worked.
|
|
func (p Plan) Open() bool { return p.State == PlanBuilding || p.State == PlanRolling }
|
|
|
|
// SavePlan writes a plan, new or changed, whole: the plan is small and read as one thing.
|
|
func (i *Inventory) SavePlan(ctx context.Context, p Plan) error {
|
|
tiers, err := json.Marshal(p.Tiers)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
modules, err := json.Marshal(p.Modules)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = i.store.Pool().Exec(ctx,
|
|
`insert into release_plan (id, repository, commit_hash, created, updated, state, tier, tiers, modules, note)
|
|
values ($1, $2, $3, $4, now(), $5, $6, $7, $8, $9)
|
|
on conflict (id) do update set updated = now(), state = excluded.state, tier = excluded.tier,
|
|
tiers = excluded.tiers, modules = excluded.modules, note = excluded.note`,
|
|
p.ID, p.Repository, p.Commit, p.Created, p.State, p.Tier, tiers, modules, p.Note)
|
|
return err
|
|
}
|
|
|
|
// 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`)
|
|
}
|
|
|
|
// RecentPlans is the last few plans, newest first, open or not — what the overview shows.
|
|
func (i *Inventory) RecentPlans(ctx context.Context, limit int) ([]Plan, error) {
|
|
return i.plans(ctx, fmt.Sprintf(`order by created desc limit %d`, limit))
|
|
}
|
|
|
|
// PlanByID is one plan.
|
|
func (i *Inventory) PlanByID(ctx context.Context, id string) (Plan, error) {
|
|
plans, err := i.plans(ctx, `where id = '`+id+`'`)
|
|
if err != nil {
|
|
return Plan{}, err
|
|
}
|
|
if len(plans) == 0 {
|
|
return Plan{}, fmt.Errorf("no plan %s", id)
|
|
}
|
|
return plans[0], nil
|
|
}
|
|
|
|
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
|
|
from release_plan `+tail)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []Plan
|
|
for rows.Next() {
|
|
var p Plan
|
|
var tiers, modules []byte
|
|
if err := rows.Scan(&p.ID, &p.Repository, &p.Commit, &p.Created, &p.Updated, &p.State,
|
|
&p.Tier, &tiers, &modules, &p.Note); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := json.Unmarshal(tiers, &p.Tiers); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := json.Unmarshal(modules, &p.Modules); err != nil {
|
|
return nil, err
|
|
}
|
|
if p.Modules == nil {
|
|
p.Modules = map[string]*PlanModule{}
|
|
}
|
|
out = append(out, p)
|
|
}
|
|
if errors.Is(rows.Err(), pgx.ErrNoRows) {
|
|
return nil, nil
|
|
}
|
|
return out, rows.Err()
|
|
}
|