A pushed v2.4.0 tag can trigger a production deployment at 09:12, while GitHub still has no downloadable, subscriber-notified release at 16:40. Treating those as the same publication event is how a correct commit becomes an unannounced—or prematurely announced—release.

The useful mental model is not “tag versus release.” It is a two-stage publishing system: first identify the exact source commit, then decide whether that version is ready to be presented to users. GitHub releases are based on Git tags, but GitHub keeps release metadata separately, including release notes, attached files, pre-release status, and the choice of which release is latest.

1. A tag identifies code; a release publishes a version

A Git tag is a named pointer to a point in repository history. If v2.4.0 points to commit 8f3c1ab, every build, audit, rollback, and support investigation can refer to that exact source state.

A GitHub release uses that tag as its foundation, then adds the information people need to consume the software. That can include a title, release notes, links or uploaded binary files, a pre-release marker, and a latest-release decision. The tag can exist without a release; the git/git repository is a visible example of a repository that has tags but no GitHub Releases.

Question Git tag GitHub release
What does it identify? A specific commit in Git history A published software version based on a tag
Can it exist by itself? Yes No; it is based on a tag
Can it include release notes? Only tag annotation text, if you create an annotated tag Yes, with a title and release notes
Can it be marked pre-release? No Yes
Can it be selected as “latest”? No Yes

The practical consequence is that a tag is a source-control fact, while a release is a product communication and distribution decision. Your workflow should preserve that boundary.

2. Use a worked policy before creating v2.4.0

Consider a repository named acme/widget-cli. It ships a command-line tool as versioned source and platform binaries. The team has merged the changes intended for version 2.4.0, tests are green on the default branch, and the release manager wants a repeatable process.

Before running any command, write down what each event means. This prevents a common failure mode: one engineer assumes “push tag” means “deploy,” while another assumes “create release” means “announce,” and both automations run.

  • Tag v2.4.0: freeze the exact source commit selected for version 2.4.0.
  • Build from v2.4.0: create and verify the distributable artifacts from frozen source.
  • Publish GitHub release: make notes and verified artifacts visible to users.
  • Set as latest: declare that 2.4.0, not an older stable version or a newer pre-release, is the primary version users should see.

This policy lets the team separate operational timing from public timing. You might create the tag during a deployment window, wait for smoke testing, then publish the release after the artifacts and notes are ready. GitHub documentation explicitly notes that a tag date and release date can differ because they can be created at different times.

3. Create an annotated tag from the reviewed commit

Start by checking that the local checkout contains the reviewed commit you intend to release. In this example, the team releases the current commit on main. An annotated tag is a good default because it records a tagger, timestamp, and message rather than creating only a lightweight name.

git switch main
git pull --ff-only origin main
git status
git tag -a v2.4.0 -m "Release v2.4.0"
git show v2.4.0
git push origin v2.4.0

The git show v2.4.0 check matters more than it looks. It gives the release manager one last chance to inspect the tagged commit before sharing the tag with CI, deployment jobs, package builders, or external users.

Do not create v2.4.0 from an arbitrary local branch merely because the version number is correct. A tag gives a version a long-lived name; if it points to the wrong commit, fixing the documentation later does not fix builds that already consumed it.

After the push, the repository has a version marker. It does not yet necessarily have a public GitHub release page, notes, release assets, subscriber notification, or a “Latest” badge.

4. Build from the tag, not from whatever main becomes next

Once v2.4.0 exists, build the package from that reference. The next merge to main may happen five minutes later. If your release artifacts are built from the branch tip instead of the tag, the file users download can contain code that is not actually part of version 2.4.0.

A minimal shell-oriented build sequence might look like this:

git fetch --tags origin
git checkout v2.4.0
npm ci
npm test
npm run build

The exact tooling differs for Go, Python, Rust, Java, or Node.js. The invariant does not: record and build from the tag name. In CI, pass the tag reference into the build and stamp it into the artifact metadata where your ecosystem supports that.

For widget-cli, the release manager waits until the tagged build produces the intended files, such as a macOS archive, Linux archive, and checksum file. That wait is why “tag pushed” and “release published” should not automatically be treated as the same event.

The tradeoff is a small delay between code freeze and announcement. The gain is that the GitHub release points to artifacts made from the same immutable-looking reference users can inspect in the repository.

5. Publish the GitHub release only when the package is ready

