fix(release): gate channel and distribution acceptance
This commit is contained in:
@@ -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'}`)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user