Skip to content

Commit 4d15916

Browse files
feat: Add multiplatform container image support (amd64, arm64, s390x) (#1520)
* feat: Add multiplatform container image support (amd64, arm64, s390x) Add comprehensive multiplatform Docker build support with: - New docker-multiplatform.yml workflow: - Parallel native builds for amd64 (ubuntu-latest) and arm64 (ubuntu-24.04-arm) - QEMU emulation for s390x on ubuntu-latest - Multiplatform manifest creation with buildx imagetools - Security scanning (Trivy, Grype, Syft SBOM) on amd64 - Cosign keyless signing for all architectures - Updated docker-release.yml: - Use buildx imagetools create for manifest handling - Preserves all architecture variants when tagging releases - Updated ibm-cloud-code-engine.yml: - Explicit --platform linux/amd64 flag for consistent builds - Updated Containerfile.lite for multiplatform compatibility: - Use ubi10-minimal as runtime base instead of scratch - Eliminates dnf --installroot which fails under QEMU emulation - Uses microdnf for runtime package installation - Maintains security scanning compatibility (RPM database preserved) - Enhanced Makefile targets: - container-build-multi: Build multiplatform image locally - container-inspect-manifest: Inspect multiplatform manifest in registry Closes #80 Signed-off-by: Mihai Criveti <[email protected]> * Include missing Container.scratch file in MANIFEST.in (#1529) Signed-off-by: Brian Hussey <[email protected]> * Bh/multi arch builds 2 (#1532) * feat: Add multiplatform container image support (amd64, arm64, s390x) Add comprehensive multiplatform Docker build support with: - New docker-multiplatform.yml workflow: - Parallel native builds for amd64 (ubuntu-latest) and arm64 (ubuntu-24.04-arm) - QEMU emulation for s390x on ubuntu-latest - Multiplatform manifest creation with buildx imagetools - Security scanning (Trivy, Grype, Syft SBOM) on amd64 - Cosign keyless signing for all architectures - Updated docker-release.yml: - Use buildx imagetools create for manifest handling - Preserves all architecture variants when tagging releases - Updated ibm-cloud-code-engine.yml: - Explicit --platform linux/amd64 flag for consistent builds - Updated Containerfile.lite for multiplatform compatibility: - Use ubi10-minimal as runtime base instead of scratch - Eliminates dnf --installroot which fails under QEMU emulation - Uses microdnf for runtime package installation - Maintains security scanning compatibility (RPM database preserved) - Enhanced Makefile targets: - container-build-multi: Build multiplatform image locally - container-inspect-manifest: Inspect multiplatform manifest in registry Closes #80 Signed-off-by: Mihai Criveti <[email protected]> * Include missing Container.scratch file in MANIFEST.in (#1529) Signed-off-by: Brian Hussey <[email protected]> * Update documentation for multi-architecture image use Signed-off-by: Brian Hussey <[email protected]> * Revert unneeded changes in one part of the doc. Signed-off-by: Brian Hussey <[email protected]> * Add recursive signing to cosign step. Signed-off-by: Brian Hussey <[email protected]> --------- Signed-off-by: Mihai Criveti <[email protected]> Signed-off-by: Brian Hussey <[email protected]> Co-authored-by: Mihai Criveti <[email protected]> --------- Signed-off-by: Mihai Criveti <[email protected]> Signed-off-by: Brian Hussey <[email protected]> Co-authored-by: Brian Hussey <[email protected]>
1 parent d0edab6 commit 4d15916

File tree

9 files changed

+1001
-134
lines changed

9 files changed

+1001
-134
lines changed
Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
# ===============================================================
2+
# Multiplatform Docker Build Workflow
3+
# ===============================================================
4+
#
5+
# This workflow builds container images for multiple architectures:
6+
# - linux/amd64 (native on ubuntu-latest)
7+
# - linux/arm64 (native on ubuntu-24.04-arm)
8+
# - linux/s390x (QEMU emulation on ubuntu-latest)
9+
#
10+
# Pipeline:
11+
# 1. Lint Dockerfile with Hadolint
12+
# 2. Build platform images in parallel
13+
# 3. Create multiplatform manifest
14+
# 4. Security scan (Trivy, Grype, SBOM)
15+
# 5. Sign with Cosign (keyless OIDC)
16+
#
17+
# ===============================================================
18+
19+
name: Multiplatform Docker Build
20+
21+
on:
22+
push:
23+
branches: ["main"]
24+
paths:
25+
- 'Containerfile.lite'
26+
- 'mcpgateway/**'
27+
- 'plugins/**'
28+
- 'pyproject.toml'
29+
- '.github/workflows/docker-multiplatform.yml'
30+
pull_request:
31+
branches: ["main"]
32+
paths:
33+
- 'Containerfile.lite'
34+
- 'mcpgateway/**'
35+
- 'plugins/**'
36+
- 'pyproject.toml'
37+
- '.github/workflows/docker-multiplatform.yml'
38+
schedule:
39+
- cron: "17 18 * * 2" # Weekly rebuild (Tuesday 18:17 UTC) for CVE patches
40+
workflow_dispatch:
41+
inputs:
42+
platforms:
43+
description: 'Platforms to build (comma-separated)'
44+
required: false
45+
default: 'linux/amd64,linux/arm64,linux/s390x'
46+
47+
permissions:
48+
contents: read
49+
packages: write
50+
security-events: write
51+
actions: read
52+
id-token: write
53+
54+
env:
55+
REGISTRY: ghcr.io
56+
IMAGE_NAME: ${{ github.repository }}
57+
58+
jobs:
59+
# ---------------------------------------------------------------
60+
# Lint Dockerfile (architecture-independent, run once)
61+
# ---------------------------------------------------------------
62+
lint:
63+
name: Lint Dockerfile
64+
runs-on: ubuntu-latest
65+
66+
steps:
67+
- name: Checkout code
68+
uses: actions/checkout@v5
69+
70+
- name: Hadolint
71+
id: hadolint
72+
continue-on-error: true
73+
run: |
74+
curl -sSL https://github.com/hadolint/hadolint/releases/latest/download/hadolint-Linux-x86_64 -o /usr/local/bin/hadolint
75+
chmod +x /usr/local/bin/hadolint
76+
hadolint -f sarif Containerfile.lite > hadolint-results.sarif || true
77+
78+
- name: Upload Hadolint SARIF
79+
if: always() && hashFiles('hadolint-results.sarif') != ''
80+
uses: github/codeql-action/upload-sarif@v3
81+
with:
82+
sarif_file: hadolint-results.sarif
83+
84+
# ---------------------------------------------------------------
85+
# Build each platform in parallel
86+
# ---------------------------------------------------------------
87+
build:
88+
name: Build ${{ matrix.suffix }}
89+
needs: lint
90+
strategy:
91+
fail-fast: false
92+
matrix:
93+
include:
94+
- platform: linux/amd64
95+
runner: ubuntu-latest
96+
suffix: amd64
97+
- platform: linux/arm64
98+
runner: ubuntu-24.04-arm
99+
suffix: arm64
100+
- platform: linux/s390x
101+
runner: ubuntu-latest
102+
suffix: s390x
103+
qemu: true
104+
105+
runs-on: ${{ matrix.runner }}
106+
107+
steps:
108+
- name: Checkout code
109+
uses: actions/checkout@v5
110+
111+
- name: Set image name lowercase
112+
run: |
113+
IMAGE_NAME_LC=$(echo "${{ env.IMAGE_NAME }}" | tr '[:upper:]' '[:lower:]')
114+
echo "IMAGE_NAME_LC=${IMAGE_NAME_LC}" >> $GITHUB_ENV
115+
116+
- name: Set up QEMU
117+
if: matrix.qemu
118+
uses: docker/setup-qemu-action@v3
119+
with:
120+
platforms: ${{ matrix.platform }}
121+
122+
- name: Set up Docker Buildx
123+
uses: docker/setup-buildx-action@v3
124+
125+
- name: Log in to GHCR
126+
uses: docker/login-action@v3
127+
with:
128+
registry: ${{ env.REGISTRY }}
129+
username: ${{ github.actor }}
130+
password: ${{ secrets.GITHUB_TOKEN }}
131+
132+
- name: Extract metadata
133+
id: meta
134+
uses: docker/metadata-action@v5
135+
with:
136+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LC }}
137+
tags: |
138+
type=raw,value=${{ matrix.suffix }}-${{ github.sha }}
139+
140+
- name: Build and push
141+
id: build
142+
uses: docker/build-push-action@v6
143+
with:
144+
context: .
145+
file: Containerfile.lite
146+
platforms: ${{ matrix.platform }}
147+
push: true
148+
tags: ${{ steps.meta.outputs.tags }}
149+
labels: ${{ steps.meta.outputs.labels }}
150+
cache-from: type=gha,scope=build-${{ matrix.suffix }}
151+
cache-to: type=gha,mode=max,scope=build-${{ matrix.suffix }}
152+
provenance: false
153+
154+
- name: Export digest
155+
run: |
156+
mkdir -p /tmp/digests
157+
digest="${{ steps.build.outputs.digest }}"
158+
touch "/tmp/digests/${digest#sha256:}"
159+
echo "Digest for ${{ matrix.suffix }}: $digest"
160+
161+
- name: Upload digest
162+
uses: actions/upload-artifact@v4
163+
with:
164+
name: digest-${{ matrix.suffix }}
165+
path: /tmp/digests/*
166+
if-no-files-found: error
167+
retention-days: 1
168+
169+
# ---------------------------------------------------------------
170+
# Create multiplatform manifest
171+
# ---------------------------------------------------------------
172+
manifest:
173+
name: Create Manifest
174+
needs: build
175+
runs-on: ubuntu-latest
176+
177+
steps:
178+
- name: Set image name lowercase
179+
run: |
180+
IMAGE_NAME_LC=$(echo "${{ env.IMAGE_NAME }}" | tr '[:upper:]' '[:lower:]')
181+
echo "IMAGE_NAME_LC=${IMAGE_NAME_LC}" >> $GITHUB_ENV
182+
183+
- name: Set up Docker Buildx
184+
uses: docker/setup-buildx-action@v3
185+
186+
- name: Log in to GHCR
187+
uses: docker/login-action@v3
188+
with:
189+
registry: ${{ env.REGISTRY }}
190+
username: ${{ github.actor }}
191+
password: ${{ secrets.GITHUB_TOKEN }}
192+
193+
- name: Create and push manifest
194+
run: |
195+
SHA=${{ github.sha }}
196+
IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LC }}
197+
198+
echo "Creating multiplatform manifest..."
199+
docker buildx imagetools create \
200+
--tag "${IMAGE}:${SHA}" \
201+
--tag "${IMAGE}:latest" \
202+
"${IMAGE}:amd64-${SHA}" \
203+
"${IMAGE}:arm64-${SHA}" \
204+
"${IMAGE}:s390x-${SHA}"
205+
206+
echo "Manifest created successfully"
207+
208+
- name: Inspect manifest
209+
run: |
210+
IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LC }}
211+
echo "Inspecting multiplatform manifest..."
212+
docker buildx imagetools inspect "${IMAGE}:latest"
213+
214+
# ---------------------------------------------------------------
215+
# Security scanning (amd64 only - sufficient for CVE detection)
216+
# ---------------------------------------------------------------
217+
scan:
218+
name: Security Scan
219+
needs: manifest
220+
runs-on: ubuntu-latest
221+
if: github.ref == 'refs/heads/main'
222+
223+
steps:
224+
- name: Checkout code
225+
uses: actions/checkout@v5
226+
227+
- name: Set image name lowercase
228+
run: |
229+
IMAGE_NAME_LC=$(echo "${{ env.IMAGE_NAME }}" | tr '[:upper:]' '[:lower:]')
230+
echo "IMAGE_NAME_LC=${IMAGE_NAME_LC}" >> $GITHUB_ENV
231+
232+
- name: Log in to GHCR
233+
uses: docker/login-action@v3
234+
with:
235+
registry: ${{ env.REGISTRY }}
236+
username: ${{ github.actor }}
237+
password: ${{ secrets.GITHUB_TOKEN }}
238+
239+
- name: Pull amd64 image for scanning
240+
run: |
241+
docker pull --platform linux/amd64 ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LC }}:latest
242+
243+
- name: Generate SBOM (Syft)
244+
uses: anchore/sbom-action@v0
245+
with:
246+
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LC }}:latest
247+
output-file: sbom.spdx.json
248+
249+
- name: Upload SBOM
250+
uses: actions/upload-artifact@v4
251+
with:
252+
name: sbom
253+
path: sbom.spdx.json
254+
retention-days: 30
255+
256+
- name: Trivy vulnerability scan
257+
uses: aquasecurity/trivy-action@master
258+
with:
259+
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LC }}:latest
260+
format: sarif
261+
output: trivy-results.sarif
262+
severity: CRITICAL,HIGH
263+
exit-code: 0
264+
265+
- name: Upload Trivy SARIF
266+
if: always() && hashFiles('trivy-results.sarif') != ''
267+
uses: github/codeql-action/upload-sarif@v3
268+
with:
269+
sarif_file: trivy-results.sarif
270+
271+
- name: Install Grype
272+
run: |
273+
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin
274+
275+
- name: Grype vulnerability scan
276+
continue-on-error: true
277+
run: |
278+
grype ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LC }}:latest --scope all-layers --only-fixed
279+
280+
- name: Grype SARIF report
281+
continue-on-error: true
282+
run: |
283+
grype ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LC }}:latest --scope all-layers --output sarif --file grype-results.sarif
284+
285+
- name: Upload Grype SARIF
286+
if: always() && hashFiles('grype-results.sarif') != ''
287+
uses: github/codeql-action/upload-sarif@v3
288+
with:
289+
sarif_file: grype-results.sarif
290+
291+
# ---------------------------------------------------------------
292+
# Sign images with Cosign (keyless OIDC)
293+
# ---------------------------------------------------------------
294+
sign:
295+
name: Sign Images
296+
needs: [manifest, scan]
297+
runs-on: ubuntu-latest
298+
if: github.ref == 'refs/heads/main'
299+
300+
steps:
301+
- name: Set image name lowercase
302+
run: |
303+
IMAGE_NAME_LC=$(echo "${{ env.IMAGE_NAME }}" | tr '[:upper:]' '[:lower:]')
304+
echo "IMAGE_NAME_LC=${IMAGE_NAME_LC}" >> $GITHUB_ENV
305+
306+
- name: Install Cosign
307+
uses: sigstore/cosign-installer@v3
308+
309+
- name: Log in to GHCR
310+
uses: docker/login-action@v3
311+
with:
312+
registry: ${{ env.REGISTRY }}
313+
username: ${{ github.actor }}
314+
password: ${{ secrets.GITHUB_TOKEN }}
315+
316+
- name: Sign multiplatform image
317+
env:
318+
COSIGN_EXPERIMENTAL: "1"
319+
run: |
320+
IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LC }}
321+
SHA=${{ github.sha }}
322+
323+
echo "Signing ${IMAGE}:latest"
324+
325+
cosign sign --recursive --yes "${IMAGE}:latest"
326+
327+
echo "Signing ${IMAGE}:${SHA}"
328+
cosign sign --recursive --yes "${IMAGE}:${SHA}"
329+
330+
echo "Images signed successfully"

.github/workflows/docker-release.yml

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -102,26 +102,32 @@ jobs:
102102
password: ${{ secrets.GITHUB_TOKEN }}
103103

104104
# ----------------------------------------------------------------
105-
# Step 4 Pull the image using the commit SHA tag
105+
# Step 4 Set up Docker Buildx for multiplatform manifest handling
106106
# ----------------------------------------------------------------
107-
- name: ⬇️ Pull image by commit SHA
108-
run: |
109-
IMAGE="ghcr.io/$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')"
110-
docker pull "$IMAGE:${{ steps.meta.outputs.sha }}"
107+
- name: 🛠️ Set up Docker Buildx
108+
uses: docker/setup-buildx-action@v3
111109

112110
# ----------------------------------------------------------------
113-
# Step 5 Tag the image with the semantic version tag
111+
# Step 5 Create version tag from existing multiplatform manifest
112+
# Note: For multiplatform images, we use 'docker buildx imagetools create'
113+
# instead of 'docker pull' + 'docker tag' + 'docker push' because:
114+
# 1. Multiplatform images are manifest lists, not single images
115+
# 2. We create a new manifest that references the existing SHA-tagged image
116+
# 3. This preserves all architecture variants (amd64, arm64, s390x)
114117
# ----------------------------------------------------------------
115-
- name: 🏷️ Tag image with version
118+
- name: 🏷️ Create version tag for multiplatform manifest
116119
run: |
117120
IMAGE="ghcr.io/$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')"
118-
docker tag "$IMAGE:${{ steps.meta.outputs.sha }}" \
119-
"$IMAGE:${{ steps.meta.outputs.tag }}"
121+
echo "Creating version tag ${{ steps.meta.outputs.tag }} from ${IMAGE}:${{ steps.meta.outputs.sha }}"
122+
docker buildx imagetools create \
123+
"${IMAGE}:${{ steps.meta.outputs.sha }}" \
124+
--tag "${IMAGE}:${{ steps.meta.outputs.tag }}"
120125
121126
# ----------------------------------------------------------------
122-
# Step 6 Push the new tag to GHCR
127+
# Step 6 Verify the new version tag
123128
# ----------------------------------------------------------------
124-
- name: 🚀 Push new version tag
129+
- name: 🔍 Verify version tag
125130
run: |
126131
IMAGE="ghcr.io/$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')"
127-
docker push "$IMAGE:${{ steps.meta.outputs.tag }}"
132+
echo "Inspecting ${IMAGE}:${{ steps.meta.outputs.tag }}"
133+
docker buildx imagetools inspect "${IMAGE}:${{ steps.meta.outputs.tag }}"

.github/workflows/ibm-cloud-code-engine.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,11 +127,12 @@ jobs:
127127
ibmcloud ce project select --name "$CODE_ENGINE_PROJECT"
128128
129129
# -----------------------------------------------------------
130-
# 5️⃣ Build & tag image (cache-aware)
130+
# 5️⃣ Build & tag image (cache-aware, amd64 platform)
131131
# -----------------------------------------------------------
132132
- name: 🏗️ Build Docker image (with cache)
133133
run: |
134134
docker buildx build \
135+
--platform linux/amd64 \
135136
--file Containerfile.lite \
136137
--tag "$REGISTRY_HOSTNAME/$ICR_NAMESPACE/$IMAGE_NAME:$IMAGE_TAG" \
137138
--cache-from type=local,src=${{ env.CACHE_DIR }} \

0 commit comments

Comments
 (0)