microsoft/TypeAgent

Public

mirrored from https://github.com/microsoft/TypeAgentAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
container

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

.github/workflows/fix-dependabot-alerts.yml

575lines · modecode

1# Copyright (c) Microsoft Corporation.
2# Licensed under the MIT License.
3
4# Automatically remediate Dependabot security alerts by running the
5# fix-dependabot-alerts script, verifying the build for each fix,
6# and opening a pull request with the passing changes.
7#
8# Uses a GitHub App (via actions/create-github-app-token) to authenticate
9# with the Dependabot alerts REST API, since GITHUB_TOKEN does not have
10# access to this endpoint. Requires DEPENDABOT_APP_ID (variable) and
11# DEPENDABOT_APP_PRIVATE_KEY (secret).
12
13name: fix-dependabot-alerts
14
15on:
16 schedule:
17 # Run daily at 9:00 UTC
18 - cron: "0 9 * * *"
19 workflow_dispatch:
20 inputs:
21 dry-run:
22 description: "Dry run — analyse only, don't apply fixes"
23 type: boolean
24 default: false
25 auto-fix-args:
26 description: "Extra flags for the script (e.g. --apply-overrides=pkg1,pkg2)"
27 type: string
28 default: "--auto-fix"
29
30concurrency:
31 group: ${{ github.workflow }}
32 cancel-in-progress: true
33
34permissions:
35 contents: write
36 pull-requests: write
37
38env:
39 ELECTRON_CACHE: ${{ github.workspace }}/.cache/electron
40 ELECTRON_BUILDER_CACHE: ${{ github.workspace }}/.cache/electron-builder
41
42jobs:
43 fix-alerts:
44 runs-on: ubuntu-latest
45
46 steps:
47 - uses: actions/checkout@v4
48 with:
49 fetch-depth: 0
50
51 - uses: pnpm/action-setup@v4
52 name: Install pnpm
53 with:
54 package_json_file: ts/package.json
55
56 - uses: actions/setup-node@v4
57 with:
58 node-version: 22
59 cache: "pnpm"
60 cache-dependency-path: ts/pnpm-lock.yaml
61
62 - name: Generate GitHub App token
63 id: app-token
64 uses: actions/create-github-app-token@v1
65 with:
66 app-id: ${{ vars.DEPENDABOT_APP_ID }}
67 private-key: ${{ secrets.DEPENDABOT_APP_PRIVATE_KEY }}
68
69 - name: Verify gh authentication
70 env:
71 GH_TOKEN: ${{ steps.app-token.outputs.token }}
72 run: |
73 gh auth status
74 echo "---"
75 # Probe the Dependabot API with a 1-item page. If the call fails the
76 # subsequent discovery step would silently produce an empty workspace
77 # list and the entire job would report "No fixable alerts found" —
78 # masking infra outages as "everything's fine". Fail fast instead.
79 if ! gh api "repos/${{ github.repository }}/dependabot/alerts?per_page=1" --jq 'length'; then
80 echo "::error::Dependabot API probe failed — aborting before silently misreporting alerts"
81 exit 1
82 fi
83
84 # Install ts/ dependencies once before the per-workspace loop.
85 # The fix-dependabot-alerts.mjs script lives under ts/tools/scripts/ and
86 # imports node modules (chalk, etc.) from ts/node_modules. Without this
87 # step, when the per-workspace loop processes a non-ts workspace first
88 # (e.g. docs/), Node fails with ERR_MODULE_NOT_FOUND for chalk and the
89 # workflow silently logs "No valid analysis for <ws> — skipping".
90 - name: Install script dependencies (ts workspace)
91 working-directory: ts
92 run: |
93 corepack enable
94 pnpm install --frozen-lockfile
95
96 # Restore per-package rollback memory across runs. When a fix attempt
97 # rolls back (build/install failure), we record the package + lockfile
98 # SHA so subsequent daily runs skip retrying the same broken upgrade
99 # until either (a) the cooldown expires (default 7 days) or (b) the
100 # underlying lockfile changes (different SHA → potentially different
101 # outcome). Without this, the workflow burns CI minutes attempting
102 # the same hopeless rollback every single day.
103 - name: Restore rollback memory
104 id: rollback-cache
105 uses: actions/cache@v4
106 with:
107 path: /tmp/dep-rollback-state.json
108 key: dep-rollback-state-v1-${{ github.run_id }}
109 restore-keys: |
110 dep-rollback-state-v1-
111
112 # ── Analyse and fix alerts across workspaces ──────────────────────
113 #
114 # Auto-discovers which workspaces have open alerts by querying the
115 # Dependabot API, then for each workspace:
116 # 1. Installs dependencies (if not already installed)
117 # 2. Dry-run to discover fixable packages
118 # 3. Applies each package's fix individually
119 # 4. Build-checks after each; rolls back failures
120 # 5. Only passing fixes survive into the PR
121 - name: Analyse and fix alerts
122 id: fix
123 env:
124 GH_TOKEN: ${{ steps.app-token.outputs.token }}
125 run: |
126 SCRIPT="$GITHUB_WORKSPACE/ts/tools/scripts/fix-dependabot-alerts.mjs"
127 FLAGS="${{ inputs.auto-fix-args || '--auto-fix' }}"
128 ALL_APPLIED=""
129 ALL_ROLLED_BACK=""
130 ALL_SKIPPED_RECENT_ROLLBACK=""
131 ALL_BLOCKED_PACKAGES=""
132 ALL_NO_PATCH_PACKAGES=""
133 ALL_FAILED_WORKSPACES=""
134 TOTAL_RESOLVED=0
135 TOTAL_BLOCKED=0
136 TOTAL_NO_PATCH=0
137 TOTAL_FAILED=0
138 TOTAL_SKIPPED=0
139
140 # Rollback memory: skip retrying any (workspace,pkg) that rolled
141 # back recently against the *current* lockfile SHA. Cooldown is in
142 # days; entries older than this are considered stale and retried.
143 ROLLBACK_STATE=/tmp/dep-rollback-state.json
144 ROLLBACK_COOLDOWN_DAYS=7
145 if [ ! -f "$ROLLBACK_STATE" ]; then
146 echo '{"version":1,"rollbacks":{}}' > "$ROLLBACK_STATE"
147 fi
148 NOW_EPOCH=$(date -u +%s)
149 COOLDOWN_SECONDS=$((ROLLBACK_COOLDOWN_DAYS * 86400))
150
151 # Discover workspaces with open npm alerts from the Dependabot API.
152 #
153 # NOTE: `split("/")[0]` collapses every alert manifest_path to its
154 # leading directory (so `ts/packages/X/package.json` → `ts`). This is
155 # correct for this repo because the only pnpm workspace roots are
156 # `ts/` and `docs/`, but it would silently misroute alerts whose
157 # manifest_path points outside those roots (e.g. a future workspace
158 # under `subprojects/foo/package.json`). Revisit if/when a third
159 # workspace root is added.
160 WORKSPACES=$(gh api "repos/${{ github.repository }}/dependabot/alerts?state=open&per_page=100" \
161 --paginate \
162 --jq '[.[] | select(.dependency.package.ecosystem == "npm") | .dependency.manifest_path | split("/")[0]] | unique | .[]')
163
164 if [ -z "$WORKSPACES" ]; then
165 echo "No open npm Dependabot alerts found 🎉"
166 echo "resolved=0" >> "$GITHUB_OUTPUT"
167 echo "blocked=0" >> "$GITHUB_OUTPUT"
168 echo "no_patch=0" >> "$GITHUB_OUTPUT"
169 echo "failed=0" >> "$GITHUB_OUTPUT"
170 echo "changes=false" >> "$GITHUB_OUTPUT"
171 exit 0
172 fi
173 echo "Workspaces with alerts: $WORKSPACES"
174
175 for WS_DIR in $WORKSPACES; do
176 echo "=========================================="
177 echo "Processing workspace: $WS_DIR"
178 echo "=========================================="
179 cd "$GITHUB_WORKSPACE/$WS_DIR"
180
181 # Install dependencies for this workspace.
182 # The `ts` workspace was already installed in the "Install script
183 # dependencies (ts workspace)" step — re-running pnpm install here
184 # re-triggers postinstall scripts (e.g. better-sqlite3 copy/restore)
185 # which can segfault on the second pass. Skip the redundant install.
186 echo "::group::Installing $WS_DIR dependencies"
187 corepack enable
188 if [ "$WS_DIR" = "ts" ]; then
189 echo "ts dependencies already installed in setup step; skipping redundant install"
190 elif ! pnpm install --frozen-lockfile 2>&1; then
191 echo "::warning::Dependency install failed for $WS_DIR — skipping (analysis would be unreliable)"
192 echo "::endgroup::"
193 continue
194 fi
195 echo "::endgroup::"
196
197 # ── Step 1: Discover fixable packages ───────────────────
198 echo "::group::Analysing $WS_DIR alerts"
199 node "$SCRIPT" --dry-run --json --skip-install > /tmp/dep-analysis.json 2>/tmp/dep-analysis.log || true
200
201 if ! jq -e '.summary' /tmp/dep-analysis.json > /dev/null 2>&1; then
202 echo "::warning::No valid analysis for $WS_DIR — see log below"
203 cat /tmp/dep-analysis.log || true
204 # Surface to job summary so the failure isn't silent across runs.
205 {
206 echo "### ⚠️ Analysis failed for \`$WS_DIR\`"
207 echo ""
208 echo '```'
209 tail -30 /tmp/dep-analysis.log 2>/dev/null || echo "(no log captured)"
210 echo '```'
211 echo ""
212 } >> "$GITHUB_STEP_SUMMARY"
213 # Track for the PR body / final summary.
214 ALL_FAILED_WORKSPACES="${ALL_FAILED_WORKSPACES:+$ALL_FAILED_WORKSPACES, }$WS_DIR"
215 echo "::endgroup::"
216 continue
217 fi
218
219 WS_BLOCKED=$(jq '.summary.blocked' /tmp/dep-analysis.json)
220 WS_NO_PATCH=$(jq '.summary.noPatch' /tmp/dep-analysis.json)
221 BLOCKED_PKGS=$(jq -r '[.blocked[].package] | unique | join(", ")' /tmp/dep-analysis.json)
222 NO_PATCH_PKGS=$(jq -r '[.noPatch[].package] | unique | join(", ")' /tmp/dep-analysis.json)
223
224 # If there are blocked packages, capture --show-chains output
225 # for the PR body so reviewers can see why each was blocked
226 # without having to re-run the script locally.
227 if [ "$WS_BLOCKED" -gt 0 ]; then
228 echo "Capturing dependency chains for $WS_BLOCKED blocked package(s) in $WS_DIR"
229 CHAINS_OUT="/tmp/dep-chains-$WS_DIR.txt"
230 NO_COLOR=1 node "$SCRIPT" --dry-run --show-chains --skip-install \
231 > "$CHAINS_OUT" 2>&1 || true
232 if [ -s "$CHAINS_OUT" ]; then
233 {
234 echo ""
235 echo "===== $WS_DIR ====="
236 cat "$CHAINS_OUT"
237 } >> /tmp/all-blocked-chains.txt
238 fi
239 fi
240
241 FIXABLE=$(jq -r '.resolved[].package' /tmp/dep-analysis.json | sort -u)
242 FIXABLE_COUNT=$(echo "$FIXABLE" | grep -c . || true)
243 echo "Fixable $WS_DIR packages ($FIXABLE_COUNT): $FIXABLE"
244 echo "::endgroup::"
245
246 # Accumulate blocked/no-patch
247 TOTAL_BLOCKED=$((TOTAL_BLOCKED + WS_BLOCKED))
248 TOTAL_NO_PATCH=$((TOTAL_NO_PATCH + WS_NO_PATCH))
249 [ -n "$BLOCKED_PKGS" ] && ALL_BLOCKED_PACKAGES="${ALL_BLOCKED_PACKAGES:+$ALL_BLOCKED_PACKAGES, }$BLOCKED_PKGS"
250 [ -n "$NO_PATCH_PKGS" ] && ALL_NO_PATCH_PACKAGES="${ALL_NO_PATCH_PACKAGES:+$ALL_NO_PATCH_PACKAGES, }$NO_PATCH_PKGS"
251
252 if [ "$FIXABLE_COUNT" -eq 0 ]; then
253 echo "No fixable alerts in $WS_DIR"
254 continue
255 fi
256
257 if [ "${{ inputs.dry-run }}" == "true" ]; then
258 TOTAL_RESOLVED=$((TOTAL_RESOLVED + FIXABLE_COUNT))
259 continue
260 fi
261
262 # ── Step 2: Apply fixes one package at a time ───────────
263 for PKG in $FIXABLE; do
264 echo "::group::Fixing $WS_DIR/$PKG"
265
266 # Recompute LOCK_SHA per package: earlier successful fixes in
267 # the same run mutate pnpm-lock.yaml, so a once-per-workspace
268 # snapshot would make the rollback cooldown skip key drift away
269 # from the actual on-disk lockfile state.
270 LOCK_SHA=$(sha256sum pnpm-lock.yaml 2>/dev/null | awk '{print $1}' || echo "no-lock")
271
272 # Skip if this (workspace,pkg,lockSha) recently rolled back.
273 ROLLBACK_KEY="$WS_DIR/$PKG"
274 PREV_TS=$(jq -r --arg k "$ROLLBACK_KEY" --arg sha "$LOCK_SHA" \
275 '.rollbacks[$k] | select(.lockSha == $sha) | .timestamp // empty' \
276 "$ROLLBACK_STATE")
277 if [ -n "$PREV_TS" ]; then
278 AGE=$((NOW_EPOCH - PREV_TS))
279 if [ "$AGE" -lt "$COOLDOWN_SECONDS" ]; then
280 AGE_DAYS=$((AGE / 86400))
281 echo "⏭️ Skipping $PKG: rolled back ${AGE_DAYS}d ago against same lockfile (cooldown ${ROLLBACK_COOLDOWN_DAYS}d)"
282 ALL_SKIPPED_RECENT_ROLLBACK="$ALL_SKIPPED_RECENT_ROLLBACK $PKG"
283 TOTAL_SKIPPED=$((TOTAL_SKIPPED + 1))
284 echo "::endgroup::"
285 continue
286 fi
287 fi
288
289 # Save a rollback point
290 cp package.json /tmp/pkg-backup.json
291 cp pnpm-lock.yaml /tmp/lock-backup.yaml 2>/dev/null || true
292
293 # Build targeted args
294 read -r -a extra_args <<< "$FLAGS"
295 filtered_args=()
296 for arg in "${extra_args[@]}"; do
297 case "$arg" in
298 --auto-fix|--auto-fix=*)
299 ;;
300 *)
301 filtered_args+=("$arg")
302 ;;
303 esac
304 done
305 filtered_args+=("--auto-fix=$PKG")
306 filtered_args+=("--skip-install")
307 echo "Running: fix-dependabot-alerts.mjs ${filtered_args[*]}"
308 set +e
309 node "$SCRIPT" "${filtered_args[@]}" 2>&1
310 fix_exit=$?
311 set -e
312
313 if [ "$fix_exit" -ne 0 ] && git diff --quiet; then
314 echo "::warning::Script failed for $PKG in $WS_DIR (exit $fix_exit) with no file changes"
315 TOTAL_FAILED=$((TOTAL_FAILED + 1))
316 echo "::endgroup::"
317 continue
318 fi
319
320 if ! git diff --quiet; then
321 echo "Changes detected, reinstalling and verifying build..."
322 set +e
323 if [ "$WS_DIR" = "ts" ]; then
324 pnpm install --frozen-lockfile --strict-peer-dependencies 2>&1
325 else
326 pnpm install --frozen-lockfile 2>&1
327 fi
328 install_exit=$?
329 set -e
330
331 if [ "$install_exit" -ne 0 ]; then
332 echo "::warning::pnpm install failed after fixing $PKG in $WS_DIR — rolling back"
333 cp /tmp/pkg-backup.json package.json
334 cp /tmp/lock-backup.yaml pnpm-lock.yaml 2>/dev/null || true
335 pnpm install 2>&1 || true
336 git checkout -- . 2>/dev/null || true
337 ALL_ROLLED_BACK="$ALL_ROLLED_BACK $PKG"
338 TOTAL_FAILED=$((TOTAL_FAILED + 1))
339 jq --arg k "$ROLLBACK_KEY" --arg sha "$LOCK_SHA" \
340 --argjson ts "$NOW_EPOCH" --arg reason "install failed" \
341 '.rollbacks[$k] = {lockSha:$sha,timestamp:$ts,reason:$reason}' \
342 "$ROLLBACK_STATE" > "$ROLLBACK_STATE.tmp" && mv "$ROLLBACK_STATE.tmp" "$ROLLBACK_STATE"
343 echo "::endgroup::"
344 continue
345 fi
346
347 if [ "$WS_DIR" = "ts" ]; then
348 node tools/scripts/repo-policy-check.mjs --fix 2>/dev/null || true
349 fi
350
351 set +e
352 pnpm run build 2>&1
353 build_exit=$?
354 set -e
355
356 if [ "$build_exit" -ne 0 ]; then
357 echo "::warning::Build failed after fixing $PKG in $WS_DIR — rolling back"
358 cp /tmp/pkg-backup.json package.json
359 cp /tmp/lock-backup.yaml pnpm-lock.yaml 2>/dev/null || true
360 pnpm install 2>&1 || true
361 git checkout -- . 2>/dev/null || true
362 ALL_ROLLED_BACK="$ALL_ROLLED_BACK $PKG"
363 TOTAL_FAILED=$((TOTAL_FAILED + 1))
364 jq --arg k "$ROLLBACK_KEY" --arg sha "$LOCK_SHA" \
365 --argjson ts "$NOW_EPOCH" --arg reason "build failed" \
366 '.rollbacks[$k] = {lockSha:$sha,timestamp:$ts,reason:$reason}' \
367 "$ROLLBACK_STATE" > "$ROLLBACK_STATE.tmp" && mv "$ROLLBACK_STATE.tmp" "$ROLLBACK_STATE"
368 else
369 echo "✅ $PKG fixed and build passed"
370 ALL_APPLIED="$ALL_APPLIED $PKG"
371 TOTAL_RESOLVED=$((TOTAL_RESOLVED + 1))
372 # Successful upgrade — clear any stale rollback record.
373 jq --arg k "$ROLLBACK_KEY" 'del(.rollbacks[$k])' \
374 "$ROLLBACK_STATE" > "$ROLLBACK_STATE.tmp" && mv "$ROLLBACK_STATE.tmp" "$ROLLBACK_STATE"
375 fi
376 else
377 echo "No changes for $PKG (may already be fixed)"
378 fi
379
380 echo "::endgroup::"
381 done
382 done
383
384 # ── Step 3: Report results ──────────────────────────────────
385 # Prune rollback entries older than the cooldown window so the
386 # state file doesn't grow unbounded.
387 STALE_THRESHOLD=$((NOW_EPOCH - COOLDOWN_SECONDS))
388 jq --argjson th "$STALE_THRESHOLD" \
389 '.rollbacks |= with_entries(select(.value.timestamp >= $th))' \
390 "$ROLLBACK_STATE" > "$ROLLBACK_STATE.tmp" && mv "$ROLLBACK_STATE.tmp" "$ROLLBACK_STATE"
391
392 echo "resolved=$TOTAL_RESOLVED" >> "$GITHUB_OUTPUT"
393 echo "blocked=$TOTAL_BLOCKED" >> "$GITHUB_OUTPUT"
394 echo "no_patch=$TOTAL_NO_PATCH" >> "$GITHUB_OUTPUT"
395 echo "failed=$TOTAL_FAILED" >> "$GITHUB_OUTPUT"
396 echo "skipped=$TOTAL_SKIPPED" >> "$GITHUB_OUTPUT"
397 echo "applied_packages=$ALL_APPLIED" >> "$GITHUB_OUTPUT"
398 echo "rolled_back_packages=$ALL_ROLLED_BACK" >> "$GITHUB_OUTPUT"
399 echo "skipped_packages=$ALL_SKIPPED_RECENT_ROLLBACK" >> "$GITHUB_OUTPUT"
400 echo "blocked_packages=$ALL_BLOCKED_PACKAGES" >> "$GITHUB_OUTPUT"
401 echo "no_patch_packages=$ALL_NO_PATCH_PACKAGES" >> "$GITHUB_OUTPUT"
402 echo "failed_workspaces=$ALL_FAILED_WORKSPACES" >> "$GITHUB_OUTPUT"
403
404 cd "$GITHUB_WORKSPACE"
405 if git diff --quiet; then
406 echo "changes=false" >> "$GITHUB_OUTPUT"
407 else
408 echo "changes=true" >> "$GITHUB_OUTPUT"
409 echo "Files changed:"
410 git diff --stat
411 fi
412
413 # ── Final build + shell packaging verification ──────────────────
414 - name: Final build verification
415 if: ${{ steps.fix.outputs.changes == 'true' }}
416 id: build
417 working-directory: ts
418 run: |
419 node tools/scripts/repo-policy-check.mjs --fix || true
420 pnpm run build
421 echo "build_ok=true" >> "$GITHUB_OUTPUT"
422 env:
423 DEBUG_DEMB: true
424
425 - name: Package - shell
426 if: ${{ steps.build.outputs.build_ok == 'true' }}
427 id: shell
428 working-directory: ts
429 run: |
430 pnpm run shell:package
431 echo "shell_ok=true" >> "$GITHUB_OUTPUT"
432
433 # ── Create PR ───────────────────────────────────────────────────
434 - name: Create pull request
435 if: ${{ steps.fix.outputs.changes == 'true' && steps.build.outputs.build_ok == 'true' }}
436 env:
437 GH_TOKEN: ${{ steps.app-token.outputs.token }}
438 run: |
439 BRANCH="automated/fix-dependabot-alerts-$(date +%Y%m%d)-${{ github.run_number }}"
440
441 git config user.name "github-actions[bot]"
442 git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
443
444 git checkout -b "$BRANCH"
445 git add -A
446 git commit -m "fix: remediate Dependabot security alerts
447
448 Automated by fix-dependabot-alerts workflow.
449
450 Applied:${{ steps.fix.outputs.applied_packages }}
451 Rolled back:${{ steps.fix.outputs.rolled_back_packages || ' (none)' }}
452 Blocked: ${{ steps.fix.outputs.blocked }} package(s)
453 Shell packaging: ${{ steps.shell.outputs.shell_ok == 'true' && 'passed' || 'skipped' }}
454
455 Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>"
456
457 git push origin "$BRANCH"
458
459 # Close any previously-open PRs from this workflow before opening a
460 # new one. Each daily run produces a unique branch (date + run_number),
461 # so without dedup the repo accumulates stacking duplicate PRs when
462 # yesterday's hasn't merged yet. The freshest PR always wins because
463 # it incorporates the latest set of fixes (and re-verifies the build).
464 PREV_PRS=$(gh pr list \
465 --state open \
466 --search 'head:automated/fix-dependabot-alerts- in:branch' \
467 --json number,headRefName \
468 --jq '.[] | select(.headRefName != "'"$BRANCH"'") | .number')
469 if [ -n "$PREV_PRS" ]; then
470 echo "Closing superseded Dependabot fix PRs: $PREV_PRS"
471 for PR in $PREV_PRS; do
472 gh pr close "$PR" \
473 --delete-branch \
474 --comment "Superseded by a newer automated Dependabot fix PR." \
475 || echo "::warning::Failed to close PR #$PR"
476 done
477 fi
478
479 # Build PR body from step outputs
480 APPLIED="${{ steps.fix.outputs.applied_packages }}"
481 ROLLED="${{ steps.fix.outputs.rolled_back_packages }}"
482 RESOLVED="${{ steps.fix.outputs.resolved }}"
483 BLOCKED="${{ steps.fix.outputs.blocked }}"
484 FAILED="${{ steps.fix.outputs.failed }}"
485 BLOCKED_PKGS="${{ steps.fix.outputs.blocked_packages }}"
486 NO_PATCH_PKGS="${{ steps.fix.outputs.no_patch_packages }}"
487
488 BODY=$(cat <<'PREOF'
489 ## Automated Dependabot Alert Remediation
490
491 This PR was automatically generated by the `fix-dependabot-alerts` workflow.
492 Each fix was applied individually and build-verified before inclusion.
493
494 ### Summary
495 PREOF
496 )
497
498 BODY="$BODY
499 - **Applied ($RESOLVED):**$APPLIED
500 - **Blocked ($BLOCKED):**${BLOCKED_PKGS:- (none)}
501 - **No patch available (${{ steps.fix.outputs.no_patch }}):**${NO_PATCH_PKGS:- (none)}
502 - **Rolled back ($FAILED):**${ROLLED:- (none)}
503 - **Skipped (recent rollback, ${{ steps.fix.outputs.skipped }}):**${{ steps.fix.outputs.skipped_packages || ' (none)' }}
504 - **Workspaces with analysis failures:**${{ steps.fix.outputs.failed_workspaces && format(' {0}', steps.fix.outputs.failed_workspaces) || ' (none)' }}
505 - **Build:** ✅ Passed
506 - **Shell packaging:** ${{ steps.shell.outputs.shell_ok == 'true' && '✅ Passed' || '⚠️ Skipped' }}
507
508 > _Note: the analysis source (\`fix-dependabot-alerts.mjs\`) is broader than the GitHub Dependabot REST API — it also audits the lockfile directly. Some packages listed above may not have a corresponding open Dependabot alert, and vice versa._
509 "
510
511 # Embed dependency chain output for blocked packages so reviewers
512 # can see why each package couldn't be auto-fixed.
513 if [ -s /tmp/all-blocked-chains.txt ]; then
514 CHAINS=$(cat /tmp/all-blocked-chains.txt)
515 BODY="$BODY
516 ### Why blocked packages couldn't be auto-fixed
517 <details><summary>Dependency chains (\`--show-chains\` output)</summary>
518
519 \`\`\`
520 $CHAINS
521 \`\`\`
522
523 </details>
524 "
525 fi
526
527 BODY="$BODY
528 ### How this works
529 1. Analyses all open Dependabot alerts
530 2. Applies each fix **individually** with build verification
531 3. Rolls back any fix that breaks the build
532 4. Only passing fixes are included in this PR
533
534 ### Review checklist
535 - [ ] Check that no breaking changes were introduced
536 - [ ] Verify rolled-back packages are investigated separately
537 - [ ] Run tests locally if concerned about specific packages"
538
539 gh pr create \
540 --base main \
541 --head "$BRANCH" \
542 --title "fix: remediate Dependabot security alerts ($(date +%Y-%m-%d))" \
543 --body "$BODY" \
544 --label "dependencies,security"
545
546 # ── Summary ─────────────────────────────────────────────────────
547 - name: Job summary
548 if: always()
549 run: |
550 echo "## Dependabot Alert Remediation" >> "$GITHUB_STEP_SUMMARY"
551 echo "" >> "$GITHUB_STEP_SUMMARY"
552 echo "| Metric | Count |" >> "$GITHUB_STEP_SUMMARY"
553 echo "|--------|-------|" >> "$GITHUB_STEP_SUMMARY"
554 echo "| Applied | ${{ steps.fix.outputs.resolved || '0' }} |" >> "$GITHUB_STEP_SUMMARY"
555 echo "| Blocked | ${{ steps.fix.outputs.blocked || '0' }} |" >> "$GITHUB_STEP_SUMMARY"
556 echo "| No patch available | ${{ steps.fix.outputs.no_patch || '0' }} |" >> "$GITHUB_STEP_SUMMARY"
557 echo "| Rolled back | ${{ steps.fix.outputs.failed || '0' }} |" >> "$GITHUB_STEP_SUMMARY"
558 echo "" >> "$GITHUB_STEP_SUMMARY"
559 if [ -n "${{ steps.fix.outputs.applied_packages }}" ]; then
560 echo "**Applied:**${{ steps.fix.outputs.applied_packages }}" >> "$GITHUB_STEP_SUMMARY"
561 fi
562 if [ -n "${{ steps.fix.outputs.rolled_back_packages }}" ]; then
563 echo "**Rolled back:**${{ steps.fix.outputs.rolled_back_packages }}" >> "$GITHUB_STEP_SUMMARY"
564 fi
565 if [ -n "${{ steps.fix.outputs.failed_workspaces }}" ]; then
566 echo "**⚠️ Analysis failed for workspaces:** ${{ steps.fix.outputs.failed_workspaces }}" >> "$GITHUB_STEP_SUMMARY"
567 fi
568 echo "" >> "$GITHUB_STEP_SUMMARY"
569 if [ "${{ steps.fix.outputs.changes }}" == "true" ]; then
570 echo "✅ Changes applied and PR created" >> "$GITHUB_STEP_SUMMARY"
571 elif [ "${{ inputs.dry-run }}" == "true" ]; then
572 echo "ℹ️ Dry run — no changes applied" >> "$GITHUB_STEP_SUMMARY"
573 else
574 echo "ℹ️ No fixable alerts found" >> "$GITHUB_STEP_SUMMARY"
575 fi
576