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
20 changes: 5 additions & 15 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,23 +42,13 @@ jobs:
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}

- name: Set up python
id: setup-python
uses: actions/setup-python@v7
- name: Install uv
uses: astral-sh/setup-uv@v10.3.0
with:
python-version: '3.13'

- name: Install and configure poetry
uses: snok/install-poetry@v1
with:
version: 2.1.4
virtualenvs-create: true
virtualenvs-in-project: true
installer-parallel: true

- name: install copier
run: |
python3 -m pip install --user copier>=9
enable-cache: true
# the root has no lock file; without this the cache key never changes
cache-dependency-glob: "src/pyproject.toml.jinja"

- name: create projects with default answers and run check
env:
Expand Down
33 changes: 32 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,38 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p

## [Unreleased]

TODO: add at least one Added, Changed, Deprecated, Removed, Fixed or Security section
### Changed

- **uv replaces poetry as the package and dependency manager**
- the build backend is now `hatchling` with `uv-dynamic-versioning`, which is built on the same `dunamai` engine as `poetry-dynamic-versioning`, so existing tags produce identical version strings
- project metadata moved to the PEP 621 `[project]` table and development dependencies to the PEP 735 `[dependency-groups]` table, which retires the nine deprecation warnings `poetry check` printed on every commit
- the license is declared the PEP 639 way, as an SPDX expression plus `license-files`
- **migrating an existing project** - read this before running `copier update`:
1. work on a branch with a clean tree
2. convert a throwaway copy: `git worktree add ../uv-scratch`, then `uvx migrate-to-uv` inside it. Its `pyproject.toml` shows the PEP 440 equivalent of each caret constraint, and its `uv.lock` pins your dependencies to the versions in `poetry.lock`. Do not run it on your branch - the conversion would collide with the template's
3. `copier update --defaults`
4. `git rm poetry.lock` and copy `../uv-scratch/uv.lock` into the project - before anything locks, so the old versions are what uv starts from. Also `rm -rf .poetry`: poetry 2 installs its plugins there, git never shows it, and trivy fails `task check` on the lock file inside
5. in `pyproject.toml`, take the **template's** side of every conflict, then re-declare your own dependencies with `uv add …` and `uv add --group dev …`, copying the constraints from `../uv-scratch/pyproject.toml`. Then delete every remaining `[tool.poetry…]` table, after carrying over what the scratch copy translated for them - sources to `[[tool.uv.index]]`, scripts to `[project.scripts]`, extras to `[project.optional-dependencies]`, plugins to `[project.entry-points]` - because uv ignores them silently
6. `uv lock` and `uv sync` - a dependency moves off its old version only where the template's constraints require it
7. `task check`, commit `uv.lock`, then `git worktree remove --force ../uv-scratch` (the scratch copy always holds uncommitted changes) and `git branch -D uv-scratch`
- your own `TaskfileCustom.yaml`, `.gitlab-ci-custom.yml` and `CLAUDE.md` are project owned and are not updated: any `poetry run …`, `poetry version …` or `poetry:install` task dependency in them has to be changed by hand, and the jobs in `.gitlab-ci-custom.yml` inherit `default:`, so they no longer get the removed `before_script`
- uv 0.11.7 or newer is required, enforced by `required-version` in `pyproject.toml`
- `task poetry:install` is now `task uv:install`; every other task keeps its name
- `.python-version` now ships with generic projects too, not only plugin projects, because uv reads it to choose and if necessary download the interpreter
- the source distribution contents are pinned explicitly and are unchanged from before, except that hatchling force-includes `.gitignore` into every sdist and no setting can prevent it - a note at the top of the generated `.gitignore` warns not to put internal names there
- gitlab: the pipeline drops its `before_script` (the eccenca image ships uv and the environment is always `.venv`), caches uv's package cache keyed on `uv.lock`, and publishes with `uv publish`
- github: `astral-sh/setup-uv` replaces both `actions/setup-python` and `snok/install-poetry`; publishing is now `uv build` followed by `uv publish` because `uv publish` has no build flag
- both pipelines set `UV_LOCKED`, so a pipeline whose committed lock file no longer matches `pyproject.toml` fails instead of silently resolving something else
- **this can turn a first pipeline run red** if the lock was not committed alongside a metadata change; `uv lock` and commit the result

### Removed

- `poetry.lock` - delete it; `uv.lock` replaces it and belongs in version control
- the `pip` development dependency, a workaround for a scanner finding against the `pip` that poetry installed into every environment. uv installs none
- the `poetry-check` pre-commit hook, which has no uv equivalent. The `poetry-lock` and `poetry-install` hooks become `uv-lock` and `uv-sync`
- the `poetry:install` task, replaced by `uv:install`
- gitlab: `default: before_script`
- `poetry shell`, which uv removed deliberately - use `uv run`, or `source .venv/bin/activate`


