feat: support Baota manual deployment and managed updates
Release / release (push) Canceled after 0s

This commit is contained in:
Qiufeng
2026-08-18 03:08:28 +08:00
parent fa4517c263
commit cfdc0fed44
13 changed files with 765 additions and 269 deletions
+1 -1
View File
@@ -68,7 +68,7 @@ jobs:
./scripts/test-baota-start-fixture.sh ./scripts/test-baota-start-fixture.sh
./scripts/test-purge-fixture.sh ./scripts/test-purge-fixture.sh
./scripts/test-gitea-publish-fixture.sh ./scripts/test-gitea-publish-fixture.sh
shellcheck deploy/install.sh deploy/install-from-git.sh deploy/update.sh deploy/baota-start.sh \ shellcheck deploy/install.sh deploy/install-from-git.sh deploy/update.sh deploy/baota-start.sh deploy/baota-init.sh \
deploy/purge.sh \ deploy/purge.sh \
scripts/check-semver.sh \ scripts/check-semver.sh \
scripts/package-release.sh scripts/publish-gitea-release.sh \ scripts/package-release.sh scripts/publish-gitea-release.sh \
+105 -197
View File
@@ -41,20 +41,11 @@ npm run build
./scripts/export-openapi.sh http://127.0.0.1:18080 ./scripts/export-openapi.sh http://127.0.0.1:18080
``` ```
## R1 Preview 一键安装 ## R1 Preview 手动部署
代码仓库和 Release 已公开,Linux 服务器不需要 Gitea Token。安装器读取 代码仓库和 Release 已公开。本版交付物是签名的应用压缩包,部署方式固定为“下载、校验、解压、运行本地初始化脚本、
`https://git.awaioi.com/api/v1/repos/ERP-Team/kaidi/releases/latest`,生产机不执行 `git pull` 或现场编译。 在宝塔创建 Spring Boot 项目”;不再使用 `curl | bash` 一键安装。程序不会安装 MySQL、JDK、Nginx 或其他第三方服务,
安装脚本内置固定的发布公钥指纹,下载的应用包、清单、SBOM 和更新脚本仍须通过 RSA 签名与 SHA-256 校验。 也不会修改宝塔和反向代理配置。数据库只支持运维人员预先准备的外部 MySQL 8.4.x,PostgreSQL 18 兼容工作继续冻结。
Gitea API 只用于确认 Release tag 和资产名称;下载地址由受信 API 域名、仓库路径和 tag 重新构造,不采用
Gitea 响应中可能错误指向内网地址的 `browser_download_url`。
本版只支持由运维人员预先准备的外部 MySQL 8.4.x;安装器不会安装 MySQL、创建数据库容器或修改现有
PostgreSQL 18。PostgreSQL 兼容开发已冻结,不属于本次 Preview 发布范围。
执行命令的机器需预装 `bash`、`sudo`、`curl`、`mktemp` 和 `sha256sum`,并能访问目标 Gitea;`jq` 和 Java
由安装器补齐;Java 17 优先使用服务器已有运行时,没有合适版本时才从官方 Azul 下载。数据库服务和数据库客户端均不会被安装。默认首次安装向导直接使用 JDBC 连接浏览器中填写的
外部数据库,不需要服务器安装 MySQL CLI。应用固定安装到 `/opt/kaidi`、`/var/lib/kaidi`、
`/var/lib/kaidi-update` 和 `/etc/kaidi`。安装器不安装、不启动也不修改 Nginx、Caddy、宝塔或其他反向代理。
### 彻底清理旧安装 ### 彻底清理旧安装
@@ -76,201 +67,125 @@ curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-previe
`0600`,确认新安装及数据无误后再由 root 删除。脚本不删除外部 MySQL、系统 Java、Nginx 或宝塔站点配置; `0600`,确认新安装及数据无误后再由 root 删除。脚本不删除外部 MySQL、系统 Java、Nginx 或宝塔站点配置;
新安装必须连接一个新的空 MySQL 数据库,旧数据库保留用于回滚。 新安装必须连接一个新的空 MySQL 数据库,旧数据库保留用于回滚。
### 直接 curl 安装 ### Preview.25 直链与宝塔手动部署
本版不使用一键安装脚本。发布物是一个不带顶层目录的压缩包,下载后直接解压到版本目录,再由宝塔面板创建
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.25/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`
以及与外部 MySQL 兼容的 `mysqldump`。程序不会替你安装这些依赖。
#### 1. 下载、校验、解压
以下命令只做文件下载和解压,不执行远程脚本。`VERSION` 必须替换成发布版本;归档内的 `app.jar`、`public/`、`ops/`
必须直接位于 `RELEASE_ROOT`,不能再嵌套一层目录。
```bash ```bash
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.24/install.sh | sudo bash VERSION=1.0.0-preview.25
APP_ROOT=/www/wwwroot/kaidi
RELEASE_ROOT="$APP_ROOT/releases/$VERSION"
sudo install -d -m 0755 "$RELEASE_ROOT"
wget -O "/tmp/kaidi-finance-$VERSION.tar.gz" \
"https://git.awaioi.com/ERP-Team/kaidi/releases/download/v$VERSION/kaidi-finance-$VERSION.tar.gz"
wget -O /tmp/kaidi-SHA256SUMS \
"https://git.awaioi.com/ERP-Team/kaidi/releases/download/v$VERSION/SHA256SUMS"
(cd /tmp && grep " kaidi-finance-$VERSION.tar.gz$" kaidi-SHA256SUMS | sha256sum -c -)
sudo tar -xzf "/tmp/kaidi-finance-$VERSION.tar.gz" -C "$RELEASE_ROOT"
``` ```
这条命令会提示填写 Java 应用端口,直接回车使用 `18080`;随后安装最新签名 Release,并默认进入 `/setup` 如果服务器没有 `wget`,可使用浏览器下载后上传同一文件;`curl -fL URL -o FILE` 也只用于下载压缩包,不能替代初始化脚本。
安装向导。Java 默认只监听 `127.0.0.1:所选端口`,前端页面、API 和健康检查均由同一端口提供。无人值守安装可直接指定:
#### 2. 执行本地初始化
初始化脚本来自已下载的归档,并在本机以 root 运行。它创建 `kaidi` 启动用户、受保护配置、在线更新监听器和一次性
安装码;不会下载 Java、安装 MySQL 或修改反向代理。端口参数可改为 `1024-65535` 中的空闲端口,默认 `18080`。
```bash ```bash
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.24/install.sh \ sudo "$RELEASE_ROOT/ops/baota-init.sh" 18080
| sudo env KAIDI_APP_PORT=19090 bash sudo cat /root/kaidi-first-login.txt
``` ```
端口必须在 `1024-65535` 范围且未被其他程序监听;安装器会在安装前检查冲突。只有明确需要让其他主机直连 Java 初始化失败会回滚本次写入的配置、unit 和 `current` 链接,可以直接修正原因后重试;已经存在正式安装时不要重复执行,先按“彻底清理旧安装”流程处理。
时才设置 `KAIDI_SERVER_ADDRESS=0.0.0.0`,通常应保持默认回环绑定并由本机反向代理访问。需要在执行前独立校验安装脚本时使用:
```bash #### 3. 宝塔 Spring Boot 项目字段
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.24/install.sh -o /tmp/kaidi-install.sh
printf '%s %s\n' 6256b05025aa314d4c02e7c0f5d2ca530f28ec5d10f833504b34c09ada6e02a5 /tmp/kaidi-install.sh | sha256sum -c -
sudo bash /tmp/kaidi-install.sh
rm -f /tmp/kaidi-install.sh
```
### 从固定 Git tag 拉取后安装 在宝塔面板创建项目,类型选择 **Spring Boot**,不要选择 Tomcat。填写如下:
需要保留源码快照时,可以拉取与 Release 对应的固定 tag,再运行仓库内包装器。公共仓库不要求凭据, | 字段 | 填写值 |
包装器会在 `sudo` 前校验 `deploy/install.sh` 的固定 SHA-256,再按同一公钥信任链安装最新签名 Release。 | --- | --- |
| 项目路径 | `/www/wwwroot/kaidi/current` |
| 项目名称 | `kaidi-finance` |
| 项目 JDK | `JDK 17` |
| 项目启动命令 | `/www/wwwroot/kaidi/current/ops/baota-start.sh` |
| 启动用户 | `kaidi` |
| 项目端口 | `18080`(与初始化参数一致) |
| 绑定域名 | 业务域名,例如 `fi.awaioi.com` |
| 后端路由 | `/` |
| 前端资源 | 留空,Spring Boot 直接提供 `public/` |
| 进程守护 | 开启 |
| 项目意外重启 | 勾选 |
```bash **环境变量全部留空,不要创建 DB_URL、DB_USERNAME、DB_PASSWORD 或 FIELD_ENCRYPTION_KEY。**
git clone --branch v1.0.0-preview.24 --depth 1 https://git.awaioi.com/ERP-Team/kaidi.git kaidi-preview `ops/baota.env.example` 仅是说明文件,不能把其中的占位值粘贴到面板。启动器先读取 `/etc/kaidi/kaidi.env`,向导完成后
cd kaidi-preview 再读取 `/var/lib/kaidi/setup/application.env`;宝塔只有填写了非空覆盖值时才会覆盖受保护配置。数据库密码不会出现在
./deploy/install-from-git.sh Java 启动命令或 `ps` 参数中。
```
首次安装使用向导时不要传 `KAIDI_DB_URL`、`KAIDI_DB_USERNAME` 或 `KAIDI_DB_PASSWORD`;这些值在浏览器中填写。 #### 4. 首次向导与反向代理
只有已完成安装的修复性重装才从运行时配置读取数据库值,详见下文。
安装器会完成以下动作: 启动宝塔项目后,访问 `https://业务域名/setup`,输入 `/root/kaidi-first-login.txt` 中的一次性安装码,填写外部 MySQL
连接信息和系统管理员账号密码。迁移完成后应用会退出一次,宝塔守护进程会自动拉起正式模式;再次访问域名根路径即可登录。
- 仅在 Linux + systemd 环境执行;首版 32 位支持基线为带 systemd 的 Debian/Ubuntu x86 32 位 Linux。 首次配置数据库密码必须使用 HTTPS;没有域名时通过 SSH 隧道访问回环端口:
- 不安装或创建数据库;在 `/setup` 中连接运维人员预先准备的外部 MySQL 8.4.x。
- 识别 `x86_64`、`aarch64`、`armv7` 或 32 位 `i386/i486/i586/i686`;已有 Java 17 直接复用,否则校验 SHA-256 后下载对应的官方 Azul Java 17 JRE。
- 使用安装器内置的 SHA-256 指纹校验 Release 公钥,再用该公钥验证发布清单 RSA 签名。
- 首次安装默认启用 `/setup` 向导,不在命令行保存数据库密码;向导只接受 MySQL 8.4.x,并在提交前验证完整迁移权限。
- 安装签名 Release 到 `/opt/kaidi/releases/<version>`,以 `/opt/kaidi/current` 原子切换当前版本。
- 安装 `kaidi-finance.service`、更新监听服务和健康检查;Java 同时托管 TDesign 前端静态资源及 Vue 路由回退。
- 向导只初始化一个由操作者填写的 `SYSTEM_ADMIN` 管理员,不创建项目、财务、资料或演示账号。
- 安装器把一次性安装码写入仅 root 可读的 `/root/kaidi-first-login.txt`;完成向导后写入锁定标记并切换正式应用。
### 环境变量与密钥安全
首次安装不需要在命令行或宝塔项目中填写数据库环境变量。数据库地址、账号和密码只在 `/setup` 向导中提交,
完成后写入 `/var/lib/kaidi/setup/application.env`;字段加密密钥由安装器生成并写入
`/etc/kaidi/kaidi.env`。两个文件不会把秘密放进 Java 启动参数或 `ps` 输出,并由 Linux 文件权限限制为
root 或专用 `kaidi` 服务账号读取。运行中的 Java 仍必须持有这些值,因此 root 和该服务账号始终属于受信边界。
不要通过公网 HTTP 提交数据库密码。应先为域名配置有效 HTTPS,或通过 SSH 端口转发访问本机回环地址完成向导:
```bash ```bash
ssh -L 18080:127.0.0.1:18080 root@SERVER_IP ssh -L 18080:127.0.0.1:18080 root@SERVER_IP
``` ```
然后在本机浏览器打开 `http://127.0.0.1:18080/setup`。向导完成后自动锁定,安装码不能再次使用。 本机浏览器打开 `http://127.0.0.1:18080/setup`。反向代理只需把整个 `/` 转发到 `http://127.0.0.1:18080`,并保留以下请求头:
安装完成后先执行 `sudo cat /root/kaidi-first-login.txt`,其中会显示准确的反向代理目标和一次性安装码。配置反向代理后,
通过域名的 `/setup` 完成数据库和管理员配置,再访问域名根路径登录。Preview 使用 HTTP 时安装器默认设置
`SESSION_COOKIE_SECURE=false`;配置 HTTPS 反向代理后,应在 `/etc/kaidi/kaidi.env` 改为
`SESSION_COOKIE_SECURE=true` 并执行 `sudo systemctl restart kaidi-finance`。
以安装端口 `19090` 为例,Nginx 只需代理到 Java,不需要单独托管前端文件:
```nginx ```nginx
location / { location / {
proxy_pass http://127.0.0.1:19090; proxy_pass http://127.0.0.1:18080;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 10s;
proxy_read_timeout 120s; proxy_read_timeout 120s;
client_max_body_size 500m; client_max_body_size 500m;
} }
``` ```
宝塔、Caddy 或云网关使用同一个上游地址 `http://127.0.0.1:所选端口`。安装后执行以下命令确认应用和首次登录信息: HTTPS 启用后,在 `/etc/kaidi/kaidi.env` 将 `SESSION_COOKIE_SECURE` 设为 `true`,再从宝塔面板重启项目。健康检查:
```bash
curl -fsS http://127.0.0.1:19090/actuator/health | jq -e '.status == "UP"'
sudo systemctl --no-pager --full status kaidi-finance kaidi-update.path
sudo cat /root/kaidi-first-login.txt
```
### Linux 32 位
应用已按 Java 17 字节码构建,安装器会在 32 位 Linux 下载 `i686` JRE。MySQL 8.4 没有可用于该部署方式的
32 位服务端镜像,因此 32 位主机需要预先连接一台 MySQL 8.4 数据库,之后仍然只执行一个安装命令:
```bash
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.24/install.sh | sudo bash
```
无论主机架构如何,安装器都不会安装 MySQL、数据库客户端或创建数据库容器。在打开向导前,需要预先创建 `kaidi_finance`,并授予安装账号该库的
完整迁移权限。向导会连接数据库、核验 MySQL 8.4.x 版本,并实际探测建表、改表、索引、外键、存储过程、
临时表及读写权限;专用数据库不是 `utf8mb4/utf8mb4_0900_ai_ci` 时,向导会在迁移前自动统一默认字符集和
排序规则,全部通过后 Flyway 才会建表。
数据库管理员可在 MySQL 8.4 中按实际应用服务器地址执行以下基线 SQL:
```sql
CREATE DATABASE kaidi_finance CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
CREATE USER 'kaidi'@'KAIDI_SERVER_IP' IDENTIFIED BY 'DB_PASSWORD';
GRANT ALL PRIVILEGES ON kaidi_finance.* TO 'kaidi'@'KAIDI_SERVER_IP';
```
### 修复性重装
正常升级统一使用后台“在线更新”。只有安装文件损坏且后台更新不可用时,才在原服务器执行修复性重装;
安装器会优先读取向导完成后生成的 `/var/lib/kaidi/setup/application.env`,再回退到 `/etc/kaidi/kaidi.env`,保留数据库、字段加密密钥、
管理员数据和运维人员新增的环境变量:
```bash
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.24/install.sh \
| sudo env KAIDI_REINSTALL=true KAIDI_SETUP_WIZARD=false bash
```
没有显式设置 `KAIDI_APP_PORT` 时,修复性重装会沿用 `/etc/kaidi/kaidi.env` 中的原端口。重装失败会恢复原应用链接、Java 运行时、环境文件和 systemd 单元;脚本会明确报告回滚不完整,
不会把恢复失败吞掉。新版本安装器在 `KAIDI_REINSTALL=true` 时默认使用生产修复模式,显式的
`KAIDI_SETUP_WIZARD=false` 仅用于兼容旧版本安装器。
如果安装器已完成但向导尚未提交,可执行下面的命令重新生成一次性安装码;该恢复路径只接受仍处于向导模式且未锁定的安装,正式模式不会被覆盖。
```bash
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.24/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.24
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 ```bash
curl -fsS http://127.0.0.1:18080/actuator/health curl -fsS http://127.0.0.1:18080/actuator/health
curl -fsS http://127.0.0.1:18080/ | head curl -fsS http://127.0.0.1:18080/ | head
systemctl is-enabled kaidi-update.path
``` ```
#### 5. 后台在线更新
初始化脚本已启用 `kaidi-update.path`,后台“系统治理 → 系统更新”流程保持不变:点击“获取更新”下载并验签,确认后点击“立即重启”。
root 更新器原子切换 `/www/wwwroot/kaidi/current`,向当前 Java 进程发送停止信号;宝塔的意外重启守护随后按新 `current` 链接拉起 Java,
健康检查通过后页面等待 10 秒并自动刷新。更新器不会安装或启动 `kaidi-finance.service`,也不会修改 Nginx。
更新前请在服务器预装与外部 MySQL 兼容的 `mysqldump`,更新器只生成权限为 `0600` 的备份,不会自动安装 MySQL。迁移失败时恢复旧应用版本并保留数据库备份和事务证据,遵循至少一个版本向后兼容的迁移规则。
后续版本仍按同样方式直接下载并解压到新的 `releases/<VERSION>`;不要手工删除 `current`,在线更新负责原子切换和回滚。
### Linux 32 位
压缩包本身不绑定 CPU 架构,关键是宝塔项目选用可运行的 32 位 Java 17。32 位主机不能在本机运行 64 位 JDK,也不应尝试在本机安装
MySQL 8.4;提前准备外部 MySQL,完成上述手动解压、初始化和向导即可。
### 停用并移除程序 ### 停用并移除程序
以下命令移除应用程序和服务,但保留 `/var/lib/kaidi`、`/var/lib/kaidi-update`、`/etc/kaidi` 以及数据库, 以下命令移除应用程序和服务,但保留 `/var/lib/kaidi`、`/var/lib/kaidi-update`、`/etc/kaidi` 以及数据库,
@@ -303,42 +218,36 @@ act_runner 提供 `ubuntu-24.04` 标签,并在 tag 发布时执行后端、前
Preview 属性由 SemVer 版本名表达。之后推送 tag 即会构建、测试、签名并发布: Preview 属性由 SemVer 版本名表达。之后推送 tag 即会构建、测试、签名并发布:
```bash ```bash
git tag v1.0.0-preview.24 git tag v1.0.0-preview.25
git push origin v1.0.0-preview.24 git push origin v1.0.0-preview.25
``` ```
在线更新使用独立的 TDesign 页面:隔离的系统管理员进入“系统治理 → 系统更新”。权限与配置页只管理用户、角色、数据范围、表单模板和参数版本,不配置系统名称或域名。 在线更新仍使用独立的 TDesign 页面:系统管理员进入“系统治理 → 系统更新”。更新源由 root 在
更新源由 root 在 `/etc/kaidi/update.env` 固定为公共 Gitea Latest Release API;页面和普通 API 不能提交 `/etc/kaidi/update.env` 固定为公共 Gitea Latest Release API;页面和普通 API 不能提交 URL、Token、脚本或命令。
URL、Token、脚本或命令。更新流程固定为: 宝塔模式下,root 更新器只负责下载、验签、切换 `current` 和停止旧 Java,**不创建或重启
`kaidi-finance.service`**;宝塔项目的“项目意外重启”负责按新链接拉起 Java。
1. 点击“获取更新”,后端先读取 Release 元数据,再自动排队 `DOWNLOAD`;root 更新器从 Gitea 下载 manifest、签名和应用包,执行 RSA、 更新流程固定为:
SHA-256、版本、文件名和压缩包路径校验后缓存到 `/var/lib/kaidi-update/cache/<version>`。业务服务不停机。
3. 页面显示 `READY/等待重启` 后才出现“立即重启”;管理员点击后提交安装。未缓存或版本不一致
的包不能进入安装。
4. 安装请求持久领取到 `/var/lib/kaidi-update/processing`;进程或主机中断后由 systemd 恢复未完成事务。
5. root 更新器重新验签和验哈希,确认 `mysqldump` 成功并生成权限为 `0600` 的备份,默认保留最近 5 份。安装器不安装
`mysqldump`;启用在线更新前,运维人员须自行在应用服务器预装与外部 MySQL 兼容的 `mysqldump`。
6. 校验更新脚本和 systemd 单元后,在旧 Java 仍运行时原子切换版本链接,再由 systemd 执行一次 `restart`,不修改反向代理。
7. 同时检查 Java 健康端点和 Java 托管的静态首页。全部通过后页面显示 10 秒倒计时并自动
刷新;刷新或短暂断线发生在安装中时,页面会恢复 3 秒轮询。任一检查失败则恢复并验证上一版本。
忙碌期间检查、下载和安装按钮保持禁用,防止重复请求;这就是更新执行冷却。10 秒只用于成功后的页面刷新, 1. 点击“获取更新”,后端排队 `DOWNLOAD`;更新器下载 manifest、签名和应用包,执行 RSA、SHA-256、版本、文件名和压缩包路径校验,
不会延迟服务端切换。失败请求会被归档且不会由 systemd 自动重试,避免同一事务反复停止业务服务。 通过后缓存到 `/var/lib/kaidi-update/cache/<version>`,业务服务继续运行。
2. 页面显示 `READY/等待重启` 后才出现“立即重启”;未缓存或版本不一致的包不能进入安装。
3. 管理员点击“立即重启”,更新器预先调用宿主机已有的 `mysqldump`,备份权限为 `0600`,默认保留最近 5 份;程序不会安装 MySQL 或客户端。
4. 更新器安装并保护新版本目录,原子切换 `/www/wwwroot/kaidi/current`,校验旧 Java 的受控 PID 后发送停止信号;宝塔守护自动启动新版本。
5. 健康端点、静态首页和运行版本全部通过后,页面等待 10 秒自动刷新;期间断线由前端轮询恢复。任一检查失败则切回上一应用版本,
保留数据库备份和事务证据供人工处理。
安装器会把公共 API 地址写入 root-only 的 `/etc/kaidi/kaidi.env` 与 `/etc/kaidi/update.env`,两者权限均为 忙碌期间检查、下载和安装按钮保持禁用,防止重复请求;10 秒只用于成功后的页面刷新,不延迟服务端切换。数据库迁移必须至少保持一个版本向后兼容。
`0600`:前者供 Java 后端“检查更新”读取,后者供 root 更新器下载资产。默认 Token 为空;如改接私有镜像, 查看状态和日志:
可由运维人员在这两个文件中配置独立只读 Token,Token 不写入页面、状态 JSON、审计参数或更新日志。
数据库迁移必须保持至少一个版本的向后兼容。查看状态和日志:
```bash ```bash
sudo systemctl status kaidi-finance kaidi-update.path sudo systemctl status kaidi-update.path kaidi-update.service
sudo journalctl -u kaidi-update.service -n 100 --no-pager sudo journalctl -u kaidi-update.service -n 100 --no-pager
cat /var/lib/kaidi-update/status.json cat /var/lib/kaidi-update/status.json
``` ```
如果 `status.json` 显示 `FAILED` 且日志提示回滚未完成,失败请求会归档到 如果 `status.json` 显示 `FAILED` 且日志提示回滚未完成,失败请求会归档到
`/var/lib/kaidi-update/failed`,活动事务保留在 `transactions/active`,系统不会自动再次执行。修复日志所示的 `/var/lib/kaidi-update/failed`,活动事务保留在 `transactions/active`,系统不会自动重复安装。修复日志所示的
磁盘、权限或旧版本健康问题后,才明确执行一次: 磁盘、权限或旧版本健康问题后,才明确执行一次:
```bash ```bash
@@ -356,7 +265,7 @@ cat /var/lib/kaidi-update/status.json
```bash ```bash
KAIDI_RELEASE_SIGNING_KEY=/secure/release-signing-private.pem \ KAIDI_RELEASE_SIGNING_KEY=/secure/release-signing-private.pem \
KAIDI_TRUSTED_RELEASE_PUBLIC_KEY_SHA256=807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9 \ KAIDI_TRUSTED_RELEASE_PUBLIC_KEY_SHA256=807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9 \
./scripts/package-release.sh 1.0.0-preview.24 ./scripts/package-release.sh 1.0.0-preview.25
KAIDI_TRUSTED_RELEASE_PUBLIC_KEY_SHA256=807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9 \ KAIDI_TRUSTED_RELEASE_PUBLIC_KEY_SHA256=807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9 \
./scripts/verify-release.sh dist/release ./scripts/verify-release.sh dist/release
``` ```
@@ -367,5 +276,4 @@ SBOM、首次安装指纹文件和完整应用压缩包。
JAR、两个 SBOM、签名清单或 tag 的版本只要有一项不一致,发布即失败。 JAR、两个 SBOM、签名清单或 tag 的版本只要有一项不一致,发布即失败。
签名清单同时绑定应用包、两份 SBOM、安装器、公钥、bootstrap 指纹和 Git 源码修订;本地脏工作区会明确记录 签名清单同时绑定应用包、两份 SBOM、安装器、公钥、bootstrap 指纹和 Git 源码修订;本地脏工作区会明确记录
`source.dirty=true`,tag 工作流只接受干净 checkout 并记录 `source.dirty=false`。 `source.dirty=true`,tag 工作流只接受干净 checkout 并记录 `source.dirty=false`。
发布命令同时在终端输出 `Trusted release public-key SHA-256` 与 `Installer SHA-256`;把这两个值写入 发布命令同时输出 Release 公钥和归档校验值;将压缩包 SHA-256 写入直链部署说明后再发布。
受控部署记录,再替换上述一键安装命令中的占位符。
+241
View File
@@ -0,0 +1,241 @@
#!/usr/bin/env bash
set -Eeuo pipefail
umask 077
SERVICE_USER=kaidi
SERVICE_GROUP=kaidi
CONFIG_ROOT=/etc/kaidi
STATE_ROOT=/var/lib/kaidi
UPDATE_STATE_ROOT=/var/lib/kaidi-update
LOG_ROOT=/var/log/kaidi
UPDATER_ROOT=/opt/kaidi/bin
PUBLIC_KEY_SHA256=807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9
RELEASE_API_URL=https://git.awaioi.com/api/v1/repos/ERP-Team/kaidi/releases/latest
INIT_ARMED=false
INIT_COMMITTED=false
INIT_RELEASE_ROOT=
INIT_APP_ROOT=
log() { printf '[kaidi-baota-init] %s\n' "$*"; }
die() { printf '[kaidi-baota-init] ERROR: %s\n' "$*" >&2; exit 1; }
rollback_initialization() {
local rc=$?
trap - EXIT HUP INT TERM
if [ "$rc" -ne 0 ] && [ "$INIT_ARMED" = true ] && [ "$INIT_COMMITTED" != true ]; then
systemctl disable --now kaidi-update.path >/dev/null 2>&1 || true
systemctl stop kaidi-update.service >/dev/null 2>&1 || true
rm -f /etc/systemd/system/kaidi-update.service /etc/systemd/system/kaidi-update.path
systemctl daemon-reload >/dev/null 2>&1 || true
rm -f "$CONFIG_ROOT/kaidi.env" "$CONFIG_ROOT/update.env" \
"$CONFIG_ROOT/release-public.pem" "$UPDATER_ROOT/update.sh" \
"$UPDATE_STATE_ROOT/status.json" /root/kaidi-first-login.txt
rm -f "$INIT_APP_ROOT/current.next"
if [ -n "$INIT_RELEASE_ROOT" ] \
&& [ "$(readlink "$INIT_APP_ROOT/current" 2>/dev/null || true)" = "$INIT_RELEASE_ROOT" ]; then
rm -f "$INIT_APP_ROOT/current"
fi
log "Initialization failed; files created by this attempt were rolled back and the command can be retried"
fi
exit "$rc"
}
trap rollback_initialization EXIT
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM
sha256_file() {
sha256sum "$1" | awk '{print $1}'
}
random_secret() {
openssl rand -base64 36 | tr -d '\n/+=' | cut -c1-36
}
write_env_file() {
local output=$1 temporary name value escaped
shift
temporary="${output}.next.$$"
: > "$temporary"
while [ "$#" -gt 0 ]; do
name=$1
value=$2
shift 2
case "$value" in *$'\n'*|*$'\r'*) die "$name contains a line break" ;; esac
escaped=${value//\\/\\\\}
escaped=${escaped//\"/\\\"}
printf '%s="%s"\n' "$name" "$escaped" >> "$temporary"
done
mv -f "$temporary" "$output"
}
ensure_service_identity() {
local existing_home nologin_path
if ! getent group "$SERVICE_GROUP" >/dev/null 2>&1; then
groupadd --system "$SERVICE_GROUP"
fi
if id "$SERVICE_USER" >/dev/null 2>&1; then
existing_home=$(getent passwd "$SERVICE_USER" | awk -F: '{print $6}')
[ "$existing_home" = "$STATE_ROOT" ] \
|| die "Existing user $SERVICE_USER has unexpected home directory $existing_home"
id -nG "$SERVICE_USER" | tr ' ' '\n' | grep -Fxq "$SERVICE_GROUP" \
|| die "Existing user $SERVICE_USER is not a member of group $SERVICE_GROUP"
return 0
fi
nologin_path=$(command -v nologin 2>/dev/null || true)
[ -n "$nologin_path" ] || nologin_path=/usr/sbin/nologin
[ -x "$nologin_path" ] || die "A nologin shell is required"
useradd --system --gid "$SERVICE_GROUP" --home-dir "$STATE_ROOT" --shell "$nologin_path" "$SERVICE_USER"
}
main() {
local script_dir release_root releases_root app_root version app_port field_key setup_code setup_hash
[ "$(id -u)" -eq 0 ] || die "Run with sudo or as root"
[ "$(uname -s)" = Linux ] || die "Baota initialization only supports Linux"
command -v systemctl >/dev/null 2>&1 && [ -d /run/systemd/system ] \
|| die "systemd is required for the privileged background updater"
for command in find openssl sha256sum; do
command -v "$command" >/dev/null 2>&1 || die "$command is required"
done
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)
release_root=$(cd "$script_dir/.." && pwd -P)
releases_root=$(dirname "$release_root")
app_root=$(dirname "$releases_root")
[ "$(basename "$releases_root")" = releases ] \
|| die "Extract the release into APP_ROOT/releases/VERSION before initialization"
[ "$app_root" = /www/wwwroot/kaidi ] \
|| die "The supported Baota project root is /www/wwwroot/kaidi"
INIT_RELEASE_ROOT=$release_root
INIT_APP_ROOT=$app_root
version=$(tr -d '\r\n' < "$release_root/VERSION")
[[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.+-]+)?$ ]] \
&& [ -s "$release_root/app.jar" ] && [ -s "$release_root/public/index.html" ] \
|| die "The extracted release is incomplete"
[ -x "$release_root/ops/update.sh" ] && [ -x "$release_root/ops/baota-start.sh" ] \
&& [ -x "$release_root/ops/baota-init.sh" ] \
|| die "The extracted operations scripts are incomplete"
[ -s "$release_root/ops/release-public.pem" ] \
|| die "The release public key is missing"
[ "$(sha256_file "$release_root/ops/release-public.pem")" = "$PUBLIC_KEY_SHA256" ] \
|| die "The release public-key fingerprint is invalid"
[ ! -e "$CONFIG_ROOT/kaidi.env" ] && [ ! -e "$CONFIG_ROOT/update.env" ] \
&& [ ! -e "$STATE_ROOT/setup/locked" ] \
|| die "Kaidi is already initialized; run the published purge workflow before a fresh installation"
! systemctl cat kaidi-finance.service >/dev/null 2>&1 \
|| die "The old kaidi-finance.service still exists; run the published purge workflow first"
[ ! -e /etc/systemd/system/kaidi-update.service ] \
&& [ ! -e /etc/systemd/system/kaidi-update.path ] \
|| die "Old Kaidi update units still exist; run the published purge workflow first"
[ ! -e "$app_root/current" ] && [ ! -L "$app_root/current" ] \
|| die "The Baota current release link already exists; run the published purge workflow first"
app_port=${1:-18080}
[[ "$app_port" =~ ^[0-9]{1,5}$ ]] && [ "$app_port" -ge 1024 ] && [ "$app_port" -le 65535 ] \
|| die "Port must be an integer between 1024 and 65535"
INIT_ARMED=true
ensure_service_identity
install -d -o root -g "$SERVICE_GROUP" -m 0750 "$app_root" "$releases_root" "$UPDATER_ROOT"
install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0750 \
"$STATE_ROOT" "$STATE_ROOT/files" "$STATE_ROOT/tmp" "$LOG_ROOT"
install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0700 "$STATE_ROOT/setup"
install -d -o root -g "$SERVICE_GROUP" -m 0750 "$UPDATE_STATE_ROOT"
install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0750 "$UPDATE_STATE_ROOT/inbox"
install -d -o root -g "$SERVICE_GROUP" -m 0750 "$CONFIG_ROOT"
chown -R root:"$SERVICE_GROUP" "$release_root"
find "$release_root" -type d -exec chmod 0750 {} +
find "$release_root" -type f -exec chmod 0640 {} +
chmod 0750 "$release_root/ops/update.sh" "$release_root/ops/baota-start.sh" "$release_root/ops/baota-init.sh"
ln -sfn "$release_root" "$app_root/current.next"
mv -Tf "$app_root/current.next" "$app_root/current"
field_key=$(openssl rand -base64 32 | tr -d '\n')
setup_code="KD-$(random_secret)"
setup_hash=$(printf '%s' "$setup_code" | sha256sum | awk '{print $1}')
write_env_file "$CONFIG_ROOT/kaidi.env" \
SPRING_PROFILES_ACTIVE production \
SERVER_ADDRESS 127.0.0.1 \
SERVER_PORT "$app_port" \
SESSION_COOKIE_SECURE false \
DB_URL 'jdbc:mysql://setup.invalid:3306/kaidi_finance' \
DB_USERNAME setup_pending \
DB_PASSWORD setup_pending \
FIELD_ENCRYPTION_KEY "$field_key" \
FILE_STORAGE_ROOT "$STATE_ROOT/files" \
FILE_STORAGE_TEMP "$STATE_ROOT/tmp" \
FILE_SCANNER_ENABLED false \
FINANCE_BOOTSTRAP_ENABLED false \
FINANCE_BOOTSTRAP_PASSWORD '' \
FINANCE_SETUP_ENABLED true \
FINANCE_SETUP_TOKEN_SHA256 "$setup_hash" \
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 \
UPDATE_RELEASE_BASE_URL '' \
UPDATE_RELEASE_API_URL "$RELEASE_API_URL" \
UPDATE_RELEASE_TOKEN '' \
UPDATE_REQUEST_FILE "$UPDATE_STATE_ROOT/inbox/request.json" \
UPDATE_STATUS_FILE "$UPDATE_STATE_ROOT/status.json" \
KAIDI_PID_FILE "$STATE_ROOT/kaidi.pid"
chown root:"$SERVICE_GROUP" "$CONFIG_ROOT/kaidi.env"
chmod 0640 "$CONFIG_ROOT/kaidi.env"
install -m 0644 "$release_root/ops/release-public.pem" "$CONFIG_ROOT/release-public.pem"
install -m 0755 "$release_root/ops/update.sh" "$UPDATER_ROOT/update.sh"
write_env_file "$CONFIG_ROOT/update.env" \
UPDATE_RELEASE_BASE_URL '' \
UPDATE_RELEASE_API_URL "$RELEASE_API_URL" \
UPDATE_RELEASE_TOKEN '' \
UPDATE_REQUEST_FILE "$UPDATE_STATE_ROOT/inbox/request.json" \
UPDATE_STATUS_FILE "$UPDATE_STATE_ROOT/status.json" \
UPDATE_PUBLIC_KEY "$CONFIG_ROOT/release-public.pem" \
KAIDI_APP_ROOT "$app_root" \
KAIDI_UPDATE_STATE_ROOT "$UPDATE_STATE_ROOT" \
KAIDI_PROCESS_MANAGER baota \
KAIDI_PID_FILE "$STATE_ROOT/kaidi.pid" \
KAIDI_RUNTIME_ENV_FILE "$STATE_ROOT/setup/application.env" \
KAIDI_UPDATER_PATH "$UPDATER_ROOT/update.sh" \
KAIDI_SERVICE_USER "$SERVICE_USER" \
KAIDI_SERVICE_GROUP "$SERVICE_GROUP" \
KAIDI_UPDATE_PATH_NAME kaidi-update.path \
KAIDI_HEALTH_URL "http://127.0.0.1:$app_port/actuator/health" \
KAIDI_APP_INDEX_URL "http://127.0.0.1:$app_port/" \
KAIDI_DB_HOST setup.invalid \
KAIDI_DB_PORT 3306 \
KAIDI_DB_NAME kaidi_finance \
KAIDI_DB_USERNAME setup_pending \
KAIDI_DB_PASSWORD setup_pending
chmod 0600 "$CONFIG_ROOT/update.env"
install -m 0644 "$release_root/ops/kaidi-update.service" /etc/systemd/system/kaidi-update.service
install -m 0644 "$release_root/ops/kaidi-update.path" /etc/systemd/system/kaidi-update.path
systemd-analyze verify /etc/systemd/system/kaidi-update.service /etc/systemd/system/kaidi-update.path >/dev/null \
|| die "The privileged update units failed validation"
systemctl daemon-reload
systemctl enable --now kaidi-update.path
printf '{"state":"CURRENT","message":"Baota release is prepared","targetVersion":"%s","updatedAt":"%s"}\n' \
"$version" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$UPDATE_STATE_ROOT/status.json"
chmod 0644 "$UPDATE_STATE_ROOT/status.json"
cat > /root/kaidi-first-login.txt <<EOF
Project path: $app_root/current
Start command: $app_root/current/ops/baota-start.sh
Reverse proxy target: http://127.0.0.1:$app_port
Setup URL: /setup
Setup code: $setup_code
Version: $version
EOF
chmod 0600 /root/kaidi-first-login.txt
INIT_COMMITTED=true
log "Manual deployment is initialized for Kaidi Finance $version"
log "Create the Baota Spring Boot project with $app_root/current"
log "Baota environment variables: leave empty"
log "Setup code: /root/kaidi-first-login.txt"
}
main "$@"
+91 -4
View File
@@ -1,21 +1,94 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -Eeuo pipefail set -Eeuo pipefail
umask 027
die() { die() {
printf '[kaidi-baota] ERROR: %s\n' "$1" >&2 printf '[kaidi-baota] ERROR: %s\n' "$1" >&2
exit 1 exit 1
} }
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
RELEASE_ROOT=$(cd "$SCRIPT_DIR/.." && pwd) RELEASE_ROOT=$(cd "$SCRIPT_DIR/.." && pwd -P)
APP_JAR="$RELEASE_ROOT/app.jar" APP_JAR="$RELEASE_ROOT/app.jar"
PUBLIC_INDEX="$RELEASE_ROOT/public/index.html" PUBLIC_INDEX="$RELEASE_ROOT/public/index.html"
VERSION_FILE="$RELEASE_ROOT/VERSION" VERSION_FILE="$RELEASE_ROOT/VERSION"
CONFIG_FILE=${KAIDI_CONFIG_FILE:-/etc/kaidi/kaidi.env}
RUNTIME_ENV_FILE=${KAIDI_RUNTIME_ENV_FILE:-/var/lib/kaidi/setup/application.env}
decode_env_value() {
local raw=$1 result='' char next index=0 length
if [[ "$raw" == \"*\" && ${#raw} -ge 2 ]]; then
raw=${raw:1:${#raw}-2}
length=${#raw}
while [ "$index" -lt "$length" ]; do
char=${raw:index:1}
if [ "$char" = "\\" ] && [ $((index + 1)) -lt "$length" ]; then
next=${raw:index+1:1}
if [ "$next" = "\\" ] || [ "$next" = '"' ]; then
result+="$next"
index=$((index + 2))
continue
fi
fi
result+="$char"
index=$((index + 1))
done
printf '%s' "$result"
elif [[ "$raw" == \'*\' && ${#raw} -ge 2 ]]; then
printf '%s' "${raw:1:${#raw}-2}"
else
printf '%s' "$raw"
fi
}
is_managed_env_name() {
case "$1" in
SPRING_PROFILES_ACTIVE|SERVER_ADDRESS|SERVER_PORT|SESSION_COOKIE_SECURE|\
DB_URL|DB_USERNAME|DB_PASSWORD|FIELD_ENCRYPTION_KEY|\
FILE_STORAGE_ROOT|FILE_STORAGE_TEMP|FILE_SCANNER_ENABLED|FILE_SCANNER_HOST|FILE_SCANNER_PORT|\
FINANCE_BOOTSTRAP_ENABLED|FINANCE_BOOTSTRAP_PASSWORD|FINANCE_SETUP_ENABLED|\
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)
return 0
;;
*) return 1 ;;
esac
}
load_env_file() {
local file=$1 line name raw value size
[ -e "$file" ] || return 0
[ -f "$file" ] && [ ! -L "$file" ] || die "Configuration file is not a regular file: $file"
[ -r "$file" ] || die "Startup user cannot read configuration file: $file"
size=$(wc -c < "$file" | tr -d '[:space:]')
[ "$size" -le 65536 ] || die "Configuration file is too large: $file"
while IFS= read -r line || [ -n "$line" ]; do
case "$line" in ''|'#'*) continue ;; esac
[[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]] \
|| die "Configuration file contains an invalid line: $file"
name=${BASH_REMATCH[1]}
raw=${BASH_REMATCH[2]}
is_managed_env_name "$name" || continue
[ -n "${!name:-}" ] && continue
value=$(decode_env_value "$raw")
printf -v "$name" '%s' "$value"
export "${name?}"
done < "$file"
}
[ -s "$APP_JAR" ] || die "app.jar is missing from $RELEASE_ROOT" [ -s "$APP_JAR" ] || die "app.jar is missing from $RELEASE_ROOT"
[ -s "$PUBLIC_INDEX" ] || die "public/index.html 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" [ -s "$VERSION_FILE" ] || die "VERSION is missing from $RELEASE_ROOT"
# 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"
JAVA_BIN=${KAIDI_JAVA_BIN:-} JAVA_BIN=${KAIDI_JAVA_BIN:-}
if [ -z "$JAVA_BIN" ] && [ -n "${JAVA_HOME:-}" ]; then if [ -z "$JAVA_BIN" ] && [ -n "${JAVA_HOME:-}" ]; then
JAVA_BIN="$JAVA_HOME/bin/java" JAVA_BIN="$JAVA_HOME/bin/java"
@@ -35,15 +108,16 @@ export SERVER_ADDRESS=${SERVER_ADDRESS:-127.0.0.1}
export SERVER_PORT=${SERVER_PORT:-18080} export SERVER_PORT=${SERVER_PORT:-18080}
export FINANCE_SETUP_ENABLED=${FINANCE_SETUP_ENABLED:-false} export FINANCE_SETUP_ENABLED=${FINANCE_SETUP_ENABLED:-false}
export FINANCE_BOOTSTRAP_ENABLED=${FINANCE_BOOTSTRAP_ENABLED:-false} export FINANCE_BOOTSTRAP_ENABLED=${FINANCE_BOOTSTRAP_ENABLED:-false}
export FINANCE_UPDATE_ENABLED=${FINANCE_UPDATE_ENABLED:-false} export FINANCE_UPDATE_ENABLED=${FINANCE_UPDATE_ENABLED:-true}
export FILE_SCANNER_ENABLED=${FILE_SCANNER_ENABLED:-false} export FILE_SCANNER_ENABLED=${FILE_SCANNER_ENABLED:-false}
export FINANCE_STATIC_LOCATIONS=${FINANCE_STATIC_LOCATIONS:-file:$RELEASE_ROOT/public/} export FINANCE_STATIC_LOCATIONS=${FINANCE_STATIC_LOCATIONS:-file:$RELEASE_ROOT/public/}
export UPDATE_CURRENT_VERSION_FILE=${UPDATE_CURRENT_VERSION_FILE:-$VERSION_FILE} export UPDATE_CURRENT_VERSION_FILE=${UPDATE_CURRENT_VERSION_FILE:-$VERSION_FILE}
export APP_VERSION=${APP_VERSION:-$(tr -d '\r\n' < "$VERSION_FILE")} export APP_VERSION=${APP_VERSION:-$(tr -d '\r\n' < "$VERSION_FILE")}
export KAIDI_PID_FILE=${KAIDI_PID_FILE:-/var/lib/kaidi/kaidi.pid}
if [ "$FINANCE_SETUP_ENABLED" != true ]; then 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 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" [ -n "${!name:-}" ] || die "$name is missing from the protected Kaidi configuration"
done done
for directory in "$FILE_STORAGE_ROOT" "$FILE_STORAGE_TEMP"; do for directory in "$FILE_STORAGE_ROOT" "$FILE_STORAGE_TEMP"; do
[ -d "$directory" ] || die "Storage directory $directory does not exist" [ -d "$directory" ] || die "Storage directory $directory does not exist"
@@ -51,5 +125,18 @@ if [ "$FINANCE_SETUP_ENABLED" != true ]; then
done done
fi fi
case "$KAIDI_PID_FILE" in /*) ;; *) die "KAIDI_PID_FILE must be an absolute path" ;; esac
[ -d "$(dirname "$KAIDI_PID_FILE")" ] || die "PID directory does not exist"
[ -w "$(dirname "$KAIDI_PID_FILE")" ] || die "Startup user cannot write the PID directory"
[ ! -L "$KAIDI_PID_FILE" ] || die "PID file must not be a symbolic link"
PID_START_TIME=$(awk '{print $22}' "/proc/$$/stat" 2>/dev/null || true)
[[ "$PID_START_TIME" =~ ^[0-9]+$ ]] || die "Process start time could not be read from /proc"
PID_TEMP="${KAIDI_PID_FILE}.next.$$"
printf '%s %s\n' "$$" "$PID_START_TIME" > "$PID_TEMP"
chmod 0640 "$PID_TEMP"
mv -f "$PID_TEMP" "$KAIDI_PID_FILE"
cd "$RELEASE_ROOT" cd "$RELEASE_ROOT"
exec "$JAVA_BIN" -XX:MaxRAMPercentage=70 -Dfile.encoding=UTF-8 -jar "$APP_JAR" exec "$JAVA_BIN" -XX:MaxRAMPercentage=70 -Dfile.encoding=UTF-8 \
"-Dkaidi.release.path=$RELEASE_ROOT" "-Dkaidi.release.version=$APP_VERSION" \
-jar "$APP_JAR"
+11 -14
View File
@@ -1,14 +1,11 @@
SPRING_PROFILES_ACTIVE=production # Baota project environment variables are intentionally empty.
SERVER_ADDRESS=127.0.0.1 # Do not paste database passwords or placeholder values into the panel.
SERVER_PORT=18080 # ops/baota-start.sh loads the protected files created by ops/baota-init.sh:
SESSION_COOKIE_SECURE=false # /etc/kaidi/kaidi.env
DB_URL=jdbc:mysql://MYSQL_HOST:3306/kaidi_finance?useUnicode=true&characterEncoding=utf8&connectionTimeZone=UTC&serverTimezone=UTC # /var/lib/kaidi/setup/application.env (after the setup wizard completes)
DB_USERNAME=MYSQL_USERNAME #
DB_PASSWORD=MYSQL_PASSWORD # Optional non-secret overrides may be added deliberately, for example:
FIELD_ENCRYPTION_KEY=EXISTING_FIELD_ENCRYPTION_KEY # SERVER_PORT=18080
FILE_STORAGE_ROOT=/var/lib/kaidi/files # SESSION_COOKIE_SECURE=true
FILE_STORAGE_TEMP=/var/lib/kaidi/tmp # Never override DB_URL, DB_USERNAME, DB_PASSWORD, FIELD_ENCRYPTION_KEY,
FILE_SCANNER_ENABLED=false # FINANCE_SETUP_TOKEN_SHA256, or the update paths from the panel.
FINANCE_BOOTSTRAP_ENABLED=false
FINANCE_SETUP_ENABLED=false
FINANCE_UPDATE_ENABLED=false
+11
View File
@@ -0,0 +1,11 @@
-----BEGIN PUBLIC KEY-----
MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAwnPqkRpYUTBYbBaSnxBs
LqDHFPrxX6DINIiCyRJ/0f1c0noRr1X0bJSpTAR+yUwzWYTqIKAhZ4NSouYjaxtE
zuoW4cC6v+S/C310xM8qy+lE52GyFHpr7H6GqdQgxK2LmfS0jjCR9i8MEmi6QMk4
zdDtI1NOl+D50vk1nrMryplyfvqDpyPA+fiqXVo2NrqX9iaamTVkq55INk3X451I
u96/HBlSMw2EvoDPurzcjxV16swyJcuQQpBUbBmgp8i2enNyWONoBq/myYReTDem
CbrD+DTvqa+tDMQ+DAL2wyPa8JdoGOLhcftYWsyfpgTq6olNSnqgUV+Gp+NYpWuk
AxMi6TTDX4ByMkS65UGH2Opg483bxtl9bGCmjFfqFGO2NjxAsh6LaBApS2PNBKpu
06+Qni/Eq7u847egw89bL1xwtHIZBWDiG4tQMqyqW3M5WqoYoaB7PcZnfyRVChA4
pb+2yJll0cJ4RvKWeVrOAzb5JOn0BWjllm/csfwv9IeTAgMBAAE=
-----END PUBLIC KEY-----
+1 -2
View File
@@ -10,7 +10,6 @@ Type=oneshot
User=root User=root
Group=root Group=root
EnvironmentFile=/etc/kaidi/update.env EnvironmentFile=/etc/kaidi/update.env
EnvironmentFile=-/var/lib/kaidi/setup/application.env
ExecStart=/opt/kaidi/bin/update.sh ExecStart=/opt/kaidi/bin/update.sh
Restart=no Restart=no
Nice=10 Nice=10
@@ -19,5 +18,5 @@ IOSchedulingPriority=6
PrivateTmp=true PrivateTmp=true
ProtectHome=true ProtectHome=true
ProtectSystem=full ProtectSystem=full
ReadWritePaths=/opt/kaidi /var/lib/kaidi /var/lib/kaidi-update /var/log/kaidi /etc/systemd/system ReadWritePaths=/opt/kaidi /www/wwwroot/kaidi /var/lib/kaidi /var/lib/kaidi-update /var/log/kaidi /etc/systemd/system
UMask=0077 UMask=0077
+205 -21
View File
@@ -19,6 +19,9 @@ RELEASE_TOKEN=${UPDATE_RELEASE_TOKEN:-}
CACHE_ROOT=${KAIDI_UPDATE_CACHE_ROOT:-$STATE_ROOT/cache} CACHE_ROOT=${KAIDI_UPDATE_CACHE_ROOT:-$STATE_ROOT/cache}
SERVICE_NAME=${KAIDI_SERVICE_NAME:-kaidi-finance.service} SERVICE_NAME=${KAIDI_SERVICE_NAME:-kaidi-finance.service}
UPDATE_PATH_NAME=${KAIDI_UPDATE_PATH_NAME:-kaidi-update.path} 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}
RUNTIME_ENV_FILE=${KAIDI_RUNTIME_ENV_FILE:-/var/lib/kaidi/setup/application.env}
SERVICE_USER=${KAIDI_SERVICE_USER:-kaidi} SERVICE_USER=${KAIDI_SERVICE_USER:-kaidi}
SERVICE_GROUP=${KAIDI_SERVICE_GROUP:-kaidi} SERVICE_GROUP=${KAIDI_SERVICE_GROUP:-kaidi}
HEALTH_URL=${KAIDI_HEALTH_URL:-http://127.0.0.1:18080/actuator/health} HEALTH_URL=${KAIDI_HEALTH_URL:-http://127.0.0.1:18080/actuator/health}
@@ -30,6 +33,7 @@ SYSTEMD_ROOT=${KAIDI_SYSTEMD_ROOT:-/etc/systemd/system}
LOG_ROOT=${KAIDI_LOG_ROOT:-/var/log/kaidi} LOG_ROOT=${KAIDI_LOG_ROOT:-/var/log/kaidi}
HEALTH_ATTEMPTS=${KAIDI_UPDATE_HEALTH_ATTEMPTS:-60} HEALTH_ATTEMPTS=${KAIDI_UPDATE_HEALTH_ATTEMPTS:-60}
HEALTH_INTERVAL_SECONDS=${KAIDI_UPDATE_HEALTH_INTERVAL_SECONDS:-2} HEALTH_INTERVAL_SECONDS=${KAIDI_UPDATE_HEALTH_INTERVAL_SECONDS:-2}
SHUTDOWN_ATTEMPTS=${KAIDI_UPDATE_SHUTDOWN_ATTEMPTS:-45}
WORK_DIR= WORK_DIR=
CACHE_TEMP= CACHE_TEMP=
RELEASE_AUTH_HEADER_FILE= RELEASE_AUTH_HEADER_FILE=
@@ -40,12 +44,85 @@ case "$HEALTH_ATTEMPTS:$HEALTH_INTERVAL_SECONDS" in
*[!0-9:]* | :* | *:) printf '%s\n' "Update health-check settings must be non-negative integers" >&2; exit 1 ;; *[!0-9:]* | :* | *:) printf '%s\n' "Update health-check settings must be non-negative integers" >&2; exit 1 ;;
esac esac
[ "$HEALTH_ATTEMPTS" -ge 1 ] || { printf '%s\n' "Update health-check attempts must be at least 1" >&2; exit 1; } [ "$HEALTH_ATTEMPTS" -ge 1 ] || { printf '%s\n' "Update health-check attempts must be at least 1" >&2; exit 1; }
case "$PROCESS_MANAGER" in
systemd|baota) ;;
*) printf '%s\n' "KAIDI_PROCESS_MANAGER must be systemd or baota" >&2; exit 1 ;;
esac
case "$SHUTDOWN_ATTEMPTS" in
''|*[!0-9]*) printf '%s\n' "KAIDI_UPDATE_SHUTDOWN_ATTEMPTS must be a positive integer" >&2; exit 1 ;;
esac
[ "$SHUTDOWN_ATTEMPTS" -ge 1 ] || { printf '%s\n' "KAIDI_UPDATE_SHUTDOWN_ATTEMPTS must be at least 1" >&2; exit 1; }
case "$PID_FILE" in
/*) ;;
*) printf '%s\n' "KAIDI_PID_FILE must be an absolute path" >&2; exit 1 ;;
esac
case "$RUNTIME_ENV_FILE" in
/*) ;;
*) printf '%s\n' "KAIDI_RUNTIME_ENV_FILE must be an absolute path" >&2; exit 1 ;;
esac
bootstrap_die() { bootstrap_die() {
printf '%s\n' "$1" >&2 printf '%s\n' "$1" >&2
exit 1 exit 1
} }
decode_runtime_env_value() {
encoded_value=$1
case "$encoded_value" in
\"*\")
encoded_value=${encoded_value#\"}
encoded_value=${encoded_value%\"}
printf '%s' "$encoded_value" | sed 's/\\\"/"/g; s/\\\\/\\/g'
;;
\'*\')
encoded_value=${encoded_value#\'}
encoded_value=${encoded_value%\'}
printf '%s' "$encoded_value"
;;
*) printf '%s' "$encoded_value" ;;
esac
}
load_runtime_database_env() {
[ -e "$RUNTIME_ENV_FILE" ] || return 0
[ -f "$RUNTIME_ENV_FILE" ] && [ ! -L "$RUNTIME_ENV_FILE" ] \
|| bootstrap_die "Setup runtime environment must be a regular file"
[ -r "$RUNTIME_ENV_FILE" ] \
|| bootstrap_die "Setup runtime environment is not readable"
runtime_size=$(wc -c < "$RUNTIME_ENV_FILE" | tr -d '[:space:]')
[ "$runtime_size" -le 65536 ] \
|| bootstrap_die "Setup runtime environment is too large"
runtime_owner=$(stat -c '%u' "$RUNTIME_ENV_FILE" 2>/dev/null || true)
service_uid=$(id -u "$SERVICE_USER" 2>/dev/null || true)
[ "$runtime_owner" = 0 ] || [ "$runtime_owner" = "$service_uid" ] \
|| bootstrap_die "Setup runtime environment has an unexpected owner"
if find "$RUNTIME_ENV_FILE" -prune -perm /022 -print -quit | grep -q .; then
bootstrap_die "Setup runtime environment must not be writable by group or other users"
fi
while IFS= read -r runtime_line || [ -n "$runtime_line" ]; do
case "$runtime_line" in ''|'#'*) continue ;; esac
runtime_name=${runtime_line%%=*}
[ "$runtime_name" != "$runtime_line" ] \
|| bootstrap_die "Setup runtime environment contains an invalid line"
runtime_raw=${runtime_line#*=}
runtime_value=$(decode_runtime_env_value "$runtime_raw")
case "$runtime_name" in
KAIDI_DB_HOST) KAIDI_DB_HOST=$runtime_value; export KAIDI_DB_HOST ;;
KAIDI_DB_PORT) KAIDI_DB_PORT=$runtime_value; export KAIDI_DB_PORT ;;
KAIDI_DB_NAME) KAIDI_DB_NAME=$runtime_value; export KAIDI_DB_NAME ;;
KAIDI_DB_USERNAME) KAIDI_DB_USERNAME=$runtime_value; export KAIDI_DB_USERNAME ;;
KAIDI_DB_PASSWORD) KAIDI_DB_PASSWORD=$runtime_value; export KAIDI_DB_PASSWORD ;;
esac
done < "$RUNTIME_ENV_FILE"
case "${KAIDI_DB_PORT:-}" in
''|*[!0-9]*) bootstrap_die "Setup runtime database port is invalid" ;;
esac
[ "$KAIDI_DB_PORT" -ge 1 ] && [ "$KAIDI_DB_PORT" -le 65535 ] \
|| bootstrap_die "Setup runtime database port is invalid"
}
status() { status() {
state=$1 state=$1
message=$2 message=$2
@@ -103,6 +180,7 @@ secure_release_tree() {
find "$release_dir" -type d -exec chmod 0750 {} + || return 1 find "$release_dir" -type d -exec chmod 0750 {} + || return 1
find "$release_dir" -type f -exec chmod 0640 {} + || return 1 find "$release_dir" -type f -exec chmod 0640 {} + || return 1
chmod 0750 "$release_dir/ops/update.sh" "$release_dir/ops/baota-start.sh" chmod 0750 "$release_dir/ops/update.sh" "$release_dir/ops/baota-start.sh"
[ ! -f "$release_dir/ops/baota-init.sh" ] || chmod 0750 "$release_dir/ops/baota-init.sh"
} }
verify_release_access() { verify_release_access() {
@@ -111,12 +189,16 @@ verify_release_access() {
runuser -u "$SERVICE_USER" -- sh -c \ runuser -u "$SERVICE_USER" -- sh -c \
'cd "$1" && test -r app.jar && test -r public/index.html' sh "$release_dir" \ 'cd "$1" && test -r app.jar && test -r public/index.html' sh "$release_dir" \
|| return 1 || return 1
runuser -u "$SERVICE_USER" -- test -x "$APP_ROOT/runtime/java/bin/java" \ if [ "$PROCESS_MANAGER" = baota ]; then
|| return 1 runuser -u "$SERVICE_USER" -- test -x "$release_dir/ops/baota-start.sh"
runuser -u "$SERVICE_USER" -- "$APP_ROOT/runtime/java/bin/java" -version >/dev/null 2>&1 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
fi
} }
prepare_update_layout prepare_update_layout
load_runtime_database_env
# shellcheck disable=SC2329 # Invoked by the EXIT trap below. # shellcheck disable=SC2329 # Invoked by the EXIT trap below.
cleanup() { cleanup() {
@@ -204,18 +286,103 @@ wait_for_health() {
} }
verify_app_surface() { verify_app_surface() {
expected_release=${1:-}
wait_for_health "$HEALTH_URL" || return 1 wait_for_health "$HEALTH_URL" || return 1
index_file=$(mktemp "$STATE_ROOT/work/public-index.XXXXXX") index_file=$(mktemp "$STATE_ROOT/work/public-index.XXXXXX")
if curl -fsS "$APP_INDEX_URL" -o "$index_file" \ if curl -fsS "$APP_INDEX_URL" -o "$index_file" \
&& grep -Eiq '<!doctype|<html' "$index_file"; then && grep -Eiq '<!doctype|<html' "$index_file"; then
rm -f "$index_file" rm -f "$index_file"
if [ "$PROCESS_MANAGER" = baota ] && [ -n "$expected_release" ]; then
running_release=$(baota_process_release) || return 1
[ "$running_release" = "$expected_release" ] || return 1
fi
return 0 return 0
fi fi
rm -f "$index_file" rm -f "$index_file"
return 1 return 1
} }
baota_process_record() {
[ -f "$PID_FILE" ] && [ ! -L "$PID_FILE" ] || return 1
pid_record=$(cat "$PID_FILE" 2>/dev/null || true)
pid=${pid_record%% *}
start_time=${pid_record#* }
[ "$pid" != "$pid_record" ] || return 1
case "$pid:$start_time" in
*[!0-9:]*) return 1 ;;
esac
[ "$pid" -ne "$$" ] && [ -r "/proc/$pid/cmdline" ] || return 1
actual_start_time=$(awk '{print $22}' "/proc/$pid/stat" 2>/dev/null || true)
[ -n "$actual_start_time" ] && [ "$actual_start_time" = "$start_time" ] || return 1
owner_uid=$(stat -c '%u' "/proc/$pid" 2>/dev/null || true)
service_uid=$(id -u "$SERVICE_USER" 2>/dev/null || true)
[ -n "$owner_uid" ] && [ "$owner_uid" = "$service_uid" ] || return 1
process_args=$(mktemp "$STATE_ROOT/work/process-args.XXXXXX") || return 1
if ! tr '\000' '\n' < "/proc/$pid/cmdline" > "$process_args" 2>/dev/null; then
rm -f "$process_args"
return 1
fi
release_path=$(sed -n 's/^-Dkaidi\.release\.path=//p' "$process_args")
release_path_count=$(sed -n '/^-Dkaidi\.release\.path=/p' "$process_args" | wc -l | tr -d '[:space:]')
jar_path=$(awk 'previous == "-jar" { print; exit } { previous = $0 }' "$process_args")
rm -f "$process_args"
[ "$release_path_count" -eq 1 ] || return 1
case "$release_path" in
"$APP_ROOT"/releases/*) ;;
*) return 1 ;;
esac
[ -d "$release_path" ] && [ ! -L "$release_path" ] \
&& [ "$jar_path" = "$release_path/app.jar" ] && [ -f "$jar_path" ] \
|| return 1
printf '%s\t%s\n' "$pid" "$release_path"
}
baota_process_pid() {
baota_process_record | cut -f1
}
baota_process_release() {
baota_process_record | cut -f2-
}
restart_application() {
allow_missing=${1:-false}
if [ "$PROCESS_MANAGER" = systemd ]; then
systemctl reset-failed "$SERVICE_NAME" >/dev/null 2>&1 || true
systemctl restart "$SERVICE_NAME" || systemctl start "$SERVICE_NAME"
return
fi
old_pid=$(baota_process_pid) || {
[ "$allow_missing" = true ] && return 0
return 1
}
kill -TERM "$old_pid" 2>/dev/null || return 1
attempts=0
while [ "$attempts" -lt "$SHUTDOWN_ATTEMPTS" ]; do
if ! kill -0 "$old_pid" 2>/dev/null; then
current_pid_record=$(cat "$PID_FILE" 2>/dev/null || true)
case "$current_pid_record" in "$old_pid "*) rm -f "$PID_FILE" ;; esac
return 0
fi
attempts=$((attempts + 1))
sleep 1
done
return 1
}
application_state() {
if [ "$PROCESS_MANAGER" = systemd ]; then
systemctl is-active "$SERVICE_NAME" 2>/dev/null || printf unknown
elif baota_process_pid >/dev/null 2>&1; then
printf active
else
printf stopped
fi
}
backup_database() { backup_database() {
DATABASE_BACKUP=
[ "${KAIDI_SKIP_DB_BACKUP:-false}" = "true" ] && return [ "${KAIDI_SKIP_DB_BACKUP:-false}" = "true" ] && return
mkdir -p "$BACKUP_ROOT" mkdir -p "$BACKUP_ROOT"
chmod 0700 "$BACKUP_ROOT" chmod 0700 "$BACKUP_ROOT"
@@ -255,6 +422,7 @@ backup_database() {
fi fi
chmod 0600 "$backup_tmp" chmod 0600 "$backup_tmp"
mv -f "$backup_tmp" "$backup" mv -f "$backup_tmp" "$backup"
DATABASE_BACKUP=$backup
rm -f "$dump_file" rm -f "$dump_file"
find "$BACKUP_ROOT" -type f -name 'mysql-*.sql.gz' -printf '%T@ %p\n' \ find "$BACKUP_ROOT" -type f -name 'mysql-*.sql.gz' -printf '%T@ %p\n' \
| sort -nr | awk 'NR > 5 {sub(/^[^ ]+ /, ""); print}' | xargs -r rm -f | sort -nr | awk 'NR > 5 {sub(/^[^ ]+ /, ""); print}' | xargs -r rm -f
@@ -321,18 +489,22 @@ restore_managed_file() {
backup_operations() { backup_operations() {
OPS_BACKUP="$ACTIVE_TRANSACTION/operations-backup" OPS_BACKUP="$ACTIVE_TRANSACTION/operations-backup"
mkdir -p "$OPS_BACKUP" mkdir -p "$OPS_BACKUP"
backup_managed_file "$UPDATER_PATH" update.sh \ backup_managed_file "$UPDATER_PATH" update.sh || return 1
&& backup_managed_file "$SYSTEMD_ROOT/kaidi-finance.service" kaidi-finance.service \ if [ "$PROCESS_MANAGER" = systemd ]; then
&& backup_managed_file "$SYSTEMD_ROOT/kaidi-update.service" kaidi-update.service \ backup_managed_file "$SYSTEMD_ROOT/kaidi-finance.service" kaidi-finance.service || return 1
fi
backup_managed_file "$SYSTEMD_ROOT/kaidi-update.service" kaidi-update.service \
&& backup_managed_file "$SYSTEMD_ROOT/kaidi-update.path" kaidi-update.path && backup_managed_file "$SYSTEMD_ROOT/kaidi-update.path" kaidi-update.path
} }
apply_operations() { apply_operations() {
SYSTEMD_OPERATIONS_CHANGED=false SYSTEMD_OPERATIONS_CHANGED=false
install_operation "$RELEASE_DIR/ops/update.sh" "$UPDATER_PATH" 0755 false \ install_operation "$RELEASE_DIR/ops/update.sh" "$UPDATER_PATH" 0755 false || return 1
&& install_operation "$RELEASE_DIR/ops/kaidi-finance.service" \ if [ "$PROCESS_MANAGER" = systemd ]; then
"$SYSTEMD_ROOT/kaidi-finance.service" 0644 true \ install_operation "$RELEASE_DIR/ops/kaidi-finance.service" \
&& install_operation "$RELEASE_DIR/ops/kaidi-update.service" \ "$SYSTEMD_ROOT/kaidi-finance.service" 0644 true || return 1
fi
install_operation "$RELEASE_DIR/ops/kaidi-update.service" \
"$SYSTEMD_ROOT/kaidi-update.service" 0644 true \ "$SYSTEMD_ROOT/kaidi-update.service" 0644 true \
&& install_operation "$RELEASE_DIR/ops/kaidi-update.path" \ && install_operation "$RELEASE_DIR/ops/kaidi-update.path" \
"$SYSTEMD_ROOT/kaidi-update.path" 0644 true \ "$SYSTEMD_ROOT/kaidi-update.path" 0644 true \
@@ -343,7 +515,9 @@ apply_operations() {
restore_operations() { restore_operations() {
restore_failed=0 restore_failed=0
restore_managed_file "$UPDATER_PATH" update.sh || restore_failed=1 restore_managed_file "$UPDATER_PATH" update.sh || restore_failed=1
restore_managed_file "$SYSTEMD_ROOT/kaidi-finance.service" kaidi-finance.service || restore_failed=1 if [ "$PROCESS_MANAGER" = systemd ]; then
restore_managed_file "$SYSTEMD_ROOT/kaidi-finance.service" kaidi-finance.service || restore_failed=1
fi
restore_managed_file "$SYSTEMD_ROOT/kaidi-update.service" kaidi-update.service || restore_failed=1 restore_managed_file "$SYSTEMD_ROOT/kaidi-update.service" kaidi-update.service || restore_failed=1
restore_managed_file "$SYSTEMD_ROOT/kaidi-update.path" kaidi-update.path || restore_failed=1 restore_managed_file "$SYSTEMD_ROOT/kaidi-update.path" kaidi-update.path || restore_failed=1
systemctl daemon-reload || restore_failed=1 systemctl daemon-reload || restore_failed=1
@@ -378,6 +552,7 @@ rollback_active_transaction() {
OPS_BACKUP="$ACTIVE_TRANSACTION/operations-backup" OPS_BACKUP="$ACTIVE_TRANSACTION/operations-backup"
link_restored=true link_restored=true
operations_restored=true operations_restored=true
application_restarted=true
if [ -n "$previous_target" ] && [ -d "$previous_target" ]; then if [ -n "$previous_target" ] && [ -d "$previous_target" ]; then
if ! ln -sfn "$previous_target" "$APP_ROOT/current.next" \ if ! ln -sfn "$previous_target" "$APP_ROOT/current.next" \
@@ -388,15 +563,15 @@ rollback_active_transaction() {
link_restored=false link_restored=false
fi fi
restore_operations || operations_restored=false restore_operations || operations_restored=false
systemctl reset-failed "$SERVICE_NAME" >/dev/null 2>&1 || true restart_application true || application_restarted=false
systemctl restart "$SERVICE_NAME" || systemctl start "$SERVICE_NAME" || true if [ "$link_restored" = true ] && [ "$application_restarted" = true ] \
if [ "$link_restored" = true ] && verify_app_surface; then && verify_app_surface "$previous_target"; then
remove_failed_release "$failed_release" remove_failed_release "$failed_release"
rm -rf "$ACTIVE_TRANSACTION"
if [ "$operations_restored" = true ]; then if [ "$operations_restored" = true ]; then
fail "$reason; previous release was restored and verified" rm -rf "$ACTIVE_TRANSACTION"
fail "$reason; previous application release was restored and verified; database backup was retained"
fi fi
fail "$reason; previous release is running, but operations restoration requires manual review" fail "$reason; previous application release is running, but operations restoration requires manual review; transaction evidence was retained"
fi fi
TERMINAL_STATUS_WRITTEN=true TERMINAL_STATUS_WRITTEN=true
status FAILED "$reason; rollback is incomplete and manual recovery is required" "${TARGET_VERSION:-}" status FAILED "$reason; rollback is incomplete and manual recovery is required" "${TARGET_VERSION:-}"
@@ -430,6 +605,10 @@ recover_interrupted_transaction() {
validate_release_operations() { validate_release_operations() {
sh -n "$WORK_DIR/extracted/ops/update.sh" || return 1 sh -n "$WORK_DIR/extracted/ops/update.sh" || return 1
bash -n "$WORK_DIR/extracted/ops/baota-start.sh" || return 1 bash -n "$WORK_DIR/extracted/ops/baota-start.sh" || return 1
bash -n "$WORK_DIR/extracted/ops/baota-init.sh" || return 1
[ -s "$WORK_DIR/extracted/ops/release-public.pem" ] \
&& cmp -s "$WORK_DIR/extracted/ops/release-public.pem" "$PUBLIC_KEY" \
|| return 1
command -v systemd-analyze >/dev/null 2>&1 || return 1 command -v systemd-analyze >/dev/null 2>&1 || return 1
systemd-analyze verify \ systemd-analyze verify \
"$WORK_DIR/extracted/ops/kaidi-finance.service" \ "$WORK_DIR/extracted/ops/kaidi-finance.service" \
@@ -555,6 +734,9 @@ fi
[ -s "$WORK_DIR/extracted/public/index.html" ] || fail "Release frontend is missing" [ -s "$WORK_DIR/extracted/public/index.html" ] || fail "Release frontend is missing"
[ "$(cat "$WORK_DIR/extracted/VERSION" 2>/dev/null)" = "$TARGET_VERSION" ] || fail "Release version file mismatch" [ "$(cat "$WORK_DIR/extracted/VERSION" 2>/dev/null)" = "$TARGET_VERSION" ] || fail "Release version file mismatch"
[ -x "$WORK_DIR/extracted/ops/update.sh" ] || fail "Release updater is missing" [ -x "$WORK_DIR/extracted/ops/update.sh" ] || fail "Release updater is missing"
[ -x "$WORK_DIR/extracted/ops/baota-start.sh" ] || fail "Release Baota launcher is missing"
[ -x "$WORK_DIR/extracted/ops/baota-init.sh" ] || fail "Release Baota initializer is missing"
[ -s "$WORK_DIR/extracted/ops/release-public.pem" ] || fail "Release public key is missing"
[ -s "$WORK_DIR/extracted/ops/kaidi-finance.service" ] || fail "Release application unit is missing" [ -s "$WORK_DIR/extracted/ops/kaidi-finance.service" ] || fail "Release application unit is missing"
[ -s "$WORK_DIR/extracted/ops/kaidi-update.service" ] || fail "Release updater unit is missing" [ -s "$WORK_DIR/extracted/ops/kaidi-update.service" ] || fail "Release updater unit is missing"
[ -s "$WORK_DIR/extracted/ops/kaidi-update.path" ] || fail "Release updater path unit is missing" [ -s "$WORK_DIR/extracted/ops/kaidi-update.path" ] || fail "Release updater path unit is missing"
@@ -605,6 +787,7 @@ write_transaction_value phase PREPARED
status BACKING_UP "Backing up database before installing $TARGET_VERSION" "$TARGET_VERSION" status BACKING_UP "Backing up database before installing $TARGET_VERSION" "$TARGET_VERSION"
backup_database backup_database
[ -z "${DATABASE_BACKUP:-}" ] || write_transaction_value database-backup "$DATABASE_BACKUP"
if ! backup_operations; then if ! backup_operations; then
remove_failed_release "$RELEASE_DIR" remove_failed_release "$RELEASE_DIR"
rm -rf "$ACTIVE_TRANSACTION" rm -rf "$ACTIVE_TRANSACTION"
@@ -624,9 +807,10 @@ write_transaction_value phase APP_SWITCHED
status RUNNING "Starting and verifying release $TARGET_VERSION" "$TARGET_VERSION" status RUNNING "Starting and verifying release $TARGET_VERSION" "$TARGET_VERSION"
write_transaction_value phase HEALTH_CHECKING write_transaction_value phase HEALTH_CHECKING
systemctl reset-failed "$SERVICE_NAME" >/dev/null 2>&1 || true if ! restart_application; then
systemctl restart "$SERVICE_NAME" || systemctl start "$SERVICE_NAME" || true rollback_active_transaction "Release application restart failed"
if verify_app_surface; then fi
if verify_app_surface "$RELEASE_DIR"; then
success_message="Release $TARGET_VERSION is running" success_message="Release $TARGET_VERSION is running"
if ! systemctl start "$UPDATE_PATH_NAME" >/dev/null 2>&1; then if ! systemctl start "$UPDATE_PATH_NAME" >/dev/null 2>&1; then
success_message="$success_message; automatic update watcher could not be started" success_message="$success_message; automatic update watcher could not be started"
@@ -639,7 +823,7 @@ if verify_app_surface; then
exit 0 exit 0
fi fi
service_state=$(systemctl is-active "$SERVICE_NAME" 2>/dev/null || true) service_state=$(application_state 2>/dev/null || true)
[ -n "$service_state" ] || service_state=unknown [ -n "$service_state" ] || service_state=unknown
rollback_active_transaction \ rollback_active_transaction \
"Release health or application-surface verification failed (service state: $service_state)" "Release health or application-surface verification failed (service state: $service_state)"
+3 -1
View File
@@ -92,13 +92,15 @@ cp -R "$ROOT/frontend/dist/." "$STAGE/public/"
printf '%s\n' "$VERSION" > "$STAGE/VERSION" printf '%s\n' "$VERSION" > "$STAGE/VERSION"
cp "$ROOT/deploy/update.sh" "$STAGE/ops/update.sh" cp "$ROOT/deploy/update.sh" "$STAGE/ops/update.sh"
cp "$ROOT/deploy/baota-start.sh" "$STAGE/ops/baota-start.sh" cp "$ROOT/deploy/baota-start.sh" "$STAGE/ops/baota-start.sh"
cp "$ROOT/deploy/baota-init.sh" "$STAGE/ops/baota-init.sh"
cp "$ROOT/deploy/release-public.pem" "$STAGE/ops/release-public.pem"
cp "$ROOT/deploy/baota.env.example" "$STAGE/ops/baota.env.example" 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. # 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/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-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.service" "$STAGE/ops/kaidi-update.service"
cp "$ROOT/deploy/systemd/kaidi-update.path" "$STAGE/ops/kaidi-update.path" cp "$ROOT/deploy/systemd/kaidi-update.path" "$STAGE/ops/kaidi-update.path"
chmod 0755 "$STAGE/ops/update.sh" "$STAGE/ops/baota-start.sh" chmod 0755 "$STAGE/ops/update.sh" "$STAGE/ops/baota-start.sh" "$STAGE/ops/baota-init.sh"
ARTIFACT="kaidi-finance-$VERSION.tar.gz" ARTIFACT="kaidi-finance-$VERSION.tar.gz"
COPYFILE_DISABLE=1 tar --format=ustar -czf "$OUTPUT_DIR/$ARTIFACT" -C "$STAGE" . COPYFILE_DISABLE=1 tar --format=ustar -czf "$OUTPUT_DIR/$ARTIFACT" -C "$STAGE" .
+74 -20
View File
@@ -3,21 +3,51 @@ set -Eeuo pipefail
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
WORK=$(mktemp -d) WORK=$(mktemp -d)
trap 'rm -rf "$WORK"' EXIT EXECUTION_MARKER=/tmp/kaidi-baota-fixture-executed-$$
trap 'rm -rf "$WORK" "$EXECUTION_MARKER"' EXIT
fail() { fail() {
printf 'Baota start fixture failed: %s\n' "$1" >&2 printf 'Baota start fixture failed: %s\n' "$1" >&2
[ ! -f "$WORK/java.log" ] || sed -n '1,40p' "$WORK/java.log" >&2
exit 1 exit 1
} }
RELEASE="$WORK/release" RELEASE="$WORK/release"
mkdir -p "$RELEASE/ops" "$RELEASE/public" "$WORK/files" "$WORK/tmp" mkdir -p "$RELEASE/ops" "$RELEASE/public" "$WORK/files" "$WORK/tmp" "$WORK/state" "$WORK/mock-bin"
RELEASE_PHYSICAL=$(cd "$RELEASE" && pwd -P)
printf 'fixture jar\n' > "$RELEASE/app.jar" printf 'fixture jar\n' > "$RELEASE/app.jar"
printf '<!doctype html>\n' > "$RELEASE/public/index.html" printf '<!doctype html>\n' > "$RELEASE/public/index.html"
printf '1.0.0-preview.24\n' > "$RELEASE/VERSION" printf '1.0.0-preview.25\n' > "$RELEASE/VERSION"
cp "$ROOT/deploy/baota-start.sh" "$RELEASE/ops/baota-start.sh" cp "$ROOT/deploy/baota-start.sh" "$RELEASE/ops/baota-start.sh"
chmod 0755 "$RELEASE/ops/baota-start.sh" chmod 0755 "$RELEASE/ops/baota-start.sh"
cat > "$WORK/mock-bin/awk" <<'SH'
#!/usr/bin/env bash
case "${2:-}" in
/proc/*/stat) printf '424242\n' ;;
*) exec /usr/bin/awk "$@" ;;
esac
SH
chmod 0755 "$WORK/mock-bin/awk"
cat > "$WORK/base.env" <<EOF
SPRING_PROFILES_ACTIVE="production"
SERVER_ADDRESS="127.0.0.1"
SERVER_PORT="18080"
FIELD_ENCRYPTION_KEY="fixture-field-key"
FILE_STORAGE_ROOT="$WORK/files"
FILE_STORAGE_TEMP="$WORK/tmp"
FINANCE_UPDATE_ENABLED="true"
KAIDI_PID_FILE="$WORK/state/kaidi.pid"
EOF
cat > "$WORK/runtime.env" <<EOF
FINANCE_SETUP_ENABLED="false"
SERVER_PORT="19090"
DB_URL="jdbc:mysql://db.fixture/kaidi_finance"
DB_USERNAME="fixture-user"
DB_PASSWORD='\$(touch $EXECUTION_MARKER)'
EOF
cat > "$WORK/java" <<'SH' cat > "$WORK/java" <<'SH'
#!/usr/bin/env bash #!/usr/bin/env bash
set -Eeuo pipefail set -Eeuo pipefail
@@ -29,39 +59,63 @@ set -Eeuo pipefail
printf 'port=%s\n' "$SERVER_PORT" printf 'port=%s\n' "$SERVER_PORT"
printf 'update=%s\n' "$FINANCE_UPDATE_ENABLED" printf 'update=%s\n' "$FINANCE_UPDATE_ENABLED"
printf 'static=%s\n' "$FINANCE_STATIC_LOCATIONS" printf 'static=%s\n' "$FINANCE_STATIC_LOCATIONS"
printf 'username=%s\n' "$DB_USERNAME"
printf 'password=%s\n' "$DB_PASSWORD"
printf 'pid-record=%s\n' "$(cat "$KAIDI_PID_FILE")"
printf 'self-pid=%s\n' "$$"
} > "$MOCK_JAVA_LOG" } > "$MOCK_JAVA_LOG"
SH SH
chmod 0755 "$WORK/java" chmod 0755 "$WORK/java"
MOCK_JAVA_LOG="$WORK/java.log" \ MOCK_JAVA_LOG="$WORK/java.log" \
PATH="$WORK/mock-bin:$PATH" \
KAIDI_JAVA_BIN="$WORK/java" \ KAIDI_JAVA_BIN="$WORK/java" \
DB_URL='jdbc:mysql://db.fixture/kaidi_finance' \ KAIDI_CONFIG_FILE="$WORK/base.env" \
DB_USERNAME=fixture \ KAIDI_RUNTIME_ENV_FILE="$WORK/runtime.env" \
DB_PASSWORD='fixture-password' \ DB_URL='' DB_USERNAME='' DB_PASSWORD='' \
FIELD_ENCRYPTION_KEY='fixture-field-key' \
FILE_STORAGE_ROOT="$WORK/files" \
FILE_STORAGE_TEMP="$WORK/tmp" \
"$RELEASE/ops/baota-start.sh" "$RELEASE/ops/baota-start.sh"
grep -Fqx "cwd=$RELEASE" "$WORK/java.log" || fail 'launcher did not use the release working directory' grep -Fqx "cwd=$RELEASE_PHYSICAL" "$WORK/java.log" || fail 'launcher did not use the physical release directory'
grep -Fqx "args=-XX:MaxRAMPercentage=70 -Dfile.encoding=UTF-8 -jar $RELEASE/app.jar" "$WORK/java.log" \ grep -Fqx "args=-XX:MaxRAMPercentage=70 -Dfile.encoding=UTF-8 -Dkaidi.release.path=$RELEASE_PHYSICAL -Dkaidi.release.version=1.0.0-preview.25 -jar $RELEASE_PHYSICAL/app.jar" "$WORK/java.log" \
|| fail 'launcher did not execute the packaged Spring Boot jar' || fail 'launcher did not identify and execute the packaged Spring Boot release'
grep -Fqx 'version=1.0.0-preview.24' "$WORK/java.log" || fail 'launcher did not read VERSION' grep -Fqx 'version=1.0.0-preview.25' "$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 '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 'port=19090' "$WORK/java.log" || fail 'runtime environment did not override the base port'
grep -Fqx 'update=false' "$WORK/java.log" || fail 'launcher did not disable the systemd updater' grep -Fqx 'update=true' "$WORK/java.log" || fail 'launcher did not keep online update enabled'
grep -Fqx "static=file:$RELEASE/public/" "$WORK/java.log" \ grep -Fqx "static=file:$RELEASE_PHYSICAL/public/" "$WORK/java.log" \
|| fail 'launcher did not bind static resources to the release directory' || fail 'launcher did not bind static resources to the release directory'
grep -Fqx 'username=fixture-user' "$WORK/java.log" || fail 'launcher did not load setup database values'
grep -Fqx "password=\$(touch $EXECUTION_MARKER)" "$WORK/java.log" \
|| fail 'launcher changed a literal password while parsing the protected environment'
[ ! -e "$EXECUTION_MARKER" ] || fail 'launcher executed command syntax from a database password'
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'
MOCK_JAVA_LOG="$WORK/explicit.log" \
PATH="$WORK/mock-bin:$PATH" \
KAIDI_JAVA_BIN="$WORK/java" \
KAIDI_CONFIG_FILE="$WORK/base.env" \
KAIDI_RUNTIME_ENV_FILE="$WORK/runtime.env" \
SERVER_PORT=20000 \
"$RELEASE/ops/baota-start.sh"
grep -Fqx 'port=20000' "$WORK/explicit.log" || fail 'non-empty Baota environment did not take precedence'
cat > "$WORK/missing-db.env" <<EOF
FIELD_ENCRYPTION_KEY="fixture-field-key"
FILE_STORAGE_ROOT="$WORK/files"
FILE_STORAGE_TEMP="$WORK/tmp"
KAIDI_PID_FILE="$WORK/state/missing.pid"
EOF
if KAIDI_JAVA_BIN="$WORK/java" \ if KAIDI_JAVA_BIN="$WORK/java" \
DB_USERNAME=fixture DB_PASSWORD=fixture FIELD_ENCRYPTION_KEY=fixture \ KAIDI_CONFIG_FILE="$WORK/missing-db.env" \
FILE_STORAGE_ROOT="$WORK/files" FILE_STORAGE_TEMP="$WORK/tmp" \ KAIDI_RUNTIME_ENV_FILE="$WORK/absent.env" \
MOCK_JAVA_LOG="$WORK/missing-db.log" \ MOCK_JAVA_LOG="$WORK/missing-db.log" \
"$RELEASE/ops/baota-start.sh" > "$WORK/missing-db.out" 2>&1; then "$RELEASE/ops/baota-start.sh" > "$WORK/missing-db.out" 2>&1; then
fail 'launcher accepted a production configuration without DB_URL' fail 'launcher accepted a production configuration without DB_URL'
fi fi
grep -Fq 'DB_URL must be configured' "$WORK/missing-db.out" \ grep -Fq 'DB_URL is missing from the protected Kaidi configuration' "$WORK/missing-db.out" \
|| fail 'launcher did not report the missing database URL' || fail 'launcher did not report the missing database URL'
[ ! -e "$WORK/missing-db.log" ] || fail 'launcher started Java after configuration validation failed' [ ! -e "$WORK/missing-db.log" ] || fail 'launcher started Java after configuration validation failed'
printf 'Baota Spring Boot launcher fixture passed\n' printf 'Baota Spring Boot launcher configuration, PID identity, and injection fixtures passed\n'
+8 -4
View File
@@ -378,13 +378,17 @@ grep -Fq 'SERVER_ADDRESS "$SERVER_ADDRESS"' "$ROOT/deploy/install.sh" \
grep -Fq 'EnvironmentFile=-/var/lib/kaidi/setup/application.env' \ grep -Fq 'EnvironmentFile=-/var/lib/kaidi/setup/application.env' \
"$ROOT/deploy/systemd/kaidi-finance.service" \ "$ROOT/deploy/systemd/kaidi-finance.service" \
|| fail 'application service no longer loads the setup-completion environment' || fail 'application service no longer loads the setup-completion environment'
grep -Fq 'EnvironmentFile=-/var/lib/kaidi/setup/application.env' \ ! grep -Fq 'EnvironmentFile=-/var/lib/kaidi/setup/application.env' \
"$ROOT/deploy/systemd/kaidi-update.service" \ "$ROOT/deploy/systemd/kaidi-update.service" \
|| fail 'update service no longer loads setup database overrides' || fail 'root update service must not load an application-owned environment file'
grep -Fq 'load_runtime_database_env' "$ROOT/deploy/update.sh" \
|| fail 'update script no longer imports only the setup database fields'
# shellcheck disable=SC2016 # Match literal installer source. # 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" \ 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' || fail 'update state parent is not group-accessible to the application user'
grep -Fq 'install.sh | sudo bash' "$ROOT/README.md" \ grep -Fq 'kaidi-finance-1.0.0-preview.25.tar.gz' "$ROOT/README.md" \
|| fail 'README does not document the public one-line setup-wizard install path' || 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'
printf 'Install configuration, custom port, public Gitea, optional private token, i686, setup wizard, and MySQL 8.4 fixtures passed\n' printf 'Install configuration, custom port, public Gitea, optional private token, i686, setup wizard, and MySQL 8.4 fixtures passed\n'
+9 -5
View File
@@ -174,13 +174,20 @@ build_release() {
local unit_prefix=${3:-new} local unit_prefix=${3:-new}
local stage="$fixture/stage" local stage="$fixture/stage"
mkdir -p "$fixture/release" "$stage/public" "$stage/ops" 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
printf 'new application\n' > "$stage/app.jar" printf 'new application\n' > "$stage/app.jar"
printf '<!doctype html><title>new</title>\n' > "$stage/public/index.html" printf '<!doctype html><title>new</title>\n' > "$stage/public/index.html"
printf '%s\n' "$version" > "$stage/VERSION" printf '%s\n' "$version" > "$stage/VERSION"
printf '#!/bin/sh\nprintf "new updater\\n"\n' > "$stage/ops/update.sh" 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" printf '#!/usr/bin/env bash\nexit 0\n' > "$stage/ops/baota-start.sh"
printf '#!/usr/bin/env bash\nexit 0\n' > "$stage/ops/baota-init.sh"
cp "$fixture/release-public.pem" "$stage/ops/release-public.pem"
chmod 0755 "$stage/ops/update.sh" chmod 0755 "$stage/ops/update.sh"
chmod 0755 "$stage/ops/baota-start.sh" chmod 0755 "$stage/ops/baota-start.sh"
chmod 0755 "$stage/ops/baota-init.sh"
for name in kaidi-finance.service kaidi-update.service kaidi-update.path; do for name in kaidi-finance.service kaidi-update.service kaidi-update.path; do
printf '%s %s\n' "$unit_prefix" "$name" > "$stage/ops/$name" printf '%s %s\n' "$unit_prefix" "$name" > "$stage/ops/$name"
done done
@@ -192,10 +199,6 @@ build_release() {
jq -n --arg version "$version" --arg artifact "$artifact" --arg sha "$sha" \ jq -n --arg version "$version" --arg artifact "$artifact" --arg sha "$sha" \
'{version:$version,artifact:$artifact,sha256:$sha,publishedAt:"2026-08-16T00:00:00Z",releaseNotes:"fixture"}' \ '{version:$version,artifact:$artifact,sha256:$sha,publishedAt:"2026-08-16T00:00:00Z",releaseNotes:"fixture"}' \
> "$fixture/release/release-manifest.json" > "$fixture/release/release-manifest.json"
"$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
"$REAL_OPENSSL" dgst -sha256 -sign "$fixture/private.pem" \ "$REAL_OPENSSL" dgst -sha256 -sign "$fixture/private.pem" \
-out "$fixture/release/release-manifest.sig" "$fixture/release/release-manifest.json" -out "$fixture/release/release-manifest.sig" "$fixture/release/release-manifest.json"
} }
@@ -284,6 +287,7 @@ run_update() {
KAIDI_LOG_ROOT="$fixture/log" \ KAIDI_LOG_ROOT="$fixture/log" \
KAIDI_SYSTEMD_ROOT="$fixture/systemd" \ KAIDI_SYSTEMD_ROOT="$fixture/systemd" \
KAIDI_UPDATER_PATH="$fixture/app/bin/update.sh" \ KAIDI_UPDATER_PATH="$fixture/app/bin/update.sh" \
KAIDI_RUNTIME_ENV_FILE="$fixture/runtime.env" \
KAIDI_SKIP_DB_BACKUP="$skip_backup" \ KAIDI_SKIP_DB_BACKUP="$skip_backup" \
KAIDI_UPDATE_HEALTH_ATTEMPTS=1 \ KAIDI_UPDATE_HEALTH_ATTEMPTS=1 \
KAIDI_UPDATE_HEALTH_INTERVAL_SECONDS=0 \ KAIDI_UPDATE_HEALTH_INTERVAL_SECONDS=0 \
@@ -424,7 +428,7 @@ assert_rollback_case() {
if run_update "$fixture" fail-new > "$fixture/update.log" 2>&1; then if run_update "$fixture" fail-new > "$fixture/update.log" 2>&1; then
fail 'rollback case unexpectedly succeeded' fail 'rollback case unexpectedly succeeded'
fi fi
grep -q 'previous release was restored and verified' "$fixture/update.log" \ grep -q 'previous application release was restored and verified' "$fixture/update.log" \
|| fail 'rollback case did not report a complete restoration' || fail 'rollback case did not report a complete restoration'
[ "$(readlink "$fixture/app/current")" = "$fixture/app/releases/1.0.0-preview.1" ] \ [ "$(readlink "$fixture/app/current")" = "$fixture/app/releases/1.0.0-preview.1" ] \
|| fail 'rollback case did not restore the previous application' || fail 'rollback case did not restore the previous application'
+5
View File
@@ -169,6 +169,8 @@ compare_archive_file() {
compare_archive_file ./ops/update.sh deploy/update.sh 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-start.sh deploy/baota-start.sh
compare_archive_file ./ops/baota-init.sh deploy/baota-init.sh
compare_archive_file ./ops/release-public.pem deploy/release-public.pem
compare_archive_file ./ops/baota.env.example deploy/baota.env.example 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.path deploy/systemd/kaidi-update.path
compare_archive_file ./ops/kaidi-update.service deploy/systemd/kaidi-update.service compare_archive_file ./ops/kaidi-update.service deploy/systemd/kaidi-update.service
@@ -176,7 +178,10 @@ compare_archive_file ./ops/kaidi-finance.service deploy/systemd/kaidi-finance.se
compare_archive_file ./ops/kaidi-finance.conf deploy/nginx/kaidi-finance.conf 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' \ 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; } || { printf 'Baota Spring Boot launcher is not executable\n' >&2; exit 1; }
tar -tvzf "$ARTIFACT" ./ops/baota-init.sh | grep -Eq '^-.{2}x.{2}x.{2}x' \
|| { printf 'Baota initializer is not executable\n' >&2; exit 1; }
bash -n "$ROOT/deploy/baota-start.sh" bash -n "$ROOT/deploy/baota-start.sh"
bash -n "$ROOT/deploy/baota-init.sh"
cmp install.sh "$ROOT/deploy/install.sh" \ cmp install.sh "$ROOT/deploy/install.sh" \
|| { printf 'Release installer does not match deploy/install.sh\n' >&2; exit 1; } || { printf 'Release installer does not match deploy/install.sh\n' >&2; exit 1; }
bash -n "$ROOT/deploy/purge.sh" bash -n "$ROOT/deploy/purge.sh"