Skip to content
This repository was archived by the owner on Nov 14, 2025. It is now read-only.

Commit 185a1d5

Browse files
sapientpantsclaude
andauthored
refactor: use GHCR as intermediate storage for Docker Hub publishing (#319)
Previously attempted to publish multi-platform Docker images by extracting OCI tar archives, but docker buildx imagetools cannot work with oci-layout:// scheme or tar archives directly. This change refactors the Docker publishing workflow to: - Push multi-platform images to GHCR during Main workflow build - Use docker buildx imagetools create for registry-to-registry copy to Docker Hub - Maintain native Docker tooling without third-party dependencies - Preserve multi-platform manifest lists correctly Fixes: #<will be determined by PR> 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
1 parent 06bdf4a commit 185a1d5

File tree

4 files changed

+120
-95
lines changed

4 files changed

+120
-95
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'sonarqube-mcp-server': patch
3+
---
4+
5+
Refactor Docker Hub publishing to use GHCR as intermediate storage
6+
7+
Previously attempted to publish multi-platform Docker images by extracting OCI tar archives and using `docker buildx imagetools create` with `oci-layout://` scheme, which is not supported.
8+
9+
Now multi-platform images are pushed to GitHub Container Registry (GHCR) during the build phase, then copied to Docker Hub using `docker buildx imagetools create` for registry-to-registry transfer. This approach:
10+
11+
- Uses Docker's native buildx imagetools tooling (no third-party dependencies)
12+
- Preserves multi-platform manifest lists correctly
13+
- Maintains "build once, publish everywhere" model
14+
- Leverages GHCR's free hosting for public repositories
15+
- Simplifies the publish workflow by eliminating artifact extraction logic
16+
17+
Changes:
18+
19+
- Modified `.github/workflows/reusable-docker.yml` to push multi-platform builds to GHCR
20+
- Updated `.github/workflows/main.yml` with `packages: write` permission for GHCR
21+
- Refactored `.github/workflows/publish.yml` to copy images from GHCR to Docker Hub

.github/workflows/main.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,14 @@ concurrency:
2323
# attestations: write - Attach attestations to artifacts
2424
# security-events: write - Upload security scan results
2525
# actions: read - Access workflow runs and artifacts
26+
# packages: write - Push Docker images to GitHub Container Registry
2627
permissions:
2728
contents: write
2829
id-token: write
2930
attestations: write
3031
security-events: write
3132
actions: read
33+
packages: write
3234

3335
jobs:
3436
# =============================================================================

.github/workflows/publish.yml

Lines changed: 23 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ jobs:
258258

259259
# =============================================================================
260260
# DOCKER HUB PUBLISHING
261-
# Uses pre-built and pre-scanned Docker image from main workflow
261+
# Copies pre-built multi-platform image from GHCR to Docker Hub
262262
# =============================================================================
263263

264264
docker:
@@ -269,8 +269,7 @@ jobs:
269269
if: vars.ENABLE_DOCKER_RELEASE == 'true'
270270
permissions:
271271
contents: read
272-
packages: write # Push to GitHub Container Registry if needed
273-
actions: read # Required to download artifacts
272+
packages: read # Read from GitHub Container Registry
274273
steps:
275274
- name: Determine version
276275
id: version
@@ -296,87 +295,45 @@ jobs:
296295
exit 0
297296
fi
298297
299-
- name: Checkout code
300-
uses: actions/checkout@v4
301-
with:
302-
ref: ${{ steps.version.outputs.tag }}
303-
304-
- name: Determine artifact source
305-
id: artifact
298+
- name: Set up Docker Buildx
299+
# Required for imagetools commands
306300
if: steps.check-docker.outputs.has_credentials == 'true'
307-
# Use shared script to find the correct Docker image artifact from the release build
308-
env:
309-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
310-
run: |
311-
chmod +x .github/scripts/determine-artifact.sh
312-
.github/scripts/determine-artifact.sh \
313-
--tag "${{ steps.version.outputs.tag }}" \
314-
--repo "${{ github.repository }}" \
315-
--version "${{ steps.version.outputs.version }}" \
316-
--prefix "docker-image" \
317-
--output "$GITHUB_OUTPUT"
301+
uses: docker/setup-buildx-action@v3
318302

319-
- name: Download Docker image artifact
320-
# Download the pre-built, pre-scanned image from main workflow
321-
# This ensures we publish exactly what was tested
303+
- name: Login to GitHub Container Registry
304+
# Login to GHCR to pull the pre-built image
322305
if: steps.check-docker.outputs.has_credentials == 'true'
323-
uses: actions/download-artifact@v4
306+
uses: docker/login-action@v3
324307
with:
325-
name: ${{ steps.artifact.outputs.artifact_name }}
326-
path: ./docker-artifact
327-
run-id: ${{ steps.artifact.outputs.run_id }}
328-
github-token: ${{ secrets.GITHUB_TOKEN }}
329-
330-
- name: Verify artifact exists
331-
if: steps.check-docker.outputs.has_credentials == 'true'
332-
run: |
333-
# The artifact name format has changed to use short SHA
334-
EXPECTED_FILE="./docker-artifact/${{ steps.artifact.outputs.artifact_name }}.tar.gz"
335-
if [ ! -f "$EXPECTED_FILE" ]; then
336-
echo "❌ Docker image artifact not found!"
337-
echo "Expected: $EXPECTED_FILE"
338-
echo "Contents of ./docker-artifact:"
339-
ls -la ./docker-artifact/ || echo "Directory does not exist"
340-
echo ""
341-
echo "This usually means the main workflow didn't build a Docker image."
342-
echo "Ensure ENABLE_DOCKER_RELEASE was set to 'true' when the release was created."
343-
exit 1
344-
fi
345-
echo "✅ Found Docker image artifact: $EXPECTED_FILE"
346-
347-
- name: Set up Docker Buildx
348-
# Required for loading and pushing multi-platform images
349-
if: steps.check-docker.outputs.has_credentials == 'true'
350-
uses: docker/setup-buildx-action@v3
308+
registry: ghcr.io
309+
username: ${{ github.actor }}
310+
password: ${{ secrets.GITHUB_TOKEN }}
351311

352312
- name: Login to Docker Hub
353-
# SECURITY: Authenticate with Docker Hub
313+
# SECURITY: Authenticate with Docker Hub for pushing
354314
if: steps.check-docker.outputs.has_credentials == 'true'
355315
uses: docker/login-action@v3
356316
with:
357317
username: ${{ secrets.DOCKERHUB_USERNAME }}
358318
password: ${{ secrets.DOCKERHUB_TOKEN }}
359319

360-
- name: Push Docker image
361-
# Push the pre-built OCI multi-platform image to Docker Hub
320+
- name: Copy image from GHCR to Docker Hub
321+
# Use buildx imagetools to copy multi-platform image between registries
322+
# This properly handles multi-platform manifest lists
362323
if: steps.check-docker.outputs.has_credentials == 'true'
363324
run: |
364-
echo "📥 Decompressing and extracting OCI image..."
365-
gunzip ./docker-artifact/${{ steps.artifact.outputs.artifact_name }}.tar.gz
366-
367-
# Extract OCI layout from tar
368-
mkdir -p oci-layout
369-
tar -xf ./docker-artifact/${{ steps.artifact.outputs.artifact_name }}.tar -C oci-layout
370-
325+
SOURCE_IMAGE="ghcr.io/${{ github.repository_owner }}/sonarqube-mcp-server"
371326
TARGET_REPO="${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }}"
372327
VERSION="${{ steps.version.outputs.version }}"
373328
374-
echo "📤 Pushing multi-platform OCI image to Docker Hub..."
375-
# Use docker buildx imagetools create to push from OCI layout
376-
# This properly handles multi-platform manifest lists
329+
echo "📤 Copying multi-platform image from GHCR to Docker Hub..."
330+
echo "Source: $SOURCE_IMAGE:$VERSION"
331+
echo "Target: $TARGET_REPO:$VERSION"
332+
333+
# Copy image with version tag
377334
docker buildx imagetools create \
378335
--tag $TARGET_REPO:$VERSION \
379-
oci-layout://oci-layout
336+
$SOURCE_IMAGE:$VERSION
380337
381338
echo "🏷️ Creating additional tags..."
382339
# Create alias tags for latest, major, and major.minor versions
@@ -388,6 +345,7 @@ jobs:
388345
docker buildx imagetools create --tag $TARGET_REPO:$MAJOR.$MINOR $TARGET_REPO:$VERSION
389346
390347
echo "✅ Docker image published successfully to Docker Hub"
348+
echo "📋 Published tags: $VERSION, latest, $MAJOR, $MAJOR.$MINOR"
391349
392350
# =============================================================================
393351
# NOTIFICATION

.github/workflows/reusable-docker.yml

Lines changed: 74 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,16 @@ jobs:
8888
# Advanced Docker builder with cache support
8989
uses: docker/setup-buildx-action@v3
9090

91+
- name: Login to GitHub Container Registry
92+
# Login to GHCR for multi-platform builds that need to be pushed to registry
93+
# Single-platform builds for PRs don't need registry push
94+
if: inputs.save-artifact && contains(inputs.platforms, ',')
95+
uses: docker/login-action@v3
96+
with:
97+
registry: ghcr.io
98+
username: ${{ github.actor }}
99+
password: ${{ secrets.GITHUB_TOKEN }}
100+
91101
# =============================================================================
92102
# DOCKER BUILD
93103
# Build image with layer caching for efficiency
@@ -97,7 +107,9 @@ jobs:
97107
id: meta
98108
uses: docker/metadata-action@v5
99109
with:
100-
images: ${{ inputs.image-name }}
110+
# Use GHCR for multi-platform artifact builds, local name otherwise
111+
images: |
112+
${{ (inputs.save-artifact && contains(inputs.platforms, ',')) && format('ghcr.io/{0}/{1}', github.repository_owner, inputs.image-name) || inputs.image-name }}
101113
tags: |
102114
type=raw,value=${{ inputs.version }},enable=${{ inputs.version != '' }}
103115
type=raw,value=latest,enable=${{ inputs.version != '' }}
@@ -109,37 +121,53 @@ jobs:
109121
id: build-config
110122
run: |
111123
# Determine if we're building for multiple platforms
124+
IS_MULTI_PLATFORM="false"
112125
if echo "${{ inputs.platforms }}" | grep -q ','; then
113-
echo "is_multi_platform=true" >> $GITHUB_OUTPUT
114-
else
115-
echo "is_multi_platform=false" >> $GITHUB_OUTPUT
126+
IS_MULTI_PLATFORM="true"
116127
fi
117128
118-
# Determine if we can load the image (only for single-platform non-artifact builds)
129+
# For multi-platform builds with save-artifact, push to GHCR
130+
# For single-platform builds or PR builds, load locally or save to tar
119131
SAVE_ARTIFACT="${{ inputs.save-artifact }}"
120-
if [ "$SAVE_ARTIFACT" != "true" ] && ! echo "${{ inputs.platforms }}" | grep -q ','; then
121-
echo "can_load=true" >> $GITHUB_OUTPUT
122-
echo "output_type=" >> $GITHUB_OUTPUT
132+
SHOULD_PUSH="false"
133+
CAN_LOAD="false"
134+
OUTPUT_TYPE=""
135+
136+
if [ "$SAVE_ARTIFACT" = "true" ] && [ "$IS_MULTI_PLATFORM" = "true" ]; then
137+
# Multi-platform artifact build: push to GHCR
138+
SHOULD_PUSH="true"
139+
CAN_LOAD="false"
140+
elif [ "$SAVE_ARTIFACT" != "true" ] && [ "$IS_MULTI_PLATFORM" = "false" ]; then
141+
# Single-platform PR build: load locally
142+
CAN_LOAD="true"
123143
else
124-
# Need to output to tar for artifact or multi-platform builds
125-
echo "can_load=false" >> $GITHUB_OUTPUT
126-
# Use tag SHA if provided, otherwise use github.sha
144+
# Single-platform artifact build: save to tar
145+
CAN_LOAD="false"
127146
SHA_TO_USE="${{ inputs.tag_sha || github.sha }}"
128-
# Use OCI format for multi-platform builds (docker format doesn't support manifest lists)
129-
if echo "${{ inputs.platforms }}" | grep -q ','; then
130-
echo "output_type=type=oci,dest=${{ inputs.artifact-name }}-${SHA_TO_USE}.tar" >> $GITHUB_OUTPUT
131-
else
132-
echo "output_type=type=docker,dest=${{ inputs.artifact-name }}-${SHA_TO_USE}.tar" >> $GITHUB_OUTPUT
133-
fi
147+
OUTPUT_TYPE="type=docker,dest=${{ inputs.artifact-name }}-${SHA_TO_USE}.tar"
134148
fi
135149
150+
{
151+
echo "is_multi_platform=$IS_MULTI_PLATFORM"
152+
echo "should_push=$SHOULD_PUSH"
153+
echo "can_load=$CAN_LOAD"
154+
echo "output_type=$OUTPUT_TYPE"
155+
} >> $GITHUB_OUTPUT
156+
157+
echo "📋 Build configuration:"
158+
echo " Multi-platform: $IS_MULTI_PLATFORM"
159+
echo " Save artifact: $SAVE_ARTIFACT"
160+
echo " Should push: $SHOULD_PUSH"
161+
echo " Can load: $CAN_LOAD"
162+
echo " Output type: $OUTPUT_TYPE"
163+
136164
- name: Build Docker image
137165
id: build
138166
uses: docker/build-push-action@v6
139167
with:
140168
context: .
141169
platforms: ${{ inputs.platforms }}
142-
push: false # Never push from this reusable workflow
170+
push: ${{ steps.build-config.outputs.should_push == 'true' }}
143171
load: ${{ steps.build-config.outputs.can_load == 'true' }}
144172
tags: ${{ steps.meta.outputs.tags }}
145173
labels: ${{ steps.meta.outputs.labels }}
@@ -163,14 +191,18 @@ jobs:
163191
if [ "$CAN_LOAD" = "true" ]; then
164192
# For loaded single-platform images, scan by image reference
165193
FIRST_TAG=$(echo "${{ steps.meta.outputs.tags }}" | head -n1)
166-
echo "scan_input=" >> $GITHUB_OUTPUT
167-
echo "scan_image_ref=$FIRST_TAG" >> $GITHUB_OUTPUT
194+
{
195+
echo "scan_input="
196+
echo "scan_image_ref=$FIRST_TAG"
197+
} >> $GITHUB_OUTPUT
168198
echo "Using image reference for scanning: $FIRST_TAG"
169199
else
170200
# For multi-platform or artifact builds, scan the tar file
171201
SHA_TO_USE="${{ inputs.tag_sha || github.sha }}"
172-
echo "scan_input=${{ inputs.artifact-name }}-${SHA_TO_USE}.tar" >> $GITHUB_OUTPUT
173-
echo "scan_image_ref=" >> $GITHUB_OUTPUT
202+
{
203+
echo "scan_input=${{ inputs.artifact-name }}-${SHA_TO_USE}.tar"
204+
echo "scan_image_ref="
205+
} >> $GITHUB_OUTPUT
174206
echo "Using tar file for scanning: ${{ inputs.artifact-name }}-${SHA_TO_USE}.tar"
175207
fi
176208
@@ -231,21 +263,24 @@ jobs:
231263

232264
# =============================================================================
233265
# ARTIFACT STORAGE
234-
# Save Docker image for reuse in publish workflow
266+
# Save Docker image tar files for single-platform builds
267+
# Multi-platform builds are pushed to GHCR instead
235268
# =============================================================================
236269

237270
- name: Compress Docker image artifact
238271
# Compress the tar file to reduce storage costs
239-
if: inputs.save-artifact
272+
# Only for single-platform builds (multi-platform builds pushed to GHCR)
273+
if: inputs.save-artifact && !contains(inputs.platforms, ',')
240274
run: |
241275
SHA_TO_USE="${{ inputs.tag_sha || github.sha }}"
242276
echo "Compressing Docker image artifact..."
243277
gzip -9 ${{ inputs.artifact-name }}-${SHA_TO_USE}.tar
244278
ls -lh ${{ inputs.artifact-name }}-${SHA_TO_USE}.tar.gz
245279
246280
- name: Upload Docker image artifact
247-
# Store image for deterministic publishing
248-
if: inputs.save-artifact
281+
# Store single-platform image tar for deterministic publishing
282+
# Multi-platform images are stored in GHCR registry
283+
if: inputs.save-artifact && !contains(inputs.platforms, ',')
249284
uses: actions/upload-artifact@v4
250285
with:
251286
name: ${{ inputs.artifact-name }}-${{ inputs.tag_sha || github.sha }}
@@ -258,10 +293,19 @@ jobs:
258293
# Generate attestations for build provenance (main builds only)
259294
# =============================================================================
260295

261-
- name: Generate attestations
262-
# Creates cryptographic proof of build provenance (SLSA Level 3)
263-
# Only runs when id-token permission is available (required for attestations)
264-
if: inputs.save-artifact && inputs.version != '' && env.ACTIONS_ID_TOKEN_REQUEST_URL != ''
296+
- name: Generate attestations for GHCR images
297+
# Creates cryptographic proof of build provenance for multi-platform images
298+
# Multi-platform images are stored in GHCR registry
299+
if: inputs.save-artifact && contains(inputs.platforms, ',') && inputs.version != '' && env.ACTIONS_ID_TOKEN_REQUEST_URL != ''
300+
uses: actions/attest-build-provenance@v2
301+
with:
302+
subject-name: ghcr.io/${{ github.repository_owner }}/${{ inputs.image-name }}
303+
subject-digest: ${{ steps.build.outputs.digest }}
304+
push-to-registry: true
305+
306+
- name: Generate attestations for tar artifacts
307+
# Creates cryptographic proof of build provenance for single-platform tar files
308+
if: inputs.save-artifact && !contains(inputs.platforms, ',') && inputs.version != '' && env.ACTIONS_ID_TOKEN_REQUEST_URL != ''
265309
uses: actions/attest-build-provenance@v2
266310
with:
267311
subject-path: ${{ inputs.artifact-name }}-${{ inputs.tag_sha || github.sha }}.tar.gz

0 commit comments

Comments
 (0)