diff --git a/.github/workflows/ci-failure-analyst-reusable.yml b/.github/workflows/ci-failure-analyst-reusable.yml index f19ec7768..bd45751c2 100644 --- a/.github/workflows/ci-failure-analyst-reusable.yml +++ b/.github/workflows/ci-failure-analyst-reusable.yml @@ -146,8 +146,18 @@ jobs: TMPL PROMPT=$(envsubst < /tmp/prompt-template.txt) + _analyst_out=$(mktemp) + _analyst_rc=0 claude --print \ --permission-mode bypassPermissions \ --allowedTools Bash \ --fallback-model opus \ - -p "$PROMPT" + -p "$PROMPT" 2>&1 | tee "$_analyst_out" || _analyst_rc=${PIPESTATUS[0]} + if [ "$_analyst_rc" -ne 0 ] && \ + grep -qiE "hit your.*(session )?limit|rate.?limit|session limit|usage limit|quota exceeded|too many requests" "$_analyst_out"; then + echo "::notice::Claude rate-limited — skipping CI analysis for PR #${PR_NUMBER} (will retry on next CI failure)" + rm -f "$_analyst_out" + exit 0 + fi + rm -f "$_analyst_out" + exit "$_analyst_rc" diff --git a/.github/workflows/issue-triage-runner.yml b/.github/workflows/issue-triage-runner.yml index f0a96c9a3..89a9eb031 100644 --- a/.github/workflows/issue-triage-runner.yml +++ b/.github/workflows/issue-triage-runner.yml @@ -24,9 +24,16 @@ jobs: persist-credentials: false - name: Install claude CLI - uses: anthropics/claude-code-action/setup@787c5a0ce96a9a6cfb050ea0c8f4c05f2447c251 # main - with: - claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + 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" + export PATH="$HOME/.npm-global/bin:$PATH" + if ! command -v claude >/dev/null 2>&1; then + npm install -g "@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}" + fi - name: Install Python dependencies run: | @@ -35,6 +42,7 @@ jobs: - name: Run issue-triage (staged) id: run env: + CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} ISSUE_NUMBER: ${{ github.event.issue.number }} ISSUE_TITLE: ${{ github.event.issue.title }} ISSUE_BODY: ${{ github.event.issue.body }} diff --git a/scripts/engine.sh b/scripts/engine.sh index 72404cdbf..f35709bca 100644 --- a/scripts/engine.sh +++ b/scripts/engine.sh @@ -976,6 +976,10 @@ run_writer_with_fallback() { echo "::warning::Skipping copilot fallback: classic PAT in COPILOT_GITHUB_TOKEN is unsupported" >&2 continue fi + if [ "$engine" = "gemini" ] && [ -z "${GEMINI_API_KEY:-}${GOOGLE_API_KEY:-}" ]; then + echo "::warning::Skipping gemini fallback: GEMINI_API_KEY or GOOGLE_API_KEY not configured" >&2 + continue + fi local saved="$REVIEW_ENGINE" export REVIEW_ENGINE="$engine" diff --git a/tests/dev-lead/unit/test_engine_fallback.bats b/tests/dev-lead/unit/test_engine_fallback.bats index 7d3edea8f..4bd1badda 100644 --- a/tests/dev-lead/unit/test_engine_fallback.bats +++ b/tests/dev-lead/unit/test_engine_fallback.bats @@ -22,6 +22,7 @@ setup() { export TEST_PROMPT export DEV_LEAD_DRY_RUN="false" + export GEMINI_API_KEY="dummy-test-key" } teardown() { @@ -196,3 +197,43 @@ GHEOF [ -s "$record_file" ] rm -f "$record_file" } + +@test "fallback: gemini skipped when no auth env vars set → falls through to copilot" { + # claude exits 2 (rate-limited), no GEMINI_API_KEY/GOOGLE_API_KEY set. + # gemini should be skipped entirely (not called); copilot should succeed. + _make_stub "claude" 2 + local gemini_record + gemini_record="$(mktemp)" + _make_recording_stub "gemini" 1 "$gemini_record" + unset GEMINI_API_KEY GOOGLE_API_KEY 2>/dev/null || true + export COPILOT_GITHUB_TOKEN="stub-token" + cat > "$STUB_BIN_DIR/gh" <<'GHEOF' +#!/usr/bin/env bash +case "$*" in + *"copilot"*) echo "success output"; exit 0 ;; + *) echo "{}" ;; +esac +GHEOF + chmod +x "$STUB_BIN_DIR/gh" + _source_engine "claude" + + run run_writer_with_fallback "$TEST_PROMPT" + + [ "$status" -eq 0 ] + # gemini must NOT have been called (file should be empty) + [ ! -s "$gemini_record" ] + rm -f "$gemini_record" +} + +@test "fallback: gemini used normally when GEMINI_API_KEY is set" { + # claude exits 2 (rate-limited), GEMINI_API_KEY is set → gemini should be tried and succeed + _make_stub "claude" 2 + _make_stub "gemini" 0 + export GEMINI_API_KEY="test-api-key" + _source_engine "claude" + + run run_writer_with_fallback "$TEST_PROMPT" + + [ "$status" -eq 0 ] + unset GEMINI_API_KEY +} diff --git a/tests/dev-lead/unit/test_engine_writer.bats b/tests/dev-lead/unit/test_engine_writer.bats index e3e281bda..dad42fd82 100644 --- a/tests/dev-lead/unit/test_engine_writer.bats +++ b/tests/dev-lead/unit/test_engine_writer.bats @@ -19,6 +19,7 @@ setup() { echo "test prompt content" > "$TEST_PROMPT" export TEST_PROMPT export STUB_BIN_DIR + export GEMINI_API_KEY="dummy-test-key" } teardown() {