From ae910286fc737ab91ca29b556bcbf93d48609dbd Mon Sep 17 00:00:00 2001 From: Georg Grab Date: Fri, 4 Sep 2026 08:13:36 +0000 Subject: [PATCH 1/2] ci: reapprove internal PRs after a push dismisses an approval Branch protection dismisses approvals on every push. For PRs authored and approved by org members that is pure overhead, so restore the approval automatically, mirroring the workflow in PriorLabs/TabPFN. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/reapprove-internal-prs.yml | 141 +++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 .github/workflows/reapprove-internal-prs.yml diff --git a/.github/workflows/reapprove-internal-prs.yml b/.github/workflows/reapprove-internal-prs.yml new file mode 100644 index 0000000..48fcbd0 --- /dev/null +++ b/.github/workflows/reapprove-internal-prs.yml @@ -0,0 +1,141 @@ +# Automatically reapprove PRs from PriorLabs org members after new code is pushed. +# +# 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." + }); From e8b0f7463366d3f8d913473afd362e94289cd3f0 Mon Sep 17 00:00:00 2001 From: Georg Grab Date: Fri, 4 Sep 2026 09:26:12 +0000 Subject: [PATCH 2/2] docs(ci): reword workflow header comment --- .github/workflows/reapprove-internal-prs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/reapprove-internal-prs.yml b/.github/workflows/reapprove-internal-prs.yml index 48fcbd0..cfc4785 100644 --- a/.github/workflows/reapprove-internal-prs.yml +++ b/.github/workflows/reapprove-internal-prs.yml @@ -1,4 +1,4 @@ -# Automatically reapprove PRs from PriorLabs org members after new code is pushed. +# 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