#!/usr/bin/env bash # rebuild-runtime-images.sh — Rebuild all 6 workspace runtime Docker images. # # Run this script from the repo root (or from workspace/) after any # change to workspace/Dockerfile, entrypoint.sh, or the git credential # helper scripts. Also run after PR #640 merged. # # What this does: # 1. Builds workspace-template:base from the monorepo Dockerfile (includes # the fixed entrypoint.sh + molecule-git-token-helper.sh) # 2. For each runtime adapter, clones its standalone repo to a temp dir, # patches its Dockerfile to: # a. COPY the git credential helper into the image # b. Set git config --system to register the helper globally # Then builds and tags workspace-template:. # # Why the patch is needed: # Standalone adapter images (molecule-ai-workspace-template-*) use # ENTRYPOINT ["molecule-runtime"] — they do not run entrypoint.sh, so the # git config registration from entrypoint.sh never fires for them. Baking # it into the image via git config --system at Docker build time is the # correct permanent fix (issue #613 / PR #640). # # Prerequisites: docker, git, gh (authenticated) # # Usage (from repo root): # bash workspace/rebuild-runtime-images.sh # # To rebuild a single runtime: # bash workspace/rebuild-runtime-images.sh claude-code # set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" HELPER_SCRIPT="${SCRIPT_DIR}/scripts/molecule-git-token-helper.sh" VALID_RUNTIMES=(langgraph claude-code openclaw crewai autogen deepagents) GREEN='\033[0;32m' YELLOW='\033[1;33m' RED='\033[0;31m' NC='\033[0m' log() { echo -e "${GREEN}[rebuild]${NC} $1"; } warn() { echo -e "${YELLOW}[rebuild]${NC} $1"; } err() { echo -e "${RED}[rebuild]${NC} $1"; } # ───────────────────────────────────────────────────── # Argument: optional single runtime to rebuild # Allowlist-validated: $1 must be one of VALID_RUNTIMES. # Prevents path traversal and unexpected Docker tag injection. # ───────────────────────────────────────────────────── if [ -n "${1:-}" ]; then valid=0 for v in "${VALID_RUNTIMES[@]}"; do [ "$1" = "$v" ] && valid=1 && break done if [ "${valid}" -eq 0 ]; then err "Unknown runtime '${1}'. Valid: ${VALID_RUNTIMES[*]}" exit 1 fi RUNTIMES=("$1") else RUNTIMES=("${VALID_RUNTIMES[@]}") fi # ───────────────────────────────────────────────────── # Preflight checks # ───────────────────────────────────────────────────── if ! command -v docker >/dev/null 2>&1; then err "docker not found — run this on the host machine, not inside a workspace container" exit 1 fi if [ ! -f "${HELPER_SCRIPT}" ]; then err "molecule-git-token-helper.sh not found at ${HELPER_SCRIPT}" err "Run: git pull origin main (PR #640 adds this file)" exit 1 fi log "Building workspace-template:base from monorepo Dockerfile..." docker build \ --no-cache \ -t workspace-template:base \ -f "${SCRIPT_DIR}/Dockerfile" \ "${SCRIPT_DIR}" log "✓ workspace-template:base built" # ───────────────────────────────────────────────────── # Build each runtime adapter image # ───────────────────────────────────────────────────── TMPBASE=$(mktemp -d) trap 'rm -rf "${TMPBASE}"' EXIT SUCCESS=() FAILED=() for runtime in "${RUNTIMES[@]}"; do log "──────────────────────────────────────────" log "Building workspace-template:${runtime} ..." RUNTIME_DIR="${TMPBASE}/${runtime}" mkdir -p "${RUNTIME_DIR}" # Clone the standalone template repo REPO="Molecule-AI/molecule-ai-workspace-template-${runtime}" log " Cloning ${REPO} ..." if ! git clone --depth 1 "https://github.com/${REPO}.git" "${RUNTIME_DIR}" 2>&1; then err " Failed to clone ${REPO} — skipping ${runtime}" FAILED+=("${runtime}") continue fi # Verify a Dockerfile exists if [ ! -f "${RUNTIME_DIR}/Dockerfile" ]; then err " No Dockerfile in ${REPO} — skipping ${runtime}" FAILED+=("${runtime}") continue fi # Copy the credential helper into the build context so the Dockerfile can COPY it. cp "${HELPER_SCRIPT}" "${RUNTIME_DIR}/molecule-git-token-helper.sh" # Patch the Dockerfile: # 1. COPY the helper script into the image at a predictable path # 2. git config --system registers it globally (applies to all users in the # container, survives the root→agent gosu handoff) # 3. Re-declare ENTRYPOINT last (safe — molecule-runtime entrypoint is # unchanged, just ensuring it's after our additions) # # We do NOT replace the ENTRYPOINT or CMD — molecule-runtime remains the # entry point. The git config --system baked into the image layer means # git will call the helper on every push/fetch without any startup script. cat >> "${RUNTIME_DIR}/Dockerfile" << 'PATCH' # ─── git credential helper (issue #613 / PR #640) ─────────────────────────── # Bake the credential helper into the image so git always has a fresh # GitHub App token. git config --system writes to /etc/gitconfig which is # inherited by all users (root → agent gosu handoff). No startup script change # needed — git invokes this helper automatically on push/fetch. COPY molecule-git-token-helper.sh /usr/local/bin/molecule-git-credential-helper RUN chmod +x /usr/local/bin/molecule-git-credential-helper && \ git config --system credential.https://github.com.helper \ '!molecule-git-credential-helper' && \ echo "git credential helper registered (molecule-git-credential-helper)" # ───────────────────────────────────────────────────────────────────────────── PATCH # Build and tag # Capture docker's exit code via PIPESTATUS[0] before grep's exit code # overwrites $?. Without this, set -o pipefail causes grep's exit (0 = match # found, 1 = no match) to determine success — not docker's exit code. log " Running docker build ..." docker build \ --no-cache \ -t "workspace-template:${runtime}" \ "${RUNTIME_DIR}" 2>&1 | grep -E "^(Step|#|---|\[|✓|ERROR|error)" docker_exit=${PIPESTATUS[0]} if [ "${docker_exit}" -eq 0 ]; then log " ✓ workspace-template:${runtime} built" SUCCESS+=("${runtime}") else err " Build failed for ${runtime} (docker exit ${docker_exit})" FAILED+=("${runtime}") fi done # ───────────────────────────────────────────────────── # Summary # ───────────────────────────────────────────────────── echo "" log "══════════════════════════════════════════" log "Rebuild complete" log "══════════════════════════════════════════" if [ "${#SUCCESS[@]}" -gt 0 ]; then log "✓ Succeeded: ${SUCCESS[*]}" fi if [ "${#FAILED[@]}" -gt 0 ]; then err "✗ Failed: ${FAILED[*]}" fi echo "" log "Verify images:" docker images | grep "workspace-template" | sort echo "" log "To restart all running workspaces and pick up new images:" log " docker ps --filter name=molecule --format '{{.Names}}' | xargs -r docker rm -f" log " # Then restart workspaces via Canvas or API" if [ "${#FAILED[@]}" -gt 0 ]; then exit 1 fi