-
-
Notifications
You must be signed in to change notification settings - Fork 4
190 lines (170 loc) · 7.14 KB
/
Copy pathcombine-prs.yml
File metadata and controls
190 lines (170 loc) · 7.14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
name: Combine Dependabot PRs
# Combine all open Dependabot PRs into a single consolidated PR.
#
# Dependabot cannot group dependency updates across ecosystems
# (npm + github-actions + docker each produce their own PR). This
# workflow stitches every open PR labeled `dependencies` into one
# branch and opens a single PR, so reviewers only have to look at
# one diff and CI runs once.
#
# Triggers:
# - pull_request_target → fires right after Dependabot opens a
# `dependencies`-labeled PR. A 2-minute settle delay + concurrency
# cancel ensures that when Dependabot opens 3 PRs in a wave (one
# per ecosystem) only the final run actually performs the combine.
# - workflow_dispatch → run on demand from the Actions tab
#
# Behavior:
# - Picks all open PRs from `app/dependabot` carrying the
# `dependencies` label whose branches still merge cleanly into main.
# - Updates the stable branch `combined-deps/open` (force-with-lease)
# and opens or updates a single PR against it, so repeated runs
# converge on the same PR instead of creating new ones.
# - Skips PRs that cause a merge conflict and reports them in the
# PR body so a human can deal with them individually.
# - Does NOT close the source PRs automatically — merging the
# combined PR will auto-close them because git sees their
# commits as already in main.
#
# Security note on `pull_request_target`:
# The job runs in the base-repo context with write permissions, but
# never checks out PR-supplied code. It only invokes `gh` + `git` on
# PR refs to merge their commits into our branch. No PR-controlled
# script executes here.
#
# Token note:
# PRs created with the default `GITHUB_TOKEN` do NOT trigger downstream
# workflow runs (GitHub anti-recursion). To get CI to start on the
# combined PR automatically, add a fine-grained PAT as repository secret
# `COMBINE_PRS_TOKEN` (scopes: contents:write, pull-requests:write).
# Without that secret the workflow still falls back to `GITHUB_TOKEN`,
# the combined PR will be created, but you'll have to close+reopen it
# once to kick CI.
on:
pull_request_target:
types: [opened, reopened, ready_for_review, labeled]
workflow_dispatch:
inputs:
dry-run:
description: 'List PRs that would be combined without pushing or opening a PR'
required: false
type: boolean
default: false
permissions:
contents: read
# A wave of Dependabot PRs triggers this workflow N times in ~1-2 minutes.
# Cancel any in-flight run when a new one starts; the last run sees all PRs.
concurrency:
group: combine-prs
cancel-in-progress: true
jobs:
combine:
name: Combine open Dependabot PRs
runs-on: ubuntu-latest
# Only act on Dependabot PRs (or manual dispatch). Without this guard
# every PR open/label event would otherwise trigger the workflow.
if: >-
github.event_name == 'workflow_dispatch' ||
(github.event.pull_request.user.login == 'dependabot[bot]' &&
contains(github.event.pull_request.labels.*.name, 'dependencies'))
permissions:
contents: write
pull-requests: write
steps:
- name: Wait for Dependabot wave to settle
if: github.event_name == 'pull_request_target'
run: sleep 120
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
with:
fetch-depth: 0
ref: main
token: ${{ secrets.COMBINE_PRS_TOKEN || secrets.GITHUB_TOKEN }}
- name: Configure git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Combine open Dependabot PRs
env:
GH_TOKEN: ${{ secrets.COMBINE_PRS_TOKEN || secrets.GITHUB_TOKEN }}
DRY_RUN: ${{ inputs.dry-run }}
run: |
set -euo pipefail
DATE=$(date +%Y-%m-%d)
# Stable branch name so repeated runs (Dependabot waves, manual
# dispatches, retries) converge on a single combined PR instead
# of spawning a new one per date.
COMBINED_BRANCH="combined-deps/open"
mapfile -t PRS < <(gh pr list \
--state open \
--author "app/dependabot" \
--label dependencies \
--json number,title,headRefName \
--jq '.[] | "\(.number)\t\(.title)\t\(.headRefName)"')
if [ "${#PRS[@]}" -eq 0 ]; then
echo "No open Dependabot PRs labeled 'dependencies' found. Nothing to do."
exit 0
fi
echo "Found ${#PRS[@]} candidate PR(s):"
printf ' %s\n' "${PRS[@]}"
if [ "${DRY_RUN}" = "true" ]; then
echo "Dry run requested — exiting before any push."
exit 0
fi
git fetch origin main --quiet
git checkout -B "${COMBINED_BRANCH}" origin/main
MERGED_NUMBERS=()
MERGED_LINES=()
SKIPPED_LINES=()
for entry in "${PRS[@]}"; do
IFS=$'\t' read -r number title head_ref <<<"${entry}"
echo "::group::Merging #${number} (${head_ref})"
git fetch origin "pull/${number}/head:pr-${number}" --quiet
if git merge --no-edit --no-ff -m "Merge #${number}: ${title}" "pr-${number}"; then
MERGED_NUMBERS+=("${number}")
MERGED_LINES+=("- #${number} ${title}")
echo "Merged #${number}"
else
echo "Conflict merging #${number}, aborting that merge."
git merge --abort || true
SKIPPED_LINES+=("- #${number} ${title} _(conflict — must be resolved manually)_")
fi
echo "::endgroup::"
done
if [ "${#MERGED_NUMBERS[@]}" -eq 0 ]; then
echo "No PRs merged cleanly. Nothing to push."
exit 0
fi
git push --force-with-lease origin "${COMBINED_BRANCH}"
BODY_FILE=$(mktemp)
{
echo "Combined dependency update generated by \`combine-prs.yml\` on ${DATE}."
echo
echo "Merging this PR will auto-close the source Dependabot PRs because"
echo "their commits will be reachable from \`main\`."
echo
echo "### Combined PRs"
printf '%s\n' "${MERGED_LINES[@]}"
if [ "${#SKIPPED_LINES[@]}" -gt 0 ]; then
echo
echo "### Skipped (merge conflict)"
printf '%s\n' "${SKIPPED_LINES[@]}"
fi
} > "${BODY_FILE}"
EXISTING=$(gh pr list \
--head "${COMBINED_BRANCH}" \
--state open \
--json number \
--jq '.[0].number // empty')
if [ -n "${EXISTING}" ]; then
echo "Updating existing combined PR #${EXISTING}"
gh pr edit "${EXISTING}" \
--title "chore(deps): combined dependency updates ${DATE}" \
--body-file "${BODY_FILE}"
else
gh pr create \
--base main \
--head "${COMBINED_BRANCH}" \
--title "chore(deps): combined dependency updates ${DATE}" \
--label dependencies \
--body-file "${BODY_FILE}"
fi