#!/bin/sh # Post-receive hook: push 后自动部署容器 # 默认在服务器本机构建镜像;配置 .deploy.env 后也可从镜像仓库拉取 CI 构建好的镜像。 REPO_TOPLEVEL="" LOG_FILE="" CURRENT_BRANCH="" DEPLOY_ENV="" ENV_FILE="" START_PORT="" IMAGE_TAG="" IMAGE_SOURCE="" REMOTE_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 } load_deploy_config() { if [ ! -f ".deploy.env" ]; then echo "=== deploy config: .deploy.env not found, use defaults ===" >> "$LOG_FILE" return 0 fi echo "=== deploy config: loading .deploy.env ===" >> "$LOG_FILE" set -a # shellcheck disable=SC1091 . ./.deploy.env set +a } 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" ;; pre) cp -f env-example/.env.local.example .env.local ENV_FILE=".env.local" DEPLOY_ENV="test" START_PORT=9135 echo "=== env: copied .env.local (pre branch → test runtime) ===" >> "$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_SOURCE="${COZSWEET_DEPLOY_IMAGE_SOURCE:-build}" REMOTE_IMAGE_TAG="${COZSWEET_REMOTE_IMAGE_TAG:-${DEPLOY_ENV}-${SHORT_SHA}}" case "$IMAGE_SOURCE" in build) IMAGE_TAG="cozsweet-web:${DEPLOY_ENV}-${SHORT_SHA}" ;; pull) if [ -z "${COZSWEET_REGISTRY_IMAGE:-}" ]; then echo "=== missing COZSWEET_REGISTRY_IMAGE for pull deploy ===" >> "$LOG_FILE" exit 64 fi IMAGE_TAG="${COZSWEET_REGISTRY_IMAGE}:${REMOTE_IMAGE_TAG}" ;; *) echo "=== unsupported COZSWEET_DEPLOY_IMAGE_SOURCE: $IMAGE_SOURCE ===" >> "$LOG_FILE" exit 64 ;; esac 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 "=== image source: $IMAGE_SOURCE ===" echo "=== env file: $COZSWEET_ENV_FILE ===" echo "=== image: $COZSWEET_IMAGE ===" echo "=== remote image tag: $REMOTE_IMAGE_TAG ===" 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 } pull_container_image() { echo "=== docker pull START @ $(date +%Y-%m-%dT%H:%M:%S%z) ===" >> "$LOG_FILE" docker_compose pull web >> "$LOG_FILE" 2>&1 COMPOSE_EXIT=$? echo "=== docker pull EXIT_CODE=$COMPOSE_EXIT @ $(date +%Y-%m-%dT%H:%M:%S%z) ===" >> "$LOG_FILE" echo "" >> "$LOG_FILE" return $COMPOSE_EXIT } launch_container() { echo "=== docker compose up START @ $(date +%Y-%m-%dT%H:%M:%S%z) ===" >> "$LOG_FILE" if [ "$IMAGE_SOURCE" = "pull" ]; then docker_compose up -d --remove-orphans --no-build web >> "$LOG_FILE" 2>&1 else docker_compose up -d --remove-orphans web >> "$LOG_FILE" 2>&1 fi 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 load_deploy_config copy_env_by_branch prepare_compose_env if [ "$IMAGE_SOURCE" = "pull" ]; then if ! pull_container_image; then log_abort "docker pull failed, old container kept running" exit $COMPOSE_EXIT fi else if ! run_container_build; then log_abort "docker build failed, old container kept running" exit $COMPOSE_EXIT fi 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