You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: AGENTS.md
+4-4Lines changed: 4 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -54,7 +54,7 @@ just precommit # run every hook (commit-stage + pre-push basedpyright) over the
54
54
55
55
This is a **local-only** gate: there is no pre-commit CI job, matching the template (whose downstream CI also never runs pre-commit). The ruff and basedpyright *substance* is enforced by the existing `lint` and `typecheck` CI jobs; the hygiene hooks (eof / trailing-whitespace / check-merge-conflict / forbid-rej) have **no** CI backstop and are a local convenience here. A bare `uv run pre-commit run --all-files` runs commit-stage hooks only — basedpyright fires on push or via `just typecheck`; `just precommit` runs both.
56
56
57
-
Deliberate divergences from `template/.pre-commit-config.yaml.jinja`: ruff runs via local `uv run ruff` hooks (locked 0.15.19) instead of the `astral-sh/ruff-pre-commit` repo (pinned 0.15.18) — the venv is always synced here, so there is no bootstrap reason to keep the isolated-env repo hook; the pytest hook is dropped (the maintainer's only suite is the heavy generation matrix — CI-only). The two configs share the SHA-pinned `pre-commit-hooks` block: **bump both `rev:` pins together** (v6.0.0 = `3e8a8703…`).
57
+
Deliberate divergences from the template's `.pre-commit-config.yaml` (`template/{% if not in_existing_repo %}.pre-commit-config.yaml{% endif %}.jinja`): ruff runs via local `uv run ruff` hooks (locked 0.15.19) instead of the `astral-sh/ruff-pre-commit` repo (pinned 0.15.18) — the venv is always synced here, so there is no bootstrap reason to keep the isolated-env repo hook; the pytest hook is dropped (the maintainer's only suite is the heavy generation matrix — CI-only). The two configs share the SHA-pinned `pre-commit-hooks` block: **bump both `rev:` pins together** (v6.0.0 = `3e8a8703…`).
`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`).
66
66
67
-
Deliberate divergences from the template's `scan.yml` (`template/.github/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; 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 has no Renovate**, so the pins are static.
67
+
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; 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 has no Renovate**, so the pins are static.
68
68
69
69
Because nothing here re-derives the pins (no Renovate; the generation drift test reads only the *rendered* downstream), **bump every literal site by hand, against the template.** gitleaks (`8.30.1`) has two maintainer sites — `mise.toml` and the prose above — synced to `template/mise.toml.jinja` (CI installs via `mise exec`, so there is no third gitleaks literal). semgrep (`1.167.0`) has three — the `just scan` recipe, the `scan` job in `test-template.yml`, and the prose above — synced to `template/justfile.jinja` and the template `scan.yml`. (Mirrors the pre-commit "bump both `rev:` pins together" obligation.)
70
70
@@ -78,7 +78,7 @@ just audit # dependency vulnerability audit: pip-audit over the full locked gr
78
78
79
79
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.
80
80
81
-
Because nothing here re-derives the pin (no Renovate; the generation drift test reads only the *rendered* downstream), **bump every literal by hand, against the template.** pip-audit (`2.10.1`) has three maintainer sites — the `just audit` recipe, the `pip-audit` step in `test-template.yml`, and the prose above — synced to `template/.github/workflows/…scan.yml….jinja` (the only exact-version template site; the template justfile uses unpinned `uv run pip-audit` and template pyproject floors `pip-audit>=2.10`). No `mise.toml` or `pyproject.toml` pip-audit literal exists (uvx-run, unlike gitleaks). **Sync only the pin *value* — never the export flags:** the template's `uv export` keeps `--no-dev`, but the maintainer must not (it exports 0 packages here — see above), so a mechanical sync against the template would silently neuter the gate. (Mirrors the semgrep/gitleaks pin-sync note and the pre-commit "bump both `rev:` pins together" rule.)
81
+
Because nothing here re-derives the pin (no Renovate; the generation drift test reads only the *rendered* downstream), **bump every literal by hand, against the template.** pip-audit (`2.10.1`) has three maintainer sites — the `just audit` recipe, the `pip-audit` step in `test-template.yml`, and the prose above — synced to `template/{% if not in_existing_repo %}.github{% endif %}/workflows/…scan.yml….jinja` (the only exact-version template site; the template justfile uses unpinned `uv run pip-audit` and template pyproject floors `pip-audit>=2.10`). No `mise.toml` or `pyproject.toml` pip-audit literal exists (uvx-run, unlike gitleaks). **Sync only the pin *value* — never the export flags:** the template's `uv export` keeps `--no-dev`, but the maintainer must not (it exports 0 packages here — see above), so a mechanical sync against the template would silently neuter the gate. (Mirrors the semgrep/gitleaks pin-sync note and the pre-commit "bump both `rev:` pins together" rule.)
82
82
83
83
## Policy gate (`just policy`)
84
84
@@ -100,7 +100,7 @@ The SHA-pin sub-check overlaps the zizmor job (the security control), so its net
100
100
101
101
1. Add an `enable_*` toggle to `copier.yml`.
102
102
2. Add the conditional file(s) under `template/` (file: `{% if flag %}name{% endif %}.jinja`; dir: `{% if flag %}dir{% endif %}/`).
103
-
3. Wire it into `template/justfile.jinja` (a recipe; add it as a `ci` dep only for a *gating* layer — out-of-band checks like `scan`/`mutate` ship a recipe but stay off `ci`, and CI-only layers like renovate/sha-pin add no recipe at all). Then, where applicable: a dep in `template/pyproject.toml.jinja` (skip it for `uvx`-run tools like the scanners), a section in `template/AGENTS.md.jinja`, and a CI surface under `template/.github/workflows/` (a conditional step in `scan.yml`, or a dedicated conditional workflow file via the empty-name idiom).
103
+
3. Wire it into `template/justfile.jinja` (a recipe; add it as a `ci` dep only for a *gating* layer — out-of-band checks like `scan`/`mutate` ship a recipe but stay off `ci`, and CI-only layers like renovate/sha-pin add no recipe at all). Then, where applicable: a dep in `template/pyproject.toml.jinja` (skip it for `uvx`-run tools like the scanners), a section in `template/AGENTS.md.jinja`, and a CI surface under the template's `.github/workflows/` (a conditional step in `scan.yml`, or a dedicated conditional workflow file via the empty-name idiom). Root-only files — the `.github/` dir, `.pre-commit-config.yaml`, `renovate.json` — carry `not in_existing_repo` in their path condition (GitHub and Renovate read them only at a repository root); a new root-only file must too, and `test_existing_repo_layer`'s omission list grows with it.
104
104
4. Extend `tests/test_generation.py`: assert present-when-on AND absent-when-off, and that the layer's gate passes.
Copy file name to clipboardExpand all lines: README.md
+12Lines changed: 12 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -16,6 +16,18 @@ To update a downstream project after a new template release:
16
16
copier update --trust
17
17
```
18
18
19
+
Hooks are installed on copy unless git's `core.hooksPath` is set (pre-commit refuses to install under it); the copy then skips that step with a hint instead of failing.
20
+
21
+
### Scaffolding into an existing repository
22
+
23
+
To render the project as a subdirectory of a repository you already have, answer `in_existing_repo` with yes (or pass it as data):
This skips `git init` and the hook install, and does not render the root-only files GitHub and Renovate read only at the repository root: `.github/workflows/*.yml`, `.pre-commit-config.yaml`, and `renovate.json`. Recreate them at the root by hand if you want CI, hooks, or Renovate for the subproject (the workflows need a `working-directory`). Without the answer, copying into a subdirectory of a repository aborts rather than silently creating a nested one.
30
+
19
31
## Toggles
20
32
21
33
All toggles default to `true` — every guardrail layer ships unless you opt out.
Copy file name to clipboardExpand all lines: copier.yml
+35-4Lines changed: 35 additions & 4 deletions
Original file line number
Diff line number
Diff line change
@@ -21,21 +21,42 @@ _exclude:
21
21
22
22
_message_after_copy: |
23
23
"{{ project_name }}" is ready.
24
+
{% if in_existing_repo -%}
25
+
Rendered inside an existing repository: no git init, no hook install, and the
26
+
root-only files were not rendered (.github/workflows/*.yml, .pre-commit-config.yaml,
27
+
renovate.json). Recreate them at the repository root by hand if you want CI, hooks,
28
+
or Renovate for this subproject; the workflows need a working-directory for it.
29
+
{% endif -%}
24
30
Next:
25
31
cd {{ _copier_conf.dst_path }}
26
32
just ci # everything should be green
27
33
28
34
# Run once on initial copy only (guarded by _copier_operation, requires Copier >= 9.6).
29
35
# These are UNSAFE features: `copier copy --trust` / `copier update --trust` required.
30
36
_tasks:
31
-
- command: git init --quiet
32
-
when: "{{ _copier_operation == 'copy' }}"
37
+
# Fail closed when the destination sits inside another repository's work tree: a bare
38
+
# `git init` there silently nests a repo. Copier's documented `git init && copier copy … .`
39
+
# pattern still passes (the toplevel IS the destination). in_existing_repo=true skips this.
40
+
- command: |
41
+
if git rev-parse --is-inside-work-tree >/dev/null 2>&1 && [ "$(git rev-parse --show-toplevel)" != "$(pwd -P)" ]; then
42
+
echo "$(pwd) is inside an existing git repository; re-run with --data in_existing_repo=true" >&2
43
+
exit 1
44
+
fi
45
+
git init --quiet
46
+
when: "{{ _copier_operation == 'copy' and not in_existing_repo }}"
33
47
- command: uv lock
34
48
when: "{{ _copier_operation == 'copy' }}"
35
49
- command: uv sync
36
50
when: "{{ _copier_operation == 'copy' }}"
37
-
- command: uv run pre-commit install --install-hooks
38
-
when: "{{ _copier_operation == 'copy' and enable_precommit_install }}"
51
+
# pre-commit refuses to install while core.hooksPath is set (any scope), and a failing
52
+
# task rolls the whole copy back — skip with a hint on stderr instead.
53
+
- command: |
54
+
if git config --get core.hooksPath >/dev/null; then
55
+
echo "core.hooksPath is set: skipping 'pre-commit install'. Run 'uv run pre-commit install --install-hooks' yourself once it is unset, or dispatch pre-commit from your global hooks." >&2
56
+
else
57
+
uv run pre-commit install --install-hooks
58
+
fi
59
+
when: "{{ _copier_operation == 'copy' and enable_precommit_install and not in_existing_repo }}"
39
60
40
61
# _migrations run ONLY on update (never copy), version-gated to the release that
41
62
# introduced a breaking rename/restructure (they run when new >= declared > old).
# Scaffold into a subdirectory of an existing repository: no `git init`, no hook install,
160
+
# and the root-only files (.github/, .pre-commit-config.yaml, renovate.json) are not
161
+
# rendered — GitHub and Renovate read them only at the repository root, so they would be
162
+
# inert there, and every local deletion becomes a `copier update` conflict later.
163
+
in_existing_repo:
164
+
type: bool
165
+
default: false
166
+
help: Scaffold into a subdirectory of an existing git repository? (skips git init and hook install; omits .github/, .pre-commit-config.yaml and renovate.json for you to recreate at the repo root)
0 commit comments