fix(release): enforce August 1 business readiness

This commit is contained in:
key
2026-07-29 12:40:11 +08:00
parent c0b55efc29
commit 63968f3368
11 changed files with 1036 additions and 20 deletions
+182
View File
@@ -0,0 +1,182 @@
import assert from 'node:assert/strict'
import { createHash } from 'node:crypto'
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { spawnSync } from 'node:child_process'
import test from 'node:test'
const testDir = path.dirname(fileURLToPath(import.meta.url))
const projectRoot = path.resolve(testDir, '..', '..')
const verifierPath = path.join(projectRoot, 'scripts', 'verify-aug1-formal-content.mjs')
const pendingApprovalPath = path.join(
projectRoot,
'docs',
'content-candidates',
'aug1-release-approval.json',
)
const candidatePath = path.join(
projectRoot,
'docs',
'content-candidates',
'aug1-life-advisor-content-candidates-v0.1.json',
)
const candidateBytes = await readFile(candidatePath)
const candidate = JSON.parse(candidateBytes.toString('utf8'))
const candidateSha256 = createHash('sha256').update(candidateBytes).digest('hex')
const source = {
sourceId: 'FORMAL-OPS-001',
title: '现行生活顾问服务作业标准',
version: '2026.07',
effectiveDate: '2026-07-01',
scope: '受控内测项目',
sha256: 'a'.repeat(64),
formalPolicy: true,
}
function validApproval() {
const scenarioApprovals = Object.fromEntries(candidate.scenarios.map((item, index) => {
const highRisk = item.proposedRiskLevel === '高风险'
return [item.candidateId, {
status: 'APPROVED',
scenarioCode: item.scenarioDraft.id,
riskLevel: item.proposedRiskLevel,
sourceRefs: ['FORMAL-OPS-001:section-1'],
reviewedBy: `reviewer-${index + 1}`,
reviewedAt: '2026-07-30T09:00:00+08:00',
...(highRisk ? {
secondReviewedBy: `second-reviewer-${index + 1}`,
secondReviewedAt: '2026-07-30T10:00:00+08:00',
} : {}),
businessEvidenceRef: `evidence/scenario-${index + 1}.pdf`,
productionContentVersion: `aug1-v${index + 1}`,
productionContentHash: String(index + 1).repeat(64),
}]
}))
const policyQuestionApprovals = Object.fromEntries(candidate.policyQuestionCandidates.map((item, index) => {
const answerDisposition = item.category === 'NO_EVIDENCE'
? 'FORMAL_NO_EVIDENCE_BOUNDARY'
: item.category === 'UNAUTHORIZED'
? 'FORMAL_UNAUTHORIZED_BOUNDARY'
: 'FORMALLY_SOURCED'
return [item.id, {
status: 'APPROVED',
answerDisposition,
sourceRefs: ['FORMAL-OPS-001:section-2'],
expectedBehavior: item.expectedBehavior,
observedBehavior: '验收结果与预期边界一致',
evidenceRef: `evidence/question-${index + 1}.json`,
evidenceSha256: 'b'.repeat(64),
reviewedBy: `question-reviewer-${index + 1}`,
reviewedAt: '2026-07-30T11:00:00+08:00',
}]
}))
const signoffs = Object.fromEntries([
'businessOwner',
'knowledgeOwner',
'trainingOwner',
'channelOwner',
'releaseOwner',
].map((role) => [role, {
status: 'APPROVED',
reviewedBy: `${role}-reviewer`,
reviewedAt: '2026-07-30T12:00:00+08:00',
}]))
return {
schemaVersion: '1.0',
releaseTarget: '2026-08-01',
tenantId: '000000',
candidateFile: 'aug1-life-advisor-content-candidates-v0.1.json',
candidateSha256,
releaseStatus: 'APPROVED',
formalContentVersion: 'aug1-formal-v1',
sourceRegistry: [source],
scenarioApprovals,
policyQuestionApprovals,
signoffs,
}
}
async function verifyApproval(approval, ...options) {
const tempDir = await mkdtemp(path.join(os.tmpdir(), 'aug1-formal-'))
const tempCandidatePath = path.join(tempDir, 'aug1-life-advisor-content-candidates-v0.1.json')
const tempApprovalPath = path.join(tempDir, 'approval.json')
try {
await writeFile(tempCandidatePath, candidateBytes)
await writeFile(tempApprovalPath, JSON.stringify(approval), 'utf8')
return spawnSync(process.execPath, [verifierPath, ...options, tempApprovalPath], {
cwd: projectRoot,
encoding: 'utf8',
})
} finally {
await rm(tempDir, { recursive: true, force: true })
}
}
test('audits the checked-in pending manifest without claiming release readiness', () => {
const audit = spawnSync(process.execPath, [verifierPath, pendingApprovalPath], {
cwd: projectRoot,
encoding: 'utf8',
})
assert.equal(audit.status, 0, audit.stderr)
assert.match(audit.stdout, /approved scenarios: 0\/5/)
assert.match(audit.stdout, /approved policy questions: 0\/30/)
const strict = spawnSync(process.execPath, [verifierPath, '--strict', pendingApprovalPath], {
cwd: projectRoot,
encoding: 'utf8',
})
assert.notEqual(strict.status, 0)
assert.match(strict.stderr, /releaseStatus must be APPROVED/)
})
test('accepts complete formal evidence and emits five immutable production snapshots', async () => {
const strict = await verifyApproval(validApproval(), '--strict')
assert.equal(strict.status, 0, strict.stderr)
assert.match(strict.stdout, /strict release ready: true/)
const snapshots = await verifyApproval(validApproval(), '--scenario-snapshots')
assert.equal(snapshots.status, 0, snapshots.stderr)
const lines = snapshots.stdout.trim().split(/\r?\n/)
assert.equal(lines.length, 5)
assert.match(lines[0], /^[a-z0-9-]+\|aug1-v1\|1{64}$/)
})
test('rejects incomplete or contradictory formal approvals', async (t) => {
const cases = [
['missing question', (approval) => {
delete approval.policyQuestionApprovals[candidate.policyQuestionCandidates[0].id]
}, /question approvals must cover exactly 30\/30/],
['wrong no-evidence behavior', (approval) => {
const item = candidate.policyQuestionCandidates.find((question) => question.category === 'NO_EVIDENCE')
approval.policyQuestionApprovals[item.id].answerDisposition = 'FORMALLY_SOURCED'
}, /must preserve the formal refusal boundary/],
['same high-risk reviewer', (approval) => {
const item = candidate.scenarios.find((scenario) => scenario.proposedRiskLevel === '高风险')
approval.scenarioApprovals[item.candidateId].secondReviewedBy =
approval.scenarioApprovals[item.candidateId].reviewedBy
}, /high-risk reviewers must be different people/],
['unknown source', (approval) => {
const item = candidate.scenarios[0]
approval.scenarioApprovals[item.candidateId].sourceRefs = ['MISSING-SOURCE:section-1']
}, /is not in sourceRegistry/],
['placeholder source version', (approval) => {
approval.sourceRegistry[0].version = '待填写'
}, /formal source version is required/],
['missing owner sign-off', (approval) => {
approval.signoffs.releaseOwner.status = 'PENDING'
}, /releaseOwner: sign-off must be APPROVED/],
]
for (const [name, mutate, expected] of cases) {
await t.test(name, async () => {
const approval = validApproval()
mutate(approval)
const result = await verifyApproval(approval, '--strict')
assert.notEqual(result.status, 0)
assert.match(result.stderr, expected)
})
}
})
@@ -0,0 +1,222 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
SCRIPT="$ROOT_DIR/scripts/verify-aug1-release-readiness.sh"
PREFLIGHT="$ROOT_DIR/scripts/release-preflight.sh"
DEV_SETUP="$ROOT_DIR/docs/DEV_SETUP.md"
bash -n "$SCRIPT"
bash -n "$PREFLIGHT"
pending_output="$("$SCRIPT" --execute 2>&1 || true)"
grep -Fq 'releaseStatus must be APPROVED' <<<"$pending_output"
grep -Fq 'strict approval did not return exactly five scenario snapshots' <<<"$pending_output"
test_tmp_base="${TMPDIR:-/tmp}"
test_tmp="$(mktemp -d "$test_tmp_base/aug1-readiness-test.XXXXXX")"
cleanup() {
case "$test_tmp" in
"$test_tmp_base"/aug1-readiness-test.*) rm -rf -- "$test_tmp" ;;
*) echo "FAIL: unsafe test cleanup path: $test_tmp" >&2; exit 1 ;;
esac
}
trap cleanup EXIT
cp "$ROOT_DIR/docs/content-candidates/aug1-life-advisor-content-candidates-v0.1.json" \
"$test_tmp/aug1-life-advisor-content-candidates-v0.1.json"
node - "$test_tmp/aug1-life-advisor-content-candidates-v0.1.json" \
"$test_tmp/approval.json" "$test_tmp/scenario-rows.tsv" <<'NODE'
const fs = require('node:fs')
const crypto = require('node:crypto')
const [candidatePath, approvalPath, rowsPath] = process.argv.slice(2)
const bytes = fs.readFileSync(candidatePath)
const candidate = JSON.parse(bytes)
const source = {
sourceId: 'FORMAL-OPS-001',
title: 'test-only formal operations standard',
version: 'test-v1',
effectiveDate: '2026-07-01',
scope: 'test-only',
sha256: 'a'.repeat(64),
formalPolicy: true,
}
const scenarioApprovals = {}
const rows = []
candidate.scenarios.forEach((item, index) => {
const highRisk = item.proposedRiskLevel === '高风险'
const version = `aug1-v${index + 1}`
const hash = String(index + 1).repeat(64)
scenarioApprovals[item.candidateId] = {
status: 'APPROVED',
scenarioCode: item.scenarioDraft.id,
riskLevel: item.proposedRiskLevel,
sourceRefs: ['FORMAL-OPS-001:section-1'],
reviewedBy: `reviewer-${index + 1}`,
reviewedAt: '2026-07-30T09:00:00+08:00',
...(highRisk ? {
secondReviewedBy: `second-reviewer-${index + 1}`,
secondReviewedAt: '2026-07-30T10:00:00+08:00',
} : {}),
businessEvidenceRef: `evidence/scenario-${index + 1}.pdf`,
productionContentVersion: version,
productionContentHash: hash,
}
rows.push([
item.scenarioDraft.id,
version,
hash,
'1',
'已发布',
item.proposedRiskLevel,
String(100 + index),
'2026-07-30T09:00:00',
highRisk ? String(200 + index) : '0',
highRisk ? '2026-07-30T10:00:00' : '-',
'FORMAL-OPS-001:section-1',
].join('\t'))
})
const policyQuestionApprovals = {}
candidate.policyQuestionCandidates.forEach((item, index) => {
policyQuestionApprovals[item.id] = {
status: 'APPROVED',
answerDisposition: item.category === 'NO_EVIDENCE'
? 'FORMAL_NO_EVIDENCE_BOUNDARY'
: item.category === 'UNAUTHORIZED'
? 'FORMAL_UNAUTHORIZED_BOUNDARY'
: 'FORMALLY_SOURCED',
sourceRefs: ['FORMAL-OPS-001:section-2'],
expectedBehavior: item.expectedBehavior,
observedBehavior: 'test-only observed result',
evidenceRef: `evidence/question-${index + 1}.json`,
evidenceSha256: 'b'.repeat(64),
reviewedBy: `question-reviewer-${index + 1}`,
reviewedAt: '2026-07-30T11:00:00+08:00',
}
})
const signoffs = Object.fromEntries([
'businessOwner',
'knowledgeOwner',
'trainingOwner',
'channelOwner',
'releaseOwner',
].map((role) => [role, {
status: 'APPROVED',
reviewedBy: `${role}-reviewer`,
reviewedAt: '2026-07-30T12:00:00+08:00',
}]))
fs.writeFileSync(approvalPath, JSON.stringify({
schemaVersion: '1.0',
releaseTarget: '2026-08-01',
tenantId: '000000',
candidateFile: 'aug1-life-advisor-content-candidates-v0.1.json',
candidateSha256: crypto.createHash('sha256').update(bytes).digest('hex'),
releaseStatus: 'APPROVED',
formalContentVersion: 'aug1-formal-v1',
sourceRegistry: [source],
scenarioApprovals,
policyQuestionApprovals,
signoffs,
}))
fs.writeFileSync(rowsPath, rows.join('\n') + '\n')
NODE
mock_bin="$test_tmp/mock-bin"
mkdir -p "$mock_bin"
cat > "$mock_bin/systemctl" <<'MOCK_SYSTEMCTL'
#!/usr/bin/env bash
set -euo pipefail
printf '%s\n' "$MOCK_SERVICE_PID"
MOCK_SYSTEMCTL
cat > "$mock_bin/mysql" <<'MOCK_MYSQL'
#!/usr/bin/env bash
set -euo pipefail
query="$(cat)"
if [[ "$query" == *'COUNT(DISTINCT u.user_id)'* ]]; then
handler_count="${MOCK_HANDLER_COUNT:-2}"
printf 'direct_audit\t%s\n' "$handler_count"
printf 'direct_finance\t%s\n' "$handler_count"
printf 'direct_hr\t%s\n' "$handler_count"
printf 'direct_operations\t%s\n' "$handler_count"
printf 'direct_president\t%s\n' "$handler_count"
elif [[ "$query" == *'FROM aihr_practice_scenario'* ]]; then
cat "$MOCK_SCENARIO_ROWS"
else
echo 'FAIL: unexpected mock MySQL query' >&2
exit 90
fi
MOCK_MYSQL
cat > "$mock_bin/ssh" <<'MOCK_SSH'
#!/usr/bin/env bash
set -euo pipefail
while (($#)); do
if [[ "$1" == '--' ]]; then
shift
break
fi
shift
done
remote_script="$MOCK_REMOTE_SCRIPT"
cat > "$remote_script"
export MOCK_SERVICE_PID="$$"
PATH="$MOCK_REMOTE_BIN:$PATH" bash "$remote_script" "$@"
MOCK_SSH
chmod +x "$mock_bin/systemctl" "$mock_bin/mysql" "$mock_bin/ssh"
run_mock_readiness() {
PATH="$mock_bin:$PATH" \
MOCK_REMOTE_BIN="$mock_bin" \
MOCK_REMOTE_SCRIPT="$test_tmp/remote.sh" \
MOCK_SCENARIO_ROWS="$test_tmp/scenario-rows.tsv" \
AIHR_SMS_DEV_FIXED_CODE='test-only-fixed-code' \
AIHR_SMS_PROD_FIXED_CODE_ENABLED="${AIHR_SMS_PROD_FIXED_CODE_ENABLED:-true}" \
AIHR_AUG1_APPROVAL_PATH="$test_tmp/approval.json" \
"$SCRIPT" --execute
}
ready_output="$(run_mock_readiness)"
grep -Fq 'AUG1_FIXED_CODE_NO_REAL_SMS_MODE=PASS' <<<"$ready_output"
grep -Fq 'AUG1_FORMAL_SCENARIOS=5/5' <<<"$ready_output"
grep -Fq 'AUG1_RELEASE_READINESS=PASS' <<<"$ready_output"
handler_failure="$(
MOCK_HANDLER_COUNT=1 run_mock_readiness 2>&1 || true
)"
grep -Fq 'requires an active primary handler and backup; got 1/2' <<<"$handler_failure"
fixed_mode_failure="$(
AIHR_SMS_PROD_FIXED_CODE_ENABLED=false run_mock_readiness 2>&1 || true
)"
grep -Fq 'production fixed-code mode is not explicitly enabled' <<<"$fixed_mode_failure"
grep -Fq 'AIHR_SMS_DEV_FIXED_CODE' "$SCRIPT"
grep -Fq 'AIHR_SMS_PROD_FIXED_CODE_ENABLED' "$SCRIPT"
grep -Fq 'AUG1_FIXED_CODE_NO_REAL_SMS_MODE=PASS' "$SCRIPT"
grep -Fq 'COUNT(DISTINCT u.user_id)' "$SCRIPT"
grep -Fq 'handler_count >= 2' "$SCRIPT"
grep -Fq 'u.status = '\''0'\''' "$SCRIPT"
grep -Fq 'u.del_flag = '\''0'\''' "$SCRIPT"
grep -Fq 'review_status" == "已发布"' "$SCRIPT"
grep -Fq 'second_reviewer_user_id" != "$reviewer_user_id"' "$SCRIPT"
grep -Fq 'production content hash does not match the approved snapshot' "$SCRIPT"
grep -Fq 'AUG1_FORMAL_SCENARIOS=5/5' "$SCRIPT"
grep -Fq 'AUG1_FORMAL_POLICY_QUESTIONS=30/30' "$SCRIPT"
if grep -Eiq '(^|[[:space:]])(insert|update|delete|replace|alter|drop|truncate)[[:space:]]' "$SCRIPT"; then
echo 'FAIL: August 1 release readiness verifier contains a mutating SQL verb' >&2
exit 1
fi
if grep -Fq '/resource/sms/code' "$SCRIPT" || grep -Fq '/auth/mobile/sms-login' "$SCRIPT"; then
echo 'FAIL: final readiness verifier must inspect fixed-code state without requesting or consuming a code' >&2
exit 1
fi
if grep -Eq 'echo[[:space:]].*(fixed_code|reviewedBy|user_id)' "$SCRIPT"; then
echo 'FAIL: final readiness verifier may expose a code or identity' >&2
exit 1
fi
grep -Fq 'verify_aug1_readiness="${RELEASE_VERIFY_AUG1_READINESS:-false}"' "$PREFLIGHT"
grep -Fq 'verify-aug1-release-readiness.sh" --execute' "$PREFLIGHT"
grep -Fq 'RELEASE_VERIFY_AUG1_READINESS=true' "$DEV_SETUP"
echo 'PASS: August 1 final readiness gate is fixed-code-only, read-only, content-bound and handler-bound'
+8 -3
View File
@@ -12,22 +12,27 @@ $requiredFragments = @(
'Frozen APK hash mismatch',
'Frozen backend JAR hash mismatch',
'Content candidate hash mismatch',
'Release approval candidate hash mismatch',
"'operations/release-backend.sh'",
"'operations/release-preflight.sh'",
"'operations/verify-aug1-production-api-readonly.sh'",
"'operations/verify-aug1-authenticated-production.sh'",
"'operations/verify-aug1-content-candidates.mjs'",
"'operations/verify-aug1-formal-content.mjs'",
"'operations/verify-aug1-release-readiness.sh'",
"'operations/capture-android-acceptance.ps1'",
"'operations/go-no-go.md'",
"'operations/business-content-and-five-channel-signoff.md'",
'Release package already exists and will not be overwritten',
'Release package expected 17 files',
'ZIP verification expected 17 entries',
'Release package expected 20 files',
'ZIP verification expected 20 entries',
'ZIP entry hash mismatch',
'package_verified=true',
'authenticatedFixedCodeSmokePassed = $true',
'fixedCodeNoRealSmsConfirmed = $true',
'deployPlanSmsOrLoginTriggered = $false'
'deployPlanSmsOrLoginTriggered = $false',
"artifact = 'aug1-release-approval.json'",
'approvedPolicyQuestions = $approvedQuestionCount'
)
foreach ($fragment in $requiredFragments) {
if (-not $source.Contains($fragment)) {
+10 -2
View File
@@ -17,6 +17,11 @@ schema_without_url="$(
)"
grep -Fq 'remote verification flags require RELEASE_REMOTE_URL' <<<"$schema_without_url"
aug1_readiness_without_url="$(
RELEASE_VERIFY_AUG1_READINESS=true bash "$SCRIPT" 2>&1 || true
)"
grep -Fq 'remote verification flags require RELEASE_REMOTE_URL' <<<"$aug1_readiness_without_url"
if grep -Fq 'RELEASE_VERIFY_REMOTE_BACKEND=true requires RELEASE_VERIFY_REMOTE_MATCH=true' "$SCRIPT"; then
echo 'FAIL: backend-only verification is still coupled to static resource matching' >&2
exit 1
@@ -32,10 +37,13 @@ static_scope_line="$(grep -n '^[[:space:]]*if \[\[ "\$verify_remote_match" == "t
grep -Fq 'require_static_artifacts="$verify_remote_match"' "$SCRIPT"
grep -Fq 'require_backend_artifact="$verify_remote_backend"' "$SCRIPT"
grep -Fq 'verify_aug1_readiness="${RELEASE_VERIFY_AUG1_READINESS:-false}"' "$SCRIPT"
grep -Fq 'verify-aug1-release-readiness.sh" --execute' "$SCRIPT"
grep -Fq 'backend_jar="${RELEASE_LOCAL_BACKEND_PATH:-backend/ruoyi-admin/target/ruoyi-admin.jar}"' "$SCRIPT"
grep -Fq 'artifact_commit="${RELEASE_ARTIFACT_COMMIT:-HEAD}"' "$SCRIPT"
grep -Fq 'git merge-base --is-ancestor "$artifact_commit_sha" HEAD' "$SCRIPT"
grep -Fq 'RELEASE_VERIFY_REMOTE_BACKEND=true RELEASE_VERIFY_REMOTE_SCHEMA=true ./scripts/release-preflight.sh' "$DEV_SETUP"
grep -Fq 'RELEASE_VERIFY_REMOTE_MATCH=true RELEASE_VERIFY_REMOTE_BACKEND=true RELEASE_VERIFY_REMOTE_SCHEMA=true ./scripts/release-preflight.sh' "$DEV_SETUP"
grep -Fq 'RELEASE_VERIFY_AUG1_READINESS=true ./scripts/release-preflight.sh' "$DEV_SETUP"
grep -Fq 'RELEASE_VERIFY_REMOTE_MATCH=true RELEASE_VERIFY_REMOTE_BACKEND=true RELEASE_VERIFY_REMOTE_SCHEMA=true RELEASE_VERIFY_AUG1_READINESS=true ./scripts/release-preflight.sh' "$DEV_SETUP"
echo 'PASS: release preflight supports scoped backend/schema verification and preserves the full-package mode'
echo 'PASS: release preflight supports scoped artifact checks and a strict August 1 business-readiness mode'