making-a-release · git:20260812.45a6059 · 2026-08-12 · sha256 fa4406ebe45552e7
making-a-release git:20260812.45a6059A
Immutable. This exact content never changes and is served at /api/v1/blob/fa4406ebe45552e7.
--- 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 DMG build/release workflow. --- # 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). **`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 ``` ## 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` | | 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. - **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.