From ea2b1d53c32efa0d9b227f38aa1d0b015e20860b Mon Sep 17 00:00:00 2001 From: Luca Bandini Date: Tue, 26 May 2026 10:44:40 +0200 Subject: [PATCH 1/2] ci: also publish release images to Docker Hub MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend the release-image workflow to push the same per-arch image and the same tag set to docker.io/vechain/block-explorer alongside ghcr.io. PR images are intentionally not duplicated; they stay on GHCR only. The push happens in a single buildx invocation via multi-line `outputs:`, so each arch gets one digest that is pushed to both registries. The merge job then runs `docker buildx imagetools create` twice (once per registry) reusing the same digest sources and the same version-tag derivation logic via a small shell helper. Docker Hub does not support OIDC federation, so authentication is a long-lived token. The workflow expects: - `vars.DOCKERHUB_USERNAME` — Docker Hub org or service account name - `secrets.DOCKERHUB_TOKEN` — Organization Access Token scoped to the `block-explorer` repository Both have to be configured on the repo (Settings → Actions → Variables / Secrets) before the workflow can publish to Docker Hub. Workflow filename stays `publish-ghcr-image.yml` so branch protection required-status references keep resolving; only the `name:` header is updated to "Build & Publish Release Image". --- .github/workflows/publish-ghcr-image.yml | 75 ++++++++++++++++++------ 1 file changed, 56 insertions(+), 19 deletions(-) diff --git a/.github/workflows/publish-ghcr-image.yml b/.github/workflows/publish-ghcr-image.yml index 804740af..2dc567b7 100644 --- a/.github/workflows/publish-ghcr-image.yml +++ b/.github/workflows/publish-ghcr-image.yml @@ -1,4 +1,4 @@ -name: Build & Publish GHCR Image +name: Build & Publish Release Image on: push: @@ -17,7 +17,12 @@ concurrency: env: versionTag: ${{ inputs.version || github.ref_name }} - registry: ghcr.io + ghcrRegistry: ghcr.io + dockerHubRegistry: docker.io + # GHCR follows the GitHub repo namespace (case-sensitive), Docker Hub uses + # the lowercase `vechain/block-explorer` repo created on the org account. + imageName: ${{ github.repository }} + dockerHubImage: vechain/block-explorer jobs: build: @@ -50,10 +55,17 @@ jobs: - name: Login to GHCR uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 with: - registry: ${{ env.registry }} + registry: ${{ env.ghcrRegistry }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Login to Docker Hub + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 + with: + registry: ${{ env.dockerHubRegistry }} + username: ${{ vars.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build and push by digest id: build uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 @@ -67,7 +79,9 @@ jobs: sbom: false cache-from: type=gha,scope=buildkit-${{ matrix.arch }} cache-to: type=gha,mode=max,scope=buildkit-${{ matrix.arch }} - outputs: type=image,name=${{ env.registry }}/${{ github.repository }},push-by-digest=true,name-canonical=true,push=true + outputs: | + type=image,name=${{ env.ghcrRegistry }}/${{ env.imageName }},push-by-digest=true,name-canonical=true,push=true + type=image,name=${{ env.dockerHubRegistry }}/${{ env.dockerHubImage }},push-by-digest=true,name-canonical=true,push=true - name: Export digest run: | @@ -104,10 +118,17 @@ jobs: - name: Login to GHCR uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 with: - registry: ${{ env.registry }} + registry: ${{ env.ghcrRegistry }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Login to Docker Hub + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 + with: + registry: ${{ env.dockerHubRegistry }} + username: ${{ vars.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Extract version tags id: version run: | @@ -124,24 +145,40 @@ jobs: echo "stable=false" >> "$GITHUB_OUTPUT" fi - - name: Create manifest list and push + - name: Create manifest lists and push working-directory: /tmp/digests env: - IMAGE: ${{ env.registry }}/${{ github.repository }} + GHCR_IMAGE: ${{ env.ghcrRegistry }}/${{ env.imageName }} + DOCKERHUB_IMAGE: ${{ env.dockerHubRegistry }}/${{ env.dockerHubImage }} + FULL_VERSION: ${{ steps.version.outputs.full }} + STABLE: ${{ steps.version.outputs.stable }} + MAJOR: ${{ steps.version.outputs.major }} + MINOR: ${{ steps.version.outputs.minor }} run: | - TAGS="-t ${{ env.IMAGE }}:${{ steps.version.outputs.full }}" - - if [ "${{ steps.version.outputs.stable }}" = "true" ]; then - TAGS="$TAGS -t ${{ env.IMAGE }}:${{ steps.version.outputs.minor }}" - TAGS="$TAGS -t ${{ env.IMAGE }}:${{ steps.version.outputs.major }}" - TAGS="$TAGS -t ${{ env.IMAGE }}:latest" - fi + set -euo pipefail - IMAGE_REFS=() + # Collect per-arch digest sources once and reuse for both registries + sources=() for digest in *; do - IMAGE_REFS+=("${IMAGE}@sha256:${digest}") + sources+=("@sha256:${digest}") done - docker buildx imagetools create \ - $TAGS \ - "${IMAGE_REFS[@]}" + push_manifest() { + local image="$1" + local tags=("-t" "${image}:${FULL_VERSION}") + if [ "${STABLE}" = "true" ]; then + tags+=("-t" "${image}:${MINOR}") + tags+=("-t" "${image}:${MAJOR}") + tags+=("-t" "${image}:latest") + fi + + local refs=() + for s in "${sources[@]}"; do + refs+=("${image}${s}") + done + + docker buildx imagetools create "${tags[@]}" "${refs[@]}" + } + + push_manifest "$GHCR_IMAGE" + push_manifest "$DOCKERHUB_IMAGE" From 06b0009fd8a605e6cabdd77c0623087623111917 Mon Sep 17 00:00:00 2001 From: Luca Bandini Date: Mon, 1 Jun 2026 15:25:03 +0200 Subject: [PATCH 2/2] ci: mirror release image to Docker Hub via merge job Build job now pushes per-arch digests to GHCR only. The merge job creates the GHCR manifest list and mirrors each tag from GHCR to Docker Hub with `docker buildx imagetools create`. --- .github/workflows/publish-ghcr-image.yml | 67 ++++++++++++------------ 1 file changed, 33 insertions(+), 34 deletions(-) diff --git a/.github/workflows/publish-ghcr-image.yml b/.github/workflows/publish-ghcr-image.yml index 2dc567b7..da8de388 100644 --- a/.github/workflows/publish-ghcr-image.yml +++ b/.github/workflows/publish-ghcr-image.yml @@ -19,8 +19,6 @@ env: versionTag: ${{ inputs.version || github.ref_name }} ghcrRegistry: ghcr.io dockerHubRegistry: docker.io - # GHCR follows the GitHub repo namespace (case-sensitive), Docker Hub uses - # the lowercase `vechain/block-explorer` repo created on the org account. imageName: ${{ github.repository }} dockerHubImage: vechain/block-explorer @@ -59,13 +57,6 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Login to Docker Hub - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 - with: - registry: ${{ env.dockerHubRegistry }} - username: ${{ vars.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Build and push by digest id: build uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 @@ -79,9 +70,7 @@ jobs: sbom: false cache-from: type=gha,scope=buildkit-${{ matrix.arch }} cache-to: type=gha,mode=max,scope=buildkit-${{ matrix.arch }} - outputs: | - type=image,name=${{ env.ghcrRegistry }}/${{ env.imageName }},push-by-digest=true,name-canonical=true,push=true - type=image,name=${{ env.dockerHubRegistry }}/${{ env.dockerHubImage }},push-by-digest=true,name-canonical=true,push=true + outputs: type=image,name=${{ env.ghcrRegistry }}/${{ env.imageName }},push-by-digest=true,name-canonical=true,push=true - name: Export digest run: | @@ -145,11 +134,10 @@ jobs: echo "stable=false" >> "$GITHUB_OUTPUT" fi - - name: Create manifest lists and push + - name: Create GHCR manifest list working-directory: /tmp/digests env: GHCR_IMAGE: ${{ env.ghcrRegistry }}/${{ env.imageName }} - DOCKERHUB_IMAGE: ${{ env.dockerHubRegistry }}/${{ env.dockerHubImage }} FULL_VERSION: ${{ steps.version.outputs.full }} STABLE: ${{ steps.version.outputs.stable }} MAJOR: ${{ steps.version.outputs.major }} @@ -157,28 +145,39 @@ jobs: run: | set -euo pipefail - # Collect per-arch digest sources once and reuse for both registries - sources=() + tags=("-t" "${GHCR_IMAGE}:${FULL_VERSION}") + if [ "${STABLE}" = "true" ]; then + tags+=("-t" "${GHCR_IMAGE}:${MINOR}") + tags+=("-t" "${GHCR_IMAGE}:${MAJOR}") + tags+=("-t" "${GHCR_IMAGE}:latest") + fi + + refs=() for digest in *; do - sources+=("@sha256:${digest}") + refs+=("${GHCR_IMAGE}@sha256:${digest}") done - push_manifest() { - local image="$1" - local tags=("-t" "${image}:${FULL_VERSION}") - if [ "${STABLE}" = "true" ]; then - tags+=("-t" "${image}:${MINOR}") - tags+=("-t" "${image}:${MAJOR}") - tags+=("-t" "${image}:latest") - fi - - local refs=() - for s in "${sources[@]}"; do - refs+=("${image}${s}") - done - - docker buildx imagetools create "${tags[@]}" "${refs[@]}" + docker buildx imagetools create "${tags[@]}" "${refs[@]}" + + - name: Mirror manifest from GHCR to Docker Hub + env: + GHCR_IMAGE: ${{ env.ghcrRegistry }}/${{ env.imageName }} + DOCKERHUB_IMAGE: ${{ env.dockerHubRegistry }}/${{ env.dockerHubImage }} + FULL_VERSION: ${{ steps.version.outputs.full }} + STABLE: ${{ steps.version.outputs.stable }} + MAJOR: ${{ steps.version.outputs.major }} + MINOR: ${{ steps.version.outputs.minor }} + run: | + set -euo pipefail + + copy_tag() { + local tag="$1" + docker buildx imagetools create -t "${DOCKERHUB_IMAGE}:${tag}" "${GHCR_IMAGE}:${tag}" } - push_manifest "$GHCR_IMAGE" - push_manifest "$DOCKERHUB_IMAGE" + copy_tag "${FULL_VERSION}" + if [ "${STABLE}" = "true" ]; then + copy_tag "${MINOR}" + copy_tag "${MAJOR}" + copy_tag "latest" + fi