feat(aihr): finalize practice growth and access boundaries

This commit is contained in:
2026-07-25 00:42:19 +08:00
parent a90f40e4d8
commit c9d2012ec1
87 changed files with 6804 additions and 2538 deletions
+143
View File
@@ -71,6 +71,9 @@ 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
if [[ "${AIHR_PRACTICE_RUNTIME_SCHEMA_BOOTSTRAP:-false}" == "true" ]]; then
fail "AIHR_PRACTICE_RUNTIME_SCHEMA_BOOTSTRAP must stay false for a release; apply the formal SQL migration instead"
fi
head_epoch="$(git show -s --format=%ct HEAD)"
@@ -106,6 +109,60 @@ require_remote_business_success() {
echo "$label=200 $url"
}
require_remote_practice_schema_guard() {
local remote_ssh="${RELEASE_REMOTE_SSH:-YCWY}"
local remote_service="${RELEASE_REMOTE_SERVICE:-wygj-aihr.service}"
[[ "$remote_service" =~ ^[A-Za-z0-9_.@-]+\.service$ ]] \
|| fail "remote systemd service name is invalid: $remote_service"
local state
state="$(ssh -o BatchMode=yes -o ConnectTimeout=10 "$remote_ssh" bash -s -- "$remote_service" <<'REMOTE'
set -euo pipefail
service="$1"
matches_enabled_guard() {
grep -Eiq '(AIHR_PRACTICE_RUNTIME_SCHEMA_BOOTSTRAP|aihr\.practice\.runtime-schema-bootstrap)[[:space:]]*=[[:space:]]*"?true"?'
}
runtime_environment="$(systemctl show "$service" --property=Environment --value --no-pager)"
unit_source="$(systemctl cat "$service")"
if printf '%s\n' "$runtime_environment" | matches_enabled_guard \
|| printf '%s\n' "$unit_source" | matches_enabled_guard; then
printf 'enabled\n'
exit 0
fi
while IFS= read -r environment_file; do
[[ -z "$environment_file" ]] && continue
if [[ ! -r "$environment_file" ]]; then
printf 'unreadable:%s\n' "$environment_file"
exit 0
fi
if matches_enabled_guard < "$environment_file"; then
printf 'enabled\n'
exit 0
fi
done < <(printf '%s\n' "$unit_source" | sed -nE 's/^[[:space:]]*EnvironmentFile=-?([^[:space:]#]+).*/\1/p')
printf 'disabled-or-default\n'
REMOTE
)" || fail "remote runtime schema guard check failed: $remote_ssh:$remote_service"
case "$state" in
disabled-or-default)
echo "remote_practice_runtime_schema_bootstrap=false/default $remote_ssh:$remote_service"
;;
enabled)
fail "remote AIHR_PRACTICE_RUNTIME_SCHEMA_BOOTSTRAP is enabled; disable it and apply formal SQL migrations"
;;
unreadable:*)
fail "remote runtime schema guard cannot inspect ${state#unreadable:}; grant read access or unset the file before release"
;;
*)
fail "remote runtime schema guard returned an unexpected state: $state"
;;
esac
}
require_remote_schema() {
local remote_ssh="${RELEASE_REMOTE_SSH:-YCWY}"
local remote_db="${RELEASE_REMOTE_DB_NAME:-ry-vue}"
@@ -232,6 +289,87 @@ REMOTE
[[ -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_practice_curriculum_columns
missing_practice_curriculum_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_practice_scenario' AS table_name, 'growth_level' AS column_name
UNION ALL SELECT 'aihr_practice_scenario', 'competency_code'
UNION ALL SELECT 'aihr_practice_scenario', 'collaboration_positions'
UNION ALL SELECT 'aihr_practice_scenario', 'review_status'
UNION ALL SELECT 'aihr_practice_scenario', 'risk_level'
UNION ALL SELECT 'aihr_practice_scenario', 'curriculum_version'
UNION ALL SELECT 'aihr_practice_scenario', 'reviewer'
UNION ALL SELECT 'aihr_practice_scenario', 'reviewer_user_id'
UNION ALL SELECT 'aihr_practice_scenario', 'reviewed_time'
UNION ALL SELECT 'aihr_practice_scenario', 'second_reviewer'
UNION ALL SELECT 'aihr_practice_scenario', 'second_reviewer_user_id'
UNION ALL SELECT 'aihr_practice_scenario', 'second_reviewed_time'
UNION ALL SELECT 'aihr_practice_session', 'position_snapshot'
UNION ALL SELECT 'aihr_practice_session', 'project_type_snapshot'
UNION ALL SELECT 'aihr_practice_session', 'growth_level'
UNION ALL SELECT 'aihr_practice_session', 'competency_code'
UNION ALL SELECT 'aihr_practice_session', 'collaboration_positions'
UNION ALL SELECT 'aihr_practice_session', 'curriculum_version'
UNION ALL SELECT 'aihr_practice_session', 'growth_confirmation_level'
UNION ALL SELECT 'aihr_practice_session', 'growth_confirmed_by'
UNION ALL SELECT 'aihr_practice_session', 'growth_confirmed_time'
) 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 practice-curriculum schema check failed: $remote_ssh:$remote_db"
[[ -z "$missing_practice_curriculum_columns" ]] \
|| fail "remote practice-curriculum schema missing required columns: $(printf '%s' "$missing_practice_curriculum_columns" | tr '\n' ', ' | sed 's/, $//')"
local practice_assignment_request_unique_index
practice_assignment_request_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(
COALESCE(MIN(non_unique), -1), '|',
COALESCE(GROUP_CONCAT(column_name ORDER BY seq_in_index), ''), '|',
COALESCE(SUM(sub_part IS NOT NULL), 0)
)
FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name = 'aihr_practice_assignment'
AND index_name = 'uk_aihr_assignment_request';
SQL
REMOTE
)" || fail "remote practice-assignment idempotency-index check failed: $remote_ssh:$remote_db"
[[ "$practice_assignment_request_unique_index" = "0|tenant_id,request_key,ext_party_id|0" ]] \
|| fail "remote practice-assignment idempotency index invalid: expected unique full columns tenant_id,request_key,ext_party_id; got ${practice_assignment_request_unique_index:-missing}"
local practice_assignment_recent_content_index
practice_assignment_recent_content_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(
COALESCE(MIN(non_unique), -1), '|',
COALESCE(GROUP_CONCAT(column_name ORDER BY seq_in_index), ''), '|',
COALESCE(SUM(sub_part IS NOT NULL), 0)
)
FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name = 'aihr_practice_assignment'
AND index_name = 'idx_aihr_assignment_recent_content';
SQL
REMOTE
)" || fail "remote practice-assignment recent-content-index check failed: $remote_ssh:$remote_db"
[[ "$practice_assignment_recent_content_index" = "1|tenant_id,ext_party_id,source,content_hash,create_time|0" ]] \
|| fail "remote practice-assignment recent-content index invalid: expected full columns tenant_id,ext_party_id,source,content_hash,create_time; got ${practice_assignment_recent_content_index:-missing}"
local agent_schema_contract
agent_schema_contract="$(ssh -o BatchMode=yes -o ConnectTimeout=10 "$remote_ssh" bash -s -- "$remote_db" <<'REMOTE'
set -euo pipefail
@@ -597,6 +735,7 @@ REMOTE
|| fail "remote work-assistant schema missing required columns: $(printf '%s' "$missing_work_assistant_columns" | tr '\n' ', ' | sed 's/, $//')"
echo "remote_schema=64/64 $remote_ssh:$remote_db"
echo "remote_work_report_idempotency=3/3 $remote_ssh:$remote_db"
echo "remote_practice_curriculum_columns=13/13 $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"
@@ -639,6 +778,10 @@ if [[ -n "${RELEASE_REMOTE_URL:-}" ]]; then
require_remote_schema
fi
if [[ "${RELEASE_VERIFY_REMOTE_SCHEMA:-false}" == "true" || "${RELEASE_VERIFY_REMOTE_BACKEND:-false}" == "true" ]]; then
require_remote_practice_schema_guard
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)"