Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
329 changes: 329 additions & 0 deletions .github/workflows/docker-multiplatform.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,329 @@
# ===============================================================
# Multiplatform Docker Build Workflow
# ===============================================================
#
# This workflow builds container images for multiple architectures:
# - linux/amd64 (native on ubuntu-latest)
# - linux/arm64 (native on ubuntu-24.04-arm)
# - linux/s390x (QEMU emulation on ubuntu-latest)
#
# Pipeline:
# 1. Lint Dockerfile with Hadolint
# 2. Build platform images in parallel
# 3. Create multiplatform manifest
# 4. Security scan (Trivy, Grype, SBOM)
# 5. Sign with Cosign (keyless OIDC)
#
# ===============================================================

name: Multiplatform Docker Build

on:
push:
branches: ["main"]
paths:
- 'Containerfile.lite'
- 'mcpgateway/**'
- 'plugins/**'
- 'pyproject.toml'
- '.github/workflows/docker-multiplatform.yml'
pull_request:
branches: ["main"]
paths:
- 'Containerfile.lite'
- 'mcpgateway/**'
- 'plugins/**'
- 'pyproject.toml'
- '.github/workflows/docker-multiplatform.yml'
schedule:
- cron: "17 18 * * 2" # Weekly rebuild (Tuesday 18:17 UTC) for CVE patches
workflow_dispatch:
inputs:
platforms:
description: 'Platforms to build (comma-separated)'
required: false
default: 'linux/amd64,linux/arm64,linux/s390x'

permissions:
contents: read
packages: write
security-events: write
actions: read
id-token: write

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

jobs:
# ---------------------------------------------------------------
# Lint Dockerfile (architecture-independent, run once)
# ---------------------------------------------------------------
lint:
name: Lint Dockerfile
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v5

- name: Hadolint
id: hadolint
continue-on-error: true
run: |
curl -sSL https://github.com/hadolint/hadolint/releases/latest/download/hadolint-Linux-x86_64 -o /usr/local/bin/hadolint
chmod +x /usr/local/bin/hadolint
hadolint -f sarif Containerfile.lite > hadolint-results.sarif || true

- name: Upload Hadolint SARIF
if: always() && hashFiles('hadolint-results.sarif') != ''
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: hadolint-results.sarif

# ---------------------------------------------------------------
# Build each platform in parallel
# ---------------------------------------------------------------
build:
name: Build ${{ matrix.suffix }}
needs: lint
strategy:
fail-fast: false
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
suffix: amd64
- platform: linux/arm64
runner: ubuntu-24.04-arm
suffix: arm64
- platform: linux/s390x
runner: ubuntu-latest
suffix: s390x
qemu: true

runs-on: ${{ matrix.runner }}

steps:
- name: Checkout code
uses: actions/checkout@v5

- name: Set image name lowercase
run: |
IMAGE_NAME_LC=$(echo "${{ env.IMAGE_NAME }}" | tr '[:upper:]' '[:lower:]')
echo "IMAGE_NAME_LC=${IMAGE_NAME_LC}" >> $GITHUB_ENV

- name: Set up QEMU
if: matrix.qemu
uses: docker/setup-qemu-action@v3
with:
platforms: ${{ matrix.platform }}

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LC }}
tags: |
type=raw,value=${{ matrix.suffix }}-${{ github.sha }}

- name: Build and push
id: build
uses: docker/build-push-action@v6
with:
context: .
file: Containerfile.lite
platforms: ${{ matrix.platform }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=build-${{ matrix.suffix }}
cache-to: type=gha,mode=max,scope=build-${{ matrix.suffix }}
provenance: false

- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
echo "Digest for ${{ matrix.suffix }}: $digest"

- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digest-${{ matrix.suffix }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1

# ---------------------------------------------------------------
# Create multiplatform manifest
# ---------------------------------------------------------------
manifest:
name: Create Manifest
needs: build
runs-on: ubuntu-latest

steps:
- name: Set image name lowercase
run: |
IMAGE_NAME_LC=$(echo "${{ env.IMAGE_NAME }}" | tr '[:upper:]' '[:lower:]')
echo "IMAGE_NAME_LC=${IMAGE_NAME_LC}" >> $GITHUB_ENV

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Create and push manifest
run: |
SHA=${{ github.sha }}
IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LC }}

echo "Creating multiplatform manifest..."
docker buildx imagetools create \
--tag "${IMAGE}:${SHA}" \
--tag "${IMAGE}:latest" \
"${IMAGE}:amd64-${SHA}" \
"${IMAGE}:arm64-${SHA}" \
"${IMAGE}:s390x-${SHA}"

echo "Manifest created successfully"

- name: Inspect manifest
run: |
IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LC }}
echo "Inspecting multiplatform manifest..."
docker buildx imagetools inspect "${IMAGE}:latest"

# ---------------------------------------------------------------
# Security scanning (amd64 only - sufficient for CVE detection)
# ---------------------------------------------------------------
scan:
name: Security Scan
needs: manifest
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'

