Skip to content

Commit 97a5996

Browse files
Merge pull request #10 from codefly-dev/deps/grouping
build(deps): group every ecosystem into one pull request
2 parents ed5939f + 40d8c5d commit 97a5996

3 files changed

Lines changed: 188 additions & 17 deletions

File tree

‎.github/dependabot.yml‎

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

1223
updates:
@@ -18,12 +29,7 @@ updates:
1829
groups:
1930
pip:
2031
patterns:
21-
- "*"
22-
update-types:
23-
- minor
24-
- patch
25-
- major
26-
32+
- '*'
2733
- package-ecosystem: github-actions
2834
directory: /
2935
schedule:
@@ -32,8 +38,4 @@ updates:
3238
groups:
3339
github-actions:
3440
patterns:
35-
- "*"
36-
update-types:
37-
- minor
38-
- patch
39-
- major
41+
- '*'

‎.github/scripts/combine-deps.sh‎

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
#!/usr/bin/env bash
2+
# combine-deps — fold every open Dependabot pull request into a single one.
3+
#
4+
# Dependabot groups within an ecosystem but never across them, so even a
5+
# correctly grouped repository still opens one pull request per ecosystem.
6+
# This replays all of them onto one branch and closes the originals against it.
7+
#
8+
# Re-running is safe: the branch is rebuilt from the base branch every time, so
9+
# a pull request that has since merged or closed simply drops out.
10+
#
11+
# A repository with artefacts that must be regenerated after a bump (a lockfile
12+
# in a second module, a checked-in hash manifest) puts that in
13+
# .github/scripts/combine-deps-local.sh, which runs after the replay and before
14+
# the pull request is opened. Anything it commits rides along.
15+
#
16+
# One caveat before you close the combined pull request: closing a Dependabot
17+
# pull request tells Dependabot not to re-open one for that same version.
18+
# Merging moves the dependency, so the next pass proposes the next version and
19+
# nothing is lost. Abandoning it instead forfeits that batch until a newer
20+
# version ships.
21+
set -euo pipefail
22+
23+
BRANCH="deps/combined"
24+
BASE="$(gh repo view --json defaultBranchRef --jq .defaultBranchRef.name)"
25+
26+
git fetch --quiet origin "$BASE"
27+
28+
mapfile -t ROWS < <(
29+
gh pr list --state open --author "app/dependabot" --base "$BASE" \
30+
--limit 100 --json number,title --jq '.[] | [.number, .title] | @tsv' | sort -n
31+
)
32+
33+
if [ "${#ROWS[@]}" -eq 0 ]; then
34+
echo "combine-deps: no open Dependabot pull requests; nothing to combine."
35+
exit 0
36+
fi
37+
38+
git config user.name "github-actions[bot]"
39+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
40+
git checkout --quiet -B "$BRANCH" "origin/$BASE"
41+
42+
combined=()
43+
skipped=()
44+
45+
for row in "${ROWS[@]}"; do
46+
number="${row%%$'\t'*}"
47+
title="${row#*$'\t'}"
48+
49+
git fetch --quiet origin "refs/pull/${number}/head:refs/combine/${number}"
50+
base_point="$(git merge-base "origin/$BASE" "refs/combine/${number}")"
51+
52+
if git cherry-pick --allow-empty "${base_point}..refs/combine/${number}" >/dev/null 2>&1; then
53+
combined+=("${number}"$'\t'"${title}")
54+
echo "combine-deps: replayed #${number} — ${title}"
55+
else
56+
git cherry-pick --abort >/dev/null 2>&1 || true
57+
skipped+=("${number}"$'\t'"${title}")
58+
echo "combine-deps: SKIPPED #${number} (conflicts with an earlier bump) — ${title}"
59+
fi
60+
done
61+
62+
if [ "${#combined[@]}" -eq 0 ]; then
63+
echo "combine-deps: every pull request conflicted; leaving them all open."
64+
exit 0
65+
fi
66+
67+
if [ -x .github/scripts/combine-deps-local.sh ]; then
68+
echo "combine-deps: running repository-local post-processing."
69+
.github/scripts/combine-deps-local.sh
70+
fi
71+
72+
body_file="$(mktemp)"
73+
{
74+
echo "Every open Dependabot pull request, replayed onto one branch by"
75+
echo "\`.github/workflows/combine-deps.yml\` so the week's dependency work is"
76+
echo "reviewed and merged once instead of per ecosystem."
77+
echo
78+
echo "### Combined"
79+
echo
80+
for row in "${combined[@]}"; do
81+
echo "- #${row%%$'\t'*} — ${row#*$'\t'}"
82+
done
83+
if [ "${#skipped[@]}" -gt 0 ]; then
84+
echo
85+
echo "### Left open (conflicted on replay, merge these by hand)"
86+
echo
87+
for row in "${skipped[@]}"; do
88+
echo "- #${row%%$'\t'*} — ${row#*$'\t'}"
89+
done
90+
fi
91+
} > "$body_file"
92+
93+
git push --quiet --force-with-lease origin "$BRANCH"
94+
95+
existing="$(gh pr list --state open --head "$BRANCH" --json number --jq '.[0].number // empty')"
96+
if [ -n "$existing" ]; then
97+
gh pr edit "$existing" --body-file "$body_file"
98+
combined_pr="$existing"
99+
else
100+
gh pr create --base "$BASE" --head "$BRANCH" \
101+
--title "build(deps): combined dependency updates" --body-file "$body_file"
102+
combined_pr="$(gh pr list --state open --head "$BRANCH" --json number --jq '.[0].number')"
103+
fi
104+
105+
for row in "${combined[@]}"; do
106+
number="${row%%$'\t'*}"
107+
gh pr comment "$number" --body "Superseded by #${combined_pr}, which carries this bump together with the rest of the week's dependency updates."
108+
gh pr close "$number"
109+
done
110+
111+
echo "combine-deps: combined ${#combined[@]} pull request(s) into #${combined_pr}."

