diff --git a/.claude/skills/open-pr/SKILL.md b/.claude/skills/open-pr/SKILL.md index 6bd88e3c944..e462e860889 100644 --- a/.claude/skills/open-pr/SKILL.md +++ b/.claude/skills/open-pr/SKILL.md @@ -318,6 +318,15 @@ a 20-minute poll on the global list never saw the builds). Match the ## Gotchas +- **Never update a PR branch with `GITHUB_TOKEN`** (`update-branch` + from a workflow, or any push as `github-actions[bot]`) when its CI + has to run: GitHub holds every run on that head at + `action_required`, and the PR sits BLOCKED with no checks until + someone approves them (`gh api -X POST + repos/{owner}/{repo}/actions/runs//approve`). The `main` ruleset + is not strict, so a branch behind `main` merges anyway; this is why + `auto-update-pr-branches.yml` was retired after it put eight such + commits on #12229 (2026-10-22). - **`isOutdated` ≠ `isResolved`.** A fix-push can outdate a Copilot thread while it stays unresolved; outdated threads still count against review-clean — resolve them explicitly. diff --git a/.github/workflows/auto-update-pr-branches.yml b/.github/workflows/auto-update-pr-branches.yml deleted file mode 100644 index 3459b9c2ce4..00000000000 --- a/.github/workflows/auto-update-pr-branches.yml +++ /dev/null @@ -1,137 +0,0 @@ -name: "Auto-update PR branches" -run-name: "Auto-update PR branches after push to ${{ github.ref_name }}" - -# When `main` advances, update every open PR that has auto-merge enabled -# and is BEHIND, so polish / impl PRs squash-merge themselves end-to-end -# without manual "Update branch" clicks. -# -# NOTE (verified 2026-07-24): the `main` ruleset currently sets -# `strict_required_status_checks_policy: false`, so an up-to-date head is -# NOT actually required to merge. This file used to claim the opposite. -# That means every update-branch call is optional work which also -# invalidates the green checks on the previous head and forces a full CI -# re-run. Retiring this workflow is worth evaluating — see #9772; it is -# kept for now because the ruleset could be tightened again, and because -# behaviour changes here affect every auto-merge PR in the repo. -# -# Why we don't use GitHub's native merge queue: this repository is -# user-owned and merge_queue rules are not available on the account's -# plan — the rulesets API rejects the rule with an empty validation -# error. The auto-update workflow gives us the same end-user behavior -# (no manual "Update branch" clicks) without the plan dependency. - -on: - push: - branches: - - main - workflow_dispatch: - -permissions: - contents: write # update-branch creates a merge commit on the head ref - pull-requests: write # required to call PUT /pulls/:num/update-branch - -concurrency: - group: auto-update-pr-branches - cancel-in-progress: false - -jobs: - update: - runs-on: ubuntu-latest - steps: - - name: Wait for PR mergeable state to settle - # GitHub recomputes `mergeStateStatus` asynchronously after a - # push to main. If we list PRs immediately the field can still - # be "UNKNOWN" — and the cached head SHA on the PR can lag - # behind the actual ref, so update-branch returns "expected - # head sha didn't match current head ref." Give GitHub a - # moment to settle. - run: sleep 30 - - - name: Update PRs with auto-merge enabled - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_REPO: ${{ github.repository }} - run: | - set -eo pipefail - - # After a push to main, every open PR with auto-merge is by - # definition behind. Don't filter on `mergeStateStatus`: - # right after the push it can still be UNKNOWN, and we'd - # skip valid candidates. Just iterate every open - # auto-merge PR — update-branch is a no-op when the head - # is already up-to-date. - PRS=$(gh pr list \ - --repo "$GH_REPO" \ - --state open \ - --base main \ - --limit 200 \ - --json number,title,headRefName,autoMergeRequest) - - # Skip Dependabot branches. Updating them here is actively - # harmful: the update-branch merge commit is authored by - # github-actions[bot], and GitHub gates every workflow run - # triggered by that push behind manual approval - # ("action_required"). Because `main` advances every few minutes - # while the impl pipeline merges, the branch is re-updated long - # before anyone can approve — so CI never completes and - # auto-merge can never fire. PR #9674 accumulated 174 runs in - # 22 h that way: 162 action_required (github-actions[bot]) vs. - # only 4 green (dependabot[bot], from the original push). - # - # Leaving them alone is safe because the `main` ruleset is NOT - # strict (see the header) — a Dependabot PR merges while behind, - # so it never needs this update at all. Do NOT justify this by - # assuming Dependabot re-syncs them for us: it rebases on - # conflict / schedule / reopen, not on "merely behind", and it - # stops rebasing a branch entirely once a foreign commit lands on - # it. The point is that the head now stays put, so a branch this - # workflow already touched needs its gated run approved exactly - # once instead of being re-gated every few minutes. - # - # Skip spec PRs (`specification/`) for the same reason. spec-create - # has Claude open them as claude[bot] precisely so their CI runs - # without an approval, and its merge job turns auto-merge on only - # while it waits (up to 15 minutes) for those checks. An update here - # would put a github-actions[bot] merge commit on the head — CI held - # at action_required, and a head that no longer matches the SHA the - # merge job verified, which makes it turn auto-merge off again. - CANDIDATES=$(echo "$PRS" | jq -c ' - [ .[] - | select(.autoMergeRequest != null) - | select(.headRefName | (startswith("dependabot/") or startswith("specification/")) | not) - ] - ') - - SKIPPED=$(echo "$PRS" | jq ' - [ .[] - | select(.autoMergeRequest != null) - | select(.headRefName | startswith("dependabot/") or startswith("specification/")) - ] | length - ') - if [[ "$SKIPPED" -gt 0 ]]; then - echo "::notice::Skipping $SKIPPED Dependabot/spec PR(s) — updating them gates their CI behind manual approval, and the non-strict ruleset means being behind does not block the merge (see comment above)" - fi - - COUNT=$(echo "$CANDIDATES" | jq 'length') - echo "::notice::Found $COUNT open PR(s) with auto-merge enabled" - - if [[ "$COUNT" -eq 0 ]]; then - exit 0 - fi - - echo "$CANDIDATES" | jq -c '.[]' | while read -r pr; do - NUM=$(echo "$pr" | jq -r '.number') - TITLE=$(echo "$pr" | jq -r '.title') - BRANCH=$(echo "$pr" | jq -r '.headRefName') - echo "::notice::Updating PR #${NUM} (${BRANCH}): ${TITLE}" - - # Capture stderr so the actual GitHub error (conflict, - # SHA mismatch, etc.) lands in the workflow log instead - # of being swallowed. Failures here don't fail the job — - # one stuck PR shouldn't block the others. - if ! OUT=$(gh api -X PUT \ - "repos/${GH_REPO}/pulls/${NUM}/update-branch" \ - -H "Accept: application/vnd.github+json" 2>&1); then - echo "::warning::Could not update PR #${NUM}: ${OUT}" - fi - done diff --git a/agentic/commands/dependabot.md b/agentic/commands/dependabot.md index 41f3bf85f47..7fdee5a8540 100644 --- a/agentic/commands/dependabot.md +++ b/agentic/commands/dependabot.md @@ -26,14 +26,14 @@ known GitHub quirks. Every gotcha below cost real session round-trips. resulting merge commit is authored by `github-actions[bot]`, GitHub then gates that head's workflow runs behind manual approval (`action_required`, so the checks never report), and Dependabot permanently stops rebasing a branch once a foreign commit lands on it. PR #9674 - burned 174 runs in 22 h this way. `auto-update-pr-branches.yml` skips Dependabot branches - for the same reason (#9772). + burned 174 runs in 22 h this way, and #12229 collected eight such commits on 2026-10-22 + from `auto-update-pr-branches.yml`, which was retired for it. - **Already poisoned** (head commit authored by `github-actions[bot]`, checks stuck at `action_required`): approve the gated runs once — `gh api -X POST repos/{owner}/{repo}/actions/runs//approve`. Approved runs do report - the required contexts on a bot-authored head (verified on #9674), and since - `auto-update-pr-branches.yml` no longer touches these branches the head now stays put, so - one approval is enough. `@dependabot recreate` also works and additionally restores a + the required contexts on a bot-authored head (verified on #9674), and since no workflow + updates PR branches any more (only `impl-merge.yml` updates its own `implementation/` PRs) + the head stays put, so one approval is enough. `@dependabot recreate` also works and additionally restores a clean `dependabot[bot]` head, but it re-resolves the versions. - `mergeStateStatus` is computed async — after any update, poll it a few seconds until it stabilizes before deciding (UNKNOWN → BEHIND/CLEAN/BLOCKED). diff --git a/agentic/docs/project-guide.md b/agentic/docs/project-guide.md index 320881ef54d..ef5620104c7 100644 --- a/agentic/docs/project-guide.md +++ b/agentic/docs/project-guide.md @@ -764,7 +764,6 @@ Issue ready for maintainer review | **notify-deployment.yml** | Deployment notifications | | **util-claude.yml** | Claude utility workflow | | **daily-regen.yml** | Scheduled: picks the oldest specs, runs spec polish + cross-library similarity audit, then dispatches bulk-generate | -| **auto-update-pr-branches.yml** | When `main` advances, updates open PRs that have auto-merge enabled, except Dependabot and spec (`specification/`) PRs — its `GITHUB_TOKEN` merge commit would hold their CI at `action_required` | | **watchdog-stuck-jobs.yml** | Periodic safety net: detects and unsticks stalled impl-pipeline PRs | | **bot-serving-check.yml** | Daily synthetic monitor for the nginx bot -> seo-proxy path; routes and titles derived from `api/routers/seo.py` and the spec files, and a failure opens/comments the fixed-title issue "Bot serving check is red" (closed again by the next green run) | diff --git a/changelog.d/retire-auto-update-pr-branches.md b/changelog.d/retire-auto-update-pr-branches.md new file mode 100644 index 00000000000..00013a66766 --- /dev/null +++ b/changelog.d/retire-auto-update-pr-branches.md @@ -0,0 +1,9 @@ +### Removed + +- **The auto-update workflow for PR branches.** `auto-update-pr-branches.yml` + merged `main` into every open PR with auto-merge after each push to `main`. + The `main` ruleset's checks are not strict, so no PR needed it, and its + merge commits, authored by `github-actions[bot]`, held each PR's CI at + `action_required` until someone approved the runs by hand (eight times on + one PR on 2026-10-22). Auto-merge now fires on the PR's own green head. + (#12237) diff --git a/prompts/workflow-prompts/spec-polish-claude.md b/prompts/workflow-prompts/spec-polish-claude.md index 9ebabe375fb..276924b50a1 100644 --- a/prompts/workflow-prompts/spec-polish-claude.md +++ b/prompts/workflow-prompts/spec-polish-claude.md @@ -95,11 +95,10 @@ Then open the PR. Use a HEREDOC for the body so multi-line markdown survives: )") # Enable auto-merge so the PR squash-merges automatically once required - # checks (Run Linting / Tests / Frontend Tests) go green and the head - # is in sync with main. Auto-merge handles the strict status-check - # policy by updating the branch on our behalf. If auto-merge cannot be - # enabled (e.g. checks already complete and branch is up-to-date), this - # is non-fatal — the PR still exists for human follow-up. + # checks (Run Linting / Tests / Frontend Tests) go green. The main + # ruleset's status checks are not strict, so a branch behind main + # merges too. If auto-merge cannot be enabled (e.g. checks already + # complete), this is non-fatal — the PR still exists for human follow-up. gh pr merge "$PR_URL" --auto --squash --delete-branch || \ echo "::warning::Could not enable auto-merge for $PR_URL — leaving PR open for manual handling"