Skip to content

fix(gha): automate PR review-state and stale labels#636

Merged
edelauna merged 1 commit into
mainfrom
chore/pr-maintenance-fix
Jun 15, 2026
Merged

fix(gha): automate PR review-state and stale labels#636
edelauna merged 1 commit into
mainfrom
chore/pr-maintenance-fix

Conversation

@edelauna

@edelauna edelauna commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Description

Automates pull-request review-state labels and aligns stale handling with who is expected to act next.

  • Reconciles open PRs hourly from the trusted default-branch workflow.
  • Applies awaiting-author while requested changes remain unresolved.
  • Moves a PR to awaiting-review after all change-requesting reviewers are re-requested.
  • Removes review-state labels after approval or dismissal.
  • Uses separate stale-inactive and stale-awaiting-author labels so the 60-day and 14-day policies cannot interfere.
  • Limits the 14+7 auto-close policy to PRs waiting on author changes; PRs waiting on maintainers are not auto-closed.
  • Handles multiple reviewers, pagination, fork PRs, overlapping runs, and per-PR API failures.
  • Pins third-party actions to immutable commit SHAs.

Security Model

The reconciliation runs only on a schedule or manual dispatch, so it executes workflow code from the trusted branch rather than code from a contributor fork. It does not check out or execute PR contents. The token is scoped to pull-requests: write for review reads and label mutations.

Test Procedure

Notes

actions/stale@v9 currently emits GitHub’s Node.js 20 deprecation warning. No newer release is available; GitHub will run JavaScript actions with Node.js 24 by default starting June 16, 2026.

Checklist

  • Self-reviewed
  • Manually tested against repository PRs
  • Documentation and contributor impact considered
  • External actions pinned to immutable commits

Summary by CodeRabbit

  • Chores
    • Updated GitHub Actions workflow dependencies to latest versions.
    • Enhanced PR review state labeling workflow with improved error handling and concurrency controls to prevent overlapping runs.

@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The label-pr-review-state workflow gains pull-requests: write permission, a concurrency group, a bump to actions/github-script v8, and per-PR try/catch error handling that collects failures and calls core.setFailed at the end. The stale.yml workflow pins actions/stale to a commit SHA in both jobs.

Changes

GitHub Actions workflow hardening

Layer / File(s) Summary
Permissions, concurrency, and script initialization
.github/workflows/label-pr-review-state.yml
permissions expanded to pull-requests: write; concurrency group label-pr-review-state added; actions/github-script bumped to v8; failures accumulator array initialized at script start.
Label reconciliation logic and per-PR error handling
.github/workflows/label-pr-review-state.yml
PR loop review pagination, desired-label determination, and label add/remove reconciliation are now wrapped in try/catch; exceptions are collected in failures via core.error, and core.setFailed is called after the loop if any PR failed.
Pin actions/stale to commit SHA
.github/workflows/stale.yml
Both stale-inactivity and stale-review-inactivity jobs replace the floating actions/stale@v9 tag with a pinned commit SHA.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • Zoo-Code-Org/Zoo-Code#631: Introduced the same workflow files being modified here (label-pr-review-state.yml and stale.yml), making it the direct predecessor of these changes.

Suggested reviewers

  • taltas
  • hannesrudolph
  • JamesRobert20
  • navedmerchant

Poem

🐇 Hop, hop! No two runs shall clash,
A concurrency gate keeps workflows from a crash.
Each PR gets a try, each error a catch,
Pinned SHA commits — a securityatch!
With setFailed at the end if things go wrong,
This rabbit's workflows are sturdy and strong! 🌟

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main changes: automating PR review-state labeling and stale label handling in GitHub Actions workflows.
Description check ✅ Passed The description comprehensively covers implementation details, security model, and test procedures, though it omits the 'Related GitHub Issue' section required by the template.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/pr-maintenance-fix

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov

codecov Bot commented Jun 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@edelauna edelauna changed the title fix(gha): updating stale workflow logic fix(gha): automate PR review-state and stale labels Jun 15, 2026
@edelauna edelauna marked this pull request as ready for review June 15, 2026 13:53

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
.github/workflows/label-pr-review-state.yml (1)

11-13: 💤 Low value

Consider cancel-in-progress: true for stronger consistency guarantees.

The current setting allows overlapping runs to both complete, which could cause label-state races if two runs reconcile the same PR simultaneously (e.g., one run reads labels, another updates them, first run overwrites based on stale read).

