Files
ITWorx_MCP_Hub-Public/scripts/build_contextforge_compat_image.sh
T

171 lines
8.0 KiB
Bash

#!/usr/bin/env bash
set -Eeuo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
MANIFEST="$ROOT/infrastructure/images/contextforge/compat-image.env"
CONTRACT="$ROOT/infrastructure/images/contextforge/runtime-contract.json"
ENTRYPOINT="$ROOT/infrastructure/images/contextforge/entrypoint.sh"
RUNTIME_VERIFIER="$ROOT/scripts/contextforge_runtime_contract.py"
for required in "$MANIFEST" "$CONTRACT" "$ENTRYPOINT" "$RUNTIME_VERIFIER"; do
[[ -f "$required" ]] || {
echo "FATAL: required ContextForge compatibility input is missing: $required" >&2
exit 66
}
done
# shellcheck source=/dev/null
source "$MANIFEST"
: "${CONTEXTFORGE_UPSTREAM_COMMIT:?missing upstream commit}"
: "${CONTEXTFORGE_ALEMBIC_HEAD:?missing Alembic head}"
: "${CONTEXTFORGE_MCP_SDK:?missing MCP SDK version}"
: "${CONTEXTFORGE_LOCAL_IMAGE:?missing local output image}"
command -v git >/dev/null 2>&1 || { echo "FATAL: git is required" >&2; exit 69; }
command -v docker >/dev/null 2>&1 || { echo "FATAL: docker is required" >&2; exit 69; }
command -v python3 >/dev/null 2>&1 || { echo "FATAL: python3 is required" >&2; exit 69; }
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
SRC="$TMP/contextforge"
BASE_IMAGE="${CONTEXTFORGE_LOCAL_IMAGE}-upstream"
STAGED_IMAGE="${CONTEXTFORGE_LOCAL_IMAGE}-staged"
REVISION_PREFIX="${CONTEXTFORGE_ALEMBIC_HEAD}_"
MCP_REQUIREMENT="mcp==${CONTEXTFORGE_MCP_SDK}"
fail() {
echo "FATAL: $*" >&2
exit 70
}
sha256_file() {
python3 - "$1" <<'PY'
import hashlib
import sys
from pathlib import Path
print(hashlib.sha256(Path(sys.argv[1]).read_bytes()).hexdigest())
PY
}
read_contract() {
python3 - "$CONTRACT" "$1" <<'PY'
import json
import sys
from pathlib import Path
payload = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
value = payload
for part in sys.argv[2].split("."):
value = value[part]
if not isinstance(value, str) or not value:
raise SystemExit(2)
print(value)
PY
}
CONTRACT_SOURCE_COMMIT="$(read_contract sourceCommit)"
CONTRACT_ALEMBIC_HEAD="$(read_contract alembicHead)"
CONTRACT_MCP_SDK="$(read_contract mcpSdk)"
CONTRACT_REQUIRED_SYMBOL="$(read_contract requiredSymbol)"
CONTRACT_SHA256="$(sha256_file "$CONTRACT")"
ENTRYPOINT_SHA256="$(sha256_file "$ENTRYPOINT")"
VERIFIER_SHA256="$(sha256_file "$RUNTIME_VERIFIER")"
[[ "$CONTRACT_SOURCE_COMMIT" == "$CONTEXTFORGE_UPSTREAM_COMMIT" ]] || fail "contract source commit disagrees with compatibility manifest"
[[ "$CONTRACT_ALEMBIC_HEAD" == "$CONTEXTFORGE_ALEMBIC_HEAD" ]] || fail "contract Alembic head disagrees with compatibility manifest"
[[ "$CONTRACT_MCP_SDK" == "$CONTEXTFORGE_MCP_SDK" ]] || fail "contract MCP SDK disagrees with compatibility manifest"
[[ "$CONTRACT_REQUIRED_SYMBOL" == "McpError" ]] || fail "unsupported required MCP symbol: $CONTRACT_REQUIRED_SYMBOL"
echo "=== FETCH EXACT CONTEXTFORGE SOURCE ==="
git init -q "$SRC"
git -C "$SRC" remote add origin https://github.com/IBM/mcp-context-forge.git
git -C "$SRC" fetch --depth 1 origin "$CONTEXTFORGE_UPSTREAM_COMMIT"
git -C "$SRC" checkout --detach FETCH_HEAD
ACTUAL_COMMIT="$(git -C "$SRC" rev-parse HEAD)"
[[ "$ACTUAL_COMMIT" == "$CONTEXTFORGE_UPSTREAM_COMMIT" ]] || fail "source commit mismatch: expected $CONTEXTFORGE_UPSTREAM_COMMIT, got $ACTUAL_COMMIT"
MIGRATION_FILE="$({
find "$SRC/mcpgateway/alembic/versions" -maxdepth 1 -type f -name "${REVISION_PREFIX}*.py" -print -quit
} 2>/dev/null || true)"
[[ -n "$MIGRATION_FILE" ]] || fail "migration ${REVISION_PREFIX}*.py is absent from source commit"
[[ -f "$SRC/Containerfile" ]] || fail "upstream Containerfile is missing"
echo "source commit : $ACTUAL_COMMIT"
echo "migration : ${MIGRATION_FILE#$SRC/}"
echo "contract sha : $CONTRACT_SHA256"
echo
echo "=== BUILD UPSTREAM SOURCE IMAGE ==="
docker build --pull=false -f "$SRC/Containerfile" -t "$BASE_IMAGE" "$SRC"
BASE_USER="$(docker image inspect -f '{{.Config.User}}' "$BASE_IMAGE")"
LAYER="$TMP/Containerfile.compat"
cp "$CONTRACT" "$TMP/contextforge-runtime-contract.json"
cp "$ENTRYPOINT" "$TMP/contextforge-entrypoint.sh"
cp "$RUNTIME_VERIFIER" "$TMP/contextforge-runtime-contract.py"
{
echo "FROM $BASE_IMAGE"
echo "USER root"
echo 'COPY contextforge-runtime-contract.json /opt/itworx/contextforge-runtime-contract.json'
echo 'COPY contextforge-entrypoint.sh /opt/itworx/contextforge-entrypoint.sh'
echo 'COPY contextforge-runtime-contract.py /opt/itworx/contextforge-runtime-contract.py'
echo 'RUN chmod 0555 /opt/itworx/contextforge-entrypoint.sh /opt/itworx/contextforge-runtime-contract.py'
printf 'LABEL io.itworx.contextforge.contract-sha256="%s" \\\n' "$CONTRACT_SHA256"
printf ' io.itworx.contextforge.entrypoint-sha256="%s" \\\n' "$ENTRYPOINT_SHA256"
printf ' io.itworx.contextforge.verifier-sha256="%s" \\\n' "$VERIFIER_SHA256"
printf ' io.itworx.contextforge.source-commit="%s" \\\n' "$CONTEXTFORGE_UPSTREAM_COMMIT"
printf ' io.itworx.contextforge.alembic-head="%s" \\\n' "$CONTEXTFORGE_ALEMBIC_HEAD"
printf ' io.itworx.contextforge.mcp-sdk="%s"\n' "$CONTEXTFORGE_MCP_SDK"
printf 'RUN /app/.venv/bin/python -m pip install --no-cache-dir --upgrade %q \\\n' "$MCP_REQUIREMENT"
printf '%s\n' ' && /app/.venv/bin/python -c "import importlib.metadata as md; from mcp import McpError; assert md.version(\"mcp\") == \"'"${CONTEXTFORGE_MCP_SDK}"'\"; print(\"MCP SDK\", md.version(\"mcp\"), \"McpError OK\")"'
if [[ -n "$BASE_USER" ]]; then
printf 'USER %s\n' "$BASE_USER"
fi
} > "$LAYER"
echo
echo "=== LAYER EXACT MCP SDK AND CONTRACT ==="
docker build --pull=false -f "$LAYER" -t "$STAGED_IMAGE" "$TMP"
echo
echo "=== VERIFY RUNTIME CONTRACT ==="
docker run --rm --entrypoint /bin/sh "$STAGED_IMAGE" -lc '/app/.venv/bin/python -c "import importlib.metadata as md; from mcp import McpError; assert md.version(\"mcp\") == \"'"$CONTEXTFORGE_MCP_SDK"'\"; print(\"McpError OK\")"'
IMAGE_HEAD="$(docker run --rm --entrypoint /bin/sh "$STAGED_IMAGE" -lc 'cd /app && /app/.venv/bin/alembic -c mcpgateway/alembic.ini heads' | awk 'NR == 1 {print $1}')"
[[ "$IMAGE_HEAD" == "$CONTEXTFORGE_ALEMBIC_HEAD" ]] || fail "Alembic head mismatch: expected $CONTEXTFORGE_ALEMBIC_HEAD, got ${IMAGE_HEAD:-empty}"
embedded_sha() {
docker run --rm --entrypoint /app/.venv/bin/python "$STAGED_IMAGE" -c 'import hashlib,sys; from pathlib import Path; print(hashlib.sha256(Path(sys.argv[1]).read_bytes()).hexdigest())' "$1"
}
[[ "$(embedded_sha /opt/itworx/contextforge-runtime-contract.json)" == "$CONTRACT_SHA256" ]] || fail "embedded runtime contract digest mismatch"
[[ "$(embedded_sha /opt/itworx/contextforge-entrypoint.sh)" == "$ENTRYPOINT_SHA256" ]] || fail "embedded entrypoint digest mismatch"
[[ "$(embedded_sha /opt/itworx/contextforge-runtime-contract.py)" == "$VERIFIER_SHA256" ]] || fail "embedded verifier digest mismatch"
label_value() {
docker image inspect -f "{{ index .Config.Labels \"$1\" }}" "$STAGED_IMAGE"
}
[[ "$(label_value io.itworx.contextforge.contract-sha256)" == "$CONTRACT_SHA256" ]] || fail "contract label mismatch"
[[ "$(label_value io.itworx.contextforge.entrypoint-sha256)" == "$ENTRYPOINT_SHA256" ]] || fail "entrypoint label mismatch"
[[ "$(label_value io.itworx.contextforge.verifier-sha256)" == "$VERIFIER_SHA256" ]] || fail "verifier label mismatch"
[[ "$(label_value io.itworx.contextforge.source-commit)" == "$CONTEXTFORGE_UPSTREAM_COMMIT" ]] || fail "source label mismatch"
[[ "$(label_value io.itworx.contextforge.alembic-head)" == "$CONTEXTFORGE_ALEMBIC_HEAD" ]] || fail "Alembic label mismatch"
[[ "$(label_value io.itworx.contextforge.mcp-sdk)" == "$CONTEXTFORGE_MCP_SDK" ]] || fail "MCP SDK label mismatch"
echo "MCP SDK : $CONTEXTFORGE_MCP_SDK"
echo "Alembic head : $IMAGE_HEAD"
echo "Contract SHA : $CONTRACT_SHA256"
docker tag "$STAGED_IMAGE" "$CONTEXTFORGE_LOCAL_IMAGE"
echo
echo "======================================================"
echo " SUCCESS: CONTEXTFORGE COMPATIBILITY IMAGE VERIFIED"
echo " source commit : $CONTEXTFORGE_UPSTREAM_COMMIT"
echo " Alembic head : $CONTEXTFORGE_ALEMBIC_HEAD"
echo " MCP SDK : $CONTEXTFORGE_MCP_SDK"
echo " contract SHA : $CONTRACT_SHA256"
echo " local image : $CONTEXTFORGE_LOCAL_IMAGE"
echo "======================================================"