341 lines
17 KiB
Bash
341 lines
17 KiB
Bash
#!/usr/bin/env bash
|
|
set -Eeuo pipefail
|
|
|
|
# Candidate-only mode emits a local SHA-scoped image and does not publish or deploy it.
|
|
MODE="legacy"
|
|
if [[ "$#" -gt 0 ]]; then
|
|
[[ "$#" -eq 1 && "$1" == "--release-candidate" ]] || {
|
|
echo "FATAL: supported mode is --release-candidate (or no arguments for legacy mode)" >&2
|
|
exit 64
|
|
}
|
|
MODE="release-candidate"
|
|
fi
|
|
|
|
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"
|
|
SECURITY_REQUIREMENTS="$ROOT/infrastructure/images/contextforge/security-requirements.txt"
|
|
CANDIDATE_RECIPE="$ROOT/scripts/contextforge_candidate_recipe.py"
|
|
|
|
for required in "$MANIFEST" "$CONTRACT" "$ENTRYPOINT" "$RUNTIME_VERIFIER" "$SECURITY_REQUIREMENTS"; 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}"
|
|
|
|
if [[ "$MODE" == "release-candidate" ]]; then
|
|
[[ -f "$CANDIDATE_RECIPE" ]] || {
|
|
echo "FATAL: the source-locked candidate recipe helper is missing" >&2
|
|
exit 66
|
|
}
|
|
# Validate every interpolated value before invoking Git, Docker, or Python.
|
|
# These fixed-format fields become Docker tags, Git comparisons, and labels.
|
|
: "${APPOPS_SOURCE_REPOSITORY:?missing APPOPS_SOURCE_REPOSITORY}"
|
|
: "${APPOPS_SOURCE_REVISION:?missing APPOPS_SOURCE_REVISION}"
|
|
: "${APPOPS_SOURCE_TREE:?missing APPOPS_SOURCE_TREE}"
|
|
: "${APPOPS_BUILD_ID:?missing APPOPS_BUILD_ID}"
|
|
: "${APPOPS_BUILD_DATE:?missing APPOPS_BUILD_DATE}"
|
|
: "${SOURCE_DATE_EPOCH:?missing SOURCE_DATE_EPOCH}"
|
|
: "${GITEA_REPOSITORY:?missing GITEA_REPOSITORY}"
|
|
: "${GITEA_COMMIT_SHA:?missing GITEA_COMMIT_SHA}"
|
|
|
|
[[ "$APPOPS_SOURCE_REPOSITORY" == "jens/itworx_mcp_hub" ]] || {
|
|
echo "FATAL: release-candidate source repository is not the protected Hub repository" >&2
|
|
exit 64
|
|
}
|
|
[[ "$GITEA_REPOSITORY" == "$APPOPS_SOURCE_REPOSITORY" ]] || {
|
|
echo "FATAL: Gitea and AppOps repositories disagree" >&2
|
|
exit 64
|
|
}
|
|
[[ "$APPOPS_SOURCE_REVISION" =~ ^[0-9a-f]{40}$ ]] || {
|
|
echo "FATAL: APPOPS_SOURCE_REVISION must be a full lowercase Git SHA" >&2
|
|
exit 64
|
|
}
|
|
[[ "$GITEA_COMMIT_SHA" == "$APPOPS_SOURCE_REVISION" ]] || {
|
|
echo "FATAL: Gitea and AppOps revisions disagree" >&2
|
|
exit 64
|
|
}
|
|
[[ "$APPOPS_SOURCE_TREE" =~ ^[0-9a-f]{40}$ ]] || {
|
|
echo "FATAL: APPOPS_SOURCE_TREE must be a full lowercase Git tree SHA" >&2
|
|
exit 64
|
|
}
|
|
[[ "$APPOPS_BUILD_ID" == "${APPOPS_SOURCE_REVISION:0:12}" ]] || {
|
|
echo "FATAL: APPOPS_BUILD_ID must be the first 12 revision characters" >&2
|
|
exit 64
|
|
}
|
|
[[ "$APPOPS_BUILD_DATE" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$ ]] || {
|
|
echo "FATAL: APPOPS_BUILD_DATE must be canonical UTC with millisecond precision" >&2
|
|
exit 64
|
|
}
|
|
[[ "$SOURCE_DATE_EPOCH" =~ ^[1-9][0-9]{0,11}$ ]] || {
|
|
echo "FATAL: SOURCE_DATE_EPOCH must be a canonical positive integer of at most 12 digits" >&2
|
|
exit 64
|
|
}
|
|
[[ "$CONTEXTFORGE_LOCAL_IMAGE" == "itworx-mcp-hub/contextforge-schema:12d4a0c7789c" ]] || {
|
|
echo "FATAL: compatibility manifest image name is not the reviewed Hub image" >&2
|
|
exit 64
|
|
}
|
|
fi
|
|
|
|
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; }
|
|
|
|
if [[ "$MODE" == "release-candidate" ]]; then
|
|
python3 - "$APPOPS_BUILD_DATE" <<'PY'
|
|
from datetime import datetime
|
|
import sys
|
|
|
|
try:
|
|
datetime.strptime(sys.argv[1], "%Y-%m-%dT%H:%M:%S.%fZ")
|
|
except ValueError as exc:
|
|
raise SystemExit("FATAL: APPOPS_BUILD_DATE is not a valid UTC calendar timestamp") from exc
|
|
PY
|
|
fi
|
|
|
|
TMP="$(mktemp -d)"
|
|
trap 'rm -rf "$TMP"' EXIT
|
|
SRC="$TMP/contextforge"
|
|
if [[ "$MODE" == "release-candidate" ]]; then
|
|
CANDIDATE_IMAGE="itworx-mcp-hub/contextforge-schema:${APPOPS_SOURCE_REVISION}"
|
|
BASE_IMAGE="itworx-mcp-hub/contextforge-schema:${APPOPS_SOURCE_REVISION}-upstream"
|
|
STAGED_IMAGE="itworx-mcp-hub/contextforge-schema:${APPOPS_SOURCE_REVISION}-staged"
|
|
else
|
|
BASE_IMAGE="${CONTEXTFORGE_LOCAL_IMAGE}-upstream"
|
|
STAGED_IMAGE="${CONTEXTFORGE_LOCAL_IMAGE}-staged"
|
|
fi
|
|
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")"
|
|
SECURITY_REQUIREMENTS_SHA256="$(sha256_file "$SECURITY_REQUIREMENTS")"
|
|
|
|
[[ "$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"
|
|
|
|
if [[ "$MODE" == "release-candidate" ]]; then
|
|
[[ -z "$(git -C "$ROOT" status --porcelain --untracked-files=all)" ]] || fail "Hub source checkout has changes"
|
|
[[ "$(git -C "$ROOT" rev-parse HEAD)" == "$APPOPS_SOURCE_REVISION" ]] || fail "checked-out Hub revision differs from frozen release metadata"
|
|
[[ "$(git -C "$ROOT" rev-parse 'HEAD^{tree}')" == "$APPOPS_SOURCE_TREE" ]] || fail "checked-out Hub source tree differs from frozen release metadata"
|
|
[[ "$(git -C "$ROOT" show -s --format=%ct HEAD)" == "$SOURCE_DATE_EPOCH" ]] || fail "source date epoch differs from frozen release metadata"
|
|
|
|
ensure_candidate_tag_absent() {
|
|
local image="$1"
|
|
local diagnostic="$TMP/docker-image-inspect.err"
|
|
if docker image inspect "$image" >/dev/null 2>"$diagnostic"; then
|
|
fail "release-candidate image tag already exists: $image"
|
|
fi
|
|
local error_line
|
|
IFS= read -r error_line < "$diagnostic" || true
|
|
case "$error_line" in
|
|
*"No such image"*|*"no such image"*|*"No such object"*|*"no such object"*|*"reference does not exist"*) ;;
|
|
*) fail "could not prove release-candidate image tag is absent: $image" ;;
|
|
esac
|
|
}
|
|
|
|
# Never build over or retag an existing revision-scoped image. Docker errors
|
|
# other than its explicit not-found response fail closed.
|
|
ensure_candidate_tag_absent "$BASE_IMAGE"
|
|
ensure_candidate_tag_absent "$STAGED_IMAGE"
|
|
ensure_candidate_tag_absent "$CANDIDATE_IMAGE"
|
|
|
|
fi
|
|
|
|
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 ==="
|
|
if [[ "$MODE" == "release-candidate" ]]; then
|
|
python3 "$CANDIDATE_RECIPE" prepare --source-dir "$SRC"
|
|
BUILD_ARG_FILE="$TMP/contextforge-build-args.txt"
|
|
python3 "$CANDIDATE_RECIPE" build-args > "$BUILD_ARG_FILE"
|
|
mapfile -t CANDIDATE_BUILD_ARGS < "$BUILD_ARG_FILE"
|
|
[[ "${#CANDIDATE_BUILD_ARGS[@]}" -eq 9 ]] || fail "candidate base/build argument contract is incomplete"
|
|
CANDIDATE_DOCKER_ARGS=()
|
|
for build_arg in "${CANDIDATE_BUILD_ARGS[@]}"; do
|
|
[[ "$build_arg" =~ ^[A-Z_]+=([A-Za-z0-9./:@_-]+)$ ]] || fail "candidate build argument is malformed"
|
|
CANDIDATE_DOCKER_ARGS+=(--build-arg "$build_arg")
|
|
done
|
|
docker build --pull=false --platform linux/amd64 "${CANDIDATE_DOCKER_ARGS[@]}" -f "$SRC/Containerfile.itworx-candidate" -t "$BASE_IMAGE" "$SRC"
|
|
else
|
|
docker build --pull=false -f "$SRC/Containerfile" -t "$BASE_IMAGE" "$SRC"
|
|
fi
|
|
|
|
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"
|
|
cp "$SECURITY_REQUIREMENTS" "$TMP/contextforge-security-requirements.txt"
|
|
|
|
{
|
|
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 'COPY contextforge-security-requirements.txt /opt/itworx/contextforge-security-requirements.txt'
|
|
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 'LABEL io.itworx.contextforge.security-requirements-sha256="%s"\n' "$SECURITY_REQUIREMENTS_SHA256"
|
|
if [[ "$MODE" == "release-candidate" ]]; then
|
|
printf 'LABEL org.opencontainers.image.source="https://gitea.example.com/jens/itworx-mcp-hub" \\\n'
|
|
printf ' org.opencontainers.image.revision="%s" \\\n' "$APPOPS_SOURCE_REVISION"
|
|
printf ' org.opencontainers.image.source-tree="%s" \\\n' "$APPOPS_SOURCE_TREE"
|
|
printf ' org.opencontainers.image.version="%s" \\\n' "$APPOPS_BUILD_ID"
|
|
printf ' org.opencontainers.image.source-date-epoch="%s" \\\n' "$SOURCE_DATE_EPOCH"
|
|
printf ' org.opencontainers.image.created="%s"\n' "$APPOPS_BUILD_DATE"
|
|
fi
|
|
if [[ "$MODE" == "release-candidate" ]]; then
|
|
echo 'RUN /app/.venv/bin/python -c "import importlib.metadata as md; import importlib.util; from mcp import McpError; assert md.version(\"mcp\") == \"1.30.0\"; assert md.version(\"PyJWT\") == \"2.15.0\"; assert md.version(\"urllib3\") == \"2.8.0\"; assert all(importlib.util.find_spec(name) is None for name in (\"pip\", \"setuptools\", \"wheel\", \"uv\"))"'
|
|
else
|
|
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\")"'
|
|
echo 'RUN /app/.venv/bin/python -m pip install --no-cache-dir --no-deps --require-hashes --only-binary=:all: -r /opt/itworx/contextforge-security-requirements.txt \'
|
|
echo ' && /app/.venv/bin/python -m pip check \'
|
|
echo ' && /app/.venv/bin/python -c "import importlib.metadata as md; assert md.version(\"PyJWT\") == \"2.15.0\"; assert md.version(\"urllib3\") == \"2.8.0\""'
|
|
fi
|
|
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"
|
|
[[ "$(embedded_sha /opt/itworx/contextforge-security-requirements.txt)" == "$SECURITY_REQUIREMENTS_SHA256" ]] || fail "embedded security requirements 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"
|
|
[[ "$(label_value io.itworx.contextforge.security-requirements-sha256)" == "$SECURITY_REQUIREMENTS_SHA256" ]] || fail "security requirements label mismatch"
|
|
if [[ "$MODE" == "release-candidate" ]]; then
|
|
[[ "$(label_value org.opencontainers.image.source)" == "https://gitea.example.com/jens/itworx-mcp-hub" ]] || fail "OCI source label mismatch"
|
|
[[ "$(label_value org.opencontainers.image.revision)" == "$APPOPS_SOURCE_REVISION" ]] || fail "OCI revision label mismatch"
|
|
[[ "$(label_value org.opencontainers.image.source-tree)" == "$APPOPS_SOURCE_TREE" ]] || fail "OCI source tree label mismatch"
|
|
[[ "$(label_value org.opencontainers.image.version)" == "$APPOPS_BUILD_ID" ]] || fail "OCI version label mismatch"
|
|
[[ "$(label_value org.opencontainers.image.source-date-epoch)" == "$SOURCE_DATE_EPOCH" ]] || fail "OCI source date epoch label mismatch"
|
|
[[ "$(label_value org.opencontainers.image.created)" == "$APPOPS_BUILD_DATE" ]] || fail "OCI created label mismatch"
|
|
fi
|
|
|
|
echo "MCP SDK : $CONTEXTFORGE_MCP_SDK"
|
|
echo "Alembic head : $IMAGE_HEAD"
|
|
echo "Contract SHA : $CONTRACT_SHA256"
|
|
|
|
if [[ "$MODE" == "release-candidate" ]]; then
|
|
# Recheck immediately before publishing. There is intentionally no cleanup or
|
|
# retagging of any static compatibility image in candidate mode.
|
|
ensure_candidate_tag_absent "$CANDIDATE_IMAGE"
|
|
docker tag "$STAGED_IMAGE" "$CANDIDATE_IMAGE"
|
|
else
|
|
docker tag "$STAGED_IMAGE" "$CONTEXTFORGE_LOCAL_IMAGE"
|
|
fi
|
|
|
|
echo
|
|
echo "======================================================"
|
|
if [[ "$MODE" == "release-candidate" ]]; then
|
|
echo " CANDIDATE IMAGE BUILT AND VERIFIED (NOT APPROVED OR DEPLOYED)"
|
|
else
|
|
echo " SUCCESS: CONTEXTFORGE COMPATIBILITY IMAGE VERIFIED"
|
|
fi
|
|
echo " source commit : $CONTEXTFORGE_UPSTREAM_COMMIT"
|
|
echo " Alembic head : $CONTEXTFORGE_ALEMBIC_HEAD"
|
|
echo " MCP SDK : $CONTEXTFORGE_MCP_SDK"
|
|
echo " contract SHA : $CONTRACT_SHA256"
|
|
if [[ "$MODE" == "release-candidate" ]]; then
|
|
echo " candidate tag : $CANDIDATE_IMAGE"
|
|
else
|
|
echo " local image : $CONTEXTFORGE_LOCAL_IMAGE"
|
|
fi
|
|
echo "======================================================"
|