From 2441ba3b019ae99b94ffa3bfc1c83a140dc780a6 Mon Sep 17 00:00:00 2001 From: Marcelo Valle Date: Thu, 18 Jun 2026 13:18:28 +0100 Subject: [PATCH] ci: separate lint, test, and coverage into parallel jobs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split the single test job in ci.yml (which ran lint + install + tests sequentially) into three independent parallel jobs: - lint — only needs ruff via uvx, skips package installation (faster) - test — builds/installs the wheel and runs pytest - total-coverage — runs pytest --cov, posts Total Coverage and Patch Coverage checks to GitHub (gated on pull_request events) The Total Coverage check now enforces an 80% threshold (previously always success) and includes a per-file coverage report table in its summary, so the full report is visible directly on the PR checks without digging into job logs. Patch Coverage keeps its 80% threshold. Consolidated the standalone test-coverage.yml workflow into the total-coverage job and deleted the old file. Pre-commit behavior is unchanged locally — everything still runs together via the pytest hook. --- .github/scripts/coverage_report.py | 43 ++++++++++++++++---- .github/workflows/ci.yml | 62 ++++++++++++++++++++++++++--- .github/workflows/test-coverage.yml | 42 ------------------- 3 files changed, 93 insertions(+), 54 deletions(-) delete mode 100644 .github/workflows/test-coverage.yml diff --git a/.github/scripts/coverage_report.py b/.github/scripts/coverage_report.py index 77d8229..6e78f5b 100644 --- a/.github/scripts/coverage_report.py +++ b/.github/scripts/coverage_report.py @@ -12,6 +12,7 @@ import sys PATCH_THRESHOLD = 80 +TOTAL_THRESHOLD = 80 def get_total_coverage(coverage_json_path="coverage.json"): @@ -25,6 +26,27 @@ def get_total_coverage(coverage_json_path="coverage.json"): return (totals["covered_lines"] / num_statements) * 100 +def format_coverage_report(coverage_json_path="coverage.json"): + """Build a markdown table of per-file coverage from coverage.json.""" + with open(coverage_json_path) as f: + data = json.load(f) + + rows = [] + for file_path, file_info in sorted(data["files"].items()): + totals = file_info["summary"] + num_statements = totals["num_statements"] + if num_statements == 0: + continue + pct = (totals["covered_lines"] / num_statements) * 100 + missing = totals["missing_lines"] + rows.append(f"| {file_path} | {num_statements} | {missing} | {pct:.1f}% |") + + if not rows: + return "No tracked files." + header = "| File | Stmts | Miss | Cover |\n| --- | --- | --- | --- |" + return header + "\n" + "\n".join(rows) + + def get_changed_lines_pr(): """Return dict of {filepath: set(line_numbers)} changed in this PR. @@ -158,7 +180,7 @@ def create_check_run(name, conclusion, title, summary): def run_ci(): - """CI mode: report total and patch coverage as GitHub Check Runs.""" + """CI mode: report total coverage via the job summary and patch coverage as a Check Run.""" coverage_path = "coverage.json" if not os.path.exists(coverage_path): print("Error: coverage.json not found. Tests may have failed.") @@ -173,12 +195,13 @@ def run_ci(): f"Patch coverage: {patch_pct:.1f}% ({trackable_count} trackable lines changed)" ) - create_check_run( - name="Total Coverage", - conclusion="success", - title=f"Total Coverage: {total_pct:.1f}%", - summary=f"Overall project test coverage is **{total_pct:.1f}%**.", - ) + summary_path = os.environ.get("GITHUB_STEP_SUMMARY") + if summary_path: + with open(summary_path, "a") as f: + f.write(f"## Total Coverage: {total_pct:.1f}%\n") + f.write(f"(threshold: {TOTAL_THRESHOLD}%)\n\n") + f.write(format_coverage_report(coverage_path)) + f.write("\n") patch_conclusion = "success" if patch_pct >= PATCH_THRESHOLD else "failure" patch_summary = ( @@ -192,6 +215,12 @@ def run_ci(): summary=patch_summary, ) + if total_pct < TOTAL_THRESHOLD: + print( + f"FAIL: total coverage {total_pct:.1f}% is below {TOTAL_THRESHOLD}% threshold" + ) + sys.exit(1) + def run_precommit(): """Pre-commit mode: check patch coverage of staged changes, exit non-zero if < threshold.""" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 412e5ec..8711309 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,7 @@ on: branches: [ "main" ] jobs: - test: + lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -22,15 +22,30 @@ jobs: with: python-version: "3.12" - - name: Install dependencies - run: | - uv pip install --system hatchling pytest pre-commit - - name: Check Code Formatting run: | uvx ruff format --check --exclude boilerplates/ uvx ruff check --select F401 --exclude boilerplates/ + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install dependencies + run: | + uv pip install --system hatchling + - name: Validate Packing and Installation run: | # Builds the wheel and installs it the way a user would @@ -40,3 +55,40 @@ jobs: run: | # Runs pytest, verifying that the installed package works uv run pytest tests/ + + total-coverage: + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install uv + uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install dependencies + run: | + uv pip install --system hatchling + + - name: Install package + run: | + uv pip install --system . + + - name: Run tests with coverage + run: | + uv run pytest --cov=synaflow --cov-report=json --cov-report=term + + - name: Report coverage to GitHub + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + python .github/scripts/coverage_report.py diff --git a/.github/workflows/test-coverage.yml b/.github/workflows/test-coverage.yml deleted file mode 100644 index 1d66f95..0000000 --- a/.github/workflows/test-coverage.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Test Coverage - -on: - pull_request: - branches: ["main"] - -jobs: - coverage: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Install uv - uses: astral-sh/setup-uv@v3 - with: - enable-cache: true - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - - name: Install dependencies - run: | - uv pip install --system hatchling - - - name: Install package - run: | - uv pip install --system . - - - name: Run tests with coverage - run: | - uv run pytest --cov=synaflow --cov-report=json --cov-report=term - - - name: Report coverage to GitHub - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} - run: | - python .github/scripts/coverage_report.py