Browse Source
* Add update-api command * Api diff command * Missed flag * Restrict commands running on fork PRs * Add concurrency * Filter github.event.comment.author_association even before workflow started * Use steps.pr.outputs.sha * Only push api/ changespull/20892/head
committed by
GitHub
2 changed files with 303 additions and 0 deletions
@ -0,0 +1,180 @@ |
|||
name: Output API Diff |
|||
|
|||
on: |
|||
issue_comment: |
|||
types: [created] |
|||
|
|||
permissions: {} |
|||
|
|||
concurrency: |
|||
group: api-diff-${{ github.event.issue.number }} |
|||
cancel-in-progress: true |
|||
|
|||
jobs: |
|||
api-diff: |
|||
name: Output API Diff |
|||
if: >- |
|||
github.event.issue.pull_request |
|||
&& contains(github.event.comment.body, '/api-diff') |
|||
&& contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association) |
|||
runs-on: ubuntu-latest |
|||
|
|||
permissions: |
|||
contents: read |
|||
pull-requests: write |
|||
|
|||
steps: |
|||
- name: Check maintainer permission |
|||
uses: actions/github-script@v7 |
|||
with: |
|||
script: | |
|||
const { data: permLevel } = await github.rest.repos.getCollaboratorPermissionLevel({ |
|||
owner: context.repo.owner, |
|||
repo: context.repo.repo, |
|||
username: context.payload.comment.user.login, |
|||
}); |
|||
const allowed = ['admin', 'maintain', 'write']; |
|||
if (!allowed.includes(permLevel.permission)) { |
|||
core.setFailed(`User @${context.payload.comment.user.login} does not have write access.`); |
|||
} |
|||
|
|||
- name: Add reaction to acknowledge command |
|||
uses: actions/github-script@v7 |
|||
with: |
|||
script: | |
|||
await github.rest.reactions.createForIssueComment({ |
|||
owner: context.repo.owner, |
|||
repo: context.repo.repo, |
|||
comment_id: context.payload.comment.id, |
|||
content: 'eyes', |
|||
}); |
|||
|
|||
- name: Get PR branch info |
|||
id: pr |
|||
uses: actions/github-script@v7 |
|||
with: |
|||
script: | |
|||
const { data: pr } = await github.rest.pulls.get({ |
|||
owner: context.repo.owner, |
|||
repo: context.repo.repo, |
|||
pull_number: context.issue.number, |
|||
}); |
|||
if (pr.head.repo.full_name !== `${context.repo.owner}/${context.repo.repo}`) { |
|||
core.setFailed('Cannot run /api-diff on fork PRs — would execute untrusted code.'); |
|||
return; |
|||
} |
|||
core.setOutput('ref', pr.head.ref); |
|||
core.setOutput('sha', pr.head.sha); |
|||
|
|||
- name: Checkout PR branch |
|||
uses: actions/checkout@v4 |
|||
with: |
|||
ref: ${{ steps.pr.outputs.sha }} |
|||
|
|||
- name: Setup .NET |
|||
uses: actions/setup-dotnet@v4 |
|||
with: |
|||
global-json-file: global.json |
|||
|
|||
- name: Run OutputApiDiff |
|||
run: dotnet run --project ./nukebuild/_build.csproj -- OutputApiDiff |
|||
|
|||
- name: Post API diff as PR comment |
|||
if: always() && steps.pr.outcome == 'success' |
|||
uses: actions/github-script@v7 |
|||
with: |
|||
script: | |
|||
const fs = require('fs'); |
|||
const path = require('path'); |
|||
|
|||
const diffDir = path.join(process.env.GITHUB_WORKSPACE, 'artifacts', 'api-diff', 'markdown'); |
|||
const mergedPath = path.join(diffDir, '_diff.md'); |
|||
|
|||
let body; |
|||
if (fs.existsSync(mergedPath)) { |
|||
let diff = fs.readFileSync(mergedPath, 'utf8').trim(); |
|||
if (!diff || diff.toLowerCase().includes('no changes')) { |
|||
body = '### API Diff\n\n✅ No public API changes detected in this PR.'; |
|||
} else { |
|||
const MAX_COMMENT_LENGTH = 60000; // GitHub comment limit is 65536 |
|||
const header = '### API Diff\n\n'; |
|||
const footer = '\n\n---\n_Generated by `/api-diff` command._'; |
|||
const budget = MAX_COMMENT_LENGTH - header.length - footer.length; |
|||
|
|||
if (diff.length > budget) { |
|||
diff = diff.substring(0, budget) + '\n\n> ⚠️ Output truncated. See the [full workflow run](' + |
|||
`${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}` + |
|||
') for complete diff.'; |
|||
} |
|||
|
|||
body = header + diff + footer; |
|||
} |
|||
} else { |
|||
body = '### API Diff\n\n⚠️ No diff output was produced. Check the [workflow run](' + |
|||
`${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}` + |
|||
') for details.'; |
|||
} |
|||
|
|||
// Collapse into <details> if large |
|||
if (body.length > 2000) { |
|||
const inner = body; |
|||
body = '<details>\n<summary>📋 API Diff (click to expand)</summary>\n\n' + inner + '\n\n</details>'; |
|||
} |
|||
|
|||
// Update existing bot comment or create a new one |
|||
const marker = '<!-- api-diff-bot -->'; |
|||
body = marker + '\n' + body; |
|||
|
|||
const { data: comments } = await github.rest.issues.listComments({ |
|||
owner: context.repo.owner, |
|||
repo: context.repo.repo, |
|||
issue_number: context.issue.number, |
|||
per_page: 100, |
|||
}); |
|||
const existing = comments.find(c => c.body?.includes(marker)); |
|||
|
|||
if (existing) { |
|||
await github.rest.issues.updateComment({ |
|||
owner: context.repo.owner, |
|||
repo: context.repo.repo, |
|||
comment_id: existing.id, |
|||
body, |
|||
}); |
|||
} else { |
|||
await github.rest.issues.createComment({ |
|||
owner: context.repo.owner, |
|||
repo: context.repo.repo, |
|||
issue_number: context.issue.number, |
|||
body, |
|||
}); |
|||
} |
|||
|
|||
- name: Add success reaction |
|||
if: success() |
|||
uses: actions/github-script@v7 |
|||
with: |
|||
script: | |
|||
await github.rest.reactions.createForIssueComment({ |
|||
owner: context.repo.owner, |
|||
repo: context.repo.repo, |
|||
comment_id: context.payload.comment.id, |
|||
content: 'rocket', |
|||
}); |
|||
|
|||
- name: Report failure |
|||
if: failure() |
|||
uses: actions/github-script@v7 |
|||
with: |
|||
script: | |
|||
await github.rest.reactions.createForIssueComment({ |
|||
owner: context.repo.owner, |
|||
repo: context.repo.repo, |
|||
comment_id: context.payload.comment.id, |
|||
content: '-1', |
|||
}); |
|||
await github.rest.issues.createComment({ |
|||
owner: context.repo.owner, |
|||
repo: context.repo.repo, |
|||
issue_number: context.issue.number, |
|||
body: `❌ \`/api-diff\` failed. [See logs](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}).`, |
|||
}); |
|||
@ -0,0 +1,123 @@ |
|||
name: Update API Suppressions |
|||
|
|||
on: |
|||
issue_comment: |
|||
types: [created] |
|||
|
|||
permissions: {} |
|||
|
|||
concurrency: |
|||
group: update-api-${{ github.event.issue.number }} |
|||
cancel-in-progress: true |
|||
|
|||
jobs: |
|||
update-api: |
|||
name: Update API Suppressions |
|||
if: >- |
|||
github.event.issue.pull_request |
|||
&& contains(github.event.comment.body, '/update-api') |
|||
&& contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association) |
|||
runs-on: ubuntu-latest |
|||
|
|||
permissions: |
|||
contents: write |
|||
pull-requests: write |
|||
|
|||
steps: |
|||
- name: Check maintainer permission |
|||
uses: actions/github-script@v7 |
|||
with: |
|||
script: | |
|||
const { data: permLevel } = await github.rest.repos.getCollaboratorPermissionLevel({ |
|||
owner: context.repo.owner, |
|||
repo: context.repo.repo, |
|||
username: context.payload.comment.user.login, |
|||
}); |
|||
const allowed = ['admin', 'maintain', 'write']; |
|||
if (!allowed.includes(permLevel.permission)) { |
|||
core.setFailed(`User @${context.payload.comment.user.login} does not have write access.`); |
|||
} |
|||
|
|||
- name: Add reaction to acknowledge command |
|||
uses: actions/github-script@v7 |
|||
with: |
|||
script: | |
|||
await github.rest.reactions.createForIssueComment({ |
|||
owner: context.repo.owner, |
|||
repo: context.repo.repo, |
|||
comment_id: context.payload.comment.id, |
|||
content: 'eyes', |
|||
}); |
|||
|
|||
- name: Get PR branch info |
|||
id: pr |
|||
uses: actions/github-script@v7 |
|||
with: |
|||
script: | |
|||
const { data: pr } = await github.rest.pulls.get({ |
|||
owner: context.repo.owner, |
|||
repo: context.repo.repo, |
|||
pull_number: context.issue.number, |
|||
}); |
|||
if (pr.head.repo.full_name !== `${context.repo.owner}/${context.repo.repo}`) { |
|||
core.setFailed('Cannot run /update-api on fork PRs — would execute untrusted code with write permissions.'); |
|||
return; |
|||
} |
|||
core.setOutput('ref', pr.head.ref); |
|||
core.setOutput('sha', pr.head.sha); |
|||
|
|||
- name: Checkout PR branch |
|||
uses: actions/checkout@v4 |
|||
with: |
|||
ref: ${{ steps.pr.outputs.sha }} |
|||
token: ${{ secrets.GITHUB_TOKEN }} |
|||
|
|||
- name: Setup .NET |
|||
uses: actions/setup-dotnet@v4 |
|||
with: |
|||
global-json-file: global.json |
|||
|
|||
- name: Run ValidateApiDiff |
|||
run: dotnet run --project ./nukebuild/_build.csproj -- ValidateApiDiff --update-api-suppression true |
|||
|
|||
- name: Commit and push changes |
|||
run: | |
|||
git config user.name "github-actions[bot]" |
|||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com" |
|||
git add api/ |
|||
if git diff --cached --quiet; then |
|||
echo "No API suppression changes to commit." |
|||
else |
|||
git commit -m "Update API suppressions" |
|||
git push origin HEAD:${{ steps.pr.outputs.ref }} |
|||
fi |
|||
|
|||
- name: Add success reaction |
|||
if: success() |
|||
uses: actions/github-script@v7 |
|||
with: |
|||
script: | |
|||
await github.rest.reactions.createForIssueComment({ |
|||
owner: context.repo.owner, |
|||
repo: context.repo.repo, |
|||
comment_id: context.payload.comment.id, |
|||
content: 'rocket', |
|||
}); |
|||
|
|||
- name: Report failure |
|||
if: failure() |
|||
uses: actions/github-script@v7 |
|||
with: |
|||
script: | |
|||
await github.rest.reactions.createForIssueComment({ |
|||
owner: context.repo.owner, |
|||
repo: context.repo.repo, |
|||
comment_id: context.payload.comment.id, |
|||
content: '-1', |
|||
}); |
|||
await github.rest.issues.createComment({ |
|||
owner: context.repo.owner, |
|||
repo: context.repo.repo, |
|||
issue_number: context.issue.number, |
|||
body: `❌ \`/update-api\` failed. [See logs](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}).`, |
|||
}); |
|||
Loading…
Reference in new issue