gongxiangp2/scripts/git-incremental-deploy.ps1
2026-07-04 13:08:10 +08:00

627 lines
22 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#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)
$usePasswordAuth = -not [string]::IsNullOrWhiteSpace($BuildServerPassword)
$plinkPath = $null
$pscpPath = $null
$sshPath = $null
$scpPath = $null
if ($usePasswordAuth) {
$plinkPath = Get-PlinkPath
$pscpPath = Get-PscpPath
}
else {
$sshCommand = Get-Command ssh.exe -ErrorAction SilentlyContinue
$scpCommand = Get-Command scp.exe -ErrorAction SilentlyContinue
if (-not $sshCommand -or -not $scpCommand) {
throw 'Build server password is empty, and OpenSSH ssh.exe/scp.exe were not found for key-based deployment.'
}
$sshPath = $sshCommand.Source
$scpPath = $scpCommand.Source
}
$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))
$authLabel = if ($usePasswordAuth) { 'password auth' } else { 'SSH key auth' }
Write-Info "Connecting to build server $BuildServerUser@$BuildServerIP ($authLabel)"
# 原生程序(plink/pscp)会把 git 等子进程的提示信息写到 stderr。
# 在 ErrorActionPreference=Stop 下,PowerShell 会把这些非致命的 stderr
# 当成 NativeCommandError 抛出,导致部署在 Git Sync 等阶段误中断。
# 这里在调用原生程序的局部作用域内改为 Continue,让 stderr 通过 2>&1
# 合并进管道正常显示;真实失败仍由 $LASTEXITCODE 捕获,不会掩盖错误。
$previousErrorAction = $ErrorActionPreference
$ErrorActionPreference = 'Continue'
try {
$remoteSpec = "${BuildServerUser}@${BuildServerIP}:$remoteTemp"
$copyOutput = $null
if ($usePasswordAuth) {
$copyOutput = & $pscpPath -batch -pw $BuildServerPassword $localTemp $remoteSpec 2>&1
}
else {
$copyOutput = & $scpPath -o BatchMode=yes -o StrictHostKeyChecking=no -o ConnectTimeout=10 $localTemp $remoteSpec 2>&1
}
$copyOutput |
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"
$runOutput = $null
if ($usePasswordAuth) {
$runOutput = & $plinkPath -batch -pw $BuildServerPassword "$BuildServerUser@$BuildServerIP" $command 2>&1
}
else {
$runOutput = & $sshPath -o BatchMode=yes -o StrictHostKeyChecking=no -o ConnectTimeout=10 "$BuildServerUser@$BuildServerIP" $command 2>&1
}
$runOutput |
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 {
$ErrorActionPreference = $previousErrorAction
Remove-Item $localTemp -Force -ErrorAction SilentlyContinue
}
}
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"
-v "/mnt/PoolOne/apps/youyidian-3d/public-traffic-logs:/var/log/nginx:ro"
--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.'