Releases and Packages
GitHub Releases attach versioned, downloadable artifacts and notes to a Git tag; GitHub Packages is a registry that hosts your built artifacts — npm, Docker, Maven, NuGet — next to the code. Together they cover the two halves of shipping a version: announcing it to humans and serving it to machines.
The three things involved — a tag, a release, and a package — are easy to conflate but do different jobs, and the most important rule across all of them is immutability: once a version is published, consumers pin to it, so re-pointing it later hands them different code than they audited.
Tags vs Releases
A Git tag is a pure pointer to a commit, with no assets. A release is metadata — notes and downloadable files — layered on top of that tag, with the tag serving as the immutable anchor. The tag is the contract; the release is the human-facing wrapper around it.
Release Notes
Notes can be written by hand, but GitHub auto-generates them from the pull requests merged since the previous tag, and a release.yml config groups those entries into categories based on PR labels. Hand-writing notes every time is wasted effort once labels and that config are in place.
Release Assets
A release can carry uploaded binaries and checksums as assets, served and download-counted by GitHub. These are the files a human clicks to download, distinct from a package a build tool resolves.
GitHub Packages
Packages is a registry supporting npm, the Container registry (ghcr.io), Maven, Gradle, NuGet, and RubyGems, with authentication by token. The Container registry is the notable one: publishing images to ghcr.io lets them inherit the repository's access control instead of requiring a separate credential for an external registry. Package visibility must be set deliberately, since defaults can leave an internal artifact public.
Automating Releases
Cutting a version should be one git push of a tag. A workflow triggered on on: push: tags can build the artifacts, create the release with an action like softprops/action-gh-release, and publish the package — tying the whole sequence to the release event so nothing is done by hand.
Git tag — a pure Git pointer to a commit, with no assets. Release — wraps a tag with notes and downloadable files for humans to read and grab.
Package — a machine-consumable artifact in a registry that npm install or docker pull resolves. For one version you often produce all three: tag the commit, cut a release with notes, and publish the package.
- Moving or re-pointing a tag after a release — consumers who pinned that version now get different code than they audited.
- Writing release notes by hand every time — GitHub auto-generates them from merged PRs once you configure labels and
release.yml. - Publishing a Docker image to an external registry when
ghcr.iowould inherit repo permissions and avoid a separate credential. - Not setting package visibility — a package can default in ways that leave an internal artifact public.
- Treating a pre-release as stable in automation — consumers'
^ranges then pull a-rctag they should not.
- Configure auto-generated release notes with a
release.ymlso notes are built from PR labels, not hand-typed. - Tag immutably and never re-point a published tag; cut a new version instead.
- Publish container images to
ghcr.ioto inherit repository access control rather than managing a separate registry credential. - Mark non-final builds as "pre-release" so semver ranges and download links treat them correctly.
- Automate tag-triggered releases in Actions with
on: push: tagsso cutting a version is onegit push.
Knowledge Check
How do a tag, a release, and a package relate for one version?
- The tag anchors the commit, the release wraps it with notes and files, and the package is the artifact a build tool resolves
- They are simply three different names for one and the same underlying object stored in the repo
- A package contains the tag, which in turn contains the release for that one version of the code, nesting all three inside one another in a strict hierarchy
- A release entirely replaces and supersedes the tag once it has been published to consumers, leaving the tag with nothing left to point at
Why is re-pointing a published tag a problem?
- Consumers who pinned that version now resolve to different code than they originally audited
- GitHub permanently locks the entire repository the moment a previously published tag gets re-pointed
- The release notes attached to that version are deleted automatically
- Older clones can no longer fetch from the repository at all
Why publish a container image to ghcr.io rather than an external registry?
- The image inherits the repository's access control, avoiding a separate registry credential to manage
- External registries are not able to host Docker images at all, only
ghcr.iocan - Only
ghcr.iosupports version tags on its images, a capability that external registries entirely lack - It makes the image public by default, which is a hard requirement for any GitHub-hosted package
What does marking a build as "pre-release" protect against?
- Consumers' semver ranges and "latest" download links treating an unfinished
-rcbuild as stable - The release being deleted by a scheduled cleanup automation before it ever has a chance to ship
- The underlying tag suddenly becoming mutable and re-pointable
- The package registry rejecting the artifact upload outright
You got correct