diff --git a/CHANGELOG.md b/CHANGELOG.md index 31244ab..1a27688 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ This project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Repository audit engine with 13 weighted checks. - Text, JSON, and GitHub-flavored Markdown renderers. - Configurable minimum score and stable exit codes. +- Explicit UTF-8 report files through `--output PATH`. - Composite GitHub Action. - CI matrix and tag-driven GitHub Release workflow. diff --git a/README.md b/README.md index 4b4552b..cb96230 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,15 @@ Write a pull request or GitHub Actions job summary: repoready . --format markdown >> "$GITHUB_STEP_SUMMARY" ``` +Write output directly without shell redirection: + +```bash +repoready . --format json --output reports/repoready.json +``` + +RepoReady does not create missing parent directories. Write failures return exit +code `2` with the destination path and operating-system error. + Exit codes: | Code | Meaning | diff --git a/action.yml b/action.yml index 60825aa..031e933 100644 --- a/action.yml +++ b/action.yml @@ -19,6 +19,10 @@ inputs: description: Output format: text, json, or markdown required: false default: text + output: + description: Destination path, or - for stdout + required: false + default: "-" runs: using: composite @@ -30,8 +34,10 @@ runs: REPOREADY_INPUT_PATH: ${{ inputs.path }} REPOREADY_INPUT_SCORE: ${{ inputs.min-score }} REPOREADY_INPUT_FORMAT: ${{ inputs.format }} + REPOREADY_INPUT_OUTPUT: ${{ inputs.output }} run: | export PYTHONPATH="${REPOREADY_ACTION_PATH}/src${PYTHONPATH:+:${PYTHONPATH}}" python -m repoready "${REPOREADY_INPUT_PATH}" \ --min-score "${REPOREADY_INPUT_SCORE}" \ - --format "${REPOREADY_INPUT_FORMAT}" + --format "${REPOREADY_INPUT_FORMAT}" \ + --output "${REPOREADY_INPUT_OUTPUT}" diff --git a/src/repoready/cli.py b/src/repoready/cli.py index 350b1b5..8a8509e 100644 --- a/src/repoready/cli.py +++ b/src/repoready/cli.py @@ -5,6 +5,7 @@ import argparse import sys from collections.abc import Sequence +from pathlib import Path from . import __version__ from .checks import audit_repository @@ -44,6 +45,12 @@ def build_parser() -> argparse.ArgumentParser: default=80, help="minimum score required for exit code 0 (default: 80)", ) + parser.add_argument( + "--output", + default="-", + metavar="PATH", + help="write report to PATH instead of stdout; use - for stdout (default: -)", + ) parser.add_argument( "--version", action="version", @@ -67,7 +74,16 @@ def main(argv: Sequence[str] | None = None) -> int: "text": render_text, } renderer = renderers[args.format] - print(renderer(report)) + output = renderer(report) + if args.output == "-": + print(output) + else: + destination = Path(args.output).expanduser() + try: + destination.write_text(f"{output}\n", encoding="utf-8") + except OSError as error: + parser.error(f"Could not write report to {destination}: {error}") + return 0 if report.passes_threshold else 1 diff --git a/tests/test_cli.py b/tests/test_cli.py index ae318d2..cd7a878 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -49,6 +49,51 @@ def test_markdown_format(self) -> None: self.assertEqual(exit_code, 0) self.assertIn("| Status | Check | Points | Detail |", stdout.getvalue()) + def test_writes_selected_format_to_output_file(self) -> None: + with tempfile.TemporaryDirectory() as directory: + destination = Path(directory) / "reports" / "repoready.md" + destination.parent.mkdir() + stdout = io.StringIO() + with contextlib.redirect_stdout(stdout): + exit_code = main( + [ + directory, + "--format", + "markdown", + "--min-score", + "0", + "--output", + str(destination), + ] + ) + + output = destination.read_text(encoding="utf-8") + + self.assertEqual(exit_code, 0) + self.assertEqual(stdout.getvalue(), "") + self.assertTrue(output.startswith("## RepoReady:")) + self.assertTrue(output.endswith("\n")) + self.assertFalse(output.endswith("\n\n")) + + def test_output_file_requires_existing_parent(self) -> None: + with tempfile.TemporaryDirectory() as directory: + destination = Path(directory) / "missing" / "report.json" + stderr = io.StringIO() + with contextlib.redirect_stderr(stderr): + with self.assertRaises(SystemExit) as raised: + main( + [ + directory, + "--format", + "json", + "--output", + str(destination), + ] + ) + + self.assertEqual(raised.exception.code, 2) + self.assertIn("Could not write report", stderr.getvalue()) + if __name__ == "__main__": unittest.main()