Skip to content

Reapproval gate: per-reviewer staleness, synchronize-only trigger, rename churn→changes#4053

Draft
Copilot wants to merge 5 commits intomainfrom
copilot/add-reapproval-gate-workflow
Draft

Reapproval gate: per-reviewer staleness, synchronize-only trigger, rename churn→changes#4053
Copilot wants to merge 5 commits intomainfrom
copilot/add-reapproval-gate-workflow

Conversation

Copy link
Contributor

Copilot AI commented Mar 11, 2026

Three incremental improvements to the reapproval gate workflow.

Trigger narrowed to synchronize only

The gate previously fired on opened, reopened, and ready_for_review — none of which can have an existing approval to protect. Now only runs when:

  • a new commit is pushed (pull_request: synchronize)
  • a review is submitted (pull_request_review: submitted)

Per-reviewer staleness instead of global latest approval

Previously the gate found the single globally-latest approval and re-requested all approvers if churn exceeded the threshold. Now each reviewer is evaluated independently:

  • Reviewer whose approval post-dates the last significant commit → VALID, not bothered
  • Reviewer whose approval predates >10 LOC of new commits → STALE, re-request sent only to them

find_latest_approval() replaced by find_latest_approval_per_user() → Dict[str, Dict]; run_gate iterates per reviewer and collects only stale ones.

Merge-from-main commits still skipped

is_merge_from_main() continues to exclude "Update branch" merge commits from the LOC count — those lines were already reviewed when merged to main.

Rename: churnchanges

Replaced throughout code, variable names, docstrings, and docs for clarity.

🧪 Checklist

  • Unit tests added.
  • The documentation updated.
  • Change follows security best practices.
Original prompt

Implement an automated re-approval gate for PRs in openvinotoolkit/model_server.

Goal

  • If a PR has been approved, and subsequent changes introduce more than 10 LOC of change (additions + deletions > 10), then the PR should require re-approval before merge.
  • However, updates that are just merges from the main branch should be treated as OK (they should not trigger the re-approval requirement; i.e., ignore merge-from-main commits for the LOC threshold).

Repository context

  • Repo: openvinotoolkit/model_server
  • Default branch: main

Required behavior

  1. Add a GitHub Actions workflow that runs on relevant PR events (at minimum: pull_request synchronize/opened/reopened/ready_for_review, and pull_request_review submitted) and produces a required status check (name it something stable like reapproval-gate).
  2. The workflow should:
    • Determine whether the PR currently has at least one approval.
    • Determine the amount of code churn (additions + deletions) introduced after the most recent approval.
    • Ignore merge commits that merge main into the PR branch when computing churn (treat them as 0 for the purpose of this threshold).
    • If churn_after_approval > 10, fail the reapproval-gate check (with a clear message). If <= 10, pass.
    • When it fails, re-request review from prior approvers (or at least from the PR author’s requested reviewers), if feasible via the GitHub API token available to Actions.
  3. Update repository documentation (e.g., CONTRIBUTING.md or a new doc under docs/) to describe the behavior and how to configure it (including that the check should be marked as required in branch protection settings).
  4. Ensure the workflow uses least privileges and appropriate permissions in the workflow file.
  5. Add tests for the core churn calculation logic (unit tests) if there is a reasonable place to add them; otherwise, include a small script with self-tests.

Implementation notes / constraints

  • Use only GitHub-native capabilities (Actions + API). No paid external services.
  • Keep runtime small (seconds).
  • Prefer a dedicated script (Node.js or Python) checked into the repo (e.g., .github/scripts/reapproval_gate.{js,py}) rather than a huge inline workflow.

Acceptance criteria

  • On a PR that is approved and then receives >10 additions+deletions (excluding merge-from-main commits), the reapproval-gate check becomes failing and blocks merge when required.
  • On a PR that is approved and then only has merges from main, the check remains passing.
  • On a PR that is approved and then gets <=10 additions+deletions, the check remains passing.
  • Clear logs / output explain the calculation.

The following is the prior conversation context from the user's chat exploration (may be truncated):

User: Is there a setting in github copilot so that when I do a review - my approval will be reverted as soon as someone pushes new commits?

Assistant: No—GitHub Copilot doesn’t control review state, and there isn’t a Copilot setting to auto-revoke your approval when new commits are pushed.

That behavior is controlled by GitHub Pull Request branch protection:

