DevOps.Academy
CoursesCurriculumPricing

    Loading…

    ↑ ↓ move · ↵ open · Ctrl K anywhere

    ◔My learningSign in
    DevOps.Academy

    Free, hands-on DevOps lessons. Made with care in India.

    Learn

    All coursesLinux roadmapCurriculumMy learning

    Academy

    PricingCertificates

    Legal

    PrivacyTermsContact

    devops.rajeev.pro

    Git & GitHubLesson 11 of 13
    Your progress · 0%
    On this page
    1. Annotated and lightweight tags
    2. Pushing, listing and deleting tags
    3. Semantic versioning
    4. GitHub Releases
    ↖ Course roadmap
    Lesson 11 · Tags & releases

    Tags and releases: version numbers people can trust

    Mark a commit as a version with an annotated tag, push and delete tags, choose version numbers with semantic versioning, and publish GitHub Releases with generated notes that can trigger a deployment.

    30 min read · intermediate · hands-on

    By the end of this lesson you'll be able to

    0 of 6 complete

    Annotated and lightweight tags

    A tag is a permanent name for one commit: "this is version 1.0.0". Unlike a branch, it never moves. There are two kinds:

    $ git tag v1.0.0-light                         # lightweight: just a name for a commit
    $ git tag -a v1.0.0 -m "First stable release"  # annotated: a real object with metadata
    $ git cat-file -t v1.0.0-light
    commit
    $ git cat-file -t v1.0.0
    tag
    $ git show v1.0.0
    tag v1.0.0
    Tagger: Rajeev Kumar <rajeev@example.com>
    Date:   Wed Oct 14 04:38:37 2026 +0000
    
    First stable release
    
    commit f5b6404cff08e75f5798c0fa368f916d154b4fc1
    
    git tag v1.0.0-light
    • gitversion control
    LightweightAnnotated (-a)
    Storesonly the commit it points attagger, date, message, optional signature
    Used by git describeonly with --tagsyes
    Use forprivate bookmarksevery release

    Tag an older commit by naming it: git tag -a v0.9.0 9ece149 -m "…". Sign tags with git tag -s (GPG or SSH signing set up), and GitHub marks them Verified.

    Pushing, listing and deleting tags

    git push sends branches, not tags. Push them on purpose:

    $ git push origin v1.0.0                 # one tag
     * [new tag]         v1.0.0 -> v1.0.0
    $ git push --follow-tags                 # commits, plus annotated tags pointing at them
    $ git config --global push.followTags true   # make that the default
    $ git ls-remote --tags origin            # the tags GitHub has
    
    git push origin v1.0.0
    • gitversion control
    • pushupload commits
    git config --global push.followTags true
    • configread or set settings
    • --globalfor your user, in every repo
    $ git tag -l "v1.*" --sort=-v:refname    # list, newest version first
    v1.0.1
    v1.0.0
    $ git log --oneline v1.0.0..v1.0.1       # what changed between two releases
    9ece149 Fix the login redirect
    $ git checkout v1.0.0                    # look at a release (detached HEAD)
    $ git tag -d v1.0.0-light                # delete locally
    $ git push origin --delete v1.0.1        # delete on GitHub
    
    git tag -l "v1.*" --sort=-v:refname
    • gitversion control
    git log --oneline v1.0.0..v1.0.1
    • loghistory
    git push origin --delete v1.0.1
    • pushupload commits

    Naming every build: git describe

    $ git describe --tags
    v1.0.1-1-gf367a0f
    
    git describe --tags
    • gitversion control

    Read it as: nearest tag v1.0.1, 1 commit after it, at commit f367a0f (the g is for Git). Pipelines use it to stamp builds between releases, so a bug report says exactly which code was running.

    Never move a published tag

    Once a tag is pushed, people and pipelines have fetched it, and Git won't replace a tag someone already has. If v1.2.0 turned out to be broken, don't re-tag a different commit: release v1.2.1.

    Semantic versioning

    SemVer gives version numbers a meaning: MAJOR.MINOR.PATCH.

    Breaking change: MAJOR. New feature: MINOR. Bug fix: PATCH.© Diagram: DevOps Academy

    Breaking change: MAJOR. New feature: MINOR. Bug fix: PATCH.© Diagram: DevOps Academy
    Change since 2.4.1Next version
    fix a bug, nothing else changes for users2.4.2
    add an optional setting, a new endpoint2.5.0
    remove or rename something users rely on3.0.0
    a test build of the next major3.0.0-rc.1 (pre-release, sorts before 3.0.0)

    0.x versions mean "not stable yet: anything may change". Tag with a v prefix (v2.4.1): it's the common convention and what most tools expect. Conventional Commits (from the Commits lesson) make this automatic: fix: means a patch, feat: a minor, feat!: or BREAKING CHANGE: a major, and tools like release-please read your commits to pick the number and write the changelog.

    GitHub Releases

    A release is a tag plus a page on GitHub: a title, release notes, and optional files (binaries, archives). It's what users see under Releases, and what they get notified about.

    $ gh release create v1.0.1 --generate-notes            # notes built from merged pull requests
    $ gh release create v2.0.0-rc.1 --prerelease --generate-notes
    $ gh release create v1.1.0 --draft --notes-from-tag    # a draft, using the annotated tag's message
    $ gh release create v1.1.0 ./dist/app-linux-amd64 --generate-notes   # attach a build
    $ gh release list
    $ gh release view v1.0.1 --web
    

    If the tag doesn't exist yet, gh release create creates it on the default branch (use --verify-tag to refuse instead). --generate-notes lists every pull request merged since the previous release, grouped by label if you add a .github/release.yml, with credit to each contributor.

    Releases that deploy

    A published release is also an event that GitHub Actions can react to. This site works that way: merging to main deploys to a staging environment, and publishing a GitHub Release vX.Y.Z runs a workflow that moves the production branch to that tag, which deploys it to the live site. Production changes only when someone deliberately cuts a release:

    # .github/workflows/release.yml (shortened from this site's real workflow)
    on:
      release:
        types: [published]
    jobs:
      promote:
        runs-on: ubuntu-latest
        environment: production
        steps:
          - uses: actions/checkout@v4
            with: { fetch-depth: 0 }
          - name: The release must be on main
            run: |
              sha=$(git rev-list -n 1 "refs/tags/$GITHUB_REF_NAME")
              git merge-base --is-ancestor "$sha" origin/main || exit 1
              echo "SHA=$sha" >> "$GITHUB_ENV"
          - name: Move production to the release (fast-forward only)
            run: git push origin "$SHA:refs/heads/production"
    

    The real file also checks that CI passed on that commit and applies database migrations first. Because the push isn't forced, production can only move forward: a rollback means redeploying an older build, then releasing a fix.

    The CI/CD course builds pipelines like this from scratch.

    In production

    Release from main only, after CI passes. Keep a CHANGELOG.md or rely on generated notes, but make every release say what changed for users. Protect release tags with a tag ruleset (Settings → Rules), so only maintainers can create or delete v* tags.

    ⌘
    Mini mission

    Cut your first two releases

    Release the shop repository as v1.0.0, then fix a bug and release v1.0.1. Use annotated tags, push them, publish both as GitHub Releases with generated notes, and show what changed between them.

    Input: The shop repository on GitHub, with gh logged inOutput: Two releases on GitHub; git describe naming later builds
    Reveal one safe solution
    $ git switch main && git pull
    $ git tag -a v1.0.0 -m "First stable release"
    $ git push origin v1.0.0
    $ gh release create v1.0.0 --generate-notes --title "v1.0.0"
    $ echo "Fixed: login redirect" >> README.md && git commit -am "Fix the login redirect"
    $ git push
    $ git tag -a v1.0.1 -m "Fix the login redirect"
    $ git push --follow-tags
    $ gh release create v1.0.1 --generate-notes
    $ git log --oneline v1.0.0..v1.0.1
    $ git tag -l --sort=-v:refname
    v1.0.1
    v1.0.0
    $ echo "search" >> README.md && git commit -am "Add search"
    $ git describe --tags
    v1.0.1-1-g…
    
    git switch main && git pull
    • gitversion control
    • &&run the next command only if this one worked
    • pulldownload and merge
    git push origin v1.0.0
    • pushupload commits
    echo "Fixed: login redirect" >> README.md && git commit -am "Fix the login redirect"
    • echoprint text
    • >>append output to a file
    • commitsave staged changes
    git log --oneline v1.0.0..v1.0.1
    • loghistory

    The bug fix is a PATCH bump because nothing changed for users except the fix. On GitHub, Releases now shows v1.0.1 as Latest, with the fix listed in its notes.

    Try it yourself

    0 of 6 steps done

    Run each task in your own account or terminal, then tick it off.

    Knowledge check

    Quick check

    Question 1 of 5

    Why use an annotated tag (git tag -a) for a release instead of a lightweight one?

    Go deeper

    Official reading for this lesson

    • GuidePro Git: taggingPro Git ↗
    • ArticleSemantic Versioning 2.0.0semver.org ↗
    • DocsGitHub Docs: about releasesGitHub Docs ↗
    • DocsGitHub Docs: automatically generated release notesGitHub Docs ↗
    • GuideKeep a ChangelogKeep a Changelog ↗

    Or get every quiz answer right and it completes itself.

    ← PreviousRewriting history: amend, interactive rebase and safe force-pushesNext lesson →Hooks and automation: let Git check your work