ci: gate the rebrand on compatibility, packaging and provenance evidence

A rebrand of this size cannot be reviewed by reading the diff. The risk is not
what was renamed but what was renamed by accident: an environment variable, a
metric, a header, a route, a storage marker or an exported symbol that some
deployment depends on. So the invariants get a machine check.

rebrand-guard extracts the identifiers a product rename must not touch - the
go.mod module path, github.com/minio/* imports, MINIO_* variables, minio_*
metric literals, x-minio-* headers, route string literals, the hardcoded grid
routes in internal/grid/manager.go, the reserved bucket root, .minio.sys
markers, arn:minio and minio:s3 policy values, every exported symbol in cmd/
and internal/, and every branded string literal - and compares them to a
checked-in baseline. Any removal or addition fails the build, in both
directions, so a future change cannot quietly drop one either.

The recorded delta against the pre-rebrand tree is: module path, 137 imports,
19 metrics, 15 storage markers, 58 policy values and all 9014 exported symbols
byte-identical; environment 437 -> 436, headers 85 -> 84, routes 331 -> 330.
Each of those three is accounted for. MINIO_CONFIG_DIR and MINIO_VERSION were a
shell local and a compose variable in test scripts, never server settings.
x-minio-common was a docker-compose extension key, never an HTTP header.
/api/health/upload was the SUBNET upload path in the deleted callhome.go.

verify-rebrand.sh covers what the identifier guard cannot see: that the retired
upstream delivery paths stay deleted, that the packaging and container files
name silo, that silo.service keeps its Conflicts and both EnvironmentFile
lines, that silo.env contains no active assignment that would shadow the legacy
file, that no active delivery surface still publishes pgsty/minio, and that no
non-test Go source contains a dl.min.io, subnet.min.io, api.min.io,
slack.min.io or play.min.io URL.

Both, plus the entrypoint argv test, run in go.yml, release.yml and
test-release.yml. test-release.yml also gets the path filters right: it was
watching minio.service, which no longer exists, so the packaging gate would
have gone silently dormant. It now watches the unit, defaults, sysusers,
lifecycle scripts, helm chart and both guards, and asserts the four-file
payload, the absence of any cross-name Provides/Obsoletes/Conflicts, and that a
container started with the legacy `minio server` argv really runs silo as PID 1.

release.yml and docker-release.yml additionally verify that the checked-out
revision matches GITHUB_SHA before building. GitHub's OIDC certificate records
GITHUB_SHA, not the ref passed to actions/checkout, so a manual dispatch from
the wrong ref would have produced provenance describing different source from
the bytes being published. They now emit SBOMs and attestations for archives,
packages and images, and docker-release verifies the release workflow's
attestation on every artifact it consumes. Images publish to pgsty/silo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Feng Ruohang
2026-08-06 08:48:39 +08:00
parent e071bb77e4
commit bd8df51665
7 changed files with 11291 additions and 66 deletions
+98 -18
View File
@@ -4,12 +4,15 @@ on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
tag: tag:
description: "Published RELEASE.* tag to package as pgsty/minio" description: "Published RELEASE.* tag to package as pgsty/silo"
required: true required: true
type: string type: string
permissions: permissions:
contents: read contents: read
id-token: write
attestations: write
artifact-metadata: write
concurrency: concurrency:
group: docker-release group: docker-release
@@ -71,6 +74,15 @@ jobs:
ref: ${{ inputs.tag }} ref: ${{ inputs.tag }}
fetch-depth: 0 fetch-depth: 0
- name: Verify workflow identity matches release source
run: |
set -euo pipefail
CHECKED_OUT_REVISION="$(git rev-parse HEAD)"
if [ "${CHECKED_OUT_REVISION}" != "${GITHUB_SHA}" ]; then
echo "Checked out ${CHECKED_OUT_REVISION}, but workflow identity is ${GITHUB_SHA}. Dispatch this workflow from ${RELEASE_TAG}." >&2
exit 1
fi
- name: Prepare verified Docker contexts - name: Prepare verified Docker contexts
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -79,9 +91,9 @@ jobs:
assets_dir="docker-release/assets" assets_dir="docker-release/assets"
mkdir -p "${assets_dir}" mkdir -p "${assets_dir}"
amd64_archive="minio_${PKG_VERSION}_linux_amd64.tar.gz" amd64_archive="silo_${PKG_VERSION}_linux_amd64.tar.gz"
arm64_archive="minio_${PKG_VERSION}_linux_arm64.tar.gz" arm64_archive="silo_${PKG_VERSION}_linux_arm64.tar.gz"
checksums="minio_${PKG_VERSION}_checksums.txt" checksums="silo_${PKG_VERSION}_checksums.txt"
gh release download "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" \ gh release download "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" \
--dir "${assets_dir}" \ --dir "${assets_dir}" \
--pattern "${amd64_archive}" \ --pattern "${amd64_archive}" \
@@ -94,6 +106,16 @@ jobs:
cd "${assets_dir}" cd "${assets_dir}"
grep -F " ${amd64_archive}" "${checksums}" | sha256sum --check grep -F " ${amd64_archive}" "${checksums}" | sha256sum --check
grep -F " ${arm64_archive}" "${checksums}" | sha256sum --check grep -F " ${arm64_archive}" "${checksums}" | sha256sum --check
# The checksum manifest and both archives must have provenance from
# this repository's release workflow at the exact checked-out tag.
for artifact in "${checksums}" "${amd64_archive}" "${arm64_archive}"; do
gh attestation verify "${artifact}" \
--repo "${GITHUB_REPOSITORY}" \
--signer-workflow "${GITHUB_REPOSITORY}/.github/workflows/release.yml" \
--source-digest "$(git -C "${GITHUB_WORKSPACE}" rev-parse HEAD)" \
--source-ref "refs/tags/${RELEASE_TAG}" >/dev/null
done
cd "${GITHUB_WORKSPACE}" cd "${GITHUB_WORKSPACE}"
# Dockerfile.goreleaser expects the binary at the context root and # Dockerfile.goreleaser expects the binary at the context root and
@@ -101,9 +123,9 @@ jobs:
# GoReleaser used to assemble via extra_files. # GoReleaser used to assemble via extra_files.
for arch in amd64 arm64; do for arch in amd64 arm64; do
context="docker-release/${arch}" context="docker-release/${arch}"
archive="${assets_dir}/minio_${PKG_VERSION}_linux_${arch}.tar.gz" archive="${assets_dir}/silo_${PKG_VERSION}_linux_${arch}.tar.gz"
mkdir -p "${context}/dockerscripts" mkdir -p "${context}/dockerscripts"
tar -xzf "${archive}" -C "${context}" minio tar -xzf "${archive}" -C "${context}" silo
cp Dockerfile.goreleaser LICENSE CREDITS "${context}/" cp Dockerfile.goreleaser LICENSE CREDITS "${context}/"
cp dockerscripts/docker-entrypoint.sh dockerscripts/download-static-curl.sh \ cp dockerscripts/docker-entrypoint.sh dockerscripts/download-static-curl.sh \
"${context}/dockerscripts/" "${context}/dockerscripts/"
@@ -125,7 +147,13 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Install Syft
uses: anchore/sbom-action/download-syft@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
with:
syft-version: v1.50.0
- name: Build and push amd64 image - name: Build and push amd64 image
id: build-amd64
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: docker-release/amd64 context: docker-release/amd64
@@ -133,14 +161,15 @@ jobs:
platforms: linux/amd64 platforms: linux/amd64
push: true push: true
tags: | tags: |
pgsty/minio:${{ env.RELEASE_TAG }}-amd64 pgsty/silo:${{ env.RELEASE_TAG }}-amd64
pgsty/minio:latest-amd64 pgsty/silo:latest-amd64
labels: | labels: |
org.opencontainers.image.version=${{ env.RELEASE_TAG }} org.opencontainers.image.version=${{ env.RELEASE_TAG }}
org.opencontainers.image.created=${{ env.PUBLISHED_AT }} org.opencontainers.image.created=${{ env.PUBLISHED_AT }}
org.opencontainers.image.revision=${{ env.RELEASE_REVISION }} org.opencontainers.image.revision=${{ env.RELEASE_REVISION }}
- name: Build and push arm64 image - name: Build and push arm64 image
id: build-arm64
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: docker-release/arm64 context: docker-release/arm64
@@ -148,8 +177,8 @@ jobs:
platforms: linux/arm64 platforms: linux/arm64
push: true push: true
tags: | tags: |
pgsty/minio:${{ env.RELEASE_TAG }}-arm64 pgsty/silo:${{ env.RELEASE_TAG }}-arm64
pgsty/minio:latest-arm64 pgsty/silo:latest-arm64
labels: | labels: |
org.opencontainers.image.version=${{ env.RELEASE_TAG }} org.opencontainers.image.version=${{ env.RELEASE_TAG }}
org.opencontainers.image.created=${{ env.PUBLISHED_AT }} org.opencontainers.image.created=${{ env.PUBLISHED_AT }}
@@ -158,13 +187,64 @@ jobs:
- name: Publish multi-architecture manifests - name: Publish multi-architecture manifests
run: | run: |
set -euo pipefail set -euo pipefail
mkdir -p docker-release/metadata
docker buildx imagetools create \ docker buildx imagetools create \
--tag "pgsty/minio:${RELEASE_TAG}" \ --tag "pgsty/silo:${RELEASE_TAG}" \
"pgsty/minio:${RELEASE_TAG}-amd64" \ --metadata-file docker-release/metadata/release.json \
"pgsty/minio:${RELEASE_TAG}-arm64" "pgsty/silo:${RELEASE_TAG}-amd64" \
"pgsty/silo:${RELEASE_TAG}-arm64"
docker buildx imagetools create \ docker buildx imagetools create \
--tag "pgsty/minio:latest" \ --tag "pgsty/silo:latest" \
"pgsty/minio:latest-amd64" \ --metadata-file docker-release/metadata/latest.json \
"pgsty/minio:latest-arm64" "pgsty/silo:latest-amd64" \
docker buildx imagetools inspect "pgsty/minio:${RELEASE_TAG}" "pgsty/silo:latest-arm64"
docker buildx imagetools inspect "pgsty/minio:latest" docker buildx imagetools inspect "pgsty/silo:${RELEASE_TAG}"
docker buildx imagetools inspect "pgsty/silo:latest"
RELEASE_DIGEST="$(jq -r '."containerimage.descriptor".digest' docker-release/metadata/release.json)"
LATEST_DIGEST="$(jq -r '."containerimage.descriptor".digest' docker-release/metadata/latest.json)"
if ! [[ "${RELEASE_DIGEST}" =~ ^sha256:[0-9a-f]{64}$ ]]; then
echo "Invalid release manifest digest: ${RELEASE_DIGEST}" >&2
exit 1
fi
if [ "${RELEASE_DIGEST}" != "${LATEST_DIGEST}" ]; then
echo "Release and latest tags resolved to different manifests" >&2
exit 1
fi
echo "SILO_IMAGE_DIGEST=${RELEASE_DIGEST}" >> "${GITHUB_ENV}"
- name: Generate architecture image SBOMs
env:
AMD64_DIGEST: ${{ steps.build-amd64.outputs.digest }}
ARM64_DIGEST: ${{ steps.build-arm64.outputs.digest }}
SYFT_CHECK_FOR_APP_UPDATE: "false"
run: |
set -euo pipefail
mkdir -p docker-release/sbom
syft "registry:index.docker.io/pgsty/silo@${AMD64_DIGEST}" \
--output "spdx-json=docker-release/sbom/linux-amd64.spdx.json"
syft "registry:index.docker.io/pgsty/silo@${ARM64_DIGEST}" \
--output "spdx-json=docker-release/sbom/linux-arm64.spdx.json"
- name: Attest amd64 image SBOM
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
with:
subject-name: index.docker.io/pgsty/silo
subject-digest: ${{ steps.build-amd64.outputs.digest }}
sbom-path: docker-release/sbom/linux-amd64.spdx.json
push-to-registry: true
- name: Attest arm64 image SBOM
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
with:
subject-name: index.docker.io/pgsty/silo
subject-digest: ${{ steps.build-arm64.outputs.digest }}
sbom-path: docker-release/sbom/linux-arm64.spdx.json
push-to-registry: true
- name: Attest multi-architecture image provenance
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
with:
subject-name: index.docker.io/pgsty/silo
subject-digest: ${{ env.SILO_IMAGE_DIGEST }}
push-to-registry: true
+11 -4
View File
@@ -34,11 +34,12 @@ jobs:
- name: Check gofmt - name: Check gofmt
run: | run: |
unformatted=$(gofmt -l main.go cmd internal) mapfile -t unformatted < <(gofmt -l main.go cmd internal \
if [ -n "${unformatted}" ]; then buildscripts/rebrand-guard buildscripts/helm-migration-guard)
if [ "${#unformatted[@]}" -ne 0 ]; then
echo "The following files are not gofmt-formatted:" echo "The following files are not gofmt-formatted:"
echo "${unformatted}" printf '%s\n' "${unformatted[@]}"
gofmt -d ${unformatted} gofmt -d "${unformatted[@]}"
exit 1 exit 1
fi fi
@@ -50,6 +51,12 @@ jobs:
- name: Vet - name: Vet
run: go vet ./... run: go vet ./...
- name: Verify rebrand compatibility contracts
run: |
go run ./buildscripts/rebrand-guard
buildscripts/verify-rebrand.sh
dockerscripts/docker-entrypoint_test.sh
quality: quality:
name: Lint, Generated Files name: Lint, Generated Files
runs-on: ubuntu-latest runs-on: ubuntu-latest
+93 -2
View File
@@ -12,6 +12,9 @@ on:
permissions: permissions:
contents: write contents: write
id-token: write
attestations: write
artifact-metadata: write
jobs: jobs:
release: release:
@@ -36,12 +39,28 @@ jobs:
- name: Verify clean checkout - name: Verify clean checkout
run: | run: |
set -euo pipefail set -euo pipefail
# GitHub's OIDC certificate records GITHUB_SHA, not the ref passed to
# actions/checkout. A manual dispatch must therefore be launched from
# the release tag itself; otherwise the provenance identity would
# describe different source from the bytes being published.
CHECKED_OUT_REVISION="$(git rev-parse HEAD)"
if [ "${CHECKED_OUT_REVISION}" != "${GITHUB_SHA}" ]; then
echo "Checked out ${CHECKED_OUT_REVISION}, but workflow identity is ${GITHUB_SHA}. Dispatch from the release tag." >&2
exit 1
fi
if [ -n "$(git status --porcelain)" ]; then if [ -n "$(git status --porcelain)" ]; then
echo "Refusing to release from a dirty working tree:" >&2 echo "Refusing to release from a dirty working tree:" >&2
git status --porcelain >&2 git status --porcelain >&2
exit 1 exit 1
fi fi
- name: Verify rebrand compatibility contracts
run: |
set -euo pipefail
go run ./buildscripts/rebrand-guard
buildscripts/verify-rebrand.sh
dockerscripts/docker-entrypoint_test.sh
- name: Compute release variables - name: Compute release variables
env: env:
# Passed through the environment, never interpolated into the script # Passed through the environment, never interpolated into the script
@@ -74,6 +93,19 @@ jobs:
echo "Package version: ${PKG_VERSION}" echo "Package version: ${PKG_VERSION}"
echo "LDFLAGS: ${LDFLAGS}" echo "LDFLAGS: ${LDFLAGS}"
# Both installer actions are pinned to immutable commits. The explicit
# tool versions keep the release format reproducible across workflow
# reruns while the installers verify the downloaded executables.
- name: Install Syft
uses: anchore/sbom-action/download-syft@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
with:
syft-version: v1.50.0
- name: Install Cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
with:
cosign-release: v3.1.2
- name: Build Draft release with GoReleaser - name: Build Draft release with GoReleaser
uses: goreleaser/goreleaser-action@v6 uses: goreleaser/goreleaser-action@v6
with: with:
@@ -100,18 +132,77 @@ jobs:
set -euo pipefail set -euo pipefail
buildscripts/package-release.sh buildscripts/package-release.sh
- name: Generate package SBOMs and signed checksum manifest
env:
SYFT_CHECK_FOR_APP_UPDATE: "false"
run: |
set -euo pipefail
packages_dir="dist/packages"
mapfile -t packages < <(find "${packages_dir}" -maxdepth 1 -type f \
\( -name '*.rpm' -o -name '*.deb' -o -name '*.apk' \) | sort)
if [ "${#packages[@]}" -ne 6 ]; then
echo "Expected six Linux packages, found ${#packages[@]}" >&2
printf '%s\n' "${packages[@]}" >&2
exit 1
fi
for package in "${packages[@]}"; do
syft "${package}" --output "spdx-json=${package}.sbom.json"
done
manifest="${packages_dir}/silo_${PKG_VERSION}_packages_checksums.txt"
(
cd "${packages_dir}"
mapfile -t subjects < <(find . -maxdepth 1 -type f \
\( -name '*.rpm' -o -name '*.deb' -o -name '*.apk' -o -name '*.sbom.json' \) | sort)
if [ "${#subjects[@]}" -ne 12 ]; then
echo "Expected six packages and six SBOMs, found ${#subjects[@]} subjects" >&2
exit 1
fi
sha256sum "${subjects[@]}" | sed 's# \./# #' > "$(basename "${manifest}")"
)
cosign sign-blob --bundle="${manifest}.sigstore.json" "${manifest}" --yes
- name: Attest downloadable release artifacts
id: attest-release
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
with:
subject-path: |
dist/*.tar.gz
dist/*.zip
dist/*.sbom.json
dist/*_checksums.txt
dist/*.sigstore.json
dist/packages/*.rpm
dist/packages/*.deb
dist/packages/*.apk
dist/packages/*.sha256sum
dist/packages/*.sbom.json
dist/packages/*_checksums.txt
dist/packages/*.sigstore.json
- name: Preserve provenance bundle as a release asset
env:
BUNDLE_PATH: ${{ steps.attest-release.outputs.bundle-path }}
run: |
set -euo pipefail
test -s "${BUNDLE_PATH}"
cp "${BUNDLE_PATH}" "dist/silo_${PKG_VERSION}_provenance.sigstore.json"
- name: Upload nFPM packages to Draft release - name: Upload nFPM packages to Draft release
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
set -euo pipefail set -euo pipefail
mapfile -t files < <(find dist/packages -maxdepth 1 -type f \ mapfile -t files < <(find dist/packages -maxdepth 1 -type f \
\( -name '*.rpm' -o -name '*.deb' -o -name '*.apk' -o -name '*.sha256sum' \) | sort) \( -name '*.rpm' -o -name '*.deb' -o -name '*.apk' -o -name '*.sha256sum' \
-o -name '*.sbom.json' -o -name '*_checksums.txt' -o -name '*.sigstore.json' \) | sort)
if [ "${#files[@]}" -eq 0 ]; then if [ "${#files[@]}" -eq 0 ]; then
echo "No packages were generated." echo "No packages were generated."
exit 1 exit 1
fi fi
gh release upload "${RELEASE_TAG}" "${files[@]}" gh release upload "${RELEASE_TAG}" "${files[@]}" \
"dist/silo_${PKG_VERSION}_provenance.sigstore.json"
- name: Upload dist artifact - name: Upload dist artifact
if: always() if: always()
+152 -42
View File
@@ -7,13 +7,27 @@ on:
- ".github/goreleaser.yml" - ".github/goreleaser.yml"
- ".github/nfpm.yml" - ".github/nfpm.yml"
- "Dockerfile.goreleaser" - "Dockerfile.goreleaser"
- "dockerscripts/download-static-curl.sh"
- "dockerscripts/docker-entrypoint.sh" - "dockerscripts/docker-entrypoint.sh"
- "minio.service" - "dockerscripts/docker-entrypoint_test.sh"
- "silo.service"
- "silo.env"
- "silo.sysusers"
- "buildscripts/package-release.sh" - "buildscripts/package-release.sh"
- "buildscripts/package/postinstall.sh"
- "buildscripts/package/preremove.sh"
- "buildscripts/package/lifecycle_test.sh"
- "buildscripts/minio-upgrade.sh"
- "buildscripts/sign-release-rpms.sh" - "buildscripts/sign-release-rpms.sh"
- "buildscripts/verify-build-provenance.sh" - "buildscripts/verify-build-provenance.sh"
- "buildscripts/verify-rebrand.sh"
- "buildscripts/verify-helm-migration.sh"
- "buildscripts/helm-migration-guard/**"
- "helm/silo/**"
- "buildscripts/rebrand-guard/**"
- "buildscripts/gen-ldflags.go" - "buildscripts/gen-ldflags.go"
- ".github/workflows/release.yml" - ".github/workflows/release.yml"
- ".github/workflows/docker-release.yml"
- ".github/workflows/test-release.yml" - ".github/workflows/test-release.yml"
- ".gitignore" - ".gitignore"
@@ -56,15 +70,41 @@ jobs:
version: "~> v2" version: "~> v2"
args: check --config .github/goreleaser.yml args: check --config .github/goreleaser.yml
- name: Validate Helm chart and legacy upgrade identity
run: buildscripts/verify-helm-migration.sh
- name: Install Syft
uses: anchore/sbom-action/download-syft@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
with:
syft-version: v1.50.0
- name: Build snapshot artifacts - name: Build snapshot artifacts
uses: goreleaser/goreleaser-action@v6 uses: goreleaser/goreleaser-action@v6
with: with:
version: "~> v2" version: "~> v2"
args: release --snapshot --clean --skip=publish,docker --config .github/goreleaser.yml # A pull-request snapshot has no trusted release identity. Exercise
# the SBOM/checksum pipeline here, and reserve keyless signing for
# the tag-triggered release workflow with GitHub OIDC.
args: release --snapshot --clean --skip=publish,docker,sign --config .github/goreleaser.yml
env: env:
LDFLAGS: ${{ env.LDFLAGS }} LDFLAGS: ${{ env.LDFLAGS }}
PKG_VERSION: ${{ env.PKG_VERSION }} PKG_VERSION: ${{ env.PKG_VERSION }}
- name: Verify archive SBOM and checksum coverage
run: |
set -euo pipefail
mapfile -t archives < <(find dist -maxdepth 1 -type f -name 'silo_*.tar.gz' | sort)
mapfile -t sboms < <(find dist -maxdepth 1 -type f -name 'silo_*.tar.gz.sbom.json' | sort)
test "${#archives[@]}" -eq 6
test "${#sboms[@]}" -eq 6
manifest="dist/silo_${PKG_VERSION}_checksums.txt"
test -s "${manifest}"
(
cd dist
sha256sum --check "$(basename "${manifest}")"
)
test "$(wc -l < "${manifest}" | tr -d ' ')" -eq 12
- name: Verify binary provenance stamps - name: Verify binary provenance stamps
run: | run: |
set -euo pipefail set -euo pipefail
@@ -91,12 +131,12 @@ jobs:
# These are the public download names; a drift here breaks every # These are the public download names; a drift here breaks every
# script that fetches packages by URL. # script that fetches packages by URL.
expected=( expected=(
"minio-${PKG_VERSION}-1.aarch64.rpm" "silo-${PKG_VERSION}-1.aarch64.rpm"
"minio-${PKG_VERSION}-1.x86_64.rpm" "silo-${PKG_VERSION}-1.x86_64.rpm"
"minio_${PKG_VERSION}_aarch64.apk" "silo_${PKG_VERSION}_aarch64.apk"
"minio_${PKG_VERSION}_amd64.deb" "silo_${PKG_VERSION}_amd64.deb"
"minio_${PKG_VERSION}_arm64.deb" "silo_${PKG_VERSION}_arm64.deb"
"minio_${PKG_VERSION}_x86_64.apk" "silo_${PKG_VERSION}_x86_64.apk"
) )
for package in "${expected[@]}"; do for package in "${expected[@]}"; do
@@ -128,9 +168,11 @@ jobs:
test -n "${value}" test -n "${value}"
done done
service_sha="$(sha256sum ../../minio.service | awk '{print $1}')" service_sha="$(sha256sum ../../silo.service | awk '{print $1}')"
defaults_sha="$(sha256sum ../../silo.env | awk '{print $1}')"
sysusers_sha="$(sha256sum ../../silo.sysusers | awk '{print $1}')"
rpm_file="minio-${PKG_VERSION}-1.x86_64.rpm" rpm_file="silo-${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}"
test "$(rpm -qp --queryformat '%{URL}' "${rpm_file}")" = "${expected_url}" test "$(rpm -qp --queryformat '%{URL}' "${rpm_file}")" = "${expected_url}"
@@ -138,31 +180,55 @@ jobs:
test "$(rpm -qp --queryformat '%{DESCRIPTION}' "${rpm_file}")" = "${expected_description}" test "$(rpm -qp --queryformat '%{DESCRIPTION}' "${rpm_file}")" = "${expected_description}"
test "$(rpm -qp --queryformat '%{LICENSE}' "${rpm_file}")" = "${expected_license}" test "$(rpm -qp --queryformat '%{LICENSE}' "${rpm_file}")" = "${expected_license}"
test "$(rpm -qp --queryformat '%{GROUP}' "${rpm_file}")" = "${expected_group}" test "$(rpm -qp --queryformat '%{GROUP}' "${rpm_file}")" = "${expected_group}"
# Both payload entries: the unit file is as much a part of the rpm -qpl "${rpm_file}" | grep -Fx '/usr/bin/silo'
# package as the binary, and losing it would install a server with rpm -qpl "${rpm_file}" | grep -Fx '/usr/lib/systemd/system/silo.service'
# nothing to start it. rpm -qpl "${rpm_file}" | grep -Fx '/etc/default/silo'
rpm -qpl "${rpm_file}" | grep -Fx '/usr/local/bin/minio' rpm -qpl "${rpm_file}" | grep -Fx '/usr/lib/sysusers.d/silo.conf'
rpm -qpl "${rpm_file}" | grep -Fx '/usr/lib/systemd/system/minio.service' test "$(rpm -qpl "${rpm_file}" | wc -l)" -eq 4
test "$(rpm -qpl "${rpm_file}" | wc -l)" -eq 2 if rpm -qp --conflicts "${rpm_file}" | grep -qi minio; then
echo "RPM must not declare a cross-name conflict with MinIO" >&2
exit 1
fi
if rpm -qp --obsoletes "${rpm_file}" | grep -qi minio; then
echo "RPM must not obsolete a MinIO package" >&2
exit 1
fi
if rpm -qp --provides "${rpm_file}" | grep -qi minio; then
echo "RPM must not provide a MinIO package alias" >&2
exit 1
fi
deb_file="minio_${PKG_VERSION}_amd64.deb" deb_file="silo_${PKG_VERSION}_amd64.deb"
test "$(dpkg-deb --field "${deb_file}" Maintainer)" = "${expected_packager}" test "$(dpkg-deb --field "${deb_file}" Maintainer)" = "${expected_packager}"
test "$(dpkg-deb --field "${deb_file}" Version)" = "${PKG_VERSION}" test "$(dpkg-deb --field "${deb_file}" Version)" = "${PKG_VERSION}"
test "$(dpkg-deb --field "${deb_file}" License)" = "${expected_license}" test "$(dpkg-deb --field "${deb_file}" License)" = "${expected_license}"
test "$(dpkg-deb --field "${deb_file}" Section)" = "utils" test "$(dpkg-deb --field "${deb_file}" Section)" = "utils"
test "$(dpkg-deb --field "${deb_file}" Homepage)" = "${expected_url}" test "$(dpkg-deb --field "${deb_file}" Homepage)" = "${expected_url}"
test "$(dpkg-deb --field "${deb_file}" Description)" = "${expected_description}" 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 'usr/bin/silo$'
dpkg-deb --contents "${deb_file}" | grep -E 'usr/lib/systemd/system/minio\.service$' dpkg-deb --contents "${deb_file}" | grep -E 'usr/lib/systemd/system/silo\.service$'
dpkg-deb --contents "${deb_file}" | grep -E 'etc/default/silo$'
dpkg-deb --contents "${deb_file}" | grep -E 'usr/lib/sysusers\.d/silo\.conf$'
test "$(dpkg-deb --contents "${deb_file}" | awk '$1 !~ /^d/ { count++ } END { print count + 0 }')" -eq 4
test -z "$(dpkg-deb --field "${deb_file}" Conflicts)"
test -z "$(dpkg-deb --field "${deb_file}" Replaces)"
test -z "$(dpkg-deb --field "${deb_file}" Provides)"
apk_info="$(tar -xOzf "minio_${PKG_VERSION}_x86_64.apk" .PKGINFO)" apk_info="$(tar -xOzf "silo_${PKG_VERSION}_x86_64.apk" .PKGINFO)"
grep -Fx "pkgver = ${PKG_VERSION}" <<< "${apk_info}" grep -Fx "pkgver = ${PKG_VERSION}" <<< "${apk_info}"
grep -Fx "url = ${expected_url}" <<< "${apk_info}" grep -Fx "url = ${expected_url}" <<< "${apk_info}"
grep -Fx "maintainer = ${expected_packager}" <<< "${apk_info}" grep -Fx "maintainer = ${expected_packager}" <<< "${apk_info}"
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 "silo_${PKG_VERSION}_x86_64.apk" | grep -Fx 'usr/bin/silo'
tar -tzf "minio_${PKG_VERSION}_x86_64.apk" | grep -Fx 'usr/lib/systemd/system/minio.service' tar -tzf "silo_${PKG_VERSION}_x86_64.apk" | grep -Fx 'usr/lib/systemd/system/silo.service'
tar -tzf "silo_${PKG_VERSION}_x86_64.apk" | grep -Fx 'etc/default/silo'
tar -tzf "silo_${PKG_VERSION}_x86_64.apk" | grep -Fx 'usr/lib/sysusers.d/silo.conf'
test "$(tar -tzf "silo_${PKG_VERSION}_x86_64.apk" | awk '$0 !~ /^\./ && $0 !~ /\/$/ { count++ } END { print count + 0 }')" -eq 4
if grep -Ei '^provides = .*minio' <<< "${apk_info}"; then
echo "APK must not provide a MinIO package alias" >&2
exit 1
fi
for arch in amd64 arm64; do for arch in amd64 arm64; do
if [ "${arch}" = amd64 ]; then if [ "${arch}" = amd64 ]; then
@@ -175,9 +241,9 @@ jobs:
apk_arch=aarch64 apk_arch=aarch64
fi fi
test "$(rpm -qp --queryformat '%{ARCH}' "minio-${PKG_VERSION}-1.${rpm_arch}.rpm")" = "${rpm_arch}" test "$(rpm -qp --queryformat '%{ARCH}' "silo-${PKG_VERSION}-1.${rpm_arch}.rpm")" = "${rpm_arch}"
test "$(dpkg-deb --field "minio_${PKG_VERSION}_${deb_arch}.deb" Architecture)" = "${deb_arch}" test "$(dpkg-deb --field "silo_${PKG_VERSION}_${deb_arch}.deb" Architecture)" = "${deb_arch}"
grep -Fx "arch = ${apk_arch}" <<< "$(tar -xOzf "minio_${PKG_VERSION}_${apk_arch}.apk" .PKGINFO)" grep -Fx "arch = ${apk_arch}" <<< "$(tar -xOzf "silo_${PKG_VERSION}_${apk_arch}.apk" .PKGINFO)"
# Accepted weakness: this takes the first match, unsorted, where # Accepted weakness: this takes the first match, unsorted, where
# find_binary in package-release.sh demands exactly one. It cannot # find_binary in package-release.sh demands exactly one. It cannot
@@ -186,7 +252,7 @@ jobs:
# goamd64 gains a second level, or if find_binary's exactly-one # goamd64 gains a second level, or if find_binary's exactly-one
# contract is ever relaxed -- at that point this weak copy would be # contract is ever relaxed -- at that point this weak copy would be
# the only one left choosing silently. # the only one left choosing silently.
source_binary="$(find .. -maxdepth 2 -type f -path "../minio_linux_${arch}*/minio" | head -n 1)" source_binary="$(find .. -maxdepth 2 -type f -path "../silo_linux_${arch}*/silo" | head -n 1)"
source_sha="$(sha256sum "${source_binary}" | awk '{print $1}')" source_sha="$(sha256sum "${source_binary}" | awk '{print $1}')"
# Do not pipe rpm2cpio here: Debian's build exits non-zero even when # Do not pipe rpm2cpio here: Debian's build exits non-zero even when
@@ -194,15 +260,23 @@ jobs:
# rpm's own digests instead -- -K checks the payload against the # rpm's own digests instead -- -K checks the payload against the
# header, and FILEDIGESTS is the sha256 rpm itself verifies on # header, and FILEDIGESTS is the sha256 rpm itself verifies on
# install. # install.
rpm -K "minio-${PKG_VERSION}-1.${rpm_arch}.rpm" rpm -K "silo-${PKG_VERSION}-1.${rpm_arch}.rpm"
rpm_sha="$(rpm -qp --queryformat '[%{FILENAMES} %{FILEDIGESTS}\n]' \ rpm_sha="$(rpm -qp --queryformat '[%{FILENAMES} %{FILEDIGESTS}\n]' \
"minio-${PKG_VERSION}-1.${rpm_arch}.rpm" | awk '$1 == "/usr/local/bin/minio" { print $2 }')" "silo-${PKG_VERSION}-1.${rpm_arch}.rpm" | awk '$1 == "/usr/bin/silo" { print $2 }')"
rpm_service_sha="$(rpm -qp --queryformat '[%{FILENAMES} %{FILEDIGESTS}\n]' \ rpm_service_sha="$(rpm -qp --queryformat '[%{FILENAMES} %{FILEDIGESTS}\n]' \
"minio-${PKG_VERSION}-1.${rpm_arch}.rpm" | awk '$1 == "/usr/lib/systemd/system/minio.service" { print $2 }')" "silo-${PKG_VERSION}-1.${rpm_arch}.rpm" | awk '$1 == "/usr/lib/systemd/system/silo.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}')" rpm_defaults_sha="$(rpm -qp --queryformat '[%{FILENAMES} %{FILEDIGESTS}\n]' \
deb_service_sha="$(ar p "minio_${PKG_VERSION}_${deb_arch}.deb" data.tar.gz | tar -xzOf - ./usr/lib/systemd/system/minio.service | sha256sum | awk '{print $1}')" "silo-${PKG_VERSION}-1.${rpm_arch}.rpm" | awk '$1 == "/etc/default/silo" { print $2 }')"
apk_sha="$(tar -xzOf "minio_${PKG_VERSION}_${apk_arch}.apk" usr/local/bin/minio | sha256sum | awk '{print $1}')" rpm_sysusers_sha="$(rpm -qp --queryformat '[%{FILENAMES} %{FILEDIGESTS}\n]' \
apk_service_sha="$(tar -xzOf "minio_${PKG_VERSION}_${apk_arch}.apk" usr/lib/systemd/system/minio.service | sha256sum | awk '{print $1}')" "silo-${PKG_VERSION}-1.${rpm_arch}.rpm" | awk '$1 == "/usr/lib/sysusers.d/silo.conf" { print $2 }')"
deb_sha="$(ar p "silo_${PKG_VERSION}_${deb_arch}.deb" data.tar.gz | tar -xzOf - ./usr/bin/silo | sha256sum | awk '{print $1}')"
deb_service_sha="$(ar p "silo_${PKG_VERSION}_${deb_arch}.deb" data.tar.gz | tar -xzOf - ./usr/lib/systemd/system/silo.service | sha256sum | awk '{print $1}')"
deb_defaults_sha="$(ar p "silo_${PKG_VERSION}_${deb_arch}.deb" data.tar.gz | tar -xzOf - ./etc/default/silo | sha256sum | awk '{print $1}')"
deb_sysusers_sha="$(ar p "silo_${PKG_VERSION}_${deb_arch}.deb" data.tar.gz | tar -xzOf - ./usr/lib/sysusers.d/silo.conf | sha256sum | awk '{print $1}')"
apk_sha="$(tar -xzOf "silo_${PKG_VERSION}_${apk_arch}.apk" usr/bin/silo | sha256sum | awk '{print $1}')"
apk_service_sha="$(tar -xzOf "silo_${PKG_VERSION}_${apk_arch}.apk" usr/lib/systemd/system/silo.service | sha256sum | awk '{print $1}')"
apk_defaults_sha="$(tar -xzOf "silo_${PKG_VERSION}_${apk_arch}.apk" etc/default/silo | sha256sum | awk '{print $1}')"
apk_sysusers_sha="$(tar -xzOf "silo_${PKG_VERSION}_${apk_arch}.apk" usr/lib/sysusers.d/silo.conf | sha256sum | awk '{print $1}')"
test "${source_sha}" = "${rpm_sha}" test "${source_sha}" = "${rpm_sha}"
test "${source_sha}" = "${deb_sha}" test "${source_sha}" = "${deb_sha}"
@@ -210,6 +284,12 @@ jobs:
test "${service_sha}" = "${rpm_service_sha}" test "${service_sha}" = "${rpm_service_sha}"
test "${service_sha}" = "${deb_service_sha}" test "${service_sha}" = "${deb_service_sha}"
test "${service_sha}" = "${apk_service_sha}" test "${service_sha}" = "${apk_service_sha}"
test "${defaults_sha}" = "${rpm_defaults_sha}"
test "${defaults_sha}" = "${deb_defaults_sha}"
test "${defaults_sha}" = "${apk_defaults_sha}"
test "${sysusers_sha}" = "${rpm_sysusers_sha}"
test "${sysusers_sha}" = "${deb_sysusers_sha}"
test "${sysusers_sha}" = "${apk_sysusers_sha}"
done done
find . -maxdepth 1 -type f | sort find . -maxdepth 1 -type f | sort
@@ -223,19 +303,20 @@ jobs:
# goreleaser already produced; the mcli-download build stage is skipped # goreleaser already produced; the mcli-download build stage is skipped
# on purpose to keep this gate offline and deterministic. # on purpose to keep this gate offline and deterministic.
ctx="$(mktemp -d)" ctx="$(mktemp -d)"
tar -xzf "dist/minio_${PKG_VERSION}_linux_amd64.tar.gz" -C "${ctx}" minio tar -xzf "dist/silo_${PKG_VERSION}_linux_amd64.tar.gz" -C "${ctx}" silo
cp dockerscripts/docker-entrypoint.sh "${ctx}/docker-entrypoint.sh" cp dockerscripts/docker-entrypoint.sh "${ctx}/docker-entrypoint.sh"
{ {
echo "FROM registry.access.redhat.com/ubi9/ubi-micro:latest" echo "FROM registry.access.redhat.com/ubi9/ubi-micro:latest"
echo "COPY minio /usr/bin/minio" echo "COPY silo /usr/bin/silo"
echo "COPY docker-entrypoint.sh /usr/bin/docker-entrypoint.sh" echo "COPY docker-entrypoint.sh /usr/bin/docker-entrypoint.sh"
echo "RUN mkdir -p /data && chmod 0777 /data && chmod +x /usr/bin/minio /usr/bin/docker-entrypoint.sh" echo "RUN mkdir -p /data && chmod 0777 /data && chmod +x /usr/bin/silo /usr/bin/docker-entrypoint.sh"
echo 'ENV HOME=/tmp'
echo 'ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]' echo 'ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]'
echo 'CMD ["minio"]' echo 'CMD ["silo"]'
} > "${ctx}/Dockerfile" } > "${ctx}/Dockerfile"
docker build -t minio-runtime-test:snapshot "${ctx}" docker build -t silo-runtime-test:snapshot "${ctx}"
# PID 1 must be minio, not the entry shell, on every privilege path, so # PID 1 must be silo, not the entry shell, on every privilege path, so
# a SIGTERM from docker stop reaches the server and it exits gracefully # a SIGTERM from docker stop reaches the server and it exits gracefully
# instead of being killed at the stop timeout. Regression guard for the # instead of being killed at the stop timeout. Regression guard for the
# exec-into-chroot entrypoint fix. # exec-into-chroot entrypoint fix.
@@ -244,7 +325,7 @@ jobs:
docker rm -f "${name}" >/dev/null 2>&1 || true docker rm -f "${name}" >/dev/null 2>&1 || true
docker run -d --name "${name}" \ docker run -d --name "${name}" \
-e MINIO_CI_CD=1 -e MINIO_ROOT_USER=ciadmin -e MINIO_ROOT_PASSWORD=ciadmin-secret-123 \ -e MINIO_CI_CD=1 -e MINIO_ROOT_USER=ciadmin -e MINIO_ROOT_PASSWORD=ciadmin-secret-123 \
"$@" minio-runtime-test:snapshot minio server /data --address :9000 >/dev/null "$@" silo-runtime-test:snapshot silo server /data --address :9000 >/dev/null
up="" up=""
for _ in $(seq 1 60); do for _ in $(seq 1 60); do
if docker logs "${name}" 2>&1 | grep -q "API:"; then up=1; break; fi if docker logs "${name}" 2>&1 | grep -q "API:"; then up=1; break; fi
@@ -263,15 +344,44 @@ jobs:
[ "${code}" = "0" ] || { echo "non-zero exit (${name}): ${code}"; exit 1; } [ "${code}" = "0" ] || { echo "non-zero exit (${name}): ${code}"; exit 1; }
[ "${elapsed}" -lt 10 ] || { echo "shutdown too slow (${name}): ${elapsed}s - signal not forwarded"; exit 1; } [ "${elapsed}" -lt 10 ] || { echo "shutdown too slow (${name}): ${elapsed}s - signal not forwarded"; exit 1; }
} }
assert_graceful minio-rt-default assert_graceful silo-rt-default
assert_graceful minio-rt-dropuser -e MINIO_USERNAME=minio-user -e MINIO_GROUPNAME=minio-group assert_graceful silo-rt-dropuser -e MINIO_USERNAME=silo-user -e MINIO_GROUPNAME=silo-group
assert_graceful silo-rt-rootless --user 1001:1001
# The compatibility shim translates only the legacy first argv token;
# the image contains no /usr/bin/minio file.
docker run --rm silo-runtime-test:snapshot sh -c 'test ! -e /usr/bin/minio'
docker run --rm -d --name silo-rt-legacy \
-e MINIO_CI_CD=1 -e MINIO_ROOT_USER=ciadmin -e MINIO_ROOT_PASSWORD=ciadmin-secret-123 \
silo-runtime-test:snapshot minio server /data --address :9000 >/dev/null
sleep 2
test "$(docker exec silo-rt-legacy cat /proc/1/comm)" = silo
docker rm -f silo-rt-legacy >/dev/null
- name: Validate release scripts - name: Validate release scripts
run: | run: |
set -euo pipefail set -euo pipefail
bash -n buildscripts/package-release.sh bash -n buildscripts/package-release.sh
bash -n buildscripts/minio-upgrade.sh
bash -n buildscripts/sign-release-rpms.sh bash -n buildscripts/sign-release-rpms.sh
bash -n buildscripts/verify-build-provenance.sh bash -n buildscripts/verify-build-provenance.sh
bash -n buildscripts/verify-rebrand.sh
bash -n buildscripts/verify-helm-migration.sh
sh -n buildscripts/package/postinstall.sh
sh -n buildscripts/package/preremove.sh
bash -n buildscripts/package/lifecycle_test.sh
buildscripts/package/lifecycle_test.sh
bash -n dockerscripts/docker-entrypoint_test.sh
bash -n dockerscripts/download-static-curl.sh
dockerscripts/docker-entrypoint_test.sh
go run ./buildscripts/rebrand-guard
buildscripts/verify-rebrand.sh
test -x buildscripts/package-release.sh test -x buildscripts/package-release.sh
test -x buildscripts/sign-release-rpms.sh test -x buildscripts/sign-release-rpms.sh
test -x buildscripts/verify-build-provenance.sh test -x buildscripts/verify-build-provenance.sh
test -x buildscripts/verify-rebrand.sh
test -x buildscripts/verify-helm-migration.sh
test -x buildscripts/package/postinstall.sh
test -x buildscripts/package/preremove.sh
test -x buildscripts/package/lifecycle_test.sh
test -x dockerscripts/docker-entrypoint_test.sh
File diff suppressed because it is too large Load Diff
+480
View File
@@ -0,0 +1,480 @@
// Copyright 2026 PGSTY contributors.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// rebrand-guard records the compatibility identifiers that a product rebrand
// must not accidentally rename. It intentionally excludes product branding and
// delivery names, which are validated by buildscripts/verify-rebrand.sh.
package main
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"flag"
"fmt"
"go/ast"
"go/parser"
"go/token"
"os"
"os/exec"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
)
const manifestVersion = 3
var (
minioImportRE = regexp.MustCompile(`github\.com/minio/[A-Za-z0-9_./-]+`)
envRE = regexp.MustCompile(`\b_?MINIO_[A-Z0-9_]+\b`)
metricRE = regexp.MustCompile(`\bminio_[A-Za-z0-9_]+\b`)
headerRE = regexp.MustCompile(`(?i)\bx-minio-[a-z0-9_-]+\b`)
routeRE = regexp.MustCompile(`^/[A-Za-z0-9._~!$&'()*+,;=:@%/?{}=-]*`)
storageRE = regexp.MustCompile(`\.minio\.sys(?:/[A-Za-z0-9._${}-]+)*`)
policyRE = regexp.MustCompile(`(?:arn:minio|minio:s3)[A-Za-z0-9_:/.*${}-]*`)
brandRE = regexp.MustCompile(`(?i)(^|[^a-z0-9_])minio([^a-z0-9_]|$)`)
)
type manifest struct {
Version int `json:"version"`
ModulePath string `json:"module_path"`
MinioImports []string `json:"minio_imports"`
Environment []string `json:"environment"`
Metrics []string `json:"metrics"`
Headers []string `json:"headers"`
Routes []string `json:"routes"`
RouteRoots []string `json:"route_roots"`
GridRoutes []string `json:"grid_routes"`
StorageMarkers []string `json:"storage_markers"`
PolicyValues []string `json:"policy_values"`
ExportedSymbols []string `json:"exported_symbols"`
BrandAllowlist []string `json:"brand_allowlist"`
}
func main() {
write := flag.Bool("write", false, "replace the checked-in compatibility baseline")
flag.Parse()
repo, err := gitOutput("rev-parse", "--show-toplevel")
if err != nil {
fatal(err)
}
repo = strings.TrimSpace(repo)
baselinePath := filepath.Join(repo, "buildscripts", "rebrand-guard", "compat-baseline.json")
current, err := collect(repo)
if err != nil {
fatal(err)
}
if *write {
if err := writeManifest(baselinePath, current); err != nil {
fatal(err)
}
fmt.Printf("wrote %s\n", baselinePath)
printSummary(current)
return
}
want, err := readManifest(baselinePath)
if err != nil {
fatal(err)
}
if err := compare(want, current); err != nil {
fatal(err)
}
printSummary(current)
fmt.Println("Silo rebrand compatibility baseline is unchanged")
}
func collect(repo string) (manifest, error) {
files, err := trackedFiles(repo)
if err != nil {
return manifest{}, err
}
sets := map[string]map[string]struct{}{
"imports": {},
"env": {},
"metrics": {},
"headers": {},
"routes": {},
"roots": {},
"grid": {},
"storage": {},
"policy": {},
"exported": {},
"brand": {},
}
modulePath := ""
fset := token.NewFileSet()
for _, rel := range files {
if rel == "SILO_REBRANDING_MIGRATION.md" ||
strings.HasPrefix(rel, "buildscripts/rebrand-guard/") ||
strings.HasPrefix(rel, "buildscripts/helm-migration-guard/") {
continue
}
path := filepath.Join(repo, filepath.FromSlash(rel))
data, err := os.ReadFile(path)
if errors.Is(err, os.ErrNotExist) {
continue
}
if err != nil {
return manifest{}, fmt.Errorf("read %s: %w", rel, err)
}
if bytes.IndexByte(data, 0) >= 0 {
continue
}
text := string(data)
addMatches(sets["env"], envRE, text, false)
addMatches(sets["headers"], headerRE, text, true)
addMatches(sets["storage"], storageRE, text, false)
addMatches(sets["policy"], policyRE, text, false)
if strings.HasSuffix(rel, ".go") && (strings.HasPrefix(rel, "cmd/") || strings.HasPrefix(rel, "internal/")) {
addMatches(sets["metrics"], metricRE, text, false)
}
if rel == "go.mod" {
addMatches(sets["imports"], minioImportRE, text, false)
for _, line := range strings.Split(text, "\n") {
fields := strings.Fields(line)
if len(fields) == 2 && fields[0] == "module" {
modulePath = fields[1]
break
}
}
}
if strings.HasSuffix(rel, ".go") {
file, err := parser.ParseFile(fset, path, data, parser.SkipObjectResolution)
if err != nil {
return manifest{}, fmt.Errorf("parse %s: %w", rel, err)
}
for _, spec := range file.Imports {
value, err := strconv.Unquote(spec.Path.Value)
if err == nil && strings.HasPrefix(value, "github.com/minio/") {
sets["imports"][value] = struct{}{}
}
}
collectStringMatches(sets["routes"], routeRE, file)
collectNamedStringValues(sets["roots"], rel, file, "minioReservedBucket")
if rel == "internal/grid/manager.go" {
collectStringMatches(sets["grid"], routeRE, file)
}
if !strings.HasSuffix(rel, "_test.go") {
collectExported(sets["exported"], filepath.ToSlash(filepath.Dir(rel)), file)
if strings.HasPrefix(rel, "cmd/") || strings.HasPrefix(rel, "internal/") {
collectBrandStrings(sets["brand"], rel, file)
}
}
}
}
// This was a shell-local PID variable in the generated inspect script,
// never a supported environment setting.
delete(sets["env"], "MINIO_SRVR_PID")
if modulePath == "" {
return manifest{}, errors.New("go.mod module path was not found")
}
return manifest{
Version: manifestVersion,
ModulePath: modulePath,
MinioImports: sorted(sets["imports"]),
Environment: sorted(sets["env"]),
Metrics: sorted(sets["metrics"]),
Headers: sorted(sets["headers"]),
Routes: sorted(sets["routes"]),
RouteRoots: sorted(sets["roots"]),
GridRoutes: sorted(sets["grid"]),
StorageMarkers: sorted(sets["storage"]),
PolicyValues: sorted(sets["policy"]),
ExportedSymbols: sorted(sets["exported"]),
BrandAllowlist: sorted(sets["brand"]),
}, nil
}
func collectBrandStrings(dst map[string]struct{}, rel string, file *ast.File) {
ast.Inspect(file, func(node ast.Node) bool {
literal, ok := node.(*ast.BasicLit)
if !ok || literal.Kind != token.STRING {
return true
}
value, err := strconv.Unquote(literal.Value)
if err != nil || !brandRE.MatchString(value) || strings.HasPrefix(value, "github.com/minio/") {
return true
}
dst[filepath.ToSlash(rel)+"="+strconv.Quote(value)] = struct{}{}
return true
})
}
func collectNamedStringValues(dst map[string]struct{}, rel string, file *ast.File, names ...string) {
wanted := make(map[string]struct{}, len(names))
for _, name := range names {
wanted[name] = struct{}{}
}
for _, decl := range file.Decls {
gen, ok := decl.(*ast.GenDecl)
if !ok {
continue
}
for _, rawSpec := range gen.Specs {
spec, ok := rawSpec.(*ast.ValueSpec)
if !ok {
continue
}
for i, name := range spec.Names {
if _, ok := wanted[name.Name]; !ok || i >= len(spec.Values) {
continue
}
literal, ok := spec.Values[i].(*ast.BasicLit)
if !ok || literal.Kind != token.STRING {
continue
}
value, err := strconv.Unquote(literal.Value)
if err == nil {
dst[filepath.ToSlash(rel)+":"+name.Name+"="+value] = struct{}{}
}
}
}
}
}
func collectStringMatches(dst map[string]struct{}, re *regexp.Regexp, file *ast.File) {
ast.Inspect(file, func(node ast.Node) bool {
literal, ok := node.(*ast.BasicLit)
if !ok || literal.Kind != token.STRING {
return true
}
value, err := strconv.Unquote(literal.Value)
if err == nil {
addMatches(dst, re, value, false)
}
return true
})
}
func trackedFiles(repo string) ([]string, error) {
cmd := exec.Command("git", "-C", repo, "ls-files", "--cached", "--others", "--exclude-standard", "-z")
out, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("git ls-files: %w", err)
}
parts := bytes.Split(out, []byte{0})
files := make([]string, 0, len(parts))
for _, part := range parts {
if len(part) > 0 {
files = append(files, string(part))
}
}
return files, nil
}
func addMatches(dst map[string]struct{}, re *regexp.Regexp, text string, lower bool) {
for _, match := range re.FindAllString(text, -1) {
if lower {
match = strings.ToLower(match)
}
dst[match] = struct{}{}
}
}
func collectExported(dst map[string]struct{}, dir string, file *ast.File) {
prefix := dir + ":" + file.Name.Name + ":"
for _, decl := range file.Decls {
switch decl := decl.(type) {
case *ast.FuncDecl:
if !ast.IsExported(decl.Name.Name) {
continue
}
if decl.Recv == nil {
dst[prefix+"func:"+decl.Name.Name] = struct{}{}
continue
}
receiver := receiverName(decl.Recv.List[0].Type)
dst[prefix+"method:"+receiver+"."+decl.Name.Name] = struct{}{}
case *ast.GenDecl:
for _, spec := range decl.Specs {
switch spec := spec.(type) {
case *ast.TypeSpec:
if !ast.IsExported(spec.Name.Name) {
continue
}
dst[prefix+"type:"+spec.Name.Name] = struct{}{}
collectExportedFields(dst, prefix, spec.Name.Name, spec.Type)
case *ast.ValueSpec:
kind := strings.ToLower(decl.Tok.String())
for _, name := range spec.Names {
if ast.IsExported(name.Name) {
dst[prefix+kind+":"+name.Name] = struct{}{}
}
}
}
}
}
}
}
func collectExportedFields(dst map[string]struct{}, prefix, typeName string, expr ast.Expr) {
var fields *ast.FieldList
switch typed := expr.(type) {
case *ast.StructType:
fields = typed.Fields
case *ast.InterfaceType:
fields = typed.Methods
default:
return
}
for _, field := range fields.List {
for _, name := range field.Names {
if ast.IsExported(name.Name) {
dst[prefix+"field:"+typeName+"."+name.Name] = struct{}{}
}
}
}
}
func receiverName(expr ast.Expr) string {
switch expr := expr.(type) {
case *ast.Ident:
return expr.Name
case *ast.StarExpr:
return receiverName(expr.X)
case *ast.IndexExpr:
return receiverName(expr.X)
case *ast.IndexListExpr:
return receiverName(expr.X)
case *ast.SelectorExpr:
return receiverName(expr.X) + "." + expr.Sel.Name
default:
return fmt.Sprintf("%T", expr)
}
}
func sorted(set map[string]struct{}) []string {
values := make([]string, 0, len(set))
for value := range set {
values = append(values, value)
}
sort.Strings(values)
return values
}
func writeManifest(path string, value manifest) error {
data, err := json.MarshalIndent(value, "", " ")
if err != nil {
return err
}
data = append(data, '\n')
return os.WriteFile(path, data, 0o644)
}
func readManifest(path string) (manifest, error) {
data, err := os.ReadFile(path)
if err != nil {
return manifest{}, fmt.Errorf("read compatibility baseline (run go run ./buildscripts/rebrand-guard --write once): %w", err)
}
var value manifest
if err := json.Unmarshal(data, &value); err != nil {
return manifest{}, err
}
if value.Version != manifestVersion {
return manifest{}, fmt.Errorf("unsupported compatibility baseline version %d", value.Version)
}
return value, nil
}
func compare(want, got manifest) error {
var failures []string
if want.ModulePath != got.ModulePath {
failures = append(failures, fmt.Sprintf("module_path: want %q, got %q", want.ModulePath, got.ModulePath))
}
checks := []struct {
name string
want, got []string
}{
{"minio_imports", want.MinioImports, got.MinioImports},
{"environment", want.Environment, got.Environment},
{"metrics", want.Metrics, got.Metrics},
{"headers", want.Headers, got.Headers},
{"routes", want.Routes, got.Routes},
{"route_roots", want.RouteRoots, got.RouteRoots},
{"grid_routes", want.GridRoutes, got.GridRoutes},
{"storage_markers", want.StorageMarkers, got.StorageMarkers},
{"policy_values", want.PolicyValues, got.PolicyValues},
{"exported_symbols", want.ExportedSymbols, got.ExportedSymbols},
{"brand_allowlist", want.BrandAllowlist, got.BrandAllowlist},
}
for _, check := range checks {
if missing, added := setDiff(check.want, check.got); len(missing) > 0 || len(added) > 0 {
var b strings.Builder
fmt.Fprintf(&b, "%s compatibility set changed", check.name)
for _, value := range missing {
fmt.Fprintf(&b, "\n - %s", value)
}
for _, value := range added {
fmt.Fprintf(&b, "\n + %s", value)
}
failures = append(failures, b.String())
}
}
if len(failures) > 0 {
return errors.New(strings.Join(failures, "\n"))
}
return nil
}
func setDiff(want, got []string) (missing, added []string) {
wantSet := make(map[string]struct{}, len(want))
gotSet := make(map[string]struct{}, len(got))
for _, value := range want {
wantSet[value] = struct{}{}
}
for _, value := range got {
gotSet[value] = struct{}{}
}
for _, value := range want {
if _, ok := gotSet[value]; !ok {
missing = append(missing, value)
}
}
for _, value := range got {
if _, ok := wantSet[value]; !ok {
added = append(added, value)
}
}
return missing, added
}
func printSummary(value manifest) {
fmt.Printf("compatibility manifest: imports=%d env=%d metrics=%d headers=%d routes=%d roots=%d grid=%d storage=%d policy=%d exported=%d brand=%d sha256=%s\n",
len(value.MinioImports), len(value.Environment), len(value.Metrics), len(value.Headers),
len(value.Routes), len(value.RouteRoots), len(value.GridRoutes), len(value.StorageMarkers), len(value.PolicyValues),
len(value.ExportedSymbols), len(value.BrandAllowlist), manifestDigest(value))
}
func manifestDigest(value manifest) string {
data, _ := json.Marshal(value)
sum := sha256.Sum256(data)
return hex.EncodeToString(sum[:])
}
func gitOutput(args ...string) (string, error) {
out, err := exec.Command("git", args...).CombinedOutput()
if err != nil {
return "", fmt.Errorf("git %s: %s: %w", strings.Join(args, " "), strings.TrimSpace(string(out)), err)
}
return string(out), nil
}
func fatal(err error) {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
+152
View File
@@ -0,0 +1,152 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
repo_dir="$(cd "${script_dir}/.." && pwd)"
cd "${repo_dir}"
fail() {
echo "Silo rebrand verification failed: $*" >&2
exit 1
}
require_file() {
[ -f "$1" ] || fail "missing required file: $1"
}
require_text() {
local file="$1"
local text="$2"
grep -Fq -- "${text}" "${file}" || fail "${file} does not contain: ${text}"
}
reject_text() {
local file="$1"
local text="$2"
if grep -Fq -- "${text}" "${file}"; then
fail "${file} still contains forbidden delivery text: ${text}"
fi
}
for file in \
.github/goreleaser.yml \
.github/nfpm.yml \
buildscripts/package/lifecycle_test.sh \
buildscripts/verify-helm-migration.sh \
Dockerfile.goreleaser \
dockerscripts/download-static-curl.sh \
dockerscripts/docker-entrypoint.sh \
helm/silo/Chart.yaml \
helm/silo/values.yaml \
silo.service \
silo.env \
silo.sysusers; do
require_file "${file}"
done
for retired_path in \
CNAME _config.yml index.yaml helm-reindex.sh helm/minio helm-releases \
Dockerfile Dockerfile.cicd Dockerfile.hotfix Dockerfile.release \
Dockerfile.release.old_cpu Dockerfile.scratch docker-buildx.sh \
minio.service cmd/callhome.go buildscripts/upgrade-tests .github/logo.svg \
docs/federation/lookup/bucket-lookup.png \
docs/screenshots/Minio_Cloud_Native_Arch.jpg \
docs/screenshots/Minio_Cloud_Native_Arch.png \
docs/screenshots/Minio_Cloud_Native_Arch.svg \
docs/screenshots/Architecture-diagram_distributed_8.jpg \
docs/screenshots/Architecture-diagram_distributed_8.png \
docs/screenshots/Architecture-diagram_distributed_8.svg \
docs/screenshots/Architecture-diagram_distributed_16.jpg \
docs/screenshots/Architecture-diagram_distributed_16.png \
docs/screenshots/Architecture-diagram_distributed_16.svg \
docs/screenshots/Architecture-diagram_distributed_nm.png \
docs/screenshots/Example-1.jpg docs/screenshots/Example-1.png \
docs/screenshots/Example-2.jpg docs/screenshots/Example-2.png \
docs/screenshots/Example-3.jpg docs/screenshots/Example-3.png \
docs/screenshots/pic1.png docs/screenshots/pic2.png \
docs/metrics/prometheus/grafana/grafana-minio.png \
docs/metrics/prometheus/grafana/bucket/grafana-bucket.png \
docs/metrics/prometheus/grafana/node/grafana-node.png \
docs/metrics/prometheus/grafana/replication/grafana-replication-cluster.png \
docs/metrics/prometheus/grafana/replication/grafana-replication-node.png; do
[ ! -e "${retired_path}" ] || fail "retired upstream delivery path remains: ${retired_path}"
done
require_text .github/goreleaser.yml "binary: silo"
require_text .github/goreleaser.yml 'name_template: "silo_{{ .Env.PKG_VERSION }}_{{ .Os }}_{{ .Arch }}"'
require_text .github/goreleaser.yml "sboms:"
require_text .github/goreleaser.yml "artifacts: archive"
require_text .github/goreleaser.yml "cmd: cosign"
# shellcheck disable=SC2016 # Match the literal GoReleaser template variable.
require_text .github/goreleaser.yml 'signature: "${artifact}.sigstore.json"'
require_text .github/nfpm.yml "name: silo"
require_text .github/nfpm.yml "dst: /usr/bin/silo"
require_text .github/nfpm.yml "dst: /etc/default/silo"
require_text .github/nfpm.yml "dst: /usr/lib/sysusers.d/silo.conf"
require_text buildscripts/package/lifecycle_test.sh "Silo package lifecycle checks passed"
require_text buildscripts/verify-helm-migration.sh "Silo Helm lint, render, legacy-upgrade, and package checks passed"
require_text silo.service "Conflicts=minio.service"
require_text silo.service "EnvironmentFile=-/etc/default/minio"
require_text silo.service "EnvironmentFile=-/etc/default/silo"
# shellcheck disable=SC2016 # Match the literal service environment variables.
require_text silo.service 'ExecStart=/usr/bin/silo server $MINIO_OPTS $MINIO_VOLUMES'
require_text README.md "/etc/systemd/system/silo.service.d/10-legacy-user.conf"
require_text README_ZH.md "/etc/systemd/system/silo.service.d/10-legacy-user.conf"
require_text Dockerfile.goreleaser "COPY silo /usr/bin/silo"
require_text Dockerfile.goreleaser 'CMD ["silo"]'
require_text Dockerfile.goreleaser "MC_AMD64_SHA256="
require_text Dockerfile.goreleaser "Published checksum drift"
require_text dockerscripts/download-static-curl.sh "sha256sum -c"
require_text helm/silo/Chart.yaml "name: silo"
require_text helm/silo/values.yaml "repository: pgsty/silo"
require_text helm/silo/templates/deployment.yaml "/usr/bin/docker-entrypoint.sh silo server"
require_text helm/silo/templates/statefulset.yaml "/usr/bin/docker-entrypoint.sh silo server"
require_text docs/orchestration/docker-compose/docker-compose.yaml 'http://silo{1...4}/data{1...2}'
require_text docs/resiliency/docker-compose.yaml 'http://silo{1...4}/data{1...8}'
require_text docs/distributed/DECOMMISSION.md 'systemctl restart silo'
# shellcheck disable=SC2016 # Match the literal shell variable.
require_text docs/resiliency/resiliency-tests.sh 'docker exec resiliency-silo$NODE-1'
require_text .github/workflows/release.yml "Attest downloadable release artifacts"
require_text .github/workflows/release.yml "packages_checksums.txt"
require_text .github/workflows/docker-release.yml "Attest multi-architecture image provenance"
require_text .github/workflows/docker-release.yml "index.docker.io/pgsty/silo"
for file in .github/nfpm.yml Dockerfile.goreleaser silo.service; do
reject_text "${file}" "/usr/bin/minio"
reject_text "${file}" "/usr/local/bin/minio"
done
reject_text Dockerfile.goreleaser "MINIO_UPDATE_MINISIGN_PUBKEY"
reject_text buildscripts/minio-upgrade.sh "docker system prune"
reject_text buildscripts/minio-upgrade.sh "docker volume prune"
reject_text docs/orchestration/docker-compose/docker-compose.yaml 'http://minio{1...4}'
reject_text docs/resiliency/docker-compose.yaml 'http://minio{1...4}'
reject_text docs/distributed/DECOMMISSION.md 'systemctl restart minio'
reject_text docs/resiliency/resiliency-tests.sh 'resiliency-minio'
reject_text docs/resiliency/resiliency-tests.sh 'docker system prune'
reject_text docs/resiliency/resiliency-tests.sh 'docker image prune'
reject_text docs/resiliency/resiliency-tests.sh 'docker ps -q'
if grep -Ev '^[[:space:]]*(#|$)' silo.env | grep -q '='; then
fail "silo.env must not contain active assignments that shadow /etc/default/minio"
fi
if rg -n 'pgsty/minio:' .github/workflows Dockerfile.goreleaser helm/silo; then
fail "an active delivery surface still publishes the frozen pgsty/minio image"
fi
network_hits="$(rg -n --glob '*.go' --glob '!**/*_test.go' \
'https?://[^"`[:space:]]*(dl\.min\.io|subnet\.min\.io|api\.min\.io|slack\.min\.io|play\.min\.io)' \
cmd internal || true)"
network_hits="$(printf '%s\n' "${network_hits}" | grep -Ev '^[^:]+:[0-9]+:[[:space:]]*//' || true)"
if [ -n "${network_hits}" ]; then
printf '%s\n' "${network_hits}" >&2
fail "runtime code still contains an upstream MinIO service endpoint"
fi
require_text cmd/build-constants.go 'MinioReleaseBaseURL = ""'
require_text cmd/globals.go "globalInplaceUpdateDisabled = true"
reject_text cmd/globals.go "subnetAdminPublicKey"
reject_text cmd/admin-handlers.go "getSubnetAdminPublicKey"
echo "Silo delivery and runtime rebrand checks passed"