211 lines
7.5 KiB
PowerShell
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
|
|
}
|