-
Notifications
You must be signed in to change notification settings - Fork 55
feat(workflows): add closed-PR comment redirect #1328
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,165 @@ | ||
| name: Closed PR Comment Redirect | ||
|
|
||
| # COE AI-7: alert users who comment on closed PRs to open a GitHub issue. | ||
| # An external user reported the v1.4.8 SPII leak by commenting on the | ||
| # already-closed revert PR. Closed-PR comments are not surfaced to oncall, | ||
| # but issues are. This workflow: | ||
| # 1. Replies to the commenter with a link to open a new issue. | ||
| # 2. Notifies oncall via the existing SLACK_WEBHOOK_URL. | ||
| # Maintainers (admin/maintain/write) are skipped so internal back-and-forth | ||
| # doesn't trigger the redirect. | ||
|
|
||
| on: | ||
| issue_comment: | ||
| types: [created] | ||
|
|
||
| permissions: | ||
| pull-requests: write | ||
| issues: read | ||
|
|
||
| jobs: | ||
| redirect: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing concurrency control makes the marker-comment dedup racy. The This is a realistic scenario: someone posts two follow-up comments back-to-back, or two different external users comment around the same time. Suggest adding a per-PR concurrency group at the workflow or job level, e.g.: concurrency:
group: closed-pr-comment-${{ github.event.issue.number }}
cancel-in-progress: falseThis serializes runs for the same PR so the marker check is reliable. |
||
| runs-on: ubuntu-latest | ||
| # Only fire on PRs (issue_comment fires for issues too) that are closed. | ||
| if: >- | ||
| github.event.issue.pull_request != null && | ||
| github.event.issue.state == 'closed' && | ||
| github.event.comment.user.type != 'Bot' | ||
| steps: | ||
| - name: Check commenter permission | ||
| id: perm | ||
| uses: actions/github-script@v9 | ||
| with: | ||
| script: | | ||
| // External users on private repos can 404 here; treat any | ||
| // failure as "not a maintainer" so the redirect still fires. | ||
| let permission = 'none'; | ||
| try { | ||
| const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| username: context.payload.comment.user.login, | ||
| }); | ||
| permission = data.permission; | ||
| } catch (err) { | ||
| core.info(`Permission lookup failed for ${context.payload.comment.user.login}: ${err.message}. Treating as non-maintainer.`); | ||
| } | ||
| const skip = ['admin', 'maintain', 'write'].includes(permission); | ||
| core.setOutput('skip', String(skip)); | ||
| core.info(`Commenter ${context.payload.comment.user.login} permission=${permission} skip=${skip}`); | ||
|
|
||
| - name: Check for existing redirect comment | ||
| id: existing | ||
| if: steps.perm.outputs.skip != 'true' | ||
| uses: actions/github-script@v9 | ||
| with: | ||
| script: | | ||
| // Marker we embed in our reply so we don't double-post on the same PR. | ||
| const marker = '<!-- closed-pr-comment-redirect -->'; | ||
| const comments = await github.paginate( | ||
| github.rest.issues.listComments, | ||
| { | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: context.payload.issue.number, | ||
| per_page: 100, | ||
| } | ||
| ); | ||
| const alreadyPosted = comments.some(c => c.body && c.body.includes(marker)); | ||
| core.setOutput('already_posted', String(alreadyPosted)); | ||
|
|
||
| - name: Post redirect comment | ||
| if: steps.perm.outputs.skip != 'true' && steps.existing.outputs.already_posted != 'true' | ||
| # The Slack alert is the load-bearing part of this workflow (it's | ||
| # what closes the COE detection gap). If posting the bot reply | ||
| # fails (rate limit, transient error), don't block oncall paging. | ||
| continue-on-error: true | ||
| uses: actions/github-script@v9 | ||
| env: | ||
| # Repos with issue templates use /issues/new/choose; repos without | ||
| # templates should change this to /issues/new. | ||
| ISSUES_NEW_URL: https://github.com/${{ github.repository }}/issues/new/choose | ||
| with: | ||
| script: | | ||
| const commenter = context.payload.comment.user.login; | ||
| const issuesNewUrl = process.env.ISSUES_NEW_URL; | ||
| const body = [ | ||
| '<!-- closed-pr-comment-redirect -->', | ||
| '', | ||
| `Thanks for the report, @${commenter} — feedback like this is exactly`, | ||
| "how we catch the things we missed. Because this PR is already", | ||
| "closed, the team won't see follow-up comments here.", | ||
| '', | ||
| 'Would you mind opening a new issue so we can track it properly?', | ||
| issuesNewUrl, | ||
| '', | ||
| 'If this is a security issue, please report it privately via', | ||
| 'https://aws.amazon.com/security/vulnerability-reporting/ instead', | ||
| 'of a public issue.', | ||
| ].join('\n'); | ||
|
|
||
| await github.rest.issues.createComment({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: context.payload.issue.number, | ||
| body, | ||
| }); | ||
|
|
||
| - name: Compute PR state | ||
| id: pr_state | ||
| if: steps.perm.outputs.skip != 'true' | ||
| uses: actions/github-script@v9 | ||
| with: | ||
| script: | | ||
| // PRs surface as `issue` events; merged_at is null when closed-not-merged. | ||
| const mergedAt = context.payload.issue.pull_request && | ||
| context.payload.issue.pull_request.merged_at; | ||
| core.setOutput('state', mergedAt ? 'merged' : 'closed'); | ||
|
|
||
| - name: Notify Slack | ||
| if: steps.perm.outputs.skip != 'true' | ||
| # Attacker-controlled fields are passed through env: rather than | ||
| # interpolated into the YAML payload, to prevent workflow injection. | ||
| # Schema is uniform across event types: every workflow sends the | ||
| # same 20 keys so Slack-side branching on event_type is reliable. | ||
| # For closed-PR comments, the issue_* fields are empty (this isn't | ||
| # an issue) and the pr_*/comment_* fields carry the real data. | ||
| env: | ||
| REPOSITORY: ${{ github.repository }} | ||
| CREATED_AT: ${{ github.event.comment.created_at }} | ||
| PR_NUMBER: ${{ github.event.issue.number }} | ||
| PR_TITLE: ${{ github.event.issue.title }} | ||
| PR_URL: ${{ github.event.issue.html_url }} | ||
| PR_AUTHOR: ${{ github.event.issue.user.login }} | ||
| PR_STATE: ${{ steps.pr_state.outputs.state }} | ||
| PR_CLOSED_AT: ${{ github.event.issue.closed_at }} | ||
| PR_MERGED_AT: ${{ github.event.issue.pull_request.merged_at }} | ||
| COMMENT_ID: ${{ github.event.comment.id }} | ||
| COMMENT_URL: ${{ github.event.comment.html_url }} | ||
| COMMENT_AUTHOR: ${{ github.event.comment.user.login }} | ||
| COMMENT_BODY: ${{ github.event.comment.body }} | ||
| uses: slackapi/slack-github-action@v2.1.1 | ||
| with: | ||
| webhook: ${{ secrets.SLACK_WEBHOOK_URL }} | ||
| webhook-type: webhook-trigger | ||
| payload: | | ||
| event_type: "closed_pr_comment" | ||
| repository: "${{ env.REPOSITORY }}" | ||
| created_at: "${{ env.CREATED_AT }}" | ||
| issue_number: "" | ||
| issue_title: "" | ||
| issue_url: "" | ||
| issue_author: "" | ||
| issue_body: "" | ||
| labels: "" | ||
| pr_number: "${{ env.PR_NUMBER }}" | ||
| pr_title: ${{ toJSON(env.PR_TITLE) }} | ||
| pr_url: "${{ env.PR_URL }}" | ||
| pr_author: "${{ env.PR_AUTHOR }}" | ||
| pr_state: "${{ env.PR_STATE }}" | ||
| pr_closed_at: "${{ env.PR_CLOSED_AT }}" | ||
| pr_merged_at: "${{ env.PR_MERGED_AT }}" | ||
| comment_id: "${{ env.COMMENT_ID }}" | ||
| comment_url: "${{ env.COMMENT_URL }}" | ||
| comment_author: "${{ env.COMMENT_AUTHOR }}" | ||
| comment_body: ${{ toJSON(env.COMMENT_BODY) }} | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i'm not sure we want to be publishing details of the COE publicly.