Read a module whole while a plan has overtaken its build source, and plan every view alike (hq ADR 0267, review)

A failed build, or a plan closed before reaching a module, left the
closure its last good build said, and a fix-forward to a newly imported
package would have moved nothing. A missed merge moving only a module
that packages the repository was never caught up, and an older merge
read as history for it through a look that was not its own. The gate,
a pull request's check, the what-if and a delivery's order now read the
same view the merge handler does.
This commit is contained in:
jochen
2026-10-10 03:20:36 +02:00
parent 6c616838a5
commit 150f038ff8
9 changed files with 235 additions and 92 deletions
+46 -2
View File
@@ -91,6 +91,10 @@ type ReadRepository struct {
// Own is the module's own repository, whose Paths narrow what its own directory — or, for a module
// built from its repository's root, the whole repository — would otherwise be.
Own bool `json:"own,omitempty"`
// Built is when the build these were read from was asked, and Looked when the module's newest build of
// any outcome was: what the planner judges a build source's age, and a merge's news, by. Never stored.
Built time.Time `json:"-"`
Looked time.Time `json:"-"`
}
// BuildSource is the build source a build read in one repository (novox/hq ADR 0267): Repository and Ref
@@ -338,8 +342,41 @@ func (i *Inventory) BuiltAgainst(ctx context.Context) (map[string][]string, erro
// none — off the trunk, from a builder that predates it, or of a source not known — leaves its module read
// as before: every file of a context, and its own directory or root.
func (i *Inventory) ReadRepositories(ctx context.Context) (map[string][]ReadRepository, error) {
// The newest build of each module whatever its outcome: a failed one newer than the newest that
// worked leaves that one's build source stale — the merge it was asked for may have changed the closure
// (novox/hq ADR 0267), so its module is read whole until a build works again.
newest := map[string]struct {
at time.Time
failed bool
}{}
tried, err := i.store.Pool().Query(ctx,
`select distinct on (module) module, coalesce(asked, at), failed <> ''
from build
where module is not null and module <> ''
order by module, `+newestRequestFirst)
if err != nil {
return nil, err
}
for tried.Next() {
var module string
var at time.Time
var failed bool
if err := tried.Scan(&module, &at, &failed); err != nil {
tried.Close()
return nil, err
}
newest[module] = struct {
at time.Time
failed bool
}{at, failed}
}
tried.Close()
if err := tried.Err(); err != nil {
return nil, err
}
rows, err := i.store.Pool().Query(ctx,
`select distinct on (module) module, built_contexts, build_sources
`select distinct on (module) module, built_contexts, build_sources, coalesce(asked, at)
from build
where module is not null and module <> '' and failed = ''
order by module, `+newestRequestFirst)
@@ -352,7 +389,8 @@ func (i *Inventory) ReadRepositories(ctx context.Context) (map[string][]ReadRepo
for rows.Next() {
var module string
var raw, rawSources []byte
if err := rows.Scan(&module, &raw, &rawSources); err != nil {
var built time.Time
if err := rows.Scan(&module, &raw, &rawSources, &built); err != nil {
return nil, err
}
var of []ReadRepository
@@ -367,7 +405,13 @@ func (i *Inventory) ReadRepositories(ctx context.Context) (map[string][]ReadRepo
sources = nil
}
}
if n, known := newest[module]; known && n.failed && n.at.After(built) {
sources = nil
}
if of = WithBuildSources(of, sources); len(of) > 0 {
for k := range of {
of[k].Built, of[k].Looked = built, newest[module].at
}
read[module] = of
}
}
+33
View File
@@ -5,6 +5,7 @@ import (
"reflect"
"strings"
"testing"
"time"
)
// A build result was answered to whoever asked and kept nowhere, so "when did this last build",
@@ -200,6 +201,12 @@ func TestABuildsBuildSourceComesBackOverWhatItRead(t *testing.T) {
{Repository: "novox/other"},
{Paths: []string{"modules/route-proxy/Dockerfile", "modules/route-proxy/module.json"}, Own: true},
}
for k := range read["route-proxy"] {
if read["route-proxy"][k].Built.IsZero() {
t.Errorf("no build time on %+v", read["route-proxy"][k])
}
read["route-proxy"][k].Built, read["route-proxy"][k].Looked = time.Time{}, time.Time{}
}
if !reflect.DeepEqual(read["route-proxy"], want) {
t.Fatalf("read back %+v\nwanted %+v", read["route-proxy"], want)
}
@@ -214,3 +221,29 @@ func TestABuildsBuildSourceComesBackOverWhatItRead(t *testing.T) {
t.Fatalf("the build source was stored among what the build read: %s", stored)
}
}
// A build newer than the newest that worked, and failed, leaves that one's build source stale: its module is
// read whole until a build works again (novox/hq ADR 0267).
func TestAFailedNewerBuildLeavesTheBuildSourceStale(t *testing.T) {
inv := fresh(t)
ctx := context.Background()
worked := aBuild("worked", "route-proxy", "")
worked.Asked = time.Now().Add(-time.Hour)
worked.Read = []ReadRepository{{Repository: "novox/mesh-controller", Ref: "main"}}
worked.Sources = []BuildSource{{Paths: []string{"modules/route-proxy/module.json"}},
{Repository: "novox/mesh-controller", Ref: "main", Paths: []string{"examples/route-proxy/"}}}
failed := aBuild("failed", "route-proxy", "compile error")
failed.Asked = time.Now()
for _, b := range []Build{worked, failed} {
if err := inv.RecordBuild(ctx, b); err != nil {
t.Fatal(err)
}
}
read, err := inv.ReadRepositories(ctx)
if err != nil {
t.Fatal(err)
}
if got := read["route-proxy"]; len(got) != 1 || got[0].Paths != nil || got[0].Own || !got[0].Looked.After(got[0].Built) {
t.Fatalf("after a newer failed build the proxy reads as %+v", got)
}
}