From 30f34701267aee67a9584f0d6a6ddb250b3f951b Mon Sep 17 00:00:00 2001 From: Alessio Attilio Date: Thu, 23 Jul 2026 12:08:33 +0200 Subject: [PATCH] move Docker image builds to CI for consistent manifest format across releases Disable SBOM and provenance in buildx, add manifest type verification, and add GitHub Actions workflow to build images deterministically in CI instead of locally on release persons' machines. Signed-off-by: Alessio Attilio --- .github/workflows/release-docker.yaml | 99 +++++++++++++++++++++++++++ scripts/build-docker.sh | 1 + scripts/release.sh | 43 +++++++----- scripts/test.sh | 1 - scripts/test_images.sh | 22 ++++++ 5 files changed, 149 insertions(+), 17 deletions(-) create mode 100644 .github/workflows/release-docker.yaml diff --git a/.github/workflows/release-docker.yaml b/.github/workflows/release-docker.yaml new file mode 100644 index 000000000000..634372151bcd --- /dev/null +++ b/.github/workflows/release-docker.yaml @@ -0,0 +1,99 @@ +--- +name: Build and push release Docker images + +on: + release: + types: [published] + +permissions: read-all + +env: + PLATFORMS: linux/amd64,linux/arm64,linux/ppc64le,linux/s390x + +jobs: + build-and-push: + name: Build and push Docker images + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Set up Go + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + with: + go-version-file: .go-version + + - name: Set up QEMU + uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 + with: + driver-opts: image=moby/buildkit:v0.31.2 + + - name: Login to GCR + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 + with: + registry: gcr.io + username: _json_key + password: ${{ secrets.GCR_SERVICE_ACCOUNT_KEY }} + + - name: Login to Quay + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 + with: + registry: quay.io + username: ${{ secrets.QUAY_USERNAME }} + password: ${{ secrets.QUAY_PASSWORD }} + + - name: Download release binaries + env: + GH_TOKEN: ${{ github.token }} + RELEASE: ${{ github.event.release.tag_name }} + run: | + mkdir -p release + for platform in $(echo "${PLATFORMS}" | tr ',' ' '); do + arch="${platform#linux/}" + tarball="etcd-${RELEASE}-linux-${arch}.tar.gz" + echo "Downloading ${tarball}..." + gh release download "${RELEASE}" --pattern "${tarball}" --dir release + tar -C release -zxf "release/${tarball}" + done + + - name: Build and push Docker images + run: | + docker buildx build \ + --build-arg="VERSION=${{ github.event.release.tag_name }}" \ + --build-arg="BUILDDIR=release" \ + --platform="${PLATFORMS}" \ + --sbom=false --provenance=false \ + --push \ + -t "gcr.io/etcd-development/etcd:${{ github.event.release.tag_name }}" \ + -t "quay.io/coreos/etcd:${{ github.event.release.tag_name }}" \ + . + + - name: Verify manifest type + run: | + IMAGE="gcr.io/etcd-development/etcd:${{ github.event.release.tag_name }}" + MEDIA_TYPE=$(docker manifest inspect "${IMAGE}" | grep -o '"mediaType"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"mediaType"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/') + echo "Manifest type: ${MEDIA_TYPE}" + case "${MEDIA_TYPE}" in + application/vnd.docker.distribution.manifest.list.v2+json|application/vnd.oci.image.index.v1+json) + echo "OK: valid multi-platform manifest" + ;; + *) + echo "::error::Invalid manifest type: ${MEDIA_TYPE}" + exit 1 + ;; + esac + + - name: Test Docker images + run: | + VERSION="${{ github.event.release.tag_name }}" + for registry in "gcr.io/etcd-development/etcd" "quay.io/coreos/etcd"; do + IMAGE="${registry}:${VERSION}" + echo "Testing ${IMAGE}..." + docker run --rm "${IMAGE}" etcd --version | grep -q "etcd Version: ${VERSION#v}" + docker run --rm "${IMAGE}" etcdctl version | grep -q "etcdctl version: ${VERSION#v}" + docker run --rm "${IMAGE}" etcdutl version | grep -q "etcdutl version: ${VERSION#v}" + echo "OK: ${IMAGE}" + done diff --git a/scripts/build-docker.sh b/scripts/build-docker.sh index db99670fbdf0..37dc0bfefede 100755 --- a/scripts/build-docker.sh +++ b/scripts/build-docker.sh @@ -70,6 +70,7 @@ else docker buildx build --build-arg="VERSION=${VERSION}" \ --build-arg="BUILDDIR=${BUILDDIR}" \ --platform="${PLATFORMS}" \ + --sbom=false --provenance=false \ --push \ "${tag_args[@]}" \ . diff --git a/scripts/release.sh b/scripts/release.sh index 0813b4e8d6c0..68f5fad2e0df 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -48,11 +48,14 @@ help() { echo "" echo "WARNING: This script does not send announcement emails. This step must be performed manually AFTER running this tool." echo "" + echo "NOTE: Docker images are built by GitHub Actions (release-docker.yaml) after the release" + echo " is published. Use --local-docker to build Docker images locally instead." + echo "" echo " args:" echo " version: version of etcd to release, e.g. 'v3.2.18'" echo " flags:" echo " --in-place: build binaries using current branch." - echo " --no-docker-push: skip docker image pushes." + echo " --local-docker: build and push Docker images locally (not recommended)." echo " --no-gh-release: skip creating the GitHub release using gh." echo " --no-upload: skip gs://etcd binary artifact uploads." echo "" @@ -246,7 +249,7 @@ main() { log_callout "Building release..." if [ "${DRY_RUN}" == "true" ] || [ "${NO_DOCKER_PUSH}" == 1 ]; then - log_callout "Skipping docker push. --no-docker-push flag is set." + log_callout "Skipping docker build. Docker images will be built by GitHub Actions after release." # Explicitly set NO_DOCKER_PUSH to 1, if DRY_RUN is true. NO_DOCKER_PUSH=1 else @@ -309,20 +312,24 @@ main() { ### Release validation mkdir -p downloads - # Check image versions - local images=("quay.io/coreos/etcd:${RELEASE_VERSION}" "gcr.io/etcd-development/etcd:${RELEASE_VERSION}") - if [ -n "${OCI_REGISTRY:-}" ]; then - images=("${OCI_REGISTRY}/${OCI_PATH:-etcd}:${RELEASE_VERSION}") - fi - - for IMAGE in "${images[@]}"; do - # shellcheck disable=SC2155 - local image_version=$(docker run --rm "${IMAGE}" etcd --version | grep "etcd Version" | awk -F: '{print $2}' | tr -d '[:space:]') - if [ "${image_version}" != "${VERSION}" ]; then - log_error "Check failed: etcd --version output for ${IMAGE} is incorrect: ${image_version}" - exit 1 + # Check image versions (skip if Docker images are built by CI) + if [ "${NO_DOCKER_PUSH}" == 0 ]; then + local images=("quay.io/coreos/etcd:${RELEASE_VERSION}" "gcr.io/etcd-development/etcd:${RELEASE_VERSION}") + if [ -n "${OCI_REGISTRY:-}" ]; then + images=("${OCI_REGISTRY}/${OCI_PATH:-etcd}:${RELEASE_VERSION}") fi - done + + for IMAGE in "${images[@]}"; do + # shellcheck disable=SC2155 + local image_version=$(docker run --rm "${IMAGE}" etcd --version | grep "etcd Version" | awk -F: '{print $2}' | tr -d '[:space:]') + if [ "${image_version}" != "${VERSION}" ]; then + log_error "Check failed: etcd --version output for ${IMAGE} is incorrect: ${image_version}" + exit 1 + fi + done + else + log_callout "Skipping Docker image validation. Images will be validated by GitHub Actions." + fi # Check gsutil binary versions # shellcheck disable=SC2155 @@ -411,7 +418,7 @@ main() { POSITIONAL=() NO_UPLOAD=0 -NO_DOCKER_PUSH=0 +NO_DOCKER_PUSH=1 IN_PLACE=0 NO_GH_RELEASE=0 @@ -430,6 +437,10 @@ while test $# -gt 0; do NO_UPLOAD=1 shift ;; + --local-docker) + NO_DOCKER_PUSH=0 + shift + ;; --no-docker-push) NO_DOCKER_PUSH=1 shift diff --git a/scripts/test.sh b/scripts/test.sh index 88c64acd3f09..6ae7ab7f113c 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -584,7 +584,6 @@ EOF fi DRY_RUN=true run "${ETCD_ROOT_DIR}/scripts/release.sh" --no-upload --no-docker-push --no-gh-release --in-place "${VERSION}" - VERSION="${VERSION}" run "${ETCD_ROOT_DIR}/scripts/test_images.sh" } function mod_tidy_pass { diff --git a/scripts/test_images.sh b/scripts/test_images.sh index 17b9865ac92a..475feeea1d03 100755 --- a/scripts/test_images.sh +++ b/scripts/test_images.sh @@ -92,6 +92,25 @@ function put_get_check { fi } +# Verify that the image has a consistent manifest type (multi-platform list/index). +function verify_manifest_type { + local image=$1 + local media_type + + media_type=$(docker manifest inspect "${image}" 2>/dev/null | grep -o '"mediaType"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"mediaType"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/') + + case "${media_type}" in + application/vnd.docker.distribution.manifest.list.v2+json|application/vnd.oci.image.index.v1+json) + log_success "Manifest type for ${image}: ${media_type}" + ;; + *) + log_error "Error: Invalid manifest type for ${image}: ${media_type}" + log_error "Expected application/vnd.docker.distribution.manifest.list.v2+json or application/vnd.oci.image.index.v1+json" + exit 1 + ;; + esac +} + function main { local version="$1" local repository=${REPOSITORY:-"gcr.io/etcd-development/etcd"} @@ -104,6 +123,9 @@ function main { exit 1 fi + log_callout "Verifying manifest type." + verify_manifest_type "${image}" + log_callout "Running version check." run_version_check "${image}" "${version}" "/usr/local/bin/etcd" "--version" run_version_check "${image}" "${version}" "/usr/local/bin/etcdctl" "version"