Compare commits

..
Author SHA1 Message Date
Qiufeng 52403c6f95 fix: prevent production TDesign chunk cycles
Release / release (push) Canceled after 0s
2026-08-17 20:46:20 +08:00
Qiufeng 4f626da9db fix: make Linux installation permissions deterministic
Release / release (push) Successful in 3s
2026-08-17 20:16:04 +08:00
Qiufeng dc35a48b44 fix: make installer health checks quiet and diagnostic
Release / release (push) Failing after 10s
2026-08-17 19:29:08 +08:00
Qiufeng e5b419920e fix: retry Gitea release uploads 2026-08-17 18:04:21 +08:00
Qiufeng 8d63d60df5 fix: keep setup wizard preflight successful
Release / release (push) Failing after 37s
2026-08-17 17:56:57 +08:00
Qiufeng 74585a5f07 fix: reuse local Java before downloading runtime
Release / release (push) Failing after 20s
2026-08-17 17:44:13 +08:00
16 changed files with 530 additions and 81 deletions
+1
View File
@@ -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: |
+2
View File
@@ -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
+13 -13
View File
@@ -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
```
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+29
View File
@@ -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([]);
});
+1
View File
@@ -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",
+1
View File
@@ -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 },
+31
View File
@@ -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 } } }],
});
-1
View File
@@ -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',
+1 -1
View File
@@ -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 \
+2
View File
@@ -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}' \
+93 -2
View File
@@ -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'
+57 -1
View File
@@ -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() {