forked from valkey-io/valkey-glide
-
Notifications
You must be signed in to change notification settings - Fork 0
370 lines (309 loc) · 15.7 KB
/
dependabot-management.yml
File metadata and controls
370 lines (309 loc) · 15.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
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
name: Enhanced Dependabot Management
# This workflow implements timing constraints for dependabot PRs:
# - Patch updates: Auto-approved after 1 hour (safe bug fixes)
# - Minor updates: Auto-approved after 1 week (except 0.x versions)
# - Major updates: Never auto-approved (breaking changes)
# - 0.x minor updates: Never auto-approved (unstable/breaking changes)
#
# Security considerations:
# - Only operates on dependabot-created PRs
# - Requires CI checks to pass before auto-approval
# - Manual review always possible before timing constraints are met
on:
schedule:
# Run every Monday at 10:00 AM UTC (after dependabot runs)
- cron: "0 10 * * 1"
workflow_dispatch:
inputs:
dry_run:
description: "Run in dry-run mode (no actual changes)"
required: false
default: "false"
type: boolean
permissions:
contents: read
pull-requests: write
issues: write
jobs:
manage-dependabot-prs:
runs-on: ubuntu-latest
name: Manage Dependabot PRs with timing constraints
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: "18"
- name: Install dependencies
run: |
cd .github/dependabot-deps
npm install
- name: Create PR management script
run: |
cat > dependabot-manager.js << 'EOF'
const { Octokit } = require("./.github/dependabot-deps/node_modules/@octokit/rest");
const semver = require("./.github/dependabot-deps/node_modules/semver");
const octokit = new Octokit({
auth: process.env.GITHUB_TOKEN,
});
const owner = process.env.GITHUB_REPOSITORY.split('/')[0];
const repo = process.env.GITHUB_REPOSITORY.split('/')[1];
const dryRun = process.env.DRY_RUN === 'true';
// Constants for timing constraints
const HOURS_TO_AUTO_APPROVE_PATCH = 1;
const DAYS_TO_AUTO_APPROVE_MINOR = 7;
const PATCH_APPROVAL_THRESHOLD_DAYS = HOURS_TO_AUTO_APPROVE_PATCH / 24; // Convert to days
// Regex for parsing dependabot PR titles
const DEPENDABOT_TITLE_REGEX = /^Bump (.+) from (.+) to (.+)$/;
// Helper function to parse dependabot PR title
function parseDependabotTitle(title) {
const match = title.match(DEPENDABOT_TITLE_REGEX);
if (!match) return null;
const [, depName, fromVersion, toVersion] = match;
return { depName, fromVersion, toVersion };
}
// Helper function to identify dependabot PRs more flexibly
function isDependabotPR(pr) {
return pr.user.login === 'dependabot[bot]' ||
pr.user.login === 'dependabot-preview[bot]' ||
pr.user.type === 'Bot' && pr.user.login.includes('dependabot') ||
pr.head.ref.startsWith('dependabot/');
}
// Helper function to determine ecosystem label from PR branch
function getEcosystemLabel(pr) {
const branch = pr.head.ref;
// Extract ecosystem from dependabot branch name
// Pattern: dependabot/{ecosystem}/{dependency_name}-{version}
const branchMatch = branch.match(/^dependabot\/([^\/]+)\//);
if (branchMatch) {
const ecosystem = branchMatch[1];
// Map dependabot ecosystem names to our label conventions
const ecosystemMap = {
'npm_and_yarn': 'node 🐢',
'npm': 'node 🐢',
'gradle': 'java ☕',
'cargo': 'Rust ⚙️',
'gomod': 'go 🏃',
'pip': 'python 🐍',
'github_actions': 'github-actions'
};
return ecosystemMap[ecosystem] || ecosystem;
}
return null;
}
async function main() {
console.log(`Managing Dependabot PRs for ${owner}/${repo}`);
console.log(`Dry run mode: ${dryRun}`);
// Get all open PRs created by dependabot
const { data: prs } = await octokit.rest.pulls.list({
owner,
repo,
state: 'open',
});
const dependabotPrs = prs.filter(isDependabotPR);
console.log(`Found ${dependabotPrs.length} dependabot PRs`);
for (const pr of dependabotPrs) {
await processPR(pr);
}
}
async function processPR(pr) {
console.log(`\nProcessing PR #${pr.number}: ${pr.title}`);
// Parse the PR title to extract dependency info
const parsedTitle = parseDependabotTitle(pr.title);
if (!parsedTitle) {
console.log(` Skipping - unable to parse title: ${pr.title}`);
return;
}
const { depName, fromVersion, toVersion } = parsedTitle;
console.log(` Dependency: ${depName} (${fromVersion} -> ${toVersion})`);
// Determine update type
const updateType = getUpdateType(fromVersion, toVersion);
console.log(` Update type: ${updateType}`);
// Add changelog to PR if not already present
await addChangelogToPR(pr, depName, fromVersion, toVersion);
// Apply labels based on update type
await applyLabels(pr, updateType);
// Check if PR should be auto-approved based on timing
const shouldAutoApprove = await shouldAutoApprovePR(pr, updateType);
console.log(` Should auto-approve: ${shouldAutoApprove}`);
if (shouldAutoApprove && !dryRun) {
await autoApprovePR(pr);
}
}
function getUpdateType(fromVersion, toVersion) {
try {
const from = semver.coerce(fromVersion);
const to = semver.coerce(toVersion);
if (!from || !to) {
return 'unknown';
}
if (semver.major(from) !== semver.major(to)) {
return 'major';
} else if (semver.minor(from) !== semver.minor(to)) {
return 'minor';
} else if (semver.patch(from) !== semver.patch(to)) {
return 'patch';
}
return 'unknown';
} catch (error) {
console.log(` Error determining update type: ${error.message}`);
return 'unknown';
}
}
async function shouldAutoApprovePR(pr, updateType) {
const createdAt = new Date(pr.created_at);
const now = new Date();
const ageInDays = (now - createdAt) / (1000 * 60 * 60 * 24);
console.log(` PR age: ${ageInDays.toFixed(1)} days`);
// Never auto-approve major version updates
if (updateType === 'major') {
console.log(` Major update - never auto-approve`);
return false;
}
// Check if this is a 0.x version (unstable) - don't auto-approve minor updates
if (updateType === 'minor') {
const parsedTitle = parseDependabotTitle(pr.title);
if (parsedTitle) {
const { fromVersion, toVersion } = parsedTitle;
try {
const from = semver.coerce(fromVersion);
const to = semver.coerce(toVersion);
if (from && to && (semver.major(from) === 0 || semver.major(to) === 0)) {
console.log(` 0.x version detected - not auto-approving minor update`);
return false;
}
} catch (error) {
console.log(` Error parsing version for 0.x check: ${error.message}`);
}
}
}
switch (updateType) {
case 'patch':
// Patch updates: wait for CI completion
return ageInDays >= PATCH_APPROVAL_THRESHOLD_DAYS;
case 'minor':
// Minor updates: after 1 week (but not for 0.x versions, handled above)
return ageInDays >= DAYS_TO_AUTO_APPROVE_MINOR;
default:
// Unknown updates: do not auto-approve
return false;
}
}
async function addChangelogToPR(pr, depName, fromVersion, toVersion) {
try {
// Check if changelog is already added
const { data: prData } = await octokit.rest.pulls.get({
owner,
repo,
pull_number: pr.number,
});
const body = prData.body || '';
if (body.includes('## Changelog') || body.includes('## Release Notes')) {
console.log(` Changelog already present`);
return;
}
// Add basic changelog information
const changelog = `## Changelog\n\nUpdated ${depName} from ${fromVersion} to ${toVersion}\n\n📋 To view detailed changes, visit the package repository or release notes.`;
const newBody = `${body}\n\n${changelog}`;
if (!dryRun) {
await octokit.rest.pulls.update({
owner,
repo,
pull_number: pr.number,
body: newBody,
});
console.log(` Added changelog to PR`);
} else {
console.log(` Would add changelog to PR (dry run)`);
}
} catch (error) {
console.log(` Error adding changelog: ${error.message}`);
}
}
async function applyLabels(pr, updateType) {
try {
const labels = ['dependencies'];
// Add update type label
if (updateType !== 'unknown') {
labels.push(`dependency-${updateType}`);
}
// Add ecosystem-specific label based on PR branch or existing labels
const ecosystemLabel = getEcosystemLabel(pr);
if (ecosystemLabel) {
labels.push(ecosystemLabel);
}
if (!dryRun) {
await octokit.rest.issues.addLabels({
owner,
repo,
issue_number: pr.number,
labels: labels,
});
console.log(` Added labels: ${labels.join(', ')}`);
} else {
console.log(` Would add labels: ${labels.join(', ')} (dry run)`);
}
} catch (error) {
console.log(` Error adding labels: ${error.message}`);
}
}
async function autoApprovePR(pr) {
try {
// Check if PR has required checks passing
const { data: checks } = await octokit.rest.checks.listForRef({
owner,
repo,
ref: pr.head.sha,
});
// Only allow auto-approval if all checks have concluded successfully
const incompleteChecks = checks.check_runs.filter(check =>
check.conclusion !== 'success' && check.conclusion !== 'skipped'
);
if (incompleteChecks.length > 0) {
const failedChecks = incompleteChecks.filter(check =>
check.conclusion === 'failure' || check.conclusion === 'cancelled'
);
const pendingChecks = incompleteChecks.filter(check =>
check.status === 'in_progress' || check.status === 'queued' || check.conclusion === null
);
if (failedChecks.length > 0) {
console.log(` Cannot auto-approve - failed checks: ${failedChecks.map(c => c.name).join(', ')}`);
} else {
console.log(` Cannot auto-approve - pending checks: ${pendingChecks.map(c => c.name).join(', ')}`);
}
return;
}
// Auto-approve the PR
await octokit.rest.pulls.createReview({
owner,
repo,
pull_number: pr.number,
event: 'APPROVE',
body: 'Auto-approved by enhanced dependabot workflow after timing constraints were met.',
});
console.log(` Auto-approved PR #${pr.number}`);
} catch (error) {
console.log(` Error auto-approving PR: ${error.message}`);
}
}
main().catch(console.error);
EOF
- name: Run Dependabot PR management
env:
DRY_RUN: ${{ inputs.dry_run || 'false' }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
run: |
node dependabot-manager.js
- name: Summary
env:
DRY_RUN: ${{ inputs.dry_run || 'false' }}
run: |
echo "## Dependabot Management Summary" >> $GITHUB_STEP_SUMMARY
echo "- Processed dependabot PRs with timing constraints" >> $GITHUB_STEP_SUMMARY
echo "- Patch updates: Auto-approve after 1 hour" >> $GITHUB_STEP_SUMMARY
echo "- Minor updates: Auto-approve after 1 week (except for 0.x versions)" >> $GITHUB_STEP_SUMMARY
echo "- Major updates: Never auto-approve (manual review required)" >> $GITHUB_STEP_SUMMARY
echo "- 0.x versions: Never auto-approve minor updates (unstable)" >> $GITHUB_STEP_SUMMARY
echo "- Dry run mode: \"$DRY_RUN\"" >> $GITHUB_STEP_SUMMARY