#!/usr/bin/env bash set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" HOST="${STUDIO_HOST:-10.10.0.2}" GRAY_WEB_PORT="${GRAY_WEB_PORT:-15174}" GRAY_API_PORT="${GRAY_API_PORT:-18085}" REQUIRE_IMAGE_INTEGRATION=0 CANDIDATE_DIR="" usage() { cat <<'EOF' Usage: bash scripts/check-gray-ready.sh --candidate-dir DIR [--require-image-integration] Read-only checks only. This script never uploads, restarts, or changes 103. Checks: - candidate checksum and traceability manifest - current memind_adm production health - reserved gray ports are free on 103 - expected image_make release is healthy on 103 - optional end-to-end Portal/image_make runtime wiring readiness EOF } while [[ $# -gt 0 ]]; do case "$1" in --candidate-dir) shift [[ $# -gt 0 ]] || { echo "--candidate-dir requires a directory" >&2; exit 2; } CANDIDATE_DIR="$1" ;; --require-image-integration) REQUIRE_IMAGE_INTEGRATION=1 ;; -h|--help) usage exit 0 ;; *) echo "Unknown argument: $1" >&2 usage >&2 exit 2 ;; esac shift done [[ -n "${CANDIDATE_DIR}" ]] || { usage >&2; exit 2; } if [[ "${CANDIDATE_DIR}" != /* ]]; then CANDIDATE_DIR="${ROOT}/${CANDIDATE_DIR}" fi [[ -d "${CANDIDATE_DIR}" ]] || { echo "Candidate directory not found: ${CANDIDATE_DIR}" >&2; exit 1; } manifest="$(find "${CANDIDATE_DIR}" -maxdepth 1 -type f -name '*.manifest.txt' -print -quit)" sha_file="$(find "${CANDIDATE_DIR}" -maxdepth 1 -type f -name '*.sha256' -print -quit)" bundle="$(find "${CANDIDATE_DIR}" -maxdepth 1 -type f -name '*.tar.gz' -print -quit)" [[ -n "${manifest}" && -n "${sha_file}" && -n "${bundle}" ]] || { echo "Candidate bundle, manifest, or checksum file missing in ${CANDIDATE_DIR}" >&2 exit 1 } ( cd "${CANDIDATE_DIR}" shasum -a 256 -c "$(basename "${sha_file}")" ) if tar -tzf "${bundle}" | grep -Eq '(^|/)(\.env($|\.)|[^/]*\.pid$|src/|release-candidates/)'; then echo "Candidate contains forbidden runtime or development assets" >&2 tar -tzf "${bundle}" | grep -E '(^|/)(\.env($|\.)|[^/]*\.pid$|src/|release-candidates/)' >&2 || true exit 1 fi manifest_value() { local key="$1" sed -n "s/^${key}=//p" "${manifest}" | head -n 1 } release_id="$(manifest_value release_id)" git_head="$(manifest_value git_head)" memind_head="$(manifest_value memind_git_head)" memind_status="$(manifest_value memind_git_status)" image_make_release_id="$(manifest_value image_make_release_id)" [[ -n "${release_id}" && -n "${git_head}" && -n "${memind_head}" ]] || { echo "Candidate manifest is missing release traceability fields" >&2 exit 1 } [[ "${memind_status}" == "clean" ]] || { echo "Candidate manifest does not prove a clean Memind shared source" >&2 exit 1 } [[ "${image_make_release_id}" != "not-declared" && -n "${image_make_release_id}" ]] || { echo "Candidate manifest must declare IMAGE_MAKE_RELEASE_ID" >&2 exit 1 } echo "candidate_release=${release_id}" echo "candidate_git_head=${git_head}" echo "candidate_memind_head=${memind_head}" echo "candidate_image_make_release=${image_make_release_id}" set +e remote_result="$(ssh -o BatchMode=yes -o ConnectTimeout=15 "${HOST}" \ "GRAY_WEB_PORT='${GRAY_WEB_PORT}' GRAY_API_PORT='${GRAY_API_PORT}' IMAGE_MAKE_RELEASE_ID='${image_make_release_id}' REQUIRE_IMAGE_INTEGRATION='${REQUIRE_IMAGE_INTEGRATION}' /bin/bash" <<'REMOTE' set -euo pipefail api_code="$(curl -sS -o /dev/null -w '%{http_code}' http://127.0.0.1:8085/health || true)" web_code="$(curl -sS -o /dev/null -w '%{http_code}' http://127.0.0.1:5174/ops/ || true)" image_code="$(curl -sS -o /dev/null -w '%{http_code}' http://127.0.0.1:18083/health || true)" [[ "${api_code}" == "200" && "${web_code}" == "200" && "${image_code}" == "200" ]] || { echo "health_failed admin_api=${api_code} admin_web=${web_code} image_make=${image_code}" >&2 exit 1 } for port in "${GRAY_WEB_PORT}" "${GRAY_API_PORT}"; do if lsof -nP -iTCP:"${port}" -sTCP:LISTEN -t | grep -q .; then echo "gray_port_in_use=${port}" >&2 exit 1 fi done export DOCKER_HOST=unix:///Users/john/.colima/default/docker.sock docker_bin=/usr/local/bin/docker container="$(${docker_bin} ps --format '{{.Names}} {{.Ports}}' | awk '/18083/ {print $1; exit}')" [[ -n "${container}" ]] || { echo "image_make_container_missing" >&2; exit 1; } image="$(${docker_bin} inspect --format '{{.Config.Image}}' "${container}")" health="$(${docker_bin} inspect --format '{{.State.Health.Status}}' "${container}")" [[ "${image}" == *"${IMAGE_MAKE_RELEASE_ID}"* && "${health}" == "healthy" ]] || { echo "image_make_release_mismatch image=${image} health=${health}" >&2 exit 1 } worker_enabled="$(${docker_bin} inspect --format '{{range .Config.Env}}{{println .}}{{end}}' "${container}" | sed -n 's/^WORKER_ENABLED=//p' | head -n 1)" config_url_present=0 if ${docker_bin} inspect --format '{{range .Config.Env}}{{println .}}{{end}}' "${container}" | grep -q '^IMAGE_MAKE_MEMIND_CONFIG_URL=.'; then config_url_present=1 fi portal_route_present=0 if grep -q '/internal/image-make/runtime-config' /Users/john/Project/Memind/server.mjs 2>/dev/null; then portal_route_present=1 fi echo "admin_api=200 admin_web=200 image_make=200" echo "gray_ports_free=${GRAY_WEB_PORT},${GRAY_API_PORT}" echo "image_make_image=${image}" echo "image_make_worker_enabled=${worker_enabled:-unset}" echo "image_make_config_url_present=${config_url_present}" echo "portal_runtime_route_present=${portal_route_present}" if [[ "${REQUIRE_IMAGE_INTEGRATION}" == "1" ]]; then [[ "${worker_enabled}" == "true" && "${config_url_present}" == "1" && "${portal_route_present}" == "1" ]] || { echo "image_make_integration_not_ready" >&2 exit 1 } fi REMOTE )" remote_status=$? set -e printf '%s\n' "${remote_result}" if [[ "${remote_status}" -ne 0 ]]; then echo "gray_preflight=failed" >&2 exit "${remote_status}" fi echo "gray_preflight=passed"