Skip to content

Commit 719305c

Browse files
committed
fix: improve reliability of dependency automation workflows
- Replace check_suite trigger with workflow_run for deterministic auto-merge firing after Dependency Validation completes - Use combined status API to cover both GitHub Actions and Prow statuses - Expand dependency summary to diff all changed lockfiles, not just root - Discover all package.json files dynamically for override consistency - Document upstream package exclusion and label coupling between configs Made-with: Cursor
1 parent b0478b5 commit 719305c

4 files changed

Lines changed: 75 additions & 49 deletions

File tree

.github/dependabot.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ updates:
158158
open-pull-requests-limit: 5
159159
target-branch: "main"
160160
labels:
161+
# Keep in sync with exempt-pr-labels in .github/workflows/stale.yml
161162
- "dependencies"
162163
ignore:
163164
- dependency-name: "*"
@@ -171,3 +172,8 @@ updates:
171172
dependency-type: development
172173
security-patches:
173174
applies-to: security-updates
175+
176+
# Upstream-synced packages (model-registry, notebooks) are intentionally
177+
# excluded from Dependabot. Their deps are managed by their upstream repos
178+
# and synced in via the upstream-sync workflow. The dependency-validation
179+
# workflow still audits them when upstream syncs land.

.github/workflows/dependabot-auto-merge.yml

Lines changed: 22 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,32 @@
11
name: Dependabot Auto-merge
22

33
on:
4-
check_suite:
4+
# Trigger after the Dependency Validation workflow completes on Dependabot PRs.
5+
# Unlike check_suite (which fires per-app and can miss Prow statuses),
6+
# workflow_run fires deterministically when the named workflow finishes.
7+
workflow_run:
8+
workflows: ["Dependency Validation"]
59
types:
610
- completed
711

812
permissions:
913
contents: read
1014
pull-requests: write
15+
statuses: read
1116

1217
jobs:
1318
auto-merge:
1419
runs-on: ubuntu-latest
1520
if: >
16-
github.event.check_suite.conclusion == 'success' &&
17-
github.event.check_suite.app.slug == 'github-actions'
21+
github.event.workflow_run.conclusion == 'success' &&
22+
github.event.workflow_run.event == 'pull_request'
1823
steps:
19-
- name: Find Dependabot PR from check suite
24+
- name: Find Dependabot PR from workflow run
2025
id: pr
2126
env:
2227
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
2328
run: |
24-
HEAD_BRANCH="${{ github.event.check_suite.head_branch }}"
29+
HEAD_BRANCH="${{ github.event.workflow_run.head_branch }}"
2530
2631
if [[ ! "$HEAD_BRANCH" =~ ^dependabot/ ]]; then
2732
echo "Not a Dependabot branch, skipping"
@@ -39,41 +44,28 @@ jobs:
3944
fi
4045
echo "number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
4146
42-
- name: Verify all required checks passed
47+
- name: Verify combined commit status
4348
id: checks
4449
if: steps.pr.outputs.number
4550
env:
4651
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
4752
run: |
48-
HEAD_SHA="${{ github.event.check_suite.head_sha }}"
53+
HEAD_SHA="${{ github.event.workflow_run.head_sha }}"
4954
50-
CHECKS=$(gh api \
51-
"repos/${{ github.repository }}/commits/${HEAD_SHA}/check-runs" \
52-
--paginate \
53-
--jq '.check_runs[] | "\(.name)\t\(.conclusion // "pending")"')
55+
# The combined status API merges both check runs (GitHub Actions)
56+
# and commit statuses (Prow) into a single verdict.
57+
COMBINED=$(gh api "repos/${{ github.repository }}/commits/${HEAD_SHA}/status" \
58+
--jq '.state')
5459
55-
echo "All check runs:"
56-
echo "$CHECKS"
60+
echo "Combined commit status: $COMBINED"
5761
58-
READY=true
59-
60-
# Fail if any check run has explicitly failed or been cancelled
61-
FAILED=$(echo "$CHECKS" | grep -E " (failure|cancelled|timed_out|action_required)$" || true)
62-
if [ -n "$FAILED" ]; then
63-
echo "Failed checks found:"
64-
echo "$FAILED"
65-
READY=false
66-
fi
67-
68-
# Fail if any check is still pending
69-
PENDING=$(echo "$CHECKS" | grep -E " (pending|queued|in_progress)$" || true)
70-
if [ -n "$PENDING" ]; then
71-
echo "Checks still pending:"
72-
echo "$PENDING"
73-
READY=false
62+
if [ "$COMBINED" != "success" ]; then
63+
echo "Not all checks/statuses have passed (state=$COMBINED), skipping"
64+
echo "ready=false" >> "$GITHUB_OUTPUT"
65+
exit 0
7466
fi
7567
76-
echo "ready=$READY" >> "$GITHUB_OUTPUT"
68+
echo "ready=true" >> "$GITHUB_OUTPUT"
7769
7870
- uses: actions/checkout@v4
7971
if: steps.checks.outputs.ready == 'true'

