docs(gmail-native): correct the Desktop OAuth client claim #1477
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 '.[]') |