Skip to content

Release branches and tags

  • Lesson 13
  • 30 min
  • Module 5: Release and hotfix practice
Lesson 13 of 15 · Git and Team Development EssentialsRelease branches and tags

What you will be able to do

Give the help desk a version of the notes that does not change while your team continues to work. After this lesson, you can say which reference holds new work, which holds the next release, and which holds the released version.

Before you startWhat to know and to have readyAbout 30 minutes

What you need to know

What you need on your computer

  • The folder ~/projects/nextia-learning-labs. You cloned it in Pull-request sequence: Set up the practice lab. If it is missing, do step 2 of that section.
  • Git, with your name and email address, from Set up Git: Step 2: tell Git who you are. Git writes them in each tag that you make.
  • This lesson uses a new practice state, called release. You make it in Set up the practice state. The setup command deletes your old c14-lab folder and makes it again. You do not need your work from Module 4.
  • The hashes of the commits that the setup script makes are the same as in this lesson. Commits that you make yourself have different hashes.

The team writes help desk notes in ticket-notes. Until now, all work went onto main. Now the help desk wants a fixed version that it can use every day. The help desk calls it version 1.0.

main cannot be that version. The team commits to main every day, so main changes every day. The help desk needs notes that change only when the team decides to fix something.

So the team uses three references. A reference is a name that points to a commit, for example a branch name.

Reference Example What it holds Does it move?
Development main New work. All finished changes go here. Yes, with each merged change.
Release candidate release/1.0 The next release. The team tests it. Only for fixes.
Production v1.0.0 The version that the help desk uses. Never.

A release branch is a branch that you make from main when the next version is ready. It is the release candidate: the version that the team tests before it gives the version to the help desk. After you make it, it accepts fixes only. New work continues on main, and goes into the next release.

A tag is a fixed name for one commit. A branch moves when you commit on it. A tag never moves. When the team releases version 1.0, it puts the tag v1.0.0 on the release commit. “Production” is the name for what the users have now, so the tag marks production.

Most teams write versions as three numbers: MAJOR.MINOR.PATCH.

Change Example When
PATCH 1.0.0 → 1.0.1 A fix only. Nothing new.
MINOR 1.0.1 → 1.1.0 New content that does not change how people use the notes.
MAJOR 1.1.0 → 2.0.0 A change that people must learn again, for example a new file layout.

The v at the start of v1.0.0 is a common convention for tag names. The release branch has only the first two numbers, release/1.0, because all 1.0.x fixes go onto it.

  1. Go to your projects folder, and run the setup script with the scenario release:

    Terminal
    cd ~/projects
    sh nextia-learning-labs/C14/setup.sh release --reset
    Output
    Ready: /Users/amira/projects/c14-lab
    ticket-notes your clone
    sam-notes Sam's clone
    team-remote.git the shared remote
    Next: cd c14-lab/ticket-notes
    To start this scenario again: sh nextia-learning-labs/C14/setup.sh release --reset
  2. Go into your clone, and look at the history:

    Terminal
    cd c14-lab/ticket-notes
    git log --oneline -3
    Output
    465d7ea (HEAD -> main, origin/main, origin/HEAD) Add escalation rules
    f330fdc Add ticket 104
    e058b3f Add ticket 103

    If your hashes are different, run the setup command again. Do not continue with different hashes.

Worked example: Amira releases version 1.0

Section titled “Worked example: Amira releases version 1.0”

The team agrees that main at 465d7ea is ready to become version 1.0.

Current branch: main. Amira runs the check first, because a release must pass it:

Terminal
sh check.sh
Output
PASS: 4 ticket lines checked

She makes the release branch from main, and pushes it, so that the team can see it:

Terminal
git switch -c release/1.0
git push -u origin release/1.0
Output
Switched to a new branch 'release/1.0'
To /Users/amira/projects/c14-lab/team-remote.git
* [new branch] release/1.0 -> release/1.0
branch 'release/1.0' set up to track 'origin/release/1.0'.

Current branch: now release/1.0. The / in the name is only part of the name. Teams use release/ to keep all release branches together in lists.

The team tested the candidate, and found no problem. Amira tags the commit as version 1.0.0. The -a option makes an annotated tag: a tag that also stores who made it, when, and a message.

Terminal
git tag -a v1.0.0 -m "Help desk notes 1.0"
git tag
Output
v1.0.0

git tag with no name lists all tags. To see what a tag points to, use git show:

