A customer downloads v2.4.2, sees the exact commit your team shipped, and still cannot install the product because the Windows installer exists only in a CI run from three weeks ago. The tag is technically correct, but it is not a usable delivery point for anyone who needs a binary, checksum, migration note, or stable download link.
That distinction is the practical boundary between a Git tag and a GitHub Release. A tag answers, “Which commit represents this version?” A GitHub Release turns that tagged commit into a package people can find, understand, download, and share without knowing your branch conventions or CI system.
Start with the decision: are you marking history or distributing software?
Use a plain tag when the audience is primarily your repository, automation, or other developers who can build from source. Use a GitHub Release when somebody needs an artifact or a human-readable explanation of what changed.
The two are connected, not competing. GitHub Releases depend on Git tags: when you draft a release, GitHub asks you to choose an existing tag or create one. The tag remains the immutable-looking version marker in Git history; the release is the publishing layer attached to that marker.
| Need | Plain Git tag | GitHub Release |
|---|---|---|
Mark commit v1.8.0 |
Yes | Yes, through its tag |
| Trigger a deployment workflow | Usually sufficient | Possible, but adds a publishing event |
| Publish a macOS app, Docker helper, or CLI binary | No artifact destination by itself | Yes, attach files |
| Give support staff one public URL | Awkward commit/tag URL | Yes, release page URL |
| Explain breaking configuration changes | Requires external documentation | Release title and notes |
| Label an unstable build | Only by naming convention | Built-in prerelease option |
The key test is simple: if a user must do more than clone the repository and run a build command, a release usually earns its maintenance cost.
Choose a plain tag for source-controlled milestones
A tag is enough for internal milestones where the version label is the important output. For example, a Terraform module repository may tag v0.14.0 after its tests pass. Consumers using a Git reference, a module registry, or their own automation need the version boundary, not a ZIP containing compiled software.
The same applies to a backend service that is deployed only by your own CI/CD pipeline. A tag push can tell GitHub Actions, Jenkins, or another system to build image api:v3.6.1 and deploy it. Publishing a GitHub Release as well can be useful for audit notes, but it is not required to make the deployment happen.
git switch main
git pull --ff-only
git tag -a v3.6.1 -m "Release v3.6.1"
git push origin v3.6.1
Prefer an annotated tag for a formal version marker. It records tag metadata and a message, rather than behaving like a lightweight pointer alone. If your release process includes cryptographic verification, Git also supports signed tags; GitHub’s own documentation points readers to tag signing as related guidance.
The tradeoff people skip: a tag name becomes part of your automation contract. If production deploys on every pushed tag matching v*, then an engineer pushing v3.6.2-rc.1 may trigger more than they intended. Use a tighter pattern such as stable semantic versions, or separate release-candidate prefixes from production tag patterns.
Publish a GitHub Release when binaries leave your build system
A release becomes valuable when your CI system produces files that cannot be reconstructed conveniently by the downloader. Typical examples include .msi installers, signed .dmg files, Linux tarballs, a compiled Go CLI, firmware images, and a desktop application bundle.
Consider a cross-platform command-line tool called logship. Tag v2.4.2 identifies the source. The release should provide the output a user actually needs:
logship_2.4.2_linux_amd64.tar.gzlogship_2.4.2_darwin_arm64.tar.gzlogship_2.4.2_windows_amd64.zipchecksums.txt
Without a release, users must find a CI run, determine whether artifacts have expired or are accessible, and guess which job matches their operating system. That turns a download into a support task. With a release, the tag, artifacts, and notes sit on one page tied to the same version.
GitHub also creates source-code archives for releases. Those are useful for source consumers, but do not mistake them for a replacement for built artifacts. A source ZIP does not contain the result of your signing, packaging, cross-compilation, or installer-generation steps unless those files are committed to the tagged repository.
Release notes are a delivery interface, not a changelog dump
The release page is where a maintainer translates commit-level work into upgrade-level decisions. A list of 48 merged pull requests may be accurate, but it is not what an operator needs when deciding whether to move from v2.3.0 to v2.4.0.
Write notes around actions and risk. For a database-backed service, put the required migration command above feature announcements. For a CLI, show a changed flag with its replacement. For a library, identify removed APIs and the lowest supported runtime version.
## Upgrade notes
- Run `logship migrate` before starting version 2.4.2.
- The `--token-file` option replaces `--token-path`.
- Configuration files using `output: jsonl` continue to work unchanged.
## Included fixes
- Fix retry behavior after an HTTP 429 response.
- Add Windows ARM64 package.
- Correct timestamp handling for UTC offsets.
This is where GitHub Releases add value beyond the tag message. A tag annotation is a poor place to maintain a structured upgrade guide, and a commit history is arranged around how work was developed, not how users consume it. The release page gives support, security, and documentation teams a single URL to cite.
If a release fixes a security vulnerability, GitHub’s documentation recommends publishing a security advisory in the repository as well. Do not rely on a release note alone for a security communication that needs broader tracking and notification behavior.
Use prereleases for candidate builds that are real but not stable
A release candidate often needs almost all of the machinery of a final release: installers for QA, notes for testers, and a fixed version for bug reports. It does not need the implication that every user should upgrade.
GitHub Releases include a prerelease option specifically for this case. Create v4.0.0-rc.1, attach the candidate packages, and mark it as a prerelease. Testers can download exactly the same artifact your team approved, while ordinary users are not told that it is the current production version.
This is safer than putting a file called candidate.zip in a generic downloads location. The prerelease has a tag, notes, and an explicit stability signal. A bug report can name v4.0.0-rc.1; your engineers can compare it directly with v4.0.0-rc.2.
Be deliberate about promotion. If the final build is byte-for-byte identical to the candidate, you can publish a final release associated with the appropriate final tag only if that matches your versioning policy. Many teams instead build and tag a final v4.0.0 so the published version and package filenames say “stable” everywhere. The important part is not the convention; it is avoiding a release candidate that silently becomes production through an overly broad deployment rule.
Separate the tag trigger from the release-publish trigger
Teams frequently start with one event—“a tag was pushed”—and use it for build, deploy, and publish. That is convenient until the person allowed to create tags is not the person allowed to announce a public release.
A cleaner workflow has two checkpoints. First, pushing v2.4.2 builds and verifies artifacts. Second, creating or publishing the GitHub Release makes those artifacts publicly discoverable and supplies the final notes. GitHub provides distinct tag and release workflows, so you can use the tag as the immutable build input and the release as the publication approval.
- Create and push the annotated tag after the target commit is approved.
- Build packages from that exact tag in CI.
- Run platform tests and produce a checksum file.
- Draft a GitHub Release that selects
v2.4.2. - Upload the tested packages and paste upgrade notes.
- Publish the release after the release owner reviews it.
This split prevents a common organizational mismatch: developers need freedom to mark internal versions, while release managers need control over what becomes an endorsed download. It also keeps draft notes out of public view while legal, documentation, or support review is still underway.
Give users stable links instead of teaching them repository navigation
Tags are easy to browse for maintainers. They are less friendly in installation instructions, customer tickets, and README files. A GitHub Release gives each version its own shareable page, and GitHub documents a stable “latest release” URL pattern: append /releases/latest to the repository URL.
https://github.com/OWNER/REPOSITORY/releases/latest
That link is useful for a “Download the current stable version” button in documentation. It avoids hard-coding v2.4.2, which becomes stale at the next release. Link to a specific release page when you need reproducibility, such as an incident report or upgrade runbook; use the latest-release URL when you intentionally want the moving stable target.
There is a second-order cost here: “latest” is a product promise. Once installers, shell scripts, or internal wiki pages point at the latest release, publishing a mistaken release can redirect users immediately. Treat the final Publish action as a distribution change, not merely an administrative label.
For machine downloads, release assets also create a predictable place to fetch named files. Keep filenames versioned and platform-specific. A file named tool.zip is convenient until someone downloads it six months later and cannot tell which version it contains.
Do not attach every CI artifact to every release
GitHub Releases are a distribution catalog, not a substitute for all CI artifact storage. Attach the files a user must install or verify. Keep transient test reports, coverage output, debug logs, and every intermediate build in CI storage or another purpose-built location.
A useful release asset set is usually small: one package per supported platform or architecture, one checksum manifest, and perhaps a signature file if your distribution process uses signatures. The goal is that a user can scan the list in 15 seconds and choose the correct download.
| Artifact | Attach to release? | Reason |
|---|---|---|
| Signed Windows installer | Yes | It is the user-installable product. |
| Linux ARM64 tarball | Yes | It is a supported distribution target. |
checksums.txt |
Yes | It lets downloaders verify the selected package. |
| Unit-test XML report | No | Useful to CI and maintainers, not installers. |
| Debug build with symbols | Usually no | Publish only when users need it for a documented support workflow. |
Release assets create a retention expectation. Once documentation links to an installer, deleting or replacing it breaks reproducibility and user trust. Decide before publishing whether your project supports old releases indefinitely, only supported versions, or source-only access after a retirement date.
A practical rule for libraries, services, CLIs, and applications
Different project types reach the tag-versus-release boundary at different points. A reusable library can often begin with tags because package managers or source dependencies resolve versions directly. A desktop application should almost always publish releases because the user expects installers. A service may need only tags internally, unless customers, self-hosted operators, or auditors need visible release notes.
- Internal service: tag for deployment; add a release when a broader audience needs an auditable change summary.
- Open-source library: tag every version; publish a release when notes, source archives, or packaged extras help adopters upgrade.
- CLI tool: publish a release as soon as you support direct binary downloads on two or more operating systems.
- Desktop or mobile-adjacent application: publish releases because installers and user-facing fixes are the delivery product.
- Infrastructure module: tags may be enough when consumers pin versions, but releases help when upgrades have manual migration steps.
Do not create a GitHub Release solely because a version number exists. A release page with no assets, no notes, and no user-facing audience adds another ceremony without solving a delivery problem. Conversely, do not hide a distributable product behind tags just because your CI pipeline can build it.
Set up one release workflow this week
Pick the most recent version your users can download today and inspect the path from repository to installation. If it requires opening an Actions run, asking an engineer for a file, or reading commits to find a breaking change, make that version’s next successor a GitHub Release.
- Define a tag format, such as
vMAJOR.MINOR.PATCH, and document which tags can trigger production deployment. - Create a release checklist with four required items: tag selected, packages attached, checksum included, and upgrade notes reviewed.
- Use a draft release while binaries and notes are being prepared.
- Mark beta or release-candidate versions as prereleases rather than making users infer stability from a suffix.
- Add
/releases/latestto your README only after you are ready to maintain “latest” as a stable promise. - For the next release, ask a non-developer to download and install it from the release page without CI access.
That final test exposes the real answer. If the person can identify the right file, verify what changed, and get a working installation from one page, publish a GitHub Release. If all they need is an exact commit reference for tooling or source builds, the tag has already done its job.