Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
2b9f838
Add rendered workflow contracts for Python template
leynos May 31, 2026
6da5178
Add act switch to generated test target
leynos May 31, 2026
958ee77
Add parent template test Makefile
leynos May 31, 2026
1e8f361
Address workflow validation review comments
leynos May 31, 2026
527e70f
Document rendered test helpers
leynos Jun 1, 2026
13ff3b3
Address helper contract review findings
leynos Jun 1, 2026
12e52e5
Normalize developer guide spelling
leynos Jun 3, 2026
04e678f
Add direct helper error-path tests
leynos Jun 3, 2026
0afdfb8
Tighten helper unit test contracts
leynos Jun 3, 2026
c4e02b2
Cover parent Makefile and helper module roles
leynos Jun 3, 2026
e47324d
Anchor Makefile tests and strengthen snapshots
leynos Jun 3, 2026
38beb8b
Document snapshot quality guidance
leynos Jun 3, 2026
8e1cda7
Validate AGENTS make target command contracts
leynos Jun 3, 2026
ed0e2d3
Guard parent test target uvx lookup
leynos Jun 3, 2026
8577d0a
Document parent Makefile help target
leynos Jun 3, 2026
1cfa739
Prove Rust test target runs doctests
leynos Jun 3, 2026
d193267
Tighten doctest regression review fixes
leynos Jun 3, 2026
7be16d8
Add generated dependency audit gate
leynos Jun 3, 2026
dd96c24
Split act validation into separate workflows
leynos Jun 3, 2026
a22c033
Stop installing mdformat-all in parent CI
leynos Jun 4, 2026
c7e44d5
Always run Rust doctests in generated tests
leynos Jun 4, 2026
c22ba24
Harden parent workflow tool installs
leynos Jun 4, 2026
3e88bf1
Document parent Makefile and CI workflows
leynos Jun 4, 2026
ffbdbe2
Harden generated act validation workflow
leynos Jun 4, 2026
a280a9a
Document audit in README quality gate flow
leynos Jun 5, 2026
add4767
Clarify workflow contract assertion failures
leynos Jun 5, 2026
22de12b
Clear RUSTFLAGS during generated Rust tool installs
leynos Jun 5, 2026
e235e3a
Report act coverage failures after log checks
leynos Jun 5, 2026
67cf116
Document temporary lint suppression follow-ups
leynos Jun 5, 2026
60ebe6a
Update shared actions coverage pin
leynos Jun 5, 2026
e480d72
Require Node24-capable act for validation
leynos Jun 5, 2026
b44b9a3
Forward nested act GitHub token explicitly
leynos Jun 6, 2026
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/act-validation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: Act Validation

on:
push:
branches: [main]
pull_request:

permissions:
contents: read

jobs:
act-validation:
runs-on: ubuntu-latest
env:
ACT_VERSION: v0.2.84
MARKDOWNLINT_CLI2_VERSION: 0.22.1
MBAKE_VERSION: 1.4.6
steps:
- name: Check out repository
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
with:
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: '3.13'

- name: Install uv
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5

- name: Set up Rust
uses: leynos/shared-actions/.github/actions/setup-rust@455d9ed03477c0026da96c2541ca26569a74acac
with:
toolchain: stable

- name: Install template test tools
run: |
act_archive=act_Linux_x86_64.tar.gz
act_url="https://github.com/nektos/act/releases/download/${ACT_VERSION}/${act_archive}"
curl -fsSLo "${act_archive}" "${act_url}"
act_sha256="$(
curl -fsSL "https://api.github.com/repos/nektos/act/releases/tags/${ACT_VERSION}" |
python -c 'import json, sys; release = json.load(sys.stdin); print(next(asset["digest"].split(":", 1)[1] for asset in release["assets"] if asset["name"] == "act_Linux_x86_64.tar.gz"))'
)"
printf '%s %s\n' "${act_sha256}" "${act_archive}" | sha256sum -c -
sudo tar -xzf "${act_archive}" -C /usr/local/bin act
npm install -g "markdownlint-cli2@${MARKDOWNLINT_CLI2_VERSION}"
uv tool install "mbake==${MBAKE_VERSION}"