Terminal
git show v1.0.0
Output
tag v1.0.0
Tagger: Amira Khan <amira@example.com>
Date: Mon Oct 5 22:03:44 2026 -0400
Help desk notes 1.0
commit 465d7ea589a41feac19db85754dd27d487d78f76
Author: Amira Khan <amira@example.com>
Date: Tue Oct 6 09:09:00 2026 -0400
Add escalation rules
diff --git a/escalation.txt b/escalation.txt
...

The first part is the tag: who made it, when, and the message. The second part is the commit that the tag points to, 465d7ea.

git push does not send tags. Amira checks this:

Terminal
git push
Output
Everything up-to-date

The tag is still only in her repository. She pushes it by name:

Terminal
git push origin v1.0.0
Output
To /Users/amira/projects/c14-lab/team-remote.git
* [new tag] v1.0.0 -> v1.0.0

Now Sam can get it. In sam-notes, a fetch shows the new branch and the new tag:

Terminal (Sam)
git fetch
git tag
Output
From /Users/amira/projects/c14-lab/team-remote
* [new branch] release/1.0 -> origin/release/1.0
* [new tag] v1.0.0 -> v1.0.0
v1.0.0

Current branch: main. Amira switches back with git switch main. She starts a new feature, a ticket export. She makes export.txt:

export.txt
Export format: one CSV row for each ticket.
TODO: decide the columns.

The export is not finished, but she commits and pushes her progress on main:

Terminal
git add export.txt
git commit -m "Start the ticket export (not finished)"
git push
Output (end)
[main eaa41a2] Start the ticket export (not finished)
1 file changed, 2 insertions(+)
create mode 100644 export.txt
To /Users/amira/projects/c14-lab/team-remote.git
465d7ea..eaa41a2 main -> main

Now she looks at all references. --decorate shows the branch and tag names next to the commits:

Terminal
git log --oneline --graph --all --decorate
Output (first lines)
* eaa41a2 (HEAD -> main, origin/main, origin/HEAD) Start the ticket export (not finished)
* 465d7ea (tag: v1.0.0, origin/release/1.0, release/1.0) Add escalation rules
* f330fdc Add ticket 104

Read it from the top:

  • main moved forward to eaa41a2. Development continues.
  • release/1.0 stayed on 465d7ea. The unfinished export is not in the release candidate.
  • v1.0.0 stayed on 465d7ea. The help desk still has exactly version 1.0.

The release branch can still move: if the help desk finds a bug in 1.0, a fix goes onto release/1.0, and the team tags v1.0.1. The next lesson shows how. The tag v1.0.0 never moves. It always means the version that the help desk got first.

  1. Run the setup command from Set up the practice state, and go into c14-lab/ticket-notes.

  2. Run git status. The output must include On branch main and working tree clean.

  3. Run sh check.sh. The output must start with PASS.

  4. Make the release branch, and push it:

    Terminal
    git switch -c release/1.0
    git push -u origin release/1.0

    Current branch: now release/1.0. The output includes * [new branch] release/1.0 -> release/1.0.

  5. Tag the release:

    Terminal
    git tag -a v1.0.0 -m "Help desk notes 1.0"
  6. Run git show v1.0.0. Check that the commit line shows 465d7ea.

  7. Push the tag:

    Terminal
    git push origin v1.0.0

    The output includes * [new tag] v1.0.0 -> v1.0.0.

  8. Switch to main:

    Terminal
    git switch main
  9. In your editor, make export.txt with the two lines from the worked example. Save it.

  10. Commit and push it on main:

    Terminal
    git add export.txt
    git commit -m "Start the ticket export (not finished)"
    git push
  11. Run git log --oneline --graph --all --decorate. Check three things:

    • main is on your new commit.
    • release/1.0 is on 465d7ea.
    • tag: v1.0.0 is on 465d7ea.
  12. Go to Sam’s clone, and check that he can see the tag:

    Terminal
    cd ../sam-notes
    git fetch
    git tag

    The output ends with v1.0.0. Then go back with cd ../ticket-notes.

The export is now finished. The team wants to release it as version 1.1.

  1. On main, complete export.txt. For example, replace the TODO line with Columns: number, summary, action. Commit and push.
  2. Make the release candidate for 1.1, tag it as the released version 1.1.0, and push the branch and the tag. Choose the names yourself, with the rules from this lesson.
  3. Prove three things with commands:
    • release/1.0 and v1.0.0 did not move.
    • Version 1.0.0 does not contain export.txt.
    • Version 1.1.0 contains export.txt.
