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