# ===============================================================
# ☁️ ContextForge ▸ Build, Cache & Deploy to IBM Code Engine
# ===============================================================
#
# This workflow:
# - Restores / updates a local **BuildKit layer cache** ❄️
# - Builds the Docker image from **Containerfile.lite** 🏗️
# - Pushes the image to **IBM Container Registry (ICR)** 📤
# - Creates / updates an **IBM Cloud Code Engine** app 🚀
#
# ---------------------------------------------------------------
# Required repository **secrets** (environment: production)
# ---------------------------------------------------------------
# ┌──────────────────────────────────┬────────────────────────┐
# │ Secret name │ Purpose │
# ├──────────────────────────────────┼────────────────────────┤
# │ IBM_CLOUD_API_KEY │ IBM Cloud IAM API key │
# │ CF_JWT_SECRET_KEY │ JWT signing key │
# │ CF_BASIC_AUTH_PASSWORD │ Admin UI password │
# │ CF_AUTH_ENCRYPTION_SECRET │ Stored-secret cipher │
# │ CF_PLATFORM_ADMIN_PASSWORD │ Bootstrap admin pass │
# │ CF_DEFAULT_USER_PASSWORD │ Default user pass │
# └──────────────────────────────────┴────────────────────────┘
#
# ---------------------------------------------------------------
# Required repository **variables** (environment: production)
# ---------------------------------------------------------------
# ┌────────────────────────────┬──────────────────────────────┐
# │ Variable name │ Example value │
# ├────────────────────────────┼──────────────────────────────┤
# │ IBM_CLOUD_REGION │ us-south │
# │ REGISTRY_HOSTNAME │ us.icr.io │
# │ ICR_NAMESPACE │ myspace │
# │ APP_NAME │ mcpgateway │
# │ CODE_ENGINE_PROJECT │ my-ce-project │
# │ CODE_ENGINE_REGISTRY_SECRET│ my-registry-secret │
# │ CODE_ENGINE_PORT │ "4444" │
# └────────────────────────────┴──────────────────────────────┘
# * Note: CODE_ENGINE_REGISTRY_SECRET is the name of the CE
# registry pull secret, not the secret value itself.
# Triggers:
# - Every push to `main`
# - Syncs the Code Engine secret `mcpgateway-dev` from
# `.env.example` + GitHub Secrets (merge; never logged).
# ---------------------------------------------------------------
name: Deploy to IBM Code Engine
on:
push:
branches: ["main"]
# -----------------------------------------------------------------
# Minimal permissions (Principle of Least Privilege)
# -----------------------------------------------------------------
permissions:
contents: read
# -----------------------------------------------------------------
# Global environment (secrets & variables)
# -----------------------------------------------------------------
env:
# Build metadata
GITHUB_SHA: ${{ github.sha }}
CACHE_DIR: /tmp/.buildx-cache # BuildKit layer cache dir
# IBM Cloud region (variable)
IBM_CLOUD_REGION: ${{ vars.IBM_CLOUD_REGION }}
# Registry coordinates (variables)
REGISTRY_HOSTNAME: ${{ vars.REGISTRY_HOSTNAME }}
ICR_NAMESPACE: ${{ vars.ICR_NAMESPACE }}
# Image / app naming (variables)
IMAGE_NAME: ${{ vars.APP_NAME }}
IMAGE_TAG: ${{ github.sha }}
# Code Engine deployment (variables)
CODE_ENGINE_APP_NAME: ${{ vars.APP_NAME }}
CODE_ENGINE_PROJECT: ${{ vars.CODE_ENGINE_PROJECT }}
CODE_ENGINE_REGISTRY_SECRET: ${{ vars.CODE_ENGINE_REGISTRY_SECRET }}
PORT: ${{ vars.CODE_ENGINE_PORT }}
jobs:
build-push-deploy:
name: 🚀 Build, Cache, Push & Deploy
runs-on: ubuntu-latest
environment: production
steps:
# -----------------------------------------------------------
# 0️⃣ Checkout repository
# -----------------------------------------------------------
- name: ⬇️ Checkout source
uses: actions/checkout@v5
# -----------------------------------------------------------
# 1️⃣ Validate required secrets & variables
# -----------------------------------------------------------
- name: ✅ Validate configuration
env:
CF_IBM_CLOUD_API_KEY: ${{ secrets.IBM_CLOUD_API_KEY }}
CF_JWT_SECRET_KEY: ${{ secrets.CF_JWT_SECRET_KEY }}
CF_BASIC_AUTH_PASSWORD: ${{ secrets.CF_BASIC_AUTH_PASSWORD }}
CF_AUTH_ENCRYPTION_SECRET: ${{ secrets.CF_AUTH_ENCRYPTION_SECRET }}
CF_PLATFORM_ADMIN_PASSWORD: ${{ secrets.CF_PLATFORM_ADMIN_PASSWORD }}
CF_DEFAULT_USER_PASSWORD: ${{ secrets.CF_DEFAULT_USER_PASSWORD }}
run: |
missing=()
placeholder=()
for var in CF_IBM_CLOUD_API_KEY CF_JWT_SECRET_KEY CF_BASIC_AUTH_PASSWORD \
CF_AUTH_ENCRYPTION_SECRET CF_PLATFORM_ADMIN_PASSWORD \
CF_DEFAULT_USER_PASSWORD; do
val="${!var}"
if [ -z "$val" ]; then
missing+=("$var")
elif [ "$val" = "-" ] || [ "$val" = "changeme" ] || [ "$val" = "CHANGE_ME" ]; then
placeholder+=("$var")
fi
done
for var in IBM_CLOUD_REGION REGISTRY_HOSTNAME ICR_NAMESPACE \
IMAGE_NAME CODE_ENGINE_APP_NAME CODE_ENGINE_PROJECT \
CODE_ENGINE_REGISTRY_SECRET PORT; do
if [ -z "${!var}" ]; then
missing+=("$var")
fi
done
if [ ${#missing[@]} -gt 0 ]; then
echo "::error::Required secrets/variables are empty or unconfigured: ${missing[*]}"
exit 1
fi
if [ ${#placeholder[@]} -gt 0 ]; then
echo "::error::Secrets contain placeholder values (e.g. '-', 'changeme'): ${placeholder[*]}"
echo "::error::Update these in GitHub → Settings → Environments → production → Secrets"
exit 1
fi
echo "All required secrets and variables are present."
# -----------------------------------------------------------
# 2️⃣ Set up Docker Buildx
# -----------------------------------------------------------
- name: 🛠️ Set up Docker Buildx
uses: docker/setup-buildx-action@v3.11.1
# -----------------------------------------------------------
# 3️⃣ Restore BuildKit layer cache
# -----------------------------------------------------------
- name: 🔄 Restore BuildKit cache
uses: actions/cache@v4
with:
path: ${{ env.CACHE_DIR }}
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: ${{ runner.os }}-buildx-
# -----------------------------------------------------------
# 4️⃣ Install IBM Cloud CLI + plugins & login
# -----------------------------------------------------------
- name: 🧰 Install IBM Cloud CLI
uses: IBM/actions-ibmcloud-cli@v1
with:
api_key: ${{ secrets.IBM_CLOUD_API_KEY }}
region: ${{ vars.IBM_CLOUD_REGION }}
plugins: container-registry, code-engine
# -----------------------------------------------------------
# 5️⃣ Authenticate to IBM Cloud Code Engine project
# -----------------------------------------------------------
- name: 🔐 IBM Cloud Code Engine login
run: |
ibmcloud cr region-set "$IBM_CLOUD_REGION"
ibmcloud cr login
ibmcloud ce project select --name "$CODE_ENGINE_PROJECT"
# -----------------------------------------------------------
# 6️⃣ 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 }} \
--cache-to type=local,dest=${{ env.CACHE_DIR }},mode=max \
--load \
.
# -----------------------------------------------------------
# 7️⃣ Push image to IBM Container Registry
# -----------------------------------------------------------
- name: 📤 Push image to ICR
run: |
docker push "$REGISTRY_HOSTNAME/$ICR_NAMESPACE/$IMAGE_NAME:$IMAGE_TAG"
# -----------------------------------------------------------
# 8️⃣ Build .env from template + GitHub Secrets, sync to CE
# -----------------------------------------------------------
- name: 🔑 Sync Code Engine secrets
env:
CF_JWT_SECRET_KEY: ${{ secrets.CF_JWT_SECRET_KEY }}
CF_BASIC_AUTH_PASSWORD: ${{ secrets.CF_BASIC_AUTH_PASSWORD }}
CF_AUTH_ENCRYPTION_SECRET: ${{ secrets.CF_AUTH_ENCRYPTION_SECRET }}
CF_PLATFORM_ADMIN_PASSWORD: ${{ secrets.CF_PLATFORM_ADMIN_PASSWORD }}
CF_DEFAULT_USER_PASSWORD: ${{ secrets.CF_DEFAULT_USER_PASSWORD }}
run: |
# Ensure .env.deploy is removed on exit (success or failure)
trap 'rm -f .env.deploy' EXIT
# Build .env from .env.example, replacing secret placeholders.
# Uses Python to avoid shell quoting issues (sed delimiter
# collisions with |, &, \) and grep exit-code edge cases.
#
# IMPORTANT: Output ONLY uncommented KEY=VALUE lines.
# CE's --from-env-file silently skips updates when the file
# contains comments, blank lines, or section headers from
# .env.example. Stripping to clean KEY=VALUE lines fixes this.
python3 -c "
import os, sys
replacements = {
'JWT_SECRET_KEY': os.environ['CF_JWT_SECRET_KEY'],
'BASIC_AUTH_PASSWORD': os.environ['CF_BASIC_AUTH_PASSWORD'],
'AUTH_ENCRYPTION_SECRET': os.environ['CF_AUTH_ENCRYPTION_SECRET'],
'PLATFORM_ADMIN_PASSWORD': os.environ['CF_PLATFORM_ADMIN_PASSWORD'],
'DEFAULT_USER_PASSWORD': os.environ['CF_DEFAULT_USER_PASSWORD'],
}
replaced = set()
with open('.env.example') as f:
lines = f.readlines()
with open('.env.deploy', 'w') as out:
for line in lines:
stripped = line.strip()
# Skip comments and blank lines — CE --from-env-file
# needs a clean KEY=VALUE-only file to reliably update.
if not stripped or stripped.startswith('#'):
continue
if '=' not in stripped:
continue
key = stripped.split('=', 1)[0]
if key in replacements:
val = replacements[key]
if '\n' in val or '\r' in val:
print(f'::error::Secret for {key} contains embedded newlines')
raise SystemExit(1)
out.write(f'{key}={val}\n')
replaced.add(key)
else:
out.write(f'{stripped}\n')
missing = set(replacements) - replaced
if missing:
print(f'::error::Keys not found in .env.example: {missing}')
print('Hint: check that .env.example uses KEY=value format (no spaces around =)')
sys.exit(1)
"
echo "✅ Generated .env.deploy ($(wc -l < .env.deploy) KEY=VALUE lines)"
# Update the secret if it exists, otherwise create it.
if ibmcloud ce secret get --name mcpgateway-dev > /dev/null 2>&1; then
ibmcloud ce secret update --name mcpgateway-dev --from-env-file .env.deploy
else
ibmcloud ce secret create --name mcpgateway-dev --format generic --from-env-file .env.deploy
fi
# Belt-and-suspenders: explicitly set each secret with --from-literal
# to guarantee overwrite regardless of --from-env-file parsing quirks.
ibmcloud ce secret update --name mcpgateway-dev \
--from-literal "JWT_SECRET_KEY=$CF_JWT_SECRET_KEY" \
--from-literal "BASIC_AUTH_PASSWORD=$CF_BASIC_AUTH_PASSWORD" \
--from-literal "AUTH_ENCRYPTION_SECRET=$CF_AUTH_ENCRYPTION_SECRET" \
--from-literal "PLATFORM_ADMIN_PASSWORD=$CF_PLATFORM_ADMIN_PASSWORD" \
--from-literal "DEFAULT_USER_PASSWORD=$CF_DEFAULT_USER_PASSWORD"
echo "✅ Code Engine secret 'mcpgateway-dev' synced"
# Verify the 5 critical secrets are no longer placeholders
echo "🔍 Verifying secret values were updated..."
for key in JWT_SECRET_KEY BASIC_AUTH_PASSWORD AUTH_ENCRYPTION_SECRET \
PLATFORM_ADMIN_PASSWORD DEFAULT_USER_PASSWORD; do
val=$(ibmcloud ce secret get --name mcpgateway-dev --decode -o json 2>/dev/null \
| python3 -c "import json,sys; print(json.load(sys.stdin).get('data',{}).get('$key',''))" 2>/dev/null || echo "")
if [ "$val" = "-" ] || [ -z "$val" ]; then
echo "::error::Secret key $key still has placeholder value after update"
exit 1
fi
done
echo "✅ All 5 secrets verified (no placeholders)"
# -----------------------------------------------------------
# 9️⃣ Deploy (create or update) Code Engine application
# -----------------------------------------------------------
- name: 🚀 Deploy to Code Engine
run: |
if ibmcloud ce application get --name "$CODE_ENGINE_APP_NAME" > /dev/null 2>&1; then
echo "🔁 Updating existing application..."
ibmcloud ce application update \
--name "$CODE_ENGINE_APP_NAME" \
--image "$REGISTRY_HOSTNAME/$ICR_NAMESPACE/$IMAGE_NAME:$IMAGE_TAG" \
--registry-secret "$CODE_ENGINE_REGISTRY_SECRET" \
--env-from-secret mcpgateway-dev \
--cpu 1 --memory 2G
else
echo "🆕 Creating new application..."
ibmcloud ce application create \
--name "$CODE_ENGINE_APP_NAME" \
--image "$REGISTRY_HOSTNAME/$ICR_NAMESPACE/$IMAGE_NAME:$IMAGE_TAG" \
--registry-secret "$CODE_ENGINE_REGISTRY_SECRET" \
--port "$PORT" \
--env-from-secret mcpgateway-dev \
--cpu 1 --memory 2G
fi
# -----------------------------------------------------------
# 🔟 Show deployment status
# -----------------------------------------------------------
- name: 📈 Display deployment status
run: ibmcloud ce application get --name "$CODE_ENGINE_APP_NAME"