Skip to content

Add bin/bump-deps: one command to see and bump every pinned dependency - #94

Merged
jfmercer merged 2 commits into
masterfrom
dependency-bump-tooling
Jul 28, 2026
Merged

Add bin/bump-deps: one command to see and bump every pinned dependency#94
jfmercer merged 2 commits into
masterfrom
dependency-bump-tooling

Conversation

@jfmercer

Copy link
Copy Markdown
Owner

Addresses topic #4 from yesterday's backlog. Pins were spread across four files with no shared mechanism, and Dependabot reads none of them — so drift was invisible until someone went looking.

The design point

A pin is not always a version. There are four classes here, and a tool that treated them uniformly would be actively harmful — auto-updating a GPG key fingerprint would defeat the entire reason it's pinned.

Class Where What bumping means
VERSION install.sh, ci.yaml, linux installs Latest stable release. Automatic.
EXTERNAL 14 × .chezmoiexternal.yaml Move to master HEAD, recompute sha256. Confirmed per entry with a compare URL, because it pulls upstream code nobody has read into every interactive shell and tmux session.
CONTENT RUSTUP_INIT_SHA256 A hash over an unversioned URL. No version to compare — a change means upstream rewrote the installer. Reported; requires --accept.
ANCHOR EZA_KEY_FPR A GPG public-key fingerprint. Never rewritten. A mismatch is key rotation or an attack; prints a security notice pointing at upstream's own announcement.

Two things that look like pins but aren't, and are left alone: .chezmoiversion is a floor, not a pin; Homebrew and apt packages are deliberately rolling.

Cross-file invariants

Nothing previously checked these:

  1. .chezmoiversioninstall.sh's CHEZMOI_VERSION. The floor is the minimum chezmoi permitted to read this source. If it outruns the version the bootstrap installs, a fresh machine installs chezmoi and is then refused by it — silent bootstrap breakage. Verified by simulating a 2.99.0 floor.
  2. No external may track a moving ref (master/HEAD).
  3. Actions pinned to an exact release, not a floating major tag.

Invariant 3 found a real one: gitleaks-action was pinned to v3, a tag upstream moves. Now v3.0.0.

Interface

bump-deps              # status table: class, current, latest
bump-deps --check      # terse; exit 1 if action needed (CI mode)
bump-deps --apply      # bump versions; confirm each external
bump-deps --self-test  # prove the in-place editing is surgical

After --apply it runs chezmoi apply --dry-run --refresh-externals and re-checks, so a bad checksum fails immediately rather than at your next real apply.

Implementation choices

Python, stdlib only — no pip, no venv, no requirements.txt. A dependency tool that needs dependencies is its own bootstrap problem. gh supplies the API (already in both package lists, authenticated on CI). It runs by hand, so interpreter startup is irrelevant — unlike the bin/ shell scripts on the PATH hot path.

No YAML library, deliberately. 36 of .chezmoiexternal.yaml's 136 lines are comments explaining why each thing is pinned. yaml.safe_load/dump would erase every one. Edits are line-oriented so the file stays byte-stable apart from the values being changed.

Verification

Everything below was executed, not reasoned about:

  • --self-test rewrites each pin to a sentinel and asserts only the captured value changed — no surrounding syntax, no line-count change — then restores every file. It's wired into CI so that guarantee is enforced rather than assumed. Confirms actions/checkout correctly edits all 4 occurrences and the externals edit preserves comments.
  • Both alarm paths driven with fabricated failures: a rotated EZA_KEY_FPR produces the security notice, exits non-zero, and is not rewritten even under --apply ("nothing to do"). A changed RUSTUP_INIT_SHA256 is reported and left alone until --accept rustup.
  • Both invariants driven with fabricated violations (a 2.99.0 floor; an external reverted to master.tar.gz).
  • Detects and classifies all 21 pins correctly; --check exits 0 with everything current.
  • shellcheck correctly skips the Python file (shebang selection), which is exactly why the py_compile step was added — otherwise it'd be unchecked.
  • Self-test leaves the tree clean, so it won't trip the apply job's no-drift gate.