steps:
- name: Checkout code
uses: actions/checkout@v5

- name: Set image name lowercase
run: |
IMAGE_NAME_LC=$(echo "${{ env.IMAGE_NAME }}" | tr '[:upper:]' '[:lower:]')
echo "IMAGE_NAME_LC=${IMAGE_NAME_LC}" >> $GITHUB_ENV

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Pull amd64 image for scanning
run: |
docker pull --platform linux/amd64 ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LC }}:latest

- name: Generate SBOM (Syft)
uses: anchore/sbom-action@v0
with:
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LC }}:latest
output-file: sbom.spdx.json

- name: Upload SBOM
uses: actions/upload-artifact@v4
with:
name: sbom
path: sbom.spdx.json
retention-days: 30

- name: Trivy vulnerability scan
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LC }}:latest
format: sarif
output: trivy-results.sarif
severity: CRITICAL,HIGH
exit-code: 0

- name: Upload Trivy SARIF
if: always() && hashFiles('trivy-results.sarif') != ''
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: trivy-results.sarif

- name: Install Grype
run: |
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin

- name: Grype vulnerability scan
continue-on-error: true
run: |
grype ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LC }}:latest --scope all-layers --only-fixed

- name: Grype SARIF report
continue-on-error: true
run: |
grype ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LC }}:latest --scope all-layers --output sarif --file grype-results.sarif

- name: Upload Grype SARIF
if: always() && hashFiles('grype-results.sarif') != ''
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: grype-results.sarif

# ---------------------------------------------------------------
# Sign images with Cosign (keyless OIDC)
# ---------------------------------------------------------------
sign:
name: Sign Images
needs: [manifest, scan]
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'

steps:
- name: Set image name lowercase
run: |
IMAGE_NAME_LC=$(echo "${{ env.IMAGE_NAME }}" | tr '[:upper:]' '[:lower:]')
echo "IMAGE_NAME_LC=${IMAGE_NAME_LC}" >> $GITHUB_ENV

- name: Install Cosign
uses: sigstore/cosign-installer@v3

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Sign multiplatform image
env:
COSIGN_EXPERIMENTAL: "1"
run: |
IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LC }}
SHA=${{ github.sha }}

echo "Signing ${IMAGE}:latest"
cosign sign --yes "${IMAGE}:latest"

echo "Signing ${IMAGE}:${SHA}"
cosign sign --yes "${IMAGE}:${SHA}"

echo "Images signed successfully"
30 changes: 18 additions & 12 deletions .github/workflows/docker-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,26 +102,32 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}

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

# ----------------------------------------------------------------
# Step 5 Tag the image with the semantic version tag
# Step 5 Create version tag from existing multiplatform manifest
# Note: For multiplatform images, we use 'docker buildx imagetools create'
# instead of 'docker pull' + 'docker tag' + 'docker push' because:
# 1. Multiplatform images are manifest lists, not single images
# 2. We create a new manifest that references the existing SHA-tagged image
# 3. This preserves all architecture variants (amd64, arm64, s390x)
# ----------------------------------------------------------------
- name: 🏷️ Tag image with version
- name: 🏷️ Create version tag for multiplatform manifest
run: |
IMAGE="ghcr.io/$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')"
docker tag "$IMAGE:${{ steps.meta.outputs.sha }}" \
"$IMAGE:${{ steps.meta.outputs.tag }}"
echo "Creating version tag ${{ steps.meta.outputs.tag }} from ${IMAGE}:${{ steps.meta.outputs.sha }}"
docker buildx imagetools create \
"${IMAGE}:${{ steps.meta.outputs.sha }}" \
--tag "${IMAGE}:${{ steps.meta.outputs.tag }}"

# ----------------------------------------------------------------
# Step 6 Push the new tag to GHCR
# Step 6 Verify the new version tag
# ----------------------------------------------------------------
- name: 🚀 Push new version tag
- name: 🔍 Verify version tag
run: |
IMAGE="ghcr.io/$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')"
docker push "$IMAGE:${{ steps.meta.outputs.tag }}"
echo "Inspecting ${IMAGE}:${{ steps.meta.outputs.tag }}"
docker buildx imagetools inspect "${IMAGE}:${{ steps.meta.outputs.tag }}"
3 changes: 2 additions & 1 deletion .github/workflows/ibm-cloud-code-engine.yml
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,12 @@ jobs:
ibmcloud ce project select --name "$CODE_ENGINE_PROJECT"

# -----------------------------------------------------------
# 5️⃣ Build & tag image (cache-aware)
# 5️⃣ Build & tag image (cache-aware, amd64 platform)
# -----------------------------------------------------------
- name: 🏗️ Build Docker image (with cache)
run: |
docker buildx build \
--platform linux/amd64 \
--file Containerfile.lite \
--tag "$REGISTRY_HOSTNAME/$ICR_NAMESPACE/$IMAGE_NAME:$IMAGE_TAG" \
--cache-from type=local,src=${{ env.CACHE_DIR }} \
Expand Down
Loading
Loading