A perfectly tested 42 MB CLI can still send users the wrong download when v2.4.0-rc.1 is published as GitHub's latest release. The expensive mistake is treating a Git tag, a prerelease page, and the Latest badge as one action; GitHub presents them as related but separate decisions.

A dependable release process separates the three questions that users, automation, and maintainers ask: which commit is this, is this build safe for production, and which release should a new visitor download? Once those questions have separate owners and gates, prereleases stop accidentally becoming defaults and release history stops looking mysterious.

1. Treat the tag, release, and Latest badge as separate controls

A Git tag identifies a point in repository history. For a release candidate, v2.4.0-rc.1 should identify the exact commit that produced the candidate binary. That tag is useful even before anybody outside the team downloads it: CI can build from it, testers can check it out, and a bug report can name it precisely.

A GitHub release is the distribution and communication record built around a tag. It can contain release notes, downloadable assets, and a prerelease designation. Publishing it is the moment you intentionally make that tagged build visible to users who follow repository releases.

The Latest badge is a third decision. It tells visitors which release GitHub should present as the current default release. Do not use it as shorthand for “most recently published.” A release can be newly published and intentionally not latest because it is a release candidate, a preview, or a maintenance fix for an older supported line.

Control Question it answers Example decision
Git tag Which source commit produced this build? Create v2.4.0-rc.1 at commit 8f3c2d1.
GitHub release Should people be able to find notes and download assets? Publish a prerelease with Linux, macOS, and Windows archives.
Latest release What should a new user regard as the default stable download? Keep v2.3.4 latest while v2.4.0-rc.1 is tested.

2. Write the release policy before the version number exists

The useful policy is not “we use semantic versioning.” It is a short set of decisions that an engineer can apply at 17:00 on release day. For example: every downloadable build gets an immutable version tag; every candidate is a prerelease; only production-approved stable versions can be latest; and a maintenance release does not automatically replace the current major line.

That last rule matters for repositories maintaining two release lines. Suppose v3.2.0 is the current product release and v2.9.8 fixes a security issue for customers who cannot yet move to version 3. Publishing v2.9.8 may be urgent, but making it Latest would direct new installations toward an older major version.

GitHub can automatically assign the latest label using semantic versioning when you do not explicitly choose a latest release. That default is convenient for a simple single-line project. It is not a substitute for product policy when you ship previews, hotfixes, and parallel major versions.

  • Candidate rule: versions containing -rc, -beta, or -alpha are always prereleases and never latest.
  • Stable rule: a stable version becomes latest only after the production acceptance gate passes.
  • Maintenance rule: releases for an older major line are published, but latest is set deliberately rather than inferred.
  • Traceability rule: every asset name includes the exact version, such as acme-cli_2.4.0_linux_amd64.tar.gz.

3. Walk through one release: v2.4.0 from candidate to stable

Assume a team is preparing v2.4.0 of a command-line tool. On Monday, the release manager selects commit 8f3c2d1 on the release branch after unit tests, integration tests, and packaging checks pass. That commit becomes the first candidate, not the stable release.

The team creates tag v2.4.0-rc.1, builds binaries from that tag, and publishes a GitHub release marked This is a pre-release. Testers now have a release page, release notes, checksums, and fixed asset URLs. Regular users can see that the build exists, but GitHub does not frame it as the default production download.

On Wednesday, a Windows installer defect requires one source change. The team creates v2.4.0-rc.2 at the corrected commit and publishes another prerelease. This is better than replacing the first candidate’s assets: a tester who reports a failure can say “rc.1,” while the fix can be evaluated as “rc.2.”

After acceptance, the commit behind v2.4.0-rc.2 is approved for production. The team creates the final tag v2.4.0 on that same commit, publishes the stable v2.4.0 release, and explicitly marks it latest. The candidate tags remain useful historical evidence rather than being rewritten out of existence.

4. Create the tag when the source is frozen, not when the blog post is ready

Create a release tag at the point where you can make a strong statement: “these source files produced these assets.” That point is normally after CI has validated the selected commit and before distribution testing begins. Waiting until marketing copy is complete couples source identity to a non-technical scheduling task.

For a candidate, use a distinct prerelease version instead of moving a tag. A simple Git workflow might look like this:

git checkout release/2.4
git pull --ff-only
git log -1 --oneline
git tag -a v2.4.0-rc.1 -m "Release candidate 1 for 2.4.0"
git push origin v2.4.0-rc.1

An annotated tag records tag metadata as well as the target commit, which makes it a better release marker than an informal branch name. If your organization signs tags, this is also the point to enforce that requirement consistently rather than signing only final releases.

Do not retarget v2.4.0-rc.1 after a failure. Retagging makes two different builds share one human-readable identity. The immediate convenience costs time later when a customer, tester, or incident responder tries to reproduce exactly what they downloaded.

