From 8eb1cbad83639e63817614823bc0c49526100bcc Mon Sep 17 00:00:00 2001 From: Qiufeng Date: Tue, 18 Aug 2026 16:25:41 +0800 Subject: [PATCH] fix: stabilize systemd deployment and release updates --- README.md | 75 ++++++++++++------- .../src/main/resources/application-local.yml | 6 ++ backend/src/main/resources/application.yml | 5 +- deploy/baota-init.sh | 5 +- deploy/baota-start.sh | 20 ++++- deploy/env.production.example | 12 +-- deploy/install-from-git.sh | 2 +- deploy/install.sh | 50 ++++++++++--- deploy/systemd/kaidi-finance.service | 3 +- deploy/update.sh | 8 +- scripts/package-release.sh | 22 +++++- scripts/test-baota-start-fixture.sh | 32 ++++++++ scripts/test-install-fixture.sh | 41 +++++++--- scripts/test-update-fixture.sh | 25 ++++++- scripts/verify-release.sh | 21 +++++- 15 files changed, 253 insertions(+), 74 deletions(-) diff --git a/README.md b/README.md index e4693c5..994e958 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,13 @@ ## 本地开发 -后端默认连接本机 MySQL `127.0.0.1:3307`,启动 Java 服务: +本地开发数据库由环境变量显式指定,启动 Java 服务: ```bash cd backend +SPRING_PROFILES_ACTIVE=local \ +DB_URL='jdbc:mysql://127.0.0.1:3307/kaidi_finance?useUnicode=true&characterEncoding=utf8&connectionTimeZone=UTC&serverTimezone=UTC' \ +DB_USERNAME=kaidi DB_PASSWORD=kaidi_local_2026 \ ./mvnw spring-boot:run ``` @@ -51,12 +54,28 @@ npm run build 两种方式不能同时运行在同一个端口。程序不会安装 MySQL,数据库只支持运维人员预先准备的外部 MySQL 8.4.x, PostgreSQL 18 兼容工作继续冻结。 +### 生产部署契约(先看这里) + +- **生产机不执行 `git clone`、`git pull` 或远程脚本拼接。** 源码仓库是 + `https://git.awaioi.com/ERP-Team/kaidi.git`,生产安装和后台更新使用同一仓库生成的公开 Gitea Release。 +- 安装器先读取 Gitea Release API,再下载 `release-manifest.json`、签名、公钥和压缩包,校验固定公钥指纹、RSA 签名、版本、文件名和 SHA-256;校验失败不会安装。 +- 后台“获取版本”只查询 Release;“下载”才下载并校验;“立即更新并重启”才备份数据库、切换 `current` 并重启服务。更新器下载的是 Release 制品,不是 Git 工作树。 +- MySQL 是**已有数据库**,应用只通过向导写入的 `DB_URL`、`DB_USERNAME`、`DB_PASSWORD` 连接它;安装器不安装 MySQL、不创建数据库、不修改数据库服务。填写 `127.0.0.1` 表示 MySQL 与 Java 应用在同一台服务器,端口以实际监听端口为准,不默认假设 `3307`。 +- `KAIDI_APP_PORT` 只决定 Java 回环监听端口,默认 `18080`;反向代理必须指向安装器输出的 `PROXY_TARGET`。安装器不会替你修改 Nginx 或宝塔站点配置。 +- 发布归档使用无顶层目录的 `tar.gz`,只包含 `app.jar`、`public/`、`ops/`、`VERSION`;打包阶段禁用 macOS 扩展属性并拒绝开发数据库回退值。 + +### 32 位 Linux 支持边界 + +当前支持的是 **i386/i486/i586/i686 + glibc + systemd + 可运行的 32 位 Java 17**。安装器会校验用户空间位数、Java 架构、glibc 和 `/lib/ld-linux.so.2`(兼容 `/lib32`、`/lib/i386-linux-gnu` 路径);musl 或缺少 32 位加载器的系统会在安装前明确失败,不会安装后才出现无法启动。 + +32 位服务器应使用首次访问 `/setup` 的向导输入外部 MySQL 8.4 连接信息,这条路径不需要在服务器安装 MySQL 客户端。在线更新前仍需准备与数据库兼容的 `mysqldump`;如果 32 位系统无法提供该客户端,应先关闭在线更新或从独立备份机执行数据库备份,不要在更新过程中临时安装数据库。 + ### systemd 一键安装(推荐) 先在宝塔面板停止并删除当前错误的 Java 项目,再执行: ```bash -curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.30/install.sh | sudo env KAIDI_APP_PORT=18080 bash +curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.31/install.sh | sudo env KAIDI_APP_PORT=18080 bash ``` 该命令只安装程序运行所需的 systemd 单元,自动创建 `kaidi` 用户并检测现有 Java 17(包括 @@ -69,9 +88,9 @@ 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.24/purge.sh \ +curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.31/purge.sh \ -o /tmp/kaidi-purge.sh \ - && printf '%s %s\n' c49c5460260d2c67d88aced99463e3d98a67f8ef0309f5c8d5d8a8f5e4addf36 /tmp/kaidi-purge.sh \ + && printf '%s %s\n' 44dbf10a9540aa9235bb8aff2dec603f1febbd45072cd8c4f6c40b25a6127801 /tmp/kaidi-purge.sh \ | sha256sum -c - \ && sudo env KAIDI_PURGE_CONFIRM=DELETE_LOCAL_KAIDI_INSTALLATION bash /tmp/kaidi-purge.sh \ && rm -f /tmp/kaidi-purge.sh @@ -83,20 +102,20 @@ curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-previe `0600`,确认新安装及数据无误后再由 root 删除。脚本不删除外部 MySQL、系统 Java、Nginx 或宝塔站点配置; 新安装必须连接一个新的空 MySQL 数据库,旧数据库保留用于回滚。 -### Preview.25 直链与宝塔手动部署 +### Preview.31 直链与宝塔手动部署 本版不使用一键安装脚本。发布物是一个不带顶层目录的压缩包,下载后直接解压到版本目录,再由宝塔面板创建 Spring Boot 项目。数据库、JDK、Nginx 和宝塔本身都由运维人员准备;程序不会自动安装 MySQL 或任何第三方服务。 下载地址: -`https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.25/kaidi-finance-1.0.0-preview.25.tar.gz` +`https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.31/kaidi-finance-1.0.0-preview.31.tar.gz` -校验文件:`https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.25/SHA256SUMS` +校验文件:`https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.31/SHA256SUMS` 服务器要求:Linux + systemd、Java 17(宝塔项目选择 JDK 17)、可访问外部 MySQL 8.4.x;32 位 Linux 需要宿主机 已经提供可运行的 32 位 Java 17。程序只连接已有数据库,不安装数据库客户端或数据库服务。初始化只需要 -`bash`、`openssl`、`sha256sum` 和基础 Linux 工具;启用在线更新前,另行准备 `curl`、`jq`、`flock`、`gzip`、`runuser` +`bash`、`openssl`、`sha256sum` 和基础 Linux 工具;选择 systemd 在线更新前,另行准备 `curl`、`jq`、`flock`、`gzip`、`runuser` 以及与外部 MySQL 兼容的 `mysqldump`。程序不会替你安装这些依赖。 #### 1. 下载、校验、解压 @@ -105,7 +124,7 @@ Spring Boot 项目。数据库、JDK、Nginx 和宝塔本身都由运维人员 必须直接位于 `RELEASE_ROOT`,不能再嵌套一层目录。 ```bash -VERSION=1.0.0-preview.25 +VERSION=1.0.0-preview.31 APP_ROOT=/www/wwwroot/kaidi RELEASE_ROOT="$APP_ROOT/releases/$VERSION" sudo install -d -m 0755 "$RELEASE_ROOT" @@ -121,8 +140,8 @@ sudo tar -xzf "/tmp/kaidi-finance-$VERSION.tar.gz" -C "$RELEASE_ROOT" #### 2. 执行本地初始化 -初始化脚本来自已下载的归档,并在本机以 root 运行。它创建 `kaidi` 启动用户、受保护配置、在线更新监听器和一次性 -安装码;不会下载 Java、安装 MySQL 或修改反向代理。端口参数可改为 `1024-65535` 中的空闲端口,默认 `18080`。 +初始化脚本来自已下载的归档,并在本机以 root 运行。它创建 `kaidi` 启动用户、受保护配置和一次性安装码;不会下载 +Java、安装 MySQL 或修改反向代理。宝塔手动模式不会开放在线更新按钮。端口参数可改为 `1024-65535` 中的空闲端口,默认 `18080`。 ```bash sudo "$RELEASE_ROOT/ops/baota-init.sh" 18080 @@ -184,24 +203,24 @@ HTTPS 启用后,在 `/etc/kaidi/kaidi.env` 将 `SESSION_COOKIE_SECURE` 设为 ```bash curl -fsS http://127.0.0.1:18080/actuator/health curl -fsS http://127.0.0.1:18080/ | head -systemctl is-enabled kaidi-update.path ``` -#### 5. 后台在线更新 +#### 5. 手动部署模式的更新边界 -初始化脚本已启用 `kaidi-update.path`。后台“系统治理 → 系统更新”按明确阶段执行:点击“获取版本”只读取版本信息, -发现新版本后点击“立即更新”才开始下载和验签;页面显示“下载完成”后,再点击“立即更新并重启”。 -root 更新器原子切换 `/www/wwwroot/kaidi/current`,向当前 Java 进程发送停止信号;宝塔的意外重启守护随后按新 `current` 链接拉起 Java, -健康检查通过后页面等待 10 秒并自动刷新。更新器不会安装或启动 `kaidi-finance.service`,也不会修改 Nginx。 +宝塔手动部署不启用 Preview 的在线更新契约。需要升级时,先停止宝塔项目,下载并校验新的 Release 压缩包, +解压到新的 `releases/`,再由宝塔项目切换路径并启动;确认健康检查通过后再删除旧版本。 +不要在宝塔项目仍运行时点击“系统治理 → 系统更新”,也不要让宝塔守护和 `kaidi-finance.service` 同时管理同一端口。 +需要后台点击“获取版本 → 下载 → 立即更新并重启”的稳定流程时,使用上一节的 systemd 一键安装方式。 -更新前请在服务器预装与外部 MySQL 兼容的 `mysqldump`,更新器只生成权限为 `0600` 的备份,不会自动安装 MySQL。迁移失败时恢复旧应用版本并保留数据库备份和事务证据,遵循至少一个版本向后兼容的迁移规则。 +手动升级前请先由运维人员完成外部 MySQL 备份;程序不会安装 MySQL 或客户端,也不会修改反向代理配置。 -后续版本仍按同样方式直接下载并解压到新的 `releases/`;不要手工删除 `current`,在线更新负责原子切换和回滚。 +后续版本仍按同样方式直接下载并解压到新的 `releases/`,保留可回滚的旧目录。 ### Linux 32 位 -压缩包本身不绑定 CPU 架构,关键是宝塔项目选用可运行的 32 位 Java 17。32 位主机不能在本机运行 64 位 JDK,也不应尝试在本机安装 -MySQL 8.4;提前准备外部 MySQL,完成上述手动解压、初始化和向导即可。 +压缩包本身不绑定 CPU 架构,关键是宿主机提供 glibc、systemd 和可运行的 32 位 Java 17。32 位主机不能在本机运行 64 位 JDK,也不应尝试在本机安装 +MySQL 8.4;提前准备外部 MySQL,完成上述向导即可。选择 systemd 在线更新前,还必须确认宿主机能执行与数据库版本兼容的 +`mysqldump`;宝塔手动模式只做手动制品替换和人工数据库备份。 ### 停用并移除程序 @@ -235,14 +254,15 @@ act_runner 提供 `ubuntu-24.04` 标签,并在 tag 发布时执行后端、前 Preview 属性由 SemVer 版本名表达。之后推送 tag 即会构建、测试、签名并发布: ```bash -git tag v1.0.0-preview.25 -git push origin v1.0.0-preview.25 +git tag v1.0.0-preview.31 +git push origin v1.0.0-preview.31 ``` 在线更新仍使用独立的 TDesign 页面:系统管理员进入“系统治理 → 系统更新”。更新源由 root 在 `/etc/kaidi/update.env` 固定为公共 Gitea Latest Release API;页面和普通 API 不能提交 URL、Token、脚本或命令。 -宝塔模式下,root 更新器只负责下载、验签、切换 `current` 和停止旧 Java,**不创建或重启 -`kaidi-finance.service`**;宝塔项目的“项目意外重启”负责按新链接拉起 Java。 +**Preview 的稳定在线更新契约只支持 systemd 一键安装模式**:更新器由 +`kaidi-update.service` 执行,直接停止、切换并重启 `kaidi-finance.service`,不依赖宝塔面板的意外重启。 +宝塔手动部署仅用于手动启动和反向代理;其面板进程管理与 systemd 更新器不是同一套生命周期,暂不作为在线更新的稳定路径。 更新流程固定为: @@ -251,7 +271,8 @@ git push origin v1.0.0-preview.25 文件名和压缩包路径校验,通过后缓存到 `/var/lib/kaidi-update/cache/`,业务服务继续运行。 3. 页面实时轮询并依次显示下载已排队、下载中、校验中和 `READY/下载完成`;只有下载完成后才显示“立即更新并重启”。 4. 管理员点击“立即更新并重启”,更新器预先调用宿主机已有的 `mysqldump`,备份权限为 `0600`,默认保留最近 5 份;程序不会安装 MySQL 或客户端。 -5. 更新器安装并保护新版本目录,原子切换 `/www/wwwroot/kaidi/current`,校验旧 Java 的受控 PID 后发送停止信号;宝塔守护自动启动新版本。 +5. 更新器安装并保护新版本目录,原子切换 `/opt/kaidi/current`(宝塔手动部署才使用 + `/www/wwwroot/kaidi/current`),停止并重启 systemd 管理的应用服务。 6. 健康端点、静态首页和运行版本全部通过后,页面等待 10 秒自动刷新;期间断线或命令响应丢失由前端状态重同步恢复。任一检查失败则切回上一应用版本, 保留数据库备份和事务证据供人工处理。 @@ -286,7 +307,7 @@ cat /var/lib/kaidi-update/status.json ```bash KAIDI_RELEASE_SIGNING_KEY=/secure/release-signing-private.pem \ KAIDI_TRUSTED_RELEASE_PUBLIC_KEY_SHA256=807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9 \ - ./scripts/package-release.sh 1.0.0-preview.25 + ./scripts/package-release.sh 1.0.0-preview.31 KAIDI_TRUSTED_RELEASE_PUBLIC_KEY_SHA256=807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9 \ ./scripts/verify-release.sh dist/release ``` diff --git a/backend/src/main/resources/application-local.yml b/backend/src/main/resources/application-local.yml index 5335b1c..666f4d3 100644 --- a/backend/src/main/resources/application-local.yml +++ b/backend/src/main/resources/application-local.yml @@ -1,3 +1,9 @@ +spring: + datasource: + url: ${DB_URL} + username: ${DB_USERNAME} + password: ${DB_PASSWORD} + server: servlet: session: diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 1bed27f..6d66efb 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -17,11 +17,8 @@ spring: resources: static-locations: ${FINANCE_STATIC_LOCATIONS:file:./public/} profiles: - default: local + default: production datasource: - url: ${DB_URL:jdbc:mysql://127.0.0.1:3307/kaidi_finance?useUnicode=true&characterEncoding=utf8&connectionTimeZone=UTC&serverTimezone=UTC} - username: ${DB_USERNAME:kaidi} - password: ${DB_PASSWORD:kaidi_local_2026} hikari: maximum-pool-size: 20 minimum-idle: 2 diff --git a/deploy/baota-init.sh b/deploy/baota-init.sh index 0dc342b..927d2d4 100755 --- a/deploy/baota-init.sh +++ b/deploy/baota-init.sh @@ -155,6 +155,9 @@ main() { field_key=$(openssl rand -base64 32 | tr -d '\n') setup_code="KD-$(random_secret)" setup_hash=$(printf '%s' "$setup_code" | sha256sum | awk '{print $1}') + # The Baota process manager does not own a restartable application unit. + # Keep its updater files for diagnostics, but do not expose online update + # actions until the panel lifecycle is integrated with the transaction state machine. write_env_file "$CONFIG_ROOT/kaidi.env" \ SPRING_PROFILES_ACTIVE production \ SERVER_ADDRESS 127.0.0.1 \ @@ -174,7 +177,7 @@ main() { FINANCE_SETUP_ENV_FILE "$STATE_ROOT/setup/application.env" \ FINANCE_SETUP_MARKER_FILE "$STATE_ROOT/setup/locked" \ FINANCE_SETUP_RESTART_AFTER_COMPLETE true \ - FINANCE_UPDATE_ENABLED true \ + FINANCE_UPDATE_ENABLED false \ UPDATE_RELEASE_BASE_URL '' \ UPDATE_RELEASE_API_URL "$RELEASE_API_URL" \ UPDATE_RELEASE_TOKEN '' \ diff --git a/deploy/baota-start.sh b/deploy/baota-start.sh index d7ab3b9..3904738 100755 --- a/deploy/baota-start.sh +++ b/deploy/baota-start.sh @@ -51,7 +51,7 @@ is_managed_env_name() { FINANCE_SETUP_TOKEN_SHA256|FINANCE_SETUP_ENV_FILE|FINANCE_SETUP_MARKER_FILE|\ FINANCE_SETUP_RESTART_AFTER_COMPLETE|FINANCE_UPDATE_ENABLED|\ UPDATE_RELEASE_BASE_URL|UPDATE_RELEASE_API_URL|UPDATE_RELEASE_TOKEN|\ - UPDATE_REQUEST_FILE|UPDATE_STATUS_FILE|UPDATE_CURRENT_VERSION_FILE|APP_VERSION|KAIDI_PID_FILE) + UPDATE_REQUEST_FILE|UPDATE_STATUS_FILE|UPDATE_CURRENT_VERSION_FILE|APP_VERSION|KAIDI_PID_FILE|KAIDI_JAVA_BIN) return 0 ;; *) return 1 ;; @@ -85,14 +85,23 @@ load_env_file() { # The setup-generated runtime file has precedence over the base file. Non-empty # variables supplied by Baota have precedence over both; empty panel fields do not -# mask the protected configuration written by the setup wizard. -load_env_file "$RUNTIME_ENV_FILE" -load_env_file "$CONFIG_FILE" +# mask the protected configuration written by the setup wizard. The systemd unit +# loads both files itself as root, then sets KAIDI_ENV_PRELOADED so the service +# user never needs traversal permission for /etc/kaidi (which is intentionally +# root-only). +if [ "${KAIDI_ENV_PRELOADED:-false}" != true ]; then + load_env_file "$RUNTIME_ENV_FILE" + load_env_file "$CONFIG_FILE" +fi JAVA_BIN=${KAIDI_JAVA_BIN:-} if [ -z "$JAVA_BIN" ] && [ -n "${JAVA_HOME:-}" ]; then JAVA_BIN="$JAVA_HOME/bin/java" fi +if [ -z "$JAVA_BIN" ] && [ -x /opt/kaidi/runtime/java/bin/java ]; then + # Compatibility for installations created before external Java paths were persisted. + JAVA_BIN=/opt/kaidi/runtime/java/bin/java +fi if [ -z "$JAVA_BIN" ]; then JAVA_BIN=$(command -v java 2>/dev/null || true) fi @@ -102,6 +111,9 @@ if [[ "$JAVA_BIN" == */* ]]; then else command -v "$JAVA_BIN" >/dev/null 2>&1 || die "Java executable $JAVA_BIN is not available" fi +JAVA_VERSION=$("$JAVA_BIN" -version 2>&1 | sed -n 's/.*version "\([0-9][0-9]*\).*/\1/p' | head -n 1) +[[ "$JAVA_VERSION" =~ ^[0-9]+$ ]] && [ "$JAVA_VERSION" -ge 17 ] \ + || die "Java 17 or newer is required" export SPRING_PROFILES_ACTIVE=${SPRING_PROFILES_ACTIVE:-production} export SERVER_ADDRESS=${SERVER_ADDRESS:-127.0.0.1} diff --git a/deploy/env.production.example b/deploy/env.production.example index fc5a7c5..342fdcb 100644 --- a/deploy/env.production.example +++ b/deploy/env.production.example @@ -1,26 +1,26 @@ SPRING_PROFILES_ACTIVE=production SERVER_PORT=18080 SERVER_ADDRESS=127.0.0.1 -SESSION_COOKIE_SECURE=false +SESSION_COOKIE_SECURE=true -DB_URL=jdbc:mysql://127.0.0.1:3307/kaidi_finance?useUnicode=true&characterEncoding=utf8&connectionTimeZone=UTC&serverTimezone=UTC -DB_USERNAME=kaidi +DB_URL=jdbc:mysql://DB_HOST:DB_PORT/DB_NAME?useUnicode=true&characterEncoding=utf8&connectionTimeZone=UTC&serverTimezone=UTC +DB_USERNAME=DB_USERNAME DB_PASSWORD=CHANGE_ME FIELD_ENCRYPTION_KEY=BASE64_32_BYTE_KEY FILE_STORAGE_ROOT=/var/lib/kaidi/files FILE_STORAGE_TEMP=/var/lib/kaidi/tmp -FILE_SCANNER_ENABLED=false +FILE_SCANNER_ENABLED=true FINANCE_BOOTSTRAP_ENABLED=false FINANCE_BOOTSTRAP_PASSWORD= -APP_VERSION=1.0.0-preview.9 +APP_VERSION=1.0.0-preview.31 UPDATE_CURRENT_VERSION_FILE=/opt/kaidi/current/VERSION FINANCE_UPDATE_ENABLED=true # Use either a stable direct asset base URL or the public Gitea latest-release API. UPDATE_RELEASE_BASE_URL= UPDATE_RELEASE_API_URL=https://git.awaioi.com/api/v1/repos/ERP-Team/kaidi/releases/latest -UPDATE_RELEASE_TOKEN=READ_ONLY_GITEA_TOKEN +UPDATE_RELEASE_TOKEN= UPDATE_REQUEST_FILE=/var/lib/kaidi-update/inbox/request.json UPDATE_STATUS_FILE=/var/lib/kaidi-update/status.json diff --git a/deploy/install-from-git.sh b/deploy/install-from-git.sh index 1db14fe..fd99453 100755 --- a/deploy/install-from-git.sh +++ b/deploy/install-from-git.sh @@ -5,7 +5,7 @@ umask 077 ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) INSTALLER="$ROOT/deploy/install.sh" -INSTALLER_SHA256=${KAIDI_INSTALLER_SHA256:-6256b05025aa314d4c02e7c0f5d2ca530f28ec5d10f833504b34c09ada6e02a5} +INSTALLER_SHA256=${KAIDI_INSTALLER_SHA256:-6a454aff209e62d7ac75b7f97670d700ec05be710854a02136f41f55e82f9fa9} RELEASE_API_URL=${KAIDI_RELEASE_API_URL:-https://git.awaioi.com/api/v1/repos/ERP-Team/kaidi/releases/latest} PUBLIC_KEY_SHA256=${KAIDI_RELEASE_PUBLIC_KEY_SHA256:-807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9} TOKEN_FILE=${KAIDI_RELEASE_TOKEN_FILE:-} diff --git a/deploy/install.sh b/deploy/install.sh index a09e0ab..1a814e3 100755 --- a/deploy/install.sh +++ b/deploy/install.sh @@ -31,6 +31,7 @@ INSTALL_TRANSACTION_ARMED=false INSTALL_COMMITTED=false RELEASE_DIR= BACKUP_DIR= +JAVA_BIN= JAVA_STAGED_DIR= JAVA_PREVIOUS_DIR= JAVA_ACTIVATED=false @@ -81,6 +82,15 @@ preflight_host() { esac [ "$host_bits" = "$expected_bits" ] \ || die "CPU architecture and userspace word size do not match: $HOST_ARCH/$host_bits-bit" + if [ "$HOST_ARCH" = x86 ]; then + getconf GNU_LIBC_VERSION >/dev/null 2>&1 \ + || die "32-bit Linux deployment requires glibc; the downloaded i686 Java runtime is not compatible with musl" + if ! for loader in /lib/ld-linux.so.2 /lib32/ld-linux.so.2 /lib/i386-linux-gnu/ld-linux.so.2; do + [ -x "$loader" ] && break + done; then + die "32-bit Linux deployment requires the glibc loader /lib/ld-linux.so.2" + fi + fi [[ "$TRUSTED_PUBLIC_KEY_SHA256" =~ ^[0-9A-Fa-f]{64}$ ]] \ || die "Set KAIDI_RELEASE_PUBLIC_KEY_SHA256 to the trusted release public-key SHA-256" case "$REINSTALL" in @@ -209,6 +219,17 @@ install_packages() { fi } +preflight_runtime_commands() { + local command_name + for command_name in \ + awk bash chmod chown cp curl cut date find getconf getent grep gzip head id install jq journalctl \ + ln mktemp mv nologin openssl readlink rm runuser sed sha256sum sleep sort systemctl \ + systemd-analyze tar tr useradd usermod groupadd wc flock setsid; do + command -v "$command_name" >/dev/null 2>&1 \ + || die "Required command is missing after dependency preparation: $command_name" + done +} + azul_arch() { case "${HOST_ARCH:-$(normalized_host_arch)}" in x86_64) printf x86 ;; @@ -282,12 +303,13 @@ system_java_home() { prepare_java() { local api java_metadata package_metadata package_uuid java_url java_sha256 actual_sha256 system_home java_line + JAVA_BIN= + JAVA_STAGED_DIR= if system_home=$(system_java_home); then - JAVA_STAGED_DIR="$WORK_DIR/java.next" - mkdir -p "$JAVA_STAGED_DIR" - log "Copying existing Java 17 runtime from $system_home into the managed application directory" - COPYFILE_DISABLE=1 cp -R "$system_home/." "$JAVA_STAGED_DIR/" - java_line=$("$JAVA_STAGED_DIR/bin/java" -version 2>&1 | head -n 1) + JAVA_BIN="$system_home/bin/java" + java_line=$("$JAVA_BIN" -version 2>&1 | head -n 1) \ + || die "The existing Java runtime cannot start on this host" + log "Using existing Java 17+ runtime at $system_home" log "Local Java verified: $java_line" return 0 fi @@ -312,11 +334,14 @@ prepare_java() { tar -xzf "$WORK_DIR/java.tar.gz" --strip-components=1 -C "$JAVA_STAGED_DIR" java_arch_matches_host "$JAVA_STAGED_DIR/bin/java" \ || die "Downloaded Java runtime architecture does not match $HOST_ARCH" - java_line=$("$JAVA_STAGED_DIR/bin/java" -version 2>&1 | head -n 1) + java_line=$("$JAVA_STAGED_DIR/bin/java" -version 2>&1 | head -n 1) \ + || die "The downloaded Java runtime cannot start; verify glibc and the host architecture" + JAVA_BIN="$APP_ROOT/runtime/java/bin/java" log "Downloaded Java verified: $java_line" } activate_java() { + [ -n "$JAVA_STAGED_DIR" ] || return 0 mkdir -p "$APP_ROOT/runtime" JAVA_PREVIOUS_DIR="$WORK_DIR/java.previous" if [ -e "$APP_ROOT/runtime/java" ] || [ -L "$APP_ROOT/runtime/java" ]; then @@ -383,10 +408,12 @@ verify_service_access() { runuser -u "$SERVICE_USER" -- sh -c \ 'cd "$1" && test -r app.jar && test -r public/index.html' sh "$APP_ROOT/current" \ || die "$SERVICE_USER cannot traverse or read the active release" - runuser -u "$SERVICE_USER" -- test -x "$APP_ROOT/runtime/java/bin/java" \ - || die "$SERVICE_USER cannot execute the managed Java runtime" - runuser -u "$SERVICE_USER" -- "$APP_ROOT/runtime/java/bin/java" -version >/dev/null 2>&1 \ - || die "The managed Java runtime cannot execute as $SERVICE_USER" + [ -n "$JAVA_BIN" ] && [ -x "$JAVA_BIN" ] \ + || die "The selected Java runtime is unavailable at $JAVA_BIN" + runuser -u "$SERVICE_USER" -- test -x "$JAVA_BIN" \ + || die "$SERVICE_USER cannot execute the selected Java runtime" + runuser -u "$SERVICE_USER" -- "$JAVA_BIN" -version >/dev/null 2>&1 \ + || die "The selected Java runtime cannot execute as $SERVICE_USER" runuser -u "$SERVICE_USER" -- test -w "$STATE_ROOT/files" \ || die "$SERVICE_USER cannot write the file-storage directory" runuser -u "$SERVICE_USER" -- test -w "$UPDATE_STATE_ROOT/inbox" \ @@ -828,6 +855,7 @@ validate_inputs configure_app_port preflight_host install_packages +preflight_runtime_commands BASE=${RELEASE_BASE_URL%/} WORK_DIR=$(mktemp -d) @@ -926,6 +954,7 @@ if [ "$SETUP_WIZARD" = true ]; then fi write_env_file_preserving_unknown "$CONFIG_ROOT/kaidi.env" \ SPRING_PROFILES_ACTIVE production \ + KAIDI_JAVA_BIN "$JAVA_BIN" \ SERVER_PORT "$APP_PORT" \ SERVER_ADDRESS "$SERVER_ADDRESS" \ SESSION_COOKIE_SECURE "$SESSION_COOKIE_SECURE" \ @@ -963,6 +992,7 @@ write_env_file "$CONFIG_ROOT/update.env" \ KAIDI_APP_ROOT "$APP_ROOT" \ KAIDI_UPDATE_STATE_ROOT "$UPDATE_STATE_ROOT" \ KAIDI_SERVICE_NAME kaidi-finance.service \ + KAIDI_JAVA_BIN "$JAVA_BIN" \ KAIDI_HEALTH_URL "$HEALTH_URL" \ KAIDI_APP_INDEX_URL "$APP_INDEX_URL" \ KAIDI_DB_CONTAINER "${KAIDI_DB_CONTAINER:-}" \ diff --git a/deploy/systemd/kaidi-finance.service b/deploy/systemd/kaidi-finance.service index 7f9e839..18de15f 100644 --- a/deploy/systemd/kaidi-finance.service +++ b/deploy/systemd/kaidi-finance.service @@ -11,8 +11,9 @@ User=kaidi Group=kaidi EnvironmentFile=/etc/kaidi/kaidi.env EnvironmentFile=-/var/lib/kaidi/setup/application.env +Environment=KAIDI_ENV_PRELOADED=true WorkingDirectory=/opt/kaidi/current -ExecStart=/opt/kaidi/runtime/java/bin/java -XX:MaxRAMPercentage=70 -Dfile.encoding=UTF-8 -jar /opt/kaidi/current/app.jar +ExecStart=/opt/kaidi/current/ops/baota-start.sh SuccessExitStatus=143 Restart=on-failure RestartSec=5 diff --git a/deploy/update.sh b/deploy/update.sh index 6d7952b..ba512af 100755 --- a/deploy/update.sh +++ b/deploy/update.sh @@ -19,6 +19,7 @@ RELEASE_API_URL=${UPDATE_RELEASE_API_URL:-} RELEASE_TOKEN=${UPDATE_RELEASE_TOKEN:-} CACHE_ROOT=${KAIDI_UPDATE_CACHE_ROOT:-$STATE_ROOT/cache} SERVICE_NAME=${KAIDI_SERVICE_NAME:-kaidi-finance.service} +JAVA_BIN=${KAIDI_JAVA_BIN:-$APP_ROOT/runtime/java/bin/java} UPDATE_PATH_NAME=${KAIDI_UPDATE_PATH_NAME:-kaidi-update.path} PROCESS_MANAGER=${KAIDI_PROCESS_MANAGER:-systemd} PID_FILE=${KAIDI_PID_FILE:-/var/lib/kaidi/kaidi.pid} @@ -276,8 +277,9 @@ verify_release_access() { if [ "$PROCESS_MANAGER" = baota ]; then runuser -u "$SERVICE_USER" -- test -x "$release_dir/ops/baota-start.sh" else - runuser -u "$SERVICE_USER" -- test -x "$APP_ROOT/runtime/java/bin/java" \ - && runuser -u "$SERVICE_USER" -- "$APP_ROOT/runtime/java/bin/java" -version >/dev/null 2>&1 + runuser -u "$SERVICE_USER" -- test -x "$release_dir/ops/baota-start.sh" \ + && runuser -u "$SERVICE_USER" -- test -x "$JAVA_BIN" \ + && runuser -u "$SERVICE_USER" -- "$JAVA_BIN" -version >/dev/null 2>&1 fi } @@ -1009,7 +1011,7 @@ if command -v restorecon >/dev/null 2>&1; then fi if ! verify_release_access "$RELEASE_DIR"; then rm -rf "$RELEASE_DIR" - fail "Service user cannot access the release or managed Java runtime" + fail "Service user cannot access the release or selected Java runtime" fi PREVIOUS_TARGET=$(readlink "$APP_ROOT/current" 2>/dev/null || true) diff --git a/scripts/package-release.sh b/scripts/package-release.sh index 11df0a8..e6a57da 100755 --- a/scripts/package-release.sh +++ b/scripts/package-release.sh @@ -75,13 +75,24 @@ JAR="$ROOT/backend/target/finance-system-$VERSION.jar" JAR_METADATA_DIR="$WORK/jar-metadata" mkdir -p "$JAR_METADATA_DIR" (cd "$JAR_METADATA_DIR" && jar -xf "$JAR" \ - META-INF/MANIFEST.MF META-INF/maven/com.kaidi/finance-system/pom.properties) + META-INF/MANIFEST.MF META-INF/maven/com.kaidi/finance-system/pom.properties \ + BOOT-INF/classes/application.yml BOOT-INF/classes/application-local.yml \ + BOOT-INF/classes/application-production.yml) JAR_POM_VERSION=$(sed -n 's/^version=//p' \ "$JAR_METADATA_DIR/META-INF/maven/com.kaidi/finance-system/pom.properties") JAR_IMPLEMENTATION_VERSION=$(sed -n 's/^Implementation-Version: //p' \ "$JAR_METADATA_DIR/META-INF/MANIFEST.MF" | tr -d '\r') [ "$JAR_POM_VERSION" = "$VERSION" ] && [ "$JAR_IMPLEMENTATION_VERSION" = "$VERSION" ] \ || { printf 'JAR metadata versions do not match release %s\n' "$VERSION" >&2; exit 1; } +grep -Fq 'default: production' "$JAR_METADATA_DIR/BOOT-INF/classes/application.yml" \ + || { printf 'Release JAR must default to the production profile\n' >&2; exit 1; } +if grep -Eq '127\.0\.0\.1:3307|kaidi_local_2026' \ + "$JAR_METADATA_DIR/BOOT-INF/classes/application.yml" \ + "$JAR_METADATA_DIR/BOOT-INF/classes/application-local.yml" \ + "$JAR_METADATA_DIR/BOOT-INF/classes/application-production.yml"; then + printf 'Release JAR contains a development database fallback\n' >&2 + exit 1 +fi rm -rf "$OUTPUT_DIR" mkdir -p "$OUTPUT_DIR" @@ -102,6 +113,15 @@ 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" "$STAGE/ops/baota-start.sh" "$STAGE/ops/baota-init.sh" +if find "$STAGE" -type l -print -quit | grep -q .; then + printf 'Release stage contains a symbolic link\n' >&2 + exit 1 +fi +if find "$STAGE" -type f -print | grep -Eq '(^|/)(\.DS_Store|__MACOSX|[^/]*\._[^/]*)$'; then + printf 'Release stage contains macOS metadata\n' >&2 + exit 1 +fi + ARTIFACT="kaidi-finance-$VERSION.tar.gz" COPYFILE_DISABLE=1 tar --format=ustar -czf "$OUTPUT_DIR/$ARTIFACT" -C "$STAGE" . SHA256=$(sha256_file "$OUTPUT_DIR/$ARTIFACT") diff --git a/scripts/test-baota-start-fixture.sh b/scripts/test-baota-start-fixture.sh index 9184fc7..04bd824 100755 --- a/scripts/test-baota-start-fixture.sh +++ b/scripts/test-baota-start-fixture.sh @@ -51,6 +51,10 @@ EOF cat > "$WORK/java" <<'SH' #!/usr/bin/env bash set -Eeuo pipefail +if [ "${1:-}" = -version ]; then + printf 'openjdk version "17-fixture"\n' >&2 + exit 0 +fi { printf 'cwd=%s\n' "$PWD" printf 'args=%s\n' "$*" @@ -92,6 +96,34 @@ PID_RECORD=$(sed -n 's/^pid-record=//p' "$WORK/java.log") SELF_PID=$(sed -n 's/^self-pid=//p' "$WORK/java.log") [ "$PID_RECORD" = "$SELF_PID 424242" ] || fail 'PID file did not bind the PID to its proc start time' +# systemd reads the root-only EnvironmentFile before dropping to User=kaidi. +# The launcher must therefore be able to start with those files intentionally +# unreadable by the service user. +chmod 000 "$WORK/base.env" "$WORK/runtime.env" +MOCK_JAVA_LOG="$WORK/preloaded.log" \ +PATH="$WORK/mock-bin:$PATH" \ +KAIDI_ENV_PRELOADED=true \ +KAIDI_JAVA_BIN="$WORK/java" \ +SPRING_PROFILES_ACTIVE=production \ +SERVER_ADDRESS=127.0.0.1 \ +SERVER_PORT=21000 \ +DB_URL='jdbc:mysql://db.fixture/kaidi_finance' \ +DB_USERNAME=fixture-user \ +DB_PASSWORD='fixture-password' \ +FIELD_ENCRYPTION_KEY=fixture-field-key \ +FILE_STORAGE_ROOT="$WORK/files" \ +FILE_STORAGE_TEMP="$WORK/tmp" \ +FINANCE_SETUP_ENABLED=false \ +FINANCE_UPDATE_ENABLED=true \ +FILE_SCANNER_ENABLED=false \ +KAIDI_PID_FILE="$WORK/state/preloaded.pid" \ +KAIDI_CONFIG_FILE="$WORK/base.env" \ +KAIDI_RUNTIME_ENV_FILE="$WORK/runtime.env" \ + "$RELEASE/ops/baota-start.sh" +grep -Fqx 'port=21000' "$WORK/preloaded.log" \ + || fail 'launcher still depended on the root-only configuration files under systemd' +chmod 0644 "$WORK/base.env" "$WORK/runtime.env" + MOCK_JAVA_LOG="$WORK/explicit.log" \ PATH="$WORK/mock-bin:$PATH" \ KAIDI_JAVA_BIN="$WORK/java" \ diff --git a/scripts/test-install-fixture.sh b/scripts/test-install-fixture.sh index c35e2d9..041ee5c 100755 --- a/scripts/test-install-fixture.sh +++ b/scripts/test-install-fixture.sh @@ -29,6 +29,7 @@ mode_of() { sed -n '/^normalized_host_arch()/,/^}/p' "$ROOT/deploy/install.sh" sed -n '/^azul_arch()/,/^}/p' "$ROOT/deploy/install.sh" sed -n '/^java_arch_matches_host()/,/^}/p' "$ROOT/deploy/install.sh" + sed -n '/^java_home_from_bin()/,/^}/p' "$ROOT/deploy/install.sh" sed -n '/^system_java_home()/,/^}/p' "$ROOT/deploy/install.sh" sed -n '/^sha256_file()/,/^}/p' "$ROOT/deploy/install.sh" sed -n '/^prepare_java()/,/^}/p' "$ROOT/deploy/install.sh" @@ -192,7 +193,7 @@ cat > "$WORK/fake-jre/bin/java" <<'JAVA' if [ "${1:-}" = '-XshowSettings:properties' ]; then printf ' os.arch = x86\n' >&2 fi -printf 'openjdk version "17-fixture"\n' >&2 +printf 'openjdk version "17.0.8"\n' >&2 JAVA chmod 0755 "$WORK/fake-jre/bin/java" tar -czf "$WORK/java-fixture.tar.gz" -C "$WORK" fake-jre @@ -213,16 +214,22 @@ download() { esac } ARCH_FIXTURE=i686 +APP_ROOT="$WORK/app" export KAIDI_JAVA_HOME="$WORK/missing-java-home" prepare_java >/dev/null 2>&1 [ -x "$JAVA_STAGED_DIR/bin/java" ] || fail 'verified i686 Java runtime was not staged' +[ "$JAVA_BIN" = "$APP_ROOT/runtime/java/bin/java" ] \ + || fail 'downloaded Java did not select the managed fallback path' rm -rf "$JAVA_STAGED_DIR" +JAVA_STAGED_DIR= export KAIDI_JAVA_HOME="$WORK/fake-jre" prepare_java >/dev/null 2>&1 -[ -d "$JAVA_STAGED_DIR" ] && [ ! -L "$JAVA_STAGED_DIR" ] \ - || fail 'existing Java 17 runtime was not copied into managed storage' -cmp -s "$WORK/fake-jre/bin/java" "$JAVA_STAGED_DIR/bin/java" \ - || fail 'installer staged an unexpected local Java runtime' +[ -z "$JAVA_STAGED_DIR" ] \ + || fail 'installer copied an existing server Java runtime into application storage' +EXPECTED_LOCAL_JAVA=$(readlink -f "$WORK/fake-jre/bin/java" 2>/dev/null \ + || printf '%s' "$WORK/fake-jre/bin/java") +[ "$JAVA_BIN" = "$EXPECTED_LOCAL_JAVA" ] \ + || fail 'installer did not select the existing server Java executable directly' export RELEASE_API_URL=https://gitea.fixture.invalid/api/v1/repos/ERP-Team/kaidi/releases/latest RELEASE_TOKEN=fixture-read-only-token @@ -280,6 +287,12 @@ grep -Fq '[ "$APP_ROOT" = /opt/kaidi ]' "$ROOT/deploy/install.sh" \ || fail 'installer no longer rejects unsupported custom roots' grep -Fq '8.4.*) ;;' "$ROOT/deploy/install.sh" \ || fail 'installer no longer enforces MySQL 8.4.x' +grep -Fq '32-bit Linux deployment requires glibc' "$ROOT/deploy/install.sh" \ + || fail 'installer does not reject unsupported musl 32-bit hosts before downloading Java' +grep -Fq '32-bit Linux deployment requires the glibc loader' "$ROOT/deploy/install.sh" \ + || fail 'installer does not reject a 32-bit host without the glibc loader' +grep -Fq 'preflight_runtime_commands' "$ROOT/deploy/install.sh" \ + || fail 'installer does not validate required runtime commands' # shellcheck disable=SC2016 # Match literal installer source. grep -Fq 'write_env_file_preserving_unknown "$CONFIG_ROOT/kaidi.env"' "$ROOT/deploy/install.sh" \ || fail 'installer no longer uses the reinstall-safe environment writer' @@ -289,14 +302,14 @@ grep -Fq 'download "$api" "$java_metadata"' "$ROOT/deploy/install.sh" \ # shellcheck disable=SC2016 # Match literal installer source. grep -Fq 'java_sha256=$(jq -er' "$ROOT/deploy/install.sh" \ || fail 'installer no longer obtains the Java runtime SHA-256' -grep -Fq 'Copying existing Java 17 runtime' "$ROOT/deploy/install.sh" \ +grep -Fq 'Using existing Java 17+ runtime' "$ROOT/deploy/install.sh" \ || fail 'installer no longer reuses a local Java 17 runtime' # shellcheck disable=SC2016 # Match literal installer source. -grep -Fq 'cp -R "$system_home/." "$JAVA_STAGED_DIR/"' "$ROOT/deploy/install.sh" \ - || fail 'installer no longer copies a local Java runtime into managed storage' +grep -Fq 'JAVA_BIN="$system_home/bin/java"' "$ROOT/deploy/install.sh" \ + || fail 'installer no longer records the selected server Java path' # shellcheck disable=SC2016 # Match literal installer source. -! grep -Fq 'ln -s "$system_home"' "$ROOT/deploy/install.sh" \ - || fail 'installer must not link the service to an externally managed Java directory' +! grep -Fq 'cp -R "$system_home/."' "$ROOT/deploy/install.sh" \ + || fail 'installer copied an existing server Java runtime into managed storage' # shellcheck disable=SC2016 # Match the literal installer command. grep -Fq -- '--connect-timeout 1 --max-time 2 "$HEALTH_URL" 2>/dev/null' "$ROOT/deploy/install.sh" \ || fail 'installer no longer performs a quiet bounded health check' @@ -317,6 +330,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 'ExecStart=/opt/kaidi/current/ops/baota-start.sh' "$ROOT/deploy/systemd/kaidi-finance.service" \ + || fail 'application service bypasses the Java-selecting launcher' +grep -Fqx 'Environment=KAIDI_ENV_PRELOADED=true' "$ROOT/deploy/systemd/kaidi-finance.service" \ + || fail 'systemd service user would need to read the root-only configuration directory' grep -Fqx 'Restart=no' "$ROOT/deploy/systemd/kaidi-update.service" \ || fail 'update service can automatically repeat a failed switching transaction' grep -Fq -- '-/www/wwwroot/kaidi' "$ROOT/deploy/systemd/kaidi-update.service" \ @@ -392,9 +409,11 @@ grep -Fq 'load_runtime_database_env' "$ROOT/deploy/update.sh" \ # shellcheck disable=SC2016 # Match literal installer source. grep -Fq 'install -d -o root -g "$SERVICE_GROUP" -m 0750 "$UPDATE_STATE_ROOT"' "$ROOT/deploy/install.sh" \ || fail 'update state parent is not group-accessible to the application user' -grep -Fq 'kaidi-finance-1.0.0-preview.25.tar.gz' "$ROOT/README.md" \ +grep -Fq 'kaidi-finance-1.0.0-preview.31.tar.gz' "$ROOT/README.md" \ || fail 'README does not document the public manual-deployment artifact' grep -Fq 'ops/baota-init.sh' "$ROOT/README.md" \ || fail 'README does not document the local Baota initialization command' +grep -Fq 'FINANCE_UPDATE_ENABLED false' "$ROOT/deploy/baota-init.sh" \ + || fail 'Baota manual mode still exposes the unstable online updater' printf 'Install configuration, custom port, public Gitea, optional private token, i686, setup wizard, and MySQL 8.4 fixtures passed\n' diff --git a/scripts/test-update-fixture.sh b/scripts/test-update-fixture.sh index 0cf326c..715780e 100755 --- a/scripts/test-update-fixture.sh +++ b/scripts/test-update-fixture.sh @@ -221,10 +221,14 @@ build_release() { local unit_prefix=${3:-new} local stage="$fixture/stage" mkdir -p "$fixture/release" "$stage/public" "$stage/ops" - "$REAL_OPENSSL" genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 \ - -out "$fixture/private.pem" >/dev/null 2>&1 - "$REAL_OPENSSL" pkey -in "$fixture/private.pem" -pubout \ - -out "$fixture/release-public.pem" >/dev/null 2>&1 + if [ ! -s "$WORK/fixture-private.pem" ]; then + "$REAL_OPENSSL" genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 \ + -out "$WORK/fixture-private.pem" >/dev/null 2>&1 + "$REAL_OPENSSL" pkey -in "$WORK/fixture-private.pem" -pubout \ + -out "$WORK/fixture-public.pem" >/dev/null 2>&1 + fi + cp "$WORK/fixture-private.pem" "$fixture/private.pem" + cp "$WORK/fixture-public.pem" "$fixture/release-public.pem" printf 'new application\n' > "$stage/app.jar" printf 'new\n' > "$stage/public/index.html" printf '%s\n' "$version" > "$stage/VERSION" @@ -681,17 +685,30 @@ assert_install_without_verified_cache_rejected() { || fail 'install without cache changed the active application' } +printf '[update-fixture] success\n' assert_success_case +printf '[update-fixture] identical-systemd-operations\n' assert_identical_systemd_operations_case +printf '[update-fixture] path-watcher-failure\n' assert_update_path_failure_keeps_application_case +printf '[update-fixture] rollback\n' assert_rollback_case +printf '[update-fixture] incomplete-rollback-recovery-lock\n' assert_incomplete_rollback_requires_manual_recovery_case +printf '[update-fixture] download-failure\n' assert_download_failure_case +printf '[update-fixture] database-backup-failure\n' assert_database_failure_case +printf '[update-fixture] unhealthy-baseline\n' assert_unhealthy_baseline_blocks_restart_case +printf '[update-fixture] invalid-database-url\n' assert_invalid_database_url_blocks_restart_case +printf '[update-fixture] symlink-request\n' assert_symlink_request_rejected +printf '[update-fixture] missing-verified-cache\n' assert_install_without_verified_cache_rejected +printf '[update-fixture] private-gitea\n' assert_private_gitea_release_case +printf '[update-fixture] cross-origin-gitea\n' assert_cross_origin_gitea_browser_url_ignored printf 'Online update download, confirmation, success, rollback, failure, and unsafe-request fixtures passed\n' diff --git a/scripts/verify-release.sh b/scripts/verify-release.sh index c01df77..6af8d3e 100755 --- a/scripts/verify-release.sh +++ b/scripts/verify-release.sh @@ -153,17 +153,36 @@ if grep -Eq '(^|/)(signing-private|private-key|id_rsa)' <<< "$ARCHIVE_LIST"; the printf 'Release archive contains private key material\n' >&2 exit 1 fi +if grep -Eq '(^|/)(\.DS_Store|__MACOSX|[^/]*\._[^/]*)$' <<< "$ARCHIVE_LIST"; then + printf 'Release archive contains macOS metadata\n' >&2 + exit 1 +fi +if tar -tvzf "$ARTIFACT" | grep -Eq '^l'; then + printf 'Release archive contains a symbolic link\n' >&2 + exit 1 +fi [ "$(tar -xOf "$ARTIFACT" ./VERSION)" = "$VERSION" ] \ || { printf 'Release archive version does not match the manifest\n' >&2; exit 1; } tar -xOf "$ARTIFACT" ./app.jar > "$WORK/app.jar" (cd "$WORK" && jar -xf app.jar \ - META-INF/MANIFEST.MF META-INF/maven/com.kaidi/finance-system/pom.properties) + META-INF/MANIFEST.MF META-INF/maven/com.kaidi/finance-system/pom.properties \ + BOOT-INF/classes/application.yml BOOT-INF/classes/application-local.yml \ + BOOT-INF/classes/application-production.yml) JAR_POM_VERSION=$(sed -n 's/^version=//p' \ "$WORK/META-INF/maven/com.kaidi/finance-system/pom.properties") JAR_IMPLEMENTATION_VERSION=$(sed -n 's/^Implementation-Version: //p' \ "$WORK/META-INF/MANIFEST.MF" | tr -d '\r') [ "$JAR_POM_VERSION" = "$VERSION" ] && [ "$JAR_IMPLEMENTATION_VERSION" = "$VERSION" ] \ || { printf 'Release JAR metadata versions do not match the manifest\n' >&2; exit 1; } +grep -Fq 'default: production' "$WORK/BOOT-INF/classes/application.yml" \ + || { printf 'Release JAR does not default to the production profile\n' >&2; exit 1; } +if grep -Eq '127\.0\.0\.1:3307|kaidi_local_2026' \ + "$WORK/BOOT-INF/classes/application.yml" \ + "$WORK/BOOT-INF/classes/application-local.yml" \ + "$WORK/BOOT-INF/classes/application-production.yml"; then + printf 'Release JAR contains a development database fallback\n' >&2 + exit 1 +fi compare_archive_file() { archive_path=$1