Skip to content

fix(deploy): gate branch-deploy approval check on exit code (EXSC-687) - #2128

Open
0xDEnYO wants to merge 2 commits into
mainfrom
fix/exsc-687-verify-approvals-exit-code
Open

fix(deploy): gate branch-deploy approval check on exit code (EXSC-687)#2128
0xDEnYO wants to merge 2 commits into
mainfrom
fix/exsc-687-verify-approvals-exit-code

Conversation

@0xDEnYO

@0xDEnYO 0xDEnYO commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Which Linear task belongs to this PR?

https://linear.app/lifi-linear/issue/EXSC-687

Why did I implement it this way?

Exit-code contract instead of a stdout sentinel. verify-approvals.ts wrote OK to stdout unconditionally at the end of its run() — the individual checks only logged to stderr via console.error and then fell through to the same final write. Its only consumer, deployUpgradesToSAFE.sh, captured that stdout into VERIFIED and compared it to "OK", so a run that had just reported "Missing required approvals" still matched and continued into the proposal loop. On top of that, the shell if had no else branch, so the non-matching case returned success and the deploy step ended without any signal at all. Exit codes are the contract the rest of this repo's shell/TS boundary relies on, so the script now exits non-zero on any failure and writes OK only when the failure list is empty, and the shell gates on if ! bun … ; then error …; return 1; fi. The success path is unchanged: an approved PR still prints OK, still logs "PR has been approved. Continuing...", and still runs the same loop (that block moved out of the old if, which is why its indentation shows up in the diff).

Pagination. getFilesInPR called octokit.rest.pulls.listFiles without pagination, which returns at most 30 entries. Any PR touching more than 30 files could omit the very src/Facets/<Facet>.sol entry being checked and report " is not included in this PR" for a facet that is in fact there. All four list calls (pulls.list, pulls.listFiles, pulls.listReviews, teams.listMembersInOrg) now go through octokit.paginate with per_page: 100, so the reviews and team-membership lookups can't be truncated either — the hand-rolled paging loop in getOpenPRsForBranch was replaced by the same mechanism rather than left as a second way of doing the same thing.

Explicit token precondition. GH_TOKEN was read by deployUpgradesToSAFE.sh and passed through as --token, but it existed nowhere else in the repo and was missing from .env.example, so a contributor who never set it got --token "". Because this repo is public the unauthenticated PR and review lookups still succeed and only the team-membership call fails, which is a degraded state that is hard to recognise from the output. The token is now resolved (flag first, GH_TOKEN from the environment as fallback) before the Octokit client is constructed, and a missing or blank value aborts immediately with a message naming the variable and the scopes it needs. .env.example documents it with an empty placeholder.

Fail-closed helpers. getPRApprovers and getFilesInPR caught their errors, logged them and returned undefined; every downstream check used ?. and read the resulting empty list as "nothing to report", so a rate-limited or unauthorised call produced the same result as a clean PR. Both now let the error propagate, and getTeamMembers rethrows with the scope hint instead of collapsing into a generic message. The orchestration is split into a pure collectApprovalFailures predicate plus verifyApprovals for the I/O, and runMain sits behind if (import.meta.main) (matching the other citty scripts here) so the new verify-approvals.test.ts can exercise the policy, the pagination, the missing-token path and the exit-code contract without touching the network. The check's semantics are otherwise unchanged — same conditions, same messages.

Governance impact (rule 105): this changes only the pre-proposal approval check for branch-based deploys; it does not alter Safe threshold, timelock delay, roles, or how any transaction is authorized. The check becomes strictly more likely to block, never less.

Checklist before requesting a review

Checklist for reviewer (DO NOT DEPLOY and contracts BEFORE CHECKING THIS!!!)

  • I have checked that any arbitrary calls to external contracts are validated and or restricted
  • I have checked that any privileged calls (i.e. storage modifications) are validated and or restricted
  • I have ensured that any new contracts have had AT A MINIMUM 1 preliminary audit conducted on by <company/auditor>

verify-approvals.ts printed the "OK" marker unconditionally, so
deployUpgradesToSAFE.sh - which compared that marker - continued even
after the check had logged failures. The script now exits non-zero on
any failure and prints the marker only on the fully successful path,
and the shell caller gates on the exit code instead.

Also paginates the GitHub list calls (listFiles capped at 30 entries,
producing false "facet is not included in this PR" results), turns the
swallowed helper errors into throws so a failed lookup can no longer
read as an empty result, and requires GH_TOKEN up front (now documented
in .env.example).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@0xDEnYO, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 35 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: bca08cea-d367-40a9-8c9c-42c8ec7bbdc6

📥 Commits

Reviewing files that changed from the base of the PR and between 22b6c3c and 125bbb1.

📒 Files selected for processing (3)
  • script/deploy/deployUpgradesToSAFE.sh
  • script/deploy/github/verify-approvals.test.ts
  • script/deploy/github/verify-approvals.ts