.github/workflows/dependency-validation.yml

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -133,12 +133,18 @@ jobs:
133133
run: |
134134
node -e "
135135
const fs = require('fs');
136-
const paths = ['package.json', 'frontend/package.json', 'backend/package.json'];
136+
const path = require('path');
137+
const { execSync } = require('child_process');
138+
139+
const found = execSync(
140+
'find . -name package.json -not -path \"*/node_modules/*\" -not -path \"*/base/*\"',
141+
{ encoding: 'utf8' }
142+
).trim().split('\n').filter(Boolean).map(p => p.replace(/^\.\//, ''));
143+
137144
const overrides = {};
138145
let hasError = false;
139146
140-
for (const p of paths) {
141-
if (!fs.existsSync(p)) continue;
147+
for (const p of found) {
142148
const pkg = JSON.parse(fs.readFileSync(p, 'utf8'));
143149
if (!pkg.overrides) continue;
144150
@@ -165,15 +171,18 @@ jobs:
165171
console.error('Override ranges diverge across package.json files.');
166172
process.exit(1);
167173
} else {
168-
console.log('All shared overrides are consistent.');
174+
console.log('All shared overrides are consistent (' + found.length + ' files checked).');
169175
}
170176
"
171177
172178
dependency-summary:
179+
needs: detect-dirs
173180
runs-on: ubuntu-latest
174181
steps:
175182
- name: Checkout PR head
176183
uses: actions/checkout@v4
184+
with:
185+
fetch-depth: 0
177186

178187
- name: Checkout base
179188
uses: actions/checkout@v4
@@ -183,28 +192,46 @@ jobs:
183192

184193
- name: Generate dependency diff summary
185194
id: diff
195+
env:
196+
CHANGED_DIRS: ${{ needs.detect-dirs.outputs.dirs }}
186197
run: |
187198
node -e "
188199
const fs = require('fs');
189-
const baseLock = JSON.parse(fs.readFileSync('base/package-lock.json', 'utf8'));
190-
const headLock = JSON.parse(fs.readFileSync('package-lock.json', 'utf8'));
200+
const path = require('path');
201+
const dirs = JSON.parse(process.env.CHANGED_DIRS || '[]');
191202
192-
const basePkgs = baseLock.packages || {};
193-
const headPkgs = headLock.packages || {};
203+
const lockfiles = dirs
204+
.map(d => d === '.' ? 'package-lock.json' : path.join(d, 'package-lock.json'))
205+
.filter(f => fs.existsSync(f) && fs.existsSync(path.join('base', f)));
206+
207+
if (lockfiles.length === 0) {
208+
fs.writeFileSync('dep-summary.md', 'No dependency version changes detected.\n');
209+
process.exit(0);
210+
}
194211
195212
const changes = [];
196-
for (const [name, info] of Object.entries(headPkgs)) {
197-
const basePkg = basePkgs[name];
198-
if (!basePkg) {
199-
changes.push('| \`' + name.replace('node_modules/', '') + '\` | — | ' + info.version + ' | Added |');
200-
} else if (basePkg.version !== info.version) {
201-
changes.push('| \`' + name.replace('node_modules/', '') + '\` | ' + basePkg.version + ' | ' + info.version + ' | Updated |');
213+
for (const lockfile of lockfiles) {
214+
const baseLock = JSON.parse(fs.readFileSync(path.join('base', lockfile), 'utf8'));
215+
const headLock = JSON.parse(fs.readFileSync(lockfile, 'utf8'));
216+
const basePkgs = baseLock.packages || {};
217+
const headPkgs = headLock.packages || {};
218+
const dir = path.dirname(lockfile);
219+
const prefix = dir === '.' ? '' : dir + ': ';
220+
221+
for (const [name, info] of Object.entries(headPkgs)) {
222+
const basePkg = basePkgs[name];
223+
const short = prefix + name.replace('node_modules/', '');
224+
if (!basePkg) {
225+
changes.push('| \`' + short + '\` | — | ' + info.version + ' | Added |');
226+
} else if (basePkg.version !== info.version) {
227+
changes.push('| \`' + short + '\` | ' + basePkg.version + ' | ' + info.version + ' | Updated |');
228+
}
202229
}
203-
}
204-
for (const name of Object.keys(basePkgs)) {
205-
if (!headPkgs[name]) {
206-
const shortName = name.replace('node_modules/', '');
207-
changes.push('| \`' + shortName + '\` | ' + basePkgs[name].version + ' | — | Removed |');
230+
for (const name of Object.keys(basePkgs)) {
231+
if (!headPkgs[name]) {
232+
const short = prefix + name.replace('node_modules/', '');
233+
changes.push('| \`' + short + '\` | ' + basePkgs[name].version + ' | — | Removed |');
234+
}
208235
}
209236
}
210237

.github/workflows/stale.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,5 @@ jobs:
2828
stale-pr-message: 'This PR is stale because it has been open 21 days with no activity. Remove stale label or comment or this will be closed in 7 days.'
2929
close-pr-message: 'This PR was closed because it has been stale for 21+7 days with no activity.'
3030
stale-pr-label: 'Stale'
31+
# Must match the label Dependabot applies in .github/dependabot.yml
3132
exempt-pr-labels: 'dependencies'

0 commit comments

Comments
 (0)