Setting: "Dismiss stale pull request approvals when new commits are pushed"
Where:   Repository → Settings → Branches → Branch protection rules → (edit rule)
Effect:  If new commits are pushed to the PR branch after you approved, your approval is dismissed and you must re-approve.

If you don’t see it, you likely lack admin permission on the repo or there’s no branch protection rule for the target branch yet.

User: I know I can set this but this will revoke everyone approval, probably even copilot.

Assistant: That’s correct: “Dismiss stale pull request approvals when new commits are pushed” applies to the branch protection rule, so it dismisses all stale approvals for that PR target branch (i.e., everyone’s), not just yours.

GitHub doesn’t have a built-in “revoke only my approval on new commits” setting.

Options:
1) Use branch protection "Dismiss stale approvals" (global for that branch).
2) Don’t use "Approve"; instead leave a non-approval review ("Comment") so you’re not counted as approved.
3) Use CODEOWNERS/required reviewers so the right people must re-approve, but it’s still not per-person auto-dismiss.
4) Ask for a GitHub feature: per-reviewer “auto-dismiss my approval on push”.

About Copilot: if “Copilot” is shown as an approving reviewer, it’s still just a GitHub review from an account/bot. If stale-approval dismissal is enabled, those approvals are dismissed too when new commits are pushed.

If you tell me your goal (e.g., “I want to ensure I always re-check after changes, but I don’t want to slow down others”), I can suggest the least disruptive workflow for your team.

User: The issue is that PR had approval, but later on quire large changes were introduced and there was no request for re-approval

Assistant: GitHub won’t a...

This pull request was created from Copilot chat.


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Co-authored-by: atobiszei <36039266+atobiszei@users.noreply.github.com>
Copilot AI changed the title [WIP] Add automated re-approval gate for PRs Add automated reapproval gate for PRs with significant post-approval changes Mar 11, 2026
@atobiszei atobiszei requested a review from Copilot March 11, 2026 09:57
…/ready_for_review)

Co-authored-by: atobiszei <36039266+atobiszei@users.noreply.github.com>
Copilot AI changed the title Add automated reapproval gate for PRs with significant post-approval changes Narrow reapproval-gate trigger: run only on new commits and review submissions Mar 11, 2026
@@ -0,0 +1,310 @@
#!/usr/bin/env python3
# Copyright 2024 Intel Corporation
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check for years

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a GitHub Actions “reapproval gate” to prevent PRs from merging after accumulating significant post-approval churn without re-review, with supporting scripts, tests, and documentation.

Changes:

  • Introduces a reapproval-gate required-check workflow that evaluates churn after the latest approval and can re-request review.
  • Adds a stdlib-only Python implementation plus offline unit tests for core decision logic.
  • Documents the gate behavior and updates contribution guidance.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
.github/workflows/reapproval-gate.yml Defines the required-check workflow and permissions, runs tests + gate script on PR/review events.
.github/scripts/reapproval_gate.py Implements approval detection, post-approval churn calculation, merge-from-main skipping, and re-requesting reviewers.
.github/scripts/test_reapproval_gate.py Adds offline unit tests for merge-from-main detection and latest-approval selection logic.
docs/reapproval-gate.md Documents decision flow, configuration, merge-from-main detection, and setup steps.
CONTRIBUTING.md Adds contributor-facing summary of the new gate and standard contribution guidance.

You can also share your feedback on Copilot code review. Take the survey.


steps:
- name: Checkout repository
uses: actions/checkout@v4
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The workflow checks out and executes .github/scripts/reapproval_gate.py from the PR’s code while granting pull-requests: write. For same-repo PRs, a contributor could modify that script in the PR and have it run with a write-scoped GITHUB_TOKEN (workflow remains from base, but checkout pulls untrusted code). Consider checking out the base ref (e.g., the target branch) for running the gate logic, or switching to pull_request_target with an explicit checkout of the base SHA so only trusted code is executed when write permissions are used.

Suggested change
uses: actions/checkout@v4
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.base.sha }}

Copilot uses AI. Check for mistakes.
Comment on lines +65 to +66
with urllib.request.urlopen(req) as response:
return json.loads(response.read())
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

urllib.request.urlopen(req) is called without a timeout. A transient network stall could hang the workflow job until Actions kills it, making the required check flaky. Set an explicit timeout (and consider a small retry/backoff for 5xx/rate-limit responses) to keep the gate reliable.

