diff --git a/.chezmoiignore b/.chezmoiignore index 887c5ae..1756f30 100644 --- a/.chezmoiignore +++ b/.chezmoiignore @@ -5,6 +5,9 @@ docs/ README.md install_* .gitattributes +Makefile +tools.env +ci/ {{ if ne .chezmoi.os "windows" }} AppData/ Documents/WindowsPowerShell/Profile.ps1 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..944747b --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,60 @@ +# The same check a workstation runs (`make check`), on the two operating +# systems the templates branch on (ADR-0017, ci.mirrors-the-local-check). +# A red step here is the Makefile target to run at home. +name: CI + +on: + push: + branches: [main] + pull_request: + +# The default token in a public repository may carry write; nothing here +# needs more than a read. +permissions: + contents: read + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + check: + name: check + runs-on: ${{ matrix.os }} + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + steps: + # Pinned by commit, the tag in the comment (toolchain.tools-pinned-with-it). + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v7.0.1 + + # The private terms, from the repository secret, into a file outside the + # workspace. Through `env:`, never in the script text. Required wherever + # the secret should exist โ€” a push, or a pull request from this + # repository; a fork's pull request has no secret and the check says + # so loudly and passes, as on a machine not set up yet. + - name: private terms + env: + PRIVATE_TERMS: ${{ secrets.PRIVATE_TERMS }} + FROM_THIS_REPOSITORY: ${{ github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository }} + run: | + if [ -n "$PRIVATE_TERMS" ]; then + printf '%s\n' "$PRIVATE_TERMS" > "$RUNNER_TEMP/private-terms" + echo "PRIVATE_TERMS_FILE=$RUNNER_TEMP/private-terms" >> "$GITHUB_ENV" + fi + if [ "$FROM_THIS_REPOSITORY" = "true" ]; then + echo "PRIVATE_REFS_REQUIRE_TERMS=1" >> "$GITHUB_ENV" + fi + + # The four targets `make check` runs, one step each, so a red step is + # the target to run at home. + - name: make tools + run: make tools + - name: make render + run: make render + - name: make lint + run: make lint + - name: make private-refs + run: make private-refs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e5074f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +# Pinned tools `make tools` downloads (tools.env). A dot-file at the source +# root is not a chezmoi target, so nothing here is deployed. +.tools/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..617755f --- /dev/null +++ b/Makefile @@ -0,0 +1,30 @@ +# The one check target (ADR-0017, ci.mirrors-the-local-check): what a +# workstation runs is what .github/workflows/ci.yaml runs, step for step. +# Each target is its own script under scripts/, so a red job names its step +# and the scripts are themselves under `make lint`. + +.POSIX: + +check: tools render lint private-refs + @echo " ok tools render lint private-refs" >&2 + +# Pinned tools into .tools/ (tools.env); never the machine's. +tools: + @sh scripts/tools.sh + +# Every template renders against placeholder data, hermetically. +render: tools + @sh scripts/render.sh + +# shellcheck over every tracked shell script. +lint: tools + @sh scripts/lint.sh + +# No private identifier in any tracked plaintext file. +private-refs: + @sh scripts/check-private-refs.sh --tree + +clean: + @rm -rf .tools + +.PHONY: check tools render lint private-refs clean diff --git a/README.md b/README.md index 5304e08..70668fc 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,19 @@ # Dotfiles +[![CI](https://github.com/fredericrous/dotfiles/actions/workflows/ci.yaml/badge.svg)](https://github.com/fredericrous/dotfiles/actions/workflows/ci.yaml) + personal environment files +## Checks + +`make check` is what CI runs, with the same commands and the same pinned +tools (`tools.env`, installed into `.tools/`): every template renders +against placeholder data, the shell scripts pass shellcheck, and no tracked +plaintext file names private infrastructure. The private terms live outside +the tree (`~/.config/chezmoi/private-terms`) and, for CI, in the +`PRIVATE_TERMS` repository secret โ€” a copy: whoever edits the file re-sets +the secret with `gh secret set PRIVATE_TERMS < ~/.config/chezmoi/private-terms`. + ## Requirements import your gpg keys diff --git a/amont.conf b/amont.conf index 0824709..4a9c154 100644 --- a/amont.conf +++ b/amont.conf @@ -2,3 +2,5 @@ # # stage name scope severity command pre-commit private-refs * block scripts/check-private-refs.sh +# What CI runs, before the remote sees the push (ADR-0017). Warm: ~10 s. +pre-push check * block make check diff --git a/ci/chezmoi.toml b/ci/chezmoi.toml new file mode 100644 index 0000000..7ad3c20 --- /dev/null +++ b/ci/chezmoi.toml @@ -0,0 +1,15 @@ +# The configuration `make render` runs chezmoi with: placeholder data for the +# five keys the templates read, none of them a real value. The real values +# live in ~/.config/chezmoi/chezmoi.toml on a workstation, generated by +# .chezmoi.toml.tmpl at `chezmoi init` and never committed. + +[data] + bwserver = "" + +[data.forge] + host = "forge.example.invalid" + slug = "example" + +[data.work] + dir = "" + config = "" diff --git a/docs/plans/2026-09-30-dotfiles-ci.md b/docs/plans/2026-09-30-dotfiles-ci.md new file mode 100644 index 0000000..d70d517 --- /dev/null +++ b/docs/plans/2026-09-30-dotfiles-ci.md @@ -0,0 +1,302 @@ +--- +status: active +branch: feat/ci +repos: [dotfiles] +adrs: [ADR-0017] +--- +# dotfiles gets the CI its rules require + +## Review panel + +๐Ÿ‘‰ **Decide:** none โ€” approve if dotfiles gets one `make check` (pinned tools in `.tools/`, hermetic render, shellcheck, tree-wide private-refs) mirrored by GitHub Actions on Linux and macOS, and this PR is the first live F4b cycle. +๐Ÿ“ dotfiles ยท nothing built yet ยท next: worktree, plan commit, Phase 2. Panel: backend, platform (`adds=ops`). +**Changed by review:** `.chezmoiignore` covers the new machinery (it would have been deployed to `$HOME`); release assets with pinned SHA-256s instead of an installer piped to `sh`; the `PRIVATE_TERMS` secret is created before the push, passed via `env:`, required on same-repo runs, names-only in logs. +**Verdicts:** backend round 1 approve-with-changes (7, all applied); round 2 backend + platform approve-with-changes (5 + 5, all applied); final binding backend + platform approve-with-changes. +**Carried into Phase 2** (final binding, no body change): NUL-safe `git ls-files -z` loops in `--tree`, staged mode and `lint` (a tracked path holds a space); the render loop prints `render: $f` on failure and lists templates with a git pathspec; `--persistent-state` on every chezmoi call; an empty pattern under `PRIVATE_REFS_REQUIRE_TERMS=1` is a failure. + +๐Ÿ“„ Full reviews: [2026-09-30-dotfiles-ci.reviews.md](2026-09-30-dotfiles-ci.reviews.md) + +## Implementation review + +Round 1 `approve-with-changes` (52k tokens, 78 s), Delta `approve-with-changes` (31k, 30 s), tree `19bd0a5e4c1d`. +Fixed: an invalid regex in the terms file passed silently (grep exit 2 read as no match) โ€” real, would have shipped; four workflow steps to match the targets; status lines and usage to stderr; usage exits 2; no `|| true` in lint's shebang read. +Deliberate: lint's unreadable-file guard (`[ -r "$f" ]`) is the next change's, not a third pass; the checksum wording in Behaviour now matches the code (`digest` + compare, not `shasum -c`). +Record-only items (phase ticks, this section) live under `docs/plans/`, outside the canonical tree, so the reviewed tree is the pushed tree. + +## Context + +`fredericrous/dotfiles` is a public GitHub repository with an amont +pre-commit gate and no continuous-integration workflow. ADR-0017 makes that +a defect (`ci.from-first-commit`: a repository has a workflow in its first +commit or in the commit that adds its first check target), and the working +method's soak notes recorded it as a trap on 2026-09-28. The repository is +also where every Claude skill and agent lives, so a template that no longer +renders, or a private hostname that slips into a plaintext file, reaches +every session and the public. + +This change is also **Phase 4 of the implementation-review plan** +(`decisions:docs/plans/2026-09-29-implementation-reviewer-before-push.md`): +the first real F4b cycle, on a code diff with a plan, whose tokens, seconds, +verdict and finding quality feed the 2026-10-06 soak review. + +## Goal + +One check target, `make check`, that a workstation and GitHub Actions run +with the same commands and pinned tools: every template renders with +placeholder data, the shell scripts pass shellcheck, and no plaintext file in +the tree carries a private identifier. The workflow runs it on Linux and +macOS for every push to `main` and every pull request. + +## Non-goals + +- A Windows render. The `AppData` and PowerShell branches stay unverified + until a runner is worth its minutes. +- Forgejo. The repository is public on GitHub, so GitHub Actions + (`ci.system-follows-visibility`). +- Changing what amont's pre-commit checks; `private-refs` at pre-commit + keeps scanning the staged files. +- Publishing the private terms. They stay outside the tree on the + workstation and in a repository secret in CI. +- A fleet CI template or reusable workflow; one repository, its own file. + +## Behaviour + +### `make check`, the one target (`ci.mirrors-the-local-check`) + +`check: tools render lint private-refs`, each a target of its own so a red +job names its step. The Makefile is POSIX make plus `/bin/sh`, since the +repository has no language of its own. + +- **`tools`** installs the pinned tools into `.tools/` (gitignored) and + never uses the machine's (`toolchain.pinned-exactly-in-the-repository`, + `toolchain.tools-pinned-with-it`, `toolchain.one-pin-read-by-both`): + - `tools.env` holds the only pins: `CHEZMOI=v2.72.1`, + `SHELLCHECK=v0.11.0`. The Makefile reads it; nothing else states a + version. + - Both tools are downloaded as the **release asset of the pinned tag**, + straight from GitHub releases, never through an installer script + piped to `sh`: `chezmoi___.tar.gz` and + `shellcheck-...tar.xz` for the host + โ€” chezmoi names them `linux_amd64`, `darwin_amd64`, `darwin_arm64`, + shellcheck `linux.x86_64`, `darwin.x86_64`, `darwin.aarch64`; the + Makefile maps `uname -s`/`uname -m` to both spellings. `tools.env` + holds, beside each version, the **SHA-256 of each of the six assets**: + chezmoi's from its published `chezmoi__checksums.txt`; shellcheck + publishes no checksums file, so its three are computed at pin time + from the asset downloaded over HTTPS from the GitHub release, and pin + what was reviewed rather than what upstream attests. The Makefile + verifies every download โ€” `shasum -a 256` (or `sha256sum`) compared to + the pinned digest โ€” before extracting; a mismatch fails naming the asset. An unknown host is a failure that + names the host, not a fallback to PATH. + - Afterwards `.tools/chezmoi --version` and `.tools/shellcheck + --version` must contain their pins, or the target fails. + - Idempotent: a `.tools/` already at the pinned version is not + re-downloaded, so a second `make tools` sends no request. +- **`render`** proves every template renders: + - `ci/chezmoi.toml` (committed) carries placeholder `[data]`: + `bwserver = ""`, `forge.host = "forge.example.invalid"`, + `forge.slug = "example"`, `work.dir = ""`, `work.config = ""` โ€” the + five keys the templates read, none of them a real value. + - `.tools/chezmoi --config ci/chezmoi.toml --persistent-state + "$dst/state.boltdb" --source . --destination "$dst" apply --exclude + encrypted,externals,scripts` with `dst` a fresh temp directory: every + template is executed against the placeholders and written there; + encrypted files need no key, externals no network, and no `run_once_*` + script runs. The state file goes into `dst` too, because chezmoi + otherwise writes it beside the config file and dirties the tree. + Afterwards the target asserts that `$dst/.zshrc`, + `$dst/.claude/CLAUDE.md` and `$dst/.claude/skills/worktree-task/SKILL.md` + exist and are non-empty, that `$dst/Makefile`, `$dst/tools.env` and + `$dst/ci` do NOT exist (the repository machinery never reaches a home + directory), and removes `dst`. + - The two `run_once_*.tmpl` scripts are excluded from the apply (they + must not run) but are the only templates that call `include` and + `lookPath`, so each is rendered on its own: `.tools/chezmoi --config + ci/chezmoi.toml --source . execute-template < $f` for every tracked + `run_once_*.tmpl`, output discarded, exit status kept. + - The config template itself: `.tools/chezmoi --config ci/chezmoi.toml + execute-template --init --promptString "bitwarden server url=x" + < .chezmoi.toml.tmpl | grep -q 'encryption = "gpg"'` โ€” with the ci + config, so `promptStringOnce` never reads the workstation's real data + and the run is the same at home and on the runner. +- **`lint`**: `.tools/shellcheck` over every tracked `*.sh` and every + tracked file whose first line is a `sh`/`bash` shebang + (`scripts/check-private-refs.sh`, `install_dependencies_debian.sh`, + `mac_defaults.sh` today; the list is computed, not written down). fish + and PowerShell files are not shellcheck's business. +- **`private-refs`**: `scripts/check-private-refs.sh --tree` โ€” a new mode + of the existing script that scans every tracked plaintext file + (`git ls-files`, `encrypted_*` skipped) instead of the staged set, with + the same terms file (`PRIVATE_TERMS_FILE`, default + `~/.config/chezmoi/private-terms`). Two differences from the staged + mode, both because a CI log is public: on a hit it prints **file names + only**, never the matching lines (a regex term defeats GitHub's secret + masking), and a path that itself matches a term is printed as + ``; and with `PRIVATE_REFS_REQUIRE_TERMS=1` a missing terms file is + a failure, not the loud pass โ€” CI sets it on every push to `main` and on + a pull request from this repository, and leaves it unset only for a + fork's pull request, which has no secret (`general.no-disabled-safety`). + Without the flag the script behaves exactly as today, so amont's + pre-commit entry is untouched. + +### The workflow (`.github/workflows/ci.yaml`) + +- `on: push: branches: [main]` and `pull_request`; `concurrency` cancels a + superseded run; `timeout-minutes: 10`. +- `permissions: contents: read` at the top level (the default token in a + public repository may carry write), and `actions/checkout` pinned by + commit SHA with its tag in a comment (`toolchain.tools-pinned-with-it` + applies to actions too). +- One job, `check`, `strategy.matrix.os: [ubuntu-latest, macos-latest]` + (`fail-fast: false`, so both branches of the templates report): the + checkout, then a step that receives the `PRIVATE_TERMS` repository secret + through `env:` (never inside the `run:` text) and writes it with + `printf '%s\n' "$PRIVATE_TERMS" > "$RUNNER_TEMP/private-terms"` โ€” outside + `$GITHUB_WORKSPACE` โ€” exporting `PRIVATE_TERMS_FILE` when it is + non-empty, and exporting `PRIVATE_REFS_REQUIRE_TERMS=1` when + `github.event_name == 'push'` or the pull request's head repository is + this one โ€” so a missing secret fails the job where the secret should + exist and only a fork's pull request gets the loud pass; then `make + check`. +- The job name and the step are the same words as the Makefile targets, so + a red line in the run is a target to run at home. + +### The workstation side + +- `amont.conf` gains `pre-push check * block make check`, so a push + runs what CI runs before the remote sees it (ADR-0009). Its cost is + measured in Phase 2 (a warm `make check` wall time) before the entry is + added, and recorded below; if the warm run is over 20 s, the pre-push + entry runs `make lint private-refs` only and the render stays CI's. +- `.gitignore` (new, at the source root; chezmoi ignores dot-files there, + so nothing is deployed) holds `.tools/`. +- `.chezmoiignore` gains `Makefile`, `tools.env` and `ci/`: chezmoi + deploys every non-dot source file it is not told to ignore, and these + three are repository machinery like `amont.conf` and `scripts/`. +- `README.md` gains the CI badge and one sentence: `make check` is what CI + runs. + +## Phases + +- [x] Phase 1 โ€” this plan, `docs/plans/2026-09-30-dotfiles-ci.md`, the + first commit. +- [x] Phase 2 โ€” `tools.env` (versions and asset checksums), `Makefile` + (`tools`, `render`, `lint`, `private-refs`, `check`), `ci/chezmoi.toml`, + `.gitignore`, `.chezmoiignore` entries, and the `--tree` mode of + `scripts/check-private-refs.sh` (names only, `PRIVATE_REFS_REQUIRE_TERMS`). + `make check` green locally on this Mac; recorded before Phase 3: the + warm and the cold (empty `.tools/`) wall time of `make check`, the count + of shellcheck findings the existing scripts produce today, and the + `--tree` hit count (file names) against the real terms file โ€” a hit + already in the tree is fixed in this phase, since `--tree` fails on it + where the staged mode never did. +- [x] Phase 3 โ€” `.github/workflows/ci.yaml`, the `amont.conf` pre-push + entry, the README line. **Before the push**: `gh secret set + PRIVATE_TERMS --repo fredericrous/dotfiles < ~/.config/chezmoi/private-terms` + (stdin, never argv), otherwise the first same-repository run fails by + design. Then F4b (the first real implementation review), push, both + runners green, merge. +- [ ] Phase 4 โ€” record the F4b cycle (tokens, seconds, verdict, whether + each finding was real) in the implementation-review plan's Verification, + in decisions, as its Phase 4; set that plan's Phase 4 ticked. (Opened + right after this PR merges; this plan's tick rides the next dotfiles + change.) + +## Decision log + +- 2026-09-30 โ€” tools are downloaded by release tag into `.tools/`, not + taken from Homebrew or apt: the fleet's toolchain rules want one pin read + by both the workstation and the runner, and a `brew install shellcheck` + in the workflow would be a second, floating version. +- 2026-09-30 โ€” the render check applies into a temp directory rather than + `--dry-run`: a dry run also executes templates, but writing the files + lets the target assert on what came out, and `--exclude + encrypted,externals,scripts` keeps it hermetic. +- 2026-09-30 โ€” `private-refs --tree` is a mode of the existing script, not + a second script: one term file, one message, one place to fix. +- 2026-09-30 โ€” the `PRIVATE_TERMS` secret is a copy of the workstation's + terms file and can drift from it; whoever edits the file re-sets the + secret (`gh secret set โ€ฆ < file`), and the README says so. + +## Verification (input โ†’ expected โ†’ actual) + +- `make tools` on this Mac โ†’ `.tools/chezmoi --version` prints v2.72.1, + `.tools/shellcheck --version` prints 0.11.0; a second run downloads + nothing. +- `tools.env` edited to a version that does not exist โ†’ `make tools` fails + naming the tool and the tag; one checksum digit changed โ†’ fails naming + the asset (falsifications), then restored. +- `git status --porcelain` after `make check` โ†’ empty (no state file, no + tool, nothing rendered inside the tree). +- `.tools/chezmoi --config ci/chezmoi.toml --source . managed | grep -E + '^(Makefile|tools.env|ci)'` โ†’ prints nothing. +- `make render` โ†’ the temp destination holds `.zshrc`, `.claude/CLAUDE.md`, + `.claude/skills/worktree-task/SKILL.md`; a template broken on purpose + (an unclosed `{{`) โ†’ `make render` fails naming the file + (falsification), then restored. +- `make lint` โ†’ shellcheck runs over the computed list (printed) and exits + 0; a script with an unquoted `$var` in a for loop added on purpose โ†’ + fails (falsification), then removed. +- `make private-refs` with `PRIVATE_TERMS_FILE` pointing at a temp file + containing a term that IS in the tree (a word from `README.md`) โ†’ exit 1 + naming the file and not the line; with the real terms file โ†’ exit 0; + with no file โ†’ the loud NOT-checking line and exit 0; + `PRIVATE_REFS_REQUIRE_TERMS=1 PRIVATE_TERMS_FILE=/nonexistent make + private-refs` โ†’ exit 1 naming the missing file. +- `make render` with `run_once_fisher.fish.tmpl` broken on purpose (an + unclosed `{{`) โ†’ fails naming that file (falsification), then restored. +- `make check` โ†’ all four, green, wall time recorded. + - Phase 2 actuals (2026-09-30, this Mac, Intel): cold `make check` 15 s, + warm 9โ€“10 s (pre-push keeps the full target, under 20 s); `make tools` + twice โ†’ second run "(present)" for both, no request; wrong version โ†’ + "tools: could not download โ€ฆ/v0.11.99/โ€ฆ"; one checksum digit โ†’ "tools: + checksum mismatch for shellcheck-v0.11.0.darwin.x86_64.tar.xz"; + `git status --porcelain` after `make check` โ†’ only the intended files; + `chezmoi managed` โ†’ none of `Makefile`, `tools.env`, `ci`; broken + `run_once_fisher.fish.tmpl` โ†’ "render: โ€ฆ does not render"; broken + `dot_claude/CLAUDE.md.tmpl` โ†’ chezmoi names it ("unclosed action โ€ฆ + CLAUDE.md.tmpl:336"); an unquoted-loop script โ†’ 2 ร— SC2086, exit 1; a + README word as a term โ†’ exit 1 naming `README.md`, line not shown; the + real terms โ†’ 0 hits over 96 files (= tracked minus `encrypted_*`); no + terms file โ†’ "NOT checking", exit 0; `PRIVATE_REFS_REQUIRE_TERMS=1` + with no file, with a comments-only file, and with `[unclosed` โ†’ exit 1 + each; a term only in the include line of the `Application Support` + settings template โ†’ that path named (the NUL-safe loop reaches it); + the term `code` โ†’ `` ร—4, the path never printed. + shellcheck baseline on the tracked scripts before the fixes: 1 finding + in the three scripts the plan named, 20 more in the six git helpers + the shebang rule pulled in, all fixed (two were real bugs: literal + backslash-quotes to `git commit`, `${2:REMOVED}` for a default). +- GitHub Actions: the PR's `check (ubuntu-latest)` and `check + (macos-latest)` โ†’ both green; the run's step names are the target names; + the downloaded run log contains no `NOT checking` line and no term from + the secret. + - dotfiles#14, run 36642805700 (2026-09-30): ubuntu 6 s, macos 12 s, both + `success`; steps `make tools`, `make render`, `make lint`, `make + private-refs`; the log holds 0 `NOT checking` lines, 0 matches of any + term, `private-refs ok (96 plaintext files scanned)` on both runners, + both tools reporting their pins on both. The `PRIVATE_TERMS` secret was + set before the push with `gh secret set โ€ฆ < file`. +- A deliberate hit on a throwaway branch (a placeholder term added to the + secret's file locally, `--tree` run with it) โ†’ exit 1 naming the file + and NOT printing the term. +- Pre-push cost: warm `make check` wall time on this Mac, recorded, and the + shellcheck finding count on the existing scripts before any fix. +- F4b: `amont-agent tree-sha --block` โ†’ the block; the + `implementation-review` agent โ†’ its verdict and findings; each finding + fixed or `deliberate:`; the push โ†’ silent, journal `unconfirmed reviewed` + (the hook's live `reviewed` case, still unverified). Tokens and seconds + recorded here and in the implementation-review plan. + - 2026-09-30: block `repo=chezmoi sha=19bd0a5eโ€ฆ`; round 1 + approve-with-changes (52k, 78 s, 6 findings, 1 real bug: the invalid + regex); Delta approve-with-changes (31k, 30 s); the push of this branch + โ†’ the hook silent, one journal line `implementation-review unconfirmed + reviewed`, pass file `by-tree/chezmoi/19bd0a5eโ€ฆ.json` written by the + hook. Found on the hook itself: its Bash guard refused a read-only `ls` + of the pass files (amont-agent#61). + +## Outcome + +(filled when the plan closes) + + diff --git a/docs/plans/2026-09-30-dotfiles-ci.reviews.md b/docs/plans/2026-09-30-dotfiles-ci.reviews.md new file mode 100644 index 0000000..ff53ac7 --- /dev/null +++ b/docs/plans/2026-09-30-dotfiles-ci.reviews.md @@ -0,0 +1,36 @@ +# Full reviews โ€” dotfiles gets the CI its rules require + +Reference for [the plan](2026-09-30-dotfiles-ci.md). Written by the panel on 2026-09-30; not part of the reviewed body. + +## Full reviews (reference) +### plan-review-backend, round 1 โ€” approve-with-changes (39k, 62 s) + +1. [blocking] `Makefile`, `tools.env`, `ci/` would be deployed to `$HOME` (`.chezmoiignore:1-7`). โ†’ applied: ignored; `render` asserts their absence. +2. [blocking] CI infrastructure undeclared. โ†’ applied: `adds=ops`. +3. [high] `ci/chezmoistate.boltdb` would dirty the tree. โ†’ applied: `--persistent-state "$dst/state.boltdb"`; `git status --porcelain` check. +4. [high] Matching lines printed into a public log. โ†’ applied: names only in `--tree`. +5. [medium] Silent pass on a missing secret for pushes to `main`. โ†’ applied: `PRIVATE_REFS_REQUIRE_TERMS=1` on push and same-repo PRs. +6. [medium] Installer script piped to `sh`, no checksums. โ†’ applied: release assets + SHA-256 per asset in `tools.env`. +7. [low] Unmeasured pre-push cost, no shellcheck baseline. โ†’ applied: measured in Phase 2. +Would measure: `chezmoi managed` shows none of the three; `git status --porcelain` empty; a deliberate hit names the file, not the term. + +### plan-review-backend, round 2 โ€” approve-with-changes (36k, 57 s) + +1โ€“7 resolved. New: 1. [medium] no test of the `REQUIRE_TERMS` failure path โ†’ applied. 2. [medium] shellcheck may publish no checksums; asset naming โ†’ applied (computed at pin time; mapping written out). 3. [low] config-template check without the ci config โ†’ applied. 4. [low] pre-push cost unbounded โ†’ applied (20 s, else `lint private-refs`). 5. [low] a path can carry a term โ†’ applied (``). + +### plan-review-platform, round 1 โ€” approve-with-changes (36k, 50 s) + +1. [high] `run_once_*.tmpl` skipped by the apply โ†’ applied: each rendered with `execute-template`; falsification added. +2. [high] no step creates the secret; first same-repo run would fail โ†’ applied: `gh secret set โ€ฆ < file` before the push; drift recorded. +3. [medium] secret placement โ†’ applied: `env:` + `printf` into `$RUNNER_TEMP`. +4. [medium] no `permissions:`, unpinned action โ†’ applied: `contents: read`, checkout pinned by SHA. +5. [low] `--tree` baseline and cold time โ†’ applied. +Would measure: a broken `run_once` template goes red; first same-repo run red then green after the secret; cold vs warm `make check`. + +### plan-review-backend, final binding โ€” approve-with-changes (37k, 49 s) + +1โ€“5 resolved. **Carried into Phase 2:** [high] `for f in $staged` splits `private_Library/private_Application Support/โ€ฆ/settings.json.tmpl` on the space and skips it silently โ€” `--tree`, the staged mode and `lint` read `git ls-files -z` / `git diff --cached -z` NUL-safely; a term planted only in that file โ†’ exit 1. [medium] `lint`'s list built from `-z` and its count compared to `git ls-files`. [low] under `REQUIRE_TERMS`, an empty pattern (comments-only terms file) is a failure; falsification with `# x`. [low] the `include`/`lookPath` claim holds today; no edit. + +### plan-review-platform, final binding โ€” approve-with-changes (32k, 30 s) + +1โ€“5 resolved. **Carried into Phase 2:** [medium] `execute-template < $f` makes chezmoi name `stdin`, so the Makefile prints `render: $f` and exits 1 itself. [medium] `--persistent-state "$dst/state.boltdb"` on both `execute-template` calls too, run before `dst` is removed. [low] the two `run_once` templates are at different depths โ€” list them with `git ls-files '*run_once_*.tmpl'`, print the list, fail under 2 entries. diff --git a/install_dependencies_debian.sh b/install_dependencies_debian.sh index 0eefae6..e0b5da2 100644 --- a/install_dependencies_debian.sh +++ b/install_dependencies_debian.sh @@ -11,7 +11,7 @@ if [ "$EUID" -ne 0 ] fi # create user qaunix -useradd -g sysadmin -d /home/qaunix -m -p $(echo qaunix | openssl passwd -1 -stdin) -s /bin/bash qaunix +useradd -g sysadmin -d /home/qaunix -m -p "$(echo qaunix | openssl passwd -1 -stdin)" -s /bin/bash qaunix # set dns echo "search mydns.test" >> /etc/resolvconf/resolv.conf.d/base diff --git a/private_dot_config/git/bin/executable_git-author-prev b/private_dot_config/git/bin/executable_git-author-prev index 3a6bab4..f4a98c8 100644 --- a/private_dot_config/git/bin/executable_git-author-prev +++ b/private_dot_config/git/bin/executable_git-author-prev @@ -3,8 +3,8 @@ SCRIPT_NAME=$(basename "$0" | sed 's/git-//') if [ "$#" -ne 2 ]; then - echo -e "Usage:\n git $SCRIPT_NAME \$name \$email"; - exit 1; + printf 'Usage:\n git %s \n' "$SCRIPT_NAME" >&2 + exit 2 fi -git commit --amend --author \"$1 <$2>\" -C HEAD \ No newline at end of file +git commit --amend --author "$1 <$2>" -C HEAD diff --git a/private_dot_config/git/bin/executable_git-author-rewrite b/private_dot_config/git/bin/executable_git-author-rewrite index 1769171..ee1b43e 100644 --- a/private_dot_config/git/bin/executable_git-author-rewrite +++ b/private_dot_config/git/bin/executable_git-author-rewrite @@ -3,8 +3,8 @@ SCRIPT_NAME=$(basename "$0" | sed 's/git-//') if [ "$#" -eq 0 ] || [ "$#" -lt 3 ]; then - echo -e "Usage:\n git $SCRIPT_NAME \$old_email \$name \$new_email"; - exit 1; + printf 'Usage:\n git %s \n' "$SCRIPT_NAME" >&2 + exit 2 fi -git filter-repo --mailmap <(echo "$2 <$3> <$1>") ${@:4} +git filter-repo --mailmap <(echo "$2 <$3> <$1>") "${@:4}" diff --git a/private_dot_config/git/bin/executable_git-coauthor b/private_dot_config/git/bin/executable_git-coauthor index ef5c3cd..3c0a233 100644 --- a/private_dot_config/git/bin/executable_git-coauthor +++ b/private_dot_config/git/bin/executable_git-coauthor @@ -1,16 +1,16 @@ -#!/bin/sh +#!/bin/bash # coauthor Search author $name in previous commits and append the # trailer Co-authored-by to the last commit SCRIPT_NAME=$(basename "$0" | sed 's/git-//') if [ "$#" -ne 1 ]; then - printf "Usage:\n git $SCRIPT_NAME \$name\n"; - exit 1; + printf 'Usage:\n git %s \n' "$SCRIPT_NAME" >&2 + exit 2 fi set -e -o pipefail OLD_MSG=$(git log --format=%B -1) -CO_AUTHOR=$(git log --format='%aN <%ae>' | sort -u | rg $1 --context=0) -if [ ! -z \"$CO_AUTHOR\" ]; then - git commit --amend -m\"$OLD_MSG\" -m\"Co-authored-by: $CO_AUTHOR\" +CO_AUTHOR=$(git log --format='%aN <%ae>' | sort -u | rg "$1" --context=0) +if [ -n "$CO_AUTHOR" ]; then + git commit --amend -m "$OLD_MSG" -m "Co-authored-by: $CO_AUTHOR" fi diff --git a/private_dot_config/git/bin/executable_git-fixup b/private_dot_config/git/bin/executable_git-fixup index 887e6c4..d273c48 100644 --- a/private_dot_config/git/bin/executable_git-fixup +++ b/private_dot_config/git/bin/executable_git-fixup @@ -3,13 +3,11 @@ SCRIPT_NAME=$(basename "$0" | sed 's/git-//') if [ "$#" -eq 0 ]; then - printf "Usage:\n git $SCRIPT_NAME \$sha\n"; - exit 1; + printf 'Usage:\n git %s \n' "$SCRIPT_NAME" >&2 + exit 2 fi SHA=$(git rev-parse "$1") -git commit --fixup=$SHA ${@:2} -if [ $? -ne 0 ]; then - EDITOR=true - git rebase -i --autostash --autosquash $SHA^ +if ! git commit --fixup="$SHA" "${@:2}"; then + EDITOR=true git rebase -i --autostash --autosquash "$SHA^" fi diff --git a/private_dot_config/git/bin/executable_git-replace-history b/private_dot_config/git/bin/executable_git-replace-history index aa3911c..ab44596 100644 --- a/private_dot_config/git/bin/executable_git-replace-history +++ b/private_dot_config/git/bin/executable_git-replace-history @@ -3,8 +3,8 @@ SCRIPT_NAME=$(basename "$0" | sed 's/git-//') if [ "$#" -eq 0 ] || [ "$#" -gt 2 ]; then - printf "Usage:\n git $SCRIPT_NAME \$old_text [\$new_text:REMOVED]\n"; - exit 1; + printf 'Usage:\n git %s [, default REMOVED]\n' "$SCRIPT_NAME" >&2 + exit 2 fi -git filter-repo --force --replace-text <(echo "$1==>${2:REMOVED}") +git filter-repo --force --replace-text <(echo "$1==>${2:-REMOVED}") diff --git a/private_dot_config/git/bin/executable_git-rm-history b/private_dot_config/git/bin/executable_git-rm-history index 6b5e7b6..88244f9 100644 --- a/private_dot_config/git/bin/executable_git-rm-history +++ b/private_dot_config/git/bin/executable_git-rm-history @@ -1,10 +1,10 @@ -#!/bin/sh +#!/bin/bash # rm-history Erase a $path from git history SCRIPT_NAME=$(basename "$0" | sed 's/git-//') if [ "$#" -eq 0 ]; then - printf "Usage:\n git $SCRIPT_NAME \$path [...args]\n"; - exit 1; + printf 'Usage:\n git %s [...args]\n' "$SCRIPT_NAME" >&2 + exit 2 fi -git filter-repo --path "$1" --invert-paths ${@:2} +git filter-repo --path "$1" --invert-paths "${@:2}" diff --git a/scripts/check-private-refs.sh b/scripts/check-private-refs.sh index ad5e51f..983f578 100755 --- a/scripts/check-private-refs.sh +++ b/scripts/check-private-refs.sh @@ -11,20 +11,42 @@ # script carried them inline and so re-introduced the forge host into the public # repository โ€” the guard leaking the thing it guards. # -# Scoped to what is STAGED, so a pre-existing reference in an untouched file is -# not this commit's problem โ€” the rule amont's own checks follow. +# Two modes: +# (default) the STAGED files โ€” amont's pre-commit gate. A pre-existing +# reference in an untouched file is not this commit's problem, +# the rule amont's own checks follow. On a hit the matching +# lines are shown: the terminal is the committer's own. +# --tree every tracked file โ€” `make check`, and therefore CI. On a hit +# only file names are printed, never the lines, because a CI +# log is public and a regex term defeats GitHub's secret +# masking; a path that itself matches is ``. +# +# The terms file is read from $PRIVATE_TERMS_FILE. Without one the check +# fails OPEN, loudly: failing closed would block every commit on a machine +# not set up yet, including the commit that sets it up. CI is different โ€” +# there the secret should exist โ€” so PRIVATE_REFS_REQUIRE_TERMS=1 makes a +# missing or empty terms file a failure (general.no-disabled-safety). # # Values, not mechanisms: `~/.netrc` and mTLS are standard and belong in # readable documentation. Ban what names a person or a machine. set -eu +mode=staged +case "${1:-}" in + --tree) mode=tree ;; + '') ;; + *) echo "usage: $0 [--tree]" >&2; exit 2 ;; +esac + TERMS_FILE="${PRIVATE_TERMS_FILE:-$HOME/.config/chezmoi/private-terms}" +require="${PRIVATE_REFS_REQUIRE_TERMS:-}" if [ ! -r "$TERMS_FILE" ]; then - # Fail OPEN, but loudly. Failing closed would block every commit on a machine - # not set up yet, including the commit that sets it up; silence would leave - # the repository unguarded with nothing to notice. + if [ -n "$require" ]; then + echo "private-refs: no term list at $TERMS_FILE and PRIVATE_REFS_REQUIRE_TERMS is set โ€” failing." >&2 + exit 1 + fi echo "private-refs: no term list at $TERMS_FILE โ€” NOT checking." >&2 echo " create it (one regex per line) or set PRIVATE_TERMS_FILE." >&2 exit 0 @@ -32,28 +54,72 @@ fi PATTERN=$(sed -e 's/#.*//' -e 's/[[:space:]]*$//' "$TERMS_FILE" \ | grep -v '^$' | paste -sd '|' -) -[ -n "$PATTERN" ] || exit 0 +if [ -z "$PATTERN" ]; then + if [ -n "$require" ]; then + echo "private-refs: the term list at $TERMS_FILE holds no term and PRIVATE_REFS_REQUIRE_TERMS is set โ€” failing." >&2 + exit 1 + fi + exit 0 +fi +# A term list that is not a valid extended regex would make every grep below +# exit 2, which `if` reads as "no match": a silent pass. Refuse it instead, +# naming the file and never the terms. +if printf '' | grep -qiE "$PATTERN"; then :; else + if [ $? -gt 1 ]; then + echo "private-refs: the term list at $TERMS_FILE is not a valid extended regex โ€” failing." >&2 + exit 1 + fi +fi -staged=$(git diff --cached --name-only --diff-filter=ACM) -[ -n "$staged" ] || exit 0 +# The files to scan, one per line, spaces intact (no tracked path holds a +# newline). NUL from git, because a tracked path here contains a space and +# word-splitting used to skip it without a word. +files=$(mktemp "${TMPDIR:-/tmp}/private-refs.XXXXXX") +trap 'rm -f "$files"' EXIT INT TERM +if [ "$mode" = tree ]; then + git ls-files -z | tr '\0' '\n' > "$files" +else + git diff --cached --name-only -z --diff-filter=ACM | tr '\0' '\n' > "$files" +fi +[ -s "$files" ] || exit 0 found='' -for f in $staged; do +scanned=0 +while IFS= read -r f; do + [ -n "$f" ] || continue # ciphertext cannot match, and skipping it keeps the scan cheap case "$f" in *encrypted_*) continue ;; esac [ -f "$f" ] || continue - # the staged content, not the working tree: they can differ - if git show ":$f" 2>/dev/null | grep -qiE "$PATTERN"; then - found="$found $f" + scanned=$((scanned + 1)) + if [ "$mode" = tree ]; then + content() { cat "$f"; } + else + # the staged content, not the working tree: they can differ + content() { git show ":$f" 2>/dev/null; } fi -done + if content | grep -qiE "$PATTERN"; then + found="$found +$f" + fi +done < "$files" -[ -n "$found" ] || exit 0 +if [ -z "$found" ]; then + [ "$mode" = tree ] && echo " private-refs ok ($scanned plaintext files scanned)" >&2 + exit 0 +fi echo "private identifiers in plaintext, in a PUBLIC repository:" >&2 -for f in $found; do - echo " $f" >&2 - git show ":$f" | grep -inE "$PATTERN" | head -3 | sed 's/^/ /' >&2 +printf '%s\n' "$found" | sed '/^$/d' | while IFS= read -r f; do + if [ "$mode" = tree ]; then + if printf '%s' "$f" | grep -qiE "$PATTERN"; then + echo " " >&2 + else + echo " $f" >&2 + fi + else + echo " $f" >&2 + git show ":$f" | grep -inE "$PATTERN" | head -3 | sed 's/^/ /' >&2 + fi done cat >&2 <<'EOF' diff --git a/scripts/lint.sh b/scripts/lint.sh new file mode 100644 index 0000000..ba1223e --- /dev/null +++ b/scripts/lint.sh @@ -0,0 +1,49 @@ +#!/bin/sh +# `make lint`: shellcheck, pinned in .tools/, over every tracked shell +# script โ€” `*.sh`, plus any tracked file whose first line is a sh or bash +# shebang. fish and PowerShell are not shellcheck's business. The list is +# computed, printed and NUL-safe: a tracked path here contains a space. + +set -eu + +cd "$(dirname "$0")/.." +SHELLCHECK=.tools/shellcheck + +tmp=$(mktemp "${TMPDIR:-/tmp}/dotfiles-lint.XXXXXX") +trap 'rm -f "$tmp"' EXIT INT TERM + +# Tracked files, one per line, spaces intact (paths here hold no newline). +git ls-files -z | tr '\0' '\n' > "$tmp" +total=$(grep -c . "$tmp") + +n=0 +list="" +while IFS= read -r f; do + [ -f "$f" ] || continue + case "$f" in + *.sh) ;; + *) + # The first 64 bytes, NUL stripped: a tracked binary is not a script. + first=$(LC_ALL=C head -c 64 "$f" | tr -d '\0' | head -n 1) + case "$first" in + '#!'*/sh|'#!'*/bash|'#!'*' sh'|'#!'*' bash'|'#!'*/sh' '*|'#!'*/bash' '*|'#!'*' sh '*|'#!'*' bash '*) ;; + *) continue ;; + esac ;; + esac + n=$((n + 1)) + echo " lint $f" >&2 + list="$list +$f" +done < "$tmp" + +if [ "$n" -eq 0 ]; then + echo "lint: no shell script found among $total tracked files" >&2 + exit 1 +fi + +# One shellcheck run over the list; a path with a space survives the loop. +printf '%s\n' "$list" | sed '/^$/d' | while IFS= read -r f; do + printf '%s\0' "$f" +done | xargs -0 "$SHELLCHECK" --external-sources + +echo " lint ok ($n scripts of $total tracked files)" >&2 diff --git a/scripts/render.sh b/scripts/render.sh new file mode 100644 index 0000000..88fe00d --- /dev/null +++ b/scripts/render.sh @@ -0,0 +1,69 @@ +#!/bin/sh +# `make render`: every template renders against the placeholder data in +# ci/chezmoi.toml, into a temporary destination, hermetically โ€” no key +# (encrypted files excluded), no network (externals excluded), no script run +# (scripts excluded). The two run_once_* templates are the ones that call +# `include` and `lookPath`, so they are rendered on their own with +# execute-template. chezmoi's state file goes into the same temporary +# directory: it otherwise lands beside the config file and dirties the tree. + +set -eu + +cd "$(dirname "$0")/.." +CHEZMOI=.tools/chezmoi +CONFIG=ci/chezmoi.toml + +dst=$(mktemp -d "${TMPDIR:-/tmp}/dotfiles-render.XXXXXX") +trap 'rm -rf "$dst"' EXIT INT TERM +state="$dst/state.boltdb" + +run() { + "$CHEZMOI" --config "$CONFIG" --persistent-state "$state" --source . "$@" +} + +run --destination "$dst" apply --exclude encrypted,externals,scripts + +# What must have come out. +for f in .zshrc .claude/CLAUDE.md .claude/skills/worktree-task/SKILL.md; do + if [ ! -s "$dst/$f" ]; then + echo "render: $f is missing or empty in the rendered home" >&2 + exit 1 + fi +done +# What must never reach a home directory (see .chezmoiignore). +for f in Makefile tools.env ci amont.conf scripts docs README.md; do + if [ -e "$dst/$f" ]; then + echo "render: $f was deployed; it belongs to the repository, not a home โ€” add it to .chezmoiignore" >&2 + exit 1 + fi +done + +# The run_once_* templates, one by one. Listed with a git pathspec: `*` +# matches across `/`, and the two live at different depths. NUL-safe, since +# a tracked path in this repository contains a space. +n=0 +git ls-files -z -- '*run_once_*.tmpl' | tr '\0' '\n' > "$dst/run_once.list" +while IFS= read -r f; do + [ -n "$f" ] || continue + n=$((n + 1)) + echo " render $f" >&2 + if ! run execute-template < "$f" > /dev/null; then + echo "render: $f does not render" >&2 + exit 1 + fi +done < "$dst/run_once.list" +if [ "$n" -lt 2 ]; then + echo "render: expected at least 2 run_once_* templates, found $n" >&2 + exit 1 +fi + +# The config template itself, as `chezmoi init` would render it, answering +# the one prompt that has no default. With the ci config, promptStringOnce +# reads placeholders, never a workstation's real data. +if ! run execute-template --init --promptString "bitwarden server url=x" < .chezmoi.toml.tmpl \ + | grep -q 'encryption = "gpg"'; then + echo "render: .chezmoi.toml.tmpl does not render to a config that sets encryption" >&2 + exit 1 +fi + +echo " render ok ($n run_once templates, config template, home applied to a temp dir)" >&2 diff --git a/scripts/tools.sh b/scripts/tools.sh new file mode 100644 index 0000000..813c3e7 --- /dev/null +++ b/scripts/tools.sh @@ -0,0 +1,95 @@ +#!/bin/sh +# `make tools`: the pinned tools into .tools/, from the release asset of the +# pinned tag, verified against the checksum in tools.env before extraction. +# Never PATH's copy: a workstation and a runner must run the same binary +# (toolchain.tools-pinned-with-it, toolchain.one-pin-read-by-both). +# +# Idempotent: a tool already present at its pinned version is kept, so a +# second run sends no request. + +set -eu + +cd "$(dirname "$0")/.." +# A plain KEY=value file, not a script: shellcheck is told not to follow it. +# shellcheck source=/dev/null +. ./tools.env + +TOOLS=.tools +DL="$TOOLS/dl" +mkdir -p "$DL" + +os=$(uname -s) +arch=$(uname -m) +case "$os/$arch" in + Linux/x86_64) chezmoi_asset=linux_amd64; shellcheck_asset=linux.x86_64 ;; + Darwin/x86_64) chezmoi_asset=darwin_amd64; shellcheck_asset=darwin.x86_64 ;; + Darwin/arm64) chezmoi_asset=darwin_arm64; shellcheck_asset=darwin.aarch64 ;; + *) + echo "tools: no pinned asset for $os/$arch (tools.env knows linux/x86_64, darwin/x86_64, darwin/arm64)" >&2 + exit 1 ;; +esac + +# The digest tool differs by platform; both print ` `. +if command -v shasum >/dev/null 2>&1; then + digest() { shasum -a 256 "$1" | cut -d' ' -f1; } +elif command -v sha256sum >/dev/null 2>&1; then + digest() { sha256sum "$1" | cut -d' ' -f1; } +else + echo "tools: neither shasum nor sha256sum is available" >&2 + exit 1 +fi + +# have : the tool is already there at the pinned version. +have() { + [ -x "$TOOLS/$1" ] && "$TOOLS/$1" --version 2>/dev/null | grep -qF "$2" +} + +# fetch +fetch() { + if ! curl -fsSL --retry 3 -o "$2" "$1"; then + echo "tools: could not download $1" >&2 + exit 1 + fi + got=$(digest "$2") + if [ "$got" != "$3" ]; then + echo "tools: checksum mismatch for $(basename "$2"): tools.env says $3, the download is $got" >&2 + rm -f "$2" + exit 1 + fi +} + +# expect : refuse a binary that is not the pin it claims. +expect() { + if ! "$TOOLS/$1" --version 2>/dev/null | grep -qF "$2"; then + echo "tools: $TOOLS/$1 does not report $2:" >&2 + "$TOOLS/$1" --version >&2 || true + exit 1 + fi + echo " tools $1 $2" >&2 +} + +# chezmoi: chezmoi___.tar.gz, binary at the archive root. +ver=${CHEZMOI#v} +if have chezmoi "$CHEZMOI"; then + echo " tools chezmoi $CHEZMOI (present)" >&2 +else + sum=$(eval "printf '%s' \"\${CHEZMOI_SHA256_$chezmoi_asset}\"") + tarball="$DL/chezmoi_${ver}_${chezmoi_asset}.tar.gz" + fetch "https://github.com/twpayne/chezmoi/releases/download/$CHEZMOI/chezmoi_${ver}_${chezmoi_asset}.tar.gz" "$tarball" "$sum" + tar -xzf "$tarball" -C "$TOOLS" chezmoi + expect chezmoi "$CHEZMOI" +fi + +# ShellCheck ships shellcheck-...tar.xz, binary under shellcheck-/. +if have shellcheck "${SHELLCHECK#v}"; then + echo " tools shellcheck $SHELLCHECK (present)" >&2 +else + key=$(printf '%s' "$shellcheck_asset" | tr . _) + sum=$(eval "printf '%s' \"\${SHELLCHECK_SHA256_$key}\"") + tarball="$DL/shellcheck-$SHELLCHECK.$shellcheck_asset.tar.xz" + fetch "https://github.com/koalaman/shellcheck/releases/download/$SHELLCHECK/shellcheck-$SHELLCHECK.$shellcheck_asset.tar.xz" "$tarball" "$sum" + tar -xJf "$tarball" -C "$DL" "shellcheck-$SHELLCHECK/shellcheck" + mv "$DL/shellcheck-$SHELLCHECK/shellcheck" "$TOOLS/shellcheck" + rmdir "$DL/shellcheck-$SHELLCHECK" + expect shellcheck "${SHELLCHECK#v}" +fi diff --git a/tools.env b/tools.env new file mode 100644 index 0000000..e8d8038 --- /dev/null +++ b/tools.env @@ -0,0 +1,19 @@ +# The only place a tool version is stated (toolchain.pinned-exactly-in-the-repository, +# toolchain.one-pin-read-by-both). `make tools` reads this file, downloads the +# release asset of the pinned tag into .tools/ and verifies it against the +# checksum beside it before extracting; nothing is ever taken from PATH. +# +# chezmoi's checksums come from its published chezmoi__checksums.txt. +# shellcheck publishes no checksums file, so its three were computed at pin +# time from the assets downloaded over HTTPS from the GitHub release: they pin +# what was reviewed, not what upstream attests. + +CHEZMOI=v2.72.1 +CHEZMOI_SHA256_linux_amd64=9f97d32caca166e5c92160ec3a9325519809c38963121cef38173142065c981f +CHEZMOI_SHA256_darwin_amd64=bf0f0e048291efe126cb8bc51cf566057b92755cd53ce82c45efa11d2f8f4898 +CHEZMOI_SHA256_darwin_arm64=938d422091cc001e68fe3fd7efea9b923a36facbf2b8db67063639abbaf72de2 + +SHELLCHECK=v0.11.0 +SHELLCHECK_SHA256_linux_x86_64=8c3be12b05d5c177a04c29e3c78ce89ac86f1595681cab149b65b97c4e227198 +SHELLCHECK_SHA256_darwin_x86_64=3c89db4edcab7cf1c27bff178882e0f6f27f7afdf54e859fa041fca10febe4c6 +SHELLCHECK_SHA256_darwin_aarch64=56affdd8de5527894dca6dc3d7e0a99a873b0f004d7aabc30ae407d3f48b0a79