Skip to content
Open
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
46 changes: 40 additions & 6 deletions .github/workflows/test-template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,26 @@ concurrency:
cancel-in-progress: true

jobs:
automation:
runs-on: ubuntu-latest
timeout-minutes: 180
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
version: "0.13.4"
enable-cache: true
python-version: "3.11"
- uses: extractions/setup-just@53165ef7e734c5c07cb06b3c8e7b647c5aa16db3 # v4.0.0
- name: Verify automation helper
env:
TMPDIR: ${{ runner.temp }}
HYPOTHESIS_PROFILE: ci
run: just automation-ci

test:
strategy:
fail-fast: false
Expand All @@ -28,7 +48,7 @@ jobs:
persist-credentials: false
- uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
version: "0.11.23"
version: "0.13.4"
enable-cache: true
python-version: ${{ matrix.python-version }}
- uses: extractions/setup-just@53165ef7e734c5c07cb06b3c8e7b647c5aa16db3 # v4.0.0
Expand All @@ -46,7 +66,7 @@ jobs:
persist-credentials: false
- uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
version: "0.11.23"
version: "0.13.4"
enable-cache: true
- name: zizmor (workflow audit)
env:
Expand All @@ -62,7 +82,7 @@ jobs:
persist-credentials: false
- uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
version: "0.11.23"
version: "0.13.4"
enable-cache: true
- name: Install dependencies
run: uv sync --locked
Expand All @@ -78,7 +98,7 @@ jobs:
persist-credentials: false
- uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
version: "0.11.23"
version: "0.13.4"
enable-cache: true
- name: Install dependencies
run: uv sync --locked
Expand All @@ -98,14 +118,28 @@ jobs:
persist-credentials: false
- uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
version: "0.11.23"
version: "0.13.4"
enable-cache: true
- name: pip-audit
run: |
uv export --frozen --no-emit-project --no-hashes -o requirements-audit.txt
uvx pip-audit@2.10.1 -r requirements-audit.txt
- name: semgrep
run: uvx semgrep@1.167.0 scan --config .semgrep.yml --metrics=off --error .
run: |
# Bound only this child: Python 3.15 exposes an unsigned RLIM_INFINITY.
# Bash -s uses KiB; 1048576 KiB = 1 GiB, above Semgrep's 1,000,000,000-byte soft target.
# Include equality below: Bash rounds soft-limit reads down to whole KiB.
(
hard_stack=$(ulimit -H -s) || exit;
if [[ "$hard_stack" == unlimited ]]; then
soft_stack=$(ulimit -S -s) || exit;
if [[ "$soft_stack" == unlimited ]] || (( soft_stack >= 1048576 )); then
ulimit -S -s 1048576 || exit;
fi;
ulimit -H -s 1048576 || exit;
fi;
uvx semgrep@1.167.0 scan --config .semgrep.yml --metrics=off --error .;
)
# gitleaks is installed from the mise.toml pin (single source of truth);
# install: false brings up the mise CLI only. persist_github_token
# re-exports MISE_GITHUB_TOKEN (v5.1.1+ default is off) so the later
Expand Down
23 changes: 15 additions & 8 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,28 @@

This repo is a Copier template. `template/` holds the generated project as `.jinja`.

`automation/` is a separate, Copier-generated helper project outside the rendered
template. Read its `AGENTS.md` before editing it. The shared agent procedure is
`workflow/SKILL.md`; its host-routing guide preserves existing implementation and
review authority. Editing those repository artifacts does not install live skills.
The maintainer-harness details below apply to root `tests/`; the helper has its own
lockfile, strict configuration and substantive product tests.

## Run every gate (`just ci`)

```bash
just ci # fmt-check + lint + typecheck + test + policy + audit, then "ci: all gates passed"
just ci # root gates + automation-ci, then "ci: all gates passed"
```

