Files
prop-ai-hr/scripts/release-preflight.sh
T
admin 163b08ecfe feat(aihr): 重构运营后台与租户知识治理
- 重构运营总览、侧栏和内容运营工作台\n- 增加大喇叭、成果审核与运营统计链路\n- 补齐租户、知识空间、分类和知识维护闭环\n- 加固标签页租户上下文、停用租户写保护与迁移预检
2026-07-23 01:24:22 +08:00

500 lines
22 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_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 missing
missing="$(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 required.table_name
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_knowledge_space_grant'
UNION ALL SELECT 'aihr_knowledge_app'
UNION ALL SELECT 'aihr_knowledge_app_space'
UNION ALL SELECT 'aihr_knowledge_category'
UNION ALL SELECT 'aihr_knowledge_query_log'
UNION ALL SELECT 'aihr_knowledge_conversation'
UNION ALL SELECT 'aihr_knowledge_admin_audit'
UNION ALL SELECT 'aihr_learning_question'
UNION ALL SELECT 'aihr_practice_question_feedback'
UNION ALL SELECT 'aihr_onboard_exam'
UNION ALL SELECT 'aihr_onboard_exam_target'
UNION ALL SELECT 'aihr_onboard_exam_question'
UNION ALL SELECT 'aihr_onboard_exam_attempt'
UNION ALL SELECT 'aihr_onboard_exam_answer'
UNION ALL SELECT 'aihr_web_search_provider'
UNION ALL SELECT 'aihr_web_ai_secret'
UNION ALL SELECT 'aihr_web_ai_query_audit'
UNION ALL SELECT 'aihr_community_question'
UNION ALL SELECT 'aihr_community_answer'
UNION ALL SELECT 'aihr_incentive_rule'
UNION ALL SELECT 'aihr_points_ledger'
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'
UNION ALL SELECT 'aihr_memory_candidate'
UNION ALL SELECT 'aihr_assistant_capture'
UNION ALL SELECT 'aihr_assistant_capture_status_log'
UNION ALL SELECT 'aihr_daily_work_result'
UNION ALL SELECT 'aihr_service_memory'
UNION ALL SELECT 'aihr_service_memory_version'
UNION ALL SELECT 'aihr_work_report'
UNION ALL SELECT 'aihr_broadcast_message'
UNION ALL SELECT 'aihr_broadcast_read'
UNION ALL SELECT 'aihr_broadcast_version'
) 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
ORDER BY required.table_name;
SQL
REMOTE
)" || fail "remote schema check failed: $remote_ssh:$remote_db"
[[ -z "$missing" ]] || fail "remote schema missing required tables: $(printf '%s' "$missing" | tr '\n' ', ' | sed 's/, $//')"
local missing_work_report_columns
missing_work_report_columns="$(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 required.column_name
FROM (
SELECT 'request_key' AS column_name
UNION ALL SELECT 'request_hash'
) required
LEFT JOIN information_schema.columns actual
ON actual.table_schema = DATABASE()
AND actual.table_name = 'aihr_work_report'
AND actual.column_name = required.column_name
WHERE actual.column_name IS NULL
ORDER BY required.column_name;
SQL
REMOTE
)" || fail "remote work-report schema check failed: $remote_ssh:$remote_db"
[[ -z "$missing_work_report_columns" ]] \
|| fail "remote work-report schema missing required columns: $(printf '%s' "$missing_work_report_columns" | tr '\n' ', ' | sed 's/, $//')"
local missing_knowledge_category_columns
missing_knowledge_category_columns="$(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(required.table_name, '.', required.column_name)
FROM (
SELECT 'aihr_knowledge_category' AS table_name, 'tenant_id' AS column_name
UNION ALL SELECT 'aihr_knowledge_category', 'knowledge_id'
UNION ALL SELECT 'aihr_knowledge_category', 'code'
UNION ALL SELECT 'aihr_knowledge_category', 'name'
UNION ALL SELECT 'aihr_knowledge_category', 'status'
UNION ALL SELECT 'aihr_knowledge_category', 'sort_order'
UNION ALL SELECT 'aihr_knowledge_attach', 'category_id'
) required
LEFT JOIN information_schema.columns actual
ON actual.table_schema = DATABASE()
AND actual.table_name = required.table_name
AND actual.column_name = required.column_name
WHERE actual.column_name IS NULL
ORDER BY required.table_name, required.column_name;
SQL
REMOTE
)" || fail "remote knowledge-category schema check failed: $remote_ssh:$remote_db"
[[ -z "$missing_knowledge_category_columns" ]] \
|| fail "remote knowledge-category schema missing required columns: $(printf '%s' "$missing_knowledge_category_columns" | tr '\n' ', ' | sed 's/, $//')"
local knowledge_attach_category_index
knowledge_attach_category_index="$(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(
MIN(non_unique), '|',
COALESCE(GROUP_CONCAT(column_name ORDER BY seq_in_index), ''), '|',
SUM(sub_part IS NOT NULL)
)
FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name = 'aihr_knowledge_attach'
AND index_name = 'idx_aihr_knowledge_attach_category';
SQL
REMOTE
)" || fail "remote knowledge-attachment category-index check failed: $remote_ssh:$remote_db"
[[ "$knowledge_attach_category_index" = "1|tenant_id,knowledge_id,category_id|0" ]] \
|| fail "remote knowledge-attachment category index invalid: expected full columns tenant_id,knowledge_id,category_id; got ${knowledge_attach_category_index:-missing}"
local knowledge_category_code_index
knowledge_category_code_index="$(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(
MIN(non_unique), '|',
COALESCE(GROUP_CONCAT(column_name ORDER BY seq_in_index), ''), '|',
SUM(sub_part IS NOT NULL)
)
FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name = 'aihr_knowledge_category'
AND index_name = 'uk_aihr_knowledge_category_code';
SQL
REMOTE
)" || fail "remote knowledge-category unique-index check failed: $remote_ssh:$remote_db"
[[ "$knowledge_category_code_index" = "0|tenant_id,knowledge_id,code|0" ]] \
|| fail "remote knowledge-category unique index invalid: expected unique full columns tenant_id,knowledge_id,code; got ${knowledge_category_code_index:-missing}"
local work_report_unique_index
work_report_unique_index="$(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(
MIN(non_unique), '|',
COALESCE(GROUP_CONCAT(column_name ORDER BY seq_in_index), ''), '|',
SUM(sub_part IS NOT NULL)
)
FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name = 'aihr_work_report'
AND index_name = 'uk_aihr_work_report_request';
SQL
REMOTE
)" || fail "remote work-report unique-index check failed: $remote_ssh:$remote_db"
[[ "$work_report_unique_index" = "0|tenant_id,submitter_user_id,request_key|0" ]] \
|| fail "remote work-report unique index invalid: expected unique full columns tenant_id,submitter_user_id,request_key; got ${work_report_unique_index:-missing}"
local missing_broadcast_publish_columns
missing_broadcast_publish_columns="$(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 required.column_name
FROM (
SELECT 'publish_request_key' AS column_name
UNION ALL SELECT 'publish_request_hash'
UNION ALL SELECT 'withdraw_reason'
) required
LEFT JOIN information_schema.columns actual
ON actual.table_schema = DATABASE()
AND actual.table_name = 'aihr_broadcast_message'
AND actual.column_name = required.column_name
WHERE actual.column_name IS NULL
ORDER BY required.column_name;
SQL
REMOTE
)" || fail "remote broadcast publish-audit schema check failed: $remote_ssh:$remote_db"
[[ -z "$missing_broadcast_publish_columns" ]] \
|| fail "remote broadcast message schema missing required columns: $(printf '%s' "$missing_broadcast_publish_columns" | tr '\n' ', ' | sed 's/, $//')"
local broadcast_publish_unique_index
broadcast_publish_unique_index="$(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(
MIN(non_unique), '|',
COALESCE(GROUP_CONCAT(column_name ORDER BY seq_in_index), ''), '|',
SUM(sub_part IS NOT NULL)
)
FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name = 'aihr_broadcast_message'
AND index_name = 'uk_aihr_broadcast_message_publish_request';
SQL
REMOTE
)" || fail "remote broadcast publish unique-index check failed: $remote_ssh:$remote_db"
[[ "$broadcast_publish_unique_index" = "0|tenant_id,published_by,publish_request_key|0" ]] \
|| fail "remote broadcast publish unique index invalid: expected unique full columns tenant_id,published_by,publish_request_key; got ${broadcast_publish_unique_index:-missing}"
local broadcast_read_unique_index
broadcast_read_unique_index="$(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(
MIN(non_unique), '|',
COALESCE(GROUP_CONCAT(column_name ORDER BY seq_in_index), ''), '|',
SUM(sub_part IS NOT NULL)
)
FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name = 'aihr_broadcast_read'
AND index_name = 'uk_aihr_broadcast_read';
SQL
REMOTE
)" || fail "remote broadcast-read unique-index check failed: $remote_ssh:$remote_db"
[[ "$broadcast_read_unique_index" = "0|tenant_id,message_id,user_id|0" ]] \
|| fail "remote broadcast-read unique index invalid: expected unique full columns tenant_id,message_id,user_id; got ${broadcast_read_unique_index:-missing}"
local broadcast_version_unique_index
broadcast_version_unique_index="$(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(
MIN(non_unique), '|',
COALESCE(GROUP_CONCAT(column_name ORDER BY seq_in_index), ''), '|',
SUM(sub_part IS NOT NULL)
)
FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name = 'aihr_broadcast_version'
AND index_name = 'uk_aihr_broadcast_version';
SQL
REMOTE
)" || fail "remote broadcast-version unique-index check failed: $remote_ssh:$remote_db"
[[ "$broadcast_version_unique_index" = "0|tenant_id,message_id,version|0" ]] \
|| fail "remote broadcast-version unique index invalid: expected unique full columns tenant_id,message_id,version; got ${broadcast_version_unique_index:-missing}"
local missing_work_assistant_columns
missing_work_assistant_columns="$(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(required.table_name, '.', required.column_name)
FROM (
SELECT 'aihr_memory_candidate' AS table_name, 'work_date' AS column_name
UNION ALL SELECT 'aihr_memory_candidate', 'source_snapshot_json'
UNION ALL SELECT 'aihr_assistant_capture', 'work_date'
UNION ALL SELECT 'aihr_assistant_capture', 'source_snapshot_json'
UNION ALL SELECT 'aihr_assistant_capture', 'business_status'
UNION ALL SELECT 'aihr_knowledge_conversation', 'project_code'
UNION ALL SELECT 'aihr_knowledge_conversation', 'broadcast_message_id'
UNION ALL SELECT 'aihr_daily_work_result', 'content_hash'
) required
LEFT JOIN information_schema.columns actual
ON actual.table_schema = DATABASE()
AND actual.table_name = required.table_name
AND actual.column_name = required.column_name
WHERE actual.column_name IS NULL
ORDER BY required.table_name, required.column_name;
SQL
REMOTE
)" || fail "remote work-assistant schema check failed: $remote_ssh:$remote_db"
[[ -z "$missing_work_assistant_columns" ]] \
|| fail "remote work-assistant schema missing required columns: $(printf '%s' "$missing_work_assistant_columns" | tr '\n' ', ' | sed 's/, $//')"
echo "remote_schema=47/47 $remote_ssh:$remote_db"
echo "remote_work_report_idempotency=3/3 $remote_ssh:$remote_db"
echo "remote_knowledge_category_columns=7/7 $remote_ssh:$remote_db"
echo "remote_broadcast_publish_audit_columns=3/3 $remote_ssh:$remote_db"
echo "remote_broadcast_publish_idempotency=3/3 $remote_ssh:$remote_db"
echo "remote_broadcast_read_idempotency=3/3 $remote_ssh:$remote_db"
echo "remote_broadcast_version_uniqueness=3/3 $remote_ssh:$remote_db"
echo "remote_broadcast_question_context=1/1 $remote_ssh:$remote_db"
echo "remote_work_assistant_columns=8/8 $remote_ssh:$remote_db"
}
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
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_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"