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
57 changes: 57 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: CI

on:
pull_request:
push:
branches: [main]

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
name: test
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v6
with:
enable-cache: true

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Install dependencies
# Unit tests mock engines and do not need Apple Silicon ML extras.
run: uv sync --extra dev

- name: Run unit tests with coverage
run: >
uv run pytest
--cov=podtx
--cov-branch
--cov-report=term-missing
--cov-report=xml:coverage.xml
--cov-report=json:coverage.json

- name: Enforce statement coverage ratchet
env:
# Repo Actions variable (Settings → Secrets and variables → Actions → Variables).
# Falls back to 65 in the script if unset.
COVERAGE_RATCHET_MIN: ${{ vars.COVERAGE_RATCHET_MIN }}
run: uv run python scripts/check_coverage_ratchet.py

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
files: coverage.xml
flags: unittests
name: unit
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,10 @@ PLAN.md

# Local example / e2e artifacts
examples/

# Coverage artifacts
.coverage
.coverage.*
coverage.xml
coverage.json
htmlcov/
17 changes: 17 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,20 @@ Please use an [issue template](https://github.com/frarredondo/podtx-cli/issues/n
uv sync --extra all --extra dev
uv run pytest
```

### CI

Pull requests and pushes to `main` run the **`test`** GitHub Actions job (`.github/workflows/ci.yml`):

1. `uv sync --extra dev`
2. `uv run pytest` with line + branch coverage (`pytest-cov`)
3. Statement-coverage ratchet (`scripts/check_coverage_ratchet.py`), floor from repo Actions variable **`COVERAGE_RATCHET_MIN`** (default **65** if unset)
4. Upload `coverage.xml` to Codecov (badge in README)

ML extras (`parakeet` / `whisper`) are not installed in CI; unit tests do not require them. Branch coverage is reported to Codecov but is not gated yet.

Raise the ratchet over time by updating **`COVERAGE_RATCHET_MIN`** under *Settings → Secrets and variables → Actions → Variables* (no code change required). Locally: `COVERAGE_RATCHET_MIN=65 uv run python scripts/check_coverage_ratchet.py`.

Repository secret `CODECOV_TOKEN` is required for Codecov uploads on protected branches.

Once the workflow has run at least once on `main`, you can mark **`test`** as a required status check on the Protect main ruleset.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# podtx-cli

[![CI](https://github.com/frarredondo/podtx-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/frarredondo/podtx-cli/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/frarredondo/podtx-cli/graph/badge.svg)](https://codecov.io/gh/frarredondo/podtx-cli)

CLI (`podtx`) to pull podcast episodes from an RSS feed and transcribe them locally on Apple Silicon.

Default engine: **NVIDIA Parakeet TDT v3** via [`parakeet-mlx`](https://github.com/senstella/parakeet-mlx).
Expand Down Expand Up @@ -123,4 +126,6 @@ uv sync --extra all --extra dev
uv run pytest
```

CI runs the unit suite with coverage on pull requests via the GitHub Actions job **`test`** (see [CONTRIBUTING.md](CONTRIBUTING.md)). Statement coverage is ratcheted via repo variable **`COVERAGE_RATCHET_MIN`** (default 65%); branch coverage is reported but not gated yet.

See [CONTRIBUTING.md](CONTRIBUTING.md) for how to file bugs and feature requests.
19 changes: 19 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Codecov config: https://docs.codecov.com/docs/codecov-yaml
# CI enforces a statement-coverage ratchet locally; Codecov statuses are informational.

coverage:
status:
project:
default:
target: auto
threshold: 1%
informational: true
patch:
default:
target: 80%
informational: true

comment:
layout: "reach,diff,flags,files"
behavior: default
require_changes: false
13 changes: 13 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ all = [
]
dev = [
"pytest>=8.0.0",
"pytest-cov>=7.0.0",
]

[project.scripts]
Expand All @@ -41,3 +42,15 @@ module-root = "src"
[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src"]

[tool.coverage.run]
branch = true
source = ["podtx"]
relative_files = true

[tool.coverage.report]
show_missing = true
skip_covered = false
precision = 0
# Line/statement ratchet is enforced by scripts/check_coverage_ratchet.py
# (branch coverage is measured and uploaded, but not gated yet).
69 changes: 69 additions & 0 deletions scripts/check_coverage_ratchet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""Fail if statement (line) coverage drops below the ratchet floor.

Branch coverage is measured in CI and uploaded to Codecov, but is not gated
here yet.

The floor comes from env ``COVERAGE_RATCHET_MIN`` (GitHub Actions repo variable
in CI). Default is 65 to match the 2026-08 unit-suite baseline without ML extras.
"""

from __future__ import annotations

import json
import os
import sys
from pathlib import Path

_DEFAULT_RATCHET_MIN = 65.0


def _ratchet_min() -> float:
raw = os.environ.get("COVERAGE_RATCHET_MIN", "").strip()
if not raw:
return _DEFAULT_RATCHET_MIN
try:
return float(raw)
except ValueError:
print(
f"error: COVERAGE_RATCHET_MIN must be a number, got {raw!r}",
file=sys.stderr,
)
raise SystemExit(2) from None


def main() -> int:
floor = _ratchet_min()
report = Path("coverage.json")
if not report.is_file():
print(
f"error: missing {report}; run pytest with --cov-report=json:coverage.json",
file=sys.stderr,
)
return 2

data = json.loads(report.read_text(encoding="utf-8"))
totals = data["totals"]
statements = float(totals["percent_statements_covered"])
branches = float(totals.get("percent_branches_covered") or 0.0)
combined = float(totals.get("percent_covered") or 0.0)

print(
f"coverage ratchet: statements={statements:.2f}% "
f"(min {floor:.0f}%) | "
f"branches={branches:.2f}% (informational) | "
f"combined={combined:.2f}% (informational)"
)

if statements < floor:
print(
f"error: statement coverage {statements:.2f}% is below ratchet "
f"{floor:.0f}%",
file=sys.stderr,
)
return 1
return 0


if __name__ == "__main__":
raise SystemExit(main())
8 changes: 6 additions & 2 deletions tests/test_format_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,12 @@ def test_cli_format_feed(tmp_path: Path) -> None:
],
)
assert result.exit_code == 0, result.stdout + result.stderr
assert "feed-a-ep" in result.stdout
body = (root / "feed-a" / "feed-a-ep.txt").read_text(encoding="utf-8")
# Don't assert basename substrings in stdout: Rich may soft-wrap long paths.
assert "1 ok" in result.stdout
assert "0 failed" in result.stdout
out_txt = root / "feed-a" / "feed-a-ep.txt"
assert out_txt.is_file()
body = out_txt.read_text(encoding="utf-8")
assert "the the" not in body.lower()


Expand Down
Loading
Loading