Files
minio/buildscripts/sign-release-rpms.sh
T
Feng Ruohang 4679314556 build(packages): unify the rpm and deb release segment as 1PGSTY
The rpm carried a bare -1 release while the deb carried none. Both now
ship the PGDG-style 1PGSTY segment: silo-VER-1PGSTY.arch.rpm and
silo_VER-1PGSTY_arch.deb, with nfpm rendering it as the RPM Release tag
and the Debian revision respectively. The apk stays bare because Alpine
pkgrel admits only -r<integer>.

sign-release-rpms.sh declares the value once as expected_release;
package-release.sh names artifacts from its own PKG_RELEASE copy, and
test-release.yml evals the signing script's value and asserts both the
download names and the packaged RPM Release/DEB Version against it, so
the copies cannot drift apart silently.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 21:02:15 +08:00

288 lines
9.2 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# These are the single source of truth for the package identity: .github/nfpm.yml
# must agree with them, and test-release.yml asserts that it does. Drift the
# other way round would only surface here, on the maintainer's machine, after
# the build has already run and uploaded.
expected_fingerprint="9592A7BC7A682E7333376E09E7935D8DB9BD8B20"
expected_release="1PGSTY"
expected_vendor="PGSTY"
expected_packager="Ruohang Feng (@Vonng) <rh@vonng.com>"
expected_url="https://silo.pgsty.com"
expected_summary="S3-Interface Libre Object Storage, a community-maintained S3-compatible server."
expected_description="S3-Interface Libre Object Storage, a community-maintained S3-compatible server."
expected_license="AGPL-3.0-or-later"
expected_group="Applications/File"
expected_payload="/etc/default/silo
/usr/bin/silo
/usr/lib/systemd/system/silo.service
/usr/lib/sysusers.d/silo.conf
/usr/share/doc/silo/LICENSE
/usr/share/doc/silo/NOTICE"
repository="${GH_REPO:-pgsty/silo}"
container="${DNFUPDATE_CONTAINER:-dnfupdate}"
upload=false
release_tag=""
usage() {
cat <<'EOF'
Usage: buildscripts/sign-release-rpms.sh RELEASE.TAG [--upload] [--repo OWNER/REPO] [--container NAME]
Downloads the two unsigned RPMs from a Draft GitHub Release, signs them with
the expected Pigsty key in the local dnfupdate container, verifies the result,
and regenerates their .sha256sum files. Nothing is uploaded unless --upload is
provided.
EOF
}
while [ "$#" -gt 0 ]; do
case "$1" in
--upload)
upload=true
;;
--repo)
shift
if [ "$#" -eq 0 ]; then
echo "--repo requires OWNER/REPO" >&2
exit 1
fi
repository="$1"
;;
--container)
shift
if [ "$#" -eq 0 ]; then
echo "--container requires a name" >&2
exit 1
fi
container="$1"
;;
-h|--help)
usage
exit 0
;;
-*)
echo "Unknown option: $1" >&2
usage >&2
exit 1
;;
*)
if [ -n "${release_tag}" ]; then
echo "Only one release tag may be specified" >&2
exit 1
fi
release_tag="$1"
;;
esac
shift
done
if [ -z "${release_tag}" ]; then
usage >&2
exit 1
fi
for command in docker gh; do
if ! command -v "${command}" >/dev/null 2>&1; then
echo "${command} is required" >&2
exit 1
fi
done
version_hyphen="${release_tag#RELEASE.}"
package_version="$(printf '%s\n' "${version_hyphen}" | sed -E \
's/^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2})-([0-9]{2})-([0-9]{2})Z$/\1\2\3\4\5\6.0.0/')"
if [ "${package_version}" = "${version_hyphen}" ]; then
echo "Invalid release tag: ${release_tag}" >&2
exit 1
fi
if [ "$(gh release view "${release_tag}" --repo "${repository}" --json isDraft --jq .isDraft)" != "true" ]; then
echo "Refusing to sign: ${release_tag} is not a Draft release" >&2
exit 1
fi
if [ "$(docker inspect --format '{{.State.Running}}' "${container}" 2>/dev/null || true)" != "true" ]; then
echo "Signing container is not running: ${container}" >&2
exit 1
fi
secret_fingerprints="$(docker exec "${container}" \
gpg --batch --with-colons --list-secret-keys 2>/dev/null |
awk -F: '$1 == "fpr" { print toupper($10) }')"
if ! printf '%s\n' "${secret_fingerprints}" | grep -Fxq "${expected_fingerprint}"; then
echo "Expected signing key is not available in ${container}: ${expected_fingerprint}" >&2
exit 1
fi
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
repo_dir="$(cd "${script_dir}/.." && pwd)"
work_root="${SIGN_WORKDIR:-${repo_dir}/.release-sign}"
mkdir -p "${work_root}"
work_dir="$(mktemp -d "${work_root}/${release_tag}.XXXXXX")"
unsigned_dir="${work_dir}/unsigned"
signed_dir="${work_dir}/signed"
mkdir -p "${unsigned_dir}" "${signed_dir}"
chmod 700 "${work_dir}" "${unsigned_dir}" "${signed_dir}"
rpm_files=(
"silo-${package_version}-${expected_release}.x86_64.rpm"
"silo-${package_version}-${expected_release}.aarch64.rpm"
)
download_patterns=()
for rpm_file in "${rpm_files[@]}"; do
download_patterns+=(--pattern "${rpm_file}" --pattern "${rpm_file}.sha256sum")
done
echo "Downloading RPMs from Draft release ${repository}@${release_tag}"
gh release download "${release_tag}" --repo "${repository}" \
--dir "${unsigned_dir}" "${download_patterns[@]}"
sha256_digest() {
local file="$1"
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "${file}" | awk '{print $1}'
else
shasum -a 256 "${file}" | awk '{print $1}'
fi
}
for rpm_file in "${rpm_files[@]}"; do
rpm_path="${unsigned_dir}/${rpm_file}"
checksum_path="${rpm_path}.sha256sum"
test -s "${rpm_path}"
test -s "${checksum_path}"
actual_line="$(sha256_digest "${rpm_path}") ${rpm_file}"
published_line="$(tr -d '\n' < "${checksum_path}")"
if [ "${actual_line}" != "${published_line}" ]; then
echo "Checksum mismatch for ${rpm_file}" >&2
exit 1
fi
done
safe_tag="$(printf '%s' "${release_tag}" | tr -c 'A-Za-z0-9._-' '_')"
container_dir="/tmp/silo-sign-${safe_tag}-$$"
docker exec "${container}" mkdir -p "${container_dir}"
cleanup_container() {
local rpm_file
for rpm_file in "${rpm_files[@]}"; do
docker exec "${container}" rm -f "${container_dir}/${rpm_file}" >/dev/null 2>&1 || true
done
docker exec "${container}" rmdir "${container_dir}" >/dev/null 2>&1 || true
}
trap cleanup_container EXIT
assert_rpm_tag() {
local rpm_path="$1"
local tag="$2"
local expected="$3"
local actual
actual="$(docker exec "${container}" rpm -qp --queryformat "%{${tag}}" "${rpm_path}")"
if [ "${actual}" != "${expected}" ]; then
echo "Unexpected RPM ${tag}: ${actual}" >&2
echo "Expected RPM ${tag}: ${expected}" >&2
exit 1
fi
}
for rpm_file in "${rpm_files[@]}"; do
case "${rpm_file}" in
*.x86_64.rpm)
expected_arch="x86_64"
;;
*.aarch64.rpm)
expected_arch="aarch64"
;;
*)
echo "Unexpected RPM filename: ${rpm_file}" >&2
exit 1
;;
esac
echo "Signing ${rpm_file} with ${expected_fingerprint}"
docker cp "${unsigned_dir}/${rpm_file}" "${container}:${container_dir}/${rpm_file}" >/dev/null
container_rpm="${container_dir}/${rpm_file}"
assert_rpm_tag "${container_rpm}" NAME silo
assert_rpm_tag "${container_rpm}" VERSION "${package_version}"
assert_rpm_tag "${container_rpm}" RELEASE "${expected_release}"
assert_rpm_tag "${container_rpm}" ARCH "${expected_arch}"
assert_rpm_tag "${container_rpm}" VENDOR "${expected_vendor}"
assert_rpm_tag "${container_rpm}" PACKAGER "${expected_packager}"
assert_rpm_tag "${container_rpm}" URL "${expected_url}"
assert_rpm_tag "${container_rpm}" LICENSE "${expected_license}"
assert_rpm_tag "${container_rpm}" GROUP "${expected_group}"
assert_rpm_tag "${container_rpm}" SUMMARY "${expected_summary}"
assert_rpm_tag "${container_rpm}" DESCRIPTION "${expected_description}"
rpm_payload="$(docker exec "${container}" rpm -qpl "${container_rpm}")"
if [ "${rpm_payload}" != "${expected_payload}" ]; then
echo "Unexpected RPM payload for ${rpm_file}:" >&2
printf '%s\n' "${rpm_payload}" >&2
exit 1
fi
docker exec "${container}" rpmsign \
--define "_gpg_name ${expected_fingerprint}" \
--addsign "${container_rpm}"
signature_output="$(docker exec "${container}" rpmkeys --checksig --verbose "${container_rpm}")"
printf '%s\n' "${signature_output}"
if ! printf '%s\n' "${signature_output}" | tr '[:upper:]' '[:lower:]' | grep -q 'key id b9bd8b20: ok'; then
echo "Signature verification failed for ${rpm_file}" >&2
exit 1
fi
docker cp "${container}:${container_dir}/${rpm_file}" "${signed_dir}/${rpm_file}" >/dev/null
signed_digest="$(sha256_digest "${signed_dir}/${rpm_file}")"
printf '%s %s' "${signed_digest}" "${rpm_file}" > "${signed_dir}/${rpm_file}.sha256sum"
docker exec "${container}" rpm -qp --queryformat \
$'Name: %{NAME}\nVersion: %{VERSION}-%{RELEASE}\nArch: %{ARCH}\nVendor: %{VENDOR}\nPackager: %{PACKAGER}\nURL: %{URL}\n' \
"${container_rpm}"
echo "SHA256: ${signed_digest}"
done
if [ "${upload}" = true ]; then
upload_files=()
for rpm_file in "${rpm_files[@]}"; do
upload_files+=("${signed_dir}/${rpm_file}" "${signed_dir}/${rpm_file}.sha256sum")
done
echo "Replacing RPMs in Draft release ${release_tag}"
gh release upload "${release_tag}" --repo "${repository}" --clobber "${upload_files[@]}"
for rpm_file in "${rpm_files[@]}"; do
for asset in "${rpm_file}" "${rpm_file}.sha256sum"; do
local_digest="sha256:$(sha256_digest "${signed_dir}/${asset}")"
remote_digest=""
for attempt in 1 2 3 4 5; do
remote_digest="$(gh release view "${release_tag}" --repo "${repository}" --json assets \
--jq ".assets[] | select(.name == \"${asset}\") | .digest")"
if [ "${local_digest}" = "${remote_digest}" ]; then
break
fi
if [ "${attempt}" -lt 5 ]; then
sleep 2
fi
done
if [ "${local_digest}" != "${remote_digest}" ]; then
echo "GitHub asset digest mismatch for ${asset}" >&2
echo "Local: ${local_digest}" >&2
echo "Remote: ${remote_digest}" >&2
exit 1
fi
echo "Verified GitHub asset: ${asset} ${remote_digest}"
done
done
else
echo
echo "Signed RPMs are ready for review in: ${signed_dir}"
echo "Re-run with --upload to replace the RPM assets in the Draft release."
fi