289 lines
12 KiB
Bash
Executable File
289 lines
12 KiB
Bash
Executable File
#!/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
|
|
if [[ "${RELEASE_VERIFY_REMOTE_BACKEND:-false}" == "true" && "${RELEASE_VERIFY_REMOTE_SCHEMA:-false}" != "true" ]]; then
|
|
fail "RELEASE_VERIFY_REMOTE_BACKEND=true requires RELEASE_VERIFY_REMOTE_SCHEMA=true"
|
|
fi
|
|
if [[ "${RELEASE_VERIFY_REMOTE_SCHEMA:-false}" == "true" && "${RELEASE_VERIFY_REMOTE_MATCH:-false}" != "true" ]]; then
|
|
fail "RELEASE_VERIFY_REMOTE_SCHEMA=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"
|
|
}
|
|
|
|
require_remote_schema() {
|
|
local remote_ssh="${RELEASE_REMOTE_SSH:-YCWY}"
|
|
local remote_db="${RELEASE_REMOTE_DB_NAME:-ry-vue}"
|
|
[[ "$remote_db" =~ ^[A-Za-z0-9_-]+$ ]] || fail "remote database name is invalid: $remote_db"
|
|
|
|
local issues
|
|
issues="$(ssh -o BatchMode=yes -o ConnectTimeout=10 "$remote_ssh" bash -s -- "$remote_db" <<'REMOTE'
|
|
set -euo pipefail
|
|
db="$1"
|
|
mysql --batch --skip-column-names --connect-timeout=5 "$db" <<'SQL'
|
|
SELECT CONCAT('missing_table:', required.table_name) AS issue
|
|
FROM (
|
|
SELECT 'aihr_prompt_template' AS table_name
|
|
UNION ALL SELECT 'aihr_practice_scenario'
|
|
UNION ALL SELECT 'aihr_practice_rubric'
|
|
UNION ALL SELECT 'aihr_practice_rubric_dimension'
|
|
UNION ALL SELECT 'aihr_practice_audio'
|
|
UNION ALL SELECT 'aihr_practice_audio_upload'
|
|
UNION ALL SELECT 'aihr_practice_calibration'
|
|
UNION ALL SELECT 'aihr_knowledge_gap'
|
|
UNION ALL SELECT 'aihr_knowledge_answer_feedback'
|
|
UNION ALL SELECT 'aihr_candidate_material'
|
|
UNION ALL SELECT 'aihr_interview_result'
|
|
UNION ALL SELECT 'aihr_candidate_employee_link'
|
|
UNION ALL SELECT 'aihr_position_responsibility'
|
|
UNION ALL SELECT 'aihr_sop_applicability'
|
|
UNION ALL SELECT 'aihr_onboard_task'
|
|
UNION ALL SELECT 'aihr_qualification_gate'
|
|
) required
|
|
LEFT JOIN information_schema.tables actual
|
|
ON actual.table_schema = DATABASE() AND actual.table_name = required.table_name
|
|
WHERE actual.table_name IS NULL
|
|
UNION ALL
|
|
SELECT 'missing_column:sys_oss.tenant_id'
|
|
WHERE NOT EXISTS (
|
|
SELECT 1
|
|
FROM information_schema.columns
|
|
WHERE table_schema = DATABASE()
|
|
AND table_name = 'sys_oss'
|
|
AND column_name = 'tenant_id'
|
|
)
|
|
UNION ALL
|
|
SELECT CONCAT(
|
|
'tenant_collation:', aihr.table_name, '.', aihr.column_name,
|
|
'=', COALESCE(aihr.collation_name, 'NULL'),
|
|
',expected=', core.collation_name
|
|
)
|
|
FROM information_schema.columns aihr
|
|
JOIN information_schema.columns core
|
|
ON core.table_schema = aihr.table_schema
|
|
AND core.table_name = 'sys_oss'
|
|
AND core.column_name = 'tenant_id'
|
|
WHERE aihr.table_schema = DATABASE()
|
|
AND LEFT(aihr.table_name, 5) = 'aihr_'
|
|
AND aihr.column_name = 'tenant_id'
|
|
AND aihr.collation_name <> core.collation_name
|
|
ORDER BY issue;
|
|
SQL
|
|
REMOTE
|
|
)" || fail "remote schema check failed: $remote_ssh:$remote_db"
|
|
|
|
[[ -z "$issues" ]] || fail "remote schema incompatible: $(printf '%s' "$issues" | tr '\n' ', ' | sed 's/, $//')"
|
|
|
|
local processing_join_count
|
|
processing_join_count="$(ssh -o BatchMode=yes -o ConnectTimeout=10 "$remote_ssh" bash -s -- "$remote_db" <<'REMOTE'
|
|
set -euo pipefail
|
|
db="$1"
|
|
mysql --batch --skip-column-names --connect-timeout=5 "$db" <<'SQL'
|
|
SELECT COUNT(*)
|
|
FROM aihr_knowledge_attach a
|
|
LEFT JOIN sys_oss o
|
|
ON o.oss_id = a.oss_id
|
|
AND o.tenant_id = a.tenant_id;
|
|
SQL
|
|
REMOTE
|
|
)" || fail "remote processing join check failed: $remote_ssh:$remote_db"
|
|
[[ "$processing_join_count" =~ ^[0-9]+$ ]] || fail "remote processing join count is invalid: $processing_join_count"
|
|
echo "remote_schema=16/16 $remote_ssh:$remote_db"
|
|
echo "remote_tenant_collation=aligned"
|
|
echo "remote_processing_join_count=$processing_join_count"
|
|
}
|
|
|
|
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_SCHEMA:-false}" == "true" ]]; then
|
|
require_remote_schema
|
|
fi
|
|
|
|
if [[ "${RELEASE_VERIFY_REMOTE_MATCH:-false}" == "true" ]]; then
|
|
if [[ "${RELEASE_VERIFY_REMOTE_STATIC:-true}" == "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"
|
|
else
|
|
echo "remote_static_match=skipped"
|
|
fi
|
|
|
|
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_sha_file="$(mktemp)"
|
|
ssh -o BatchMode=yes -o ConnectTimeout=10 "$remote_ssh" bash -s -- "$remote_backend_path" "$backend_module_jar_name" > "$remote_backend_module_sha_file" <<'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
|
|
remote_backend_module_sha256="$(sed -n '1p' "$remote_backend_module_sha_file")"
|
|
rm -f "$remote_backend_module_sha_file"
|
|
[[ -n "$remote_backend_module_sha256" ]] || fail "remote backend module hash check returned no value: $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"
|