‎.github/workflows/combine-deps.yml‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Folds every open Dependabot pull request into a single one.
2+
#
3+
# Dependabot groups within an ecosystem but never across them (see the comment
4+
# in .github/dependabot.yml), so its best case is one pull request per
5+
# ecosystem. This runs after its weekly pass and combines whatever is open.
6+
#
7+
# REQUIRES the DEPS_COMBINE_TOKEN secret — a PAT (or GitHub App token) with
8+
# `repo` scope. The built-in GITHUB_TOKEN is not usable here for two separate
9+
# reasons: this organisation has "Allow GitHub Actions to create and approve
10+
# pull requests" turned off, so it cannot open the combined pull request at
11+
# all; and even with that enabled, a branch pushed or a pull request opened by
12+
# GITHUB_TOKEN does not trigger workflow runs, so the combined pull request
13+
# would sit with no CI and never go green. An organisation-level secret covers
14+
# every repository at once.
15+
name: combine-deps
16+
17+
on:
18+
schedule:
19+
# Mondays, a few hours after Dependabot's weekly pass has opened its pull
20+
# requests. Off the hour: :00 is the busiest minute on the shared runners.
21+
- cron: '23 9 * * 1'
22+
workflow_dispatch:
23+
24+
permissions:
25+
contents: write
26+
pull-requests: write
27+
28+
concurrency:
29+
group: combine-deps
30+
cancel-in-progress: false
31+
32+
jobs:
33+
combine:
34+
name: Combine open Dependabot pull requests
35+
runs-on: ubuntu-latest
36+
timeout-minutes: 15
37+
steps:
38+
# The secrets context is not available to `if:`, so this is checked in the
39+
# step body rather than as a condition.
40+
- name: Require a token that can open a pull request and trigger CI
41+
env:
42+
COMBINE_TOKEN: ${{ secrets.DEPS_COMBINE_TOKEN }}
43+
run: |
44+
if [ -z "$COMBINE_TOKEN" ]; then
45+
echo "::error::DEPS_COMBINE_TOKEN is not set. Add it as an organisation secret (a PAT with 'repo' scope)."
46+
echo "GITHUB_TOKEN cannot be used here: Actions is not permitted to create pull requests in"
47+
echo "this organisation, and a pull request it opens does not trigger CI, so the combined"
48+
echo "pull request would never go green."
49+
exit 1
50+
fi
51+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
52+
with:
53+
fetch-depth: 0
54+
token: ${{ secrets.DEPS_COMBINE_TOKEN }}
55+
- name: Combine
56+
env:
57+
GH_TOKEN: ${{ secrets.DEPS_COMBINE_TOKEN }}
58+
run: bash .github/scripts/combine-deps.sh

0 commit comments

Comments
 (0)