From 226258c01cbab72e8e28224f0a0731bcdca54a80 Mon Sep 17 00:00:00 2001 From: jufty-bot Date: Fri, 9 Oct 2026 00:17:20 -0600 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20Keep=20source=20version=20at=200?= =?UTF-8?q?.0.0=20and=20stamp=20release=20builds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- copier.yaml | 4 -- ...sh_to_docker_hub %}publish.yaml{% endif %} | 10 +-- template/.releaserc.json | 9 +-- template/Taskfile.yaml | 13 ++-- template/docs/contributing.md | 8 +++ template/pyproject.toml | 2 +- ...build_docker_image %}Dockerfile{% endif %} | 8 +-- tests/answers.yml | 1 - .../.github/workflows/publish.yaml | 10 +-- tests/example-project/.releaserc.json | 9 +-- tests/example-project/Dockerfile | 8 +-- tests/example-project/Taskfile.yaml | 13 ++-- tests/example-project/docs/contributing.md | 8 +++ tests/example-project/pyproject.toml | 2 +- tests/test_copier.py | 62 +++++++++++++++++++ 15 files changed, 110 insertions(+), 57 deletions(-) diff --git a/copier.yaml b/copier.yaml index ff04648..4a6c3eb 100644 --- a/copier.yaml +++ b/copier.yaml @@ -24,10 +24,6 @@ project_description: type: str help: Short summary of the project. default: Brief description of the project -version: - type: str - help: Initial package version. - default: 0.1.0 user_name: type: str help: Primary project author's name. diff --git a/template/.github/workflows/{% if publish_to_pypi or publish_to_docker_hub %}publish.yaml{% endif %} b/template/.github/workflows/{% if publish_to_pypi or publish_to_docker_hub %}publish.yaml{% endif %} index 9365eca..cecbd74 100644 --- a/template/.github/workflows/{% if publish_to_pypi or publish_to_docker_hub %}publish.yaml{% endif %} +++ b/template/.github/workflows/{% if publish_to_pypi or publish_to_docker_hub %}publish.yaml{% endif %} @@ -26,6 +26,8 @@ jobs: setup-uv: true task: dist github-token: ${{"{{"}} secrets.GITHUB_TOKEN {{"}}"}} + env: + RELEASE_VERSION: ${{"{{"}} github.event.release.tag_name {{"}}"}} - name: pypi uses: pypa/gh-action-pypi-publish@release/v1{% endif %} {% if publish_to_docker_hub == True %} @@ -40,11 +42,9 @@ jobs: username: ${{"{{"}} secrets.DOCKER_USERNAME {{"}}"}} password: ${{"{{"}} secrets.DOCKER_TOKEN {{"}}"}} - name: version - run: | - TAG_NAME="${{"{{"}} github.event.release.tag_name {{"}}"}}" - RELEASE_VERSION="${TAG_NAME#v}" - echo "RELEASE_VERSION=${RELEASE_VERSION}" - echo "RELEASE_VERSION=${RELEASE_VERSION}" >> ${GITHUB_ENV} + env: + TAG_NAME: ${{"{{"}} github.event.release.tag_name {{"}}"}} + run: echo "RELEASE_VERSION=${TAG_NAME#v}" >> "$GITHUB_ENV" - name: docker uses: juftin/actions/taskfile@v1 with: diff --git a/template/.releaserc.json b/template/.releaserc.json index d5e6010..201c160 100644 --- a/template/.releaserc.json +++ b/template/.releaserc.json @@ -19,14 +19,7 @@ [ "@semantic-release/exec", { - "prepareCmd": "uv version ${nextRelease.version} && uv build" - } - ], - [ - "@semantic-release/git", - { - "assets": ["pyproject.toml", "uv.lock"], - "message": "🔖 {{ package_name }} ${nextRelease.version}\n\n${nextRelease.notes}\n[skip ci]" + "prepareCmd": "UV_LOCKED=0 uv version --frozen ${nextRelease.version} && uv build && UV_LOCKED=0 uv version --frozen 0.0.0" } ], [ diff --git a/template/Taskfile.yaml b/template/Taskfile.yaml index 9dc0c4e..231f1f1 100644 --- a/template/Taskfile.yaml +++ b/template/Taskfile.yaml @@ -108,20 +108,20 @@ tasks: desc: Build Distribution Artifacts cmds: - cmd: rm -rf {{"{{"}}.ROOT_DIR{{"}}"}}/dist + - cmd: UV_LOCKED=0 uv version --frozen "{{"{{"}}.RELEASE_VERSION | default "0.0.0"{{"}}"}}" - cmd: uv build + - cmd: UV_LOCKED=0 uv version --frozen 0.0.0 ####################################### docker: desc: Build Docker Image cmd: | docker buildx build \ - --tag {{"{{"}}.DOCKER_REPO{{"}}"}}:{{"{{"}}.RELEASE_VERSION{{"}}"}} \ + --build-arg RELEASE_VERSION={{"{{"}}.RELEASE_VERSION | default "0.0.0"{{"}}"}} \ + --tag {{"{{"}}.DOCKER_REPO{{"}}"}}:{{"{{"}}.RELEASE_VERSION | default "local"{{"}}"}} \ --tag {{"{{"}}.DOCKER_REPO{{"}}"}}:latest \ --file {{"{{"}}.ROOT_DIR{{"}}"}}/Dockerfile \ {{"{{"}}.CLI_ARGS | default ""{{"}}"}} \ {{"{{"}}.ROOT_DIR{{"}}"}} - vars: - RELEASE_VERSION: - ref: .RELEASE_VERSION | default "local" requires: vars: [DOCKER_REPO] status: @@ -164,12 +164,9 @@ tasks: - | docker run \ --rm -it \ - {{"{{"}}.DOCKER_REPO{{"}}"}}:{{"{{"}}.DOCKER_TAG_PREFIX{{"}}"}}{{"{{"}}.RELEASE_VERSION{{"}}"}} \ + {{"{{"}}.DOCKER_REPO{{"}}"}}:{{"{{"}}.DOCKER_TAG_PREFIX{{"}}"}}{{"{{"}}.RELEASE_VERSION | default "local"{{"}}"}} \ {{"{{"}}.CLI_ARGS{{"}}"}} deps: ["docker"] - vars: - RELEASE_VERSION: - ref: .RELEASE_VERSION | default "local" status: - test ! -f {{"{{"}}.ROOT_DIR{{"}}"}}/{{"{{"}}.DOCKERFILE | default "Dockerfile"{{"}}"}} requires: diff --git a/template/docs/contributing.md b/template/docs/contributing.md index 170d9c9..fc341d7 100644 --- a/template/docs/contributing.md +++ b/template/docs/contributing.md @@ -91,6 +91,14 @@ checks on the code before it is committed. You can install the pre-commit hooks manually, or they will be set up when you run `task install`. This project uses [semantic-versioning] standards, managed by [semantic-release]. + +The source version stays at `0.0.0`. Releases create tags and artifacts without +committing version changes. Package and Docker builds use `RELEASE_VERSION` from +the release tag (with an optional `v` prefix); local builds default to `0.0.0`. +For example, `RELEASE_VERSION=v1.2.3 task build` builds version `1.2.3`. +Package builds reset the source version to `0.0.0` after building and leave +`uv.lock` unchanged. Docker applies the version inside the image. + Releases for this project are handled entirely by CI/CD via pull requests being merged into the `main` branch. Contributions follow the [gitmoji] standards with [conventional commits]. diff --git a/template/pyproject.toml b/template/pyproject.toml index dbb8fe1..5b76e73 100644 --- a/template/pyproject.toml +++ b/template/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "{{ package_name }}" -version = "{{ version }}" +version = "0.0.0" description = "{{ project_description }}" readme = "README.md" requires-python = ">=3.10" diff --git a/template/{% if build_docker_image %}Dockerfile{% endif %} b/template/{% if build_docker_image %}Dockerfile{% endif %} index 09531ce..8b8653b 100644 --- a/template/{% if build_docker_image %}Dockerfile{% endif %} +++ b/template/{% if build_docker_image %}Dockerfile{% endif %} @@ -21,12 +21,12 @@ COPY pyproject.toml /app/pyproject.toml COPY uv.lock /app/uv.lock COPY src/{{ project_slug }} /app/src/{{ project_slug }} -# Install project +# Install project with the release version, retaining locked dependencies. +ARG RELEASE_VERSION=0.0.0 RUN --mount=from=ghcr.io/astral-sh/uv,source=/uv,target=/bin/uv \ --mount=type=cache,target=/root/.cache/uv \ - --mount=type=bind,source=uv.lock,target=uv.lock \ - --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ - uv sync --no-dev --all-extras + UV_LOCKED=0 uv version --frozen "${RELEASE_VERSION#v}" && \ + UV_LOCKED=0 uv sync --frozen --no-dev --all-extras # Environment Variables ENV PATH="/app/.venv/bin:${PATH}" \ diff --git a/tests/answers.yml b/tests/answers.yml index 346cfa9..4f03f6f 100644 --- a/tests/answers.yml +++ b/tests/answers.yml @@ -1,7 +1,6 @@ project_name: example-project package_name: example-project project_description: Brief description of the project -version: 0.1.0 user_name: Justin Flannery user_email: juftin@juftin.com github_user: juftin diff --git a/tests/example-project/.github/workflows/publish.yaml b/tests/example-project/.github/workflows/publish.yaml index db2484e..463eaf2 100644 --- a/tests/example-project/.github/workflows/publish.yaml +++ b/tests/example-project/.github/workflows/publish.yaml @@ -25,6 +25,8 @@ jobs: setup-uv: true task: dist github-token: ${{ secrets.GITHUB_TOKEN }} + env: + RELEASE_VERSION: ${{ github.event.release.tag_name }} - name: pypi uses: pypa/gh-action-pypi-publish@release/v1 @@ -39,11 +41,9 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_TOKEN }} - name: version - run: | - TAG_NAME="${{ github.event.release.tag_name }}" - RELEASE_VERSION="${TAG_NAME#v}" - echo "RELEASE_VERSION=${RELEASE_VERSION}" - echo "RELEASE_VERSION=${RELEASE_VERSION}" >> ${GITHUB_ENV} + env: + TAG_NAME: ${{ github.event.release.tag_name }} + run: echo "RELEASE_VERSION=${TAG_NAME#v}" >> "$GITHUB_ENV" - name: docker uses: juftin/actions/taskfile@v1 with: diff --git a/tests/example-project/.releaserc.json b/tests/example-project/.releaserc.json index 22193cb..201c160 100644 --- a/tests/example-project/.releaserc.json +++ b/tests/example-project/.releaserc.json @@ -19,14 +19,7 @@ [ "@semantic-release/exec", { - "prepareCmd": "uv version ${nextRelease.version} && uv build" - } - ], - [ - "@semantic-release/git", - { - "assets": ["pyproject.toml", "uv.lock"], - "message": "🔖 example-project ${nextRelease.version}\n\n${nextRelease.notes}\n[skip ci]" + "prepareCmd": "UV_LOCKED=0 uv version --frozen ${nextRelease.version} && uv build && UV_LOCKED=0 uv version --frozen 0.0.0" } ], [ diff --git a/tests/example-project/Dockerfile b/tests/example-project/Dockerfile index 43240a9..32c459a 100644 --- a/tests/example-project/Dockerfile +++ b/tests/example-project/Dockerfile @@ -21,12 +21,12 @@ COPY pyproject.toml /app/pyproject.toml COPY uv.lock /app/uv.lock COPY src/example_project /app/src/example_project -# Install project +# Install project with the release version, retaining locked dependencies. +ARG RELEASE_VERSION=0.0.0 RUN --mount=from=ghcr.io/astral-sh/uv,source=/uv,target=/bin/uv \ --mount=type=cache,target=/root/.cache/uv \ - --mount=type=bind,source=uv.lock,target=uv.lock \ - --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ - uv sync --no-dev --all-extras + UV_LOCKED=0 uv version --frozen "${RELEASE_VERSION#v}" && \ + UV_LOCKED=0 uv sync --frozen --no-dev --all-extras # Environment Variables ENV PATH="/app/.venv/bin:${PATH}" \ diff --git a/tests/example-project/Taskfile.yaml b/tests/example-project/Taskfile.yaml index 3094049..9b9af06 100644 --- a/tests/example-project/Taskfile.yaml +++ b/tests/example-project/Taskfile.yaml @@ -108,20 +108,20 @@ tasks: desc: Build Distribution Artifacts cmds: - cmd: rm -rf {{.ROOT_DIR}}/dist + - cmd: UV_LOCKED=0 uv version --frozen "{{.RELEASE_VERSION | default "0.0.0"}}" - cmd: uv build + - cmd: UV_LOCKED=0 uv version --frozen 0.0.0 ####################################### docker: desc: Build Docker Image cmd: | docker buildx build \ - --tag {{.DOCKER_REPO}}:{{.RELEASE_VERSION}} \ + --build-arg RELEASE_VERSION={{.RELEASE_VERSION | default "0.0.0"}} \ + --tag {{.DOCKER_REPO}}:{{.RELEASE_VERSION | default "local"}} \ --tag {{.DOCKER_REPO}}:latest \ --file {{.ROOT_DIR}}/Dockerfile \ {{.CLI_ARGS | default ""}} \ {{.ROOT_DIR}} - vars: - RELEASE_VERSION: - ref: .RELEASE_VERSION | default "local" requires: vars: [DOCKER_REPO] status: @@ -164,12 +164,9 @@ tasks: - | docker run \ --rm -it \ - {{.DOCKER_REPO}}:{{.DOCKER_TAG_PREFIX}}{{.RELEASE_VERSION}} \ + {{.DOCKER_REPO}}:{{.DOCKER_TAG_PREFIX}}{{.RELEASE_VERSION | default "local"}} \ {{.CLI_ARGS}} deps: ["docker"] - vars: - RELEASE_VERSION: - ref: .RELEASE_VERSION | default "local" status: - test ! -f {{.ROOT_DIR}}/{{.DOCKERFILE | default "Dockerfile"}} requires: diff --git a/tests/example-project/docs/contributing.md b/tests/example-project/docs/contributing.md index 170d9c9..fc341d7 100644 --- a/tests/example-project/docs/contributing.md +++ b/tests/example-project/docs/contributing.md @@ -91,6 +91,14 @@ checks on the code before it is committed. You can install the pre-commit hooks manually, or they will be set up when you run `task install`. This project uses [semantic-versioning] standards, managed by [semantic-release]. + +The source version stays at `0.0.0`. Releases create tags and artifacts without +committing version changes. Package and Docker builds use `RELEASE_VERSION` from +the release tag (with an optional `v` prefix); local builds default to `0.0.0`. +For example, `RELEASE_VERSION=v1.2.3 task build` builds version `1.2.3`. +Package builds reset the source version to `0.0.0` after building and leave +`uv.lock` unchanged. Docker applies the version inside the image. + Releases for this project are handled entirely by CI/CD via pull requests being merged into the `main` branch. Contributions follow the [gitmoji] standards with [conventional commits]. diff --git a/tests/example-project/pyproject.toml b/tests/example-project/pyproject.toml index 82d7216..99f7419 100644 --- a/tests/example-project/pyproject.toml +++ b/tests/example-project/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "example-project" -version = "0.1.0" +version = "0.0.0" description = "Brief description of the project" readme = "README.md" requires-python = ">=3.10" diff --git a/tests/test_copier.py b/tests/test_copier.py index 5062dfd..783d0b9 100644 --- a/tests/test_copier.py +++ b/tests/test_copier.py @@ -1,8 +1,12 @@ """Verify that Copier renders and updates the expected Python project.""" +import json +import os import shutil import subprocess import sys +import tarfile +import zipfile from pathlib import Path import pytest @@ -355,3 +359,61 @@ def test_copier_removes_consolidated_ci_workflows(tmp_path: Path) -> None: assert (project / ".github/workflows/ci.yaml").is_file() for filename in legacy_workflows: assert not (project / ".github/workflows" / filename).exists() + + +@pytest.mark.parametrize( + ("release_ref", "expected_version"), + [("", "0.0.0"), ("v1.2.3", "1.2.3"), ("2.0.0-beta.1", "2.0.0b1")], +) +def test_distribution_uses_release_version_without_changing_source( + rendered_project: Path, + release_ref: str, + expected_version: str, +) -> None: + """Stamp wheel and sdist metadata while preserving source and lock bytes.""" + pyproject = rendered_project / "pyproject.toml" + assert 'version = "0.0.0"' in pyproject.read_text() + original_pyproject = pyproject.read_bytes() + lockfile = rendered_project / "uv.lock" + lockfile.write_text("version = 1\n") + original_lockfile = lockfile.read_bytes() + + task_executable = shutil.which("task") + assert task_executable is not None + result = subprocess.run( + [task_executable, "dist"], + cwd=rendered_project, + env={**os.environ, "RELEASE_VERSION": release_ref}, + check=False, + capture_output=True, + text=True, + ) + + assert result.returncode == 0, result.stderr + assert pyproject.read_bytes() == original_pyproject + assert lockfile.read_bytes() == original_lockfile + wheel = next((rendered_project / "dist").glob("*.whl")) + with zipfile.ZipFile(wheel) as archive: + metadata_path = next( + name for name in archive.namelist() if name.endswith(".dist-info/METADATA") + ) + assert f"Version: {expected_version}\n" in archive.read(metadata_path).decode() + sdist = next((rendered_project / "dist").glob("*.tar.gz")) + with tarfile.open(sdist) as archive: + metadata = next( + member + for member in archive.getmembers() + if member.name.endswith("/PKG-INFO") + ) + metadata_file = archive.extractfile(metadata) + assert metadata_file is not None + assert f"Version: {expected_version}\n" in metadata_file.read().decode() + + +def test_releases_do_not_commit_version_changes(rendered_project: Path) -> None: + """Create release artifacts without configuring release process commits.""" + release_config = json.loads((rendered_project / ".releaserc.json").read_text()) + plugins = release_config["plugins"] + assert "@semantic-release/git" not in [ + plugin[0] if isinstance(plugin, list) else plugin for plugin in plugins + ]