You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
## Why is this change necessary?
The template had no `Release` workflow. The existing `Publish` /
`Publish to Staging` workflows re-run lint/test/build themselves,
duplicating everything CI already does for the commit. We want a release
path that instead relies on a passing CI run — modeled on the
`copier-nuxt-python-intranet-app` `release.yaml` — while publishing to
PyPI (using the existing `publish.yaml` as the PyPI reference).
While wiring this up we also found that instantiated repos hit pylint
`R0801 duplicate-code` between `git_tag.py` and
`extract_project_version.py`, because this repo carried a pre-refactor
fork of `git_tag.py` (symlinked from `copier_template_resources/`) that
parsed `pyproject.toml` itself.
## How does this change address the issue?
- **CI builds the distribution.** Added a `build` job to `ci.yaml.jinja`
that runs `uv build --no-sources` and uploads a
`python-package-distributions` artifact (`if-no-files-found: error`),
wired into `workflow-summary` (needs + fail-check).
- **New `release.yaml.jinja`.** Manual `workflow_dispatch` with a
`dry_run` input, generated for both libraries and executables. A `guard`
job requires the ci.yaml run for the current commit to be
`completed`/`success` (including the `workflow-summary` job), extracts
the version via `extract_project_version.py`, and confirms the tag is
absent — real releases must run from `main`. The flow then branches on
`deploy_as_executable`:
- **Library:** download the CI-built dist (never rebuilds), publish to
Test PyPI and verify a fresh install, push the `v<version>` tag, publish
to PyPI and verify again, then create a GitHub Release with generated
notes.
- **Executable:** push the `v<version>` tag, download every
`exe-{os}-{python}` artifact the CI `executable` job built, package each
(tar on Linux / zip on Windows), and attach them all to a GitHub
Release. No PyPI publish.
All tag/publish/release jobs are skipped on `dry_run`.
- **Aligned `git_tag.py` with base/nuxt.** Replaced the local fork with
the base version (`--version` required, no `pyproject.toml` parsing),
and collapsed the `copier_template_resources/` symlink into a real file
at `template/.github/workflows/git_tag.py` (matching its sibling
`extract_project_version.py`). Updated all call sites in `publish.yaml`
and `release.yaml` to source the version from
`extract_project_version.py` and pass `--version`.
- **Docs.** Documented the `Release` workflow in the generated README,
plus a TODO to eventually merge `Publish` / `Publish to Staging` into
`Release`.
## What side effects does this change have?
- CI now has an extra `build` job on every run (gated `{% if not
is_frozen_executable %}` to match `publish.yaml`).
- `git_tag.py`'s CLI changed: `--version` is now required and it no
longer prints/parses the version. Any external caller relying on the old
no-arg print behavior must switch to `extract_project_version.py`.
- `create-tag` gained a `needs` dependency (`guard` in `release.yaml`,
`get-values` in `publish.yaml`) to obtain the version.
- `template/.github/workflows/git_tag.py` changed from a symlink to a
regular file; `copier_template_resources/git_tag.py` was deleted.
## How is this change tested?
Rendered the template with `copier` (data1 / PyPI) and verified:
- `ci.yaml`, `release.yaml`, `publish.yaml` all parse as YAML; job
graph, `needs`, and `dry_run` guards asserted correct.
- All `git_tag.py` call sites pass `--version`; version sourced from
`extract_project_version.py`.
- pylint on the rendered scripts: `10.00/10` (previously exit 8 /
`R0801`). ruff and pyright pass on the source `git_tag.py`.
- CodeArtifact conditional branches render structurally (kept for parity
so existing CodeArtifact users aren't broken).
Final enforcement happens in the `lint-matrix` CI job, which generates
across data1/2/3 and runs pre-commit on the output.
## Other
- Fixed a pre-existing template bug: `is_frozen_executable` (used to
gate `entrypoint.py`, `main.py`, `publish.yaml`,
`publish_to_staging.yaml`) is undefined and evaluates falsy — so
executable projects shipped with no `entrypoint.py`/`main.py` yet still
got PyPI publish workflows. Renamed all guards (plus the CI `build` job)
to the real `deploy_as_executable` question. `release.yaml` is
intentionally ungated — it serves both cases and branches internally;
`publish.yaml` / `publish_to_staging.yaml` stay library-only. Verified
renders: executables now get `entrypoint.py`/`main.py`, no publish, and
a `release.yaml` that attaches the CI-built executables; libraries get
publish/release + the CI build job and no entrypoint/main.
- Test-data coverage gap (follow-up): after the fix, no
`tests/copier_data` file is PyPI + library (data1/data3 = PyPI +
executable, data2 = CodeArtifact + library), so `lint-matrix` no longer
exercises the PyPI-library publish/release path. Consider a data file
with `python_package_registry: PyPI` and `deploy_as_executable: no`.
- `release.yaml`'s `create-tag` pushes the tag before
`publish-to-primary`; the GitHub Release step then attaches to that
existing tag (softprops/action-gh-release handles a pre-existing tag
gracefully — no conflict).
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added a gated **Release** workflow with dry-run support, version
extraction, and safe Git tag checks.
* Generated templates now support publishing releases.
* Optional packaging and uploading of platform executables during
release.
* Added staging publishing and install/verification prior to production
release.
* **Improvements**
* CI now conditionally builds/uploads distribution artifacts for release
and updates the Codecov action reference.
* Templates now automate version extraction and tag management for the
release pipeline.
* **Documentation**
* Updated release guidance to reflect the new **Release** workflow and
deployment options.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: template/.github/workflows/ci.yaml.jinja
+49-3Lines changed: 49 additions & 3 deletions
Original file line number
Diff line number
Diff line change
@@ -97,7 +97,7 @@ jobs:
97
97
{%endraw%}{%ifuse_codecov%}{%raw%} - name: Upload coverage to Codecov
98
98
# only upload coverage from fastest job
99
99
if: ${{ matrix.JOB_MATCHING_DEV_ENV && github.actor != 'dependabot[bot]' }} # dependabot by default doesn't have access to the necessary secret...and dependabot should never be changing coverage anyway so it's fine not uploading it
- name: OIDC Auth for Installing any dependencies that uv may need for build (sometimes it likes to install setuptools...even if it's already in the package dependencies)
result=subprocess.run( # noqa: S603 # this is trusted input, it's our own arguments being passed in
15
+
["git", "ls-remote", "--exit-code", "--tags", remote, f"refs/tags/{tag}"], # noqa: S607 # if `git` isn't in PATH already, then there are bigger problems to solve
16
+
stdout=subprocess.DEVNULL,
17
+
check=False,
18
+
)
19
+
ifresult.returncode==0:
20
+
raiseException(f"Error: tag '{tag}' exists on remote '{remote}'") # noqa: TRY002 # not worth a custom exception
21
+
if (
22
+
result.returncode!=no_matching_refs_return_code
23
+
): # anything else is a real error (bad remote, auth failure, network)
24
+
raiseException(f"git ls-remote exited with code {result.returncode} (remote={remote!r})") # noqa: TRY002 # not worth a custom exception
25
+
26
+
27
+
defmain() ->None:
28
+
parser=argparse.ArgumentParser(
29
+
description=("Confirm that git tag v<version> is not present on a remote, or create and push the tag.")
30
+
)
31
+
_=parser.add_argument(
32
+
"--version",
33
+
required=True,
34
+
help="Version string (e.g. 1.0.6 or v1.0.6); the tag will always be v<version>",
_=subprocess.run(["git", "tag", tag], check=True) # noqa: S603,S607 # this is trusted input, it's our own version string. and if `git` isn't in PATH, then there are larger problems anyway
59
+
_=subprocess.run(["git", "push", args.remote, tag], check=True) # noqa: S603,S607 # this is trusted input, it's our own version string. and if `git` isn't in PATH, then there are larger problems anyway
0 commit comments