6.9 KiB
Release Packaging
Releases are published by GitHub Actions. Pushing a v* tag is the whole
release procedure; the rest of this document describes what that automation
runs, and how to reproduce it by hand when needed.
Workflows
| Workflow | Trigger | Result |
|---|---|---|
ci.yml |
push to main, every pull request |
gofmt, go vet, go test on Linux and macOS, plus a cross-build of all five release targets |
release.yml |
push of a v* tag |
runs make release-check, then release.sh, then publishes the GitHub release |
nightly.yml |
push to main |
rebuilds the tip of main and replaces the nightly prerelease |
release.yml builds through release.sh rather than reimplementing packaging,
so CI and a local run stay in step. Linux .deb and .rpm packages are built
with nFPM v2.47.0 from the exact Linux tarball binaries. See
Reproducibility for what that guarantees.
Release notes
release.yml looks for docs/releases/<tag>.md. If that file exists it becomes
the release body; otherwise GitHub generates notes from commit history. Write
the file before pushing the tag when a release deserves a real description.
The nightly prerelease
nightly.yml force-moves a rolling nightly tag to the tip of main and
republishes a prerelease from it. It is marked prerelease deliberately, so
GitHub's Latest badge stays on the newest v* release.
Because that tag moves, version discovery in build.sh and release.sh is
pinned with --match 'v*'. Without the filter git describe would select
nightly and stamp binaries with it instead of v<last release>-<n>-g<sha>.
Keep the filter if you touch those scripts.
Create a Tag
Use a semantic version tag. Pushing it is what triggers release.yml:
git status --short
git tag -a v0.2.0 -m "sshkeeper v0.2.0"
git push origin v0.2.0
The remaining sections describe the manual equivalent, which is still the way to test packaging locally or to recover if Actions is unavailable.
The release script uses git describe --tags --match 'v*' --always --dirty by
default. The --match 'v*' filter matters: nightly builds move a nightly tag
across main, and without the filter git describe would pick that tag and
stamp binaries nightly instead of v<last release>-<n>-g<sha>. You can also
pass the version explicitly:
./release.sh v0.2.0
or:
VERSION=v0.2.0 ./release.sh
For reproducible archives, the script uses SOURCE_DATE_EPOCH. By default it
uses the timestamp of the latest git commit. To force a specific timestamp:
SOURCE_DATE_EPOCH=1760000000 ./release.sh v0.2.0
Run Release Checks
Before packaging, run:
make release-check
This runs:
go test ./...go vet ./...- native
go build linux/amd64cross-build withCGO_ENABLED=0linux/arm64cross-build withCGO_ENABLED=0darwin/amd64cross-build withCGO_ENABLED=0darwin/arm64cross-build withCGO_ENABLED=0windows/amd64cross-build withCGO_ENABLED=0
Build Artifacts
Linux package generation requires nFPM v2.47.0. GitHub Actions installs this exact version; for a local release build install the same tool first:
go install github.com/goreleaser/nfpm/v2/cmd/nfpm@v2.47.0
Then run:
./release.sh v0.2.0
Expected files in dist/:
sshkeeper_v0.2.0_linux_amd64.tar.gz
sshkeeper_v0.2.0_linux_arm64.tar.gz
sshkeeper_v0.2.0_darwin_amd64.tar.gz
sshkeeper_v0.2.0_darwin_arm64.tar.gz
sshkeeper_v0.2.0_windows_amd64.zip
sshkeeper_0.2.0-1_amd64.deb
sshkeeper_0.2.0-1_arm64.deb
sshkeeper-0.2.0-1.x86_64.rpm
sshkeeper-0.2.0-1.aarch64.rpm
checksums.txt
Each archive contains:
sshkeeperorsshkeeper.exeREADME.mdLICENSEdocs/guide.md
Linux packages install the same release binary as /usr/bin/sshkeeper and add
README, LICENSE, and the user guide under /usr/share/doc/sshkeeper/. Debian
packages depend on openssh-client; RPM packages depend on openssh-clients.
The package revision starts at 1 and is reset when the upstream version changes.
Package maintainer scripts also migrate known legacy command paths. Existing
/usr/local/bin/sshkeeper and per-user ~/.local/bin/sshkeeper entries are moved
to a non-destructive *.legacy-backup and replaced by symlinks to the packaged
/usr/bin/sshkeeper. This makes package installation authoritative even for a
shell that already cached the old command path. Home-directory changes are executed as the account owner rather than as root.
Package removal restores backups; package upgrades keep the redirect active. Migration behavior is covered by
packaging/scripts/test-legacy-migration.sh and is part of make release-check.
Verify Checksums
From the dist/ directory:
sha256sum -c checksums.txt
Expected result: every archive reports OK.
Reproducibility
Rebuilding the same commit with the same Go version reproduces the binaries
byte for byte. release.sh pins everything that would otherwise vary:
-
-trimpathandCGO_ENABLED=0keep build paths and the host toolchain out of the binary; -
SOURCE_DATE_EPOCH(the commit timestamp) sets every archive mtime; -
tar --sort=name --owner=0 --group=0 --numeric-ownerfixes entry order and ownership, andgzip -ndrops the compression timestamp; -
normalize_packageforces 755 on directories and the program and 644 on everything else, so the builder's umask cannot leak into the archive. -
the Windows zip is packaged under
LC_ALL=CandTZ=UTC, becausesortorders entries by locale and zip stores DOS local time with no zone; -
nFPM receives the same
SOURCE_DATE_EPOCHand packages files extracted from the already-built Linux tarballs, so.deb/.rpmcontain the identical Linux binary rather than triggering a second compile.
With those in place the archives themselves reproduce across hosts: a build on
ubuntu-latest (umask 022, C locale, UTC) and one on a workstation (umask 002,
ru_RU.UTF-8, UTC+08) produce identical checksums for all five archives.
The strongest check is still the binary, since it does not depend on the host's
tar and gzip at all:
tar -xzf sshkeeper_<version>_linux_amd64.tar.gz
sha256sum sshkeeper_<version>_linux_amd64/sshkeeper
Comparing whole-archive hashes works too, as long as both builds used the same Go version.
Publish in GitHub Release
release.yml does this automatically on tag push. To publish by hand, upload:
- all five platform archives
checksums.txt
Release notes should mention platform status:
- Linux and macOS are primary release targets.
- macOS builds are available as tar.gz for amd64 and arm64 and require the
system
sshclient. - Windows build is experimental and requires OpenSSH Client available as
ssh.exeinPATH.
Packaging TODO
Native .deb and .rpm packages are part of the release pipeline. Remaining
package channels:
- Arch PKGBUILD / AUR
- Homebrew tap
- Scoop manifest
- Winget later