diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e328c29..14337a4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,9 +1,10 @@ name: CI +# CI gates pull requests only. The Codecov baseline for `main` is refreshed +# by coverage-baseline.yml on a schedule, not by a push trigger. on: - push: - branches: [main] pull_request: + workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -32,21 +33,26 @@ jobs: - name: Type-check run: mypy src/shimkit - name: Tests + # The coverage floor is [tool.coverage.report] fail_under in + # pyproject.toml, so local runs and CI enforce the same number. run: | pytest -q --cov=shimkit \ --cov-report=term \ - --cov-report=xml:coverage.xml \ - --cov-fail-under=80 + --cov-report=xml:coverage.xml - name: Upload coverage to Codecov # Only upload from one matrix cell to avoid double-counting. # Ubuntu 3.12 is the canonical row; macOS / other Pythons # produce identical coverage modulo platform-gated tests. - if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12' - uses: codecov/codecov-action@v7 + if: matrix.os == 'ubuntu-latest' && matrix.python == '3.12' + uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1 with: files: ./coverage.xml flags: unit - # Public repos use OIDC token-less upload; no secret required. + disable_search: true + # Uses CODECOV_TOKEN when one is configured; otherwise a tokenless + # upload. Codecov being down or refusing the upload never blocks a + # PR: the local fail_under floor is the gate. + token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: false security: diff --git a/.github/workflows/coverage-baseline.yml b/.github/workflows/coverage-baseline.yml new file mode 100644 index 0000000..92cba03 --- /dev/null +++ b/.github/workflows/coverage-baseline.yml @@ -0,0 +1,46 @@ +name: Coverage baseline + +# Refreshes the Codecov baseline for `main` so PR comparisons have a recent +# base commit to diff against. Runs weekly (Monday 06:00 America/New_York, +# matching dependabot.yml; 11:00 UTC) and on demand. It replaces the old +# `push: branches: [main]` trigger on ci.yml, which re-ran the full matrix +# after every merge. Scheduled runs always use the default branch. + +on: + schedule: + - cron: '0 11 * * 1' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + coverage: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 + with: + python-version: '3.12' + cache: pip + - name: Install dev dependencies + run: | + python -m pip install --upgrade pip + pip install -e ".[dev]" + - name: Tests with coverage + run: | + pytest -q --cov=shimkit \ + --cov-report=term \ + --cov-report=xml:coverage.xml + - name: Upload coverage to Codecov + uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1 + with: + files: ./coverage.xml + flags: unit + disable_search: true + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a2ed9b..02afcb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,25 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Unreleased] +### Changed + +- CI runs on `pull_request` and `workflow_dispatch` only; the + `push: branches: [main]` trigger is gone. A new + `coverage-baseline.yml` refreshes the Codecov baseline for `main` + weekly (Monday 06:00 America/New_York) and on demand. +- Coverage floor raised from 80% to 84% (measured 84.81%, rounded + down) and moved from the CI command line to + `[tool.coverage.report] fail_under`, so local runs enforce it too. +- `codecov/codecov-action` pinned to the v7.1.1 commit SHA, and it + uses `CODECOV_TOKEN` when one is configured. +- `codecov.yml` added: carryforward flags, a blocking project status + with 1% slack, and an advisory patch status. + +### Fixed + +- The Codecov upload step never ran. Its condition read + `matrix.python-version`, but the matrix key is `python`. + ## [0.19.0] — 2026-05-16 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b702d94..81ddbc4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -120,6 +120,22 @@ A tool joins shimkit only if it shares ≥2 of `Platform` / `Shell` / `SHIMKIT_CONFIG=`, then `reset_cache()`. The autouse fixture resets between tests so leakage isn't a concern. +### Coverage + +- The enforced floor is `[tool.coverage.report] fail_under` in + `pyproject.toml`; plain `pytest --cov=shimkit` fails below it, locally + and in CI. It is the measured total rounded down. Raise it when coverage + rises; never lower it to get a PR through. +- Every PR uploads coverage to Codecov from the Ubuntu / Python 3.12 CI + cell. The upload is advisory (`fail_ci_if_error: false`), so a Codecov + outage never blocks a merge. +- `main` has no push trigger. `coverage-baseline.yml` refreshes the + Codecov baseline every Monday (06:00 America/New_York) and on demand + (`gh workflow run coverage-baseline.yml`). `codecov.yml` turns on + carryforward flags so a stale baseline never reads as 0%. GitHub + pauses schedules after 60 days without repository activity; re-enable + it from the Actions tab if that happens. + ## Releasing See [`docs/release.md`](docs/release.md). Releases are diff --git a/README.md b/README.md index 2fa871c..16c6806 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # shimkit +[![codecov](https://codecov.io/gh/simtabi/shimkit/graph/badge.svg)](https://codecov.io/gh/simtabi/shimkit) + A toolkit of developer utilities. Python tools, shimmed by bash. ``` diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..6264375 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,32 @@ +# Codecov configuration. Validate with: +# curl --data-binary @codecov.yml https://codecov.io/validate +codecov: + require_ci_to_pass: true + +coverage: + precision: 2 + round: down + status: + project: + default: + # Blocking, with 1% slack for platform-gated lines and noise. + target: auto + threshold: 1% + informational: false + patch: + default: + # Reported on every PR, but advisory: the enforced floor is + # fail_under in pyproject.toml. + target: auto + threshold: 5% + informational: true + +flag_management: + default_rules: + # A flag missing from an upload inherits its last known value, so a + # stale or missing `main` baseline never reads as 0% coverage. + carryforward: true + +comment: + layout: "diff, flags, files" + require_changes: true diff --git a/pyproject.toml b/pyproject.toml index ed468b7..c7cb9d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -103,6 +103,15 @@ filterwarnings = [ "ignore::PendingDeprecationWarning", ] +[tool.coverage.run] +source = ["shimkit"] + +[tool.coverage.report] +# Local, enforced floor, independent of Codecov. Set to the measured total +# rounded down (CI 2026-10-02: 84.81% ubuntu, 84.87% macos) so it ratchets: +# raise it when coverage rises, never lower it to make a PR pass. +fail_under = 84 + [tool.ruff] line-length = 100 target-version = "py310"