-
Notifications
You must be signed in to change notification settings - Fork 2
refactor(action): simplify v2 flow to CLI incremental/full contract #69
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
Open
ivanmilevtues
wants to merge
17
commits into
main
Choose a base branch
from
action-v2-contract-only-simplification
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
f61339f
feat(action): simplify action flow to incremental/full CLI contract
ivanmilevtues d59229f
fix(action): restore manifest and CI checks
ivanmilevtues 4bc6cf3
fix(ci): align dogfood workflows with action inputs
ivanmilevtues 220cc37
fix(lint): make local runner shellcheck clean
ivanmilevtues de457c0
fix(action): restore review authentication compatibility
ivanmilevtues e13086e
fix(action): run engine on supported Python
ivanmilevtues 060e7e3
fix(action): honor engine fallback contract
ivanmilevtues 6397805
fix(action): surface engine contract errors
ivanmilevtues a7259e0
fix(action): parse logged engine responses
ivanmilevtues 96de6a7
fix(action): normalize direct LLM keys
ivanmilevtues 812ed52
fix(review): retain baseline through rendering
ivanmilevtues bde08fa
fix(review): read boolean render metadata
ivanmilevtues fa74487
fix(review): post rendered diagram content
ivanmilevtues 463ab43
fix(action): stream CLI progress logs
ivanmilevtues 07de887
fix(review): restore slash command feedback
ivanmilevtues b6d04c3
fix(review): refine comment presentation
ivanmilevtues 3024fdb
fix(sync): preserve user CodeBoarding config
ivanmilevtues File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,170 @@ | ||
| #!/usr/bin/env python3 | ||
| """Thin helper to execute CodeBoarding CLI incremental/full commands. | ||
|
|
||
| The action is intentionally logic-light: all analysis orchestration happens in | ||
| shell through this script's small JSON contract parser, which only invokes | ||
| CodeBoarding's own ``incremental`` and ``full`` commands. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import argparse | ||
| import json | ||
| import subprocess | ||
| import sys | ||
| from pathlib import Path | ||
|
|
||
| PROG = "codeboarding" | ||
|
|
||
|
|
||
| class AnalysisError(RuntimeError): | ||
| pass | ||
|
|
||
|
|
||
| def _parse_bool(value: object, *, field: str) -> bool: | ||
| if isinstance(value, bool): | ||
| return value | ||
| if isinstance(value, str): | ||
| lowered = value.strip().lower() | ||
| if lowered in {"true", "1", "yes", "y"}: | ||
| return True | ||
| if lowered in {"false", "0", "no", "n"}: | ||
| return False | ||
| raise AnalysisError(f"Invalid contract field '{field}': {value!r}") | ||
|
|
||
|
|
||
| def _normalize_analysis_path(payload: dict, output_dir: str) -> Path: | ||
| path = payload.get("analysis_path") | ||
| if not isinstance(path, str) or not path.strip(): | ||
| raise AnalysisError("Missing or empty 'analysis_path' in CLI response") | ||
|
|
||
| candidate = Path(path) | ||
| if not candidate.is_absolute(): | ||
| candidate = Path(output_dir) / candidate | ||
| return candidate | ||
|
|
||
|
|
||
| def _parse_cli_response(raw: str, output_dir: str) -> tuple[bool, Path | None, dict]: | ||
| if not raw.strip(): | ||
| raise AnalysisError("CodeBoarding command produced no JSON output") | ||
|
|
||
| try: | ||
| payload = json.loads(raw) | ||
| except json.JSONDecodeError as exc: | ||
| payload = None | ||
| lines = raw.splitlines() | ||
| for index, line in enumerate(lines): | ||
| if line.lstrip().startswith("{"): | ||
| try: | ||
| payload = json.loads("\n".join(lines[index:])) | ||
| except json.JSONDecodeError: | ||
| continue | ||
| if payload is None: | ||
| raise AnalysisError(f"Invalid CodeBoarding JSON response: {exc}") from exc | ||
|
|
||
| if not isinstance(payload, dict): | ||
| raise AnalysisError("CodeBoarding JSON response is not an object") | ||
|
|
||
| requires_full = _parse_bool(payload.get("requiresFullAnalysis"), field="requiresFullAnalysis") | ||
| if requires_full and not payload.get("analysis_path"): | ||
| return True, None, payload | ||
|
|
||
| analysis_path = _normalize_analysis_path(payload, output_dir) | ||
|
|
||
| if not analysis_path.is_file(): | ||
| raise AnalysisError(f"analysis_path points to a non-file: {analysis_path}") | ||
|
|
||
| return requires_full, analysis_path, payload | ||
|
|
||
|
|
||
| def _run_command(args: list[str], output_dir: Path) -> str: | ||
| process = subprocess.Popen( | ||
| args, | ||
| stdout=subprocess.PIPE, | ||
| text=True, | ||
| bufsize=1, | ||
| cwd=str(output_dir.parent), | ||
| env=None, | ||
| ) | ||
| if process.stdout is None: # pragma: no cover - guaranteed by stdout=PIPE | ||
| raise AnalysisError(f"Unable to read command output ({' '.join(args)})") | ||
|
|
||
| stdout_lines: list[str] = [] | ||
| for line in process.stdout: | ||
| stdout_lines.append(line) | ||
| # The shell captures this helper's stdout as its result contract. Mirror | ||
| # CLI stdout to stderr so engine progress remains visible in Actions. | ||
| print(line, end="", file=sys.stderr, flush=True) | ||
|
|
||
| return_code = process.wait() | ||
| stdout = "".join(stdout_lines) | ||
| if return_code != 0: | ||
| details = stdout.strip() or f"exit code {return_code}; see command logs above" | ||
| raise AnalysisError(f"Command failed ({' '.join(args)}): {details}") | ||
|
|
||
| return stdout | ||
|
|
||
|
|
||
| def run_incremental(checkout: Path, output_dir: Path) -> tuple[bool, Path | None, dict]: | ||
| output_dir.mkdir(parents=True, exist_ok=True) | ||
| raw = _run_command([PROG, "incremental", "--local", str(checkout), "--output-dir", str(output_dir)], output_dir) | ||
| return _parse_cli_response(raw, str(output_dir)) | ||
|
|
||
|
|
||
| def run_full(checkout: Path, output_dir: Path, depth_level: str) -> Path: | ||
| output_dir.mkdir(parents=True, exist_ok=True) | ||
| _run_command( | ||
| [ | ||
| PROG, | ||
| "full", | ||
| "--local", | ||
| str(checkout), | ||
| "--output-dir", | ||
| str(output_dir), | ||
| "--depth-level", | ||
| str(depth_level), | ||
| "--force", | ||
| ], | ||
| output_dir, | ||
| ) | ||
| analysis_path = output_dir / "analysis.json" | ||
| if not analysis_path.is_file(): | ||
| raise AnalysisError(f"Full analysis did not produce: {analysis_path}") | ||
| return analysis_path | ||
|
|
||
|
|
||
| def main(argv: list[str] | None = None) -> int: | ||
| parser = argparse.ArgumentParser(description=__doc__) | ||
| parser.add_argument("mode", choices=["incremental", "full"], help="Which CLI command to invoke") | ||
| parser.add_argument("--checkout", required=True, help="Path to repository checkout") | ||
| parser.add_argument("--output-dir", required=True, help="Action-owned output directory") | ||
| parser.add_argument("--depth-level", help="Depth passed to full analyses") | ||
|
|
||
| args = parser.parse_args(argv) | ||
| checkout = Path(args.checkout) | ||
| output_dir = Path(args.output_dir) | ||
| if not checkout.is_dir(): | ||
| raise SystemExit(f"Missing checkout directory: {checkout}") | ||
|
|
||
| if args.mode == "incremental": | ||
| requires_full, analysis_path, _ = run_incremental(checkout, output_dir) | ||
| print(f"analysis_mode=incremental") | ||
| print(f"requires_full_analysis={str(requires_full).lower()}") | ||
| print(f"analysis_path={analysis_path or ''}") | ||
| return 0 | ||
|
|
||
| if not args.depth_level: | ||
| raise SystemExit("--depth-level is required for mode=full") | ||
| analysis_path = run_full(checkout, output_dir, args.depth_level) | ||
| print(f"analysis_mode=full") | ||
| print("requires_full_analysis=false") | ||
| print(f"analysis_path={analysis_path}") | ||
| return 0 | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| try: | ||
| raise SystemExit(main()) | ||
| except AnalysisError as exc: | ||
| print(f"::error::{exc}", file=sys.stderr) | ||
| raise SystemExit(1) | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
When the CLI returns a relative path such as
out/analysis.json, it is relative to the subprocess working directory (output_dir.parent), but this code prefixesoutput_dirand looks forout/out/analysis.json. The newly added success test exercises exactly this contract and fails for that reason; resolve relative paths against the same working directory used by_run_commandso valid CLI output does not abort analysis.Useful? React with 👍 / 👎.