Skip to content
130 changes: 130 additions & 0 deletions .github/workflows/ai-investigate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# AI failure investigation — when a gate fails, the AI investigates with FULL
# context, because everything it needs is co-located in the repo:
# tests + PRD + DESIGN + ADRs + schemas + code, per component
# (docs/<unit>/specs/* and src/**/specs/*, registered in artifacts.toml).
#
# Trigger: any mandatory gate workflow finishing red on a PR branch.
# Output: an investigation comment on the PR — root-cause hypothesis, the
# spec section the failure violates (PRD/DESIGN traceability), suspect files,
# and a proposed fix or test. Advisory, never a gate: humans decide, AI digs.
#
# Optional: ANTHROPIC_API_KEY repo secret. When it is not set, the job skips
# its work and passes cleanly — this workflow is advisory and never a gate.

name: AI Investigate Failure

on:
workflow_run:
workflows:
- "Backend Lint & Test"
- "E2E — Bronze to API"
- "Security Gates"
- "Helm Validate"
- "Docs Gates"
- "Nightly Install Test"
- "Release Train"
types: [completed]

permissions:
contents: read
actions: read
pull-requests: write
issues: write

jobs:
investigate:
# Only for failures on SAME-REPO branches. A malicious fork cannot trigger
# this (which would run on the default branch with secret access + Bash and
# a head_sha checkout) — closes the workflow_run secret-exfiltration vector.
if: ${{ github.event.workflow_run.conclusion == 'failure' && github.event.workflow_run.head_repository.full_name == github.repository }}
runs-on: ubuntu-latest
timeout-minutes: 15
# Job-level env can read `secrets`; step-level `if` then gates on it. The
# ANTHROPIC_API_KEY secret is optional, so when it is absent the heavy steps
# are skipped and the workflow still passes (advisory, never a gate) instead
# of erroring out inside the action with no key.
env:
HAS_ANTHROPIC_KEY: ${{ secrets.ANTHROPIC_API_KEY != '' }}
# Defense-in-depth: best-effort scrub of ANTHROPIC_API_KEY from the
# subprocesses the action's Bash tool spawns, so a same-repo branch
# can't exfiltrate it via a planted command. The same-repo gate above
# already blocks forks; this hardens the same-repo path.
CLAUDE_CODE_SUBPROCESS_ENV_SCRUB: "1"
steps:
# nosemgrep: yaml.github-actions.security.workflow-run-target-code-checkout.workflow-run-target-code-checkout -- mitigated: job gated on head_repository == this repo (same-repo only), persist-credentials:false, no secrets used
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.workflow_run.head_sha }}
persist-credentials: false
Comment thread
coderabbitai[bot] marked this conversation as resolved.

- name: Note when skipped (no API key)
if: env.HAS_ANTHROPIC_KEY != 'true'
run: echo "ANTHROPIC_API_KEY not configured — advisory AI investigation skipped; workflow passes."

- name: Collect failing job logs
if: env.HAS_ANTHROPIC_KEY == 'true'
# Untrusted workflow_run.* values are passed via env (shell quotes them),
# never interpolated into the script body — avoids template injection.
env:
GH_TOKEN: ${{ github.token }}
RUN_ID: ${{ github.event.workflow_run.id }}
WF_NAME: ${{ github.event.workflow_run.name }}
WF_BRANCH: ${{ github.event.workflow_run.head_branch }}
WF_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
set -euo pipefail
mkdir -p /tmp/failure
# Best-effort: this is an advisory job, so log retrieval must never
# abort it (under pipefail a failing fallback pipeline would, which
# then skips the AI step via implicit success()).
if ! gh run view "$RUN_ID" --repo "$GITHUB_REPOSITORY" --log-failed \
> /tmp/failure/failed-jobs.log 2>/dev/null; then
gh run view "$RUN_ID" --repo "$GITHUB_REPOSITORY" --log 2>/dev/null \
| tail -800 > /tmp/failure/failed-jobs.log || true
fi
[ -s /tmp/failure/failed-jobs.log ] || echo "(no logs retrieved)" > /tmp/failure/failed-jobs.log
{
echo "workflow: $WF_NAME"
echo "branch: $WF_BRANCH"
echo "sha: $WF_SHA"
} > /tmp/failure/meta.txt

- name: AI investigation (full co-located context)
if: env.HAS_ANTHROPIC_KEY == 'true'
# SHA-pinned to v1 per the repo's hash-pin policy (CI supply-chain
# hardening — same as actions/checkout above); the trailing `# v1`
# documents the channel so Dependabot/Renovate can bump the pin. Runs
# only on workflow_run after a gate failed; advisory, no release artifact.
uses: anthropics/claude-code-action@30544b674398ee15c84819bd87caf8a87e8c7b55 # v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
Comment thread
SharedQA marked this conversation as resolved.
prompt: |
A mandatory CI gate failed. Investigate the root cause using the
full co-located context of this repository.

Failure metadata: /tmp/failure/meta.txt (workflow, branch, sha)
Failing job logs: /tmp/failure/failed-jobs.log

Method:
1. Read the logs; identify the first real failure (not cascades).
2. Locate the failing code/test/model. Read its CO-LOCATED specs:
the component's specs/PRD.md, specs/DESIGN.md, specs/ADR/*,
specs/schemas/* (see docs/DOCS_MAP.md and
cypilot/config/artifacts.toml for the code↔spec mapping).
3. Determine: is the code violating the spec, is the test wrong,
or is the spec stale? Cite the exact PRD/DESIGN section.
4. Identify suspect files/commits (git log on the touched paths).

Then post ONE comment on the PR for the branch named in
/tmp/failure/meta.txt (use `gh pr comment`) containing: ① root-cause
hypothesis with confidence, ② the spec section it traces to,
③ suspect files with line refs, ④ a concrete proposed fix or
failing-test reproduction, ⑤ what to check if the hypothesis is
wrong. Be brief and factual. If no open PR exists for the branch,
open an issue titled "CI failure investigation" instead.
# Public-repo hardening: NO blanket Bash. The agent gets read/analysis
# tools plus only the exact git/gh commands it needs to inspect history
# and post its findings — so a prompt-injection in the failing logs
# can't run an arbitrary command (e.g. curl out ANTHROPIC_API_KEY).
# Layered with CLAUDE_CODE_SUBPROCESS_ENV_SCRUB + the same-repo gate.
claude_args: "--allowedTools Read,Grep,Glob,Bash(git log:*),Bash(gh pr list:*),Bash(gh pr view:*),Bash(gh pr comment:*),Bash(gh issue create:*),Bash(gh issue list:*) --max-turns 30"
Loading