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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
8 changes: 7 additions & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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}"
18 changes: 17 additions & 1 deletion src/repoready/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand All @@ -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


Expand Down
45 changes: 45 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()