Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 19 additions & 48 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
136 changes: 136 additions & 0 deletions .github/workflows/coverage-comment.yml
Original file line number Diff line number Diff line change
@@ -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
});
}
1 change: 1 addition & 0 deletions .github/workflows/pr-review-auto-fix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
Loading