From 2c73ad7eb0143801558e97e4dd437e2677751145 Mon Sep 17 00:00:00 2001 From: Qiufeng Date: Tue, 18 Aug 2026 00:02:45 +0800 Subject: [PATCH] fix: support panel-managed deployment safely --- .gitea/workflows/release.yml | 5 +- README.md | 74 +++++++++++++++++++++++++---- deploy/baota-start.sh | 55 +++++++++++++++++++++ deploy/baota.env.example | 14 ++++++ deploy/install.sh | 4 +- deploy/systemd/kaidi-update.path | 2 - deploy/systemd/kaidi-update.service | 3 +- deploy/update.sh | 16 ++++--- scripts/package-release.sh | 4 +- scripts/test-baota-start-fixture.sh | 67 ++++++++++++++++++++++++++ scripts/test-install-fixture.sh | 5 ++ scripts/test-update-fixture.sh | 33 +++++++++++++ scripts/verify-release.sh | 5 ++ 13 files changed, 265 insertions(+), 22 deletions(-) create mode 100755 deploy/baota-start.sh create mode 100644 deploy/baota.env.example create mode 100755 scripts/test-baota-start-fixture.sh diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 560cbd8..b2ad17e 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -65,12 +65,15 @@ jobs: ./scripts/test-install-fixture.sh ./scripts/test-git-install-fixture.sh ./scripts/test-update-fixture.sh + ./scripts/test-baota-start-fixture.sh ./scripts/test-gitea-publish-fixture.sh - shellcheck deploy/install.sh deploy/install-from-git.sh deploy/update.sh scripts/check-semver.sh \ + shellcheck deploy/install.sh deploy/install-from-git.sh deploy/update.sh deploy/baota-start.sh \ + scripts/check-semver.sh \ scripts/package-release.sh scripts/publish-gitea-release.sh \ scripts/generate-release-key.sh scripts/test-install-fixture.sh \ scripts/test-git-install-fixture.sh \ scripts/test-update-fixture.sh scripts/test-gitea-publish-fixture.sh \ + scripts/test-baota-start-fixture.sh \ scripts/verify-release.sh - name: Build and sign release assets diff --git a/README.md b/README.md index 8b06d0c..dee1b4c 100644 --- a/README.md +++ b/README.md @@ -163,7 +163,7 @@ GRANT ALL PRIVILEGES ON kaidi_finance.* TO 'kaidi'@'KAIDI_SERVER_IP'; 管理员数据和运维人员新增的环境变量: ```bash -curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.22/install.sh \ +curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.23/install.sh \ | sudo env KAIDI_REINSTALL=true KAIDI_SETUP_WIZARD=false bash ``` @@ -174,10 +174,68 @@ curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-previe 如果安装器已完成但向导尚未提交,可执行下面的命令重新生成一次性安装码;该恢复路径只接受仍处于向导模式且未锁定的安装,正式模式不会被覆盖。 ```bash -curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.22/install.sh \ +curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.23/install.sh \ | sudo env KAIDI_REINSTALL=true KAIDI_SETUP_WIZARD=true bash ``` +### 宝塔面板手动部署 + +宝塔使用 **Spring Boot** 项目类型。发布物是包含嵌入式 Tomcat 的可执行 JAR,不使用外置 Tomcat 项目。 +先下载并解压签名 Release;项目目录必须指向同时包含 `app.jar`、`VERSION`、`public/` 和 `ops/` 的目录。 +推荐采用版本目录加 `current` 软链接: + +```bash +VERSION=1.0.0-preview.23 +RELEASE_ROOT=/www/wwwroot/kaidi/releases/$VERSION +sudo install -d -o root -g kaidi -m 0750 "$RELEASE_ROOT" +curl -fL "https://git.awaioi.com/ERP-Team/kaidi/releases/download/v$VERSION/kaidi-finance-$VERSION.tar.gz" \ + -o "/tmp/kaidi-finance-$VERSION.tar.gz" +sudo tar -xzf "/tmp/kaidi-finance-$VERSION.tar.gz" -C "$RELEASE_ROOT" +sudo chown -R root:kaidi "$RELEASE_ROOT" +sudo find "$RELEASE_ROOT" -type d -exec chmod 0750 {} + +sudo find "$RELEASE_ROOT" -type f -exec chmod 0640 {} + +sudo chmod 0750 "$RELEASE_ROOT/ops/baota-start.sh" +sudo ln -sfn "$RELEASE_ROOT" /www/wwwroot/kaidi/current.next +sudo mv -Tf /www/wwwroot/kaidi/current.next /www/wwwroot/kaidi/current +``` + +宝塔 Java 项目字段: + +| 字段 | 值 | +| --- | --- | +| 项目类型 | Spring Boot | +| 项目路径 | `/www/wwwroot/kaidi/current` | +| 项目名称 | `kaidi-finance` | +| 项目 JDK | JDK 17 | +| 项目启动命令 | `/www/wwwroot/kaidi/current/ops/baota-start.sh` | +| 启动用户 | `kaidi`,禁止使用 root | +| 项目端口 | `18080` | +| 绑定域名 | 实际业务域名,例如 `fi.awaioi.com` | +| 后端路由 | `/` | +| 前端资源 | 留空,由 Spring Boot 提供 `public/` | +| 进程守护 | 开启 | +| 项目意外重启 | 勾选 | + +环境变量按 `ops/baota.env.example` 填入宝塔。已完成安装的服务器必须沿用 +`/var/lib/kaidi/setup/application.env` 中的 `DB_URL`、`DB_USERNAME`、`DB_PASSWORD`,以及 +`/etc/kaidi/kaidi.env` 中的 `FIELD_ENCRYPTION_KEY`;不要生成新的字段加密密钥。HTTP Preview 使用 +`SESSION_COOKIE_SECURE=false`,切换到 HTTPS 后改为 `true`。宝塔模式固定设置 +`FINANCE_UPDATE_ENABLED=false`,版本更新通过上传新目录、切换 `current`、重启项目完成。 + +启用宝塔守护前停用原 systemd 进程,避免两个进程管理器反复争用端口或互相重启: + +```bash +sudo systemctl disable --now kaidi-update.path kaidi-finance.service +sudo systemctl stop kaidi-update.service +``` + +启动后在服务器验证 Java 上游,再配置域名反向代理: + +```bash +curl -fsS http://127.0.0.1:18080/actuator/health +curl -fsS http://127.0.0.1:18080/ | head +``` + ### 停用并移除程序 以下命令移除应用程序和服务,但保留 `/var/lib/kaidi`、`/var/lib/kaidi-update`、`/etc/kaidi` 以及数据库, @@ -225,12 +283,12 @@ URL、Token、脚本或命令。更新流程固定为: 4. 安装请求持久领取到 `/var/lib/kaidi-update/processing`;进程或主机中断后由 systemd 恢复未完成事务。 5. root 更新器重新验签和验哈希,确认 `mysqldump` 成功并生成权限为 `0600` 的备份,默认保留最近 5 份。安装器不安装 `mysqldump`;启用在线更新前,运维人员须自行在应用服务器预装与外部 MySQL 兼容的 `mysqldump`。 -6. 校验更新脚本和 systemd 单元后,原子切换 updater、systemd 和应用版本,不修改反向代理。 -7. 同时检查 Java 健康端点、更新 path unit 和 Java 托管的静态首页。全部通过后页面显示 10 秒倒计时并自动 +6. 校验更新脚本和 systemd 单元后,在旧 Java 仍运行时原子切换版本链接,再由 systemd 执行一次 `restart`,不修改反向代理。 +7. 同时检查 Java 健康端点和 Java 托管的静态首页。全部通过后页面显示 10 秒倒计时并自动 刷新;刷新或短暂断线发生在安装中时,页面会恢复 3 秒轮询。任一检查失败则恢复并验证上一版本。 忙碌期间检查、下载和安装按钮保持禁用,防止重复请求;这就是更新执行冷却。10 秒只用于成功后的页面刷新, -不会延迟服务端切换。systemd 在 300 秒内连续失败 3 次后停止自动重试,避免失败任务空跑。 +不会延迟服务端切换。失败请求会被归档且不会由 systemd 自动重试,避免同一事务反复停止业务服务。 安装器会把公共 API 地址写入 root-only 的 `/etc/kaidi/kaidi.env` 与 `/etc/kaidi/update.env`,两者权限均为 `0600`:前者供 Java 后端“检查更新”读取,后者供 root 更新器下载资产。默认 Token 为空;如改接私有镜像, @@ -244,9 +302,9 @@ sudo journalctl -u kaidi-update.service -n 100 --no-pager cat /var/lib/kaidi-update/status.json ``` -如果 `status.json` 显示 `FAILED` 且日志提示回滚未完成,不要删除 -`/var/lib/kaidi-update/processing/request.json` 或活动事务目录。systemd 在 300 秒内连续失败 3 次后会停止自动重试, -修复日志所示的磁盘、权限或旧版本健康问题后执行: +如果 `status.json` 显示 `FAILED` 且日志提示回滚未完成,失败请求会归档到 +`/var/lib/kaidi-update/failed`,活动事务保留在 `transactions/active`,系统不会自动再次执行。修复日志所示的 +磁盘、权限或旧版本健康问题后,才明确执行一次: ```bash sudo systemctl reset-failed kaidi-update.service kaidi-update.path diff --git a/deploy/baota-start.sh b/deploy/baota-start.sh new file mode 100755 index 0000000..740fc69 --- /dev/null +++ b/deploy/baota-start.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +die() { + printf '[kaidi-baota] ERROR: %s\n' "$1" >&2 + exit 1 +} + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +RELEASE_ROOT=$(cd "$SCRIPT_DIR/.." && pwd) +APP_JAR="$RELEASE_ROOT/app.jar" +PUBLIC_INDEX="$RELEASE_ROOT/public/index.html" +VERSION_FILE="$RELEASE_ROOT/VERSION" + +[ -s "$APP_JAR" ] || die "app.jar is missing from $RELEASE_ROOT" +[ -s "$PUBLIC_INDEX" ] || die "public/index.html is missing from $RELEASE_ROOT" +[ -s "$VERSION_FILE" ] || die "VERSION is missing from $RELEASE_ROOT" + +JAVA_BIN=${KAIDI_JAVA_BIN:-} +if [ -z "$JAVA_BIN" ] && [ -n "${JAVA_HOME:-}" ]; then + JAVA_BIN="$JAVA_HOME/bin/java" +fi +if [ -z "$JAVA_BIN" ]; then + JAVA_BIN=$(command -v java 2>/dev/null || true) +fi +[ -n "$JAVA_BIN" ] || die "Java 17 or newer was not found" +if [[ "$JAVA_BIN" == */* ]]; then + [ -x "$JAVA_BIN" ] || die "Java executable is not available at $JAVA_BIN" +else + command -v "$JAVA_BIN" >/dev/null 2>&1 || die "Java executable $JAVA_BIN is not available" +fi + +export SPRING_PROFILES_ACTIVE=${SPRING_PROFILES_ACTIVE:-production} +export SERVER_ADDRESS=${SERVER_ADDRESS:-127.0.0.1} +export SERVER_PORT=${SERVER_PORT:-18080} +export FINANCE_SETUP_ENABLED=${FINANCE_SETUP_ENABLED:-false} +export FINANCE_BOOTSTRAP_ENABLED=${FINANCE_BOOTSTRAP_ENABLED:-false} +export FINANCE_UPDATE_ENABLED=${FINANCE_UPDATE_ENABLED:-false} +export FILE_SCANNER_ENABLED=${FILE_SCANNER_ENABLED:-false} +export FINANCE_STATIC_LOCATIONS=${FINANCE_STATIC_LOCATIONS:-file:$RELEASE_ROOT/public/} +export UPDATE_CURRENT_VERSION_FILE=${UPDATE_CURRENT_VERSION_FILE:-$VERSION_FILE} +export APP_VERSION=${APP_VERSION:-$(tr -d '\r\n' < "$VERSION_FILE")} + +if [ "$FINANCE_SETUP_ENABLED" != true ]; then + for name in DB_URL DB_USERNAME DB_PASSWORD FIELD_ENCRYPTION_KEY FILE_STORAGE_ROOT FILE_STORAGE_TEMP; do + [ -n "${!name:-}" ] || die "$name must be configured in the Baota project environment" + done + for directory in "$FILE_STORAGE_ROOT" "$FILE_STORAGE_TEMP"; do + [ -d "$directory" ] || die "Storage directory $directory does not exist" + [ -w "$directory" ] || die "Startup user cannot write storage directory $directory" + done +fi + +cd "$RELEASE_ROOT" +exec "$JAVA_BIN" -XX:MaxRAMPercentage=70 -Dfile.encoding=UTF-8 -jar "$APP_JAR" diff --git a/deploy/baota.env.example b/deploy/baota.env.example new file mode 100644 index 0000000..5c76ca0 --- /dev/null +++ b/deploy/baota.env.example @@ -0,0 +1,14 @@ +SPRING_PROFILES_ACTIVE=production +SERVER_ADDRESS=127.0.0.1 +SERVER_PORT=18080 +SESSION_COOKIE_SECURE=false +DB_URL=jdbc:mysql://MYSQL_HOST:3306/kaidi_finance?useUnicode=true&characterEncoding=utf8&connectionTimeZone=UTC&serverTimezone=UTC +DB_USERNAME=MYSQL_USERNAME +DB_PASSWORD=MYSQL_PASSWORD +FIELD_ENCRYPTION_KEY=EXISTING_FIELD_ENCRYPTION_KEY +FILE_STORAGE_ROOT=/var/lib/kaidi/files +FILE_STORAGE_TEMP=/var/lib/kaidi/tmp +FILE_SCANNER_ENABLED=false +FINANCE_BOOTSTRAP_ENABLED=false +FINANCE_SETUP_ENABLED=false +FINANCE_UPDATE_ENABLED=false diff --git a/deploy/install.sh b/deploy/install.sh index b684727..db78a76 100755 --- a/deploy/install.sh +++ b/deploy/install.sh @@ -344,7 +344,7 @@ secure_release_tree() { chown -R root:"$SERVICE_GROUP" "$release_dir" find "$release_dir" -type d -exec chmod 0750 {} + find "$release_dir" -type f -exec chmod 0640 {} + - chmod 0750 "$release_dir/ops/update.sh" + chmod 0750 "$release_dir/ops/update.sh" "$release_dir/ops/baota-start.sh" } restore_security_contexts() { @@ -848,6 +848,8 @@ find "$WORK_DIR/extracted" -type l -print -quit | grep -q . \ [ -s "$WORK_DIR/extracted/public/index.html" ] || die "Release frontend is missing" [ "$(cat "$WORK_DIR/extracted/VERSION")" = "$VERSION" ] || die "Release version mismatch" [ -x "$WORK_DIR/extracted/ops/update.sh" ] || die "Release updater is missing" +[ -x "$WORK_DIR/extracted/ops/baota-start.sh" ] || die "Baota Spring Boot launcher is missing" +bash -n "$WORK_DIR/extracted/ops/baota-start.sh" || die "Baota Spring Boot launcher is invalid" configure_database preflight_database diff --git a/deploy/systemd/kaidi-update.path b/deploy/systemd/kaidi-update.path index 37efecd..de57bdf 100644 --- a/deploy/systemd/kaidi-update.path +++ b/deploy/systemd/kaidi-update.path @@ -4,8 +4,6 @@ Description=Watch for Kaidi Finance update requests [Path] PathChanged=/var/lib/kaidi-update/inbox PathExists=/var/lib/kaidi-update/inbox/request.json -PathChanged=/var/lib/kaidi-update/processing -PathExists=/var/lib/kaidi-update/processing/request.json Unit=kaidi-update.service [Install] diff --git a/deploy/systemd/kaidi-update.service b/deploy/systemd/kaidi-update.service index f74d8ce..5c22273 100644 --- a/deploy/systemd/kaidi-update.service +++ b/deploy/systemd/kaidi-update.service @@ -12,8 +12,7 @@ Group=root EnvironmentFile=/etc/kaidi/update.env EnvironmentFile=-/var/lib/kaidi/setup/application.env ExecStart=/opt/kaidi/bin/update.sh -Restart=on-failure -RestartSec=15 +Restart=no Nice=10 IOSchedulingClass=best-effort IOSchedulingPriority=6 diff --git a/deploy/update.sh b/deploy/update.sh index 485636b..27df625 100755 --- a/deploy/update.sh +++ b/deploy/update.sh @@ -102,7 +102,7 @@ secure_release_tree() { chown -R root:"$SERVICE_GROUP" "$release_dir" || return 1 find "$release_dir" -type d -exec chmod 0750 {} + || return 1 find "$release_dir" -type f -exec chmod 0640 {} + || return 1 - chmod 0750 "$release_dir/ops/update.sh" + chmod 0750 "$release_dir/ops/update.sh" "$release_dir/ops/baota-start.sh" } verify_release_access() { @@ -379,7 +379,6 @@ rollback_active_transaction() { link_restored=true operations_restored=true - systemctl stop "$SERVICE_NAME" || true if [ -n "$previous_target" ] && [ -d "$previous_target" ]; then if ! ln -sfn "$previous_target" "$APP_ROOT/current.next" \ || ! mv -Tf "$APP_ROOT/current.next" "$APP_ROOT/current"; then @@ -390,7 +389,7 @@ rollback_active_transaction() { fi restore_operations || operations_restored=false systemctl reset-failed "$SERVICE_NAME" >/dev/null 2>&1 || true - systemctl start "$SERVICE_NAME" || true + systemctl restart "$SERVICE_NAME" || systemctl start "$SERVICE_NAME" || true if [ "$link_restored" = true ] && verify_app_surface; then remove_failed_release "$failed_release" rm -rf "$ACTIVE_TRANSACTION" @@ -400,8 +399,11 @@ rollback_active_transaction() { fail "$reason; previous release is running, but operations restoration requires manual review" fi TERMINAL_STATUS_WRITTEN=true - status FAILED "$reason; rollback is incomplete and will be retried" "${TARGET_VERSION:-}" - printf '%s\n' "$reason; rollback is incomplete and will be retried" >&2 + status FAILED "$reason; rollback is incomplete and manual recovery is required" "${TARGET_VERSION:-}" + if ! archive_processing_request; then + printf '%s\n' "Update request could not be archived after an incomplete rollback" >&2 + fi + printf '%s\n' "$reason; rollback is incomplete and manual recovery is required" >&2 exit 1 } @@ -427,6 +429,7 @@ recover_interrupted_transaction() { validate_release_operations() { sh -n "$WORK_DIR/extracted/ops/update.sh" || return 1 + bash -n "$WORK_DIR/extracted/ops/baota-start.sh" || return 1 command -v systemd-analyze >/dev/null 2>&1 || return 1 systemd-analyze verify \ "$WORK_DIR/extracted/ops/kaidi-finance.service" \ @@ -613,7 +616,6 @@ apply_operations || rollback_active_transaction "Release operations files could write_transaction_value phase OPS_APPLIED write_transaction_value phase APP_SWITCHING -systemctl stop "$SERVICE_NAME" || rollback_active_transaction "Application service could not be stopped" if ! ln -sfn "$RELEASE_DIR" "$APP_ROOT/current.next" \ || ! mv -Tf "$APP_ROOT/current.next" "$APP_ROOT/current"; then rollback_active_transaction "Release application link could not be activated" @@ -623,7 +625,7 @@ write_transaction_value phase APP_SWITCHED status RUNNING "Starting and verifying release $TARGET_VERSION" "$TARGET_VERSION" write_transaction_value phase HEALTH_CHECKING systemctl reset-failed "$SERVICE_NAME" >/dev/null 2>&1 || true -systemctl start "$SERVICE_NAME" || true +systemctl restart "$SERVICE_NAME" || systemctl start "$SERVICE_NAME" || true if verify_app_surface; then success_message="Release $TARGET_VERSION is running" if ! systemctl start "$UPDATE_PATH_NAME" >/dev/null 2>&1; then diff --git a/scripts/package-release.sh b/scripts/package-release.sh index c1cd552..458d38c 100755 --- a/scripts/package-release.sh +++ b/scripts/package-release.sh @@ -91,12 +91,14 @@ cp "$JAR" "$STAGE/app.jar" cp -R "$ROOT/frontend/dist/." "$STAGE/public/" printf '%s\n' "$VERSION" > "$STAGE/VERSION" cp "$ROOT/deploy/update.sh" "$STAGE/ops/update.sh" +cp "$ROOT/deploy/baota-start.sh" "$STAGE/ops/baota-start.sh" +cp "$ROOT/deploy/baota.env.example" "$STAGE/ops/baota.env.example" # Kept in the archive so Preview.9's updater can complete the one-time transition. cp "$ROOT/deploy/nginx/kaidi-finance.conf" "$STAGE/ops/kaidi-finance.conf" cp "$ROOT/deploy/systemd/kaidi-finance.service" "$STAGE/ops/kaidi-finance.service" cp "$ROOT/deploy/systemd/kaidi-update.service" "$STAGE/ops/kaidi-update.service" cp "$ROOT/deploy/systemd/kaidi-update.path" "$STAGE/ops/kaidi-update.path" -chmod 0755 "$STAGE/ops/update.sh" +chmod 0755 "$STAGE/ops/update.sh" "$STAGE/ops/baota-start.sh" ARTIFACT="kaidi-finance-$VERSION.tar.gz" COPYFILE_DISABLE=1 tar --format=ustar -czf "$OUTPUT_DIR/$ARTIFACT" -C "$STAGE" . diff --git a/scripts/test-baota-start-fixture.sh b/scripts/test-baota-start-fixture.sh new file mode 100755 index 0000000..a838e0b --- /dev/null +++ b/scripts/test-baota-start-fixture.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +WORK=$(mktemp -d) +trap 'rm -rf "$WORK"' EXIT + +fail() { + printf 'Baota start fixture failed: %s\n' "$1" >&2 + exit 1 +} + +RELEASE="$WORK/release" +mkdir -p "$RELEASE/ops" "$RELEASE/public" "$WORK/files" "$WORK/tmp" +printf 'fixture jar\n' > "$RELEASE/app.jar" +printf '\n' > "$RELEASE/public/index.html" +printf '1.0.0-preview.23\n' > "$RELEASE/VERSION" +cp "$ROOT/deploy/baota-start.sh" "$RELEASE/ops/baota-start.sh" +chmod 0755 "$RELEASE/ops/baota-start.sh" + +cat > "$WORK/java" <<'SH' +#!/usr/bin/env bash +set -Eeuo pipefail +{ + printf 'cwd=%s\n' "$PWD" + printf 'args=%s\n' "$*" + printf 'version=%s\n' "$APP_VERSION" + printf 'address=%s\n' "$SERVER_ADDRESS" + printf 'port=%s\n' "$SERVER_PORT" + printf 'update=%s\n' "$FINANCE_UPDATE_ENABLED" + printf 'static=%s\n' "$FINANCE_STATIC_LOCATIONS" +} > "$MOCK_JAVA_LOG" +SH +chmod 0755 "$WORK/java" + +MOCK_JAVA_LOG="$WORK/java.log" \ +KAIDI_JAVA_BIN="$WORK/java" \ +DB_URL='jdbc:mysql://db.fixture/kaidi_finance' \ +DB_USERNAME=fixture \ +DB_PASSWORD='fixture-password' \ +FIELD_ENCRYPTION_KEY='fixture-field-key' \ +FILE_STORAGE_ROOT="$WORK/files" \ +FILE_STORAGE_TEMP="$WORK/tmp" \ + "$RELEASE/ops/baota-start.sh" + +grep -Fqx "cwd=$RELEASE" "$WORK/java.log" || fail 'launcher did not use the release working directory' +grep -Fqx "args=-XX:MaxRAMPercentage=70 -Dfile.encoding=UTF-8 -jar $RELEASE/app.jar" "$WORK/java.log" \ + || fail 'launcher did not execute the packaged Spring Boot jar' +grep -Fqx 'version=1.0.0-preview.23' "$WORK/java.log" || fail 'launcher did not read VERSION' +grep -Fqx 'address=127.0.0.1' "$WORK/java.log" || fail 'launcher did not default to loopback' +grep -Fqx 'port=18080' "$WORK/java.log" || fail 'launcher did not default to port 18080' +grep -Fqx 'update=false' "$WORK/java.log" || fail 'launcher did not disable the systemd updater' +grep -Fqx "static=file:$RELEASE/public/" "$WORK/java.log" \ + || fail 'launcher did not bind static resources to the release directory' + +if KAIDI_JAVA_BIN="$WORK/java" \ + DB_USERNAME=fixture DB_PASSWORD=fixture FIELD_ENCRYPTION_KEY=fixture \ + FILE_STORAGE_ROOT="$WORK/files" FILE_STORAGE_TEMP="$WORK/tmp" \ + MOCK_JAVA_LOG="$WORK/missing-db.log" \ + "$RELEASE/ops/baota-start.sh" > "$WORK/missing-db.out" 2>&1; then + fail 'launcher accepted a production configuration without DB_URL' +fi +grep -Fq 'DB_URL must be configured' "$WORK/missing-db.out" \ + || fail 'launcher did not report the missing database URL' +[ ! -e "$WORK/missing-db.log" ] || fail 'launcher started Java after configuration validation failed' + +printf 'Baota Spring Boot launcher fixture passed\n' diff --git a/scripts/test-install-fixture.sh b/scripts/test-install-fixture.sh index d49dcdf..19b62b7 100755 --- a/scripts/test-install-fixture.sh +++ b/scripts/test-install-fixture.sh @@ -51,6 +51,7 @@ mkdir -p "$release_permissions/public" "$release_permissions/ops" printf 'jar\n' > "$release_permissions/app.jar" printf 'html\n' > "$release_permissions/public/index.html" printf '#!/bin/sh\n' > "$release_permissions/ops/update.sh" +printf '#!/usr/bin/env bash\n' > "$release_permissions/ops/baota-start.sh" chmod -R 0777 "$release_permissions" ( # shellcheck disable=SC2329 # Invoked indirectly by the sourced installer helper. @@ -316,6 +317,10 @@ grep -Fq 'systemctl reset-failed kaidi-finance.service' "$ROOT/deploy/install.sh || fail 'installer can execute before the complete curl stream is parsed' grep -Fqx 'StartLimitBurst=3' "$ROOT/deploy/systemd/kaidi-finance.service" \ || fail 'application service no longer has a bounded restart burst' +grep -Fqx 'Restart=no' "$ROOT/deploy/systemd/kaidi-update.service" \ + || fail 'update service can automatically repeat a failed switching transaction' +! grep -Fq '/var/lib/kaidi-update/processing' "$ROOT/deploy/systemd/kaidi-update.path" \ + || fail 'update path can automatically repeat a claimed switching transaction' # shellcheck disable=SC2016 # Match literal installer source. grep -Fq '[ "$actual_sha256" = "$java_sha256" ]' "$ROOT/deploy/install.sh" \ || fail 'installer no longer verifies the Java runtime SHA-256' diff --git a/scripts/test-update-fixture.sh b/scripts/test-update-fixture.sh index f39bd48..214c17a 100755 --- a/scripts/test-update-fixture.sh +++ b/scripts/test-update-fixture.sh @@ -178,7 +178,9 @@ build_release() { printf 'new\n' > "$stage/public/index.html" printf '%s\n' "$version" > "$stage/VERSION" printf '#!/bin/sh\nprintf "new updater\\n"\n' > "$stage/ops/update.sh" + printf '#!/usr/bin/env bash\nexit 0\n' > "$stage/ops/baota-start.sh" chmod 0755 "$stage/ops/update.sh" + chmod 0755 "$stage/ops/baota-start.sh" for name in kaidi-finance.service kaidi-update.service kaidi-update.path; do printf '%s %s\n' "$unit_prefix" "$name" > "$stage/ops/$name" done @@ -364,6 +366,10 @@ assert_success_case() { [ ! -e "$fixture/state/processing/request.json" ] \ || fail 'success case left a claimed request behind' grep -qx 'daemon-reload' "$fixture/systemctl.log" || fail 'systemd units were not reloaded' + grep -qx 'restart kaidi-finance.service' "$fixture/systemctl.log" \ + || fail 'success case did not restart the application after the atomic switch' + ! grep -qx 'stop kaidi-finance.service' "$fixture/systemctl.log" \ + || fail 'success case stopped the application before switching releases' ! grep -qi nginx "$fixture/systemctl.log" || fail 'updater unexpectedly managed Nginx' [ "$(mode_of "$fixture/app")" = 750 ] || fail 'application root is not traversable by the service group' [ "$(mode_of "$fixture/app/releases/$version")" = 750 ] || fail 'release root mode is not 0750' @@ -426,6 +432,10 @@ assert_rollback_case() { || fail 'rollback case did not restore the previous updater' grep -qx 'old kaidi-update.path' "$fixture/systemd/kaidi-update.path" \ || fail 'rollback case did not restore the previous path unit' + ! grep -qx 'stop kaidi-finance.service' "$fixture/systemctl.log" \ + || fail 'rollback case stopped the application before restoring the previous release' + [ "$(grep -c '^restart kaidi-finance.service$' "$fixture/systemctl.log")" -ge 2 ] \ + || fail 'rollback case did not restart both the candidate and restored releases' [ "$(jq -r '.state' "$fixture/state/status.json")" = FAILED ] \ || fail 'rollback case did not persist FAILED' [ ! -e "$fixture/app/releases/$version" ] \ @@ -434,6 +444,28 @@ assert_rollback_case() { || fail 'rollback case did not archive the failed request' } +assert_incomplete_rollback_requires_manual_recovery_case() { + local fixture="$WORK/incomplete-rollback" + local version='1.0.0-preview.2' + mkdir -p "$fixture" + write_mock_commands "$fixture/mock-bin" + build_release "$fixture" "$version" + prepare_installation "$fixture" "$version" + download_and_prepare_install "$fixture" "$version" + + if run_update "$fixture" fail > "$fixture/update.log" 2>&1; then + fail 'incomplete rollback case unexpectedly succeeded' + fi + grep -Fq 'manual recovery is required' "$fixture/update.log" \ + || fail 'incomplete rollback case did not require explicit recovery' + [ ! -e "$fixture/state/processing/request.json" ] \ + || fail 'incomplete rollback case left an automatically retriggered processing request' + [ -d "$fixture/state/transactions/active" ] \ + || fail 'incomplete rollback case did not preserve transaction evidence' + find "$fixture/state/failed" -type f -name 'request-*.json' -print -quit | grep -q . \ + || fail 'incomplete rollback case did not archive its claimed request' +} + assert_download_failure_case() { local fixture="$WORK/download-failure" local version='1.0.0-preview.2' @@ -521,6 +553,7 @@ assert_success_case assert_identical_systemd_operations_case assert_update_path_failure_keeps_application_case assert_rollback_case +assert_incomplete_rollback_requires_manual_recovery_case assert_download_failure_case assert_database_failure_case assert_symlink_request_rejected diff --git a/scripts/verify-release.sh b/scripts/verify-release.sh index 2a4b40e..d7003db 100755 --- a/scripts/verify-release.sh +++ b/scripts/verify-release.sh @@ -160,10 +160,15 @@ compare_archive_file() { } compare_archive_file ./ops/update.sh deploy/update.sh +compare_archive_file ./ops/baota-start.sh deploy/baota-start.sh +compare_archive_file ./ops/baota.env.example deploy/baota.env.example compare_archive_file ./ops/kaidi-update.path deploy/systemd/kaidi-update.path compare_archive_file ./ops/kaidi-update.service deploy/systemd/kaidi-update.service compare_archive_file ./ops/kaidi-finance.service deploy/systemd/kaidi-finance.service compare_archive_file ./ops/kaidi-finance.conf deploy/nginx/kaidi-finance.conf +tar -tvzf "$ARTIFACT" ./ops/baota-start.sh | grep -Eq '^-.{2}x.{2}x.{2}x' \ + || { printf 'Baota Spring Boot launcher is not executable\n' >&2; exit 1; } +bash -n "$ROOT/deploy/baota-start.sh" cmp install.sh "$ROOT/deploy/install.sh" \ || { printf 'Release installer does not match deploy/install.sh\n' >&2; exit 1; }