-
Notifications
You must be signed in to change notification settings - Fork 2
Report analysis diagnostics, and fail with a specific comment when the LLM quota runs out #124
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
280255a
6c66dd8
a58c63a
d2357b0
89c2343
5d1b135
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -498,6 +498,19 @@ runs: | |
| retention-days: 30 | ||
| if-no-files-found: ignore | ||
|
|
||
| # An analysis that finished is not an analysis that finished cleanly. The engine | ||
| # records every degradation it survived; without this the run is green and the | ||
| # committed diagram is short of a language with nothing to say so. | ||
| - name: Read analysis diagnostics | ||
| id: sync_diagnostics | ||
| if: always() && steps.guard.outputs.skip != 'true' && steps.guard.outputs.mode == 'sync' && steps.sync_analyze.outputs.analysis_path != '' | ||
| continue-on-error: true | ||
| shell: bash | ||
| env: | ||
| ANALYSIS_PATH: ${{ steps.sync_analyze.outputs.analysis_path }} | ||
| DIAGNOSTICS_OUT: ${{ runner.temp }}/codeboarding-diagnostics.md | ||
| run: 'python3 "$GITHUB_ACTION_PATH/scripts/analysis_diagnostics.py" --analysis "$ANALYSIS_PATH" --out "$DIAGNOSTICS_OUT"' | ||
|
|
||
| - name: Write sync summary | ||
| if: always() && steps.guard.outputs.skip != 'true' && steps.guard.outputs.mode == 'sync' | ||
| shell: bash | ||
|
|
@@ -507,6 +520,7 @@ runs: | |
| FILES: ${{ steps.sync_commit.outputs.files_written }} | ||
| STRATEGY: ${{ inputs.sync_strategy }} | ||
| PR_URL: ${{ steps.sync_commit.outputs.sync_pr_url }} | ||
| DIAGNOSTICS_MD: ${{ steps.sync_diagnostics.outputs.markdown_path }} | ||
| run: "$GITHUB_ACTION_PATH/scripts/action/sync-summary.sh" | ||
|
|
||
| - name: Analyze pull request | ||
|
|
@@ -568,6 +582,16 @@ runs: | |
| retention-days: 30 | ||
| if-no-files-found: ignore | ||
|
|
||
| - name: Read analysis diagnostics | ||
| id: review_diagnostics | ||
| if: steps.guard.outputs.skip != 'true' && steps.guard.outputs.mode == 'review' && steps.review_analyze.outputs.analysis_path != '' | ||
| continue-on-error: true | ||
| shell: bash | ||
| env: | ||
| ANALYSIS_PATH: ${{ steps.review_analyze.outputs.analysis_path }} | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the merge-base analysis is degraded but the subsequent head analysis completes cleanly, the rendered review still compares against an incomplete base and can report structures missing from that base as PR additions, yet this step reads only the head analysis path. Because Useful? React with 👍 / 👎. |
||
| DIAGNOSTICS_OUT: ${{ runner.temp }}/codeboarding-diagnostics.md | ||
| run: 'python3 "$GITHUB_ACTION_PATH/scripts/analysis_diagnostics.py" --analysis "$ANALYSIS_PATH" --out "$DIAGNOSTICS_OUT"' | ||
|
|
||
| - name: Render review diagram | ||
| id: review_render | ||
| if: steps.guard.outputs.skip != 'true' && steps.guard.outputs.mode == 'review' | ||
|
|
@@ -628,6 +652,7 @@ runs: | |
| HEAD_SHA: ${{ steps.guard.outputs.head_sha }} | ||
| # Analysed files whose content hash differs between base and head, from the render step. | ||
| ANALYSED_FILES_CHANGED: ${{ steps.review_render.outputs.analysed_files_changed }} | ||
| DIAGNOSTICS_MD: ${{ steps.review_diagnostics.outputs.markdown_path }} | ||
| run: "$GITHUB_ACTION_PATH/scripts/action/build-review-comment.sh" | ||
|
|
||
| - name: Post review comment | ||
|
|
@@ -653,12 +678,37 @@ runs: | |
| BODY: ${{ steps.review_body.outputs.path }} | ||
| run: cat "$BODY" >> "$GITHUB_STEP_SUMMARY" | ||
|
|
||
| # Skipped when the run stopped on a credential problem: that path already replaced | ||
| # this same sticky comment with the input and secret to fix, and "see the workflow | ||
| # The engine stops rather than publish a map without AI naming when the LLM quota runs | ||
| # out or its credentials are refused. The run stays failed; this says why, in the job | ||
| # summary and (below) in the pull request, instead of "see the workflow logs". | ||
| - name: Read engine failure | ||
| id: engine_failure | ||
| if: failure() && steps.guard.outputs.skip != 'true' && (steps.sync_analyze.outcome == 'failure' || steps.review_analyze.outcome == 'failure') | ||
| continue-on-error: true | ||
| shell: bash | ||
| env: | ||
| MODE: ${{ steps.guard.outputs.mode }} | ||
| LLM: ${{ inputs.llm }} | ||
| PR_NUMBER: ${{ steps.guard.outputs.pr_number }} | ||
| HEAD_SHA: ${{ steps.guard.outputs.head_sha }} | ||
| run: 'python3 "$GITHUB_ACTION_PATH/scripts/action/engine_failure.py"' | ||
|
|
||
| - name: Report engine failure | ||
| if: failure() && steps.engine_failure.outputs.reason != '' && steps.guard.outputs.mode == 'review' && steps.guard.outputs.pr_number != '' | ||
| continue-on-error: true | ||
| uses: marocchino/sticky-pull-request-comment@v2 | ||
| with: | ||
| header: ${{ steps.guard.outputs.comment_id }} | ||
| number: ${{ steps.guard.outputs.pr_number }} | ||
| GITHUB_TOKEN: ${{ inputs.github_token }} | ||
| path: ${{ steps.engine_failure.outputs.body_path }} | ||
|
|
||
| # Skipped when the run stopped on a credential problem or an engine refusal: those | ||
| # paths replaced this same sticky comment with what to fix, and "see the workflow | ||
| # logs" posted over the top of it would send the reader hunting for what they had | ||
| # just been told. | ||
| - name: Post review failure | ||
| if: failure() && steps.guard.outputs.skip != 'true' && steps.guard.outputs.mode == 'review' && steps.review_comment.outcome != 'success' && steps.llm.outputs.error == '' | ||
| if: failure() && steps.guard.outputs.skip != 'true' && steps.guard.outputs.mode == 'review' && steps.review_comment.outcome != 'success' && steps.llm.outputs.error == '' && steps.engine_failure.outputs.reason == '' | ||
| continue-on-error: true | ||
| uses: marocchino/sticky-pull-request-comment@v2 | ||
| with: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,9 @@ if [[ ! "$DEPTH_CAP" =~ ^[1-9][0-9]*$ ]]; then | |
| echo "::error::depth_cap must be a positive integer." | ||
| exit 1 | ||
| fi | ||
| # analyze_repository.py records an engine refusal here for the steps that report it; one | ||
| # left by an earlier use of the action in this job is not this run's. | ||
| rm -f "$RUNNER_TEMP/codeboarding-engine-error.json" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a job invokes the action twice and lets the first quota-aborted invocation continue, a second review with an invalid Useful? React with 👍 / 👎. |
||
| parse_output() { | ||
| local output="$1" | ||
| ANALYSIS_MODE="$(awk -F= '$1 == "analysis_mode" {print $2; exit}' <<< "$output")" | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| #!/usr/bin/env python3 | ||
| """Explain a run the engine refused to finish, in the pull request and the job summary. | ||
|
|
||
| analyze_repository.py records the engine's refusal (quota used up, credentials rejected) in | ||
| $RUNNER_TEMP/codeboarding-engine-error.json. This renders it for a person: a sticky comment | ||
| body in review mode, and the job summary in both modes. The run itself stays failed; this | ||
| only replaces "see the workflow logs" with the reason and what to change. | ||
|
|
||
| Writes `reason` (the engine's `kind`, empty when there is nothing to report) and `body_path` | ||
| to $GITHUB_OUTPUT. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import json | ||
| import os | ||
| import sys | ||
| from pathlib import Path | ||
|
|
||
| ERROR_FILE = "codeboarding-engine-error.json" | ||
| DOCS = "https://github.com/CodeBoarding/CodeBoarding-action#authentication-and-providers" | ||
| HEADINGS = { | ||
| "llm_quota_exhausted": "stopped: LLM quota used up", | ||
| "llm_auth": "stopped: LLM credentials rejected", | ||
| } | ||
| OWN_KEY = "use your own LLM key: set `llm` to your provider and pass its key input, for example `llm: anthropic` with `anthropic_api_key`" | ||
| LICENSE = "use a CodeBoarding license: `llm: license` with `license_key`" | ||
|
|
||
|
|
||
| def _quota(llm: str) -> list[str]: | ||
| lines = ["The LLM provider refused the analysis because the token quota is used up."] | ||
| if llm == "hosted": | ||
| lines += [ | ||
| "", | ||
| "On `llm: hosted` that is CodeBoarding's free tier, whose allowance is per GitHub owner per week and resets Monday 00:00 UTC. To analyze before then, either:", | ||
| "", | ||
| f"- {OWN_KEY};", | ||
| f"- or {LICENSE}.", | ||
| ] | ||
| elif llm == "license": | ||
| lines += [ | ||
| "", | ||
| f"On `llm: license` that is your CodeBoarding plan's allowance. To analyze before it renews, {OWN_KEY}.", | ||
| ] | ||
| else: | ||
| lines += [ | ||
| "", | ||
| f"On `llm: {llm}` that is the quota of your own provider account. Raise it with the provider, or {LICENSE}.", | ||
| ] | ||
| return lines | ||
|
|
||
|
|
||
| def _auth(llm: str) -> list[str]: | ||
| return [ | ||
| "The LLM provider rejected this run's credentials.", | ||
| "", | ||
| f"Check the key or license this workflow passes for `llm: {llm}`; see [Authentication and providers]({DOCS}).", | ||
| ] | ||
|
|
||
|
|
||
| def render(error: dict, env: dict[str, str]) -> str: | ||
| kind = error["kind"] | ||
| mode = env.get("MODE", "review") | ||
| # credential_check.py accepts any case, so `Hosted` must read as hosted here too. | ||
| llm = env.get("LLM", "").strip().lower() or "hosted" | ||
| lines = [f"### CodeBoarding {mode} · {HEADINGS[kind]}", ""] | ||
| lines.append("CodeBoarding stopped instead of publishing a map without AI naming.") | ||
| lines += _quota(llm) if kind == "llm_quota_exhausted" else _auth(llm) | ||
| lines.append("") | ||
| if mode == "sync": | ||
| lines.append("The baseline was not updated, and no base analysis was published.") | ||
| else: | ||
| lines.append("No diagram was posted for this run.") | ||
| server = env.get("GITHUB_SERVER_URL", "https://github.com") | ||
| repository = env.get("GITHUB_REPOSITORY", "") | ||
| run_id = env.get("GITHUB_RUN_ID", "") | ||
| lines += [ | ||
| "", | ||
| f"<sub>run [{run_id}]({server}/{repository}/actions/runs/{run_id}) · attempt {env.get('GITHUB_RUN_ATTEMPT', '1')}</sub>", | ||
| ] | ||
| if mode == "review": | ||
| # The review comment's machine-readable line, with `failure` in place of the counts a | ||
| # stopped run does not have. One line, `key=value`, no spaces in values. | ||
| platform = f"https://app.codeboarding.org/{repository}/pull/{env.get('PR_NUMBER', '')}" | ||
| lines.append(f"<!-- codeboarding: platform_url={platform} head={env.get('HEAD_SHA', '')} failure={kind} -->") | ||
| return "\n".join(lines) + "\n" | ||
|
|
||
|
|
||
| def main() -> int: | ||
| env = dict(os.environ) | ||
| runner_temp = Path(env["RUNNER_TEMP"]) | ||
| outputs = [] | ||
| try: | ||
| error = json.loads((runner_temp / ERROR_FILE).read_text(encoding="utf-8")) | ||
| except (OSError, ValueError): | ||
| error = None | ||
| if isinstance(error, dict) and error.get("kind") in HEADINGS: | ||
| body = render(error, env) | ||
| path = runner_temp / "codeboarding-engine-failure.md" | ||
| path.write_text(body, encoding="utf-8") | ||
| summary = env.get("GITHUB_STEP_SUMMARY") | ||
| if summary: | ||
| with open(summary, "a", encoding="utf-8") as handle: | ||
| handle.write(body) | ||
| outputs = [f"reason={error['kind']}", f"body_path={path}"] | ||
| with open(env["GITHUB_OUTPUT"], "a", encoding="utf-8") as handle: | ||
| handle.writelines(line + "\n" for line in outputs) | ||
| return 0 | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| sys.exit(main()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
feat:If this commit is merged or rebased with its current subject,
Report analysis diagnostics in workflow runsis not a Conventional Commit, so release-please will skip this adopter-facing feature and will not propose the release that moves thev1tag. Use afeat:subject for the commit and PR title so consumers actually receive the new workflow behavior.AGENTS.md reference: AGENTS.md:L88-L93
Useful? React with 👍 / 👎.