Dependabot Auto-fix #7
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 |