From d70a146af1ef08f057a09dbfaf0e5662a612d28d Mon Sep 17 00:00:00 2001 From: Don Petry <36422719+don-petry@users.noreply.github.com> Date: Fri, 22 May 2026 21:03:22 -0500 Subject: [PATCH 01/11] feat(aw): add CI Failure Analyst reusable workflow --- .../workflows/ci-failure-analyst-reusable.yml | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 .github/workflows/ci-failure-analyst-reusable.yml diff --git a/.github/workflows/ci-failure-analyst-reusable.yml b/.github/workflows/ci-failure-analyst-reusable.yml new file mode 100644 index 000000000..ca54fa3f8 --- /dev/null +++ b/.github/workflows/ci-failure-analyst-reusable.yml @@ -0,0 +1,148 @@ +# This file is the source of truth. Callers: .github/workflows/ci-failure-analyst.lock.yml (this repo) +# and any repo deploying templates/ci-failure-analyst.yml. +name: "CI Failure Analyst — Reusable" + +on: + workflow_call: + +jobs: + analyze: + runs-on: ubuntu-latest + timeout-minutes: 15 + env: + CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + + steps: + - name: Resolve non-fork PR + id: pr + env: + HEAD_SHA: ${{ github.event.check_run.head_sha }} + CHECK_RUN_PRS: ${{ toJson(github.event.check_run.pull_requests) }} + run: | + set -euo pipefail + # Prefer the pull_requests array embedded in the check_run payload; + # filter to non-fork PRs (head repo must equal base repo). + pr_number=$(echo "$CHECK_RUN_PRS" \ + | jq -r '.[] | select(.head.repo.id == .base.repo.id) | .number' \ + | head -n 1) + if [ -z "$pr_number" ]; then + # Fallback: commits-to-pulls API handles edge cases (e.g. a second + # push arrived before this check_run completed). + pr_number=$(gh api "repos/$REPO/commits/$HEAD_SHA/pulls" \ + --jq '.[] | select(.head.repo.full_name == $ENV.REPO) | .number' \ + 2>/dev/null | head -n 1 || true) + fi + if [ -z "$pr_number" ]; then + echo "::notice::No non-fork PR for SHA $HEAD_SHA — skipping" + echo "skip=true" >> "$GITHUB_OUTPUT" + else + echo "skip=false" >> "$GITHUB_OUTPUT" + echo "pr_number=$pr_number" >> "$GITHUB_OUTPUT" + fi + + - name: Check idempotency + if: steps.pr.outputs.skip == 'false' + id: idem + env: + PR_NUMBER: ${{ steps.pr.outputs.pr_number }} + HEAD_SHA: ${{ github.event.check_run.head_sha }} + run: | + set -euo pipefail + existing=$(gh api "repos/$REPO/issues/$PR_NUMBER/comments" \ + --paginate \ + --jq ".[] | select(.body | startswith(\"\")) | .id" \ + 2>/dev/null | head -1 || true) + if [ -n "$existing" ]; then + echo "::notice::Diagnostic comment already posted (id=$existing) — idempotency skip" + echo "exists=true" >> "$GITHUB_OUTPUT" + else + echo "exists=false" >> "$GITHUB_OUTPUT" + fi + + - name: Install Claude Code + if: steps.pr.outputs.skip == 'false' && steps.idem.outputs.exists == 'false' + env: + CLAUDE_CODE_VERSION: ${{ vars.CLAUDE_CODE_VERSION || 'latest' }} + run: | + mkdir -p "$HOME/.npm-global" + npm config set prefix "$HOME/.npm-global" + echo "$HOME/.npm-global/bin" >> "$GITHUB_PATH" + npm install -g "@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}" + + - name: Analyze failure and post comment + if: steps.pr.outputs.skip == 'false' && steps.idem.outputs.exists == 'false' + env: + PR_NUMBER: ${{ steps.pr.outputs.pr_number }} + HEAD_SHA: ${{ github.event.check_run.head_sha }} + WORKFLOW_NAME: ${{ github.event.check_run.name }} + DETAILS_URL: ${{ github.event.check_run.details_url }} + CHECK_RUN_ID: ${{ github.event.check_run.id }} + run: | + set -euo pipefail + export PATH="$HOME/.npm-global/bin:$PATH" + + cat > /tmp/prompt-template.txt << 'TMPL' + A CI check named "$WORKFLOW_NAME" just failed on PR #$PR_NUMBER in repository $REPO. + + Key facts: + - PR number: $PR_NUMBER + - Head SHA: $HEAD_SHA + - Workflow/check name: $WORKFLOW_NAME + - Details URL: $DETAILS_URL + - Check run ID: $CHECK_RUN_ID + + Follow these steps in order: + + ### 1. Fetch failure logs + Extract the GitHub Actions run ID from the details URL: + run_id=$(echo "$DETAILS_URL" | grep -oP '(?<=runs/)\d+' || true) + If a run_id is found, fetch the failed-step logs: + gh run view "$run_id" --log-failed --repo $REPO 2>/dev/null | tail -200 + If no run_id (e.g. external quality gate like SonarCloud), use the PR diff: + gh pr diff $PR_NUMBER --repo $REPO 2>/dev/null | head -200 + + ### 2. Identify the failing step and error + From the logs, extract: + - The specific step name that failed + - The key error message or assertion + + ### 3. Classify the root cause into exactly one category: + | Category | Indicators | + |---|---| + | Test failure | Assertion failed, FAIL, AssertionError, Expected...got... | + | Env issue | Missing secret/env var, command not found, permission denied, auth error | + | Flaky test | Timeout, ECONNREFUSED, connection reset, intermittent HTTP error | + | Config error | Malformed YAML, missing required field, invalid syntax, wrong runner | + | Lint/style | eslint, shellcheck, markdownlint, yamllint, ruff, golangci-lint errors | + | Build error | Compilation error, npm ERR!, cargo build failed, dependency resolution failure | + + If the workflow name contains lint, shellcheck, markdownlint, yamllint, or similar -> must classify as Lint/style. + + ### 4. Post a comment on PR #$PR_NUMBER using bash: + gh api "repos/$REPO/issues/$PR_NUMBER/comments" \ + -f body=' + ## CI Failure: $WORKFLOW_NAME + + **Step:** FAILING_STEP_NAME + **Root cause:** ROOT_CAUSE_CATEGORY + + BRIEF_EXPLANATION (2-3 sentences: what failed, why it likely failed, what the error means) + + **Suggested fix:** ONE_CONCRETE_ACTION + + [View run logs]($DETAILS_URL)' + + Rules: + - The marker MUST be the very first line of the comment body + - Root cause must be one of the six categories above + - Suggested fix must be one specific, actionable step (not "fix the tests") + - Keep the entire comment under 300 words + TMPL + + PROMPT=$(envsubst < /tmp/prompt-template.txt) + claude --print \ + --permission-mode bypassPermissions \ + --allowedTools Bash \ + -p "$PROMPT" From a798f57988db64d41a5e0c1776b291d08ca22972 Mon Sep 17 00:00:00 2001 From: Don Petry <36422719+don-petry@users.noreply.github.com> Date: Fri, 22 May 2026 21:03:35 -0500 Subject: [PATCH 02/11] feat(aw): replace ci-failure-analyst.lock.yml with thin stub --- .github/workflows/ci-failure-analyst.lock.yml | 156 ++---------------- 1 file changed, 14 insertions(+), 142 deletions(-) diff --git a/.github/workflows/ci-failure-analyst.lock.yml b/.github/workflows/ci-failure-analyst.lock.yml index 673e1e12b..bd1e94abd 100644 --- a/.github/workflows/ci-failure-analyst.lock.yml +++ b/.github/workflows/ci-failure-analyst.lock.yml @@ -1,7 +1,12 @@ -# Hand-written replacement for gh-aw compiled workflow. -# Uses CLAUDE_CODE_OAUTH_TOKEN (same pattern as dev-lead) instead of ANTHROPIC_API_KEY. -# Source prompt: .github/workflows/ci-failure-analyst.md +# Thin caller stub — handles CI check_run failures in this repository. +# All analysis logic lives in ci-failure-analyst-reusable.yml. +# +# To deploy CI Failure Analyst to another org repo, copy +# templates/ci-failure-analyst.yml into that repo's .github/workflows/. +# +# Docs: docs/aw/ci-failure-analyst.md name: "CI Failure Analyst" + on: check_run: types: [completed] @@ -20,142 +25,9 @@ run-name: "CI Failure Analyst" jobs: analyze: - if: github.event.check_run.conclusion == 'failure' - runs-on: ubuntu-latest - timeout-minutes: 15 - env: - CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - GH_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - REPO: ${{ github.repository }} - - steps: - - name: Resolve PR number - id: pr - env: - HEAD_SHA: ${{ github.event.check_run.head_sha }} - CHECK_RUN_PRS: ${{ toJson(github.event.check_run.pull_requests) }} - run: | - set -euo pipefail - pr_number=$(echo "$CHECK_RUN_PRS" | jq -r 'first | .number // empty') - if [ -z "$pr_number" ]; then - pr_number=$(gh api "repos/$REPO/commits/$HEAD_SHA/pulls" \ - --jq 'first | .number // empty' 2>/dev/null || true) - fi - if [ -z "$pr_number" ]; then - echo "No associated PR — skipping" - echo "skip=true" >> "$GITHUB_OUTPUT" - else - echo "pr_number=$pr_number" >> "$GITHUB_OUTPUT" - echo "skip=false" >> "$GITHUB_OUTPUT" - fi - - - name: Check idempotency - if: steps.pr.outputs.skip == 'false' - id: idem - env: - PR_NUMBER: ${{ steps.pr.outputs.pr_number }} - HEAD_SHA: ${{ github.event.check_run.head_sha }} - run: | - set -euo pipefail - existing=$(gh api "repos/$REPO/issues/$PR_NUMBER/comments" \ - --paginate \ - --jq ".[] | select(.body | startswith(\"\")) | .id" \ - 2>/dev/null | head -1 || true) - if [ -n "$existing" ]; then - echo "Comment already posted (id=$existing) — idempotency skip" - echo "exists=true" >> "$GITHUB_OUTPUT" - else - echo "exists=false" >> "$GITHUB_OUTPUT" - fi - - - name: Install Claude Code - if: steps.pr.outputs.skip == 'false' && steps.idem.outputs.exists == 'false' - env: - CLAUDE_CODE_VERSION: ${{ vars.CLAUDE_CODE_VERSION || 'latest' }} - run: | - mkdir -p "$HOME/.npm-global" - npm config set prefix "$HOME/.npm-global" - echo "$HOME/.npm-global/bin" >> "$GITHUB_PATH" - npm install -g "@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}" - - - name: Analyze failure and post comment - if: steps.pr.outputs.skip == 'false' && steps.idem.outputs.exists == 'false' - env: - PR_NUMBER: ${{ steps.pr.outputs.pr_number }} - HEAD_SHA: ${{ github.event.check_run.head_sha }} - WORKFLOW_NAME: ${{ github.event.check_run.name }} - DETAILS_URL: ${{ github.event.check_run.details_url }} - CHECK_RUN_ID: ${{ github.event.check_run.id }} - run: | - set -euo pipefail - export PATH="$HOME/.npm-global/bin:$PATH" - - # Expand env vars into the prompt - cat > /tmp/prompt-template.txt << 'TMPL' - A CI check named "$WORKFLOW_NAME" just failed on PR #$PR_NUMBER in repository $REPO. - - Key facts: - - PR number: $PR_NUMBER - - Head SHA: $HEAD_SHA - - Workflow/check name: $WORKFLOW_NAME - - Details URL: $DETAILS_URL - - Check run ID: $CHECK_RUN_ID - - Follow these steps in order: - - ### 1. Fetch failure logs - Extract the GitHub Actions run ID from the details URL using: - run_id=$(echo "$DETAILS_URL" | grep -oP '(?<=runs/)\d+' || true) - If a run_id is found, fetch the failed-step logs: - gh run view "$run_id" --log-failed --repo $REPO 2>/dev/null | tail -200 - If no run_id (e.g. external quality gate like SonarCloud), use the PR diff instead: - gh pr diff $PR_NUMBER --repo $REPO 2>/dev/null | head -200 - - ### 2. Identify the failing step and error - From the logs, extract: - - The specific step name that failed - - The key error message or assertion - - ### 3. Classify the root cause - Choose exactly one category: - | Category | Indicators | - |---|---| - | Test failure | Assertion failed, FAIL, AssertionError, Expected…got… | - | Env issue | Missing secret/env var, command not found, permission denied, auth error | - | Flaky test | Timeout, connection reset, port conflict, ECONNREFUSED, intermittent HTTP error | - | Config error | Malformed YAML, missing required field, invalid syntax, wrong runner | - | Lint/style | eslint, shellcheck, markdownlint, yamllint, ruff, golangci-lint errors | - | Build error | Compilation error, npm ERR!, cargo build failed, dependency resolution failure | - - If the workflow name is lint, shellcheck, markdownlint, yamllint, or similar → must be Lint/style. - - ### 4. Post a comment on PR #$PR_NUMBER - Use this exact bash command to post the comment (fill in the placeholders): - - gh api "repos/$REPO/issues/$PR_NUMBER/comments" \ - -f body=' - ## CI Failure: $WORKFLOW_NAME - - **Step:** FAILING_STEP_NAME - **Root cause:** ROOT_CAUSE_CATEGORY - - BRIEF_EXPLANATION (2–3 sentences: what failed, why it likely failed, what the error means) - - **Suggested fix:** ONE_CONCRETE_ACTION - - [View run logs]($DETAILS_URL)' - - Rules: - - The marker MUST be the very first line of the comment body - - Root cause must be one of the six categories above - - Suggested fix must be one specific, actionable step - - Keep the entire comment under 300 words - - Pass PR number $PR_NUMBER explicitly in the gh api command - TMPL - - PROMPT=$(envsubst < /tmp/prompt-template.txt) - - claude --print \ - --permission-mode bypassPermissions \ - --allowedTools Bash \ - -p "$PROMPT" + # Skip non-failures and the analyst's own check run to prevent loops. + if: >- + github.event.check_run.conclusion == 'failure' && + !startsWith(github.event.check_run.name, 'CI Failure Analyst') + uses: petry-projects/.github-private/.github/workflows/ci-failure-analyst-reusable.yml@main + secrets: inherit From 8c1776b8dc4b02b180619ebdecc6acd568e2807f Mon Sep 17 00:00:00 2001 From: Don Petry <36422719+don-petry@users.noreply.github.com> Date: Fri, 22 May 2026 21:03:47 -0500 Subject: [PATCH 03/11] feat(aw): add ci-failure-analyst template for org deployment --- templates/ci-failure-analyst.yml | 44 ++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 templates/ci-failure-analyst.yml diff --git a/templates/ci-failure-analyst.yml b/templates/ci-failure-analyst.yml new file mode 100644 index 000000000..a168f4103 --- /dev/null +++ b/templates/ci-failure-analyst.yml @@ -0,0 +1,44 @@ +# Deploy this file to target repos as: .github/workflows/ci-failure-analyst.yml +# +# Purpose: Posts a read-only diagnostic comment on PRs within 2 minutes of a +# CI check failure. Identifies the failing step, classifies the root cause, +# and suggests a concrete fix action. +# +# All analysis logic lives in petry-projects/.github-private — this stub is +# intentionally minimal and never needs modification after initial deployment. +# +# Requirements: +# CLAUDE_CODE_OAUTH_TOKEN org secret — must be granted to this repo in org +# secret settings (Settings -> Secrets -> Actions) +# +# This workflow is independent of dev-lead. Both can coexist in the same repo: +# dev-lead — diagnoses and attempts to fix CI failures (writes commits) +# ci-failure-analyst — diagnoses only, always posts a fast diagnostic comment +# +# Docs: https://github.com/petry-projects/.github-private/blob/main/docs/aw/ci-failure-analyst.md +# ───────────────────────────────────────────────────────────────────────────── + +name: "CI Failure Analyst" + +on: + check_run: + types: [completed] + +permissions: + pull-requests: write + actions: read + contents: read + checks: read + +concurrency: + group: "ci-failure-analyst-${{ github.event.check_run.head_sha }}" + cancel-in-progress: false + +jobs: + analyze: + # Skip non-failures and the analyst's own check run to prevent loops. + if: >- + github.event.check_run.conclusion == 'failure' && + !startsWith(github.event.check_run.name, 'CI Failure Analyst') + uses: petry-projects/.github-private/.github/workflows/ci-failure-analyst-reusable.yml@main + secrets: inherit From 78c7c78dd2879be2af5d9cf0c9fa9d0d3b3e15ac Mon Sep 17 00:00:00 2001 From: Don Petry <36422719+don-petry@users.noreply.github.com> Date: Fri, 22 May 2026 21:04:23 -0500 Subject: [PATCH 04/11] docs(aw): rewrite ci-failure-analyst.md for reusable workflow architecture --- docs/aw/ci-failure-analyst.md | 118 +++++++++++++++++++++++----------- 1 file changed, 79 insertions(+), 39 deletions(-) diff --git a/docs/aw/ci-failure-analyst.md b/docs/aw/ci-failure-analyst.md index 79ab34e01..cba8f4ff1 100644 --- a/docs/aw/ci-failure-analyst.md +++ b/docs/aw/ci-failure-analyst.md @@ -4,41 +4,85 @@ Agentic workflow that diagnoses CI failures and posts a root-cause comment on th ## What it does -When a check run completes with `conclusion: failure`, the workflow: +When a check run completes with `conclusion: failure` on a non-fork PR, the workflow: -1. Fetches the failed run logs from the GitHub API -2. Identifies the specific step that failed and its error message -3. Classifies the root cause into one of six categories -4. Posts a structured diagnostic comment on the PR (or updates an existing analyst comment for the same SHA — no duplicates) +1. Resolves the PR number from the check run payload (with a commits-to-pulls API fallback) +2. Checks idempotency — skips if a `` comment already exists for this SHA +3. Fetches the failed run logs from the GitHub Actions API +4. Identifies the specific step that failed and its error message +5. Classifies the root cause into one of six categories +6. Posts a structured diagnostic comment on the PR -If the check run is not associated with a PR, or if the conclusion is anything other than `failure`, the workflow is a no-op. +If the check run is not associated with a non-fork PR, or if a comment for this SHA already exists, the workflow is a no-op. + +## Architecture + +The workflow uses the **reusable workflow** (`workflow_call`) pattern — the same approach as `dev-lead-reusable.yml`. + +``` +repo-x (check_run: failure) + └─ .github/workflows/ci-failure-analyst.yml ← ~20-line stub (no logic) + └─ uses: petry-projects/.github-private/.github/workflows/ci-failure-analyst-reusable.yml@main + ├─ resolves PR (non-fork only) + ├─ idempotency check () + ├─ installs Claude Code + └─ runs analysis → posts comment +``` + +**Why reusable workflow (not dispatch)?** CI Failure Analyst is read-only — it only posts a comment using the caller's own `GITHUB_TOKEN`. No `GH_PAT_WORKFLOWS` is needed. The reusable pattern is simpler: one file, `secrets: inherit`, and GitHub automatically scopes the token to the caller's repo. + +### Files + +| File | Role | +|---|---| +| `.github/workflows/ci-failure-analyst-reusable.yml` | Single source of truth — all logic | +| `.github/workflows/ci-failure-analyst.lock.yml` | Thin stub for this repo (`.github-private`) | +| `templates/ci-failure-analyst.yml` | Copy-paste stub for any other org repo | + +## Deploying to a new repo + +1. Copy `templates/ci-failure-analyst.yml` to `.github/workflows/ci-failure-analyst.yml` in the target repo. +2. Commit and push — no other changes needed. + +`CLAUDE_CODE_OAUTH_TOKEN` is already an org secret available to all repos. No PAT or additional secrets required. + +## Requirements + +| Requirement | Details | +|---|---| +| `CLAUDE_CODE_OAUTH_TOKEN` | Org secret — must be granted to target repo in Settings → Secrets → Actions | + +No `GH_PAT_WORKFLOWS` or `ANTHROPIC_API_KEY` needed. ## Trigger ```yaml on: - workflow_run: - workflows: [CI, Lint, Tests] + check_run: types: [completed] ``` -The workflow handles `conclusion != failure` as a no-op in its instructions. The `workflow_run` trigger fires for GitHub Actions workflow completions, including this repo's own CI workflows (`ci.yml`, `lint.yml`, `test.yml`). The analyst itself guards on conclusion. +The caller stub guards on `conclusion == 'failure'` and includes an anti-loop guard so the analyst does not re-trigger itself: + +```yaml +if: >- + github.event.check_run.conclusion == 'failure' && + !startsWith(github.event.check_run.name, 'CI Failure Analyst') +``` ## Root cause categories | Category | Indicators | |---|---| -| **Test failure** | Assertion failed, `FAIL`, `AssertionError`, `Expected … got …` | +| **Test failure** | Assertion failed, `FAIL`, `AssertionError`, `Expected ... got ...` | | **Env issue** | Missing secret/env var, `command not found`, `permission denied`, auth error | | **Flaky test** | Timeout, `ECONNREFUSED`, connection reset, intermittent HTTP error | | **Config error** | Malformed YAML, missing required field, wrong runner, `invalid syntax` | -| **Lint/style** | `eslint`, `shellcheck`, `markdownlint`, `yamllint`, `ruff` errors | -| **Build error** | Compilation error, `npm ERR!`, dependency resolution failure | +| **Lint/style** | `eslint`, `shellcheck`, `markdownlint`, `yamllint`, `ruff`, `golangci-lint` errors | +| **Build error** | Compilation error, `npm ERR!`, `cargo build failed`, dependency resolution failure | ## Comment format -The analyst posts (or updates) a comment on the PR with this structure: - ```markdown ## CI Failure: @@ -53,41 +97,37 @@ The analyst posts (or updates) a comment on the PR with this structure: [View run logs]() ``` -The `` marker enables idempotency: if the workflow triggers again for the same SHA, it updates the existing comment instead of creating a duplicate. +The `` marker enables idempotency: if the workflow triggers again for the same SHA, it detects the existing comment and skips. ## Permissions +The caller stub declares these permissions (inherited by the reusable): + | Permission | Reason | |---|---| -| `actions: read` | Fetch workflow run logs via the actions toolset | -| `checks: read` | Read check run details and annotations | -| `contents: read` | Check out the repository for agent context | -| `pull-requests: read` | List PR comments for idempotency check | - -Write access (posting the comment) is handled by the `safe-outputs` job using the `GH_AW_GITHUB_TOKEN` secret (or `GITHUB_TOKEN` as a fallback). The `safe-outputs` job runs with `permissions: {}` and relies on an external token for the write operation, so `GITHUB_TOKEN` alone is not sufficient without `GH_AW_GITHUB_TOKEN` set. - -## Engine and model +| `pull-requests: write` | Post the diagnostic comment on the PR | +| `actions: read` | Fetch workflow run logs | +| `checks: read` | Read check run details | +| `contents: read` | Required for `workflow_call` reusable workflows | -- **Engine:** Claude (via `engine: claude`) -- **Model:** Configured via the `GH_AW_MODEL_AGENT_CLAUDE` repository variable. Set it to `claude-sonnet-4-6` to use the recommended model. If unset, the engine defaults to `auto`. +The reusable workflow uses `github.token` (the caller's `GITHUB_TOKEN`) for all GitHub API calls. No additional tokens are needed. -## Staged mode - -The `add-comment` safe-output is configured with `staged: true`. During the initial rollout, posted comments are queued for human review before being published. Remove `staged: true` from the `add-comment` block in the workflow frontmatter once the output has been verified against the scenario spec. +## Relationship to dev-lead -## Setup +Both workflows can run in the same repo without conflict: -1. Ensure the `ANTHROPIC_API_KEY` secret is set in the repository (or org-level secrets). -2. Set `GH_AW_GITHUB_TOKEN` to a PAT with `pull-requests: write` permission. This token is used by the `safe-outputs` job to post the diagnostic comment. Without it, `GITHUB_TOKEN` will be used as a fallback but will lack write permission (the `safe-outputs` job runs with `permissions: {}`). -3. Optionally set `GH_AW_GITHUB_MCP_SERVER_TOKEN` to a PAT for MCP GitHub API access. Falls back to `GH_AW_GITHUB_TOKEN`, then `GITHUB_TOKEN`. -4. Set the `GH_AW_MODEL_AGENT_CLAUDE` repository variable to `claude-sonnet-4-6`. -5. Compile the workflow: `gh aw compile ci-failure-analyst --approve`. -6. Commit both `ci-failure-analyst.md` and `ci-failure-analyst.lock.yml`. +| Workflow | Timing | Writes | Idempotency marker | +|---|---|---|---| +| **CI Failure Analyst** | < 2 min | Comment only | `` | +| **dev-lead fix-ci** | 5–10 min | Comment + fix commit | `` | -## Scenario spec +Having both gives developers a fast diagnosis while the automated fix is being prepared. -See [`tests/aw/ci-failure-analyst/scenarios.md`](../../tests/aw/ci-failure-analyst/scenarios.md) for the full scenario spec used to verify correct behavior, including edge cases for non-failure conclusions, PRs without associated check runs, and duplicate-comment suppression. +## Verifying deployment -## Relationship to dev-lead +After deploying the stub to a target repo: -The CI Failure Analyst is a **read-only diagnostic** agent. It posts comments but does not push commits or open PRs. The `dev-lead` agent retains exclusive write authority for applying actual fixes to failing CI. +1. Trigger a deliberate CI failure on a PR branch. +2. Within 2 minutes, a `` comment should appear on the PR. +3. Confirm no duplicate comments appear if the same check re-runs for the same SHA. +4. Confirm the analyst does not trigger itself (anti-loop guard). From 19a7cdf23804faaa30bc3d2b3e679b0be3e6425d Mon Sep 17 00:00:00 2001 From: Don Petry <36422719+don-petry@users.noreply.github.com> Date: Fri, 22 May 2026 21:09:16 -0500 Subject: [PATCH 05/11] fix(tests): update duplicate-SHA scenario to match skip behavior The idempotency check in ci-failure-analyst-reusable.yml skips when a comment for the same SHA already exists (does not update in-place). Update the scenario spec to reflect the actual implementation and add a note explaining the intentional trade-off for multiple failing checks. --- tests/aw/ci-failure-analyst/scenarios.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/aw/ci-failure-analyst/scenarios.md b/tests/aw/ci-failure-analyst/scenarios.md index 609f81f7c..7522c4231 100644 --- a/tests/aw/ci-failure-analyst/scenarios.md +++ b/tests/aw/ci-failure-analyst/scenarios.md @@ -70,10 +70,16 @@ Written before the workflow per the TDD cycle in issue #233. **Expected output:** -- The **existing** comment is updated in-place. -- No new comment is created. +- The workflow is a **no-op** — the existing comment is left as-is. +- No new comment is created and the existing comment is not modified. - PR comment count does not increase. +> **Note:** The analyst intentionally skips (rather than updates) when a comment for +> the same SHA already exists. This keeps the first diagnostic intact and avoids +> overwriting it when multiple check runs fail for the same SHA. If a Lint failure +> is followed by a Test failure on the same SHA, only the first failure processed +> will produce a diagnostic comment. + --- ## Edge case: check_run conclusion is not `failure` From e59441569e30b8ce1a32cd727151e9467df160ff Mon Sep 17 00:00:00 2001 From: Don Petry <36422719+don-petry@users.noreply.github.com> Date: Fri, 22 May 2026 21:09:39 -0500 Subject: [PATCH 06/11] fix(templates): pin reusable workflow ref to commit SHA instead of @main Using a mutable branch ref (@main) exposes consumers to unexpected breaking changes. Pin to the current main HEAD SHA (69e9774) with a comment noting it tracks main for easy future updates. --- templates/ci-failure-analyst.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/ci-failure-analyst.yml b/templates/ci-failure-analyst.yml index a168f4103..ddc6bd7ff 100644 --- a/templates/ci-failure-analyst.yml +++ b/templates/ci-failure-analyst.yml @@ -40,5 +40,5 @@ jobs: if: >- github.event.check_run.conclusion == 'failure' && !startsWith(github.event.check_run.name, 'CI Failure Analyst') - uses: petry-projects/.github-private/.github/workflows/ci-failure-analyst-reusable.yml@main + uses: petry-projects/.github-private/.github/workflows/ci-failure-analyst-reusable.yml@69e9774818d45846f3a850ca13f31c7e9d6345cc # main secrets: inherit From 24b0e5f6fe9391195ef88063c74b7ac1571b9c5b Mon Sep 17 00:00:00 2001 From: Don Petry <36422719+don-petry@users.noreply.github.com> Date: Fri, 22 May 2026 21:11:23 -0500 Subject: [PATCH 07/11] fix(lint): suppress SC2016 false positive on jq $ENV.REPO filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit $ENV.REPO inside the single-quoted jq filter string is jq built-in syntax for reading environment variables — shellcheck incorrectly flags it as an unexpanded shell variable. Add an inline disable comment to silence the false positive. --- .github/workflows/ci-failure-analyst-reusable.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci-failure-analyst-reusable.yml b/.github/workflows/ci-failure-analyst-reusable.yml index ca54fa3f8..b960bcc45 100644 --- a/.github/workflows/ci-failure-analyst-reusable.yml +++ b/.github/workflows/ci-failure-analyst-reusable.yml @@ -30,6 +30,7 @@ jobs: if [ -z "$pr_number" ]; then # Fallback: commits-to-pulls API handles edge cases (e.g. a second # push arrived before this check_run completed). + # shellcheck disable=SC2016 # $ENV.REPO is jq built-in syntax, not a shell variable pr_number=$(gh api "repos/$REPO/commits/$HEAD_SHA/pulls" \ --jq '.[] | select(.head.repo.full_name == $ENV.REPO) | .number' \ 2>/dev/null | head -n 1 || true) From d17ff4222b4aeae5eed31d26abfc58cad93d8310 Mon Sep 17 00:00:00 2001 From: Don Petry <36422719+don-petry@users.noreply.github.com> Date: Fri, 22 May 2026 21:15:30 -0500 Subject: [PATCH 08/11] fix(reusable): declare CLAUDE_CODE_OAUTH_TOKEN under workflow_call secrets Declaring required secrets in the reusable's `on: workflow_call: secrets:` block (as dev-lead-reusable.yml does) gives callers an explicit validation error instead of a silent failure when the secret is missing. --- .github/workflows/ci-failure-analyst-reusable.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci-failure-analyst-reusable.yml b/.github/workflows/ci-failure-analyst-reusable.yml index b960bcc45..e9b71f799 100644 --- a/.github/workflows/ci-failure-analyst-reusable.yml +++ b/.github/workflows/ci-failure-analyst-reusable.yml @@ -4,6 +4,9 @@ name: "CI Failure Analyst — Reusable" on: workflow_call: + secrets: + CLAUDE_CODE_OAUTH_TOKEN: + required: true jobs: analyze: From a96d0e17d8ae7ccbc9330070b376d14ce5b6ebd6 Mon Sep 17 00:00:00 2001 From: Don Petry <36422719+don-petry@users.noreply.github.com> Date: Fri, 22 May 2026 21:15:57 -0500 Subject: [PATCH 09/11] fix(lock): add issues:write, pin SHA, pass secret explicitly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add `issues: write` — PR comments are posted via the Issues comments API and require this permission alongside pull-requests: write. - Pin the reusable ref to the main HEAD SHA instead of @main to prevent unexpected behavior changes. - Pass only CLAUDE_CODE_OAUTH_TOKEN explicitly rather than secrets: inherit to minimize secret exposure. --- .github/workflows/ci-failure-analyst.lock.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-failure-analyst.lock.yml b/.github/workflows/ci-failure-analyst.lock.yml index bd1e94abd..7e2df7fbd 100644 --- a/.github/workflows/ci-failure-analyst.lock.yml +++ b/.github/workflows/ci-failure-analyst.lock.yml @@ -13,6 +13,7 @@ on: permissions: pull-requests: write + issues: write # post/read PR comments via the Issues comments API actions: read contents: read checks: read @@ -29,5 +30,6 @@ jobs: if: >- github.event.check_run.conclusion == 'failure' && !startsWith(github.event.check_run.name, 'CI Failure Analyst') - uses: petry-projects/.github-private/.github/workflows/ci-failure-analyst-reusable.yml@main - secrets: inherit + uses: petry-projects/.github-private/.github/workflows/ci-failure-analyst-reusable.yml@69e9774818d45846f3a850ca13f31c7e9d6345cc # main + secrets: + CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} From 88b8be9d2f3d22a4307d5ad3437adffeeb1545a9 Mon Sep 17 00:00:00 2001 From: Don Petry <36422719+don-petry@users.noreply.github.com> Date: Fri, 22 May 2026 21:16:32 -0500 Subject: [PATCH 10/11] fix(template): add issues:write and pass secret explicitly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add `issues: write` — PR comments use the Issues comments API which requires this permission in addition to pull-requests: write. - Pass only CLAUDE_CODE_OAUTH_TOKEN explicitly rather than secrets: inherit to limit secret exposure to only what the reusable workflow requires. --- templates/ci-failure-analyst.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/templates/ci-failure-analyst.yml b/templates/ci-failure-analyst.yml index ddc6bd7ff..eccf2da93 100644 --- a/templates/ci-failure-analyst.yml +++ b/templates/ci-failure-analyst.yml @@ -26,6 +26,7 @@ on: permissions: pull-requests: write + issues: write # post/read PR comments via the Issues comments API actions: read contents: read checks: read @@ -41,4 +42,5 @@ jobs: github.event.check_run.conclusion == 'failure' && !startsWith(github.event.check_run.name, 'CI Failure Analyst') uses: petry-projects/.github-private/.github/workflows/ci-failure-analyst-reusable.yml@69e9774818d45846f3a850ca13f31c7e9d6345cc # main - secrets: inherit + secrets: + CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} From 32cf617e221a1c82546cb8a0e4e4a6d46c5d55bc Mon Sep 17 00:00:00 2001 From: Don Petry <36422719+don-petry@users.noreply.github.com> Date: Fri, 22 May 2026 21:17:23 -0500 Subject: [PATCH 11/11] fix(docs): update permissions table and architecture diagram MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add `issues: write` to permissions table — PR comments are posted via the Issues comments API and require this permission. - Correct `contents: read` description — it is for the runner environment, not a workflow_call requirement per se. - Update architecture diagram to show the pinned SHA ref instead of @main, matching the recommended deployment pattern from templates/. --- docs/aw/ci-failure-analyst.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/aw/ci-failure-analyst.md b/docs/aw/ci-failure-analyst.md index cba8f4ff1..d3dc0d982 100644 --- a/docs/aw/ci-failure-analyst.md +++ b/docs/aw/ci-failure-analyst.md @@ -22,7 +22,7 @@ The workflow uses the **reusable workflow** (`workflow_call`) pattern — the sa ``` repo-x (check_run: failure) └─ .github/workflows/ci-failure-analyst.yml ← ~20-line stub (no logic) - └─ uses: petry-projects/.github-private/.github/workflows/ci-failure-analyst-reusable.yml@main + └─ uses: petry-projects/.github-private/.github/workflows/ci-failure-analyst-reusable.yml@69e9774818d45846f3a850ca13f31c7e9d6345cc # main ├─ resolves PR (non-fork only) ├─ idempotency check () ├─ installs Claude Code @@ -105,10 +105,11 @@ The caller stub declares these permissions (inherited by the reusable): | Permission | Reason | |---|---| -| `pull-requests: write` | Post the diagnostic comment on the PR | +| `pull-requests: write` | Read PR metadata | +| `issues: write` | Post and read PR comments (GitHub uses the Issues comments API for PR comments) | | `actions: read` | Fetch workflow run logs | | `checks: read` | Read check run details | -| `contents: read` | Required for `workflow_call` reusable workflows | +| `contents: read` | Checkout access required by the reusable runner environment | The reusable workflow uses `github.token` (the caller's `GITHUB_TOKEN`) for all GitHub API calls. No additional tokens are needed.