Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,9 @@ Every run reports what it resolved, so the answer never has to be inferred from
- a job-summary table naming the tier and provider;
- on a configuration failure, an error annotation and — in review mode — a pull request
comment with the fix, so the person who has to add the secret sees it where they are.
- when the provider rejects the key or its quota runs out during the analysis, the run fails
with an error annotation saying which, and in review mode the failure comment says it too.
No partial analysis is published.

## Model selection

Expand Down
2 changes: 2 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -668,4 +668,6 @@ runs:
message: |
### CodeBoarding review · failed

${{ steps.review_analyze.outputs.failure_reason }}

See the [workflow logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}).
13 changes: 13 additions & 0 deletions scripts/analyze_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,20 @@

import argparse
import json
import os
import shutil
import subprocess
import sys
from pathlib import Path

PROG = "codeboarding"

# The engine's exit codes for an LLM refusal the user has to fix, and what to tell them.
ENGINE_REFUSALS = {
2: "The LLM provider rejected the API key. Check the key's secret and re-run.",
3: "The LLM provider's token or credit quota is exhausted. Add credits or raise the quota, then re-run.",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bump the engine before advertising quota failures

action.yml:362 still installs codeboarding==0.14.4, which does not emit the newly handled exit code 3; the corresponding engine change requires a later release. Consequently, quota exhaustion can retain the old behavior—including publishing partial analysis—while this commit's README promises an actionable failure and no partial output. Ship the supporting engine release and update both the pin and mirrored provider table with this change.

AGENTS.md reference: AGENTS.md:L12-L16

Useful? React with 👍 / 👎.

Comment on lines +18 to +19

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Tailor refusal advice for hosted tiers

When llm: hosted or llm: license is selected, neither message describes a fix the caller controls: these modes use the constant placeholder key from configure-auth.sh and authenticate through the OIDC relay, which forwards upstream HTTP failures unchanged. A rejected OIDC/license credential can therefore be reported as a provider API-key secret to check, while a hosted-quota rejection tells the caller to add provider credits they do not own. Pass the resolved tier into this mapping and give hosted/license users the corresponding OIDC, licence, or plan remedy.

Useful? React with 👍 / 👎.

}


class AnalysisError(RuntimeError):
pass
Expand Down Expand Up @@ -84,6 +91,12 @@ def _run_command(args: list[str], output_dir: Path) -> str:
return_code = process.wait()
stdout = "".join(stdout_lines)
if return_code != 0:
reason = ENGINE_REFUSALS.get(return_code)
if reason:
# Written straight to the step's outputs: the shell never sees this script's stdout on failure.
with open(os.environ.get("GITHUB_OUTPUT", os.devnull), "a", encoding="utf-8") as outputs:
outputs.write(f"failure_reason={reason}\n")
raise AnalysisError(reason)
details = stdout.strip() or f"exit code {return_code}; see command logs above"
raise AnalysisError(f"Command failed ({' '.join(args)}): {details}")

Expand Down
31 changes: 31 additions & 0 deletions tests/test_analyze_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,37 @@ def test_run_command_streams_stdout_to_action_logs(self) -> None:
self.assertIn('{"requiresFullAnalysis": true}', stderr.getvalue())
self.assertEqual(stdout, 'Analyzing repository...\n{"requiresFullAnalysis": true}\n')

def _fail_with(self, exit_code: int, outputs: Path) -> ar.AnalysisError:
command = [sys.executable, "-c", f"import sys; sys.exit({exit_code})"]
with patch.dict("os.environ", {"GITHUB_OUTPUT": str(outputs)}), patch("sys.stderr", io.StringIO()):
with self.assertRaises(ar.AnalysisError) as caught:
ar._run_command(command, outputs.parent / "out")
return caught.exception

def test_run_command_names_an_exhausted_quota(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
outputs = Path(tmp) / "github_output"

error = self._fail_with(3, outputs)

self.assertIn("quota is exhausted", str(error))
self.assertEqual(outputs.read_text(encoding="utf-8"), f"failure_reason={error}\n")

def test_run_command_names_a_rejected_key(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
error = self._fail_with(2, Path(tmp) / "github_output")

self.assertIn("rejected the API key", str(error))

def test_run_command_leaves_other_failures_unnamed(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
outputs = Path(tmp) / "github_output"

error = self._fail_with(1, outputs)

self.assertIn("exit code 1", str(error))
self.assertFalse(outputs.exists())

def test_parse_main_incremental_success(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
Expand Down
Loading