Releases, Tags & Versioning
A release turns a set of merged pull requests into something players or customers receive. Maintainers run releases, but every developer needs to understand version numbers, tags, and how their pull request titles become release notes. This guide covers all three.
Step 1: Understand Semantic Versioning
Studio projects use Semantic Versioning (opens in a new tab): MAJOR.MINOR.PATCH, for example 2.4.1. Each part tells users how risky an upgrade is.
| Bump | When | Commit types that cause it | Example |
|---|---|---|---|
| MAJOR | A breaking change: existing users or integrations must change something. | feat!: or a BREAKING CHANGE: footer | 2.4.1 → 3.0.0 |
| MINOR | New, backwards-compatible functionality. | feat: | 2.4.1 → 2.5.0 |
| PATCH | Backwards-compatible bug fixes only. | fix:, perf: | 2.4.1 → 2.4.2 |
- Bumping a number resets those to its right: 2.4.1 → 2.5.0, not 2.5.1.
- Pre-releases add a suffix:
2.5.0-rc.1is a release candidate, sorted before2.5.0. - Versions
0.x.ymean "still in early development": anything may change. Games in early access often stay at0.x. - The Conventional Commit types from the Daily Git Terminal Workflow map directly onto these bumps, which is one reason we use them.
Step 2: Mark Releases with Annotated Tags
A tag is a permanent label on a commit. Unlike a branch, it never moves, so v2.5.0 always means exactly the code that shipped. Always use annotated tags (-a) for releases; they record who tagged, when, and a message. Tag names start with v.
git switch main && git pull
git tag -a v2.5.0 -m "Release 2.5.0" # tag the current commit
git push origin v2.5.0 # tags are not pushed by a normal git push
git tag -l "v2.*" # list tags
git show v2.5.0 # what's in a tag
git switch --detach v2.4.1 # look at an old release (detached HEAD)
Never move or delete a published tag. Other people, build systems, and package managers rely on it meaning one thing forever. If a release is wrong, publish a new version instead.
Step 3: Follow the Release Flow
Work moves through the protected branches in one direction. Maintainers run each promotion; developers keep their pull requests small and well-titled so each stage is easy to test.
feature/issue-42 ─┐
feature/issue-57 ─┼─▶ Dev ──promote──▶ QA ──promote──▶ main ──▶ tag v2.5.0 ──▶ GitHub Release
fix/issue-61 ─────┘ (integration) (release (production)
candidate,
v2.5.0-rc.1)
- Dev: finished work accumulates through reviewed pull requests.
- QA: a maintainer promotes Dev to QA when a release is ready to test. QA and BAs test the release candidate there; fixes go into Dev and are promoted again.
- main: once QA signs off, QA is promoted to main, the release commit is tagged, and the GitHub Release is published.
Step 4: Publish a GitHub Release
A GitHub Release wraps a tag with release notes and optional downloadable files, such as game builds. Maintainers publish them from the terminal:
gh release create v2.5.0 --title "v2.5.0" --generate-notes # notes built from merged PRs
gh release create v2.5.0-rc.1 --prerelease --generate-notes # mark as a pre-release
gh release create v2.5.0 --draft --generate-notes ./Builds/Win64.zip # draft with an attached build
gh release list
gh release view v2.5.0
gh release download v2.5.0 --pattern "*.zip"
Publish as a draft first, review the notes, then publish from the GitHub web page. If the tag doesn't exist yet, gh release create creates it from the default branch, so tag explicitly first to be certain which commit you're releasing.
Step 5: Write Release Notes People Can Use
--generate-notes lists the titles of the pull requests merged since the previous release. That makes your PR title part of the release notes: "Fix crash when saving to a read-only folder" helps readers, "Update SaveSystem.cs" doesn't. Group the notes by label with a .github/release.yml file:
changelog:
exclude:
labels: [chore]
categories:
- title: New Features
labels: [feature]
- title: Bug Fixes
labels: [bug]
- title: Documentation
labels: [docs]
- title: Other Changes
labels: ["*"]
Generated notes are a starting point. For player- or customer-facing releases, a maintainer or BA rewrites them for that audience: lead with what changed for them, explain breaking changes and any action they need to take, and leave out internal refactors.
Step 6: Understand Hotfixes
Sometimes production has an urgent bug and the fix can't wait for the next Dev → QA → main cycle. A hotfix is the one exception to the normal flow, and only maintainers start one:
- A
hotfix/…branch is created frommain(the code that's actually in production). - The minimal fix goes through a reviewed pull request into
main, and is tagged as a PATCH release (v2.5.1). - The fix is then merged back into
QAandDev, so it isn't lost in the next release.
If you're asked to write a hotfix, keep it as small as possible: fix the bug and nothing else.
Step 7: Keep Version Numbers in Sync
The version in the code should match the tag, so a crash report or support ticket tells you exactly which code was running. Update it in the release pull request, using a chore(release): v2.5.0 commit.
| Project type | Where the version lives |
|---|---|
| Unity | Project Settings > Player > Version (stored in ProjectSettings/ProjectSettings.asset). Android and iOS also have a build number that must increase with every store upload. |
| .NET | <Version>2.5.0</Version> in the .csproj, or a shared Directory.Build.props. |
| Node.js | "version" in package.json; npm version 2.5.0 --no-git-tag-version updates it and the lockfile. |
| Unity packages (UPM) | "version" in the package's package.json, matching the Git tag consumers pin to. |