This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
# Hardening CLI And Sync Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Make Gitea mirror fail reliably on API/git/config/path errors and expose a proper package CLI entrypoint.
|
||||
|
||||
**Architecture:** Keep the existing module layout, but move executable orchestration into an importable `src.cli` module. Preserve `gitea-mirror.py` as a compatibility wrapper while making `process_repo` return success/failure for aggregate exit handling.
|
||||
|
||||
**Tech Stack:** Python 3.12, requests, pydantic v2, pytest, ruff, uv.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: API Errors Are Fatal
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/gitea_api.py`
|
||||
- Test: `tests/test_gitea_api.py`
|
||||
|
||||
- [ ] **Step 1: Write failing tests**
|
||||
|
||||
```python
|
||||
def test_get_repositories_raises_on_http_error(monkeypatch):
|
||||
api = GiteaApi(endpoint="https://example.com", api_token="token")
|
||||
session = FakeSession([FakeResponse(status_code=500, data=[])])
|
||||
monkeypatch.setattr(requests, "session", lambda: session)
|
||||
|
||||
with pytest.raises(GiteaApiError):
|
||||
api.get_repositories()
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `uv run pytest tests/test_gitea_api.py -v`
|
||||
Expected: FAIL because `GiteaApiError` does not exist or is not raised.
|
||||
|
||||
- [ ] **Step 3: Implement minimal code**
|
||||
|
||||
Add `GiteaApiError`, request timeout, network exception handling, JSON/schema exception handling, and raise instead of returning `[]`.
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `uv run pytest tests/test_gitea_api.py -v`
|
||||
Expected: PASS.
|
||||
|
||||
### Task 2: Git Failures Affect Exit Code
|
||||
|
||||
**Files:**
|
||||
- Create: `src/cli.py`
|
||||
- Modify: `gitea-mirror.py`
|
||||
- Test: `tests/test_mirror_process.py`, `tests/test_cli.py`
|
||||
|
||||
- [ ] **Step 1: Write failing tests**
|
||||
|
||||
```python
|
||||
def test_process_repo_returns_false_when_clone_fails(monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(cli, "git_clone", lambda **kwargs: False)
|
||||
assert not cli.process_repo(config=make_config(tmp_path, FetchConfig(ssh_key="/tmp/key")), repo=make_repo())
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `uv run pytest tests/test_mirror_process.py tests/test_cli.py -v`
|
||||
Expected: FAIL because `process_repo` currently returns `None` and no importable CLI module exists.
|
||||
|
||||
- [ ] **Step 3: Implement minimal code**
|
||||
|
||||
Move orchestration to `src.cli`, return booleans from `process_repo`, aggregate failures in `run`, and make `main` return non-zero for failures.
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `uv run pytest tests/test_mirror_process.py tests/test_cli.py -v`
|
||||
Expected: PASS.
|
||||
|
||||
### Task 3: Validate Repository Paths And Config
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/models.py`
|
||||
- Modify: `src/repository_name.py`
|
||||
- Test: `tests/test_config.py`, `tests/test_repository_name.py`
|
||||
|
||||
- [ ] **Step 1: Write failing tests**
|
||||
|
||||
```python
|
||||
def test_output_path_rejects_parent_traversal(tmp_path):
|
||||
repo = make_repo()
|
||||
with pytest.raises(ValueError):
|
||||
get_repository_path(str(tmp_path), "../{name}", repo)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `uv run pytest tests/test_config.py tests/test_repository_name.py -v`
|
||||
Expected: FAIL because validation is missing.
|
||||
|
||||
- [ ] **Step 3: Implement minimal code**
|
||||
|
||||
Add repository format validation in `Config`, log level validation in `LoggingConfig`, and a safe output path helper that rejects absolute paths and paths escaping `out_dir`.
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `uv run pytest tests/test_config.py tests/test_repository_name.py -v`
|
||||
Expected: PASS.
|
||||
|
||||
### Task 4: Package CLI Entrypoint
|
||||
|
||||
**Files:**
|
||||
- Modify: `pyproject.toml`
|
||||
- Modify: `gitea-mirror.py`
|
||||
- Test: `tests/test_cli.py`
|
||||
|
||||
- [ ] **Step 1: Write failing test**
|
||||
|
||||
```python
|
||||
def test_project_defines_console_script():
|
||||
data = tomllib.loads(Path("pyproject.toml").read_text())
|
||||
assert data["project"]["scripts"]["gitea-mirror"] == "src.cli:main"
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `uv run pytest tests/test_cli.py -v`
|
||||
Expected: FAIL because `[project.scripts]` is absent.
|
||||
|
||||
- [ ] **Step 3: Implement minimal code**
|
||||
|
||||
Add `[project.scripts] gitea-mirror = "src.cli:main"` and make `gitea-mirror.py` delegate to `src.cli.main`.
|
||||
|
||||
- [ ] **Step 4: Run full verification**
|
||||
|
||||
Run: `make check`
|
||||
Expected: PASS.
|
||||
Reference in New Issue
Block a user