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
@@ -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)) {