diff --git a/.github/workflows/release-docker.yaml b/.github/workflows/release-docker.yaml new file mode 100644 index 00000000000..634372151bc --- /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 db99670fbdf..37dc0bfefed 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 0813b4e8d6c..68f5fad2e0d 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 88c64acd3f0..6ae7ab7f113 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 17b9865ac92..475feeea1d0 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"