Setting cancel-in-progress: true would prevent this race at the cost of potentially leaving some PRs unprocessed if a run is cancelled mid-flight. Given that this is hourly reconciliation and labels will be corrected on the next run, the current choice is acceptable but slightly riskier.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/label-pr-review-state.yml around lines 11 - 13, In the
concurrency configuration block for label-pr-review-state, change the
cancel-in-progress setting from false to true to prevent potential race
conditions where overlapping workflow runs could simultaneously read and update
the same PR labels, causing one run to overwrite changes from another based on
stale data. This ensures only one run executes at a time for this workflow.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/label-pr-review-state.yml:
- Around line 49-51: The code attempts to call .map() on pr.requested_reviewers
without checking if it exists first, which will crash if requested_reviewers is
undefined. Guard against this by adding a null/undefined check before the .map()
call in the requestedReviewers Set initialization, providing an empty array as a
fallback when pr.requested_reviewers is not defined.
- Around line 40-43: The code accesses r.user.login without first checking if
r.user is null, which will cause a null-reference error when a reviewer's
account has been deleted. Add a guard condition to check that r.user is not null
before attempting to access r.user.login in the latest.set() call. This should
be done in the loop where reviews are being iterated and the latest map is being
populated to ensure null users are skipped gracefully.
- Around line 46-60: The logic for determining the PR label state only considers
individual requested reviewers from pr.requested_reviewers but does not account
for team review requests, which are retrieved via a separate GitHub API
endpoint. When a team is requested to review and one of its members submits a
changes-requested review, the workflow cannot match the team member to the team
request, causing incorrect label assignment. Add a separate API call to fetch
team review requests (teams are returned separately from individual users), then
expand the requestedReviewers set to include both individual user logins and the
logins of all members within requested teams. This ensures that when
changeRequestReviewers are checked against requestedReviewers using the every()
method, team members are properly recognized as part of a requested team review.

---

Nitpick comments:
In @.github/workflows/label-pr-review-state.yml:
- Around line 11-13: In the concurrency configuration block for
label-pr-review-state, change the cancel-in-progress setting from false to true
to prevent potential race conditions where overlapping workflow runs could
simultaneously read and update the same PR labels, causing one run to overwrite
changes from another based on stale data. This ensures only one run executes at
a time for this workflow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d181b2d5-d2c8-4dfd-a1c1-3c634dd13f3f

📥 Commits

Reviewing files that changed from the base of the PR and between 085bc7f and 361b9a1.

📒 Files selected for processing (2)
  • .github/workflows/label-pr-review-state.yml
  • .github/workflows/stale.yml

