From cef5257e10c3b5a4d45a1b02212ef49df2387699 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sun, 23 Aug 2026 11:13:18 -0700 Subject: [PATCH 1/5] Adopt the Hub's Reusable validate-task.yml Replaces this repo's carried validate-task.yml with the hub-hosted reusable workflow (ptr727/ProjectTemplate#924, #950), pinned at the released 2.0.445 tag. The three callers (test-pull-request.yml, publish-release.yml, deploy-site.yml) now pass markdown-exclude-globs to keep the vendored PaperMod theme and the imported WordPress content archive out of the hub's markdownlint pass. The new .github/actions/validate/action.yml hook carries this repo's own domain checks the hub lint/unit-test jobs don't cover: config and environment-docs validation, and the Hugo build plus URL-parity gate. It no longer carries a shell-script step, since the hub's shell lint step now covers extensionless scripts by shebang and runs shfmt alongside shellcheck. --- .github/actions/validate/action.yml | 58 ++++++++++++ .github/workflows/deploy-site.yml | 8 +- .github/workflows/publish-release.yml | 22 +++-- .github/workflows/test-pull-request.yml | 14 ++- .github/workflows/validate-task.yml | 118 ------------------------ 5 files changed, 90 insertions(+), 130 deletions(-) create mode 100644 .github/actions/validate/action.yml delete mode 100644 .github/workflows/validate-task.yml diff --git a/.github/actions/validate/action.yml b/.github/actions/validate/action.yml new file mode 100644 index 0000000..2cd81c9 --- /dev/null +++ b/.github/actions/validate/action.yml @@ -0,0 +1,58 @@ +# Blog's own validate hook, run by the hub's validate-task.yml validate job. +# The fleet doc-lint block and the generic unit-test job in validate-task.yml do not cover this repo's own domain checks. +name: Validate repository (Blog) +description: Config/env-docs checks and the Hugo build/URL-parity gate. + +runs: + using: composite + steps: + + - name: Validate config step + shell: bash + run: | + set -Eeuo pipefail + for f in spec/*.json version.json .editorconfig-checker.json; do + jq empty "$f" + done + python3 -c 'import yaml,sys; yaml.safe_load(open("hugo.yaml"))' + + # Every configuration value is described once, in ENVIRONMENT.md. + # A new value gets added wherever its author is working, and nothing else notices a missing row. + # This check runs both directions: undocumented values, and rows describing nothing. + - name: Check environment docs step + shell: bash + run: python3 checks/check-env-docs.py + + # The hub's validate job checkout is shallow. + # The mtime restore below needs full history to date each file from its last commit. + # This hook deepens the checkout before the build. + - name: Deepen checkout step + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + + # The pin lives in the action, so validation and the deploy cannot install different generators. + - name: Install Hugo step + uses: ./.github/actions/install-hugo + + # The same action the deploy uses, so the two agree on who restores the mtimes. + - name: Restore file mtimes step + uses: chetan/git-restore-mtime-action@d186aca54f8760da4dec55313195e51ed3ebb0b3 # v2.3 + with: + args: static + + # This is the build and URL-contract gate, not an extra step beside one. + # The script runs `hugo --panicOnWarning` and check-url-parity itself, so a separate build would repeat both. + # --panicOnWarning is the real gate, and two PaperMod templates are overridden in layouts/ so it can stay on. + # Every local asset reference must resolve, and every legacy image URL from the WordPress import must too. + # + # Running the script here is also the point: it was linted and never run, so a broken caller contract reached a deploy. + # An empty scratch root exercises the deploy's path short of the transport, with no previous release for the hard-link guard. + # REQUIRE_BROTLI stays unset, since installing brotli would cost an apt round trip per pull request. + - name: Assemble release bundle step + shell: bash + env: + MTIME_RESTORED: '1' + run: | + set -Eeuo pipefail + deploy/make-release.sh "${RUNNER_TEMP}/validate-bundle" "$(date -u +%Y%m%d-%H%M%S)" diff --git a/.github/workflows/deploy-site.yml b/.github/workflows/deploy-site.yml index aa727d0..318a658 100644 --- a/.github/workflows/deploy-site.yml +++ b/.github/workflows/deploy-site.yml @@ -40,9 +40,15 @@ jobs: validate: name: Validate sources job needs: [ assert-ref ] - uses: ./.github/workflows/validate-task.yml + uses: ptr727/ProjectTemplate/.github/workflows/validate-task.yml@3927fec1e5d6b6b756d8ddbc6763c658a2804b91 # 2.0.445 permissions: contents: read + with: + # This repo vendors the PaperMod theme and carries an imported WordPress content archive, neither authored here. + # .markdownlint-cli2.jsonc is carried verbatim, so it cannot scope them locally. + markdown-exclude-globs: | + !content/** + !themes/*/** deploy: name: Deploy site job diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index b461ba4..ee967fc 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -23,20 +23,23 @@ jobs: actor: ${{ github.actor }} ref_name: ${{ github.ref_name }} - # The same reusable gate the PR runs - a dispatch cannot release a ref that fails validation. - # This repo's own validate-task.yml carries the Hugo build and URL-parity gate alongside the generic - # linters (see AGENTS.md/GOVERNANCE.md for why this repo has not yet adopted the hub-hosted validate-task.yml). + # The same reusable gate the pull request runs. + # A dispatch cannot release a ref that fails validation. + # No ref input to pin: the called workflow already checks out this run's dispatch-time github.sha by default. + # That is the same commit the publish job below builds. validate: name: Validate sources job needs: [plan] if: ${{ needs.plan.outputs.publish == 'true' }} - uses: ./.github/workflows/validate-task.yml - with: - # Pin the same dispatch-time commit publish below builds, so a push landing after dispatch - # cannot make validate and publish run against two different commits. - ref: ${{ github.sha }} + uses: ptr727/ProjectTemplate/.github/workflows/validate-task.yml@3927fec1e5d6b6b756d8ddbc6763c658a2804b91 # 2.0.445 permissions: contents: read + with: + # This repo vendors the PaperMod theme and carries an imported WordPress content archive, neither authored here. + # .markdownlint-cli2.jsonc is carried verbatim, so it cannot scope them locally. + markdown-exclude-globs: | + !content/** + !themes/*/** # Publish the dispatched branch, where main is a release and develop a prerelease. # NBGV computes the tag; the release is the tag plus the auto source archive, README, and LICENSE. @@ -50,7 +53,8 @@ jobs: contents: write actions: write with: - # Full history for NBGV; pin the dispatch-time commit - a push landing after dispatch must not release unvalidated. + # Full history for NBGV. + # Pin the dispatch-time commit, since a push landing after dispatch must not release unvalidated. ref: ${{ github.sha }} branch: ${{ github.ref_name }} smoke: false diff --git a/.github/workflows/test-pull-request.yml b/.github/workflows/test-pull-request.yml index 47b6cb0..bb33587 100644 --- a/.github/workflows/test-pull-request.yml +++ b/.github/workflows/test-pull-request.yml @@ -1,5 +1,8 @@ name: Test pull request action +# Thin caller: the gate is the hub's reusable validate-task.yml, which this repo reaches rather than carries. +# This is a release-model repo with no build target, so the trigger is pull_request only. +# It has no direct-commit allowance to keep advisory. on: pull_request: branches: [ main, develop ] @@ -9,14 +12,21 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +permissions: {} + jobs: - # The reusable validation gate (validate-task.yml) - the same job publish-release runs before a release. validate: name: Validate sources job - uses: ./.github/workflows/validate-task.yml + uses: ptr727/ProjectTemplate/.github/workflows/validate-task.yml@3927fec1e5d6b6b756d8ddbc6763c658a2804b91 # 2.0.445 permissions: contents: read + with: + # This repo vendors the PaperMod theme and carries an imported WordPress content archive, neither authored here. + # .markdownlint-cli2.jsonc is carried verbatim, so it cannot scope them locally. + markdown-exclude-globs: | + !content/** + !themes/*/** # GitHub Actions does not support required status checks on conditional jobs. # A single always-run aggregator therefore gates the merge. diff --git a/.github/workflows/validate-task.yml b/.github/workflows/validate-task.yml deleted file mode 100644 index e19b23c..0000000 --- a/.github/workflows/validate-task.yml +++ /dev/null @@ -1,118 +0,0 @@ -name: Validate task - -# The single validation gate, reused by test-pull-request (feeding the required check) and publish-release. - -on: - workflow_call: - inputs: - # Empty (the default) checks out github.sha, which is already the dispatch-time commit for - # every job in this run. A caller that publishes/deploys off a separately-pinned ref (a - # cross-repo reusable-workflow call, which gets its own checkout context) passes that same - # ref here explicitly, so what validate checks stays provably the same commit as what ships. - ref: - required: false - type: string - default: '' - -jobs: - - # Source-only repo: lint plus a site build gated on the URL contract, using the same configs the editor uses. - # There are no unit tests, because the thing under test is a set of URLs rather than a library. - lint: - name: Lint sources job - runs-on: ubuntu-latest - permissions: - contents: read - - steps: - - # Full history, because the mtime restore below has no commit to date a file from without it. - - name: Checkout code step - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ${{ inputs.ref || github.sha }} - fetch-depth: 0 - - # Doc linters run as pinned action wrappers. - # The editorconfig-checker action is install-only, so it runs via Docker instead. - # The markdown glob excludes the imported archive and the vendored theme trees. - # Neither is authored here, and .markdownlint-cli2.jsonc is carried verbatim so it cannot scope them. - # The theme exclusion reaches inside a theme directory rather than all of themes/, so a file we author about a vendored tree is still linted. - - name: Lint Markdown step - uses: DavidAnson/markdownlint-cli2-action@21c1be1b93ad9ed58fa840aacc3f279cde2a72ff # v24.2.0 - with: - globs: | - **/*.md - !content/** - !themes/*/** - !public/** - - # The cspell gate covers README + HISTORY only. - # Gating all *.md would mean endlessly padding cspell.json for technical terms. - # Broad live spell-check is the editor extension's job. - # See CODESTYLE.md "Markdown and Spelling". - - name: Spell check step - uses: streetsidesoftware/cspell-action@e0668cf020899e887ee8ad4d173c31738a79eae8 # v9.0.1 - with: - files: | - README.md - HISTORY.md - incremental_files_only: false - - - name: Lint workflows step - uses: raven-actions/actionlint@3d39aea434753780c3b3d4a1a31c854b4dbf49d7 # v2.2.0 - - - name: Check EditorConfig step - run: docker run --rm --pull=always -v "$PWD":/check --workdir /check mstruebing/editorconfig-checker:latest - - # The shell clean-compile is shellcheck at default severity plus `shfmt -d`, both reporting nothing. - # The formatter reads .editorconfig, which pins the .sh files to tabs. - # ops/vps-backup-pull carries no extension, so it takes the [*] default of four spaces instead. - # Use `-d` rather than `-w` here and locally: the container writes as root and would take ownership of the tree. - - name: Lint shell scripts step - run: | - set -Eeuo pipefail - scripts=(checks/check-live-urls.sh deploy/make-release.sh ops/vps-backup-pull ops/install.sh capture/run-wp2hugo.sh) - docker run --rm --pull=always -v "$PWD":/mnt --workdir /mnt \ - koalaman/shellcheck:stable "${scripts[@]}" - docker run --rm --pull=always -v "$PWD":/mnt --workdir /mnt \ - mvdan/shfmt:latest -d "${scripts[@]}" - - - name: Validate config step - run: | - set -Eeuo pipefail - for f in spec/*.json version.json .editorconfig-checker.json; do - jq empty "$f" - done - python3 -c 'import yaml,sys; yaml.safe_load(open("hugo.yaml"))' - - # Every configuration value is described once, in ENVIRONMENT.md. - # A new value gets added wherever its author is working, and nothing else notices a - # missing row. Runs both directions: undocumented values, and rows describing nothing. - - name: Check environment docs step - run: python3 checks/check-env-docs.py - - # The pin lives in the action, so validation and the deploy cannot install different generators. - - name: Install Hugo step - uses: ./.github/actions/install-hugo - - # The same action the deploy uses, so the two agree on who restores the mtimes. - - name: Restore file mtimes step - uses: chetan/git-restore-mtime-action@d186aca54f8760da4dec55313195e51ed3ebb0b3 # v2.3 - with: - args: static - - # This is the build and URL-contract gate, not an extra step beside one. - # The script runs `hugo --panicOnWarning` and check-url-parity itself, so a separate build would repeat both. - # --panicOnWarning is the real gate, and two PaperMod templates are overridden in layouts/ so it can stay on. - # 328 URLs must render, 778 legacy image URLs must resolve, and every local asset reference must exist. - # - # Running the script here is also the point: it was linted and never run, so a broken caller contract reached a deploy. - # An empty scratch root exercises the deploy's path short of the transport, with no previous release for the hard-link guard. - # REQUIRE_BROTLI stays unset, since installing brotli would cost an apt round trip per pull request. - - name: Assemble release bundle step - env: - MTIME_RESTORED: '1' - run: | - set -Eeuo pipefail - deploy/make-release.sh "${RUNNER_TEMP}/validate-bundle" "$(date -u +%Y%m%d-%H%M%S)" From 51d01f2121a24767fcd92e678180749a650583f8 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sun, 23 Aug 2026 11:32:00 -0700 Subject: [PATCH 2/5] Pin the Deepen-Checkout Step to github.sha The re-checkout had no explicit ref, so it could resolve to a different commit than the rest of the job validated (notably on a workflow_dispatch run, where the ref can move after dispatch). Pinning ref: ${{ github.sha }} matches the commit every other step in the job already runs against. --- .github/actions/validate/action.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/actions/validate/action.yml b/.github/actions/validate/action.yml index 2cd81c9..6897c68 100644 --- a/.github/actions/validate/action.yml +++ b/.github/actions/validate/action.yml @@ -29,6 +29,7 @@ runs: - name: Deepen checkout step uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + ref: ${{ github.sha }} fetch-depth: 0 # The pin lives in the action, so validation and the deploy cannot install different generators. From edc3d69cc935d6a062c775b39d6bc98172bde48d Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sun, 23 Aug 2026 14:20:31 -0700 Subject: [PATCH 3/5] Exclude the Vendored Theme from the Repo-Gate sha-pin Scan Re-pins the three validate-task.yml callers to the released 2.0.448 tag (ptr727/ProjectTemplate#957, fixed by hub PR #958) and sets the new repo-gate-exclude-globs input to themes/*/**. The hub's validate-task.yml lint job runs a repo-gate step that scans every tracked workflow file for SHA-pinned actions. Blog's old carried copy never had this step, so adopting the hub's version was what surfaced it failing on themes/PaperMod's own vendored CI workflows, which pin actions by floating tag and stay byte-identical to upstream per themes/README.md's documented invariant. Verified locally: repo_gate.py --check sha-pin --exclude 'themes/*/**' reports 0 issues against this tree. --- .github/workflows/deploy-site.yml | 6 +++++- .github/workflows/publish-release.yml | 6 +++++- .github/workflows/test-pull-request.yml | 6 +++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy-site.yml b/.github/workflows/deploy-site.yml index 318a658..9d902ee 100644 --- a/.github/workflows/deploy-site.yml +++ b/.github/workflows/deploy-site.yml @@ -40,7 +40,7 @@ jobs: validate: name: Validate sources job needs: [ assert-ref ] - uses: ptr727/ProjectTemplate/.github/workflows/validate-task.yml@3927fec1e5d6b6b756d8ddbc6763c658a2804b91 # 2.0.445 + uses: ptr727/ProjectTemplate/.github/workflows/validate-task.yml@cd7bae9ea830d2e42d1f7bb6e45b3e7f631c0668 # 2.0.448 permissions: contents: read with: @@ -49,6 +49,10 @@ jobs: markdown-exclude-globs: | !content/** !themes/*/** + # The vendored theme stays byte-identical to upstream. + # Its own floating-tag action pins are never locally edited to satisfy this repo's sha-pin gate. + repo-gate-exclude-globs: | + themes/*/** deploy: name: Deploy site job diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index ee967fc..1b321fe 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -31,7 +31,7 @@ jobs: name: Validate sources job needs: [plan] if: ${{ needs.plan.outputs.publish == 'true' }} - uses: ptr727/ProjectTemplate/.github/workflows/validate-task.yml@3927fec1e5d6b6b756d8ddbc6763c658a2804b91 # 2.0.445 + uses: ptr727/ProjectTemplate/.github/workflows/validate-task.yml@cd7bae9ea830d2e42d1f7bb6e45b3e7f631c0668 # 2.0.448 permissions: contents: read with: @@ -40,6 +40,10 @@ jobs: markdown-exclude-globs: | !content/** !themes/*/** + # The vendored theme stays byte-identical to upstream. + # Its own floating-tag action pins are never locally edited to satisfy this repo's sha-pin gate. + repo-gate-exclude-globs: | + themes/*/** # Publish the dispatched branch, where main is a release and develop a prerelease. # NBGV computes the tag; the release is the tag plus the auto source archive, README, and LICENSE. diff --git a/.github/workflows/test-pull-request.yml b/.github/workflows/test-pull-request.yml index bb33587..55deb8b 100644 --- a/.github/workflows/test-pull-request.yml +++ b/.github/workflows/test-pull-request.yml @@ -18,7 +18,7 @@ jobs: validate: name: Validate sources job - uses: ptr727/ProjectTemplate/.github/workflows/validate-task.yml@3927fec1e5d6b6b756d8ddbc6763c658a2804b91 # 2.0.445 + uses: ptr727/ProjectTemplate/.github/workflows/validate-task.yml@cd7bae9ea830d2e42d1f7bb6e45b3e7f631c0668 # 2.0.448 permissions: contents: read with: @@ -27,6 +27,10 @@ jobs: markdown-exclude-globs: | !content/** !themes/*/** + # The vendored theme stays byte-identical to upstream. + # Its own floating-tag action pins are never locally edited to satisfy this repo's sha-pin gate. + repo-gate-exclude-globs: | + themes/*/** # GitHub Actions does not support required status checks on conditional jobs. # A single always-run aggregator therefore gates the merge. From 41d46f70d2d539c6fc4b7badcf9685d9e895d29f Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sun, 23 Aug 2026 14:21:38 -0700 Subject: [PATCH 4/5] Fix Stale Trigger Comment in test-pull-request.yml The header comment claimed pull_request was the only trigger, but workflow_dispatch was right below it in the same on: block. --- .github/workflows/test-pull-request.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-pull-request.yml b/.github/workflows/test-pull-request.yml index 55deb8b..0c3ccda 100644 --- a/.github/workflows/test-pull-request.yml +++ b/.github/workflows/test-pull-request.yml @@ -1,8 +1,9 @@ name: Test pull request action # Thin caller: the gate is the hub's reusable validate-task.yml, which this repo reaches rather than carries. -# This is a release-model repo with no build target, so the trigger is pull_request only. +# This is a release-model repo with no build target, so the automatic trigger is pull_request only. # It has no direct-commit allowance to keep advisory. +# A manual run stays available through workflow_dispatch. on: pull_request: branches: [ main, develop ] From 1c9d3f92cdfda09c9137edf6192078f5624951f3 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sun, 23 Aug 2026 14:31:59 -0700 Subject: [PATCH 5/5] Fix Overclaiming Exclusion Comment The comment said content/ was 'neither authored here', but it is this repo's whole Hugo content tree, which also holds newly-authored posts alongside the imported WordPress archive, not only imported content. --- .github/workflows/deploy-site.yml | 2 +- .github/workflows/publish-release.yml | 2 +- .github/workflows/test-pull-request.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy-site.yml b/.github/workflows/deploy-site.yml index 9d902ee..2fb1f9f 100644 --- a/.github/workflows/deploy-site.yml +++ b/.github/workflows/deploy-site.yml @@ -44,7 +44,7 @@ jobs: permissions: contents: read with: - # This repo vendors the PaperMod theme and carries an imported WordPress content archive, neither authored here. + # This repo's whole Hugo content tree (content/) and the vendored PaperMod theme are both excluded here. # .markdownlint-cli2.jsonc is carried verbatim, so it cannot scope them locally. markdown-exclude-globs: | !content/** diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 1b321fe..ad4fa90 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -35,7 +35,7 @@ jobs: permissions: contents: read with: - # This repo vendors the PaperMod theme and carries an imported WordPress content archive, neither authored here. + # This repo's whole Hugo content tree (content/) and the vendored PaperMod theme are both excluded here. # .markdownlint-cli2.jsonc is carried verbatim, so it cannot scope them locally. markdown-exclude-globs: | !content/** diff --git a/.github/workflows/test-pull-request.yml b/.github/workflows/test-pull-request.yml index 0c3ccda..f8e49a5 100644 --- a/.github/workflows/test-pull-request.yml +++ b/.github/workflows/test-pull-request.yml @@ -23,7 +23,7 @@ jobs: permissions: contents: read with: - # This repo vendors the PaperMod theme and carries an imported WordPress content archive, neither authored here. + # This repo's whole Hugo content tree (content/) and the vendored PaperMod theme are both excluded here. # .markdownlint-cli2.jsonc is carried verbatim, so it cannot scope them locally. markdown-exclude-globs: | !content/**