From 0a40c046cfbf76573ed55693992ce8a9ec5a8dd3 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 1 Oct 2026 22:38:49 +0200 Subject: [PATCH] plans --what-if: the plan a merge would produce, read before merging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Given a repository and the files a branch changes (or the modules by name), plans answers with the tiers the merge handler would produce — the modules those files touch, what packages their source, everything reachable along the dependency relation — and what each tier does: built and sent to its machines, or built and left because its policy records. Saved nowhere. On the seat as plans {repository, paths|modules}. --- cmd/mesh-controller/release_plan.go | 94 +++++++++++++++++++++++++++++ cmd/mesh-controller/seatverbs.go | 10 +++ internal/catalogue/verbs.go | 7 ++- 3 files changed, 109 insertions(+), 2 deletions(-) diff --git a/cmd/mesh-controller/release_plan.go b/cmd/mesh-controller/release_plan.go index 535433e..83976f5 100644 --- a/cmd/mesh-controller/release_plan.go +++ b/cmd/mesh-controller/release_plan.go @@ -576,6 +576,9 @@ func openPlans(plans []inventory.Plan) ([]inventory.Plan, int) { func plansCommand(ctx context.Context, args []string) error { set := flag.NewFlagSet("plans", flag.ContinueOnError) limit := set.Int("n", 10, "how many to show") + whatIf := set.String("what-if", "", "owner/repository: the plan a merge there would produce, saving nothing — with --paths or --modules") + paths := set.String("paths", "", "the files the merge would change, comma-separated, from the repository's root") + modules := set.String("modules", "", "or the modules it would change, comma-separated") positionals, err := parseAround(set, args) if err != nil { return err @@ -616,6 +619,9 @@ func plansCommand(ctx context.Context, args []string) error { } return nil } + if *whatIf != "" { + return planWhatIf(ctx, inv, *whatIf, splitList(*paths), splitList(*modules)) + } if len(positionals) == 2 && positionals[0] == "stop" { p, err := inv.PlanByID(ctx, positionals[1]) if err != nil { @@ -646,3 +652,91 @@ func plansCommand(ctx context.Context, args []string) error { } return nil } + +// planWhatIf is the plan a merge would produce, computed the way the merge handler computes one +// and saved nowhere: the modules the repository's changed files touch (or the modules named), what +// packages their source, everything reachable from them, in tiers. For reading before merging. +func planWhatIf(ctx context.Context, inv *inventory.Inventory, repository string, paths, modules []string) error { + owner, repo, found := strings.Cut(repository, "/") + if !found { + return fmt.Errorf("--what-if takes owner/repository, not %q", repository) + } + m := link.SourceMoved{Owner: owner, Repo: repo, Base: "main", Commit: "what-if", Paths: paths} + entries, err := inv.Catalogued(ctx) + if err != nil { + return err + } + read, err := inv.ReadRepositories(ctx) + if err != nil { + return err + } + var from, packaging []inventory.Entry + named := map[string]bool{} + for _, name := range modules { + named[name] = true + } + for _, e := range entries { + switch { + case named[e.Manifest.Module]: + from = append(from, e) + case len(named) == 0 && sourceIs(e.Source, m): + from = append(from, e) + case readsFrom(read[e.Manifest.Module], m): + packaging = append(packaging, e) + } + } + if len(named) == 0 { + from = whatTheMergeTouched(from, entries, m) + } + moved := append(append([]inventory.Entry{}, from...), packaging...) + if len(moved) == 0 { + fmt.Printf("a merge of %s changing %s would build nothing the mesh holds\n", repository, + orNone(strings.Join(append(paths, modules...), ", "))) + return nil + } + edges, err := inv.Dependencies(ctx) + if err != nil { + return err + } + var names []string + for _, e := range moved { + names = append(names, e.Manifest.Module) + } + p := planOfMerge(m, names, edges) + fmt.Printf("a merge of %s would build %d module(s) in %d tier(s):\n", repository, len(p.Modules), len(p.Tiers)) + rolls := map[string]string{} + for i, tier := range p.Tiers { + fmt.Printf(" tier %d\n", i) + for _, name := range tier { + how := "built; its policy records, so nothing is sent" + if u, err := inv.UpgradeOf(ctx, name); err == nil && u.RollOut { + running, _ := inv.Running(ctx, name) + how = "built, then sent to " + orNone(strings.Join(running, ", ")) + rolls[name] = how + } + fmt.Printf(" %-22s %s\n", name, how) + } + } + if hasCycle(p.Tiers, edges) { + fmt.Println(" the last tier depends on itself and would be built together, in no order") + } + if len(packaging) > 0 { + var also []string + for _, e := range packaging { + also = append(also, e.Manifest.Module) + } + fmt.Printf(" %s package source from %s, so they are rebuilt without their own source moving\n", + strings.Join(also, ", "), repository) + } + return nil +} + +func splitList(s string) []string { + var out []string + for _, part := range strings.Split(s, ",") { + if part = strings.TrimSpace(part); part != "" { + out = append(out, part) + } + } + return out +} diff --git a/cmd/mesh-controller/seatverbs.go b/cmd/mesh-controller/seatverbs.go index 08922ca..bb9db6d 100644 --- a/cmd/mesh-controller/seatverbs.go +++ b/cmd/mesh-controller/seatverbs.go @@ -70,6 +70,16 @@ func argvFor(verb string, args map[string]any) ([]string, error) { } return []string{"builds"}, nil case "plans": + if r := str("repository"); r != "" { + argv := []string{"plans", "--what-if", r} + if p := str("paths"); p != "" { + argv = append(argv, "--paths", p) + } + if m := str("modules"); m != "" { + argv = append(argv, "--modules", m) + } + return argv, nil + } if id := str("stop"); id != "" { return []string{"plans", "stop", id}, nil } diff --git a/internal/catalogue/verbs.go b/internal/catalogue/verbs.go index 0f1a473..aa36a0a 100644 --- a/internal/catalogue/verbs.go +++ b/internal/catalogue/verbs.go @@ -94,8 +94,11 @@ var ControllerVerbs = []Verb{ {Name: "plans", Description: "What the last merges produced and where each stands (novox/hq ADR 0162): " + "the tiers, the tier a plan is at, what it waits for and since when; one plan whole, given its id.", Input: schema(map[string]string{ - "id": "a plan's id (as `plans` lists them): that plan, tier by tier", - "stop": "a plan's id: stop it — what was asked still builds, nothing further is asked", + "id": "a plan's id (as `plans` lists them): that plan, tier by tier", + "stop": "a plan's id: stop it — what was asked still builds, nothing further is asked", + "repository": "owner/repository: the plan a merge there would produce, saving nothing (what-if); with paths or modules", + "paths": "with repository: the files the merge would change, comma-separated, from the repository's root", + "modules": "with repository: or the modules it would change, comma-separated", }, nil)}, {Name: "plan", Description: "What one machine would run, and why: the declaration the mesh would send it.", Input: schema(map[string]string{"node": "the machine's name"}, []string{"node"})},