Files
prop-ai-hr/scripts/verify-aug1-release-readiness.sh
T

221 lines
7.5 KiB
Bash

#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
FORMAL_VERIFIER="$ROOT_DIR/scripts/verify-aug1-formal-content.mjs"
APPROVAL_PATH="${AIHR_AUG1_APPROVAL_PATH:-$ROOT_DIR/docs/content-candidates/aug1-release-approval.json}"
REMOTE_SSH="${AIHR_AUG1_REMOTE_SSH:-YCWY}"
REMOTE_SERVICE="${AIHR_AUG1_REMOTE_SERVICE:-wygj-aihr.service}"
REMOTE_DB="${AIHR_AUG1_REMOTE_DB:-ry-vue}"
TENANT_ID="${AIHR_AUG1_TENANT_ID:-000000}"
if [[ "${1:-}" != "--execute" || -n "${2:-}" ]]; then
cat <<'USAGE'
Usage: ./scripts/verify-aug1-release-readiness.sh --execute
Runs the final read-only August 1 business release gate. It first requires a
strictly approved formal-content manifest, then verifies that production uses
fixed-code login without real SMS, each direct channel has at least two active
handlers, and the five approved scenario snapshots are published unchanged.
It does not print identities, the fixed code, source content, or access tokens.
USAGE
exit 2
fi
[[ "$REMOTE_DB" =~ ^[A-Za-z0-9_-]+$ ]] || {
echo "FAIL: invalid remote database name" >&2
exit 3
}
[[ "$TENANT_ID" =~ ^[A-Za-z0-9_-]{1,20}$ ]] || {
echo "FAIL: invalid tenant ID" >&2
exit 3
}
[[ "$REMOTE_SERVICE" =~ ^[A-Za-z0-9_.@-]+\.service$ ]] || {
echo "FAIL: invalid remote service name" >&2
exit 3
}
mapfile -t scenario_snapshots < <(
node "$FORMAL_VERIFIER" --scenario-snapshots "$APPROVAL_PATH"
)
[[ "${#scenario_snapshots[@]}" -eq 5 ]] || {
echo "FAIL: strict approval did not return exactly five scenario snapshots" >&2
exit 4
}
for snapshot in "${scenario_snapshots[@]}"; do
[[ "$snapshot" =~ ^[a-z0-9][a-z0-9-]{1,79}\|[A-Za-z0-9._-]{1,30}\|[0-9a-f]{64}$ ]] || {
echo "FAIL: strict approval returned an invalid scenario snapshot" >&2
exit 4
}
done
ssh -o BatchMode=yes -o ConnectTimeout=10 "$REMOTE_SSH" bash -s -- \
"$REMOTE_SERVICE" "$REMOTE_DB" "$TENANT_ID" "${scenario_snapshots[@]}" <<'REMOTE'
set -euo pipefail
service="$1"
db="$2"
tenant="$3"
shift 3
snapshots=("$@")
pid="$(systemctl show -p MainPID --value "$service")"
[[ -n "$pid" && "$pid" != "0" ]] || {
echo "FAIL: production service is not running" >&2
exit 10
}
fixed_code_present="false"
fixed_mode_enabled="false"
while IFS='=' read -r key value; do
case "$key" in
AIHR_SMS_DEV_FIXED_CODE)
[[ -n "$value" ]] && fixed_code_present="true"
;;
AIHR_SMS_PROD_FIXED_CODE_ENABLED)
[[ "${value,,}" == "true" ]] && fixed_mode_enabled="true"
;;
esac
done < <(tr '\0' '\n' < "/proc/$pid/environ")
[[ "$fixed_code_present" == "true" && "$fixed_mode_enabled" == "true" ]] || {
echo "FAIL: production fixed-code mode is not explicitly enabled; real SMS must not be used for this release" >&2
exit 11
}
echo "AUG1_FIXED_CODE_NO_REAL_SMS_MODE=PASS"
required_roles=(
direct_president
direct_finance
direct_hr
direct_audit
direct_operations
)
declare -A handler_counts=()
while IFS=$'\t' read -r role_key handler_count; do
[[ "$role_key" =~ ^direct_(president|finance|hr|audit|operations)$ ]] || {
echo "FAIL: production returned an unexpected direct-channel role" >&2
exit 12
}
[[ "$handler_count" =~ ^[0-9]+$ ]] || {
echo "FAIL: production returned an invalid direct-channel handler count" >&2
exit 12
}
handler_counts["$role_key"]="$handler_count"
done < <(
mysql --batch --skip-column-names --connect-timeout=5 "$db" <<SQL
SELECT required.role_key, COUNT(DISTINCT u.user_id) AS handler_count
FROM (
SELECT 'direct_president' AS role_key
UNION ALL SELECT 'direct_finance'
UNION ALL SELECT 'direct_hr'
UNION ALL SELECT 'direct_audit'
UNION ALL SELECT 'direct_operations'
) required
LEFT JOIN sys_role r
ON r.tenant_id = '$tenant'
AND r.role_key = required.role_key
AND r.status = '0'
AND r.del_flag = '0'
LEFT JOIN sys_user_role ur
ON ur.role_id = r.role_id
LEFT JOIN sys_user u
ON u.tenant_id = r.tenant_id
AND u.user_id = ur.user_id
AND u.status = '0'
AND u.del_flag = '0'
GROUP BY required.role_key
ORDER BY required.role_key;
SQL
)
for role_key in "${required_roles[@]}"; do
handler_count="${handler_counts[$role_key]:-0}"
(( handler_count >= 2 )) || {
echo "FAIL: $role_key requires an active primary handler and backup; got $handler_count/2" >&2
exit 13
}
echo "AUG1_DIRECT_HANDLER_COUNT_${role_key^^}=$handler_count"
done
declare -A expected_versions=()
declare -A expected_hashes=()
scenario_sql_values=""
for snapshot in "${snapshots[@]}"; do
IFS='|' read -r scenario_code content_version content_hash <<<"$snapshot"
[[ "$scenario_code" =~ ^[a-z0-9][a-z0-9-]{1,79}$ ]]
[[ "$content_version" =~ ^[A-Za-z0-9._-]{1,30}$ ]]
[[ "$content_hash" =~ ^[0-9a-f]{64}$ ]]
expected_versions["$scenario_code"]="$content_version"
expected_hashes["$scenario_code"]="$content_hash"
if [[ -n "$scenario_sql_values" ]]; then
scenario_sql_values+=","
fi
scenario_sql_values+="'$scenario_code'"
done
declare -A observed_scenarios=()
while IFS=$'\t' read -r scenario_code content_version content_hash enabled review_status risk_level \
reviewer_user_id reviewed_time second_reviewer_user_id second_reviewed_time sop_refs; do
[[ -n "${expected_versions[$scenario_code]:-}" ]] || {
echo "FAIL: production returned an unexpected August 1 scenario" >&2
exit 14
}
[[ "$content_version" == "${expected_versions[$scenario_code]}" ]] || {
echo "FAIL: $scenario_code production content version does not match the approved snapshot" >&2
exit 15
}
[[ "${content_hash,,}" == "${expected_hashes[$scenario_code]}" ]] || {
echo "FAIL: $scenario_code production content hash does not match the approved snapshot" >&2
exit 15
}
[[ "$enabled" == "1" && "$review_status" == "已发布" ]] || {
echo "FAIL: $scenario_code is not enabled and published" >&2
exit 16
}
[[ "$risk_level" == "常规" || "$risk_level" == "高风险" ]] || {
echo "FAIL: $scenario_code has not completed risk classification" >&2
exit 16
}
[[ "$reviewer_user_id" =~ ^[1-9][0-9]*$ && -n "$reviewed_time" ]] || {
echo "FAIL: $scenario_code is missing first-review evidence" >&2
exit 16
}
if [[ "$risk_level" == "高风险" ]]; then
[[ "$second_reviewer_user_id" =~ ^[1-9][0-9]*$ \
&& "$second_reviewer_user_id" != "$reviewer_user_id" \
&& -n "$second_reviewed_time" ]] || {
echo "FAIL: $scenario_code is high risk but lacks independent second-review evidence" >&2
exit 16
}
fi
[[ -n "$sop_refs" && "$sop_refs" != *"待补"* && "$sop_refs" != *"待定"* ]] || {
echo "FAIL: $scenario_code has no finalized SOP references" >&2
exit 16
}
observed_scenarios["$scenario_code"]="true"
done < <(
mysql --batch --skip-column-names --connect-timeout=5 "$db" <<SQL
SELECT scenario_code, content_version, content_hash, enabled, review_status, risk_level,
COALESCE(reviewer_user_id, 0), COALESCE(DATE_FORMAT(reviewed_time, '%Y-%m-%dT%H:%i:%s'), '-'),
COALESCE(second_reviewer_user_id, 0), COALESCE(DATE_FORMAT(second_reviewed_time, '%Y-%m-%dT%H:%i:%s'), '-'),
COALESCE(NULLIF(sop_refs, ''), '-')
FROM aihr_practice_scenario
WHERE tenant_id = '$tenant'
AND scenario_code IN ($scenario_sql_values)
ORDER BY scenario_code;
SQL
)
for snapshot in "${snapshots[@]}"; do
scenario_code="${snapshot%%|*}"
[[ "${observed_scenarios[$scenario_code]:-false}" == "true" ]] || {
echo "FAIL: approved scenario $scenario_code is missing from production" >&2
exit 17
}
done
echo "AUG1_FORMAL_SCENARIOS=5/5"
echo "AUG1_FORMAL_POLICY_QUESTIONS=30/30"
echo "AUG1_CHANNEL_ACCEPTANCE=5/5"
echo "AUG1_OWNER_SIGNOFFS=5/5"
echo "AUG1_DISTRIBUTION_APPROVAL=PASS"
echo "AUG1_RELEASE_READINESS=PASS"
REMOTE