Flake-input bump runbook¶
This page is the reviewer playbook for the two Renovate PRs that touch
flake-input pins in flake.nix:
cachix/git-hooks.nix— master HEAD tracker (Renovate manager added 2026-05-17, PR #62). Fires whenever upstream master moves.NixOS/nixpkgs— stable-branch tracker (Renovate manager added 2026-05-17, PR #64). Fires when the next NixOS GA tag (YY.MM) lands plus the global 7-dayminimumReleaseAgequarantine.
Both managers are intentionally manual-merge. They do pure text
substitution on flake.nix and do not refresh flake.lock.
The lockfile refresh — historically the reason this runbook existed —
is now performed automatically by the renovate-flake-lock-refresh
workflow, which fires on every ci completion against a renovate/*
branch, detects the bumped input from the PR title, runs
nix flake update --update-input <name>, and commits the refreshed
flake.lock back to the PR branch (App-signed via REST
PUT /contents). Watch the PR for a follow-on
chore(flake): refresh flake.lock for <input> commit a few minutes
after ci first goes green.
The manual runbook below is the fallback when the auto-refresh
does not fire — most often because the PR title format changed and
no longer matches the case arm in
.github/workflows/renovate-flake-lock-refresh.yml. If auto-refresh
silently does not act, an issue is filed under the
renovate-flake-lock-refresh-failure label; pursue the manual
steps below in the meantime.
Why a runbook¶
renovate.json defines custom-regex managers that bump the URL fragment
in inputs.*.url. Renovate's hosted SaaS does not run
postUpgradeTasks (nix flake update is not in Mend's allowed-command
list), and switching to self-hosted Renovate for one command would add
significant ops surface for a solo-maintainer repo. See PR #66 (or
search "postUpgradeTasks" in the spec history) for the analysis.
The cost of this gap is one reviewer touch per bump:
cachix/git-hooks.nix: maybe a handful of bumps per year.NixOS/nixpkgs: twice per year (MayYY.05, NovemberYY.11).
The wide-blast-radius nature of these bumps (especially nixpkgs) means a human review pass was needed anyway.
Expected breakage surface of a nixpkgs bump¶
A major NixOS/nixpkgs bump (e.g. 25.11 → 26.05) tends to drag in
new versions of every tool the devShell + CI + image touch. The visible
fallout falls into a small set of recurring classes. Walk the list
before merging, even when CI is green — some failures land later (next
cron tick, next contributor PR).
- Formatter rewrites.
nixfmt,prettier,mdformat,shfmt, andtaploall move with nixpkgs. A new minor version often rewrites whitespace, line wrapping, or quoting conventions across Markdown / YAML / JSON / Nix / shell / TOML. Accept vianix fmt; do not pin around it. - mkdocs-macros strictness. The site build (
nix build "path:$(pwd)#site") aborts on a literal{{ ... }}outside a Jinja2 raw block. New plugin behavior occasionally starts treating a previously-quiet block as macro input. Wrap the block in raw tags; do not loosen--strict. - mkdocs --strict warnings. Plugin upgrades can promote warnings to errors (broken anchors, missing nav entries, deprecated options). Fix forward; pin the misbehaving plugin only as a last resort and document the pin reason in the same PR.
- zizmor major version. New major versions change rule severities
or add rules that surface on existing workflows.
nix flake checkfails on the new finding. Fix the workflow; only as a last resort raise--min-severityinflake.nix, and never abovelowwithout a security-review entry. - CRITICAL CVEs in image base layers.
image-cve-scan(ci.yml) is the canonical surface. The new nixpkgs may carry an unfixedCRITICALCVE incoreutils,bashInteractive,gnused, etc. The CRITICAL-fail gate (post-PR #90) flags this loudly; the remediation is "wait for nixpkgs to patch + bump again", not a code change here. Skim the Security tab post-merge. - Image base-layer tool renames. If a tool the image previously
shipped is gone from the new nixpkgs (rename, removal,
refactor-to-a-module),
image-smoke'slinpeas -hrun inside the image will surfacecommand not found. Walkpkgs.buildEnv.pathsinflake.nixagainst the smoke output. gh attestation verifytrust-root staleness. Newerubuntu-latestimages carry a newerghCLI, which ships an updated Sigstore TUF trust-root. A nixpkgs bump does not affect this directly, but a coincident runner-image rotation can cause spurious verify failures the same day — confirm by re-running theverify-latest-releasecron 24h later before assuming attestation drift.- pre-commit-hooks lib drift. When
nixpkgslib symbols change between releases,cachix/git-hooks.nix(and the hooks it enables) sometimes break before the project's own pin bumps. Surfaces asnix flake checkfailures unrelated to any workflow change. See "Interaction between the two pins" below.
Step 4 of the step-by-step below contains the same surface as a symptom → fix lookup table; use this section to anticipate before the PR arrives, and the table to triage after CI fails.
When the Renovate PR arrives¶
PR title looks like one of:
Update dependency cachix/git-hooks.nix to <new-SHA>Update dependency NixOS/nixpkgs to <YY.MM>
Diff: exactly one line in flake.nix changed. flake.lock is not
touched. CI required checks will fail on flake-check (lock-out-of-date
error) until you complete the steps below.
Step-by-step¶
1. Check out the PR locally¶
2. Refresh flake.lock¶
For a cachix/git-hooks.nix bump:
For a NixOS/nixpkgs bump:
Both commands rewrite flake.lock in place. Confirm the diff is sane
(git diff flake.lock) — should show new lastModified /
narHash / rev for the relevant node, nothing else.
3. Run formatter¶
Newer nixpkgs ships newer treefmt + prettier + shfmt. These will sometimes rewrite tracked files (markdown line wraps, blank-line trimming, etc.). Run the formatter explicitly so the diff lands in this PR rather than fighting CI:
Stage anything that changes:
4. Check expected side-effect classes¶
The 2026-05-17 nixos-25.05 → 25.11 bump (PR #63) hit five distinct side-effect classes. Use them as a checklist for any future nixpkgs bump:
| Class | Symptom | Fix |
|---|---|---|
prettier |
YAML / Markdown / JSON whitespace diffs | accept the rewrite via nix fmt |
mkdocs-macros strictness |
nix build "path:$(pwd)#site" aborts on a {{ ... }} literal inside a code block |
wrap the offending block in {% raw %}...{% endraw %} (mirrors the pre-existing convention in docs/architecture/ci.md) |
zizmor major version |
nix flake check fails on a previously-quiet workflow finding |
fix the workflow or, as a last resort, adjust --min-severity in flake.nix (do not raise above low without a security-review entry) |
mkdocs --strict |
Build fails on a new plugin warning | fix forward; pin the misbehaving plugin only as a last resort and document the pin reason in the same PR |
| linpeas-image base layers | image-cve-scan SARIF changes; image-smoke could surface command not found regressions |
smoke test locally (step 6) — adjust buildEnv.paths in flake.nix only if a required tool genuinely disappeared from nixpkgs |
cachix/git-hooks.nix bumps in isolation usually only hit the zizmor
row and only when the pre-commit-hooks repo changes hook versions in
lock-step.
5. Build everything¶
nix build .#linpeas --print-build-logs
nix build .#linpeas-bundle --print-build-logs
nix build .#linpeas-image --print-build-logs
nix build "path:$(pwd)#site" --print-build-logs
The path:$(pwd)#site form is required for the site derivation —
it bypasses the git filter so the gitignored docs/_data/dashboard.yml
is visible to the build.
6. Run all test suites¶
nix develop --command bash tests/gen-dashboard-data.test.sh
nix develop --command bash tests/check-pr-workflows-no-secrets.test.sh
nix develop --command bash tests/check-required-checks-no-paths.test.sh
nix develop --command bash tests/check-tag-protection.test.sh
nix develop --command bash tests/check-renovate-invariants.test.sh
nix develop --command bash tests/check-uses-sha-pinned.test.sh
All six must exit 0. If any fail, do not disable the test — debug the regression. The lint scripts encode binding security invariants.
7. Image smoke¶
nix build .#linpeas-image --out-link result-image
VERSION="$(jq --raw-output .version linpeas-pin.json)"
docker rmi "rvenutolo/linpeas:${VERSION}" 2>/dev/null || true
docker load --input result-image
docker run --rm "rvenutolo/linpeas:${VERSION}" -h 2>&1 \
| grep --count 'command not found'
# Expect 0
A non-zero count means a tool the image previously shipped is missing
from the new nixpkgs. Compare pkgs.buildEnv.paths in flake.nix
against the missing tool's package name — usually a rename. Update the
path list in the same PR.
8. Run flake check¶
All pre-commit hooks must pass: actionlint, deadnix, nixpkgs-fmt,
treefmt, shellcheck, statix, uses-sha-pinned, yamllint,
zizmor, readme-flake-show-fresh.
9. Commit the refresh¶
git add flake.lock <any-side-effect-files>
git commit -m "chore(flake): refresh flake.lock for <pin name>"
git push
treefmt is wired as a pre-commit hook — it will run on every staged
file change. If it rewrites something, re-stage and commit.
10. Wait for CI, merge¶
Watch the PR's required checks. If everything is green, merge-commit
through the GitHub UI or gh pr merge <num> --merge --delete-branch.
Note: every commit on the branch must independently satisfy Conventional
Commits (commitlint is a required check) and be signed
(required_signatures is enforced), since each lands verbatim on
main under the merge-commit-only ruleset. The PR title becomes the
merge-commit subject and must itself satisfy Conventional Commits
(pr-title-lint is a required check).
11. Post-merge¶
For NixOS/nixpkgs bumps specifically:
- The CVE-scan SARIF on
mainwill change — surfacing CVEs is advisory only (ci.yml'simage-cve-scanjob is intentionally outside required-checks). Skim the Security tab for any newCRITICALrows. The remediation path for an unfixed base-layer CVE is the next nixpkgs bump. - The Pages cron (14:00 UTC daily) will rebuild the dashboard on its next tick. Push-trigger and release-trigger also rebuild immediately.
- The next
update-flake-lock.ymlcron run (weekly, Monday 06:00 UTC) will refresh within-YY.MMpatches automatically.
Interaction between the two pins¶
If a NixOS/nixpkgs bump and a cachix/git-hooks.nix bump arrive in
separate Renovate PRs, the order matters when the new nixpkgs lib
adds or removes something git-hooks.nix depends on (this is the
class of incompatibility that caused the prior nixpkgs branch mismatch in the first place).
The safe order:
- Bump
nixpkgsfirst. This forces anylibdrift to surface on a single PR. - If
flake checkfails becausegit-hooks.nixis now incompatible, bumpgit-hooks.nixin the same PR (rebase on top of the nixpkgs PR before merging). - Then close out the standalone
git-hooks.nixPR.
If both PRs land cleanly when merged independently, no action needed.
What changed historically¶
- 2026-05-17 — Bumped
nixos-25.05→nixos-25.11(PR #63). Advancedpre-commit-hooksto upstream master61ab0e80.... Added Renovate trackers for both pins (PRs #62 and #64). See the referenced PRs for the full closure rationale.
update-flake-lock credential split¶
update-flake-lock.yml mirrors the update-linpeas.yml split:
compute-lockjob haspermissions: contents: readand must not referencesecrets.BUMP_APP_PRIVATE_KEYoractions/create-github-app-token. Nix-evaluating actions confined here.push-and-mergejob uses only GitHub-owned action SHAs (actions/checkout,actions/download-artifact,actions/create-github-app-token,step-security/harden-runner). No third-party action without a security-review entry.DeterminateSystems/update-flake-lockaction removed entirely — accepted BUMP_PAT directly aswith: token:. Lock updates now vianix flake updatein the read-only job.- App installation token flows only to
gh api/gh prviaGH_TOKEN. Nogit push. Commit lands via RESTPUT /contents→ web-flow signed. flake.lockartifact carries JSON shape guard:push-and-mergeverifies.nodes | type == "object"before committing.
renovate-flake-lock-refresh auto-refresh¶
renovate-flake-lock-refresh.yml auto-completes the post-Renovate-PR
lockfile-refresh step that hosted Renovate cannot run itself (no Nix
on Renovate's SaaS runners; no postUpgradeTasks allowlist).
Trigger: workflow_run of ci completing on a renovate/* head
branch. The identify job gates on ALL of:
- PR author ==
renovate[bot](or legacyrenovate). - PR head branch starts with
renovate/. - PR diff touches
flake.nix. - PR title contains a known dep name (
cachix/git-hooks.nix→pre-commit-hooksinput;NixOS/nixpkgs→nixpkgsinput).
Adding a new auto-refreshable input requires three coordinated
edits in the same PR: (1) extend the case arm in
identify, (2) add a Renovate customManager in renovate.json,
(3) extend the manual fallback runbook in
docs/architecture/flake-input-bumps.md.
Credential split mirrors update-flake-lock.yml:
identify+compute-refreshjobs:permissions: contents: read, no App-key reference. Untrusted Nix-evaluating actions confined tocompute-refresh.push-refreshjob: holdsBUMP_APP_PRIVATE_KEYonly. Commits refreshedflake.lockto PR branch via RESTPUT /contents→ web-flow signed by GitHub. Nogit push.
Loop-breaker: push-refresh compares git hash-object flake.lock
vs branch's blob SHA; bails on match. Protects against the
ci → refresh → ci cycle.
Not in required-checks. Lockfile refresh on a PR cannot block the PR's own merge gate (chicken-and-egg).