flake-update-cascade: process repositories in dependency order and propagate advanced pins #176

Merged
vnprc merged 2 commits from agent/cascade-dependency-order into master 2026-09-09 15:49:22 +01:00
Member

One flake-update-cascade run now brings every workspace repository to a consistent set of locks instead of one run per level of the chain. Repositories are processed leaves first, each after the workspace repositories it pins, and a pin of a workspace repository that is behind that repository's branch head is moved whether or not its input was on the command line, so a lock commit the run pushes to vm reaches archetypes and then deploy in the same pass. What stays identical: a workspace with no pins between its repositories produces the same output, commits and exit status as before, and every existing suite is unchanged apart from the commit message, which now names the paths that moved in that repository rather than the command line. What proves it: a chained three-repository mock fixture in every mode, a hand-run fixture over real git and real nix with bare origins, nix flake check, a read-only review pass whose findings are fixed in the second commit, and two --dry-runs over the public chain in this workspace, all below. If a run does something wrong, the pre-flight is still atomic and a cycle or any pre-flight error touches nothing; a bad lock commit is one revert per repository, and --dry-run shows the first wave before any push.

Also folded in: the paths a repository is updated on are read from its lock after the tool's own git pull, so a checkout whose pull changed its input graph is judged on its current inputs instead of being reported already up to date on paths that no longer exist (allod/tools#175).

Closes allod/tools#171
Closes allod/tools#175

Design

The issue's design and validation sections are the spec; they predate the head-reading change in 090ceab, so what they call "the remote-tracking head after git fetch origin" is read here with git ls-remote at the pin's own URL, and a moved pin is written with nix flake lock --override-input as every other moved input is.

  • A workspace pin is a root input (a string edge, not a follows) whose original declares a git or GitHub branch and whose repository, compared by host and path with scheme, user, port, query and .git suffix ignored, is the origin of another workspace repository inside the push boundary (the forge, or allowed-external-remotes). A pin naming a revision, or carrying dir, host or submodules, is neither ordered on nor propagated; a pin of a repository the workspace does not hold, or holds outside the push boundary, is external.
  • Order: topological sort over those pins among the repositories the run goes on to process, ties broken by directory order, so with nothing chaining the order is the old one. A cycle is a pre-flight error on each member, x: dependency cycle: x → y → x, and nothing is touched; a cycle through skipped repositories, or a repository pinning itself, constrains nothing. The order and the cycle check come from the locks before the run's own pulls; everything else is decided on the lock after the pull.
  • Update set per repository: the requested names resolved through the lock graph as before, plus every workspace pin. planLock compares each against its branch head and keeps only what moved, so a pin at its head costs one ls-remote and nothing else, and a repository whose only stale inputs are external behaves as before. After a direct push the head every downstream pin reads is seeded from the checkout's HEAD, so the next repository down pins exactly what was pushed with no second round trip. A repository with no named input but with workspace pins is now examined (pulling... / already up to date) and pre-flighted where it was skipped before; that and "a run naming only an external input also moves internal pins that are behind" are the two visible changes on an existing workspace.
  • --pr: a repository whose pin of a default branch targets a repository this run moved on a PR branch is pulled and then waits, reported as waiting on PR #42 (allod/vm), with nothing written; the wait is transitive through a waiting repository; a pin of a release branch or a tag never waits; the run ends listing the waiting repositories and their PRs and says the same command re-run after merge continues; exit 0. A failed forge pr create fails the repository and leaves nothing downstream waiting; an existing PR has its title and body refreshed to what moved this time.
  • --dry-run: the plan in dependency order; pins behind already-merged commits are shown in full; a pin of a commit this run would push is named on the repository's own lines and again at the end as not shown. Without --pr, a protected repository's update is shown, marked a direct run skips this repository, and promises nothing downstream.
  • Commit message and PR title: flake.lock: update <paths that moved here> in lock order, e.g. flake.lock: update allod-tools, vm/nixpkgs. The PR branch name still comes from the command line so re-runs find their branch.

