From d88f46ccee345a9c2fabe2d221d9a9e56bc11aec Mon Sep 17 00:00:00 2001 From: Feng Ruohang Date: Wed, 5 Aug 2026 06:29:22 +0800 Subject: [PATCH] ci: build the release runtime image and assert graceful shutdown The release image (Dockerfile.goreleaser) was only ever built by docker-release.yml, which is workflow_dispatch only - so the runtime layer and entrypoint were never exercised by CI until an actual publish, where a broken COPY path or a signal-handling regression would surface at the worst possible moment. MC already covers this in its test-release; the server did not. Add an offline, deterministic smoke: assemble a minimal image from the linux/amd64 binary goreleaser already built and the real entrypoint, then start the server and `docker stop` it on both the default and the MINIO_USERNAME drop-privilege paths, asserting PID 1 is minio, the exit is 0, the shutdown is sub-timeout, and the "Exiting on signal" log is present. The mcli-download build stage is skipped deliberately - it hits the GitHub API and would make this gate flaky and non-reproducible; its simple, checksum-guarded logic still runs at publish time. This is the regression guard for the exec-into-chroot entrypoint fix. Also add dockerscripts/docker-entrypoint.sh to the pull_request paths so an entrypoint change actually triggers this pipeline. Verified locally end to end: the fixed entrypoint passes both paths; reverting the exec makes the drop-privilege path time out to exit 137 and the step fails. Co-authored-by: Claude --- .github/workflows/test-release.yml | 53 ++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/.github/workflows/test-release.yml b/.github/workflows/test-release.yml index c48d1bdc5..b8e0d59ca 100644 --- a/.github/workflows/test-release.yml +++ b/.github/workflows/test-release.yml @@ -7,6 +7,7 @@ on: - ".github/goreleaser.yml" - ".github/nfpm.yml" - "Dockerfile.goreleaser" + - "dockerscripts/docker-entrypoint.sh" - "minio.service" - "buildscripts/package-release.sh" - "buildscripts/sign-release-rpms.sh" @@ -213,6 +214,58 @@ jobs: find . -maxdepth 1 -type f | sort + - name: Build release runtime image and verify graceful shutdown + run: | + set -euo pipefail + # docker-release.yml is workflow_dispatch only, so this is the only + # automated build of the release runtime layer and entrypoint before a + # real publish. Assemble a minimal image from the linux/amd64 binary + # goreleaser already produced; the mcli-download build stage is skipped + # on purpose to keep this gate offline and deterministic. + ctx="$(mktemp -d)" + tar -xzf "dist/minio_${PKG_VERSION}_linux_amd64.tar.gz" -C "${ctx}" minio + cp dockerscripts/docker-entrypoint.sh "${ctx}/docker-entrypoint.sh" + { + echo "FROM registry.access.redhat.com/ubi9/ubi-micro:latest" + echo "COPY minio /usr/bin/minio" + 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 'ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]' + echo 'CMD ["minio"]' + } > "${ctx}/Dockerfile" + docker build -t minio-runtime-test:snapshot "${ctx}" + + # PID 1 must be minio, not the entry shell, on every privilege path, so + # a SIGTERM from docker stop reaches the server and it exits gracefully + # instead of being killed at the stop timeout. Regression guard for the + # exec-into-chroot entrypoint fix. + assert_graceful() { + name="$1"; shift + docker rm -f "${name}" >/dev/null 2>&1 || true + docker run -d --name "${name}" \ + -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 + up="" + for _ in $(seq 1 60); do + if docker logs "${name}" 2>&1 | grep -q "API:"; then up=1; break; fi + if [ "$(docker inspect -f '{{.State.Running}}' "${name}")" != "true" ]; then break; fi + sleep 1 + done + if [ -z "${up}" ]; then echo "server did not start (${name}):"; docker logs "${name}" | tail -5; exit 1; fi + pid1="$(docker exec "${name}" cat /proc/1/comm 2>/dev/null || echo '?')" + start="$(date +%s)"; docker stop -t 15 "${name}" >/dev/null; end="$(date +%s)" + code="$(docker inspect -f '{{.State.ExitCode}}' "${name}")" + elapsed=$((end - start)) + echo "${name}: pid1=${pid1} stop=${elapsed}s exit=${code}" + graceful=0; docker logs "${name}" 2>&1 | grep -q "Exiting on signal" && graceful=1 + docker rm -f "${name}" >/dev/null 2>&1 || true + [ "${graceful}" = "1" ] || { echo "no graceful-shutdown log (${name}) - signal not forwarded"; 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; } + } + assert_graceful minio-rt-default + assert_graceful minio-rt-dropuser -e MINIO_USERNAME=minio-user -e MINIO_GROUPNAME=minio-group + - name: Validate release scripts run: | set -euo pipefail