Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52403c6f95 | ||
|
|
4f626da9db | ||
|
|
dc35a48b44 | ||
|
|
e5b419920e | ||
|
|
8d63d60df5 | ||
|
|
74585a5f07 |
@@ -57,6 +57,7 @@ jobs:
|
||||
npm run build
|
||||
npx playwright install --with-deps chromium
|
||||
npm run test:e2e
|
||||
npm run test:dist
|
||||
|
||||
- name: Verify release contracts and scripts
|
||||
run: |
|
||||
|
||||
@@ -4,6 +4,8 @@ frontend/dist/
|
||||
frontend/node_modules/
|
||||
frontend/playwright-report/
|
||||
frontend/test-results/
|
||||
frontend/playwright-report-dist/
|
||||
frontend/test-results-dist/
|
||||
.idea/
|
||||
.vscode/
|
||||
*.iml
|
||||
|
||||
@@ -52,21 +52,21 @@ Gitea 响应中可能错误指向内网地址的 `browser_download_url`。
|
||||
PostgreSQL 18。PostgreSQL 兼容开发已冻结,不属于本次 Preview.12 发布范围。
|
||||
|
||||
执行命令的机器需预装 `bash`、`sudo`、`curl`、`mktemp` 和 `sha256sum`,并能访问目标 Gitea;`jq` 和 Java
|
||||
由安装器补齐,数据库服务和数据库客户端均不会被安装。默认首次安装向导直接使用 JDBC 连接浏览器中填写的
|
||||
由安装器补齐;Java 17 优先使用服务器已有运行时,没有合适版本时才从官方 Azul 下载。数据库服务和数据库客户端均不会被安装。默认首次安装向导直接使用 JDBC 连接浏览器中填写的
|
||||
外部数据库,不需要服务器安装 MySQL CLI。应用固定安装到 `/opt/kaidi`、`/var/lib/kaidi`、
|
||||
`/var/lib/kaidi-update` 和 `/etc/kaidi`。安装器不安装、不启动也不修改 Nginx、Caddy、宝塔或其他反向代理。
|
||||
|
||||
### 直接 curl 安装
|
||||
|
||||
```bash
|
||||
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.12/install.sh | sudo bash
|
||||
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.17/install.sh | sudo bash
|
||||
```
|
||||
|
||||
这条命令会提示填写 Java 应用端口,直接回车使用 `18080`;随后安装最新签名 Release,并默认进入 `/setup`
|
||||
安装向导。Java 默认只监听 `127.0.0.1:所选端口`,前端页面、API 和健康检查均由同一端口提供。无人值守安装可直接指定:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.12/install.sh \
|
||||
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.17/install.sh \
|
||||
| sudo env KAIDI_APP_PORT=19090 bash
|
||||
```
|
||||
|
||||
@@ -74,8 +74,8 @@ curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-previe
|
||||
时才设置 `KAIDI_SERVER_ADDRESS=0.0.0.0`,通常应保持默认回环绑定并由本机反向代理访问。需要在执行前独立校验安装脚本时使用:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.12/install.sh -o /tmp/kaidi-install.sh
|
||||
printf '%s %s\n' ccc648cf63b9f495fd94ffb6ddd9374b1dd26e4acbc1ef63c1e3482e55b4ea65 /tmp/kaidi-install.sh | sha256sum -c -
|
||||
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.17/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
|
||||
```
|
||||
@@ -86,7 +86,7 @@ rm -f /tmp/kaidi-install.sh
|
||||
包装器会在 `sudo` 前校验 `deploy/install.sh` 的固定 SHA-256,再按同一公钥信任链安装最新签名 Release。
|
||||
|
||||
```bash
|
||||
git clone --branch v1.0.0-preview.12 --depth 1 https://git.awaioi.com/ERP-Team/kaidi.git kaidi-preview
|
||||
git clone --branch v1.0.0-preview.17 --depth 1 https://git.awaioi.com/ERP-Team/kaidi.git kaidi-preview
|
||||
cd kaidi-preview
|
||||
./deploy/install-from-git.sh
|
||||
```
|
||||
@@ -98,7 +98,7 @@ cd kaidi-preview
|
||||
|
||||
- 仅在 Linux + systemd 环境执行;首版 32 位支持基线为带 systemd 的 Debian/Ubuntu x86 32 位 Linux。
|
||||
- 不安装或创建数据库;在 `/setup` 中连接运维人员预先准备的外部 MySQL 8.4.x。
|
||||
- 识别 `x86_64`、`aarch64`、`armv7` 或 32 位 `i386/i486/i586/i686`,校验 SHA-256 后安装对应的 Azul Java 17 JRE。
|
||||
- 识别 `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,并在提交前验证 DDL/DML 权限。
|
||||
- 安装签名 Release 到 `/opt/kaidi/releases/<version>`,以 `/opt/kaidi/current` 原子切换当前版本。
|
||||
@@ -141,7 +141,7 @@ sudo cat /root/kaidi-first-login.txt
|
||||
32 位服务端镜像,因此 32 位主机需要预先连接一台 MySQL 8.4 数据库,之后仍然只执行一个安装命令:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.12/install.sh | sudo bash
|
||||
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.17/install.sh | sudo bash
|
||||
```
|
||||
|
||||
无论主机架构如何,安装器都不会安装 MySQL、数据库客户端或创建数据库容器。在打开向导前,需要预先创建 `kaidi_finance`,并授予安装账号该库的
|
||||
@@ -161,7 +161,7 @@ GRANT ALL PRIVILEGES ON kaidi_finance.* TO 'kaidi'@'KAIDI_SERVER_IP';
|
||||
管理员数据和运维人员新增的环境变量:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.12/install.sh \
|
||||
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.17/install.sh \
|
||||
| sudo env KAIDI_REINSTALL=true KAIDI_SETUP_WIZARD=false bash
|
||||
```
|
||||
|
||||
@@ -171,7 +171,7 @@ curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-previe
|
||||
如果安装器已完成但向导尚未提交,可执行下面的命令重新生成一次性安装码;该恢复路径只接受仍处于向导模式且未锁定的安装,正式模式不会被覆盖。
|
||||
|
||||
```bash
|
||||
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.12/install.sh \
|
||||
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.17/install.sh \
|
||||
| sudo env KAIDI_REINSTALL=true bash
|
||||
```
|
||||
|
||||
@@ -207,8 +207,8 @@ act_runner 提供 `ubuntu-24.04` 标签,并在 tag 发布时执行后端、前
|
||||
Preview 属性由 SemVer 版本名表达。之后推送 tag 即会构建、测试、签名并发布:
|
||||
|
||||
```bash
|
||||
git tag v1.0.0-preview.12
|
||||
git push origin v1.0.0-preview.12
|
||||
git tag v1.0.0-preview.17
|
||||
git push origin v1.0.0-preview.17
|
||||
```
|
||||
|
||||
在线更新使用独立的 TDesign 页面:隔离的系统管理员进入“系统治理 → 系统更新”。权限与配置页只管理用户、角色、数据范围、表单模板和参数版本,不配置系统名称或域名。
|
||||
@@ -260,7 +260,7 @@ cat /var/lib/kaidi-update/status.json
|
||||
```bash
|
||||
KAIDI_RELEASE_SIGNING_KEY=/secure/release-signing-private.pem \
|
||||
KAIDI_TRUSTED_RELEASE_PUBLIC_KEY_SHA256=807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9 \
|
||||
./scripts/package-release.sh 1.0.0-preview.12
|
||||
./scripts/package-release.sh 1.0.0-preview.17
|
||||
KAIDI_TRUSTED_RELEASE_PUBLIC_KEY_SHA256=807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9 \
|
||||
./scripts/verify-release.sh dist/release
|
||||
```
|
||||
|
||||
@@ -5,7 +5,7 @@ umask 077
|
||||
|
||||
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
INSTALLER="$ROOT/deploy/install.sh"
|
||||
INSTALLER_SHA256=${KAIDI_INSTALLER_SHA256:-ccc648cf63b9f495fd94ffb6ddd9374b1dd26e4acbc1ef63c1e3482e55b4ea65}
|
||||
INSTALLER_SHA256=${KAIDI_INSTALLER_SHA256:-bc9001197af843dc323907a98023064ccc4f184e000543d2086b8a6f8161dbc7}
|
||||
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:-}
|
||||
|
||||
+243
-57
@@ -28,6 +28,9 @@ BACKUP_DIR=
|
||||
JAVA_STAGED_DIR=
|
||||
JAVA_PREVIOUS_DIR=
|
||||
JAVA_ACTIVATED=false
|
||||
SERVICE_USER=kaidi
|
||||
SERVICE_GROUP=kaidi
|
||||
HOST_ARCH=
|
||||
|
||||
log() { printf '[kaidi-install] %s\n' "$*"; }
|
||||
die() { printf '[kaidi-install] ERROR: %s\n' "$*" >&2; exit 1; }
|
||||
@@ -36,14 +39,42 @@ sha256_file() {
|
||||
sha256sum "$1" | awk '{print $1}'
|
||||
}
|
||||
|
||||
normalized_host_arch() {
|
||||
local machine host_bits
|
||||
machine=$(uname -m)
|
||||
host_bits=$(getconf LONG_BIT 2>/dev/null || true)
|
||||
case "$machine" in
|
||||
x86_64|amd64)
|
||||
[ "$host_bits" = 32 ] && printf x86 || printf x86_64
|
||||
;;
|
||||
i386|i486|i586|i686) printf x86 ;;
|
||||
aarch64|arm64)
|
||||
[ "$host_bits" = 32 ] && printf arm || printf aarch64
|
||||
;;
|
||||
armv7l|armv6l) printf arm ;;
|
||||
*) die "Unsupported CPU architecture: $machine" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
preflight_host() {
|
||||
local host_bits expected_bits
|
||||
[ "$(uname -s)" = Linux ] || die "The installer only supports Linux"
|
||||
[ "$APP_ROOT" = /opt/kaidi ] && [ "$STATE_ROOT" = /var/lib/kaidi ] \
|
||||
&& [ "$UPDATE_STATE_ROOT" = /var/lib/kaidi-update ] && [ "$CONFIG_ROOT" = /etc/kaidi ] \
|
||||
|| die "Custom installation roots are not supported by the packaged systemd configuration"
|
||||
command -v systemctl >/dev/null 2>&1 || die "systemd is required"
|
||||
command -v systemd-analyze >/dev/null 2>&1 || die "systemd-analyze is required"
|
||||
command -v getconf >/dev/null 2>&1 || die "getconf is required"
|
||||
[ -d /run/systemd/system ] || die "systemd is not running as PID 1"
|
||||
[ -d /etc/systemd/system ] || die "/etc/systemd/system is missing"
|
||||
HOST_ARCH=$(normalized_host_arch)
|
||||
host_bits=$(getconf LONG_BIT 2>/dev/null || true)
|
||||
case "$HOST_ARCH" in
|
||||
x86_64|aarch64) expected_bits=64 ;;
|
||||
x86|arm) expected_bits=32 ;;
|
||||
esac
|
||||
[ "$host_bits" = "$expected_bits" ] \
|
||||
|| die "CPU architecture and userspace word size do not match: $HOST_ARCH/$host_bits-bit"
|
||||
[[ "$TRUSTED_PUBLIC_KEY_SHA256" =~ ^[0-9A-Fa-f]{64}$ ]] \
|
||||
|| die "Set KAIDI_RELEASE_PUBLIC_KEY_SHA256 to the trusted release public-key SHA-256"
|
||||
case "$REINSTALL" in
|
||||
@@ -62,40 +93,47 @@ preflight_host() {
|
||||
[ ! -e "$STATE_ROOT/setup/locked" ] \
|
||||
|| die "The setup wizard is already locked; use a normal repair reinstall"
|
||||
fi
|
||||
log "Environment verified: $(uname -sr), architecture=$HOST_ARCH, userspace=${host_bits}-bit, systemd"
|
||||
}
|
||||
|
||||
[ "$(id -u)" -eq 0 ] || die "Run with sudo or as root"
|
||||
[ -z "$RELEASE_TOKEN" ] || [ -z "$RELEASE_TOKEN_FILE" ] \
|
||||
|| die "Set only one of KAIDI_RELEASE_TOKEN or KAIDI_RELEASE_TOKEN_FILE"
|
||||
if [ -n "$RELEASE_TOKEN_FILE" ]; then
|
||||
[ -f "$RELEASE_TOKEN_FILE" ] && [ ! -L "$RELEASE_TOKEN_FILE" ] \
|
||||
|| die "KAIDI_RELEASE_TOKEN_FILE must be a regular file"
|
||||
[ "$(wc -c < "$RELEASE_TOKEN_FILE" | tr -d '[:space:]')" -le 512 ] \
|
||||
|| die "KAIDI_RELEASE_TOKEN_FILE is too large"
|
||||
RELEASE_TOKEN=$(cat "$RELEASE_TOKEN_FILE")
|
||||
fi
|
||||
[ "$REINSTALL" = "true" ] || [ ! -e "$APP_ROOT/current" ] \
|
||||
|| die "Kaidi Finance is already installed; use the system update page"
|
||||
if [ -z "$RELEASE_API_URL" ]; then
|
||||
case "$RELEASE_BASE_URL" in
|
||||
*OWNER/REPO*) die "Set KAIDI_RELEASE_BASE_URL or KAIDI_RELEASE_API_URL to the Git release source" ;;
|
||||
esac
|
||||
fi
|
||||
[ -z "$RELEASE_TOKEN" ] || { [ "${#RELEASE_TOKEN}" -le 512 ] \
|
||||
&& ! printf '%s' "$RELEASE_TOKEN" | grep -q '[[:cntrl:]]'; } \
|
||||
|| die "KAIDI_RELEASE_TOKEN is invalid"
|
||||
validate_inputs() {
|
||||
[ "$(id -u)" -eq 0 ] || die "Run with sudo or as root"
|
||||
[ -z "$RELEASE_TOKEN" ] || [ -z "$RELEASE_TOKEN_FILE" ] \
|
||||
|| die "Set only one of KAIDI_RELEASE_TOKEN or KAIDI_RELEASE_TOKEN_FILE"
|
||||
if [ -n "$RELEASE_TOKEN_FILE" ]; then
|
||||
[ -f "$RELEASE_TOKEN_FILE" ] && [ ! -L "$RELEASE_TOKEN_FILE" ] \
|
||||
|| die "KAIDI_RELEASE_TOKEN_FILE must be a regular file"
|
||||
[ "$(wc -c < "$RELEASE_TOKEN_FILE" | tr -d '[:space:]')" -le 512 ] \
|
||||
|| die "KAIDI_RELEASE_TOKEN_FILE is too large"
|
||||
RELEASE_TOKEN=$(cat "$RELEASE_TOKEN_FILE")
|
||||
fi
|
||||
[ "$REINSTALL" = "true" ] || [ ! -e "$APP_ROOT/current" ] \
|
||||
|| die "Kaidi Finance is already installed; use the system update page"
|
||||
if [ -z "$RELEASE_API_URL" ]; then
|
||||
case "$RELEASE_BASE_URL" in
|
||||
*OWNER/REPO*) die "Set KAIDI_RELEASE_BASE_URL or KAIDI_RELEASE_API_URL to the Git release source" ;;
|
||||
esac
|
||||
fi
|
||||
[ -z "$RELEASE_TOKEN" ] || { [ "${#RELEASE_TOKEN}" -le 512 ] \
|
||||
&& ! printf '%s' "$RELEASE_TOKEN" | grep -q '[[:cntrl:]]'; } \
|
||||
|| die "KAIDI_RELEASE_TOKEN is invalid"
|
||||
}
|
||||
|
||||
download() {
|
||||
local url=$1 output=$2
|
||||
case "$url" in
|
||||
https://*)
|
||||
curl --fail --silent --show-error --location --proto '=https' --proto-redir '=https' \
|
||||
--tlsv1.2 "$url" -o "$output"
|
||||
if ! curl --fail --silent --show-error --location --proto '=https' --proto-redir '=https' \
|
||||
--tlsv1.2 --connect-timeout 15 --max-time 600 "$url" -o "$output"; then
|
||||
die "Download failed: $url"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
[ "${KAIDI_ALLOW_INSECURE_UPDATE:-false}" = "true" ] || die "Release URLs must use HTTPS"
|
||||
curl --fail --silent --show-error --location --proto '=http,https' \
|
||||
--proto-redir '=http,https' "$url" -o "$output"
|
||||
if ! curl --fail --silent --show-error --location --proto '=http,https' \
|
||||
--proto-redir '=http,https' --connect-timeout 15 --max-time 600 "$url" -o "$output"; then
|
||||
die "Download failed: $url"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
@@ -166,17 +204,66 @@ install_packages() {
|
||||
}
|
||||
|
||||
azul_arch() {
|
||||
case "$(uname -m)" in
|
||||
x86_64|amd64) printf x86 ;;
|
||||
i386|i486|i586|i686) printf i686 ;;
|
||||
aarch64|arm64) printf arm_64 ;;
|
||||
armv7l|armv6l) printf arm ;;
|
||||
*) die "Unsupported CPU architecture: $(uname -m)" ;;
|
||||
case "${HOST_ARCH:-$(normalized_host_arch)}" in
|
||||
x86_64) printf x86 ;;
|
||||
x86) printf i686 ;;
|
||||
aarch64) printf arm_64 ;;
|
||||
arm) printf arm ;;
|
||||
esac
|
||||
}
|
||||
|
||||
java_arch_matches_host() {
|
||||
local java_bin=$1 java_arch host_arch
|
||||
host_arch=${HOST_ARCH:-$(normalized_host_arch)}
|
||||
java_arch=$(
|
||||
"$java_bin" -XshowSettings:properties -version 2>&1 \
|
||||
| sed -n 's/^[[:space:]]*os\.arch = [[:space:]]*//p' \
|
||||
| head -n 1
|
||||
)
|
||||
case "$host_arch:$java_arch" in
|
||||
x86_64:amd64|x86_64:x86_64|x86:x86|x86:i386|x86:i486|x86:i586|x86:i686|\
|
||||
aarch64:aarch64|aarch64:arm64|arm:arm) return 0 ;;
|
||||
*) return 1 ;;
|
||||
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
|
||||
[ -x "$java_bin" ] || return 1
|
||||
java_major=$(
|
||||
"$java_bin" -version 2>&1 \
|
||||
| sed -n 's/.*version "\([0-9][0-9]*\).*/\1/p' \
|
||||
| head -n 1
|
||||
)
|
||||
[[ "$java_major" =~ ^[0-9]+$ ]] && [ "$java_major" -ge 17 ] || return 1
|
||||
java_arch_matches_host "$java_bin" || return 1
|
||||
resolved_java=$(readlink -f "$java_bin" 2>/dev/null || printf '%s' "$java_bin")
|
||||
case "$resolved_java" in
|
||||
*/bin/java) home=${resolved_java%/bin/java} ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
[ -x "$home/bin/java" ] || return 1
|
||||
printf '%s\n' "$home"
|
||||
}
|
||||
|
||||
prepare_java() {
|
||||
local api java_metadata package_metadata package_uuid java_url java_sha256 actual_sha256
|
||||
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
|
||||
JAVA_STAGED_DIR="$WORK_DIR/java.next"
|
||||
mkdir -p "$JAVA_STAGED_DIR"
|
||||
log "Copying existing Java 17 runtime from $system_home into the managed application directory"
|
||||
COPYFILE_DISABLE=1 cp -R "$system_home/." "$JAVA_STAGED_DIR/"
|
||||
java_line=$("$JAVA_STAGED_DIR/bin/java" -version 2>&1 | head -n 1)
|
||||
log "Local Java verified: $java_line"
|
||||
return 0
|
||||
fi
|
||||
log "No local Java 17 runtime found; downloading the official Azul Java 17 runtime"
|
||||
api="https://api.azul.com/metadata/v1/zulu/packages/?java_version=17&os=linux&arch=$(azul_arch)&archive_type=tar.gz&java_package_type=jre&release_status=ga&availability_types=CA&latest=true"
|
||||
java_metadata="$WORK_DIR/zulu-metadata.json"
|
||||
download "$api" "$java_metadata"
|
||||
@@ -195,18 +282,88 @@ prepare_java() {
|
||||
[ "$actual_sha256" = "$java_sha256" ] || die "Java 17 runtime SHA-256 verification failed"
|
||||
mkdir -p "$JAVA_STAGED_DIR"
|
||||
tar -xzf "$WORK_DIR/java.tar.gz" --strip-components=1 -C "$JAVA_STAGED_DIR"
|
||||
"$JAVA_STAGED_DIR/bin/java" -version
|
||||
java_arch_matches_host "$JAVA_STAGED_DIR/bin/java" \
|
||||
|| die "Downloaded Java runtime architecture does not match $HOST_ARCH"
|
||||
java_line=$("$JAVA_STAGED_DIR/bin/java" -version 2>&1 | head -n 1)
|
||||
log "Downloaded Java verified: $java_line"
|
||||
}
|
||||
|
||||
activate_java() {
|
||||
mkdir -p "$APP_ROOT/runtime"
|
||||
JAVA_PREVIOUS_DIR="$WORK_DIR/java.previous"
|
||||
if [ -e "$APP_ROOT/runtime/java" ]; then
|
||||
if [ -e "$APP_ROOT/runtime/java" ] || [ -L "$APP_ROOT/runtime/java" ]; then
|
||||
mv "$APP_ROOT/runtime/java" "$JAVA_PREVIOUS_DIR"
|
||||
fi
|
||||
mv "$JAVA_STAGED_DIR" "$APP_ROOT/runtime/java"
|
||||
chown -R root:"$SERVICE_GROUP" "$APP_ROOT/runtime/java"
|
||||
chmod -R u=rwX,g=rX,o= "$APP_ROOT/runtime/java"
|
||||
JAVA_ACTIVATED=true
|
||||
"$APP_ROOT/runtime/java/bin/java" -version
|
||||
}
|
||||
|
||||
ensure_service_identity() {
|
||||
local existing_home nologin_path
|
||||
command -v getent >/dev/null 2>&1 || die "getent is required"
|
||||
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"
|
||||
if ! id -nG "$SERVICE_USER" | tr ' ' '\n' | grep -Fxq "$SERVICE_GROUP"; then
|
||||
usermod --append --groups "$SERVICE_GROUP" "$SERVICE_USER"
|
||||
fi
|
||||
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"
|
||||
}
|
||||
|
||||
prepare_managed_layout() {
|
||||
command -v runuser >/dev/null 2>&1 || die "runuser is required"
|
||||
install -d -o root -g "$SERVICE_GROUP" -m 0750 \
|
||||
"$APP_ROOT" "$APP_ROOT/releases" "$APP_ROOT/runtime" "$APP_ROOT/bin"
|
||||
install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0750 \
|
||||
"$STATE_ROOT" "$STATE_ROOT/files" "$STATE_ROOT/tmp" /var/log/kaidi
|
||||
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 root -m 0700 "$CONFIG_ROOT"
|
||||
}
|
||||
|
||||
secure_release_tree() {
|
||||
local release_dir=$1
|
||||
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"
|
||||
}
|
||||
|
||||
restore_security_contexts() {
|
||||
command -v restorecon >/dev/null 2>&1 || return 0
|
||||
restorecon -RF "$APP_ROOT" "$STATE_ROOT" "$UPDATE_STATE_ROOT" /var/log/kaidi \
|
||||
/etc/systemd/system/kaidi-finance.service \
|
||||
/etc/systemd/system/kaidi-update.service \
|
||||
/etc/systemd/system/kaidi-update.path >/dev/null 2>&1 \
|
||||
|| log "SELinux context restoration reported a warning; service access checks will decide whether installation can continue"
|
||||
}
|
||||
|
||||
verify_service_access() {
|
||||
# shellcheck disable=SC2016 # Positional parameters are expanded by the child shell.
|
||||
runuser -u "$SERVICE_USER" -- sh -c \
|
||||
'cd "$1" && test -r app.jar && test -r public/index.html' sh "$APP_ROOT/current" \
|
||||
|| die "$SERVICE_USER cannot traverse or read the active release"
|
||||
runuser -u "$SERVICE_USER" -- test -x "$APP_ROOT/runtime/java/bin/java" \
|
||||
|| die "$SERVICE_USER cannot execute the managed Java runtime"
|
||||
runuser -u "$SERVICE_USER" -- "$APP_ROOT/runtime/java/bin/java" -version >/dev/null 2>&1 \
|
||||
|| die "The managed Java runtime cannot execute as $SERVICE_USER"
|
||||
runuser -u "$SERVICE_USER" -- test -w "$STATE_ROOT/files" \
|
||||
|| die "$SERVICE_USER cannot write the file-storage directory"
|
||||
runuser -u "$SERVICE_USER" -- test -w "$UPDATE_STATE_ROOT/inbox" \
|
||||
|| die "$SERVICE_USER cannot write the update inbox"
|
||||
log "Service-user filesystem and Java access checks passed"
|
||||
}
|
||||
|
||||
random_secret() { openssl rand -base64 36 | tr -d '\n/+=' | cut -c1-36; }
|
||||
@@ -352,7 +509,7 @@ configure_database() {
|
||||
DB_URL='jdbc:mysql://setup.invalid:3306/kaidi_finance'
|
||||
DB_USERNAME=setup_pending
|
||||
DB_PASSWORD=setup_pending
|
||||
return
|
||||
return 0
|
||||
fi
|
||||
if [ "$REINSTALL" = true ]; then
|
||||
[ -s "$CONFIG_ROOT/kaidi.env" ] || die "KAIDI_REINSTALL needs the existing $CONFIG_ROOT/kaidi.env"
|
||||
@@ -367,7 +524,7 @@ configure_database() {
|
||||
if [ -n "$DB_URL" ]; then
|
||||
[ -n "$DB_USERNAME" ] && [ -n "$DB_PASSWORD" ] \
|
||||
|| die "KAIDI_DB_USERNAME and KAIDI_DB_PASSWORD are required with KAIDI_DB_URL"
|
||||
return
|
||||
return 0
|
||||
fi
|
||||
[ "$REINSTALL" != true ] \
|
||||
|| die "KAIDI_REINSTALL needs database credentials in the existing installation or command environment"
|
||||
@@ -400,7 +557,7 @@ database_name() {
|
||||
|
||||
preflight_database() {
|
||||
local client host port name version table
|
||||
[ "$SETUP_WIZARD" != true ] || return
|
||||
[ "$SETUP_WIZARD" != true ] || return 0
|
||||
case "$DB_URL" in
|
||||
jdbc:mysql://*) ;;
|
||||
*) die "KAIDI_DB_URL must start with jdbc:mysql://" ;;
|
||||
@@ -502,12 +659,25 @@ write_env_file_preserving_unknown() {
|
||||
}
|
||||
|
||||
wait_for_health() {
|
||||
local attempts=0
|
||||
local attempts=0 active_state restart_count
|
||||
log "Waiting for application startup at $HEALTH_URL"
|
||||
while [ "$attempts" -lt 60 ]; do
|
||||
if curl -fsS "$HEALTH_URL" | jq -e '.status == "UP"' >/dev/null 2>&1; then
|
||||
return
|
||||
if curl --fail --silent --connect-timeout 1 --max-time 2 "$HEALTH_URL" 2>/dev/null \
|
||||
| jq -e '.status == "UP"' >/dev/null 2>&1; then
|
||||
log "Application health check is UP"
|
||||
return 0
|
||||
fi
|
||||
active_state=$(systemctl show kaidi-finance.service --property=ActiveState --value 2>/dev/null || printf unknown)
|
||||
restart_count=$(systemctl show kaidi-finance.service --property=NRestarts --value 2>/dev/null || printf 0)
|
||||
[[ "$restart_count" =~ ^[0-9]+$ ]] || restart_count=0
|
||||
if [ "$active_state" = failed ] || [ "$restart_count" -ge 3 ]; then
|
||||
journalctl -u kaidi-finance.service -n 80 --no-pager >&2 || true
|
||||
die "Application service failed during startup (state=$active_state, restarts=$restart_count)"
|
||||
fi
|
||||
attempts=$((attempts + 1))
|
||||
if [ $((attempts % 5)) -eq 0 ]; then
|
||||
log "Application is still starting (state=$active_state, elapsed=$((attempts * 2))s)"
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
journalctl -u kaidi-finance.service -n 80 --no-pager >&2 || true
|
||||
@@ -537,15 +707,25 @@ backup_managed_state() {
|
||||
fi
|
||||
index=$((index + 1))
|
||||
done
|
||||
systemctl is-active --quiet kaidi-finance.service && PREVIOUS_APP_ACTIVE=true || PREVIOUS_APP_ACTIVE=false
|
||||
systemctl is-enabled --quiet kaidi-finance.service && PREVIOUS_APP_ENABLED=true || PREVIOUS_APP_ENABLED=false
|
||||
systemctl is-active --quiet kaidi-update.path && PREVIOUS_UPDATE_ACTIVE=true || PREVIOUS_UPDATE_ACTIVE=false
|
||||
systemctl is-enabled --quiet kaidi-update.path && PREVIOUS_UPDATE_ENABLED=true || PREVIOUS_UPDATE_ENABLED=false
|
||||
systemctl is-active --quiet kaidi-finance.service >/dev/null 2>&1 \
|
||||
&& PREVIOUS_APP_ACTIVE=true || PREVIOUS_APP_ACTIVE=false
|
||||
systemctl is-enabled --quiet kaidi-finance.service >/dev/null 2>&1 \
|
||||
&& PREVIOUS_APP_ENABLED=true || PREVIOUS_APP_ENABLED=false
|
||||
systemctl is-active --quiet kaidi-update.path >/dev/null 2>&1 \
|
||||
&& PREVIOUS_UPDATE_ACTIVE=true || PREVIOUS_UPDATE_ACTIVE=false
|
||||
systemctl is-enabled --quiet kaidi-update.path >/dev/null 2>&1 \
|
||||
&& PREVIOUS_UPDATE_ENABLED=true || PREVIOUS_UPDATE_ENABLED=false
|
||||
INSTALL_TRANSACTION_ARMED=true
|
||||
}
|
||||
|
||||
restore_unit_state() {
|
||||
local unit=$1 was_enabled=$2 was_active=$3
|
||||
if ! systemctl cat "$unit" >/dev/null 2>&1; then
|
||||
systemctl reset-failed "$unit" >/dev/null 2>&1 || true
|
||||
[ "$was_enabled" = false ] && [ "$was_active" = false ]
|
||||
return
|
||||
fi
|
||||
systemctl reset-failed "$unit" >/dev/null 2>&1 || true
|
||||
if [ "$was_enabled" = true ]; then
|
||||
systemctl enable "$unit" >/dev/null 2>&1
|
||||
else
|
||||
@@ -578,7 +758,7 @@ rollback_install() {
|
||||
local index=0 path rollback_failed=false
|
||||
set +e
|
||||
log "Installation failed; restoring the previous managed state"
|
||||
systemctl stop kaidi-update.path kaidi-finance.service >/dev/null 2>&1 || rollback_failed=true
|
||||
systemctl stop kaidi-update.path kaidi-finance.service >/dev/null 2>&1 || true
|
||||
for path in "${MANAGED_PATHS[@]}"; do
|
||||
restore_managed_path "$path" "$index" || rollback_failed=true
|
||||
index=$((index + 1))
|
||||
@@ -586,7 +766,7 @@ rollback_install() {
|
||||
if [ -n "$RELEASE_DIR" ]; then
|
||||
rm -rf -- "$RELEASE_DIR" || rollback_failed=true
|
||||
fi
|
||||
if [ -d "$JAVA_PREVIOUS_DIR" ]; then
|
||||
if [ -d "$JAVA_PREVIOUS_DIR" ] || [ -L "$JAVA_PREVIOUS_DIR" ]; then
|
||||
rm -rf -- "$APP_ROOT/runtime/java" || rollback_failed=true
|
||||
mkdir -p "$APP_ROOT/runtime" || rollback_failed=true
|
||||
mv "$JAVA_PREVIOUS_DIR" "$APP_ROOT/runtime/java" || rollback_failed=true
|
||||
@@ -613,8 +793,10 @@ cleanup() {
|
||||
exit "$result"
|
||||
}
|
||||
|
||||
main() {
|
||||
trap cleanup EXIT
|
||||
|
||||
validate_inputs
|
||||
configure_app_port
|
||||
preflight_host
|
||||
install_packages
|
||||
@@ -661,25 +843,18 @@ find "$WORK_DIR/extracted" -type l -print -quit | grep -q . \
|
||||
[ "$(cat "$WORK_DIR/extracted/VERSION")" = "$VERSION" ] || die "Release version mismatch"
|
||||
[ -x "$WORK_DIR/extracted/ops/update.sh" ] || die "Release updater is missing"
|
||||
|
||||
prepare_java
|
||||
configure_database
|
||||
preflight_database
|
||||
|
||||
id kaidi >/dev/null 2>&1 || useradd --system --home "$STATE_ROOT" --shell /usr/sbin/nologin kaidi
|
||||
mkdir -p "$APP_ROOT/releases" "$APP_ROOT/bin" "$STATE_ROOT/files" "$STATE_ROOT/tmp" \
|
||||
"$STATE_ROOT/setup" "$UPDATE_STATE_ROOT/inbox" "$CONFIG_ROOT" /var/log/kaidi
|
||||
chown -R kaidi:kaidi "$STATE_ROOT" "$UPDATE_STATE_ROOT/inbox" /var/log/kaidi
|
||||
chown root:kaidi "$UPDATE_STATE_ROOT"
|
||||
chmod 0750 "$STATE_ROOT" "$STATE_ROOT/files" "$STATE_ROOT/tmp" "$UPDATE_STATE_ROOT" "$UPDATE_STATE_ROOT/inbox"
|
||||
chmod 0700 "$STATE_ROOT/setup"
|
||||
ensure_service_identity
|
||||
prepare_managed_layout
|
||||
prepare_java
|
||||
|
||||
RELEASE_DIR="$APP_ROOT/releases/$VERSION"
|
||||
[ ! -e "$RELEASE_DIR" ] || RELEASE_DIR="$APP_ROOT/releases/${VERSION}-reinstall-$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
backup_managed_state
|
||||
activate_java
|
||||
mv "$WORK_DIR/extracted" "$RELEASE_DIR"
|
||||
chown -R root:root "$RELEASE_DIR"
|
||||
chmod -R go-w "$RELEASE_DIR"
|
||||
secure_release_tree "$RELEASE_DIR"
|
||||
ln -sfn "$RELEASE_DIR" "$APP_ROOT/current.next"
|
||||
mv -Tf "$APP_ROOT/current.next" "$APP_ROOT/current"
|
||||
install -m 0755 "$RELEASE_DIR/ops/update.sh" "$APP_ROOT/bin/update.sh"
|
||||
@@ -771,7 +946,15 @@ chmod 0600 "$CONFIG_ROOT/update.env"
|
||||
install -m 0644 "$RELEASE_DIR/ops/kaidi-finance.service" /etc/systemd/system/kaidi-finance.service
|
||||
install -m 0644 "$RELEASE_DIR/ops/kaidi-update.service" /etc/systemd/system/kaidi-update.service
|
||||
install -m 0644 "$RELEASE_DIR/ops/kaidi-update.path" /etc/systemd/system/kaidi-update.path
|
||||
restore_security_contexts
|
||||
verify_service_access
|
||||
systemd-analyze verify \
|
||||
/etc/systemd/system/kaidi-finance.service \
|
||||
/etc/systemd/system/kaidi-update.service \
|
||||
/etc/systemd/system/kaidi-update.path >/dev/null \
|
||||
|| die "Installed systemd units failed validation"
|
||||
systemctl daemon-reload
|
||||
systemctl reset-failed kaidi-finance.service >/dev/null 2>&1 || true
|
||||
systemctl enable kaidi-finance.service
|
||||
systemctl restart kaidi-finance.service
|
||||
wait_for_health
|
||||
@@ -822,3 +1005,6 @@ if [ "$REINSTALL" != true ] && [ "$SETUP_WIZARD" != true ]; then
|
||||
log "Temporary credentials: /root/kaidi-first-login.txt"
|
||||
log "Change the temporary password immediately after first sign-in"
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
Description=Kaidi Finance System
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
StartLimitIntervalSec=60
|
||||
StartLimitBurst=3
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
|
||||
+53
-5
@@ -18,6 +18,8 @@ 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}
|
||||
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}
|
||||
APP_INDEX_URL=${KAIDI_APP_INDEX_URL:-http://127.0.0.1:18080/}
|
||||
LOCK_FILE=$STATE_ROOT/update.lock
|
||||
@@ -38,9 +40,10 @@ case "$HEALTH_ATTEMPTS:$HEALTH_INTERVAL_SECONDS" in
|
||||
esac
|
||||
[ "$HEALTH_ATTEMPTS" -ge 1 ] || { printf '%s\n' "Update health-check attempts must be at least 1" >&2; exit 1; }
|
||||
|
||||
mkdir -p "$STATE_ROOT/inbox" "$PROCESSING_DIR" "$FAILED_REQUEST_ROOT" "$TRANSACTION_ROOT" \
|
||||
"$STATE_ROOT/work" "$APP_ROOT/releases" "$LOG_ROOT" "$BACKUP_ROOT" "$CACHE_ROOT"
|
||||
chmod 0700 "$PROCESSING_DIR" "$FAILED_REQUEST_ROOT" "$TRANSACTION_ROOT" "$BACKUP_ROOT" "$CACHE_ROOT"
|
||||
bootstrap_die() {
|
||||
printf '%s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
status() {
|
||||
state=$1
|
||||
@@ -77,6 +80,43 @@ fail() {
|
||||
exit 1
|
||||
}
|
||||
|
||||
prepare_update_layout() {
|
||||
id "$SERVICE_USER" >/dev/null 2>&1 || bootstrap_die "Service user $SERVICE_USER is missing"
|
||||
id -nG "$SERVICE_USER" | tr ' ' '\n' | grep -Fxq "$SERVICE_GROUP" \
|
||||
|| bootstrap_die "Service user $SERVICE_USER is not a member of group $SERVICE_GROUP"
|
||||
command -v runuser >/dev/null 2>&1 || bootstrap_die "runuser is required"
|
||||
install -d -o root -g "$SERVICE_GROUP" -m 0750 \
|
||||
"$APP_ROOT" "$APP_ROOT/releases" "$APP_ROOT/runtime" "$APP_ROOT/bin" "$STATE_ROOT" \
|
||||
|| bootstrap_die "Managed application or update directories could not be prepared"
|
||||
install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0750 "$STATE_ROOT/inbox" "$LOG_ROOT" \
|
||||
|| bootstrap_die "Writable update directories could not be prepared"
|
||||
install -d -o root -g root -m 0700 \
|
||||
"$PROCESSING_DIR" "$FAILED_REQUEST_ROOT" "$TRANSACTION_ROOT" "$STATE_ROOT/work" \
|
||||
"$BACKUP_ROOT" "$CACHE_ROOT" \
|
||||
|| bootstrap_die "Private update directories could not be prepared"
|
||||
}
|
||||
|
||||
secure_release_tree() {
|
||||
release_dir=$1
|
||||
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"
|
||||
}
|
||||
|
||||
verify_release_access() {
|
||||
release_dir=$1
|
||||
# shellcheck disable=SC2016 # Positional parameters are expanded by the child shell.
|
||||
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
|
||||
}
|
||||
|
||||
prepare_update_layout
|
||||
|
||||
# shellcheck disable=SC2329 # Invoked by the EXIT trap below.
|
||||
cleanup() {
|
||||
rc=$?
|
||||
@@ -329,6 +369,7 @@ rollback_active_transaction() {
|
||||
rollback_ok=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
|
||||
remove_failed_release "$failed_release"
|
||||
@@ -520,8 +561,14 @@ if [ -e "$RELEASE_DIR" ]; then
|
||||
rm -rf "$RELEASE_DIR" || fail "Failed release staging directory could not be cleaned"
|
||||
fi
|
||||
mv "$WORK_DIR/extracted" "$RELEASE_DIR" || fail "Release directory could not be activated"
|
||||
chown -R root:root "$RELEASE_DIR" || fail "Release ownership could not be secured"
|
||||
chmod -R go-w "$RELEASE_DIR" || fail "Release permissions could not be secured"
|
||||
secure_release_tree "$RELEASE_DIR" || fail "Release ownership or permissions could not be secured"
|
||||
if command -v restorecon >/dev/null 2>&1; then
|
||||
restorecon -RF "$APP_ROOT" >/dev/null 2>&1 || true
|
||||
fi
|
||||
if ! verify_release_access "$RELEASE_DIR"; then
|
||||
rm -rf "$RELEASE_DIR"
|
||||
fail "Service user cannot access the release or managed Java runtime"
|
||||
fi
|
||||
|
||||
PREVIOUS_TARGET=$(readlink "$APP_ROOT/current" 2>/dev/null || true)
|
||||
mkdir "$ACTIVE_TRANSACTION"
|
||||
@@ -552,6 +599,7 @@ write_transaction_value phase APP_SWITCHED
|
||||
|
||||
status RUNNING "Starting and verifying release $TARGET_VERSION" "$TARGET_VERSION"
|
||||
write_transaction_value phase HEALTH_CHECKING
|
||||
systemctl reset-failed "$SERVICE_NAME" >/dev/null 2>&1 || true
|
||||
if systemctl start "$SERVICE_NAME" \
|
||||
&& systemctl is-active --quiet kaidi-update.path \
|
||||
&& verify_app_surface; then
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
test('release build boots the setup wizard without runtime errors', async ({ page }) => {
|
||||
const pageErrors: string[] = [];
|
||||
const consoleErrors: string[] = [];
|
||||
|
||||
page.on('pageerror', (error) => pageErrors.push(error.message));
|
||||
page.on('console', (message) => {
|
||||
if (message.type() === 'error') consoleErrors.push(message.text());
|
||||
});
|
||||
await page.route('**/api/v1/setup/status', async (route) => {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
data: {
|
||||
required: true,
|
||||
locked: false,
|
||||
supportedDatabaseTypes: ['MYSQL'],
|
||||
message: '请完成安装',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const response = await page.goto('/setup');
|
||||
expect(response?.ok()).toBe(true);
|
||||
await expect(page.getByRole('heading', { name: '安装向导', exact: true })).toBeVisible();
|
||||
expect(pageErrors).toEqual([]);
|
||||
expect(consoleErrors).toEqual([]);
|
||||
});
|
||||
@@ -18,6 +18,7 @@
|
||||
"site:preview": "npm run build && cp -r dist _site",
|
||||
"test": "vitest run",
|
||||
"test:e2e": "playwright test",
|
||||
"test:dist": "playwright test --config playwright.dist.config.ts",
|
||||
"test:a11y": "playwright test e2e/accessibility.e2e.ts",
|
||||
"audit:dependencies": "npm audit --audit-level=high --registry=https://registry.npmjs.org",
|
||||
"check:production": "node scripts/check-production-hygiene.mjs",
|
||||
|
||||
@@ -6,6 +6,7 @@ const baseURL = `http://127.0.0.1:${port}`;
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
testMatch: '**/*.e2e.ts',
|
||||
testIgnore: 'dist-smoke.e2e.ts',
|
||||
outputDir: 'test-results',
|
||||
timeout: 30_000,
|
||||
expect: { timeout: 8_000 },
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { defineConfig } from '@playwright/test';
|
||||
|
||||
const port = Number(process.env.PLAYWRIGHT_DIST_PORT || 3012);
|
||||
const baseURL = `http://127.0.0.1:${port}`;
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
testMatch: 'dist-smoke.e2e.ts',
|
||||
outputDir: 'test-results-dist',
|
||||
timeout: 30_000,
|
||||
expect: { timeout: 8_000 },
|
||||
fullyParallel: false,
|
||||
workers: 1,
|
||||
forbidOnly: Boolean(process.env.CI),
|
||||
retries: process.env.CI ? 1 : 0,
|
||||
reporter: process.env.CI ? [['list'], ['html', { open: 'never', outputFolder: 'playwright-report-dist' }]] : 'list',
|
||||
use: {
|
||||
baseURL,
|
||||
locale: 'zh-CN',
|
||||
timezoneId: 'Asia/Shanghai',
|
||||
screenshot: 'only-on-failure',
|
||||
trace: 'retain-on-failure',
|
||||
},
|
||||
webServer: {
|
||||
command: `npm run preview -- --host 127.0.0.1 --port ${port}`,
|
||||
url: baseURL,
|
||||
reuseExistingServer: false,
|
||||
timeout: 120_000,
|
||||
},
|
||||
projects: [{ name: 'chromium-dist', use: { browserName: 'chromium', viewport: { width: 1366, height: 768 } } }],
|
||||
});
|
||||
@@ -55,7 +55,6 @@ export default ({ mode }: ConfigEnv): UserConfig => {
|
||||
name: 'tdesign-vendor',
|
||||
test: /node_modules[\\/]tdesign-(?:vue-next|icons-vue-next)[\\/]/,
|
||||
priority: 30,
|
||||
maxSize: 480 * 1024,
|
||||
},
|
||||
{
|
||||
name: 'vue-vendor',
|
||||
|
||||
@@ -99,7 +99,7 @@ cp "$ROOT/deploy/systemd/kaidi-update.path" "$STAGE/ops/kaidi-update.path"
|
||||
chmod 0755 "$STAGE/ops/update.sh"
|
||||
|
||||
ARTIFACT="kaidi-finance-$VERSION.tar.gz"
|
||||
COPYFILE_DISABLE=1 tar -czf "$OUTPUT_DIR/$ARTIFACT" -C "$STAGE" .
|
||||
COPYFILE_DISABLE=1 tar --format=ustar -czf "$OUTPUT_DIR/$ARTIFACT" -C "$STAGE" .
|
||||
SHA256=$(sha256_file "$OUTPUT_DIR/$ARTIFACT")
|
||||
cp "$ROOT/backend/target/backend-sbom.json" "$OUTPUT_DIR/backend-sbom.cdx.json"
|
||||
(cd "$ROOT/frontend" && npm sbom --omit=dev --package-lock-only \
|
||||
|
||||
@@ -48,6 +48,7 @@ request() {
|
||||
local method=$1 url=$2 output=$3 input=${4:-} status
|
||||
local args=(
|
||||
--silent --show-error --proto '=https' --tlsv1.2
|
||||
--connect-timeout 15 --max-time 600 --retry 5 --retry-all-errors --retry-delay 2
|
||||
--request "$method" --header "@$AUTH_HEADER"
|
||||
--output "$output" --write-out '%{http_code}'
|
||||
)
|
||||
@@ -99,6 +100,7 @@ for name in "${ASSETS[@]}"; do
|
||||
>> "$WORK/expected-assets.jsonl"
|
||||
encoded_name=$(jq -rn --arg value "$name" '$value | @uri')
|
||||
upload_status=$(curl --silent --show-error --proto '=https' --tlsv1.2 \
|
||||
--connect-timeout 15 --max-time 1800 --retry 6 --retry-all-errors --retry-delay 2 \
|
||||
--header "@$AUTH_HEADER" \
|
||||
--form "attachment=@$path;type=application/octet-stream" \
|
||||
--output "$WORK/upload.json" --write-out '%{http_code}' \
|
||||
|
||||
@@ -10,6 +10,10 @@ fail() {
|
||||
exit 1
|
||||
}
|
||||
|
||||
mode_of() {
|
||||
stat -c '%a' "$1" 2>/dev/null || stat -f '%Lp' "$1"
|
||||
}
|
||||
|
||||
# Load only pure helper functions. The installer itself must never run in this fixture.
|
||||
{
|
||||
sed -n '/^decode_env_value()/,/^}/p' "$ROOT/deploy/install.sh"
|
||||
@@ -22,17 +26,55 @@ fail() {
|
||||
sed -n '/^configure_app_port()/,/^}/p' "$ROOT/deploy/install.sh"
|
||||
sed -n '/^is_semver()/,/^}/p' "$ROOT/deploy/install.sh"
|
||||
sed -n '/^write_env_file_preserving_unknown()/,/^}/p' "$ROOT/deploy/install.sh"
|
||||
sed -n '/^normalized_host_arch()/,/^}/p' "$ROOT/deploy/install.sh"
|
||||
sed -n '/^azul_arch()/,/^}/p' "$ROOT/deploy/install.sh"
|
||||
sed -n '/^java_arch_matches_host()/,/^}/p' "$ROOT/deploy/install.sh"
|
||||
sed -n '/^system_java_home()/,/^}/p' "$ROOT/deploy/install.sh"
|
||||
sed -n '/^sha256_file()/,/^}/p' "$ROOT/deploy/install.sh"
|
||||
sed -n '/^prepare_java()/,/^}/p' "$ROOT/deploy/install.sh"
|
||||
sed -n '/^download_release_url()/,/^}/p' "$ROOT/deploy/install.sh"
|
||||
sed -n '/^release_asset_url()/,/^}/p' "$ROOT/deploy/install.sh"
|
||||
sed -n '/^download_release_asset()/,/^}/p' "$ROOT/deploy/install.sh"
|
||||
sed -n '/^install_packages()/,/^}/p' "$ROOT/deploy/install.sh"
|
||||
sed -n '/^preflight_database()/,/^}/p' "$ROOT/deploy/install.sh"
|
||||
sed -n '/^secure_release_tree()/,/^}/p' "$ROOT/deploy/install.sh"
|
||||
sed -n '/^restore_unit_state()/,/^}/p' "$ROOT/deploy/install.sh"
|
||||
} > "$WORK/helpers.sh"
|
||||
# shellcheck disable=SC1090,SC1091
|
||||
source "$WORK/helpers.sh"
|
||||
|
||||
export SETUP_WIZARD=true
|
||||
preflight_database || fail 'setup wizard database preflight returned a failure status'
|
||||
|
||||
release_permissions="$WORK/release-permissions"
|
||||
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"
|
||||
chmod -R 0777 "$release_permissions"
|
||||
(
|
||||
# shellcheck disable=SC2329 # Invoked indirectly by the sourced installer helper.
|
||||
chown() { return 0; }
|
||||
export SERVICE_GROUP=fixture
|
||||
secure_release_tree "$release_permissions"
|
||||
)
|
||||
[ "$(mode_of "$release_permissions")" = 750 ] || fail 'release root mode is not 0750'
|
||||
[ "$(mode_of "$release_permissions/public")" = 750 ] || fail 'release directory mode is not 0750'
|
||||
[ "$(mode_of "$release_permissions/app.jar")" = 640 ] || fail 'release file mode is not 0640'
|
||||
[ "$(mode_of "$release_permissions/ops/update.sh")" = 750 ] || fail 'release updater mode is not 0750'
|
||||
|
||||
(
|
||||
# shellcheck disable=SC2329 # Invoked indirectly by the sourced installer helper.
|
||||
systemctl() {
|
||||
case "$1" in
|
||||
cat) return 1 ;;
|
||||
reset-failed) return 0 ;;
|
||||
*) return 97 ;;
|
||||
esac
|
||||
}
|
||||
restore_unit_state missing.service false false
|
||||
) || fail 'rollback treated an absent first-install unit as an incomplete restoration'
|
||||
|
||||
# shellcheck disable=SC2034 # Referenced by the extracted installer helper.
|
||||
REINSTALL=true
|
||||
# shellcheck disable=SC2034 # Referenced by the extracted installer helper.
|
||||
@@ -132,10 +174,23 @@ for arch in i386 i486 i586 i686; do
|
||||
ARCH_FIXTURE=$arch
|
||||
[ "$(azul_arch)" = i686 ] || fail "$arch did not map to the Azul i686 runtime"
|
||||
done
|
||||
(
|
||||
ARCH_FIXTURE=x86_64
|
||||
# shellcheck disable=SC2329 # Invoked indirectly by the sourced architecture helper.
|
||||
getconf() { printf '32\n'; }
|
||||
[ "$(normalized_host_arch)" = x86 ] \
|
||||
|| fail '32-bit userspace on an x86_64 kernel was not normalized to x86'
|
||||
export HOST_ARCH=x86
|
||||
[ "$(azul_arch)" = i686 ] \
|
||||
|| fail '32-bit userspace on an x86_64 kernel did not select the i686 Java runtime'
|
||||
)
|
||||
|
||||
mkdir -p "$WORK/fake-jre/bin"
|
||||
cat > "$WORK/fake-jre/bin/java" <<'JAVA'
|
||||
#!/usr/bin/env sh
|
||||
if [ "${1:-}" = '-XshowSettings:properties' ]; then
|
||||
printf ' os.arch = x86\n' >&2
|
||||
fi
|
||||
printf 'openjdk version "17-fixture"\n' >&2
|
||||
JAVA
|
||||
chmod 0755 "$WORK/fake-jre/bin/java"
|
||||
@@ -157,8 +212,16 @@ download() {
|
||||
esac
|
||||
}
|
||||
ARCH_FIXTURE=i686
|
||||
export KAIDI_JAVA_HOME="$WORK/missing-java-home"
|
||||
prepare_java >/dev/null 2>&1
|
||||
[ -x "$JAVA_STAGED_DIR/bin/java" ] || fail 'verified i686 Java runtime was not staged'
|
||||
rm -rf "$JAVA_STAGED_DIR"
|
||||
export KAIDI_JAVA_HOME="$WORK/fake-jre"
|
||||
prepare_java >/dev/null 2>&1
|
||||
[ -d "$JAVA_STAGED_DIR" ] && [ ! -L "$JAVA_STAGED_DIR" ] \
|
||||
|| fail 'existing Java 17 runtime was not copied into managed storage'
|
||||
cmp -s "$WORK/fake-jre/bin/java" "$JAVA_STAGED_DIR/bin/java" \
|
||||
|| fail 'installer staged an unexpected local Java runtime'
|
||||
|
||||
export RELEASE_API_URL=https://gitea.fixture.invalid/api/v1/repos/ERP-Team/kaidi/releases/latest
|
||||
RELEASE_TOKEN=fixture-read-only-token
|
||||
@@ -225,6 +288,34 @@ grep -Fq 'download "$api" "$java_metadata"' "$ROOT/deploy/install.sh" \
|
||||
# shellcheck disable=SC2016 # Match literal installer source.
|
||||
grep -Fq 'java_sha256=$(jq -er' "$ROOT/deploy/install.sh" \
|
||||
|| fail 'installer no longer obtains the Java runtime SHA-256'
|
||||
grep -Fq 'Copying existing Java 17 runtime' "$ROOT/deploy/install.sh" \
|
||||
|| fail 'installer no longer reuses a local Java 17 runtime'
|
||||
# shellcheck disable=SC2016 # Match literal installer source.
|
||||
grep -Fq 'cp -R "$system_home/." "$JAVA_STAGED_DIR/"' "$ROOT/deploy/install.sh" \
|
||||
|| fail 'installer no longer copies a local Java runtime into managed storage'
|
||||
# shellcheck disable=SC2016 # Match literal installer source.
|
||||
! grep -Fq 'ln -s "$system_home"' "$ROOT/deploy/install.sh" \
|
||||
|| fail 'installer must not link the service to an externally managed Java directory'
|
||||
# shellcheck disable=SC2016 # Match the literal installer command.
|
||||
grep -Fq -- '--connect-timeout 1 --max-time 2 "$HEALTH_URL" 2>/dev/null' "$ROOT/deploy/install.sh" \
|
||||
|| fail 'installer no longer performs a quiet bounded health check'
|
||||
grep -Fq 'restart_count" -ge 3' "$ROOT/deploy/install.sh" \
|
||||
|| fail 'installer no longer stops early after repeated service restarts'
|
||||
# shellcheck disable=SC2016 # Match literal installer source.
|
||||
grep -Fq 'install -d -o root -g "$SERVICE_GROUP" -m 0750' "$ROOT/deploy/install.sh" \
|
||||
|| fail 'installer no longer creates traversable root-owned application directories'
|
||||
# shellcheck disable=SC2016 # Match literal installer source.
|
||||
grep -Fq 'find "$release_dir" -type d -exec chmod 0750 {} +' "$ROOT/deploy/install.sh" \
|
||||
|| fail 'installer no longer secures release directory traversal permissions'
|
||||
# shellcheck disable=SC2016 # Match literal installer source.
|
||||
grep -Fq 'runuser -u "$SERVICE_USER" -- sh -c' "$ROOT/deploy/install.sh" \
|
||||
|| fail 'installer no longer validates the release as the service user'
|
||||
grep -Fq 'systemctl reset-failed kaidi-finance.service' "$ROOT/deploy/install.sh" \
|
||||
|| fail 'installer no longer resets stale systemd failure state'
|
||||
[ "$(tail -n 1 "$ROOT/deploy/install.sh")" = 'main "$@"' ] \
|
||||
|| 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'
|
||||
# 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'
|
||||
@@ -282,8 +373,8 @@ 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'
|
||||
# shellcheck disable=SC2016 # Match the literal installer source.
|
||||
grep -Fq 'chown root:kaidi "$UPDATE_STATE_ROOT"' "$ROOT/deploy/install.sh" \
|
||||
# shellcheck disable=SC2016 # Match literal installer source.
|
||||
grep -Fq 'install -d -o root -g "$SERVICE_GROUP" -m 0750 "$UPDATE_STATE_ROOT"' "$ROOT/deploy/install.sh" \
|
||||
|| fail 'update state parent is not group-accessible to the application user'
|
||||
grep -Fq 'install.sh | sudo bash' "$ROOT/README.md" \
|
||||
|| fail 'README does not document the public one-line setup-wizard install path'
|
||||
|
||||
@@ -11,6 +11,10 @@ fail() {
|
||||
exit 1
|
||||
}
|
||||
|
||||
mode_of() {
|
||||
stat -c '%a' "$1" 2>/dev/null || stat -f '%Lp' "$1"
|
||||
}
|
||||
|
||||
sed -n '/^is_semver()/,/^}/p' "$ROOT/deploy/update.sh" > "$WORK/update-semver.sh"
|
||||
# shellcheck disable=SC1090,SC1091
|
||||
source "$WORK/update-semver.sh"
|
||||
@@ -21,6 +25,20 @@ for version in 01.2.3 1.02.3 1.2.03 1.2.3-01 1.2.3-alpha..1; do
|
||||
! is_semver "$version" || fail "updater accepted invalid SemVer $version"
|
||||
done
|
||||
|
||||
missing_service_user="kaidi-fixture-missing-$$"
|
||||
if KAIDI_APP_ROOT="$WORK/bootstrap/app" \
|
||||
KAIDI_UPDATE_STATE_ROOT="$WORK/bootstrap/state" \
|
||||
KAIDI_LOG_ROOT="$WORK/bootstrap/log" \
|
||||
KAIDI_SERVICE_USER="$missing_service_user" \
|
||||
KAIDI_SERVICE_GROUP="$missing_service_user" \
|
||||
sh "$ROOT/deploy/update.sh" > "$WORK/bootstrap.log" 2>&1; then
|
||||
fail 'updater accepted a missing service identity during bootstrap'
|
||||
fi
|
||||
grep -Fq "Service user $missing_service_user is missing" "$WORK/bootstrap.log" \
|
||||
|| fail 'updater bootstrap failure did not preserve its diagnostic'
|
||||
! grep -Eq 'No such file|nonexistent directory|cannot create' "$WORK/bootstrap.log" \
|
||||
|| fail 'updater bootstrap failure was masked by an unavailable status directory'
|
||||
|
||||
write_mock_commands() {
|
||||
local mock_bin=$1
|
||||
mkdir -p "$mock_bin"
|
||||
@@ -99,6 +117,33 @@ SH
|
||||
cat > "$mock_bin/chown" <<'SH'
|
||||
#!/bin/sh
|
||||
exit 0
|
||||
SH
|
||||
|
||||
cat > "$mock_bin/runuser" <<'SH'
|
||||
#!/bin/sh
|
||||
[ "${1:-}" = -u ] || exit 2
|
||||
shift 2
|
||||
[ "${1:-}" = -- ] && shift
|
||||
exec "$@"
|
||||
SH
|
||||
|
||||
cat > "$mock_bin/install" <<'SH'
|
||||
#!/usr/bin/env bash
|
||||
if [[ " $* " != *" -d "* ]]; then
|
||||
exec /usr/bin/install "$@"
|
||||
fi
|
||||
mode=0755
|
||||
paths=()
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
-d) shift ;;
|
||||
-o|-g) shift 2 ;;
|
||||
-m) mode=$2; shift 2 ;;
|
||||
*) paths+=("$1"); shift ;;
|
||||
esac
|
||||
done
|
||||
mkdir -p "${paths[@]}"
|
||||
chmod "$mode" "${paths[@]}"
|
||||
SH
|
||||
|
||||
cat > "$mock_bin/mv" <<'SH'
|
||||
@@ -170,13 +215,18 @@ prepare_installation() {
|
||||
local fixture=$1
|
||||
local version=$2
|
||||
mkdir -p "$fixture/app/releases/1.0.0-preview.1/public" "$fixture/app/bin" \
|
||||
"$fixture/state/inbox" "$fixture/systemd" "$fixture/log"
|
||||
"$fixture/app/runtime/java/bin" "$fixture/state/inbox" "$fixture/systemd" "$fixture/log"
|
||||
printf 'old application\n' > "$fixture/app/releases/1.0.0-preview.1/app.jar"
|
||||
printf '<!doctype html><title>old</title>\n' > "$fixture/app/releases/1.0.0-preview.1/public/index.html"
|
||||
printf '1.0.0-preview.1\n' > "$fixture/app/releases/1.0.0-preview.1/VERSION"
|
||||
ln -s "$fixture/app/releases/1.0.0-preview.1" "$fixture/app/current"
|
||||
printf 'old update.sh\n' > "$fixture/app/bin/update.sh"
|
||||
chmod 0755 "$fixture/app/bin/update.sh"
|
||||
cat > "$fixture/app/runtime/java/bin/java" <<'SH'
|
||||
#!/bin/sh
|
||||
exit 0
|
||||
SH
|
||||
chmod 0755 "$fixture/app/runtime/java/bin/java"
|
||||
for name in kaidi-finance.service kaidi-update.service kaidi-update.path; do
|
||||
printf 'old %s\n' "$name" > "$fixture/systemd/$name"
|
||||
done
|
||||
@@ -222,6 +272,8 @@ 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" \
|
||||
KAIDI_SERVICE_USER="$(id -un)" \
|
||||
KAIDI_SERVICE_GROUP="$(id -gn)" \
|
||||
KAIDI_APP_ROOT="$fixture/app" \
|
||||
KAIDI_UPDATE_STATE_ROOT="$fixture/state" \
|
||||
KAIDI_LOG_ROOT="$fixture/log" \
|
||||
@@ -310,6 +362,10 @@ assert_success_case() {
|
||||
|| fail 'success case left a claimed request behind'
|
||||
grep -qx 'daemon-reload' "$fixture/systemctl.log" || fail 'systemd units were not reloaded'
|
||||
! 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'
|
||||
[ "$(mode_of "$fixture/app/current/app.jar")" = 640 ] || fail 'release file mode is not 0640'
|
||||
[ "$(mode_of "$fixture/app/current/ops/update.sh")" = 750 ] || fail 'release updater mode is not 0750'
|
||||
}
|
||||
|
||||
assert_rollback_case() {
|
||||
|
||||
Reference in New Issue
Block a user