## [9.10.0] 2026-10-17
Expand Down
45 changes: 40 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ projects and runs *their* checks:

1. `check:generate:cases` renders every test case in `tests/*.yml` into a
`<case>_dir` directory at the repository root and git-inits it.
2. `check:validate:cases` runs `poetry update && task check` inside each one.
2. `check:validate:cases` runs `uv sync && task check` inside each one, then
`check:build:case`.

The `tests/*.yml` files are copier answer files. `plugin`,
`plugin-github-pypi`, `generic-project` and `generic-github-pypi` cover
Expand Down Expand Up @@ -464,7 +465,8 @@ cd plugin_dir && claude -p "Which files in this repository belong to the templat

The answer has to come from `.claude/rules/`. Check the generic case too — it
must contain `.claude/settings.json`, `.claude/rules/copier-template.md` and
only the `copier-update` skill, plus `release` when `github_page` is answered,
only the `copier-update` and `template-feedback` skills, plus `release` when
`github_page` is answered,
with no plugin material.

### `src/Taskfile.yaml` is deliberately not a copier template
Expand Down Expand Up @@ -511,7 +513,7 @@ In the badge rows of `README.md`, `src/README.md.jinja` and
`src/README-public.md.jinja`, `mypy-shield` points at
`https://www.mypy-lang.org/static/mypy_badge.svg`. It is the only badge not
served by shields.io, so it renders flat — no gradient, no logo — while
`poetry`, `ruff` and `copier` beside it have both. This reads as an oversight
`uv`, `ruff` and `copier` beside it have both. This reads as an oversight
and is not one.

That SVG is the badge mypy itself publishes for exactly this use. Replacing it
Expand Down Expand Up @@ -562,12 +564,12 @@ goes red for reasons unrelated to the change gets disabled or `--force`d, and
then it protects nothing — the same reasoning that kept an automated
product-name check out of `task check` in report #74.

It also costs more than it looks. The toolchain is otherwise entirely Poetry
It also costs more than it looks. The toolchain is otherwise entirely uv
managed, so every generated project, every contributor machine and every runner
would need a non-Python binary installed for it.

And most of the links in a freshly generated project are the template's own —
`documentation.eccenca.com`, the badge endpoints, `python-poetry.org`,
`documentation.eccenca.com`, the badge endpoints, `docs.astral.sh`,
`taskfile.dev`. Those are fixed here, once, for everybody. Checking them again
in each of fifty downstream projects re-verifies the template's URLs fifty
times over and still says nothing about the one link a plugin author actually
Expand Down Expand Up @@ -596,3 +598,36 @@ reports `DEP002 'cmem-plugin-base' defined as a dependency but not used`. Three
correct signals say the same true thing, so silencing the third one changes
nothing a user would notice — while the shell conditional it needs would have
to hold on the Windows branch of that task too.

### The generated `.gitignore` is published inside every sdist

Hatchling force-includes the VCS ignore file into every source distribution,
from `get_default_build_data()` in `hatchling/builders/sdist.py`, and force
includes bypass `exclude`, `only-include` and `ignore-vcs` alike. No setting
removes it. The warning header at the top of `src/.gitignore` is the
mitigation; the file itself cannot be kept out without changing build backend,
and no other backend offers dunamai's version semantics.

### `check:trivy` scans the whole repository, `.venv` included

The scan is deliberately not narrowed to the lock file. Generic projects
rendered from this template regularly carry material beyond the Python package —
a single page frontend with its own dependencies, a Dockerfile — and a
lockfile-only scan would silently skip exactly those. `.venv` is scanned too; it
yields nothing beyond `uv.lock`.