Review pass

A read-only review of the first commit (c8ac5a8) produced ten findings, recorded in the PR comment. Seven are fixed in bdaf108: waiting decided on the pre-pull lock, propagation blind to the pin's branch, cycles among skipped repositories aborting every run, dry runs promising a wave behind a protected repository, the pushed head re-read from the remote, a failed PR creation reported as success, and an existing PR never retitled. Three are the issue's own design and are stated in the README: pre-flight now covering every pinning repository, one PR branch per command line, and a waiting repository deferring its own inputs one round per chain level.

Risk

  • The commit -m line changes for every run that names more than one input, and for propagated pins it names an input that was not on the command line. Anything parsing those messages sees the new shape; nothing in the workspace does.
  • flake-update-cascade <external-input> now also moves stale workspace pins in the same commit. That is the design's stated intent, and it is what makes one run converge, but it widens what a routine nixpkgs bump can touch.
  • Pre-flight now reads remote get-url origin for every repository, including ones without a lock, and a repository with workspace pins is subject to the branch, dirty-tree and unpushed checks even when no named input reaches it. A dirty archetypes therefore blocks a home-manager run it used to skip through. That is the atomic pre-flight doing its job, since the run may commit there.
  • Running two different input names in --pr mode before the first PR merges opens two PRs carrying the same propagated pin, because the branch is named after the command line.
  • Head reads over the git protocol: one per workspace pin per run. Still no GitHub REST calls.
  • repoIdentity overlaps cmd/allod/patch.go:remoteIdentity, which rejects the file paths and query strings the chain fixture needs, so the two stay separate.
  • Out of scope, unchanged: transitive nodes a downstream lock owns ahead of the intermediate repository (the nix.md gotcha), pinning unmerged PR revisions to chain a --pr cascade without merges, allod/tools#170, and anything about flake-status.

Validation

nix flake check on bdaf108: go-checks (gofmt, vet, go test ./... with and without the site tag), cascade-suites (every mock-driven suite against the packaged program, now including dependency-order.sh and post-pull-lock.sh), allod-parity. Every pre-existing suite passes unchanged except flake-update-cascade-multiple-inputs.sh, whose expected commit line is now flake.lock: update allod-tools, vm/nixpkgs.

New suites, from the issue's validation section plus one scenario per review finding:

  • tests/flake/flake-update-cascade/dependency-order.sh (62 checks): the chained leaf/mid/top fixture with a mock git whose per-repository tracking head advances on push. Direct mode pushes leaf, mid, top in that order with mid's pin equal to the leaf head the run pushed and top's to mid's, each commit naming only what moved there, and never asks the remote for a head this run pushed; the same with top first in directory order, proving the sort and not the walk decides; --pr gives leaf a PR, refreshes its title, and reports mid and top as waiting on PR #42 (leaf) with nothing committed there, exit 0, plus the closing summary; an already-merged wave (mid's pin behind leaf's head, no new leaf commit, a run naming an unrelated input) moves mid and then top; a dry run shows leaf, names the pins it cannot show, changes no lock; a release-branch pin is read but neither waits nor is promised a commit; a protected repository in a dry run is shown, named as skipped, and promises nothing downstream; a pin the pull brings in waits on the PR; a cycle among repositories in active-pr-branches does not fail the run; two repositories pinning each other fail pre-flight naming both with no nix or pull; a repository whose workspace pins are all current is examined and left alone while one with neither a named input nor a workspace pin is skipped as before.
  • tests/flake/flake-update-cascade/post-pull-lock.sh (8 checks): a mock pull that rewrites the lock. When the pull turns a nested vm/demo into a direct demo, nix is asked about demo and never about vm/demo; when the pull removes the input, the repository is skipped after the pull with no head read, no nix call, and no already up to date.
  • tests/flake/flake-update-cascade/failures.sh gains a failed forge pr create: status 1, the checkout returned to the default branch with the lock restored.
  • tests/flake/flake-update-cascade-chain.sh, run by hand like the nixConfig suite: real git and real nix over three repositories with bare origins under a temporary directory, chained through git+file:// inputs, one direct run naming only leaf's external input. Result on bdaf108: three origins each gained exactly one commit, in order, each pin equal to the commit the run had just pushed, every checkout clean; commits flake.lock: update demo, flake.lock: update leaf, leaf/demo, flake.lock: update mid, mid/leaf/demo.
  • Go unit tests for identity normalisation, the topological sort and its tiebreak, cycle detection and its message, workspace-pin selection with the default-branch flag and the update set, a pushed head answering ahead of the cache, dropping cached heads by identity, and RootPins.

