From c7039d81e12c73719256cd1f62ccf545c8169950 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 22 Oct 2026 18:09:43 +0200 Subject: [PATCH] =?UTF-8?q?Refuse=20build,=20rebuild=20and=20replay=20--re?= =?UTF-8?q?gister=20by=20name,=20naming=20what=20replaced=20them,=20and=20?= =?UTF-8?q?delete=20the=20hand=20builds=20(issue=201851,=20A11a,=20ADR=200?= =?UTF-8?q?501=20=C2=A75)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ask_bus_look_integration_test.go | 5 - cmd/mesh-controller/build.go | 237 ------------------ cmd/mesh-controller/dependents_test.go | 10 - cmd/mesh-controller/key_takein.go | 3 - cmd/mesh-controller/main.go | 24 +- cmd/mesh-controller/not_asked_id_test.go | 24 -- cmd/mesh-controller/order_test.go | 42 ---- cmd/mesh-controller/plan_retry.go | 95 ------- cmd/mesh-controller/queue.go | 202 +-------------- cmd/mesh-controller/queue_integration_test.go | 62 ----- cmd/mesh-controller/queue_test.go | 87 +------ cmd/mesh-controller/seatverbs.go | 51 ++-- cmd/mesh-controller/seatverbs_test.go | 93 +++---- cmd/mesh-controller/try.go | 35 --- cmd/mesh-controller/upgrades.go | 63 +---- .../walk_from_keys_integration_test.go | 2 +- .../walk_said_integration_test.go | 25 +- .../walk_wait_rebuild_754_integration_test.go | 63 ----- 18 files changed, 91 insertions(+), 1032 deletions(-) delete mode 100644 cmd/mesh-controller/not_asked_id_test.go delete mode 100644 cmd/mesh-controller/walk_wait_rebuild_754_integration_test.go diff --git a/cmd/mesh-controller/ask_bus_look_integration_test.go b/cmd/mesh-controller/ask_bus_look_integration_test.go index 83bbee9f..d4b1eee4 100644 --- a/cmd/mesh-controller/ask_bus_look_integration_test.go +++ b/cmd/mesh-controller/ask_bus_look_integration_test.go @@ -116,11 +116,6 @@ func TestALookFailsTheTierTheBusRefusedPastTheBound(t *testing.T) { t.Fatalf("a bus silent past its bound did not fail the plan in words: %s %q", got.State, got.Note) } - // Rebuilt by hand while the bus still reloads: said as waiting, not as a failure and not with an empty reason. - joined, said, err := joinAPlan(ctx, open, "a") - if err != nil || !joined || !strings.Contains(said, "not asked yet: the bus did not answer") { - t.Fatalf("a rebuild left for the bus is said as %v %q %v", joined, said, err) - } // And retried by hand: the module is named as waiting, not as asked again. got, _ = inv.PlanByID(ctx, "plan-bus") if got.State == inventory.PlanFailed { diff --git a/cmd/mesh-controller/build.go b/cmd/mesh-controller/build.go index 326dcf12..a9c640cc 100644 --- a/cmd/mesh-controller/build.go +++ b/cmd/mesh-controller/build.go @@ -28,112 +28,6 @@ import ( // cheapest next step. That is how novox/hq ADR 0001 records `hal/sdk` reaching 34,636: // nothing in it was wrong, and no one edit was the one that should have been a new file. -// buildCommand builds a module from its source and records what came out. -// -// **Run where there is a container runtime**, which is why it is a command rather than something -// the control plane does on its own: building needs to run things on a machine, and what the -// control plane may send a machine is bounded by the declaration language. This is the shape the -// builder module will take when it is given work over the broker; today a person runs it, and the -// mesh records the result the same way either way. -// buildOn rebuilds every module the mesh holds that stands on the named module's artifacts — the -// rebuild a changed base needs, which nothing else asks for: their sources did not move, and -// "behind" does not see a base that did (novox/hq 04-ISSUES/131). Bases first among them too. -func buildOn(ctx context.Context, base string, wait time.Duration) error { - open, err := openStores(ctx) - if err != nil { - return err - } - defer open.Close() - held, err := open.inventory.Catalogued(ctx) - if err != nil { - return err - } - against, err := open.inventory.BuiltAgainst(ctx) - if err != nil { - return err - } - var on []inventory.Entry - for _, e := range held { - if standsOnModule(e, base, against) { - on = append(on, e) - } - } - if len(on) == 0 { - fmt.Printf("nothing the mesh holds stands on %s\n", base) - return nil - } - on = orderByBases(on, against) - fmt.Printf("%d module(s) stand on %s:\n", len(on), base) - var failed []string - for _, e := range on { - fmt.Printf("--- %s\n", e.Manifest.Module) - source := buildSource{Repository: e.Source.Repository, Seat: e.Source.Seat} - if err := buildOne(ctx, source, e.Source.Path, e.Source.Ref, wait); err != nil { - fmt.Printf(" %v\n", err) - failed = append(failed, e.Manifest.Module) - } - } - if len(failed) > 0 { - return fmt.Errorf("%d of %d could not be built: %s", len(failed), len(on), strings.Join(failed, ", ")) - } - fmt.Printf("\n%d module(s) rebuilt on %s. `push --behind` sends them on\n", len(on), base) - return nil -} - -func buildCommand(ctx context.Context, args []string) error { - set := flag.NewFlagSet("build", flag.ContinueOnError) - ref := set.String("ref", "", "the branch, tag or commit to build") - // A module is a repository and a path within it (novox/hq ADR 0069). Empty is the repository's - // root, which is the ordinary case and why this is a flag rather than a second argument. - path := set.String("path", "", "the module's directory inside the repository") - wait := set.Duration("wait", 10*time.Minute, "how long to wait for a builder to answer") - dryRun := set.Bool("dry-run", false, "build and print the manifest, recording nothing") - // Every module whose source has moved, rather than one named repository. - // - // **The mirror of `push --behind`, and the same argument** (novox/hq ADR 0010): the mesh - // already knows which modules are behind their source, so making a person read that list and - // retype each repository is asking them to be the loop. Naming a repository and asking which - // ones need building are different requests, so they are not combined. - behind := set.Bool("behind", false, "every module the mesh holds older than its source has") - on := set.String("on", "", "rebuild every module that stands on this module's artifacts — the rebuild a changed base needs") - // A repository on the mesh's own forge, named by its path there (novox/hq ADR 0111). Without it - // the repository is external, cloned exactly as given — see source.go. - self := set.Bool("self", false, "the repository is a path on the forge holding the git seat") - positionals, err := parseAround(set, args) - if err != nil { - return err - } - if *on != "" { - if len(positionals) != 0 || *behind || *self { - return errors.New("build --on names a base and nothing else") - } - return buildOn(ctx, *on, *wait) - } - - if *behind { - if len(positionals) != 0 || *self { - return errors.New("build or build --behind, not both: one names a " + - "repository and the other asks which need building") - } - return buildBehind(ctx, *wait) - } - if len(positionals) != 1 { - return errors.New("build [--self] [--path P] [--ref R] [--wait D] [--dry-run] | build --behind | build --on ") - } - source := buildSource{Repository: positionals[0]} - if *self { - if err := onASeat(source.Repository); err != nil { - return err - } - source.Seat = gitSeat - } - - if *dryRun { - return buildAndShow(ctx, source, *path, *ref, *wait) - } - return buildOne(ctx, source, *path, *ref, *wait) -} - // buildFrom turns what a builder said into what the mesh keeps. // // **By digest and path, never by where it was pushed** (novox/hq 04-ISSUES/102). The builder @@ -355,86 +249,6 @@ func builderCommand(ctx context.Context, args []string) error { return issueOnTheNewBus(ctx, inv, m, node, address) } -// buildBehind builds every module the mesh holds older than its source has. -// -// **This is the loop novox/hq ADR 0010 replaced a pipeline with, closed.** The mesh already -// records where each module came from and what its source last had; until this, a person read -// that list and retyped each repository — which is a person being the loop, and the thing a -// pipeline was doing before it was taken away. -// -// Each is built and recorded on its own. **One failing does not stop the others**, for the same -// reason one broken module no longer blocks a machine's whole declaration: a mesh where one bad -// repository holds back nine good ones is a mesh where nobody dares add the tenth. -func buildBehind(ctx context.Context, wait time.Duration) error { - open, err := openStores(ctx) - if err != nil { - return err - } - defer open.Close() - inv := open.inventory - - held, err := inv.Catalogued(ctx) - if err != nil { - return err - } - var stale []inventory.Entry - for _, e := range held { - if !e.Source.Current() { - stale = append(stale, e) - } - } - if len(stale) == 0 { - // Said rather than doing nothing quietly: "nothing needed building" and "this did not - // run" must never look the same. - fmt.Println("every module the mesh holds is what its source last had") - return nil - } - - fmt.Printf("%d module(s) behind their source:\n", len(stale)) - for _, e := range stale { - fmt.Printf(" %s %s < %s\n", - e.Manifest.Module, short(e.Source.BuiltFrom), short(e.Source.Head)) - } - fmt.Println() - - // Bases first: a module built before the module it stands on is built against the old one - // and reports success (novox/hq 04-ISSUES/131). - against, err := inv.BuiltAgainst(ctx) - if err != nil { - return err - } - stale = orderByBases(stale, against) - - var failed []string - for _, e := range stale { - fmt.Printf("--- %s\n", e.Manifest.Module) - // Its own recorded ref, not its head commit: a module tracking a branch should be built - // from that branch, and pinning to the commit the mesh happened to notice would quietly - // turn a tracked branch into a pin. - source := buildSource{Repository: e.Source.Repository, Seat: e.Source.Seat} - if err := buildOne(ctx, source, e.Source.Path, e.Source.Ref, wait); err != nil { - fmt.Printf(" %v\n", err) - failed = append(failed, e.Manifest.Module) - } - } - - if len(failed) > 0 { - return fmt.Errorf("%d of %d could not be built: %s", - len(failed), len(stale), strings.Join(failed, ", ")) - } - fmt.Printf("\n%d module(s) built. `push --behind` sends them to the machines running them\n", - len(stale)) - return nil -} - -// buildOne asks a build machine for one repository and records everything that came back. -// -// Separated from the command so `--behind` can walk a list without a second path to the same act. -func buildOne(ctx context.Context, source buildSource, path, ref string, wait time.Duration) error { - _, err := buildOneAsked(ctx, source, path, ref, wait, false, "build") - return err -} - // recordAsked keeps a build request once it is asked (novox/hq issue 325), with who asked it, in a store // opened for the purpose: the asks reach here from processes that hold none. func recordAsked(ctx context.Context, r inventory.BuildRequest) { @@ -867,57 +681,6 @@ func publishable(result link.BuildResult, follows string) error { return nil } -// buildAndShow builds and prints the manifest without recording anything. -func buildAndShow(ctx context.Context, source buildSource, path, ref string, wait time.Duration) error { - repository, err := cloneFrom(ctx, source) - if err != nil { - return err - } - if err := verbMayAsk(ctx, source, repository, path); err != nil { - return err - } - ident, err := openIdentity(ctx) - if err != nil { - return err - } - defer ident.Close() - server, err := connectLink(ctx, nil, nil, nil) - if err != nil { - return err - } - defer server.Close() - - ask, err := askOverOn(buildSeatHeld(ctx)) - if err != nil { - return err - } - defer ask.Close() - - result, err := ask.Submit(ctx, link.BuildRequest{ - ID: link.NewBuildID(time.Now()), - Repository: repository, Path: path, Ref: ref, - Held: heldBy(ctx), Seats: seatBases(ctx), - DryRun: true, - }, wait) - if err != nil { - return err - } - if result.Failed != "" { - return fmt.Errorf("%s could not build %s:\n%s", result.On, result.Repository, result.Failed) - } - manifest, err := catalogue.ParseManifest(result.Manifest) - if err != nil { - return fmt.Errorf("%s built %s and what came back is not a manifest: %w", - result.On, result.Repository, err) - } - body, err := json.MarshalIndent(manifest, "", " ") - if err != nil { - return err - } - fmt.Println(string(body)) - return nil -} - // answers is what the three questions came back with, read once. type answers struct { wrong []inventory.Doing diff --git a/cmd/mesh-controller/dependents_test.go b/cmd/mesh-controller/dependents_test.go index 05c306b1..bc9386a9 100644 --- a/cmd/mesh-controller/dependents_test.go +++ b/cmd/mesh-controller/dependents_test.go @@ -35,16 +35,6 @@ func TestAMergeOfABaseTakesWhatStandsOnItAlong(t *testing.T) { t.Fatalf("%s was rebuilt and stands on nothing that moved (%v)", n, names) } } - // The dependents come in base order when the merge orders them: the runtime, then shop, then - // the plugin that stands on shop. - ordered := orderByBases(append([]inventory.Entry{entry("mesh-tools")}, got...), against) - pos := map[string]int{} - for i, e := range ordered { - pos[e.Manifest.Module] = i - } - if !(pos["mesh-tools"] < pos["shop"] && pos["shop"] < pos["shop-plugin"]) { - t.Fatalf("not in base order: %v", ordered) - } // Nothing moved: nothing follows. if more := dependentsOf(nil, entries, against); len(more) != 0 { t.Fatalf("with nothing moved, %d module(s) were rebuilt", len(more)) diff --git a/cmd/mesh-controller/key_takein.go b/cmd/mesh-controller/key_takein.go index 435cfacb..b418c049 100644 --- a/cmd/mesh-controller/key_takein.go +++ b/cmd/mesh-controller/key_takein.go @@ -57,9 +57,6 @@ func keyBuildID(k builderkeys.Key) string { return fmt.Sprintf("key-%s-%d", part, k.Revision) } -// isKeyBuildID says a recorded build was taken from a builder's key. -func isKeyBuildID(id string) bool { return strings.HasPrefix(id, "key-") } - // readArchive is the archive a key's reference names, read from the artifact store and checked against its digest. A // variable so a test needs no store. var readArchive = func(ctx context.Context, inv *inventory.Inventory, reference string) ([]byte, error) { diff --git a/cmd/mesh-controller/main.go b/cmd/mesh-controller/main.go index d3f380b2..e3e346f7 100644 --- a/cmd/mesh-controller/main.go +++ b/cmd/mesh-controller/main.go @@ -73,8 +73,9 @@ func run() error { } switch args[0] { - case "build": - return buildCommand(ctx, args[1:]) + case "build", "rebuild": + // **The hand build goes with the asks** (novox/hq ADR 0501 §5): refused by name, naming the replacement. + return removedByName(args[0]) case "builder": return builderCommand(ctx, args[1:]) case "board": @@ -98,8 +99,6 @@ func run() error { return cancelCommand(ctx, args[1:]) case "clear": return clearCommand(ctx, args[1:]) - case "rebuild": - return rebuildCommand(ctx, args[1:]) case "replay": return replayCommand(ctx, args[1:]) case "kill": @@ -475,3 +474,20 @@ func connectionName(command, verb string) string { } return "mesh-controller command " + command } + +// removedByName refuses a command the controller no longer has, naming what replaced it (novox/hq ADR 0501 §4, §5; +// ADR 0429 §18; ADR 0503 §3; issue 1851, A11, and issue 1318). +func removedByName(command string) error { + replacement := map[string]string{ + "build": "the controller asks no build (ADR 0501 §5): the builder builds every fingerprint the indexer writes, a " + + "module is registered from its done key, and a branch is tried with `try`; a module from a repository the " + + "catalogue does not build is registered at the controller's terminal with `register`", + "rebuild": "the controller asks no build (ADR 0501 §5): a failed key is built again by the builder as a new " + + "revision (ADR 0429 §18), and a walk's Retry waits for it", + "replay --register": "a build is registered only from the builder's done key of a trunk fingerprint (ADR 0501 " + + "§1, §2); `replay` without --register still shows what a build of a recorded commit makes", + "forget-checks": "build-agent makes no check build from R4e (ADR 0503 §3), so there is nothing to let go of", + "delivery-plan": "a delivery's plan is read with `delivery-order` and the checker's verdict (ADR 0503 §3)", + }[command] + return fmt.Errorf("`%s` is removed: %s", command, replacement) +} diff --git a/cmd/mesh-controller/not_asked_id_test.go b/cmd/mesh-controller/not_asked_id_test.go deleted file mode 100644 index 557589f4..00000000 --- a/cmd/mesh-controller/not_asked_id_test.go +++ /dev/null @@ -1,24 +0,0 @@ -package main - -import ( - "testing" - - "github.com/novox/mesh-controller/internal/chain" -) - -// 1146: a merge's ask that failed is kept under an id the bus takes as a key, for a module at the repository's root -// (".") as for one in a folder. -func TestTheIdOfABuildNotAskedIsAKeyTheBusTakes(t *testing.T) { - for dir, want := range map[string]string{ - ".": "not-asked-0177ea7a", - "modules/mesh-delivery-view": "not-asked-0177ea7a-modules-mesh-delivery-view", - } { - id := notAskedID("0177ea7a00000000", dir) - if id != want { - t.Errorf("notAskedID(%q) = %q, want %q", dir, id, want) - } - if !chain.ValidKey("req." + id) { - t.Errorf("notAskedID(%q) = %q: the bus takes no key req.%s", dir, id, id) - } - } -} diff --git a/cmd/mesh-controller/order_test.go b/cmd/mesh-controller/order_test.go index 9bd36d56..5aa455b1 100644 --- a/cmd/mesh-controller/order_test.go +++ b/cmd/mesh-controller/order_test.go @@ -28,48 +28,6 @@ func stoodOn(edges map[string][]string) map[string][]string { return out } -// A module built before the module it stands on is built against the old one and reports success -// (novox/hq 04-ISSUES/131). So bases come first, however the set arrived. -func TestBasesAreBuiltBeforeWhatStandsOnThem(t *testing.T) { - in := []inventory.Entry{entry("app"), entry("runtime"), entry("other"), entry("base")} - edges := stoodOn(map[string][]string{"app": {"runtime"}, "runtime": {"base"}}) - got := orderByBases(in, edges) - pos := map[string]int{} - for i, e := range got { - pos[e.Manifest.Module] = i - } - if !(pos["base"] < pos["runtime"] && pos["runtime"] < pos["app"]) { - t.Fatalf("bases not first: %v", pos) - } - if len(got) != 4 { - t.Fatalf("an entry was lost or doubled: %d", len(got)) - } - // A base outside the set is not waited for: it is not being rebuilt. - got = orderByBases([]inventory.Entry{entry("app")}, stoodOn(map[string][]string{"app": {"elsewhere"}})) - if len(got) != 1 { - t.Fatalf("a dependency outside the set changed the set: %v", got) - } -} - -// A module registered from its manifest and never built still names its bases there; once built, -// the recorded edge is what says so. Both are read, and a module never stands on itself. -func TestWhatStandsOnAModuleIsReadFromItsBuildOrItsManifest(t *testing.T) { - built := entry("gitea") - edges := stoodOn(map[string][]string{"gitea": {"mesh-tools"}}) - if !standsOnModule(built, "mesh-tools", edges) { - t.Fatal("a recorded edge was not read") - } - if standsOnModule(built, "gitea", edges) || standsOnModule(built, "postgres", edges) { - t.Fatal("an edge was invented") - } - fresh := inventory.Entry{Manifest: catalogue.Manifest{Module: "plex", Build: &catalogue.Build{ - On: []catalogue.BuildsOn{{Arg: "RUNTIME_BASE", Module: "mesh-tools", Artifact: "runtime"}}, - }}} - if !standsOnModule(fresh, "mesh-tools", nil) { - t.Fatal("a manifest's own base was not read") - } -} - // A merge names a repository the way the forge does; a source is recorded the way a build was // asked for. The two meet on owner/repo and branch, whichever form the record took. func TestAMergeMatchesTheSourcesBuiltFromIt(t *testing.T) { diff --git a/cmd/mesh-controller/plan_retry.go b/cmd/mesh-controller/plan_retry.go index 86dc803e..6cca81db 100644 --- a/cmd/mesh-controller/plan_retry.go +++ b/cmd/mesh-controller/plan_retry.go @@ -442,101 +442,6 @@ func retryWords(p inventory.Plan, failed, asked, waiting []string) (note, said s return note, said } -// joinAPlan asks a module again for the plan that holds it unbuilt or failed, if one does: open plans -// first, then the most recent failed one that nothing newer supersedes. Says whether it joined one. -func joinAPlan(ctx context.Context, open *stores, module string) (bool, string, error) { - inv := open.inventory - release, err := inv.HoldPlans(ctx, true) - if err != nil { - return false, "", err - } - defer release() - openPlans, err := inv.OpenPlans(ctx) - if err != nil { - return false, "", err - } - recent, err := inv.RecentPlans(ctx, 20) - if err != nil { - return false, "", err - } - p, found := planHolding(module, openPlans, recent) - if !found { - return false, "", nil - } - // **A walk that waits for its delivery's word asks nothing** (novox/hq ADR 0239, issue 754): a rebuild does not - // join it, and its outcome is not the walk's. The walk asks its own tier once it has its word. - if p.Waiting() { - return false, fmt.Sprintf("not joining %s (%s at %s): it waits for %s's word, so this build is not its "+ - "outcome; once let go, the walk asks %s itself", p.ID, p.Repository, short(p.Commit), p.Delivery.Awaits, - module), nil - } - entries, err := inv.Catalogued(ctx) - if err != nil { - return false, "", err - } - byName := map[string]inventory.Entry{} - for _, e := range entries { - byName[e.Manifest.Module] = e - } - was := p.State - askModule(ctx, &p, module, byName) - s := p.Modules[module] - resumed(&p, fmt.Sprintf("tier %d: %s rebuilt by hand", p.Tier, module)) - if err := inv.SavePlan(ctx, &p); err != nil { - return false, "", err - } - if s.State == "" && s.BusWaitFrom != nil { - return true, busWaitWords(p, []string{module}), nil - } - if s.State != "asked" { - return true, "", fmt.Errorf("%s could not be asked for %s: %s", module, p.ID, s.Why) - } - said := fmt.Sprintf("rebuild asked as %s, joining %s at tier %d (%s at %s): the plan takes this build as %s's outcome", - s.Build, p.ID, p.Tier, p.Repository, short(p.Commit), module) - switch { - case was == inventory.PlanFailed && p.State == inventory.PlanBuilding: - said += "; the plan had failed and builds again from this tier" - case was == inventory.PlanFailed: - said += "; the plan stays failed while " + strings.Join(failedIn(p), ", ") + " failed too — `plans retry " + p.ID + "` asks them" - } - return true, said, nil -} - -// planHolding is the plan a rebuild of a module joins: an open plan whose current tier holds it not -// yet built, else the newest failed plan whose current tier does, and that no open plan of its -// repository supersedes. -func planHolding(module string, openPlans, recent []inventory.Plan) (inventory.Plan, bool) { - holds := func(p inventory.Plan) bool { - if p.Tier >= len(p.Tiers) { - return false - } - for _, m := range p.Tiers[p.Tier] { - if m == module { - s := p.Modules[m] - return s == nil || s.State != "built" - } - } - return false - } - for _, p := range openPlans { - if holds(p) { - return p, true - } - } - sorted := append([]inventory.Plan(nil), recent...) - sort.SliceStable(sorted, func(i, j int) bool { return sorted[i].Created.After(sorted[j].Created) }) - for _, p := range sorted { - if p.State != inventory.PlanFailed || !holds(p) { - continue - } - if _, superseded := newerOpenPlan(p, openPlans); superseded { - continue - } - return p, true - } - return inventory.Plan{}, false -} - // retryRollouts sends each module whose rollout stopped to the machines it was first sent to, again, // records that send as the first anew, and sets the plan rolling: from there it goes on as the plan // would have — the rest sent once those report they applied it, the next tier after (ADR 0218). diff --git a/cmd/mesh-controller/queue.go b/cmd/mesh-controller/queue.go index f82e2919..a11ade45 100644 --- a/cmd/mesh-controller/queue.go +++ b/cmd/mesh-controller/queue.go @@ -477,89 +477,6 @@ func clearQueue(ctx context.Context, js *broker.JetStream, open *stores, seat st return b.String(), nil } -// rebuildCommand asks the module's current source again — or, given a build's id, that build's -// repository, path and ref — under a new id. -func rebuildCommand(ctx context.Context, args []string) error { - if len(args) != 1 { - return errors.New("rebuild ") - } - open, err := openStores(ctx) - if err != nil { - return err - } - defer open.Close() - inv := open.inventory - entries, err := inv.Catalogued(ctx) - if err != nil { - return err - } - var source buildSource - var path, ref, module string - if b, found, err := inv.BuildByID(ctx, args[0]); err != nil { - return err - } else if found { - source, module = sourceOfBuild(b, entries) - path, ref = b.Path, b.Ref - // **A build at a commit is rebuilt at what its module follows now** (novox/hq ADR 0219, issue - // 219): asked now, a rebuild of an old commit would be the newest ask of the module and roll - // that commit out over everything since. Building that commit again is `replay`, which says - // what it would do and refuses to register it while anything newer is asked or registered. - if e, known := entryNamed(entries, module); known && ref != "" && followedBranch(ref) == "" { - ref = followedBranch(e.Source.Ref) - follows := ref - if follows == "" { - follows = "the repository's default branch" - } - fmt.Printf("%s was built at commit %s; a rebuild asks what %s follows now, %s — `replay %s` "+ - "builds that commit\n", b.ID, short(b.Ref), module, follows, b.ID) - } - } else if e, known := entryNamed(entries, args[0]); known { - // As a plan asks it: the branch it follows, never a commit a build once named (issue 215). - source = buildSource{Repository: e.Source.Repository, Seat: e.Source.Seat} - path, ref, module = e.Source.Path, followedBranch(e.Source.Ref), e.Manifest.Module - } else { - return fmt.Errorf("%s is neither a module the catalogue holds nor a build the mesh recorded", args[0]) - } - - // **A module a plan holds unbuilt, or failed, joins that plan** rather than running beside it: the - // plan would ask it again, or stay failed on an outcome a rebuild has replaced. A walk waiting for its word - // is not joined (novox/hq issue 754): the rebuild runs beside it, and says so. - var beside string - if module != "" { - joined, said, err := joinAPlan(ctx, open, module) - if err != nil { - return err - } - if joined { - fmt.Println(said) - return nil - } - beside = said - } - id, err := askABuild(ctx, source, path, ref) - if err != nil { - return err - } - recordAsked(ctx, inventory.BuildRequest{ID: id, Repository: source.Repository, Seat: source.Seat, Path: path, - Ref: ref, For: "rebuild"}) - if beside != "" { - fmt.Printf("rebuild asked as %s, beside the walk: %s\n", id, beside) - return nil - } - fmt.Printf("rebuild asked as %s\n", id) - return nil -} - -// entryNamed is the catalogue's entry for a module. -func entryNamed(entries []inventory.Entry, name string) (inventory.Entry, bool) { - for _, e := range entries { - if e.Manifest.Module == name { - return e, true - } - } - return inventory.Entry{}, false -} - // sourceOfBuild is where a recorded build's repository is asked from: the catalogued module's own // source when the build is of one — on its seat, so the outcome registers it as the mesh records it // (ADR 0111) — else the repository as it was cloned. @@ -576,14 +493,17 @@ func sourceOfBuild(b inventory.Build, entries []inventory.Entry) (buildSource, s // replayCommand asks a recorded build's repository and path again at the commit it built. func replayCommand(ctx context.Context, args []string) error { set := flag.NewFlagSet("replay", flag.ContinueOnError) - register := set.Bool("register", false, "register what it builds, as any build is") - older := set.Bool("older", false, "with --register: even though a newer build of the module is registered") + register := set.Bool("register", false, "removed (novox/hq ADR 0501 §5): refused by name") positionals, err := parseAround(set, args) if err != nil { return err } + // **Registering a replay goes with the asks** (novox/hq ADR 0429 §18, ADR 0501 §5): refused by name. + if *register { + return removedByName("replay --register") + } if len(positionals) != 1 { - return errors.New("replay [--register [--older]]") + return errors.New("replay ") } open, err := openStores(ctx) if err != nil { @@ -603,86 +523,15 @@ func replayCommand(ctx context.Context, args []string) error { return err } source, module := sourceOfBuild(b, entries) - var history []inventory.Build - if module != "" { - if history, err = inv.Builds(ctx, module, 100); err != nil { - return err - } + if b.Commit == "" { + return fmt.Errorf("%s recorded no commit — it failed before it knew what it was building, so there is "+ + "nothing to replay", b.ID) } - // What is asked of the module and not yet answered, when the replay would be registered. - var outstanding []string - if *register { - js, err := dialTheBus() - if err != nil { - return err - } - q, err := link.ReadQueue(ctx, js, buildSeatHeld(ctx)) - js.Close() - if err != nil { - return err - } - plans, err := inv.OpenPlans(ctx) - if err != nil { - return err - } - outstanding = outstandingFor(module, b.Repository, b.Path, q, plans) - } - if err := replayRefusal(b, module, history, *register, *older, outstanding); err != nil { - return err - } - asker := "" - if *register { - asker = "replay" - } - id, err := buildOneAsked(ctx, source, b.Path, b.Commit, 0, !*register, asker) + id, err := buildOneAsked(ctx, source, b.Path, b.Commit, 0, true, "") if err != nil { return err } - fmt.Println(replaySaid(b, module, id, *register)) - return nil -} - -// replayRefusal is why a replay is not asked, or nothing (novox/hq ADR 0219, issue 207). -// -// A replay re-asks a recorded build at the commit it built, under a new id — and an id is when it -// was asked, which is what orders builds of one module (issue 219). So a registered replay of an -// older commit is newer than everything since, and would roll that older commit out as the module's -// current version: what issue 207 recorded happening by accident. It is therefore a dry run unless -// --register says otherwise, and --register is refused when a newer build of a different commit is -// registered, unless --older says that is the point. -// -// **And refused while anything newer is outstanding**, --older or not: an ask of the module in the -// queue, or an open plan holding it unbuilt. Asked now, the replay would be newer than those asks, -// and their builds — made from newer source — would be recorded and never registered (issue 219 -// orders by ask), the plan waiting on them sending the older commit instead. -func replayRefusal(b inventory.Build, module string, history []inventory.Build, register, older bool, - outstanding []string) error { - if b.Commit == "" { - return fmt.Errorf("%s recorded no commit — it failed before it knew what it was building, so there is "+ - "nothing to replay; `rebuild %s` asks its repository, path and ref again", b.ID, b.ID) - } - if older && !register { - return errors.New("--older only says what --register may do; a dry run registers nothing") - } - if register && len(outstanding) > 0 { - return fmt.Errorf("%s is asked and not yet answered — %s — and a registered replay asked now would "+ - "replace what those build (novox/hq issue 219). Wait for them, or `replay %s` without --register to look", - orNone(module), strings.Join(outstanding, "; "), b.ID) - } - if !register || older { - return nil - } - for _, h := range history { - if h.ID == b.ID || !h.Worked() || h.Commit == b.Commit { - continue - } - if h.AskedOrAt().After(b.AskedOrAt()) { - return fmt.Errorf("a newer build of %s is registered — %s, from %s — and registering a replay of %s "+ - "would roll that older commit out as %s's current version (novox/hq issue 207). `replay %s` "+ - "without --register looks at it; --register --older registers it anyway", - module, h.ID, short(h.Commit), short(b.Commit), module, b.ID) - } - } + fmt.Println(replaySaid(b, module, id, false)) return nil } @@ -822,32 +671,3 @@ func holdersAmong(entries []inventory.Entry, seat string) []string { sort.Strings(out) return out } - -// outstandingFor is everything asked of a module and not yet answered: its asks in the build queue, -// by repository and path, and every open plan holding it not yet built. -func outstandingFor(module, repository, path string, q link.Queue, plans []inventory.Plan) []string { - var out []string - for _, a := range q.Asks { - if repositoryMatches(a.Repository, repository) && a.Path == path { - out = append(out, fmt.Sprintf("%s %s in the queue", a.ID, a.State)) - } - } - if module == "" { - return out - } - for _, p := range plans { - if !p.Open() { - continue - } - for _, tier := range p.Tiers { - for _, m := range tier { - if m == module { - if st := p.Modules[m]; st == nil || st.State != "built" { - out = append(out, fmt.Sprintf("%s holds it not yet built", p.ID)) - } - } - } - } - } - return out -} diff --git a/cmd/mesh-controller/queue_integration_test.go b/cmd/mesh-controller/queue_integration_test.go index c0cb98f3..5bb334f2 100644 --- a/cmd/mesh-controller/queue_integration_test.go +++ b/cmd/mesh-controller/queue_integration_test.go @@ -71,44 +71,6 @@ func TestAFailedPlanIsRetriedAndGoesOnThroughItsLaterTiers(t *testing.T) { } } -func TestARebuildJoinsThePlanHoldingTheModule(t *testing.T) { - open := aMesh(t) - ctx := t.Context() - asked := asksRecorded(t) - twoTiers(t, open) - before := time.Now().UTC().Add(-time.Hour) - failed := inventory.Plan{ID: "plan-join", Repository: "novox/a", Branch: "main", Commit: "c0ffee", - Created: before, State: inventory.PlanFailed, Tiers: [][]string{{"a"}, {"b"}}, - Note: "a failed to build in tier 0", - Modules: map[string]*inventory.PlanModule{"a": {State: "failed", AskedAt: &before, Build: "build-1", Why: link.CancelledByHand}}} - if err := open.inventory.SavePlan(ctx, &failed); err != nil { - t.Fatal(err) - } - if err := rebuildCommand(ctx, []string{"a"}); err != nil { - t.Fatal(err) - } - p, err := open.inventory.PlanByID(ctx, failed.ID) - if err != nil { - t.Fatal(err) - } - if p.State != inventory.PlanBuilding || p.Modules["a"].State != "asked" || p.Modules["a"].Build == "build-1" { - t.Fatalf("the rebuild did not join the failed plan: %s %+v", p.State, p.Modules["a"]) - } - if len(*asked) != 1 { - t.Fatalf("asked %v", *asked) - } - // b is in a tier not yet reached: nothing holds it in its current tier, so it is asked alone. - if err := rebuildCommand(ctx, []string{"b"}); err != nil { - t.Fatal(err) - } - if p, _ = open.inventory.PlanByID(ctx, failed.ID); p.Modules["b"] != nil { - t.Fatalf("a module the plan has not reached joined it: %+v", p.Modules["b"]) - } - if len(*asked) != 2 { - t.Fatalf("asked %v", *asked) - } -} - func aBuildQueue(t *testing.T) (*broker.JetStream, func(id, repository string) link.BuildRequest) { t.Helper() url := testbus.URL(t) @@ -425,30 +387,6 @@ func TestAPlanIsAnsweredOnlyByTheBuildItAskedFor(t *testing.T) { } } -func TestARebuildOfACommitAsksWhatTheModuleFollows(t *testing.T) { - open := aMesh(t) - ctx := t.Context() - asked := asksRecorded(t) - twoTiers(t, open) - var refs []string - asksOfEveryModule(t) // the one ask left is the builder's own (ADR 0453 §5): this test is of what a walk does around it - was := askABuild - askABuild = func(c context.Context, source buildSource, path, ref string) (string, error) { - refs = append(refs, ref) - return was(c, source, path, ref) - } - if err := open.inventory.RecordBuild(ctx, inventory.Build{ID: "build-at-commit", Repository: "novox/a", - Ref: "0123456789abcdef0123456789abcdef01234567", Module: "a", Commit: "0123456789abcdef0123456789abcdef01234567"}); err != nil { - t.Fatal(err) - } - if err := rebuildCommand(ctx, []string{"build-at-commit"}); err != nil { - t.Fatal(err) - } - if len(refs) != 1 || refs[0] != "main" || len(*asked) != 1 { - t.Fatalf("asked %v at %v", *asked, refs) - } -} - func TestCancelOfAnAskNobodySaidIsWithdrawnWhenItStarts(t *testing.T) { js, ask := aBuildQueue(t) open := aMesh(t) diff --git a/cmd/mesh-controller/queue_test.go b/cmd/mesh-controller/queue_test.go index 43219812..da2fea1e 100644 --- a/cmd/mesh-controller/queue_test.go +++ b/cmd/mesh-controller/queue_test.go @@ -101,89 +101,6 @@ func TestRetryRefusesWhatItCannotResume(t *testing.T) { // `rebuild` of a module a failed plan holds joins that plan; one held by nothing runs alone. -// A failed plan an open plan of its repository supersedes is not joined: the open one holds the module. -func TestARebuildJoinsTheOpenPlanBeforeASupersededFailedOne(t *testing.T) { - at := time.Date(2026, 10, 5, 12, 0, 0, 0, time.UTC) - failed := inventory.Plan{ID: "plan-old", Repository: "novox/a", Branch: "main", Created: at, State: inventory.PlanFailed, - Tiers: [][]string{{"a"}}, Modules: map[string]*inventory.PlanModule{"a": {State: "failed"}}} - newer := inventory.Plan{ID: "plan-new", Repository: "novox/a", Branch: "main", Created: at.Add(time.Hour), - State: inventory.PlanBuilding, Tiers: [][]string{{"x"}, {"a"}}, Modules: map[string]*inventory.PlanModule{}} - if _, found := planHolding("a", []inventory.Plan{newer}, []inventory.Plan{newer, failed}); found { - t.Fatal("joined a failed plan a newer open one supersedes") - } - if p, found := planHolding("a", nil, []inventory.Plan{failed}); !found || p.ID != "plan-old" { - t.Fatalf("did not join the failed plan holding it: %v %s", found, p.ID) - } - built := failed - built.Modules = map[string]*inventory.PlanModule{"a": {State: "built"}} - if _, found := planHolding("a", nil, []inventory.Plan{built}); found { - t.Fatal("joined a plan that has the module built") - } -} - -// replay is a dry run unless registered, and registering an older commit than one registered since -// is refused unless --older says it is meant (novox/hq issue 207). -func TestReplayRefusesToRollAnOlderCommitOutUnlessToldTo(t *testing.T) { - asked := time.Date(2026, 10, 5, 12, 0, 0, 0, time.UTC) - old := inventory.Build{ID: "build-old", Module: "a", Commit: "0ldc0mm1t", Asked: asked} - newer := inventory.Build{ID: "build-new", Module: "a", Commit: "n3wc0mm1t", Asked: asked.Add(time.Hour)} - history := []inventory.Build{newer, old} - - if err := replayRefusal(old, "a", history, false, false, nil); err != nil { - t.Errorf("a dry run was refused: %v", err) - } - err := replayRefusal(old, "a", history, true, false, nil) - if err == nil || !strings.Contains(err.Error(), "build-new") || !strings.Contains(err.Error(), "--older") { - t.Errorf("registering an older commit than the one registered was not refused: %v", err) - } - if err := replayRefusal(old, "a", history, true, true, nil); err != nil { - t.Errorf("--register --older was refused: %v", err) - } - if err := replayRefusal(newer, "a", history, true, false, nil); err != nil { - t.Errorf("registering the newest build again was refused: %v", err) - } - // A newer build of the same commit, or one that failed, is not a newer version to roll back from. - same := inventory.Build{ID: "build-same", Module: "a", Commit: old.Commit, Asked: asked.Add(2 * time.Hour)} - broken := inventory.Build{ID: "build-broken", Module: "a", Failed: "no", Asked: asked.Add(3 * time.Hour)} - if err := replayRefusal(old, "a", []inventory.Build{broken, same, old}, true, false, nil); err != nil { - t.Errorf("refused for a newer build of the same commit or a failed one: %v", err) - } - if err := replayRefusal(inventory.Build{ID: "build-x", Failed: "clone"}, "", nil, false, false, nil); err == nil { - t.Error("a build that recorded no commit was replayed") - } - if err := replayRefusal(old, "a", history, false, true, nil); err == nil { - t.Error("--older without --register was taken") - } - // **Nothing newer outstanding**, --older or not: an ask of the module in the queue, or an open plan - // holding it unbuilt, would be replaced by a replay asked now (issue 219). - q := link.Queue{Asks: []link.QueuedAsk{ - {ID: "build-queued", Repository: "https://forge.example/novox/a.git", State: link.AskWaiting}, - {ID: "build-elsewhere", Repository: "https://forge.example/novox/b.git", State: link.AskWaiting}, - {ID: "build-other-path", Repository: "https://forge.example/novox/a.git", Path: "sub", State: link.AskWaiting}, - }} - plans := []inventory.Plan{ - {ID: "plan-holds", State: inventory.PlanBuilding, Tiers: [][]string{{"x"}, {"a"}}, Modules: map[string]*inventory.PlanModule{}}, - {ID: "plan-built", State: inventory.PlanRolling, Tiers: [][]string{{"a"}}, Modules: map[string]*inventory.PlanModule{"a": {State: "built"}}}, - {ID: "plan-failed", State: inventory.PlanFailed, Tiers: [][]string{{"a"}}, Modules: map[string]*inventory.PlanModule{}}, - } - outstanding := outstandingFor("a", "novox/a", "", q, plans) - if len(outstanding) != 2 || !strings.Contains(outstanding[0], "build-queued") || !strings.Contains(outstanding[1], "plan-holds") { - t.Fatalf("outstanding: %v", outstanding) - } - if err := replayRefusal(old, "a", history, true, true, outstanding); err == nil || !strings.Contains(err.Error(), "build-queued") { - t.Errorf("registered over an outstanding ask: %v", err) - } - if err := replayRefusal(old, "a", history, false, false, outstanding); err != nil { - t.Errorf("a dry run was refused for what is outstanding: %v", err) - } - if said := replaySaid(old, "a", "build-1", false); !strings.Contains(said, "dry run") || !strings.Contains(said, "--register") { - t.Errorf("a dry replay does not say what it is: %q", said) - } - if said := replaySaid(old, "a", "build-1", true); !strings.Contains(said, "registered") || !strings.Contains(said, "rolled out") { - t.Errorf("a registered replay does not say what it does: %q", said) - } -} - // A plan waiting on builds of a seat whose every holder is paused says so and is not late; a seat // paused on some holders only is not a reason the plan is waiting. func TestAPlanWaitingOnAPausedSeatSaysSoAndIsNotLate(t *testing.T) { @@ -241,9 +158,9 @@ func TestTheQueueVerbsRunTheirCommands(t *testing.T) { {"cancel", map[string]any{"id": "build-1"}, []string{"cancel", "build-1"}}, {"clear", nil, []string{"clear"}}, {"clear", map[string]any{"dead": "true"}, []string{"clear", "--dead"}}, - {"rebuild", map[string]any{"what": "gitea"}, []string{"rebuild", "gitea"}}, + {"rebuild", map[string]any{"what": "gitea"}, []string{"rebuild"}}, {"replay", map[string]any{"id": "build-1"}, []string{"replay", "build-1"}}, - {"replay", map[string]any{"id": "build-1", "register": "true", "older": "true"}, []string{"replay", "build-1", "--register", "--older"}}, + {"replay", map[string]any{"id": "build-1", "register": "true", "older": "true"}, []string{"replay", "build-1", "--register"}}, {"kill", map[string]any{"id": "build-1"}, []string{"kill", "build-1"}}, {"pause", nil, []string{"pause"}}, {"resume", map[string]any{"node": "ace"}, []string{"resume", "ace"}}, diff --git a/cmd/mesh-controller/seatverbs.go b/cmd/mesh-controller/seatverbs.go index b7b00d09..c07fe9ba 100644 --- a/cmd/mesh-controller/seatverbs.go +++ b/cmd/mesh-controller/seatverbs.go @@ -86,6 +86,14 @@ type verbArguments struct { misread []string // arguments the command line read that the schema does not declare: a bug here } +// passOver takes every argument given as read: a removed verb is refused by name whatever was asked of it (novox/hq ADR +// 0501 §5), so no argument of a table of before is refused as passed over in its place. +func (a *verbArguments) passOver() { + for k := range a.given { + a.used[k] = true + } +} + // controllerVerb is this binary's own definition of a verb: what it runs is what it declares, so the // arguments are checked against the table compiled beside argvFor, not a row a newer or older build // wrote. @@ -522,21 +530,19 @@ func (a *verbArguments) commandLine() ([]string, error) { } return []string{"clear"}, nil case "rebuild": - if err := need("what"); err != nil { - return nil, err - } - return []string{"rebuild", str("what")}, nil + // Removed (ADR 0501 §5): the command refuses it by name, naming its replacement, whatever was asked. + a.passOver() + return []string{"rebuild"}, nil case "replay": if err := need("id"); err != nil { return nil, err } argv := []string{"replay", str("id")} + // A table of before still offers register: passed on, so the command refuses it by name (ADR 0501 §5). if on("register") { argv = append(argv, "--register") } - if on("older") { - argv = append(argv, "--older") - } + a.passOver() return argv, nil case "pause", "resume": if n := str("node"); n != "" { @@ -1076,34 +1082,9 @@ func (a *verbArguments) commandLine() ([]string, error) { } return []string{"module", "issue", str("module"), "--node", str("node")}, nil case "build": - // The command's three shapes (`build --on`, `build --behind`, `build `), one per - // call: each branch reads only its own argument, so another given beside it is refused as - // passed over rather than dropped. Asked and not waited for, the same as a single build. - if b := str("on"); b != "" { - return []string{"build", "--on", b, "--wait", "0"}, nil - } - if on("behind") { - return []string{"build", "--behind", "--wait", "0"}, nil - } - if a.given["repository"] == "" { - // No shape named: the command says its usage, which names all three — the answer the - // caller needs, and the same as `rotate` given neither of its shapes. - return []string{"build"}, nil - } - // Not waited for: a tool call cannot hold a connection for the minutes a build takes; the - // daemon takes the outcome in when it comes and the id follows the build (issue 176). A - // repository given without a scheme is a path on the forge holding the git seat. - argv := []string{"build", str("repository"), "--wait", "0"} - if !strings.Contains(str("repository"), "://") && !strings.HasPrefix(str("repository"), "git@") { - argv = append(argv, "--self") - } - if p := str("path"); p != "" { - argv = append(argv, "--path", p) - } - if r := str("ref"); r != "" { - argv = append(argv, "--ref", r) - } - return argv, nil + // Removed (ADR 0501 §5): the command refuses it by name, naming its replacement, whatever was asked. + a.passOver() + return []string{"build"}, nil } return nil, fmt.Errorf("%q is a verb of the %s seat's table that this binary has no command line for", verb, catalogue.ControllerSeatName) diff --git a/cmd/mesh-controller/seatverbs_test.go b/cmd/mesh-controller/seatverbs_test.go index 361fdc49..49756a89 100644 --- a/cmd/mesh-controller/seatverbs_test.go +++ b/cmd/mesh-controller/seatverbs_test.go @@ -1,6 +1,7 @@ package main import ( + "context" "errors" "fmt" "os" @@ -21,66 +22,6 @@ func TestBuildsWithAnIdReadsThatBuildsLog(t *testing.T) { } } -// The build tool takes a repository as a URL or as its path on the forge holding the git seat, and -// says which it was given, so the command reads the path as a seat source rather than handing it to -// git as written (novox/hq issue 176). And it never waits: the id follows the build. -func TestTheBuildToolTellsAForgePathFromAURL(t *testing.T) { - argv, _ := argvFor("build", map[string]any{"repository": "novox/mesh-catalog", "path": "modules/x"}) - if line := strings.Join(argv, " "); !strings.Contains(line, "--self") || !strings.Contains(line, "--wait 0") { - t.Fatalf("a forge path is a seat source, not waited for; got %q", line) - } - argv, _ = argvFor("build", map[string]any{"repository": "https://example.tld/o/r.git"}) - if line := strings.Join(argv, " "); strings.Contains(line, "--self") { - t.Fatalf("a URL is cloned as given; got %q", line) - } -} - -// The build tool has the command's three shapes: a repository, a base whose dependents are rebuilt -// (`--on`), or everything behind its source (`--behind`) — each asked, not waited for, one per call. -// Given neither, the command line is what it was: nothing new reaches it. -func TestTheBuildToolTakesOnAndBehind(t *testing.T) { - cases := []struct { - args map[string]any - want string - }{ - {map[string]any{"on": "mesh-tools"}, "build --on mesh-tools --wait 0"}, - {map[string]any{"behind": "true"}, "build --behind --wait 0"}, - {map[string]any{"behind": true}, "build --behind --wait 0"}, - {map[string]any{"repository": "novox/x"}, "build novox/x --wait 0 --self"}, - {map[string]any{"repository": "novox/x", "behind": "false"}, "build novox/x --wait 0 --self"}, - {map[string]any{"repository": "novox/x", "path": "modules/x", "ref": "main"}, - "build novox/x --wait 0 --self --path modules/x --ref main"}, - } - for _, c := range cases { - argv, err := argvFor("build", c.args) - if err != nil { - t.Errorf("%v: %v", c.args, err) - continue - } - if got := strings.Join(argv, " "); got != c.want { - t.Errorf("%v became %q, not %q", c.args, got, c.want) - } - } - // Naming no shape runs the bare command, which answers its usage naming all three. - if argv, err := argvFor("build", map[string]any{}); err != nil || strings.Join(argv, " ") != "build" { - t.Errorf("a build naming nothing became %v, %v", argv, err) - } - // One shape per call: a second beside it is refused, not dropped, and so is half of one. - for _, refused := range []map[string]any{ - {"path": "modules/x"}, - {"on": "mesh-tools", "repository": "novox/x"}, - {"on": "mesh-tools", "behind": "true"}, - {"behind": "true", "repository": "novox/x"}, - {"behind": "true", "path": "modules/x"}, - {"on": "mesh-tools", "ref": "main"}, - {"behind": "yes"}, - } { - if argv, err := argvFor("build", refused); err == nil { - t.Errorf("%v was accepted as %v", refused, argv) - } - } -} - // `rotate` is one verb with two shapes (ADR 0114, issue 180): a pair credential by provision, or a // module's own secret by machine, module and name. func TestRotateTakesAProvisionOrAnOwnSecret(t *testing.T) { @@ -171,9 +112,35 @@ func TestActsDoNotBlockTheCall(t *testing.T) { if strings.Join(argv, " ") != "push one --wait 0s --why w" || !answersFirst(argv) { t.Fatalf("a named push with no setting waits, or holds its caller: %v", argv) } - argv, _ = argvFor("build", map[string]any{"repository": "novox/x", "path": "modules/x"}) - if strings.Join(argv, " ") != "build novox/x --wait 0 --self --path modules/x" { - t.Fatalf("build: %v", argv) +} + +// A11a (ADR 0501 §5; issue 1851): the build, rebuild and replay --register verbs reach a command that refuses them by +// name, naming the replacement, whatever a table of before asked of them; the command lines refuse the same. +func TestTheRemovedVerbsAreRefusedByName(t *testing.T) { + for _, c := range []struct { + verb string + args map[string]any + want string + }{ + {"build", map[string]any{"repository": "novox/x", "path": "modules/x"}, "build"}, + {"build", map[string]any{"on": "mesh-tools"}, "build"}, + {"rebuild", map[string]any{"what": "gitea"}, "rebuild"}, + {"replay", map[string]any{"id": "build-1", "register": "true", "older": "true"}, "replay build-1 --register"}, + } { + argv, err := argvFor(c.verb, c.args) + if err != nil || strings.Join(argv, " ") != c.want { + t.Errorf("%s %v: %v %v, want %q", c.verb, c.args, argv, err, c.want) + } + } + for _, removed := range []string{"build", "rebuild"} { + err := removedByName(removed) + if err == nil || !strings.Contains(err.Error(), "`"+removed+"` is removed") || !strings.Contains(err.Error(), "ADR 0501") { + t.Errorf("%s: %v", removed, err) + } + } + err := replayCommand(context.Background(), []string{"build-1", "--register"}) + if err == nil || !strings.Contains(err.Error(), "`replay --register` is removed") { + t.Errorf("replay --register: %v", err) } } diff --git a/cmd/mesh-controller/try.go b/cmd/mesh-controller/try.go index d306975e..7dcd4ed3 100644 --- a/cmd/mesh-controller/try.go +++ b/cmd/mesh-controller/try.go @@ -12,9 +12,6 @@ import ( "strings" "time" - "github.com/nats-io/nats.go" - - "github.com/novox/mesh-controller/internal/broker" "github.com/novox/mesh-controller/internal/link" ) @@ -300,35 +297,3 @@ func printTry(a tryAnswer) error { fmt.Println(string(out)) return nil } - -// buildLines is everything the build seat said of one build, as the bus keeps it, in order. -func buildLines(ctx context.Context, id string) ([]link.BuildLine, error) { - js, err := aBus() - if err != nil { - return nil, fmt.Errorf("cannot reach the bus to read a build's log: %w", err) - } - defer js.Close() - subject := link.BuildLogOf("*", id) - sub, err := js.Context().PullSubscribe(subject, "", - nats.BindStream(broker.EventsStream), nats.DeliverAll(), nats.AckNone()) - if err != nil { - return nil, fmt.Errorf("cannot read %s from the bus: %w", subject, err) - } - defer func() { _ = sub.Unsubscribe() }() - var lines []link.BuildLine - for { - batch, err := sub.Fetch(200, nats.MaxWait(2*time.Second)) - if err != nil && !errors.Is(err, nats.ErrTimeout) && !errors.Is(err, context.DeadlineExceeded) { - return nil, fmt.Errorf("reading a build's log: %w", err) - } - for _, msg := range batch { - var line link.BuildLine - if json.Unmarshal(msg.Data, &line) == nil { - lines = append(lines, line) - } - } - if len(batch) < 200 { - return lines, nil - } - } -} diff --git a/cmd/mesh-controller/upgrades.go b/cmd/mesh-controller/upgrades.go index 07a3c964..f2140362 100644 --- a/cmd/mesh-controller/upgrades.go +++ b/cmd/mesh-controller/upgrades.go @@ -404,15 +404,6 @@ func movesOfMerge(ctx context.Context, inv *inventory.Inventory, m link.SourceMo return names, nil } -// notAskedID is the id a merge's build that could not be asked is kept under: a key the bus takes, so a module at the -// repository's root (".") is kept as one in a folder is. -func notAskedID(commit, dir string) string { - if dir == "." || dir == "" { - return "not-asked-" + short(commit) - } - return fmt.Sprintf("not-asked-%s-%s", short(commit), strings.ReplaceAll(strings.Trim(dir, "/"), "/", "-")) -} - // actingOnMerges keeps one merge acted on at a time (novox/hq issue 266). var actingOnMerges sync.Mutex @@ -1085,50 +1076,6 @@ func anyInside(paths []string, dir string) bool { return false } -// orderByBases is the entries with every base before what stands on it: a module whose build stood -// on another's artifact comes after that module. Entries outside the set are not waited for — they -// are not being rebuilt. Stable for what has no order between it. -// -// `against` is what each module's newest build stood on (inventory.BuiltAgainst): the edges are -// derived from builds, not declared, because a recorded manifest no longer carries `build.on`. -func orderByBases(entries []inventory.Entry, against map[string][]string) []inventory.Entry { - inSet := map[string]bool{} - for _, e := range entries { - inSet[e.Manifest.Module] = true - } - var out []inventory.Entry - placed := map[string]bool{} - var place func(e inventory.Entry, seen map[string]bool) - place = func(e inventory.Entry, seen map[string]bool) { - name := e.Manifest.Module - if placed[name] || seen[name] { - return - } - seen[name] = true - for _, base := range entries { - if base.Manifest.Module != name && inSet[base.Manifest.Module] && standsOnModule(e, base.Manifest.Module, against) { - place(base, seen) - } - } - placed[name] = true - out = append(out, e) - } - for _, e := range entries { - place(e, map[string]bool{}) - } - return out -} - -// standsOn is whether anything in the set is built on the named module's artifacts. -func standsOn(entries []inventory.Entry, module string, against map[string][]string) bool { - for _, e := range entries { - if standsOnModule(e, module, against) { - return true - } - } - return false -} - // standsOnModule is whether an entry's build stood on the named module: by what its newest build // recorded it was handed (`artifact-store:///@…`, the module's own artifact), or // — for a module registered from a manifest and not yet built — by the base its manifest names. @@ -1152,6 +1099,16 @@ func standsOnModule(e inventory.Entry, module string, against map[string][]strin return false } +// standsOn is whether anything in the set is built on the named module's artifacts. +func standsOn(entries []inventory.Entry, module string, against map[string][]string) bool { + for _, e := range entries { + if standsOnModule(e, module, against) { + return true + } + } + return false +} + // isHistory is whether a merge made at mergedAt predates the last time the source was seen. A merge // with no time on it is taken as news: refusing it would silence a forge that says less. func isHistory(mergedAt string, seen time.Time) bool { diff --git a/cmd/mesh-controller/walk_from_keys_integration_test.go b/cmd/mesh-controller/walk_from_keys_integration_test.go index d4609105..bc8960c1 100644 --- a/cmd/mesh-controller/walk_from_keys_integration_test.go +++ b/cmd/mesh-controller/walk_from_keys_integration_test.go @@ -69,7 +69,7 @@ func TestAWalkTakesItsTierFromTheBuildersKeysAndSendsIt(t *testing.T) { t.Fatal(err) } for _, m := range []string{"app1", "app2"} { - if current[m].Commit != p.Commit || !isKeyBuildID(p.Modules[m].Build) { + if current[m].Commit != p.Commit || !strings.HasPrefix(p.Modules[m].Build, "key-") { t.Fatalf("A6: %s stands on %+v (%s), not the key's build at %s", m, current[m], p.Modules[m].Build, p.Commit) } } diff --git a/cmd/mesh-controller/walk_said_integration_test.go b/cmd/mesh-controller/walk_said_integration_test.go index 37190421..f53ed71c 100644 --- a/cmd/mesh-controller/walk_said_integration_test.go +++ b/cmd/mesh-controller/walk_said_integration_test.go @@ -206,35 +206,12 @@ func TestAWalkEndedByCancelOrClearIsSaidEnded(t *testing.T) { } } -func TestAWalkRestartedByARebuildIsSaidStarted(t *testing.T) { - open := aMesh(t) - asksRecorded(t) - next := planMovedHeard(t) - twoTiers(t, open) - before := time.Now().UTC().Add(-time.Hour) - ended := before.Add(time.Minute) - failed := inventory.Plan{ID: "plan-308-rebuild", Repository: "novox/a", Branch: "main", Commit: "c0ffee", - Created: before, State: inventory.PlanFailed, Tiers: [][]string{{"a"}, {"b"}}, - Modules: map[string]*inventory.PlanModule{"a": {State: "failed", AskedAt: &before, Build: "build-1", - Why: link.CancelledByHand}}, - Times: &inventory.PlanTimes{Cut: &before, Started: &before, Ended: &ended, Class: inventory.ClassLeaf}} - if err := open.inventory.SavePlan(t.Context(), &failed); err != nil { - t.Fatal(err) - } - if err := asTyped(t, "rebuild", "a"); err != nil { - t.Fatal(err) - } - if started := next(inventory.WalkStarted); started["walk"] != failed.ID { - t.Fatalf("the rebuilt walk's start: %v", started) - } -} - func TestAWalkStoppedByHandAndRetriedEndsFailedWhenItFailsAgain(t *testing.T) { open := aMesh(t) ctx := t.Context() asksRecorded(t) twoTiers(t, open) - for _, again := range [][]string{{"plans", "retry"}, {"rebuild", "a"}} { + for _, again := range [][]string{{"plans", "retry"}} { before := time.Now().UTC().Add(-time.Hour) walk := aStartedWalkAsking(t, open, "plan-308-again-"+again[0], "build-1", before) if err := asTyped(t, "plans", "stop", walk.ID, "--why", "a test"); err != nil { diff --git a/cmd/mesh-controller/walk_wait_rebuild_754_integration_test.go b/cmd/mesh-controller/walk_wait_rebuild_754_integration_test.go deleted file mode 100644 index ae027544..00000000 --- a/cmd/mesh-controller/walk_wait_rebuild_754_integration_test.go +++ /dev/null @@ -1,63 +0,0 @@ -//go:build integration - -package main - -import ( - "testing" - "time" - - "github.com/novox/mesh-controller/internal/inventory" - "github.com/novox/mesh-controller/internal/link" -) - -func TestARebuildInTheWaitIsNotTheWalksBuild(t *testing.T) { - open := aMesh(t) - ctx := t.Context() - asked := asksRecorded(t) - twoTiers(t, open) - created := time.Now().UTC().Add(-10 * time.Minute) - plan := inventory.Plan{ID: "plan-754", Repository: "novox/a", Branch: "main", Commit: "c0ffee", Created: created, - State: inventory.PlanBuilding, Tiers: [][]string{{"a", "b"}}, Modules: map[string]*inventory.PlanModule{}, - Delivery: &inventory.PlanDelivery{Awaits: "mesh-delivery"}} - if err := open.inventory.SavePlan(ctx, &plan); err != nil { - t.Fatal(err) - } - - // A rebuild of a, beside the waiting walk: not joined, and its outcome not taken. - joined, said, err := joinAPlan(ctx, open, "a") - if err != nil || joined || said == "" { - t.Fatalf("a rebuild joined a waiting walk: %v %q %v", joined, said, err) - } - planBuilt(ctx, open, "a", "c0ffee", "", time.Now().UTC(), "build-rebuild") - p, _ := open.inventory.PlanByID(ctx, plan.ID) - if s := p.Modules["a"]; s != nil && s.State != "" { - t.Fatalf("the waiting walk took a rebuild as its build: %+v", s) - } - if len(*asked) != 0 { - t.Fatalf("a waiting walk asked %v", *asked) - } - - // Let go: both are asked, and their outcomes complete the tier. - if _, err := letGo(ctx, open.inventory, plan.ID, "operator", "test"); err != nil { - t.Fatal(err) - } - advancePlans(ctx, open) - p, _ = open.inventory.PlanByID(ctx, plan.ID) - for _, m := range []string{"a", "b"} { - if s := p.Modules[m]; s == nil || s.State != "asked" || s.Build == "" { - t.Fatalf("let go, %s is %+v", m, s) - } - } - if len(*asked) != 2 { - t.Fatalf("asked %v", *asked) - } - for _, m := range []string{"a", "b"} { - id := p.Modules[m].Build - at, _ := link.BuildAskedAt(id) - planBuilt(ctx, open, m, "c0ffee", "", at, id) - } - advancePlans(ctx, open) - if p, _ = open.inventory.PlanByID(ctx, plan.ID); p.Open() && p.Tier == 0 { - t.Fatalf("the walk did not get past its tier: %s at %d, %q", p.State, p.Tier, p.Note) - } -}