From 011a37cceee6733a66f0e9c0da8885227536df07 Mon Sep 17 00:00:00 2001 From: Jonathan Zhang Date: Tue, 12 May 2026 22:49:29 -0700 Subject: [PATCH] feat(coverage-floor): add test_command input for layered Python repos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds optional `test_command:` input to the coverage-floor.yml reusable that lets callers replace the default language-specific install + test invocation with a custom shell command. The override MUST produce the coverage file the workflow expects (`coverage.json` for python, `cov.out` for go, `coverage/coverage-summary.json` for js); the existing file-existence checks + MEASURED extraction at the end of each case branch still run. Mirrors the equivalent `test_command:` input already supported by tests-runner.yml. Same use case both there and here: layered repos where the default `uv sync` / `pip install -r requirements.txt` path can't pick up the right package + extras. Concrete trigger: whois-api-llc/wxa_marketing (today). Project layout is root `pyproject.toml` for the `marketing-engine` package (with httpx + pytest-asyncio + aiosqlite in `[project.optional-dependencies].test`) + tests under `backend/` that import `marketing_engine.*`. The default Python path falls into `[[ -f requirements.txt ]] → pip install -r requirements.txt` from `backend/`, which doesn't install the engine package, so `from httpx import AsyncClient` in conftest.py fails. After this lands, the caller can override with e.g.: with: language: python working_directory: backend test_command: | python -m venv .venv .venv/bin/pip install -q --upgrade pip .venv/bin/pip install -q -e "..[test]" pytest-cov .venv/bin/pytest --cov --cov-report=json --quiet || true Same pattern already de-risked on tests-runner.yml (in wxa_marketing#25, in flight today). ## Test plan - [x] `python -m yaml.safe_load` clean - [x] `bash -n` clean on the measure step's embedded run script - [x] All three case branches (python/go/js) short-circuit on `INPUT_TEST_COMMAND` set; coverage-file existence checks still enforce the right artifact gets produced - [x] Defaults preserved: empty `test_command` keeps the existing auto-detection behavior on every repo that doesn't pass it - [ ] Self-test on this repo (selftest/sample.py) stays green (`.coverage-floor` at root still seeds 99% for the self-test) **Auto-merge rationale:** in-tree change to a reusable workflow under `.github/workflows/**` — high-risk surface per fleet policy. Diff is additive (1 new input declaration, 3 short-circuit blocks inside an existing measure step); no permission/secret changes, no behavior change for callers that don't set the input. Per the auto-merge cheat-sheet self-test: worst case is "callers measure coverage differently for one cycle, git revert and we're back" — auto-merge territory. Bot's regex decides. **Codex pre-review:** trivial — 44-line additive diff in a single existing step. Skipping per <50-line carve-out. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/coverage-floor.yml | 44 ++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/.github/workflows/coverage-floor.yml b/.github/workflows/coverage-floor.yml index 283f830..9d84ebd 100644 --- a/.github/workflows/coverage-floor.yml +++ b/.github/workflows/coverage-floor.yml @@ -102,6 +102,25 @@ on: required: false type: string default: "" + test_command: + description: | + Caller-provided shell command to install deps and run tests with coverage. + Completely replaces the default language-specific install + pytest/go-test/npm-test + invocation. The command MUST produce the coverage file expected by the language: + - python: `coverage.json` at working_directory (use `pytest --cov --cov-report=json`) + - go: `cov.out` at working_directory (use `go test -coverprofile=cov.out ./...`) + - js: `coverage/coverage-summary.json` at working_directory (configure + vitest/jest with json-summary reporter) + Use when the default install path can't handle your repo's structure (e.g. layered + projects where root pyproject defines the package and tests live in a subdir, or + repos that need pre-install steps the reusable doesn't know about). Run with + `set -euo pipefail` style discipline. Trailing `|| true` is appropriate after the + test invocation so the coverage file gets parsed even when tests fail (coverage + floor cares about the number, not pass/fail — that's tests-runner.yml's job). + Default empty (use the language-specific default path). + required: false + type: string + default: "" secrets: AUTOMERGE_PAT: description: "PAT used to push the seed branch and open the seed PR so that pull_request workflows actually fire (GITHUB_TOKEN doesn't trigger them — recursion prevention). Already deployed fleet-wide for `claude-author-automerge.yml`. Caller must explicitly forward this secret (see callers/coverage-floor.yml in topcoder1/dotclaude for the syntax). Required for the post-merge seed flow to land without manual unblock." @@ -258,11 +277,22 @@ jobs: working-directory: ${{ inputs.working_directory || '.' }} env: LANG_TYPE: ${{ steps.detect.outputs.language }} + INPUT_TEST_COMMAND: ${{ inputs.test_command || '' }} run: | set -euo pipefail + # When the caller provides test_command, it replaces the language-specific + # install + pytest/go-test/npm-test invocation. The coverage-file existence + # check + MEASURED extraction at the end of each case branch still runs, + # so the caller's command MUST produce the file the workflow expects + # (Python: coverage.json, Go: cov.out, JS: coverage/coverage-summary.json). + # Used by layered repos where the default `uv sync` / `pip install -r` + # path can't pick up the right package + extras. case "$LANG_TYPE" in python) - if [[ -f pyproject.toml ]]; then + if [[ -n "$INPUT_TEST_COMMAND" ]]; then + echo "==> caller test_command override (python)" + eval "$INPUT_TEST_COMMAND" + elif [[ -f pyproject.toml ]]; then # --all-extras installs every [project.optional-dependencies] # extra (e.g. `dev`, `test`) in addition to [dependency-groups]. # Without it, repos that put test deps only in optional- @@ -294,7 +324,12 @@ jobs: MEASURED=$(jq '.totals.percent_covered' coverage.json) ;; go) - go test -coverprofile=cov.out ./... 2>&1 || true + if [[ -n "$INPUT_TEST_COMMAND" ]]; then + echo "==> caller test_command override (go)" + eval "$INPUT_TEST_COMMAND" + else + go test -coverprofile=cov.out ./... 2>&1 || true + fi if [[ ! -f cov.out ]]; then echo "::error::go test did not produce cov.out — no testable packages?" exit 1 @@ -313,7 +348,10 @@ jobs: # - bun: bun test --coverage emits coverage-summary.json by default # If neither is present, the workflow fails with a clear error # pointing at the caller's test config. - if [[ -f bun.lockb || -f bun.lock ]]; then + if [[ -n "$INPUT_TEST_COMMAND" ]]; then + echo "==> caller test_command override (js)" + eval "$INPUT_TEST_COMMAND" + elif [[ -f bun.lockb || -f bun.lock ]]; then curl -fsSL https://bun.sh/install | bash export PATH="$HOME/.bun/bin:$PATH" bun install --frozen-lockfile