From f5470596499a5c7f5962ddd5753a5a2fac3861e9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 10 May 2026 17:58:10 +0000 Subject: [PATCH 1/4] feat(auto-rebase): detect stale PRs in daily status and add @claude fallback - org_status.sh: query headRefName/baseRefName, compute behind_by per PR via the REST compare API, surface a Needs Rebase column in the existing per-repo open-PRs table, and emit /tmp/needs-rebase.json for the follow-up commenter. - daily-org-status.yml: post an idempotent @claude rebase request on each stale PR (sentinel ) using GH_PAT_WORKFLOWS so the comment author is OWNER and the Claude reusable workflow fires. - auto-rebase-reusable.yml: when update-branch is blocked by the missing workflows permission, post an @claude rebase request via GH_PAT_WORKFLOWS (sentinel ); fall back to the original manual-rebase comment when GH_PAT_WORKFLOWS is unset. - standards/workflows/auto-rebase.yml: document the optional GH_PAT_WORKFLOWS secret that enables the @claude fallback for consumer repos. --- .github/workflows/auto-rebase-reusable.yml | 71 ++++++++++++++++------ .github/workflows/daily-org-status.yml | 40 ++++++++++++ scripts/org_status.sh | 37 ++++++++++- standards/workflows/auto-rebase.yml | 6 +- 4 files changed, 130 insertions(+), 24 deletions(-) diff --git a/.github/workflows/auto-rebase-reusable.yml b/.github/workflows/auto-rebase-reusable.yml index 66d77baa..ad24a2e6 100644 --- a/.github/workflows/auto-rebase-reusable.yml +++ b/.github/workflows/auto-rebase-reusable.yml @@ -20,12 +20,19 @@ # The merge method preserves the original commits. # # Failure modes handled gracefully: -# - without `workflows` permission (403): posts an idempotent comment asking -# the author to rebase manually (sentinel: ) +# - without `workflows` permission (403): posts an idempotent @claude comment +# asking Claude to perform an agentic rebase +# (sentinel: ). Requires GH_PAT_WORKFLOWS +# so the comment's author_association is OWNER — the Claude reusable +# workflow only fires on @claude mentions from OWNER/MEMBER/COLLABORATOR. +# Falls back to a manual-rebase request via github.token if the PAT is +# not configured (sentinel: ). # - merge conflict (422): posts an idempotent comment asking the author to # resolve conflicts (sentinel: ) # -# No secrets required — uses github.token only. No auto-merge logic. +# Optional secret: GH_PAT_WORKFLOWS (classic PAT or fine-grained with +# pull_requests: write) — needed for the @claude fallback. Inherited via +# `secrets: inherit` from the calling thin-stub workflow. name: Auto-rebase non-Dependabot PRs (Reusable) on: @@ -41,6 +48,7 @@ jobs: - name: Update behind non-Dependabot PRs env: GH_TOKEN: ${{ github.token }} + GH_PAT_WORKFLOWS: ${{ secrets.GH_PAT_WORKFLOWS }} REPO: ${{ github.repository }} run: | # Find open non-Dependabot PRs from the same repo (exclude forks) @@ -78,25 +86,48 @@ jobs: if echo "$UPDATE_OUTPUT" | grep -qF "without \`workflows\` permission"; then # GitHub blocks update-branch when merging the base would add # .github/workflows/ changes and the token lacks the 'workflows' - # permission. Ask the author to rebase manually. + # permission. Preferred fallback: ask Claude to rebase via an + # @claude mention. The mention only triggers Claude if the + # comment author has author_association OWNER/MEMBER/COLLABORATOR, + # so we must post via GH_PAT_WORKFLOWS (a human-owned PAT) and + # NOT the github-actions[bot] github.token. # - # Idempotent: skip if sentinel comment already exists. - SENTINEL="" - ALREADY_POSTED=$(gh pr view "$PR_NUMBER" --repo "$REPO" \ - --json comments --jq "[.comments[] | select(.body | contains(\"$SENTINEL\"))] | length") - if [[ "$ALREADY_POSTED" -gt 0 ]]; then - echo " Skipping — blocked comment already posted" + # If GH_PAT_WORKFLOWS is not configured, fall back to the + # original manual-rebase comment (no @claude mention). + if [[ -n "${GH_PAT_WORKFLOWS:-}" ]]; then + SENTINEL="" + ALREADY_POSTED=$(GH_TOKEN="$GH_PAT_WORKFLOWS" gh pr view "$PR_NUMBER" --repo "$REPO" \ + --json comments --jq "[.comments[] | select(.body | contains(\"$SENTINEL\"))] | length") + if [[ "$ALREADY_POSTED" -gt 0 ]]; then + echo " Skipping — Claude rebase request already posted" + else + echo " Posting @claude rebase request (workflows permission missing)" + CLAUDE_BODY="$SENTINEL" + CLAUDE_BODY+=$'\n'"@claude **Auto-rebase blocked** — the base branch (\`$BASE_BRANCH\`) contains \`.github/workflows/\` changes that require the \`workflows\` permission to merge into this branch, and the auto-rebase workflow's \`GITHUB_TOKEN\` does not have that scope." + CLAUDE_BODY+=$'\n\n'"Please rebase this branch onto \`origin/$BASE_BRANCH\`, resolve any conflicts, and force-push the result. If the rebase produces test or CI failures, fix them in the same push and leave a brief note explaining what changed." + CLAUDE_BODY+=$'\n\n'"\`\`\`"$'\n'"git fetch origin" + CLAUDE_BODY+=$'\n'"git rebase origin/$BASE_BRANCH" + CLAUDE_BODY+=$'\n'"git push --force-with-lease"$'\n'"\`\`\`" + GH_TOKEN="$GH_PAT_WORKFLOWS" gh pr comment "$PR_NUMBER" --repo "$REPO" --body "$CLAUDE_BODY" + fi else - echo " Posting manual-rebase request (workflows permission missing)" - BLOCKED_BODY="" - BLOCKED_BODY+=$'\n'"**Auto-rebase blocked** — the base branch contains \`.github/workflows/\` changes" - BLOCKED_BODY+=" that require the \`workflows\` permission to merge into this branch," - BLOCKED_BODY+=" but the auto-rebase workflow's token does not have that permission." - BLOCKED_BODY+=$'\n\n'"Please rebase this branch manually:" - BLOCKED_BODY+=$'\n'"\`\`\`"$'\n'"git fetch origin" - BLOCKED_BODY+=$'\n'"git rebase origin/$BASE_BRANCH" - BLOCKED_BODY+=$'\n'"git push --force-with-lease"$'\n'"\`\`\`" - gh pr comment "$PR_NUMBER" --repo "$REPO" --body "$BLOCKED_BODY" + SENTINEL="" + ALREADY_POSTED=$(gh pr view "$PR_NUMBER" --repo "$REPO" \ + --json comments --jq "[.comments[] | select(.body | contains(\"$SENTINEL\"))] | length") + if [[ "$ALREADY_POSTED" -gt 0 ]]; then + echo " Skipping — blocked comment already posted" + else + echo " Posting manual-rebase request (workflows permission missing; GH_PAT_WORKFLOWS unset)" + BLOCKED_BODY="" + BLOCKED_BODY+=$'\n'"**Auto-rebase blocked** — the base branch contains \`.github/workflows/\` changes" + BLOCKED_BODY+=" that require the \`workflows\` permission to merge into this branch," + BLOCKED_BODY+=" but the auto-rebase workflow's token does not have that permission." + BLOCKED_BODY+=$'\n\n'"Please rebase this branch manually:" + BLOCKED_BODY+=$'\n'"\`\`\`"$'\n'"git fetch origin" + BLOCKED_BODY+=$'\n'"git rebase origin/$BASE_BRANCH" + BLOCKED_BODY+=$'\n'"git push --force-with-lease"$'\n'"\`\`\`" + gh pr comment "$PR_NUMBER" --repo "$REPO" --body "$BLOCKED_BODY" + fi fi elif echo "$UPDATE_OUTPUT" | grep -qi "merge conflict"; then # Merge conflict — ask the author to resolve it. diff --git a/.github/workflows/daily-org-status.yml b/.github/workflows/daily-org-status.yml index 6ef5de85..6f354b5b 100644 --- a/.github/workflows/daily-org-status.yml +++ b/.github/workflows/daily-org-status.yml @@ -15,6 +15,7 @@ jobs: permissions: issues: write # create issue in this repo contents: read + pull-requests: write # post @claude rebase requests on stale PRs steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -31,6 +32,7 @@ jobs: env: GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS }} CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + REBASE_LIST_FILE: /tmp/needs-rebase.json run: | chmod +x scripts/org_status.sh bash scripts/org_status.sh > /tmp/report.md @@ -54,3 +56,41 @@ jobs: --title "Org Status — $DATE" \ --body-file /tmp/report.md \ --label daily-report + + - name: Request @claude rebase on stale PRs + # Posts an idempotent @claude comment on each PR flagged as needing + # rebase by org_status.sh (sentinel: ). + # Uses GH_PAT_WORKFLOWS so the comment author_association is OWNER — + # the Claude reusable workflow only fires for OWNER/MEMBER/COLLABORATOR. + env: + GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS }} + REBASE_LIST_FILE: /tmp/needs-rebase.json + run: | + [ -s "$REBASE_LIST_FILE" ] || { echo "No rebase list file — skipping"; exit 0; } + COUNT=$(jq 'length' "$REBASE_LIST_FILE") + if [ "$COUNT" -eq 0 ]; then + echo "No PRs need rebase" + exit 0 + fi + echo "Requesting @claude rebase on $COUNT PR(s)" + SENTINEL="" + jq -c '.[]' "$REBASE_LIST_FILE" | while IFS= read -r pr; do + REPO=$(echo "$pr" | jq -r '.repo') + NUMBER=$(echo "$pr" | jq -r '.number') + BASE=$(echo "$pr" | jq -r '.baseRefName') + BEHIND=$(echo "$pr" | jq -r '.behindBy') + ALREADY=$(gh pr view "$NUMBER" --repo "$REPO" \ + --json comments --jq "[.comments[] | select(.body | contains(\"$SENTINEL\"))] | length") + if [ "$ALREADY" -gt 0 ]; then + echo " $REPO#$NUMBER — request already posted, skipping" + continue + fi + BODY="$SENTINEL" + BODY+=$'\n'"@claude this branch is **$BEHIND commit(s) behind \`$BASE\`** and needs to be brought up to date before it can land." + BODY+=$'\n\n'"Please rebase (or merge) \`origin/$BASE\` into this branch, resolve any conflicts, and push the result. If the rebase reveals issues that require code changes, address them in the same push and leave a brief note explaining what changed." + BODY+=$'\n\n'"\`\`\`"$'\n'"git fetch origin" + BODY+=$'\n'"git rebase origin/$BASE" + BODY+=$'\n'"git push --force-with-lease"$'\n'"\`\`\`" + gh pr comment "$NUMBER" --repo "$REPO" --body "$BODY" + echo " $REPO#$NUMBER — comment posted" + done diff --git a/scripts/org_status.sh b/scripts/org_status.sh index 948b5a2d..e92a0be7 100644 --- a/scripts/org_status.sh +++ b/scripts/org_status.sh @@ -41,6 +41,7 @@ collect_classify_prs() { pageInfo{hasNextPage endCursor} nodes{ number title createdAt isDraft + headRefName baseRefName labels(first:20){nodes{name}} reviewDecision statusCheckRollup{state} @@ -70,6 +71,8 @@ collect_classify_prs() { title: .title, opened: (.createdAt | split("T")[0]), url: ("https://github.com/" + $owner + "/" + $repo + "/pull/" + (.number|tostring)), + headRefName: .headRefName, + baseRefName: .baseRefName, labels: [.labels.nodes[].name], isDraft: .isDraft, ci: (.statusCheckRollup.state // null), @@ -108,6 +111,32 @@ done echo "Total open PRs: $(echo "$ALL_PRS" | jq 'length')" >&2 echo "::endgroup::" >&2 +# ── Behind-Base Detection ───────────────────────────────────────────────────── +# For each PR, compute behind_by via the REST compare API. PRs with behind_by > 0 +# need to be rebased/merged with their base branch before they can land. +# (GraphQL has no equivalent field; mergeable only reports CONFLICTING.) +echo "::group::Computing behind_by per PR" >&2 +AUGMENTED='[]' +while IFS= read -r pr; do + [ -z "$pr" ] && continue + pr_repo=$(echo "$pr" | jq -r '.repo') + pr_head=$(echo "$pr" | jq -r '.headRefName') + pr_base=$(echo "$pr" | jq -r '.baseRefName') + behind=$(gh api "repos/${pr_repo}/compare/${pr_base}...${pr_head}" --jq '.behind_by' 2>/dev/null || echo 0) + [[ "$behind" =~ ^[0-9]+$ ]] || behind=0 + augmented_pr=$(echo "$pr" | jq --argjson b "$behind" '. + {behindBy: $b, needsRebase: ($b > 0)}') + AUGMENTED=$(jq -n --argjson a "$AUGMENTED" --argjson p "$augmented_pr" '$a + [$p]') +done <<< "$(echo "$ALL_PRS" | jq -c '.[]')" +ALL_PRS="$AUGMENTED" +NEEDS_REBASE_COUNT=$(echo "$ALL_PRS" | jq '[.[] | select(.needsRebase)] | length') +echo "PRs needing rebase: $NEEDS_REBASE_COUNT" >&2 +echo "::endgroup::" >&2 + +# Emit machine-readable list for the workflow's follow-up @claude commenter. +NEEDS_REBASE_LIST=$(echo "$ALL_PRS" | jq '[.[] | select(.needsRebase) | + {repo, number, headRefName, baseRefName, behindBy, url}]') +echo "$NEEDS_REBASE_LIST" > "${REBASE_LIST_FILE:-/tmp/needs-rebase.json}" + # Pre-aggregate PR counts by category per repo (keeps prompt size manageable) PR_BY_REPO=$(echo "$ALL_PRS" | jq ' sort_by(.repo) | group_by(.repo) | map({ @@ -120,7 +149,8 @@ PR_BY_REPO=$(echo "$ALL_PRS" | jq ' approved: ([.[] | select(.category=="Approved")] | length), awaiting_review: ([.[] | select(.category=="Awaiting Review")] | length), no_ci_policy: ([.[] | select(.category=="No CI / No Policy")] | length), - dep_bumps: ([.[] | select(.isDepBump)] | length) + dep_bumps: ([.[] | select(.isDepBump)] | length), + needs_rebase: ([.[] | select(.needsRebase)] | length) }) | sort_by(-.total)') NEEDS_REVIEW_PRS=$(echo "$ALL_PRS" | jq '[.[] | select(.needsHumanReview)]') @@ -288,10 +318,11 @@ Org-wide blocker summary table (sum all repos). You MUST include the header row Rows in this order: Awaiting Review, CI Failing, CI Pending, Changes Requested, Approved, Draft, No CI / No Policy, **TOTAL** Per-repo breakdown table (omit repos with 0 total PRs). You MUST include the header row and separator row: -| Repo | Total | Awaiting Review | CI Failing | CI Pending | Changes Req | Approved | No CI/Policy | Draft | -|---|---|---|---|---|---|---|---|---| +| Repo | Total | Awaiting Review | CI Failing | CI Pending | Changes Req | Approved | No CI/Policy | Draft | Needs Rebase | +|---|---|---|---|---|---|---|---|---|---| - Repo name as a link to the repo: [owner/repo](https://github.com/owner/repo) - Add ⚠ next to repo name if CI Failing > 5 or Awaiting Review > 10 +- Needs Rebase column: render the \`needs_rebase\` count from the data. If the count is 0 render —. If > 0 render the number followed by 🔄 (e.g. \`3 🔄\`). ### \`## Open PRs — Needs Human Review\` Full table for PRs with needsHumanReview == true: diff --git a/standards/workflows/auto-rebase.yml b/standards/workflows/auto-rebase.yml index 0e60e37c..f7b54165 100644 --- a/standards/workflows/auto-rebase.yml +++ b/standards/workflows/auto-rebase.yml @@ -18,7 +18,11 @@ # # Auto-rebase non-Dependabot PRs — thin caller for the org-level reusable. # To adopt: copy this file to .github/workflows/auto-rebase.yml in your repo. -# No secrets required — uses GITHUB_TOKEN only. +# Required secret: none (uses GITHUB_TOKEN for branch updates). +# Optional secret: GH_PAT_WORKFLOWS — if set, the reusable falls back to an +# @claude rebase request when update-branch is blocked by the missing +# `workflows` permission. Without it, the workflow only posts a manual +# rebase request to the PR author. name: Auto-rebase non-Dependabot PRs on: From 6809838f97c222347aeba3fdcb288df6e81378d9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 10 May 2026 17:59:41 +0000 Subject: [PATCH 2/4] fix(ci): wrap long lines in rebase comment bodies (yamllint 200-col limit) --- .github/workflows/auto-rebase-reusable.yml | 6 ++++-- .github/workflows/daily-org-status.yml | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/auto-rebase-reusable.yml b/.github/workflows/auto-rebase-reusable.yml index ad24a2e6..beb86ffb 100644 --- a/.github/workflows/auto-rebase-reusable.yml +++ b/.github/workflows/auto-rebase-reusable.yml @@ -103,8 +103,10 @@ jobs: else echo " Posting @claude rebase request (workflows permission missing)" CLAUDE_BODY="$SENTINEL" - CLAUDE_BODY+=$'\n'"@claude **Auto-rebase blocked** — the base branch (\`$BASE_BRANCH\`) contains \`.github/workflows/\` changes that require the \`workflows\` permission to merge into this branch, and the auto-rebase workflow's \`GITHUB_TOKEN\` does not have that scope." - CLAUDE_BODY+=$'\n\n'"Please rebase this branch onto \`origin/$BASE_BRANCH\`, resolve any conflicts, and force-push the result. If the rebase produces test or CI failures, fix them in the same push and leave a brief note explaining what changed." + CLAUDE_BODY+=$'\n'"@claude **Auto-rebase blocked** — the base branch (\`$BASE_BRANCH\`) contains \`.github/workflows/\` changes that require the" + CLAUDE_BODY+=" \`workflows\` permission to merge into this branch, and the auto-rebase workflow's \`GITHUB_TOKEN\` does not have that scope." + CLAUDE_BODY+=$'\n\n'"Please rebase this branch onto \`origin/$BASE_BRANCH\`, resolve any conflicts, and force-push the result." + CLAUDE_BODY+=" If the rebase produces test or CI failures, fix them in the same push and leave a brief note explaining what changed." CLAUDE_BODY+=$'\n\n'"\`\`\`"$'\n'"git fetch origin" CLAUDE_BODY+=$'\n'"git rebase origin/$BASE_BRANCH" CLAUDE_BODY+=$'\n'"git push --force-with-lease"$'\n'"\`\`\`" diff --git a/.github/workflows/daily-org-status.yml b/.github/workflows/daily-org-status.yml index 6f354b5b..f5b79b36 100644 --- a/.github/workflows/daily-org-status.yml +++ b/.github/workflows/daily-org-status.yml @@ -87,7 +87,8 @@ jobs: fi BODY="$SENTINEL" BODY+=$'\n'"@claude this branch is **$BEHIND commit(s) behind \`$BASE\`** and needs to be brought up to date before it can land." - BODY+=$'\n\n'"Please rebase (or merge) \`origin/$BASE\` into this branch, resolve any conflicts, and push the result. If the rebase reveals issues that require code changes, address them in the same push and leave a brief note explaining what changed." + BODY+=$'\n\n'"Please rebase (or merge) \`origin/$BASE\` into this branch, resolve any conflicts, and push the result." + BODY+=" If the rebase reveals issues that require code changes, address them in the same push and leave a brief note explaining what changed." BODY+=$'\n\n'"\`\`\`"$'\n'"git fetch origin" BODY+=$'\n'"git rebase origin/$BASE" BODY+=$'\n'"git push --force-with-lease"$'\n'"\`\`\`" From 012e09271d2dbb0ddf531c9234afe685e8cfdfff Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 10 May 2026 18:39:06 +0000 Subject: [PATCH 3/4] =?UTF-8?q?refactor:=20scope=20PR=20to=20reporting=20o?= =?UTF-8?q?nly=20=E2=80=94=20drop=20@claude=20rebase=20actions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Following manual validation, @claude cannot perform branch operations (rebase/merge/history rewrite), so the agentic rebase fallback in #231 will not work. Reduce the PR to its reporting-only core: - Revert .github/workflows/auto-rebase-reusable.yml and standards/workflows/auto-rebase.yml to main. - Revert .github/workflows/daily-org-status.yml (drop the @claude comment step, pull-requests: write permission, REBASE_LIST_FILE env). - scripts/org_status.sh: drop the /tmp/needs-rebase.json sidecar; keep the behind_by detection and Needs Rebase column in the existing per-repo open-PRs table. --- .github/workflows/auto-rebase-reusable.yml | 73 ++++++---------------- .github/workflows/daily-org-status.yml | 41 ------------ scripts/org_status.sh | 5 -- standards/workflows/auto-rebase.yml | 6 +- 4 files changed, 21 insertions(+), 104 deletions(-) diff --git a/.github/workflows/auto-rebase-reusable.yml b/.github/workflows/auto-rebase-reusable.yml index beb86ffb..66d77baa 100644 --- a/.github/workflows/auto-rebase-reusable.yml +++ b/.github/workflows/auto-rebase-reusable.yml @@ -20,19 +20,12 @@ # The merge method preserves the original commits. # # Failure modes handled gracefully: -# - without `workflows` permission (403): posts an idempotent @claude comment -# asking Claude to perform an agentic rebase -# (sentinel: ). Requires GH_PAT_WORKFLOWS -# so the comment's author_association is OWNER — the Claude reusable -# workflow only fires on @claude mentions from OWNER/MEMBER/COLLABORATOR. -# Falls back to a manual-rebase request via github.token if the PAT is -# not configured (sentinel: ). +# - without `workflows` permission (403): posts an idempotent comment asking +# the author to rebase manually (sentinel: ) # - merge conflict (422): posts an idempotent comment asking the author to # resolve conflicts (sentinel: ) # -# Optional secret: GH_PAT_WORKFLOWS (classic PAT or fine-grained with -# pull_requests: write) — needed for the @claude fallback. Inherited via -# `secrets: inherit` from the calling thin-stub workflow. +# No secrets required — uses github.token only. No auto-merge logic. name: Auto-rebase non-Dependabot PRs (Reusable) on: @@ -48,7 +41,6 @@ jobs: - name: Update behind non-Dependabot PRs env: GH_TOKEN: ${{ github.token }} - GH_PAT_WORKFLOWS: ${{ secrets.GH_PAT_WORKFLOWS }} REPO: ${{ github.repository }} run: | # Find open non-Dependabot PRs from the same repo (exclude forks) @@ -86,50 +78,25 @@ jobs: if echo "$UPDATE_OUTPUT" | grep -qF "without \`workflows\` permission"; then # GitHub blocks update-branch when merging the base would add # .github/workflows/ changes and the token lacks the 'workflows' - # permission. Preferred fallback: ask Claude to rebase via an - # @claude mention. The mention only triggers Claude if the - # comment author has author_association OWNER/MEMBER/COLLABORATOR, - # so we must post via GH_PAT_WORKFLOWS (a human-owned PAT) and - # NOT the github-actions[bot] github.token. + # permission. Ask the author to rebase manually. # - # If GH_PAT_WORKFLOWS is not configured, fall back to the - # original manual-rebase comment (no @claude mention). - if [[ -n "${GH_PAT_WORKFLOWS:-}" ]]; then - SENTINEL="" - ALREADY_POSTED=$(GH_TOKEN="$GH_PAT_WORKFLOWS" gh pr view "$PR_NUMBER" --repo "$REPO" \ - --json comments --jq "[.comments[] | select(.body | contains(\"$SENTINEL\"))] | length") - if [[ "$ALREADY_POSTED" -gt 0 ]]; then - echo " Skipping — Claude rebase request already posted" - else - echo " Posting @claude rebase request (workflows permission missing)" - CLAUDE_BODY="$SENTINEL" - CLAUDE_BODY+=$'\n'"@claude **Auto-rebase blocked** — the base branch (\`$BASE_BRANCH\`) contains \`.github/workflows/\` changes that require the" - CLAUDE_BODY+=" \`workflows\` permission to merge into this branch, and the auto-rebase workflow's \`GITHUB_TOKEN\` does not have that scope." - CLAUDE_BODY+=$'\n\n'"Please rebase this branch onto \`origin/$BASE_BRANCH\`, resolve any conflicts, and force-push the result." - CLAUDE_BODY+=" If the rebase produces test or CI failures, fix them in the same push and leave a brief note explaining what changed." - CLAUDE_BODY+=$'\n\n'"\`\`\`"$'\n'"git fetch origin" - CLAUDE_BODY+=$'\n'"git rebase origin/$BASE_BRANCH" - CLAUDE_BODY+=$'\n'"git push --force-with-lease"$'\n'"\`\`\`" - GH_TOKEN="$GH_PAT_WORKFLOWS" gh pr comment "$PR_NUMBER" --repo "$REPO" --body "$CLAUDE_BODY" - fi + # Idempotent: skip if sentinel comment already exists. + SENTINEL="" + ALREADY_POSTED=$(gh pr view "$PR_NUMBER" --repo "$REPO" \ + --json comments --jq "[.comments[] | select(.body | contains(\"$SENTINEL\"))] | length") + if [[ "$ALREADY_POSTED" -gt 0 ]]; then + echo " Skipping — blocked comment already posted" else - SENTINEL="" - ALREADY_POSTED=$(gh pr view "$PR_NUMBER" --repo "$REPO" \ - --json comments --jq "[.comments[] | select(.body | contains(\"$SENTINEL\"))] | length") - if [[ "$ALREADY_POSTED" -gt 0 ]]; then - echo " Skipping — blocked comment already posted" - else - echo " Posting manual-rebase request (workflows permission missing; GH_PAT_WORKFLOWS unset)" - BLOCKED_BODY="" - BLOCKED_BODY+=$'\n'"**Auto-rebase blocked** — the base branch contains \`.github/workflows/\` changes" - BLOCKED_BODY+=" that require the \`workflows\` permission to merge into this branch," - BLOCKED_BODY+=" but the auto-rebase workflow's token does not have that permission." - BLOCKED_BODY+=$'\n\n'"Please rebase this branch manually:" - BLOCKED_BODY+=$'\n'"\`\`\`"$'\n'"git fetch origin" - BLOCKED_BODY+=$'\n'"git rebase origin/$BASE_BRANCH" - BLOCKED_BODY+=$'\n'"git push --force-with-lease"$'\n'"\`\`\`" - gh pr comment "$PR_NUMBER" --repo "$REPO" --body "$BLOCKED_BODY" - fi + echo " Posting manual-rebase request (workflows permission missing)" + BLOCKED_BODY="" + BLOCKED_BODY+=$'\n'"**Auto-rebase blocked** — the base branch contains \`.github/workflows/\` changes" + BLOCKED_BODY+=" that require the \`workflows\` permission to merge into this branch," + BLOCKED_BODY+=" but the auto-rebase workflow's token does not have that permission." + BLOCKED_BODY+=$'\n\n'"Please rebase this branch manually:" + BLOCKED_BODY+=$'\n'"\`\`\`"$'\n'"git fetch origin" + BLOCKED_BODY+=$'\n'"git rebase origin/$BASE_BRANCH" + BLOCKED_BODY+=$'\n'"git push --force-with-lease"$'\n'"\`\`\`" + gh pr comment "$PR_NUMBER" --repo "$REPO" --body "$BLOCKED_BODY" fi elif echo "$UPDATE_OUTPUT" | grep -qi "merge conflict"; then # Merge conflict — ask the author to resolve it. diff --git a/.github/workflows/daily-org-status.yml b/.github/workflows/daily-org-status.yml index f5b79b36..6ef5de85 100644 --- a/.github/workflows/daily-org-status.yml +++ b/.github/workflows/daily-org-status.yml @@ -15,7 +15,6 @@ jobs: permissions: issues: write # create issue in this repo contents: read - pull-requests: write # post @claude rebase requests on stale PRs steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -32,7 +31,6 @@ jobs: env: GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS }} CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - REBASE_LIST_FILE: /tmp/needs-rebase.json run: | chmod +x scripts/org_status.sh bash scripts/org_status.sh > /tmp/report.md @@ -56,42 +54,3 @@ jobs: --title "Org Status — $DATE" \ --body-file /tmp/report.md \ --label daily-report - - - name: Request @claude rebase on stale PRs - # Posts an idempotent @claude comment on each PR flagged as needing - # rebase by org_status.sh (sentinel: ). - # Uses GH_PAT_WORKFLOWS so the comment author_association is OWNER — - # the Claude reusable workflow only fires for OWNER/MEMBER/COLLABORATOR. - env: - GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS }} - REBASE_LIST_FILE: /tmp/needs-rebase.json - run: | - [ -s "$REBASE_LIST_FILE" ] || { echo "No rebase list file — skipping"; exit 0; } - COUNT=$(jq 'length' "$REBASE_LIST_FILE") - if [ "$COUNT" -eq 0 ]; then - echo "No PRs need rebase" - exit 0 - fi - echo "Requesting @claude rebase on $COUNT PR(s)" - SENTINEL="" - jq -c '.[]' "$REBASE_LIST_FILE" | while IFS= read -r pr; do - REPO=$(echo "$pr" | jq -r '.repo') - NUMBER=$(echo "$pr" | jq -r '.number') - BASE=$(echo "$pr" | jq -r '.baseRefName') - BEHIND=$(echo "$pr" | jq -r '.behindBy') - ALREADY=$(gh pr view "$NUMBER" --repo "$REPO" \ - --json comments --jq "[.comments[] | select(.body | contains(\"$SENTINEL\"))] | length") - if [ "$ALREADY" -gt 0 ]; then - echo " $REPO#$NUMBER — request already posted, skipping" - continue - fi - BODY="$SENTINEL" - BODY+=$'\n'"@claude this branch is **$BEHIND commit(s) behind \`$BASE\`** and needs to be brought up to date before it can land." - BODY+=$'\n\n'"Please rebase (or merge) \`origin/$BASE\` into this branch, resolve any conflicts, and push the result." - BODY+=" If the rebase reveals issues that require code changes, address them in the same push and leave a brief note explaining what changed." - BODY+=$'\n\n'"\`\`\`"$'\n'"git fetch origin" - BODY+=$'\n'"git rebase origin/$BASE" - BODY+=$'\n'"git push --force-with-lease"$'\n'"\`\`\`" - gh pr comment "$NUMBER" --repo "$REPO" --body "$BODY" - echo " $REPO#$NUMBER — comment posted" - done diff --git a/scripts/org_status.sh b/scripts/org_status.sh index e92a0be7..358cb955 100644 --- a/scripts/org_status.sh +++ b/scripts/org_status.sh @@ -132,11 +132,6 @@ NEEDS_REBASE_COUNT=$(echo "$ALL_PRS" | jq '[.[] | select(.needsRebase)] | length echo "PRs needing rebase: $NEEDS_REBASE_COUNT" >&2 echo "::endgroup::" >&2 -# Emit machine-readable list for the workflow's follow-up @claude commenter. -NEEDS_REBASE_LIST=$(echo "$ALL_PRS" | jq '[.[] | select(.needsRebase) | - {repo, number, headRefName, baseRefName, behindBy, url}]') -echo "$NEEDS_REBASE_LIST" > "${REBASE_LIST_FILE:-/tmp/needs-rebase.json}" - # Pre-aggregate PR counts by category per repo (keeps prompt size manageable) PR_BY_REPO=$(echo "$ALL_PRS" | jq ' sort_by(.repo) | group_by(.repo) | map({ diff --git a/standards/workflows/auto-rebase.yml b/standards/workflows/auto-rebase.yml index f7b54165..0e60e37c 100644 --- a/standards/workflows/auto-rebase.yml +++ b/standards/workflows/auto-rebase.yml @@ -18,11 +18,7 @@ # # Auto-rebase non-Dependabot PRs — thin caller for the org-level reusable. # To adopt: copy this file to .github/workflows/auto-rebase.yml in your repo. -# Required secret: none (uses GITHUB_TOKEN for branch updates). -# Optional secret: GH_PAT_WORKFLOWS — if set, the reusable falls back to an -# @claude rebase request when update-branch is blocked by the missing -# `workflows` permission. Without it, the workflow only posts a manual -# rebase request to the PR author. +# No secrets required — uses GITHUB_TOKEN only. name: Auto-rebase non-Dependabot PRs on: From 953a3758ac40c4fde3253298467684f592f4b618 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 10 May 2026 19:01:22 +0000 Subject: [PATCH 4/4] perf(org-status): replace O(n^2) behind_by accumulation with NDJSON slurp Address Copilot review on PR #231: - Accumulate one augmented PR per line of NDJSON, then slurp into a single array at the end via jq -s '.'. Avoids reparsing a growing array on each iteration of the loop, which was O(n^2) in time and memory. - Replace silent compare-API fallback (|| echo 0) with a warning logged to stderr so transient API failures or fork-PR 404s are visible in the workflow log instead of silently zeroing behindBy. --- scripts/org_status.sh | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/scripts/org_status.sh b/scripts/org_status.sh index 358cb955..1a7eec18 100644 --- a/scripts/org_status.sh +++ b/scripts/org_status.sh @@ -116,18 +116,25 @@ echo "::endgroup::" >&2 # need to be rebased/merged with their base branch before they can land. # (GraphQL has no equivalent field; mergeable only reports CONFLICTING.) echo "::group::Computing behind_by per PR" >&2 -AUGMENTED='[]' +# Accumulate one augmented PR per line of NDJSON, then slurp into a single +# array at the end — avoids O(n²) reparse of a growing array each iteration. +AUGMENTED_NDJSON="" while IFS= read -r pr; do [ -z "$pr" ] && continue pr_repo=$(echo "$pr" | jq -r '.repo') pr_head=$(echo "$pr" | jq -r '.headRefName') pr_base=$(echo "$pr" | jq -r '.baseRefName') - behind=$(gh api "repos/${pr_repo}/compare/${pr_base}...${pr_head}" --jq '.behind_by' 2>/dev/null || echo 0) + if compare_response=$(gh api "repos/${pr_repo}/compare/${pr_base}...${pr_head}" --jq '.behind_by' 2>&1); then + behind="$compare_response" + else + echo " WARN: compare ${pr_repo} ${pr_base}...${pr_head} failed — treating as up to date: $compare_response" >&2 + behind=0 + fi [[ "$behind" =~ ^[0-9]+$ ]] || behind=0 - augmented_pr=$(echo "$pr" | jq --argjson b "$behind" '. + {behindBy: $b, needsRebase: ($b > 0)}') - AUGMENTED=$(jq -n --argjson a "$AUGMENTED" --argjson p "$augmented_pr" '$a + [$p]') + AUGMENTED_NDJSON+=$(echo "$pr" | jq -c --argjson b "$behind" '. + {behindBy: $b, needsRebase: ($b > 0)}') + AUGMENTED_NDJSON+=$'\n' done <<< "$(echo "$ALL_PRS" | jq -c '.[]')" -ALL_PRS="$AUGMENTED" +ALL_PRS=$(echo "$AUGMENTED_NDJSON" | jq -cs '.') NEEDS_REBASE_COUNT=$(echo "$ALL_PRS" | jq '[.[] | select(.needsRebase)] | length') echo "PRs needing rebase: $NEEDS_REBASE_COUNT" >&2 echo "::endgroup::" >&2