From ff7aff5af343b51b000c1e6dced3ed445c7ca4f9 Mon Sep 17 00:00:00 2001 From: umair Date: Fri, 17 Apr 2026 09:39:13 +0100 Subject: [PATCH 1/2] ci: skip Claude review on dependabot PRs and harden lockfile regen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two small hardening changes to the CI workflows that touch dependabot: - claude-review.yml: exclude PRs opened by dependabot[bot] from the auto-review job. Dependabot PRs are routine lockfile/version bumps — Claude review on them is noise, delays merge, and consumes API budget with no meaningful signal. The explicit-mention paths (issue_comment, pull_request_review_comment, etc.) are unchanged, so a maintainer can still request a review on a dependabot PR by @-mentioning Claude. - dependabot-lockfile.yml: pass --ignore-scripts to pnpm install in the lockfile-regeneration step. This step installs dependencies from a dependabot-authored PR, so any freshly-bumped package's postinstall hook would otherwise run with the workflow's token (including permissions to push to the PR branch). --ignore-scripts eliminates that arbitrary-code-execution surface during the regen. Build scripts on our own code aren't needed here — the step only resolves/installs to produce a consistent lockfile. --- .github/workflows/claude-review.yml | 3 ++- .github/workflows/dependabot-lockfile.yml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/claude-review.yml b/.github/workflows/claude-review.yml index a1a2fc83b..5d898e01e 100644 --- a/.github/workflows/claude-review.yml +++ b/.github/workflows/claude-review.yml @@ -20,7 +20,8 @@ jobs: timeout-minutes: 15 if: > (github.event_name == 'pull_request' && - github.event.pull_request.draft == false) || + github.event.pull_request.draft == false && + github.event.pull_request.user.login != 'dependabot[bot]') || (github.event_name == 'issue_comment' && github.event.issue.pull_request && contains(github.event.comment.body, '@claude') && diff --git a/.github/workflows/dependabot-lockfile.yml b/.github/workflows/dependabot-lockfile.yml index c18b522ce..9299747fc 100644 --- a/.github/workflows/dependabot-lockfile.yml +++ b/.github/workflows/dependabot-lockfile.yml @@ -288,7 +288,7 @@ jobs: - name: Install dependencies if: steps.wait-for-checks.outputs.failed_count > 0 - run: pnpm install --frozen-lockfile + run: pnpm install --frozen-lockfile --ignore-scripts - name: Fix failures with Claude if: steps.wait-for-checks.outputs.failed_count > 0 From 7d94a81298944868bcb198e0d0c4c4a7c2762671 Mon Sep 17 00:00:00 2001 From: umair Date: Fri, 17 Apr 2026 09:54:16 +0100 Subject: [PATCH 2/2] ci: split dependabot Claude fix into workflow_run-gated job MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the 25-minute polling loop in dependabot-lockfile.yml with an event-driven workflow that fires only after upstream CI finishes — and only when there's actually something to fix. Also drops the "empty success" run that was overwriting Claude's fix run in the Actions tab history. Architecture (two workflows, one purpose): 1. dependabot-lockfile.yml (renamed to "Regenerate Dependabot Lockfile"): regenerates pnpm-lock.yaml on dependabot PRs and pushes the update. Nothing else. The fix-failures job and its 25-minute wait-for-checks polling loop are gone. 2. dependabot-claude-fix.yml (new, "Fix Dependabot PRs"): fires on workflow_run.completed for the four CI workflows we care about — Run Tests, E2E Tests, Web CLI E2E Tests (Parallel), Security Audit. Each firing is a gate check, not an immediate trigger: a. Look up the PR the triggering workflow ran on. Exit if the author isn't dependabot[bot] or the PR is from a fork. b. Loop-prevention guard (≤20 successful runs per branch), same shape as the lockfile workflow's guard. c. Compute expected workflows dynamically for this PR — check the PR's changed files against each workflow's on.pull_request.paths filter. Run Tests has a paths list; the other three always run. d. Gate: for every expected workflow, check its run status on this HEAD SHA. If any is still queued/running, or hasn't produced a run record yet, exit 0. A later workflow_run event (from whichever upstream finishes next) will re-run the gate. e. When the gate opens (last expected workflow has finished), gather the failures from all of them in one pass, feed the aggregated summary + logs into the existing Claude prompt, and run the fix step. Concurrency is keyed on HEAD SHA with cancel-in-progress=true: near-simultaneous completions can fire two workflow_run events that both see "all complete"; the second cancels the first and we get a single Claude run per SHA. New pushes (Claude's fix, another dependabot commit) get their own concurrency lane. Why this shape: - No runner time burned on green dependabot PRs. The old workflow polled for up to 25 minutes on every PR, even when CI passed outright. - Actions tab history is self-describing. The old fix-failures job always ran to completion and usually succeeded with nothing to do, so the most recent "Fix Dependabot PRs" row in the Actions tab was typically an empty success, hiding the earlier run that actually did the migration work. Now the workflow only runs when there's something to fix, so every row in its history represents real work. - Claude still sees all failures at once. The gate deliberately waits for every expected workflow before proceeding, so the single Claude invocation handles the full failure set — matching the behaviour of the old polling loop. Implementation notes: - The "Run Tests" paths list is duplicated in the gate step (kept in sync with test.yml on.pull_request.paths via a comment). The matcher handles exact paths and DIR/** globs, which covers every pattern this repo currently uses. - workflow_run always uses the default-branch version of the workflow file; prompt edits take effect on subsequent runs after merge, not in the PR that edits them. - workflow_run-triggered jobs can use write tokens, so the App token flow for pushing Claude's fix commits works the same as before. - The Claude prompt is preserved verbatim except for the failure inputs (now come from steps.gate.outputs instead of steps.wait-for-checks.outputs) and the PR number reference for the final gh pr comment call. --- .github/workflows/dependabot-claude-fix.yml | 477 ++++++++++++++++++++ .github/workflows/dependabot-lockfile.yml | 396 +--------------- 2 files changed, 484 insertions(+), 389 deletions(-) create mode 100644 .github/workflows/dependabot-claude-fix.yml diff --git a/.github/workflows/dependabot-claude-fix.yml b/.github/workflows/dependabot-claude-fix.yml new file mode 100644 index 000000000..dcc461840 --- /dev/null +++ b/.github/workflows/dependabot-claude-fix.yml @@ -0,0 +1,477 @@ +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: read + 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<> "$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" + + - 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@v5 + 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 + 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" diff --git a/.github/workflows/dependabot-lockfile.yml b/.github/workflows/dependabot-lockfile.yml index 9299747fc..fe4b374a9 100644 --- a/.github/workflows/dependabot-lockfile.yml +++ b/.github/workflows/dependabot-lockfile.yml @@ -1,12 +1,15 @@ -name: Fix Dependabot PRs +name: Regenerate Dependabot Lockfile + +# Regenerates pnpm-lock.yaml on dependabot PRs and pushes the update back to +# the PR branch. Fixing CI failures on the regenerated lockfile is handled by +# the separate dependabot-claude-fix workflow, which triggers via workflow_run +# after the CI workflows finish — so we no longer poll for check results here. on: pull_request_target: branches: [main] permissions: - actions: read - checks: read contents: write pull-requests: write @@ -18,9 +21,6 @@ jobs: regen-lockfile: runs-on: ubuntu-latest timeout-minutes: 10 - outputs: - skip: ${{ steps.guard.outputs.skip }} - head_sha: ${{ steps.get-sha.outputs.sha }} steps: - name: Check if Dependabot PR @@ -86,393 +86,11 @@ jobs: - name: Commit lockfile changes if: steps.guard.outputs.skip != 'true' - id: lockfile run: | if git diff --quiet pnpm-lock.yaml; then - echo "changed=false" >> "$GITHUB_OUTPUT" + echo "No lockfile changes." else git add pnpm-lock.yaml git commit -m "fix(deps): regenerate pnpm-lock.yaml" git push - echo "changed=true" >> "$GITHUB_OUTPUT" - fi - - - name: Get HEAD SHA - if: steps.guard.outputs.skip != 'true' - id: get-sha - run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" - - fix-failures: - needs: regen-lockfile - if: needs.regen-lockfile.outputs.skip != 'true' - runs-on: ubuntu-latest - timeout-minutes: 45 - - steps: - - name: Generate App Token - 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: Wait for CI checks to complete - id: wait-for-checks - env: - GH_TOKEN: ${{ github.token }} - HEAD_SHA: ${{ needs.regen-lockfile.outputs.head_sha }} - REPO: ${{ github.repository }} - run: | - # Default output so downstream steps have a defined value even if this step fails - echo "failed_count=0" >> "$GITHUB_OUTPUT" - - POLL_INTERVAL=30 - MAX_POLL_TIME=1500 # 25 minutes - INITIAL_WAIT=60 - - # Checks to skip: our own workflow jobs, Vercel (prefix match), PR tooling - # NOTE: keep in sync — if you rename jobs in other workflows, update here - SKIP_PATTERN="^(regen-lockfile|fix-failures|Vercel.*|claude-review|generate-overview|Generate PR Overview)$" - - # Expected CI checks and their source workflows: - # test -> test.yml (unit + lint + integration) - # e2e-cli -> e2e-tests.yml (CLI E2E) - # audit -> audit.yml (security audit) - # setup -> e2e-web-cli-parallel.yml (Web CLI E2E build prep) - EXPECTED_CHECKS=("test" "e2e-cli" "setup" "audit") - MIN_EXPECTED=3 - - echo "Waiting for CI checks on SHA: $HEAD_SHA" - echo "SHA source: regen-lockfile job output (may be a new commit if lockfile was pushed)" - echo "Initial wait of ${INITIAL_WAIT}s for checks to be queued..." - sleep "$INITIAL_WAIT" - - start_time=$(date +%s) - - while true; do - elapsed=$(( $(date +%s) - start_time )) - if [[ $elapsed -ge $MAX_POLL_TIME ]]; then - echo "::warning::Timed out after ${MAX_POLL_TIME}s waiting for checks" - if [[ -n "$ci_checks" ]]; then - still_pending=$(echo "$ci_checks" | jq -c 'select(.status != "completed")' | jq -r '.name' 2>/dev/null || true) - if [[ -n "$still_pending" ]]; then - echo "::warning::Still pending at timeout: ${still_pending}" - fi - fi - break - fi - - # Fetch all check runs for this SHA (handles pagination) - all_checks=$(gh api "repos/${REPO}/commits/${HEAD_SHA}/check-runs" \ - --paginate \ - --jq '.check_runs[] | {name: .name, status: .status, conclusion: .conclusion, details_url: .details_url}' \ - 2>/dev/null) || { - echo "::warning::API call failed (elapsed: ${elapsed}s), retrying in 10s..." - sleep 10 - continue - } - - # Filter out non-CI checks - ci_checks=$(echo "$all_checks" | jq -c "select(.name | test(\"${SKIP_PATTERN}\") | not)" 2>/dev/null) - - if [[ -z "$ci_checks" ]]; then - echo "No CI checks found yet (elapsed: ${elapsed}s), waiting..." - sleep "$POLL_INTERVAL" - continue - fi - - # Count how many expected checks have appeared - appeared=0 - for check_name in "${EXPECTED_CHECKS[@]}"; do - if echo "$ci_checks" | jq -e "select(.name == \"${check_name}\")" > /dev/null 2>&1; then - appeared=$((appeared + 1)) - fi - done - - if [[ $appeared -lt $MIN_EXPECTED && $elapsed -lt 300 ]]; then - echo "Only ${appeared}/${MIN_EXPECTED} expected checks appeared (elapsed: ${elapsed}s), waiting..." - sleep "$POLL_INTERVAL" - continue - fi - - # Check if all CI checks are completed - total=$(echo "$ci_checks" | jq -s 'length') - pending=$(echo "$ci_checks" | jq -c 'select(.status != "completed")' | jq -s 'length') - - echo "Check status: $((total - pending))/${total} completed (elapsed: ${elapsed}s)" - - if [[ "$pending" -eq 0 && "$total" -gt 0 ]]; then - echo "All CI checks completed." - break - fi - - sleep "$POLL_INTERVAL" - done - - # Fail explicitly if we timed out without ever receiving check data - if [[ $elapsed -ge $MAX_POLL_TIME && -z "$ci_checks" ]]; then - echo "::error::Timed out waiting for CI checks — no check data received" - exit 1 fi - - # Collect failures (include cancelled — usually means an upstream job failed) - failed_checks=$(echo "$ci_checks" | jq -c 'select(.conclusion == "failure" or .conclusion == "cancelled")' 2>/dev/null) - failed_count=0 - if [[ -n "$failed_checks" ]]; then - failed_count=$(echo "$failed_checks" | jq -s 'length') - fi - - echo "failed_count=${failed_count}" >> "$GITHUB_OUTPUT" - - if [[ "$failed_count" -eq 0 ]]; then - echo "All checks passed! Nothing to fix." - exit 0 - fi - - echo "Found ${failed_count} failed check(s)" - - # List failed check names - failed_names=$(echo "$failed_checks" | jq -r '.name' | sort) - echo "Failed: ${failed_names}" - - # Extract unique workflow run IDs from details_url - # URL format: https://github.com/{owner}/{repo}/actions/runs/{run_id}/job/{job_id} - run_ids=$(echo "$failed_checks" | jq -r '.details_url' | sed -n 's|.*/runs/\([0-9]*\)/.*|\1|p' | sort -u) - - # Fetch failed logs for each workflow run - failure_logs="" - for run_id in $run_ids; do - run_name=$(gh api "repos/${REPO}/actions/runs/${run_id}" --jq '.name' 2>/dev/null || echo "unknown") - run_url="https://github.com/${REPO}/actions/runs/${run_id}" - 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 - - # Write outputs using randomised delimiters to avoid collision with log content - delim_summary="EOF_$(openssl rand -hex 16)" - delim_logs="EOF_$(openssl rand -hex 16)" - { - echo "failure_summary<<${delim_summary}" - echo "Failed checks: $(echo "$failed_names" | tr '\n' ', ' | sed 's/, $//')" - echo "${delim_summary}" - echo "failure_logs<<${delim_logs}" - echo "$failure_logs" - echo "${delim_logs}" - } >> "$GITHUB_OUTPUT" - - - name: Checkout Dependabot branch - if: steps.wait-for-checks.outputs.failed_count > 0 - uses: actions/checkout@v6 - with: - ref: ${{ github.event.pull_request.head.ref }} - token: ${{ steps.generate-token.outputs.token }} - - - name: Set up pnpm - if: steps.wait-for-checks.outputs.failed_count > 0 - uses: pnpm/action-setup@v5 - with: - version: 10 - - - name: Set up Node.js - if: steps.wait-for-checks.outputs.failed_count > 0 - uses: actions/setup-node@v6 - with: - node-version: "22.x" - - - name: Install dependencies - if: steps.wait-for-checks.outputs.failed_count > 0 - run: pnpm install --frozen-lockfile --ignore-scripts - - - name: Fix failures with Claude - if: steps.wait-for-checks.outputs.failed_count > 0 - 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.wait-for-checks.outputs.failure_summary }} - - ${{ steps.wait-for-checks.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 ${{ github.event.pull_request.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"