Scheduled check

.github/workflows/deps.yaml — weekly cron plus workflow_dispatch. Runs --check, keeps one tracking issue in sync (updates rather than reopening), closes it when everything is current, and titles CONTENT/ANCHOR changes as security signals rather than routine updates. It never commits.

The cron is the one thing I can't verify here; it needs a real scheduled run or a manual workflow_dispatch. Worth triggering once by hand after merge.

Also in this PR

The two housekeeping changes that were sitting uncommitted: your .chezmoiversion bump to 2.71.1, and the CLAUDE.md note that the GitLab SSH block was retired.

jfmercer added 2 commits July 28, 2026 06:27
- .chezmoiversion: 2.70.5 -> 2.71.1 (John's change). This is a floor, not a
  pin: it is the minimum chezmoi that may read this source directory. It now
  equals install.sh's CHEZMOI_VERSION, which is the constraint that matters --
  if the floor ever exceeds the pinned installer, a fresh bootstrap installs
  chezmoi and then chezmoi refuses to run. bin/bump-deps asserts that.
- CLAUDE.md: note that the GitLab SSH block was retired, so a reader is not
  left hunting for config that is deliberately gone.
Pins were scattered across four files with no shared mechanism, and Dependabot
reads none of them, so drift was invisible. bin/bump-deps walks all of them.

The design point worth keeping: a pin is not always a version. Four classes,
handled differently, because conflating them would be actively harmful --
auto-updating a GPG key fingerprint would defeat the entire reason it is pinned.

  VERSION   install.sh, ci.yaml, linux installs. Latest release, automatic.
  EXTERNAL  the 14 .chezmoiexternal.yaml entries. Move to master HEAD and
            recompute the checksum, confirmed per entry with a compare URL,
            because it pulls upstream code nobody has read into every shell.
  CONTENT   RUSTUP_INIT_SHA256 -- a hash over an *unversioned* URL. There is no
            version to compare; a change means upstream rewrote the installer.
            Reported, and requires --accept.
  ANCHOR    EZA_KEY_FPR -- a GPG public-key fingerprint. NEVER rewritten. A
            mismatch is key rotation or an attack; it prints a security notice
            and tells you to verify against upstream's own announcement.

It also asserts cross-file invariants that nothing previously checked:

- .chezmoiversion must not exceed install.sh's CHEZMOI_VERSION. The floor is the
  minimum chezmoi permitted to read this source, so if it outruns the version the
  bootstrap installs, a fresh machine installs chezmoi and is then refused by it.
  Verified by simulating a 2.99.0 floor.
- no external may track a moving ref (master/HEAD)
- Actions must be pinned to an exact release, not a floating major tag

That last check found a real one: gitleaks-action was pinned to `v3`, which
upstream moves. Now v3.0.0.

Python rather than shell because this needs GitHub API parsing, sha256 over
downloads and per-class logic; it runs by hand so interpreter startup is
irrelevant. Stdlib only -- no pip, no venv -- since a dependency tool that needs
dependencies is its own bootstrap problem. Deliberately no YAML library: 36 of
.chezmoiexternal.yaml's 136 lines are comments explaining why things are pinned,
and load/dump would erase them, so edits are line-oriented and surgical.

- bin/bump-deps: new. --self-test proves the in-place editing changes only the
  pinned value and no surrounding syntax, then restores every file; it is wired
  into CI so that guarantee is enforced rather than assumed.
- .github/workflows/deps.yaml: new. Weekly --check, keeps one tracking issue in
  sync, titles CONTENT/ANCHOR changes as security signals. Never commits.
- .github/workflows/ci.yaml: py_compile step, because the shellcheck steps select
  by shebang and would skip Python entirely; plus the self-test; plus the
  gitleaks-action pin fix.
- .gitignore: __pycache__/ from the py_compile step.
- CLAUDE.md: document the tool, the four classes, and the invariants.
@jfmercer
jfmercer merged commit e4fa2e2 into master Jul 28, 2026
7 checks passed
@jfmercer
jfmercer deleted the dependency-bump-tooling branch July 28, 2026 14:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant