ci: verify release provenance, names, checksums, and payloads

Refuse dirty release checkouts and verify that every GoReleaser binary records the tagged revision with vcs.modified=false before packaging or publication.

Exercise the complete nFPM output in the release test pipeline: assert the six public names, validate every checksum and package identity field, and prove that RPM, DEB, and APK payloads contain the exact source binary and systemd unit. Validate release scripts and make their identity expectations the single source of truth.

Co-authored-by: ChatGPT <noreply@openai.com>
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Feng Ruohang
2026-08-04 14:56:24 +08:00
parent 10c7670b80
commit cf7df097b2
3 changed files with 175 additions and 6 deletions
+14
View File
@@ -29,6 +29,15 @@ jobs:
go-version-file: go.mod go-version-file: go.mod
cache: true cache: true
- name: Verify clean checkout
run: |
set -euo pipefail
if [ -n "$(git status --porcelain)" ]; then
echo "Refusing to release from a dirty working tree:" >&2
git status --porcelain >&2
exit 1
fi
- name: Compute release variables - name: Compute release variables
run: | run: |
set -euo pipefail set -euo pipefail
@@ -84,6 +93,11 @@ jobs:
LDFLAGS: ${{ env.LDFLAGS }} LDFLAGS: ${{ env.LDFLAGS }}
PKG_VERSION: ${{ env.PKG_VERSION }} PKG_VERSION: ${{ env.PKG_VERSION }}
- name: Verify binary provenance stamps
run: |
set -euo pipefail
buildscripts/verify-build-provenance.sh
- name: Install nFPM - name: Install nFPM
run: | run: |
set -euo pipefail set -euo pipefail
+110 -6
View File
@@ -5,10 +5,16 @@ on:
pull_request: pull_request:
paths: paths:
- ".github/goreleaser.yml" - ".github/goreleaser.yml"
- ".github/nfpm.yml"
- "Dockerfile.goreleaser" - "Dockerfile.goreleaser"
- "minio.service" - "minio.service"
- "buildscripts/package-release.sh"
- "buildscripts/sign-release-rpms.sh"
- "buildscripts/verify-build-provenance.sh"
- "buildscripts/gen-ldflags.go"
- ".github/workflows/release.yml" - ".github/workflows/release.yml"
- ".github/workflows/test-release.yml" - ".github/workflows/test-release.yml"
- ".gitignore"
permissions: permissions:
contents: read contents: read
@@ -35,9 +41,11 @@ jobs:
VERSION_COLON="2026-02-14T12:00:00Z" VERSION_COLON="2026-02-14T12:00:00Z"
PKG_VERSION="20260214120000.0.0" PKG_VERSION="20260214120000.0.0"
LDFLAGS="$(MINIO_RELEASE=RELEASE go run buildscripts/gen-ldflags.go "${VERSION_COLON}")" LDFLAGS="$(MINIO_RELEASE=RELEASE go run buildscripts/gen-ldflags.go "${VERSION_COLON}")"
echo "RELEASE_TAG=${RELEASE_TAG}" >> "${GITHUB_ENV}" {
echo "PKG_VERSION=${PKG_VERSION}" >> "${GITHUB_ENV}" echo "RELEASE_TAG=${RELEASE_TAG}"
echo "LDFLAGS=${LDFLAGS}" >> "${GITHUB_ENV}" echo "PKG_VERSION=${PKG_VERSION}"
echo "LDFLAGS=${LDFLAGS}"
} >> "${GITHUB_ENV}"
echo "PKG_VERSION: ${PKG_VERSION}" echo "PKG_VERSION: ${PKG_VERSION}"
echo "LDFLAGS: ${LDFLAGS}" echo "LDFLAGS: ${LDFLAGS}"
@@ -56,27 +64,61 @@ jobs:
LDFLAGS: ${{ env.LDFLAGS }} LDFLAGS: ${{ env.LDFLAGS }}
PKG_VERSION: ${{ env.PKG_VERSION }} PKG_VERSION: ${{ env.PKG_VERSION }}
- name: Install nFPM - name: Verify binary provenance stamps
run: |
set -euo pipefail
buildscripts/verify-build-provenance.sh
- name: Install package validation tools
run: | run: |
set -euo pipefail set -euo pipefail
go install github.com/goreleaser/nfpm/v2/cmd/nfpm@v2.47.0 go install github.com/goreleaser/nfpm/v2/cmd/nfpm@v2.47.0
echo "$(go env GOPATH)/bin" >> "${GITHUB_PATH}" echo "$(go env GOPATH)/bin" >> "${GITHUB_PATH}"
sudo apt-get update
sudo apt-get install --yes rpm binutils
- name: Package snapshot binaries with nFPM - name: Package snapshot binaries with nFPM
run: | run: |
set -euo pipefail set -euo pipefail
buildscripts/package-release.sh buildscripts/package-release.sh
- name: Verify package metadata - name: Validate package names, checksums, metadata, and payload
working-directory: dist/packages
run: | run: |
set -euo pipefail set -euo pipefail
cd dist/packages
# These are the public download names; a drift here breaks every
# script that fetches packages by URL.
expected=(
"minio-${PKG_VERSION}-1.aarch64.rpm"
"minio-${PKG_VERSION}-1.x86_64.rpm"
"minio_${PKG_VERSION}_aarch64.apk"
"minio_${PKG_VERSION}_amd64.deb"
"minio_${PKG_VERSION}_arm64.deb"
"minio_${PKG_VERSION}_x86_64.apk"
)
for package in "${expected[@]}"; do
test -s "${package}"
test -s "${package}.sha256sum"
sha256sum --check "${package}.sha256sum"
done
test "$(find . -maxdepth 1 -type f \( -name '*.rpm' -o -name '*.deb' -o -name '*.apk' \) | wc -l)" -eq 6
# The signing script asserts these same values, but it runs on the # The signing script asserts these same values, but it runs on the
# maintainer's machine after the release workflow has already built # maintainer's machine after the release workflow has already built
# and uploaded. Take its expectations as the single source of truth # and uploaded. Take its expectations as the single source of truth
# so nfpm.yml and the signing script cannot drift apart without # so nfpm.yml and the signing script cannot drift apart without
# failing here first, while a fix is still cheap. # failing here first, while a fix is still cheap.
#
# This grep is deliberately limited to the seven identity variables,
# all of which are single-line. That is what makes the eval safe:
# should one ever become multi-line, the grep captures an
# unterminated quote and the eval aborts on a syntax error under
# set -e rather than quietly binding an empty value and comparing
# against nothing. expected_payload is multi-line by design and must
# stay out of this set for the same reason.
eval "$(grep -E '^expected_(vendor|packager|url|summary|description|license|group)=' \ eval "$(grep -E '^expected_(vendor|packager|url|summary|description|license|group)=' \
../../buildscripts/sign-release-rpms.sh)" ../../buildscripts/sign-release-rpms.sh)"
for value in "${expected_vendor}" "${expected_packager}" "${expected_url}" \ for value in "${expected_vendor}" "${expected_packager}" "${expected_url}" \
@@ -85,6 +127,8 @@ jobs:
test -n "${value}" test -n "${value}"
done done
service_sha="$(sha256sum ../../minio.service | awk '{print $1}')"
rpm_file="minio-${PKG_VERSION}-1.x86_64.rpm" rpm_file="minio-${PKG_VERSION}-1.x86_64.rpm"
test "$(rpm -qp --queryformat '%{VENDOR}' "${rpm_file}")" = "${expected_vendor}" test "$(rpm -qp --queryformat '%{VENDOR}' "${rpm_file}")" = "${expected_vendor}"
test "$(rpm -qp --queryformat '%{PACKAGER}' "${rpm_file}")" = "${expected_packager}" test "$(rpm -qp --queryformat '%{PACKAGER}' "${rpm_file}")" = "${expected_packager}"
@@ -98,6 +142,7 @@ jobs:
# nothing to start it. # nothing to start it.
rpm -qpl "${rpm_file}" | grep -Fx '/usr/local/bin/minio' rpm -qpl "${rpm_file}" | grep -Fx '/usr/local/bin/minio'
rpm -qpl "${rpm_file}" | grep -Fx '/lib/systemd/system/minio.service' rpm -qpl "${rpm_file}" | grep -Fx '/lib/systemd/system/minio.service'
test "$(rpm -qpl "${rpm_file}" | wc -l)" -eq 2
deb_file="minio_${PKG_VERSION}_amd64.deb" deb_file="minio_${PKG_VERSION}_amd64.deb"
test "$(dpkg-deb --field "${deb_file}" Maintainer)" = "${expected_packager}" test "$(dpkg-deb --field "${deb_file}" Maintainer)" = "${expected_packager}"
@@ -116,5 +161,64 @@ jobs:
grep -Fx "license = ${expected_license}" <<< "${apk_info}" grep -Fx "license = ${expected_license}" <<< "${apk_info}"
grep -Fx "pkgdesc = ${expected_description}" <<< "${apk_info}" grep -Fx "pkgdesc = ${expected_description}" <<< "${apk_info}"
tar -tzf "minio_${PKG_VERSION}_x86_64.apk" | grep -Fx 'usr/local/bin/minio' tar -tzf "minio_${PKG_VERSION}_x86_64.apk" | grep -Fx 'usr/local/bin/minio'
tar -tzf "minio_${PKG_VERSION}_x86_64.apk" | grep -Fx 'lib/systemd/system/minio.service'
for arch in amd64 arm64; do
if [ "${arch}" = amd64 ]; then
rpm_arch=x86_64
deb_arch=amd64
apk_arch=x86_64
else
rpm_arch=aarch64
deb_arch=arm64
apk_arch=aarch64
fi
test "$(rpm -qp --queryformat '%{ARCH}' "minio-${PKG_VERSION}-1.${rpm_arch}.rpm")" = "${rpm_arch}"
test "$(dpkg-deb --field "minio_${PKG_VERSION}_${deb_arch}.deb" Architecture)" = "${deb_arch}"
grep -Fx "arch = ${apk_arch}" <<< "$(tar -xOzf "minio_${PKG_VERSION}_${apk_arch}.apk" .PKGINFO)"
# Accepted weakness: this takes the first match, unsorted, where
# find_binary in package-release.sh demands exactly one. It cannot
# be reached with an ambiguous match today, because packaging runs
# earlier in this same job and hard-fails on one. Revisit if
# goamd64 gains a second level, or if find_binary's exactly-one
# contract is ever relaxed -- at that point this weak copy would be
# the only one left choosing silently.
source_binary="$(find .. -maxdepth 2 -type f -path "../minio_linux_${arch}*/minio" | head -n 1)"
source_sha="$(sha256sum "${source_binary}" | awk '{print $1}')"
# Do not pipe rpm2cpio here: Debian's build exits non-zero even when
# it writes a correct payload, which trips `set -o pipefail`. Use
# rpm's own digests instead -- -K checks the payload against the
# header, and FILEDIGESTS is the sha256 rpm itself verifies on
# install.
rpm -K "minio-${PKG_VERSION}-1.${rpm_arch}.rpm"
rpm_sha="$(rpm -qp --queryformat '[%{FILENAMES} %{FILEDIGESTS}\n]' \
"minio-${PKG_VERSION}-1.${rpm_arch}.rpm" | awk '$1 == "/usr/local/bin/minio" { print $2 }')"
rpm_service_sha="$(rpm -qp --queryformat '[%{FILENAMES} %{FILEDIGESTS}\n]' \
"minio-${PKG_VERSION}-1.${rpm_arch}.rpm" | awk '$1 == "/lib/systemd/system/minio.service" { print $2 }')"
deb_sha="$(ar p "minio_${PKG_VERSION}_${deb_arch}.deb" data.tar.gz | tar -xzOf - ./usr/local/bin/minio | sha256sum | awk '{print $1}')"
deb_service_sha="$(ar p "minio_${PKG_VERSION}_${deb_arch}.deb" data.tar.gz | tar -xzOf - ./lib/systemd/system/minio.service | sha256sum | awk '{print $1}')"
apk_sha="$(tar -xzOf "minio_${PKG_VERSION}_${apk_arch}.apk" usr/local/bin/minio | sha256sum | awk '{print $1}')"
apk_service_sha="$(tar -xzOf "minio_${PKG_VERSION}_${apk_arch}.apk" lib/systemd/system/minio.service | sha256sum | awk '{print $1}')"
test "${source_sha}" = "${rpm_sha}"
test "${source_sha}" = "${deb_sha}"
test "${source_sha}" = "${apk_sha}"
test "${service_sha}" = "${rpm_service_sha}"
test "${service_sha}" = "${deb_service_sha}"
test "${service_sha}" = "${apk_service_sha}"
done
find . -maxdepth 1 -type f | sort find . -maxdepth 1 -type f | sort
- name: Validate release scripts
run: |
set -euo pipefail
bash -n buildscripts/package-release.sh
bash -n buildscripts/sign-release-rpms.sh
bash -n buildscripts/verify-build-provenance.sh
test -x buildscripts/package-release.sh
test -x buildscripts/sign-release-rpms.sh
test -x buildscripts/verify-build-provenance.sh
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
set -euo pipefail
# Asserts that every binary GoReleaser produced is stamped by the Go toolchain
# as built from this exact commit with a clean working tree. A stray untracked
# file (for example an un-ignored dist/) silently turns every release binary
# into a "+dirty" pseudo-version, which destroys the link between a published
# artifact and its tag. Catch that here instead of after publishing.
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
repo_dir="$(cd "${script_dir}/.." && pwd)"
dist_dir="${DIST_DIR:-${repo_dir}/dist}"
expected_count="${EXPECTED_BINARY_COUNT:-6}"
if ! command -v go >/dev/null 2>&1; then
echo "go is required" >&2
exit 1
fi
if [ ! -d "${dist_dir}" ]; then
echo "Missing GoReleaser dist directory: ${dist_dir}" >&2
exit 1
fi
revision="$(git -C "${repo_dir}" rev-parse HEAD)"
count=0
while IFS= read -r binary; do
count=$((count + 1))
info="$(go version -m "${binary}")"
if ! grep -qF "vcs.revision=${revision}" <<< "${info}"; then
echo "Unexpected vcs.revision in ${binary} (expected ${revision})" >&2
grep -F 'vcs.' <<< "${info}" >&2 || true
exit 1
fi
if ! grep -qF 'vcs.modified=false' <<< "${info}"; then
echo "Binary was built from a dirty working tree: ${binary}" >&2
grep -F 'vcs.' <<< "${info}" >&2 || true
exit 1
fi
done < <(find "${dist_dir}" -maxdepth 2 -type f \( -name 'minio' -o -name 'minio.exe' \) | sort)
if [ "${count}" -ne "${expected_count}" ]; then
echo "Expected ${expected_count} release binaries, found ${count}" >&2
exit 1
fi
echo "Verified ${count} binaries built from ${revision} with a clean tree"