From 10c7670b804c80b484c080220ddc9b92138beba0 Mon Sep 17 00:00:00 2001 From: Feng Ruohang Date: Tue, 4 Aug 2026 15:51:47 +0800 Subject: [PATCH] build: package releases with nFPM under the PGSTY identity Replace minio/pkger's hard-coded upstream identity with an in-tree nFPM configuration. Packages now name PGSTY as vendor and maintainer, use the SILO homepage and SPDX license, and preserve the established package names, versions, payload paths, modes, and checksum format. Resolve both the release binary and systemd unit independently of the caller's working directory. The release and test workflows share the same package script, while the signing script consumes the same final metadata contract. Co-authored-by: ChatGPT Co-authored-by: Claude --- .github/nfpm.yml | 38 +++++++++ .github/workflows/release.yml | 41 ++-------- .github/workflows/test-release.yml | 83 +++++++++++++------- buildscripts/package-release.sh | 120 +++++++++++++++++++++++++++++ buildscripts/sign-release-rpms.sh | 19 ++--- 5 files changed, 233 insertions(+), 68 deletions(-) create mode 100644 .github/nfpm.yml create mode 100755 buildscripts/package-release.sh diff --git a/.github/nfpm.yml b/.github/nfpm.yml new file mode 100644 index 000000000..706e80185 --- /dev/null +++ b/.github/nfpm.yml @@ -0,0 +1,38 @@ +name: minio +arch: ${NFPM_ARCH} +platform: linux +version: ${PKG_VERSION} +version_schema: none +release: ${NFPM_RELEASE} +section: utils +priority: optional +maintainer: "Ruohang Feng (@Vonng) " +description: S3-Interface Libre Object Storage, Community-maintained MinIO server fork. +vendor: PGSTY +homepage: https://silo.pgsty.com +license: AGPL-3.0-or-later + +contents: + - src: ${NFPM_SOURCE} + dst: /usr/local/bin/minio + expand: true + file_info: + mode: 0755 + owner: root + group: root + - src: ${NFPM_UNIT} + dst: /lib/systemd/system/minio.service + expand: true + file_info: + mode: 0644 + owner: root + group: root + +rpm: + group: Applications/File + compression: gzip:9 + +deb: + compression: gzip + fields: + License: AGPL-3.0-or-later diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d77f5282f..cc1bf2ee6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -84,49 +84,24 @@ jobs: LDFLAGS: ${{ env.LDFLAGS }} PKG_VERSION: ${{ env.PKG_VERSION }} - - name: Install pkger + - name: Install nFPM run: | - go install github.com/minio/pkger/v2@v2.6.18 + set -euo pipefail + go install github.com/goreleaser/nfpm/v2/cmd/nfpm@v2.47.0 echo "$(go env GOPATH)/bin" >> "${GITHUB_PATH}" - - name: Prepare package layout + - name: Build nFPM packages run: | set -euo pipefail - copy_binary() { - local arch="$1" - local pattern="$2" - local src - src="$(find dist -maxdepth 2 -type f -path "dist/${pattern}/minio" | head -n1 || true)" - if [ -z "${src}" ]; then - echo "Missing GoReleaser binary for ${arch} (${pattern})" - exit 1 - fi - mkdir -p "dist/linux-${arch}" - cp "${src}" "dist/linux-${arch}/minio.${RELEASE_TAG}" - } + buildscripts/package-release.sh - copy_binary amd64 "minio_linux_amd64*" - copy_binary arm64 "minio_linux_arm64*" - - - name: Build standard pkger packages - run: | - set -euo pipefail - pkger -r "${RELEASE_TAG}" --appName minio --releaseDir dist --ignore - - # Keep only full package files; drop convenience symlinks (minio.rpm/minio.deb/minio.apk) - find dist/linux-* -maxdepth 1 -type l \ - \( -name 'minio.rpm' -o -name 'minio.deb' -o -name 'minio.apk' \) -delete - - find dist -maxdepth 2 -type f \ - \( -name '*.rpm' -o -name '*.deb' -o -name '*.apk' -o -name '*.sha256sum' -o -name 'downloads-minio.json' \) | sort - - - name: Upload pkger artifacts to Draft release + - name: Upload nFPM packages to Draft release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail - mapfile -t files < <(find dist -maxdepth 2 -type f \ - \( -name '*.rpm' -o -name '*.deb' -o -name '*.apk' -o -name '*.sha256sum' -o -name 'downloads-minio.json' \) | sort) + mapfile -t files < <(find dist/packages -maxdepth 1 -type f \ + \( -name '*.rpm' -o -name '*.deb' -o -name '*.apk' -o -name '*.sha256sum' \) | sort) if [ "${#files[@]}" -eq 0 ]; then echo "No packages were generated." exit 1 diff --git a/.github/workflows/test-release.yml b/.github/workflows/test-release.yml index fcc6aa101..8f453404b 100644 --- a/.github/workflows/test-release.yml +++ b/.github/workflows/test-release.yml @@ -56,34 +56,65 @@ jobs: LDFLAGS: ${{ env.LDFLAGS }} PKG_VERSION: ${{ env.PKG_VERSION }} - - name: Install pkger - run: | - go install github.com/minio/pkger/v2@v2.6.18 - echo "$(go env GOPATH)/bin" >> "${GITHUB_PATH}" - - - name: Package snapshot binaries with pkger + - name: Install nFPM run: | set -euo pipefail - copy_binary() { - local arch="$1" - local pattern="$2" - local src - src="$(find dist -maxdepth 2 -type f -path "dist/${pattern}/minio" | head -n1 || true)" - if [ -z "${src}" ]; then - echo "Missing GoReleaser binary for ${arch} (${pattern})" - exit 1 - fi - mkdir -p "dist/linux-${arch}" - cp "${src}" "dist/linux-${arch}/minio.${RELEASE_TAG}" - } + go install github.com/goreleaser/nfpm/v2/cmd/nfpm@v2.47.0 + echo "$(go env GOPATH)/bin" >> "${GITHUB_PATH}" - copy_binary amd64 "minio_linux_amd64*" - copy_binary arm64 "minio_linux_arm64*" - pkger -r "${RELEASE_TAG}" --appName minio --releaseDir dist --ignore + - name: Package snapshot binaries with nFPM + run: | + set -euo pipefail + buildscripts/package-release.sh - # Keep only full package files; drop convenience symlinks (minio.rpm/minio.deb/minio.apk) - find dist/linux-* -maxdepth 1 -type l \ - \( -name 'minio.rpm' -o -name 'minio.deb' -o -name 'minio.apk' \) -delete + - name: Verify package metadata + working-directory: dist/packages + run: | + set -euo pipefail - find dist -maxdepth 2 -type f \ - \( -name '*.rpm' -o -name '*.deb' -o -name '*.apk' -o -name '*.sha256sum' -o -name 'downloads-minio.json' \) | sort + # The signing script asserts these same values, but it runs on the + # maintainer's machine after the release workflow has already built + # and uploaded. Take its expectations as the single source of truth + # so nfpm.yml and the signing script cannot drift apart without + # failing here first, while a fix is still cheap. + eval "$(grep -E '^expected_(vendor|packager|url|summary|description|license|group)=' \ + ../../buildscripts/sign-release-rpms.sh)" + for value in "${expected_vendor}" "${expected_packager}" "${expected_url}" \ + "${expected_summary}" "${expected_description}" \ + "${expected_license}" "${expected_group}"; do + test -n "${value}" + done + + rpm_file="minio-${PKG_VERSION}-1.x86_64.rpm" + test "$(rpm -qp --queryformat '%{VENDOR}' "${rpm_file}")" = "${expected_vendor}" + test "$(rpm -qp --queryformat '%{PACKAGER}' "${rpm_file}")" = "${expected_packager}" + test "$(rpm -qp --queryformat '%{URL}' "${rpm_file}")" = "${expected_url}" + test "$(rpm -qp --queryformat '%{SUMMARY}' "${rpm_file}")" = "${expected_summary}" + test "$(rpm -qp --queryformat '%{DESCRIPTION}' "${rpm_file}")" = "${expected_description}" + test "$(rpm -qp --queryformat '%{LICENSE}' "${rpm_file}")" = "${expected_license}" + test "$(rpm -qp --queryformat '%{GROUP}' "${rpm_file}")" = "${expected_group}" + # Both payload entries: the unit file is as much a part of the + # package as the binary, and losing it would install a server with + # nothing to start it. + rpm -qpl "${rpm_file}" | grep -Fx '/usr/local/bin/minio' + rpm -qpl "${rpm_file}" | grep -Fx '/lib/systemd/system/minio.service' + + deb_file="minio_${PKG_VERSION}_amd64.deb" + test "$(dpkg-deb --field "${deb_file}" Maintainer)" = "${expected_packager}" + test "$(dpkg-deb --field "${deb_file}" Version)" = "${PKG_VERSION}" + test "$(dpkg-deb --field "${deb_file}" License)" = "${expected_license}" + test "$(dpkg-deb --field "${deb_file}" Section)" = "utils" + test "$(dpkg-deb --field "${deb_file}" Homepage)" = "${expected_url}" + test "$(dpkg-deb --field "${deb_file}" Description)" = "${expected_description}" + dpkg-deb --contents "${deb_file}" | grep -E 'usr/local/bin/minio$' + dpkg-deb --contents "${deb_file}" | grep -E 'lib/systemd/system/minio\.service$' + + apk_info="$(tar -xOzf "minio_${PKG_VERSION}_x86_64.apk" .PKGINFO)" + grep -Fx "pkgver = ${PKG_VERSION}" <<< "${apk_info}" + grep -Fx "url = ${expected_url}" <<< "${apk_info}" + grep -Fx "maintainer = ${expected_packager}" <<< "${apk_info}" + grep -Fx "license = ${expected_license}" <<< "${apk_info}" + grep -Fx "pkgdesc = ${expected_description}" <<< "${apk_info}" + tar -tzf "minio_${PKG_VERSION}_x86_64.apk" | grep -Fx 'usr/local/bin/minio' + + find . -maxdepth 1 -type f | sort diff --git a/buildscripts/package-release.sh b/buildscripts/package-release.sh new file mode 100755 index 000000000..42cd56726 --- /dev/null +++ b/buildscripts/package-release.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env bash + +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_dir="$(cd "${script_dir}/.." && pwd)" +dist_dir="${DIST_DIR:-${repo_dir}/dist}" +nfpm_config="${NFPM_CONFIG:-${repo_dir}/.github/nfpm.yml}" + +if [ -z "${PKG_VERSION:-}" ]; then + echo "PKG_VERSION is required" >&2 + exit 1 +fi + +# Re-validate the shape release.yml derived from the tag. The packages are +# named from this, so a malformed value would ship under a name no repository +# can order against. +if ! [[ "${PKG_VERSION}" =~ ^[0-9]{14}\.0\.0$ ]]; then + echo "Invalid PKG_VERSION: ${PKG_VERSION}" >&2 + exit 1 +fi + +if ! command -v nfpm >/dev/null 2>&1; then + echo "nfpm is required" >&2 + exit 1 +fi + +if [ ! -f "${nfpm_config}" ]; then + echo "Missing nFPM config: ${nfpm_config}" >&2 + exit 1 +fi + +# nfpm resolves a relative content src against the current directory, not +# against the config file, so the unit path is passed in absolute. Otherwise +# this only works when invoked from the repository root and fails elsewhere on +# a message that names the file rather than the cause. +unit_file="${repo_dir}/minio.service" +if [ ! -f "${unit_file}" ]; then + echo "Missing systemd unit: ${unit_file}" >&2 + exit 1 +fi + +packages_dir="${dist_dir}/packages" +mkdir -p "${packages_dir}" + +# Two spaces, no trailing newline: sign-release-rpms.sh parses these files to +# check download integrity before it signs, and regenerates them afterwards in +# the same shape. +sha256_file() { + local file="$1" + local digest + + if command -v sha256sum >/dev/null 2>&1; then + digest="$(sha256sum "${file}" | awk '{print $1}')" + else + digest="$(shasum -a 256 "${file}" | awk '{print $1}')" + fi + + printf '%s %s' "${digest}" "$(basename "${file}")" > "${file}.sha256sum" +} + +find_binary() { + local goarch="$1" + local matches + local count + + # Must resolve to exactly one binary. Picking the first of several build + # variants (an added goamd64 level, a stale dist entry) would silently ship a + # package whose contents do not match its name. + matches="$(find "${dist_dir}" -maxdepth 2 -type f \ + -path "${dist_dir}/minio_linux_${goarch}*/minio" | sort)" + count="$(printf '%s' "${matches}" | grep -c . || true)" + + if [ "${count}" -eq 0 ]; then + echo "Missing GoReleaser binary for linux/${goarch}" >&2 + exit 1 + fi + if [ "${count}" -ne 1 ]; then + echo "Expected exactly one GoReleaser binary for linux/${goarch}, found ${count}:" >&2 + printf '%s\n' "${matches}" >&2 + exit 1 + fi + + printf '%s\n' "${matches}" +} + +build_arch() { + local goarch="$1" + local rpm_arch="$2" + local deb_arch="$3" + local apk_arch="$4" + local source + local rpm_file + local deb_file + local apk_file + + source="$(find_binary "${goarch}")" + + # These names are the public download names and must not drift; RPM carries a + # release number, DEB and APK do not, matching what pkger produced. + rpm_file="${packages_dir}/minio-${PKG_VERSION}-1.${rpm_arch}.rpm" + deb_file="${packages_dir}/minio_${PKG_VERSION}_${deb_arch}.deb" + apk_file="${packages_dir}/minio_${PKG_VERSION}_${apk_arch}.apk" + + PKG_VERSION="${PKG_VERSION}" NFPM_RELEASE=1 NFPM_ARCH="${goarch}" NFPM_SOURCE="${source}" NFPM_UNIT="${unit_file}" \ + nfpm package --config "${nfpm_config}" --packager rpm --target "${rpm_file}" + PKG_VERSION="${PKG_VERSION}" NFPM_RELEASE='' NFPM_ARCH="${goarch}" NFPM_SOURCE="${source}" NFPM_UNIT="${unit_file}" \ + nfpm package --config "${nfpm_config}" --packager deb --target "${deb_file}" + PKG_VERSION="${PKG_VERSION}" NFPM_RELEASE='' NFPM_ARCH="${goarch}" NFPM_SOURCE="${source}" NFPM_UNIT="${unit_file}" \ + nfpm package --config "${nfpm_config}" --packager apk --target "${apk_file}" + + sha256_file "${rpm_file}" + sha256_file "${deb_file}" + sha256_file "${apk_file}" +} + +build_arch amd64 x86_64 amd64 x86_64 +build_arch arm64 aarch64 arm64 aarch64 + +find "${packages_dir}" -maxdepth 1 -type f | sort diff --git a/buildscripts/sign-release-rpms.sh b/buildscripts/sign-release-rpms.sh index feb653305..d062e8470 100755 --- a/buildscripts/sign-release-rpms.sh +++ b/buildscripts/sign-release-rpms.sh @@ -2,16 +2,17 @@ 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_vendor="MinIO, Inc." -expected_packager="MinIO Development " -expected_url="https://min.io" -expected_summary="MinIO is a High Performance Object Storage released under AGPLv3." -expected_description="MinIO is a High Performance Object Storage released under AGPLv3. -It is API compatible with Amazon S3 cloud storage service. Use MinIO to build -high performance infrastructure for machine learning, analytics and application -data workloads." -expected_license="AGPLv3" +expected_vendor="PGSTY" +expected_packager="Ruohang Feng (@Vonng) " +expected_url="https://silo.pgsty.com" +expected_summary="S3-Interface Libre Object Storage, Community-maintained MinIO server fork." +expected_description="S3-Interface Libre Object Storage, Community-maintained MinIO server fork." +expected_license="AGPL-3.0-or-later" expected_group="Applications/File" expected_payload="/lib/systemd/system/minio.service /usr/local/bin/minio"