Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6784efae75 |
@@ -530,6 +530,15 @@ func takeIn(ctx context.Context, inv *inventory.Inventory, result link.BuildResu
|
|||||||
if result.Source != nil && result.Source.Seat != "" {
|
if result.Source != nil && result.Source.Seat != "" {
|
||||||
recorded.Repository, recorded.Seat = result.Source.Repository, result.Source.Seat
|
recorded.Repository, recorded.Seat = result.Source.Repository, result.Source.Seat
|
||||||
}
|
}
|
||||||
|
// **A build at a commit does not change the branch a module follows** (novox/hq 04-ISSUES/215):
|
||||||
|
// the commit is built and recorded as what it was built from, and the module keeps following
|
||||||
|
// what it followed before — the repository's default branch for one new to the catalogue.
|
||||||
|
if followedBranch(result.Ref) == "" && result.Ref != "" {
|
||||||
|
recorded.Ref = ""
|
||||||
|
if was, err := inv.SourceOf(ctx, manifest.Module); err == nil {
|
||||||
|
recorded.Ref = followedBranch(was.Ref)
|
||||||
|
}
|
||||||
|
}
|
||||||
if err := namesNoInstallation(manifest); err != nil {
|
if err := namesNoInstallation(manifest); err != nil {
|
||||||
return manifest, kept, fmt.Errorf("%s built %s (%s), and the mesh does not register it: %w",
|
return manifest, kept, fmt.Errorf("%s built %s (%s), and the mesh does not register it: %w",
|
||||||
result.On, result.Repository, short(result.Commit), err)
|
result.On, result.Repository, short(result.Commit), err)
|
||||||
|
|||||||
@@ -63,3 +63,40 @@ func TestABuildHeardIsRecordedAndRegistered(t *testing.T) {
|
|||||||
t.Fatalf("a failure is said in the builder's words: %v", err)
|
t.Fatalf("a failure is said in the builder's words: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// novox/hq 04-ISSUES/215: a build asked at a commit is recorded as built from that commit, and the
|
||||||
|
// module keeps following the branch it followed — a new one, the default branch.
|
||||||
|
func TestABuildAtACommitKeepsTheBranchTheModuleFollows(t *testing.T) {
|
||||||
|
open := aMesh(t)
|
||||||
|
ctx := t.Context()
|
||||||
|
manifest, _ := json.Marshal(map[string]any{"module": "unifi", "version": "1"})
|
||||||
|
result := func(id, ref, commit string) link.BuildResult {
|
||||||
|
return link.BuildResult{ID: id, Repository: "http://forge.internal:20000/novox/mesh-catalog.git",
|
||||||
|
Path: "modules/unifi", Ref: ref, On: "anchor", Commit: commit, Manifest: manifest,
|
||||||
|
Source: &link.SourceOnSeat{Seat: "git", Repository: "novox/mesh-catalog"}}
|
||||||
|
}
|
||||||
|
if _, _, err := takeIn(ctx, open.inventory, result("b-1", "main", "1111111aaaa")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, _, err := takeIn(ctx, open.inventory, result("b-2", "9c97a8a", "9c97a8a1d2c3")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
src, err := open.inventory.SourceOf(ctx, "unifi")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if src.Ref != "main" || src.BuiltFrom != "9c97a8a1d2c3" {
|
||||||
|
t.Errorf("after a build at a commit the module follows %q, built from %q; want main, 9c97a8a1d2c3", src.Ref, src.BuiltFrom)
|
||||||
|
}
|
||||||
|
|
||||||
|
// One new to the catalogue, first built at a commit, follows the default branch.
|
||||||
|
other, _ := json.Marshal(map[string]any{"module": "letta", "version": "1"})
|
||||||
|
r := result("b-3", "deadbeef", "deadbeefcafe")
|
||||||
|
r.Manifest, r.Path = other, "modules/letta"
|
||||||
|
if _, _, err := takeIn(ctx, open.inventory, r); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if src, _ := open.inventory.SourceOf(ctx, "letta"); src.Ref != "" {
|
||||||
|
t.Errorf("a module first built at a commit follows %q, want the default branch", src.Ref)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -211,3 +211,27 @@ func TestWhatAHandedOverModuleRecordsAboutItsSource(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// novox/hq 04-ISSUES/215: a module once built at a commit still follows its branch — a merge into it
|
||||||
|
// matches the module, and a plan re-asks the branch, not the old commit.
|
||||||
|
func TestAModuleBuiltAtACommitStillFollowsItsBranch(t *testing.T) {
|
||||||
|
m := link.SourceMoved{Owner: "novox", Repo: "mesh-catalog", Base: "main"}
|
||||||
|
pinned := inventory.Source{Repository: "novox/mesh-catalog", Seat: "git", Ref: "9c97a8a"}
|
||||||
|
if !sourceIs(pinned, m) {
|
||||||
|
t.Error("a module whose record names a commit is left out of a merge into its branch")
|
||||||
|
}
|
||||||
|
full := inventory.Source{Repository: "novox/mesh-catalog", Seat: "git", Ref: "9c97a8a1d2c3b4a5f60718293a4b5c6d7e8f9012"}
|
||||||
|
if !sourceIs(full, m) {
|
||||||
|
t.Error("a full commit hash is read as a branch")
|
||||||
|
}
|
||||||
|
if got := followedBranch("9c97a8a"); got != "" {
|
||||||
|
t.Errorf("a plan would re-ask the old commit %q", got)
|
||||||
|
}
|
||||||
|
if got := followedBranch("release"); got != "release" {
|
||||||
|
t.Errorf("a branch is not followed as named: %q", got)
|
||||||
|
}
|
||||||
|
// A module that follows another branch is still not this merge's.
|
||||||
|
if sourceIs(inventory.Source{Repository: "novox/mesh-catalog", Seat: "git", Ref: "release"}, m) {
|
||||||
|
t.Error("a module following another branch was matched")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -272,7 +272,8 @@ func askTier(ctx context.Context, inv *inventory.Inventory, p *inventory.Plan) e
|
|||||||
}
|
}
|
||||||
source := buildSource{Repository: e.Source.Repository, Seat: e.Source.Seat}
|
source := buildSource{Repository: e.Source.Repository, Seat: e.Source.Seat}
|
||||||
fmt.Printf(" tier %d: ", p.Tier)
|
fmt.Printf(" tier %d: ", p.Tier)
|
||||||
if err := buildOne(ctx, source, e.Source.Path, e.Source.Ref, 0); err != nil {
|
// The branch it follows, never a commit a build once named (novox/hq 04-ISSUES/215).
|
||||||
|
if err := buildOne(ctx, source, e.Source.Path, followedBranch(e.Source.Ref), 0); err != nil {
|
||||||
state.State = "failed"
|
state.State = "failed"
|
||||||
state.Why = err.Error()
|
state.Why = err.Error()
|
||||||
p.State = inventory.PlanFailed
|
p.State = inventory.PlanFailed
|
||||||
@@ -399,24 +400,6 @@ func advanceOnce(ctx context.Context, open *stores, p *inventory.Plan,
|
|||||||
}
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
// **Asked: settle from the build records first** (novox/hq 04-ISSUES/214). An outcome is taken
|
|
||||||
// in by whichever controller hears it, and a merge to the controller's own repository replaces
|
|
||||||
// the controller in its first tier: the build that produced the new one is recorded, and the
|
|
||||||
// plan never hears it. The record is the fact; a build recorded after the ask is that tier's
|
|
||||||
// outcome, whoever was listening.
|
|
||||||
recorded := map[string][]inventory.Build{}
|
|
||||||
for _, m := range tier {
|
|
||||||
if s := p.Modules[m]; s != nil && s.State == "asked" {
|
|
||||||
builds, err := inv.Builds(ctx, m, 5)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
recorded[m] = builds
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if settleFromRecords(p, tier, recorded) {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
// Asked: wait for every build.
|
// Asked: wait for every build.
|
||||||
var latest time.Time
|
var latest time.Time
|
||||||
for _, m := range tier {
|
for _, m := range tier {
|
||||||
@@ -773,41 +756,3 @@ func splitList(s string) []string {
|
|||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// settleFromRecords marks every module of the tier still `asked` built — or failed — from a build
|
|
||||||
// recorded after it was asked, and says whether it changed anything (novox/hq 04-ISSUES/214).
|
|
||||||
// Newest first, as Builds answers: the first record after the ask is the outcome of that ask.
|
|
||||||
func settleFromRecords(p *inventory.Plan, tier []string, recorded map[string][]inventory.Build) bool {
|
|
||||||
changed := false
|
|
||||||
for _, m := range tier {
|
|
||||||
s := p.Modules[m]
|
|
||||||
if s == nil || s.State != "asked" || s.AskedAt == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
var outcome *inventory.Build
|
|
||||||
for i := range recorded[m] {
|
|
||||||
b := recorded[m][i]
|
|
||||||
if b.At.Before(*s.AskedAt) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
outcome = &b
|
|
||||||
}
|
|
||||||
if outcome == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
at := outcome.At
|
|
||||||
if outcome.Worked() {
|
|
||||||
s.State = "built"
|
|
||||||
s.BuiltAt = &at
|
|
||||||
s.Commit = outcome.Commit
|
|
||||||
} else {
|
|
||||||
s.State = "failed"
|
|
||||||
s.Why = outcome.Failed
|
|
||||||
p.State = inventory.PlanFailed
|
|
||||||
p.Note = fmt.Sprintf("%s failed to build in tier %d", m, p.Tier)
|
|
||||||
}
|
|
||||||
fmt.Printf("%s: %s settled from the build records as %s (%s)\n", p.ID, m, s.State, outcome.ID)
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
return changed
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -115,40 +115,3 @@ func TestACycleIsOneLastTierAndSaidSo(t *testing.T) {
|
|||||||
t.Fatalf("a cycle should be one tier of two, said: %v", tiers)
|
t.Fatalf("a cycle should be one tier of two, said: %v", tiers)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// novox/hq 04-ISSUES/214: a plan whose build outcome was recorded while no controller followed it —
|
|
||||||
// the controller rebuilding itself — settles from the build records instead of waiting for ever.
|
|
||||||
func TestAPlanSettlesAnAskedBuildFromTheRecords(t *testing.T) {
|
|
||||||
asked := time.Date(2026, 10, 3, 19, 20, 0, 0, time.UTC)
|
|
||||||
p := inventory.Plan{ID: "plan-1", Tiers: [][]string{{"mesh-controller", "builder"}, {"route-proxy"}},
|
|
||||||
Modules: map[string]*inventory.PlanModule{
|
|
||||||
"mesh-controller": {State: "asked", AskedAt: &asked},
|
|
||||||
"builder": {State: "asked", AskedAt: &asked},
|
|
||||||
}}
|
|
||||||
records := map[string][]inventory.Build{
|
|
||||||
// Newest first, as Builds answers: the build after the ask is the outcome.
|
|
||||||
"mesh-controller": {
|
|
||||||
{ID: "build-2", Commit: "2ebbb799", At: asked.Add(4 * time.Minute)},
|
|
||||||
{ID: "build-1", Commit: "06ea2168", At: asked.Add(-10 * time.Minute)},
|
|
||||||
},
|
|
||||||
// Only a build from before the ask: not this ask's outcome.
|
|
||||||
"builder": {{ID: "build-0", Commit: "06ea2168", At: asked.Add(-time.Hour)}},
|
|
||||||
}
|
|
||||||
if !settleFromRecords(&p, p.Tiers[0], records) {
|
|
||||||
t.Fatal("nothing settled, though the controller's build is recorded after the ask")
|
|
||||||
}
|
|
||||||
if s := p.Modules["mesh-controller"]; s.State != "built" || s.Commit != "2ebbb799" || s.BuiltAt == nil {
|
|
||||||
t.Errorf("the controller's ask is %+v, want built from 2ebbb799", s)
|
|
||||||
}
|
|
||||||
if s := p.Modules["builder"]; s.State != "asked" {
|
|
||||||
t.Errorf("an ask with no record after it was settled: %+v", s)
|
|
||||||
}
|
|
||||||
|
|
||||||
// A failure recorded after the ask fails the plan, as hearing it would have.
|
|
||||||
q := inventory.Plan{ID: "plan-2", Tiers: [][]string{{"x"}},
|
|
||||||
Modules: map[string]*inventory.PlanModule{"x": {State: "asked", AskedAt: &asked}}}
|
|
||||||
settleFromRecords(&q, q.Tiers[0], map[string][]inventory.Build{"x": {{ID: "b", Failed: "no", At: asked.Add(time.Minute)}}})
|
|
||||||
if q.State != inventory.PlanFailed || q.Modules["x"].State != "failed" {
|
|
||||||
t.Errorf("a recorded failure did not fail the plan: %+v %+v", q, q.Modules["x"])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -283,6 +284,14 @@ func (f following) SourceMoved(ctx context.Context, m link.SourceMoved) error {
|
|||||||
if isHistory(m.MergedAt, lastLookAt(entries, m)) {
|
if isHistory(m.MergedAt, lastLookAt(entries, m)) {
|
||||||
packaging = nil
|
packaging = nil
|
||||||
}
|
}
|
||||||
|
// Said, never silent (novox/hq 04-ISSUES/215): a module built from this repository that follows
|
||||||
|
// another branch is not part of this merge, and whoever is waiting for its change should read why.
|
||||||
|
for _, e := range entries {
|
||||||
|
if sameRepository(e.Source.Repository, m) && !sourceIs(e.Source, m) {
|
||||||
|
fmt.Printf(" %s is built from %s/%s and follows %s, not %s; this merge leaves it out\n",
|
||||||
|
e.Manifest.Module, m.Owner, m.Repo, e.Source.Ref, m.Base)
|
||||||
|
}
|
||||||
|
}
|
||||||
touched := whatTheMergeTouched(from, entries, m)
|
touched := whatTheMergeTouched(from, entries, m)
|
||||||
for _, e := range touched {
|
for _, e := range touched {
|
||||||
if err := inv.SourceMoved(ctx, e.Manifest.Module, m.Commit); err != nil {
|
if err := inv.SourceMoved(ctx, e.Manifest.Module, m.Commit); err != nil {
|
||||||
@@ -345,7 +354,24 @@ func sourceIs(s inventory.Source, m link.SourceMoved) bool {
|
|||||||
if !sameRepository(s.Repository, m) {
|
if !sameRepository(s.Repository, m) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return s.Ref == "" || s.Ref == m.Base
|
ref := followedBranch(s.Ref)
|
||||||
|
return ref == "" || ref == m.Base
|
||||||
|
}
|
||||||
|
|
||||||
|
// commitRef is a ref that names a commit rather than a branch: what `build --ref <commit>` asks for.
|
||||||
|
var commitRef = regexp.MustCompile(`^[0-9a-f]{7,40}$`)
|
||||||
|
|
||||||
|
// followedBranch is the branch a recorded ref means a module follows (novox/hq 04-ISSUES/215). **A
|
||||||
|
// commit is never a branch to follow.** A build asked at a commit — to try one, or to pin it during a
|
||||||
|
// fix — recorded that commit as the module's ref; every merge after it then failed to match the
|
||||||
|
// module, its plan left it out without saying so, and every plan that rebuilt it asked for that same
|
||||||
|
// old commit again. A commit recorded so is read as the repository's default branch, which is what
|
||||||
|
// the module followed before it; a branch is followed as named.
|
||||||
|
func followedBranch(ref string) string {
|
||||||
|
if commitRef.MatchString(strings.TrimSpace(ref)) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return ref
|
||||||
}
|
}
|
||||||
|
|
||||||
// sameRepository is whether a recorded repository is the one a merge names, in either spelling it
|
// sameRepository is whether a recorded repository is the one a merge names, in either spelling it
|
||||||
|
|||||||
Reference in New Issue
Block a user