-
Notifications
You must be signed in to change notification settings - Fork 25
ci: reapprove internal PRs after a push dismisses an approval #377
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
Open
ggprior
wants to merge
2
commits into
main
Choose a base branch
from
georg/reapprove-internal-prs
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,141 @@ | ||
| # Automatically reapprove PRs from PriorLabs org members after new code is pushed to them. | ||
| # | ||
| # The branch protection rules dismiss all approvals when new code is pushed to a PR, to | ||
| # ensure the most recent push is reviewed and approved. This is important for external | ||
| # collaborators, but is extra overhead for internal PRs where we trust the authors. In | ||
| # this case, the workflow automatically approves the current version of the PR if a | ||
| # previous version was already approved. | ||
| name: Reapprove internal PRs | ||
|
|
||
| on: | ||
| pull_request_review: | ||
| types: [dismissed] | ||
|
|
||
| permissions: | ||
| pull-requests: write | ||
|
|
||
| # One push can dismiss several approvals, and each dismissal starts its own run. Run | ||
| # them one at a time per PR, so that a later run sees the approval an earlier one | ||
| # created instead of adding a duplicate. | ||
| concurrency: | ||
| group: ${{ github.workflow }}-${{ github.event.pull_request.number }} | ||
| cancel-in-progress: false | ||
| # We need to check every review that was dismissed, in case it was an approval. | ||
| queue: max | ||
|
|
||
| jobs: | ||
| reapprove: | ||
| name: Restore dismissed approval | ||
| # The PR does not come from a fork. This also means the author has write access to | ||
| # the repository, as otherwise they could not have pushed the branch. The next step | ||
| # checks the author's org membership as well, for defence in depth. | ||
| if: github.event.pull_request.head.repo.full_name == github.repository | ||
| runs-on: ubuntu-slim | ||
| steps: | ||
| - name: Check the author and the reviewer are org members | ||
| id: membership | ||
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | ||
| with: | ||
| # We cannot read the memberships with GITHUB_TOKEN. | ||
| github-token: ${{ secrets.PRIORLABS_ORG_MEMBERSHIP_READ_PAT }} | ||
| script: | | ||
| const org = context.repo.owner; | ||
|
|
||
| async function isOrgMember(username) { | ||
| try { | ||
| await github.rest.orgs.checkMembershipForUser({ org, username }); | ||
| return true; | ||
| } catch (error) { | ||
| if (error.status === 404) { | ||
| return false; | ||
| } | ||
| throw error; | ||
| } | ||
| } | ||
|
|
||
| const author = context.payload.pull_request.user.login; | ||
| if (!(await isOrgMember(author))) { | ||
| core.info(`The author ${author} is not an org member, not approving.`); | ||
| return; | ||
| } | ||
|
|
||
| // Without this check, an external user can trigger this bot to approve a PR | ||
| // by submitting an approval themselves. Not a big risk, as the PR's author | ||
| // must still be an internal user. We include 'github-actions[bot]' so that | ||
| // the workflow can reapprove again after a second push from the user. | ||
| const reviewer = context.payload.review.user.login; | ||
| if (reviewer !== "github-actions[bot]" && !(await isOrgMember(reviewer))) { | ||
| core.info(`The reviewer ${reviewer} is not an org member, not approving.`); | ||
| return; | ||
| } | ||
|
|
||
| core.setOutput("trusted", "true"); | ||
|
|
||
| - name: Restore the dismissed approval | ||
| if: steps.membership.outputs.trusted == 'true' | ||
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | ||
| with: | ||
| script: | | ||
| const { owner, repo } = context.repo; | ||
| const pullNumber = context.payload.pull_request.number; | ||
| const headSha = context.payload.pull_request.head.sha; | ||
| const dismissedReview = context.payload.review; | ||
|
|
||
| const reviews = await github.paginate(github.rest.pulls.listReviews, { | ||
| owner, | ||
| repo, | ||
| pull_number: pullNumber, | ||
| per_page: 100, | ||
| }); | ||
|
|
||
| // Check if any of the reviews of this PR already approved the head commit. | ||
| // They might come from a human or an earlier run of the workflow. | ||
| // In this case, exit early to avoid approving again. | ||
| if (reviews.some((r) => r.commit_id === headSha && r.state === "APPROVED")) { | ||
| core.info("The current head is already approved, not approving again."); | ||
| return; | ||
| } | ||
|
|
||
| // The payload to this workflow doesn't tell us if the dismissed review was | ||
| // previously an approval, or why it was dismissed. To find this out, we | ||
| // find the review_dismissed event in the PR timeline. | ||
| const timeline = await github.paginate( | ||
| github.rest.issues.listEventsForTimeline, | ||
| { owner, repo, issue_number: pullNumber, per_page: 100 }, | ||
| ); | ||
| const dismissal = timeline | ||
| .filter((e) => e.event === "review_dismissed") | ||
| .map((e) => e.dismissed_review) | ||
| .find((d) => d?.review_id === dismissedReview.id); | ||
| if (!dismissal) { | ||
| core.info(`Found no dismissal of review ${dismissedReview.id}.`); | ||
| return; | ||
| } | ||
|
|
||
| // The dismissed review may not have been an approval (e.g. if it requested | ||
| // changes), in which case we don't want to approve. | ||
| if (dismissal.state !== "approved") { | ||
| core.info("The dismissed review was not an approval, not approving."); | ||
| return; | ||
| } | ||
| // We only want to reapprove when the dismissal was due to new code being | ||
| // pushed, not if the reviewer manually dismissed the review. | ||
| if (!dismissal.dismissal_commit_id) { | ||
| core.info("The approval was dismissed by hand, not restoring it."); | ||
| return; | ||
| } | ||
|
|
||
| core.info("Approving to restore the dismissed approval."); | ||
| // This approves the current HEAD, not the commit "headSha" that triggered | ||
| // this workflow. This avoids a race condition when someone makes two pushes | ||
| // in quick succession, as the workflow would only be triggered for the | ||
| // first one. It's safe, because the push can only come from an org member. | ||
| await github.rest.pulls.createReview({ | ||
| owner, | ||
| repo, | ||
| pull_number: pullNumber, | ||
| event: "APPROVE", | ||
| body: | ||
| "Reapproving after a push, as this is a PR from an internal author " + | ||
| "and was already approved." | ||
| }); | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Workflow grants write permissions globally
Low Severity
This workflow sets
pull-requests: writeat the workflow level instead of defaulting to empty permissions and granting write only on thereapprovejob. That regresses the repository convention that workflow-level permissions stay empty and each job opts in.Triggered by learned rule: GHA release workflows: SHA-pin, no interpolation, bot token
Reviewed by Cursor Bugbot for commit e8b0f74. Configure here.