#!/usr/bin/env bash set -Eeuo pipefail ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) WORK=$(mktemp -d) trap 'rm -rf "$WORK"' EXIT fail() { printf 'Install fixture failed: %s\n' "$1" >&2 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" sed -n '/^read_env_file()/,/^}/p' "$ROOT/deploy/install.sh" sed -n '/^read_existing_env()/,/^}/p' "$ROOT/deploy/install.sh" sed -n '/^read_setup_env()/,/^}/p' "$ROOT/deploy/install.sh" sed -n '/^read_reinstall_env()/,/^}/p' "$ROOT/deploy/install.sh" sed -n '/^port_is_listening()/,/^}/p' "$ROOT/deploy/install.sh" sed -n '/^valid_app_port()/,/^}/p' "$ROOT/deploy/install.sh" sed -n '/^configure_app_port()/,/^}/p' "$ROOT/deploy/install.sh" sed -n '/^configure_database()/,/^}/p' "$ROOT/deploy/install.sh" sed -n '/^database_host()/,/^}/p' "$ROOT/deploy/install.sh" sed -n '/^database_port()/,/^}/p' "$ROOT/deploy/install.sh" sed -n '/^database_name()/,/^}/p' "$ROOT/deploy/install.sh" sed -n '/^validate_database_configuration()/,/^}/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 '/^java_home_from_bin()/,/^}/p' "$ROOT/deploy/install.sh" sed -n '/^system_java_home()/,/^}/p' "$ROOT/deploy/install.sh" sed -n '/^sha256_file()/,/^}/p' "$ROOT/deploy/install.sh" sed -n '/^prepare_java()/,/^}/p' "$ROOT/deploy/install.sh" 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" # Avoid invoking the host's macOS `log` utility while exercising extracted # installer helpers. # shellcheck disable=SC2329 # Referenced by the sourced installer helper functions. log() { printf '%s\n' "$*" >/dev/null; } export SETUP_WIZARD=true preflight_database || fail 'setup wizard database preflight returned a failure status' REINSTALL=false # shellcheck disable=SC2034 # Consumed by the extracted configure_database helper. KAIDI_DB_URL='' KAIDI_DB_USERNAME='' KAIDI_DB_PASSWORD='' configure_database [ -z "$DB_URL$DB_USERNAME$DB_PASSWORD" ] \ || fail 'setup wizard retained a fake database configuration' [ -z "$(database_host)$(database_port)$(database_name)" ] \ || fail 'setup wizard exposed placeholder database coordinates to the updater' mkdir -p "$WORK/mysql-bin" cat > "$WORK/mysql-bin/mysql" <<'SH' #!/bin/sh exit 0 SH chmod 0755 "$WORK/mysql-bin/mysql" # Production-mode validation is intentionally side-effect free: it validates # the JDBC shape but never invokes a MySQL client or emits DDL/DML. SETUP_WIZARD=false DB_URL='jdbc:mysql://127.0.0.1:3306/kaidi_finance?useUnicode=true' DB_USERNAME='fixture-user' DB_PASSWORD='fixture-password' mysql_probe_marker="$WORK/mysql-probe-called" cat > "$WORK/mysql-bin/mysql" < "$mysql_probe_marker" exit 99 SH chmod 0755 "$WORK/mysql-bin/mysql" preflight_database || fail 'side-effect-free database configuration validation returned a failure status' [ ! -e "$mysql_probe_marker" ] || fail 'installer invoked a MySQL client during configuration validation' 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" printf '#!/usr/bin/env bash\n' > "$release_permissions/ops/baota-start.sh" chmod -R 0777 "$release_permissions" ( # shellcheck disable=SC2329 # Invoked indirectly by the sourced installer helper. 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. WORK_DIR=$WORK # shellcheck disable=SC2034 # Referenced by the extracted installer helper. CONFIG_ROOT=$WORK STATE_ROOT=$WORK/state mkdir -p "$STATE_ROOT/setup" env_file="$WORK/kaidi.env" cat > "$env_file" <<'ENV' # operator-owned values must survive a reinstall FINANCE_SESSION_ABSOLUTE_TIMEOUT="45m" FILE_SCANNER_HOST="scanner.internal" FILE_SCANNER_PORT="3310" DB_URL="jdbc:mysql://old/kaidi_finance" APP_VERSION="old" ENV # shellcheck disable=SC2016 # These shell characters must remain literal data. literal_password='pw$HOME-$(touch PLACEHOLDER)-`id`-back\slash-"quote"' literal_password=${literal_password/PLACEHOLDER/$WORK\/executed} write_env_file_preserving_unknown "$env_file" \ DB_URL 'jdbc:mysql://new/kaidi_finance' \ DB_PASSWORD "$literal_password" \ APP_VERSION '1.0.0-preview.1' grep -qx 'FINANCE_SESSION_ABSOLUTE_TIMEOUT="45m"' "$env_file" \ || fail 'reinstall removed the custom session timeout' grep -qx 'FILE_SCANNER_HOST="scanner.internal"' "$env_file" \ || fail 'reinstall removed the custom scanner host' grep -qx 'DB_URL="jdbc:mysql://new/kaidi_finance"' "$env_file" \ || fail 'reinstall did not replace the managed database URL' [ "$(grep -c '^DB_URL=' "$env_file")" -eq 1 ] || fail 'reinstall duplicated a managed key' [ "$(grep -c '^APP_VERSION=' "$env_file")" -eq 1 ] || fail 'reinstall duplicated the application version' [ "$(read_existing_env DB_PASSWORD)" = "$literal_password" ] \ || fail 'reinstall changed literal shell characters in the database password' [ ! -e "$WORK/executed" ] || fail 'reinstall executed database password content' cat > "$STATE_ROOT/setup/application.env" <<'ENV' DB_URL="jdbc:mysql://runtime/kaidi_finance" DB_USERNAME="runtime-user" DB_PASSWORD="runtime-password" ENV grep -qx 'jdbc:mysql://runtime/kaidi_finance' <(read_reinstall_env DB_URL) \ || fail 'reinstall did not prefer the completed setup runtime database URL' grep -qx 'runtime-user' <(read_reinstall_env DB_USERNAME) \ || fail 'reinstall did not prefer the completed setup runtime database user' for port in 1024 18080 65535; do valid_app_port "$port" || fail "installer rejected valid application port $port" done for port in 0 80 1023 65536 invalid 18080.0; do ! valid_app_port "$port" || fail "installer accepted invalid application port $port" done port_is_listening() { return 1; } log() { printf '%s\n' "$*" >/dev/null; } # shellcheck disable=SC2329 # Invoked by the extracted installer helper. die() { printf '%s\n' "$*" >&2; return 1; } export APP_PORT=19090 SERVER_ADDRESS=127.0.0.1 HEALTH_URL='' APP_INDEX_URL='' REINSTALL=false configure_app_port [ "$HEALTH_URL" = 'http://127.0.0.1:19090/actuator/health' ] \ || fail 'selected application port did not reach the health URL' [ "$APP_INDEX_URL" = 'http://127.0.0.1:19090/' ] \ || fail 'selected application port did not reach the frontend URL' export APP_PORT=19091 SERVER_ADDRESS=::1 HEALTH_URL='' APP_INDEX_URL='' configure_app_port [ "$HEALTH_URL" = 'http://[::1]:19091/actuator/health' ] \ || fail 'IPv6 bind address did not produce a bracketed health URL' [ "$APP_INDEX_URL" = 'http://[::1]:19091/' ] \ || fail 'IPv6 bind address did not produce a bracketed frontend URL' [ "$PROXY_TARGET" = 'http://[::1]:19091' ] \ || fail 'IPv6 bind address did not produce a bracketed reverse-proxy target' for version in 0.0.0 1.2.3-alpha- 1.2.3--alpha 1.2.3-alpha+build.07; do is_semver "$version" || fail "installer rejected valid SemVer $version" "$ROOT/scripts/check-semver.sh" "$version" || fail "release workflow rejected valid SemVer $version" done 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 "installer accepted invalid SemVer $version" ! "$ROOT/scripts/check-semver.sh" "$version" >/dev/null 2>&1 \ || fail "release workflow accepted invalid SemVer $version" done ARCH_FIXTURE= uname() { if [ "${1:-}" = -m ]; then printf '%s\n' "$ARCH_FIXTURE" else command uname "$@" fi } die() { return 1 } 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.0.8"\n' >&2 JAVA chmod 0755 "$WORK/fake-jre/bin/java" tar -czf "$WORK/java-fixture.tar.gz" -C "$WORK" fake-jre java_fixture_sha=$(sha256sum "$WORK/java-fixture.tar.gz" | awk '{print $1}') cat > "$WORK/java-list.json" <<'JSON' [{"name":"zulu17-fixture-linux_i686.tar.gz","package_uuid":"fixture-package"}] JSON cat > "$WORK/java-detail.json" </dev/null 2>&1 [ -x "$JAVA_STAGED_DIR/bin/java" ] || fail 'verified i686 Java runtime was not staged' [ "$JAVA_BIN" = "$APP_ROOT/runtime/java/bin/java" ] \ || fail 'downloaded Java did not select the managed fallback path' rm -rf "$JAVA_STAGED_DIR" JAVA_STAGED_DIR= export KAIDI_JAVA_HOME="$WORK/fake-jre" prepare_java >/dev/null 2>&1 [ -z "$JAVA_STAGED_DIR" ] \ || fail 'installer copied an existing server Java runtime into application storage' EXPECTED_LOCAL_JAVA=$(readlink -f "$WORK/fake-jre/bin/java" 2>/dev/null \ || printf '%s' "$WORK/fake-jre/bin/java") [ "$JAVA_BIN" = "$EXPECTED_LOCAL_JAVA" ] \ || fail 'installer did not select the existing server Java executable directly' export RELEASE_API_URL=https://gitea.fixture.invalid/api/v1/repos/ERP-Team/kaidi/releases/latest RELEASE_TOKEN=fixture-read-only-token RELEASE_AUTH_HEADER_FILE=$WORK/release-auth-header printf 'Authorization: token %s\n' "$RELEASE_TOKEN" > "$RELEASE_AUTH_HEADER_FILE" chmod 0600 "$RELEASE_AUTH_HEADER_FILE" cat > "$WORK/release-api.json" <<'JSON' {"tag_name":"v1.0.0-preview.11","assets":[ {"name":"release-manifest.json","browser_download_url":"https://gitea.fixture.invalid/assets/release-manifest.json"} ]} JSON printf 'signed manifest fixture\n' > "$WORK/release-manifest.fixture" : > "$WORK/release-curl.log" curl() { local output='' url='' header_file='' argument='' printf '%s\n' "$*" >> "$WORK/release-curl.log" for argument in "$@"; do case "$argument" in @*) header_file=${argument#@} ;; https://*) url=$argument ;; esac done while [ "$#" -gt 0 ]; do case "$1" in -o) shift; output=$1 ;; esac shift done [ "$header_file" = "$RELEASE_AUTH_HEADER_FILE" ] || return 90 grep -Fqx "Authorization: token $RELEASE_TOKEN" "$header_file" || return 91 [ "$url" = https://gitea.fixture.invalid/ERP-Team/kaidi/releases/download/v1.0.0-preview.11/release-manifest.json ] \ || return 92 cp "$WORK/release-manifest.fixture" "$output" } [ "$(release_asset_url release-manifest.json)" = \ https://gitea.fixture.invalid/ERP-Team/kaidi/releases/download/v1.0.0-preview.11/release-manifest.json ] \ || fail 'installer did not construct the trusted Gitea release asset URL' download_release_asset release-manifest.json "$WORK/downloaded-manifest.json" cmp -s "$WORK/release-manifest.fixture" "$WORK/downloaded-manifest.json" \ || fail 'installer did not download the private Gitea release asset' ! grep -Fq "$RELEASE_TOKEN" "$WORK/release-curl.log" \ || fail 'installer leaked the private Gitea token into curl process arguments' ! grep -Fq -- '--location' "$WORK/release-curl.log" \ || fail 'installer allowed authenticated Gitea redirects' jq '.assets[0].browser_download_url = "https://assets.fixture.invalid/release-manifest.json"' \ "$WORK/release-api.json" > "$WORK/release-api.cross-origin.json" mv "$WORK/release-api.cross-origin.json" "$WORK/release-api.json" [ "$(release_asset_url release-manifest.json)" = \ https://gitea.fixture.invalid/ERP-Team/kaidi/releases/download/v1.0.0-preview.11/release-manifest.json ] \ || fail 'installer trusted the cross-origin browser download URL' # shellcheck disable=SC2016 # Match literal installer source. grep -Fq '[ "$APP_ROOT" = /opt/kaidi ]' "$ROOT/deploy/install.sh" \ || fail 'installer no longer rejects unsupported custom roots' grep -Fq '8.4.*) ;;' "$ROOT/deploy/install.sh" \ || fail 'installer no longer enforces MySQL 8.4.x' ! grep -Fq 'jdbc:mysql://setup.invalid' "$ROOT/deploy/install.sh" \ || fail 'installer still writes a fake setup database URL' ! grep -Fq 'jdbc:mysql://setup.invalid' "$ROOT/deploy/baota-init.sh" \ || fail 'Baota initializer still writes a fake setup database URL' ! grep -Fq 'KAIDI_DB_HOST setup.invalid' "$ROOT/deploy/baota-init.sh" \ || fail 'Baota updater still points at a fake setup database host' ! grep -Fq 'systemctl enable --now kaidi-update.path' "$ROOT/deploy/baota-init.sh" \ || fail 'Baota initializer still enables the systemd updater' # shellcheck disable=SC2016 # Match literal initializer source. ! grep -Fq 'install -m 0644 "$release_root/ops/kaidi-update.service"' "$ROOT/deploy/baota-init.sh" \ || fail 'Baota initializer still installs the systemd updater unit' grep -Fq '32-bit Linux deployment requires glibc' "$ROOT/deploy/install.sh" \ || fail 'installer does not reject unsupported musl 32-bit hosts before downloading Java' grep -Fq '32-bit Linux deployment requires the glibc loader' "$ROOT/deploy/install.sh" \ || fail 'installer does not reject a 32-bit host without the glibc loader' grep -Fq 'preflight_runtime_commands' "$ROOT/deploy/install.sh" \ || fail 'installer does not validate required runtime commands' # shellcheck disable=SC2016 # Match literal installer source. grep -Fq 'KAIDI_MYSQLDUMP_BIN "${KAIDI_MYSQLDUMP_BIN:-}"' "$ROOT/deploy/install.sh" \ || fail 'installer does not preserve a custom mysqldump path for online updates' # shellcheck disable=SC2016 # Match literal installer source. grep -Fq 'write_env_file_preserving_unknown "$CONFIG_ROOT/kaidi.env"' "$ROOT/deploy/install.sh" \ || fail 'installer no longer uses the reinstall-safe environment writer' # shellcheck disable=SC2016 # Match literal installer source. grep -Fq 'download "$api" "$java_metadata"' "$ROOT/deploy/install.sh" \ || fail 'installer no longer applies the restricted downloader to Java metadata' # 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 'Using existing Java 17+ runtime' "$ROOT/deploy/install.sh" \ || fail 'installer no longer reuses a local Java 17 runtime' # shellcheck disable=SC2016 # Match literal installer source. grep -Fq 'JAVA_BIN="$system_home/bin/java"' "$ROOT/deploy/install.sh" \ || fail 'installer no longer records the selected server Java path' # shellcheck disable=SC2016 # Match literal installer source. ! grep -Fq 'cp -R "$system_home/."' "$ROOT/deploy/install.sh" \ || fail 'installer copied an existing server Java runtime into managed storage' # shellcheck disable=SC2016 # Match the literal installer command. grep -Fq -- '--connect-timeout 1 --max-time 2 "$HEALTH_URL" 2>/dev/null' "$ROOT/deploy/install.sh" \ || fail 'installer no longer performs a quiet bounded health check' 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' grep -Fqx 'ExecStart=/opt/kaidi/current/ops/baota-start.sh' "$ROOT/deploy/systemd/kaidi-finance.service" \ || fail 'application service bypasses the Java-selecting launcher' grep -Fqx 'Environment=KAIDI_ENV_PRELOADED=true' "$ROOT/deploy/systemd/kaidi-finance.service" \ || fail 'systemd service user would need to read the root-only configuration directory' grep -Fqx 'Restart=no' "$ROOT/deploy/systemd/kaidi-update.service" \ || fail 'update service can automatically repeat a failed switching transaction' grep -Fq -- '-/www/wwwroot/kaidi' "$ROOT/deploy/systemd/kaidi-update.service" \ || fail 'update service requires the Baota application path on a systemd-only installation' grep -Fqx 'PathExists=/var/lib/kaidi-update/processing/request.json' "$ROOT/deploy/systemd/kaidi-update.path" \ || fail 'update path does not resume an interrupted claimed request' grep -Fqx 'PathExists=/var/lib/kaidi-update/transactions/active' "$ROOT/deploy/systemd/kaidi-update.path" \ || fail 'update path does not resume an interrupted switching transaction' # shellcheck disable=SC2016 # Match literal installer source. grep -Fq '[ "$actual_sha256" = "$java_sha256" ]' "$ROOT/deploy/install.sh" \ || fail 'installer no longer verifies the Java runtime SHA-256' grep -Fq 'https://git.awaioi.com/api/v1/repos/ERP-Team/kaidi/releases/latest' "$ROOT/deploy/install.sh" \ || fail 'installer default release source is not the public Gitea latest API' grep -Fq '/www/server/java/*/bin/java' "$ROOT/deploy/install.sh" \ || fail 'installer does not search the common Baota Java installation path' # shellcheck disable=SC2016 # Match literal installer source. grep -Fq 'TRUSTED_PUBLIC_KEY_SHA256=${KAIDI_RELEASE_PUBLIC_KEY_SHA256:-807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9}' "$ROOT/deploy/install.sh" \ || fail 'installer does not pin the default release public-key fingerprint' # shellcheck disable=SC2016 # Match literal installer source. grep -Fq 'if [ "${KAIDI_SETUP_WIZARD+x}" = x ]; then' "$ROOT/deploy/install.sh" \ || fail 'installer no longer distinguishes an explicit setup-wizard selection' # shellcheck disable=SC2016 # Match literal installer source. grep -Fq 'elif [ "$REINSTALL" = true ]; then' "$ROOT/deploy/install.sh" \ || fail 'repair reinstall no longer defaults to production mode' ! grep -Fq 'docker run' "$ROOT/deploy/install.sh" \ || fail 'installer must not create a MySQL container' ! grep -Fq 'install_docker' "$ROOT/deploy/install.sh" \ || fail 'installer must not install Docker or MySQL automatically' package_log="$WORK/package-manager.log" for package_manager in apt-get dnf yum; do package_bin="$WORK/package-manager-$package_manager" mkdir "$package_bin" cat > "$package_bin/$package_manager" <<'SH' #!/bin/sh printf '%s %s\n' "${0##*/}" "$*" >> "$PACKAGE_LOG" SH chmod +x "$package_bin/$package_manager" : > "$package_log" ( PATH="$package_bin" PACKAGE_LOG="$package_log" export PATH PACKAGE_LOG install_packages ) if grep -Eqi '(^|[[:space:]])(default-mysql-client|mysql(-client|-server)?|mariadb(-client|-server)?)([[:space:]]|$)' "$package_log"; then fail "installer asked $package_manager to install a database package" fi done grep -Fq 'An external MySQL 8.4 database is required' "$ROOT/deploy/install.sh" \ || fail 'installer does not require an operator-managed external MySQL database' grep -Fq 'KAIDI_RELEASE_TOKEN_FILE' "$ROOT/deploy/install.sh" \ || fail 'installer no longer supports a protected initial Gitea token file' grep -Fq 'KAIDI_SETUP_WIZARD' "$ROOT/deploy/install.sh" \ || fail 'installer no longer supports first-run setup mode' grep -Fq 'FINANCE_SETUP_TOKEN_SHA256' "$ROOT/deploy/install.sh" \ || fail 'installer no longer writes the one-time setup-code hash' # shellcheck disable=SC2016 # Match literal installer source. grep -Fq 'SERVER_PORT "$APP_PORT"' "$ROOT/deploy/install.sh" \ || fail 'installer does not persist the selected application port' # shellcheck disable=SC2016 # Match literal installer source. grep -Fq 'SERVER_ADDRESS "$SERVER_ADDRESS"' "$ROOT/deploy/install.sh" \ || fail 'installer does not persist the selected bind address' ! grep -Eqi 'nginx|/etc/nginx/' "$ROOT/deploy/install.sh" \ || fail 'installer must not install, start, or modify Nginx' grep -Fq 'EnvironmentFile=-/var/lib/kaidi/setup/application.env' \ "$ROOT/deploy/systemd/kaidi-finance.service" \ || fail 'application service no longer loads the setup-completion environment' ! grep -Fq 'EnvironmentFile=-/var/lib/kaidi/setup/application.env' \ "$ROOT/deploy/systemd/kaidi-update.service" \ || fail 'root update service must not load an application-owned environment file' grep -Fq 'load_runtime_database_env' "$ROOT/deploy/update.sh" \ || fail 'update script no longer imports only the setup database fields' # shellcheck disable=SC2016 # Match literal installer source. grep -Fq 'install -d -o root -g "$SERVICE_GROUP" -m 0750 "$UPDATE_STATE_ROOT"' "$ROOT/deploy/install.sh" \ || fail 'update state parent is not group-accessible to the application user' manual_version=$(sed -n 's/^VERSION=\(1\.0\.0-preview\.[0-9][0-9]*\)$/\1/p' "$ROOT/README.md" | sed -n '1p') [ -n "$manual_version" ] \ || fail 'README does not declare a preview version for the public manual-deployment artifact' grep -Fq "kaidi-finance-$manual_version.tar.gz" "$ROOT/README.md" \ || fail 'README does not document the public manual-deployment artifact for its declared version' grep -Fq 'ops/baota-init.sh' "$ROOT/README.md" \ || fail 'README does not document the local Baota initialization command' grep -Fq 'FINANCE_UPDATE_ENABLED false' "$ROOT/deploy/baota-init.sh" \ || fail 'Baota manual mode still exposes the unstable online updater' printf 'Install configuration, custom port, public Gitea, optional private token, i686, setup wizard, and MySQL 8.4 fixtures passed\n'