YouYiDianNotificationCenter/scripts/git-build-deploy.ps1
2026-07-16 21:04:56 +08:00

211 lines
7.5 KiB
PowerShell

#Requires -Version 5.1
param(
[string]$RepoUrl = 'http://git.1080p.hk/WuYongJie/YouYiDianNotificationCenter.git',
[string]$Branch = 'main',
[string]$Commit = '',
[string]$BuildServer = 'root@192.168.40.206',
[string]$ProductionServer = 'root@192.168.58.20',
[string]$RemoteBuildDir = '/root/youyidian-notification-center-build',
[string]$RegistryHost = '192.168.40.206:5000',
[string]$ImageName = 'youyidian-notification-center',
[switch]$DryRun
)
$ErrorActionPreference = 'Stop'
function ConvertTo-ShellSingleQuoted([string]$Value) {
return "'" + $Value.Replace("'", "'\''") + "'"
}
$remoteScript = @'
set -Eeuo pipefail
REPO_URL=__REPO_URL__
BRANCH=__BRANCH__
COMMIT_INPUT=__COMMIT__
BUILD_DIR=__BUILD_DIR__
PRODUCTION_SERVER=__PRODUCTION_SERVER__
REGISTRY_HOST=__REGISTRY_HOST__
IMAGE_NAME=__IMAGE_NAME__
DRY_RUN=__DRY_RUN__
log() { printf '[INFO] %s\n' "$*"; }
ok() { printf '[OK] %s\n' "$*"; }
fail() { printf '[ERR] %s\n' "$*" >&2; exit 1; }
for command in git docker curl ssh; do
command -v "$command" >/dev/null 2>&1 || fail "构建机缺少命令: $command"
done
curl -fsS "http://127.0.0.1:${REGISTRY_HOST##*:}/v2/" >/dev/null \
|| fail '构建机内部镜像仓库不可用'
ssh -o BatchMode=yes -o ConnectTimeout=10 "$PRODUCTION_SERVER" 'true' \
|| fail '构建机无法连接生产机'
if [ ! -d "$BUILD_DIR/.git" ]; then
rm -rf "$BUILD_DIR"
git clone "$REPO_URL" "$BUILD_DIR"
fi
cd "$BUILD_DIR"
git remote set-url origin "$REPO_URL"
git fetch --prune origin "$BRANCH"
target="origin/$BRANCH"
if [ -n "$COMMIT_INPUT" ]; then
target="$COMMIT_INPUT"
fi
git rev-parse --verify "${target}^{commit}" >/dev/null
if [ "$DRY_RUN" = 1 ]; then
ok "预检通过,目标提交: $(git rev-parse "${target}^{commit}")"
exit 0
fi
git checkout --detach "$target"
git reset --hard "$target"
git clean -fdx
commit_sha="$(git rev-parse HEAD)"
short_sha="$(git rev-parse --short=12 HEAD)"
image_tag="production-${short_sha}"
local_commit="localhost:${REGISTRY_HOST##*:}/${IMAGE_NAME}:${image_tag}"
local_latest="localhost:${REGISTRY_HOST##*:}/${IMAGE_NAME}:production-latest"
remote_commit="${REGISTRY_HOST}/${IMAGE_NAME}:${image_tag}"
base_image="localhost:${REGISTRY_HOST##*:}/yyd-base/node:22-alpine"
log "构建提交 ${commit_sha}"
docker pull "$base_image" >/dev/null \
|| fail "内部基础镜像不可用: $base_image"
docker pull "$local_latest" >/dev/null 2>&1 || true
build_args=(--build-arg "BASE_IMAGE=$base_image")
if docker image inspect "$local_latest" >/dev/null 2>&1; then
build_args+=(--cache-from "$local_latest")
fi
DOCKER_BUILDKIT=1 docker build "${build_args[@]}" \
-t "$local_commit" \
-t "$local_latest" \
.
docker push "$local_commit"
docker push "$local_latest"
ok "镜像已推送: $remote_commit"
if ! ssh -o BatchMode=yes -o ConnectTimeout=10 "$PRODUCTION_SERVER" \
"docker pull '$remote_commit'"; then
log '生产机无法直接拉取,改用 SSH 传输镜像'
docker save "$local_commit" | ssh -o BatchMode=yes "$PRODUCTION_SERVER" docker load
ssh -o BatchMode=yes "$PRODUCTION_SERVER" \
"docker tag '$local_commit' '$remote_commit'"
fi
ssh -o BatchMode=yes "$PRODUCTION_SERVER" \
"REMOTE_IMAGE='$remote_commit' IMAGE_TAG='$image_tag' COMMIT_SHA='$commit_sha' bash -s" <<'PRODUCTION'
set -Eeuo pipefail
target=/mnt/PoolOne/apps/youyidian-notification-center
backup_root=/mnt/PoolOne/apps/youyidian-notification-center-backups
cd "$target"
test -f .env
test -f docker-compose.yml
mkdir -p "$backup_root"
stamp="$(date +%Y%m%d%H%M%S)"
cp -a .env "$backup_root/env-before-git-release-$stamp"
chmod 600 "$backup_root/env-before-git-release-$stamp"
if docker container inspect youyidian-notification-center >/dev/null 2>&1; then
was_running="$(docker inspect -f '{{.State.Running}}' youyidian-notification-center)"
if [ "$was_running" = true ]; then
docker pause youyidian-notification-center >/dev/null
cleanup_unpause() {
docker unpause youyidian-notification-center >/dev/null 2>&1 || true
}
trap cleanup_unpause EXIT INT TERM
fi
data_backup="$backup_root/notification-data-before-git-release-$stamp"
mkdir -p "$data_backup"
# 整体复制可同时保存 notifications.db、-journal、-wal 与 -shm。
if ! docker cp youyidian-notification-center:/app/data/. "$data_backup/" || ! test -s "$data_backup/notifications.db"; then
fail_message='通知中心 SQLite 备份失败,拒绝重建生产容器'
if [ "$was_running" = true ]; then
docker unpause youyidian-notification-center >/dev/null || true
trap - EXIT INT TERM
fi
printf '[ERR] %s\n' "$fail_message" >&2
exit 1
fi
if [ "$was_running" = true ]; then
docker unpause youyidian-notification-center >/dev/null
trap - EXIT INT TERM
fi
fi
old_image="$(docker inspect -f '{{.Config.Image}}' youyidian-notification-center 2>/dev/null || true)"
if [ -n "$old_image" ] && docker image inspect "$old_image" >/dev/null 2>&1; then
docker tag "$old_image" youyidian-notification-center:rollback-before-release
fi
docker tag "$REMOTE_IMAGE" "youyidian-notification-center:$IMAGE_TAG"
YYD_NOTIFICATION_IMAGE_TAG="$IMAGE_TAG" docker compose up -d --no-build --force-recreate
healthy=0
for _ in $(seq 1 30); do
if curl -fsS --max-time 3 http://127.0.0.1:3088/health >/tmp/yyd-notification-health.json; then
healthy=1
break
fi
sleep 2
done
if [ "$healthy" != 1 ]; then
docker compose logs --tail 120
if docker image inspect youyidian-notification-center:rollback-before-release >/dev/null 2>&1; then
YYD_NOTIFICATION_IMAGE_TAG=rollback-before-release docker compose up -d --no-build --force-recreate
fi
exit 1
fi
printf '%s' "$COMMIT_SHA" > DEPLOYED_COMMIT
curl -fsS --max-time 10 https://sc.jiansun.vip/notification-center/health
printf '\nDEPLOYED_COMMIT='
cat DEPLOYED_COMMIT
printf '\nCONTAINER_IMAGE='
docker inspect -f '{{.Config.Image}}' youyidian-notification-center
printf '\n'
PRODUCTION
ok "通知中心已部署到 ${commit_sha}"
'@
$replacements = @{
'__REPO_URL__' = ConvertTo-ShellSingleQuoted $RepoUrl
'__BRANCH__' = ConvertTo-ShellSingleQuoted $Branch
'__COMMIT__' = ConvertTo-ShellSingleQuoted $Commit
'__BUILD_DIR__' = ConvertTo-ShellSingleQuoted $RemoteBuildDir
'__PRODUCTION_SERVER__' = ConvertTo-ShellSingleQuoted $ProductionServer
'__REGISTRY_HOST__' = ConvertTo-ShellSingleQuoted $RegistryHost
'__IMAGE_NAME__' = ConvertTo-ShellSingleQuoted $ImageName
'__DRY_RUN__' = if ($DryRun) { '1' } else { '0' }
}
foreach ($entry in $replacements.GetEnumerator()) {
$remoteScript = $remoteScript.Replace($entry.Key, $entry.Value)
}
$remoteScript = $remoteScript.Replace("`r`n", "`n")
$localTemp = Join-Path ([System.IO.Path]::GetTempPath()) ("yyd-notification-deploy-{0}.sh" -f [guid]::NewGuid().ToString('N'))
$remoteTemp = "/tmp/$(Split-Path $localTemp -Leaf)"
try {
[System.IO.File]::WriteAllText(
$localTemp,
$remoteScript,
[System.Text.UTF8Encoding]::new($false)
)
& scp -o BatchMode=yes -o ConnectTimeout=10 $localTemp "${BuildServer}:$remoteTemp"
if ($LASTEXITCODE -ne 0) {
throw "通知中心发布脚本上传失败,退出码: $LASTEXITCODE"
}
& ssh -o BatchMode=yes -o ConnectTimeout=10 $BuildServer "chmod 700 '$remoteTemp' && bash '$remoteTemp'; code=`$?; rm -f '$remoteTemp'; exit `$code"
if ($LASTEXITCODE -ne 0) {
throw "通知中心构建机发布失败,退出码: $LASTEXITCODE"
}
}
finally {
Remove-Item -LiteralPath $localTemp -Force -ErrorAction SilentlyContinue
}