Spaces:
Sleeping
Sleeping
| set -euo pipefail | |
| SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | |
| PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" | |
| BRANCH="${BRANCH:-$(git -C "$PROJECT_ROOT" branch --show-current)}" | |
| PORT="${PORT:-3000}" | |
| LISTEN_HOST="${MC_HOSTNAME:-0.0.0.0}" | |
| LOG_PATH="${LOG_PATH:-/tmp/mc.log}" | |
| VERIFY_HOST="${VERIFY_HOST:-127.0.0.1}" | |
| PID_FILE="${PID_FILE:-$PROJECT_ROOT/.next/standalone/server.pid}" | |
| SOURCE_DATA_DIR="$PROJECT_ROOT/.data" | |
| BUILD_DATA_DIR="$PROJECT_ROOT/.next/build-runtime" | |
| NODE_VERSION_FILE="$PROJECT_ROOT/.nvmrc" | |
| use_project_node() { | |
| if [[ ! -f "$NODE_VERSION_FILE" ]]; then | |
| return | |
| fi | |
| if [[ -z "${NVM_DIR:-}" ]]; then | |
| export NVM_DIR="$HOME/.nvm" | |
| fi | |
| if [[ -s "$NVM_DIR/nvm.sh" ]]; then | |
| # shellcheck disable=SC1090 | |
| source "$NVM_DIR/nvm.sh" | |
| nvm use >/dev/null | |
| fi | |
| } | |
| list_listener_pids() { | |
| local combined="" | |
| if command -v lsof >/dev/null 2>&1; then | |
| combined+="$( | |
| lsof -tiTCP:"$PORT" -sTCP:LISTEN 2>/dev/null || true | |
| )"$'\n' | |
| fi | |
| if command -v ss >/dev/null 2>&1; then | |
| combined+="$( | |
| ss -ltnp 2>/dev/null | awk -v port=":$PORT" ' | |
| index($4, port) || index($5, port) { | |
| if (match($0, /pid=[0-9]+/)) { | |
| print substr($0, RSTART + 4, RLENGTH - 4) | |
| } | |
| } | |
| ' | |
| )"$'\n' | |
| fi | |
| printf '%s\n' "$combined" | awk -v port="$PORT" ' | |
| /^[0-9]+$/ { | |
| seen[$0] = 1 | |
| } | |
| END { | |
| for (pid in seen) { | |
| print pid | |
| } | |
| } | |
| ' | sort -u | |
| } | |
| stop_pid() { | |
| local pid="$1" | |
| local label="$2" | |
| if [[ -z "$pid" ]] || ! kill -0 "$pid" 2>/dev/null; then | |
| return | |
| fi | |
| echo "==> stopping $label (pid=$pid)" | |
| kill "$pid" 2>/dev/null || true | |
| for _ in $(seq 1 10); do | |
| if ! kill -0 "$pid" 2>/dev/null; then | |
| return | |
| fi | |
| sleep 1 | |
| done | |
| echo "==> force stopping $label (pid=$pid)" | |
| kill -9 "$pid" 2>/dev/null || true | |
| } | |
| stop_existing_server() { | |
| local -a candidate_pids=() | |
| if [[ -f "$PID_FILE" ]]; then | |
| candidate_pids+=("$(cat "$PID_FILE" 2>/dev/null || true)") | |
| fi | |
| while IFS= read -r pid; do | |
| candidate_pids+=("$pid") | |
| done < <(list_listener_pids) | |
| if command -v pgrep >/dev/null 2>&1; then | |
| while IFS= read -r pid; do | |
| candidate_pids+=("$pid") | |
| done < <(pgrep -f "$PROJECT_ROOT/.next/standalone/server.js" || true) | |
| fi | |
| if [[ ${#candidate_pids[@]} -eq 0 ]]; then | |
| return | |
| fi | |
| declare -A seen=() | |
| for pid in "${candidate_pids[@]}"; do | |
| [[ -z "$pid" ]] && continue | |
| [[ -n "${seen[$pid]:-}" ]] && continue | |
| seen[$pid]=1 | |
| stop_pid "$pid" "standalone server" | |
| done | |
| for _ in $(seq 1 10); do | |
| if [[ -z "$(list_listener_pids | head -n1)" ]]; then | |
| rm -f "$PID_FILE" | |
| return | |
| fi | |
| sleep 1 | |
| done | |
| echo "error: port $PORT is still in use after stopping existing server" >&2 | |
| exit 1 | |
| } | |
| load_env() { | |
| set -a | |
| if [[ -f .env ]]; then | |
| # shellcheck disable=SC1091 | |
| source .env | |
| fi | |
| if [[ -f .env.local ]]; then | |
| # shellcheck disable=SC1091 | |
| source .env.local | |
| fi | |
| set +a | |
| } | |
| migrate_runtime_data_dir() { | |
| local target_data_dir="${MISSION_CONTROL_DATA_DIR:-$SOURCE_DATA_DIR}" | |
| if [[ "$target_data_dir" == "$SOURCE_DATA_DIR" ]]; then | |
| return | |
| fi | |
| mkdir -p "$target_data_dir" | |
| local source_db="$SOURCE_DATA_DIR/mission-control.db" | |
| local target_db="$target_data_dir/mission-control.db" | |
| if [[ -s "$target_db" || ! -s "$source_db" ]]; then | |
| return | |
| fi | |
| echo "==> migrating runtime data to $target_data_dir" | |
| if command -v sqlite3 >/dev/null 2>&1; then | |
| local target_db_tmp="$target_db.tmp" | |
| rm -f "$target_db_tmp" | |
| sqlite3 "$source_db" ".backup '$target_db_tmp'" | |
| mv "$target_db_tmp" "$target_db" | |
| if [[ -f "$SOURCE_DATA_DIR/mission-control-tokens.json" ]]; then | |
| cp "$SOURCE_DATA_DIR/mission-control-tokens.json" "$target_data_dir/mission-control-tokens.json" | |
| fi | |
| if [[ -d "$SOURCE_DATA_DIR/backups" ]]; then | |
| rsync -a "$SOURCE_DATA_DIR/backups"/ "$target_data_dir/backups"/ | |
| fi | |
| else | |
| rsync -a \ | |
| --exclude 'mission-control.db-shm' \ | |
| --exclude 'mission-control.db-wal' \ | |
| --exclude '*.db-shm' \ | |
| --exclude '*.db-wal' \ | |
| "$SOURCE_DATA_DIR"/ "$target_data_dir"/ | |
| fi | |
| } | |
| cd "$PROJECT_ROOT" | |
| use_project_node | |
| echo "==> fetching branch $BRANCH" | |
| git fetch origin "$BRANCH" | |
| git merge --ff-only FETCH_HEAD | |
| load_env | |
| migrate_runtime_data_dir | |
| echo "==> stopping existing standalone server before rebuild" | |
| stop_existing_server | |
| echo "==> installing dependencies" | |
| pnpm install --frozen-lockfile | |
| echo "==> rebuilding standalone bundle" | |
| rm -rf .next | |
| mkdir -p "$BUILD_DATA_DIR" | |
| MISSION_CONTROL_DATA_DIR="$BUILD_DATA_DIR" \ | |
| MISSION_CONTROL_DB_PATH="$BUILD_DATA_DIR/mission-control.db" \ | |
| MISSION_CONTROL_TOKENS_PATH="$BUILD_DATA_DIR/mission-control-tokens.json" \ | |
| pnpm build | |
| echo "==> starting standalone server" | |
| load_env | |
| PORT="$PORT" HOSTNAME="$LISTEN_HOST" nohup bash "$PROJECT_ROOT/scripts/start-standalone.sh" >"$LOG_PATH" 2>&1 & | |
| new_pid=$! | |
| echo "$new_pid" > "$PID_FILE" | |
| echo "==> verifying process and static assets" | |
| for _ in $(seq 1 20); do | |
| if curl -fsS "http://$VERIFY_HOST:$PORT/login" >/dev/null 2>&1; then | |
| break | |
| fi | |
| sleep 1 | |
| done | |
| login_html="$(curl -fsS "http://$VERIFY_HOST:$PORT/login")" | |
| css_path="$(printf '%s\n' "$login_html" | sed -n 's|.*\(/_next/static/chunks/[^"]*\.css\).*|\1|p' | sed -n '1p')" | |
| if [[ -z "${css_path:-}" ]]; then | |
| echo "error: no css asset found in rendered login HTML" >&2 | |
| exit 1 | |
| fi | |
| listener_pid="$(list_listener_pids | head -n1)" | |
| if [[ -z "${listener_pid:-}" ]]; then | |
| echo "error: no listener detected on port $PORT after startup" >&2 | |
| exit 1 | |
| fi | |
| if [[ "$listener_pid" != "$new_pid" ]]; then | |
| echo "error: port $PORT is owned by pid=$listener_pid, expected new pid=$new_pid" >&2 | |
| exit 1 | |
| fi | |
| css_disk_path="$PROJECT_ROOT/.next/standalone/.next${css_path#/_next}" | |
| if [[ ! -f "$css_disk_path" ]]; then | |
| echo "error: rendered css asset missing on disk: $css_disk_path" >&2 | |
| exit 1 | |
| fi | |
| content_type="$(curl -fsSI "http://$VERIFY_HOST:$PORT$css_path" | awk 'BEGIN{IGNORECASE=1} /^content-type:/ {print $2}' | tr -d '\r')" | |
| if [[ "${content_type:-}" != text/css* ]]; then | |
| echo "error: css asset served with unexpected content-type: ${content_type:-missing}" >&2 | |
| exit 1 | |
| fi | |
| echo "==> deployed commit $(git rev-parse --short HEAD)" | |
| echo " pid=$new_pid port=$PORT css=$css_path" | |