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).
This commit is contained in:
Guarzo
2026-08-01 19:23:33 +00:00
parent b7ddbc486a
commit b73f9f31fe
2 changed files with 194 additions and 77 deletions
+81
View File
@@ -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
+113 -77
View File
@@ -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 <<EOF
## 🧪 Test Results Summary
**Overall Quality Score: ${OVERALL_SCORE}%** ${OVERALL_STATUS}
### 📊 Metrics Dashboard
| Category | Status | Count | Details |
|----------|---------|-------|---------|
| 📝 **Code Formatting** | ${FORMAT_STATUS} | ${FORMAT_COUNT} issues | \`mix format --check-formatted\` |
| 🔨 **Compilation** | ${COMPILE_STATUS} | ${COMPILE_WARNINGS} warnings | \`mix compile\` |
| 🧪 **Tests** | ${TESTS_STATUS} | ${TESTS_FAILURES}/${TESTS_TOTAL} failed | Success rate: ${TESTS_SUCCESS_RATE}% |
| 📊 **Coverage** | ${COVERAGE_STATUS} | ${COVERAGE_PERCENTAGE}% | \`mix coveralls\` |
| 🎯 **Credo** | ${CREDO_STATUS} | ${CREDO_TOTAL} issues | High: ${CREDO_HIGH}, Normal: ${CREDO_NORMAL}, Low: ${CREDO_LOW} |
| 🔍 **Dialyzer** | ${DIALYZER_STATUS} | ${DIALYZER_ERRORS} errors, ${DIALYZER_WARNINGS} warnings | \`mix dialyzer\` |
### 🎯 Quality Gates
Based on the project's quality thresholds:
- **Compilation Warnings**: ${COMPILE_WARNINGS}/148 (limit: 148)
- **Credo Issues**: ${CREDO_TOTAL}/87 (limit: 87)
- **Dialyzer Warnings**: ${DIALYZER_WARNINGS}/161 (limit: 161)
- **Test Coverage**: ${COVERAGE_PERCENTAGE}%/50% (minimum: 50%)
- **Test Failures**: ${TESTS_FAILURES}/0 (limit: 0)
<details>
<summary>📈 Progress Toward Goals</summary>
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})
</details>
<details>
<summary>🔧 Quick Actions</summary>
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
\`\`\`
</details>
---
🤖 *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)
<details>
<summary>📈 Progress Toward Goals</summary>
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 }})
</details>
<details>
<summary>🔧 Quick Actions</summary>
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
```
</details>
---
🤖 *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