Contributing
Contributions are welcome, and they are greatly appreciated.
Working on the template
Two trees matter and are easy to confuse:
project/is the template source. Its.jinjafiles are rendered into a new project, and directory names containing{% ... %}are literal names on disk, not placeholders.tests/expected/holds the rendered output for each supported combination of answers.
Fixtures are the specification. Regenerate them with make regen-fixtures; never edit one by
hand to match an expectation. scripts/regen_fixtures.py is the single source of truth for
which combinations exist — the full cross product of layout, git provider and package manager,
plus the publish and license edges — and tests/test_copier.bats reads the same set back by
iterating the directories, reconstructing each render's answers from its name. Neither the
golden test nor the fixture list needs a per-fixture case, so they cannot drift apart.
Run make tests before every commit, and make tests-integration whenever you touch
project/noxfile.py.jinja, project/pyproject.toml.jinja or a pipeline template — that target
generates real projects and runs their toolchains, which is the only way to know a change works.
The repository holds its own Python to the same bar it ships. make checks, which make tests
runs first, gates the scripts under scripts/ with black, ruff, and mypy, using the same
configuration in pyproject.toml that the template puts in a generated project. The same three
run on commit through pre-commit, scoped to scripts/ so they never touch the .jinja templates
or the rendered fixtures.
make tests also runs scripts/check_pipelines.py, which parses every generated YAML rather
than reading it. Jinja whitespace control can move a key into the block scalar above it,
producing a file that still parses and means something else entirely; a textual diff cannot see
that, so the fixture would record the broken output just as faithfully as correct output. That
checker deliberately excludes GitHub Actions ${{ ... }} expression syntax from its
unrendered-construct scan, because the templates emit it on purpose; the negative lookbehind in
the pattern is what keeps a real expression from being flagged as leftover Jinja.
Adding a project layout
A layout is a kind of project the generator can produce. Adding one is deliberately confined to a few places, so shipped layouts are not disturbed:
- Add the value to
project_layoutincopier.yml, with help text describing the kind of project rather than the file tree it emits. - Condition the content that varies. Use in-file Jinja for files that mostly stay the same
(
project/pyproject.toml.jinja,project/noxfile.py.jinja), and conditional directory names for whole trees that only one layout wants, followingproject/{% if project_layout == 'ml' %}notebooks{% endif %}/. - Add the layout segment to
LAYOUTSinscripts/regen_fixtures.py. The fixtures are the cross product of the dimensions, so this creates one fixture per provider and package manager automatically; the golden test picks them up without a per-fixture case. - Add a case to
tests/test_integration.batsthat generates the layout and runs itsinstall,checksandtests. The golden suite proves the layout renders; the integration suite proves the generated project works. - Run
make regen-fixtures, then confirmgit diff --exit-code tests/expected/shows changes only under the new layout's fixtures. Any movement elsewhere means the new layout disturbed an existing one, which is a defect rather than something to regenerate away.
Two rules constrain what a layout may carry. It must not add a dependency unless one of the generated project's own tasks exercises it, and any framework it does add must run locally with no account or server to provision. Both exist because every generated project pays for each dependency, including the projects that wanted a different choice.
A layout that is deployed rather than installed should default include_dockerfile on. A
layout whose framework collects telemetry must decline it in generated output rather than
inherit consent the project's owner never gave — see the .telemetry file the dataeng
layout ships.
Layouts differ in what they generate, never in whether the quality floor applies: the same task names, the same checks at the same strictness, the same release topology.
Commit message convention
Commit messages follow conventions based on the Angular style.
Structure
<type>(<scope>): <subject>
<body>
<footer>
Example
feat(directive): A new feature of code
A description of the new feature.
It contains **important** information.
BREAKING CHANGES:
Explanation, code, etc.
Issue #10: https://github.com/namespace/project/issues/10
Related to PR namespace/other-project#15: https://github.com/namespace/other-project/pull/15
Guidelines
- Scope and body are optional.
- Subject and body must be valid Markdown.
- Body must add trailers at the end, for example issues and PR references or co-authors.
- Subject must have proper casing, i.e. uppercase for first letter if it makes sense.
- Subject must have no dot at the end and no punctuation.
- Type can be:
feat: New feature implementation.fix: Bug fix.docs: Documentation changes.style: Code style or format changes.refactor: Changes that are not features or bug fixes.tests: Test additions or corrections.chore: Maintenance code changes.
Pull Request guidelines
Link to any related issue in the Pull Request message. We also recommend using fixups:
git commit --fixup=SHA
Once all the changes are approved, you can squash your commits:
git rebase -i --autosquash master
And force-push:
git push -f