flake-update-cascade: resolve follows edges when reporting revisions #154

Merged
vnprc merged 1 commit from agent/cascade-follows-rev into master 2026-08-22 03:17:23 +01:00
Member

flake-update-cascade stops dying when an update collapses a pinned input into a follows. It hit this on a real run today, against a private allod-deploy picking up allod/archetypes#39: Nix wrote the new lock, and the tool then ended with jq: error … Cannot index object with array before committing or pushing it, and before reaching any later repo in the cascade.

Nothing about how the tool selects or performs updates changes. The fix is in the helper that reports each path's before and after revision, and the run that failed now completes.

If this is wrong, revert the commit. The tool is a workspace helper; nothing depends on its internals and no lock content changes.

What went wrong

Update paths are discovered from the pre-update lock, then each path's revisions are read from the pre- and post-update locks to report what moved. Between those two reads the update can change the graph's shape.

In a flake.lock an input edge has two shapes: a pinned input is the target node's name as a string, and a follows is an absolute path from root as an array. get_update_paths already knows this and skips arrays while walking:

| select(.value | type == "string")

get_rev did not:

| reduce ($path | split("/"))[] as $part ("root"; $lock.nodes[.].inputs[$part])

So when allod/archetypes#39's nexus.inputs.vm.follows = "vm" reached a consumer, the path archetypes/nexus/vm — a real pin before the update — became ["archetypes", "vm"] after it. The reduce fed that array back into $lock.nodes[...] and jq exited 5. Under set -euo pipefail that ends the whole cascade at the worst point: after the new lock is on disk, before it is committed.

The same cause produced the run's earlier warning: 'archetypes/nexus/vm' does not match any input of this flake.

What changed

get_rev walks one edge at a time and resolves a follows array by restarting the walk from root along it, which also handles a redirect whose target is itself a redirect. A path that no longer resolves yields empty instead of an error.

Risk

Low, and bounded to reporting. get_rev feeds one echo of the form path: old → new; it does not decide what is updated, staged, committed or pushed. A path that stops resolving now prints a half-empty line rather than ending the run, which is the intended degradation — the cascade's job is to land the lock, and a cosmetic gap in the summary must not cost that.

Recurrence is not hypothetical: allod/archetypes#48's companion adds another follows redirect, and every future one lands in consumers the same way.

Validation

  • bash tests/flake/flake-update-cascade-follows.sh — new. Builds a lock with two vm nodes, one reached through nexus, and a mock update that collapses the nested one to ["archetypes", "vm"] and deletes its node. Asserts the run exits 0, that no Cannot index object with array appears, that the still-pinned path reports normally, and that the collapsed path resolves through its follows to the node it now redirects to.
  • The same test against the unfixed script exits 5 — the same status the real run produced — so it fails for the reason it claims to test.
  • tests/flake/flake-status.sh, flake-update-cascade-multiple-inputs.sh, flake-update-cascade-nixconfig.sh all pass unchanged.

Not covered: no test drives a real nix flake update against a real forge. The mock reproduces the lock shapes, not Nix's behaviour producing them.

`flake-update-cascade` stops dying when an update collapses a pinned input into a `follows`. It hit this on a real run today, against a private `allod-deploy` picking up allod/archetypes#39: Nix wrote the new lock, and the tool then ended with `jq: error … Cannot index object with array` before committing or pushing it, and before reaching any later repo in the cascade. Nothing about how the tool selects or performs updates changes. The fix is in the helper that reports each path's before and after revision, and the run that failed now completes. If this is wrong, revert the commit. The tool is a workspace helper; nothing depends on its internals and no lock content changes. ## What went wrong Update paths are discovered from the **pre-update** lock, then each path's revisions are read from the pre- and **post-update** locks to report what moved. Between those two reads the update can change the graph's shape. In a `flake.lock` an input edge has two shapes: a pinned input is the target node's name as a string, and a `follows` is an absolute path from root as an array. `get_update_paths` already knows this and skips arrays while walking: ``` | select(.value | type == "string") ``` `get_rev` did not: ``` | reduce ($path | split("/"))[] as $part ("root"; $lock.nodes[.].inputs[$part]) ``` So when allod/archetypes#39's `nexus.inputs.vm.follows = "vm"` reached a consumer, the path `archetypes/nexus/vm` — a real pin before the update — became `["archetypes", "vm"]` after it. The reduce fed that array back into `$lock.nodes[...]` and jq exited 5. Under `set -euo pipefail` that ends the whole cascade at the worst point: after the new lock is on disk, before it is committed. The same cause produced the run's earlier `warning: 'archetypes/nexus/vm' does not match any input of this flake`. ## What changed `get_rev` walks one edge at a time and resolves a `follows` array by restarting the walk from root along it, which also handles a redirect whose target is itself a redirect. A path that no longer resolves yields empty instead of an error. ## Risk Low, and bounded to reporting. `get_rev` feeds one `echo` of the form `path: old → new`; it does not decide what is updated, staged, committed or pushed. A path that stops resolving now prints a half-empty line rather than ending the run, which is the intended degradation — the cascade's job is to land the lock, and a cosmetic gap in the summary must not cost that. Recurrence is not hypothetical: allod/archetypes#48's companion adds another `follows` redirect, and every future one lands in consumers the same way. ## Validation - `bash tests/flake/flake-update-cascade-follows.sh` — new. Builds a lock with two `vm` nodes, one reached through `nexus`, and a mock update that collapses the nested one to `["archetypes", "vm"]` and deletes its node. Asserts the run exits 0, that no `Cannot index object with array` appears, that the still-pinned path reports normally, and that the collapsed path resolves through its `follows` to the node it now redirects to. - The same test against the unfixed script exits 5 — the same status the real run produced — so it fails for the reason it claims to test. - `tests/flake/flake-status.sh`, `flake-update-cascade-multiple-inputs.sh`, `flake-update-cascade-nixconfig.sh` all pass unchanged. **Not covered:** no test drives a real `nix flake update` against a real forge. The mock reproduces the lock shapes, not Nix's behaviour producing them.
vnprc approved these changes 2026-08-22 03:17:18 +01:00
vnprc merged commit 36f5d9bdff into master 2026-08-22 03:17:23 +01:00
vnprc deleted branch agent/cascade-follows-rev 2026-08-22 03:17:24 +01:00
Sign in to join this conversation.
No description provided.