Skip to content
Merged
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
16 changes: 16 additions & 0 deletions .github/workflows/template-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,22 @@ jobs:
test ! -e /tmp/out-app/.github/workflows/publish.yml
echo "Both variants rendered and validated."

for kind in app library; do
out="/tmp/out-$kind"
test -f "$out/changelog.d/TEMPLATE.md"
grep -q '<!-- scriv-insert-here -->' "$out/CHANGELOG.md"
! grep -q '## \[Unreleased\]' "$out/CHANGELOG.md" || exit 1
printf '### Added\n\n- Smoke one.\n' > "$out/changelog.d/smoke1.md"
printf '### Added\n\n- Smoke two.\n' > "$out/changelog.d/smoke2.md"
(cd "$out" && uvx --from 'scriv>=1.8' scriv collect)
grep -q '^## \[0\.1\.0\] - ' "$out/CHANGELOG.md"
# two fragments in one category must collect into one tight list
grep -A1 '^- Smoke one\.$' "$out/CHANGELOG.md" | grep -q '^- Smoke two\.$'
test ! -e "$out/changelog.d/smoke1.md"
test -f "$out/changelog.d/TEMPLATE.md"
done
echo "scriv collects fragments into CHANGELOG.md in both variants."

# use_typos defaults on, and answering no drops the hook
uvx copier copy --trust --defaults --vcs-ref=HEAD \
--data project_name="sample-no-typos" \
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ Entries for 1.0.0 through 1.5.2 were backfilled from git history after the fact,

### Added