- name: Check Docker runtime
run: docker info

- name: Run template tests with act validation
env:
ACT_GITHUB_TOKEN: ${{ github.token }}
run: make test WITH_ACT=1
43 changes: 43 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: CI

on:
push:
branches: [main]
pull_request:

permissions:
contents: read

env:
MARKDOWNLINT_CLI2_VERSION: 0.22.1
MBAKE_VERSION: 1.4.6

jobs:
test:
runs-on: ubuntu-latest
Comment thread
coderabbitai[bot] marked this conversation as resolved.
steps:
- name: Check out repository
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
with:
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: '3.13'

- name: Install uv
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5

- name: Set up Rust
uses: leynos/shared-actions/.github/actions/setup-rust@455d9ed03477c0026da96c2541ca26569a74acac
with:
toolchain: stable

- name: Install template test tools
run: |
npm install -g "markdownlint-cli2@${MARKDOWNLINT_CLI2_VERSION}"
uv tool install "mbake==${MBAKE_VERSION}"

- name: Run template tests
run: make test
27 changes: 27 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
.PHONY: help check-fmt lint test typecheck

MAKEFLAGS += --no-print-directory

UV := $(shell command -v uvx 2>/dev/null)
WITH_ACT ?= 0
ACT_TEST_ENV = $(if $(filter 1 true yes on,$(WITH_ACT)),RUN_ACT_VALIDATION=1,)