Walkthrough

The PR refactors GitHub approval verification into exported helpers, adds paginated lookup and validation tests, documents GH_TOKEN, and makes non-main Safe upgrade deployments abort when approval verification fails.

Changes

GitHub approval verification

Layer / File(s) Summary
Approval policy and token contracts
script/deploy/github/verify-approvals.ts, script/deploy/github/verify-approvals.test.ts
Adds token resolution, facet parsing, pure approval-failure evaluation, and tests for their validation rules.
GitHub lookup orchestration
script/deploy/github/verify-approvals.ts, script/deploy/github/verify-approvals.test.ts
Uses paginated GitHub lookups for PRs, files, approvers, and teams, propagates lookup errors, and tests the verification flow.
CLI reporting and executable wiring
script/deploy/github/verify-approvals.ts, script/deploy/github/verify-approvals.test.ts
Centralizes success and failure reporting, requires CLI inputs, authenticates Octokit, and tests CLI behavior.
Deployment approval gate
.env.example, script/deploy/deployUpgradesToSAFE.sh
Documents GH_TOKEN and runs approval verification for non-main branches before facet-cut computation and Safe proposal submission.

Estimated code review effort: 3 (Moderate) | ~30 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: gating deploy approval on exit code.
Description check ✅ Passed The description follows the template with task link, rationale, and both checklists filled out.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/exsc-687-verify-approvals-exit-code

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.

@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)
script/deploy/github/verify-approvals.ts (1)

99-108: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Filter open pull requests with the head parameter

GitHub’s list-pulls endpoint accepts head in owner:branch form, so this can request only PRs on the target branch instead of paginating every open pull request and filtering client-side. This reduces deploy-list network traffic as the repo accumulates more open PRs.

♻️ Proposed fix
 const getOpenPRsForBranch = async (octokit: Octokit, branch: string) => {
   const pullRequests = await octokit.paginate(octokit.rest.pulls.list, {
     owner: OWNER,
     repo: REPO,
     state: 'open',
+    head: `${OWNER}:${branch}`,
     per_page: PER_PAGE,
   })

-  return pullRequests.filter((pullRequest) => pullRequest.head.ref === branch)
+  return pullRequests
 }
🤖 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 `@script/deploy/github/verify-approvals.ts` around lines 99 - 108, Update
getOpenPRsForBranch to pass the GitHub pulls.list head parameter as
`${OWNER}:${branch}` while retaining the open state and pagination options.
Remove the client-side pullRequests.filter by head.ref so the API performs the
branch filtering.
🤖 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 `@script/deploy/deployUpgradesToSAFE.sh`:
- Line 37: Quote the GIT_BRANCH variable in the condition within
deployUpgradesToSAFE.sh, changing the comparison to use the repository-standard
quoted Bash variable form while preserving the existing main-branch check.
- Around line 49-58: Update the script-selection iteration around SCRIPTS to
preserve each newline-delimited facet name exactly, including whitespace and
glob characters: read the selections into an array or iterate with read -r, and
use an uppercase iteration variable. Use that preserved variable when
constructing UPDATE_SCRIPT and related messages.

In `@script/deploy/github/verify-approvals.ts`:
- Around line 140-155: Update getPRApprovers to deduplicate reviews by
user.login, retaining each user’s last submission in the oldest-first reviews
list before filtering for APPROVED and returning logins. Ensure a later
CHANGES_REQUESTED removes that user from approvers, and add coverage for an
approve-then-request-changes sequence.

---

Nitpick comments:
In `@script/deploy/github/verify-approvals.ts`:
- Around line 99-108: Update getOpenPRsForBranch to pass the GitHub pulls.list
head parameter as `${OWNER}:${branch}` while retaining the open state and
pagination options. Remove the client-side pullRequests.filter by head.ref so
the API performs the branch filtering.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4c3a62e4-57f3-40f3-b870-d5668ddbad30

📥 Commits

Reviewing files that changed from the base of the PR and between 358c2b9 and 22b6c3c.

📒 Files selected for processing (4)
  • .env.example
  • script/deploy/deployUpgradesToSAFE.sh
  • script/deploy/github/verify-approvals.test.ts
  • script/deploy/github/verify-approvals.ts

Comment thread script/deploy/deployUpgradesToSAFE.sh Outdated
Comment thread script/deploy/deployUpgradesToSAFE.sh Outdated
Comment thread script/deploy/github/verify-approvals.ts
…n facet loop (EXSC-687)

- getPRApprovers now dedupes reviews to each user's latest state-changing
  submission, so a later CHANGES_REQUESTED or a dismissal supersedes an
  earlier approval (COMMENTED leaves it standing); covered by three new tests
- quote GIT_BRANCH in the main-branch condition
- iterate selected facet names with a fd-3 read loop instead of unquoted
  word-splitting, so names are never glob-expanded and inner commands keep
  their own stdin

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants