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"