Live witnesses over this workspace with bdaf108's binary, git progress and pull chatter removed; direct and --pr mode were not run against real repositories, the mutating path's witness is the bare-origin fixture above.

flake-update-cascade nixpkgs --dry-run --pr, 26 seconds, exit 0: dependency order and the first-wave notice for every pin that follows a commit the run would push.

==> allod/inventory
  pulling...
  updating nixpkgs (dry-run)...
  nixpkgs: 93108a5 → 6aefcda
  dry-run: no changes made

==> allod/nexus
  pulling...
  updating nixpkgs (dry-run)...
  nixpkgs: 93108a5 → 6aefcda
  dry-run: no changes made

==> allod/profiles
  pulling...
  updating nixpkgs (dry-run)...
  nixpkgs: 93108a5 → 6aefcda
  dry-run: no changes made

==> allod/secrets
  pulling...
  inventory: allod/inventory moves in this run; a dry run cannot show the revision it will pin
  updating nixpkgs (dry-run)...
  nixpkgs: 93108a5 → 6aefcda
  dry-run: no changes made

==> allod/tools
  pulling...
  updating nixpkgs (dry-run)...
  nixpkgs: dc5d91f → d6524aa
  dry-run: no changes made

==> allod/vm
  pulling...
  updating nixpkgs (dry-run)...
  nixpkgs: 93108a5 → 6aefcda
  dry-run: no changes made

==> allod/archetypes
  pulling...
  allod-tools: allod/tools moves in this run; a dry run cannot show the revision it will pin
  inventory: allod/inventory moves in this run; a dry run cannot show the revision it will pin
  nexus: allod/nexus moves in this run; a dry run cannot show the revision it will pin
  profiles: allod/profiles moves in this run; a dry run cannot show the revision it will pin
  secrets: allod/secrets moves in this run; a dry run cannot show the revision it will pin
  vm: allod/vm moves in this run; a dry run cannot show the revision it will pin
  updating vm/nixpkgs (dry-run)...
  vm/nixpkgs: 93108a5 → 6aefcda
  dry-run: no changes made

==> allod/deploy
  pulling...
  archetypes: allod/archetypes moves in this run; a dry run cannot show the revision it will pin
  inventory: allod/inventory moves in this run; a dry run cannot show the revision it will pin
  profiles: allod/profiles moves in this run; a dry run cannot show the revision it will pin
  secrets: allod/secrets moves in this run; a dry run cannot show the revision it will pin
  updating archetypes/vm/nixpkgs inventory/nixpkgs profiles/nixpkgs secrets/nixpkgs (dry-run)...
  archetypes/vm/nixpkgs: 93108a5 → 6aefcda
  inventory/nixpkgs: 93108a5 → 6aefcda
  profiles/nixpkgs: 93108a5 → 6aefcda
  secrets/nixpkgs: 93108a5 → 6aefcda
  dry-run: no changes made

Dry run: allod/secrets, allod/archetypes, allod/deploy also take the commits above once they are pushed; a dry run cannot show the revisions they will pin.

