fix: normalize file modes when packaging a release
release.sh normalized entry order, ownership and mtimes, but not permissions, so the archives inherited the builder's umask. A host with umask 002 packaged 664/775 while ubuntu-latest packaged 644/755, and the two archives hashed differently even though every file inside was byte-identical: CI -rw-r--r-- README.md local -rw-rw-r-- README.md CI -rwxr-xr-x sshkeeper local -rwxrwxr-x sshkeeper Force 755 on directories and the program, 644 on everything else. Building the same commit under umask 002 and umask 022 now yields identical checksums. Also correct the reproducibility claim in the release docs. What is reproducible is the binary, given the same commit and Go version; the archive hash still depends on the host tar and gzip, so the documented verification step now compares the extracted binary instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
0a02f0fc60
commit
878f7b4472
|
|
@ -13,7 +13,8 @@ runs, and how to reproduce it by hand when needed.
|
||||||
| `nightly.yml` | push to `main` | rebuilds the tip of `main` and replaces the `nightly` prerelease |
|
| `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,
|
`release.yml` builds through `release.sh` rather than reimplementing packaging,
|
||||||
so a local run produces byte-identical archives.
|
so CI and a local run stay in step. See [Reproducibility](#reproducibility) for
|
||||||
|
what that guarantees.
|
||||||
|
|
||||||
### Release notes
|
### Release notes
|
||||||
|
|
||||||
|
|
@ -123,6 +124,30 @@ sha256sum -c checksums.txt
|
||||||
|
|
||||||
Expected result: every archive reports `OK`.
|
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:
|
||||||
|
|
||||||
|
- `-trimpath` and `CGO_ENABLED=0` keep 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-owner` fixes entry order and
|
||||||
|
ownership, and `gzip -n` drops the compression timestamp;
|
||||||
|
- `normalize_package` forces 755 on directories and the program and 644 on
|
||||||
|
everything else, so the builder's umask cannot leak into the archive.
|
||||||
|
|
||||||
|
To check a published build, compare the binary inside the archive rather than
|
||||||
|
the archive hash:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tar -xzf sshkeeper_<version>_linux_amd64.tar.gz
|
||||||
|
sha256sum sshkeeper_<version>_linux_amd64/sshkeeper
|
||||||
|
```
|
||||||
|
|
||||||
|
The archive hash additionally depends on the `tar` and `gzip` implementations
|
||||||
|
on the build host, so it is the weaker check of the two.
|
||||||
|
|
||||||
## Publish in GitHub Release
|
## Publish in GitHub Release
|
||||||
|
|
||||||
`release.yml` does this automatically on tag push. To publish by hand, upload:
|
`release.yml` does this automatically on tag push. To publish by hand, upload:
|
||||||
|
|
|
||||||
14
release.sh
14
release.sh
|
|
@ -28,6 +28,16 @@ package_docs() {
|
||||||
|
|
||||||
normalize_package() {
|
normalize_package() {
|
||||||
local package_dir="$1"
|
local package_dir="$1"
|
||||||
|
local binary="$2"
|
||||||
|
|
||||||
|
# Permissions must not depend on the builder's umask. Without this, a host
|
||||||
|
# with umask 002 packages 664/775 while one with umask 022 packages
|
||||||
|
# 644/755, and the archives differ even though every file inside is
|
||||||
|
# byte-identical.
|
||||||
|
find "${package_dir}" -type d -exec chmod 755 {} +
|
||||||
|
find "${package_dir}" -type f -exec chmod 644 {} +
|
||||||
|
chmod 755 "${package_dir}/${binary}"
|
||||||
|
|
||||||
find "${package_dir}" -exec touch -h -d "@${SOURCE_DATE_EPOCH}" {} +
|
find "${package_dir}" -exec touch -h -d "@${SOURCE_DATE_EPOCH}" {} +
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -43,7 +53,7 @@ build_tarball() {
|
||||||
|
|
||||||
GOOS="${goos}" GOARCH="${goarch}" CGO_ENABLED=0 go build -trimpath -ldflags "${LDFLAGS}" -o "${package_dir}/${APP}" .
|
GOOS="${goos}" GOARCH="${goarch}" CGO_ENABLED=0 go build -trimpath -ldflags "${LDFLAGS}" -o "${package_dir}/${APP}" .
|
||||||
package_docs "${package_dir}"
|
package_docs "${package_dir}"
|
||||||
normalize_package "${package_dir}"
|
normalize_package "${package_dir}" "${APP}"
|
||||||
tar --sort=name --owner=0 --group=0 --numeric-owner --mtime="@${SOURCE_DATE_EPOCH}" -cf - -C "${DIST_DIR}" "$(basename "${package_dir}")" | gzip -n > "${archive}"
|
tar --sort=name --owner=0 --group=0 --numeric-owner --mtime="@${SOURCE_DATE_EPOCH}" -cf - -C "${DIST_DIR}" "$(basename "${package_dir}")" | gzip -n > "${archive}"
|
||||||
rm -rf "${package_dir}"
|
rm -rf "${package_dir}"
|
||||||
}
|
}
|
||||||
|
|
@ -60,7 +70,7 @@ build_zip() {
|
||||||
|
|
||||||
GOOS="${goos}" GOARCH="${goarch}" CGO_ENABLED=0 go build -trimpath -ldflags "${LDFLAGS}" -o "${package_dir}/${APP}.exe" .
|
GOOS="${goos}" GOARCH="${goarch}" CGO_ENABLED=0 go build -trimpath -ldflags "${LDFLAGS}" -o "${package_dir}/${APP}.exe" .
|
||||||
package_docs "${package_dir}"
|
package_docs "${package_dir}"
|
||||||
normalize_package "${package_dir}"
|
normalize_package "${package_dir}" "${APP}.exe"
|
||||||
(cd "${DIST_DIR}" && find "$(basename "${package_dir}")" -print | sort | zip -X -q "$(basename "${archive}")" -@)
|
(cd "${DIST_DIR}" && find "$(basename "${package_dir}")" -print | sort | zip -X -q "$(basename "${archive}")" -@)
|
||||||
rm -rf "${package_dir}"
|
rm -rf "${package_dir}"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue