diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 098c22e..953a837 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,38 @@ jobs: uses: gtbuchanan/tooling/.github/workflows/pre-commit.yml@main with: use-pnpm: true + + # Render every template natively on each hosted OS. The render-templates hook + # self-selects via `chezmoi ignored`, so each runner checks only the templates + # deployed on its own OS — real coverage of each platform's own branches, + # which the single-OS pre-commit job above cannot give. android has no hosted + # runner and is covered by render-templates-android instead. + render-templates: + name: Render Templates (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - uses: gtbuchanan/tooling/.github/actions/mise-setup@main + - run: mise run test:templates + strategy: + fail-fast: false + matrix: + os: [macos-latest, ubuntu-latest, windows-latest] + + # android has no hosted runner, so render its templates in a Termux container + # (native x86_64 image on the x64 runner — no QEMU). Same test:templates engine + # as every other leg, only with the android ignore set active. + render-templates-android: + name: Render Templates (android) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - uses: gtbuchanan/tooling/.github/actions/mise-setup@main + - run: mise run test:templates-android name: CI on: diff --git a/AGENTS.md b/AGENTS.md index 44d8897..ac97b70 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -175,6 +175,21 @@ Scripts under `.chezmoiscripts/` are platform-gated via `.chezmoiignore`. Each p directory is excluded on non-matching OSes. Use the template variables above for finer-grained conditionals within a script (e.g., WSL vs. native Linux, personal vs. ewn). +## Template Render Lint + +`scripts/lint-templates.sh` (the hk `render-templates` step) proves every +`*.tmpl` renders, skipping ones chezmoi ignores on the current OS/hosttype (via +`chezmoi ignored`). CI runs it natively per-OS plus, since android has no hosted +runner, in a Termux container (`mise run test:templates-android`); see +`.github/workflows/ci.yml`. + +`chezmoi ignored` realizes every `.chezmoiexternal` entry (downloading archives, +SSH-cloning repos) just to enumerate targets, which breaks the offline check on +fresh CI and in the container. chezmoi has no flag to suppress this, so +`home/.chezmoiexternal.yaml.tmpl` carries a `{{ if not (get . "lintSkipExternals") }}` +guard that the lint trips via `--override-data` on that one call. **Do not +remove it** — the rationale is in both files' comments. + ## Formatting - UTF-8, 2-space indent, LF line endings, final newline, trim trailing whitespace. diff --git a/hk.pkl b/hk.pkl index f8a5d6d..5a37fba 100644 --- a/hk.pkl +++ b/hk.pkl @@ -35,9 +35,13 @@ local allSteps = (baseSteps) { check = "gtb verify mise" } + // Delegates to the test:templates mise task (the single render entrypoint the + // CI legs also call) rather than invoking lint-templates.sh directly, so the + // hook and CI render through one definition. The task self-selects via + // `chezmoi ignored`, checking only this OS's templates. ["render-templates"] { glob = List("**/*.tmpl") - check = "sh scripts/lint-templates.sh {{files}}" + check = "mise run test:templates -- {{files}}" } // PSScriptAnalyzer over PowerShell sources. The `**/*.ps1` glob leaves diff --git a/home/.chezmoiexternal.yaml.tmpl b/home/.chezmoiexternal.yaml.tmpl index 0908549..4668dbc 100644 --- a/home/.chezmoiexternal.yaml.tmpl +++ b/home/.chezmoiexternal.yaml.tmpl @@ -1,4 +1,10 @@ --- +{{- /* lintSkipExternals is set true only by scripts/lint-templates.sh (via + --override-data) to render this file empty, so `chezmoi ignored` need not + realize these externals — downloading archives, SSH-cloning repos — merely to + compute the template ignore set. `get` reads it without erroring when unset + (chezmoi runs templates with missingkey=error). Normal apply never sets it. */ -}} +{{- if not (get . "lintSkipExternals") }} {{- if eq .hosttype "ewn" }} '.agents/skills/atlassian-cli': exact: true @@ -114,3 +120,4 @@ type: git-repo url: git@gist.github.com:1a9699e8350d2eacc73de902c29d8ea0.git {{- end }} +{{- end }} diff --git a/mise-tasks/test/templates b/mise-tasks/test/templates new file mode 100755 index 0000000..1d07824 --- /dev/null +++ b/mise-tasks/test/templates @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +#MISE description="Render source templates for the current platform" +# +# The single entrypoint for template rendering: the hk render-templates hook and +# the CI legs all call this. It renders each template through chezmoi for the +# host OS (scripts/lint-templates.sh), so it self-selects via `chezmoi ignored` +# — a Windows runner checks the Windows templates, a Termux host the android +# ones, etc. Given file arguments it renders just those (the hook passes changed +# files); with none it renders every source template (CI). +set -euo pipefail + +# Run from the repo root regardless of caller cwd, and without git: the android +# leg renders inside a container where the worktree's .git is an unmounted host +# path, and only cwd-relative paths below are portable there. $0 is this task +# file (…/mise-tasks/test/templates), so two levels up is the root. +cd "$(dirname "$0")/../.." + +if [ "$#" -gt 0 ]; then + sh scripts/lint-templates.sh "$@" +else + # Every chezmoi source template lives under the chezmoi root (home/). `-exec … + # +` batches them into one invocation and propagates a non-zero render exit; + # portable where bash arrays / `mapfile` (bash 3.2 on macOS) are not. + find home -type f -name '*.tmpl' -exec sh scripts/lint-templates.sh {} + +fi diff --git a/mise-tasks/test/templates-android b/mise-tasks/test/templates-android new file mode 100755 index 0000000..58016c6 --- /dev/null +++ b/mise-tasks/test/templates-android @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +#MISE description="Render templates in a Termux (android) environment" +# +# android has no hosted CI runner and chezmoi's `.chezmoi.os` comes from GOOS +# (unfakeable), so the android templates can only be rendered by a real Termux +# userland. On a Termux device that's native; elsewhere we borrow one from +# termux/termux-docker. Either way the render itself is the shared test:templates +# engine, so this leg checks exactly what every other platform's leg does. +set -euo pipefail + +if [ "$(uname -o 2>/dev/null)" = "Android" ]; then + exec mise run test:templates +else + exec bash scripts/docker-run.sh scripts/render-templates-termux.sh +fi diff --git a/mise.toml b/mise.toml index 82b61a0..66459d9 100644 --- a/mise.toml +++ b/mise.toml @@ -35,9 +35,10 @@ minimum_release_age = "3d" minimum_release_age_excludes = ["npm:@gtbuchanan/*"] # mise.tasks.toml is generated by `gtb sync mise`; this include is the one -# manual hookup (the verify-mise hk step checks it stays wired). +# manual hookup (the verify-mise hk step checks it stays wired). mise-tasks/ +# holds hand-authored file tasks (e.g. test:templates) alongside it. [task_config] -includes = ["mise.tasks.toml"] +includes = ["mise-tasks", "mise.tasks.toml"] [tools] actionlint = "1.7.12" diff --git a/scripts/docker-run.sh b/scripts/docker-run.sh new file mode 100755 index 0000000..170f513 --- /dev/null +++ b/scripts/docker-run.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# +# Run a Termux-side script inside termux/termux-docker on a non-Termux host — +# the dev-host / CI entrypoint the `test:templates-android` mise task dispatches +# to (on a Termux device that task runs natively). The image arch is matched to +# the host so it runs natively — no QEMU — on both x86_64 CI runners and Apple +# Silicon. chezmoi reports `.chezmoi.os == android` from any Termux arch, so +# unlike claude-code-termux (which forces aarch64 for its .deb) we never need a +# fixed arch or binfmt emulation. Requires Docker (Docker Desktop on +# Windows/macOS, or dockerd). +# +# scripts/docker-run.sh +set -euo pipefail + +if [ "$#" -eq 0 ]; then + echo "usage: scripts/docker-run.sh