Fix Dependabot PRs #948
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
| name: Fix Dependabot PRs | |
| # Fires after any of the listed CI workflows finishes on a PR. Gates on: | |
| # 1. The triggering workflow ran on a pull_request event. | |
| # 2. The PR author is dependabot[bot]. | |
| # 3. Every CI workflow expected to run for this PR (based on on.pull_request.paths) | |
| # has completed for this HEAD SHA — so Claude sees the full failure set at once. | |
| # Exits early (0) on any of these not being satisfied, so a new workflow_run event | |
| # from a later-completing upstream workflow can re-evaluate. | |
| # | |
| # Concurrency is keyed on HEAD SHA: near-simultaneous completions still only produce | |
| # one surviving Claude run per SHA, but subsequent pushes (Claude's own fix, or a | |
| # fresh dependabot commit) get their own lane. | |
| on: | |
| workflow_run: | |
| workflows: | |
| - "Run Tests" | |
| - "E2E Tests" | |
| - "Web CLI E2E Tests (Parallel)" | |
| - "Security Audit" | |
| types: [completed] | |
| permissions: | |
| actions: read | |
| checks: write | |
| contents: write | |
| pull-requests: write | |
| concurrency: | |
| group: dependabot-claude-fix-${{ github.event.workflow_run.head_sha }} | |
| cancel-in-progress: true | |
| jobs: | |
| gate-and-fix: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| # Only consider PRs from the same repo (dependabot qualifies). | |
| # Forks would have an empty pull_requests array. | |
| if: > | |
| github.event.workflow_run.event == 'pull_request' && | |
| github.event.workflow_run.pull_requests[0] != null | |
| steps: | |
| - name: Identify PR and check author | |
| id: pr | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPO: ${{ github.repository }} | |
| PR_NUM: ${{ github.event.workflow_run.pull_requests[0].number }} | |
| run: | | |
| set -euo pipefail | |
| PR_JSON=$(gh api "repos/$REPO/pulls/$PR_NUM") | |
| AUTHOR=$(jq -r '.user.login' <<< "$PR_JSON") | |
| HEAD_REF=$(jq -r '.head.ref' <<< "$PR_JSON") | |
| echo "PR #$PR_NUM author: $AUTHOR" | |
| echo "PR head ref: $HEAD_REF" | |
| if [[ "$AUTHOR" != "dependabot[bot]" ]]; then | |
| echo "Not a dependabot PR, exiting." | |
| echo "skip=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "skip=false" >> "$GITHUB_OUTPUT" | |
| echo "pr_number=$PR_NUM" >> "$GITHUB_OUTPUT" | |
| echo "head_ref=$HEAD_REF" >> "$GITHUB_OUTPUT" | |
| - name: Loop-prevention guard | |
| id: loop-guard | |
| if: steps.pr.outputs.skip != 'true' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPO: ${{ github.repository }} | |
| HEAD_REF: ${{ steps.pr.outputs.head_ref }} | |
| run: | | |
| set -euo pipefail | |
| # Cap how many times this workflow has already completed successfully on | |
| # this branch, to avoid runaway loops if Claude's fixes keep triggering | |
| # new CI failures. | |
| RUN_COUNT=$(gh api \ | |
| "repos/$REPO/actions/workflows/dependabot-claude-fix.yml/runs?branch=$HEAD_REF&status=success" \ | |
| --jq '.total_count') | |
| echo "Prior successful runs on $HEAD_REF: $RUN_COUNT" | |
| if [[ "$RUN_COUNT" -ge 20 ]]; then | |
| echo "Hit loop cap (20). Skipping to avoid runaway iteration." | |
| echo "skip=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "skip=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Compute expected workflows for this PR | |
| id: expected | |
| if: steps.pr.outputs.skip != 'true' && steps.loop-guard.outputs.skip != 'true' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPO: ${{ github.repository }} | |
| PR_NUM: ${{ steps.pr.outputs.pr_number }} | |
| run: | | |
| set -euo pipefail | |
| # Fetch the PR's changed files (handles pagination for large diffs). | |
| CHANGED=$(gh api "repos/$REPO/pulls/$PR_NUM/files" --paginate --jq '.[].filename') | |
| echo "Changed files in PR:" | |
| printf ' %s\n' $CHANGED | |
| # Minimal path-filter matcher covering the patterns this repo actually uses | |
| # in on.pull_request.paths: | |
| # - "PATH/**" → file must start with "PATH/" | |
| # - exact path → file must equal pattern | |
| # Keep this in sync if any monitored workflow adopts more exotic globs. | |
| matches_pattern() { | |
| local file="$1" pattern="$2" | |
| if [[ "$pattern" == *"/**" ]]; then | |
| [[ "$file" == "${pattern%/**}/"* ]] | |
| else | |
| [[ "$file" == "$pattern" ]] | |
| fi | |
| } | |
| any_changed_matches() { | |
| local patterns=("$@") file pat | |
| while IFS= read -r file; do | |
| [[ -z "$file" ]] && continue | |
| for pat in "${patterns[@]}"; do | |
| matches_pattern "$file" "$pat" && return 0 | |
| done | |
| done <<< "$CHANGED" | |
| return 1 | |
| } | |
| EXPECTED=() | |
| # Run Tests (test.yml) — paths-filtered. | |
| # Keep this list in sync with .github/workflows/test.yml on.pull_request.paths. | |
| RUN_TESTS_PATHS=( | |
| "src/**" | |
| "test/**" | |
| "package.json" | |
| "pnpm-lock.yaml" | |
| "tsconfig.json" | |
| "scripts/**" | |
| "examples/**" | |
| ".github/workflows/test.yml" | |
| ".claude/**" | |
| ) | |
| if any_changed_matches "${RUN_TESTS_PATHS[@]}"; then | |
| EXPECTED+=("Run Tests") | |
| fi | |
| # These three have no paths filter — always expected on PRs to main. | |
| EXPECTED+=("E2E Tests") | |
| EXPECTED+=("Web CLI E2E Tests (Parallel)") | |
| EXPECTED+=("Security Audit") | |
| echo "Expected workflows for this PR:" | |
| printf ' %s\n' "${EXPECTED[@]}" | |
| # Emit as newline-delimited output. | |
| { | |
| echo "list<<EOF_EXPECTED" | |
| printf '%s\n' "${EXPECTED[@]}" | |
| echo "EOF_EXPECTED" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Gate — are all expected workflows complete for this SHA? | |
| id: gate | |
| if: steps.pr.outputs.skip != 'true' && steps.loop-guard.outputs.skip != 'true' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPO: ${{ github.repository }} | |
| HEAD_SHA: ${{ github.event.workflow_run.head_sha }} | |
| EXPECTED: ${{ steps.expected.outputs.list }} | |
| run: | | |
| set -euo pipefail | |
| # Default output so downstream `if:` checks don't fail on unset. | |
| echo "proceed=false" >> "$GITHUB_OUTPUT" | |
| # All workflow runs for this SHA on the PR event. | |
| RUNS_JSON=$(gh api \ | |
| "repos/$REPO/actions/runs?head_sha=$HEAD_SHA&event=pull_request&per_page=100" \ | |
| --paginate \ | |
| --jq '[.workflow_runs[] | {name, status, conclusion, id, html_url}]') | |
| all_complete=true | |
| failed_runs='[]' | |
| while IFS= read -r wf; do | |
| [[ -z "$wf" ]] && continue | |
| # Most recent run matching this workflow name (runs are already ordered newest-first). | |
| run=$(jq -c --arg n "$wf" '[.[] | select(.name == $n)] | first' <<< "$RUNS_JSON") | |
| if [[ "$run" == "null" || -z "$run" ]]; then | |
| echo " [$wf] no run yet for SHA $HEAD_SHA — still queuing, waiting." | |
| all_complete=false | |
| continue | |
| fi | |
| status=$(jq -r '.status' <<< "$run") | |
| conclusion=$(jq -r '.conclusion' <<< "$run") | |
| echo " [$wf] status=$status conclusion=$conclusion" | |
| if [[ "$status" != "completed" ]]; then | |
| all_complete=false | |
| continue | |
| fi | |
| if [[ "$conclusion" == "failure" || "$conclusion" == "cancelled" ]]; then | |
| failed_runs=$(jq --argjson r "$run" '. += [$r]' <<< "$failed_runs") | |
| fi | |
| done <<< "$EXPECTED" | |
| if [[ "$all_complete" != "true" ]]; then | |
| echo "Not all expected workflows have completed yet. Next workflow_run event will re-gate." | |
| exit 0 | |
| fi | |
| failed_count=$(jq 'length' <<< "$failed_runs") | |
| echo "All expected workflows complete. Failed: $failed_count" | |
| if [[ "$failed_count" -eq 0 ]]; then | |
| echo "All passed — nothing to fix." | |
| exit 0 | |
| fi | |
| echo "proceed=true" >> "$GITHUB_OUTPUT" | |
| failed_names=$(jq -r '[.[].name] | sort | join(", ")' <<< "$failed_runs") | |
| echo "Failing: $failed_names" | |
| # Pull the failed logs for each run so Claude sees the actual error output. | |
| failure_logs="" | |
| while IFS= read -r run_id; do | |
| [[ -z "$run_id" ]] && continue | |
| run_name=$(jq -r --arg i "$run_id" '.[] | select((.id | tostring) == $i) | .name' <<< "$failed_runs") | |
| run_url=$(jq -r --arg i "$run_id" '.[] | select((.id | tostring) == $i) | .html_url' <<< "$failed_runs") | |
| echo "Fetching failed logs for $run_name (run $run_id)..." | |
| logs=$(gh run view "$run_id" --repo "$REPO" --log-failed 2>&1 | tail -n 500) || logs="Failed to fetch logs. View manually: $run_url" | |
| failure_logs="${failure_logs} | |
| === Failed workflow: ${run_name} (run ${run_id}) === | |
| URL: ${run_url} | |
| ${logs} | |
| " | |
| done < <(jq -r '.[].id' <<< "$failed_runs") | |
| # Random delimiters so the log body can't collide with the heredoc marker. | |
| delim_s="EOF_$(openssl rand -hex 16)" | |
| delim_l="EOF_$(openssl rand -hex 16)" | |
| { | |
| echo "failure_summary<<${delim_s}" | |
| echo "Failed checks: $failed_names" | |
| echo "${delim_s}" | |
| echo "failure_logs<<${delim_l}" | |
| echo "$failure_logs" | |
| echo "${delim_l}" | |
| } >> "$GITHUB_OUTPUT" | |
| # Create a check run on the PR's HEAD SHA so the fix job is visible in | |
| # the PR checks list. workflow_run-triggered runs are otherwise attached | |
| # to the default branch, not the PR, so nothing shows up in the PR UI | |
| # until we emit an explicit check run. | |
| - name: Open in-progress check on PR head SHA | |
| id: check-run | |
| if: steps.gate.outputs.proceed == 'true' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPO: ${{ github.repository }} | |
| HEAD_SHA: ${{ github.event.workflow_run.head_sha }} | |
| RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| FAILED_SUMMARY: ${{ steps.gate.outputs.failure_summary }} | |
| run: | | |
| set -euo pipefail | |
| CHECK_ID=$(gh api -X POST "repos/$REPO/check-runs" \ | |
| -f name="Fix Dependabot PRs" \ | |
| -f head_sha="$HEAD_SHA" \ | |
| -f status=in_progress \ | |
| -f details_url="$RUN_URL" \ | |
| -f "output[title]=Analysing CI failures and preparing fix" \ | |
| -f "output[summary]=${FAILED_SUMMARY:-Gathering failure logs...}" \ | |
| --jq '.id') | |
| echo "id=$CHECK_ID" >> "$GITHUB_OUTPUT" | |
| echo "Opened check run $CHECK_ID on $HEAD_SHA" | |
| - name: Generate App Token | |
| if: steps.gate.outputs.proceed == 'true' | |
| id: generate-token | |
| uses: actions/create-github-app-token@v3 | |
| with: | |
| app-id: ${{ secrets.CI_APP_ID }} | |
| private-key: ${{ secrets.CI_APP_PRIVATE_KEY }} | |
| - name: Checkout Dependabot branch | |
| if: steps.gate.outputs.proceed == 'true' | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ steps.pr.outputs.head_ref }} | |
| token: ${{ steps.generate-token.outputs.token }} | |
| - name: Set up pnpm | |
| if: steps.gate.outputs.proceed == 'true' | |
| uses: pnpm/action-setup@v6 | |
| with: | |
| version: 10 | |
| - name: Set up Node.js | |
| if: steps.gate.outputs.proceed == 'true' | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: "22.x" | |
| - name: Install dependencies | |
| if: steps.gate.outputs.proceed == 'true' | |
| run: pnpm install --frozen-lockfile --ignore-scripts | |
| - name: Fix failures with Claude | |
| id: claude | |
| if: steps.gate.outputs.proceed == 'true' | |
| uses: anthropics/claude-code-action@v1 | |
| with: | |
| anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} | |
| github_token: ${{ steps.generate-token.outputs.token }} | |
| allowed_bots: "dependabot[bot],ci-lockfile-regen[bot]" | |
| prompt: | | |
| You are fixing a Dependabot PR where CI checks are failing after a dependency | |
| bump. The lockfile has already been regenerated. Your job is to understand what | |
| changed, fix the code to work with the new dependency version, and verify the fix. | |
| Read .claude/CLAUDE.md for project context. | |
| ## CRITICAL RULES | |
| 1. **NEVER revert or downgrade the dependency bump.** The goal is to update our | |
| code to work with the new version, not to undo the update. Do not modify | |
| package.json to revert version changes. Do not run `pnpm add` to install an | |
| older version. The dependency update is intentional — fix our code instead. | |
| 2. **Fix forward.** Adapt imports, types, configs, API calls, and test code to | |
| match the new dependency version's API. | |
| 3. **If the fix is too complex** (would require architectural changes, design | |
| decisions between multiple valid approaches, or changes where you're not | |
| confident in the correctness), stop early and post a PR comment explaining | |
| what a human needs to do. Do NOT attempt risky or speculative fixes. | |
| ## Step 1: Understand the update | |
| Parse the PR title and body to identify: | |
| - Package name(s) being updated | |
| - Old version → new version | |
| - Whether this is a patch, minor, or major bump | |
| - Scope: runtime dependency, devDependency, build tool, or type definitions | |
| (check package.json — is it in `dependencies`, `devDependencies`, or `peerDependencies`?) | |
| Dependabot title patterns: | |
| - `chore(deps): bump PACKAGE from X to Y in /PATH` | |
| - `chore(deps-dev): bump PACKAGE from X to Y in /PATH` | |
| - `chore(deps): bump the GROUP group across N directories with M updates` | |
| For group bumps, also read the PR body for individual package details. | |
| ## Step 2: Research what changed | |
| For each updated package, find out what changed between versions. Check in order: | |
| 1. GitHub Releases page for the package repo | |
| 2. CHANGELOG.md in the package repo | |
| 3. npm package page (npmjs.com/package/PACKAGE) | |
| 4. Web search for "PACKAGE changelog X to Y" or "PACKAGE migration guide vX to vY" | |
| For each notable change found (deprecation, behavior change, renamed/removed API, | |
| changed default, new required config, peer dependency change): | |
| 1. Grep this repository to check if we use the affected API/feature | |
| 2. Note specific files and line numbers where we're affected | |
| 3. Determine the migration path (renamed method, new import path, config change, etc.) | |
| This cross-referencing is critical. Do NOT just summarize the changelog — verify | |
| each notable change against our actual codebase. | |
| ## Step 2b: Check migration concerns | |
| Proactively check each of these where applicable: | |
| **Peer dependencies**: Does the new version require peer dep updates we haven't | |
| made? Check for version conflicts in `pnpm install` output or `pnpm why`. | |
| **Type changes**: Do updated types break existing usage? Removed/renamed exports, | |
| changed function signatures, narrowed types. This is especially relevant for | |
| `@types/*` packages and TypeScript-first libraries. | |
| **Config files**: Does the package have a config file in our repo (e.g., tsconfig, | |
| eslint config, vitest config, oclif config in package.json)? Have config options | |
| changed between versions? | |
| **Module format**: Has the package changed its ESM/CJS module format? This repo | |
| uses ESM (`"type": "module"` in package.json). | |
| **React/bundler compatibility**: For React ecosystem packages, check for duplicate | |
| React instances (a common cause of "Cannot read properties of null (reading | |
| 'useState')"). Use `pnpm why react` to check for multiple React versions. Fix | |
| with `overrides` in package.json if needed. | |
| **Monorepo impact**: This is a pnpm workspace monorepo with packages at | |
| `packages/react-web-cli` and `examples/web-cli`. Check if the updated dependency | |
| is used in multiple workspace packages and whether they all need updates. | |
| ## Step 3: Analyse the CI failures | |
| ${{ steps.gate.outputs.failure_summary }} | |
| ${{ steps.gate.outputs.failure_logs }} | |
| Cross-reference the failure logs with what you learned in Step 2. For each failure: | |
| - Identify the root cause (type error, missing export, changed behavior, etc.) | |
| - Map it to a specific change in the new dependency version | |
| - Determine the fix | |
| ## Step 4: Assess complexity and decide | |
| Before making changes, assess the total scope: | |
| **Fix it yourself** if: | |
| - Type/import updates (renamed exports, changed signatures) | |
| - Config file adjustments (new required options, renamed keys) | |
| - API migrations with clear 1:1 mappings from changelog | |
| - Test updates to match new behavior | |
| - Peer dependency adjustments in package.json (adding resolutions/overrides) | |
| - React/bundler duplicate-instance fixes (adding overrides to deduplicate) | |
| **Stop and comment** if: | |
| - The migration requires architectural changes or design decisions | |
| - Multiple valid approaches exist and a human should choose | |
| - The changelog is unclear about the migration path | |
| - You're not confident the fix is correct | |
| If stopping: post a detailed PR comment using `gh pr comment` explaining: | |
| - What broke and why (with specific file:line references) | |
| - What the new version changed (with links to changelog/migration guide) | |
| - What a human needs to do to fix it | |
| - Your recommended approach if you have one | |
| Then exit without making code changes. | |
| ## Step 5: Fix the code | |
| If proceeding with the fix: | |
| 1. Make the minimum changes needed — do not refactor unrelated code | |
| 2. Fix ALL failures, not just the first one. The CI logs may show multiple | |
| distinct issues | |
| 3. Verify your changes: | |
| ```bash | |
| pnpm run build | |
| pnpm exec eslint . | |
| pnpm test:unit | |
| pnpm --filter @ably/react-web-cli test | |
| ``` | |
| 4. If verification reveals new issues, fix those too. Iterate until clean. | |
| 5. Commit your changes with a descriptive message explaining what was migrated | |
| and why (reference the dependency version change) | |
| 6. Push to the current branch | |
| ## Step 6: Post assessment comment | |
| After fixing (or deciding to stop), post a comment on the PR using | |
| `gh pr comment ${{ steps.pr.outputs.pr_number }} --repo ${{ github.repository }}` | |
| with this structure: | |
| ``` | |
| ## Dependabot Fix Assessment | |
| **Package**: `name` `old` → `new` (patch/minor/major) | |
| **Scope**: runtime / devDependency / build tool / type definitions | |
| **Workspace**: root / packages/react-web-cli / examples/web-cli | |
| ### What changed upstream | |
| - [Key changes between versions relevant to this repo] | |
| - [Link to changelog/release notes] | |
| ### Migration concerns checked | |
| - Peer dependencies: OK / [issue found] | |
| - Type changes: OK / [issue found] | |
| - Config files: OK / [issue found] | |
| - Module format: OK / [issue found] | |
| - React compatibility: OK / [issue found] | |
| - Monorepo impact: OK / [issue found] | |
| ### What broke | |
| - [Failed check]: [root cause] — [file:line if applicable] | |
| ### What was fixed | |
| - [Description of each change made, or "No code changes — see below"] | |
| ### Verification | |
| - Build: ✅/❌ | |
| - Lint: ✅/❌ | |
| - Unit tests: ✅/❌ | |
| - Web CLI tests: ✅/❌ | |
| ### Notes for reviewer | |
| - [Anything the reviewer should pay attention to, or "None"] | |
| ``` | |
| claude_args: | | |
| --max-turns 50 | |
| --model claude-sonnet-4-6 | |
| --allowedTools "Bash,Read,Write,Edit,Glob,Grep,WebSearch,WebFetch" | |
| # Close out the check run we opened above. Runs even if a prior step | |
| # failed or the job was cancelled, so the PR's checks list never shows | |
| # a perpetually-in-progress row. | |
| - name: Close check run | |
| if: always() && steps.check-run.outputs.id != '' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPO: ${{ github.repository }} | |
| CHECK_ID: ${{ steps.check-run.outputs.id }} | |
| CLAUDE_OUTCOME: ${{ steps.claude.outcome }} | |
| RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| run: | | |
| set -euo pipefail | |
| case "$CLAUDE_OUTCOME" in | |
| success) | |
| CONCLUSION=success | |
| TITLE="Claude finished" | |
| SUMMARY="See the PR comment for the fix assessment, or the [run logs]($RUN_URL) for full detail." | |
| ;; | |
| failure) | |
| CONCLUSION=failure | |
| TITLE="Fix step errored" | |
| SUMMARY="The Claude step failed. Check the [run logs]($RUN_URL)." | |
| ;; | |
| cancelled) | |
| CONCLUSION=cancelled | |
| TITLE="Cancelled" | |
| SUMMARY="The fix run was cancelled, usually because a newer workflow_run event superseded it." | |
| ;; | |
| *) | |
| # Any other state (e.g. skipped because the Claude step's own | |
| # `if:` evaluated to false) shouldn't really happen once the | |
| # gate proceeded, but emit a neutral conclusion to keep the | |
| # check row meaningful. | |
| CONCLUSION=neutral | |
| TITLE="No fix applied" | |
| SUMMARY="Gate opened but the Claude step did not run. See [run logs]($RUN_URL)." | |
| ;; | |
| esac | |
| gh api -X PATCH "repos/$REPO/check-runs/$CHECK_ID" \ | |
| -f status=completed \ | |
| -f conclusion="$CONCLUSION" \ | |
| -f "output[title]=$TITLE" \ | |
| -f "output[summary]=$SUMMARY" \ | |
| > /dev/null | |
| echo "Closed check $CHECK_ID with conclusion=$CONCLUSION" |