Releases and packages

Two distribution surfaces on GitHub: a Release is a tagged, human-facing bundle (notes + downloadable assets), while GitHub Packages is a registry hosting installable artifacts (npm, Maven, NuGet, RubyGems, containers).

Why it matters

A Git tag marks a commit; a Release turns it into something users consume — changelog, binaries, a stable download URL. Packages then let consumers npm install or docker pull that build by version, scoped to your org and gated by GitHub auth. Together they are the “ship it” half of CI/CD, usually fired from a tag push.

How it works

A Release wraps a Git tag with metadata and uploaded assets; a Package is published to a per-ecosystem registry endpoint with its own auth.

SurfaceAddressed byAuth to installTypical asset
Releasea git tag (v2.3.0)none (public)binaries, .zip, checksums
Container registryghcr.io/org/img:tagtoken / GITHUB_TOKENOCI image
npm/Maven/NuGetregistry URL + scopetoken in registry configpackage tarball
  • Tag → Release — a Release requires a tag; creating one in the UI or via gh release create v2.3.0 will create the tag if missing. Mark it prerelease or draft to stage before announcing.
  • Auto notes — “Generate release notes” diffs since the last tag, grouping merged PRs (honoring release.yml categories) — pairs with semver tags.
  • ghcr.io is the modern container registry (Docker Packages is deprecated); push with a GITHUB_TOKEN from Actions.
  • Immutability gap — Release assets are downloadable URLs but the underlying tag can be force-moved; consumers should pin by checksum for true integrity.

Example

Cut a release with a built binary from CI:

on: { push: { tags: ['v*'] } }
permissions: { contents: write }      # needed to create releases
jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: make build                 # → ./bin/app
      - run: gh release create "$GITHUB_REF_NAME" ./bin/app --generate-notes
        env: { GH_TOKEN: ${{ github.token }} }

Pitfalls

  • Moving a published tag — re-tagging v2.3.0 to a new commit silently changes what a Release points at; downloaders who pinned the tag get different bytes. Cut a new version instead.
  • Editing release assets in place — replacing an uploaded .zip keeps the URL but changes contents; ship a new release or publish checksums.
  • latest container tag:latest is a moving pointer, not a version; pin deploys to an immutable digest (@sha256:...).
  • Missing contents: writegh release create in Actions fails with 403 unless the workflow grants write permission.

See also