Two directories are excluded. `.claude` (issue #109), because a git worktree
created there is a second checkout whose lock file would be scanned as if it
were this project's. `.uv-cache`, because it is uv's package cache, not project
content, and the gitlab pipeline keeps it inside the project (GitLab can only
cache paths there). Both live as `--skip-dirs` flags in `check:trivy`, because
such a flag replaces `TRIVY_SKIP_DIRS` rather than adding to it: an exclusion
moved into the environment silently stops working.

### `poetry shell` has no successor and none is shipped

uv removed the concept deliberately. The supported equivalents are `uv run`,
which the whole build plan already uses, and `source .venv/bin/activate`, which
the generated README documents. A `shell` task was considered and rejected: a
task runs in a subshell and cannot leave the caller in an activated
environment.
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# cmem-plugin-template

[![workflow][build-shield-main]][github-actions] [![workflow][build-shield-develop]][github-actions] [![version][version-shield]][changelog] ![python][python-shield]
[![poetry][poetry-shield]][poetry-link] [![ruff][ruff-shield]][ruff-link] [![mypy][mypy-shield]][mypy-link] [![copier][copier-shield]][copier]
[![uv][uv-shield]][uv-link] [![ruff][ruff-shield]][ruff-link] [![mypy][mypy-shield]][mypy-link] [![copier][copier-shield]][copier]
[![eccenca Corporate Memory][cmem-shield]][cmem]

This repository contains a [copier](https://copier.readthedocs.io/) template.
Expand Down Expand Up @@ -38,7 +38,7 @@ You can use it to bootstrap the following types of project:

## Features

- [Python / poetry](https://python-poetry.org/) project with
- [Python / uv](https://docs.astral.sh/uv/) project with
- [pytest](https://www.pytest.org/) (incl. [memray](https://bloomberg.github.io/memray/) + [pytest-dotenv](https://github.com/quiqua/pytest-dotenv) + [code coverage](https://github.com/pytest-dev/pytest-cov)) as the testing framework,
- [ruff](https://docs.astral.sh/ruff/) as all-hands linter and formatter,
- [mypy](https://mypy-lang.org/) as type checker,
Expand Down Expand Up @@ -127,7 +127,7 @@ task: Available tasks for this project:
* check:trivy: Scan for vulnerabilities using Trivy
* format:fix: Format Python files and fix obvious issues
* format:fix-unsafe: Format Python files and fix 'unsafe' issues
* poetry:install: Install dependencies managed by Poetry
* uv:install: Install dependencies managed by uv
```

You can extend this task list by creating a file `TaskfileCustom.yaml` in your repository root:
Expand Down Expand Up @@ -155,18 +155,18 @@ The following tools are needed for local task execution:
- Python 3.13.x
- [copier](https://copier.readthedocs.io/) (>= v9) for project template rendering and updating
- [task](https://taskfile.dev/) (>= v3.29) for running build tasks (make sure to follow the installation instructions to avoid confusion with taskwarrior)
- [poetry](https://python-poetry.org/) (>= v2.1) for packaging and dependency managing (needed plugins such as [poetry-dynamic-versioning](https://github.com/mtkennerly/poetry-dynamic-versioning) are declared with `[tool.poetry.requires-plugins]` and installed automatically)
- [uv](https://docs.astral.sh/uv/) (>= v0.11.7) for packaging and dependency management, with [hatchling](https://hatch.pypa.io/latest/) and [uv-dynamic-versioning](https://github.com/ninoseki/uv-dynamic-versioning) as the build backend, declared in `[build-system]` and installed automatically
- [pre-commit](https://pre-commit.com/) (>= v2.20) for managing pre-commit hooks (optional)

Example installation of the requirements with [pipx](https://pipx.pypa.io/) on Ubuntu:
Example installation of the requirements on Ubuntu, using [pipx](https://pipx.pypa.io/) for the Python tools and the official installer for uv:

``` shell-session
sudo sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b /usr/local/bin
python3 -m pip install --user pipx
python3 -m pipx ensurepath
pipx install copier
pipx install pre-commit
pipx install poetry
curl -LsSf https://astral.sh/uv/install.sh | sh
```

### Integration Tests
Expand Down Expand Up @@ -276,6 +276,6 @@ OAUTH_GRANT_TYPE="client_credentials"
[mypy-shield]: https://www.mypy-lang.org/static/mypy_badge.svg
[ruff-link]: https://docs.astral.sh/ruff/
[ruff-shield]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json&label=Code%20Style
[poetry-link]: https://python-poetry.org/
[poetry-shield]: https://img.shields.io/endpoint?url=https://python-poetry.org/badge/v0.json
[uv-link]: https://docs.astral.sh/uv/
[uv-shield]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json

27 changes: 0 additions & 27 deletions TASKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,33 +31,6 @@ be a real case rather than a theoretical one.
question against the grain of `github_page`, and confirm each project gets
exactly the pipeline files it can run.

## Migrate the generated `pyproject.toml` to PEP 621 `[project]` metadata

`src/pyproject.toml.jinja` still declares `name`, `version`, `description`,
`authors`, `license`, `readme`, `keywords` and `classifiers` under
`[tool.poetry]`. Poetry 2.x deprecates all of them in favour of the PEP 621
`[project]` table.

This is not latent. Running `poetry check` in a freshly generated project prints
nine deprecation warnings, and `poetry-check` is a pre-commit hook that fires on
every commit touching `pyproject.toml`. Every author using this template is
therefore trained to ignore `poetry check` output, which is exactly where a real
configuration error would eventually appear.

The migration is fiddly rather than hard, because the project is dynamically
versioned. `poetry check` spells out the required handling for each field: some
move to `[project]` verbatim, while `version`, `readme` and `classifiers` must
either move *and* be declared static, or stay in `[tool.poetry]` and be listed in
`[project.dynamic]`. `poetry-dynamic-versioning` needs `version` to remain
dynamic.

**Verification:** render each case in `tests/` and run `poetry check` until it is
silent, then `task build` to confirm the dynamic version is still applied to the
resulting artifacts.

This rewrites the metadata block of every generated project, so it deserves to
be the headline of its own release rather than a line item in a dependency bump.

## Enforce the documentation conventions in generated plugin projects

The conventions for user-facing plugin text - what belongs in the task
Expand Down
40 changes: 37 additions & 3 deletions Taskfile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ tasks:
deps:
- clean
cmds:
- copier copy --vcs-ref {{.TEMPLATE_REVISION}} . new_dir
- uvx --from 'copier>=9' copier copy --vcs-ref {{.TEMPLATE_REVISION}} . new_dir

check:generate:case:
desc: generate a project with the current version from a TEST_CASE
Expand All @@ -40,7 +40,7 @@ tasks:
- rm -rf {{.TEST_CASE}}_dir
- mkdir {{.TEST_CASE}}_dir
- cp tests/{{.TEST_CASE}}.yml {{.TEST_CASE}}_dir/.copier-answers.yml
- copier copy --vcs-ref {{.TEMPLATE_REVISION}} --defaults . {{.TEST_CASE}}_dir
- uvx --from 'copier>=9' copier copy --vcs-ref {{.TEMPLATE_REVISION}} --defaults . {{.TEST_CASE}}_dir
- git config --global init.defaultBranch develop
- cd {{.TEST_CASE}}_dir && git init .
- cd {{.TEST_CASE}}_dir && git add .
Expand Down Expand Up @@ -74,8 +74,42 @@ tasks:
cmds:
- task: check:hook:case
vars: {TEST_CASE: '{{.TEST_CASE}}'}
- cd {{.TEST_CASE}}_dir && poetry update
- cd {{.TEST_CASE}}_dir && uv sync
- cd {{.TEST_CASE}}_dir && task check
- task: check:build:case
vars: {TEST_CASE: '{{.TEST_CASE}}'}

check:build:case:
desc: build a generated project and check the shape of its artifacts
summary: |
Nothing else here runs `task build`, so a change to the sdist include
list - or a hatchling upgrade changing its defaults - would silently ship
tests, pipeline configuration and agent files inside every plugin while
every check stayed green.
internal: true
cmds:
- cd {{.TEST_CASE}}_dir && task build
- cmd: |
cd {{.TEST_CASE}}_dir
sdist=$(ls dist/*.tar.gz)
list=$(tar tzf "$sdist" | sed 's|^[^/]*/||' | sort -u)
for unwanted in tests/ .github/ .claude/; do
if printf '%s\n' "$list" | grep -q "^$unwanted"; then
echo "sdist must not ship $unwanted (found in $sdist)"
exit 1
fi
done
case "$sdist" in
*-0.0.0.tar.gz)
echo "version was not derived from git: $sdist"
exit 1
;;
esac
if grep -q '^-e ' dist/requirements.txt; then
echo "requirements.txt must not contain the project itself"
exit 1
fi
echo "artifact check ok: $sdist"

check:hook:case:
desc: smoke test the Stop hook a generated project ships
Expand Down
15 changes: 5 additions & 10 deletions src/.github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ jobs:

runs-on: ubuntu-latest
concurrency: testing_environment
env:
UV_LOCKED: "1"

steps:
- name: Check out repository
Expand All @@ -42,18 +44,11 @@ jobs:
- name: Install Task
uses: arduino/setup-task@v3

- name: Set up python
id: setup-python
uses: actions/setup-python@v7
- name: Install uv
uses: astral-sh/setup-uv@v10.3.0
with:
python-version: '3.13'

- name: Install and configure poetry
uses: snok/install-poetry@v1
with:
virtualenvs-create: true
virtualenvs-in-project: true
installer-parallel: true
enable-cache: true

- name: mypy
run: |
Expand Down
19 changes: 6 additions & 13 deletions src/.github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,16 @@ jobs:
- name: Install Task
uses: arduino/setup-task@v3

- name: Set up python
id: setup-python
uses: actions/setup-python@v7
- name: Install uv
uses: astral-sh/setup-uv@v10.3.0
with:
python-version: '3.13'

- name: Install and configure poetry
uses: snok/install-poetry@v1
with:
virtualenvs-create: true
virtualenvs-in-project: true
installer-parallel: true
- name: Build Package
run: uv build

- name: Publish Package
env:
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
run: |
poetry config pypi-token.pypi "$PYPI_TOKEN"
poetry publish --build
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }}
run: uv publish

Loading
Loading