-
Notifications
You must be signed in to change notification settings - Fork 622
Expand file tree
/
Copy pathgroup_dependabot_security_updates.yml
More file actions
268 lines (246 loc) · 10.7 KB
/
group_dependabot_security_updates.yml
File metadata and controls
268 lines (246 loc) · 10.7 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
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# Workflow: Group Dependabot PRs
# Description:
# This GitHub Actions workflow automatically groups open Dependabot PRs by ecosystem (pip, npm).
# It cherry-picks individual PR changes into grouped branches, resolves merge conflicts automatically, and opens consolidated PRs.
# It also closes the original Dependabot PRs and carries over their labels and metadata.
# Improvements:
# - Handles multiple conflicting files during cherry-pick
# - Deduplicates entries in PR description
# - Avoids closing original PRs unless grouped PR creation succeeds
# - More efficient retry logic
# - Ecosystem grouping is now configurable via native YAML map
# - Uses safe namespaced branch naming (e.g. actions/grouped-...) to avoid developer conflict
# - Ensures PR body formatting uses real newlines for better readability
# - Adds strict error handling for script robustness
# - Accounts for tool dependencies (jq, gh) and race conditions
# - Optimized PR metadata lookup by preloading into associative array
# - Supports --dry-run mode for validation/testing without side effects
# - Note: PRs created during workflow execution will be picked up in the next scheduled run.
name: Group Dependabot PRs
on:
schedule:
- cron: '0 0 * * *' # Run daily at midnight UTC
workflow_dispatch:
inputs:
group_config_pip:
description: "Group name for pip ecosystem"
required: false
default: "backend"
group_config_npm:
description: "Group name for npm ecosystem"
required: false
default: "frontend"
group_config_yarn:
description: "Group name for yarn ecosystem"
required: false
default: "frontend"
dry_run:
description: "Run in dry-run mode (no changes will be pushed or PRs created/closed)"
required: false
default: false
type: boolean
jobs:
group-dependabot-prs:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TARGET_BRANCH: "main"
DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }}
GROUP_CONFIG_PIP: ${{ github.event.inputs.group_config_pip || 'backend' }}
GROUP_CONFIG_NPM: ${{ github.event.inputs.group_config_npm || 'frontend' }}
GROUP_CONFIG_YARN: ${{ github.event.inputs.group_config_yarn || 'frontend' }}
steps:
- name: Checkout default branch
uses: actions/checkout@v4
- name: Set up Git
run: |
git config --global user.name "github-actions"
git config --global user.email "[email protected]"
- name: Install required tools
run: |
sudo apt-get update
sudo apt-get install -y jq gh
shell: bash
- name: Enable strict error handling
shell: bash
run: |
set -euo pipefail
- name: Fetch open Dependabot PRs targeting main
id: fetch_prs
run: |
gh pr list \
--search "author:dependabot[bot] base:$TARGET_BRANCH is:open" \
--limit 100 \
--json number,title,headRefName,labels,files,url \
--jq '[.[] | {number, title, url, ref: .headRefName, labels: [.labels[].name], files: [.files[].path]}]' > prs.json
cat prs.json
- name: Validate prs.json
run: |
jq empty prs.json 2> jq_error.log || { echo "Malformed JSON in prs.json: $(cat jq_error.log)"; exit 1; }
- name: Check if any PRs exist
id: check_prs
run: |
count=$(jq length prs.json)
echo "Found $count PRs"
if [ "$count" -eq 0 ]; then
echo "No PRs to group. Exiting."
echo "skip=true" >> $GITHUB_OUTPUT
fi
- name: Exit early if no PRs
if: steps.check_prs.outputs.skip == 'true'
run: exit 0
- name: Dry-run validation (CI/test only)
if: env.DRY_RUN == 'true'
run: |
echo "Running in dry-run mode. No changes will be pushed or PRs created/closed."
# Optionally, add more validation logic here (e.g., check grouped files, print planned actions).
- name: Group PRs by ecosystem and cherry-pick with retry
run: |
declare -A GROUP_CONFIG=(
[pip]="${GROUP_CONFIG_PIP:-backend}"
[npm]="${GROUP_CONFIG_NPM:-frontend}"
[yarn]="${GROUP_CONFIG_YARN:-frontend}"
)
mkdir -p grouped
jq -c '.[]' prs.json | while read pr; do
ref=$(echo "$pr" | jq -r '.ref')
number=$(echo "$pr" | jq -r '.number')
group="misc"
for key in "${!GROUP_CONFIG[@]}"; do
if [[ "$ref" == *"$key"* ]]; then
group="${GROUP_CONFIG[$key]}"
break
fi
done
echo "$number $ref $group" >> grouped/$group.txt
done
shopt -s nullglob
grouped_files=(grouped/*.txt)
if [ ${#grouped_files[@]} -eq 0 ]; then
echo "No groups were formed. Exiting."
exit 0
fi
declare -A pr_metadata_map
while IFS=$'\t' read -r number title url labels; do
pr_metadata_map["$number"]="$title|$url|$labels"
done < <(jq -r '.[] | "\(.number)\t\(.title)\t\(.url)\t\(.labels | join(","))"' prs.json)
for file in "${grouped_files[@]}"; do
group_name=$(basename "$file" .txt)
# Sanitize group_name: allow only alphanum, dash, underscore
safe_group_name=$(echo "$group_name" | tr -c '[:alnum:]_-' '-')
branch_name="security/grouped-${safe_group_name}-updates"
git checkout -B "$branch_name"
while read -r number ref group; do
git fetch origin "$ref"
if ! git cherry-pick FETCH_HEAD; then
echo "Conflict found in $ref. Attempting to resolve."
conflict_files=($(git diff --name-only --diff-filter=U))
if [ ${#conflict_files[@]} -gt 0 ]; then
echo "Resolving conflicts in files: ${conflict_files[*]}"
for conflict_file in "${conflict_files[@]}"; do
echo "Resolving conflict in $conflict_file"
git checkout --theirs "$conflict_file"
git add "$conflict_file"
done
git cherry-pick --continue || {
echo "Failed to continue cherry-pick. Aborting."
git cherry-pick --abort
continue 2
}
else
echo "No conflicting files found. Aborting."
git cherry-pick --abort
continue 2
fi
fi
done < "$file"
# Non-destructive push: check for drift before force-pushing
if [ "$DRY_RUN" == "true" ]; then
echo "[DRY-RUN] Skipping git push for $branch_name"
else
remote_hash=$(git ls-remote origin "$branch_name" | awk '{print $1}')
local_hash=$(git rev-parse "$branch_name")
if [ -n "$remote_hash" ] && [ "$remote_hash" != "$local_hash" ]; then
echo "Remote branch $branch_name has diverged. Skipping force-push to avoid overwriting changes."
continue
fi
git push --force-with-lease origin "$branch_name"
fi
new_lines=""
while read -r number ref group; do
IFS="|" read -r title url _ <<< "${pr_metadata_map["$number"]}"
new_lines+="$title - [#$number]($url)\n"
done < "$file"
pr_title="chore(deps): bump grouped $group_name Dependabot updates"
# Add --state open to ensure only open PRs are considered
existing_url=$(gh pr list --head "$branch_name" --base "$TARGET_BRANCH" --state open --json url --jq '.[0].url // empty')
if [ -n "$existing_url" ]; then
echo "PR already exists: $existing_url"
pr_url="$existing_url"
current_body=$(gh pr view "$pr_url" --json body --jq .body)
# Simplified duplicate-detection using Bash array
IFS=$'\n' read -d '' -r -a current_lines < <(printf '%s\0' "$current_body")
IFS=$'\n' read -d '' -r -a new_lines_arr < <(printf '%b\0' "$new_lines")
declare -A seen
for line in "${current_lines[@]}"; do
seen["$line"]=1
done
filtered_lines=""
for line in "${new_lines_arr[@]}"; do
if [[ -n "$line" && -z "${seen["$line"]}" ]]; then
filtered_lines+="$line\n"
fi
done
# Ensure a newline separator between the existing body and new lines
if [ -n "$filtered_lines" ]; then
new_body="$current_body"$'\n'"$filtered_lines"
else
new_body="$current_body"
fi
if [ "$DRY_RUN" == "true" ]; then
echo "[DRY-RUN] Would update PR body for $pr_url"
else
tmpfile=$(mktemp)
printf '%s' "$new_body" > "$tmpfile"
gh pr edit "$pr_url" --body-file "$tmpfile"
rm -f "$tmpfile"
fi
else
pr_body=$(printf "This PR groups multiple open PRs by Dependabot for %s.\n\n%b" "$group_name" "$new_lines")
if [ "$DRY_RUN" == "true" ]; then
echo "[DRY-RUN] Would create PR titled: $pr_title"
echo "$pr_body"
pr_url=""
else
pr_url=$(gh pr create \
--title "$pr_title" \
--body "$pr_body" \
--base "$TARGET_BRANCH" \
--head "$branch_name")
fi
fi
if [ -n "$pr_url" ]; then
for number in $(cut -d ' ' -f1 "$file"); do
IFS="|" read -r _ _ labels <<< "${pr_metadata_map["$number"]}"
IFS="," read -ra label_arr <<< "$labels"
for label in "${label_arr[@]}"; do
if [ "$DRY_RUN" == "true" ]; then
echo "[DRY-RUN] Would add label $label to $pr_url"
else
gh pr edit "$pr_url" --add-label "$label"
fi
done
if [ "$DRY_RUN" == "true" ]; then
echo "[DRY-RUN] Would close PR #$number"
else
gh pr close "$number" --comment "Grouped into $pr_url."
fi
done
echo "Grouped PR created. Leaving branch $branch_name for now."
else
echo "Grouped PR was not created. Skipping closing of original PRs."
fi
done