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
36 changes: 19 additions & 17 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
# Dependency updates for every ecosystem this repository ships.
#
# Each ecosystem is ONE catch-all group covering minor, patch and major, with
# open-pull-requests-limit 1, so a week yields at most one pull request per
# ecosystem instead of one per dependency. Dependabot cannot group across
# ecosystems, so the number of entries below is the floor on weekly PRs.
# Each ecosystem is ONE catch-all group with open-pull-requests-limit 1, so a
# week yields at most one pull request per ecosystem instead of one per
# dependency. Dependabot cannot group ACROSS ecosystems, so the entries below
# are the floor on what Dependabot itself opens; .github/workflows/combine-
# deps.yml runs afterwards and folds whatever is open into a single pull
# request.
#
# The github-actions entry exists because every action is pinned by commit SHA;
# nothing else would ever move those pins.
# The groups deliberately carry NO `update-types:` filter. That filter only
# recognises semver update types, so a Docker base image pinned by digest
# matches none of them and falls OUT of the group into its own ungrouped pull
# request, one per directory. Omitting the filter makes `patterns: "*"` mean
# what it says: everything, digests included. For the same reason majors are
# not split into a second group — that is a guaranteed extra pull request.
#
# Security updates keep their own group where one is defined: `applies-to`
# separates them from version updates and they are not capped by
# open-pull-requests-limit, because an urgent fix should not queue behind
# routine work.
version: 2

updates:
Expand All @@ -18,12 +29,7 @@ updates:
groups:
pip:
patterns:
- "*"
update-types:
- minor
- patch
- major

- '*'
- package-ecosystem: github-actions
directory: /
schedule:
Expand All @@ -32,8 +38,4 @@ updates:
groups:
github-actions:
patterns:
- "*"
update-types:
- minor
- patch
- major
- '*'
111 changes: 111 additions & 0 deletions .github/scripts/combine-deps.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#!/usr/bin/env bash
# combine-deps — fold every open Dependabot pull request into a single one.
#
# Dependabot groups within an ecosystem but never across them, so even a
# correctly grouped repository still opens one pull request per ecosystem.
# This replays all of them onto one branch and closes the originals against it.
#
# Re-running is safe: the branch is rebuilt from the base branch every time, so
# a pull request that has since merged or closed simply drops out.
#
# A repository with artefacts that must be regenerated after a bump (a lockfile
# in a second module, a checked-in hash manifest) puts that in
# .github/scripts/combine-deps-local.sh, which runs after the replay and before
# the pull request is opened. Anything it commits rides along.
#
# One caveat before you close the combined pull request: closing a Dependabot
# pull request tells Dependabot not to re-open one for that same version.
# Merging moves the dependency, so the next pass proposes the next version and
# nothing is lost. Abandoning it instead forfeits that batch until a newer
# version ships.
set -euo pipefail

BRANCH="deps/combined"
BASE="$(gh repo view --json defaultBranchRef --jq .defaultBranchRef.name)"

git fetch --quiet origin "$BASE"

mapfile -t ROWS < <(
gh pr list --state open --author "app/dependabot" --base "$BASE" \
--limit 100 --json number,title --jq '.[] | [.number, .title] | @tsv' | sort -n
)

if [ "${#ROWS[@]}" -eq 0 ]; then
echo "combine-deps: no open Dependabot pull requests; nothing to combine."
exit 0
fi

git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git checkout --quiet -B "$BRANCH" "origin/$BASE"

combined=()
skipped=()

for row in "${ROWS[@]}"; do
number="${row%%$'\t'*}"
title="${row#*$'\t'}"

git fetch --quiet origin "refs/pull/${number}/head:refs/combine/${number}"
base_point="$(git merge-base "origin/$BASE" "refs/combine/${number}")"

if git cherry-pick --allow-empty "${base_point}..refs/combine/${number}" >/dev/null 2>&1; then
combined+=("${number}"$'\t'"${title}")
echo "combine-deps: replayed #${number} — ${title}"
else
git cherry-pick --abort >/dev/null 2>&1 || true
skipped+=("${number}"$'\t'"${title}")
echo "combine-deps: SKIPPED #${number} (conflicts with an earlier bump) — ${title}"
fi
done