`just ci` is the complete local gate — `ci: fmt-check lint typecheck test policy audit` ending `@echo "ci: all gates passed"`, with `verify` a bare alias (`verify: ci`). It **mirrors** `template/justfile.jinja`'s `ci`: the template chains `policy` and `audit` when `enable_policy_tests`/`enable_dependency_audit` are on (they are), so the faithful maintainer recipe includes both. `test` is the heavy generation matrix, so `just ci` needs a roomy `TMPDIR` — the default 4G tmpfs `/tmp` can overflow (export e.g. `TMPDIR=/path/to/roomy/dir` first). The fast inner loop is `just fmt-check lint typecheck` (one invocation, no matrix, no network); there is deliberately **no** `check:` recipe (the template ships none). `audit` is non-hermetic (it queries the OSV/PyPI advisory DB), so `just ci` reaches the network exactly as `just audit` does; scanners (`just scan`) deliberately stay **off** `ci` (CI-only), matching the template, as does the changelog check (`changelog.yml`, no recipe — see "Changelog check").
`just ci` is the complete local gate — `ci: fmt-check lint typecheck test policy audit automation-ci` ending `@echo "ci: all gates passed"`, with `verify` a bare alias (`verify: ci`). The maintainer gates mirror `template/justfile.jinja`'s `ci`: the template chains `policy` and `audit` when `enable_policy_tests`/`enable_dependency_audit` are on (they are), so the maintainer recipe includes both. The additive `automation-ci` dependency syncs the helper's locked environment and runs its complete generated gate. `test` is the heavy generation matrix, so `just ci` needs a roomy `TMPDIR` — the default 4G tmpfs `/tmp` can overflow (export e.g. `TMPDIR=/path/to/roomy/dir` first). The fast inner loop is `just fmt-check lint typecheck` (one invocation, no matrix, no network); there is deliberately **no** `check:` recipe (the template ships none). `audit` is non-hermetic (it queries the OSV/PyPI advisory DB), so `just ci` reaches the network exactly as `just audit` does; scanners (`just scan`) deliberately stay **off** `ci` (CI-only), matching the template, as does the changelog check (`changelog.yml`, no recipe — see "Changelog check").

This maintainer `just ci` is distinct from the **downstream's** `just ci` referenced under "Run the tests": `just test` renders each project in the answer matrix and runs *its* `just ci` — a different, generated recipe.

