fix(release): enforce August 1 business readiness
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
#!/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_OWNER_SIGNOFFS=5/5"
|
||||
echo "AUG1_RELEASE_READINESS=PASS"
|
||||
REMOTE
|
||||
Reference in New Issue
Block a user