5.3 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. 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
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
checksums.txt
Each archive contains:
sshkeeperorsshkeeper.exeREADME.mdLICENSEdocs/guide.md
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.
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
Prepare these package channels after the first archive-based release:
- deb package
- Arch PKGBUILD / AUR
- rpm later
- Homebrew tap
- Scoop manifest
- Winget later