From 6cf2244e23c0a48d0686d9be20487022b9bb0625 Mon Sep 17 00:00:00 2001 From: "synacktra.work@gmail.com" Date: Sat, 13 Dec 2025 08:28:44 +0530 Subject: [PATCH 1/3] fix(workflow): refactor reusable Docker publish workflow Refactored the reusable Docker publish workflow to generate proper multi-architecture images. Each platform build now pushes by digest and uploads its digest as an artifact, and a final job assembles the multi-arch manifest from those digests. --- .github/workflows/docker-reusable-publish.yml | 175 ++++++++++++------ 1 file changed, 117 insertions(+), 58 deletions(-) diff --git a/.github/workflows/docker-reusable-publish.yml b/.github/workflows/docker-reusable-publish.yml index 3472883f..c4dc5db1 100644 --- a/.github/workflows/docker-reusable-publish.yml +++ b/.github/workflows/docker-reusable-publish.yml @@ -39,20 +39,19 @@ jobs: - linux/amd64 - linux/arm64 steps: - - name: Checkout repository + - name: Checkout uses: actions/checkout@v4 - name: Prepare platform tag id: platform run: | - # Convert platform (e.g., linux/amd64) to a valid tag suffix (e.g., linux-amd64) - PLATFORM_TAG=$(echo "${{ matrix.platform }}" | sed 's/\//-/g') - echo "tag=${PLATFORM_TAG}" >> $GITHUB_OUTPUT + TAG=$(echo "${{ matrix.platform }}" | sed 's/\//-/g') + echo "tag=${TAG}" >> $GITHUB_OUTPUT - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Log in to Docker Hub + - name: Login to Docker Hub uses: docker/login-action@v3 with: username: ${{ inputs.docker_hub_org }} @@ -67,7 +66,22 @@ jobs: tags: | type=raw,value=${{ github.sha }} - - name: Extract metadata (main branch) + - name: Build & push digest (PR) + if: github.event_name == 'pull_request' + id: build-pr + uses: docker/build-push-action@v5 + with: + context: ./${{ inputs.context_dir }} + file: ./${{ inputs.context_dir }}/${{ inputs.dockerfile_path }} + push: true + platforms: ${{ matrix.platform }} + outputs: type=registry,name=${{ inputs.docker_hub_org }}/${{ inputs.image_name }},push-by-digest=true + labels: ${{ steps.meta-pr.outputs.labels }} + cache-from: | + type=registry,ref=${{ inputs.docker_hub_org }}/${{ inputs.image_name }}:buildcache-${{ steps.platform.outputs.tag }} + cache-to: type=registry,ref=${{ inputs.docker_hub_org }}/${{ inputs.image_name }}:buildcache-${{ steps.platform.outputs.tag }},mode=max + + - name: Extract metadata (main) if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' id: meta-main uses: docker/metadata-action@v5 @@ -76,7 +90,22 @@ jobs: tags: | type=raw,value=latest - - name: Extract metadata (semantic version tag) + - name: Build & push digest (main) + if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' + id: build-main + uses: docker/build-push-action@v5 + with: + context: ./${{ inputs.context_dir }} + file: ./${{ inputs.context_dir }}/${{ inputs.dockerfile_path }} + push: true + platforms: ${{ matrix.platform }} + outputs: type=registry,name=${{ inputs.docker_hub_org }}/${{ inputs.image_name }},push-by-digest=true + labels: ${{ steps.meta-main.outputs.labels }} + cache-from: | + type=registry,ref=${{ inputs.docker_hub_org }}/${{ inputs.image_name }}:buildcache-${{ steps.platform.outputs.tag }} + cache-to: type=registry,ref=${{ inputs.docker_hub_org }}/${{ inputs.image_name }}:buildcache-${{ steps.platform.outputs.tag }},mode=max + + - name: Extract metadata (semver) if: startsWith(github.ref, format('refs/tags/{0}', inputs.tag_prefix)) id: meta-semver uses: docker/metadata-action@v5 @@ -88,68 +117,98 @@ jobs: type=semver,pattern={{major}},prefix=${{ inputs.tag_prefix }} type=raw,value=latest - - name: Build and push Docker image (PR) - if: github.event_name == 'pull_request' - uses: docker/build-push-action@v5 - with: - context: ./${{ inputs.context_dir }} - file: ./${{ inputs.context_dir }}/${{ inputs.dockerfile_path }} - push: true - tags: ${{ steps.meta-pr.outputs.tags }} - labels: ${{ steps.meta-pr.outputs.labels }} - platforms: ${{ matrix.platform }} - cache-from: | - type=registry,ref=${{ inputs.docker_hub_org }}/${{ inputs.image_name }}:buildcache-${{ steps.platform.outputs.tag }} - type=registry,ref=${{ inputs.docker_hub_org }}/${{ inputs.image_name }}:latest - cache-to: type=registry,ref=${{ inputs.docker_hub_org }}/${{ inputs.image_name }}:buildcache-${{ steps.platform.outputs.tag }},mode=max - - - name: Build and push Docker image (main branch) - if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' - uses: docker/build-push-action@v5 - with: - context: ./${{ inputs.context_dir }} - file: ./${{ inputs.context_dir }}/${{ inputs.dockerfile_path }} - push: true - tags: ${{ steps.meta-main.outputs.tags }} - labels: ${{ steps.meta-main.outputs.labels }} - platforms: ${{ matrix.platform }} - cache-from: | - type=registry,ref=${{ inputs.docker_hub_org }}/${{ inputs.image_name }}:buildcache-${{ steps.platform.outputs.tag }} - type=registry,ref=${{ inputs.docker_hub_org }}/${{ inputs.image_name }}:latest - cache-to: type=registry,ref=${{ inputs.docker_hub_org }}/${{ inputs.image_name }}:buildcache-${{ steps.platform.outputs.tag }},mode=max - - - name: Build and push Docker image (semantic version tag) + - name: Build & push digest (semver) if: startsWith(github.ref, format('refs/tags/{0}', inputs.tag_prefix)) + id: build-semver uses: docker/build-push-action@v5 with: context: ./${{ inputs.context_dir }} file: ./${{ inputs.context_dir }}/${{ inputs.dockerfile_path }} push: true - tags: ${{ steps.meta-semver.outputs.tags }} - labels: ${{ steps.meta-semver.outputs.labels }} platforms: ${{ matrix.platform }} + outputs: type=registry,name=${{ inputs.docker_hub_org }}/${{ inputs.image_name }},push-by-digest=true + labels: ${{ steps.meta-semver.outputs.labels }} cache-from: | type=registry,ref=${{ inputs.docker_hub_org }}/${{ inputs.image_name }}:buildcache-${{ steps.platform.outputs.tag }} - type=registry,ref=${{ inputs.docker_hub_org }}/${{ inputs.image_name }}:latest cache-to: type=registry,ref=${{ inputs.docker_hub_org }}/${{ inputs.image_name }}:buildcache-${{ steps.platform.outputs.tag }},mode=max - - name: Image digest - if: github.event_name == 'pull_request' || github.ref == 'refs/heads/main' || startsWith(github.ref, format('refs/tags/{0}', inputs.tag_prefix)) + - name: Export digest + id: export-digest run: | - if [ "${{ github.event_name }}" == "pull_request" ]; then - echo "Image pushed with digest ${{ steps.meta-pr.outputs.digest }}" - elif [[ "${{ github.ref }}" == refs/tags/${{ inputs.tag_prefix }}* ]]; then - echo "Image pushed with digest ${{ steps.meta-semver.outputs.digest }}" - else - echo "Image pushed with digest ${{ steps.meta-main.outputs.digest }}" - fi + mkdir -p /tmp/digests + digest="${{ steps.build-pr.outputs.digest || steps.build-main.outputs.digest || steps.build-semver.outputs.digest }}" + echo "$digest" > "/tmp/digests/${{ steps.platform.outputs.tag }}.txt" - - name: print image tags + - name: Upload digest artifact (unique per platform) + uses: actions/upload-artifact@v4 + with: + name: digests-${{ steps.platform.outputs.tag }} + path: /tmp/digests/*.txt + retention-days: 1 + + publish-manifest-list: + runs-on: ubuntu-latest + needs: + - build-and-push + + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ inputs.docker_hub_org }} + password: ${{ secrets.DOCKER_HUB_TOKEN }} + + - name: Extract final metadata + id: metadata + uses: docker/metadata-action@v5 + with: + images: ${{ inputs.docker_hub_org }}/${{ inputs.image_name }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=sha + type=semver,pattern={{version}},prefix=${{ inputs.tag_prefix }} + type=semver,pattern={{major}}.{{minor}},prefix=${{ inputs.tag_prefix }} + type=semver,pattern={{major}},prefix=${{ inputs.tag_prefix }} + flavor: | + latest=true + + - name: Download all digest artifacts + uses: actions/download-artifact@v4 + with: + pattern: digests-* + path: /tmp/digests + merge-multiple: true + + - name: Create & push multi-arch manifest run: | - if [ "${{ github.event_name }}" == "pull_request" ]; then - echo "Image tags: ${{ steps.meta-pr.outputs.tags }}" - elif [[ "${{ github.ref }}" == refs/tags/${{ inputs.tag_prefix }}* ]]; then - echo "Image tags: ${{ steps.meta-semver.outputs.tags }}" - else - echo "Image tags: ${{ steps.meta-main.outputs.tags }}" - fi + IMAGE="${{ inputs.docker_hub_org }}/${{ inputs.image_name }}" + + DIGEST_ARGS="" + for f in $(find /tmp/digests -type f -name "*.txt"); do + d=$(cat "$f") + DIGEST_ARGS="$DIGEST_ARGS ${IMAGE}@${d}" + done + + echo "Using digests:" + echo "$DIGEST_ARGS" + + # Create manifest for each tag produced by metadata-action + echo "${DOCKER_METADATA_OUTPUT_JSON}" | jq -r '.tags[]' | while read TAG; do + echo "Creating manifest: $IMAGE:$TAG" + docker buildx imagetools create --tag "$IMAGE:$TAG" $DIGEST_ARGS + done + + - name: Inspect pushed manifests + run: | + IMAGE="${{ inputs.docker_hub_org }}/${{ inputs.image_name }}" + echo "Inspecting manifests:" + + echo "${DOCKER_METADATA_OUTPUT_JSON}" | jq -r '.tags[]' | while read TAG; do + echo "" + echo "Inspecting: $IMAGE:$TAG" + docker buildx imagetools inspect "$IMAGE:$TAG" + done From 0646634ba32a0999b55dc4117ab63472946f568e Mon Sep 17 00:00:00 2001 From: "synacktra.work@gmail.com" Date: Sat, 13 Dec 2025 08:44:21 +0530 Subject: [PATCH 2/3] fix(workflow): correct manifest tag handling in multi-arch publish step Updated the manifest creation and inspection steps to loop over metadata-action's `.tags[]` directly and use the full tag references as-is. The previous version attempted to prefix tags with the image name, which produced invalid references (e.g. image:image:tag). Using the full tags emitted by metadata-action ensures correct manifest creation for PR tags, SHA tags, semver tags, and latest. --- .github/workflows/docker-reusable-publish.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/docker-reusable-publish.yml b/.github/workflows/docker-reusable-publish.yml index c4dc5db1..68f74aed 100644 --- a/.github/workflows/docker-reusable-publish.yml +++ b/.github/workflows/docker-reusable-publish.yml @@ -197,9 +197,9 @@ jobs: echo "$DIGEST_ARGS" # Create manifest for each tag produced by metadata-action - echo "${DOCKER_METADATA_OUTPUT_JSON}" | jq -r '.tags[]' | while read TAG; do - echo "Creating manifest: $IMAGE:$TAG" - docker buildx imagetools create --tag "$IMAGE:$TAG" $DIGEST_ARGS + echo "${DOCKER_METADATA_OUTPUT_JSON}" | jq -r '.tags[]' | while read FULL_TAG; do + echo "Creating manifest: $FULL_TAG" + docker buildx imagetools create --tag "$FULL_TAG" $DIGEST_ARGS done - name: Inspect pushed manifests @@ -207,8 +207,8 @@ jobs: IMAGE="${{ inputs.docker_hub_org }}/${{ inputs.image_name }}" echo "Inspecting manifests:" - echo "${DOCKER_METADATA_OUTPUT_JSON}" | jq -r '.tags[]' | while read TAG; do + echo "${DOCKER_METADATA_OUTPUT_JSON}" | jq -r '.tags[]' | while read FULL_TAG; do echo "" - echo "Inspecting: $IMAGE:$TAG" - docker buildx imagetools inspect "$IMAGE:$TAG" + echo "Inspecting: $FULL_TAG" + docker buildx imagetools inspect "$FULL_TAG" done From 6a56f9c06325eef899f7063aac6b206790acb68a Mon Sep 17 00:00:00 2001 From: "synacktra.work@gmail.com" Date: Sat, 13 Dec 2025 09:10:02 +0530 Subject: [PATCH 3/3] fix(workflow): correct metadata extraction to prevent PRs from publishing unwanted tags Split the final metadata-action step into PR, main, and semver-specific blocks so each event only generates the appropriate tags. This prevents PR runs from pushing `latest` or semver tags, ensuring the publish job creates multi-arch manifests only for the tags intended for that event. --- .github/workflows/docker-reusable-publish.yml | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/.github/workflows/docker-reusable-publish.yml b/.github/workflows/docker-reusable-publish.yml index 68f74aed..e13585e1 100644 --- a/.github/workflows/docker-reusable-publish.yml +++ b/.github/workflows/docker-reusable-publish.yml @@ -161,20 +161,33 @@ jobs: username: ${{ inputs.docker_hub_org }} password: ${{ secrets.DOCKER_HUB_TOKEN }} - - name: Extract final metadata - id: metadata + - name: Extract final metadata (PR) + if: github.event_name == 'pull_request' uses: docker/metadata-action@v5 with: images: ${{ inputs.docker_hub_org }}/${{ inputs.image_name }} tags: | - type=ref,event=branch type=ref,event=pr type=sha + + - name: Extract final metadata (main) + if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' + uses: docker/metadata-action@v5 + with: + images: ${{ inputs.docker_hub_org }}/${{ inputs.image_name }} + tags: | + type=raw,value=latest + + - name: Extract final metadata (semver) + if: startsWith(github.ref, format('refs/tags/{0}', inputs.tag_prefix)) + uses: docker/metadata-action@v5 + with: + images: ${{ inputs.docker_hub_org }}/${{ inputs.image_name }} + tags: | type=semver,pattern={{version}},prefix=${{ inputs.tag_prefix }} type=semver,pattern={{major}}.{{minor}},prefix=${{ inputs.tag_prefix }} type=semver,pattern={{major}},prefix=${{ inputs.tag_prefix }} - flavor: | - latest=true + type=raw,value=latest - name: Download all digest artifacts uses: actions/download-artifact@v4