Comment on lines +40 to +43
for (const r of reviews) {
if (r.state !== 'COMMENTED') {
latest.set(r.user.login, r);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard against deleted reviewer accounts.

If a reviewer's account has been deleted, r.user may be null, causing a null-reference error on r.user.login. This would fail the reconciliation for that PR.

🛡️ Proposed fix to handle null user
                 for (const r of reviews) {
-                  if (r.state !== 'COMMENTED') {
+                  if (r.state !== 'COMMENTED' && r.user) {
                     latest.set(r.user.login, r);
                   }
                 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (const r of reviews) {
if (r.state !== 'COMMENTED') {
latest.set(r.user.login, r);
}
for (const r of reviews) {
if (r.state !== 'COMMENTED' && r.user) {
latest.set(r.user.login, r);
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/label-pr-review-state.yml around lines 40 - 43, The code
accesses r.user.login without first checking if r.user is null, which will cause
a null-reference error when a reviewer's account has been deleted. Add a guard
condition to check that r.user is not null before attempting to access
r.user.login in the latest.set() call. This should be done in the loop where
reviews are being iterated and the latest map is being populated to ensure null
users are skipped gracefully.

Comment on lines +46 to +60
const changeRequestReviewers = [...latest.entries()]
.filter(([, review]) => review.state === 'CHANGES_REQUESTED')
.map(([login]) => login);
const requestedReviewers = new Set(
pr.requested_reviewers.map(reviewer => reviewer.login),
);

let desiredLabel = null;
if (changeRequestReviewers.length > 0) {
desiredLabel = changeRequestReviewers.every(
reviewer => requestedReviewers.has(reviewer),
)
? 'awaiting-review'
: 'awaiting-author';
}
let desiredLabel = null;
if (changeRequestReviewers.length > 0) {
desiredLabel = changeRequestReviewers.every(
reviewer => requestedReviewers.has(reviewer),
)
? 'awaiting-review'
: 'awaiting-author';
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check if any open PRs have requested_teams to understand the impact

gh pr list --state open --json number,requestedReviewers,reviews --limit 100 | \
  jq '.[] | select(.requestedReviewers != null) | select(.requestedReviewers | length > 0) | {number, requestedReviewers}'

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 745


🏁 Script executed:

cat -n .github/workflows/label-pr-review-state.yml

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 4121


🌐 Web query:

GitHub API PR requested_teams field structure how to access team review requests

💡 Result:

To access team review requests for a GitHub Pull Request via the REST API, you should use the dedicated review requests endpoint rather than relying solely on the primary Pull Request object [1][2]. Accessing Team Review Requests: The primary Pull Request object (returned by GET /repos/{owner}/{repo}/pulls/{pull_number}) does not include a detailed list of requested teams in its response body. To retrieve this information, you must call the "Get all requested reviewers for a pull request" endpoint [2][3]: GET /repos/{owner}/{repo}/pulls/{pull_number}/requested_reviewers This endpoint returns a JSON object containing two distinct arrays [2][4]: 1. users: A list of individual users whose review has been requested [2][4]. 2. teams: A list of teams whose review has been requested [2][4]. The team objects within the teams array follow the standard Team structure, which includes the following key fields: - id: The unique identifier for the team [1][2]. - slug: The URL-friendly name of the team [1][2]. - name: The display name of the team [1][2]. - url/html_url: Links to the team's resource [1][2]. - description: A description of the team [1][2]. Library Implementations: If you are using common GitHub API client libraries, they often provide helper methods to abstract this call: - PyGithub: Use the pull_request.get_review_requests method, which returns a tuple containing two PaginatedLists: one for users and one for teams [5][6]. - github3.py: Access the requested_teams attribute on the PullRequest object (which may trigger a call to the review requests endpoint) [7][8]. - Octokit.net: The PullRequest model typically includes a RequestedTeams property that maps the API response to a collection of Team objects [9]. When requesting new reviews, use the corresponding POST endpoint [2][3]: POST /repos/{owner}/{repo}/pulls/{pull_number}/requested_reviewers Provide the team_reviewers parameter as an array of team slugs to request reviews from specific teams [2][3].

Citations:


Team review requests are not handled.

The logic only checks pr.requested_reviewers (individual users) from the main PR object, but team review requests require a separate API call. GitHub's /requested_reviewers endpoint returns both users and teams, but the workflow doesn't call it. If a team is requested to review and one of its members submits a "changes requested" review, the workflow can't match the team to update the label correctly.

To fix this, call the additional requested_reviewers endpoint to retrieve both users and teams:

📋 Proposed fix to include team reviewers
                 const reviews = await github.paginate(github.rest.pulls.listReviews, {
                   owner, repo, pull_number: pr.number, per_page: 100,
                 });
 
+                const reviewRequests = await github.rest.pulls.requestedReviewers({
+                  owner, repo, pull_number: pr.number,
+                });
+
                 // Reviews are returned chronologically, so later entries replace
                 // each reviewer's earlier decision.
                 const latest = new Map();
                 for (const r of reviews) {
                   if (r.state !== 'COMMENTED') {
                     latest.set(r.user.login, r);
                   }
                 }
 
                 const changeRequestReviewers = [...latest.entries()]
                   .filter(([, review]) => review.state === 'CHANGES_REQUESTED')
                   .map(([login]) => login);
                 const requestedReviewers = new Set(
-                  pr.requested_reviewers.map(reviewer => reviewer.login),
+                  [
+                    ...reviewRequests.users.map(u => u.login),
+                    ...reviewRequests.teams.map(t => t.slug),
+                  ],
                 );

This requires additional logic to handle the fact that team members submit reviews individually but team requests are at the team level—consider mapping individual team member logins to their team slugs or adding team members as re-requestable reviewers directly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/label-pr-review-state.yml around lines 46 - 60, The logic
for determining the PR label state only considers individual requested reviewers
from pr.requested_reviewers but does not account for team review requests, which
are retrieved via a separate GitHub API endpoint. When a team is requested to
review and one of its members submits a changes-requested review, the workflow
cannot match the team member to the team request, causing incorrect label
assignment. Add a separate API call to fetch team review requests (teams are
returned separately from individual users), then expand the requestedReviewers
set to include both individual user logins and the logins of all members within
requested teams. This ensures that when changeRequestReviewers are checked
against requestedReviewers using the every() method, team members are properly
recognized as part of a requested team review.

Comment on lines +49 to +51
const requestedReviewers = new Set(
pr.requested_reviewers.map(reviewer => reviewer.login),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard against undefined requested_reviewers.

The pr.requested_reviewers field might be undefined in some edge cases, causing a crash on .map(...).

🛡️ Proposed fix to handle undefined requested_reviewers
                 const requestedReviewers = new Set(
-                  pr.requested_reviewers.map(reviewer => reviewer.login),
+                  (pr.requested_reviewers || []).map(reviewer => reviewer.login),
                 );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/label-pr-review-state.yml around lines 49 - 51, The code
attempts to call .map() on pr.requested_reviewers without checking if it exists
first, which will crash if requested_reviewers is undefined. Guard against this
by adding a null/undefined check before the .map() call in the
requestedReviewers Set initialization, providing an empty array as a fallback
when pr.requested_reviewers is not defined.

@edelauna edelauna added this pull request to the merge queue Jun 15, 2026
Merged via the queue into main with commit e0dd61a Jun 15, 2026
23 of 24 checks passed
@edelauna edelauna deleted the chore/pr-maintenance-fix branch June 15, 2026 16:53
edelauna added a commit that referenced this pull request Jun 17, 2026
nigeldelviero pushed a commit to nigeldelviero/Zoo-Code that referenced this pull request Jun 22, 2026
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.

2 participants