diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 43bbe568..c660b0be 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -92,14 +92,13 @@ jobs: -v --no-cov --tb=short || echo "Tests failed but continuing" timeout-minutes: 10 - # Coverage reporting (PR only) + # Coverage data collection (PR only) - saves artifact for coverage-comment workflow coverage-report: name: Coverage Report if: github.event_name == 'pull_request' runs-on: ubuntu-latest permissions: contents: read - pull-requests: write steps: - name: Checkout PR branch @@ -153,54 +152,26 @@ jobs: DIFF=$(python -c "print(f'{float(\"$PR_COVERAGE\") - float(\"$MAIN_COVERAGE\"):.1f}')") echo "diff=$DIFF" >> "$GITHUB_OUTPUT" - - name: Post coverage comment - uses: actions/github-script@v8 + - name: Save coverage data for comment workflow env: PR_COVERAGE: ${{ steps.coverage.outputs.coverage_pct }} MAIN_COVERAGE: ${{ steps.main_coverage.outputs.main_coverage_pct }} DIFF: ${{ steps.diff.outputs.diff }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + mkdir -p coverage-data + cat > coverage-data/coverage.json << EOF + { + "pr_coverage": "$PR_COVERAGE", + "main_coverage": "$MAIN_COVERAGE", + "diff": "$DIFF", + "pr_number": "$PR_NUMBER" + } + EOF + + - name: Upload coverage data + uses: actions/upload-artifact@v4 with: - script: | - const prCoverage = process.env.PR_COVERAGE; - const mainCoverage = process.env.MAIN_COVERAGE; - const diff = parseFloat(process.env.DIFF); - - const emoji = diff >= 0 ? '📈' : '📉'; - const diffText = diff >= 0 ? `+${diff}%` : `${diff}%`; - const status = diff >= 0 ? '✅' : '⚠️'; - - const body = `## ${emoji} Test Coverage Report\n\n` + - `| Branch | Coverage |\n` + - `|--------|----------|\n` + - `| **This PR** | ${prCoverage}% |\n` + - `| Main | ${mainCoverage}% |\n` + - `| **Diff** | ${status} ${diffText} |\n\n` + - `---\n\n` + - `*Coverage calculated from unit tests only*`; - - const comments = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - }); - - const existingComment = comments.data.find(comment => - comment.user.login === 'github-actions[bot]' && - comment.body.includes('Test Coverage Report') - ); - - if (existingComment) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existingComment.id, - body: body - }); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: body - }); - } + name: coverage-data + path: coverage-data/ + retention-days: 1 diff --git a/.github/workflows/coverage-comment.yml b/.github/workflows/coverage-comment.yml new file mode 100644 index 00000000..ed214c44 --- /dev/null +++ b/.github/workflows/coverage-comment.yml @@ -0,0 +1,136 @@ +name: Coverage Comment + +# This workflow runs after CI completes and posts coverage comments to PRs. +# It uses workflow_run to run in the upstream repo context with write permissions, +# enabling coverage comments on fork PRs that would otherwise fail due to +# insufficient permissions (see: https://github.com/anthropics/claude-code-action/issues/339) + +on: + workflow_run: + workflows: ["CI (Tests & Quality)"] + types: [completed] + +jobs: + post-comment: + name: Post Coverage Comment + runs-on: ubuntu-latest + if: > + github.event.workflow_run.event == 'pull_request' && + github.event.workflow_run.conclusion == 'success' + permissions: + pull-requests: write + actions: read + + steps: + - name: Get PR number from workflow run + id: pr_info + uses: actions/github-script@v7 + with: + script: | + // Get the PR associated with the workflow run (trusted source) + const workflowRun = await github.rest.actions.getWorkflowRun({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.payload.workflow_run.id + }); + + // Extract PR number from the workflow run's pull_requests array + const pullRequests = workflowRun.data.pull_requests; + if (!pullRequests || pullRequests.length === 0) { + core.setFailed('No pull request associated with this workflow run'); + return; + } + + const prNumber = pullRequests[0].number; + core.setOutput('pr_number', prNumber); + console.log(`PR number from workflow run: ${prNumber}`); + + - name: Download coverage data + uses: actions/download-artifact@v4 + with: + name: coverage-data + path: coverage-data + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Read coverage data + id: coverage + env: + TRUSTED_PR_NUMBER: ${{ steps.pr_info.outputs.pr_number }} + run: | + if [ -f coverage-data/coverage.json ]; then + PR_COVERAGE=$(jq -r '.pr_coverage' coverage-data/coverage.json) + MAIN_COVERAGE=$(jq -r '.main_coverage' coverage-data/coverage.json) + DIFF=$(jq -r '.diff' coverage-data/coverage.json) + ARTIFACT_PR_NUMBER=$(jq -r '.pr_number' coverage-data/coverage.json) + + # Security: Validate PR number matches (prevent cross-PR comment injection) + if [ "$ARTIFACT_PR_NUMBER" != "$TRUSTED_PR_NUMBER" ]; then + echo "::error::PR number mismatch: artifact=$ARTIFACT_PR_NUMBER, expected=$TRUSTED_PR_NUMBER" + exit 1 + fi + + { + echo "pr_coverage=$PR_COVERAGE" + echo "main_coverage=$MAIN_COVERAGE" + echo "diff=$DIFF" + echo "pr_number=$TRUSTED_PR_NUMBER" + } >> "$GITHUB_OUTPUT" + else + echo "Coverage data not found" + exit 1 + fi + + - name: Post coverage comment + uses: actions/github-script@v7 + env: + PR_COVERAGE: ${{ steps.coverage.outputs.pr_coverage }} + MAIN_COVERAGE: ${{ steps.coverage.outputs.main_coverage }} + DIFF: ${{ steps.coverage.outputs.diff }} + PR_NUMBER: ${{ steps.coverage.outputs.pr_number }} + with: + script: | + const prCoverage = process.env.PR_COVERAGE; + const mainCoverage = process.env.MAIN_COVERAGE; + const diff = parseFloat(process.env.DIFF); + const prNumber = parseInt(process.env.PR_NUMBER); + + const emoji = diff >= 0 ? '📈' : '📉'; + const diffText = diff >= 0 ? `+${diff}%` : `${diff}%`; + const status = diff >= 0 ? '✅' : '⚠️'; + + const body = `## ${emoji} Test Coverage Report\n\n` + + `| Branch | Coverage |\n` + + `|--------|----------|\n` + + `| **This PR** | ${prCoverage}% |\n` + + `| Main | ${mainCoverage}% |\n` + + `| **Diff** | ${status} ${diffText} |\n\n` + + `---\n\n` + + `*Coverage calculated from unit tests only*`; + + const comments = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + }); + + const existingComment = comments.data.find(comment => + comment.user.login === 'github-actions[bot]' && + comment.body.includes('Test Coverage Report') + ); + + if (existingComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existingComment.id, + body: body + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: body + }); + } diff --git a/.github/workflows/pr-review-auto-fix.yml b/.github/workflows/pr-review-auto-fix.yml index 2d9fbc1f..a675c553 100644 --- a/.github/workflows/pr-review-auto-fix.yml +++ b/.github/workflows/pr-review-auto-fix.yml @@ -42,6 +42,7 @@ jobs: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + allowed_non_write_users: '*' # Enable reviews on fork PRs prompt: | Run the /review-agentready command on this pull request.