diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6e9732fb..aecc87d0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,14 +65,17 @@ jobs: github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-latest permissions: - contents: write + contents: read + outputs: + has_patch: ${{ steps.patch.outputs.has_patch }} + head_sha: ${{ steps.patch.outputs.head_sha }} steps: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ github.event.pull_request.head.ref }} - token: ${{ secrets.GITHUB_TOKEN }} + persist-credentials: false - name: Setup Node.js uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v6 @@ -87,21 +90,87 @@ jobs: npm run format npm run lint -- --fix - - name: Commit and push auto-fixes + # No push token is available in this job: PR-controlled tooling (npm + # scripts, git hooks) runs here, so the fixes are exported as a patch and + # applied + pushed by the separate 'autofix-push' job on a clean checkout. + - name: Export auto-fixes as patch + id: patch run: | - if git diff --quiet; then - echo "No formatting or lint fixes needed." + git add -u -- . ':(exclude).github/workflows' + git diff --cached --binary > "${RUNNER_TEMP}/autofix.patch" + git checkout -- . + echo "head_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + if [ -s "${RUNNER_TEMP}/autofix.patch" ]; then + echo "has_patch=true" >> "$GITHUB_OUTPUT" else - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git add -A - git commit -m "chore(ci): apply prettier/eslint auto-fixes" - git push + echo "has_patch=false" >> "$GITHUB_OUTPUT" fi - name: Verify no remaining issues after auto-fix run: npm run check + # Passed as an artifact: job outputs are capped at ~1 MB. + - name: Upload auto-fix patch + if: steps.patch.outputs.has_patch == 'true' + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 + with: + name: autofix-patch + path: ${{ runner.temp }}/autofix.patch + if-no-files-found: error + + autofix-push: + needs: autofix + if: needs.autofix.outputs.has_patch == 'true' + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Checkout code (clean, no PR-installed tooling) + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.event.pull_request.head.ref }} + persist-credentials: false + + - name: Download auto-fix patch + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 + with: + name: autofix-patch + path: ${{ runner.temp }}/autofix-patch + + - name: Commit and push auto-fixes + env: + GH_TOKEN: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS }} + HEAD_REF: ${{ github.event.pull_request.head.ref }} + EXPECTED_SHA: ${{ needs.autofix.outputs.head_sha }} + run: | + if [ -z "${GH_TOKEN}" ]; then + echo "::error::GH_PAT_DON_PETRY (or legacy GH_PAT_WORKFLOWS) is required for autofix pushes." + exit 1 + fi + if [ "$(git rev-parse HEAD)" != "${EXPECTED_SHA}" ]; then + echo "::notice::PR head moved from ${EXPECTED_SHA} to $(git rev-parse HEAD); skipping stale autofix push." + exit 0 + fi + numstat_file="${RUNNER_TEMP}/autofix-numstat.bin" + if ! git apply --numstat -z "${RUNNER_TEMP}/autofix-patch/autofix.patch" > "${numstat_file}"; then + echo "::error::unable to inspect autofix patch; refusing to push." + exit 1 + fi + if tr '\0' '\n' < "${numstat_file}" | grep -Eiq '^(([0-9]+|-)[[:space:]]+([0-9]+|-)[[:space:]]+)?(\./)?\.github/workflows/'; then + echo "::error::autofix patch touches workflow files; refusing to push." + exit 1 + fi + git apply --index "${RUNNER_TEMP}/autofix-patch/autofix.patch" + if git diff --cached --name-only -z | tr '\0' '\n' | grep -Eiq '^(\./)?\.github/workflows/'; then + echo "::error::autofix patch touches workflow files; refusing to push." + exit 1 + fi + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git commit -m "chore(ci): apply prettier/eslint auto-fixes" + git push -- "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" "HEAD:refs/heads/${HEAD_REF}" + secret-scan: name: Secret scan (gitleaks) runs-on: ubuntu-latest diff --git a/.prettierignore b/.prettierignore index 21171e7e..4289de18 100644 --- a/.prettierignore +++ b/.prettierignore @@ -34,9 +34,8 @@ # reflow comments, re-introducing drift, so it must not reformat this file. .github/workflows/persona-mention.yml # pr-review is a standards-owned caller stub copied VERBATIM from -# combsmith/.github/standards/workflows/pr-review.yml. The canonical uses -# double-quoted YAML scalars and a two-space gap before the `# NOSONAR` markers; -# Prettier would rewrite both, re-introducing drift, and the CI autofix job -# cannot push workflow-file changes (no `workflows` permission), so it must not -# reformat this file. +# combsmith/.github/standards/workflows/pr-review.yml. Its exact bytes +# (including the two-space gap before the `# NOSONAR` marker) are enforced by the +# central byte-identity stub-drift guard. Prettier would collapse that gap and +# reflow comments, re-introducing drift, so it must not reformat this file. .github/workflows/pr-review.yml