-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcopier.yml
More file actions
215 lines (198 loc) · 8.37 KB
/
Copy pathcopier.yml
File metadata and controls
215 lines (198 loc) · 8.37 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# ---- Copier mechanics -------------------------------------------------------
_subdirectory: template
_answers_file: .copier-answers.yml
_min_copier_version: "9.6.0"
_envops:
keep_trailing_newline: true
# _exclude REPLACES Copier's default ignore list, so the defaults are repeated here
# plus LICENSE-APACHE.txt (vendored verbatim and pulled into LICENSE via {% include %},
# never emitted as its own file in the generated project).
_exclude:
- "copier.yml"
- "copier.yaml"
- "~*"
- "*.py[co]"
- "__pycache__"
- ".git"
- ".DS_Store"
- ".svn"
- "LICENSE-APACHE.txt"
_message_after_copy: |
"{{ project_name }}" is ready.
{% if in_existing_repo -%}
Rendered inside an existing repository: no git init, no hook install, and the
root-only files were not rendered (.github/workflows/*.yml, .pre-commit-config.yaml,
renovate.json). Recreate them at the repository root by hand if you want CI, hooks,
or Renovate for this subproject; the workflows need a working-directory for it.
{% endif -%}
Next:
cd {{ _copier_conf.dst_path }}
just ci # everything should be green
# Run once on initial copy only (guarded by _copier_operation, requires Copier >= 9.6).
# These are UNSAFE features: `copier copy --trust` / `copier update --trust` required.
_tasks:
# Fail closed when the destination sits inside another repository's work tree: a bare
# `git init` there silently nests a repo. Copier's documented `git init && copier copy … .`
# pattern still passes (the toplevel IS the destination). in_existing_repo=true skips this.
- command: |
if git rev-parse --is-inside-work-tree >/dev/null 2>&1 && [ "$(git rev-parse --show-toplevel)" != "$(pwd -P)" ]; then
echo "$(pwd) is inside an existing git repository; re-run with --data in_existing_repo=true" >&2
exit 1
fi
git init --quiet
when: "{{ _copier_operation == 'copy' and not in_existing_repo }}"
- command: uv lock
when: "{{ _copier_operation == 'copy' }}"
- command: uv sync
when: "{{ _copier_operation == 'copy' }}"
# pre-commit refuses to install while core.hooksPath is set (any scope), and a failing
# task rolls the whole copy back — skip with a hint on stderr instead.
- command: |
if git config --get core.hooksPath >/dev/null; then
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
else
uv run pre-commit install --install-hooks
fi
when: "{{ _copier_operation == 'copy' and enable_precommit_install and not in_existing_repo }}"
# _migrations run ONLY on update (never copy), version-gated to the release that
# introduced a breaking rename/restructure (they run when new >= declared > old).
# None are needed yet; add one when a variable is renamed or a file moves across
# releases — a bare overwrite would otherwise orphan or conflict the change. Example:
# _migrations:
# - version: v2.0.0
# command: ["{{ _copier_python }}", "-c", "import pathlib; pathlib.Path('old.txt').unlink(missing_ok=True)"]
# ---- Identity / metadata ----------------------------------------------------
project_name:
type: str
help: Human-readable project name.
# Free text, but must not be blank: an empty title renders a nameless AGENTS.md
# header and module docstring. Whitespace-only is rejected too. Mirrors the
# package_name validator: an empty render == valid. Control chars are rejected too:
# project_name reaches markdown headers and module docstrings, where a newline yields
# a malformed header / split docstring (cosmetic, but never intended).
validator: >-
{% if not (project_name | trim) %}
project_name must not be empty
{% elif project_name | regex_search('[\x00-\x1f]') %}
project_name must not contain control characters
{% endif %}
package_name:
type: str
help: Importable package name (lowercase, underscores).
default: "{{ project_name | lower | replace(' ', '_') | replace('-', '_') }}"
# The shape regex enforces a lowercase identifier but not keyword-safety: `class`,
# `import`, `as`, ... match yet render an unimportable `src/<kw>/` (`import class` is
# a SyntaxError) and break `--cov=<kw>`. Reject hard keywords too. Jinja has no
# `keyword` module, so the list is explicit; uppercase keywords can't reach here (the
# regex bars a leading capital) and soft keywords (match/case/type) stay legal.
# The control-char check MUST come first: Python's `$` matches before a trailing newline,
# so `^[a-z][a-z0-9_]*$` accepts "demo_project\n", which would render invalid TOML
# (`name = "demo_project<NL>"`) and a broken `src/<name><NL>/` dir at copy time.
validator: >-
{% if package_name | regex_search('[\x00-\x1f]') %}
package_name must not contain control characters
{% elif not (package_name | regex_search('^[a-z][a-z0-9_]*$')) %}
package_name must match ^[a-z][a-z0-9_]*$
{% elif package_name in ["and", "as", "assert", "async", "await", "break", "class", "continue", "def", "del", "elif", "else", "except", "finally", "for", "from", "global", "if", "import", "in", "is", "lambda", "nonlocal", "not", "or", "pass", "raise", "return", "try", "while", "with", "yield"] %}
package_name must not be a Python keyword
{% endif %}
author_name:
type: str
default: Your Name
# author_name/author_email/description are free text rendered into single-line TOML basic
# strings; a newline or other C0 control char breaks the TOML and aborts the copy-time
# `uv lock`. Unreachable via the single-line prompt, but a multi-line scalar in a --data /
# answers file would hit it — reject hard, mirroring the package_name validator.
validator: >-
{% if author_name | regex_search('[\x00-\x1f]') %}
author_name must not contain control characters
{% endif %}
author_email:
type: str
default: you@example.com
validator: >-
{% if author_email | regex_search('[\x00-\x1f]') %}
author_email must not contain control characters
{% endif %}
description:
type: str
default: A new Python project.
validator: >-
{% if description | regex_search('[\x00-\x1f]') %}
description must not contain control characters
{% endif %}
license:
type: str
default: MIT
choices:
- MIT
- Apache-2.0
- ISC
- proprietary
python_version:
type: str
default: "3.13"
choices:
- "3.13"
- "3.12"
- "3.11"
project_type:
type: str
default: library
choices:
library: library
application: application
# ---- Layout -----------------------------------------------------------------
# Scaffold into a subdirectory of an existing repository: no `git init`, no hook install,
# and the root-only files (.github/, .pre-commit-config.yaml, renovate.json) are not
# rendered — GitHub and Renovate read them only at the repository root, so they would be
# inert there, and every local deletion becomes a `copier update` conflict later.
in_existing_repo:
type: bool
default: false
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)
# ---- Guardrail toggles / tuning ---------------------------------------------
ruff_ruleset:
type: str
default: all
choices:
all (every rule, version-coupled): all
curated (explicit allowlist): curated
coverage_floor:
type: int
default: 85
help: Minimum branch-coverage percentage (fail_under).
# Reject a floor that no-ops the gate (<= 0) or can never pass (> 100, coverage
# caps at 100). Mirrors the package_name validator: empty render == valid.
validator: >-
{% if coverage_floor < 1 or coverage_floor > 100 %}
coverage_floor must be between 1 and 100
{% endif %}
enable_property_tests:
type: bool
default: true
enable_mutation_tests:
type: bool
default: true
enable_policy_tests:
type: bool
default: true
enable_scanners:
type: bool
default: true
enable_dependency_audit:
type: bool
default: true
enable_renovate:
type: bool
default: true
enable_sha_pin_policy:
type: bool
default: true
# Hidden helper (never prompted). The generation-test harness passes this as False
# (see tests/conftest.py) to skip the slow pre-commit hook-install task; a real
# `copier copy` leaves it true so the downstream gets its hooks installed.
enable_precommit_install:
type: bool
default: true
when: false