- Scaffolded projects keep their changelog as [scriv](https://scriv.readthedocs.io/) fragments: each PR adds a file under `changelog.d/` (`uv run scriv create`) instead of editing `CHANGELOG.md`, and `uv run scriv collect` writes them into `CHANGELOG.md` at release time. Two PRs that both edited `## [Unreleased]` conflicted on every merge; fragments are separate files, so they never do. The scaffold gains `[tool.scriv]` in `pyproject.toml` (Keep a Changelog categories, `## [X.Y.Z] - YYYY-MM-DD` headings, the version read from `__init__.py` for libraries or `pyproject.toml` for apps), `scriv` in the dev group, `changelog.d/TEMPLATE.md`, and a `<!-- scriv-insert-here -->` marker in place of `## [Unreleased]`. `RELEASING.md` and the README say how to use them.
- `CHANGELOG.md` is listed in `_skip_if_exists`, so `copier update` leaves an existing changelog alone. It does create one if the file is missing, so a project that keeps its changelog elsewhere gets a new `CHANGELOG.md` proposed on each update.
- **Upgrading an existing project:** `copier update` brings in the config and `changelog.d/TEMPLATE.md` but not the marker. By hand:
- Replace the `## [Unreleased]` heading in `CHANGELOG.md` with `<!-- scriv-insert-here -->`, and move any unreleased entries into a fragment under `changelog.d/`. Skipping this makes `scriv collect` fail with `Entry 'Changelog' is not a valid version!`; its hint about `scriv-end-here` is not the fix.
- Check that `[tool.scriv] version` reads the file your build backend reads. Libraries get `src/<pkg>/__init__.py`, which is right for the template's hatchling setup. A project that moved to a static `version` in `pyproject.toml` (for example, uv_build) needs `literal: pyproject.toml: project.version`, or collect writes a stale version.
- `python-ci.yml` and `node-ci.yml` take an `lfs` input, passed through to `actions/checkout`. It defaults to `false`, because an LFS pull costs bandwidth against the account quota on every run and most projects have nothing in LFS. Turn it on for a repo whose tests read LFS-tracked fixtures: without it the checkout produces pointer files, and the failure surfaces as whatever the reading library says about malformed input — `FzErrorFormat: no objects found` from PyMuPDF, in the case that prompted this — with nothing anywhere in the output mentioning LFS.
- Repo settings as code, opt-in via `use_repo_settings` (default off, so
`copier update --defaults` leaves existing projects alone): the scaffold
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ standard so the repos don't drift:
- Optional TypeScript/JavaScript side: **Biome** (lint + format) in the
pre-commit stack, plus a shared **`node-ci`** workflow for type-check and
build
- **Keep a Changelog** `CHANGELOG.md`; libraries also get PyPI trusted
publishing (`publish.yml` + `RELEASING.md`)
- **Keep a Changelog** `CHANGELOG.md`, written from per-PR fragments in
`changelog.d/` by [scriv](https://scriv.readthedocs.io/), so concurrent PRs
never conflict on it; libraries also get PyPI trusted publishing
(`publish.yml` + `RELEASING.md`)
- A Claude Code `SessionStart` hook that pre-warms the toolchain. With a
frontend it also points corepack at `registry.npmjs.org`, since some sandbox
egress proxies block `repo.yarnpkg.com` and corepack then fails before Yarn or
Expand Down
6 changes: 6 additions & 0 deletions copier.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
_subdirectory: template
_min_copier_version: "9.0.0"

# A project's changelog is its own once scaffolded. Without this, every
# `copier update` that touched CHANGELOG.md.jinja would 3-way merge the
# template's few lines into a long project history and conflict.
_skip_if_exists:
- CHANGELOG.md

# Trim block-tag newlines so conditionals don't leave stray blank lines in
# rendered text/markdown (keeps mdformat happy on a fresh scaffold).
_envops:
Expand Down
6 changes: 1 addition & 5 deletions template/CHANGELOG.md.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,4 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- Initial project scaffold.
<!-- scriv-insert-here -->
2 changes: 2 additions & 0 deletions template/README.md.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ uv run pytest
uv run basedpyright src
```

Each pull request adds a changelog fragment rather than editing `CHANGELOG.md`, so concurrent PRs don't conflict. Run `uv run scriv create`, uncomment the sections that apply in the new file under `changelog.d/`, and commit it with the change, or delete it if the change needs no entry. At release time, bump the version{% if project_kind == 'app' %} in `pyproject.toml`{% endif %} and run `uv run scriv collect`, which writes the fragments into `CHANGELOG.md` under the new version and deletes them.

Linting (ruff, [zizmor](https://docs.zizmor.sh/), mdformat) runs via [pre-commit](https://pre-commit.com); CI runs the same stack plus basedpyright and pytest via the shared [`python-ci`](https://github.com/{{ github_owner }}/python-project-template) reusable workflow.
{% if publish_to_pypi %}

Expand Down
49 changes: 49 additions & 0 deletions template/changelog.d/TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<!--
A changelog fragment: one file per pull request, collected into CHANGELOG.md
at release time by `uv run scriv collect`.

Uncomment the section(s) that apply and replace the example bullet. Write for
someone reading the release notes: what changed and why it matters to them.
-->

<!--
### Added

- A new capability.

-->

<!--
### Changed

- A change to existing behaviour.

-->

<!--
### Deprecated

- Something that will be removed in a future release.

-->

<!--
### Removed

- Something that is gone.

-->

<!--
### Fixed

- A bug that no longer happens.

-->

<!--
### Security

- A vulnerability that is fixed.

-->
22 changes: 22 additions & 0 deletions template/pyproject.toml.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ dev = [
"pytest-cov>=7.0.0",
"basedpyright>=1.39",
"pre-commit>=4.0.0",
"scriv>=1.8",
]

{% if project_kind == "library" -%}
Expand All @@ -41,6 +42,27 @@ packages = ["src/{{ package_name }}"]
[tool.uv]
package = false
{% endif %}

[tool.scriv]
# One fragment per pull request in changelog.d/, collected into CHANGELOG.md
# at release time, so concurrent PRs never edit the same lines of the changelog.
format = "md"
fragment_directory = "changelog.d"
categories = ["Added", "Changed", "Deprecated", "Removed", "Fixed", "Security"]
skip_fragments = "[A-Z]*" # TEMPLATE.md
new_fragment_template = "file: TEMPLATE.md"
{% raw %}entry_title_template = "[{{ version }}] - {{ date.strftime('%Y-%m-%d') }}"{% endraw +%}
md_header_level = "2"
md_html_anchors = false
# Without this, two fragments in one category leave a blank line between them,
# which makes the list loose and fails mdformat on the release commit.
compact_fragments = true
{% if project_kind == "library" %}
version = "literal: src/{{ package_name }}/__init__.py: __version__"
{% else %}
version = "literal: pyproject.toml: project.version"
{% endif %}

[tool.ruff]
line-length = 100
src = ["src"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ The first tagged release creates the PyPI project and converts the pending publi
## Each release

1. Bump `__version__` in `src/{{ package_name }}/__init__.py` (single source of truth — `pyproject.toml` reads it).
2. Move the `## [Unreleased]` entries in `CHANGELOG.md` under a new `## [X.Y.Z] - YYYY-MM-DD` heading.
2. Run `uv run scriv collect`. It reads the new version, writes the fragments in `changelog.d/` into `CHANGELOG.md` under `## [X.Y.Z] - YYYY-MM-DD`, and deletes them.
3. Commit, then tag and push:
```bash
git tag vX.Y.Z
Expand Down
Loading