From aaadb5695c46b527b65615a880afb54bb1aaa7a6 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 24 Oct 2026 17:15:31 +0200 Subject: [PATCH] Keep B8's upgrades.go, which the commit before overwrote from an older copy; only the policy's rejudgement is added (issue 1945, B11) --- cmd/mesh-controller/upgrades.go | 392 +------------------------------- 1 file changed, 8 insertions(+), 384 deletions(-) diff --git a/cmd/mesh-controller/upgrades.go b/cmd/mesh-controller/upgrades.go index 5256f697..fb17aaf6 100644 --- a/cmd/mesh-controller/upgrades.go +++ b/cmd/mesh-controller/upgrades.go @@ -13,7 +13,6 @@ import ( "sync" "time" - "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) @@ -43,30 +42,6 @@ func (f following) Upgraded(ctx context.Context, u link.Upgraded) error { return nil } -// askAgainOnGrants marks a send that stopped at its grants as one to ask again (novox/hq issue 249): -// an announcement handled by a send whose memberships could not be issued is held and redelivered, -// rather than taken as handled with the machines left on the old version. -func askAgainOnGrants(err error) error { - if err != nil && errors.Is(err, errGrants) && !errors.Is(err, link.ErrTryAgain) { - return fmt.Errorf("%w: %w", link.ErrTryAgain, err) - } - return err -} - -// readableList names machines the way a sentence does, because this is read by a person deciding -// whether an upgrade went where they expected. -func readableList(names []string) string { - switch len(names) { - case 0: - return "nothing" - case 1: - return names[0] - case 2: - return names[0] + " and " + names[1] - } - return strings.Join(names[:len(names)-1], ", ") + " and " + names[len(names)-1] -} - func shortCommit(commit string) string { if len(commit) > 8 { return commit[:8] @@ -74,13 +49,6 @@ func shortCommit(commit string) string { return commit } -func isAre(n int) string { - if n == 1 { - return "is" - } - return "are" -} - // Announceable is every build this mesh recorded, in the shape the builder announces one. // // **The catalogue asks for this when it starts, and the answer is the graph's foundation** @@ -125,25 +93,6 @@ func (f following) Announceable(ctx context.Context) ([]link.Announcement, error return out, nil } -// notNow marks a store that could not be read right now, or plans another controller is working (issue 921), so -// the announcement is held rather than lost; anything else is returned as it was. -func notNow(err error) error { - if inventory.Unreachable(err) { - return fmt.Errorf("%w: %w", link.ErrTryAgain, err) - } - return err -} - -// unsaved marks the failure to save the plan a merge was added to, whatever it was: the merge is carried until that save -// writes it (inventory.AddMerge), so the announcement is held and handed over again, never taken (novox/hq ADR 0407 -// decision 3, ADR 0408, issue 1092). The bus acknowledges it only after the save succeeded. -func unsaved(err error) error { - if err == nil { - return nil - } - return fmt.Errorf("%w: the merge is not kept until its plan is saved: %w", link.ErrTryAgain, err) -} - // SourceMoved is the forge announcing a merge. It is put into the open batch (novox/hq ADR 0276), which // becomes one walk when its merge window closes: hearMerge, and cutBatchesHeld in batches.go. func (f following) SourceMoved(ctx context.Context, m link.SourceMoved) error { @@ -153,82 +102,6 @@ func (f following) SourceMoved(ctx context.Context, m link.SourceMoved) error { return keepForgeMerge(ctx, m) } -// movesOfMerge is what one merge moves, acted on: every module recorded as built from that repository and -// branch is marked as moved to the merge commit; a module the merge deleted is forgotten or said; a new module -// is asked for and sent nowhere. The names returned are what the walk builds, bases first along the graph -// (novox/hq 04-ISSUES/131). Nothing is built or pushed here: the walk asks its tiers. Called when a batch is -// cut, once per repository, with the latest merge of the repository's branch and every file the batch's -// merges of it changed. -func movesOfMerge(ctx context.Context, inv *inventory.Inventory, m link.SourceMoved, entries []inventory.Entry, - read map[string][]inventory.ReadRepository) ([]string, error) { - from, packaging, already := mergeCandidates(m, entries, read) - if len(from) == 0 && len(packaging) == 0 { - // "Already built from it" and "nothing reads it" are different facts, and reading the first - // as the second sends somebody looking for a broken trigger when the mesh is up to date. - if already > 0 { - fmt.Printf("%s/%s merged into %s (%.8s); %d module(s) the mesh holds are already built "+ - "from it\n", m.Owner, m.Repo, m.Base, m.Commit, already) - return nil, nil - } - fmt.Print(nothingReadsIt(m.Owner+"/"+m.Repo, m)) - return nil, 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, added, _ := touchedBy(from, entries, m, read) - // **A module the merge deleted is not built** (novox/hq ADR 0236): its manifest is gone, so the build - // seat finds nothing saying what it is, and the plan failed on it (`has no module.json at …`) with - // every other module of its tier left unsent. It is forgotten where nothing holds it, said otherwise. - touched, deleted := splitDeleted(touched, m) - for _, e := range deleted { - retireDeleted(ctx, inv, e, m) - } - for _, e := range touched { - if err := inv.SourceMoved(ctx, e.Manifest.Module, m.Commit); err != nil { - return nil, notNow(err) - } - } - // **A new module is built and registered, and sent nowhere** (novox/hq issue 300): the delivery plan - // says so, and assigning it is a person's act, which needs it registered first. - // Nothing asks its first build (novox/hq ADR 0501 §6): the builder builds every fingerprint the indexer writes, and - // the controller registers the module from its first done key (new_modules.go). - var built []string - if len(from) > 0 { - built = added - } - moved := append(append([]inventory.Entry{}, touched...), packaging...) - if len(moved) == 0 { - if len(built) > 0 { - fmt.Printf("%s/%s merged into %s (%.8s); it changed no module the mesh holds, and adds %s: "+ - "registered from the builder's done key of each, and sent nowhere\n", m.Owner, m.Repo, m.Base, m.Commit, - strings.Join(built, ", ")) - return nil, nil - } - fmt.Printf("%s/%s merged into %s (%.8s); it changed nothing any module the mesh holds is "+ - "built from\n", m.Owner, m.Repo, m.Base, m.Commit) - return nil, nil - } - // Why each is in it (issue 363): the files of its build source the merge changed, or why it is read whole. - for _, e := range moved { - fmt.Printf(" %s: %s\n", e.Manifest.Module, whyMoved(e, read[e.Manifest.Module], m)) - } - for _, e := range packaging { - fmt.Printf(" %s reads %s/%s through its build context: its own source record is left where it is\n", - e.Manifest.Module, m.Owner, m.Repo) - } - var names []string - for _, e := range moved { - names = append(names, e.Manifest.Module) - } - return names, nil -} - // actingOnMerges keeps one merge acted on at a time (novox/hq issue 266). var actingOnMerges sync.Mutex @@ -292,63 +165,6 @@ func lookedAtCommit(read []inventory.ReadRepository, commit string) bool { // historyMargin is how far a packaging module's last look is taken back before a merge is history for it. const historyMargin = time.Minute -// whyMoved is why a merge moves a module, as a plan says it (novox/hq ADR 0267, issue 363): the changed -// files in its build source, or why it is read whole. For a module built from the merged repository or one -// whose build context is that repository; a dependent is in a plan for what it stands on. -func whyMoved(e inventory.Entry, read []inventory.ReadRepository, m link.SourceMoved) string { - if len(m.Paths) == 0 || m.PathsTruncated { - return "read whole: the merge's changed files were not all said" - } - whole := "no build source recorded" - for _, r := range read { - if r.Own && r.Whole != "" { - whole = r.Whole - } - } - held := func(paths []string) []string { - var in []string - for _, p := range m.Paths { - if gosource.SourceHolds(paths, p) { - in = append(in, p) - } - } - return in - } - changed := func(where string, in []string) string { - return fmt.Sprintf("its build source%s changed: %d changed file(s) in it, e.g. %s", where, len(in), in[0]) - } - if sameRepository(e.Source.Repository, m) { - if own := ownSource(read); own != nil { - if in := held(own); len(in) > 0 { - return changed("", in) - } - } - dir := strings.Trim(e.Source.Path, "/") - if dir == "" { - return "read whole: " + whole + ", so every file of its repository is its build source" - } - for _, p := range m.Paths { - if inside(p, dir) { - return "read whole: " + whole + ", so its directory is its build source; e.g. " + p - } - } - return "read whole: " + whole - } - for _, r := range read { - if r.Own || !sameRepository(r.Repository, m) || !followsBase(r.Ref, m.Base) { - continue - } - if len(r.Paths) > 0 { - if in := held(r.Paths); len(in) > 0 { - return changed(" in "+m.Owner+"/"+m.Repo, in) - } - continue - } - return "read whole: " + whole + ", so every file of " + m.Owner + "/" + m.Repo + ", which its build context is, is its build source" - } - return "read whole: " + whole -} - // lookedOf is when a module packaging another repository was last looked at, as readForPlanning says. func lookedOf(read []inventory.ReadRepository) time.Time { var at time.Time @@ -360,21 +176,6 @@ func lookedOf(read []inventory.ReadRepository) time.Time { return at } -// wouldMove is the modules acting on this merge would move and rebuild — SourceMoved's judgement, made -// without acting (novox/hq issue 266). Empty for a merge already acted on: acting marks each module built -// from the repository as looked at, so the merge then reads as history for it. -// -// **The ones packaging source from it too** (novox/hq ADR 0267): with a module moved only by the files of -// its build source, a merge can move a packaging module and nothing built from the repository, and a missed -// one of those was never acted on. A packaging module's look is its newest build or plan (lookedAt), so a -// merge acted on for it reads as history once its plan is made. -func wouldMove(m link.SourceMoved, entries []inventory.Entry, - read map[string][]inventory.ReadRepository) []inventory.Entry { - from, packaging, _ := mergeCandidates(m, entries, read) - touched, _ := splitDeleted(whatTheMergeTouched(from, entries, m, read), m) - return append(touched, packaging...) -} - // splitDeleted parts the modules a merge touched into those it changed and those whose manifest it // removed — deleted at their source (ADR 0236). func splitDeleted(touched []inventory.Entry, m link.SourceMoved) (kept, deleted []inventory.Entry) { @@ -397,39 +198,6 @@ func splitDeleted(touched []inventory.Entry, m link.SourceMoved) (kept, deleted return kept, deleted } -// retireDeleted is what the mesh does with a module deleted at its source: its record says the merge -// was looked at, so it is not acted on again; it is forgotten where nothing holds it; where a machine -// runs it or the mesh holds something for it, that is said, and nothing is built. -func retireDeleted(ctx context.Context, inv *inventory.Inventory, e inventory.Entry, m link.SourceMoved) { - name := e.Manifest.Module - if err := inv.SourceMoved(ctx, name, m.Commit); err != nil { - fmt.Printf(" %s was deleted from %s/%s at %.8s, and that could not be recorded: %v\n", name, m.Owner, m.Repo, m.Commit, err) - } - err := inv.ForgetModule(ctx, name) - switch { - case err == nil: - fmt.Printf(" %s was deleted from %s/%s at %.8s: forgotten, nothing built\n", name, m.Owner, m.Repo, m.Commit) - case errors.Is(err, inventory.ErrStillAssigned) || errors.Is(err, inventory.ErrStillHolds): - fmt.Printf(" %s was deleted from %s/%s at %.8s and is not built; the mesh still runs or holds it, so it is "+ - "kept until a person unassigns it and `module forget %s`: %v\n", name, m.Owner, m.Repo, m.Commit, name, - firstLine(err.Error())) - default: - fmt.Printf(" %s was deleted from %s/%s at %.8s and is not built; forgetting it failed: %v\n", name, m.Owner, - m.Repo, m.Commit, err) - } -} - -// nothingReadsIt is what is said of a merge no module the mesh holds reads. **A merge into a branch other than the -// trunk is said to start no delivery** (novox/hq ADR 0401 clause 3, issue 889): every delivery is cut from a -// repository's trunk, and whoever merged into a feature branch should not look for a broken trigger. -func nothingReadsIt(repository string, m link.SourceMoved) string { - if m.Base != "" && m.Base != "main" { - return fmt.Sprintf("%s merged into %s (%.8s), not the trunk of %s; a merge into any other branch starts no "+ - "delivery\n", repository, m.Base, m.Commit, repository) - } - return fmt.Sprintf("%s merged into %s (%.8s); nothing the mesh holds reads it\n", repository, m.Base, m.Commit) -} - // sourceIs is whether a recorded source is the repository and branch a merge announced. A source on // the git seat is recorded as its path on the forge; one elsewhere as the URL it was cloned from. // An empty recorded ref is the repository's default branch, which is what a merge into the base @@ -531,79 +299,12 @@ func readsDirectory(e inventory.Entry, p string) bool { return strings.Trim(e.Source.Path, "/") == "" || inside(p, e.Source.Path) } -// unchangedOfMerge are the modules a merge changed files for under a reading without recorded build sources, but -// whose recorded build source holds none of them, each with why (novox/hq issue 611): not built, because the build -// the mesh holds is theirs (ADR 0267 §5). That reading — a delivery plan made while another walk built the module -// (issue 606) — would have moved them; the walk says they matched what runs rather than leaving them unnamed. -// A module built from the repository whose directory, or whole repository when built from its root, holds a -// changed file, and a module packaging the repository's branch, are each read so. A merge whose changed files -// were not all said moves every one of them: none is said here. Without acting. -func unchangedOfMerge(m link.SourceMoved, entries []inventory.Entry, - read map[string][]inventory.ReadRepository) map[string]string { - if len(m.Paths) == 0 || m.PathsTruncated { - return nil - } - from, _, _ := mergeCandidates(m, entries, read) - touched, _, _ := touchedBy(from, entries, m, read) - out := map[string]string{} - for _, e := range from { - name := e.Manifest.Module - if ownSource(read[name]) == nil || slices.ContainsFunc(touched, func(t inventory.Entry) bool { - return t.Manifest.Module == name - }) { - continue - } - var in []string - for _, p := range m.Paths { - if readsDirectory(e, p) { - in = append(in, p) - } - } - if len(in) > 0 { - out[name] = fmt.Sprintf("its build source holds none of the %d changed file(s) in its directory (e.g. %s): "+ - "the build the mesh holds is its build", len(in), in[0]) - } - } - for _, e := range entries { - name := e.Manifest.Module - if sourceIs(e.Source, m) || !packagesSaid(read[name], m) || readsFrom(read[name], m) { - continue - } - out[name] = fmt.Sprintf("its build source in %s/%s holds none of the %d changed file(s) (e.g. %s): "+ - "the build the mesh holds is its build", m.Owner, m.Repo, len(m.Paths), m.Paths[0]) - } - if len(out) == 0 { - return nil - } - return out -} - -// packagesSaid is whether a module's newest trunk build said the build source it read in a merge's repository and -// branch, as a context beside its own. -func packagesSaid(read []inventory.ReadRepository, m link.SourceMoved) bool { - for _, r := range read { - if !r.Own && sameRepository(r.Repository, m) && followsBase(r.Ref, m.Base) && len(r.Paths) > 0 { - return true - } - } - return false -} - // readForPlanning is what each module's build read, as a change is mapped onto it — for a merge's reach, a pull request's // check and the facts alike (novox/hq ADR 0238), so they cannot disagree. No plan overtakes it any more (ADR 0539 §17). func readForPlanning(ctx context.Context, inv *inventory.Inventory) (map[string][]inventory.ReadRepository, error) { return inv.ReadRepositories(ctx) } -// whatTheMergeTouched narrows the modules built from a repository to the ones the merge changed: **a -// changed file touches exactly the modules whose build reads it** (novox/hq issue 280, ADR 0238). It is -// touchedBy's first answer; touchedBy is the one place the mesh maps a changed file onto its modules. -func whatTheMergeTouched(candidates, known []inventory.Entry, m link.SourceMoved, - read map[string][]inventory.ReadRepository) []inventory.Entry { - touched, _, _ := touchedBy(candidates, known, m, read) - return touched -} - // touchedBy maps a merge's changed files onto the mesh's modules — **the one place it is done**, for the // merge handler, the release planner's what-if, the merge gate and a pull request's check alike (novox/hq // ADR 0238), so planning and gating cannot disagree about what a change touches. @@ -757,49 +458,6 @@ func inside(path, dir string) bool { return path == dir || strings.HasPrefix(path, dir+"/") } -// anyInside is whether any of these changed files is in a directory. -func anyInside(paths []string, dir string) bool { - for _, p := range paths { - if inside(p, dir) { - 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. -func standsOnModule(e inventory.Entry, module string, against map[string][]string) bool { - if e.Manifest.Module == module { - return false - } - if e.Manifest.Build != nil { - for _, on := range e.Manifest.Build.On { - if on.Module == module { - return true - } - } - } - prefix := catalogue.ArtifactStoreScheme + module + "/" - for _, ref := range against[e.Manifest.Module] { - if strings.HasPrefix(ref, prefix) { - return true - } - } - 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 { @@ -813,52 +471,10 @@ func isHistory(mergedAt string, seen time.Time) bool { return at.Before(seen) } -// dependentsOf is every catalogued module that stands on one of the moved modules, directly or -// through another dependent, and is not itself among them — in the catalogue's order, so the -// answer is the same each time. A module standing on nothing that moved is left alone: a merge -// rebuilds what it changed and what is built on top of that, not the catalogue. -func dependentsOf(moved, entries []inventory.Entry, against map[string][]string) []inventory.Entry { - bases := map[string]bool{} - for _, e := range moved { - bases[e.Manifest.Module] = true - } - var out []inventory.Entry - taken := map[string]bool{} - for grew := true; grew; { - grew = false - for _, e := range entries { - name := e.Manifest.Module - if bases[name] || taken[name] { - continue - } - for base := range bases { - if standsOnModule(e, base, against) { - taken[name] = true - out = append(out, e) - grew = true - break - } - } - } - for _, e := range out { - bases[e.Manifest.Module] = true - } - } - return out -} - // moduleManifestFile is the file that says what a module is, in its directory (the build seat's // ManifestName). const moduleManifestFile = "module.json" -// deletedAtSource is whether a build failed because its module's manifest is not at its source any -// more — the build seat's own words (internal/builder) — which a merge that deleted the module causes -// when its announcer did not say which files went (ADR 0236). Such a module is not a failure of the plan. -func deletedAtSource(failed string) bool { - return strings.Contains(failed, "has no "+moduleManifestFile+" at ") && - strings.Contains(failed, "so there is nothing saying what it is") -} - // reachOfMerge is what a merge of these changed files into this branch would move, without acting: a pull request's // reach, as the check reckons it. func reachOfMerge(m link.SourceMoved, entries []inventory.Entry, read map[string][]inventory.ReadRepository, @@ -1030,3 +646,11 @@ func sayUpgrade(module string, u inventory.Upgrade) string { return fmt.Sprintf("when %s moves, one machine running it is sent the new version first and judged at "+ "the gate; the rest follow once it passes, and a build that fails is put back there%s", module, from) } + +// whatTheMergeTouched narrows the modules built from a repository to the ones the merge changed: touchedBy's first answer +// (novox/hq issue 280, ADR 0238). +func whatTheMergeTouched(candidates, known []inventory.Entry, m link.SourceMoved, + read map[string][]inventory.ReadRepository) []inventory.Entry { + touched, _, _ := touchedBy(candidates, known, m, read) + return touched +}