CI does **not** run `just ci`. It keeps parallel per-gate jobs (`test`/`typecheck`/`lint`/`scan`, plus the PR-only `changelog` workflow — see "Changelog check"), each inlining its command; only `test` fans out over the OS×Python matrix, and the `scan` job runs `uvx`/`mise exec` (not `uv run`); because every gate is enforced independently (audit via the `scan` job's `pip-audit` step), `just ci` here is the local reproduction of the PR gate, not the CI entry point. Pointing the CI jobs at these recipes (the gap-audit "candidate F") is deferred: the `scan` job aggregates pip-audit + semgrep + gitleaks as three steps that don't map onto the `audit`+`scan` recipe split, so it is a real restructure, not a one-line swap, and the follow-up must reconcile the recipe encoding with the parallel jobs.
CI does **not** run the root `just ci`. It keeps parallel per-gate jobs (`test`/`typecheck`/`lint`/`scan`, plus `automation` running `just automation-ci` and the PR-only `changelog` workflow — see "Changelog check"), each inlining its command; only `test` fans out over the OS×Python matrix, and the `scan` job runs `uvx`/`mise exec` (not `uv run`); because every gate is enforced independently (audit via the `scan` job's `pip-audit` step), `just ci` here is the local reproduction of the PR gate, not the CI entry point. Pointing the CI jobs at these recipes (the gap-audit "candidate F") is deferred: the `scan` job aggregates pip-audit + semgrep + gitleaks as three steps that don't map onto the `audit`+`scan` recipe split, so it is a real restructure, not a one-line swap, and the follow-up must reconcile the recipe encoding with the parallel jobs.

**Forward-sync:** the policy-tests layer (gap #4) has landed — `policy` is now a `ci` member (see "Policy gate"). When the property-tests layer (gap #9) lands, add `fuzz` to **both** the `ci` recipe **and** this section, to keep mirroring the template's conditional `ci` (`fmt-check lint typecheck test{% if enable_property_tests %} fuzz{% endif %}{% if enable_policy_tests %} policy{% endif %}{% if enable_dependency_audit %} audit{% endif %}`).

Deliberate divergences from `template/justfile.jinja`'s `ci`: **none in the gate list** — `ci: fmt-check lint typecheck test policy audit`, `verify: ci`, and the `@echo` are exact. The divergences are structural: the maintainer's CI never runs `just ci` (parallel per-gate jobs — only `test` matrixed — with inline `uv run`/`uvx`/`mise exec` commands, vs the downstream `ci.yml`'s single `just ci`); there is no `check:` fast split (use `just fmt-check lint typecheck`); and candidate F is deferred. (The `audit` recipe's dropped `--no-dev` is a separate divergence documented under "Dependency audit", not a `ci`-list divergence.)
Deliberate divergences from `template/justfile.jinja`'s `ci`: the additive `automation-ci` dependency verifies this repository's helper project; all original maintainer gates, `verify: ci`, and the `@echo` are preserved. The divergences are structural: the maintainer's CI never runs `just ci` (parallel per-gate jobs — only `test` matrixed — with inline `uv run`/`uvx`/`mise exec` commands, vs the downstream `ci.yml`'s single `just ci`); there is no `check:` fast split (use `just fmt-check lint typecheck`); and candidate F is deferred. (The `audit` recipe's dropped `--no-dev` is a separate divergence documented under "Dependency audit", not a `ci`-list divergence.)

## Run the tests

Expand All @@ -28,7 +35,7 @@ just test # renders the answer matrix, installs each project, runs its `just
## Type-check this repo

```bash
just typecheck # basedpyright recommended over tests/ (the only Python surface; no src/)
just typecheck # basedpyright recommended over the maintainer tests/ harness
```

The maintainer harness is held to the same `recommended` bar the template ships. CI enforces it (the `typecheck` job in `.github/workflows/test-template.yml`); `failOnWarnings` makes any finding fail the build.
Expand All @@ -41,7 +48,7 @@ just fmt # ruff check --fix . + ruff format . (apply safe fixes)
just fmt-check # ruff format --check . (CI's format gate)
```

The maintainer harness runs the same full `select=["ALL"]` ruleset the template ships, scoped to `tests/` (the only Python surface; `template/` is Jinja). Every config-level ignore is load-bearing and audited — there are **no inline `# noqa`**. CI enforces `ruff check` + `ruff format --check` (the `lint` job in `.github/workflows/test-template.yml`).
The maintainer harness runs the same full `select=["ALL"]` ruleset the template ships over `tests/` (`template/` is Jinja). Ruff discovers the nested helper's own equally strict configuration under `automation/`. Every config-level ignore is load-bearing and audited — there are **no inline `# noqa`**. CI enforces `ruff check` + `ruff format --check` (the `lint` job in `.github/workflows/test-template.yml`).

## Pre-commit

Expand All @@ -62,7 +69,7 @@ Deliberate divergences from the template's `.pre-commit-config.yaml` (`template/
just scan # out-of-band secret + SAST scan: semgrep (no-eval) + gitleaks (full history)
```

`just scan` runs semgrep's `no-eval` rule and a gitleaks **full-history** secret scan (`.gitleaks.toml` = default ruleset). It is out-of-band (chained into no recipe), but CI enforces it: the `scan` job in `.github/workflows/test-template.yml` is a blocking PR gate. gitleaks is pinned in `mise.toml` (`gitleaks = "8.30.1"`) and installed in CI via `jdx/mise-action` + `mise exec`; semgrep runs via `uvx semgrep@1.167.0` (no dep, like zizmor). **semgrep scans non-test Python only** — its built-in `.semgrepignore` excludes `tests/`, and there is no `src/`, so on this repo it currently scans **0 files** (a forward guard that mirrors the shipped gate and fires the moment any non-test Python is added at root); gitleaks scans the whole tree + full history regardless of language and is the substantive gate here. Never pass semgrep `--config auto` (it drops the pinned rule and needs metrics on); never hardcode the gitleaks version in CI (install via `mise exec`).
`just scan` runs semgrep's `no-eval` rule and a gitleaks **full-history** secret scan (`.gitleaks.toml` = default ruleset). It is out-of-band (chained into no recipe), but CI enforces it: the `scan` job in `.github/workflows/test-template.yml` is a blocking PR gate. gitleaks is pinned in `mise.toml` (`gitleaks = "8.30.1"`) and installed in CI via `jdx/mise-action` + `mise exec`; semgrep runs via `uvx semgrep@1.167.0` (no dep, like zizmor). **semgrep scans non-test Python only** — its built-in `.semgrepignore` excludes `tests/`. The new helper under `automation/src/` makes this a substantive product scan; verify its measured file inventory rather than carrying forward the earlier empty-scan result. Gitleaks scans the whole tree + full history regardless of language. Never pass semgrep `--config auto` (it drops the pinned rule and needs metrics on); never hardcode the gitleaks version in CI (install via `mise exec`).

Deliberate divergences from the template's `scan.yml` (`template/{% if not in_existing_repo %}.github{% endif %}/workflows/…scan.yml….jinja`): the maintainer folds scanning into the existing `test-template.yml` as a sibling `scan` job (the template consolidates into a standalone `scan.yml`), matching the one-workflow / per-tool layout and letting the existing zizmor job audit it (the changelog check is the one exception: its trigger types need a separate workflow — see "Changelog check"); zizmor stays its own job here rather than a step in `scan` (already dogfooded standalone). The CI `mise-action` comment drops the template's "kept fresh by Renovate" note; the maintainer's `mise.toml` pin is Renovate-managed too (see "Renovate"), but the comment names only the single-source-of-truth property.

Expand All @@ -74,7 +81,7 @@ Renovate re-derives only the `mise.toml` gitleaks pin (see "Renovate"); every ot
just audit # dependency vulnerability audit: pip-audit over the full locked graph (also a `just ci` member)
```

`just audit` exports the fully-resolved lockfile (`uv export --frozen --no-emit-project --no-hashes -o requirements-audit.txt`) and runs `uvx pip-audit@2.10.1 -r requirements-audit.txt`. It is chained into `just ci` (the local aggregate — see "Run every gate") and independently enforced in CI: the `pip-audit` step in the `scan` job of `.github/workflows/test-template.yml` is a blocking PR gate. Unlike semgrep — which scans 0 files here — pip-audit is the **substantive** dependency gate: it audits the real ~35-package dev+transitive graph (copier / pytest / ruff / basedpyright / pre-commit / plumbum / pyyaml + transitives), because **`--no-dev` is dropped**. Never restore `--no-dev`: `package = false` puts every dep in the dev group, so `--no-dev` would export 0 packages and pass vacuously. `requirements-audit.txt` is `rm`'d on success, gitignored, and lingers (harmlessly) only after a failed run. pip-audit is the one **non-hermetic** step in the `scan` job — it queries the OSV/PyPI advisory DB, so this gate can turn red on a newly-published CVE (or an advisory-DB outage) independent of any code change; a `--frozen` **export** failure instead means `uv.lock` is stale (re-lock), distinct from a pip-audit non-zero (a CVE). pip-audit runs via `uvx` (no pyproject dep, like semgrep/zizmor).
`just audit` exports the fully-resolved lockfile (`uv export --frozen --no-emit-project --no-hashes -o requirements-audit.txt`) and runs `uvx pip-audit@2.10.1 -r requirements-audit.txt`. It is chained into `just ci` (the local aggregate — see "Run every gate") and independently enforced in CI: the `pip-audit` step in the `scan` job of `.github/workflows/test-template.yml` is a blocking PR gate. Pip-audit audits the maintainer's real ~35-package dev+transitive graph (copier / pytest / ruff / basedpyright / pre-commit / plumbum / pyyaml + transitives), because **`--no-dev` is dropped**. Never restore `--no-dev`: `package = false` puts every dep in the dev group, so `--no-dev` would export 0 packages and pass vacuously. `requirements-audit.txt` is `rm`'d on success, gitignored, and lingers (harmlessly) only after a failed run. pip-audit is the one **non-hermetic** step in the `scan` job — it queries the OSV/PyPI advisory DB, so this gate can turn red on a newly-published CVE (or an advisory-DB outage) independent of any code change; a `--frozen` **export** failure instead means `uv.lock` is stale (re-lock), distinct from a pip-audit non-zero (a CVE). pip-audit runs via `uvx` (no pyproject dep, like semgrep/zizmor).

Deliberate divergences from the template's dependency-audit layer: `--no-dev` is dropped (above); pip-audit runs via `uvx pip-audit@2.10.1` in both the recipe and CI with **no** pyproject dep (the template adds `pip-audit>=2.10` to its dev group and runs `uv run pip-audit` locally); it is folded into `test-template.yml`'s `scan` job as a step (the template ships it in a standalone `scan.yml`); and, like the template, `audit` is chained into `just ci` (see "Run every gate") while additionally enforced in CI as the `pip-audit` step in the `scan` job.

Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
PR-only `changelog.yml` check that fails when `src/` or `pyproject.toml` change without a
new `CHANGELOG.md` line; the `skip-changelog` label bypasses it. CI-only, no recipe.

### Fixed

- Keep Semgrep's native engine working when uv selects Python 3.15: bound an unlimited
hard stack to 1 GiB only inside the scanner child. Preserve finite limits, scanner
pins, rules and targets, and leave the caller and later gitleaks stage unchanged.

### Changed

- Pin uv 0.13.4 in generated mise and CI configuration to use the patched TLS trust bundle.
- Bump the generated project's ruff to `>=0.17,<0.18` (and the shipped `ruff-pre-commit`
hook to v0.17.1). Under `ruff_ruleset = "all"`, ignore `CPY001`, which ruff 0.17 stabilised
and would otherwise demand a copyright header in every file.
Expand Down
Loading
Loading