Skip to content

docs(gmail-native): correct the Desktop OAuth client claim #1477

docs(gmail-native): correct the Desktop OAuth client claim

docs(gmail-native): correct the Desktop OAuth client claim #1477

Workflow file for this run

name: PR Review (Scheduled)
on:
pull_request:
types: [opened, synchronize, reopened]
schedule:
- cron: '*/5 * * * *'
workflow_dispatch: {}
# Slash command: a maintainer comments "/review" on a PR to trigger an
# immediate targeted review. issue_comment always runs in the base repo
# with a full-permission GITHUB_TOKEN, so fork PRs work too (unlike the
# pull_request path) and there is no dependency on the best-effort cron.
issue_comment:
types: [created]
concurrency:
# /review commands get their own group per PR so they neither cancel
# nor get cancelled by the scheduled poller (or other PRs' commands).
group: pr-review-poller-${{ github.event_name == 'issue_comment' && github.event.issue.number || 'poll' }}
cancel-in-progress: true
permissions:
statuses: write
issues: write
# write needed for the 👀 ack reaction on /review comments — reactions on
# PR comments are pull-request scope (403 with read-only)
pull-requests: write
jobs:
poll-and-review:
runs-on: ubuntu-latest
timeout-minutes: 5
# On pull_request events from forks, GITHUB_TOKEN is read-only, so writing
# the commit status 403s and the run fails. Skip the event-driven path for
# fork PRs — the scheduled cron (full permissions) still reviews them.
# schedule/workflow_dispatch always run; same-repo PRs run on the event.
# issue_comment runs only for "/review" comments on PRs from maintainers
# (OWNER/MEMBER/COLLABORATOR) — comment body is never interpolated into
# shell, only the numeric issue number is used.
if: >-
(github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
startsWith(github.event.comment.body, '/review') &&
contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)) ||
(github.event_name != 'issue_comment' &&
(github.event_name != 'pull_request' ||
github.event.pull_request.head.repo.full_name == github.repository))
steps:
- name: Find PRs needing review
id: poll
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -eo pipefail
if [ "${{ github.event_name }}" = "issue_comment" ]; then
# Targeted path: "/review" comment on a PR from a maintainer
# (association gated at the job level). Ack with 👀 so the
# requester knows the command was picked up.
PR_NUMBER=${{ github.event.issue.number }}
FORCE=1
gh api "repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions" \
-f content=eyes >/dev/null || true
PRS=$(gh api "repos/${{ github.repository }}/pulls/${PR_NUMBER}" \
--jq 'select(
.state == "open" and
.draft == false and
(any(.labels[]; .name == "review-limit-reached") | not)
) | {number, sha: .head.sha, labels: [.labels[].name]}' | jq -s '.')
if [ "$(echo "$PRS" | jq 'length')" -eq 0 ]; then
echo "PR #${PR_NUMBER}: not eligible (closed, draft, or review-limit-reached)"
fi
else
FORCE=0
# Collect eligible open PRs (non-draft, no review-limit-reached label)
# Include safe-to-review labeled PRs regardless of author_association
# Note: --paginate with array jq emits one array per page; stream objects then collect
PRS=$(gh api --paginate "repos/${{ github.repository }}/pulls?state=open&per_page=100" \
--jq '.[] | select(
.draft == false and
(any(.labels[]; .name == "review-limit-reached") | not) and
(
(.author_association | IN("OWNER","MEMBER","COLLABORATOR","CONTRIBUTOR")) or
any(.labels[]; .name == "safe-to-review")
)
) | {number, sha: .head.sha, labels: [.labels[].name]}' | jq -s '.')
fi
TARGETS="[]"
while IFS= read -r ROW; do
NUMBER=$(echo "$ROW" | jq -r '.number')
SHA=$(echo "$ROW" | jq -r '.sha')
LABELS=$(echo "$ROW" | jq -c '.labels')
# Get latest status for this SHA (single API call)
STATUS_JSON=$(gh api "repos/${{ github.repository }}/commits/${SHA}/statuses" \
--jq '[.[] | select(.context == "OpenAB PR Review")] | sort_by(.created_at) | last | {state, created_at} // empty')
STATE=$(echo "$STATUS_JSON" | jq -r '.state // empty')
# Skip if still pending, UNLESS pending > 30 min (stale)
if [ "$STATE" = "pending" ]; then
PENDING_AT=$(echo "$STATUS_JSON" | jq -r '.created_at // empty')
if [ -n "$PENDING_AT" ]; then
PENDING_TS=$(date -d "$PENDING_AT" +%s 2>/dev/null || date -u +%s)
NOW_TS=$(date -u +%s)
ELAPSED=$(( NOW_TS - PENDING_TS ))
if [ "$ELAPSED" -lt 1800 ]; then
echo "PR #${NUMBER}: still pending (${ELAPSED}s), skipping"
continue
fi
echo "PR #${NUMBER}: pending stale (${ELAPSED}s > 30m), re-triggering"
fi
fi
# Skip if already reviewed this exact SHA (success or failure)
# failure = CHANGES REQUESTED — only re-review when new commits are pushed (new SHA)
# An explicit /review command (FORCE=1) bypasses this: a maintainer
# asking again means re-review even if this SHA was already reviewed.
if [ "$FORCE" -eq 0 ] && { [ "$STATE" = "success" ] || [ "$STATE" = "failure" ]; }; then
echo "PR #${NUMBER}: already reviewed on HEAD (${STATE}), skipping"
continue
fi
# Trigger review: no status yet, or error (webhook failure), or stale pending
echo "PR #${NUMBER}: needs review (state=${STATE:-none})"
TARGETS=$(echo "$TARGETS" | jq --argjson row "$ROW" '. + [$row]')
done < <(echo "$PRS" | jq -c '.[]')
echo "targets=$(echo "$TARGETS" | jq -c '.')" >> "$GITHUB_OUTPUT"
echo "count=$(echo "$TARGETS" | jq 'length')" >> "$GITHUB_OUTPUT"
- name: Trigger reviews
if: fromJSON(steps.poll.outputs.count) > 0
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
WEBHOOK_URL: ${{ secrets.OAB_REVIEW_ACTION_WEBHOOK }}
BOT_UID: ${{ secrets.OAB_REVIEW_ACTION_BOT_UID }}
TARGETS: ${{ steps.poll.outputs.targets }}
run: |
set -eo pipefail
while IFS= read -r ROW; do
NUMBER=$(echo "$ROW" | jq -r '.number')
SHA=$(echo "$ROW" | jq -r '.sha')
LABELS=$(echo "$ROW" | jq -c '.labels')
# Circuit breaker check
COUNT=$(gh api --paginate "repos/${{ github.repository }}/pulls/${NUMBER}/commits" \
--jq '.[].sha' | while read -r C_SHA; do
gh api "repos/${{ github.repository }}/commits/${C_SHA}/statuses" \
--paginate --jq '[.[] | select(.context == "OpenAB PR Review" and .state == "pending")] | length'
done | awk '{s+=$1} END {print s+0}')
if [ "${COUNT}" -ge 30 ]; then
gh api "repos/${{ github.repository }}/issues/${NUMBER}/labels" \
--method POST -f "labels[]=review-limit-reached"
gh api "repos/${{ github.repository }}/statuses/${SHA}" \
-f state="error" \
-f context="OpenAB PR Review" \
-f description="Circuit breaker: exceeded 30 review cycles"
echo "::error::PR #${NUMBER}: circuit breaker triggered"
continue
fi
# Set pending status
gh api "repos/${{ github.repository }}/statuses/${SHA}" \
-f state="pending" \
-f context="OpenAB PR Review" \
-f description="Review in progress..." \
-f target_url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
# Trigger Discord webhook
PR_URL="https://github.com/${{ github.repository }}/pull/${NUMBER}"
MODE=""
if echo "$LABELS" | jq -e 'any(. == "auto-fix")' > /dev/null 2>&1; then
MODE="\n__mode: auto-fix__"
fi
PAYLOAD=$(printf '<@%s> lead group review %s\n\n__commit: %s__%s' "$BOT_UID" "$PR_URL" "$SHA" "$MODE" | jq -Rs '{content: .}')
if ! curl -sS --fail-with-body --max-time 10 -X POST "$WEBHOOK_URL" \
-H "Content-Type: application/json" -d "$PAYLOAD"; then
gh api "repos/${{ github.repository }}/statuses/${SHA}" \
-f state="error" \
-f context="OpenAB PR Review" \
-f description="Failed to trigger review — webhook error"
echo "::warning::PR #${NUMBER}: webhook failed"
fi
echo "PR #${NUMBER}: review triggered"
done < <(echo "$TARGETS" | jq -c '.[]')