Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 37 additions & 5 deletions standards/dependabot-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -147,6 +148,36 @@ Behavior:
- **Major** updates are left for human review
- Uses `gh pr merge --auto --squash` so the merge only happens after CI passes

## 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`. 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

See [`workflows/dependency-audit.yml`](workflows/dependency-audit.yml).
Expand All @@ -169,9 +200,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.
1 change: 1 addition & 0 deletions standards/workflows/dependabot-automerge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
134 changes: 134 additions & 0 deletions standards/workflows/dependabot-rebase.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# Dependabot update and merge 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.
# 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:
# 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
#
# 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. 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:
branches:
- main

concurrency:
group: dependabot-update-and-merge
cancel-in-progress: false

permissions: {}

jobs:
update-and-merge:
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: Update and merge 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

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 — 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 " Warning: failed to merge PR #$PR_NUMBER"
fi
done <<< "$PRS"
Loading