Skip to content

Commit 252ad5b

Browse files
refactor: Improve Dependabot PR grouping workflow by switching from cherry-pick to merge strategy, enhancing conflict resolution and sorting by PR number
1 parent cb668b3 commit 252ad5b

File tree

1 file changed

+91
-35
lines changed

1 file changed

+91
-35
lines changed

.github/workflows/group-dependabot-security-updates.yml

Lines changed: 91 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
# Workflow: Group Dependabot PRs
22
# Description:
33
# This GitHub Actions workflow automatically groups open Dependabot PRs by ecosystem (pip, npm).
4-
# It cherry-picks individual PR changes into grouped branches, resolves merge conflicts automatically, and opens consolidated PRs.
4+
# It merges individual PR changes into grouped branches in chronological order (sorted by PR number), resolves merge conflicts automatically, and opens consolidated PRs.
55
# It also closes the original Dependabot PRs and carries over their labels and metadata.
66
# Improvements:
7-
# - Handles multiple conflicting files during cherry-pick
7+
# - Uses merge strategy instead of cherry-pick for better conflict resolution
8+
# - Sorts PRs by number at fetch time for consistent chronological processing
9+
# - Handles multiple conflicting files during merge
810
# - Deduplicates entries in PR description
911
# - Avoids closing original PRs unless grouped PR creation succeeds
1012
# - More efficient retry logic
1113
# - Ecosystem grouping is now configurable via native YAML map
12-
# - Uses safe namespaced branch naming (e.g. actions/grouped-...) to avoid developer conflict
14+
# - Uses safe namespaced branch naming (e.g. security/grouped-...) to avoid developer conflict
1315
# - Ensures PR body formatting uses real newlines for better readability
1416
# - Adds strict error handling for script robustness
1517
# - Accounts for tool dependencies (jq, gh) and race conditions
@@ -74,14 +76,14 @@ jobs:
7476
run: |
7577
set -euo pipefail
7678
77-
- name: Fetch open Dependabot PRs targeting main
79+
- name: Fetch open Dependabot PRs targeting main (sorted by PR number)
7880
id: fetch_prs
7981
run: |
8082
gh pr list \
8183
--search "author:dependabot[bot] base:$TARGET_BRANCH is:open" \
8284
--limit 100 \
8385
--json number,title,headRefName,labels,files,url \
84-
--jq '[.[] | {number, title, url, ref: .headRefName, labels: [.labels[].name], files: [.files[].path]}]' > prs.json
86+
--jq '[.[] | {number, title, url, ref: .headRefName, labels: [.labels[].name], files: [.files[].path]}] | sort_by(.number)' > prs.json
8587
cat prs.json
8688
8789
- name: Validate prs.json
@@ -107,14 +109,16 @@ jobs:
107109
run: |
108110
echo "Running in dry-run mode. No changes will be pushed or PRs created/closed."
109111
110-
- name: Group PRs by ecosystem and cherry-pick with retry
112+
- name: Group PRs by ecosystem and merge in sorted order
111113
run: |
112114
declare -A GROUP_CONFIG=(
113115
[pip]="${GROUP_CONFIG_PIP:-backend}"
114116
[npm]="${GROUP_CONFIG_NPM:-frontend}"
115117
[yarn]="${GROUP_CONFIG_YARN:-frontend}"
116118
)
117119
mkdir -p grouped
120+
121+
# Group PRs by ecosystem (data is already sorted by PR number)
118122
jq -c '.[]' prs.json | while read pr; do
119123
ref=$(echo "$pr" | jq -r '.ref')
120124
number=$(echo "$pr" | jq -r '.number')
@@ -136,93 +140,132 @@ jobs:
136140
exit 0
137141
fi
138142
143+
# Pre-load PR metadata for efficient lookup
139144
declare -A pr_metadata_map
140145
while IFS=$'\t' read -r number title url labels; do
141146
pr_metadata_map["$number"]="$title|$url|$labels"
142147
done < <(jq -r '.[] | "\(.number)\t\(.title)\t\(.url)\t\(.labels | join(","))"' prs.json)
143148
149+
# Process each group
144150
for file in "${grouped_files[@]}"; do
145151
group_name=$(basename "$file" .txt)
146152
safe_group_name=$(echo "$group_name" | tr -c '[:alnum:]_-' '-')
147153
branch_name="security/grouped-${safe_group_name}-updates"
154+
155+
echo "Processing group: $group_name (branch: $branch_name)"
156+
157+
# Create or checkout the grouped branch
148158
git checkout -B "$branch_name"
149159
150-
git fetch origin "$branch_name" || true
151-
git merge origin/"$branch_name" --no-edit || true
160+
# Try to merge existing grouped branch if it exists on remote
161+
git fetch origin "$branch_name" 2>/dev/null || true
162+
if git rev-parse "origin/$branch_name" >/dev/null 2>&1; then
163+
echo "Merging existing remote branch origin/$branch_name"
164+
git merge origin/"$branch_name" --no-edit || true
165+
fi
152166
167+
# Merge PRs in sorted order (file is already sorted by PR number from jq)
168+
merge_success=true
153169
while read -r number ref group; do
170+
echo "Merging PR #$number ($ref) into $branch_name"
154171
git fetch origin "$ref"
155-
if ! git cherry-pick FETCH_HEAD; then
156-
echo "Conflict found in $ref. Attempting to resolve."
172+
173+
if ! git merge FETCH_HEAD --no-edit -m "Merge PR #$number: $(echo "${pr_metadata_map["$number"]}" | cut -d'|' -f1)"; then
174+
echo "Conflict found when merging PR #$number ($ref). Attempting to resolve."
157175
conflict_files=($(git diff --name-only --diff-filter=U))
176+
158177
if [ ${#conflict_files[@]} -gt 0 ]; then
159178
echo "Resolving conflicts in files: ${conflict_files[*]}"
160179
for conflict_file in "${conflict_files[@]}"; do
161-
echo "Resolving conflict in $conflict_file"
180+
echo "Resolving conflict in $conflict_file using theirs strategy"
162181
git checkout --theirs "$conflict_file"
163182
git add "$conflict_file"
164183
done
165-
git cherry-pick --continue || {
166-
echo "Failed to continue cherry-pick. Aborting."
167-
git cherry-pick --abort
168-
continue 2
169-
}
184+
185+
if git commit --no-edit -m "Merge PR #$number: $(echo "${pr_metadata_map["$number"]}" | cut -d'|' -f1) (with conflict resolution)"; then
186+
echo "Successfully resolved conflicts and committed merge for PR #$number"
187+
else
188+
echo "Failed to commit merge resolution for PR #$number. Aborting merge."
189+
git merge --abort
190+
merge_success=false
191+
continue
192+
fi
170193
else
171-
echo "No conflicting files found. Aborting."
172-
git cherry-pick --abort
173-
continue 2
194+
echo "No conflicting files found. Aborting merge for PR #$number."
195+
git merge --abort
196+
merge_success=false
197+
continue
174198
fi
199+
else
200+
echo "Successfully merged PR #$number without conflicts"
175201
fi
176202
done < "$file"
177203
204+
if [ "$merge_success" = false ]; then
205+
echo "Some merges failed for group $group_name. Skipping push and PR creation."
206+
continue
207+
fi
208+
209+
# Push the grouped branch
178210
if [ "$DRY_RUN" == "true" ]; then
179211
echo "[DRY-RUN] Skipping git push for $branch_name"
180212
else
181213
git push --force origin "$branch_name"
182214
fi
183215
216+
# Build PR description with all included PRs (in sorted order)
184217
new_lines=""
185218
while read -r number ref group; do
186219
IFS="|" read -r title url _ <<< "${pr_metadata_map["$number"]}"
187-
new_lines+="$title - [#$number]($url)\n"
220+
new_lines+="- $title - [#$number]($url)\n"
188221
done < "$file"
189222
190-
pr_title="chore(deps): bump grouped $group_name Dependabot updates"
223+
pr_title="chore(deps): bump grouped $group_name dependencies"
191224
existing_url=$(gh pr list --head "$branch_name" --base "$TARGET_BRANCH" --state open --json url --jq '.[0].url // empty')
192225
226+
# Create or update the grouped PR
193227
if [ -n "$existing_url" ]; then
194228
echo "PR already exists: $existing_url"
195229
pr_url="$existing_url"
196230
current_body=$(gh pr view "$pr_url" --json body --jq .body)
231+
232+
# Deduplicate entries in PR description
197233
IFS=$'\n' read -d '' -r -a current_lines < <(printf '%s\0' "$current_body")
198234
IFS=$'\n' read -d '' -r -a new_lines_arr < <(printf '%b\0' "$new_lines")
199235
declare -A seen
200236
for line in "${current_lines[@]}"; do
201237
seen["$line"]=1
202238
done
239+
203240
filtered_lines=""
204241
for line in "${new_lines_arr[@]}"; do
205242
if [[ -n "$line" && -z "${seen["$line"]}" ]]; then
206243
filtered_lines+="$line\n"
207244
fi
208245
done
246+
209247
if [ -n "$filtered_lines" ]; then
210248
new_body="$current_body"$'\n'$filtered_lines
211249
else
212250
new_body="$current_body"
213251
fi
252+
214253
if [ "$DRY_RUN" == "true" ]; then
215254
echo "[DRY-RUN] Would update PR body for $pr_url"
255+
echo "New content would be:"
256+
echo "$new_body"
216257
else
217258
tmpfile=$(mktemp)
218259
printf '%s' "$new_body" > "$tmpfile"
219260
gh pr edit "$pr_url" --body-file "$tmpfile"
220261
rm -f "$tmpfile"
262+
echo "Updated existing PR: $pr_url"
221263
fi
222264
else
223-
pr_body=$(printf "This PR groups multiple open PRs by Dependabot for %s.\n\n%b" "$group_name" "$new_lines")
265+
pr_body=$(printf "This PR groups multiple open Dependabot PRs for the %s ecosystem.\n\n**Included PRs:**\n%b" "$group_name" "$new_lines")
224266
if [ "$DRY_RUN" == "true" ]; then
225267
echo "[DRY-RUN] Would create PR titled: $pr_title"
268+
echo "PR body would be:"
226269
echo "$pr_body"
227270
pr_url=""
228271
else
@@ -231,28 +274,41 @@ jobs:
231274
--body "$pr_body" \
232275
--base "$TARGET_BRANCH" \
233276
--head "$branch_name")
277+
echo "Created new grouped PR: $pr_url"
234278
fi
235279
fi
236280
281+
# Add labels and close original PRs only if grouped PR was created/updated successfully
237282
if [ -n "$pr_url" ]; then
238-
for number in $(cut -d ' ' -f1 "$file"); do
283+
while read -r number ref group; do
239284
IFS="|" read -r _ _ labels <<< "${pr_metadata_map["$number"]}"
240-
IFS="," read -ra label_arr <<< "$labels"
241-
for label in "${label_arr[@]}"; do
242-
if [ "$DRY_RUN" == "true" ]; then
243-
echo "[DRY-RUN] Would add label $label to $pr_url"
244-
else
245-
gh pr edit "$pr_url" --add-label "$label"
246-
fi
247-
done
285+
286+
# Add labels from original PRs to grouped PR
287+
if [ -n "$labels" ]; then
288+
IFS="," read -ra label_arr <<< "$labels"
289+
for label in "${label_arr[@]}"; do
290+
if [ -n "$label" ]; then
291+
if [ "$DRY_RUN" == "true" ]; then
292+
echo "[DRY-RUN] Would add label '$label' to $pr_url"
293+
else
294+
gh pr edit "$pr_url" --add-label "$label" 2>/dev/null || echo "Failed to add label '$label'"
295+
fi
296+
fi
297+
done
298+
fi
299+
300+
# Close original PR
248301
if [ "$DRY_RUN" == "true" ]; then
249302
echo "[DRY-RUN] Would close PR #$number"
250303
else
251-
gh pr close "$number" --comment "Grouped into $pr_url."
304+
gh pr close "$number" --comment "Grouped into $pr_url for easier review and testing." || echo "Failed to close PR #$number"
252305
fi
253-
done
254-
echo "Grouped PR created. Leaving branch $branch_name for now."
306+
done < "$file"
307+
308+
echo "✅ Successfully processed group '$group_name' with branch '$branch_name'"
255309
else
256-
echo "Grouped PR was not created. Skipping closing of original PRs."
310+
echo "Grouped PR was not created for group '$group_name'. Skipping closing of original PRs."
257311
fi
258312
done
313+
314+
echo "🎉 Workflow completed successfully!"

0 commit comments

Comments
 (0)