From 2df2f717f8ec8db27d769f868b56ee872b2d7642 Mon Sep 17 00:00:00 2001 From: DJ Date: Sun, 5 Apr 2026 19:10:29 -0700 Subject: [PATCH 1/3] feat: add dependabot-rebase workflow to unblock auto-merge serialization When strict status checks require branches to be up-to-date, merging one Dependabot PR makes others fall behind. Dependabot only rebases on its weekly schedule, leaving auto-merge stalled. This workflow triggers on push to main and comments @dependabot rebase on behind PRs, preserving Dependabot's commit signature for fetch-metadata verification. Co-Authored-By: Claude Opus 4.6 (1M context) --- standards/dependabot-policy.md | 31 ++++++++-- standards/workflows/dependabot-rebase.yml | 73 +++++++++++++++++++++++ 2 files changed, 99 insertions(+), 5 deletions(-) create mode 100644 standards/workflows/dependabot-rebase.yml diff --git a/standards/dependabot-policy.md b/standards/dependabot-policy.md index 9b282c3..a01d0ca 100644 --- a/standards/dependabot-policy.md +++ b/standards/dependabot-policy.md @@ -38,6 +38,7 @@ Each repository must have: |------|---------| | `.github/dependabot.yml` | Dependabot config scoped to the repo's ecosystems | | `.github/workflows/dependabot-automerge.yml` | Auto-approve + squash-merge security PRs | +| `.github/workflows/dependabot-rebase.yml` | Rebase behind Dependabot PRs after merges | | `.github/workflows/dependency-audit.yml` | CI check — fail on known vulnerabilities | ## Dependabot Templates @@ -147,6 +148,25 @@ Behavior: - **Major** updates are left for human review - Uses `gh pr merge --auto --squash` so the merge only happens after CI passes +## Rebase Behind PRs Workflow + +See [`workflows/dependabot-rebase.yml`](workflows/dependabot-rebase.yml). + +When branch protection requires branches to be up-to-date (`strict: true`), +merging one Dependabot PR makes the others fall behind. Dependabot only rebases +PRs on its scheduled run (weekly) or when there are merge conflicts — not when +a PR merely falls behind `main`. This stalls auto-merge until the next run. + +This workflow fires on every push to `main` and asks Dependabot to rebase any +open PRs that are behind, using the `@dependabot rebase` command. Dependabot +performs the rebase with its own commit signature, preserving the automerge +workflow's ability to verify the PR author via `dependabot/fetch-metadata`. + +**Important:** never use the GitHub API `update-branch` endpoint to rebase +Dependabot PRs. It replaces Dependabot's commit signature, which breaks +`dependabot/fetch-metadata` verification and causes Dependabot to refuse +future rebases ("edited by someone other than Dependabot"). + ## Vulnerability Audit CI Check See [`workflows/dependency-audit.yml`](workflows/dependency-audit.yml). @@ -169,9 +189,10 @@ The workflow fails if any known vulnerability is found, blocking the PR from mer 1. Copy the appropriate `dependabot.yml` template to `.github/dependabot.yml`, adjusting `directory` paths as needed. 2. Add `workflows/dependabot-automerge.yml` to `.github/workflows/`. -3. Add `workflows/dependency-audit.yml` to `.github/workflows/`. -4. Ensure the repository has the GitHub App secrets (`APP_ID`, `APP_PRIVATE_KEY`) - configured for auto-merge. -5. Create the `security` and `dependencies` labels in the repository if they +3. Add `workflows/dependabot-rebase.yml` to `.github/workflows/`. +4. Add `workflows/dependency-audit.yml` to `.github/workflows/`. +5. Ensure the repository has the GitHub App secrets (`APP_ID`, `APP_PRIVATE_KEY`) + configured for auto-merge and rebase. +6. Create the `security` and `dependencies` labels in the repository if they don't already exist. -6. Add `dependency-audit` as a required status check in branch protection rules. +7. Add `dependency-audit` as a required status check in branch protection rules. diff --git a/standards/workflows/dependabot-rebase.yml b/standards/workflows/dependabot-rebase.yml new file mode 100644 index 0000000..6a0683a --- /dev/null +++ b/standards/workflows/dependabot-rebase.yml @@ -0,0 +1,73 @@ +# Dependabot rebase workflow +# Copy to .github/workflows/dependabot-rebase.yml +# +# Requires repository secrets: +# APP_ID — GitHub App ID with contents:write and pull-requests:write +# APP_PRIVATE_KEY — GitHub App private key +# +# Problem: when branch protection requires branches to be up-to-date +# (strict status checks), merging one Dependabot PR makes the others fall +# behind. Dependabot does not auto-rebase PRs that are merely behind — it +# only rebases on its next scheduled run or when there are merge conflicts. +# This leaves auto-merge stalled until the next weekly Dependabot run. +# +# Solution: after every push to main (typically a merged PR), this workflow +# finds open Dependabot PRs that are behind and asks Dependabot to rebase +# them via the @dependabot rebase command. Dependabot performs the rebase +# with its own commit signature, preserving the automerge workflow's ability +# to verify the PR author. +# +# Important: never use the GitHub API update-branch endpoint to rebase +# Dependabot PRs — it replaces Dependabot's commit signature with GitHub's, +# which breaks dependabot/fetch-metadata verification and causes Dependabot +# to refuse future rebases on that PR. +name: Dependabot rebase behind PRs + +on: + push: + branches: + - main + +permissions: {} + +jobs: + rebase: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + steps: + - name: Generate app token + id: app-token + uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3 + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Rebase behind Dependabot PRs + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + REPO: ${{ github.repository }} + run: | + # Find open Dependabot PRs + PRS=$(gh pr list --repo "$REPO" --author "app/dependabot" \ + --json number,headRefName \ + --jq '.[] | "\(.number) \(.headRefName)"') + + if [[ -z "$PRS" ]]; then + echo "No open Dependabot PRs" + exit 0 + fi + + while IFS=' ' read -r PR_NUMBER HEAD_REF; do + BEHIND=$(gh api "repos/$REPO/compare/main...$HEAD_REF" \ + --jq '.behind_by') + + if [[ "$BEHIND" -gt 0 ]]; then + echo "PR #$PR_NUMBER ($HEAD_REF) is $BEHIND commit(s) behind — requesting rebase" + gh pr comment "$PR_NUMBER" --repo "$REPO" \ + --body "@dependabot rebase" + else + echo "PR #$PR_NUMBER ($HEAD_REF) is up to date" + fi + done <<< "$PRS" From 07560216fee649cd86455221e30c2a219d52d25b Mon Sep 17 00:00:00 2001 From: DJ Date: Sun, 5 Apr 2026 19:37:14 -0700 Subject: [PATCH 2/3] fix: use API merge method and add direct merge step Based on testing in google-app-scripts: - @dependabot rebase only works from human users, not bots - API rebase breaks Dependabot ownership; API merge preserves it - GitHub auto-merge (--auto) fails due to BLOCKED mergeable_state - Add direct merge step and skip-commit-verification to automerge Co-Authored-By: Claude Opus 4.6 (1M context) --- standards/dependabot-policy.md | 35 +++++--- standards/workflows/dependabot-automerge.yml | 1 + standards/workflows/dependabot-rebase.yml | 89 ++++++++++++++++---- 3 files changed, 97 insertions(+), 28 deletions(-) diff --git a/standards/dependabot-policy.md b/standards/dependabot-policy.md index a01d0ca..66ad686 100644 --- a/standards/dependabot-policy.md +++ b/standards/dependabot-policy.md @@ -148,24 +148,35 @@ Behavior: - **Major** updates are left for human review - Uses `gh pr merge --auto --squash` so the merge only happens after CI passes -## Rebase Behind PRs Workflow +## Update and Merge Behind PRs Workflow See [`workflows/dependabot-rebase.yml`](workflows/dependabot-rebase.yml). When branch protection requires branches to be up-to-date (`strict: true`), merging one Dependabot PR makes the others fall behind. Dependabot only rebases PRs on its scheduled run (weekly) or when there are merge conflicts — not when -a PR merely falls behind `main`. This stalls auto-merge until the next run. - -This workflow fires on every push to `main` and asks Dependabot to rebase any -open PRs that are behind, using the `@dependabot rebase` command. Dependabot -performs the rebase with its own commit signature, preserving the automerge -workflow's ability to verify the PR author via `dependabot/fetch-metadata`. - -**Important:** never use the GitHub API `update-branch` endpoint to rebase -Dependabot PRs. It replaces Dependabot's commit signature, which breaks -`dependabot/fetch-metadata` verification and causes Dependabot to refuse -future rebases ("edited by someone other than Dependabot"). +a PR merely falls behind `main`. Additionally, GitHub's auto-merge (`--auto`) +may not trigger when rulesets cause `mergeable_state` to report "blocked" even +when all requirements are met. Together, these issues stall Dependabot PR +merges indefinitely. + +This workflow fires on every push to `main` and: + +1. **Updates behind PRs** — uses the GitHub API `update-branch` endpoint with + the **merge** method to bring Dependabot PR branches up to date with `main`. +2. **Merges ready PRs** — directly merges any Dependabot PR that is up-to-date, + has auto-merge enabled, and has all CI checks passing. + +Using the app token for merges ensures each merge triggers a new push to `main`, +creating a self-sustaining chain that serializes Dependabot PR merges. + +**Important:** always use the **merge** method (not rebase) with `update-branch`. +The rebase method force-pushes, replacing Dependabot's commit signature, which +breaks `dependabot/fetch-metadata` verification and causes Dependabot to refuse +future operations ("edited by someone other than Dependabot"). The merge method +preserves the original commits. The automerge workflow must use +`skip-commit-verification: true` in `dependabot/fetch-metadata` since the merge +commit is authored by GitHub, not Dependabot. ## Vulnerability Audit CI Check diff --git a/standards/workflows/dependabot-automerge.yml b/standards/workflows/dependabot-automerge.yml index 6080c9a..2e05b18 100644 --- a/standards/workflows/dependabot-automerge.yml +++ b/standards/workflows/dependabot-automerge.yml @@ -38,6 +38,7 @@ jobs: uses: dependabot/fetch-metadata@ffa630c65fa7e0ecfa0625b5ceda64399aea1b36 # v2 with: github-token: "${{ secrets.GITHUB_TOKEN }}" + skip-commit-verification: true - name: Determine if auto-merge eligible id: eligible diff --git a/standards/workflows/dependabot-rebase.yml b/standards/workflows/dependabot-rebase.yml index 6a0683a..3f78d11 100644 --- a/standards/workflows/dependabot-rebase.yml +++ b/standards/workflows/dependabot-rebase.yml @@ -1,4 +1,4 @@ -# Dependabot rebase workflow +# Dependabot update and merge workflow # Copy to .github/workflows/dependabot-rebase.yml # # Requires repository secrets: @@ -9,19 +9,28 @@ # (strict status checks), merging one Dependabot PR makes the others fall # behind. Dependabot does not auto-rebase PRs that are merely behind — it # only rebases on its next scheduled run or when there are merge conflicts. -# This leaves auto-merge stalled until the next weekly Dependabot run. +# Additionally, GitHub auto-merge (--auto) may not trigger when rulesets +# cause mergeable_state to report "blocked" even though all requirements +# are actually met. # -# Solution: after every push to main (typically a merged PR), this workflow -# finds open Dependabot PRs that are behind and asks Dependabot to rebase -# them via the @dependabot rebase command. Dependabot performs the rebase -# with its own commit signature, preserving the automerge workflow's ability -# to verify the PR author. +# Solution: after every push to main (typically a merged PR), this workflow: +# 1. Updates behind Dependabot PRs using the merge method (not rebase) +# 2. Merges any Dependabot PR that is up-to-date, approved, and passing CI # -# Important: never use the GitHub API update-branch endpoint to rebase +# Using the app token for merges ensures the resulting push to main triggers +# this workflow again, creating a self-sustaining chain that serializes +# Dependabot PR merges one at a time. +# +# Important: never use the API update-branch endpoint with rebase method on # Dependabot PRs — it replaces Dependabot's commit signature with GitHub's, # which breaks dependabot/fetch-metadata verification and causes Dependabot -# to refuse future rebases on that PR. -name: Dependabot rebase behind PRs +# to refuse future rebases on that PR. The merge method preserves the +# original commits. +# +# Note: the merge commit is authored by GitHub, not Dependabot, so the +# dependabot-automerge workflow must use skip-commit-verification: true +# in the dependabot/fetch-metadata step. +name: Dependabot update and merge on: push: @@ -31,7 +40,7 @@ on: permissions: {} jobs: - rebase: + update-and-merge: runs-on: ubuntu-latest permissions: contents: read @@ -44,7 +53,7 @@ jobs: app-id: ${{ secrets.APP_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} - - name: Rebase behind Dependabot PRs + - name: Update and merge Dependabot PRs env: GH_TOKEN: ${{ steps.app-token.outputs.token }} REPO: ${{ github.repository }} @@ -59,15 +68,63 @@ jobs: exit 0 fi + MERGED=false + while IFS=' ' read -r PR_NUMBER HEAD_REF; do BEHIND=$(gh api "repos/$REPO/compare/main...$HEAD_REF" \ --jq '.behind_by') if [[ "$BEHIND" -gt 0 ]]; then - echo "PR #$PR_NUMBER ($HEAD_REF) is $BEHIND commit(s) behind — requesting rebase" - gh pr comment "$PR_NUMBER" --repo "$REPO" \ - --body "@dependabot rebase" + echo "PR #$PR_NUMBER ($HEAD_REF) is $BEHIND commit(s) behind — merging main into branch" + gh api "repos/$REPO/pulls/$PR_NUMBER/update-branch" \ + -X PUT -f update_method=merge \ + --silent || echo "Warning: failed to update PR #$PR_NUMBER" + continue + fi + + echo "PR #$PR_NUMBER ($HEAD_REF) is up to date — checking if merge-ready" + + # Skip if we already merged one (strict mode means others are now behind) + if [[ "$MERGED" == "true" ]]; then + echo " Skipping — already merged a PR this run" + continue + fi + + # Check if auto-merge is enabled (set by the automerge workflow) + AUTO_MERGE=$(gh pr view "$PR_NUMBER" --repo "$REPO" \ + --json autoMergeRequest --jq '.autoMergeRequest != null') + + if [[ "$AUTO_MERGE" != "true" ]]; then + echo " Skipping — auto-merge not enabled" + continue + fi + + # Check if all required checks pass (look at overall rollup) + CHECKS_PASS=$(gh pr view "$PR_NUMBER" --repo "$REPO" \ + --json statusCheckRollup \ + --jq '[.statusCheckRollup[]? | select(.name != null and .status == "COMPLETED") | .conclusion] | all(. == "SUCCESS" or . == "NEUTRAL" or . == "SKIPPED")') + + CHECKS_PENDING=$(gh pr view "$PR_NUMBER" --repo "$REPO" \ + --json statusCheckRollup \ + --jq '[.statusCheckRollup[]? | select(.name != null and .status != "COMPLETED")] | length') + + if [[ "$CHECKS_PENDING" -gt 0 ]]; then + echo " Skipping — $CHECKS_PENDING check(s) still pending" + continue + fi + + if [[ "$CHECKS_PASS" != "true" ]]; then + echo " Skipping — some checks failed" + continue + fi + + echo " All checks pass — merging PR #$PR_NUMBER" + if gh api "repos/$REPO/pulls/$PR_NUMBER/merge" \ + -X PUT -f merge_method=squash \ + --silent; then + echo " Merged PR #$PR_NUMBER" + MERGED=true else - echo "PR #$PR_NUMBER ($HEAD_REF) is up to date" + echo " Warning: failed to merge PR #$PR_NUMBER" fi done <<< "$PRS" From 88c17770c041321d0ede75e3bef9a181495f004d Mon Sep 17 00:00:00 2001 From: DJ Date: Sun, 5 Apr 2026 19:39:07 -0700 Subject: [PATCH 3/3] fix: add concurrency group to prevent overlapping runs Co-Authored-By: Claude Opus 4.6 (1M context) --- standards/workflows/dependabot-rebase.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/standards/workflows/dependabot-rebase.yml b/standards/workflows/dependabot-rebase.yml index 3f78d11..46c756f 100644 --- a/standards/workflows/dependabot-rebase.yml +++ b/standards/workflows/dependabot-rebase.yml @@ -37,6 +37,10 @@ on: branches: - main +concurrency: + group: dependabot-update-and-merge + cancel-in-progress: false + permissions: {} jobs: