diff --git a/.github/workflows/_lint.yml b/.github/workflows/_lint.yml index 151a509..44aadff 100644 --- a/.github/workflows/_lint.yml +++ b/.github/workflows/_lint.yml @@ -13,3 +13,24 @@ jobs: - name: Run hooks on all files run: npx prek run --all-files --show-diff-on-failure + # Workflow EXPRESSIONS are validated only by GitHub, and a bad one rejects the + # whole workflow before any job starts — no logs, no annotations, no failing step. + # That is how `|| "manual dispatch"` (expressions take single-quoted literals only) + # took the site's deploy down after it had passed YAML parsing, `bash -n`, and the + # workflow harness. actionlint catches exactly that class. + # + # Two errors are suppressed: actionlint's `job` context schema is stale and does not + # know `job.workflow_repository` / `job.workflow_sha`, both of which are real and + # documented, and which publish-gh-pages.yml depends on to version-match assemble.mjs. + - name: Lint workflow files + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + ver=$(gh api repos/rhysd/actionlint/releases/latest --jq .tag_name | tr -d v) + gh release download "v$ver" --repo rhysd/actionlint \ + -p "actionlint_${ver}_linux_amd64.tar.gz" -O "$RUNNER_TEMP/al.tgz" + tar xzf "$RUNNER_TEMP/al.tgz" --no-same-owner -C "$RUNNER_TEMP" actionlint + "$RUNNER_TEMP/actionlint" -color \ + -ignore 'property "workflow_(repository|sha)" is not defined' + diff --git a/.github/workflows/publish-gh-pages.yml b/.github/workflows/publish-gh-pages.yml index 68ce45a..f1d34bb 100644 --- a/.github/workflows/publish-gh-pages.yml +++ b/.github/workflows/publish-gh-pages.yml @@ -132,17 +132,27 @@ jobs: # BUILT_BRANCH vs github.ref is the one thing a post-mortem here always trips over, # so state both up front rather than leaving someone to infer it from a cache miss. + # + # Every context value comes through `env:`, never interpolated into the script. + # That keeps arbitrary values out of the shell, and it avoids nesting quotes: an + # expression's string literals must be SINGLE-quoted, which cannot be written + # inside a single-quoted shell word. - name: Create staging dirs + env: + EVENT: ${{ github.event_name }} + GH_REF: ${{ github.ref }} + GH_SHA: ${{ github.sha }} + TRIGGER_URL: ${{ github.event.workflow_run.html_url }} run: | set -euo pipefail mkdir -p "$RUNNER_TEMP/site" "$RUNNER_TEMP/gather" printf '%-16s %s\n' \ - 'event' '${{ github.event_name }}' \ + 'event' "$EVENT" \ 'built branch' "$BUILT_BRANCH" \ 'default' "$DEFAULT" \ - 'github.ref' '${{ github.ref }} (always the default branch under workflow_run)' \ - 'github.sha' '${{ github.sha }} (→ pages_build_version)' \ - 'triggered by' '${{ github.event.workflow_run.html_url || "manual dispatch" }}' + 'github.ref' "$GH_REF (always the default branch under workflow_run)" \ + 'github.sha' "$GH_SHA (→ pages_build_version)" \ + 'triggered by' "${TRIGGER_URL:-manual dispatch}" # The live Pages base URL, from the Pages API — respects a custom domain # (CNAME); falls back to the conventional github.io URL if the API call @@ -252,7 +262,7 @@ jobs: while IFS=$'\t' read -r tag dest created size assetid decision; do [ "$dest" = "-" ] && continue if [ -f "$failed/$assetid" ]; then - echo "::warning::release $tag: docs.zip download failed — skipping. $(cat "$failed/$assetid.err" 2>/dev/null | head -1)" + echo "::warning::release $tag: docs.zip download failed — skipping. $(head -1 "$failed/$assetid.err" 2>/dev/null)" elif [ -s "$relcache/$assetid.zip" ]; then cp "$relcache/$assetid.zip" "$RUNNER_TEMP/gather/$dest.zip" else @@ -523,7 +533,11 @@ jobs: set -euo pipefail stable_src=$(node "$GITHUB_WORKSPACE/.mvs/assemble/assemble.mjs" generate \ --site-dir "$RUNNER_TEMP/site" --base-url "$PAGES_URL" --required "$DEFAULT") - [ -n "$stable_src" ] && ln -s "$stable_src" "$RUNNER_TEMP/site/stable" || true + # An `&& ln … || true` one-liner here would also swallow a FAILED symlink, not + # just the empty-stable_src case — and a missing stable/ alias is silent damage. + if [ -n "$stable_src" ]; then + ln -s "$stable_src" "$RUNNER_TEMP/site/stable" + fi - name: Upload Pages artifact uses: actions/upload-pages-artifact@v5 diff --git a/CLAUDE.md b/CLAUDE.md index 740fc21..8c54a83 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -161,7 +161,8 @@ consumer's repo stays checked out at the root so `assemble.mjs`'s `git tag` list so a composite action could only be pinned to a literal tag — the `job` context is the only way to self-reference at the running ref.) Note: actionlint's `job`-context schema is stale and false-flags `job.workflow_sha`/`job.workflow_repository`; the -GitHub docs confirm both. This repo lints with biome, not actionlint. +GitHub docs confirm both, so `_lint.yml` runs actionlint with exactly those two +suppressed (`-ignore 'property "workflow_(repository|sha)" is not defined'`). ### BASE_URL must be set before `myst build` ```yaml @@ -256,7 +257,7 @@ PRs always, fork PRs only when the SHA carries that status. ```bash npm test # run the unit tests -npm run test:workflows # run publish.yml's gather steps against a mock gh (needs uv) +npm run test:workflows # gather steps against a mock gh + workflow shape asserts (needs uv) npm run docs # build docs (same command CI uses) npm run docs-dev # live-preview docs with the plugin loaded from local plugins/ ```