5. Publish the prerelease after assets exist and can be tested

A prerelease is not merely a draft of release notes. Publish it when you have something a tester can install: versioned archives, installers, packages, checksums, or another concrete deliverable. GitHub releases are based on tags, so attach the prerelease to the candidate tag that produced those files.

In the GitHub release form, select This is a pre-release. That status communicates that the release may be unstable and is not ready for production. Leave the latest setting alone for the candidate; the operational rule should be simpler than interpreting semantic-version precedence under pressure: prereleases never get Latest.

A GitHub Actions pipeline should avoid compiling the same candidate twice in disconnected jobs. Build in one job, store the output as a workflow artifact, then download that artifact in the release job and upload the resulting archives to the GitHub release. The artifact handoff gives the release job the bytes that the build job tested, instead of silently creating a new build environment and hoping it produces identical output.

  1. Build from refs/tags/v2.4.0-rc.1.
  2. Run test and packaging checks against that build.
  3. Upload the build output as a workflow artifact.
  4. In a release job with repository contents write permission, download that artifact.
  5. Create the prerelease against the same tag and attach the tested files.

6. Promote by creating a stable tag, not by relabeling an ambiguous candidate

A candidate and a stable release make different promises. The candidate says “evaluate this.” The stable release says “deploy this.” Keeping those promises in separate version tags makes your release history intelligible six months later.

If v2.4.0-rc.2 passes acceptance unchanged, create v2.4.0 at the same commit. The two tags can point to the same source revision while expressing different lifecycle states. Then publish stable notes and stable assets under the final version name.

The overlooked tradeoff is asset naming. If the only archive is named acme-cli_2.4.0-rc.2_linux_amd64.tar.gz, you either need a final artifact named 2.4.0 or you need to explain why a stable release links to an RC-labelled file. Produce final-version filenames for the stable release. It avoids confusing package scripts, support staff, and users who save files locally.

If code changes after the candidate, do not call it a promotion. Make v2.4.0-rc.3, repeat the appropriate tests, and only then create v2.4.0. The final tag should never hide an untested source change behind an already-approved candidate label.

7. Mark Latest explicitly when the stable release is the default

Mark a release latest at the moment it becomes the default recommendation for a new production user. For the example workflow, that is the stable v2.4.0 release, after acceptance and after the final downloadable assets have been attached.

GitHub offers a Set as latest release option. If you do not select it, GitHub can assign the latest label automatically based on semantic versioning. Explicit selection is worth the extra click when your repository has more than one active release line or when publication order does not match recommendation order.

Release published Prerelease? Mark latest? Reason
v2.4.0-rc.1 Yes No It is a testable candidate, not the default install.
v2.4.0 No Yes It is the approved production release for the current line.
v1.18.9 No Usually no It may serve an older supported line while version 2 remains the recommended default.

Make “who may change Latest” part of the release checklist. It is a user-facing routing decision, not decorative metadata.

8. Why the tag date and release date can differ

A tag date and a GitHub release date answer different historical questions. The tag marks when the repository reference was created for a particular point in history. The release date reflects when the GitHub release record was created or published. GitHub explicitly notes that these dates can differ because the tag and release can be created at different times.

In the v2.4.0 example, the source might be tagged at 09:15 UTC after the final commit is accepted. The release page may be published at 14:30 UTC after binaries finish, checksum verification completes, and the release manager approves notes. Neither date is wrong: one is source identification timing, and the other is distribution timing.

The important operational consequence is to stop using the release publication time as proof of when code was frozen. For incident analysis, start with the tag and the commit it references. For customer communication, cite the published release. If a five-hour gap looks suspicious, inspect the approval, build, and upload stages rather than assuming someone altered the source.

Repositories using immutable releases need even more discipline. GitHub documents that immutable releases cannot be added to, replaced, or deleted after immutability applies. That makes the “build first, verify assets, then publish” sequence essential: publishing an incomplete release is no longer a harmless draft mistake.

9. Put this release flow into use this week

Start with one real release line rather than redesigning every historical tag. The goal is a repeatable path from commit to candidate to stable default, with enough evidence to answer a support question without reconstructing events from chat messages.

  1. Write a four-line policy covering candidate tags, prerelease status, stable tags, and who sets Latest.
  2. Choose one naming scheme, such as v2.4.0-rc.1 for candidates and v2.4.0 for stable releases.
  3. Change CI so a release job downloads the tested build artifact rather than building a second time.
  4. Add a release checklist item: “Is this a prerelease?” followed by “Should this replace the current Latest release?”
  5. For the next release, record the tag creation time and the release publication time in the release notes or change log process.

The resulting history is straightforward: candidate tags identify every testable build, prerelease pages distribute those builds without making them defaults, and one deliberate Latest decision points new users at the production version. That is the difference between a repository that merely has releases and one that has a release process.