588 lines
20 KiB
PowerShell
588 lines
20 KiB
PowerShell
#Requires -Version 5.1
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Git-based incremental deployment for gongxiangp2.
|
|
|
|
.DESCRIPTION
|
|
Source of truth is the Git repository. The build server fetches only Git
|
|
deltas, builds a Docker image with cache, pushes commit-tagged images to the
|
|
internal registry, and the production server pulls the selected image.
|
|
|
|
This script intentionally does not upload a source tarball from the local
|
|
workstation and does not build on the production server.
|
|
#>
|
|
|
|
param(
|
|
[ValidateSet('production', 'shadow')]
|
|
[string]$Target = 'production',
|
|
|
|
[string]$RepoUrl = 'http://git.1080p.hk/lunanting/gongxiangp2.git',
|
|
[string]$Branch = 'main',
|
|
[string]$Commit = '',
|
|
|
|
[string]$GitUsername = $env:GIT_DEPLOY_USERNAME,
|
|
[string]$GitPassword = $env:GIT_DEPLOY_PASSWORD,
|
|
[string]$GitAuthHeader = $env:GIT_AUTH_HEADER,
|
|
|
|
[string]$BuildServerIP = '192.168.42.134',
|
|
[string]$BuildServerUser = 'root',
|
|
[string]$BuildServerPassword = $env:BUILD_SERVER_PASSWORD,
|
|
[string]$RemoteBuildDir = '/root/youyidian-3d-git-build',
|
|
|
|
[string]$ProdServerIP = '192.168.58.20',
|
|
[string]$ProdServerUser = 'root',
|
|
|
|
[int]$RegistryPort = 5000,
|
|
[string]$ImageName = 'youyidian-3d-material',
|
|
|
|
[string]$DownloadTokenSecret = $env:DOWNLOAD_TOKEN_SECRET,
|
|
|
|
[switch]$DryRun,
|
|
[switch]$NoCache,
|
|
[switch]$SkipBuild,
|
|
[switch]$SkipPush,
|
|
[switch]$SkipDeploy,
|
|
[switch]$KeepOldImages,
|
|
[switch]$AggressiveBuildCacheCleanup,
|
|
[switch]$NoRollback
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
$targetConfig = @{
|
|
production = @{
|
|
Port = 4320
|
|
ContainerName = 'youyidian-3d-material'
|
|
EnvFile = '.env'
|
|
PublicAppUrl = 'https://sc.jiansun.vip'
|
|
DingtalkCallbackOrigin = 'https://sc.jiansun.vip'
|
|
S3PublicDomain = 'https://s3.jiansun.vip/gongxiangpan'
|
|
}
|
|
shadow = @{
|
|
Port = 5320
|
|
ContainerName = 'youyidian-3d-shadow'
|
|
EnvFile = '.env.shadow'
|
|
PublicAppUrl = 'http://192.168.58.20:5320'
|
|
DingtalkCallbackOrigin = 'http://192.168.58.20:5320'
|
|
S3PublicDomain = 'https://s3.jiansun.vip/gongxiangpan'
|
|
}
|
|
}
|
|
|
|
function Write-Info {
|
|
param([string]$Message)
|
|
Write-Host "[INFO] $Message" -ForegroundColor Cyan
|
|
}
|
|
|
|
function Write-Success {
|
|
param([string]$Message)
|
|
Write-Host "[OK] $Message" -ForegroundColor Green
|
|
}
|
|
|
|
function Write-Warning-Custom {
|
|
param([string]$Message)
|
|
Write-Host "[WARN] $Message" -ForegroundColor Yellow
|
|
}
|
|
|
|
function Write-Error-Custom {
|
|
param([string]$Message)
|
|
Write-Host "[ERR] $Message" -ForegroundColor Red
|
|
}
|
|
|
|
function Write-Step {
|
|
param([string]$Message)
|
|
Write-Host ""
|
|
Write-Host "==================================================" -ForegroundColor Cyan
|
|
Write-Host $Message -ForegroundColor Magenta
|
|
Write-Host "==================================================" -ForegroundColor Cyan
|
|
}
|
|
|
|
function ConvertTo-ShellSingleQuoted {
|
|
param([AllowNull()][string]$Value)
|
|
if ($null -eq $Value) {
|
|
return "''"
|
|
}
|
|
return "'" + $Value.Replace("'", "'\''") + "'"
|
|
}
|
|
|
|
function Get-PlinkPath {
|
|
$candidates = @(
|
|
'C:\Program Files\PuTTY\plink.exe',
|
|
'C:\Program Files (x86)\PuTTY\plink.exe'
|
|
)
|
|
|
|
foreach ($candidate in $candidates) {
|
|
if (Test-Path $candidate) {
|
|
return $candidate
|
|
}
|
|
}
|
|
|
|
$fromPath = Get-Command plink.exe -ErrorAction SilentlyContinue
|
|
if ($fromPath) {
|
|
return $fromPath.Source
|
|
}
|
|
|
|
throw 'Cannot find plink.exe. Please install PuTTY or add plink.exe to PATH.'
|
|
}
|
|
|
|
function Get-PscpPath {
|
|
$candidates = @(
|
|
'C:\Program Files\PuTTY\pscp.exe',
|
|
'C:\Program Files (x86)\PuTTY\pscp.exe'
|
|
)
|
|
|
|
foreach ($candidate in $candidates) {
|
|
if (Test-Path $candidate) {
|
|
return $candidate
|
|
}
|
|
}
|
|
|
|
$fromPath = Get-Command pscp.exe -ErrorAction SilentlyContinue
|
|
if ($fromPath) {
|
|
return $fromPath.Source
|
|
}
|
|
|
|
throw 'Cannot find pscp.exe. Please install PuTTY or add pscp.exe to PATH.'
|
|
}
|
|
|
|
function Protect-LogLine {
|
|
param([string]$Line)
|
|
$masked = $Line
|
|
foreach ($secret in @($GitPassword, $BuildServerPassword, $DownloadTokenSecret, $GitAuthHeader)) {
|
|
if (-not [string]::IsNullOrEmpty($secret)) {
|
|
$masked = $masked.Replace($secret, '***')
|
|
}
|
|
}
|
|
return $masked
|
|
}
|
|
|
|
function Invoke-BuildServerScript {
|
|
param([string]$ScriptText)
|
|
|
|
$plinkPath = Get-PlinkPath
|
|
$pscpPath = Get-PscpPath
|
|
$scriptId = [guid]::NewGuid().ToString('N')
|
|
$localTemp = Join-Path ([System.IO.Path]::GetTempPath()) "git-incremental-deploy-$scriptId.sh"
|
|
$remoteTemp = "/tmp/git-incremental-deploy-$scriptId.sh"
|
|
[System.IO.File]::WriteAllText($localTemp, $ScriptText, [System.Text.UTF8Encoding]::new($false))
|
|
|
|
Write-Info "Connecting to build server $BuildServerUser@$BuildServerIP"
|
|
|
|
try {
|
|
$remoteSpec = "${BuildServerUser}@${BuildServerIP}:$remoteTemp"
|
|
& $pscpPath -batch -pw $BuildServerPassword $localTemp $remoteSpec 2>&1 |
|
|
ForEach-Object {
|
|
$line = Protect-LogLine -Line ($_.ToString())
|
|
if ($line.Trim()) {
|
|
Write-Host $line -ForegroundColor Gray
|
|
}
|
|
}
|
|
|
|
if ($LASTEXITCODE -ne 0) {
|
|
return $LASTEXITCODE
|
|
}
|
|
|
|
$command = "bash '$remoteTemp'; rc=`$?; rm -f '$remoteTemp'; exit `$rc"
|
|
& $plinkPath -batch -pw $BuildServerPassword "$BuildServerUser@$BuildServerIP" $command 2>&1 |
|
|
ForEach-Object {
|
|
$line = Protect-LogLine -Line ($_.ToString())
|
|
if ($line -match '^\[OK\]') {
|
|
Write-Host $line -ForegroundColor Green
|
|
}
|
|
elseif ($line -match '^\[WARN\]') {
|
|
Write-Host $line -ForegroundColor Yellow
|
|
}
|
|
elseif ($line -match '^\[ERR\]|error|failed|fatal' ) {
|
|
Write-Host $line -ForegroundColor Red
|
|
}
|
|
else {
|
|
Write-Host $line -ForegroundColor Gray
|
|
}
|
|
}
|
|
|
|
return $LASTEXITCODE
|
|
}
|
|
finally {
|
|
Remove-Item $localTemp -Force -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
|
|
if ([string]::IsNullOrWhiteSpace($BuildServerPassword)) {
|
|
throw 'Build server password is required. Pass -BuildServerPassword or set BUILD_SERVER_PASSWORD.'
|
|
}
|
|
|
|
if ([string]::IsNullOrWhiteSpace($GitAuthHeader) -and -not [string]::IsNullOrWhiteSpace($GitUsername) -and -not [string]::IsNullOrWhiteSpace($GitPassword)) {
|
|
$pair = "${GitUsername}:${GitPassword}"
|
|
$basic = [Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($pair))
|
|
$GitAuthHeader = "Authorization: Basic $basic"
|
|
}
|
|
|
|
if ($RepoUrl -match '^https?://' -and [string]::IsNullOrWhiteSpace($GitAuthHeader)) {
|
|
Write-Warning-Custom 'HTTP Git repository has no auth header. This is OK only if the repository is public or credentials are already configured on the build server.'
|
|
}
|
|
|
|
$cfg = $targetConfig[$Target]
|
|
$rollbackFlag = if ($NoRollback) { '0' } else { '1' }
|
|
|
|
$tokens = @{
|
|
'__REPO_URL__' = ConvertTo-ShellSingleQuoted $RepoUrl
|
|
'__BRANCH__' = ConvertTo-ShellSingleQuoted $Branch
|
|
'__COMMIT__' = ConvertTo-ShellSingleQuoted $Commit
|
|
'__GIT_AUTH_HEADER__' = ConvertTo-ShellSingleQuoted $GitAuthHeader
|
|
'__REMOTE_BUILD_DIR__' = ConvertTo-ShellSingleQuoted $RemoteBuildDir
|
|
'__TARGET__' = ConvertTo-ShellSingleQuoted $Target
|
|
'__BUILD_SERVER_IP__' = ConvertTo-ShellSingleQuoted $BuildServerIP
|
|
'__PROD_SERVER_IP__' = ConvertTo-ShellSingleQuoted $ProdServerIP
|
|
'__PROD_SERVER_USER__' = ConvertTo-ShellSingleQuoted $ProdServerUser
|
|
'__REGISTRY_PORT__' = ConvertTo-ShellSingleQuoted ([string]$RegistryPort)
|
|
'__IMAGE_NAME__' = ConvertTo-ShellSingleQuoted $ImageName
|
|
'__CONTAINER_NAME__' = ConvertTo-ShellSingleQuoted $cfg.ContainerName
|
|
'__PORT__' = ConvertTo-ShellSingleQuoted ([string]$cfg.Port)
|
|
'__ENV_FILE__' = ConvertTo-ShellSingleQuoted $cfg.EnvFile
|
|
'__PUBLIC_APP_URL__' = ConvertTo-ShellSingleQuoted $cfg.PublicAppUrl
|
|
'__DINGTALK_CALLBACK_ORIGIN__' = ConvertTo-ShellSingleQuoted $cfg.DingtalkCallbackOrigin
|
|
'__S3_PUBLIC_DOMAIN__' = ConvertTo-ShellSingleQuoted $cfg.S3PublicDomain
|
|
'__DOWNLOAD_TOKEN_SECRET__' = ConvertTo-ShellSingleQuoted $DownloadTokenSecret
|
|
'__DRY_RUN__' = ConvertTo-ShellSingleQuoted ($(if ($DryRun) { '1' } else { '0' }))
|
|
'__NO_CACHE__' = ConvertTo-ShellSingleQuoted ($(if ($NoCache) { '1' } else { '0' }))
|
|
'__SKIP_BUILD__' = ConvertTo-ShellSingleQuoted ($(if ($SkipBuild) { '1' } else { '0' }))
|
|
'__SKIP_PUSH__' = ConvertTo-ShellSingleQuoted ($(if ($SkipPush) { '1' } else { '0' }))
|
|
'__SKIP_DEPLOY__' = ConvertTo-ShellSingleQuoted ($(if ($SkipDeploy) { '1' } else { '0' }))
|
|
'__KEEP_OLD_IMAGES__' = ConvertTo-ShellSingleQuoted ($(if ($KeepOldImages) { '1' } else { '0' }))
|
|
'__AGGRESSIVE_CLEANUP__' = ConvertTo-ShellSingleQuoted ($(if ($AggressiveBuildCacheCleanup) { '1' } else { '0' }))
|
|
'__ROLLBACK_ON_FAILURE__' = ConvertTo-ShellSingleQuoted $rollbackFlag
|
|
}
|
|
|
|
$remoteTemplate = @'
|
|
set -Eeuo pipefail
|
|
|
|
REPO_URL=__REPO_URL__
|
|
BRANCH=__BRANCH__
|
|
COMMIT_INPUT=__COMMIT__
|
|
GIT_AUTH_HEADER=__GIT_AUTH_HEADER__
|
|
REMOTE_BUILD_DIR=__REMOTE_BUILD_DIR__
|
|
TARGET=__TARGET__
|
|
BUILD_SERVER_IP=__BUILD_SERVER_IP__
|
|
PROD_SERVER_IP=__PROD_SERVER_IP__
|
|
PROD_SERVER_USER=__PROD_SERVER_USER__
|
|
REGISTRY_PORT=__REGISTRY_PORT__
|
|
IMAGE_NAME=__IMAGE_NAME__
|
|
CONTAINER_NAME=__CONTAINER_NAME__
|
|
PORT=__PORT__
|
|
ENV_FILE=__ENV_FILE__
|
|
PUBLIC_APP_URL=__PUBLIC_APP_URL__
|
|
DINGTALK_CALLBACK_ORIGIN=__DINGTALK_CALLBACK_ORIGIN__
|
|
S3_PUBLIC_DOMAIN=__S3_PUBLIC_DOMAIN__
|
|
DOWNLOAD_TOKEN_SECRET=__DOWNLOAD_TOKEN_SECRET__
|
|
DRY_RUN=__DRY_RUN__
|
|
NO_CACHE=__NO_CACHE__
|
|
SKIP_BUILD=__SKIP_BUILD__
|
|
SKIP_PUSH=__SKIP_PUSH__
|
|
SKIP_DEPLOY=__SKIP_DEPLOY__
|
|
KEEP_OLD_IMAGES=__KEEP_OLD_IMAGES__
|
|
AGGRESSIVE_CLEANUP=__AGGRESSIVE_CLEANUP__
|
|
ROLLBACK_ON_FAILURE=__ROLLBACK_ON_FAILURE__
|
|
|
|
log() { printf '[INFO] %s\n' "$*"; }
|
|
ok() { printf '[OK] %s\n' "$*"; }
|
|
warn() { printf '[WARN] %s\n' "$*"; }
|
|
fail() { printf '[ERR] %s\n' "$*" >&2; exit 1; }
|
|
|
|
git_cmd() {
|
|
if [ -n "$GIT_AUTH_HEADER" ]; then
|
|
git -c "http.extraHeader=$GIT_AUTH_HEADER" "$@"
|
|
else
|
|
git "$@"
|
|
fi
|
|
}
|
|
|
|
require_cmd() {
|
|
command -v "$1" >/dev/null 2>&1 || fail "Missing command on build server: $1"
|
|
}
|
|
|
|
start_remote_step() {
|
|
printf '\n==================================================\n'
|
|
printf '%s\n' "$1"
|
|
printf '==================================================\n'
|
|
}
|
|
|
|
start_remote_step "Preflight"
|
|
require_cmd git
|
|
require_cmd docker
|
|
require_cmd curl
|
|
require_cmd ssh
|
|
ok "Build server tools are available"
|
|
|
|
REMOTE_HEAD="$(git_cmd ls-remote --heads "$REPO_URL" "$BRANCH" | awk '{print $1}' | head -n 1 || true)"
|
|
if [ -z "$REMOTE_HEAD" ]; then
|
|
fail "Cannot read Git branch $BRANCH from $REPO_URL"
|
|
fi
|
|
ok "Git branch $BRANCH is reachable: ${REMOTE_HEAD:0:12}"
|
|
|
|
ssh -o BatchMode=yes -o StrictHostKeyChecking=no -o ConnectTimeout=8 "$PROD_SERVER_USER@$PROD_SERVER_IP" "echo OK" >/tmp/git-deploy-prod-check.log 2>&1 || {
|
|
cat /tmp/git-deploy-prod-check.log >&2 || true
|
|
fail "Cannot reach production server from build server"
|
|
}
|
|
ok "Production server is reachable from build server"
|
|
|
|
curl -fsS "http://localhost:${REGISTRY_PORT}/v2/" >/dev/null 2>&1 || {
|
|
fail "Docker registry is not reachable on build server port ${REGISTRY_PORT}"
|
|
}
|
|
ok "Docker registry is reachable"
|
|
|
|
if [ "$DRY_RUN" = "1" ]; then
|
|
log "Dry run completed. No code, image, or container was changed."
|
|
exit 0
|
|
fi
|
|
|
|
start_remote_step "Git Sync"
|
|
if [ ! -d "$REMOTE_BUILD_DIR/.git" ]; then
|
|
warn "Git worktree not found. Creating a fresh clone at $REMOTE_BUILD_DIR"
|
|
rm -rf "$REMOTE_BUILD_DIR"
|
|
mkdir -p "$(dirname "$REMOTE_BUILD_DIR")"
|
|
git_cmd clone "$REPO_URL" "$REMOTE_BUILD_DIR"
|
|
else
|
|
log "Using existing Git worktree: $REMOTE_BUILD_DIR"
|
|
fi
|
|
|
|
cd "$REMOTE_BUILD_DIR"
|
|
git remote set-url origin "$REPO_URL"
|
|
git_cmd fetch --prune origin "$BRANCH"
|
|
|
|
TARGET_REF="origin/$BRANCH"
|
|
if [ -n "$COMMIT_INPUT" ]; then
|
|
TARGET_REF="$COMMIT_INPUT"
|
|
fi
|
|
|
|
git checkout -B "$BRANCH" "origin/$BRANCH" >/dev/null 2>&1 || git checkout "$BRANCH"
|
|
git reset --hard "$TARGET_REF"
|
|
git clean -fdx
|
|
|
|
COMMIT_SHA="$(git rev-parse HEAD)"
|
|
SHORT_SHA="$(git rev-parse --short=12 HEAD)"
|
|
COMMIT_TAG="${TARGET}-${SHORT_SHA}"
|
|
MOVING_TAG="${TARGET}-latest"
|
|
REGISTRY_COMMIT="${BUILD_SERVER_IP}:${REGISTRY_PORT}/${IMAGE_NAME}:${COMMIT_TAG}"
|
|
REGISTRY_MOVING="${BUILD_SERVER_IP}:${REGISTRY_PORT}/${IMAGE_NAME}:${MOVING_TAG}"
|
|
|
|
test -f Dockerfile || fail "Dockerfile missing after Git sync"
|
|
test -f package.json || fail "package.json missing after Git sync"
|
|
test -f "app/api/uploads/[...path]/route.ts" || fail "app/api/uploads fallback route missing"
|
|
test -f "app/uploads/[...path]/route.ts" || fail "app/uploads fallback route missing"
|
|
|
|
ok "Synced to commit $COMMIT_SHA"
|
|
log "Image tag: $COMMIT_TAG"
|
|
|
|
if [ "$SKIP_BUILD" != "1" ]; then
|
|
start_remote_step "Docker Build"
|
|
docker pull "$REGISTRY_MOVING" >/dev/null 2>&1 || true
|
|
|
|
build_args=()
|
|
if [ "$NO_CACHE" = "1" ]; then
|
|
build_args+=(--no-cache)
|
|
elif docker image inspect "$REGISTRY_MOVING" >/dev/null 2>&1; then
|
|
build_args+=(--cache-from "$REGISTRY_MOVING")
|
|
fi
|
|
|
|
DOCKER_BUILDKIT=1 docker build "${build_args[@]}" \
|
|
-t "$IMAGE_NAME:$COMMIT_TAG" \
|
|
-t "$IMAGE_NAME:$MOVING_TAG" \
|
|
-t "$REGISTRY_COMMIT" \
|
|
-t "$REGISTRY_MOVING" \
|
|
.
|
|
|
|
ok "Docker image built: $REGISTRY_COMMIT"
|
|
else
|
|
warn "Skipping Docker build by request"
|
|
fi
|
|
|
|
if [ "$SKIP_PUSH" != "1" ]; then
|
|
start_remote_step "Push Image"
|
|
docker push "$REGISTRY_COMMIT"
|
|
docker push "$REGISTRY_MOVING"
|
|
ok "Pushed image tags: $COMMIT_TAG, $MOVING_TAG"
|
|
else
|
|
warn "Skipping image push by request"
|
|
fi
|
|
|
|
if [ "$SKIP_DEPLOY" != "1" ]; then
|
|
start_remote_step "Deploy on Production Server"
|
|
log "Ensuring production server can access image $REGISTRY_COMMIT"
|
|
if ssh -o StrictHostKeyChecking=no "$PROD_SERVER_USER@$PROD_SERVER_IP" "docker pull '$REGISTRY_COMMIT'" >/tmp/git-deploy-prod-pull.log 2>&1; then
|
|
ok "Production server pulled image from registry"
|
|
else
|
|
warn "Production server could not pull image directly; streaming image from build server"
|
|
cat /tmp/git-deploy-prod-pull.log >&2 || true
|
|
docker save "$REGISTRY_COMMIT" | ssh -o StrictHostKeyChecking=no "$PROD_SERVER_USER@$PROD_SERVER_IP" "docker load"
|
|
ok "Image loaded on production server via SSH stream"
|
|
fi
|
|
|
|
PROD_PAYLOAD="$(cat <<'PROD_SCRIPT'
|
|
set -Eeuo pipefail
|
|
|
|
log() { printf '[INFO] %s\n' "$*"; }
|
|
ok() { printf '[OK] %s\n' "$*"; }
|
|
warn() { printf '[WARN] %s\n' "$*"; }
|
|
fail() { printf '[ERR] %s\n' "$*" >&2; exit 1; }
|
|
|
|
start_container() {
|
|
local image="$1"
|
|
local deploy_version
|
|
deploy_version="$(date +%s)"
|
|
|
|
run_args=(
|
|
docker run -d
|
|
--name "$CONTAINER_NAME"
|
|
--network "$NETWORK_NAME"
|
|
-p "${PORT}:3000"
|
|
-v "${UPLOADS_MOUNT}:/app/uploads"
|
|
--env-file "$ENV_FILE_PATH"
|
|
-e "USE_REDIS=true"
|
|
-e "REDIS_URL=redis://gongxiangpan-redis:6379"
|
|
-e "NEXTAUTH_URL=$PUBLIC_APP_URL"
|
|
-e "APP_PUBLIC_URL=$PUBLIC_APP_URL"
|
|
-e "NEXT_PUBLIC_DINGTALK_CALLBACK_ORIGIN=$DINGTALK_CALLBACK_ORIGIN"
|
|
-e "S3_PUBLIC_DOMAIN=$S3_PUBLIC_DOMAIN"
|
|
-e "DISABLE_CACHE=true"
|
|
-e "DEPLOYMENT_VERSION=$deploy_version"
|
|
-e "DEPLOYMENT_COMMIT=$COMMIT_SHA"
|
|
--memory=6g
|
|
--memory-reservation=512m
|
|
--restart=unless-stopped
|
|
)
|
|
|
|
if [ -n "$DOWNLOAD_TOKEN_SECRET" ]; then
|
|
run_args+=(-e "DOWNLOAD_TOKEN_SECRET=$DOWNLOAD_TOKEN_SECRET")
|
|
fi
|
|
|
|
"${run_args[@]}" "$image"
|
|
}
|
|
|
|
require_file() {
|
|
[ -f "$1" ] || fail "Missing required file: $1"
|
|
}
|
|
|
|
command -v docker >/dev/null 2>&1 || fail "Missing docker on production server"
|
|
command -v curl >/dev/null 2>&1 || warn "curl not found; health check will use docker logs only"
|
|
|
|
require_file "$ENV_FILE_PATH"
|
|
[ -d "$UPLOADS_MOUNT" ] || fail "Uploads mount does not exist: $UPLOADS_MOUNT"
|
|
docker network inspect "$NETWORK_NAME" >/dev/null 2>&1 || fail "Docker network missing: $NETWORK_NAME"
|
|
|
|
log "Checking image $REGISTRY_COMMIT"
|
|
if docker image inspect "$REGISTRY_COMMIT" >/dev/null 2>&1; then
|
|
ok "Image already available on production server"
|
|
else
|
|
docker pull "$REGISTRY_COMMIT"
|
|
fi
|
|
|
|
OLD_IMAGE="$(docker inspect -f '{{.Config.Image}}' "$CONTAINER_NAME" 2>/dev/null || true)"
|
|
if [ -n "$OLD_IMAGE" ]; then
|
|
log "Current container image: $OLD_IMAGE"
|
|
else
|
|
warn "No existing container found"
|
|
fi
|
|
|
|
docker stop "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
|
docker rm "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
|
|
|
log "Starting new container $CONTAINER_NAME"
|
|
start_container "$REGISTRY_COMMIT" >/tmp/git-deploy-new-container-id.txt
|
|
cat /tmp/git-deploy-new-container-id.txt
|
|
|
|
healthy=0
|
|
for i in $(seq 1 12); do
|
|
running="$(docker inspect -f '{{.State.Running}}' "$CONTAINER_NAME" 2>/dev/null || echo false)"
|
|
if [ "$running" != "true" ]; then
|
|
sleep 5
|
|
continue
|
|
fi
|
|
|
|
if command -v curl >/dev/null 2>&1; then
|
|
if curl -fsS --max-time 8 "http://127.0.0.1:${PORT}/api/health" >/tmp/git-deploy-health.json 2>/tmp/git-deploy-health.err; then
|
|
healthy=1
|
|
break
|
|
fi
|
|
else
|
|
healthy=1
|
|
break
|
|
fi
|
|
|
|
sleep 5
|
|
done
|
|
|
|
if [ "$healthy" != "1" ]; then
|
|
warn "New container failed health check"
|
|
docker logs --tail 120 "$CONTAINER_NAME" 2>&1 || true
|
|
|
|
if [ "$ROLLBACK_ON_FAILURE" = "1" ] && [ -n "$OLD_IMAGE" ]; then
|
|
warn "Rolling back to $OLD_IMAGE"
|
|
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
|
start_container "$OLD_IMAGE" >/tmp/git-deploy-rollback-container-id.txt
|
|
cat /tmp/git-deploy-rollback-container-id.txt
|
|
fi
|
|
|
|
fail "Deployment failed"
|
|
fi
|
|
|
|
ok "Container is healthy on port $PORT"
|
|
docker logs --tail 20 "$CONTAINER_NAME" 2>&1 || true
|
|
PROD_SCRIPT
|
|
)"
|
|
|
|
export_script="$(printf '%s' "$PROD_PAYLOAD" | base64 -w 0)"
|
|
ssh -o StrictHostKeyChecking=no "$PROD_SERVER_USER@$PROD_SERVER_IP" \
|
|
"TARGET='$TARGET' CONTAINER_NAME='$CONTAINER_NAME' PORT='$PORT' REGISTRY_COMMIT='$REGISTRY_COMMIT' COMMIT_SHA='$COMMIT_SHA' ENV_FILE_PATH='/mnt/PoolOne/apps/youyidian-3d/$ENV_FILE' PUBLIC_APP_URL='$PUBLIC_APP_URL' DINGTALK_CALLBACK_ORIGIN='$DINGTALK_CALLBACK_ORIGIN' S3_PUBLIC_DOMAIN='$S3_PUBLIC_DOMAIN' DOWNLOAD_TOKEN_SECRET='$DOWNLOAD_TOKEN_SECRET' NETWORK_NAME='youyidian-3d_app-network' UPLOADS_MOUNT='/mnt/PoolOne/apps/youyidian-3d/uploads' ROLLBACK_ON_FAILURE='$ROLLBACK_ON_FAILURE' bash -lc \"printf '%s' '$export_script' | base64 -d | bash\""
|
|
ok "Production deployment command completed"
|
|
else
|
|
warn "Skipping production deployment by request"
|
|
fi
|
|
|
|
start_remote_step "Cleanup"
|
|
if [ "$KEEP_OLD_IMAGES" = "1" ]; then
|
|
warn "Skipping cleanup by request"
|
|
else
|
|
docker image prune -f >/dev/null 2>&1 || true
|
|
if [ "$AGGRESSIVE_CLEANUP" = "1" ]; then
|
|
docker builder prune -af --filter until=168h >/dev/null 2>&1 || true
|
|
ok "Build server cleanup completed, including old build cache"
|
|
else
|
|
ok "Build server image cleanup completed; build cache preserved"
|
|
fi
|
|
|
|
ssh -o StrictHostKeyChecking=no "$PROD_SERVER_USER@$PROD_SERVER_IP" \
|
|
"docker image prune -af --filter until=6h >/dev/null 2>&1 || true; docker container prune -f --filter until=6h >/dev/null 2>&1 || true"
|
|
ok "Production server cleanup completed"
|
|
fi
|
|
|
|
start_remote_step "Deployment Summary"
|
|
ok "Target: $TARGET"
|
|
ok "Commit: $COMMIT_SHA"
|
|
ok "Image: $REGISTRY_COMMIT"
|
|
ok "Container: $CONTAINER_NAME"
|
|
'@
|
|
|
|
$remoteScript = $remoteTemplate
|
|
foreach ($key in $tokens.Keys) {
|
|
$remoteScript = $remoteScript.Replace($key, $tokens[$key])
|
|
}
|
|
|
|
Write-Step "Git Incremental Deploy"
|
|
Write-Info "Target: $Target"
|
|
Write-Info "Repository: $RepoUrl"
|
|
Write-Info "Branch: $Branch"
|
|
if ($Commit) {
|
|
Write-Info "Commit override: $Commit"
|
|
}
|
|
if ($DryRun) {
|
|
Write-Warning-Custom 'DryRun mode: no code, image, or production container will be changed.'
|
|
}
|
|
|
|
$exitCode = Invoke-BuildServerScript -ScriptText $remoteScript
|
|
if ($exitCode -ne 0) {
|
|
Write-Error-Custom "Git incremental deployment failed with exit code $exitCode"
|
|
exit $exitCode
|
|
}
|
|
|
|
Write-Success 'Git incremental deployment finished.'
|