flake-update-cascade nixpkgs --dry-run without --pr, 38 seconds, exit 0: every repository here is in protected-branches, so each one prints protected branch (master) — a direct run skips this repository; re-run with --pr above its update, and no wave is announced. An earlier run of the first commit, before two lock PRs merged upstream, showed the already-merged wave in full: nexus: 65bafe4 → c5b9445 in archetypes and archetypes: 025bb21 → 77e9ce7 in deploy, both taken alongside the named input.

(allod/memory, allod/strategy and allod/.profile print no flake.lock, skipping and are elided from both.)

One `flake-update-cascade` run now brings every workspace repository to a consistent set of locks instead of one run per level of the chain. Repositories are processed leaves first, each after the workspace repositories it pins, and a pin of a workspace repository that is behind that repository's branch head is moved whether or not its input was on the command line, so a lock commit the run pushes to `vm` reaches `archetypes` and then `deploy` in the same pass. What stays identical: a workspace with no pins between its repositories produces the same output, commits and exit status as before, and every existing suite is unchanged apart from the commit message, which now names the paths that moved in that repository rather than the command line. What proves it: a chained three-repository mock fixture in every mode, a hand-run fixture over real git and real nix with bare origins, `nix flake check`, a read-only review pass whose findings are fixed in the second commit, and two `--dry-run`s over the public chain in this workspace, all below. If a run does something wrong, the pre-flight is still atomic and a cycle or any pre-flight error touches nothing; a bad lock commit is one revert per repository, and `--dry-run` shows the first wave before any push. Also folded in: the paths a repository is updated on are read from its lock after the tool's own `git pull`, so a checkout whose pull changed its input graph is judged on its current inputs instead of being reported `already up to date` on paths that no longer exist (allod/tools#175). Closes allod/tools#171 Closes allod/tools#175 ## Design The issue's design and validation sections are the spec; they predate the head-reading change in 090ceab, so what they call "the remote-tracking head after `git fetch origin`" is read here with `git ls-remote` at the pin's own URL, and a moved pin is written with `nix flake lock --override-input` as every other moved input is. - A workspace pin is a root input (a string edge, not a `follows`) whose `original` declares a git or GitHub branch and whose repository, compared by host and path with scheme, user, port, query and `.git` suffix ignored, is the origin of another workspace repository inside the push boundary (the forge, or `allowed-external-remotes`). A pin naming a revision, or carrying `dir`, `host` or `submodules`, is neither ordered on nor propagated; a pin of a repository the workspace does not hold, or holds outside the push boundary, is external. - Order: topological sort over those pins among the repositories the run goes on to process, ties broken by directory order, so with nothing chaining the order is the old one. A cycle is a pre-flight error on each member, `x: dependency cycle: x → y → x`, and nothing is touched; a cycle through skipped repositories, or a repository pinning itself, constrains nothing. The order and the cycle check come from the locks before the run's own pulls; everything else is decided on the lock after the pull. - Update set per repository: the requested names resolved through the lock graph as before, plus every workspace pin. `planLock` compares each against its branch head and keeps only what moved, so a pin at its head costs one `ls-remote` and nothing else, and a repository whose only stale inputs are external behaves as before. After a direct push the head every downstream pin reads is seeded from the checkout's HEAD, so the next repository down pins exactly what was pushed with no second round trip. A repository with no named input but with workspace pins is now examined (`pulling...` / `already up to date`) and pre-flighted where it was skipped before; that and "a run naming only an external input also moves internal pins that are behind" are the two visible changes on an existing workspace. - `--pr`: a repository whose pin of a default branch targets a repository this run moved on a PR branch is pulled and then waits, reported as `waiting on PR #42 (allod/vm)`, with nothing written; the wait is transitive through a waiting repository; a pin of a release branch or a tag never waits; the run ends listing the waiting repositories and their PRs and says the same command re-run after merge continues; exit 0. A failed `forge pr create` fails the repository and leaves nothing downstream waiting; an existing PR has its title and body refreshed to what moved this time. - `--dry-run`: the plan in dependency order; pins behind already-merged commits are shown in full; a pin of a commit this run would push is named on the repository's own lines and again at the end as not shown. Without `--pr`, a protected repository's update is shown, marked `a direct run skips this repository`, and promises nothing downstream. - Commit message and PR title: `flake.lock: update <paths that moved here>` in lock order, e.g. `flake.lock: update allod-tools, vm/nixpkgs`. The PR branch name still comes from the command line so re-runs find their branch. ## Review pass A read-only review of the first commit (c8ac5a8) produced ten findings, recorded in the PR comment. Seven are fixed in bdaf108: waiting decided on the pre-pull lock, propagation blind to the pin's branch, cycles among skipped repositories aborting every run, dry runs promising a wave behind a protected repository, the pushed head re-read from the remote, a failed PR creation reported as success, and an existing PR never retitled. Three are the issue's own design and are stated in the README: pre-flight now covering every pinning repository, one PR branch per command line, and a waiting repository deferring its own inputs one round per chain level. ## Risk - The `commit -m` line changes for every run that names more than one input, and for propagated pins it names an input that was not on the command line. Anything parsing those messages sees the new shape; nothing in the workspace does. - `flake-update-cascade <external-input>` now also moves stale workspace pins in the same commit. That is the design's stated intent, and it is what makes one run converge, but it widens what a routine `nixpkgs` bump can touch. - Pre-flight now reads `remote get-url origin` for every repository, including ones without a lock, and a repository with workspace pins is subject to the branch, dirty-tree and unpushed checks even when no named input reaches it. A dirty `archetypes` therefore blocks a `home-manager` run it used to skip through. That is the atomic pre-flight doing its job, since the run may commit there. - Running two different input names in `--pr` mode before the first PR merges opens two PRs carrying the same propagated pin, because the branch is named after the command line. - Head reads over the git protocol: one per workspace pin per run. Still no GitHub REST calls. - `repoIdentity` overlaps `cmd/allod/patch.go:remoteIdentity`, which rejects the file paths and query strings the chain fixture needs, so the two stay separate. - Out of scope, unchanged: transitive nodes a downstream lock owns ahead of the intermediate repository (the `nix.md` gotcha), pinning unmerged PR revisions to chain a `--pr` cascade without merges, allod/tools#170, and anything about `flake-status`. ## Validation `nix flake check` on bdaf108: `go-checks` (gofmt, vet, `go test ./...` with and without the `site` tag), `cascade-suites` (every mock-driven suite against the packaged program, now including `dependency-order.sh` and `post-pull-lock.sh`), `allod-parity`. Every pre-existing suite passes unchanged except `flake-update-cascade-multiple-inputs.sh`, whose expected commit line is now `flake.lock: update allod-tools, vm/nixpkgs`. New suites, from the issue's validation section plus one scenario per review finding: - `tests/flake/flake-update-cascade/dependency-order.sh` (62 checks): the chained leaf/mid/top fixture with a mock git whose per-repository tracking head advances on push. Direct mode pushes leaf, mid, top in that order with mid's pin equal to the leaf head the run pushed and top's to mid's, each commit naming only what moved there, and never asks the remote for a head this run pushed; the same with top first in directory order, proving the sort and not the walk decides; `--pr` gives leaf a PR, refreshes its title, and reports mid and top as `waiting on PR #42 (leaf)` with nothing committed there, exit 0, plus the closing summary; an already-merged wave (mid's pin behind leaf's head, no new leaf commit, a run naming an unrelated input) moves mid and then top; a dry run shows leaf, names the pins it cannot show, changes no lock; a release-branch pin is read but neither waits nor is promised a commit; a protected repository in a dry run is shown, named as skipped, and promises nothing downstream; a pin the pull brings in waits on the PR; a cycle among repositories in `active-pr-branches` does not fail the run; two repositories pinning each other fail pre-flight naming both with no nix or pull; a repository whose workspace pins are all current is examined and left alone while one with neither a named input nor a workspace pin is skipped as before. - `tests/flake/flake-update-cascade/post-pull-lock.sh` (8 checks): a mock pull that rewrites the lock. When the pull turns a nested `vm/demo` into a direct `demo`, nix is asked about `demo` and never about `vm/demo`; when the pull removes the input, the repository is skipped after the pull with no head read, no nix call, and no `already up to date`. - `tests/flake/flake-update-cascade/failures.sh` gains a failed `forge pr create`: status 1, the checkout returned to the default branch with the lock restored. - `tests/flake/flake-update-cascade-chain.sh`, run by hand like the nixConfig suite: real git and real nix over three repositories with bare origins under a temporary directory, chained through `git+file://` inputs, one direct run naming only leaf's external input. Result on bdaf108: three origins each gained exactly one commit, in order, each pin equal to the commit the run had just pushed, every checkout clean; commits `flake.lock: update demo`, `flake.lock: update leaf, leaf/demo`, `flake.lock: update mid, mid/leaf/demo`. - Go unit tests for identity normalisation, the topological sort and its tiebreak, cycle detection and its message, workspace-pin selection with the default-branch flag and the update set, a pushed head answering ahead of the cache, dropping cached heads by identity, and `RootPins`. Live witnesses over this workspace with bdaf108's binary, git progress and pull chatter removed; direct and `--pr` mode were not run against real repositories, the mutating path's witness is the bare-origin fixture above. `flake-update-cascade nixpkgs --dry-run --pr`, 26 seconds, exit 0: dependency order and the first-wave notice for every pin that follows a commit the run would push. ``` ==> allod/inventory pulling... updating nixpkgs (dry-run)... nixpkgs: 93108a5 → 6aefcda dry-run: no changes made ==> allod/nexus pulling... updating nixpkgs (dry-run)... nixpkgs: 93108a5 → 6aefcda dry-run: no changes made ==> allod/profiles pulling... updating nixpkgs (dry-run)... nixpkgs: 93108a5 → 6aefcda dry-run: no changes made ==> allod/secrets pulling... inventory: allod/inventory moves in this run; a dry run cannot show the revision it will pin updating nixpkgs (dry-run)... nixpkgs: 93108a5 → 6aefcda dry-run: no changes made ==> allod/tools pulling... updating nixpkgs (dry-run)... nixpkgs: dc5d91f → d6524aa dry-run: no changes made ==> allod/vm pulling... updating nixpkgs (dry-run)... nixpkgs: 93108a5 → 6aefcda dry-run: no changes made ==> allod/archetypes pulling... allod-tools: allod/tools moves in this run; a dry run cannot show the revision it will pin inventory: allod/inventory moves in this run; a dry run cannot show the revision it will pin nexus: allod/nexus moves in this run; a dry run cannot show the revision it will pin profiles: allod/profiles moves in this run; a dry run cannot show the revision it will pin secrets: allod/secrets moves in this run; a dry run cannot show the revision it will pin vm: allod/vm moves in this run; a dry run cannot show the revision it will pin updating vm/nixpkgs (dry-run)... vm/nixpkgs: 93108a5 → 6aefcda dry-run: no changes made ==> allod/deploy pulling... archetypes: allod/archetypes moves in this run; a dry run cannot show the revision it will pin inventory: allod/inventory moves in this run; a dry run cannot show the revision it will pin profiles: allod/profiles moves in this run; a dry run cannot show the revision it will pin secrets: allod/secrets moves in this run; a dry run cannot show the revision it will pin updating archetypes/vm/nixpkgs inventory/nixpkgs profiles/nixpkgs secrets/nixpkgs (dry-run)... archetypes/vm/nixpkgs: 93108a5 → 6aefcda inventory/nixpkgs: 93108a5 → 6aefcda profiles/nixpkgs: 93108a5 → 6aefcda secrets/nixpkgs: 93108a5 → 6aefcda dry-run: no changes made Dry run: allod/secrets, allod/archetypes, allod/deploy also take the commits above once they are pushed; a dry run cannot show the revisions they will pin. ``` `flake-update-cascade nixpkgs --dry-run` without `--pr`, 38 seconds, exit 0: every repository here is in `protected-branches`, so each one prints `protected branch (master) — a direct run skips this repository; re-run with --pr` above its update, and no wave is announced. An earlier run of the first commit, before two lock PRs merged upstream, showed the already-merged wave in full: `nexus: 65bafe4 → c5b9445` in archetypes and `archetypes: 025bb21 → 77e9ce7` in deploy, both taken alongside the named input. (`allod/memory`, `allod/strategy` and `allod/.profile` print `no flake.lock, skipping` and are elided from both.)
One run now brings every repository in the workspace to a consistent set
of locks. A root input whose declared repository is another workspace
repository inside the push boundary is a workspace pin: repositories are
processed after the ones they pin, ties broken by directory order, and a
cycle is a preflight error naming its members. Every workspace pin is
compared to the head of the branch it names and moved when behind,
whether or not its input was on the command line, so a lock commit the
run pushes upstream - the cached head is dropped after the push and read
again - and a lock PR merged since the last run both reach every
downstream lock in one pass. A pin at its head is left alone.

Each commit names the paths that moved in that repository, in lock
order, instead of the command line. In --pr mode a repository pinning
one this run moved on a PR branch waits, untouched, and the run ends by
listing the waiting repositories and their PRs; exit status stays 0. A
dry run prints the plan in dependency order, shows pins behind merged
commits in full, and says which pins of commits it would push it cannot
show.

The update paths are read from the lock after the tool's own pull, not
from the preflight's pre-pull copy, so a checkout whose pull changed its
input graph is judged on its current inputs (allod/tools#175).

New suites: dependency-order.sh runs a chained three-repository fixture
whose mock git serves a per-repository tracking head that advances on
push, in direct mode, in reversed directory order, in --pr mode, across
an already-merged wave, as a dry run, and as a cycle; post-pull-lock.sh
pulls a rewritten lock; flake-update-cascade-chain.sh is run by hand
over real git and nix with bare origins. The multiple-inputs suite now
expects the per-repository commit message.

Refs allod/tools#171, allod/tools#175
A repository waits on a PR only for a pin of a default branch that this
run moved, judged on the lock after its pull rather than the one
preflight read, so a pin the pull brought in waits and a release-branch
or tag pin never does; the same rule decides what a dry run promises
downstream. Only repositories the run goes on to process are ordered, so
a cycle through skipped repositories, or a repository pinning itself,
no longer stops every run. A dry run of a direct run names a protected
repository as one the real run skips and promises nothing on its
account. A direct push seeds the head every downstream pin reads with
the checkout's HEAD instead of asking the remote again. A failed forge
pr create fails the repository and leaves nothing downstream waiting on
a PR that does not exist, and an existing PR has its title and body
refreshed to name what moved this time.

Refs allod/tools#171
Author
Member

Read-only review pass on c8ac5a8 (high effort, eight finder angles, 16 candidates verified). Ten findings survived; seven are fixed in bdaf108 and three are the issue's own design, now stated in the README rather than changed.

Fixed:

  • Waiting in --pr mode was decided on the pre-pull lock, so a workspace pin the pull brought in was neither waited on nor listed. Blockers now come from the lock after the pull; the waiting repository is pulled and nothing else is touched.
  • Propagation ignored the pin's branch: a pin of a release branch or a tag was promised a default-branch commit in dry runs and made to wait on a PR in --pr mode. Only a pin of the target's default branch waits or is promised anything; every pin is still read and moved when behind.
  • Pin edges were collected from every repository, so a cycle among repositories the run would skip, or a repository pinning itself, failed pre-flight for every run. Only repositories the run goes on to process are ordered, and self-pins are ignored.
  • A dry run of a direct run showed a protected repository's update and announced a wave behind it that the real run would not make. It now says a direct run skips this repository and promises nothing downstream on its account.
  • After a direct push the cached head was dropped and read again from the remote. The push now seeds the head from the checkout's HEAD, so the next repository reads it without a round trip and cannot be misled by a remote that has not caught up.
  • forge pr create failing was reported as success with a placeholder that downstream repositories were then told to wait on. A failed creation fails the repository, restores the checkout, and leaves nothing waiting.
  • An existing PR kept the title and body of its first run while the commit named a different set of moved paths. The PR is now edited to match.

Design, disclosed in the README:

  • A repository with workspace pins but no named input is now pulled and pre-flighted, so its dirty tree blocks a run that used to skip it. The run may commit there, so the atomic pre-flight has to cover it.
  • The PR branch stays agent/flake-update-<input> so re-runs find their PR; running two different input names before the first PR merges opens two PRs carrying the same propagated pin.
  • A waiting repository defers its own named inputs too and the cascade advances one level per merge-and-rerun round, as the issue specified.

Cut as cleanup rather than defects: repoIdentity overlaps cmd/allod/patch.go:remoteIdentity, which rejects the file paths and query strings the chain fixture needs, so the two stay separate; remoteIsAllowed scans its file twice per repository.

nix flake check passes on bdaf108; the real-git chain suite and a --dry-run over this workspace were re-run on it.

Read-only review pass on c8ac5a8 (high effort, eight finder angles, 16 candidates verified). Ten findings survived; seven are fixed in bdaf108 and three are the issue's own design, now stated in the README rather than changed. Fixed: - Waiting in `--pr` mode was decided on the pre-pull lock, so a workspace pin the pull brought in was neither waited on nor listed. Blockers now come from the lock after the pull; the waiting repository is pulled and nothing else is touched. - Propagation ignored the pin's branch: a pin of a release branch or a tag was promised a default-branch commit in dry runs and made to wait on a PR in `--pr` mode. Only a pin of the target's default branch waits or is promised anything; every pin is still read and moved when behind. - Pin edges were collected from every repository, so a cycle among repositories the run would skip, or a repository pinning itself, failed pre-flight for every run. Only repositories the run goes on to process are ordered, and self-pins are ignored. - A dry run of a direct run showed a protected repository's update and announced a wave behind it that the real run would not make. It now says `a direct run skips this repository` and promises nothing downstream on its account. - After a direct push the cached head was dropped and read again from the remote. The push now seeds the head from the checkout's HEAD, so the next repository reads it without a round trip and cannot be misled by a remote that has not caught up. - `forge pr create` failing was reported as success with a placeholder that downstream repositories were then told to wait on. A failed creation fails the repository, restores the checkout, and leaves nothing waiting. - An existing PR kept the title and body of its first run while the commit named a different set of moved paths. The PR is now edited to match. Design, disclosed in the README: - A repository with workspace pins but no named input is now pulled and pre-flighted, so its dirty tree blocks a run that used to skip it. The run may commit there, so the atomic pre-flight has to cover it. - The PR branch stays `agent/flake-update-<input>` so re-runs find their PR; running two different input names before the first PR merges opens two PRs carrying the same propagated pin. - A waiting repository defers its own named inputs too and the cascade advances one level per merge-and-rerun round, as the issue specified. Cut as cleanup rather than defects: `repoIdentity` overlaps `cmd/allod/patch.go:remoteIdentity`, which rejects the file paths and query strings the chain fixture needs, so the two stay separate; `remoteIsAllowed` scans its file twice per repository. `nix flake check` passes on bdaf108; the real-git chain suite and a `--dry-run` over this workspace were re-run on it.
vnprc approved these changes 2026-09-09 15:49:17 +01:00
vnprc merged commit bdaf108698 into master 2026-09-09 15:49:22 +01:00
vnprc deleted branch agent/cascade-dependency-order 2026-09-09 15:49:23 +01:00
Sign in to join this conversation.
No description provided.