Show a model answer

The names are release/1.1 (a new MINOR version, with new content) and v1.1.0.

Terminal
git switch main
git commit -am "Finish the ticket export"
git push
git switch -c release/1.1
git tag -a v1.1.0 -m "Help desk notes 1.1"
git push -u origin release/1.1
git push origin v1.1.0
git log --oneline --graph --all --decorate -5
Output (git log only)
* 5cddf4e (HEAD -> release/1.1, tag: v1.1.0, origin/release/1.1, origin/main, origin/HEAD, main) Finish the ticket export
* eaa41a2 Start the ticket export (not finished)
* 465d7ea (tag: v1.0.0, origin/release/1.0, release/1.0) Add escalation rules
* f330fdc Add ticket 104
* e058b3f Add ticket 103

release/1.0 and v1.0.0 are still on 465d7ea. To see the files in each version, list the files in the tag:

Terminal
git ls-tree --name-only v1.0.0
Output
.gitignore
README.md
check.sh
contacts.txt
escalation.txt
tickets.txt

export.txt is not in the list. The same command with v1.1.0 lists export.txt too.

Your hashes for 5cddf4e and eaa41a2 are different, because you made those commits yourself.

Common mistake

I tagged, but Sam cannot see the tag

Symptom: you made v1.0.0 and ran git push. Sam runs git fetch, then git tag, and sees nothing:

Output (Sam)
From /Users/amira/projects/c14-lab/team-remote
* [new branch] release/1.0 -> origin/release/1.0

Cause: git push sends branches, not tags. The tag is only in your repository.

Fix:

  1. Check which tags the remote has:

    Terminal
    git ls-remote --tags origin

    If the output is empty, the remote has no tags.

  2. Push the tag by name: git push origin v1.0.0.

  3. Ask Sam to run git fetch again. The output shows * [new tag].

After the push, git ls-remote --tags origin shows two lines for the tag. The line that ends in ^{} shows the commit:

Output
2131d28c763e2fbb027721bef294f9f7bf1e405b refs/tags/v1.0.0
465d7ea589a41feac19db85754dd27d487d78f76 refs/tags/v1.0.0^{}

Common mistake

I tagged the wrong commit

Symptom: you wanted to tag the release, but you were on main. git log --oneline --decorate -2 shows the tag on the unfinished work:

Output
eaa41a2 (HEAD -> main, tag: v1.0.1, origin/main, origin/HEAD) Start the ticket export (not finished)
465d7ea (tag: v1.0.0, origin/release/1.0, release/1.0) Add escalation rules

Cause: git tag tags the commit that HEAD points to, unless you give a commit. You were on the wrong branch.

Fix: if you did not push the tag, delete it and tag the correct commit:

  1. Delete the local tag:

    Terminal
    git tag -d v1.0.1
    Output
    Deleted tag 'v1.0.1' (was edbb130)

    edbb130 is the tag itself, not a commit.

  2. Switch to the correct branch, for example git switch release/1.0.

  3. Make the tag again with git tag -a.

If you try to make a tag with a name that exists, Git refuses. For example, git tag -a v1.0.0 -m "x" shows fatal: tag 'v1.0.0' already exists.

Check your understanding

Predict: Sam merges three more pull requests into main. Which of main, release/1.0 and v1.0.0 move?

Show the answer

Only main moves. The pull requests target main, so only main gets the new commits. release/1.0 moves only when someone merges a fix into it. v1.0.0 never moves, so the help desk keeps exactly version 1.0.

Check your understanding

Practical check: show which commit each reference points to, and prove that the tag is on the remote.

Show the answer

Run git log --oneline --graph --all --decorate. Each commit shows its branch and tag names, for example (tag: v1.0.0, origin/release/1.0, release/1.0). Then run git ls-remote --tags origin. If the output lists refs/tags/v1.0.0, the remote has the tag.

Recap

  • main is for development. It moves with every merged change.
  • A release branch, for example release/1.0, is the release candidate. You make it from main, and it accepts fixes only.
  • A tag, for example v1.0.0, marks the released version. It never moves. Use git tag -a for releases.
  • git push does not send tags. Push each tag with git push origin <tag>.
  • Versions use MAJOR.MINOR.PATCH. A fix changes only the PATCH number.

Next: Cherry-pick a fix