232 lines
8.0 KiB
Bash
232 lines
8.0 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
|
|
scenario_snapshots_b64="$(
|
|
printf '%s\n' "${scenario_snapshots[@]}" | base64 | tr -d '\r\n'
|
|
)"
|
|
[[ "$scenario_snapshots_b64" =~ ^[A-Za-z0-9+/=]+$ ]] || {
|
|
echo "FAIL: strict approval snapshots could not be encoded safely" >&2
|
|
exit 4
|
|
}
|
|
|
|
ssh -o BatchMode=yes -o ConnectTimeout=10 "$REMOTE_SSH" bash -s -- \
|
|
"$REMOTE_SERVICE" "$REMOTE_DB" "$TENANT_ID" "$scenario_snapshots_b64" <<'REMOTE'
|
|
set -euo pipefail
|
|
|
|
service="$1"
|
|
db="$2"
|
|
tenant="$3"
|
|
snapshots_b64="$4"
|
|
mapfile -t snapshots < <(printf '%s' "$snapshots_b64" | base64 --decode)
|
|
[[ "${#snapshots[@]}" -eq 5 ]] || {
|
|
echo "FAIL: production received an invalid scenario snapshot payload" >&2
|
|
exit 9
|
|
}
|
|
|
|
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
|