Skip to content

Dependabot Auto-fix

Dependabot Auto-fix #7

# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
#
# Dependabot Auto-fix
# ------------------------------------------------------------------------
# Runs the `fix-dependabot-alerts` skill via GitHub Copilot CLI on a weekly
# schedule (and on-demand). Copilot enumerates open Dependabot alerts,
# applies fixes following `.claude/skills/fix-dependabot-alerts/SKILL.md`,
# runs the build + tests, and opens a single consolidated PR.
#
# Required secrets
# ------------------------------------------------------------------------
# POWER_PAGES_PUBLIC_GITHUB_APP_PRIVATE_KEY
# Private key of the existing GitHub App (app-id 2740120) that is
# already used by loc-update.yml / translations-export.yml. The App
# installation on this repo must have:
# - Contents: Read/Write
# - Pull requests: Read/Write
# Commits and the PR are authored as github-actions[bot] via this App.
#
# COPILOT_CLI_PAT
# Fine-grained PAT from a user with a Copilot license. Needed because
# GitHub App tokens cannot authenticate Copilot CLI (Copilot licenses
# are per-user). Required permissions:
# - Account: Copilot Requests: Read
# - Repository: Dependabot alerts: Read (so `gh api .../dependabot/alerts` works)
# The org-level "Copilot CLI" policy must be enabled for this user.
# ------------------------------------------------------------------------
name: Dependabot Auto-fix
on:
schedule:
# Weekly, Mondays 06:00 UTC
- cron: '0 6 * * 1'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: dependabot-autofix
cancel-in-progress: false
jobs:
autofix:
name: Copilot CLI Dependabot Auto-fix
runs-on: ubuntu-latest
timeout-minutes: 45
permissions:
contents: write
pull-requests: write
steps:
- name: Generate GitHub App Token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: 2740120
private-key: ${{ secrets.POWER_PAGES_PUBLIC_GITHUB_APP_PRIVATE_KEY }}
- name: Checkout Repository
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Install GitHub Copilot CLI
run: npm install -g @github/copilot
- name: Configure git identity
run: |
git config --local user.name "github-actions[bot]"
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Create working branch
id: branch
run: |
BRANCH="copilot/dependabot-autofix-${{ github.run_id }}"
git checkout -b "$BRANCH"
echo "name=$BRANCH" >> "$GITHUB_OUTPUT"
- name: Run Copilot CLI (fix-dependabot-alerts)
id: copilot
timeout-minutes: 30
env:
# Copilot CLI auth + Dependabot alerts read before invoking Copilot.
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_CLI_PAT }}
# App token used by `gh pr create` and `git push` so the PR
# and commits are authored by github-actions[bot].
GH_TOKEN: ${{ steps.app-token.outputs.token }}
REPO: ${{ github.repository }}
BRANCH: ${{ steps.branch.outputs.name }}
run: |
set -euo pipefail
if [ -z "${COPILOT_GITHUB_TOKEN:-}" ]; then
echo "COPILOT_CLI_PAT secret is not configured or is unavailable to this workflow."
exit 1
fi
GH_TOKEN="$COPILOT_GITHUB_TOKEN" gh api \
"repos/${REPO}/dependabot/alerts?state=open&per_page=100" \
--paginate \
--jq '.[] | {number, ghsa: .security_advisory.ghsa_id, pkg: .dependency.package.name, ecosystem: .dependency.package.ecosystem, severity: .security_vulnerability.severity, scope: .dependency.scope, manifest: .dependency.manifest_path, summary: .security_advisory.summary, vulnerable: .security_vulnerability.vulnerable_version_range, patched: .security_vulnerability.first_patched_version.identifier}' \
> alerts.json
if [ ! -s alerts.json ]; then
echo "NO_OPEN_ALERTS" | tee copilot-output.txt
exit 0
fi
# Inline the skill so behavior is identical even if Copilot CLI
# on the runner does not auto-load .claude/skills/.
SKILL_BODY="$(cat .claude/skills/fix-dependabot-alerts/SKILL.md)"
PROMPT=$(cat <<PROMPT_EOF
You are running inside a GitHub Actions job. Follow the skill
below to fix all currently open Dependabot alerts for the
repository \`${REPO}\` in a SINGLE consolidated pull request.
Environment already set up for you:
- Repository is checked out at the current working directory.
- A working branch named \`${BRANCH}\` is already created and checked out.
- \`npm ci\` has already installed dependencies.
- \`git\` is configured as github-actions[bot].
- \`alerts.json\` was generated from \`gh api repos/${REPO}/dependabot/alerts?state=open\`
before Copilot was invoked. Use it as the source of truth.
- \`GH_TOKEN\` is a GitHub App token with Contents and Pull requests
write access. Use it (it is already in env) for \`git push\` and
\`gh pr create\`.
Mandatory rules:
1. ALERT ENUMERATION — source of truth.
Read \`alerts.json\`, which was generated before Copilot was invoked.
The PR description and the count of "alerts fixed" MUST
be built one-to-one from \`alerts.json\`. Do NOT count
transitive-path occurrences of the same alert number as
separate alerts. Do NOT inflate counts based on how many
\`node_modules/**\` entries appear in \`package-lock.json\`.
2. ZERO ALERTS — exit cleanly.
If step 1 returns an empty list, do not commit, do not
push, do not open a PR. Print "NO_OPEN_ALERTS" and exit.
3. MINIMUM VIABLE FIX — prefer the smallest blast radius.
For each alert, pick the least-invasive strategy in this
strict preference order and record your choice in the PR
body:
a. Transitive dependency → add/extend an entry in the
\`overrides\` block of \`package.json\` pinning the
vulnerable package to the first patched version.
This is the preferred fix.
b. Direct dependency → bump that specific dependency in
\`package.json\` to the first patched version.
c. Only if (a) and (b) are infeasible (e.g. override
breaks peer-dep resolution, or a parent package
pins the vulnerable version), escalate to bumping a
parent package. In that case, justify the escalation
in the PR body and keep the parent bump as narrow
as possible (patch > minor > major).
Never run \`npm audit fix\` or \`npm audit fix --force\` as
a shortcut — they produce sprawling diffs with unrelated
upgrades. Use \`npm install\` / \`npm update\` targeted at
the specific package(s) you are fixing.
4. DIFF DISCIPLINE.
After the lockfile is regenerated, run:
git diff --stat origin/main
If any of the following are true, STOP, revert with
\`git checkout -- package.json package-lock.json\`, and
try a narrower strategy from rule 3:
- package.json changed a dependency that is NOT on the
alert list (except for adding entries to \`overrides\`).
- Any dependency bumped a MAJOR version that is not
strictly required to reach the first patched version.
5. VERIFY.
Run \`npm run build\` AND \`npm test\`. If either fails,
do NOT push and do NOT open a PR. Print
"BUILD_OR_TEST_FAILED" and exit non-zero.
6. OPEN THE PR (verification passed).
- \`git push -u origin ${BRANCH}\` (uses \$GH_TOKEN).
- Build the PR title and body strictly from \`alerts.json\`:
* Title: \`chore(deps): fix N open Dependabot alert(s)\`
where N = number of distinct entries in \`alerts.json\`.
* Body format:
## Summary
Fixes N open Dependabot alert(s) as enumerated by
\`gh api repos/${REPO}/dependabot/alerts?state=open\`.
## Alerts addressed
For each entry in \`alerts.json\`, one bullet:
- \`<pkg>\` (#<number>, <severity>, <ghsa>) —
vulnerable \`<vulnerable>\` → patched \`<patched>\`.
Strategy: override | direct-bump | parent-bump.
Rationale if parent-bump: <one line>.
## Collateral changes
List any other dep bumps that \`npm install\` cascaded
(e.g. peer updates) and explain why they were
necessary. If there are none, say "None".
## Verification
- \`npm run build\`: PASS
- \`npm test\`: PASS
- Open the PR with \`gh pr create --base main --head ${BRANCH}\`
using that title and body.
- Print the PR URL.
7. COMMIT / AUTHORSHIP HYGIENE.
Never manually edit integrity hashes in package-lock.json
(see skill). Use \`npm install\`, \`npm update\`, or a clean
reinstall to let npm compute them. Do not add any
\`Co-authored-by:\` trailer.
8. PERMISSIONS.
Never use \`--allow-all\` semantics; stick to the tools you
already have permission to use.
----- SKILL START -----
${SKILL_BODY}
----- SKILL END -----
PROMPT_EOF
)
copilot \
-p "$PROMPT" \
--model claude-sonnet-4.6 \
-s \
--no-ask-user \
--allow-tool 'shell(npm:*), shell(npx:*), shell(gh:*), shell(git:*), shell(node:*), write, read' \
--secret-env-vars 'GH_TOKEN' \
--share ./copilot-session.md \
| tee copilot-output.txt
- name: Summarize run
if: always()
run: |
{
echo "## Dependabot Auto-fix"
echo ""
echo "- Branch: \`${{ steps.branch.outputs.name }}\`"
if [ -f copilot-output.txt ]; then
if grep -q "NO_OPEN_ALERTS" copilot-output.txt; then
echo "- Result: no open Dependabot alerts; no PR opened."
elif grep -q "BUILD_OR_TEST_FAILED" copilot-output.txt; then
echo "- Result: build or tests failed after fixes; no PR opened."
else
PR_URL=$(grep -Eo 'https://github.com/[^ ]+/pull/[0-9]+' copilot-output.txt | head -n1 || true)
if [ -n "$PR_URL" ]; then
echo "- PR: $PR_URL"
else
echo "- Result: see Copilot session transcript artifact."
fi
fi
else
echo "- Result: Copilot step did not produce output."
fi
} >> "$GITHUB_STEP_SUMMARY"
- name: Upload Copilot session transcript
if: always()
uses: actions/upload-artifact@v4
with:
name: copilot-session-${{ github.run_id }}
path: |
copilot-session.md
copilot-output.txt
retention-days: 14
if-no-files-found: ignore