#!/bin/sh # Post-receive hook: push 后自动构建 Docker 镜像并重启容器 # pnpm / node 环境仍保留,便于服务器缺少 Docker 时做人工排查。 export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" REPO_TOPLEVEL="" LOG_FILE="" CURRENT_BRANCH="" DEPLOY_ENV="" ENV_FILE="" START_PORT="" IMAGE_TAG="" CONTAINER_NAME="" COMPOSE_PROJECT_NAME="" COMPOSE_EXIT=0 resolve_repo_context() { GIT_DIR_ABS="$(git rev-parse --absolute-git-dir 2>/dev/null)" if [ -n "$GIT_DIR_ABS" ] && [ -d "$GIT_DIR_ABS" ]; then REPO_TOPLEVEL="$(cd "$GIT_DIR_ABS/.." && pwd)" export GIT_DIR="$GIT_DIR_ABS" else REPO_TOPLEVEL="$(git rev-parse --show-toplevel 2>/dev/null || echo "$PWD")" fi unset GIT_WORK_TREE cd "$REPO_TOPLEVEL" || { echo "FATAL: cannot cd to $REPO_TOPLEVEL" >&2; exit 1; } echo "=== REPO_TOPLEVEL=$REPO_TOPLEVEL (GIT_DIR=$GIT_DIR) ===" >&2 } init_log_file() { LOG_FILE="$REPO_TOPLEVEL/logs/post-receive.log" mkdir -p "$(dirname "$LOG_FILE")" } write_metadata_header() { { echo "=== post-receive @ $(date +%Y-%m-%dT%H:%M:%S%z) ===" echo "branch: $(git rev-parse --abbrev-ref HEAD)" echo "commit: $(git rev-parse --short HEAD)" echo "cwd: $(pwd)" echo "" } > "$LOG_FILE" } sync_worktree() { echo "=== sync_worktree: git reset --hard HEAD @ $(date +%Y-%m-%dT%H:%M:%S%z) ===" >> "$LOG_FILE" git reset --hard HEAD >> "$LOG_FILE" 2>&1 } copy_env_by_branch() { CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) echo "=== branch: $CURRENT_BRANCH ===" >> "$LOG_FILE" case "$CURRENT_BRANCH" in main) cp -f env-example/.env.production.example .env.production ENV_FILE=".env.production" DEPLOY_ENV="prod" START_PORT=9185 echo "=== env: copied .env.production (main → prod) ===" >> "$LOG_FILE" ;; test) cp -f env-example/.env.local.example .env.local ENV_FILE=".env.local" DEPLOY_ENV="test" START_PORT=9135 echo "=== env: copied .env.local (test → test) ===" >> "$LOG_FILE" ;; dev) cp -f env-example/.env.development.example .env.local ENV_FILE=".env.local" DEPLOY_ENV="dev" START_PORT=9135 echo "=== env: copied .env.local (dev → dev) ===" >> "$LOG_FILE" ;; *) ENV_FILE=".env.local" DEPLOY_ENV="$CURRENT_BRANCH" START_PORT=9135 echo "=== env: UNKNOWN branch '$CURRENT_BRANCH', use defaults ===" >> "$LOG_FILE" ;; esac } prepare_compose_env() { SHORT_SHA="$(git rev-parse --short HEAD)" IMAGE_TAG="cozsweet-web:${DEPLOY_ENV}-${SHORT_SHA}" CONTAINER_NAME="cozsweet-web-${DEPLOY_ENV}" COMPOSE_PROJECT_NAME="cozsweet-web-${DEPLOY_ENV}" export COZSWEET_IMAGE="$IMAGE_TAG" export COZSWEET_CONTAINER_NAME="$CONTAINER_NAME" export COZSWEET_ENV_FILE="$ENV_FILE" export COZSWEET_HOST_PORT="$START_PORT" export COMPOSE_PROJECT_NAME export DOCKER_BUILDKIT=1 export COMPOSE_DOCKER_CLI_BUILD=1 { echo "=== deploy env: $DEPLOY_ENV ===" echo "=== env file: $COZSWEET_ENV_FILE ===" echo "=== image: $COZSWEET_IMAGE ===" echo "=== container: $COZSWEET_CONTAINER_NAME ===" echo "=== host port: $COZSWEET_HOST_PORT ===" echo "=== compose project: $COMPOSE_PROJECT_NAME ===" echo "" } >> "$LOG_FILE" } docker_compose() { if docker compose version >/dev/null 2>&1; then docker compose "$@" return $? fi if command -v docker-compose >/dev/null 2>&1; then docker-compose "$@" return $? fi echo "=== docker compose command not found ===" >> "$LOG_FILE" return 127 } run_container_build() { echo "=== docker build START @ $(date +%Y-%m-%dT%H:%M:%S%z) ===" >> "$LOG_FILE" docker_compose build web >> "$LOG_FILE" 2>&1 COMPOSE_EXIT=$? echo "=== docker build EXIT_CODE=$COMPOSE_EXIT @ $(date +%Y-%m-%dT%H:%M:%S%z) ===" >> "$LOG_FILE" echo "" >> "$LOG_FILE" return $COMPOSE_EXIT } find_port_pids() { if ! command -v ss >/dev/null 2>&1; then echo "=== ss command not found; cannot inspect legacy port $START_PORT ===" >> "$LOG_FILE" return 0 fi ss -tulnp 2>/dev/null \ | awk -v port=":$START_PORT" '$5 ~ port "$" { print }' \ | sed -n 's/.*pid=\([0-9][0-9]*\).*/\1/p' \ | sort -u } container_exists() { docker ps -a --format '{{.Names}}' 2>/dev/null | grep -Fx "$CONTAINER_NAME" >/dev/null 2>&1 } stop_legacy_next_if_needed() { if container_exists; then echo "=== existing container found; skip legacy port cleanup ===" >> "$LOG_FILE" return 0 fi PIDS="$(find_port_pids)" if [ -z "$PIDS" ]; then echo "=== no legacy process found on port $START_PORT ===" >> "$LOG_FILE" return 0 fi echo "=== first container deploy: kill legacy PIDS on port $START_PORT: $PIDS ===" >> "$LOG_FILE" kill -9 $PIDS 2>/dev/null || true sleep 3 } launch_container() { echo "=== docker compose up START @ $(date +%Y-%m-%dT%H:%M:%S%z) ===" >> "$LOG_FILE" stop_legacy_next_if_needed docker_compose up -d --remove-orphans web >> "$LOG_FILE" 2>&1 COMPOSE_EXIT=$? echo "=== docker compose up EXIT_CODE=$COMPOSE_EXIT @ $(date +%Y-%m-%dT%H:%M:%S%z) ===" >> "$LOG_FILE" echo "" >> "$LOG_FILE" return $COMPOSE_EXIT } log_abort() { echo "=== ABORTED @ $(date +%Y-%m-%dT%H:%M:%S%z) ($1) ===" >> "$LOG_FILE" } main() { resolve_repo_context init_log_file write_metadata_header sync_worktree copy_env_by_branch prepare_compose_env if ! run_container_build; then log_abort "docker build failed, old container kept running" exit $COMPOSE_EXIT fi if ! launch_container; then log_abort "docker compose up failed" exit $COMPOSE_EXIT fi echo "=== DEPLOY DONE @ $(date +%Y-%m-%dT%H:%M:%S%z) ===" >> "$LOG_FILE" } main "$@" exit 0