fix: 统一前端Harbor镜像发布与拉取部署
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
# sw2 Docker 部署
|
||||
|
||||
前端镜像统一发布至 `harbor.fuhua91.com/game/sw2:<版本>`。发布服务器需先通过 `docker login harbor.fuhua91.com` 配置 push/pull 权限;脚本在替换容器前完成推送、拉取和摘要校验。前端独立发布入口位于前端仓库 `deploy/`,也支持直接指定 Harbor 版本拉取部署。
|
||||
|
||||
沿用 226 服务器 `/www/wwwroot/live.fuhua91.com` 的 Docker 网络、持久化目录和宝塔反向代理结构。使用独立的 `sw2-backend`、`sw2-frontend`、`sw2-net`,兼容服务器现有 Docker 18.06,不依赖 Compose。
|
||||
|
||||
## 目录与端口
|
||||
|
||||
+12
-1
@@ -8,7 +8,7 @@ MODE="${2:-}"
|
||||
BASE_DIR=/www/wwwroot/sw2.fuhua91.com
|
||||
RELEASE="$BASE_DIR/releases/$TAG"
|
||||
BACKEND_IMAGE="sw2/backend:$TAG"
|
||||
FRONTEND_IMAGE="sw2/frontend:$TAG"
|
||||
FRONTEND_IMAGE="harbor.fuhua91.com/game/sw2:$TAG"
|
||||
NETWORK=sw2-net
|
||||
NAMES=(sw2-backend sw2-frontend)
|
||||
|
||||
@@ -19,6 +19,17 @@ test -f "$RELEASE/frontend/Dockerfile"
|
||||
|
||||
docker build --build-arg "VERSION=$TAG" -t "$BACKEND_IMAGE" "$RELEASE/backend"
|
||||
docker build --build-arg "VERSION=$TAG" -t "$FRONTEND_IMAGE" "$RELEASE/frontend"
|
||||
registry_args=()
|
||||
if [[ -s "$BASE_DIR/harbor-docker/config.json" ]]; then registry_args=(--config "$BASE_DIR/harbor-docker"); fi
|
||||
docker "${registry_args[@]}" push "$FRONTEND_IMAGE"
|
||||
docker "${registry_args[@]}" pull "$FRONTEND_IMAGE"
|
||||
frontend_digests=$(docker inspect --format '{{range .RepoDigests}}{{println .}}{{end}}' "$FRONTEND_IMAGE")
|
||||
frontend_ref=''
|
||||
while IFS= read -r reference; do
|
||||
if [[ "$reference" == 'harbor.fuhua91.com/game/sw2@sha256:'* && "${reference##*@}" =~ ^sha256:[a-f0-9]{64}$ ]]; then frontend_ref="$reference"; break; fi
|
||||
done <<< "$frontend_digests"
|
||||
[[ -n "$frontend_ref" ]] || { echo 'Harbor frontend digest is missing' >&2; exit 1; }
|
||||
FRONTEND_IMAGE="$frontend_ref"
|
||||
docker run --rm "$BACKEND_IMAGE" /app/kratos-admin -h >/dev/null
|
||||
docker run --rm --add-host sw2-backend:127.0.0.1 "$FRONTEND_IMAGE" nginx -t
|
||||
if [[ "$MODE" == '--build-only' ]]; then exit 0; fi
|
||||
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
# 与服务器 deploy.sh 对齐:
|
||||
# docker build --build-arg VERSION=$TAG -t sw2/frontend:$TAG .
|
||||
# docker run --rm --add-host sw2-backend:127.0.0.1 sw2/frontend:$TAG nginx -t
|
||||
# docker build --build-arg VERSION=$TAG -t harbor.fuhua91.com/game/sw2:$TAG .
|
||||
# docker push harbor.fuhua91.com/game/sw2:$TAG
|
||||
# 运行阶段与部署侧 frontend.Dockerfile 一致。本仓库在镜像内编译,
|
||||
# 产物对应部署侧的 COPY dist/。
|
||||
|
||||
@@ -19,5 +19,6 @@ ARG VERSION
|
||||
LABEL org.opencontainers.image.revision=$VERSION
|
||||
COPY nginx.sw2.conf /etc/nginx/conf.d/default.conf
|
||||
COPY --from=build /src/dist /usr/share/nginx/html/
|
||||
RUN printf '%s\n' "$VERSION" > /usr/share/nginx/html/version.txt
|
||||
HEALTHCHECK --interval=15s --timeout=5s --start-period=10s --retries=3 \
|
||||
CMD wget -q -O /dev/null http://127.0.0.1/healthz || exit 1
|
||||
|
||||
+3
-1
@@ -22,7 +22,9 @@ pnpm build
|
||||
```sh
|
||||
pnpm package:test # 构建并生成带 SHA-256 的前端发布包
|
||||
pnpm build:docker # 在 Docker 中从前端源码构建镜像
|
||||
pnpm deploy:test # 打包并只更新测试服前端
|
||||
pnpm push:docker # 构建并推送 harbor.fuhua91.com/game/sw2:<版本>
|
||||
pnpm deploy:test # 打包、推送 Harbor 并只更新测试服前端
|
||||
pnpm deploy:test --image-tag <版本> # 直接拉取已推送的 Harbor 镜像部署
|
||||
```
|
||||
|
||||
复用发布包、仅预检、SSH 参数及回退方法见 [前端独立部署说明](deploy/README.md)。
|
||||
|
||||
+30
-11
@@ -34,52 +34,70 @@ pnpm package:test --tag web-20261105-01
|
||||
|
||||
`--skip-install` 可在已经按锁文件安装依赖后使用;`--output <目录>` 可指定包输出目录。在 `sw-new/web` 中开发时也可运行:检查只覆盖前端,后端本地配置不影响打包。若已在本地生成分仓提交,可传 `--source-ref codex/publish-web`,脚本会验证该提交的根目录与当前前端源码树一致。
|
||||
|
||||
## 3. 从前端源码构建 Docker 镜像
|
||||
## 3. 构建并推送 Harbor 镜像
|
||||
|
||||
```sh
|
||||
pnpm build:docker
|
||||
pnpm build:docker --tag web-20261105-01
|
||||
# 构建后推送到 Harbor
|
||||
docker login harbor.fuhua91.com
|
||||
pnpm push:docker --tag web-20261105-01
|
||||
```
|
||||
|
||||
生成 `sw2/frontend:<版本>`。`deploy/Dockerfile` 使用多阶段构建,在 Node 容器安装依赖并构建,再将产物放入 Nginx 镜像。`deploy/runtime.Dockerfile` 用于上面的发布包,服务器无需安装 Node。
|
||||
镜像仓库统一为 **`harbor.fuhua91.com/game/sw2:<版本>`**,默认版本仍为源码提交号前 12 位。`build:docker` 只构建,`push:docker` 构建并推送到 Harbor 的 `game/sw2` 仓库。使用已有 Docker 登录凭据或机器人账号,本机推送账号需要 `game/sw2` 的 push 权限,服务器拉取账号需要 pull 权限;密码不写入脚本或发布包。
|
||||
|
||||
仓库根目录原有 `Dockerfile` 仍供 Kubernetes/Jenkins 使用,继续读取 `nginx.k8s.conf`;独立测试服发布使用 `deploy/` 下的 Dockerfile。
|
||||
`deploy/Dockerfile` 在 Node 容器内完成源码构建,`deploy/runtime.Dockerfile` 用于发布包的服务器构建。根目录 `Dockerfile` 与 Windows `pnpm pack:docker <版本>` 入口也使用上述 Harbor 仓库;`pack:docker` 构建并推送,传 `-SaveArchive` 可额外导出离线镜像到忽略的 `.cache/docker/`。
|
||||
|
||||
本机构建检测到 Buildx 时,输出 Linux amd64 的单平台 Docker 镜像并关闭默认证明附件,兼容测试服 Docker 18.06;没有 Buildx 时使用传统构建器。
|
||||
|
||||
## 4. 发布到 sw2 测试服
|
||||
|
||||
默认连接 `root@192.168.2.226:22`,部署根目录 `/www/wwwroot/sw2.fuhua91.com`。使用本机 SSH 密钥或 SSH agent;首次使用先用 `ssh root@192.168.2.226` 确认主机指纹和登录权限。脚本使用非交互 SSH,仓库不保存密码。
|
||||
|
||||
```sh
|
||||
# 构建、上传、校验 SHA-256 并发布
|
||||
# 本机打包,服务器构建并推送 Harbor,再拉取部署
|
||||
pnpm deploy:test
|
||||
|
||||
# 已通过 push:docker 推送:测试服直接从 Harbor 拉取,不再上传包或重新构建
|
||||
pnpm deploy:test --image-tag web-20261105-01
|
||||
|
||||
# 复用已经生成的包
|
||||
pnpm deploy:test --archive .cache/deploy/sw2-frontend-web-20261105-01.tar.gz
|
||||
|
||||
# 先在服务器构建镜像、验证页面版本和 /api/health,保持当前容器运行
|
||||
# 构建、推送、拉取并预检,保持当前容器运行
|
||||
pnpm deploy:test --archive .cache/deploy/sw2-frontend-web-20261105-01.tar.gz --build-only
|
||||
# 也可仅预检 Harbor 中的现有镜像
|
||||
pnpm deploy:test --image-tag web-20261105-01 --build-only
|
||||
|
||||
# 使用其他 SSH 账号、端口或密钥
|
||||
pnpm deploy:test --archive <发布包.tar.gz> --user deploy --port 22 --identity <私钥路径>
|
||||
```
|
||||
|
||||
可用 `--host` 与 `--directory` 覆盖测试服务器和部署根目录。服务器账号需要该目录写权限及 Docker 权限。现有的 `sw2-net` 网络、`sw2-backend:8000` 服务与宝塔反向代理应已就绪;脚本兼容测试服 Docker 18.06,不依赖 Docker Compose。
|
||||
可用 `--host` 与 `--directory` 覆盖测试服务器和部署根目录。服务器账号需要该目录写权限及 Docker 权限,并已执行 `docker login harbor.fuhua91.com`。发布包模式在服务器执行 push,需 push/pull 权限;`--image-tag` 模式只需 pull 权限。现有的 `sw2-net` 网络、`sw2-backend:8000` 服务与宝塔反向代理应已就绪;服务端脚本兼容 Docker 18.06,不依赖 Docker Compose。
|
||||
|
||||
若服务器已有其他项目的 Docker 登录,可为本项目前端单独配置。脚本优先读取部署根目录 `harbor-docker/config.json`,不存在时使用当前账号默认 Docker 配置:
|
||||
|
||||
```sh
|
||||
install -d -m 700 /www/wwwroot/sw2.fuhua91.com/harbor-docker
|
||||
docker --config /www/wwwroot/sw2.fuhua91.com/harbor-docker login harbor.fuhua91.com
|
||||
chmod 600 /www/wwwroot/sw2.fuhua91.com/harbor-docker/config.json
|
||||
```
|
||||
|
||||
发布过程:
|
||||
|
||||
1. 验证包结构和版本,上传至 `frontend-releases/`,校验 SHA-256。相同版本只接受同一个发布包。
|
||||
2. 构建镜像,运行临时前端容器,检查 `/healthz`、`/version.txt` 和 `/api/health`。
|
||||
2. 发布包模式先构建并推送 `harbor.fuhua91.com/game/sw2:<版本>`;两种模式随后都从 Harbor 拉取,取得镜像摘要,按 `harbor.fuhua91.com/game/sw2@sha256:…` 运行临时容器并检查 `/healthz`、`/version.txt` 和 `/api/health`。镜像推送或拉取失败时,当前容器继续运行。
|
||||
3. 预检通过后,只替换 `sw2-frontend`,仍绑定 `127.0.0.1:9601`。上一版保留为停止状态的 `sw2-frontend-previous`。
|
||||
4. 再次检查新容器;失败自动恢复上一版。成功后写入 `current-frontend-release`,后端容器、数据库、后端配置及联合发布的 `current-release` 均不修改。
|
||||
4. 正式容器使用同一个已验证的镜像摘要;再次检查失败时自动恢复上一版。成功后写入 `current-frontend-release`,镜像摘要保存在 `frontend-images/<版本>.ref`。后端容器、数据库、后端配置及联合发布的 `current-release` 均不修改。
|
||||
|
||||
Nginx 使用 Docker DNS 动态解析后端地址,后端独立重建后无需重建前端镜像。对外仍访问 `http://sw2.fuhua91.com`。
|
||||
|
||||
## 回退与验证
|
||||
|
||||
重新发布保留的旧前端包即可回退,仍只操作前端:
|
||||
指定 Harbor 中保留的旧版本即可回退,仍只操作前端:
|
||||
|
||||
```sh
|
||||
pnpm deploy:test --archive .cache/deploy/sw2-frontend-<旧版本>.tar.gz
|
||||
pnpm deploy:test --image-tag <旧版本>
|
||||
```
|
||||
|
||||
服务端手工执行同样的脚本:
|
||||
@@ -88,9 +106,10 @@ pnpm deploy:test --archive .cache/deploy/sw2-frontend-<旧版本>.tar.gz
|
||||
bash /www/wwwroot/sw2.fuhua91.com/frontend-releases/<版本>/deploy.sh <版本>
|
||||
curl -fsS http://127.0.0.1:9601/version.txt
|
||||
curl -fsS http://127.0.0.1:9601/api/health
|
||||
docker inspect --format '{{.Config.Image}}' sw2-frontend
|
||||
```
|
||||
|
||||
发布脚本通过文件锁阻止并发前端发布。永久回归测试覆盖包路径与版本验证、预检失败、发布后失败回退、只构建模式,以及后端容器保持不变:
|
||||
发布脚本通过文件锁阻止并发前端发布。永久回归测试覆盖包路径与版本验证、Harbor 推送/拉取失败、缺失镜像摘要、直接拉取模式、预检失败及发布后回退,以及后端容器保持不变:
|
||||
|
||||
```sh
|
||||
pnpm test:release
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { options, sourceInfo, run, fail } from './release.mjs'
|
||||
import { options, sourceInfo, frontendImage, run, fail } from './release.mjs'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
|
||||
try {
|
||||
const args = options({ tag: { type: 'string' }, 'source-ref': { type: 'string' } })
|
||||
if (args.help) console.log('pnpm build:docker [--tag VERSION] [--source-ref FRONTEND_COMMIT]')
|
||||
const args = options({ tag: { type: 'string' }, 'source-ref': { type: 'string' }, push: { type: 'boolean' } })
|
||||
if (args.help) console.log('pnpm build:docker [--tag VERSION] [--source-ref FRONTEND_COMMIT] [--push]\npnpm push:docker 使用相同参数并将镜像推送到 Harbor。')
|
||||
else {
|
||||
const info = sourceInfo(args)
|
||||
run('docker', ['build', '-f', 'deploy/Dockerfile', '--build-arg', `VERSION=${info.version}`, '--build-arg', `SOURCE_REVISION=${info.revision}`, '--build-arg', `SOURCE_TREE=${info.tree}`, '-t', `sw2/frontend:${info.version}`, '.'])
|
||||
console.log(`镜像:sw2/frontend:${info.version}`)
|
||||
const image = frontendImage(info.version)
|
||||
const buildx = spawnSync('docker', ['buildx', 'version'], { stdio: 'ignore' }).status === 0
|
||||
// Export a single Docker-format image for the test server's Docker 18.06.
|
||||
const build = buildx ? ['buildx', 'build', '--platform', 'linux/amd64', '--output', 'type=docker'] : ['build']
|
||||
run('docker', [...build, '-f', 'deploy/Dockerfile', '--build-arg', `VERSION=${info.version}`, '--build-arg', `SOURCE_REVISION=${info.revision}`, '--build-arg', `SOURCE_TREE=${info.tree}`, '-t', image, '.'], { env: { ...process.env, BUILDX_NO_DEFAULT_ATTESTATIONS: '1' } })
|
||||
if (args.push) run('docker', ['push', image])
|
||||
console.log(`${args.push ? '已推送镜像' : '镜像'}:${image}`)
|
||||
}
|
||||
} catch (error) { fail(error) }
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import crypto from 'node:crypto'
|
||||
import path from 'node:path'
|
||||
import fs from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { options, packageRelease, inspectPackage, deploymentTarget, shellQuote, run, fail } from './release.mjs'
|
||||
import { options, packageRelease, inspectPackage, deploymentTarget, shellQuote, frontendRoot, frontendImage, validTag, run, fail } from './release.mjs'
|
||||
|
||||
export const installScript = `set -euo pipefail
|
||||
BASE_DIR=$(cd -- "$1" && pwd -P)
|
||||
@@ -34,17 +35,26 @@ SW_DEPLOY_BASE="$BASE_DIR" bash "$RELEASE/deploy.sh" "$TAG" "$MODE"
|
||||
function main() {
|
||||
const args = options({
|
||||
archive: { type: 'string' }, tag: { type: 'string' }, 'source-ref': { type: 'string' }, 'skip-install': { type: 'boolean' },
|
||||
'image-tag': { type: 'string' },
|
||||
host: { type: 'string' }, user: { type: 'string' }, port: { type: 'string' }, identity: { type: 'string' }, directory: { type: 'string' }, 'build-only': { type: 'boolean' }
|
||||
})
|
||||
if (args.help) {
|
||||
console.log('pnpm deploy:test [--archive FILE.tar.gz] [--tag VERSION] [--build-only] [--host 192.168.2.226] [--user root] [--port 22] [--identity SSH_KEY] [--directory /www/wwwroot/sw2.fuhua91.com]\n不指定 --archive 时先独立构建前端;--build-only 仅在服务器构建并预检,不替换当前容器。')
|
||||
console.log('pnpm deploy:test [--archive FILE.tar.gz | --image-tag VERSION] [--tag VERSION] [--build-only] [--host 192.168.2.226] [--user root] [--port 22] [--identity SSH_KEY] [--directory /www/wwwroot/sw2.fuhua91.com]\n默认打包并在服务器构建、推送 Harbor 后部署;--image-tag 直接拉取已有 Harbor 镜像;--build-only 仅预检,不替换当前容器。')
|
||||
return
|
||||
}
|
||||
if (args.archive && (args.tag || args['source-ref'] || args['skip-install'])) throw new Error('--archive 复用现有包,不能再指定构建参数')
|
||||
if (args['image-tag'] && (args.archive || args.tag || args['source-ref'] || args['skip-install'])) throw new Error('--image-tag 直接使用 Harbor 镜像,不能同时指定发布包或构建参数')
|
||||
const target = deploymentTarget(args)
|
||||
const release = args.archive ? inspectPackage(args.archive) : inspectPackage(packageRelease(args).archive)
|
||||
const identity = args.identity ? ['-i', path.resolve(args.identity)] : []
|
||||
const ssh = ['-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', '-p', target.port, ...identity, target.remote]
|
||||
if (args['image-tag']) {
|
||||
const tag = validTag(args['image-tag'])
|
||||
const command = `SW_DEPLOY_BASE=${shellQuote(target.base)} bash -s -- ${shellQuote(tag)} --pull-only${args['build-only'] ? ' --build-only' : ''}`
|
||||
run('ssh', [...ssh, command], { stdio: ['pipe', 'inherit', 'inherit'], input: fs.readFileSync(path.join(frontendRoot, 'deploy/deploy.sh'), 'utf8').replaceAll('\r\n', '\n') })
|
||||
console.log(`${args['build-only'] ? 'Harbor 镜像预检通过' : 'Harbor 镜像部署完成'}:${frontendImage(tag)}`)
|
||||
return
|
||||
}
|
||||
const release = args.archive ? inspectPackage(args.archive) : inspectPackage(packageRelease(args).archive)
|
||||
const uploads = `${target.base}/frontend-releases`
|
||||
const uploaded = `${uploads}/.upload-${release.info.version}-${crypto.randomBytes(6).toString('hex')}.tar.gz`
|
||||
run('ssh', [...ssh, `test -d ${shellQuote(target.base)} && test -w ${shellQuote(target.base)} && mkdir -p ${shellQuote(uploads)}`])
|
||||
|
||||
+48
-15
@@ -2,18 +2,32 @@
|
||||
set -Eeuo pipefail
|
||||
umask 027
|
||||
|
||||
TAG="${1:?Usage: deploy.sh VERSION [--build-only]}"
|
||||
MODE="${2:-}"
|
||||
TAG="${1:?Usage: deploy.sh VERSION [--build-only] [--pull-only]}"
|
||||
shift
|
||||
BUILD_ONLY=false
|
||||
PULL_ONLY=false
|
||||
for mode in "$@"; do
|
||||
case "$mode" in
|
||||
'') ;;
|
||||
--build-only) BUILD_ONLY=true ;;
|
||||
--pull-only) PULL_ONLY=true ;;
|
||||
*) echo 'Invalid deployment mode' >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
[[ "$TAG" =~ ^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,79}$ ]] || { echo 'Invalid version' >&2; exit 1; }
|
||||
[[ -z "$MODE" || "$MODE" == '--build-only' ]] || { echo 'Invalid deployment mode' >&2; exit 1; }
|
||||
BASE_DIR="${SW_DEPLOY_BASE:-/www/wwwroot/sw2.fuhua91.com}"
|
||||
BASE_DIR=$(cd -- "$BASE_DIR" && pwd -P)
|
||||
RELEASE=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)
|
||||
[[ "$RELEASE" == "$BASE_DIR/frontend-releases/$TAG" ]] || { echo 'Release directory does not match version' >&2; exit 1; }
|
||||
if [[ -s "$BASE_DIR/harbor-docker/config.json" ]]; then export DOCKER_CONFIG="$BASE_DIR/harbor-docker"; fi
|
||||
RELEASE=''
|
||||
if [[ "$PULL_ONLY" == false ]]; then
|
||||
RELEASE=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)
|
||||
[[ "$RELEASE" == "$BASE_DIR/frontend-releases/$TAG" ]] || { echo 'Release directory does not match version' >&2; exit 1; }
|
||||
fi
|
||||
NAME=sw2-frontend
|
||||
PREVIOUS=sw2-frontend-previous
|
||||
CANDIDATE="sw2-frontend-candidate-$$"
|
||||
IMAGE="sw2/frontend:$TAG"
|
||||
IMAGE_REPOSITORY=harbor.fuhua91.com/game/sw2
|
||||
IMAGE="$IMAGE_REPOSITORY:$TAG"
|
||||
NETWORK=sw2-net
|
||||
ATTEMPTS="${SW_HEALTH_ATTEMPTS:-20}"
|
||||
[[ "$ATTEMPTS" =~ ^[1-9][0-9]?$ ]] || { echo 'Invalid health-check attempts' >&2; exit 1; }
|
||||
@@ -34,8 +48,10 @@ if exists "$PREVIOUS"; then
|
||||
fi
|
||||
! exists "$CANDIDATE" || { echo 'Candidate container already exists' >&2; exit 1; }
|
||||
docker network inspect "$NETWORK" >/dev/null
|
||||
test -s "$RELEASE/dist/index.html"
|
||||
[[ "$(cat "$RELEASE/dist/version.txt")" == "$TAG" ]] || { echo 'Frontend version marker mismatch' >&2; exit 1; }
|
||||
if [[ "$PULL_ONLY" == false ]]; then
|
||||
test -s "$RELEASE/dist/index.html"
|
||||
[[ "$(cat "$RELEASE/dist/version.txt")" == "$TAG" ]] || { echo 'Frontend version marker mismatch' >&2; exit 1; }
|
||||
fi
|
||||
|
||||
old_present=false
|
||||
old_stopped=false
|
||||
@@ -81,15 +97,32 @@ healthy() {
|
||||
return 1
|
||||
}
|
||||
|
||||
docker build --build-arg "VERSION=$TAG" -t "$IMAGE" "$RELEASE"
|
||||
docker run --rm "$IMAGE" nginx -t
|
||||
if [[ "$PULL_ONLY" == false ]]; then
|
||||
docker build --build-arg "VERSION=$TAG" -t "$IMAGE" "$RELEASE"
|
||||
docker push "$IMAGE"
|
||||
fi
|
||||
# Resolve the registry result and pin every run to its digest. Authentication
|
||||
# uses the server's docker login; credentials never enter the release package.
|
||||
docker pull "$IMAGE"
|
||||
digests=$(docker inspect --format '{{range .RepoDigests}}{{println .}}{{end}}' "$IMAGE")
|
||||
IMAGE_REF=''
|
||||
while IFS= read -r reference; do
|
||||
if [[ "$reference" == "$IMAGE_REPOSITORY@sha256:"* && "${reference##*@}" =~ ^sha256:[a-f0-9]{64}$ ]]; then
|
||||
IMAGE_REF="$reference"
|
||||
break
|
||||
fi
|
||||
done <<< "$digests"
|
||||
[[ -n "$IMAGE_REF" ]] || { echo 'Harbor image digest is missing' >&2; exit 1; }
|
||||
mkdir -p "$BASE_DIR/frontend-images"
|
||||
printf '%s\n' "$IMAGE_REF" > "$BASE_DIR/frontend-images/$TAG.ref"
|
||||
docker run --rm "$IMAGE_REF" nginx -t
|
||||
docker run -d --name "$CANDIDATE" --label com.sw2.application=sw2 --label com.sw2.component=frontend \
|
||||
--network "$NETWORK" -p 127.0.0.1::80 "$IMAGE" >/dev/null
|
||||
--network "$NETWORK" -p 127.0.0.1::80 "$IMAGE_REF" >/dev/null
|
||||
candidate_address=$(docker port "$CANDIDATE" 80/tcp)
|
||||
[[ "$candidate_address" =~ ^127\.0\.0\.1:[0-9]+$ ]] || { echo 'Unexpected candidate binding' >&2; exit 1; }
|
||||
healthy "http://$candidate_address" || { echo 'Candidate frontend or API proxy failed preflight' >&2; exit 1; }
|
||||
if [[ "$MODE" == '--build-only' ]]; then
|
||||
echo "Frontend image and API proxy preflight passed: $TAG"
|
||||
if [[ "$BUILD_ONLY" == true ]]; then
|
||||
echo "Harbor frontend image and API proxy preflight passed: $IMAGE_REF"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -104,7 +137,7 @@ created=true
|
||||
docker run -d --name "$NAME" --label com.sw2.application=sw2 --label com.sw2.component=frontend \
|
||||
--restart unless-stopped --network "$NETWORK" \
|
||||
--log-opt max-size=20m --log-opt max-file=3 \
|
||||
-p 127.0.0.1:9601:80 "$IMAGE" >/dev/null
|
||||
-p 127.0.0.1:9601:80 "$IMAGE_REF" >/dev/null
|
||||
healthy http://127.0.0.1:9601 || { echo 'Published frontend failed health checks' >&2; exit 1; }
|
||||
|
||||
if [[ -n "$old_version" ]]; then
|
||||
@@ -114,4 +147,4 @@ fi
|
||||
printf '%s\n' "$TAG" > "$BASE_DIR/current-frontend-release.tmp"
|
||||
mv -f -- "$BASE_DIR/current-frontend-release.tmp" "$BASE_DIR/current-frontend-release"
|
||||
committed=true
|
||||
printf 'Frontend deployed: %s\nBackend container was not modified.\n' "$TAG"
|
||||
printf 'Frontend deployed: %s\nImage: %s\nBackend container was not modified.\n' "$TAG" "$IMAGE_REF"
|
||||
|
||||
+43
-10
@@ -17,6 +17,11 @@ case "$command" in
|
||||
inspect)
|
||||
format=''
|
||||
if [[ "$1" == --format ]]; then format=$2; shift 2; fi
|
||||
if [[ "$format" == *RepoDigests* ]]; then
|
||||
[[ "$1" == harbor.fuhua91.com/game/sw2:new ]] || exit 1
|
||||
if [[ "$TEST_MODE" != digest-missing ]]; then printf 'harbor.fuhua91.com/game/sw2@sha256:%064d\n' 1; fi
|
||||
exit 0
|
||||
fi
|
||||
[[ -d "$TEST_STATE/$1" ]] || exit 1
|
||||
case "$format" in
|
||||
*com.sw2.application*) cat "$TEST_STATE/$1/app" ;;
|
||||
@@ -24,6 +29,8 @@ case "$command" in
|
||||
*State.Running*) cat "$TEST_STATE/$1/running" ;;
|
||||
esac ;;
|
||||
network|build) exit 0 ;;
|
||||
push) [[ "$TEST_MODE" != push-fail ]] ;;
|
||||
pull) [[ "$TEST_MODE" != pull-fail ]] ;;
|
||||
run)
|
||||
name=''; version='new'
|
||||
while (($#)); do
|
||||
@@ -60,14 +67,17 @@ chmod +x "$TEST_ROOT/bin/"*
|
||||
export PATH="$TEST_ROOT/bin:$PATH"
|
||||
export SW_HEALTH_ATTEMPTS=1
|
||||
|
||||
for scenario in success preflight-fail final-fail build-only unmanaged; do
|
||||
for scenario in success preflight-fail final-fail build-only pull-only pull-preflight push-fail pull-fail digest-missing unmanaged; do
|
||||
export TEST_STATE="$TEST_ROOT/$scenario"
|
||||
export TEST_MODE="$scenario"
|
||||
export SW_DEPLOY_BASE="$TEST_STATE/base"
|
||||
mkdir -p "$SW_DEPLOY_BASE/frontend-releases/new/dist" "$TEST_STATE/sw2-frontend" "$TEST_STATE/sw2-backend"
|
||||
cp "$SCRIPT" "$SW_DEPLOY_BASE/frontend-releases/new/deploy.sh"
|
||||
printf '<html>test</html>' > "$SW_DEPLOY_BASE/frontend-releases/new/dist/index.html"
|
||||
printf 'new\n' > "$SW_DEPLOY_BASE/frontend-releases/new/dist/version.txt"
|
||||
mkdir -p "$SW_DEPLOY_BASE" "$TEST_STATE/sw2-frontend" "$TEST_STATE/sw2-backend"
|
||||
if [[ "$scenario" != pull-only && "$scenario" != pull-preflight ]]; then
|
||||
mkdir -p "$SW_DEPLOY_BASE/frontend-releases/new/dist"
|
||||
cp "$SCRIPT" "$SW_DEPLOY_BASE/frontend-releases/new/deploy.sh"
|
||||
printf '<html>test</html>' > "$SW_DEPLOY_BASE/frontend-releases/new/dist/index.html"
|
||||
printf 'new\n' > "$SW_DEPLOY_BASE/frontend-releases/new/dist/version.txt"
|
||||
fi
|
||||
printf 'old\n' > "$SW_DEPLOY_BASE/current-frontend-release"
|
||||
printf 'sw2' > "$TEST_STATE/sw2-frontend/app"
|
||||
printf 'old' > "$TEST_STATE/sw2-frontend/version"
|
||||
@@ -75,14 +85,21 @@ for scenario in success preflight-fail final-fail build-only unmanaged; do
|
||||
printf 'old-container' > "$TEST_STATE/sw2-frontend/id"
|
||||
printf 'backend-unchanged' > "$TEST_STATE/sw2-backend/id"
|
||||
if [[ "$scenario" == unmanaged ]]; then printf 'other' > "$TEST_STATE/sw2-frontend/app"; fi
|
||||
mode=''; [[ "$scenario" != build-only ]] || mode='--build-only'
|
||||
result=0
|
||||
bash "$SW_DEPLOY_BASE/frontend-releases/new/deploy.sh" new "$mode" > "$TEST_STATE/output" 2>&1 || result=$?
|
||||
modes=()
|
||||
case "$scenario" in
|
||||
success)
|
||||
build-only) modes=(--build-only) ;;
|
||||
pull-only) modes=(--pull-only) ;;
|
||||
pull-preflight) modes=(--pull-only --build-only) ;;
|
||||
esac
|
||||
result=0
|
||||
entry="$SW_DEPLOY_BASE/frontend-releases/new/deploy.sh"
|
||||
if [[ "$scenario" == pull-only || "$scenario" == pull-preflight ]]; then entry="$SCRIPT"; fi
|
||||
bash "$entry" new "${modes[@]}" > "$TEST_STATE/output" 2>&1 || result=$?
|
||||
case "$scenario" in
|
||||
success|pull-only)
|
||||
[[ "$result" == 0 && "$(cat "$TEST_STATE/sw2-frontend/id")" == new-container && "$(cat "$TEST_STATE/sw2-frontend-previous/id")" == old-container ]]
|
||||
[[ "$(cat "$SW_DEPLOY_BASE/current-frontend-release")" == new ]] ;;
|
||||
build-only)
|
||||
build-only|pull-preflight)
|
||||
[[ "$result" == 0 && "$(cat "$TEST_STATE/sw2-frontend/id")" == old-container ]]
|
||||
[[ "$(cat "$SW_DEPLOY_BASE/current-frontend-release")" == old ]] ;;
|
||||
*)
|
||||
@@ -91,6 +108,22 @@ for scenario in success preflight-fail final-fail build-only unmanaged; do
|
||||
esac
|
||||
[[ "$(cat "$TEST_STATE/sw2-backend/id")" == backend-unchanged ]]
|
||||
! grep -E '(stop|start|rename|rm|run).*sw2-backend' "$TEST_STATE/calls"
|
||||
! grep -F 'sw2/frontend:' "$TEST_STATE/calls"
|
||||
if [[ "$scenario" == success || "$scenario" == build-only ]]; then
|
||||
grep -q '^push harbor.fuhua91.com/game/sw2:new$' "$TEST_STATE/calls"
|
||||
grep -q '^pull harbor.fuhua91.com/game/sw2:new$' "$TEST_STATE/calls"
|
||||
fi
|
||||
if [[ "$scenario" == pull-only || "$scenario" == pull-preflight ]]; then
|
||||
! grep -E '^(build|push) ' "$TEST_STATE/calls"
|
||||
grep -q '^pull harbor.fuhua91.com/game/sw2:new$' "$TEST_STATE/calls"
|
||||
fi
|
||||
if [[ "$scenario" == success || "$scenario" == pull-only ]]; then
|
||||
grep -q 'harbor.fuhua91.com/game/sw2@sha256:' "$TEST_STATE/calls"
|
||||
grep -q '^harbor.fuhua91.com/game/sw2@sha256:' "$SW_DEPLOY_BASE/frontend-images/new.ref"
|
||||
fi
|
||||
if [[ "$scenario" == push-fail || "$scenario" == pull-fail || "$scenario" == digest-missing ]]; then
|
||||
! grep -E '^(stop|rename) ' "$TEST_STATE/calls"
|
||||
fi
|
||||
[[ "$(find "$TEST_STATE" -maxdepth 1 -name 'sw2-frontend-candidate-*' | wc -l)" == 0 ]]
|
||||
done
|
||||
echo 'all deployment scenarios passed'
|
||||
|
||||
@@ -7,6 +7,8 @@ import { execFileSync, spawnSync } from 'node:child_process'
|
||||
import { parseArgs } from 'node:util'
|
||||
|
||||
export const frontendRoot = fileURLToPath(new URL('../', import.meta.url))
|
||||
export const imageRepository = 'harbor.fuhua91.com/game/sw2'
|
||||
export const frontendImage = tag => `${imageRepository}:${validTag(tag)}`
|
||||
export const options = (definitions) => parseArgs({
|
||||
options: { help: { type: 'boolean' }, ...definitions },
|
||||
args: process.argv.slice(2).filter((arg, i) => !(i === 0 && arg === '--'))
|
||||
|
||||
@@ -4,12 +4,14 @@ import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import os from 'node:os'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { frontendRoot, validTag, shellQuote, deploymentTarget, validateEntries, inspectPackage, bashExecutable } from './release.mjs'
|
||||
import { frontendRoot, frontendImage, validTag, shellQuote, deploymentTarget, validateEntries, inspectPackage, bashExecutable } from './release.mjs'
|
||||
import { installScript } from './deploy-test.mjs'
|
||||
|
||||
test('release and SSH inputs reject paths, options and shell injection', () => {
|
||||
for (const value of ['', '../release', '-latest', 'a;touch x', 'a\nother']) assert.throws(() => validTag(value))
|
||||
assert.equal(validTag('a846bd90-rc.1'), 'a846bd90-rc.1')
|
||||
assert.equal(frontendImage('a846bd90-rc.1'), 'harbor.fuhua91.com/game/sw2:a846bd90-rc.1')
|
||||
assert.throws(() => frontendImage('../other'))
|
||||
for (const args of [{ host: '-oProxyCommand=id' }, { user: 'root;id' }, { directory: '/' }, { directory: '/www/../etc' }, { port: '65536' }]) assert.throws(() => deploymentTarget(args))
|
||||
assert.equal(shellQuote("a'b$(id)"), "'a'\\''b$(id)'")
|
||||
assert.equal(deploymentTarget().base, '/www/wwwroot/sw2.fuhua91.com')
|
||||
|
||||
+30
-12
@@ -1,27 +1,45 @@
|
||||
param(
|
||||
[Parameter(Position = 0)]
|
||||
[string]$Tag = (Get-Date -Format 'yyyyMMdd')
|
||||
[string]$Tag = (Get-Date -Format 'yyyyMMdd'),
|
||||
[switch]$SaveArchive
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
if ($Tag -notmatch '^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,100}$') {
|
||||
if ($Tag -notmatch '^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,79}$') {
|
||||
Write-Error "无效版本号: $Tag"
|
||||
}
|
||||
|
||||
$Root = Split-Path -Parent $PSScriptRoot
|
||||
$OutDir = $PSScriptRoot
|
||||
$Image = "sw2/frontend:$Tag"
|
||||
$OutDir = Join-Path $Root '.cache\docker'
|
||||
$Image = "harbor.fuhua91.com/game/sw2:$Tag"
|
||||
$Tar = Join-Path $OutDir "sw2-frontend-$Tag.tar"
|
||||
|
||||
Write-Host "构建 $Image"
|
||||
docker build --build-arg "VERSION=$Tag" -t $Image $Root
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
$previousAttestations = [Environment]::GetEnvironmentVariable('BUILDX_NO_DEFAULT_ATTESTATIONS', 'Process')
|
||||
try {
|
||||
$env:BUILDX_NO_DEFAULT_ATTESTATIONS = '1'
|
||||
docker buildx version *> $null
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
docker buildx build --platform linux/amd64 --output type=docker --build-arg "VERSION=$Tag" -t $Image $Root
|
||||
} else {
|
||||
docker build --build-arg "VERSION=$Tag" -t $Image $Root
|
||||
}
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
} finally {
|
||||
[Environment]::SetEnvironmentVariable('BUILDX_NO_DEFAULT_ATTESTATIONS', $previousAttestations, 'Process')
|
||||
}
|
||||
|
||||
if (Test-Path $Tar) { Remove-Item $Tar -Force }
|
||||
Write-Host "导出 $Tar"
|
||||
docker save -o $Tar $Image
|
||||
Write-Host "推送 $Image"
|
||||
docker push $Image
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
|
||||
$Item = Get-Item $Tar
|
||||
Write-Host ("完成 {0} {1:N1} MB" -f $Item.FullName, ($Item.Length / 1MB))
|
||||
if ($SaveArchive) {
|
||||
New-Item -ItemType Directory -Force -Path $OutDir | Out-Null
|
||||
if (Test-Path -LiteralPath $Tar) { throw "镜像归档已存在:$Tar" }
|
||||
Write-Host "导出 $Tar"
|
||||
docker save -o $Tar $Image
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
$Item = Get-Item -LiteralPath $Tar
|
||||
Write-Host ("完成 {0} {1:N1} MB" -f $Item.FullName, ($Item.Length / 1MB))
|
||||
}
|
||||
Write-Host "Harbor 镜像:$Image"
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"serve": "vite --host --mode development",
|
||||
"build": "vite build --mode production",
|
||||
"build:docker": "node deploy/build-docker.mjs",
|
||||
"push:docker": "node deploy/build-docker.mjs --push",
|
||||
"package:test": "node deploy/build-package.mjs",
|
||||
"deploy:test": "node deploy/deploy-test.mjs",
|
||||
"test:release": "node --test deploy/release.test.mjs",
|
||||
|
||||
Reference in New Issue
Block a user