mirror of
https://github.com/wanderer-industries/wanderer
synced 2026-08-24 06:46:30 +00:00
Merge pull request #636 from guarzo/worktree-ci-fork-pr-comment
ci: post PR test comment via workflow_run to fix fork PR failures
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
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.
|
||||
#
|
||||
# A numeric PR number is not necessarily the *right* PR number: the artifact is
|
||||
# produced by a job that has already run fork-authored code, so it could name
|
||||
# any issue or PR in the base repository. The "Verify PR matches triggering run"
|
||||
# step below closes that by requiring the claimed PR's head SHA to equal the
|
||||
# head SHA of the run that triggered this workflow.
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
# Must match the `name:` of .github/workflows/test.yml exactly.
|
||||
workflows: ['🧪 Test Suite']
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
# `actions: read` is required: download-artifact with `run-id:` reads the
|
||||
# artifacts of a *different* workflow run, which the default token scope does
|
||||
# not cover. Without it the download 403s and the workflow silently posts
|
||||
# nothing.
|
||||
actions: read
|
||||
# Read-only PR access is only used to verify the artifact's PR number against
|
||||
# the triggering run's head SHA. The comment itself is an *issue* comment, so
|
||||
# `issues: write` is the scope that grants posting.
|
||||
pull-requests: read
|
||||
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@v8
|
||||
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: Verify PR matches triggering run
|
||||
if: steps.pr.outputs.valid == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
REPO: ${{ github.repository }}
|
||||
PR_NUMBER: ${{ steps.pr.outputs.number }}
|
||||
EXPECTED_SHA: ${{ github.event.workflow_run.head_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# The artifact comes from a job that ran untrusted code, so the PR
|
||||
# number it claims is only a hint. Accept it only if that PR's head
|
||||
# commit is the exact commit the triggering run tested; otherwise a
|
||||
# fork could steer this privileged comment at any PR or issue.
|
||||
actual_sha=$(gh api "repos/$REPO/pulls/$PR_NUMBER" --jq .head.sha)
|
||||
|
||||
if [ "$actual_sha" != "$EXPECTED_SHA" ]; then
|
||||
echo "PR #$PR_NUMBER head is $actual_sha but the triggering run tested $EXPECTED_SHA." >&2
|
||||
echo "Refusing to comment on a PR the artifact does not belong to." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PR #$PR_NUMBER head matches the triggering run ($EXPECTED_SHA)."
|
||||
|
||||
- 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
|
||||
+116
-79
@@ -6,10 +6,11 @@ on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
|
||||
# This workflow executes fork-authored code (mix deps.get / compile / test), so
|
||||
# it holds no write scopes. The test-results comment is posted by the privileged
|
||||
# pr-comment.yml workflow via workflow_run.
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
env:
|
||||
MIX_ENV: test
|
||||
@@ -253,81 +254,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@v7
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user