fix(release): gate channel and distribution acceptance

This commit is contained in:
key
2026-07-29 12:51:31 +08:00
parent 5ee002ddcd
commit 46f4616bd4
10 changed files with 209 additions and 8 deletions
+12 -2
View File
@@ -126,6 +126,9 @@ $releaseApprovalManifest = Get-Content -Raw -Encoding UTF8 -LiteralPath $release
if ([string]$releaseApprovalManifest.candidateSha256 -ne $contentSha256) {
throw "Release approval candidate hash mismatch: $($releaseApprovalManifest.candidateSha256)"
}
if ([string]$releaseApprovalManifest.expectedApkSignerSha256 -ne ([string]$evidence.app.signerSha256).ToLowerInvariant()) {
throw "Release approval APK signer mismatch: $($releaseApprovalManifest.expectedApkSignerSha256)"
}
& node (Join-Path $projectRoot 'scripts\verify-aug1-formal-content.mjs') $releaseApprovalPath | Out-Null
if ($LASTEXITCODE -ne 0) {
throw 'Release approval structure audit failed.'
@@ -134,8 +137,11 @@ $approvedScenarioCount = @($releaseApprovalManifest.scenarioApprovals.PSObject.P
Where-Object { [string]$_.Value.status -eq 'APPROVED' }).Count
$approvedQuestionCount = @($releaseApprovalManifest.policyQuestionApprovals.PSObject.Properties |
Where-Object { [string]$_.Value.status -eq 'APPROVED' }).Count
$approvedChannelCount = @($releaseApprovalManifest.channelApprovals.PSObject.Properties |
Where-Object { [string]$_.Value.status -eq 'APPROVED' }).Count
$approvedSignoffCount = @($releaseApprovalManifest.signoffs.PSObject.Properties |
Where-Object { [string]$_.Value.status -eq 'APPROVED' }).Count
$distributionApprovalStatus = [string]$releaseApprovalManifest.distributionApproval.status
$goNoGoCandidates = @(Get-ChildItem -LiteralPath (Join-Path $projectRoot 'docs') -File -Filter '*Android*Go-No-Go-20260728.md')
if ($goNoGoCandidates.Count -ne 1) {
@@ -261,7 +267,7 @@ try {
'',
'Recheck the bundled disabled content candidates with `node operations/verify-aug1-content-candidates.mjs aug1-life-advisor-content-candidates-v0.1.json`. Complete `operations/business-content-and-five-channel-signoff.md` before enabling content or binding handlers.',
'',
'Audit business evidence with `node operations/verify-aug1-formal-content.mjs aug1-release-approval.json`. The strict command and `operations/verify-aug1-release-readiness.sh --execute` must pass before the final Go decision; the latter is read-only and verifies fixed-code mode, two active handlers per channel, and immutable production scenario snapshots.',
'Audit business evidence with `node operations/verify-aug1-formal-content.mjs aug1-release-approval.json`. The strict command and `operations/verify-aug1-release-readiness.sh --execute` must pass before the final Go decision; the latter is read-only and verifies fixed-code mode, formal content, per-channel access/reply evidence, two active handlers per channel, immutable production scenario snapshots, and controlled-distribution approval.',
'',
'The fixed-code authenticated smoke is bundled as `operations/verify-aug1-authenticated-production.sh`. It requires the explicit `--execute` flag, selects an existing eligible APP identity without printing it, never enables or sends real SMS, and does not submit business records.',
'',
@@ -281,7 +287,7 @@ try {
'- Production schema: 64/64 and runtime schema bootstrap disabled/default.',
'- Production GET-only route probe: 20/20 (`1x200`, `6x401`, `13x405`).',
'- Separate fixed-code authenticated smoke passed request/login, read-only business routes, logout, old-token rejection and re-login; application logs confirmed that no real SMS was sent.',
"- Formal approval audit: $approvedScenarioCount/5 scenarios, $approvedQuestionCount/30 policy questions, $approvedSignoffCount/5 owner sign-offs.",
"- Formal approval audit: $approvedScenarioCount/5 scenarios, $approvedQuestionCount/30 policy questions, $approvedChannelCount/5 channel acceptances, $approvedSignoffCount/5 owner sign-offs, distribution $distributionApprovalStatus.",
'- Strict August 1 readiness is expected to fail closed until formal evidence and real channel handlers are complete.',
'- Backend normalized AIHR module remains mismatched until an authorized deploy.',
'- Overall result: **No-Go** pending the manual and authorization gates in `operations/go-no-go.md`.'
@@ -297,7 +303,9 @@ try {
"- Policy-question candidates: $($evidence.contentCandidates.policyQuestionCandidates)",
"- Formally approved scenarios: $approvedScenarioCount/5",
"- Formally evidenced policy questions: $approvedQuestionCount/30",
"- Approved direct-channel acceptances: $approvedChannelCount/5",
"- Owner sign-offs: $approvedSignoffCount/5",
"- Controlled-distribution approval: $distributionApprovalStatus",
'',
'Candidates are for business review only. Do not import, enable or count them toward August 1 acceptance until formal sources and item-level sign-off are complete.',
'',
@@ -363,7 +371,9 @@ try {
formalContentVersion = [string]$releaseApprovalManifest.formalContentVersion
approvedScenarios = $approvedScenarioCount
approvedPolicyQuestions = $approvedQuestionCount
approvedDirectChannels = $approvedChannelCount
approvedOwnerSignoffs = $approvedSignoffCount
controlledDistributionStatus = $distributionApprovalStatus
}
operations = @($operationSources.Keys)
manualGatesRemaining = @($evidence.manualGatesRemaining)
@@ -51,6 +51,7 @@ function validApproval() {
secondReviewedAt: '2026-07-30T10:00:00+08:00',
} : {}),
businessEvidenceRef: `evidence/scenario-${index + 1}.pdf`,
businessEvidenceSha256: 'c'.repeat(64),
productionContentVersion: `aug1-v${index + 1}`,
productionContentHash: String(index + 1).repeat(64),
}]
@@ -84,17 +85,51 @@ function validApproval() {
reviewedBy: `${role}-reviewer`,
reviewedAt: '2026-07-30T12:00:00+08:00',
}]))
const channelApprovals = Object.fromEntries([
'direct_president',
'direct_finance',
'direct_hr',
'direct_audit',
'direct_operations',
].map((role) => [role, {
status: 'APPROVED',
minimumActiveHandlers: 2,
bindingEvidenceRef: `evidence/${role}-binding.json`,
bindingEvidenceSha256: 'd'.repeat(64),
positiveAccessEvidenceRef: `evidence/${role}-positive.json`,
positiveAccessEvidenceSha256: 'e'.repeat(64),
crossChannelDenialEvidenceRef: `evidence/${role}-denial.json`,
crossChannelDenialEvidenceSha256: 'f'.repeat(64),
singleReplyEvidenceRef: `evidence/${role}-reply.json`,
singleReplyEvidenceSha256: '1'.repeat(64),
reviewedBy: `${role}-reviewer`,
reviewedAt: '2026-07-30T11:30:00+08:00',
}]))
const expectedApkSignerSha256 = '2'.repeat(64)
return {
schemaVersion: '1.0',
releaseTarget: '2026-08-01',
tenantId: '000000',
candidateFile: 'aug1-life-advisor-content-candidates-v0.1.json',
candidateSha256,
expectedApkSignerSha256,
releaseStatus: 'APPROVED',
formalContentVersion: 'aug1-formal-v1',
sourceRegistry: [source],
scenarioApprovals,
policyQuestionApprovals,
channelApprovals,
distributionApproval: {
status: 'APPROVED',
scope: 'CONTROLLED_INTERNAL_TEST',
legalDecision: 'CONTROLLED_INTERNAL_TEST_EXCEPTION_ACCEPTED',
signerDecision: 'DCLOUD_TEST_SIGNER_ACCEPTED_FOR_CONTROLLED_INTERNAL_TEST',
signerSha256: expectedApkSignerSha256,
distributionEvidenceRef: 'evidence/controlled-distribution.json',
distributionEvidenceSha256: '3'.repeat(64),
reviewedBy: 'distribution-reviewer',
reviewedAt: '2026-07-30T12:30:00+08:00',
},
signoffs,
}
}
@@ -123,6 +158,8 @@ test('audits the checked-in pending manifest without claiming release readiness'
assert.equal(audit.status, 0, audit.stderr)
assert.match(audit.stdout, /approved scenarios: 0\/5/)
assert.match(audit.stdout, /approved policy questions: 0\/30/)
assert.match(audit.stdout, /approved direct channels: 0\/5/)
assert.match(audit.stdout, /controlled distribution: PENDING/)
const strict = spawnSync(process.execPath, [verifierPath, '--strict', pendingApprovalPath], {
cwd: projectRoot,
@@ -168,6 +205,18 @@ test('rejects incomplete or contradictory formal approvals', async (t) => {
['missing owner sign-off', (approval) => {
approval.signoffs.releaseOwner.status = 'PENDING'
}, /releaseOwner: sign-off must be APPROVED/],
['missing channel acceptance', (approval) => {
delete approval.channelApprovals.direct_audit
}, /channel approvals must cover exactly 5\/5 roles/],
['channel without backup requirement', (approval) => {
approval.channelApprovals.direct_finance.minimumActiveHandlers = 1
}, /primary and backup requirement must remain 2/],
['unaccepted distribution', (approval) => {
approval.distributionApproval.status = 'PENDING'
}, /distribution approval must be APPROVED/],
['mismatched APK signer', (approval) => {
approval.distributionApproval.signerSha256 = '4'.repeat(64)
}, /distribution signer SHA-256 must match the expected APK signer/],
]
for (const [name, mutate, expected] of cases) {
@@ -59,6 +59,7 @@ candidate.scenarios.forEach((item, index) => {
secondReviewedAt: '2026-07-30T10:00:00+08:00',
} : {}),
businessEvidenceRef: `evidence/scenario-${index + 1}.pdf`,
businessEvidenceSha256: 'c'.repeat(64),
productionContentVersion: version,
productionContentHash: hash,
}
@@ -105,17 +106,51 @@ const signoffs = Object.fromEntries([
reviewedBy: `${role}-reviewer`,
reviewedAt: '2026-07-30T12:00:00+08:00',
}]))
const channelApprovals = Object.fromEntries([
'direct_president',
'direct_finance',
'direct_hr',
'direct_audit',
'direct_operations',
].map((role) => [role, {
status: 'APPROVED',
minimumActiveHandlers: 2,
bindingEvidenceRef: `evidence/${role}-binding.json`,
bindingEvidenceSha256: 'd'.repeat(64),
positiveAccessEvidenceRef: `evidence/${role}-positive.json`,
positiveAccessEvidenceSha256: 'e'.repeat(64),
crossChannelDenialEvidenceRef: `evidence/${role}-denial.json`,
crossChannelDenialEvidenceSha256: 'f'.repeat(64),
singleReplyEvidenceRef: `evidence/${role}-reply.json`,
singleReplyEvidenceSha256: '1'.repeat(64),
reviewedBy: `${role}-reviewer`,
reviewedAt: '2026-07-30T11:30:00+08:00',
}]))
const expectedApkSignerSha256 = '2'.repeat(64)
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'),
expectedApkSignerSha256,
releaseStatus: 'APPROVED',
formalContentVersion: 'aug1-formal-v1',
sourceRegistry: [source],
scenarioApprovals,
policyQuestionApprovals,
channelApprovals,
distributionApproval: {
status: 'APPROVED',
scope: 'CONTROLLED_INTERNAL_TEST',
legalDecision: 'CONTROLLED_INTERNAL_TEST_EXCEPTION_ACCEPTED',
signerDecision: 'DCLOUD_TEST_SIGNER_ACCEPTED_FOR_CONTROLLED_INTERNAL_TEST',
signerSha256: expectedApkSignerSha256,
distributionEvidenceRef: 'evidence/controlled-distribution.json',
distributionEvidenceSha256: '3'.repeat(64),
reviewedBy: 'distribution-reviewer',
reviewedAt: '2026-07-30T12:30:00+08:00',
},
signoffs,
}))
fs.writeFileSync(rowsPath, rows.join('\n') + '\n')
@@ -177,6 +212,8 @@ run_mock_readiness() {
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_CHANNEL_ACCEPTANCE=5/5' <<<"$ready_output"
grep -Fq 'AUG1_DISTRIBUTION_APPROVAL=PASS' <<<"$ready_output"
grep -Fq 'AUG1_RELEASE_READINESS=PASS' <<<"$ready_output"
handler_failure="$(
@@ -201,6 +238,8 @@ 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"
grep -Fq 'AUG1_CHANNEL_ACCEPTANCE=5/5' "$SCRIPT"
grep -Fq 'AUG1_DISTRIBUTION_APPROVAL=PASS' "$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
+4 -1
View File
@@ -13,6 +13,7 @@ $requiredFragments = @(
'Frozen backend JAR hash mismatch',
'Content candidate hash mismatch',
'Release approval candidate hash mismatch',
'Release approval APK signer mismatch',
"'operations/release-backend.sh'",
"'operations/release-preflight.sh'",
"'operations/verify-aug1-production-api-readonly.sh'",
@@ -34,7 +35,9 @@ $requiredFragments = @(
'fixedCodeNoRealSmsConfirmed = $true',
'deployPlanSmsOrLoginTriggered = $false',
"artifact = 'aug1-release-approval.json'",
'approvedPolicyQuestions = $approvedQuestionCount'
'approvedPolicyQuestions = $approvedQuestionCount',
'approvedDirectChannels = $approvedChannelCount',
'controlledDistributionStatus = $distributionApprovalStatus'
)
foreach ($fragment in $requiredFragments) {
if (-not $source.Contains($fragment)) {
+78
View File
@@ -54,6 +54,10 @@ const candidateFileValid = nonBlank(approval.candidateFile)
&& approval.candidateFile.endsWith('.json')
check(candidateFileValid, 'candidateFile must be a JSON basename beside the approval file')
check(sha256Pattern.test(approval.candidateSha256 ?? ''), 'candidateSha256 must be lowercase SHA-256')
check(
sha256Pattern.test(approval.expectedApkSignerSha256 ?? ''),
'expectedApkSignerSha256 must be lowercase SHA-256',
)
if (!candidateFileValid) {
for (const failure of failures) console.error(`FAIL: ${failure}`)
@@ -110,6 +114,7 @@ for (const source of sourceRegistry) {
const scenarioApprovals = approval.scenarioApprovals
const questionApprovals = approval.policyQuestionApprovals
const channelApprovals = approval.channelApprovals
check(
scenarioApprovals && typeof scenarioApprovals === 'object' && !Array.isArray(scenarioApprovals),
'scenarioApprovals must be an object keyed by candidate ID',
@@ -118,14 +123,29 @@ check(
questionApprovals && typeof questionApprovals === 'object' && !Array.isArray(questionApprovals),
'policyQuestionApprovals must be an object keyed by question ID',
)
check(
channelApprovals && typeof channelApprovals === 'object' && !Array.isArray(channelApprovals),
'channelApprovals must be an object keyed by direct-channel role',
)
const scenarioEntries = Object.entries(scenarioApprovals ?? {})
const questionEntries = Object.entries(questionApprovals ?? {})
const channelEntries = Object.entries(channelApprovals ?? {})
for (const [candidateId] of scenarioEntries) {
check(expectedScenarios.has(candidateId), `${candidateId}: approval references an unknown scenario`)
}
for (const [questionId] of questionEntries) {
check(expectedQuestions.has(questionId), `${questionId}: approval references an unknown question`)
}
const requiredChannels = [
'direct_president',
'direct_finance',
'direct_hr',
'direct_audit',
'direct_operations',
]
for (const [roleKey] of channelEntries) {
check(requiredChannels.includes(roleKey), `${roleKey}: approval references an unknown direct channel`)
}
const resolveSourceRefs = (refs, label) => {
check(Array.isArray(refs) && refs.length > 0, `${label}: at least one formal source reference is required`)
@@ -171,6 +191,7 @@ if (strict) {
check(entry.secondReviewedBy !== entry.reviewedBy, `${label}: high-risk reviewers must be different people`)
}
check(nonBlank(entry.businessEvidenceRef), `${label}: business evidence reference is required`)
check(sha256Pattern.test(entry.businessEvidenceSha256 ?? ''), `${label}: business evidence SHA-256 is required`)
check(nonBlank(entry.productionContentVersion), `${label}: production content version is required`)
check(sha256Pattern.test(entry.productionContentHash ?? ''), `${label}: production content hash is required`)
}
@@ -208,6 +229,29 @@ if (strict) {
requireReview(entry, label)
}
check(channelEntries.length === requiredChannels.length, 'channel approvals must cover exactly 5/5 roles')
for (const roleKey of requiredChannels) {
const entry = channelApprovals?.[roleKey]
const label = roleKey
check(Boolean(entry), `${label}: channel approval is missing`)
if (!entry) continue
check(entry.status === 'APPROVED', `${label}: channel status must be APPROVED`)
check(entry.minimumActiveHandlers === 2, `${label}: primary and backup requirement must remain 2`)
for (const evidenceName of [
'bindingEvidence',
'positiveAccessEvidence',
'crossChannelDenialEvidence',
'singleReplyEvidence',
]) {
check(nonBlank(entry[`${evidenceName}Ref`]), `${label}: ${evidenceName} reference is required`)
check(
sha256Pattern.test(entry[`${evidenceName}Sha256`] ?? ''),
`${label}: ${evidenceName} SHA-256 is required`,
)
}
requireReview(entry, label)
}
const requiredSignoffs = [
'businessOwner',
'knowledgeOwner',
@@ -224,6 +268,37 @@ if (strict) {
check(signoff?.status === 'APPROVED', `${role}: sign-off must be APPROVED`)
if (signoff) requireReview(signoff, role)
}
const distribution = approval.distributionApproval
check(
distribution && typeof distribution === 'object' && !Array.isArray(distribution),
'distributionApproval must be an object',
)
if (distribution) {
check(distribution.status === 'APPROVED', 'distribution approval must be APPROVED')
check(distribution.scope === 'CONTROLLED_INTERNAL_TEST', 'distribution scope must remain controlled internal test')
check(
['LEGAL_APPROVED', 'CONTROLLED_INTERNAL_TEST_EXCEPTION_ACCEPTED'].includes(distribution.legalDecision),
'distribution legalDecision is not approved',
)
check(
[
'ENTERPRISE_SIGNER_APPROVED',
'DCLOUD_TEST_SIGNER_ACCEPTED_FOR_CONTROLLED_INTERNAL_TEST',
].includes(distribution.signerDecision),
'distribution signerDecision is not approved',
)
check(
distribution.signerSha256 === approval.expectedApkSignerSha256,
'distribution signer SHA-256 must match the expected APK signer',
)
check(nonBlank(distribution.distributionEvidenceRef), 'controlled-distribution evidence reference is required')
check(
sha256Pattern.test(distribution.distributionEvidenceSha256 ?? ''),
'controlled-distribution evidence SHA-256 is required',
)
requireReview(distribution, 'distributionApproval')
}
}
if (failures.length > 0) {
@@ -243,12 +318,15 @@ if (scenarioSnapshots) {
} else {
const approvedScenarios = scenarioEntries.filter(([, value]) => value?.status === 'APPROVED').length
const approvedQuestions = questionEntries.filter(([, value]) => value?.status === 'APPROVED').length
const approvedChannels = channelEntries.filter(([, value]) => value?.status === 'APPROVED').length
const approvedSignoffs = Object.values(approval.signoffs ?? {}).filter((value) => value?.status === 'APPROVED').length
console.log('August 1 formal content approval audit passed')
console.log(`- releaseStatus: ${approval.releaseStatus}`)
console.log(`- formal sources: ${sourceRegistry.length}`)
console.log(`- approved scenarios: ${approvedScenarios}/5`)
console.log(`- approved policy questions: ${approvedQuestions}/30`)
console.log(`- approved direct channels: ${approvedChannels}/5`)
console.log(`- approved owner sign-offs: ${approvedSignoffs}/5`)
console.log(`- controlled distribution: ${approval.distributionApproval?.status ?? 'MISSING'}`)
console.log(`- strict release ready: ${strict ? 'true' : 'not requested'}`)
}
+2
View File
@@ -213,6 +213,8 @@ for snapshot in "${snapshots[@]}"; do
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