if [ "${#combined[@]}" -eq 0 ]; then
echo "combine-deps: every pull request conflicted; leaving them all open."
exit 0
fi

if [ -x .github/scripts/combine-deps-local.sh ]; then
echo "combine-deps: running repository-local post-processing."
.github/scripts/combine-deps-local.sh
fi

body_file="$(mktemp)"
{
echo "Every open Dependabot pull request, replayed onto one branch by"
echo "\`.github/workflows/combine-deps.yml\` so the week's dependency work is"
echo "reviewed and merged once instead of per ecosystem."
echo
echo "### Combined"
echo
for row in "${combined[@]}"; do
echo "- #${row%%$'\t'*} — ${row#*$'\t'}"
done
if [ "${#skipped[@]}" -gt 0 ]; then
echo
echo "### Left open (conflicted on replay, merge these by hand)"
echo
for row in "${skipped[@]}"; do
echo "- #${row%%$'\t'*} — ${row#*$'\t'}"
done
fi
} > "$body_file"

git push --quiet --force-with-lease origin "$BRANCH"

existing="$(gh pr list --state open --head "$BRANCH" --json number --jq '.[0].number // empty')"
if [ -n "$existing" ]; then
gh pr edit "$existing" --body-file "$body_file"
combined_pr="$existing"
else
gh pr create --base "$BASE" --head "$BRANCH" \
--title "build(deps): combined dependency updates" --body-file "$body_file"
combined_pr="$(gh pr list --state open --head "$BRANCH" --json number --jq '.[0].number')"
fi

for row in "${combined[@]}"; do
number="${row%%$'\t'*}"
gh pr comment "$number" --body "Superseded by #${combined_pr}, which carries this bump together with the rest of the week's dependency updates."
gh pr close "$number"
done

echo "combine-deps: combined ${#combined[@]} pull request(s) into #${combined_pr}."
58 changes: 58 additions & 0 deletions .github/workflows/combine-deps.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Folds every open Dependabot pull request into a single one.
#
# Dependabot groups within an ecosystem but never across them (see the comment
# in .github/dependabot.yml), so its best case is one pull request per
# ecosystem. This runs after its weekly pass and combines whatever is open.
#
# REQUIRES the DEPS_COMBINE_TOKEN secret — a PAT (or GitHub App token) with
# `repo` scope. The built-in GITHUB_TOKEN is not usable here for two separate
# reasons: this organisation has "Allow GitHub Actions to create and approve
# pull requests" turned off, so it cannot open the combined pull request at
# all; and even with that enabled, a branch pushed or a pull request opened by
# GITHUB_TOKEN does not trigger workflow runs, so the combined pull request
# would sit with no CI and never go green. An organisation-level secret covers
# every repository at once.
name: combine-deps

on:
schedule:
# Mondays, a few hours after Dependabot's weekly pass has opened its pull
# requests. Off the hour: :00 is the busiest minute on the shared runners.
- cron: '23 9 * * 1'
workflow_dispatch:

permissions:
contents: write
pull-requests: write

concurrency:
group: combine-deps
cancel-in-progress: false

jobs:
combine:
name: Combine open Dependabot pull requests
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
# The secrets context is not available to `if:`, so this is checked in the
# step body rather than as a condition.
- name: Require a token that can open a pull request and trigger CI
env:
COMBINE_TOKEN: ${{ secrets.DEPS_COMBINE_TOKEN }}
run: |
if [ -z "$COMBINE_TOKEN" ]; then
echo "::error::DEPS_COMBINE_TOKEN is not set. Add it as an organisation secret (a PAT with 'repo' scope)."
echo "GITHUB_TOKEN cannot be used here: Actions is not permitted to create pull requests in"
echo "this organisation, and a pull request it opens does not trigger CI, so the combined"
echo "pull request would never go green."
exit 1
fi
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
token: ${{ secrets.DEPS_COMBINE_TOKEN }}
- name: Combine
env:
GH_TOKEN: ${{ secrets.DEPS_COMBINE_TOKEN }}
run: bash .github/scripts/combine-deps.sh
Loading