ifeq ($(strip $(UV)),)
$(error uvx is required to run template tests. Install uv from https://docs.astral.sh/uv/getting-started/installation/)
endif

test: ## Run template tests
$(ACT_TEST_ENV) $(UV) --with pytest-copier --with pyyaml --with syrupy --with make-parser pytest tests/

check-fmt: ## Verify template test formatting
$(UV) ruff format --check tests/

lint: ## Run template test lint checks
$(UV) ruff check tests/

typecheck: ## Run template test type checks
$(UV) --with pytest --with pytest-copier --with pyyaml --with syrupy --with make-parser ty check tests/

help: ## Show available targets
@grep -E '^[a-zA-Z_-]+:.*?##' $(MAKEFILE_LIST) | \
awk 'BEGIN {FS=":.*?## "; printf "Available targets:\n"} {printf " %-15s %s\n", $$1, $$2}'
31 changes: 23 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Generic Copier Template

This repository provides a [Copier](https://copier.readthedocs.io/) template for a Python package.
This repository provides a [Copier](https://copier.readthedocs.io/) template
for a Python package.
It offers two flavours:

1. **Python Only** – a pure Python implementation.
Expand All @@ -14,22 +15,33 @@ The test suite relies on the `pytest-copier` plugin and renders generated
projects that run Ruff, Pylint via a PyPy-backed runner, `ty`, pytest, and, when
the Rust extension is enabled, Clippy, Whitaker, and nextest-aware Rust tests.

Install the `pytest-copier` test dependency before running this repository's
`pytest` suite. Generated projects install and run their own tooling, including
Ruff, Pylint via PyPy, `ty`, pytest, and, when Rust is enabled, Clippy, Whitaker,
and nextest.
Run the parent template tests through the repository `Makefile`. Run
`make help` to list the available parent Makefile targets. Parent gates include
`make check-fmt`, `make lint`, `make typecheck`, and `make test`. The `test`
target uses `uvx` to provide `pytest-copier`, `pyyaml`, `syrupy`, and
`make-parser` without a manually managed virtual environment:

```bash
pip install pytest-copier
make test
```

You can also run `scripts/setup_test_deps.sh` to install them automatically.
Run `make test WITH_ACT=1` to include the act-backed workflow integration tests
when `act` and Docker are available. Parent repository CI runs this mode in a
separate act-validation workflow so rendered in-template GitHub workflows do not
hold up the normal template test workflow.

Generated projects install and run their own tooling, including Ruff, Pylint via
PyPy, `ty`, pytest, and, when Rust is enabled, Clippy, Whitaker, and nextest.

You can also run `scripts/setup_test_deps.sh` to install parent test
dependencies into the current Python environment manually.

## Generated Quality Gate Flow

Figure: The generated `make all` quality gate runs build, formatting, linting,
typechecking, and testing. Rust-specific lint and test branches run only when
the Rust extension is enabled.
the Rust extension is enabled. The `audit` target checks Python dependencies
via `pip-audit` and, when Rust is enabled, Rust dependencies via `cargo audit`.

```mermaid
flowchart LR
Expand All @@ -38,17 +50,20 @@ flowchart LR
All --> CheckFmt[check-fmt]
All --> Lint[lint]
All --> Typecheck[typecheck]
All --> Audit[audit]
All --> Test[test]

Lint --> RuffCheck[ruff check]
Lint --> PylintPyPy[pylint-pypy via PyPy]
Typecheck --> TyCheck[ty check]
Audit --> PipAudit[pip-audit]
Test --> Pytest[pytest -v -n auto]

subgraph Rust_extension_enabled[use_rust == true]
Lint --> RustDoc[cargo doc]
Lint --> Clippy[cargo clippy]
Lint --> Whitaker[whitaker --all]
Audit --> CargoAudit[cargo audit]
Test --> Nextest[cargo nextest run]
Test --> RustDocTests[cargo test --doc]
end
Expand Down
94 changes: 93 additions & 1 deletion docs/developers-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,58 @@ This repository is a Copier template. Source files under `template/` are
rendered into generated projects, while tests under `tests/` render those
projects and run their public quality gates.

## Parent Repository Makefile

The root `Makefile` provides developer workflow targets for working on the
template itself.

- `make help` — lists all `##`-annotated targets.
- `make check-fmt` — runs Ruff formatting checks against the parent template
test suite.
- `make lint` — runs Ruff lint checks against the parent template test suite.
- `make typecheck` — runs `ty check` against the parent template test suite,
supplying the test dependencies through `uvx`.
- `make test` — runs the template test suite via `uvx`, supplying
`pytest-copier`, `pyyaml`, `syrupy`, and `make-parser` without a manually
managed virtual environment.
- `make test WITH_ACT=1` — sets `RUN_ACT_VALIDATION=1` inside the pytest
invocation, enabling the act-backed integration tests that run generated CI
workflows locally. Requires `act` and Docker to be available.

`uvx` is required; the Makefile aborts with an error if it is not found on
`PATH`.

## Parent CI Workflows

The parent repository uses two separate GitHub Actions workflows to keep
Docker-dependent tests isolated from the standard template test gate:

- `.github/workflows/ci.yml` runs `make test` on every push to `main` and on
all pull requests. It installs `markdownlint-cli2` and `mbake` at pinned
versions.
- `.github/workflows/act-validation.yml` runs `make test WITH_ACT=1`. It
additionally downloads the `act` binary at a pinned `ACT_VERSION`, verifies
its SHA-256 checksum before extraction, and confirms Docker availability via
`docker info`. The workflow exposes `ACT_GITHUB_TOKEN: ${{ github.token }}`
only to the nested act test step so actions requiring `github.token` behave
like they do on GitHub-hosted runners.

## Makefile Template

`template/Makefile.jinja` defines the generated developer workflow. The default
`all` target runs build, formatting, linting, typechecking, and tests.
The generated `audit` target is an explicit security gate run by CI. It runs
`pip-audit` for every rendered project and, when `use_rust` is enabled, also
runs `cargo audit` in the Rust extension crate.

The generated lint targets are split by language:

- `lint-python` runs Ruff and Pylint via the pinned PyPy-backed runner.
- `lint-rust` exists only when `use_rust` is enabled and runs rustdoc, Clippy,
and Whitaker.
- `lint` delegates to the applicable language-specific targets.
- `audit` exists for both generated variants and runs `pip-audit`; Rust-enabled
variants delegate to `rust-audit` for `cargo audit`.

Tool revisions are exposed as Makefile variables such as
`PYLINT_PYPY_SHIM_REF` and `WHITAKER_INSTALLER_REV`, so generated projects can
Expand All @@ -24,9 +65,18 @@ override pins without editing target recipes.

`template/.github/workflows/ci.yml.jinja` mirrors the generated local gates. It
sets up Python, optionally sets up Rust, runs `make check-fmt`, `make lint`,
and `make typecheck`, then delegates coverage to
`make typecheck`, and `make audit`, then delegates coverage to
`leynos/shared-actions/.github/actions/generate-coverage`.

The shared coverage action runs Python coverage through xdist-backed SlipCover
support. Generated pytest discovery is therefore constrained to the top-level
`tests/` tree via `tool.pytest.ini_options.testpaths`; do not add pytest unit
tests under package module directories or `unittests/` subdirectories.

`template/.github/workflows/act-validation.yml.jinja` keeps rendered workflow
validation separate from generated application CI. It installs `act`, verifies
Docker availability, and runs `make test WITH_ACT=1`.

Rust-enabled workflows pass `rust_extension/Cargo.toml` to the coverage action
because the generated Python project root does not contain a Rust manifest.

Expand All @@ -50,3 +100,45 @@ Rust documentation output, maturin configuration, and cargo error messaging.
The optional `act` tests run generated GitHub Actions workflows as black-box
integration checks. Their parser separates structured-log handling from the
domain assertions so workflow format details stay local to the adapter helpers.

### Test Helper Modules

Test helpers are organized under `tests/helpers/` into three modules, each with
a distinct responsibility:

**`tests/helpers/rendering.py`** — project rendering and command execution.

- `render_project` wraps the `pytest-copier` fixture to render a temporary
project with explicit template answers.
- `run_quality_gates` runs the generated `make all` target via the rendered
project wrapper.
- `check_generated_import` imports the generated package through `uv run` and
asserts its `hello()` return value.
- `read_generated_file` reads a file from the rendered project root as UTF-8
text, converting OS errors into `pytest.fail` exceptions.

**`tests/helpers/generated_files.py`** — file parsing with assertion-focused
error context.

- `read_generated_text` reads a `Path` and converts `OSError` into
`pytest.fail`.
- `parse_toml_file` reads and parses a TOML file, converting decode errors
into `pytest.fail`.
- `parse_yaml_mapping` parses a YAML string and asserts the result is a
mapping.
- `require_mapping` / `require_sequence` extract nested keys from a parsed
mapping, failing with a schema-path message when absent or the wrong type.

**`tests/helpers/tooling_contracts.py`** — generated tooling contract
assertions.

- `assert_common_make_targets` validates Makefile targets shared by all
generated variants.
- `assert_generated_tooling_contracts` is the single entry-point that
validates packaging configuration, AGENTS.md guidance, Makefile wiring, CI
and release workflow structure, and wheel workflow/action contracts.
- `assert_ci_coverage_action_contract` validates the shared coverage action
step and its inputs, including optional Rust cargo-manifest inputs.

All public helpers carry NumPy-style docstrings. Internal helpers (prefixed
`_`) are private to the module and not part of the test API.
2 changes: 1 addition & 1 deletion scripts/setup_test_deps.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
# Generated projects install their own linting and type-checking dependencies.
set -euo pipefail

pip install pytest-copier syrupy
pip install pytest-copier pyyaml syrupy make-parser
1 change: 1 addition & 0 deletions template/.github/actions/build-wheels/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ runs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v5
with:
Expand Down
1 change: 1 addition & 0 deletions template/.github/actions/pure-python-wheel/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ runs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v5
with:
Expand Down
Loading
Loading