diff --git a/.github/workflows/bump-v1.yml b/.github/workflows/bump-v1.yml index fe51165..2943e7b 100644 --- a/.github/workflows/bump-v1.yml +++ b/.github/workflows/bump-v1.yml @@ -106,6 +106,47 @@ jobs: print(f"changelog documents {version}, and [Unreleased] is empty") PY + # Runs after every refusal above, so a release that is going to be turned + # away never has its tag rewritten. + - name: Annotate the release tag + env: + GH_TOKEN: ${{ github.token }} + RELEASE_SHA: ${{ github.sha }} + RELEASE_TAG: ${{ github.event.release.tag_name }} + run: | + set -euo pipefail + # The Move step below puts v1 on this same commit, and copier reads a + # template's version with `git describe --tags`. That prefers an + # annotated tag over a lightweight one, and falls back to the newest + # when they tie -- so with both lightweight it answers "v1", copier + # parses that as version 1, and every consumer above 1.0.0 fails + # `copier update` with "Downgrades are not supported". Not merely the + # scheduled runs: an explicit `--vcs-ref v1.8.0` reads the same commit + # and fails identically, which strands the project entirely. + # + # Publishing a release for a tag that does not exist yet -- the GitHub + # UI's default, and `gh release create` without --verify-tag -- makes + # that lightweight tag. Rather than refuse the release over it, give + # the tag an annotation here so it outranks v1 and describe answers + # "v1.8.0". Annotating v1 too would break it again: v1 is re-tagged on + # every release, so it would always be the newer of the two. + # + # The rewrite keeps the same target commit, and the release references + # the tag by name, so the published release is unaffected. It lands + # seconds after publication, before the tag has realistically been + # fetched anywhere. + type=$(gh api "repos/${GITHUB_REPOSITORY}/git/ref/tags/${RELEASE_TAG}" --jq .object.type) + if [ "$type" = "tag" ]; then + echo "${RELEASE_TAG} is already annotated" + exit 0 + fi + tag_sha=$(gh api -X POST "repos/${GITHUB_REPOSITORY}/git/tags" \ + -f tag="${RELEASE_TAG}" -f message="${RELEASE_TAG}" \ + -f object="${RELEASE_SHA}" -f type=commit --jq .sha) + gh api -X PATCH "repos/${GITHUB_REPOSITORY}/git/refs/tags/${RELEASE_TAG}" \ + -f sha="${tag_sha}" -F force=true + echo "${RELEASE_TAG} annotated as ${tag_sha} (still on ${RELEASE_SHA})" + - name: Move v1 env: GH_TOKEN: ${{ github.token }} diff --git a/CHANGELOG.md b/CHANGELOG.md index cc848c8..27a7ec9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), Two things are worth knowing about how versions work here, because this repo ships CI rather than a package: - **`v1` is a moving major tag.** Consumers pin `python-ci.yml@v1` and `node-ci.yml@v1`, so publishing a release is what actually delivers a change to them — `.github/workflows/bump-v1.yml` moves `v1` onto each published `v1.x` release. Changes to the reusable workflows reach every project the moment that happens, with no `copier update` needed. +- **Release tags are annotated; `v1` stays lightweight.** They end up on the same commit, and copier reads a template's version with `git describe --tags`, which prefers the annotated tag. Get this backwards and copier reads the version as `1` and refuses every consumer's update as a downgrade. `bump-v1.yml` maintains this on its own — it annotates the release tag if publishing left it lightweight, and creates `v1` lightweight — so releases can still be cut from the GitHub UI. - **Changes to *scaffolded* files reach projects only through `copier update`.** `.pre-commit-config.yaml`, `biome.json`, `pyproject.toml` and friends are copied at scaffold time, so a project picks them up when it runs an update — automatically if it opted into `template-update.yml`. Entries for 1.0.0 through 1.5.2 were backfilled from git history after the fact, so they describe what each tag contained rather than having been written alongside it. ## [Unreleased] +## [1.8.1] - 2026-08-07 + +Repairs 1.8.0, which shipped a lightweight tag and left every project unable to update. Consumers land on 1.8.1 rather than 1.8.0; the contents are the same bar this fix. + +### Fixed + +- `bump-v1.yml` annotates the release tag before moving `v1` onto the same commit. Copier reads a template's version with `git describe --tags`, which prefers an annotated tag and otherwise takes the newest — so with both lightweight it answered `v1`, copier parsed that as version `1`, and every consumer above 1.0.0 failed `copier update` with "Downgrades are not supported". This stranded projects completely: an explicit `--vcs-ref v1.8.0` reads the same commit and failed identically, so there was no working update path at all. Publishing a release for a tag that doesn't exist yet creates a lightweight one, so the annotation is applied here rather than asked of whoever cuts the release. It has been wrong since 1.6.0, the first release tagged this way. + ## [1.8.0] - 2026-08-07 Swaps the type checker. Consumers pinned to `v1` keep passing without doing anything — the workflow's type-check step falls back to mypy — so the migration happens per project, on its next `copier update`. @@ -168,4 +177,5 @@ The largest release so far: an optional TypeScript side, automated template upda [1.6.0]: https://github.com/MattFisher/python-project-template/compare/v1.5.2...v1.6.0 [1.7.0]: https://github.com/MattFisher/python-project-template/compare/v1.6.0...v1.7.0 [1.8.0]: https://github.com/MattFisher/python-project-template/compare/v1.7.0...v1.8.0 -[unreleased]: https://github.com/MattFisher/python-project-template/compare/v1.8.0...HEAD +[1.8.1]: https://github.com/MattFisher/python-project-template/compare/v1.8.0...v1.8.1 +[unreleased]: https://github.com/MattFisher/python-project-template/compare/v1.8.1...HEAD