This is an automated email from the ASF dual-hosted git repository.
hgruszecki pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iggy.git
The following commit(s) were added to refs/heads/master by this push:
new dd7ae9bbc fix(ci): eliminate intermediate Docker tags using
push-by-digest (#2491)
dd7ae9bbc is described below
commit dd7ae9bbc8b96a80732e6aa43b984f6a6717e65e
Author: Hubert Gruszecki <[email protected]>
AuthorDate: Tue Dec 16 10:26:41 2025 +0100
fix(ci): eliminate intermediate Docker tags using push-by-digest (#2491)
Push images by digest instead of arch-specific tags, then create
multi-arch manifests. Removes edge-amd64/edge-arm64 tag pollution.
---
.github/actions/utils/docker-buildx/action.yml | 55 ++++++++---
.github/workflows/post-merge.yml | 122 ++++++++++++++++++++++++-
.github/workflows/publish.yml | 62 +++++++++++--
3 files changed, 214 insertions(+), 25 deletions(-)
diff --git a/.github/actions/utils/docker-buildx/action.yml
b/.github/actions/utils/docker-buildx/action.yml
index 0336fdf23..7f06d4826 100644
--- a/.github/actions/utils/docker-buildx/action.yml
+++ b/.github/actions/utils/docker-buildx/action.yml
@@ -47,6 +47,14 @@ inputs:
required: false
default: ""
+outputs:
+ digest:
+ description: "Image digest (sha256:...)"
+ value: ${{ steps.build.outputs.digest }}
+ image:
+ description: "Registry image name"
+ value: ${{ steps.config.outputs.image }}
+
runs:
using: "composite"
steps:
@@ -158,10 +166,10 @@ runs:
uses: docker/metadata-action@v5
with:
images: ${{ steps.config.outputs.image }}
+ # Tags are only used for local builds (dry-run). Push mode always uses
digest.
tags: |
- type=raw,value=${{ inputs.version }}${{ steps.suffix.outputs.suffix
}}
- type=raw,value=latest,enable=${{ steps.suffix.outputs.is_single ==
'false' && steps.config.outputs.should_push == 'true' && inputs.version !=
'test' && !contains(inputs.version, 'edge') && !contains(inputs.version, 'rc')
&& !contains(inputs.version, 'alpha') && !contains(inputs.version, 'beta') }}
- type=sha,enable=${{ inputs.version == 'test' }}
+ type=raw,value=${{ inputs.version }}${{ steps.suffix.outputs.suffix
}},enable=${{ steps.config.outputs.should_push != 'true' }}
+ type=sha,enable=${{ steps.config.outputs.should_push != 'true' &&
inputs.version == 'test' }}
- name: Determine platforms
id: platforms
@@ -322,20 +330,43 @@ runs:
echo "context=$context" >> "$GITHUB_OUTPUT"
echo "📁 Build context: $context"
- - name: Build and push
- id: build
+ - name: Build and push (by digest)
+ id: build-push
+ if: steps.config.outputs.should_push == 'true'
+ uses: docker/build-push-action@v6
+ with:
+ context: ${{ steps.ctx.outputs.context }}
+ file: ${{ steps.config.outputs.dockerfile }}
+ platforms: ${{ steps.platforms.outputs.platforms }}
+ labels: ${{ steps.meta.outputs.labels }}
+ cache-from: ${{ env.CACHE_FROM }}
+ cache-to: ${{ env.CACHE_TO }}
+ build-args: |
+ ${{ steps.bargs.outputs.all }}
+ outputs: type=image,name=${{ steps.config.outputs.image
}},push-by-digest=true,name-canonical=true,push=true
+
+ - name: Build only (dry-run)
+ id: build-only
+ if: steps.config.outputs.should_push != 'true'
uses: docker/build-push-action@v6
with:
context: ${{ steps.ctx.outputs.context }}
file: ${{ steps.config.outputs.dockerfile }}
platforms: ${{ steps.platforms.outputs.platforms }}
- push: ${{ steps.config.outputs.should_push }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: ${{ env.CACHE_FROM }}
cache-to: ${{ env.CACHE_TO }}
build-args: |
${{ steps.bargs.outputs.all }}
+ push: false
+
+ - name: Consolidate build output
+ id: build
+ shell: bash
+ run: |
+ # Output digest from push step (dry-run builds don't produce useful
digests)
+ echo "digest=${{ steps.build-push.outputs.digest }}" >>
"$GITHUB_OUTPUT"
- name: Export image (if not pushing)
if: steps.config.outputs.should_push == 'false' && inputs.task !=
'publish'
@@ -358,12 +389,14 @@ runs:
echo "| Dockerfile | \`${{ steps.config.outputs.dockerfile }}\` |"
echo "| Platforms | \`${{ steps.platforms.outputs.platforms }}\` |"
echo "| Pushed | ${{ steps.config.outputs.should_push }} |"
- if [ "${{ steps.config.outputs.should_push }}" = "true" ]; then
+ if [ -n "${{ steps.build.outputs.digest }}" ]; then
echo "| Digest | \`${{ steps.build.outputs.digest }}\` |"
- echo ""
- echo "### Pull"
- echo '```bash'
- echo "docker pull ${{ steps.config.outputs.image }}:${{
inputs.version }}"
+ fi
+ echo ""
+ if [ "${{ steps.config.outputs.should_push }}" = "true" ]; then
+ echo "### Digest (for manifest creation)"
+ echo '```'
+ echo "${{ steps.config.outputs.image }}@${{
steps.build.outputs.digest }}"
echo '```'
fi
} >> "$GITHUB_STEP_SUMMARY"
diff --git a/.github/workflows/post-merge.yml b/.github/workflows/post-merge.yml
index f1d339e34..d54380f35 100644
--- a/.github/workflows/post-merge.yml
+++ b/.github/workflows/post-merge.yml
@@ -122,6 +122,7 @@ jobs:
fi
- uses: ./.github/actions/utils/docker-buildx
+ id: docker
with:
task: publish
libc: ${{ steps.libc.outputs.libc }}
@@ -130,6 +131,29 @@ jobs:
platform: ${{ matrix.platform }}
dry_run: ${{ github.event.repository.fork }}
+ - name: Export digest
+ if: ${{ !github.event.repository.fork }}
+ shell: bash
+ run: |
+ mkdir -p ${{ runner.temp }}/digests
+ digest="${{ steps.docker.outputs.digest }}"
+ if [ -n "$digest" ]; then
+ touch "${{ runner.temp }}/digests/${digest#sha256:}"
+ echo "Exported digest: $digest"
+ else
+ echo "::error::No digest available"
+ exit 1
+ fi
+
+ - name: Upload digest
+ if: ${{ !github.event.repository.fork }}
+ uses: actions/upload-artifact@v4
+ with:
+ name: docker-digest-${{ matrix.component }}-${{ matrix.arch }}
+ path: ${{ runner.temp }}/digests/*
+ if-no-files-found: error
+ retention-days: 1
+
docker-manifests:
name: Create manifests
needs: [plan, docker-edge]
@@ -159,6 +183,21 @@ jobs:
echo "image=$image" >> "$GITHUB_OUTPUT"
echo "📦 Image: $image"
+ - name: Download amd64 digest
+ uses: actions/download-artifact@v4
+ with:
+ name: docker-digest-${{ matrix.component }}-amd64
+ path: ${{ runner.temp }}/digests
+
+ - name: Download arm64 digest
+ uses: actions/download-artifact@v4
+ with:
+ name: docker-digest-${{ matrix.component }}-arm64
+ path: ${{ runner.temp }}/digests
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
@@ -166,19 +205,92 @@ jobs:
password: ${{ env.DOCKERHUB_TOKEN }}
- name: Create and push manifest
+ working-directory: ${{ runner.temp }}/digests
run: |
IMAGE="${{ steps.config.outputs.image }}"
VERSION="edge"
- echo "Creating manifest for $IMAGE:$VERSION"
+ echo "Creating manifest for $IMAGE:$VERSION from digests:"
+ ls -la
- docker manifest create "${IMAGE}:${VERSION}" \
- "${IMAGE}:${VERSION}-amd64" \
- "${IMAGE}:${VERSION}-arm64"
+ docker buildx imagetools create \
+ -t "${IMAGE}:${VERSION}" \
+ $(printf "${IMAGE}@sha256:%s " *)
- docker manifest push "${IMAGE}:${VERSION}"
echo "✅ Pushed manifest: ${IMAGE}:${VERSION}"
+ - name: Inspect manifest
+ run: |
+ docker buildx imagetools inspect "${{ steps.config.outputs.image
}}:edge"
+
+ # TEMPORARY: Clean up legacy architecture-specific tags from DockerHub
+ # This job can be removed after the first successful run cleans up the old
tags
+ cleanup-legacy-tags:
+ name: Cleanup legacy tags
+ needs: [plan, docker-manifests]
+ if: ${{ !github.event.repository.fork &&
fromJson(needs.plan.outputs.components).include[0].component != 'noop' }}
+ runs-on: ubuntu-latest
+ env:
+ DOCKERHUB_USER: ${{ secrets.DOCKERHUB_USER }}
+ DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
+ steps:
+ - name: Delete legacy edge-amd64 and edge-arm64 tags
+ run: |
+ set -euo pipefail
+
+ # Get Docker Hub JWT token
+ echo "🔐 Authenticating with Docker Hub..."
+ TOKEN=$(curl -s -X POST \
+ -H "Content-Type: application/json" \
+ -d "{\"username\": \"${DOCKERHUB_USER}\", \"password\":
\"${DOCKERHUB_TOKEN}\"}" \
+ https://hub.docker.com/v2/users/login/ | jq -r .token)
+
+ if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then
+ echo "::error::Failed to authenticate with Docker Hub"
+ exit 1
+ fi
+
+ # Images to clean up (from .github/config/publish.yml)
+ IMAGES=(
+ "apache/iggy"
+ "apache/iggy-connect"
+ "apache/iggy-mcp"
+ "apache/iggy-web-ui"
+ "apache/iggy-bench-dashboard"
+ )
+
+ # Tags to delete
+ TAGS=("edge-amd64" "edge-arm64")
+
+ for IMAGE in "${IMAGES[@]}"; do
+ # Extract namespace and repo from image name
+ NAMESPACE="${IMAGE%%/*}"
+ REPO="${IMAGE#*/}"
+
+ for TAG in "${TAGS[@]}"; do
+ echo "🗑️ Deleting ${IMAGE}:${TAG}..."
+
+ HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \
+ -H "Authorization: JWT ${TOKEN}" \
+
"https://hub.docker.com/v2/repositories/${NAMESPACE}/${REPO}/tags/${TAG}/")
+
+ case $HTTP_CODE in
+ 200|204)
+ echo " ✅ Deleted ${IMAGE}:${TAG}"
+ ;;
+ 404)
+ echo " ⏭️ Tag ${IMAGE}:${TAG} not found (already deleted
or never existed)"
+ ;;
+ *)
+ echo " ⚠️ Failed to delete ${IMAGE}:${TAG} (HTTP
${HTTP_CODE})"
+ ;;
+ esac
+ done
+ done
+
+ echo ""
+ echo "🎉 Legacy tag cleanup complete!"
+
build-artifacts:
name: Build artifacts
uses: ./.github/workflows/_build_rust_artifacts.yml
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 4aaa794ef..e5c02f49b 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -702,6 +702,7 @@ jobs:
fi
- name: Publish Docker image
+ id: docker
uses: ./.github/actions/utils/docker-buildx
with:
task: publish
@@ -711,6 +712,29 @@ jobs:
platform: ${{ matrix.platform }}
dry_run: ${{ inputs.dry_run }}
+ - name: Export digest
+ if: ${{ !inputs.dry_run }}
+ shell: bash
+ run: |
+ mkdir -p ${{ runner.temp }}/digests
+ digest="${{ steps.docker.outputs.digest }}"
+ if [ -n "$digest" ]; then
+ touch "${{ runner.temp }}/digests/${digest#sha256:}"
+ echo "Exported digest: $digest"
+ else
+ echo "::error::No digest available"
+ exit 1
+ fi
+
+ - name: Upload digest
+ if: ${{ !inputs.dry_run }}
+ uses: actions/upload-artifact@v4
+ with:
+ name: docker-digest-${{ matrix.key }}-${{ matrix.arch }}
+ path: ${{ runner.temp }}/digests/*
+ if-no-files-found: error
+ retention-days: 1
+
# Create multi-arch Docker manifests after platform-specific images are
pushed
docker-manifests:
name: Docker manifests
@@ -746,6 +770,7 @@ jobs:
- name: Resolve image from config
id: config
+ shell: bash
run: |
if ! command -v yq >/dev/null 2>&1; then
YQ_VERSION="v4.47.1"
@@ -758,6 +783,21 @@ jobs:
echo "image=$image" >> "$GITHUB_OUTPUT"
echo "📦 Image: $image"
+ - name: Download amd64 digest
+ uses: actions/download-artifact@v4
+ with:
+ name: docker-digest-${{ matrix.key }}-amd64
+ path: ${{ runner.temp }}/digests
+
+ - name: Download arm64 digest
+ uses: actions/download-artifact@v4
+ with:
+ name: docker-digest-${{ matrix.key }}-arm64
+ path: ${{ runner.temp }}/digests
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
@@ -765,29 +805,33 @@ jobs:
password: ${{ env.DOCKERHUB_TOKEN }}
- name: Create and push manifest
+ working-directory: ${{ runner.temp }}/digests
run: |
IMAGE="${{ steps.config.outputs.image }}"
VERSION="${{ steps.ver.outputs.version }}"
- echo "Creating manifest for $IMAGE:$VERSION"
+ echo "Creating manifest for $IMAGE:$VERSION from digests:"
+ ls -la
- docker manifest create "${IMAGE}:${VERSION}" \
- "${IMAGE}:${VERSION}-amd64" \
- "${IMAGE}:${VERSION}-arm64"
+ docker buildx imagetools create \
+ -t "${IMAGE}:${VERSION}" \
+ $(printf "${IMAGE}@sha256:%s " *)
- docker manifest push "${IMAGE}:${VERSION}"
echo "✅ Pushed manifest: ${IMAGE}:${VERSION}"
# Also create 'latest' tag for stable releases (not
edge/rc/alpha/beta)
if [[ ! "$VERSION" =~ (edge|rc|alpha|beta) ]]; then
echo "Creating 'latest' manifest"
- docker manifest create "${IMAGE}:latest" \
- "${IMAGE}:${VERSION}-amd64" \
- "${IMAGE}:${VERSION}-arm64"
- docker manifest push "${IMAGE}:latest"
+ docker buildx imagetools create \
+ -t "${IMAGE}:latest" \
+ $(printf "${IMAGE}@sha256:%s " *)
echo "✅ Pushed manifest: ${IMAGE}:latest"
fi
+ - name: Inspect manifest
+ run: |
+ docker buildx imagetools inspect "${{ steps.config.outputs.image
}}:${{ steps.ver.outputs.version }}"
+
# Non-Docker, non-Rust publishing (Python, Node, Java, C#, Go SDKs)
# Note: This job runs in parallel with Docker publishing - no dependency
between them
publish: