diff --git a/.claude/commands/sync-template.md b/.claude/commands/sync-template.md index 66c7230..2022c8f 100644 --- a/.claude/commands/sync-template.md +++ b/.claude/commands/sync-template.md @@ -5,7 +5,8 @@ allowed-tools: Read, Edit, Bash, Agent Update this project's agent files with the latest CORE sections from the template repository. -1. If `scripts/sync-from-template.sh` exists, run it. Otherwise tell Tim that the sync script arrives in Stage 6 of the build, and stop. -2. The sync replaces only the content between the `` and `` markers in each agent file. It never touches the PROJECT OVERLAY section, so project-specific customisations are safe. -3. Show Tim a clear summary of what changed in each agent file. -4. Opening a pull request for the sync is a GitHub action: pre-approve it in the brief, or get Tim's approval at the time. +1. Always do the sync on one stable branch: `chore/template-sync`. Never name the branch after the template version, and never invent a new name per run. If the branch already exists, check it out and reuse it; if not, create it. One stable branch means each repository holds at most one sync branch at a time, and a new sync updates the existing pull request instead of leaving a trail of version-named branches behind. This is a deliberate anti-clutter rule; see `docs/release-process.md` under "Post-merge branch cleanup" for why. +2. If `scripts/sync-from-template.sh` exists, run it. Otherwise tell Tim that the sync script arrives in Stage 6 of the build, and stop. +3. The sync replaces only the content between the `` and `` markers in each agent file. It never touches the PROJECT OVERLAY section, so project-specific customisations are safe. +4. Show Tim a clear summary of what changed in each agent file. +5. Open or update the pull request from `chore/template-sync`. If an open sync pull request already exists, push to its branch so it updates in place rather than opening a second one. Opening a pull request is a GitHub action: pre-approve it in the brief, or get Tim's approval at the time. diff --git a/.claude/template-version b/.claude/template-version index f8e233b..77fee73 100644 --- a/.claude/template-version +++ b/.claude/template-version @@ -1 +1 @@ -1.9.0 +1.9.3 diff --git a/.github/accessibility-tools/package-lock.json b/.github/accessibility-tools/package-lock.json index 07f680a..7097e70 100644 --- a/.github/accessibility-tools/package-lock.json +++ b/.github/accessibility-tools/package-lock.json @@ -448,9 +448,9 @@ } }, "node_modules/axios": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz", - "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==", + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", + "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", "dev": true, "license": "MIT", "dependencies": { diff --git a/.github/lint-tools/package-lock.json b/.github/lint-tools/package-lock.json index 68ea5fb..4fe07f9 100644 --- a/.github/lint-tools/package-lock.json +++ b/.github/lint-tools/package-lock.json @@ -581,9 +581,9 @@ } }, "node_modules/fast-uri": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", - "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", + "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", "dev": true, "funding": [ { diff --git a/.github/workflows/accessibility.yml b/.github/workflows/accessibility.yml index 3b258cc..af1fc21 100644 --- a/.github/workflows/accessibility.yml +++ b/.github/workflows/accessibility.yml @@ -79,6 +79,17 @@ # check. This is a genuine "not applicable," not a deferred check. First # introduced as a project-local fix in Lire RSS Hermes # (docs/decisions/001-python-tool-ci-archetype.md in that project). +# +# Archetype-aware skip for wordpress-theme. A project whose +# .github/ci-archetype declares wordpress-theme is a classic WordPress +# theme: its pages are PHP templates rendered by a live WordPress +# instance, not static HTML this job can serve directly. There is no +# lightweight way to stand up a full WordPress + database install in +# this job, so every scan step is skipped, with its own log-only +# explanation. Like python-tool, this is a genuine "not applicable" for +# now, not a deferred check; a future wp-env-based job would be the +# real fix. First introduced as a project-local fix in Tim Dixon +# WordPress Theme and LLBS-Website. name: Accessibility on: @@ -124,9 +135,14 @@ jobs: else echo "is_python_tool=false" >> "$GITHUB_OUTPUT" fi + if [ "$value" = "wordpress-theme" ]; then + echo "is_wordpress_theme=true" >> "$GITHUB_OUTPUT" + else + echo "is_wordpress_theme=false" >> "$GITHUB_OUTPUT" + fi - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - if: steps.archetype.outputs.is_browser_extension != 'true' && steps.archetype.outputs.is_python_tool != 'true' + if: steps.archetype.outputs.is_browser_extension != 'true' && steps.archetype.outputs.is_python_tool != 'true' && steps.archetype.outputs.is_wordpress_theme != 'true' with: node-version: lts/* cache: npm @@ -135,17 +151,17 @@ jobs: - name: Install accessibility tools # Tools are managed in .github/accessibility-tools/package.json so # Dependabot can track and propose version bumps automatically. - if: steps.archetype.outputs.is_browser_extension != 'true' && steps.archetype.outputs.is_python_tool != 'true' + if: steps.archetype.outputs.is_browser_extension != 'true' && steps.archetype.outputs.is_python_tool != 'true' && steps.archetype.outputs.is_wordpress_theme != 'true' run: npm ci working-directory: .github/accessibility-tools - name: Add accessibility tools to PATH - if: steps.archetype.outputs.is_browser_extension != 'true' && steps.archetype.outputs.is_python_tool != 'true' + if: steps.archetype.outputs.is_browser_extension != 'true' && steps.archetype.outputs.is_python_tool != 'true' && steps.archetype.outputs.is_wordpress_theme != 'true' run: echo "$GITHUB_WORKSPACE/.github/accessibility-tools/node_modules/.bin" >> "$GITHUB_PATH" - name: Set up Chrome and ChromeDriver id: chrome - if: steps.archetype.outputs.is_browser_extension != 'true' && steps.archetype.outputs.is_python_tool != 'true' + if: steps.archetype.outputs.is_browser_extension != 'true' && steps.archetype.outputs.is_python_tool != 'true' && steps.archetype.outputs.is_wordpress_theme != 'true' uses: browser-actions/setup-chrome@2e1d749697dd1612b833dba4a722266286fbefcd # v2.1.2 with: install-chromedriver: true @@ -155,29 +171,29 @@ jobs: # the repository root). A build is required before serving because # a bundler's index.html loads ES module imports that only resolve # once built. - if: steps.archetype.outputs.is_browser_extension != 'true' && steps.archetype.outputs.is_python_tool != 'true' && hashFiles('package.json') != '' + if: steps.archetype.outputs.is_browser_extension != 'true' && steps.archetype.outputs.is_python_tool != 'true' && steps.archetype.outputs.is_wordpress_theme != 'true' && hashFiles('package.json') != '' run: npm ci - name: Build the project (npm) # --if-present no-ops when the project has no "build" script. - if: steps.archetype.outputs.is_browser_extension != 'true' && steps.archetype.outputs.is_python_tool != 'true' && hashFiles('package.json') != '' + if: steps.archetype.outputs.is_browser_extension != 'true' && steps.archetype.outputs.is_python_tool != 'true' && steps.archetype.outputs.is_wordpress_theme != 'true' && hashFiles('package.json') != '' run: npm run build --if-present - name: Serve the built site in the background (static-app / browser-extension, build present) - if: steps.archetype.outputs.is_browser_extension != 'true' && steps.archetype.outputs.is_python_tool != 'true' && hashFiles('package.json') != '' && hashFiles('dist/**') != '' + if: steps.archetype.outputs.is_browser_extension != 'true' && steps.archetype.outputs.is_python_tool != 'true' && steps.archetype.outputs.is_wordpress_theme != 'true' && hashFiles('package.json') != '' && hashFiles('dist/**') != '' run: python3 -m http.server 8080 --directory dist & - name: Serve the repository root in the background (static-content, or a bundler-free package.json project with no dist/ output) - if: steps.archetype.outputs.is_browser_extension != 'true' && steps.archetype.outputs.is_python_tool != 'true' && hashFiles('dist/**') == '' + if: steps.archetype.outputs.is_browser_extension != 'true' && steps.archetype.outputs.is_python_tool != 'true' && steps.archetype.outputs.is_wordpress_theme != 'true' && hashFiles('dist/**') == '' run: python3 -m http.server 8080 & - name: Wait for the server to be ready - if: steps.archetype.outputs.is_browser_extension != 'true' && steps.archetype.outputs.is_python_tool != 'true' + if: steps.archetype.outputs.is_browser_extension != 'true' && steps.archetype.outputs.is_python_tool != 'true' && steps.archetype.outputs.is_wordpress_theme != 'true' run: wait-on http://localhost:8080 --timeout 15000 - name: Pa11y at WCAG 2.2 AAA # Adapt the URL to match the project's entry point. - if: steps.archetype.outputs.is_browser_extension != 'true' && steps.archetype.outputs.is_python_tool != 'true' + if: steps.archetype.outputs.is_browser_extension != 'true' && steps.archetype.outputs.is_python_tool != 'true' && steps.archetype.outputs.is_wordpress_theme != 'true' env: PUPPETEER_EXECUTABLE_PATH: ${{ steps.chrome.outputs.chrome-path }} run: pa11y --standard WCAG2AAA http://localhost:8080/index.html @@ -199,7 +215,7 @@ jobs: # meet 7:1 (identical-commit rerun on SWOT Builder went from 9 # violations to 0). --load-delay is a settle delay only; it does not # change which rules run or weaken --exit. - if: steps.archetype.outputs.is_browser_extension != 'true' && steps.archetype.outputs.is_python_tool != 'true' + if: steps.archetype.outputs.is_browser_extension != 'true' && steps.archetype.outputs.is_python_tool != 'true' && steps.archetype.outputs.is_wordpress_theme != 'true' env: CHROMEDRIVER_PATH: ${{ steps.chrome.outputs.chromedriver-path }} CHROME_PATH: ${{ steps.chrome.outputs.chrome-path }} @@ -229,3 +245,16 @@ jobs: echo "browser-extension, there is no alternate accessibility gate" echo "to point to here; this is a genuine \"not applicable\", not" echo "a deferred check." + + - name: Note that this project has no lightweight rendered surface to scan (wordpress-theme) + if: steps.archetype.outputs.is_wordpress_theme == 'true' + run: | + echo "This project's .github/ci-archetype declares wordpress-theme." + echo "Its pages are PHP templates rendered by a live WordPress" + echo "instance, not static HTML this job can serve directly, so" + echo "this job is a deliberate no-op for this archetype, not a" + echo "weakened check. There is no alternate automated accessibility" + echo "gate here yet; this is a genuine \"not applicable\" for now," + echo "not a deferred check. AAA conformance for this project is" + echo "verified by Carol's manual test pass against the live or" + echo "staged site." diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 124627d..9b3a38d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,11 +17,11 @@ # code-type project that has neither manifest — a hand-written static site # with no package.json and no build step. Its contents (trimmed of # whitespace) must be one of: static-content, static-app, browser-extension, -# php, python-tool. Only "static-content" and "python-tool" excuse a project -# from the no-manifest failure below; the other three values exist so the -# file is a complete, self-documenting record of the project's archetype -# even where a manifest already tells CI what to do. See -# jacob-ci-archetypes.md section 2b. +# php, python-tool, wordpress-theme. Only "static-content", "python-tool", +# and "wordpress-theme" excuse a project from the no-manifest failure below; +# the other values exist so the file is a complete, self-documenting record +# of the project's archetype even where a manifest already tells CI what to +# do. See jacob-ci-archetypes.md section 2b. # A project with neither a manifest nor an excusing declaration fails the # job, because a code-type project is expected to have one or the other. # @@ -32,6 +32,15 @@ # project-local fix in Lire RSS Hermes # (docs/decisions/001-python-tool-ci-archetype.md in that project) and # upstreamed here. +# +# wordpress-theme: a classic WordPress theme with PHP templates, no +# composer.json (no Composer dependencies), and often no package.json +# either (or one used purely for lint tooling, with no build or test +# script). There is no lightweight way to run a real WordPress instance in +# this job, so this archetype has no build-equivalent check here beyond the +# manifest excuse; accessibility.yml separately gives it an honest no-op +# for the same reason (no live render available in CI). First introduced as +# a project-local fix in Tim Dixon WordPress Theme and LLBS-Website. name: CI on: @@ -126,12 +135,12 @@ jobs: # instance via wp-env and belong in a dedicated job once a WordPress # project reaches that point. See docs/stacks/wordpress.md. - - name: Check for a static-content or python-tool archetype declaration + - name: Check for a static-content, python-tool, or wordpress-theme archetype declaration # Reads .github/ci-archetype, trimmed of surrounding whitespace and - # newlines, and records whether it declares "static-content" or - # "python-tool". Runs unconditionally (cheap) so the fail-check - # below can read the result regardless of which manifests are - # present. + # newlines, and records whether it declares "static-content", + # "python-tool", or "wordpress-theme". Runs unconditionally (cheap) + # so the fail-check below can read the result regardless of which + # manifests are present. id: archetype run: | if [ -f .github/ci-archetype ]; then @@ -149,19 +158,25 @@ jobs: else echo "is_python_tool=false" >> "$GITHUB_OUTPUT" fi + if [ "$value" = "wordpress-theme" ]; then + echo "is_wordpress_theme=true" >> "$GITHUB_OUTPUT" + else + echo "is_wordpress_theme=false" >> "$GITHUB_OUTPUT" + fi - name: Fail if no manifest or excusing declaration was found if: | hashFiles('package.json') == '' && hashFiles('composer.json') == '' && steps.archetype.outputs.is_static_content != 'true' && - steps.archetype.outputs.is_python_tool != 'true' + steps.archetype.outputs.is_python_tool != 'true' && + steps.archetype.outputs.is_wordpress_theme != 'true' run: | echo "No package.json or composer.json found at the repository root," - echo "and .github/ci-archetype does not declare \"static-content\"" - echo "or \"python-tool\". This is a code-type project, so one of" - echo "these is expected. See the comments at the top of this" - echo "workflow for what each manifest or declaration means." + echo "and .github/ci-archetype does not declare \"static-content\"," + echo "\"python-tool\", or \"wordpress-theme\". This is a code-type" + echo "project, so one of these is expected. See the comments at the" + echo "top of this workflow for what each manifest or declaration means." exit 1 # --- Python command-line tool or automation script --- @@ -180,5 +195,5 @@ jobs: if: steps.archetype.outputs.is_python_tool == 'true' run: | python3 -m compileall -q \ - --exclude '(^\./\.git/|^\./node_modules/|^\./\.claude/)' \ + -x '(^\./\.git/|^\./node_modules/|^\./\.claude/)' \ . diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index aba1907..2031cc9 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -63,17 +63,48 @@ jobs: # DELETE this job if your project is UNBUNDLED. build: runs-on: ubuntu-latest + outputs: + is_browser_extension: ${{ steps.archetype.outputs.is_browser_extension }} steps: - name: Check out the code uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - name: Check the CI archetype + # Reads .github/ci-archetype, trimmed of surrounding whitespace and + # newlines, mirroring the equivalent step in ci.yml and + # accessibility.yml. A browser-extension project has no public + # website at all — nothing for GitHub Pages to serve — so this + # entire workflow is a deliberate no-op for that archetype, not a + # weakened deploy. See jacob-ci-archetypes.md section 2c. + id: archetype + run: | + if [ -f .github/ci-archetype ]; then + value=$(tr -d '[:space:]' < .github/ci-archetype) + else + value="" + fi + if [ "$value" = "browser-extension" ]; then + echo "is_browser_extension=true" >> "$GITHUB_OUTPUT" + else + echo "is_browser_extension=false" >> "$GITHUB_OUTPUT" + fi + + - name: Note that browser-extension projects have nothing to deploy + if: steps.archetype.outputs.is_browser_extension == 'true' + run: | + echo "This project's .github/ci-archetype declares browser-extension." + echo "A browser extension has no public website for GitHub Pages to" + echo "serve, so this workflow is a deliberate no-op for this archetype." + - name: Set up Node.js + if: steps.archetype.outputs.is_browser_extension != 'true' uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: lts/* cache: npm - name: Install dependencies + if: steps.archetype.outputs.is_browser_extension != 'true' run: npm ci - name: Build @@ -81,6 +112,7 @@ jobs: # bundler-free static-app project with a package.json for tooling # only, e.g. Braille Reference). Without --if-present, npm exits # non-zero with "Missing script: build" and the deploy fails. + if: steps.archetype.outputs.is_browser_extension != 'true' run: npm run build --if-present - name: Assemble the deploy artifact @@ -115,6 +147,7 @@ jobs: # unanchored 'data/***' also matches node_modules/css-tree/data/*, # silently publishing node_modules internals to the public site. id: artifact + if: steps.archetype.outputs.is_browser_extension != 'true' run: | if [ -d dist ]; then echo "path=dist" >> "$GITHUB_OUTPUT" @@ -136,15 +169,18 @@ jobs: fi - name: Configure Pages + if: steps.archetype.outputs.is_browser_extension != 'true' uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0 - name: Upload Pages artifact + if: steps.archetype.outputs.is_browser_extension != 'true' uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 with: path: ${{ steps.artifact.outputs.path }} deploy: needs: build + if: needs.build.outputs.is_browser_extension != 'true' runs-on: ubuntu-latest environment: name: github-pages diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index b21392e..8b35847 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -45,13 +45,16 @@ jobs: severity: CRITICAL,HIGH exit-code: '1' - # ── PRIVATE REPOS: REMOVE THIS JOB ────────────────────────────────────────── + # ── dependency-review ─────────────────────────────────────────────────────── # dependency-review requires GitHub Advanced Security. On a private repository - # without Advanced Security, this job will fail with a 403 error. - # Remove it from any private project repository. + # without it, the action fails with a 403 error. The job is kept for the + # signal it gives on public repos and where Advanced Security is enabled, but + # is marked continue-on-error so it never blocks a merge on a private repo + # that lacks the feature. Trivy and Semgrep remain the blocking security gates. # ───────────────────────────────────────────────────────────────────────────── dependency-review: runs-on: ubuntu-latest + continue-on-error: true if: github.event_name == 'pull_request' steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 diff --git a/package-lock.json b/package-lock.json index 7b292f3..f4732db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -655,9 +655,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -675,9 +672,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -695,9 +689,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -715,9 +706,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -735,9 +723,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -755,9 +740,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1131,16 +1113,16 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", + "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/braces": { @@ -1622,9 +1604,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", + "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", "dev": true, "funding": [ { @@ -2359,9 +2341,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2383,9 +2362,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2407,9 +2383,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2431,9 +2404,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [