build: pin local + CI tool versions in .tool-versions (#157) #158

Merged
james merged 1 commit from 157-pin-tool-versions into main 2026-06-20 01:21:01 +00:00
Owner

Closes #157.

Summary

  • One .tool-versions at repo root pins node, gitleaks, and actionlint.
  • CI actions/setup-node reads it via node-version-file; gitleaks
    • actionlint install steps parse their version out of it.
  • mise reads the same file locally; lefthook hints lead with mise install.
  • Renovate's mise manager (human-review per ADR-0025) handles bumps.

Behavior changes worth flagging

  • Node patch pinned. CI's node-version: "22" (resolves to latest
    22.x at install time) becomes node 22.23.0 — the current 22-LTS head.
    Runs are now reproducible; Renovate proposes patch bumps deliberately.
  • Local node moves to 22.x LTS. Maintainer was on node 26 locally.
    After mise install, local matches CI.
  • Drop env vars + duplicated literals. GITLEAKS_VERSION and
    ACTIONLINT_VERSION workflow envs are gone; the ~10 duplicated
    node-version: "22" literals in pr.yml + secrets.yml collapse to
    one source.
  • Lefthook keeps the "tools on PATH" fallback. A contributor without
    mise can still install via brew — the hook still fires; CI is
    authoritative if versions drift.

Design rationale

In ADR-0025:

  • Why .tool-versions and not mise.toml (setup-node reads
    .tool-versions natively; mise.toml it does not — same file feeds both).
  • Why keep setup-node instead of mise-shimmed node in CI (setup-node's
    npm-cache restore is worth keeping).
  • Alternatives considered: devcontainer, nix, asdf, package.json engines.

Files

  • .tool-versions — the new pin file
  • .forgejo/workflows/{pr,secrets}.yml — read versions from .tool-versions
  • lefthook.yml — hint messages lead with mise
  • README.md "Requirements" — leads with mise, brew as fallback
  • CLAUDE.md — one-line pointer under "Working in this repo"
  • docs/adr/0025-tool-versions-pin.md + docs/adr/README.md — design doc
  • renovate.jsonmise manager rule, human-review label

Test plan

  • CI green on this PR (Lint, Typecheck, Build, Test matrix,
    static-analysis with actionlint, secrets/gitleaks all succeed
    reading versions from .tool-versions)
  • Local lefthook green on git commit after mise install (already
    verified for this commit — gitleaks + actionlint + commit-msg
    passed)
  • Spot-check a fresh clone path: mise install from .tool-versions
    installs node 22.23.0, gitleaks 8.30.1, actionlint 1.7.12
