A team can ship v2.4.0 twice without moving a single commit: first when the tag is pushed, then again when the GitHub release page is published. If the binary, notes, and tag are assembled in the wrong order, users can download a file labelled v2.4.0 while maintainers are no longer certain which tested commit it came from.

The reliable model is simple: use Git to identify the exact source revision, and use GitHub to publish the human-facing package around that revision. The tag is the durable engineering record. The GitHub release is the publication event that gives users notes, downloads, a latest-release label, and—where they have subscribed—release notifications.

1. Treat the tag and the release page as two separate events

A Git tag points to a specific point in repository history. For a versioned project, that means v2.4.0 identifies one commit and lets someone later check out the source used for that version. A tag is repository data; it is not a product announcement, changelog page, or binary distribution channel.

A GitHub release is a published page based on a tag. It can contain a title, release notes, links to source code, and uploaded assets such as a CLI archive, installer, or checksum file. GitHub’s own git/git repository demonstrates the distinction: it has tags but no GitHub releases, because tags alone do not require a published release page.

Question Annotated Git tag GitHub release
What does it identify? A specific commit or object in Git history. A published package of information and downloads associated with a tag.
Where does it live? In the Git repository. On GitHub’s release interface for the repository.
What metadata can it carry? Tagger identity, timestamp, and annotation message. Release title, notes, pre-release status, latest-release selection, and assets.
What should it answer? “Which source revision is version v2.4.0?” “What should a user download and what changed?”

The important operational consequence is that the tag date and release date can differ. A tag created Tuesday after CI passes may become a public release Thursday after documentation, archives, and release notes are reviewed. That gap is not a mistake; it is useful separation between freezing source and publishing a package.

2. Define the release contract before creating a version

Before someone types git tag, decide what the version promises. This prevents the common failure mode where a team tags a commit, then discovers it has no reproducible archive, no migration note, or no answer to “what changed since the previous version?”

For a small command-line tool, the contract might be: one tested commit, a macOS archive, a Linux archive, a Windows archive, SHA-256 checksum files, and notes covering breaking changes. For a library published to a package registry, the GitHub assets may be optional, but the tag and release notes still give downstream users a durable source reference.

  • Version: Use a consistent version format such as v2.4.0. The v prefix is a convention, but consistency matters more than the chosen style.
  • Source: Identify the commit that passed the required build and test checks.
  • Artifacts: List exact filenames before building them, for example widget_2.4.0_linux_amd64.tar.gz.
  • Notes: Record breaking changes, upgrade actions, fixes, and known limitations.
  • Audience: Decide whether this is stable, a pre-release, or an internal checkpoint that should not receive a GitHub release page.

The decision rule is practical: create a GitHub release only when people outside the immediate development loop need a stable landing page or a downloadable artifact. Create a tag whenever the project needs a durable name for an exact source revision. Many projects need both; they are not substitutes.

3. Freeze the exact commit that earned the version

Do not tag “whatever is currently on main” by habit. First make the intended release commit explicit, then verify it is the commit your release process approved. The following sequence checks your local state, updates the branch, and records the commit ID you are about to version:

git switch main
git pull --ff-only origin main
git status
git log -1 --oneline
git rev-parse HEAD

git status should not show accidental source changes that would make local builds differ from the commit being tagged. git pull --ff-only is useful because it refuses to create a merge commit merely to update your local branch. The final command prints the full commit ID; put it in the release checklist or pull request discussion if your team uses one.

At this stage, run the same build and tests your project requires for a release. If artifacts are produced by CI, make sure the successful run corresponds to this commit rather than an earlier commit on the branch. A release asset can be perfectly valid as a file and still be wrong as a release artifact if it was built from a neighboring commit.

That distinction becomes expensive later. When a user reports a defect in v2.4.0, maintainers should need one lookup—tag to commit—not a reconstruction exercise involving build timestamps and chat messages.

4. Create an annotated version tag, not an unnamed marker

For published versions, create an annotated tag. Unlike a lightweight tag, an annotated tag is a Git object with a message and tagger metadata. That makes it a better release record: someone inspecting the repository can see who created it, when it was made, and the short statement attached to the version.

git tag -a v2.4.0 -m "Release v2.4.0"
git show v2.4.0
git push origin v2.4.0

The first command tags the current HEAD. If your release commit is not checked out locally, be explicit rather than relying on the current branch:

git tag -a v2.4.0 8f3c2a1 -m "Release v2.4.0"

Replace 8f3c2a1 with the approved commit ID. Then inspect before pushing. git show v2.4.0 displays the tag annotation and the object it resolves to; this is the fast check that catches a tag created on the wrong commit.

A lightweight tag, created with git tag v2.4.0, still marks a commit and can still be selected for a GitHub release. The problem is not that GitHub cannot use it. The problem is that a lightweight tag omits the release-oriented metadata that makes later auditing clearer. Use annotated tags as the default publishing rule and reserve lightweight tags for temporary local markers.

5. Verify the remote tag before opening the release form

Push the tag before drafting the GitHub release page. This ordering means the release form selects an already-existing repository reference rather than creating a tag as a side effect of publication. It also lets a second maintainer inspect the tag independently before any user-facing page appears.

