Software Release Practice HOWTO: version, tag, sign and ship
A good release lets strangers answer four questions without asking you: what version is this, what changed, may I use it, and is this really your code? Today that means a Semantic Versioning number, a signed Git tag, a changelog written for people, an SPDX license tag in every file, and a tarball with a checksum and a signature that anyone can rebuild.
The classic Software Release Practice HOWTO (first written in 2000, last revised in 2013) is kept by the LDP at tldp.org. It was written for the days of FTP sites and patches sent by email. This page covers the same ground with today's tools. The examples use a project called hello releasing version 1.4.0.
Pick a version number
Use Semantic Versioning (SemVer), currently version 2.0.0 of the specification. A version has three numbers, MAJOR.MINOR.PATCH, and each one tells users something:
- PATCH (1.4.0 → 1.4.1): bug fixes only, nothing breaks.
- MINOR (1.4.1 → 1.5.0): new features that do not break existing users. PATCH goes back to 0.
- MAJOR (1.5.0 → 2.0.0): an incompatible change to the public interface. MINOR and PATCH go back to 0.
SemVer only works if you say what your "public API" is: the command-line options, the config file format, the library functions, or the network protocol. Write that down in the README.
Three more rules worth knowing:
- 0.y.z means "unstable". Anything may change. Release 1.0.0 once people depend on the project in production.
- Pre-releases take a hyphen:
2.0.0-alpha.1,2.0.0-rc.1. They sort before the final version, so2.0.0-rc.1 < 2.0.0. - Build metadata takes a plus sign (
1.4.0+20261104) and is ignored when comparing versions.
The common Git tag v1.4.0 is a convention. The SemVer FAQ says the version itself is 1.4.0, so put the bare number in your tarball name and metadata.
Keep a changelog for people
Do not paste git log output into your release notes. Commit messages are full of merges and typo fixes. A changelog answers "what does this release change for me?". The Keep a Changelog format (version 1.1.0) is widely used: a file called CHANGELOG.md, newest version first, ISO dates (YYYY-MM-DD), and changes grouped under six headings: Added, Changed, Deprecated, Removed, Fixed and Security.
# Changelog
All notable changes to this project are documented in this file.
This project follows Semantic Versioning.
## [Unreleased]
## [1.4.0] - 2026-11-04
### Added
- `--json` option for machine-readable output.
### Fixed
- Crash when the config file is empty (#212).
### Security
- Refuse world-writable config files.
## [1.3.2] - 2026-08-19
### Fixed
- Wrong exit code when the input file is missing.
Add entries under [Unreleased] as you merge changes. At release time, rename that heading to the new version and date. If you pull a broken release, keep its entry and mark it [YANKED], for example ## [1.3.1] - 2026-08-02 [YANKED].
License every file
Pick an existing license approved by the Open Source Initiative. Writing your own license makes your code hard to reuse, because lawyers and distributions must review it from scratch. Put the full text in a LICENSE (or COPYING) file at the top of the tree.
Then add a one-line SPDX license identifier to each source file, in a comment, using the exact short name from the SPDX License List (version 3.29 as of November 2026):
// SPDX-License-Identifier: MIT
# SPDX-License-Identifier: GPL-2.0-or-later
/* SPDX-License-Identifier: Apache-2.0 OR MIT */
- Use
ORwhen users may choose,ANDwhen both apply, andWITHfor an exception such asGPL-3.0-only WITH Autoconf-exception-3.0. - GNU licenses always need
-onlyor-or-later. A bareGPL-2.0is ambiguous. - Keep existing copyright lines. The identifier states the license only.
Machines can read these tags, so distributions and companies can check your licensing without reading every file.
Tag and sign the release
Use an annotated tag for releases. The git-tag manual says lightweight tags are for private or temporary labels, and git describe ignores them by default. Better still, sign the tag:
git switch main
git pull --ff-only
# update CHANGELOG.md and the version number, then:
git commit -am "Release 1.4.0"
git tag -s v1.4.0 -m "hello 1.4.0"
git push origin main
git push origin v1.4.0
-s signs with your default key. By default that is an OpenPGP key used through GnuPG. Git can also sign with an SSH key, which many developers already have:
git config --global gpg.format ssh
git config --global user.signingKey ~/.ssh/id_ed25519.pub
Anyone can then check the tag with git tag -v v1.4.0. With SSH signatures, verification needs a list of trusted keys, set with gpg.ssh.allowedSignersFile. Each line is an email address followed by a public key. Without that file, the check fails.
Never move a published tag. Git does not overwrite tags that people have already fetched, so a re-tagged release leaves users with different code under the same name. If 1.4.0 is broken, release 1.4.1.
Build the release tarball
Build the source tarball from the tag, not from your working directory. That way, editor backup files and local changes can never slip in. git archive does this, and it sets each file's timestamp to the commit time, so the tar contents are the same every time you run it:
git archive --format=tar --prefix=hello-1.4.0/ v1.4.0 | gzip -6 -n > hello-1.4.0.tar.gz
The --prefix (with its trailing slash) makes the tarball unpack into hello-1.4.0/ instead of spraying files into the current directory. The -n stops gzip from storing a file name and time stamp. Reproducible Builds recommends piping through gzip like this rather than letting git archive compress the output itself, because gzip's own output is the part that can vary.
To leave files out of the tarball, such as CI configuration, mark them in .gitattributes:
.github/ export-ignore
.gitlab-ci.yml export-ignore
If your build generates files that users need (a configure script, for example), produce them in a clean checkout of the tag and say in the release notes how you did it.
Publish checksums and a signature
Give users two ways to check the download. A checksum catches corruption. A signature proves that the file came from you.
sha256sum hello-1.4.0.tar.gz > hello-1.4.0.tar.gz.sha256
gpg --armor --detach-sign hello-1.4.0.tar.gz # writes hello-1.4.0.tar.gz.asc
Users check them like this. With gpg --verify, give the signature first and the data file second:
sha256sum -c hello-1.4.0.tar.gz.sha256
gpg --verify hello-1.4.0.tar.gz.asc hello-1.4.0.tar.gz
A good check prints hello-1.4.0.tar.gz: OK and then Good signature from …. Publish your key fingerprint in the README and on your website, so users know which key to expect.
Make builds reproducible
A build is reproducible when anyone with the same source, build environment and instructions gets bit-for-bit identical files. Then users and distributions can rebuild your binaries and compare checksums, instead of trusting your build machine.
The usual culprits are time stamps, file order and user names. The fixes:
- Time: read
SOURCE_DATE_EPOCH(seconds since 1970-01-01 UTC) instead of the clock, and format it in UTC. Set it from the last commit withexport SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct). - Archives: sort entries and fix owners and times. With GNU tar 1.28 or later:
tar --sort=name --mtime="@${SOURCE_DATE_EPOCH}" \ --owner=0 --group=0 --numeric-owner \ --pax-option=exthdr.name=%d/PaxHeaders/%f,delete=atime,delete=ctime \ -cf product.tar build - Paths: do not embed the build directory or host name in binaries or generated docs.
Test it: build twice in two different directories, ideally as two different users, and compare the sha256sum of the results.
Make packaging easy
Most users will get your software from their distribution, not from your tarball. Packagers will thank you if you:
- Use a standard build system for your language (Autotools, Meson or CMake for C;
pyproject.tomlfor Python; Cargo for Rust) with the usual install targets. - Let the build run without network access. Distributions build offline.
- List dependencies with minimum versions, and do not bundle copies of common libraries.
- Keep tarball URLs stable and never re-upload a different file under the same name.
- Install a man page, and support
--helpand--version. - Announce security fixes clearly in the changelog's Security section.
Mistakes to avoid
- Version in five places: keep the version in one file and generate the rest, or a release will report the wrong number.
- Releasing from a dirty tree: run
git statusfirst; build from the tag. - Unsigned tags pushed by CI: if automation publishes releases, sign with a key kept for that purpose and document its fingerprint.
- Breaking changes in a minor release: users trust the numbers. If it breaks, bump MAJOR.
Next steps: Git basics and migrating old repositories, building RPM packages, and writing secure programs.
Sources
- Semantic Versioning 2.0.0, semver.org
- Keep a Changelog 1.1.0
- Handling license info (SPDX-License-Identifier), SPDX
- SPDX License List
- git-tag documentation, git-push and git-config (gpg.format, gpg.ssh.allowedSignersFile), git-scm.com
- git-archive documentation, git-scm.com
- Definition of reproducible builds, SOURCE_DATE_EPOCH and Archive metadata, reproducible-builds.org
- Operational GPG commands (--detach-sign, --verify), GnuPG manual
- sha256sum(1), Debian