Closes #157. ## Summary - One `.tool-versions` at repo root pins `node`, `gitleaks`, and `actionlint`. - CI `actions/setup-node` reads it via `node-version-file`; gitleaks + actionlint install steps parse their version out of it. - mise reads the same file locally; lefthook hints lead with `mise install`. - Renovate's `mise` manager (human-review per [ADR-0025]) handles bumps. ## Behavior changes worth flagging - **Node patch pinned.** CI's `node-version: "22"` (resolves to latest 22.x at install time) becomes `node 22.23.0` — the current 22-LTS head. Runs are now reproducible; Renovate proposes patch bumps deliberately. - **Local node moves to 22.x LTS.** Maintainer was on node 26 locally. After `mise install`, local matches CI. - **Drop env vars + duplicated literals.** `GITLEAKS_VERSION` and `ACTIONLINT_VERSION` workflow envs are gone; the ~10 duplicated `node-version: "22"` literals in `pr.yml` + `secrets.yml` collapse to one source. - **Lefthook keeps the "tools on PATH" fallback.** A contributor without mise can still install via brew — the hook still fires; CI is authoritative if versions drift. ## Design rationale In [ADR-0025](docs/adr/0025-tool-versions-pin.md): - Why `.tool-versions` and not `mise.toml` (setup-node reads `.tool-versions` natively; `mise.toml` it does not — same file feeds both). - Why keep `setup-node` instead of mise-shimmed node in CI (setup-node's npm-cache restore is worth keeping). - Alternatives considered: devcontainer, nix, asdf, `package.json` engines. ## Files - `.tool-versions` — the new pin file - `.forgejo/workflows/{pr,secrets}.yml` — read versions from `.tool-versions` - `lefthook.yml` — hint messages lead with mise - `README.md` "Requirements" — leads with mise, brew as fallback - `CLAUDE.md` — one-line pointer under "Working in this repo" - `docs/adr/0025-tool-versions-pin.md` + `docs/adr/README.md` — design doc - `renovate.json` — `mise` manager rule, human-review label ## Test plan - [ ] CI green on this PR (Lint, Typecheck, Build, Test matrix, static-analysis with actionlint, secrets/gitleaks all succeed reading versions from `.tool-versions`) - [ ] Local lefthook green on `git commit` after `mise install` (already verified for this commit — gitleaks + actionlint + commit-msg passed) - [ ] Spot-check a fresh clone path: `mise install` from `.tool-versions` installs node 22.23.0, gitleaks 8.30.1, actionlint 1.7.12 [ADR-0025]: docs/adr/0025-tool-versions-pin.md
build: pin local + CI tool versions in .tool-versions (#157)
Some checks failed
Commits / Conventional Commits (pull_request) Successful in 11s
PR / OSV-Scanner (pull_request) Successful in 40s
PR / Static analysis (pull_request) Successful in 41s
PR / Package age policy (soft) (pull_request) Successful in 17s
Secrets / gitleaks (pull_request) Successful in 19s
PR / Test (postgres) (pull_request) Successful in 1m15s
PR / Trivy (image) (pull_request) Failing after 1m26s
PR / Lint (pull_request) Successful in 1m46s
PR / Typecheck (pull_request) Successful in 1m54s
PR / Test (sqlite) (pull_request) Successful in 2m8s
PR / npm audit (pull_request) Successful in 2m15s
PR / Coverage (soft) (pull_request) Successful in 2m8s
PR / Build (pull_request) Successful in 2m24s
760ae3bbba
Local hooks and CI installed node, gitleaks, and actionlint from three
different sources, version-pinned in three different places, or not
pinned locally at all. A contributor on a brew-installed gitleaks 8.20
could pass commits that CI's pinned 8.30.1 then flagged.

One source of truth — `.tool-versions` at repo root:

  node 22.23.0
  gitleaks 8.30.1
  actionlint 1.7.12

Both halves of the system read it:
- Local: `mise install` (asdf-compat .tool-versions reader)
- CI: `actions/setup-node` via `node-version-file: .tool-versions`;
  gitleaks / actionlint install steps `awk` the version out

Node moves from CI's `"22"` major-selector to a patch-exact pin
(22.23.0, current 22-LTS head). Local maintainer node was 26 — pinning
brings local in sync with CI's LTS line.

Drops `GITLEAKS_VERSION` / `ACTIONLINT_VERSION` workflow envs and the
~10 duplicated `node-version: "22"` literals across pr.yml / secrets.yml.

Lefthook hooks keep their "tools on PATH" fallback; the "not installed"
hint now leads with `mise install` and lists `brew` as a fallback.

README "Requirements" leads with mise. CLAUDE.md gets a one-line pointer.
Renovate's `mise` manager picks up bumps; configured human-review per
ADR-0025 because a bump shifts the runtime/scanner across both halves
of the system in one step.

Rationale, alternatives (mise.toml, devcontainer, asdf, leaving
setup-node out), and the keep-setup-node tradeoff in ADR-0025.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Trivy (container image)

Threshold: high  ·  Total findings: 121  ·  At/above threshold: 1

critical high medium low
0 1 50 70
severity id package installed / range fix
high CVE-2026-12151 undici 6.25.0 6.27.0, 7.28.0, 8.5.0
<!-- scanner-comment: trivy --> ### Trivy (container image) **Threshold:** `high` &nbsp;·&nbsp; **Total findings:** 121 &nbsp;·&nbsp; **At/above threshold:** 1 | critical | high | medium | low | |---:|---:|---:|---:| | 0 | 1 | 50 | 70 | | severity | id | package | installed / range | fix | |---|---|---|---|---| | high | [CVE-2026-12151](https://avd.aquasec.com/nvd/cve-2026-12151) | undici | 6.25.0 | `6.27.0, 7.28.0, 8.5.0` |

📊 Test coverage

Patch coverage: no testable lines changed.

Overall (app/, lib/, db/, excluding UI per ADR-0019):

Metric Value Soft target
Lines 85.5% ≥ 50%
Branches 81.2% ≥ 75%
Functions 90.0% informational

Soft thresholds per ADR-0019. Coverage is informational and does not block merge.

<!-- coverage-comment --> ## 📊 Test coverage **Patch coverage:** no testable lines changed. **Overall** (`app/`, `lib/`, `db/`, excluding UI per ADR-0019): | Metric | Value | Soft target | |---|---|---| | Lines | 85.5% ✅ | ≥ 50% | | Branches | 81.2% ✅ | ≥ 75% | | Functions | 90.0% | informational | Soft thresholds per [ADR-0019](docs/adr/0019-coverage-soft-targets.md). Coverage is informational and does not block merge.
james force-pushed 157-pin-tool-versions from 760ae3bbba
Some checks failed
Commits / Conventional Commits (pull_request) Successful in 11s
PR / OSV-Scanner (pull_request) Successful in 40s
PR / Static analysis (pull_request) Successful in 41s
PR / Package age policy (soft) (pull_request) Successful in 17s
Secrets / gitleaks (pull_request) Successful in 19s
PR / Test (postgres) (pull_request) Successful in 1m15s
PR / Trivy (image) (pull_request) Failing after 1m26s
PR / Lint (pull_request) Successful in 1m46s
PR / Typecheck (pull_request) Successful in 1m54s
PR / Test (sqlite) (pull_request) Successful in 2m8s
PR / npm audit (pull_request) Successful in 2m15s
PR / Coverage (soft) (pull_request) Successful in 2m8s
PR / Build (pull_request) Successful in 2m24s
to ccfdde2490
Some checks failed
Commits / Conventional Commits (pull_request) Successful in 16s
PR / OSV-Scanner (pull_request) Successful in 35s
PR / Static analysis (pull_request) Successful in 46s
PR / Package age policy (soft) (pull_request) Successful in 17s
Secrets / gitleaks (pull_request) Successful in 17s
PR / Trivy (image) (pull_request) Failing after 1m14s
PR / npm audit (pull_request) Successful in 2m52s
PR / Lint (pull_request) Successful in 3m11s
PR / Typecheck (pull_request) Successful in 3m17s
PR / Test (postgres) (pull_request) Successful in 3m25s
PR / Test (sqlite) (pull_request) Successful in 3m31s
PR / Build (pull_request) Successful in 3m46s
PR / Coverage (soft) (pull_request) Successful in 3m32s
2026-06-20 01:07:33 +00:00
Compare
james merged commit 05f24c2819 into main 2026-06-20 01:21:01 +00:00
james deleted branch 157-pin-tool-versions 2026-06-20 01:21:01 +00:00
Sign in to join this conversation.
No description provided.