Verify the tag locally and remotely with commands that answer two different questions:

git show --no-patch v2.4.0
git ls-remote --tags origin v2.4.0

The local command checks what your clone believes the tag means. The remote command confirms that the remote repository exposes a tag named v2.4.0. In a review-oriented workflow, paste the resolved commit ID into the release issue, pull request, or release checklist.

This is also the right point to consider repository rules. GitHub’s older tag protection rules were migrated to tag rulesets in 2024, so teams that need to restrict which tags can be created or updated should manage that policy through current repository ruleset controls rather than assume legacy tag protection configuration is the durable mechanism.

The policy goal is narrow: published version names should not silently drift to different commits. Whether your repository enforces that with rulesets, required reviews, or a restricted release automation account, the reviewer should be able to answer one question: “Can v2.4.0 later mean something else?” The desired answer is no.

6. Draft the GitHub release against the existing tag

Now create the publication page. In the repository on GitHub, open Releases, choose Draft a new release, and select v2.4.0 from the tag chooser. Selecting the existing tag is the critical handoff from Git’s source identity to GitHub’s publishing interface.

  1. Set the release title to something recognizable, such as Widget CLI v2.4.0.
  2. Select the already-pushed tag v2.4.0.
  3. Draft the release notes before publishing.
  4. Upload the finished assets and checksum files.
  5. Review every link and filename while the release remains a draft.
  6. Publish only after a second person, or a deliberate self-review, verifies the tag and downloads.

A draft is not just an unfinished release. It is the review boundary between “we have named the source revision” and “we have publicly advertised a package.” Use it to keep prose, assets, and final labels from being rushed into a single irreversible-looking moment.

GitHub can mark a release as the latest release automatically when you do not explicitly choose otherwise. For a normal stable version, that behavior is usually appropriate. For a version you do not want users to interpret as the current recommended download, make the choice deliberately instead of relying on ordering alone.

7. Write notes and attach assets users can verify

Release notes should help two audiences make different decisions. Existing users need to know whether upgrading requires action. New users need to know what they can download and whether this is the stable version they should start with. A list of merged pull request titles rarely satisfies either audience without editing.

A compact notes structure

  • Highlights: Two to five user-visible improvements, such as “Adds JSON output for widget inspect.”
  • Breaking changes: State the old behavior, the new behavior, and the required migration step.
  • Fixes: Describe the bug in user terms, not only an internal issue number.
  • Upgrade notes: Include commands or configuration changes where needed.
  • Verification: Name the asset and checksum file a user should compare.

For example, if users download widget_2.4.0_linux_amd64.tar.gz, upload a corresponding widget_2.4.0_checksums.txt. The checksum file is not decorative: it gives a user a stable value to compare after downloading an archive. Keep filenames versioned. An asset named only widget-linux.tar.gz becomes ambiguous as soon as the next release exists.

Before publishing, download at least one uploaded asset from the draft page and run it or unpack it in a clean directory. This catches the tedious mistakes that CI can miss: an archive containing an extra parent directory, a filename with the wrong version, or a checksum generated before a final rebuild.

8. Handle pre-releases, fixes, and bad publication order deliberately

A pre-release is for a version that users may test but should not treat as production-ready. On GitHub, select This is a pre-release when publishing that kind of build. Do not use a stable-looking release page plus a buried warning in the notes; the pre-release designation communicates status at the point where users choose a download.

For example, use v3.0.0-rc.1 for a release candidate only if your project’s versioning rules recognize that suffix. Keep its notes focused on what testers should validate: upgrade paths, changed configuration, supported platforms, or a feature that needs feedback. When the stable version is ready, tag and publish the stable version separately rather than relabeling the release candidate as though it were the final tested source.

If you discover a defect after publishing, do not casually move the existing version tag to a replacement commit. Create a corrective version such as v2.4.1, tag the fixed commit, and publish a new release page. The extra version is cheaper than making historical source references unreliable.

If only the release notes contain an error, edit the release page and clearly correct the text. The source identity remains unchanged because the tag still points at the same commit. This is exactly why separating the tag from the release page is useful: editorial corrections do not require rewriting Git history.

9. Put this publishing checklist into use this week

Turn the workflow into a pull request template, a release issue, or a repository document. The goal is not bureaucracy; it is making three identities agree every time: the version string, the tagged commit, and the downloadable artifacts.

  1. Choose the version, such as v2.4.0, and list the required artifacts.
  2. Confirm the intended commit passed the project’s required checks.
  3. Create an annotated tag with git tag -a.
  4. Inspect it with git show --no-patch v2.4.0.
  5. Push it with git push origin v2.4.0.
  6. Draft a GitHub release by selecting that existing tag.
  7. Add upgrade-focused notes and versioned assets with checksums.
  8. Download and test at least one draft asset.
  9. Set stable versus pre-release status intentionally, then publish.

The workflow’s payoff is traceability, not ceremony. Six months after publication, a maintainer can begin with a GitHub release page, identify the exact tag, resolve it to the precise commit, and reproduce the investigation from there. That is the division of labor worth preserving: Git records what the version is; GitHub publishes what users should know and download.