#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$ROOT_DIR" fail() { echo "release-preflight: $*" >&2 exit 1 } require_file() { [[ -f "$1" ]] || fail "missing artifact: $1; build it before release" } sha256() { if command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | awk '{print $1}' else sha256sum "$1" | awk '{print $1}' fi } sha256_stream() { if command -v shasum >/dev/null 2>&1; then shasum -a 256 | awk '{print $1}' else sha256sum | awk '{print $1}' fi } normalized_jar_content_sha256() { local jar_path="$1" local manifest_path manifest_path="$(mktemp)" while IFS= read -r entry; do case "$entry" in */|META-INF/*.SF|META-INF/*.RSA|META-INF/*.DSA) continue ;; esac printf '%s %s\n' "$(unzip -p "$jar_path" "$entry" | sha256_stream)" "$entry" >> "$manifest_path" done < <(unzip -Z1 "$jar_path" | LC_ALL=C sort) sha256 "$manifest_path" rm -f "$manifest_path" } frontend_index="frontend/dist/index.html" mobile_index="mobile-uni/dist/build/h5/index.html" backend_jar="backend/ruoyi-admin/target/ruoyi-admin.jar" require_file "$frontend_index" require_file "$mobile_index" require_file "$backend_jar" backend_module_jar_name="$(unzip -Z1 "$backend_jar" | sed -nE 's#BOOT-INF/lib/(ruoyi-aihr-[^/]+\.jar)#\1#p' | head -1)" [[ -n "$backend_module_jar_name" ]] || fail "backend jar does not contain the ruoyi-aihr module" frontend_asset="$(sed -nE 's/.*src="([^"]+\.js)".*/\1/p' "$frontend_index" | head -1)" mobile_asset="$(sed -nE 's/.*src="([^"]+\.js)".*/\1/p' "$mobile_index" | head -1)" [[ -n "$frontend_asset" ]] || fail "frontend index does not reference a JavaScript entry" [[ -n "$mobile_asset" ]] || fail "mobile H5 index does not reference a JavaScript entry" frontend_asset_path="frontend/dist/${frontend_asset#/}" mobile_asset_rel="${mobile_asset#/}" mobile_asset_rel="${mobile_asset_rel#h5/}" mobile_asset_path="mobile-uni/dist/build/h5/$mobile_asset_rel" require_file "$frontend_asset_path" require_file "$mobile_asset_path" grep -q '/h5/' "$mobile_index" || fail "mobile H5 index does not contain the /h5/ base path" if [[ "${RELEASE_VERIFY_REMOTE_BACKEND:-false}" == "true" && "${RELEASE_VERIFY_REMOTE_MATCH:-false}" != "true" ]]; then fail "RELEASE_VERIFY_REMOTE_BACKEND=true requires RELEASE_VERIFY_REMOTE_MATCH=true" fi head_epoch="$(git show -s --format=%ct HEAD)" file_epoch() { if stat -f %m "$1" >/dev/null 2>&1; then stat -f %m "$1" else stat -c %Y "$1" fi } require_fresh_artifact() { local artifact="$1" local artifact_epoch artifact_epoch="$(file_epoch "$artifact")" || fail "cannot read artifact timestamp: $artifact" [[ "$artifact_epoch" -ge "$head_epoch" ]] || fail "artifact is older than HEAD; rebuild before release: $artifact" } require_fresh_artifact "$frontend_index" require_fresh_artifact "$frontend_asset_path" require_fresh_artifact "$mobile_index" require_fresh_artifact "$mobile_asset_path" require_fresh_artifact "$backend_jar" require_remote_business_success() { local label="$1" local url="$2" local body body="$(curl -fsS --max-time 15 "$url")" || fail "$label HTTP check failed: $url" if ! printf '%s' "$body" | LC_ALL=C grep -Eq '^[[:space:]]*\{[[:space:]]*"code"[[:space:]]*:[[:space:]]*200([[:space:]]*[,}])'; then fail "$label business check failed: $url" fi echo "$label=200 $url" } changed_files="$(git status --porcelain)" [[ -z "$changed_files" ]] || fail "worktree has uncommitted changes; commit the release batch before publishing" echo "commit=$(git rev-parse HEAD)" echo "head_epoch=$head_epoch" echo "worktree=clean" echo "frontend_index_sha256=$(sha256 "$frontend_index")" echo "frontend_asset=$frontend_asset" echo "frontend_asset_sha256=$(sha256 "$frontend_asset_path")" echo "mobile_index_sha256=$(sha256 "$mobile_index")" echo "mobile_asset=$mobile_asset" echo "mobile_asset_sha256=$(sha256 "$mobile_asset_path")" echo "backend_jar_sha256=$(sha256 "$backend_jar")" if [[ -n "${RELEASE_REMOTE_URL:-}" ]]; then remote="${RELEASE_REMOTE_URL%/}" curl -fsS --max-time 15 "$remote/" >/dev/null || fail "remote root check failed: $remote/" echo "remote_root=200 $remote/" require_remote_business_success "remote_tenant_list" "$remote/prod-api/auth/tenant/list" require_remote_business_success "remote_mobile_home" "$remote/prod-api/api/aihr/mobile/home/user" if [[ "${RELEASE_VERIFY_REMOTE_MATCH:-false}" == "true" ]]; then remote_frontend_asset="$(curl -fsS --max-time 15 "$remote/" | sed -nE 's/.*src="(\/assets\/[^" ]+\.js)".*/\1/p' | head -n 1)" remote_mobile_asset="$(curl -fsS --max-time 15 "$remote/h5/" | sed -nE 's/.*src="(\/h5\/assets\/[^" ]+\.js)".*/\1/p' | head -n 1)" [[ "$remote_frontend_asset" == /assets/*.js ]] || fail "remote frontend entry asset not found" [[ "$remote_mobile_asset" == /h5/assets/*.js ]] || fail "remote mobile H5 entry asset not found" remote_frontend_sha256="$(curl -fsS --max-time 15 "$remote$remote_frontend_asset" | sha256_stream)" remote_mobile_sha256="$(curl -fsS --max-time 15 "$remote$remote_mobile_asset" | sha256_stream)" local_frontend_sha256="$(sha256 "$frontend_asset_path")" local_mobile_sha256="$(sha256 "$mobile_asset_path")" echo "remote_frontend_asset=$remote_frontend_asset" echo "remote_frontend_asset_sha256=$remote_frontend_sha256" echo "remote_mobile_asset=$remote_mobile_asset" echo "remote_mobile_asset_sha256=$remote_mobile_sha256" [[ "$remote_frontend_sha256" == "$local_frontend_sha256" ]] || fail "remote frontend asset does not match local build" [[ "$remote_mobile_sha256" == "$local_mobile_sha256" ]] || fail "remote mobile H5 asset does not match local build" echo "remote_asset_match=true" if [[ "${RELEASE_VERIFY_REMOTE_BACKEND:-false}" == "true" ]]; then command -v ssh >/dev/null 2>&1 || fail "ssh is required for remote backend verification" remote_ssh="${RELEASE_REMOTE_SSH:-YCWY}" remote_backend_path="${RELEASE_REMOTE_BACKEND_PATH:-/opt/wygj/app/ruoyi-admin.jar}" [[ "$remote_backend_path" =~ ^/[A-Za-z0-9._/-]+$ ]] || fail "remote backend path must be an absolute safe path: $remote_backend_path" remote_backend_jar_sha256="$(ssh -o BatchMode=yes -o ConnectTimeout=10 "$remote_ssh" "sha256sum '$remote_backend_path'" | awk '{print $1}')" || fail "remote backend hash check failed: $remote_ssh:$remote_backend_path" [[ "$remote_backend_jar_sha256" =~ ^[0-9a-f]{64}$ ]] || fail "remote backend hash is invalid: $remote_ssh:$remote_backend_path" local_backend_jar_sha256="$(sha256 "$backend_jar")" local_backend_module_jar="$(mktemp)" unzip -p "$backend_jar" "BOOT-INF/lib/$backend_module_jar_name" > "$local_backend_module_jar" local_backend_module_sha256="$(normalized_jar_content_sha256 "$local_backend_module_jar")" rm -f "$local_backend_module_jar" remote_backend_module_sha256="$(ssh -o BatchMode=yes -o ConnectTimeout=10 "$remote_ssh" bash -s -- "$remote_backend_path" "$backend_module_jar_name" <<'REMOTE' set -euo pipefail outer="$1" module="$2" nested="$(mktemp)" manifest="$(mktemp)" cleanup() { rm -f "$nested" "$manifest" } trap cleanup EXIT unzip -p "$outer" "BOOT-INF/lib/$module" > "$nested" while IFS= read -r entry; do case "$entry" in */|META-INF/*.SF|META-INF/*.RSA|META-INF/*.DSA) continue ;; esac printf '%s %s\n' "$(unzip -p "$nested" "$entry" | sha256sum | awk '{print $1}')" "$entry" >> "$manifest" done < <(unzip -Z1 "$nested" | LC_ALL=C sort) sha256sum "$manifest" | awk '{print $1}' REMOTE )" || fail "remote backend module hash check failed: $remote_ssh:$remote_backend_path" [[ "$remote_backend_module_sha256" =~ ^[0-9a-f]{64}$ ]] || fail "remote backend module hash is invalid: $remote_ssh:$remote_backend_path" echo "remote_backend_jar_sha256=$remote_backend_jar_sha256" echo "local_backend_jar_sha256=$local_backend_jar_sha256" echo "backend_module=$backend_module_jar_name" echo "remote_backend_module_sha256=$remote_backend_module_sha256" echo "local_backend_module_sha256=$local_backend_module_sha256" [[ "$remote_backend_module_sha256" == "$local_backend_module_sha256" ]] || fail "remote AIHR module does not match local build" echo "remote_backend_module_match=true" fi fi fi echo "release-preflight: read-only checks passed"