Copilot uses AI. Check for mistakes.
print(f" {sha[:8]}: +{additions}/-{deletions} = {churn} LOC", flush=True)
total_churn += churn
evaluated += 1

Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

calculate_churn_after_approval() does one GitHub API call per commit after approval (get_commit_details inside the loop). On PRs with many post-approval commits this can be slow and may hit API rate limits. Consider short-circuiting once total_churn exceeds the threshold and/or using a less chatty API approach (e.g., compare endpoints where possible) to reduce request volume.

Suggested change
if total_churn > LOC_THRESHOLD:
print(
f" Total churn {total_churn} exceeds threshold {LOC_THRESHOLD}; "
"stopping further evaluation.",
flush=True,
)
break

Copilot uses AI. Check for mistakes.
Comment on lines +114 to +123
if not reviewers:
return
url = f"https://api.github.com/repos/{repo}/pulls/{pr_number}/requested_reviewers"
try:
make_github_request(token, "POST", url, {"reviewers": reviewers})
print(f"Re-requested review from: {', '.join(reviewers)}", flush=True)
except Exception as exc: # pylint: disable=broad-except
print(f"Warning: could not re-request reviews: {exc}", flush=True)


Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

request_re_review() posts the full reviewers list in one call. GitHub’s API limits requested reviewers per request (and certain logins like the PR author/bots can cause 422), which can make the re-request fail entirely. Consider filtering/validating the reviewer list and chunking into multiple requests (handling 422 per chunk) so the best-effort behavior still re-requests as many reviewers as possible.

Suggested change
if not reviewers:
return
url = f"https://api.github.com/repos/{repo}/pulls/{pr_number}/requested_reviewers"
try:
make_github_request(token, "POST", url, {"reviewers": reviewers})
print(f"Re-requested review from: {', '.join(reviewers)}", flush=True)
except Exception as exc: # pylint: disable=broad-except
print(f"Warning: could not re-request reviews: {exc}", flush=True)
# Normalize and de-duplicate reviewers while preserving order.
seen = set()
normalized_reviewers: List[str] = []
for reviewer in reviewers:
if not reviewer:
continue
if reviewer in seen:
continue
seen.add(reviewer)
normalized_reviewers.append(reviewer)
if not normalized_reviewers:
return
url = f"https://api.github.com/repos/{repo}/pulls/{pr_number}/requested_reviewers"
max_reviewers_per_request = 50
successful: List[str] = []
for i in range(0, len(normalized_reviewers), max_reviewers_per_request):
chunk = normalized_reviewers[i : i + max_reviewers_per_request]
try:
make_github_request(token, "POST", url, {"reviewers": chunk})
successful.extend(chunk)
except urllib.error.HTTPError as http_exc:
# 422 often indicates one or more invalid reviewers in the chunk.
if http_exc.code == 422:
print(
"Warning: bulk re-request failed with 422; "
"retrying reviewers individually.",
flush=True,
)
for reviewer in chunk:
try:
make_github_request(
token,
"POST",
url,
{"reviewers": [reviewer]},
)
successful.append(reviewer)
except urllib.error.HTTPError as inner_http_exc:
print(
f"Warning: could not re-request review from "
f"{reviewer}: HTTP {inner_http_exc.code}",
flush=True,
)
except Exception as inner_exc: # pylint: disable=broad-except
print(
f"Warning: unexpected error while re-requesting "
f"review from {reviewer}: {inner_exc}",
flush=True,
)
else:
print(
f"Warning: HTTP {http_exc.code} while re-requesting "
f"reviews for chunk {chunk}",
flush=True,
)
except Exception as exc: # pylint: disable=broad-except
print(
f"Warning: unexpected error while re-requesting reviews for "
f"chunk {chunk}: {exc}",
flush=True,
)
if successful:
print(
f"Re-requested review from: {', '.join(successful)}",
flush=True,
)

Copilot uses AI. Check for mistakes.
Copilot AI and others added 2 commits March 11, 2026 10:08
Co-authored-by: atobiszei <36039266+atobiszei@users.noreply.github.com>
Co-authored-by: atobiszei <36039266+atobiszei@users.noreply.github.com>
Copilot AI changed the title Narrow reapproval-gate trigger: run only on new commits and review submissions Reapproval gate: per-reviewer staleness, synchronize-only trigger, rename churn→changes Mar 11, 2026
Copilot stopped work on behalf of atobiszei due to an error March 11, 2026 10:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants