making-a-release · git:20261022.2dc9974 · 2026-10-22 · sha256 5695e7e0f195e967
making-a-release git:20261022.2dc9974A
Immutable. This exact content never changes and is served at /api/v1/blob/5695e7e0f195e967.
--- name: making-a-release description: Use when cutting a new fused-render release, bumping the version, or creating a release tag — bumps __version__, lands it on protected main via a bump PR, then tags vX.Y.Z and pushes the tag to trigger the release workflow (DMG/AppImage builds, GitHub Release, PyPI upload). --- # Making a Release ## Overview A release is: bump `__version__`, land that bump on `main` **through a PR**, then tag the merged commit `vX.Y.Z` and push the tag. Pushing the tag is what triggers `.github/workflows/release.yml`: build → sign → notarize → publish DMG + GitHub Release, and in parallel `build-pypi` → `publish-pypi`, which uploads the wheel to https://pypi.org/p/fused-render through trusted publishing. **`main` is a protected branch.** Direct pushes are rejected (`GH006: Changes must be made through a pull request`), so the bump PR is not optional — it is the only path. Do not try `git push origin main` first. **Single source of truth:** the version lives ONLY in `fused_render/__init__.py`. `pyproject.toml` derives it dynamically (`[tool.hatch.version]`) — never edit a version into `pyproject.toml`. **The invariant that matters:** the git tag name must equal `__version__`. Tag `v0.3.5` ⟺ `__version__ = "0.3.5"`. A mismatch ships the wrong version silently. ## Steps 1. **Start clean and on the latest `main`.** Always pull first — tagging a stale commit ships an old build. Use `--ff-only` so a diverged local branch errors out loudly instead of creating a merge commit: ```bash git switch main git pull --ff-only origin main # REQUIRED: get the latest before releasing git status --porcelain ``` If `git pull --ff-only` fails, your local `main` has diverged — reconcile it before continuing. If `git status --porcelain` prints anything, stop and resolve it first. 2. **Pick the new version.** Read the current value and the latest tag; choose the next semver: ```bash grep __version__ fused_render/__init__.py git tag --sort=-creatordate | head -1 ``` 3. **Branch, then bump.** Work on a branch from the start — `main` is protected: ```bash git switch -c bump-X.Y.Z ``` Bump `__version__` in `fused_render/__init__.py` (Edit tool). This is the only file to change. 4. **Commit** (message matches the repo's history convention exactly): ```bash git commit -am "Bump version to X.Y.Z" ``` 5. **Open the bump PR** and let it merge. Push the branch and open the PR with the `creating-pull-requests` skill (title `Bump version to X.Y.Z`), then enable auto-merge so it lands as soon as checks go green: ```bash git push -u origin HEAD gh pr create --title "Bump version to X.Y.Z" --body-file <body.md> gh pr merge <N> --squash --auto ``` Poll until it actually reports `MERGED` — do NOT tag before then: ```bash gh pr view <N> --json state,mergeStateStatus,mergeCommit ``` 6. **Return to `main` and pull the merged bump.** The squash merge creates a NEW commit, so the tag must point at that commit, not your local branch commit: ```bash git switch main git pull --ff-only origin main grep __version__ fused_render/__init__.py # confirm it reads X.Y.Z ``` 7. **Tag** — annotated, name is `v` + the exact version: ```bash git tag -a vX.Y.Z -m "vX.Y.Z" ``` 8. **Push the tag.** This is the release trigger: ```bash git push origin vX.Y.Z ``` 9. **Verify** the release workflow started, then clean up the branch: ```bash gh run list --workflow=release.yml --limit 3 git branch -d bump-X.Y.Z && git push origin --delete bump-X.Y.Z ``` Once the run finishes, confirm PyPI has the version: ```bash curl -s https://pypi.org/pypi/fused-render/json | python3 -c 'import json,sys; print(json.load(sys.stdin)["info"]["version"])' ``` If `publish-pypi` failed, rerun just that job (`gh run rerun <run-id> --failed`); `build-pypi` refusing a tag/version mismatch means the tag is wrong, not the job. ## Quick Reference | Thing | Value | |-------|-------| | Version source | `fused_render/__init__.py` → `__version__` | | Tag format | `vX.Y.Z` (must match `__version__`) | | Release trigger | pushing a `v*` tag → `.github/workflows/release.yml` | | PyPI | `publish-pypi` job in the same run; wheel only, trusted publishing, environment `pypi` | | Commit message | `Bump version to X.Y.Z` | | How the bump lands | bump PR into protected `main`, squash-merged (never a direct push) | | What gets tagged | the squash-merge commit on `main`, after `git pull` | | Do NOT edit | `pyproject.toml` version (dynamic via hatchling) | ## Common Mistakes - **Editing the version in `pyproject.toml`.** It's dynamic; the value there is ignored/derived. Edit `__init__.py` only. - **Tag name ≠ `__version__`.** e.g. tagging `v0.3.5` while `__version__` is still `0.3.4`. Bump and commit *before* tagging. - **Committing the bump straight onto local `main`.** The push will be rejected by branch protection and you'll have to move the commit to a branch anyway (`git branch bump-X.Y.Z && git reset --hard origin/main`). Branch first (step 3). - **Tagging your local bump commit instead of the merged one.** The squash merge produces a different SHA; tagging the pre-merge commit points the release at a commit that is not on `main`. Pull `main` first (step 6), then tag. - **Tagging before the PR is actually merged.** "Auto-merge enabled" is not merged — wait for `state: MERGED`. - **Forgetting the tag push.** No tag push = no release, even once the bump is on `main`. - **Reusing an existing tag.** Tags are immutable releases; pick an unused version. Check `git tag` first. PyPI is stricter still: once a version is uploaded it can never be uploaded again, even after deleting it there. A broken release ships as the next patch version. - **Skipping `git pull`.** Tagging a stale local `main` builds and ships an old commit. Always pull (steps 1 and 6) so the tag points at the true latest.