|
| 1 | +# Copyright (c) Microsoft Corporation. |
| 2 | +# Licensed under the MIT License. |
| 3 | +# |
| 4 | +# Dependabot Auto-fix |
| 5 | +# ------------------------------------------------------------------------ |
| 6 | +# Runs the `fix-dependabot-alerts` skill via GitHub Copilot CLI on a weekly |
| 7 | +# schedule (and on-demand). Copilot enumerates open Dependabot alerts, |
| 8 | +# applies fixes following `.claude/skills/fix-dependabot-alerts/SKILL.md`, |
| 9 | +# runs the build + tests, and opens a single consolidated PR. |
| 10 | +# |
| 11 | +# Required secrets |
| 12 | +# ------------------------------------------------------------------------ |
| 13 | +# POWER_PAGES_PUBLIC_GITHUB_APP_PRIVATE_KEY |
| 14 | +# Private key of the existing GitHub App (app-id 2740120) that is |
| 15 | +# already used by loc-update.yml / translations-export.yml. The App |
| 16 | +# installation on this repo must have: |
| 17 | +# - Contents: Read/Write |
| 18 | +# - Pull requests: Read/Write |
| 19 | +# Commits and the PR are authored as github-actions[bot] via this App. |
| 20 | +# |
| 21 | +# COPILOT_CLI_PAT |
| 22 | +# Fine-grained PAT from a user with a Copilot license. Needed because |
| 23 | +# GitHub App tokens cannot authenticate Copilot CLI (Copilot licenses |
| 24 | +# are per-user). Required permissions: |
| 25 | +# - Account: Copilot Requests: Read |
| 26 | +# - Repository: Dependabot alerts: Read (so `gh api .../dependabot/alerts` works) |
| 27 | +# The org-level "Copilot CLI" policy must be enabled for this user. |
| 28 | +# ------------------------------------------------------------------------ |
| 29 | + |
| 30 | +name: Dependabot Auto-fix |
| 31 | + |
| 32 | +on: |
| 33 | + schedule: |
| 34 | + # Weekly, Mondays 06:00 UTC |
| 35 | + - cron: '0 6 * * 1' |
| 36 | + workflow_dispatch: |
| 37 | + |
| 38 | +permissions: |
| 39 | + contents: read |
| 40 | + |
| 41 | +concurrency: |
| 42 | + group: dependabot-autofix |
| 43 | + cancel-in-progress: false |
| 44 | + |
| 45 | +jobs: |
| 46 | + autofix: |
| 47 | + name: Copilot CLI Dependabot Auto-fix |
| 48 | + runs-on: ubuntu-latest |
| 49 | + timeout-minutes: 45 |
| 50 | + permissions: |
| 51 | + contents: write |
| 52 | + pull-requests: write |
| 53 | + |
| 54 | + steps: |
| 55 | + - name: Generate GitHub App Token |
| 56 | + id: app-token |
| 57 | + uses: actions/create-github-app-token@v1 |
| 58 | + with: |
| 59 | + app-id: 2740120 |
| 60 | + private-key: ${{ secrets.POWER_PAGES_PUBLIC_GITHUB_APP_PRIVATE_KEY }} |
| 61 | + |
| 62 | + - name: Checkout Repository |
| 63 | + uses: actions/checkout@v4 |
| 64 | + with: |
| 65 | + fetch-depth: 0 |
| 66 | + token: ${{ steps.app-token.outputs.token }} |
| 67 | + |
| 68 | + - name: Setup Node.js |
| 69 | + uses: actions/setup-node@v4 |
| 70 | + with: |
| 71 | + node-version: '20' |
| 72 | + cache: 'npm' |
| 73 | + |
| 74 | + - name: Install dependencies |
| 75 | + run: npm ci |
| 76 | + |
| 77 | + - name: Install GitHub Copilot CLI |
| 78 | + run: npm install -g @github/copilot |
| 79 | + |
| 80 | + - name: Configure git identity |
| 81 | + run: | |
| 82 | + git config --local user.name "github-actions[bot]" |
| 83 | + git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" |
| 84 | +
|
| 85 | + - name: Create working branch |
| 86 | + id: branch |
| 87 | + run: | |
| 88 | + BRANCH="copilot/dependabot-autofix-${{ github.run_id }}" |
| 89 | + git checkout -b "$BRANCH" |
| 90 | + echo "name=$BRANCH" >> "$GITHUB_OUTPUT" |
| 91 | +
|
| 92 | + - name: Run Copilot CLI (fix-dependabot-alerts) |
| 93 | + id: copilot |
| 94 | + timeout-minutes: 30 |
| 95 | + env: |
| 96 | + # Copilot CLI auth + Dependabot alerts read. Redacted from logs. |
| 97 | + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_CLI_PAT }} |
| 98 | + # App token used by `gh pr create` and `git push` so the PR |
| 99 | + # and commits are authored by github-actions[bot]. |
| 100 | + GH_TOKEN: ${{ steps.app-token.outputs.token }} |
| 101 | + REPO: ${{ github.repository }} |
| 102 | + BRANCH: ${{ steps.branch.outputs.name }} |
| 103 | + run: | |
| 104 | + set -euo pipefail |
| 105 | +
|
| 106 | + # Inline the skill so behavior is identical even if Copilot CLI |
| 107 | + # on the runner does not auto-load .claude/skills/. |
| 108 | + SKILL_BODY="$(cat .claude/skills/fix-dependabot-alerts/SKILL.md)" |
| 109 | +
|
| 110 | + PROMPT=$(cat <<PROMPT_EOF |
| 111 | + You are running inside a GitHub Actions job. Follow the skill |
| 112 | + below to fix all currently open Dependabot alerts for the |
| 113 | + repository \`${REPO}\` in a SINGLE consolidated pull request. |
| 114 | +
|
| 115 | + Environment already set up for you: |
| 116 | + - Repository is checked out at the current working directory. |
| 117 | + - A working branch named \`${BRANCH}\` is already created and checked out. |
| 118 | + - \`npm ci\` has already installed dependencies. |
| 119 | + - \`git\` is configured as github-actions[bot]. |
| 120 | + - \`COPILOT_GITHUB_TOKEN\` is available for \`gh api\` calls that read |
| 121 | + Dependabot alerts (e.g. \`gh api repos/${REPO}/dependabot/alerts\`). |
| 122 | + - \`GH_TOKEN\` is a GitHub App token with Contents and Pull requests |
| 123 | + write access. Use it (it is already in env) for \`git push\` and |
| 124 | + \`gh pr create\`. |
| 125 | +
|
| 126 | + Mandatory rules: |
| 127 | +
|
| 128 | + 1. ALERT ENUMERATION — source of truth. |
| 129 | + Run exactly: |
| 130 | + GH_TOKEN=\$COPILOT_GITHUB_TOKEN gh api \\\\ |
| 131 | + "repos/${REPO}/dependabot/alerts?state=open&per_page=100" \\\\ |
| 132 | + --paginate \\\\ |
| 133 | + --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}' |
| 134 | + Save the raw output to \`alerts.json\`. The PR description |
| 135 | + and the count of "alerts fixed" MUST be built one-to-one |
| 136 | + from this list. Do NOT count transitive-path occurrences |
| 137 | + of the same alert number as separate alerts. Do NOT |
| 138 | + inflate counts based on how many \`node_modules/**\` |
| 139 | + entries appear in \`package-lock.json\`. |
| 140 | +
|
| 141 | + 2. ZERO ALERTS — exit cleanly. |
| 142 | + If step 1 returns an empty list, do not commit, do not |
| 143 | + push, do not open a PR. Print "NO_OPEN_ALERTS" and exit. |
| 144 | +
|
| 145 | + 3. MINIMUM VIABLE FIX — prefer the smallest blast radius. |
| 146 | + For each alert, pick the least-invasive strategy in this |
| 147 | + strict preference order and record your choice in the PR |
| 148 | + body: |
| 149 | + a. Transitive dependency → add/extend an entry in the |
| 150 | + \`overrides\` block of \`package.json\` pinning the |
| 151 | + vulnerable package to the first patched version. |
| 152 | + This is the preferred fix. |
| 153 | + b. Direct dependency → bump that specific dependency in |
| 154 | + \`package.json\` to the first patched version. |
| 155 | + c. Only if (a) and (b) are infeasible (e.g. override |
| 156 | + breaks peer-dep resolution, or a parent package |
| 157 | + pins the vulnerable version), escalate to bumping a |
| 158 | + parent package. In that case, justify the escalation |
| 159 | + in the PR body and keep the parent bump as narrow |
| 160 | + as possible (patch > minor > major). |
| 161 | + Never run \`npm audit fix\` or \`npm audit fix --force\` as |
| 162 | + a shortcut — they produce sprawling diffs with unrelated |
| 163 | + upgrades. Use \`npm install\` / \`npm update\` targeted at |
| 164 | + the specific package(s) you are fixing. |
| 165 | +
|
| 166 | + 4. DIFF DISCIPLINE. |
| 167 | + After the lockfile is regenerated, run: |
| 168 | + git diff --stat origin/main |
| 169 | + If any of the following are true, STOP, revert with |
| 170 | + \`git checkout -- package.json package-lock.json\`, and |
| 171 | + try a narrower strategy from rule 3: |
| 172 | + - package.json changed a dependency that is NOT on the |
| 173 | + alert list (except for adding entries to \`overrides\`). |
| 174 | + - Any dependency bumped a MAJOR version that is not |
| 175 | + strictly required to reach the first patched version. |
| 176 | +
|
| 177 | + 5. VERIFY. |
| 178 | + Run \`npm run build\` AND \`npm test\`. If either fails, |
| 179 | + do NOT push and do NOT open a PR. Print |
| 180 | + "BUILD_OR_TEST_FAILED" and exit non-zero. |
| 181 | +
|
| 182 | + 6. OPEN THE PR (verification passed). |
| 183 | + - \`git push -u origin ${BRANCH}\` (uses \$GH_TOKEN). |
| 184 | + - Build the PR title and body strictly from \`alerts.json\`: |
| 185 | + * Title: \`chore(deps): fix N open Dependabot alert(s)\` |
| 186 | + where N = number of distinct entries in \`alerts.json\`. |
| 187 | + * Body format: |
| 188 | + ## Summary |
| 189 | + Fixes N open Dependabot alert(s) as enumerated by |
| 190 | + \`gh api repos/${REPO}/dependabot/alerts?state=open\`. |
| 191 | + ## Alerts addressed |
| 192 | + For each entry in \`alerts.json\`, one bullet: |
| 193 | + - \`<pkg>\` (#<number>, <severity>, <ghsa>) — |
| 194 | + vulnerable \`<vulnerable>\` → patched \`<patched>\`. |
| 195 | + Strategy: override | direct-bump | parent-bump. |
| 196 | + Rationale if parent-bump: <one line>. |
| 197 | + ## Collateral changes |
| 198 | + List any other dep bumps that \`npm install\` cascaded |
| 199 | + (e.g. peer updates) and explain why they were |
| 200 | + necessary. If there are none, say "None". |
| 201 | + ## Verification |
| 202 | + - \`npm run build\`: PASS |
| 203 | + - \`npm test\`: PASS |
| 204 | + - Open the PR with \`gh pr create --base main --head ${BRANCH}\` |
| 205 | + using that title and body. |
| 206 | + - Print the PR URL. |
| 207 | +
|
| 208 | + 7. COMMIT / AUTHORSHIP HYGIENE. |
| 209 | + Never manually edit integrity hashes in package-lock.json |
| 210 | + (see skill). Use \`npm install\`, \`npm update\`, or a clean |
| 211 | + reinstall to let npm compute them. Do not add any |
| 212 | + \`Co-authored-by:\` trailer. |
| 213 | +
|
| 214 | + 8. PERMISSIONS. |
| 215 | + Never use \`--allow-all\` semantics; stick to the tools you |
| 216 | + already have permission to use. |
| 217 | +
|
| 218 | + ----- SKILL START ----- |
| 219 | + ${SKILL_BODY} |
| 220 | + ----- SKILL END ----- |
| 221 | + PROMPT_EOF |
| 222 | + ) |
| 223 | +
|
| 224 | + copilot \ |
| 225 | + -p "$PROMPT" \ |
| 226 | + --model claude-sonnet-4.6 \ |
| 227 | + -s \ |
| 228 | + --no-ask-user \ |
| 229 | + --allow-tool 'shell(npm:*), shell(npx:*), shell(gh:*), shell(git:*), shell(node:*), write, read' \ |
| 230 | + --secret-env-vars 'COPILOT_CLI_PAT' \ |
| 231 | + --share ./copilot-session.md \ |
| 232 | + | tee copilot-output.txt |
| 233 | +
|
| 234 | + - name: Summarize run |
| 235 | + if: always() |
| 236 | + run: | |
| 237 | + { |
| 238 | + echo "## Dependabot Auto-fix" |
| 239 | + echo "" |
| 240 | + echo "- Branch: \`${{ steps.branch.outputs.name }}\`" |
| 241 | + if [ -f copilot-output.txt ]; then |
| 242 | + if grep -q "NO_OPEN_ALERTS" copilot-output.txt; then |
| 243 | + echo "- Result: no open Dependabot alerts; no PR opened." |
| 244 | + elif grep -q "BUILD_OR_TEST_FAILED" copilot-output.txt; then |
| 245 | + echo "- Result: build or tests failed after fixes; no PR opened." |
| 246 | + else |
| 247 | + PR_URL=$(grep -Eo 'https://github.com/[^ ]+/pull/[0-9]+' copilot-output.txt | head -n1 || true) |
| 248 | + if [ -n "$PR_URL" ]; then |
| 249 | + echo "- PR: $PR_URL" |
| 250 | + else |
| 251 | + echo "- Result: see Copilot session transcript artifact." |
| 252 | + fi |
| 253 | + fi |
| 254 | + else |
| 255 | + echo "- Result: Copilot step did not produce output." |
| 256 | + fi |
| 257 | + } >> "$GITHUB_STEP_SUMMARY" |
| 258 | +
|
| 259 | + - name: Upload Copilot session transcript |
| 260 | + if: always() |
| 261 | + uses: actions/upload-artifact@v4 |
| 262 | + with: |
| 263 | + name: copilot-session-${{ github.run_id }} |
| 264 | + path: | |
| 265 | + copilot-session.md |
| 266 | + copilot-output.txt |
| 267 | + retention-days: 14 |
| 268 | + if-no-files-found: ignore |
0 commit comments