From b73f9f31fe8f9d331e73109b65eb1ca4440a8a80 Mon Sep 17 00:00:00 2001 From: Guarzo Date: Sat, 1 Aug 2026 19:22:51 +0000 Subject: [PATCH] ci: post PR test comment via workflow_run to fix fork PR failures PRs from forks receive a read-only GITHUB_TOKEN for pull_request events regardless of the workflow's `permissions:` block, so the comment step in test.yml failed with 403 "Resource not accessible by integration" and took the whole job down with it. Every fork PR was red despite the test run itself succeeding. Split the reporting: test.yml now renders the comment body and uploads it as an artifact, and a new pr-comment.yml posts it via workflow_run, which runs in the base repo context with a writable token. The render step interpolates tool output from env vars in the shell rather than through Actions expression substitution, so tool output can no longer inject workflow syntax. pr-comment.yml never checks out or executes PR code, re-validates the PR number as numeric, and passes the body via body-path. Also mirrors the report to $GITHUB_STEP_SUMMARY so it stays visible if the follow-up workflow does not run, and fixes the empty "Updated:" timestamp (github.event.head_commit.timestamp is unset on pull_request events). --- .github/workflows/pr-comment.yml | 81 +++++++++++++ .github/workflows/test.yml | 190 ++++++++++++++++++------------- 2 files changed, 194 insertions(+), 77 deletions(-) create mode 100644 .github/workflows/pr-comment.yml diff --git a/.github/workflows/pr-comment.yml b/.github/workflows/pr-comment.yml new file mode 100644 index 00000000..cfa96554 --- /dev/null +++ b/.github/workflows/pr-comment.yml @@ -0,0 +1,81 @@ +name: ๐Ÿ’ฌ PR Test Comment + +# Posts the test-results comment produced by the "๐Ÿงช Test Suite" workflow. +# +# Why this is a separate workflow: for pull_request events from forks GitHub +# issues a read-only GITHUB_TOKEN regardless of the `permissions:` block, so the +# test workflow itself cannot post a comment (403 "Resource not accessible by +# integration"). A workflow_run workflow runs in the base repository's context +# with a writable token, so it can. +# +# SECURITY: this workflow runs with write permissions and must never execute +# code from the pull request. Do not add actions/checkout of the PR head, and do +# not run build/test tooling here. The downloaded artifact is untrusted input: +# the comment body is passed to the API via `body-path` (never interpolated into +# a run: block or a ${{ }} expression) and the PR number is re-validated as +# numeric before use. + +on: + workflow_run: + # Must match the `name:` of .github/workflows/test.yml exactly. + workflows: ['๐Ÿงช Test Suite'] + types: [completed] + +permissions: + contents: read + pull-requests: write + issues: write + +jobs: + comment: + name: Post test results comment + runs-on: ubuntu-latest + if: github.event.workflow_run.event == 'pull_request' + steps: + - name: Download comment artifact + id: download + uses: actions/download-artifact@v4 + continue-on-error: true + with: + name: pr-comment + path: pr-comment + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Read PR number + id: pr + if: steps.download.outcome == 'success' + run: | + if [ ! -f pr-comment/pr-number ] || [ ! -f pr-comment/body.md ]; then + echo "Artifact incomplete; nothing to post." + echo "valid=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + number=$(tr -dc '0-9' < pr-comment/pr-number | head -c 10) + if [ -z "$number" ]; then + echo "PR number is not numeric; refusing to continue." + echo "valid=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + echo "number=$number" >> "$GITHUB_OUTPUT" + echo "valid=true" >> "$GITHUB_OUTPUT" + + - name: Find existing PR comment + id: find_comment + if: steps.pr.outputs.valid == 'true' + uses: peter-evans/find-comment@v3 + with: + issue-number: ${{ steps.pr.outputs.number }} + comment-author: 'github-actions[bot]' + body-includes: '## ๐Ÿงช Test Results Summary' + + - name: Create or update PR comment + if: steps.pr.outputs.valid == 'true' + uses: peter-evans/create-or-update-comment@v4 + with: + comment-id: ${{ steps.find_comment.outputs.comment-id }} + issue-number: ${{ steps.pr.outputs.number }} + edit-mode: replace + body-path: pr-comment/body.md diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ac9f87ab..bbee56c5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -253,81 +253,117 @@ jobs: fi continue-on-error: true - - name: Find existing PR comment - if: github.event_name == 'pull_request' - id: find_comment - uses: peter-evans/find-comment@v3 + # Fork PRs get a read-only GITHUB_TOKEN, so this workflow cannot post the + # comment itself. Instead the rendered body is uploaded as an artifact and + # posted by the privileged `pr-comment.yml` workflow via workflow_run. + - name: Render PR comment body + if: always() && github.event_name == 'pull_request' + env: + OVERALL_SCORE: ${{ steps.summary.outputs.overall_score }} + OVERALL_STATUS: ${{ steps.summary.outputs.overall_status }} + FORMAT_STATUS: ${{ steps.format.outputs.status }} + FORMAT_COUNT: ${{ steps.format.outputs.count }} + COMPILE_STATUS: ${{ steps.compile.outputs.status }} + COMPILE_WARNINGS: ${{ steps.compile.outputs.warnings }} + TESTS_STATUS: ${{ steps.tests.outputs.status }} + TESTS_FAILURES: ${{ steps.tests.outputs.failures }} + TESTS_TOTAL: ${{ steps.tests.outputs.total }} + TESTS_SUCCESS_RATE: ${{ steps.tests.outputs.success_rate }} + COVERAGE_STATUS: ${{ steps.coverage.outputs.status }} + COVERAGE_PERCENTAGE: ${{ steps.coverage.outputs.percentage }} + CREDO_STATUS: ${{ steps.credo.outputs.status }} + CREDO_TOTAL: ${{ steps.credo.outputs.total_issues }} + CREDO_HIGH: ${{ steps.credo.outputs.high_issues }} + CREDO_NORMAL: ${{ steps.credo.outputs.normal_issues }} + CREDO_LOW: ${{ steps.credo.outputs.low_issues }} + DIALYZER_STATUS: ${{ steps.dialyzer.outputs.status }} + DIALYZER_ERRORS: ${{ steps.dialyzer.outputs.errors }} + DIALYZER_WARNINGS: ${{ steps.dialyzer.outputs.warnings }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + mkdir -p pr-comment + # Validate the PR number so the trusted workflow can trust this file. + case "$PR_NUMBER" in + ''|*[!0-9]*) echo "Refusing to emit non-numeric PR number" >&2; exit 1 ;; + esac + printf '%s\n' "$PR_NUMBER" > pr-comment/pr-number + + # Values are interpolated by the shell from env, never by Actions + # expression substitution, so tool output cannot inject workflow syntax. + cat > pr-comment/body.md < + ๐Ÿ“ˆ Progress Toward Goals + + Target goals for the project: + - โœจ **Zero compilation warnings** (currently: ${COMPILE_WARNINGS}) + - โœจ **โ‰ค10 Credo issues** (currently: ${CREDO_TOTAL}) + - โœจ **Zero Dialyzer warnings** (currently: ${DIALYZER_WARNINGS}) + - โœจ **โ‰ฅ85% test coverage** (currently: ${COVERAGE_PERCENTAGE}%) + - โœ… **Zero test failures** (currently: ${TESTS_FAILURES}) + + + +
+ ๐Ÿ”ง Quick Actions + + To improve code quality: + \`\`\`bash + # Fix formatting issues + mix format + + # View detailed Credo analysis + mix credo --strict + + # Check Dialyzer warnings + mix dialyzer + + # Generate detailed coverage report + mix coveralls.html + \`\`\` + +
+ + --- + + ๐Ÿค– *Auto-generated by GitHub Actions* โ€ข Updated: $(date -u '+%Y-%m-%d %H:%M UTC') + + > **Note**: This comment will be updated automatically when new commits are pushed to this PR. + EOF + + # Mirror to the run summary so the report is visible even if the + # follow-up commenting workflow is unavailable. + cat pr-comment/body.md >> "$GITHUB_STEP_SUMMARY" + continue-on-error: true + + - name: Upload PR comment body + if: always() && github.event_name == 'pull_request' + uses: actions/upload-artifact@v4 with: - issue-number: ${{ github.event.pull_request.number }} - comment-author: 'github-actions[bot]' - body-includes: '## ๐Ÿงช Test Results Summary' - - - name: Create or update PR comment - if: github.event_name == 'pull_request' - uses: peter-evans/create-or-update-comment@v4 - with: - comment-id: ${{ steps.find_comment.outputs.comment-id }} - issue-number: ${{ github.event.pull_request.number }} - edit-mode: replace - body: | - ## ๐Ÿงช Test Results Summary - - **Overall Quality Score: ${{ steps.summary.outputs.overall_score }}%** ${{ steps.summary.outputs.overall_status }} - - ### ๐Ÿ“Š Metrics Dashboard - - | Category | Status | Count | Details | - |----------|---------|-------|---------| - | ๐Ÿ“ **Code Formatting** | ${{ steps.format.outputs.status }} | ${{ steps.format.outputs.count }} issues | `mix format --check-formatted` | - | ๐Ÿ”จ **Compilation** | ${{ steps.compile.outputs.status }} | ${{ steps.compile.outputs.warnings }} warnings | `mix compile` | - | ๐Ÿงช **Tests** | ${{ steps.tests.outputs.status }} | ${{ steps.tests.outputs.failures }}/${{ steps.tests.outputs.total }} failed | Success rate: ${{ steps.tests.outputs.success_rate }}% | - | ๐Ÿ“Š **Coverage** | ${{ steps.coverage.outputs.status }} | ${{ steps.coverage.outputs.percentage }}% | `mix coveralls` | - | ๐ŸŽฏ **Credo** | ${{ steps.credo.outputs.status }} | ${{ steps.credo.outputs.total_issues }} issues | High: ${{ steps.credo.outputs.high_issues }}, Normal: ${{ steps.credo.outputs.normal_issues }}, Low: ${{ steps.credo.outputs.low_issues }} | - | ๐Ÿ” **Dialyzer** | ${{ steps.dialyzer.outputs.status }} | ${{ steps.dialyzer.outputs.errors }} errors, ${{ steps.dialyzer.outputs.warnings }} warnings | `mix dialyzer` | - - ### ๐ŸŽฏ Quality Gates - - Based on the project's quality thresholds: - - **Compilation Warnings**: ${{ steps.compile.outputs.warnings }}/148 (limit: 148) - - **Credo Issues**: ${{ steps.credo.outputs.total_issues }}/87 (limit: 87) - - **Dialyzer Warnings**: ${{ steps.dialyzer.outputs.warnings }}/161 (limit: 161) - - **Test Coverage**: ${{ steps.coverage.outputs.percentage }}%/50% (minimum: 50%) - - **Test Failures**: ${{ steps.tests.outputs.failures }}/0 (limit: 0) - -
- ๐Ÿ“ˆ Progress Toward Goals - - Target goals for the project: - - โœจ **Zero compilation warnings** (currently: ${{ steps.compile.outputs.warnings }}) - - โœจ **โ‰ค10 Credo issues** (currently: ${{ steps.credo.outputs.total_issues }}) - - โœจ **Zero Dialyzer warnings** (currently: ${{ steps.dialyzer.outputs.warnings }}) - - โœจ **โ‰ฅ85% test coverage** (currently: ${{ steps.coverage.outputs.percentage }}%) - - โœ… **Zero test failures** (currently: ${{ steps.tests.outputs.failures }}) - -
- -
- ๐Ÿ”ง Quick Actions - - To improve code quality: - ```bash - # Fix formatting issues - mix format - - # View detailed Credo analysis - mix credo --strict - - # Check Dialyzer warnings - mix dialyzer - - # Generate detailed coverage report - mix coveralls.html - ``` - -
- - --- - - ๐Ÿค– *Auto-generated by GitHub Actions* โ€ข Updated: ${{ github.event.head_commit.timestamp }} - - > **Note**: This comment will be updated automatically when new commits are pushed to this PR. + name: pr-comment + path: pr-comment/ + retention-days: 1 + continue-on-error: true