Now create a GitHub release based on v2.4.0. In the GitHub web interface, choose the existing tag rather than creating a new one by accident. Give the release a useful title, such as widget-cli 2.4.0, then add notes that help an upgrader make a decision.

Release notes should answer three concrete questions: what changed, who needs to act, and how to obtain the build. A vague note such as “Various improvements” forces users to inspect commits. A better note calls out a renamed command, a fixed authentication failure, or a minimum runtime version.

## Added
- Added JSON output to `widget status`.

## Fixed
- Fixed a token refresh failure after an expired session.

## Upgrade note
- Scripts parsing human-readable `widget status` output should use
  `widget status --format json` instead.

Attach the built archives and checksum file if GitHub is your distribution channel. GitHub describes releases as a way to package software with release notes and links to binary files for other people to use. Publishing this release is the point at which repository watchers who subscribe to releases can receive a release notification without subscribing to every repository update.

6. Treat pre-releases as a separate audience contract

Suppose the team wants external testing before declaring 2.4.0 stable. Do not tag the candidate as final v2.4.0 and then hope users infer that it is unfinished. Create a clearly named candidate such as v2.4.0-rc.1, build from that tag, and publish a GitHub release marked This is a pre-release.

That checkbox is not cosmetic. It is GitHub’s explicit signal that the release is not ready for production and may be unstable. The tag preserves the candidate source; the release status communicates the support expectation.

Version Recommended GitHub release status Reader expectation
v2.4.0-rc.1 Pre-release Test it; do not assume production readiness
v2.4.0 Regular release Normal stable release process applies
v2.4.1 Regular release Stable patch release

The second-order benefit is support triage. When a user reports a bug from 2.4.0-rc.1, your team can immediately see that they tested a candidate, not the current stable release. That is much clearer than a final-looking tag with an apologetic sentence buried in a README.

7. Decide “latest” deliberately instead of trusting version order

GitHub can assign the latest-release label automatically based on semantic versioning when you do not explicitly choose it. That is useful for straightforward repositories, but it is not a substitute for a release policy.

For widget-cli, the decision rule is simple: set latest only when the release is the recommended stable download for new users. A release candidate should not become the default recommendation merely because it was published after v2.3.2.

  • Publish v2.4.0-rc.1 as a pre-release and do not treat it as the stable recommendation.
  • Publish v2.4.0 after candidate testing and set it as latest.
  • Publish v2.3.3 later only for users pinned to an older supported line; decide explicitly whether it should remain non-latest.

This matters most when you maintain more than one version line. The numerically greatest version is not always the version you want a new user to install. “Latest” is a product decision, not merely a sorting result.

8. Keep tag automation and release automation on different triggers

The cleanest automation split is to let tag creation start reproducible technical work and let release publication start public-facing work. A tag-triggered workflow can build, test, sign where applicable, and upload candidate artifacts to a controlled location. A release-published workflow can update documentation, send a changelog message, or publish to channels that should only hear about public releases.

This avoids a risky pattern: production promotion attached directly to every matching tag push. If anyone with tag-push permission can create v2.4.0, they may also trigger an irreversible deployment or announcement before release review is complete.

A practical division looks like this:

  • Tag push: validate version format, build from the tag, run tests, create artifacts.
  • Release publication: publish documentation updates, notify users, and run public distribution steps.
  • Manual approval or protected environment: perform production deployment when your organization needs a human gate.

Use the tag name as the shared identifier across all three stages. Do not use a branch name such as main for one stage and a tag for another; that creates an avoidable ambiguity as soon as new commits land.

9. Put this release workflow into use this week

You do not need a release platform redesign to get the benefit. Start with one repository and make the two records visible: a tag is the source checkpoint, and a release is the user-facing publication record built on that checkpoint.

  1. Create a RELEASING.md file with your version format, tag command, build command, and release-note checklist.
  2. Choose whether a pushed tag is allowed to deploy production, or whether it should only build artifacts.
  3. Require a release manager to verify the tagged commit before publishing the GitHub release.
  4. Use pre-release status for every beta or release candidate instead of relying on prose warnings.
  5. Write down who may set the latest-release label when you support multiple version lines.
  6. Test the full sequence once with a version such as v0.1.0-rc.1 before the next high-stakes production release.

The result is a workflow with two intentional timestamps: when the source was frozen and when users were invited to consume it. That distinction gives CI a stable input, gives release managers a review point, and gives users an honest signal about what is ready to run.