Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d61bdf9da4 | ||
|
|
7237792424 | ||
|
|
cfdc0fed44 | ||
|
|
fa4517c263 | ||
|
|
2c73ad7eb0 | ||
|
|
c35ef3d383 | ||
|
|
cea46b494d | ||
|
|
b679381946 |
@@ -65,12 +65,17 @@ jobs:
|
||||
./scripts/test-install-fixture.sh
|
||||
./scripts/test-git-install-fixture.sh
|
||||
./scripts/test-update-fixture.sh
|
||||
./scripts/test-baota-start-fixture.sh
|
||||
./scripts/test-purge-fixture.sh
|
||||
./scripts/test-gitea-publish-fixture.sh
|
||||
shellcheck deploy/install.sh deploy/install-from-git.sh deploy/update.sh scripts/check-semver.sh \
|
||||
shellcheck deploy/install.sh deploy/install-from-git.sh deploy/update.sh deploy/baota-start.sh deploy/baota-init.sh \
|
||||
deploy/purge.sh \
|
||||
scripts/check-semver.sh \
|
||||
scripts/package-release.sh scripts/publish-gitea-release.sh \
|
||||
scripts/generate-release-key.sh scripts/test-install-fixture.sh \
|
||||
scripts/test-git-install-fixture.sh \
|
||||
scripts/test-update-fixture.sh scripts/test-gitea-publish-fixture.sh \
|
||||
scripts/test-baota-start-fixture.sh scripts/test-purge-fixture.sh \
|
||||
scripts/verify-release.sh
|
||||
|
||||
- name: Build and sign release assets
|
||||
|
||||
@@ -41,141 +41,166 @@ npm run build
|
||||
./scripts/export-openapi.sh http://127.0.0.1:18080
|
||||
```
|
||||
|
||||
## R1 Preview 一键安装
|
||||
## R1 Preview 部署方式
|
||||
|
||||
代码仓库和 Release 已公开,Linux 服务器不需要 Gitea Token。安装器读取
|
||||
`https://git.awaioi.com/api/v1/repos/ERP-Team/kaidi/releases/latest`,生产机不执行 `git pull` 或现场编译。
|
||||
安装脚本内置固定的发布公钥指纹,下载的应用包、清单、SBOM 和更新脚本仍须通过 RSA 签名与 SHA-256 校验。
|
||||
Gitea API 只用于确认 Release tag 和资产名称;下载地址由受信 API 域名、仓库路径和 tag 重新构造,不采用
|
||||
Gitea 响应中可能错误指向内网地址的 `browser_download_url`。
|
||||
本版只支持由运维人员预先准备的外部 MySQL 8.4.x;安装器不会安装 MySQL、创建数据库容器或修改现有
|
||||
PostgreSQL 18。PostgreSQL 兼容开发已冻结,不属于本次 Preview 发布范围。
|
||||
代码仓库和 Release 已公开,提供两种互斥方式:
|
||||
|
||||
执行命令的机器需预装 `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、宝塔或其他反向代理。
|
||||
1. **systemd 一键安装(推荐)**:程序独占 `/opt/kaidi` 和 `kaidi-finance.service`,不创建宝塔 Java 项目。
|
||||
2. **宝塔手动部署**:下载压缩包后解压并运行本地初始化脚本,再创建 Spring Boot 项目。
|
||||
|
||||
### 直接 curl 安装
|
||||
两种方式不能同时运行在同一个端口。程序不会安装 MySQL,数据库只支持运维人员预先准备的外部 MySQL 8.4.x,
|
||||
PostgreSQL 18 兼容工作继续冻结。
|
||||
|
||||
### systemd 一键安装(推荐)
|
||||
|
||||
先在宝塔面板停止并删除当前错误的 Java 项目,再执行:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.19/install.sh | sudo bash
|
||||
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.26/install.sh | sudo env KAIDI_APP_PORT=18080 bash
|
||||
```
|
||||
|
||||
这条命令会提示填写 Java 应用端口,直接回车使用 `18080`;随后安装最新签名 Release,并默认进入 `/setup`
|
||||
安装向导。Java 默认只监听 `127.0.0.1:所选端口`,前端页面、API 和健康检查均由同一端口提供。无人值守安装可直接指定:
|
||||
该命令只安装程序运行所需的 systemd 单元,自动创建 `kaidi` 用户并检测现有 Java 17(包括
|
||||
`/www/server/java/*/bin/java`);没有可用 Java 时才下载匹配架构的运行时。它不会安装 MySQL,也不会要求宝塔面板提供
|
||||
`kaidi` 用户。安装完成后查看 `/root/kaidi-first-login.txt`,通过反向代理域名进入 `/setup`。
|
||||
|
||||
### 彻底清理旧安装
|
||||
|
||||
重新安装前先在宝塔面板停止并删除名为 `kaidi-finance` 的 Java 项目,避免面板守护进程重新拉起旧 Java。
|
||||
随后执行一键清理:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.19/install.sh \
|
||||
| sudo env KAIDI_APP_PORT=19090 bash
|
||||
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.24/purge.sh \
|
||||
-o /tmp/kaidi-purge.sh \
|
||||
&& printf '%s %s\n' c49c5460260d2c67d88aced99463e3d98a67f8ef0309f5c8d5d8a8f5e4addf36 /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
|
||||
```
|
||||
|
||||
端口必须在 `1024-65535` 范围且未被其他程序监听;安装器会在安装前检查冲突。只有明确需要让其他主机直连 Java
|
||||
时才设置 `KAIDI_SERVER_ADDRESS=0.0.0.0`,通常应保持默认回环绑定并由本机反向代理访问。需要在执行前独立校验安装脚本时使用:
|
||||
清理脚本会先停止 systemd 和遗留 Kaidi 进程,将旧配置、文件存储、安装码以及已有数据库备份归档到
|
||||
`/root/kaidi-reinstall-backups/`,再删除 `/opt/kaidi`、`/www/wwwroot/kaidi`、`/etc/kaidi`、
|
||||
`/var/lib/kaidi`、`/var/lib/kaidi-update`、`/var/log/kaidi` 和三个 systemd 单元。恢复包权限固定为
|
||||
`0600`,确认新安装及数据无误后再由 root 删除。脚本不删除外部 MySQL、系统 Java、Nginx 或宝塔站点配置;
|
||||
新安装必须连接一个新的空 MySQL 数据库,旧数据库保留用于回滚。
|
||||
|
||||
### 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
|
||||
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.19/install.sh -o /tmp/kaidi-install.sh
|
||||
printf '%s %s\n' bc9001197af843dc323907a98023064ccc4f184e000543d2086b8a6f8161dbc7 /tmp/kaidi-install.sh | sha256sum -c -
|
||||
sudo bash /tmp/kaidi-install.sh
|
||||
rm -f /tmp/kaidi-install.sh
|
||||
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"
|
||||
```
|
||||
|
||||
### 从固定 Git tag 拉取后安装
|
||||
如果服务器没有 `wget`,可使用浏览器下载后上传同一文件;`curl -fL URL -o FILE` 也只用于下载压缩包,不能替代初始化脚本。
|
||||
|
||||
需要保留源码快照时,可以拉取与 Release 对应的固定 tag,再运行仓库内包装器。公共仓库不要求凭据,
|
||||
包装器会在 `sudo` 前校验 `deploy/install.sh` 的固定 SHA-256,再按同一公钥信任链安装最新签名 Release。
|
||||
#### 2. 执行本地初始化
|
||||
|
||||
初始化脚本来自已下载的归档,并在本机以 root 运行。它创建 `kaidi` 启动用户、受保护配置、在线更新监听器和一次性
|
||||
安装码;不会下载 Java、安装 MySQL 或修改反向代理。端口参数可改为 `1024-65535` 中的空闲端口,默认 `18080`。
|
||||
|
||||
```bash
|
||||
git clone --branch v1.0.0-preview.19 --depth 1 https://git.awaioi.com/ERP-Team/kaidi.git kaidi-preview
|
||||
cd kaidi-preview
|
||||
./deploy/install-from-git.sh
|
||||
sudo "$RELEASE_ROOT/ops/baota-init.sh" 18080
|
||||
sudo cat /root/kaidi-first-login.txt
|
||||
```
|
||||
|
||||
首次安装使用向导时不要传 `KAIDI_DB_URL`、`KAIDI_DB_USERNAME` 或 `KAIDI_DB_PASSWORD`;这些值在浏览器中填写。
|
||||
只有已完成安装的修复性重装才从运行时配置读取数据库值,详见下文。
|
||||
初始化失败会回滚本次写入的配置、unit 和 `current` 链接,可以直接修正原因后重试;已经存在正式安装时不要重复执行,先按“彻底清理旧安装”流程处理。
|
||||
|
||||
安装器会完成以下动作:
|
||||
#### 3. 宝塔 Spring Boot 项目字段
|
||||
|
||||
- 仅在 Linux + systemd 环境执行;首版 32 位支持基线为带 systemd 的 Debian/Ubuntu x86 32 位 Linux。
|
||||
- 不安装或创建数据库;在 `/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`;完成向导后写入锁定标记并切换正式应用。
|
||||
在宝塔面板创建项目,类型选择 **Spring Boot**,不要选择 Tomcat。填写如下:
|
||||
|
||||
安装完成后先执行 `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`。
|
||||
| 字段 | 填写值 |
|
||||
| --- | --- |
|
||||
| 项目路径 | `/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/` |
|
||||
| 进程守护 | 开启 |
|
||||
| 项目意外重启 | 勾选 |
|
||||
|
||||
以安装端口 `19090` 为例,Nginx 只需代理到 Java,不需要单独托管前端文件:
|
||||
**环境变量全部留空,不要创建 DB_URL、DB_USERNAME、DB_PASSWORD 或 FIELD_ENCRYPTION_KEY。**
|
||||
`ops/baota.env.example` 仅是说明文件,不能把其中的占位值粘贴到面板。启动器先读取 `/etc/kaidi/kaidi.env`,向导完成后
|
||||
再读取 `/var/lib/kaidi/setup/application.env`;宝塔只有填写了非空覆盖值时才会覆盖受保护配置。数据库密码不会出现在
|
||||
Java 启动命令或 `ps` 参数中。
|
||||
|
||||
#### 4. 首次向导与反向代理
|
||||
|
||||
启动宝塔项目后,访问 `https://业务域名/setup`,输入 `/root/kaidi-first-login.txt` 中的一次性安装码,填写外部 MySQL
|
||||
连接信息和系统管理员账号密码。迁移完成后应用会退出一次,宝塔守护进程会自动拉起正式模式;再次访问域名根路径即可登录。
|
||||
首次配置数据库密码必须使用 HTTPS;没有域名时通过 SSH 隧道访问回环端口:
|
||||
|
||||
```bash
|
||||
ssh -L 18080:127.0.0.1:18080 root@SERVER_IP
|
||||
```
|
||||
|
||||
本机浏览器打开 `http://127.0.0.1:18080/setup`。反向代理只需把整个 `/` 转发到 `http://127.0.0.1:18080`,并保留以下请求头:
|
||||
|
||||
```nginx
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:19090;
|
||||
proxy_pass http://127.0.0.1:18080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_connect_timeout 10s;
|
||||
proxy_read_timeout 120s;
|
||||
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
|
||||
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. 后台在线更新
|
||||
|
||||
初始化脚本已启用 `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 位
|
||||
|
||||
应用已按 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.19/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.19/install.sh \
|
||||
| sudo env KAIDI_REINSTALL=true KAIDI_SETUP_WIZARD=false bash
|
||||
```
|
||||
|
||||
没有显式设置 `KAIDI_APP_PORT` 时,修复性重装会沿用 `/etc/kaidi/kaidi.env` 中的原端口。重装失败会恢复原应用链接、Java 运行时、环境文件和 systemd 单元;脚本会明确报告回滚不完整,
|
||||
不会把恢复失败吞掉。
|
||||
|
||||
如果安装器已完成但向导尚未提交,可执行下面的命令重新生成一次性安装码;该恢复路径只接受仍处于向导模式且未锁定的安装,正式模式不会被覆盖。
|
||||
|
||||
```bash
|
||||
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.19/install.sh \
|
||||
| sudo env KAIDI_REINSTALL=true bash
|
||||
```
|
||||
压缩包本身不绑定 CPU 架构,关键是宝塔项目选用可运行的 32 位 Java 17。32 位主机不能在本机运行 64 位 JDK,也不应尝试在本机安装
|
||||
MySQL 8.4;提前准备外部 MySQL,完成上述手动解压、初始化和向导即可。
|
||||
|
||||
### 停用并移除程序
|
||||
|
||||
@@ -204,48 +229,42 @@ base64 < release-signing-private.pem | tr -d '\n'
|
||||
Release;生产服务器从公共 Release 下载,不保存 Token。私钥不得提交到 Git。`.gitea/workflows/release.yml` 要求
|
||||
act_runner 提供 `ubuntu-24.04` 标签,并在 tag 发布时执行后端、前端、OpenAPI、Shell、安装/更新和浏览器门禁。
|
||||
|
||||
工作流先建立不可见草稿,再显式上传并核对 9 个资产的名称和大小,最后才发布为 `/releases/latest`。Gitea 的
|
||||
工作流先建立不可见草稿,再显式上传并核对 10 个资产的名称和大小,最后才发布为 `/releases/latest`。Gitea 的
|
||||
`latest` 会排除 `prerelease=true`,因此即使 tag 名含 `preview`,发布记录的 `prerelease` 也固定为 `false`;
|
||||
Preview 属性由 SemVer 版本名表达。之后推送 tag 即会构建、测试、签名并发布:
|
||||
|
||||
```bash
|
||||
git tag v1.0.0-preview.19
|
||||
git push origin v1.0.0-preview.19
|
||||
git tag v1.0.0-preview.25
|
||||
git push origin v1.0.0-preview.25
|
||||
```
|
||||
|
||||
在线更新使用独立的 TDesign 页面:隔离的系统管理员进入“系统治理 → 系统更新”。权限与配置页只管理用户、角色、数据范围、表单模板和参数版本,不配置系统名称或域名。
|
||||
更新源由 root 在 `/etc/kaidi/update.env` 固定为公共 Gitea Latest Release API;页面和普通 API 不能提交
|
||||
URL、Token、脚本或命令。更新流程固定为:
|
||||
在线更新仍使用独立的 TDesign 页面:系统管理员进入“系统治理 → 系统更新”。更新源由 root 在
|
||||
`/etc/kaidi/update.env` 固定为公共 Gitea Latest Release API;页面和普通 API 不能提交 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 单元后,原子切换 updater、systemd 和应用版本,不修改反向代理。
|
||||
7. 同时检查 Java 健康端点、更新 path unit 和 Java 托管的静态首页。全部通过后页面显示 10 秒倒计时并自动
|
||||
刷新;刷新或短暂断线发生在安装中时,页面会恢复 3 秒轮询。任一检查失败则恢复并验证上一版本。
|
||||
更新流程固定为:
|
||||
|
||||
忙碌期间检查、下载和安装按钮保持禁用,防止重复请求;这就是更新执行冷却。10 秒只用于成功后的页面刷新,
|
||||
不会延迟服务端切换。systemd 在 300 秒内连续失败 3 次后停止自动重试,避免失败任务空跑。
|
||||
1. 点击“获取更新”,后端排队 `DOWNLOAD`;更新器下载 manifest、签名和应用包,执行 RSA、SHA-256、版本、文件名和压缩包路径校验,
|
||||
通过后缓存到 `/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`,两者权限均为
|
||||
`0600`:前者供 Java 后端“检查更新”读取,后者供 root 更新器下载资产。默认 Token 为空;如改接私有镜像,
|
||||
可由运维人员在这两个文件中配置独立只读 Token,Token 不写入页面、状态 JSON、审计参数或更新日志。
|
||||
|
||||
数据库迁移必须保持至少一个版本的向后兼容。查看状态和日志:
|
||||
忙碌期间检查、下载和安装按钮保持禁用,防止重复请求;10 秒只用于成功后的页面刷新,不延迟服务端切换。数据库迁移必须至少保持一个版本向后兼容。
|
||||
查看状态和日志:
|
||||
|
||||
```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
|
||||
cat /var/lib/kaidi-update/status.json
|
||||
```
|
||||
|
||||
如果 `status.json` 显示 `FAILED` 且日志提示回滚未完成,不要删除
|
||||
`/var/lib/kaidi-update/processing/request.json` 或活动事务目录。systemd 在 300 秒内连续失败 3 次后会停止自动重试,
|
||||
修复日志所示的磁盘、权限或旧版本健康问题后执行:
|
||||
如果 `status.json` 显示 `FAILED` 且日志提示回滚未完成,失败请求会归档到
|
||||
`/var/lib/kaidi-update/failed`,活动事务保留在 `transactions/active`,系统不会自动重复安装。修复日志所示的
|
||||
磁盘、权限或旧版本健康问题后,才明确执行一次:
|
||||
|
||||
```bash
|
||||
sudo systemctl reset-failed kaidi-update.service kaidi-update.path
|
||||
@@ -262,16 +281,15 @@ 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.19
|
||||
./scripts/package-release.sh 1.0.0-preview.25
|
||||
KAIDI_TRUSTED_RELEASE_PUBLIC_KEY_SHA256=807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9 \
|
||||
./scripts/verify-release.sh dist/release
|
||||
```
|
||||
|
||||
输出位于 `dist/release/`,包含安装脚本、签名清单、公钥、SHA-256 清单、前后端 CycloneDX SBOM、
|
||||
首次安装指纹文件和完整应用压缩包。
|
||||
输出位于 `dist/release/`,包含安装脚本、彻底清理脚本、签名清单、公钥、SHA-256 清单、前后端 CycloneDX
|
||||
SBOM、首次安装指纹文件和完整应用压缩包。
|
||||
打包脚本会把 Maven `revision` 与 npm 包版本临时绑定到 Release SemVer,构建结束后恢复工作区源文件;
|
||||
JAR、两个 SBOM、签名清单或 tag 的版本只要有一项不一致,发布即失败。
|
||||
签名清单同时绑定应用包、两份 SBOM、安装器、公钥、bootstrap 指纹和 Git 源码修订;本地脏工作区会明确记录
|
||||
`source.dirty=true`,tag 工作流只接受干净 checkout 并记录 `source.dirty=false`。
|
||||
发布命令同时在终端输出 `Trusted release public-key SHA-256` 与 `Installer SHA-256`;把这两个值写入
|
||||
受控部署记录,再替换上述一键安装命令中的占位符。
|
||||
发布命令同时输出 Release 公钥和归档校验值;将压缩包 SHA-256 写入直链部署说明后再发布。
|
||||
|
||||
Executable
+241
@@ -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 "$@"
|
||||
Executable
+142
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
umask 027
|
||||
|
||||
die() {
|
||||
printf '[kaidi-baota] ERROR: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
RELEASE_ROOT=$(cd "$SCRIPT_DIR/.." && pwd -P)
|
||||
APP_JAR="$RELEASE_ROOT/app.jar"
|
||||
PUBLIC_INDEX="$RELEASE_ROOT/public/index.html"
|
||||
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 "$PUBLIC_INDEX" ] || die "public/index.html 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:-}
|
||||
if [ -z "$JAVA_BIN" ] && [ -n "${JAVA_HOME:-}" ]; then
|
||||
JAVA_BIN="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ -z "$JAVA_BIN" ]; then
|
||||
JAVA_BIN=$(command -v java 2>/dev/null || true)
|
||||
fi
|
||||
[ -n "$JAVA_BIN" ] || die "Java 17 or newer was not found"
|
||||
if [[ "$JAVA_BIN" == */* ]]; then
|
||||
[ -x "$JAVA_BIN" ] || die "Java executable is not available at $JAVA_BIN"
|
||||
else
|
||||
command -v "$JAVA_BIN" >/dev/null 2>&1 || die "Java executable $JAVA_BIN is not available"
|
||||
fi
|
||||
|
||||
export SPRING_PROFILES_ACTIVE=${SPRING_PROFILES_ACTIVE:-production}
|
||||
export SERVER_ADDRESS=${SERVER_ADDRESS:-127.0.0.1}
|
||||
export SERVER_PORT=${SERVER_PORT:-18080}
|
||||
export FINANCE_SETUP_ENABLED=${FINANCE_SETUP_ENABLED:-false}
|
||||
export FINANCE_BOOTSTRAP_ENABLED=${FINANCE_BOOTSTRAP_ENABLED:-false}
|
||||
export FINANCE_UPDATE_ENABLED=${FINANCE_UPDATE_ENABLED:-true}
|
||||
export FILE_SCANNER_ENABLED=${FILE_SCANNER_ENABLED:-false}
|
||||
export FINANCE_STATIC_LOCATIONS=${FINANCE_STATIC_LOCATIONS:-file:$RELEASE_ROOT/public/}
|
||||
export UPDATE_CURRENT_VERSION_FILE=${UPDATE_CURRENT_VERSION_FILE:-$VERSION_FILE}
|
||||
export APP_VERSION=${APP_VERSION:-$(tr -d '\r\n' < "$VERSION_FILE")}
|
||||
export KAIDI_PID_FILE=${KAIDI_PID_FILE:-/var/lib/kaidi/kaidi.pid}
|
||||
|
||||
if [ "$FINANCE_SETUP_ENABLED" != true ]; then
|
||||
for name in DB_URL DB_USERNAME DB_PASSWORD FIELD_ENCRYPTION_KEY FILE_STORAGE_ROOT FILE_STORAGE_TEMP; do
|
||||
[ -n "${!name:-}" ] || die "$name is missing from the protected Kaidi configuration"
|
||||
done
|
||||
for directory in "$FILE_STORAGE_ROOT" "$FILE_STORAGE_TEMP"; do
|
||||
[ -d "$directory" ] || die "Storage directory $directory does not exist"
|
||||
[ -w "$directory" ] || die "Startup user cannot write storage directory $directory"
|
||||
done
|
||||
fi
|
||||
|
||||
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"
|
||||
exec "$JAVA_BIN" -XX:MaxRAMPercentage=70 -Dfile.encoding=UTF-8 \
|
||||
"-Dkaidi.release.path=$RELEASE_ROOT" "-Dkaidi.release.version=$APP_VERSION" \
|
||||
-jar "$APP_JAR"
|
||||
@@ -0,0 +1,11 @@
|
||||
# Baota project environment variables are intentionally empty.
|
||||
# Do not paste database passwords or placeholder values into the panel.
|
||||
# ops/baota-start.sh loads the protected files created by ops/baota-init.sh:
|
||||
# /etc/kaidi/kaidi.env
|
||||
# /var/lib/kaidi/setup/application.env (after the setup wizard completes)
|
||||
#
|
||||
# Optional non-secret overrides may be added deliberately, for example:
|
||||
# SERVER_PORT=18080
|
||||
# SESSION_COOKIE_SECURE=true
|
||||
# Never override DB_URL, DB_USERNAME, DB_PASSWORD, FIELD_ENCRYPTION_KEY,
|
||||
# FINANCE_SETUP_TOKEN_SHA256, or the update paths from the panel.
|
||||
@@ -5,7 +5,7 @@ umask 077
|
||||
|
||||
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
INSTALLER="$ROOT/deploy/install.sh"
|
||||
INSTALLER_SHA256=${KAIDI_INSTALLER_SHA256:-bc9001197af843dc323907a98023064ccc4f184e000543d2086b8a6f8161dbc7}
|
||||
INSTALLER_SHA256=${KAIDI_INSTALLER_SHA256:-6256b05025aa314d4c02e7c0f5d2ca530f28ec5d10f833504b34c09ada6e02a5}
|
||||
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:-}
|
||||
|
||||
+40
-10
@@ -18,7 +18,13 @@ SERVER_ADDRESS=${KAIDI_SERVER_ADDRESS:-127.0.0.1}
|
||||
PROXY_TARGET=
|
||||
TRUSTED_PUBLIC_KEY_SHA256=${KAIDI_RELEASE_PUBLIC_KEY_SHA256:-807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9}
|
||||
REINSTALL=${KAIDI_REINSTALL:-false}
|
||||
SETUP_WIZARD=${KAIDI_SETUP_WIZARD:-true}
|
||||
if [ "${KAIDI_SETUP_WIZARD+x}" = x ]; then
|
||||
SETUP_WIZARD=$KAIDI_SETUP_WIZARD
|
||||
elif [ "$REINSTALL" = true ]; then
|
||||
SETUP_WIZARD=false
|
||||
else
|
||||
SETUP_WIZARD=true
|
||||
fi
|
||||
WORK_DIR=
|
||||
RELEASE_AUTH_HEADER_FILE=
|
||||
INSTALL_TRANSACTION_ARMED=false
|
||||
@@ -227,14 +233,8 @@ java_arch_matches_host() {
|
||||
esac
|
||||
}
|
||||
|
||||
system_java_home() {
|
||||
local java_bin resolved_java java_major home
|
||||
if [ -n "${KAIDI_JAVA_HOME:-}" ]; then
|
||||
home=${KAIDI_JAVA_HOME%/}
|
||||
java_bin="$home/bin/java"
|
||||
else
|
||||
java_bin=$(command -v java 2>/dev/null || true)
|
||||
fi
|
||||
java_home_from_bin() {
|
||||
local java_bin=$1 resolved_java java_major home
|
||||
[ -x "$java_bin" ] || return 1
|
||||
java_major=$(
|
||||
"$java_bin" -version 2>&1 \
|
||||
@@ -252,6 +252,34 @@ system_java_home() {
|
||||
printf '%s\n' "$home"
|
||||
}
|
||||
|
||||
system_java_home() {
|
||||
local candidate home
|
||||
if [ -n "${KAIDI_JAVA_HOME:-}" ]; then
|
||||
candidate="${KAIDI_JAVA_HOME%/}/bin/java"
|
||||
java_home_from_bin "$candidate" && return 0
|
||||
return 1
|
||||
fi
|
||||
candidate=$(command -v java 2>/dev/null || true)
|
||||
if [ -n "$candidate" ] && home=$(java_home_from_bin "$candidate"); then
|
||||
printf '%s\n' "$home"
|
||||
return 0
|
||||
fi
|
||||
# Baota commonly keeps JDKs outside PATH. Prefer an existing Java 17 there
|
||||
# before consulting the remote Azul metadata endpoint.
|
||||
for candidate in \
|
||||
/www/server/java/*/bin/java \
|
||||
/www/server/java/*/jre/bin/java \
|
||||
/usr/lib/jvm/*/bin/java \
|
||||
/usr/java/*/bin/java; do
|
||||
[ -x "$candidate" ] || continue
|
||||
if home=$(java_home_from_bin "$candidate"); then
|
||||
printf '%s\n' "$home"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
prepare_java() {
|
||||
local api java_metadata package_metadata package_uuid java_url java_sha256 actual_sha256 system_home java_line
|
||||
if system_home=$(system_java_home); then
|
||||
@@ -338,7 +366,7 @@ secure_release_tree() {
|
||||
chown -R root:"$SERVICE_GROUP" "$release_dir"
|
||||
find "$release_dir" -type d -exec chmod 0750 {} +
|
||||
find "$release_dir" -type f -exec chmod 0640 {} +
|
||||
chmod 0750 "$release_dir/ops/update.sh"
|
||||
chmod 0750 "$release_dir/ops/update.sh" "$release_dir/ops/baota-start.sh"
|
||||
}
|
||||
|
||||
restore_security_contexts() {
|
||||
@@ -842,6 +870,8 @@ find "$WORK_DIR/extracted" -type l -print -quit | grep -q . \
|
||||
[ -s "$WORK_DIR/extracted/public/index.html" ] || die "Release frontend is missing"
|
||||
[ "$(cat "$WORK_DIR/extracted/VERSION")" = "$VERSION" ] || die "Release version mismatch"
|
||||
[ -x "$WORK_DIR/extracted/ops/update.sh" ] || die "Release updater is missing"
|
||||
[ -x "$WORK_DIR/extracted/ops/baota-start.sh" ] || die "Baota Spring Boot launcher is missing"
|
||||
bash -n "$WORK_DIR/extracted/ops/baota-start.sh" || die "Baota Spring Boot launcher is invalid"
|
||||
|
||||
configure_database
|
||||
preflight_database
|
||||
|
||||
Executable
+199
@@ -0,0 +1,199 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
umask 077
|
||||
|
||||
APP_ROOT=/opt/kaidi
|
||||
BAOTA_ROOT=/www/wwwroot/kaidi
|
||||
CONFIG_ROOT=/etc/kaidi
|
||||
STATE_ROOT=/var/lib/kaidi
|
||||
UPDATE_STATE_ROOT=/var/lib/kaidi-update
|
||||
LOG_ROOT=/var/log/kaidi
|
||||
FIRST_LOGIN_FILE=/root/kaidi-first-login.txt
|
||||
BACKUP_ROOT=${KAIDI_PURGE_BACKUP_ROOT:-/root/kaidi-reinstall-backups}
|
||||
CONFIRMATION=${KAIDI_PURGE_CONFIRM:-}
|
||||
REQUIRED_CONFIRMATION=DELETE_LOCAL_KAIDI_INSTALLATION
|
||||
SERVICE_USER=kaidi
|
||||
SERVICE_GROUP=kaidi
|
||||
BACKUP_ARCHIVE=
|
||||
|
||||
log() { printf '[kaidi-purge] %s\n' "$*"; }
|
||||
die() { printf '[kaidi-purge] ERROR: %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
systemd_available() {
|
||||
command -v systemctl >/dev/null 2>&1 && [ -d /run/systemd/system ]
|
||||
}
|
||||
|
||||
validate_inputs() {
|
||||
[ "$(id -u)" -eq 0 ] || die "Run with sudo or as root"
|
||||
[ "$(uname -s)" = Linux ] || die "The purge script only supports Linux"
|
||||
[ "$CONFIRMATION" = "$REQUIRED_CONFIRMATION" ] \
|
||||
|| die "Set KAIDI_PURGE_CONFIRM=$REQUIRED_CONFIRMATION to confirm local removal"
|
||||
case "$BACKUP_ROOT" in
|
||||
/*) ;;
|
||||
*) die "KAIDI_PURGE_BACKUP_ROOT must be an absolute path" ;;
|
||||
esac
|
||||
case "$BACKUP_ROOT/" in
|
||||
"$APP_ROOT/"*|"$BAOTA_ROOT/"*|"$CONFIG_ROOT/"*|"$STATE_ROOT/"*|\
|
||||
"$UPDATE_STATE_ROOT/"*|"$LOG_ROOT/"*)
|
||||
die "The recovery backup must be outside every managed Kaidi directory"
|
||||
;;
|
||||
esac
|
||||
[ ! -L "$BACKUP_ROOT" ] || die "The recovery backup root must not be a symbolic link"
|
||||
}
|
||||
|
||||
stop_systemd_units() {
|
||||
local unit
|
||||
systemd_available || return 0
|
||||
for unit in kaidi-update.path kaidi-update.service kaidi-finance.service; do
|
||||
if systemctl cat "$unit" >/dev/null 2>&1; then
|
||||
systemctl stop "$unit" >/dev/null 2>&1 \
|
||||
|| die "Failed to stop $unit"
|
||||
systemctl disable "$unit" >/dev/null 2>&1 || true
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
related_pids() {
|
||||
local proc pid command_line
|
||||
for proc in /proc/[0-9]*; do
|
||||
[ -r "$proc/cmdline" ] || continue
|
||||
pid=${proc##*/}
|
||||
[ "$pid" != "$$" ] && [ "$pid" != "$PPID" ] || continue
|
||||
command_line=$(tr '\000' ' ' < "$proc/cmdline" 2>/dev/null || true)
|
||||
case "$command_line" in
|
||||
*"$APP_ROOT/"*|*"$BAOTA_ROOT/"*) printf '%s\n' "$pid" ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
stop_managed_processes() {
|
||||
local deadline
|
||||
local -a pids=()
|
||||
mapfile -t pids < <(related_pids)
|
||||
if [ "${#pids[@]}" -gt 0 ]; then
|
||||
log "Stopping remaining Kaidi processes: ${pids[*]}"
|
||||
kill -TERM "${pids[@]}" 2>/dev/null || true
|
||||
fi
|
||||
deadline=$((SECONDS + 5))
|
||||
while [ "$SECONDS" -lt "$deadline" ]; do
|
||||
mapfile -t pids < <(related_pids)
|
||||
[ "${#pids[@]}" -gt 0 ] || return 0
|
||||
sleep 1
|
||||
done
|
||||
kill -KILL "${pids[@]}" 2>/dev/null || true
|
||||
sleep 1
|
||||
mapfile -t pids < <(related_pids)
|
||||
[ "${#pids[@]}" -eq 0 ] \
|
||||
|| die "A process manager is restarting Kaidi; remove the Kaidi project from Baota and run this command again"
|
||||
}
|
||||
|
||||
create_recovery_backup() {
|
||||
local path relative temporary timestamp
|
||||
local -a paths=()
|
||||
for path in \
|
||||
"$CONFIG_ROOT" \
|
||||
"$STATE_ROOT" \
|
||||
"$UPDATE_STATE_ROOT/backups" \
|
||||
"$UPDATE_STATE_ROOT/failed" \
|
||||
"$UPDATE_STATE_ROOT/transactions" \
|
||||
"$FIRST_LOGIN_FILE"; do
|
||||
if [ -e "$path" ] || [ -L "$path" ]; then
|
||||
relative=${path#/}
|
||||
paths+=("$relative")
|
||||
fi
|
||||
done
|
||||
if [ "${#paths[@]}" -eq 0 ]; then
|
||||
log "No local configuration or data needs a recovery backup"
|
||||
return 0
|
||||
fi
|
||||
install -d -o root -g root -m 0700 "$BACKUP_ROOT"
|
||||
timestamp=$(date -u +%Y%m%dT%H%M%SZ)
|
||||
BACKUP_ARCHIVE="$BACKUP_ROOT/kaidi-local-state-$timestamp.tar.gz"
|
||||
temporary="$BACKUP_ARCHIVE.next.$$"
|
||||
log "Creating a root-only recovery backup at $BACKUP_ARCHIVE"
|
||||
tar -czf "$temporary" -C / -- "${paths[@]}"
|
||||
tar -tzf "$temporary" >/dev/null
|
||||
chmod 0600 "$temporary"
|
||||
mv -f "$temporary" "$BACKUP_ARCHIVE"
|
||||
}
|
||||
|
||||
remove_systemd_units() {
|
||||
local unit
|
||||
rm -f \
|
||||
/etc/systemd/system/kaidi-finance.service \
|
||||
/etc/systemd/system/kaidi-update.service \
|
||||
/etc/systemd/system/kaidi-update.path \
|
||||
/etc/systemd/system/multi-user.target.wants/kaidi-finance.service \
|
||||
/etc/systemd/system/multi-user.target.wants/kaidi-update.path
|
||||
systemd_available || return 0
|
||||
systemctl daemon-reload
|
||||
for unit in kaidi-finance.service kaidi-update.service kaidi-update.path; do
|
||||
systemctl reset-failed "$unit" >/dev/null 2>&1 || true
|
||||
done
|
||||
}
|
||||
|
||||
remove_managed_paths() {
|
||||
rm -rf -- \
|
||||
"$APP_ROOT" \
|
||||
"$BAOTA_ROOT" \
|
||||
"$CONFIG_ROOT" \
|
||||
"$STATE_ROOT" \
|
||||
"$UPDATE_STATE_ROOT" \
|
||||
"$LOG_ROOT"
|
||||
rm -f -- "$FIRST_LOGIN_FILE"
|
||||
}
|
||||
|
||||
remove_download_archives() {
|
||||
rm -f -- /tmp/kaidi-finance-*.tar.gz
|
||||
}
|
||||
|
||||
remove_service_identity() {
|
||||
local entry home shell group_entry gid members primary_users
|
||||
entry=$(getent passwd "$SERVICE_USER" 2>/dev/null || true)
|
||||
if [ -n "$entry" ]; then
|
||||
home=$(printf '%s\n' "$entry" | awk -F: '{print $6}')
|
||||
shell=$(printf '%s\n' "$entry" | awk -F: '{print $7}')
|
||||
case "$home:$shell" in
|
||||
"$STATE_ROOT:"*/nologin|"$STATE_ROOT:"*/false)
|
||||
userdel "$SERVICE_USER" \
|
||||
|| die "Failed to remove the dedicated $SERVICE_USER service account"
|
||||
;;
|
||||
*)
|
||||
log "Keeping pre-existing user $SERVICE_USER because its home or shell is not Kaidi-managed"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
group_entry=$(getent group "$SERVICE_GROUP" 2>/dev/null || true)
|
||||
[ -n "$group_entry" ] || return 0
|
||||
gid=$(printf '%s\n' "$group_entry" | awk -F: '{print $3}')
|
||||
members=$(printf '%s\n' "$group_entry" | awk -F: '{print $4}')
|
||||
primary_users=$(getent passwd | awk -F: -v gid="$gid" '$4 == gid { print $1 }')
|
||||
if [ -z "$members" ] && [ -z "$primary_users" ]; then
|
||||
groupdel "$SERVICE_GROUP" \
|
||||
|| die "Failed to remove the dedicated $SERVICE_GROUP service group"
|
||||
else
|
||||
log "Keeping group $SERVICE_GROUP because another account still uses it"
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
validate_inputs
|
||||
log "External MySQL data and reverse-proxy configuration will not be modified"
|
||||
stop_systemd_units
|
||||
stop_managed_processes
|
||||
create_recovery_backup
|
||||
remove_systemd_units
|
||||
remove_managed_paths
|
||||
remove_download_archives
|
||||
remove_service_identity
|
||||
stop_managed_processes
|
||||
log "Local Kaidi installation state has been removed"
|
||||
if [ -n "$BACKUP_ARCHIVE" ]; then
|
||||
log "Recovery backup: $BACKUP_ARCHIVE"
|
||||
fi
|
||||
log "Use a new empty MySQL database for the next installation"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -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-----
|
||||
@@ -4,8 +4,6 @@ Description=Watch for Kaidi Finance update requests
|
||||
[Path]
|
||||
PathChanged=/var/lib/kaidi-update/inbox
|
||||
PathExists=/var/lib/kaidi-update/inbox/request.json
|
||||
PathChanged=/var/lib/kaidi-update/processing
|
||||
PathExists=/var/lib/kaidi-update/processing/request.json
|
||||
Unit=kaidi-update.service
|
||||
|
||||
[Install]
|
||||
|
||||
@@ -10,15 +10,13 @@ Type=oneshot
|
||||
User=root
|
||||
Group=root
|
||||
EnvironmentFile=/etc/kaidi/update.env
|
||||
EnvironmentFile=-/var/lib/kaidi/setup/application.env
|
||||
ExecStart=/opt/kaidi/bin/update.sh
|
||||
Restart=on-failure
|
||||
RestartSec=15
|
||||
Restart=no
|
||||
Nice=10
|
||||
IOSchedulingClass=best-effort
|
||||
IOSchedulingPriority=6
|
||||
PrivateTmp=true
|
||||
ProtectHome=true
|
||||
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
|
||||
|
||||
+250
-35
@@ -18,6 +18,10 @@ 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}
|
||||
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_GROUP=${KAIDI_SERVICE_GROUP:-kaidi}
|
||||
HEALTH_URL=${KAIDI_HEALTH_URL:-http://127.0.0.1:18080/actuator/health}
|
||||
@@ -29,6 +33,7 @@ SYSTEMD_ROOT=${KAIDI_SYSTEMD_ROOT:-/etc/systemd/system}
|
||||
LOG_ROOT=${KAIDI_LOG_ROOT:-/var/log/kaidi}
|
||||
HEALTH_ATTEMPTS=${KAIDI_UPDATE_HEALTH_ATTEMPTS:-60}
|
||||
HEALTH_INTERVAL_SECONDS=${KAIDI_UPDATE_HEALTH_INTERVAL_SECONDS:-2}
|
||||
SHUTDOWN_ATTEMPTS=${KAIDI_UPDATE_SHUTDOWN_ATTEMPTS:-45}
|
||||
WORK_DIR=
|
||||
CACHE_TEMP=
|
||||
RELEASE_AUTH_HEADER_FILE=
|
||||
@@ -39,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 ;;
|
||||
esac
|
||||
[ "$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() {
|
||||
printf '%s\n' "$1" >&2
|
||||
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() {
|
||||
state=$1
|
||||
message=$2
|
||||
@@ -101,7 +179,8 @@ secure_release_tree() {
|
||||
chown -R root:"$SERVICE_GROUP" "$release_dir" || return 1
|
||||
find "$release_dir" -type d -exec chmod 0750 {} + || return 1
|
||||
find "$release_dir" -type f -exec chmod 0640 {} + || return 1
|
||||
chmod 0750 "$release_dir/ops/update.sh"
|
||||
chmod 0750 "$release_dir/ops/update.sh" "$release_dir/ops/baota-start.sh"
|
||||
[ ! -f "$release_dir/ops/baota-init.sh" ] || chmod 0750 "$release_dir/ops/baota-init.sh"
|
||||
}
|
||||
|
||||
verify_release_access() {
|
||||
@@ -110,12 +189,16 @@ verify_release_access() {
|
||||
runuser -u "$SERVICE_USER" -- sh -c \
|
||||
'cd "$1" && test -r app.jar && test -r public/index.html' sh "$release_dir" \
|
||||
|| return 1
|
||||
runuser -u "$SERVICE_USER" -- test -x "$APP_ROOT/runtime/java/bin/java" \
|
||||
|| return 1
|
||||
runuser -u "$SERVICE_USER" -- "$APP_ROOT/runtime/java/bin/java" -version >/dev/null 2>&1
|
||||
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
|
||||
fi
|
||||
}
|
||||
|
||||
prepare_update_layout
|
||||
load_runtime_database_env
|
||||
|
||||
# shellcheck disable=SC2329 # Invoked by the EXIT trap below.
|
||||
cleanup() {
|
||||
@@ -203,18 +286,103 @@ wait_for_health() {
|
||||
}
|
||||
|
||||
verify_app_surface() {
|
||||
expected_release=${1:-}
|
||||
wait_for_health "$HEALTH_URL" || return 1
|
||||
index_file=$(mktemp "$STATE_ROOT/work/public-index.XXXXXX")
|
||||
if curl -fsS "$APP_INDEX_URL" -o "$index_file" \
|
||||
&& grep -Eiq '<!doctype|<html' "$index_file"; then
|
||||
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
|
||||
fi
|
||||
rm -f "$index_file"
|
||||
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() {
|
||||
DATABASE_BACKUP=
|
||||
[ "${KAIDI_SKIP_DB_BACKUP:-false}" = "true" ] && return
|
||||
mkdir -p "$BACKUP_ROOT"
|
||||
chmod 0700 "$BACKUP_ROOT"
|
||||
@@ -254,6 +422,7 @@ backup_database() {
|
||||
fi
|
||||
chmod 0600 "$backup_tmp"
|
||||
mv -f "$backup_tmp" "$backup"
|
||||
DATABASE_BACKUP=$backup
|
||||
rm -f "$dump_file"
|
||||
find "$BACKUP_ROOT" -type f -name 'mysql-*.sql.gz' -printf '%T@ %p\n' \
|
||||
| sort -nr | awk 'NR > 5 {sub(/^[^ ]+ /, ""); print}' | xargs -r rm -f
|
||||
@@ -272,6 +441,22 @@ atomic_install() {
|
||||
mv -f "$install_temporary" "$install_destination"
|
||||
}
|
||||
|
||||
install_operation() {
|
||||
operation_source=$1
|
||||
operation_destination=$2
|
||||
operation_mode=$3
|
||||
operation_is_systemd=$4
|
||||
if [ -f "$operation_destination" ] && [ ! -L "$operation_destination" ] \
|
||||
&& cmp -s "$operation_source" "$operation_destination"; then
|
||||
chmod "$operation_mode" "$operation_destination"
|
||||
return
|
||||
fi
|
||||
atomic_install "$operation_source" "$operation_destination" "$operation_mode" || return 1
|
||||
if [ "$operation_is_systemd" = true ]; then
|
||||
SYSTEMD_OPERATIONS_CHANGED=true
|
||||
fi
|
||||
}
|
||||
|
||||
backup_managed_file() {
|
||||
backup_source=$1
|
||||
backup_name=$2
|
||||
@@ -304,27 +489,35 @@ restore_managed_file() {
|
||||
backup_operations() {
|
||||
OPS_BACKUP="$ACTIVE_TRANSACTION/operations-backup"
|
||||
mkdir -p "$OPS_BACKUP"
|
||||
backup_managed_file "$UPDATER_PATH" update.sh \
|
||||
&& backup_managed_file "$SYSTEMD_ROOT/kaidi-finance.service" kaidi-finance.service \
|
||||
&& backup_managed_file "$SYSTEMD_ROOT/kaidi-update.service" kaidi-update.service \
|
||||
backup_managed_file "$UPDATER_PATH" update.sh || return 1
|
||||
if [ "$PROCESS_MANAGER" = systemd ]; then
|
||||
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
|
||||
}
|
||||
|
||||
apply_operations() {
|
||||
atomic_install "$RELEASE_DIR/ops/update.sh" "$UPDATER_PATH" 0755 \
|
||||
&& atomic_install "$RELEASE_DIR/ops/kaidi-finance.service" \
|
||||
"$SYSTEMD_ROOT/kaidi-finance.service" 0644 \
|
||||
&& atomic_install "$RELEASE_DIR/ops/kaidi-update.service" \
|
||||
"$SYSTEMD_ROOT/kaidi-update.service" 0644 \
|
||||
&& atomic_install "$RELEASE_DIR/ops/kaidi-update.path" \
|
||||
"$SYSTEMD_ROOT/kaidi-update.path" 0644 \
|
||||
&& systemctl daemon-reload
|
||||
SYSTEMD_OPERATIONS_CHANGED=false
|
||||
install_operation "$RELEASE_DIR/ops/update.sh" "$UPDATER_PATH" 0755 false || return 1
|
||||
if [ "$PROCESS_MANAGER" = systemd ]; then
|
||||
install_operation "$RELEASE_DIR/ops/kaidi-finance.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 \
|
||||
&& install_operation "$RELEASE_DIR/ops/kaidi-update.path" \
|
||||
"$SYSTEMD_ROOT/kaidi-update.path" 0644 true \
|
||||
|| return 1
|
||||
[ "$SYSTEMD_OPERATIONS_CHANGED" = false ] || systemctl daemon-reload
|
||||
}
|
||||
|
||||
restore_operations() {
|
||||
restore_failed=0
|
||||
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.path" kaidi-update.path || restore_failed=1
|
||||
systemctl daemon-reload || restore_failed=1
|
||||
@@ -357,28 +550,35 @@ rollback_active_transaction() {
|
||||
previous_target=$(transaction_value previous-target)
|
||||
failed_release=$(transaction_value release-dir)
|
||||
OPS_BACKUP="$ACTIVE_TRANSACTION/operations-backup"
|
||||
rollback_ok=true
|
||||
link_restored=true
|
||||
operations_restored=true
|
||||
application_restarted=true
|
||||
|
||||
systemctl stop "$SERVICE_NAME" || true
|
||||
if [ -n "$previous_target" ] && [ -d "$previous_target" ]; then
|
||||
if ! ln -sfn "$previous_target" "$APP_ROOT/current.next" \
|
||||
|| ! mv -Tf "$APP_ROOT/current.next" "$APP_ROOT/current"; then
|
||||
rollback_ok=false
|
||||
link_restored=false
|
||||
fi
|
||||
else
|
||||
rollback_ok=false
|
||||
link_restored=false
|
||||
fi
|
||||
restore_operations || rollback_ok=false
|
||||
systemctl reset-failed "$SERVICE_NAME" >/dev/null 2>&1 || true
|
||||
systemctl start "$SERVICE_NAME" || rollback_ok=false
|
||||
if [ "$rollback_ok" = true ] && verify_app_surface; then
|
||||
restore_operations || operations_restored=false
|
||||
restart_application true || application_restarted=false
|
||||
if [ "$link_restored" = true ] && [ "$application_restarted" = true ] \
|
||||
&& verify_app_surface "$previous_target"; then
|
||||
remove_failed_release "$failed_release"
|
||||
rm -rf "$ACTIVE_TRANSACTION"
|
||||
fail "$reason; previous release was restored and verified"
|
||||
if [ "$operations_restored" = true ]; then
|
||||
rm -rf "$ACTIVE_TRANSACTION"
|
||||
fail "$reason; previous application release was restored and verified; database backup was retained"
|
||||
fi
|
||||
fail "$reason; previous application release is running, but operations restoration requires manual review; transaction evidence was retained"
|
||||
fi
|
||||
TERMINAL_STATUS_WRITTEN=true
|
||||
status FAILED "$reason; rollback is incomplete and will be retried" "${TARGET_VERSION:-}"
|
||||
printf '%s\n' "$reason; rollback is incomplete and will be retried" >&2
|
||||
status FAILED "$reason; rollback is incomplete and manual recovery is required" "${TARGET_VERSION:-}"
|
||||
if ! archive_processing_request; then
|
||||
printf '%s\n' "Update request could not be archived after an incomplete rollback" >&2
|
||||
fi
|
||||
printf '%s\n' "$reason; rollback is incomplete and manual recovery is required" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
@@ -404,6 +604,11 @@ recover_interrupted_transaction() {
|
||||
|
||||
validate_release_operations() {
|
||||
sh -n "$WORK_DIR/extracted/ops/update.sh" || return 1
|
||||
bash -n "$WORK_DIR/extracted/ops/baota-start.sh" || return 1
|
||||
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
|
||||
systemd-analyze verify \
|
||||
"$WORK_DIR/extracted/ops/kaidi-finance.service" \
|
||||
@@ -529,6 +734,9 @@ fi
|
||||
[ -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"
|
||||
[ -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-update.service" ] || fail "Release updater unit is missing"
|
||||
[ -s "$WORK_DIR/extracted/ops/kaidi-update.path" ] || fail "Release updater path unit is missing"
|
||||
@@ -579,6 +787,7 @@ write_transaction_value phase PREPARED
|
||||
|
||||
status BACKING_UP "Backing up database before installing $TARGET_VERSION" "$TARGET_VERSION"
|
||||
backup_database
|
||||
[ -z "${DATABASE_BACKUP:-}" ] || write_transaction_value database-backup "$DATABASE_BACKUP"
|
||||
if ! backup_operations; then
|
||||
remove_failed_release "$RELEASE_DIR"
|
||||
rm -rf "$ACTIVE_TRANSACTION"
|
||||
@@ -590,7 +799,6 @@ apply_operations || rollback_active_transaction "Release operations files could
|
||||
write_transaction_value phase OPS_APPLIED
|
||||
|
||||
write_transaction_value phase APP_SWITCHING
|
||||
systemctl stop "$SERVICE_NAME" || rollback_active_transaction "Application service could not be stopped"
|
||||
if ! ln -sfn "$RELEASE_DIR" "$APP_ROOT/current.next" \
|
||||
|| ! mv -Tf "$APP_ROOT/current.next" "$APP_ROOT/current"; then
|
||||
rollback_active_transaction "Release application link could not be activated"
|
||||
@@ -599,16 +807,23 @@ write_transaction_value phase APP_SWITCHED
|
||||
|
||||
status RUNNING "Starting and verifying release $TARGET_VERSION" "$TARGET_VERSION"
|
||||
write_transaction_value phase HEALTH_CHECKING
|
||||
systemctl reset-failed "$SERVICE_NAME" >/dev/null 2>&1 || true
|
||||
if systemctl start "$SERVICE_NAME" \
|
||||
&& systemctl is-active --quiet kaidi-update.path \
|
||||
&& verify_app_surface; then
|
||||
if ! restart_application; then
|
||||
rollback_active_transaction "Release application restart failed"
|
||||
fi
|
||||
if verify_app_surface "$RELEASE_DIR"; then
|
||||
success_message="Release $TARGET_VERSION is running"
|
||||
if ! systemctl start "$UPDATE_PATH_NAME" >/dev/null 2>&1; then
|
||||
success_message="$success_message; automatic update watcher could not be started"
|
||||
fi
|
||||
write_transaction_value phase COMMITTED
|
||||
status SUCCEEDED "Release $TARGET_VERSION is running" "$TARGET_VERSION"
|
||||
status SUCCEEDED "$success_message" "$TARGET_VERSION"
|
||||
TERMINAL_STATUS_WRITTEN=true
|
||||
complete_request
|
||||
rm -rf "$ACTIVE_TRANSACTION"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
rollback_active_transaction "Release health or application-surface verification failed"
|
||||
service_state=$(application_state 2>/dev/null || true)
|
||||
[ -n "$service_state" ] || service_state=unknown
|
||||
rollback_active_transaction \
|
||||
"Release health or application-surface verification failed (service state: $service_state)"
|
||||
|
||||
+17
-6
@@ -637,7 +637,7 @@ OA 导入路线不重新判断原 OA 的业务审批结论,但财务金额、
|
||||
|
||||
### 7.3 Starter 菜单、路由与页面映射
|
||||
|
||||
登录后的整体框架直接使用 TDesign Starter 的左侧菜单、顶部栏、面包屑和内容区。菜单名称、顺序和路由在第一轮固定如下;详情、编辑等隐藏路由不出现在左侧菜单,但必须保留面包屑和返回路径。
|
||||
登录后的整体框架直接使用 TDesign Starter 的左侧菜单、顶部栏、页签和内容区。菜单层级已经表达业务归属,首期不在内容区显示面包屑,避免与菜单和页签重复;详情、编辑等隐藏路由必须保留安全返回路径。
|
||||
|
||||
| 一级菜单/入口 | 二级菜单 | 页面 | 路由 | 默认可见身份 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
@@ -664,7 +664,7 @@ OA 导入路线不重新判断原 OA 的业务审批结论,但财务金额、
|
||||
| 系统治理 | 权限与配置 | PAGE-21 | `/governance/settings` | 系统管理员 |
|
||||
| 系统治理 | 系统更新 | PAGE-22 | `/governance/update` | 系统管理员 |
|
||||
|
||||
本地路由表固定声明组件映射,后端只返回当前身份、权限码和数据范围,前端据此过滤菜单和按钮;后端不得下发任意组件文件路径。业务页面路由的 `meta` 至少包含 `pageId`、`pageType`、`pageTemplate`、`title`、`permission`、`requiresAuth` 和面包屑信息;出现在菜单中的路由组沿用 Starter 的 `orderNo` 排序字段,不再另造 `menuOrder`。S0 页面追溯表还要逐路由冻结本地 `componentPath`、默认入口、URL query 白名单、首个读取 operationId(没有接口时明确记 `N/A` 和原因)及证据 ID;菜单、路由和页面不得出现三份不同配置。
|
||||
本地路由表固定声明组件映射,后端只返回当前身份、权限码和数据范围,前端据此过滤菜单和按钮;后端不得下发任意组件文件路径。业务页面路由的 `meta` 至少包含 `pageId`、`pageType`、`pageTemplate`、`title`、`permission` 和 `requiresAuth`;历史 `breadcrumb` 字段可保留作路由合同和返回路径元数据,但不得渲染为可见导航。出现在菜单中的路由组沿用 Starter 的 `orderNo` 排序字段,不再另造 `menuOrder`。S0 页面追溯表还要逐路由冻结本地 `componentPath`、默认入口、URL query 白名单、首个读取 operationId(没有接口时明确记 `N/A` 和原因)及证据 ID;菜单、路由和页面不得出现三份不同配置。
|
||||
|
||||
同一账号切换身份时必须清空当前身份的页签、页面缓存、待办查询和按钮权限,再加载新身份菜单;不能沿用上一个身份打开的详情页。手工输入隐藏路由或无权路由时统一进入 Starter 的 403 页面,后端接口同时返回 `403` 并写审计日志。
|
||||
|
||||
@@ -1625,12 +1625,12 @@ Pull Request 只有同时满足以下条件才能合并:页面/操作合同完
|
||||
| 区域 | 固定规则 |
|
||||
| --- | --- |
|
||||
| 登录/身份选择 | PAGE-01、PAGE-02 使用 Starter `BlankLayout`;保留 TDesign 登录视觉结构,使用甲方本地品牌素材,不显示业务侧栏 |
|
||||
| 登录后框架 | PAGE-03~PAGE-22 固定使用 Starter `side` 布局:左侧菜单、顶部栏、面包屑、页签栏和内容区 |
|
||||
| 登录后框架 | PAGE-03~PAGE-22 固定使用 Starter `side` 布局:左侧菜单、顶部栏、页签栏和内容区;不渲染面包屑 |
|
||||
| 左侧菜单 | 使用 7.3 节固定层级和顺序;支持 Starter 默认展开/收起;一级菜单图标来自 TDesign Icons,名称不得由开发人员自行缩写 |
|
||||
| 顶部栏 | 显示当前身份、身份切换、系统通知和用户菜单;删除代码仓库、外部帮助、语言切换、主题切换和布局设置入口 |
|
||||
| 面包屑 | 以 `routes.json` 页面合同为唯一层级来源,从一级菜单到当前页面完整显示;隐藏详情路由必须显示来源列表和当前页面,任一级可安全返回 |
|
||||
| 面包屑 | 首期删除可见面包屑;菜单和页签承担层级识别,隐藏详情路由通过显式返回按钮或安全返回路径返回来源列表 |
|
||||
| 页签栏 | 保留 Starter 路由页签;工作台固定不可关闭,详情页签显示“页面名 + 业务编号”;切换身份时清空旧身份页签和缓存 |
|
||||
| 内容头 | 静态列表、工作台、台账和配置页只显示顶部面包屑,不再在内容区重复模块名、页面名或功能说明;保留读屏可识别的隐藏 `h1`,刷新、新建、导入、导出等动作进入右侧紧凑工具栏。项目详情、表单详情等动态页面可以显示业务编号、对象名称和状态,但不得重复面包屑中的静态标题 |
|
||||
| 内容头 | 静态列表、工作台、台账和配置页直接进入紧凑操作栏、查询区或指标区,不显示面包屑,也不重复堆叠模块名、页面名和功能说明;保留读屏可识别的隐藏 `h1`,刷新、新建、导入、导出等动作进入右侧紧凑工具栏。项目详情、表单详情等动态页面显示业务编号、对象名称和状态,并提供明确返回路径 |
|
||||
| 页面滚动 | 页面主体纵向滚动;宽表格只在表格区域横向滚动;弹窗、抽屉和固定操作栏不得覆盖页面标题或分页 |
|
||||
|
||||
主题固定为 TDesign 浅色模式和已确认的品牌主色;首期不交付深色模式、多语言、顶部导航、混合导航或用户自定义主题。允许本地保存的只有菜单收起、非敏感表格列显示等界面偏好;身份、权限、业务记录、查询结果和敏感字段不得持久化到浏览器。
|
||||
@@ -1643,7 +1643,7 @@ PAGE-01~PAGE-22 必须从以下模板组合,不允许每个开发人员各
|
||||
| --- | --- | --- | --- |
|
||||
| 公共入口 | PAGE-01~02 | 本地品牌区 + 登录表单或三张身份卡 + 明确错误反馈 | `Form`、`Input`、`Button`、`Alert`、`Loading` |
|
||||
| 工作台 | PAGE-04~06 | 紧凑指标行 + 待办/异常主列表 + 常用入口 + 最近动态;下一屏内容在首屏可见 | `Statistic`、`Table`、`Tabs`、`Tag`、`Timeline`、必要的 `Card` |
|
||||
| 列表/台账 | PAGE-07、09、13~16、18~20 | 合同面包屑 + 紧凑操作栏 + 查询栏 + 数据表格 + 分页;内容区不重复静态页名,详情通过路由或抽屉进入 | `Form`、`Input`、`Select`、`DateRangePicker`、`Table`、`Pagination`、`Dropdown` |
|
||||
| 列表/台账 | PAGE-07、09、13~16、18~20 | 紧凑操作栏 + 查询栏 + 数据表格 + 分页;不额外渲染面包屑或重复静态页名,详情通过路由或抽屉进入 | `Form`、`Input`、`Select`、`DateRangePicker`、`Table`、`Pagination`、`Dropdown` |
|
||||
| 业务详情 | PAGE-08 | 项目摘要 + 状态/主操作 + 页签 + 描述信息/明细表/时间线;跨对象都有返回路径 | `Descriptions`、`Tabs`、`Table`、`Timeline`、`Tag`、`Drawer` |
|
||||
| 录入/导入 | PAGE-10、PAGE-12 的编辑状态 | 分组表单 + 动态明细 + 附件 + 校验结果 + 底部固定操作栏 | `Form`、`Row`、`Col`、`Input`/`MoneyInput`、仅整数 `InputNumber`、`Select`、`DatePicker`、`Upload`、`Alert` |
|
||||
| 审批处理 | PAGE-11、PAGE-17 | 待办列表 + 业务摘要 + 附件/校验 + 审批时间线 + 当前节点动作 | `Table`、`Descriptions`、`Steps`、`Timeline`、`Dialog`、`Textarea` |
|
||||
@@ -3005,6 +3005,17 @@ PAGE-16 全类回归曾暴露测试证据文件路径硬编码与应用 `finance
|
||||
|
||||
**本轮结论**:Preview.8 具备可部署的首次安装和 32 位向导框架;生产录入安装码、数据库密码和管理员密码前必须先配置 HTTPS 反向代理或可信内网隧道,HTTP 仅用于隔离 Preview 验证。
|
||||
|
||||
### 13.1.17 Preview.27 安装跳转与内容区导航收敛记录(2026-08-18)
|
||||
|
||||
| 项目 | 结果 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 安装完成跳转 | 已修复 | 完成安装后以 `required=false` 且 `locked/ready` 状态确认服务已切换,再通过 Vue Router 进入 `/login`;不再等待永远为 `false` 的 setup `ready` 标志,也不再复用安装前缓存的 `required=true` 状态 |
|
||||
| 面包屑 | 已删除可见渲染 | 菜单和路由页签已经表达层级,业务内容区不再显示“表单审批 → 表单中心”等重复导航;历史 `breadcrumb` 合同字段仅保留作元数据,不生成 DOM |
|
||||
| 内容区留白 | 已收紧 | 移除面包屑占位和底部间距,页签下内容区顶部间距固定为 16px;列表、工作台和配置页直接进入操作栏/指标/查询区 |
|
||||
| 自动化证据 | 通过 | TypeScript、ESLint、Stylelint、Vitest `12 files / 60 tests`、安装向导 Playwright `6/6`、全量页面可访问性与布局 Playwright `78/78`、production build/hygiene 均通过 |
|
||||
|
||||
**本轮结论**:业务页面不再把菜单层级重复显示为面包屑;安装完成后会稳定进入登录页,异常重启场景也通过状态确认后自动跳转。
|
||||
|
||||
### 13.2 第三方权限和自动化负向验收
|
||||
|
||||
**当前状态**:`PENDING_EVIDENCE`。以下五项是发布前必须执行并归档的负向验收,不代表当前已经完成;证据包须记录代码/依赖/配置扫描、断网运行、人工登记审计断言、执行时间、版本和可复查结果。
|
||||
|
||||
@@ -260,13 +260,7 @@ for (const route of routes) {
|
||||
await expect(page.locator('h1.page-title-sr-only')).toHaveCount(1);
|
||||
await expect(page.locator('h1:not(.page-title-sr-only)')).toHaveCount(0);
|
||||
}
|
||||
if (!['PAGE-01', 'PAGE-02', 'PAGE-23'].includes(route.pageId)) {
|
||||
const breadcrumb = page.locator('.tdesign-breadcrumb');
|
||||
await expect(breadcrumb).toBeVisible();
|
||||
for (const label of route.breadcrumb) {
|
||||
await expect(breadcrumb.getByText(label, { exact: true })).toBeVisible();
|
||||
}
|
||||
}
|
||||
await expect(page.locator('.tdesign-breadcrumb')).toHaveCount(0);
|
||||
await page.waitForLoadState('networkidle');
|
||||
if (route.pageId === 'PAGE-09') {
|
||||
await expect(page.locator('.t-input-number__decrease, .t-input-number__increase')).toHaveCount(0);
|
||||
|
||||
@@ -11,7 +11,9 @@ test('first-run wizard tests MySQL, creates the administrator and stays responsi
|
||||
data: {
|
||||
required: !completed,
|
||||
locked: completed,
|
||||
ready: completed,
|
||||
// Production status is locked after setup; readiness is not the
|
||||
// completion signal used by the redirect flow.
|
||||
ready: false,
|
||||
supportedDatabaseTypes: ['MYSQL'],
|
||||
message: completed ? '系统已完成安装' : '请完成安装',
|
||||
},
|
||||
@@ -69,6 +71,8 @@ test('first-run wizard tests MySQL, creates the administrator and stays responsi
|
||||
await expect(page.getByText('安装完成', { exact: true })).toBeVisible();
|
||||
await expect(page.getByText('安装完成,系统正在切换到正式模式')).toBeVisible();
|
||||
|
||||
await expect(page).toHaveURL(/\/login$/, { timeout: 15_000 });
|
||||
|
||||
const layout = await page.evaluate(() => ({
|
||||
clientWidth: document.documentElement.clientWidth,
|
||||
scrollWidth: document.documentElement.scrollWidth,
|
||||
@@ -128,8 +132,8 @@ test('confirms completion after the setup response is interrupted by a service r
|
||||
await page.getByRole('button', { name: '完成安装' }).click();
|
||||
await page.getByRole('button', { name: '确定', exact: true }).click();
|
||||
|
||||
await expect(page.getByText('连接暂时中断,正在确认安装结果。')).toBeVisible();
|
||||
await expect(page.getByText('安装完成', { exact: true })).toBeVisible();
|
||||
await expect(page.getByText('安装已完成,系统正在切换到正式模式')).toBeVisible();
|
||||
await expect(page.getByText('网络请求失败')).toHaveCount(0);
|
||||
await expect(page).toHaveURL(/\/login$/, { timeout: 15_000 });
|
||||
});
|
||||
|
||||
@@ -178,6 +178,16 @@ test('system administrator checks and queues a signed online update', async ({ p
|
||||
expect(installBodies).toEqual([{ version: '1.0.0-preview.2', reason: '管理员确认立即重启' }]);
|
||||
});
|
||||
|
||||
test('brand logo returns the active identity to its workbench', async ({ page }) => {
|
||||
await installFixture(page, [], []);
|
||||
await page.goto('/governance/update');
|
||||
|
||||
await page.locator('.brand-logo').click();
|
||||
|
||||
await expect(page).toHaveURL(/\/governance\/settings$/);
|
||||
await expect(page.getByText('选择工作身份', { exact: true })).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('disabled online updates expose status without an executable check action', async ({ page }) => {
|
||||
await installFixture(page, [], [], false);
|
||||
await page.goto('/governance/update');
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
export default {
|
||||
showFooter: false,
|
||||
isSidebarCompact: false,
|
||||
showBreadcrumb: true,
|
||||
menuAutoCollapsed: false,
|
||||
mode: 'light',
|
||||
layout: 'side',
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div :class="layoutCls">
|
||||
<t-head-menu :class="menuCls" :theme="menuTheme" expand-type="popup" :value="active">
|
||||
<template #logo>
|
||||
<span v-if="showLogo" class="header-logo-container" @click="handleNav('/role-select')">
|
||||
<span v-if="showLogo" class="header-logo-container" @click="handleNav(user.workbenchRoute)">
|
||||
<span class="header-brand">财务系统</span>
|
||||
</span>
|
||||
<div v-else class="header-operate-left">
|
||||
|
||||
@@ -61,7 +61,6 @@
|
||||
</t-tab-panel>
|
||||
</t-tabs>
|
||||
<t-content :class="`${prefix}-content-layout`">
|
||||
<l-breadcrumb v-if="settingStore.showBreadcrumb" />
|
||||
<l-content />
|
||||
</t-content>
|
||||
<t-footer v-if="settingStore.showFooter" :class="`${prefix}-footer-layout`">
|
||||
@@ -81,7 +80,6 @@ import { useLocale } from '@/locales/useLocale';
|
||||
import { useSettingStore, useTabsRouterStore } from '@/store';
|
||||
import type { TRouterInfo, TTabRemoveOptions } from '@/types/interface';
|
||||
|
||||
import LBreadcrumb from './Breadcrumb.vue';
|
||||
import LContent from './Content.vue';
|
||||
import LFooter from './Footer.vue';
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ import { useRouter } from 'vue-router';
|
||||
import { prefix } from '@/config/global';
|
||||
import { t } from '@/locales';
|
||||
import { getActive } from '@/router';
|
||||
import { useSettingStore } from '@/store';
|
||||
import { useSettingStore, useUserStore } from '@/store';
|
||||
import type { MenuRoute, ModeType } from '@/types/interface';
|
||||
|
||||
import pgk from '../../../package.json';
|
||||
@@ -155,6 +155,7 @@ const menuCls = computed(() => {
|
||||
|
||||
const router = useRouter();
|
||||
const settingStore = useSettingStore();
|
||||
const userStore = useUserStore();
|
||||
|
||||
const autoCollapsed = () => {
|
||||
const isCompact = window.innerWidth <= MIN_POINT;
|
||||
@@ -175,7 +176,7 @@ onUnmounted(() => {
|
||||
});
|
||||
|
||||
const goHome = () => {
|
||||
router.push('/role-select');
|
||||
router.push(userStore.workbenchRoute);
|
||||
};
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
|
||||
@@ -73,16 +73,7 @@ const choose = async (roleCode: string) => {
|
||||
try {
|
||||
await userStore.selectRole(roleCode);
|
||||
permissionStore.initRoutes();
|
||||
const target = userStore.roles.find((role) => role.code === roleCode)?.workbenchRoute;
|
||||
const fallback =
|
||||
roleCode === 'PROJECT_MANAGER'
|
||||
? '/workbench/project'
|
||||
: roleCode === 'ARCHIVE_MANAGER'
|
||||
? '/workbench/archive'
|
||||
: roleCode === 'SYSTEM_ADMIN'
|
||||
? '/governance/settings'
|
||||
: '/workbench/finance';
|
||||
await router.replace(target || fallback);
|
||||
await router.replace(userStore.workbenchRoute);
|
||||
} catch (error) {
|
||||
MessagePlugin.error((error as Error).message || '身份选择失败,请重试');
|
||||
} finally {
|
||||
|
||||
@@ -355,7 +355,7 @@ async function waitForSetupCompletion() {
|
||||
if (unmounted) return false;
|
||||
try {
|
||||
const status = await getSetupStatus();
|
||||
if (status.ready) return true;
|
||||
if (!status.required && (status.ready || status.locked)) return true;
|
||||
} catch {
|
||||
// A short connection failure is expected while systemd restarts the service.
|
||||
}
|
||||
@@ -387,7 +387,7 @@ async function openLogin() {
|
||||
window.clearInterval(countdownTimer);
|
||||
completionMessage.value = '系统正在启动,请稍候。';
|
||||
if (await waitForSetupCompletion()) {
|
||||
window.location.assign('/login');
|
||||
await router.replace('/login');
|
||||
return;
|
||||
}
|
||||
completionMessage.value = '系统启动时间较长,请稍后刷新页面。';
|
||||
|
||||
@@ -17,7 +17,20 @@ const isPublic = (to: RouteLocationNormalized) =>
|
||||
let setupStatusRequest: ReturnType<typeof getSetupStatus> | null = null;
|
||||
|
||||
function loadSetupStatus() {
|
||||
setupStatusRequest ||= getSetupStatus();
|
||||
if (setupStatusRequest) return setupStatusRequest;
|
||||
|
||||
// Share only an in-flight request. Keeping the resolved setup state would
|
||||
// send the user back to /setup after the installation marker is written.
|
||||
const request = getSetupStatus();
|
||||
setupStatusRequest = request;
|
||||
request.then(
|
||||
() => {
|
||||
if (setupStatusRequest === request) setupStatusRequest = null;
|
||||
},
|
||||
() => {
|
||||
if (setupStatusRequest === request) setupStatusRequest = null;
|
||||
},
|
||||
);
|
||||
return setupStatusRequest;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import * as authApi from '@/api/auth';
|
||||
import { usePermissionStore } from '@/store';
|
||||
|
||||
import { hasPermissionAccess } from './menu-access';
|
||||
import { resolveWorkbenchRoute } from './workbench-route';
|
||||
|
||||
const emptyUser: UserView = {
|
||||
publicId: '',
|
||||
@@ -28,6 +29,7 @@ export const useUserStore = defineStore('user', {
|
||||
getters: {
|
||||
displayName: (state) => state.user.displayName || state.user.username,
|
||||
roleName: (state) => state.roles.find((role) => role.code === state.currentRole)?.name || '',
|
||||
workbenchRoute: (state) => resolveWorkbenchRoute(state.currentRole, state.roles),
|
||||
hasRole: (state) => (roleCode: string) => state.roles.some((role) => role.code === roleCode),
|
||||
hasPermission: (state) => (permission: string) =>
|
||||
hasPermissionAccess(state.currentRole, state.permissions, permission),
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { RoleView } from '@/api/auth';
|
||||
|
||||
const fallbackWorkbenchRoutes: Record<string, string> = {
|
||||
PROJECT_MANAGER: '/workbench/project',
|
||||
FINANCE_MANAGER: '/workbench/finance',
|
||||
ARCHIVE_MANAGER: '/workbench/archive',
|
||||
SYSTEM_ADMIN: '/governance/settings',
|
||||
};
|
||||
|
||||
export function resolveWorkbenchRoute(currentRole: string | null, roles: RoleView[]): string {
|
||||
const configured = roles.find((role) => role.code === currentRole)?.workbenchRoute?.trim();
|
||||
return configured || fallbackWorkbenchRoutes[currentRole || ''] || '/role-select';
|
||||
}
|
||||
@@ -87,7 +87,7 @@
|
||||
z-index: 100;
|
||||
}
|
||||
&-tabs-nav + .@{starter-prefix}-content-layout {
|
||||
padding-top: var(--td-comp-paddingTB-xxl);
|
||||
padding-top: var(--td-comp-paddingTB-l);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import type { RoleView } from '../src/api/auth';
|
||||
import { resolveWorkbenchRoute } from '../src/store/modules/workbench-route';
|
||||
|
||||
const roles: RoleView[] = [
|
||||
{ code: 'SYSTEM_ADMIN', name: '系统管理员', workbenchRoute: '/governance/settings' },
|
||||
{ code: 'FINANCE_MANAGER', name: '财务管理人员', workbenchRoute: '/workbench/finance' },
|
||||
];
|
||||
|
||||
describe('active identity workbench route', () => {
|
||||
it('uses the route supplied for the active role', () => {
|
||||
expect(resolveWorkbenchRoute('SYSTEM_ADMIN', roles)).toBe('/governance/settings');
|
||||
expect(resolveWorkbenchRoute('FINANCE_MANAGER', roles)).toBe('/workbench/finance');
|
||||
});
|
||||
|
||||
it('falls back to the built-in role workbench when the server route is absent', () => {
|
||||
expect(resolveWorkbenchRoute('PROJECT_MANAGER', [])).toBe('/workbench/project');
|
||||
expect(resolveWorkbenchRoute('ARCHIVE_MANAGER', [])).toBe('/workbench/archive');
|
||||
});
|
||||
|
||||
it('keeps role selection for sessions without an active identity', () => {
|
||||
expect(resolveWorkbenchRoute(null, roles)).toBe('/role-select');
|
||||
});
|
||||
});
|
||||
@@ -91,12 +91,16 @@ cp "$JAR" "$STAGE/app.jar"
|
||||
cp -R "$ROOT/frontend/dist/." "$STAGE/public/"
|
||||
printf '%s\n' "$VERSION" > "$STAGE/VERSION"
|
||||
cp "$ROOT/deploy/update.sh" "$STAGE/ops/update.sh"
|
||||
cp "$ROOT/deploy/baota-start.sh" "$STAGE/ops/baota-start.sh"
|
||||
cp "$ROOT/deploy/baota-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"
|
||||
# Kept in the archive so Preview.9's updater can complete the one-time transition.
|
||||
cp "$ROOT/deploy/nginx/kaidi-finance.conf" "$STAGE/ops/kaidi-finance.conf"
|
||||
cp "$ROOT/deploy/systemd/kaidi-finance.service" "$STAGE/ops/kaidi-finance.service"
|
||||
cp "$ROOT/deploy/systemd/kaidi-update.service" "$STAGE/ops/kaidi-update.service"
|
||||
cp "$ROOT/deploy/systemd/kaidi-update.path" "$STAGE/ops/kaidi-update.path"
|
||||
chmod 0755 "$STAGE/ops/update.sh"
|
||||
chmod 0755 "$STAGE/ops/update.sh" "$STAGE/ops/baota-start.sh" "$STAGE/ops/baota-init.sh"
|
||||
|
||||
ARTIFACT="kaidi-finance-$VERSION.tar.gz"
|
||||
COPYFILE_DISABLE=1 tar --format=ustar -czf "$OUTPUT_DIR/$ARTIFACT" -C "$STAGE" .
|
||||
@@ -141,8 +145,10 @@ else
|
||||
fi
|
||||
|
||||
install -m 0755 "$ROOT/deploy/install.sh" "$OUTPUT_DIR/install.sh"
|
||||
install -m 0755 "$ROOT/deploy/purge.sh" "$OUTPUT_DIR/purge.sh"
|
||||
PUBLIC_KEY_SHA256=$(sha256_file "$OUTPUT_DIR/release-public.pem")
|
||||
INSTALLER_SHA256=$(sha256_file "$OUTPUT_DIR/install.sh")
|
||||
PURGER_SHA256=$(sha256_file "$OUTPUT_DIR/purge.sh")
|
||||
if [ -n "$TRUSTED_PUBLIC_KEY_SHA256" ]; then
|
||||
NORMALIZED_TRUSTED_PUBLIC_KEY_SHA256=$(printf '%s' "$TRUSTED_PUBLIC_KEY_SHA256" | tr '[:upper:]' '[:lower:]')
|
||||
[[ "$NORMALIZED_TRUSTED_PUBLIC_KEY_SHA256" =~ ^[0-9a-f]{64}$ ]] \
|
||||
@@ -156,6 +162,7 @@ fi
|
||||
cat > "$OUTPUT_DIR/bootstrap-checksums.txt" <<EOF
|
||||
KAIDI_RELEASE_PUBLIC_KEY_SHA256=$PUBLIC_KEY_SHA256
|
||||
KAIDI_INSTALLER_SHA256=$INSTALLER_SHA256
|
||||
KAIDI_PURGER_SHA256=$PURGER_SHA256
|
||||
EOF
|
||||
BOOTSTRAP_SHA256=$(sha256_file "$OUTPUT_DIR/bootstrap-checksums.txt")
|
||||
|
||||
@@ -163,7 +170,8 @@ RELEASE_NOTES_VALUE=${KAIDI_RELEASE_NOTES:-"Kaidi Finance Preview $VERSION"}
|
||||
VERSION="$VERSION" ARTIFACT="$ARTIFACT" SHA256="$SHA256" RELEASE_NOTES="$RELEASE_NOTES_VALUE" \
|
||||
BACKEND_SBOM_SHA256="$BACKEND_SBOM_SHA256" FRONTEND_SBOM_SHA256="$FRONTEND_SBOM_SHA256" \
|
||||
BACKEND_BUILD_VERSION="$BACKEND_BUILD_VERSION" FRONTEND_BUILD_VERSION="$FRONTEND_BUILD_VERSION" \
|
||||
INSTALLER_SHA256="$INSTALLER_SHA256" PUBLIC_KEY_SHA256="$PUBLIC_KEY_SHA256" \
|
||||
INSTALLER_SHA256="$INSTALLER_SHA256" PURGER_SHA256="$PURGER_SHA256" \
|
||||
PUBLIC_KEY_SHA256="$PUBLIC_KEY_SHA256" \
|
||||
BOOTSTRAP_SHA256="$BOOTSTRAP_SHA256" SOURCE_REVISION="$SOURCE_REVISION" SOURCE_REF="$SOURCE_REF" \
|
||||
SOURCE_DIRTY="$SOURCE_DIRTY" \
|
||||
node <<'NODE' > "$OUTPUT_DIR/release-manifest.json"
|
||||
@@ -190,6 +198,8 @@ const manifest = {
|
||||
bootstrap: {
|
||||
installer: 'install.sh',
|
||||
installerSha256: process.env.INSTALLER_SHA256,
|
||||
purger: 'purge.sh',
|
||||
purgerSha256: process.env.PURGER_SHA256,
|
||||
publicKey: 'release-public.pem',
|
||||
publicKeySha256: process.env.PUBLIC_KEY_SHA256,
|
||||
checksums: 'bootstrap-checksums.txt',
|
||||
@@ -209,14 +219,15 @@ fi
|
||||
cd "$OUTPUT_DIR"
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
sha256sum "$ARTIFACT" backend-sbom.cdx.json frontend-sbom.cdx.json \
|
||||
release-manifest.json release-manifest.sig release-public.pem install.sh \
|
||||
release-manifest.json release-manifest.sig release-public.pem install.sh purge.sh \
|
||||
bootstrap-checksums.txt > SHA256SUMS
|
||||
else
|
||||
shasum -a 256 "$ARTIFACT" backend-sbom.cdx.json frontend-sbom.cdx.json \
|
||||
release-manifest.json release-manifest.sig release-public.pem install.sh \
|
||||
release-manifest.json release-manifest.sig release-public.pem install.sh purge.sh \
|
||||
bootstrap-checksums.txt > SHA256SUMS
|
||||
fi
|
||||
)
|
||||
printf 'Release assets created in %s\n' "$OUTPUT_DIR"
|
||||
printf 'Trusted release public-key SHA-256: %s\n' "$PUBLIC_KEY_SHA256"
|
||||
printf 'Installer SHA-256: %s\n' "$INSTALLER_SHA256"
|
||||
printf 'Purger SHA-256: %s\n' "$PURGER_SHA256"
|
||||
|
||||
@@ -87,6 +87,7 @@ ASSETS=(
|
||||
release-manifest.sig
|
||||
release-public.pem
|
||||
install.sh
|
||||
purge.sh
|
||||
bootstrap-checksums.txt
|
||||
SHA256SUMS
|
||||
)
|
||||
@@ -124,7 +125,7 @@ jq -n \
|
||||
> "$WORK/publish.json"
|
||||
publish_status=$(request PATCH "$API/releases/$RELEASE_ID" "$WORK/published.json" "$WORK/publish.json")
|
||||
[ "$publish_status" = 200 ] || fail "release publication returned HTTP $publish_status"
|
||||
jq -e '.draft == false and .prerelease == false and (.assets | length) == 9' \
|
||||
jq -e '.draft == false and .prerelease == false and (.assets | length) == 10' \
|
||||
"$WORK/published.json" >/dev/null || fail 'published release state is invalid'
|
||||
|
||||
printf 'Published Gitea release %s with nine verified assets\n' "$TAG"
|
||||
printf 'Published Gitea release %s with ten verified assets\n' "$TAG"
|
||||
|
||||
Executable
+121
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
WORK=$(mktemp -d)
|
||||
EXECUTION_MARKER=/tmp/kaidi-baota-fixture-executed-$$
|
||||
trap 'rm -rf "$WORK" "$EXECUTION_MARKER"' EXIT
|
||||
|
||||
fail() {
|
||||
printf 'Baota start fixture failed: %s\n' "$1" >&2
|
||||
[ ! -f "$WORK/java.log" ] || sed -n '1,40p' "$WORK/java.log" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
RELEASE="$WORK/release"
|
||||
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 '<!doctype html>\n' > "$RELEASE/public/index.html"
|
||||
printf '1.0.0-preview.25\n' > "$RELEASE/VERSION"
|
||||
cp "$ROOT/deploy/baota-start.sh" "$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'
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
{
|
||||
printf 'cwd=%s\n' "$PWD"
|
||||
printf 'args=%s\n' "$*"
|
||||
printf 'version=%s\n' "$APP_VERSION"
|
||||
printf 'address=%s\n' "$SERVER_ADDRESS"
|
||||
printf 'port=%s\n' "$SERVER_PORT"
|
||||
printf 'update=%s\n' "$FINANCE_UPDATE_ENABLED"
|
||||
printf 'static=%s\n' "$FINANCE_STATIC_LOCATIONS"
|
||||
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"
|
||||
SH
|
||||
chmod 0755 "$WORK/java"
|
||||
|
||||
MOCK_JAVA_LOG="$WORK/java.log" \
|
||||
PATH="$WORK/mock-bin:$PATH" \
|
||||
KAIDI_JAVA_BIN="$WORK/java" \
|
||||
KAIDI_CONFIG_FILE="$WORK/base.env" \
|
||||
KAIDI_RUNTIME_ENV_FILE="$WORK/runtime.env" \
|
||||
DB_URL='' DB_USERNAME='' DB_PASSWORD='' \
|
||||
"$RELEASE/ops/baota-start.sh"
|
||||
|
||||
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 -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 identify and execute the packaged Spring Boot release'
|
||||
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 'port=19090' "$WORK/java.log" || fail 'runtime environment did not override the base port'
|
||||
grep -Fqx 'update=true' "$WORK/java.log" || fail 'launcher did not keep online update enabled'
|
||||
grep -Fqx "static=file:$RELEASE_PHYSICAL/public/" "$WORK/java.log" \
|
||||
|| 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" \
|
||||
KAIDI_CONFIG_FILE="$WORK/missing-db.env" \
|
||||
KAIDI_RUNTIME_ENV_FILE="$WORK/absent.env" \
|
||||
MOCK_JAVA_LOG="$WORK/missing-db.log" \
|
||||
"$RELEASE/ops/baota-start.sh" > "$WORK/missing-db.out" 2>&1; then
|
||||
fail 'launcher accepted a production configuration without DB_URL'
|
||||
fi
|
||||
grep -Fq 'DB_URL is missing from the protected Kaidi configuration' "$WORK/missing-db.out" \
|
||||
|| fail 'launcher did not report the missing database URL'
|
||||
[ ! -e "$WORK/missing-db.log" ] || fail 'launcher started Java after configuration validation failed'
|
||||
|
||||
printf 'Baota Spring Boot launcher configuration, PID identity, and injection fixtures passed\n'
|
||||
@@ -24,6 +24,7 @@ for name in \
|
||||
release-manifest.sig \
|
||||
release-public.pem \
|
||||
install.sh \
|
||||
purge.sh \
|
||||
bootstrap-checksums.txt \
|
||||
SHA256SUMS; do
|
||||
printf 'fixture asset %s\n' "$name" > "$RELEASE_DIR/$name"
|
||||
@@ -102,11 +103,11 @@ GITEA_TOKEN="$TOKEN" \
|
||||
KAIDI_RELEASE_DIR="$RELEASE_DIR" \
|
||||
"$ROOT/scripts/publish-gitea-release.sh" > "$WORK/publish.log"
|
||||
|
||||
grep -Fqx "Published Gitea release $TAG with nine verified assets" "$WORK/publish.log" \
|
||||
grep -Fqx "Published Gitea release $TAG with ten verified assets" "$WORK/publish.log" \
|
||||
|| fail 'publish script did not report success'
|
||||
[ "$(wc -l < "$WORK/assets.jsonl" | tr -d '[:space:]')" -eq 9 ] \
|
||||
|| fail 'publish script did not upload exactly nine assets'
|
||||
[ "$(wc -l < "$WORK/assets.jsonl" | tr -d '[:space:]')" -eq 10 ] \
|
||||
|| fail 'publish script did not upload exactly ten assets'
|
||||
! grep -Fq "$TOKEN" "$WORK/curl.log" \
|
||||
|| fail 'Gitea write token leaked into curl process arguments'
|
||||
|
||||
printf 'Gitea draft, nine-asset verification, and publication fixture passed\n'
|
||||
printf 'Gitea draft, ten-asset verification, and publication fixture passed\n'
|
||||
|
||||
@@ -51,6 +51,7 @@ mkdir -p "$release_permissions/public" "$release_permissions/ops"
|
||||
printf 'jar\n' > "$release_permissions/app.jar"
|
||||
printf 'html\n' > "$release_permissions/public/index.html"
|
||||
printf '#!/bin/sh\n' > "$release_permissions/ops/update.sh"
|
||||
printf '#!/usr/bin/env bash\n' > "$release_permissions/ops/baota-start.sh"
|
||||
chmod -R 0777 "$release_permissions"
|
||||
(
|
||||
# shellcheck disable=SC2329 # Invoked indirectly by the sourced installer helper.
|
||||
@@ -316,17 +317,26 @@ grep -Fq 'systemctl reset-failed kaidi-finance.service' "$ROOT/deploy/install.sh
|
||||
|| fail 'installer can execute before the complete curl stream is parsed'
|
||||
grep -Fqx 'StartLimitBurst=3' "$ROOT/deploy/systemd/kaidi-finance.service" \
|
||||
|| fail 'application service no longer has a bounded restart burst'
|
||||
grep -Fqx 'Restart=no' "$ROOT/deploy/systemd/kaidi-update.service" \
|
||||
|| fail 'update service can automatically repeat a failed switching transaction'
|
||||
! grep -Fq '/var/lib/kaidi-update/processing' "$ROOT/deploy/systemd/kaidi-update.path" \
|
||||
|| fail 'update path can automatically repeat a claimed switching transaction'
|
||||
# shellcheck disable=SC2016 # Match literal installer source.
|
||||
grep -Fq '[ "$actual_sha256" = "$java_sha256" ]' "$ROOT/deploy/install.sh" \
|
||||
|| fail 'installer no longer verifies the Java runtime SHA-256'
|
||||
grep -Fq 'https://git.awaioi.com/api/v1/repos/ERP-Team/kaidi/releases/latest' "$ROOT/deploy/install.sh" \
|
||||
|| fail 'installer default release source is not the public Gitea latest API'
|
||||
grep -Fq '/www/server/java/*/bin/java' "$ROOT/deploy/install.sh" \
|
||||
|| fail 'installer does not search the common Baota Java installation path'
|
||||
# shellcheck disable=SC2016 # Match literal installer source.
|
||||
grep -Fq 'TRUSTED_PUBLIC_KEY_SHA256=${KAIDI_RELEASE_PUBLIC_KEY_SHA256:-807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9}' "$ROOT/deploy/install.sh" \
|
||||
|| fail 'installer does not pin the default release public-key fingerprint'
|
||||
# shellcheck disable=SC2016 # Match literal installer source.
|
||||
grep -Fq 'SETUP_WIZARD=${KAIDI_SETUP_WIZARD:-true}' "$ROOT/deploy/install.sh" \
|
||||
|| fail 'installer does not enable the first-run setup wizard by default'
|
||||
grep -Fq 'if [ "${KAIDI_SETUP_WIZARD+x}" = x ]; then' "$ROOT/deploy/install.sh" \
|
||||
|| fail 'installer no longer distinguishes an explicit setup-wizard selection'
|
||||
# shellcheck disable=SC2016 # Match literal installer source.
|
||||
grep -Fq 'elif [ "$REINSTALL" = true ]; then' "$ROOT/deploy/install.sh" \
|
||||
|| fail 'repair reinstall no longer defaults to production mode'
|
||||
! grep -Fq 'docker run' "$ROOT/deploy/install.sh" \
|
||||
|| fail 'installer must not create a MySQL container'
|
||||
! grep -Fq 'install_docker' "$ROOT/deploy/install.sh" \
|
||||
@@ -370,13 +380,17 @@ grep -Fq 'SERVER_ADDRESS "$SERVER_ADDRESS"' "$ROOT/deploy/install.sh" \
|
||||
grep -Fq 'EnvironmentFile=-/var/lib/kaidi/setup/application.env' \
|
||||
"$ROOT/deploy/systemd/kaidi-finance.service" \
|
||||
|| 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" \
|
||||
|| 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.
|
||||
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 'install.sh | sudo bash' "$ROOT/README.md" \
|
||||
|| fail 'README does not document the public one-line setup-wizard install path'
|
||||
grep -Fq 'kaidi-finance-1.0.0-preview.25.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'
|
||||
|
||||
printf 'Install configuration, custom port, public Gitea, optional private token, i686, setup wizard, and MySQL 8.4 fixtures passed\n'
|
||||
|
||||
Executable
+87
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
WORK=$(mktemp -d)
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
|
||||
fail() {
|
||||
printf 'Purge fixture failed: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
sed '$d' "$ROOT/deploy/purge.sh" > "$WORK/purge-functions.sh"
|
||||
# shellcheck disable=SC1090,SC1091
|
||||
source "$WORK/purge-functions.sh"
|
||||
|
||||
install() {
|
||||
local -a arguments=()
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
-o|-g) shift 2 ;;
|
||||
*) arguments+=("$1"); shift ;;
|
||||
esac
|
||||
done
|
||||
command install "${arguments[@]}"
|
||||
}
|
||||
|
||||
APP_ROOT="$WORK/opt/kaidi"
|
||||
BAOTA_ROOT="$WORK/www/kaidi"
|
||||
CONFIG_ROOT="$WORK/etc/kaidi"
|
||||
STATE_ROOT="$WORK/var/lib/kaidi"
|
||||
UPDATE_STATE_ROOT="$WORK/var/lib/kaidi-update"
|
||||
LOG_ROOT="$WORK/var/log/kaidi"
|
||||
FIRST_LOGIN_FILE="$WORK/root/kaidi-first-login.txt"
|
||||
# shellcheck disable=SC2034 # Referenced by the sourced purge helper.
|
||||
BACKUP_ROOT="$WORK/recovery"
|
||||
BACKUP_ARCHIVE=
|
||||
|
||||
mkdir -p \
|
||||
"$APP_ROOT" \
|
||||
"$BAOTA_ROOT" \
|
||||
"$CONFIG_ROOT" \
|
||||
"$STATE_ROOT/files" \
|
||||
"$UPDATE_STATE_ROOT/backups" \
|
||||
"$UPDATE_STATE_ROOT/failed" \
|
||||
"$UPDATE_STATE_ROOT/transactions" \
|
||||
"$LOG_ROOT" \
|
||||
"$(dirname "$FIRST_LOGIN_FILE")" \
|
||||
"$WORK/external-mysql" \
|
||||
"$WORK/reverse-proxy"
|
||||
printf 'DB_PASSWORD="secret"\n' > "$CONFIG_ROOT/kaidi.env"
|
||||
printf 'document\n' > "$STATE_ROOT/files/document.txt"
|
||||
printf 'dump\n' > "$UPDATE_STATE_ROOT/backups/mysql.sql"
|
||||
printf 'setup code\n' > "$FIRST_LOGIN_FILE"
|
||||
printf 'database sentinel\n' > "$WORK/external-mysql/keep"
|
||||
printf 'proxy sentinel\n' > "$WORK/reverse-proxy/keep"
|
||||
|
||||
create_recovery_backup
|
||||
[ -s "$BACKUP_ARCHIVE" ] || fail 'root-only recovery archive was not created'
|
||||
[ "$(stat -c '%a' "$BACKUP_ARCHIVE" 2>/dev/null || stat -f '%Lp' "$BACKUP_ARCHIVE")" = 600 ] \
|
||||
|| fail 'recovery archive mode is not 0600'
|
||||
archive_list=$(tar -tzf "$BACKUP_ARCHIVE")
|
||||
grep -Fq "${CONFIG_ROOT#/}/kaidi.env" <<< "$archive_list" \
|
||||
|| fail 'configuration was omitted from the recovery archive'
|
||||
grep -Fq "${STATE_ROOT#/}/files/document.txt" <<< "$archive_list" \
|
||||
|| fail 'file storage was omitted from the recovery archive'
|
||||
grep -Fq "${UPDATE_STATE_ROOT#/}/backups/mysql.sql" <<< "$archive_list" \
|
||||
|| fail 'database backup was omitted from the recovery archive'
|
||||
|
||||
remove_managed_paths
|
||||
for path in "$APP_ROOT" "$BAOTA_ROOT" "$CONFIG_ROOT" "$STATE_ROOT" "$UPDATE_STATE_ROOT" "$LOG_ROOT"; do
|
||||
[ ! -e "$path" ] || fail "managed path was not removed: $path"
|
||||
done
|
||||
[ ! -e "$FIRST_LOGIN_FILE" ] || fail 'first-login file was not removed'
|
||||
[ -f "$WORK/external-mysql/keep" ] || fail 'external database state was removed'
|
||||
[ -f "$WORK/reverse-proxy/keep" ] || fail 'reverse-proxy state was removed'
|
||||
[ -s "$BACKUP_ARCHIVE" ] || fail 'recovery archive was removed with the installation'
|
||||
|
||||
[ "$(tail -n 1 "$ROOT/deploy/purge.sh")" = 'main "$@"' ] \
|
||||
|| fail 'purge actions can execute before a complete curl stream is parsed'
|
||||
# shellcheck disable=SC2016 # Match the literal guard in the purge source.
|
||||
grep -Fq 'KAIDI_PURGE_CONFIRM=$REQUIRED_CONFIRMATION' "$ROOT/deploy/purge.sh" \
|
||||
|| fail 'destructive removal no longer requires explicit confirmation'
|
||||
grep -Fq 'External MySQL data and reverse-proxy configuration will not be modified' "$ROOT/deploy/purge.sh" \
|
||||
|| fail 'purge boundary is no longer explicit'
|
||||
|
||||
printf 'Local purge and recovery fixture passed\n'
|
||||
@@ -92,6 +92,7 @@ SH
|
||||
cat > "$mock_bin/systemctl" <<'SH'
|
||||
#!/bin/sh
|
||||
printf '%s\n' "$*" >> "$MOCK_SYSTEMCTL_LOG"
|
||||
[ "${MOCK_UPDATE_PATH_START:-success}:$*" != 'fail:start kaidi-update.path' ] || exit 1
|
||||
exit 0
|
||||
SH
|
||||
|
||||
@@ -170,15 +171,25 @@ SH
|
||||
build_release() {
|
||||
local fixture=$1
|
||||
local version=$2
|
||||
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
|
||||
printf 'new application\n' > "$stage/app.jar"
|
||||
printf '<!doctype html><title>new</title>\n' > "$stage/public/index.html"
|
||||
printf '%s\n' "$version" > "$stage/VERSION"
|
||||
printf '#!/bin/sh\nprintf "new updater\\n"\n' > "$stage/ops/update.sh"
|
||||
printf '#!/usr/bin/env bash\nexit 0\n' > "$stage/ops/baota-start.sh"
|
||||
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/baota-start.sh"
|
||||
chmod 0755 "$stage/ops/baota-init.sh"
|
||||
for name in kaidi-finance.service kaidi-update.service kaidi-update.path; do
|
||||
printf 'new %s\n' "$name" > "$stage/ops/$name"
|
||||
printf '%s %s\n' "$unit_prefix" "$name" > "$stage/ops/$name"
|
||||
done
|
||||
|
||||
local artifact="kaidi-finance-$version.tar.gz"
|
||||
@@ -188,10 +199,6 @@ build_release() {
|
||||
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"}' \
|
||||
> "$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" \
|
||||
-out "$fixture/release/release-manifest.sig" "$fixture/release/release-manifest.json"
|
||||
}
|
||||
@@ -272,6 +279,7 @@ run_update() {
|
||||
MOCK_RELEASE_ORIGIN="${FIXTURE_RELEASE_ORIGIN:-https://release.fixture.invalid}" \
|
||||
MOCK_EXPECT_RELEASE_TOKEN="${FIXTURE_RELEASE_TOKEN-}" \
|
||||
MOCK_SYSTEMCTL_LOG="$fixture/systemctl.log" \
|
||||
MOCK_UPDATE_PATH_START="${MOCK_UPDATE_PATH_START:-success}" \
|
||||
KAIDI_SERVICE_USER="$(id -un)" \
|
||||
KAIDI_SERVICE_GROUP="$(id -gn)" \
|
||||
KAIDI_APP_ROOT="$fixture/app" \
|
||||
@@ -279,6 +287,7 @@ run_update() {
|
||||
KAIDI_LOG_ROOT="$fixture/log" \
|
||||
KAIDI_SYSTEMD_ROOT="$fixture/systemd" \
|
||||
KAIDI_UPDATER_PATH="$fixture/app/bin/update.sh" \
|
||||
KAIDI_RUNTIME_ENV_FILE="$fixture/runtime.env" \
|
||||
KAIDI_SKIP_DB_BACKUP="$skip_backup" \
|
||||
KAIDI_UPDATE_HEALTH_ATTEMPTS=1 \
|
||||
KAIDI_UPDATE_HEALTH_INTERVAL_SECONDS=0 \
|
||||
@@ -361,6 +370,10 @@ assert_success_case() {
|
||||
[ ! -e "$fixture/state/processing/request.json" ] \
|
||||
|| fail 'success case left a claimed request behind'
|
||||
grep -qx 'daemon-reload' "$fixture/systemctl.log" || fail 'systemd units were not reloaded'
|
||||
grep -qx 'restart kaidi-finance.service' "$fixture/systemctl.log" \
|
||||
|| fail 'success case did not restart the application after the atomic switch'
|
||||
! grep -qx 'stop kaidi-finance.service' "$fixture/systemctl.log" \
|
||||
|| fail 'success case stopped the application before switching releases'
|
||||
! grep -qi nginx "$fixture/systemctl.log" || fail 'updater unexpectedly managed Nginx'
|
||||
[ "$(mode_of "$fixture/app")" = 750 ] || fail 'application root is not traversable by the service group'
|
||||
[ "$(mode_of "$fixture/app/releases/$version")" = 750 ] || fail 'release root mode is not 0750'
|
||||
@@ -368,6 +381,41 @@ assert_success_case() {
|
||||
[ "$(mode_of "$fixture/app/current/ops/update.sh")" = 750 ] || fail 'release updater mode is not 0750'
|
||||
}
|
||||
|
||||
assert_identical_systemd_operations_case() {
|
||||
local fixture="$WORK/identical-systemd"
|
||||
local version='1.0.0-preview.2'
|
||||
mkdir -p "$fixture"
|
||||
write_mock_commands "$fixture/mock-bin"
|
||||
build_release "$fixture" "$version" old
|
||||
prepare_installation "$fixture" "$version"
|
||||
|
||||
download_and_prepare_install "$fixture" "$version"
|
||||
: > "$fixture/systemctl.log"
|
||||
run_update "$fixture" success
|
||||
[ "$(readlink "$fixture/app/current")" = "$fixture/app/releases/$version" ] \
|
||||
|| fail 'identical systemd case did not activate the new application'
|
||||
! grep -qx 'daemon-reload' "$fixture/systemctl.log" \
|
||||
|| fail 'identical systemd units triggered an unnecessary daemon reload'
|
||||
}
|
||||
|
||||
assert_update_path_failure_keeps_application_case() {
|
||||
local fixture="$WORK/path-watcher-failure"
|
||||
local version='1.0.0-preview.2'
|
||||
mkdir -p "$fixture"
|
||||
write_mock_commands "$fixture/mock-bin"
|
||||
build_release "$fixture" "$version"
|
||||
prepare_installation "$fixture" "$version"
|
||||
|
||||
download_and_prepare_install "$fixture" "$version"
|
||||
MOCK_UPDATE_PATH_START=fail run_update "$fixture" success
|
||||
[ "$(readlink "$fixture/app/current")" = "$fixture/app/releases/$version" ] \
|
||||
|| fail 'path watcher failure rolled back a healthy application'
|
||||
[ "$(jq -r '.state' "$fixture/state/status.json")" = SUCCEEDED ] \
|
||||
|| fail 'path watcher failure did not preserve successful application state'
|
||||
grep -Fq 'automatic update watcher could not be started' "$fixture/state/status.json" \
|
||||
|| fail 'path watcher failure did not preserve its diagnostic'
|
||||
}
|
||||
|
||||
assert_rollback_case() {
|
||||
local fixture="$WORK/rollback"
|
||||
local version='1.0.0-preview.2'
|
||||
@@ -380,7 +428,7 @@ assert_rollback_case() {
|
||||
if run_update "$fixture" fail-new > "$fixture/update.log" 2>&1; then
|
||||
fail 'rollback case unexpectedly succeeded'
|
||||
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'
|
||||
[ "$(readlink "$fixture/app/current")" = "$fixture/app/releases/1.0.0-preview.1" ] \
|
||||
|| fail 'rollback case did not restore the previous application'
|
||||
@@ -388,6 +436,10 @@ assert_rollback_case() {
|
||||
|| fail 'rollback case did not restore the previous updater'
|
||||
grep -qx 'old kaidi-update.path' "$fixture/systemd/kaidi-update.path" \
|
||||
|| fail 'rollback case did not restore the previous path unit'
|
||||
! grep -qx 'stop kaidi-finance.service' "$fixture/systemctl.log" \
|
||||
|| fail 'rollback case stopped the application before restoring the previous release'
|
||||
[ "$(grep -c '^restart kaidi-finance.service$' "$fixture/systemctl.log")" -ge 2 ] \
|
||||
|| fail 'rollback case did not restart both the candidate and restored releases'
|
||||
[ "$(jq -r '.state' "$fixture/state/status.json")" = FAILED ] \
|
||||
|| fail 'rollback case did not persist FAILED'
|
||||
[ ! -e "$fixture/app/releases/$version" ] \
|
||||
@@ -396,6 +448,28 @@ assert_rollback_case() {
|
||||
|| fail 'rollback case did not archive the failed request'
|
||||
}
|
||||
|
||||
assert_incomplete_rollback_requires_manual_recovery_case() {
|
||||
local fixture="$WORK/incomplete-rollback"
|
||||
local version='1.0.0-preview.2'
|
||||
mkdir -p "$fixture"
|
||||
write_mock_commands "$fixture/mock-bin"
|
||||
build_release "$fixture" "$version"
|
||||
prepare_installation "$fixture" "$version"
|
||||
download_and_prepare_install "$fixture" "$version"
|
||||
|
||||
if run_update "$fixture" fail > "$fixture/update.log" 2>&1; then
|
||||
fail 'incomplete rollback case unexpectedly succeeded'
|
||||
fi
|
||||
grep -Fq 'manual recovery is required' "$fixture/update.log" \
|
||||
|| fail 'incomplete rollback case did not require explicit recovery'
|
||||
[ ! -e "$fixture/state/processing/request.json" ] \
|
||||
|| fail 'incomplete rollback case left an automatically retriggered processing request'
|
||||
[ -d "$fixture/state/transactions/active" ] \
|
||||
|| fail 'incomplete rollback case did not preserve transaction evidence'
|
||||
find "$fixture/state/failed" -type f -name 'request-*.json' -print -quit | grep -q . \
|
||||
|| fail 'incomplete rollback case did not archive its claimed request'
|
||||
}
|
||||
|
||||
assert_download_failure_case() {
|
||||
local fixture="$WORK/download-failure"
|
||||
local version='1.0.0-preview.2'
|
||||
@@ -480,7 +554,10 @@ assert_install_without_verified_cache_rejected() {
|
||||
}
|
||||
|
||||
assert_success_case
|
||||
assert_identical_systemd_operations_case
|
||||
assert_update_path_failure_keeps_application_case
|
||||
assert_rollback_case
|
||||
assert_incomplete_rollback_requires_manual_recovery_case
|
||||
assert_download_failure_case
|
||||
assert_database_failure_case
|
||||
assert_symlink_request_rejected
|
||||
|
||||
@@ -40,6 +40,7 @@ printf '%s\n' \
|
||||
bootstrap-checksums.txt \
|
||||
frontend-sbom.cdx.json \
|
||||
install.sh \
|
||||
purge.sh \
|
||||
"$ARTIFACT" \
|
||||
release-manifest.json \
|
||||
release-manifest.sig \
|
||||
@@ -49,7 +50,7 @@ for path in ./* ./.[!.]* ./..?*; do
|
||||
basename "$path"
|
||||
done | LC_ALL=C sort > "$WORK/actual-assets.txt"
|
||||
diff -u "$WORK/expected-assets.txt" "$WORK/actual-assets.txt" \
|
||||
|| { printf 'Release directory must contain exactly the nine published assets\n' >&2; exit 1; }
|
||||
|| { printf 'Release directory must contain exactly the ten published assets\n' >&2; exit 1; }
|
||||
EXPECTED_ARTIFACT_SHA256=$(jq -er \
|
||||
'.sha256 | strings | ascii_downcase | select(test("^[0-9a-f]{64}$"))' \
|
||||
release-manifest.json)
|
||||
@@ -109,16 +110,21 @@ done
|
||||
|
||||
PUBLIC_KEY_SHA256=$(sha256_file release-public.pem)
|
||||
INSTALLER_SHA256=$(sha256_file install.sh)
|
||||
PURGER_SHA256=$(sha256_file purge.sh)
|
||||
BOOTSTRAP_SHA256=$(sha256_file bootstrap-checksums.txt)
|
||||
SIGNED_INSTALLER=$(jq -er '.bootstrap.installer' release-manifest.json)
|
||||
SIGNED_PURGER=$(jq -er '.bootstrap.purger' release-manifest.json)
|
||||
SIGNED_PUBLIC_KEY=$(jq -er '.bootstrap.publicKey' release-manifest.json)
|
||||
SIGNED_BOOTSTRAP=$(jq -er '.bootstrap.checksums' release-manifest.json)
|
||||
[ "$SIGNED_INSTALLER" = install.sh ] && [ "$SIGNED_PUBLIC_KEY" = release-public.pem ] \
|
||||
[ "$SIGNED_INSTALLER" = install.sh ] && [ "$SIGNED_PURGER" = purge.sh ] \
|
||||
&& [ "$SIGNED_PUBLIC_KEY" = release-public.pem ] \
|
||||
&& [ "$SIGNED_BOOTSTRAP" = bootstrap-checksums.txt ] \
|
||||
|| { printf 'Release manifest bootstrap asset names are invalid\n' >&2; exit 1; }
|
||||
jq -e --arg installer "$INSTALLER_SHA256" --arg publicKey "$PUBLIC_KEY_SHA256" \
|
||||
jq -e --arg installer "$INSTALLER_SHA256" --arg purger "$PURGER_SHA256" \
|
||||
--arg publicKey "$PUBLIC_KEY_SHA256" \
|
||||
--arg bootstrap "$BOOTSTRAP_SHA256" \
|
||||
'.bootstrap.installerSha256 == $installer
|
||||
and .bootstrap.purgerSha256 == $purger
|
||||
and .bootstrap.publicKeySha256 == $publicKey
|
||||
and .bootstrap.checksumsSha256 == $bootstrap' release-manifest.json >/dev/null \
|
||||
|| { printf 'Bootstrap assets do not match the signed manifest\n' >&2; exit 1; }
|
||||
@@ -131,6 +137,8 @@ NORMALIZED_TRUSTED_PUBLIC_KEY_SHA256=$(printf '%s' "$TRUSTED_PUBLIC_KEY_SHA256"
|
||||
|| { printf 'Bootstrap public-key fingerprint does not match\n' >&2; exit 1; }
|
||||
[ "$(sed -n 's/^KAIDI_INSTALLER_SHA256=//p' bootstrap-checksums.txt)" = "$INSTALLER_SHA256" ] \
|
||||
|| { printf 'Bootstrap installer digest does not match\n' >&2; exit 1; }
|
||||
[ "$(sed -n 's/^KAIDI_PURGER_SHA256=//p' bootstrap-checksums.txt)" = "$PURGER_SHA256" ] \
|
||||
|| { printf 'Bootstrap purger digest does not match\n' >&2; exit 1; }
|
||||
|
||||
ARCHIVE_LIST=$(tar -tzf "$ARTIFACT")
|
||||
if grep -Eq '(^/|(^|/)\.\.(/|$))' <<< "$ARCHIVE_LIST"; then
|
||||
@@ -160,11 +168,24 @@ compare_archive_file() {
|
||||
}
|
||||
|
||||
compare_archive_file ./ops/update.sh deploy/update.sh
|
||||
compare_archive_file ./ops/baota-start.sh deploy/baota-start.sh
|
||||
compare_archive_file ./ops/baota-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/kaidi-update.path deploy/systemd/kaidi-update.path
|
||||
compare_archive_file ./ops/kaidi-update.service deploy/systemd/kaidi-update.service
|
||||
compare_archive_file ./ops/kaidi-finance.service deploy/systemd/kaidi-finance.service
|
||||
compare_archive_file ./ops/kaidi-finance.conf deploy/nginx/kaidi-finance.conf
|
||||
tar -tvzf "$ARTIFACT" ./ops/baota-start.sh | grep -Eq '^-.{2}x.{2}x.{2}x' \
|
||||
|| { printf 'Baota Spring Boot launcher is not executable\n' >&2; exit 1; }
|
||||
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-init.sh"
|
||||
cmp install.sh "$ROOT/deploy/install.sh" \
|
||||
|| { printf 'Release installer does not match deploy/install.sh\n' >&2; exit 1; }
|
||||
bash -n "$ROOT/deploy/purge.sh"
|
||||
cmp purge.sh "$ROOT/deploy/purge.sh" \
|
||||
|| { printf 'Release purger does not match deploy/purge.sh\n' >&2; exit 1; }
|
||||
|
||||
printf 'Release %s verified; installer SHA-256: %s\n' "$VERSION" "$INSTALLER_SHA256"
|
||||
|
||||
Reference in New Issue
Block a user