fix(release): gate channel and distribution acceptance
This commit is contained in:
+1
-1
@@ -318,7 +318,7 @@ RELEASE_REMOTE_URL=https://peilian.njzhmj.top RELEASE_VERIFY_REMOTE_BACKEND=true
|
|||||||
RELEASE_REMOTE_URL=https://peilian.njzhmj.top RELEASE_VERIFY_REMOTE_MATCH=true RELEASE_VERIFY_REMOTE_BACKEND=true RELEASE_VERIFY_REMOTE_SCHEMA=true RELEASE_VERIFY_AUG1_READINESS=true ./scripts/release-preflight.sh
|
RELEASE_REMOTE_URL=https://peilian.njzhmj.top RELEASE_VERIFY_REMOTE_MATCH=true RELEASE_VERIFY_REMOTE_BACKEND=true RELEASE_VERIFY_REMOTE_SCHEMA=true RELEASE_VERIFY_AUG1_READINESS=true ./scripts/release-preflight.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
四个远端核验开关可以按实际发布单元独立组合,但只要启用任一开关就必须提供 `RELEASE_REMOTE_URL`。定向模式只要求对应的本地产物保持新鲜:静态核验要求管理端/H5,后端核验要求后端 JAR,单独 schema 或 8 月 1 日业务就绪核验不要求无关构建产物。默认以当前 `HEAD` 判断产物时间;冻结 RC 应同时设置 `RELEASE_ARTIFACT_COMMIT` 和 `RELEASE_LOCAL_BACKEND_PATH`,前者必须是当前 `HEAD` 的祖先,后者必须指向实际准备发布的 JAR,避免后续文档提交误伤冻结物或误用 `target` 下的其他构建。`RELEASE_VERIFY_REMOTE_BACKEND=true` 会打印整包 jar SHA-256 和 `ruoyi-aihr` 模块内容 SHA-256,实际匹配以模块内容 hash 为准;它不再隐式要求管理端/H5 静态资源匹配。同时启用前三项只能证明完整包匹配;8 月 1 日最终 Go 还必须增加 `RELEASE_VERIFY_AUG1_READINESS=true`。该门禁先严格校验 `docs/content-candidates/aug1-release-approval.json`:5/5 场景、30/30 问题证据及五类负责人均签认后,才继续只读检查生产固定码模式、五个直通角色各至少两名有效处理人,以及五个已发布场景的版本、内容哈希、审核人和 SOP 引用。它不请求验证码、不发送短信、不登录、不输出账号或处理人身份,也不写数据库。远端后端默认核对 `/opt/wygj/app/ruoyi-admin.jar`,如发布路径不同可通过 `RELEASE_REMOTE_BACKEND_PATH` 覆盖;路径必须是安全的绝对路径。开启远端后端或 schema 核验时,预检还会只读检查 `wygj-aihr.service` 及其 `EnvironmentFile` 的有效配置,`AIHR_PRACTICE_RUNTIME_SCHEMA_BOOTSTRAP` 必须为 `false` 或未设置;无法读取引用的环境文件会失败关闭,而不会把本机环境变量当作线上证据。
|
四个远端核验开关可以按实际发布单元独立组合,但只要启用任一开关就必须提供 `RELEASE_REMOTE_URL`。定向模式只要求对应的本地产物保持新鲜:静态核验要求管理端/H5,后端核验要求后端 JAR,单独 schema 或 8 月 1 日业务就绪核验不要求无关构建产物。默认以当前 `HEAD` 判断产物时间;冻结 RC 应同时设置 `RELEASE_ARTIFACT_COMMIT` 和 `RELEASE_LOCAL_BACKEND_PATH`,前者必须是当前 `HEAD` 的祖先,后者必须指向实际准备发布的 JAR,避免后续文档提交误伤冻结物或误用 `target` 下的其他构建。`RELEASE_VERIFY_REMOTE_BACKEND=true` 会打印整包 jar SHA-256 和 `ruoyi-aihr` 模块内容 SHA-256,实际匹配以模块内容 hash 为准;它不再隐式要求管理端/H5 静态资源匹配。同时启用前三项只能证明完整包匹配;8 月 1 日最终 Go 还必须增加 `RELEASE_VERIFY_AUG1_READINESS=true`。该门禁先严格校验 `docs/content-candidates/aug1-release-approval.json`:5/5 场景、30/30 问题证据、5/5 通道的绑定/正例/跨通道拒绝/一次回复证据、五类负责人签认及受控分发决定全部完成后,才继续只读检查生产固定码模式、五个直通角色各至少两名有效处理人,以及五个已发布场景的版本、内容哈希、审核人和 SOP 引用。受控分发决定必须明确法务批准或内部测试例外接受、企业签名或 DCloud 测试签名接受、实际签名摘要和分发证据哈希。门禁不请求验证码、不发送短信、不登录、不输出账号或处理人身份,也不写数据库。远端后端默认核对 `/opt/wygj/app/ruoyi-admin.jar`,如发布路径不同可通过 `RELEASE_REMOTE_BACKEND_PATH` 覆盖;路径必须是安全的绝对路径。开启远端后端或 schema 核验时,预检还会只读检查 `wygj-aihr.service` 及其 `EnvironmentFile` 的有效配置,`AIHR_PRACTICE_RUNTIME_SCHEMA_BOOTSTRAP` 必须为 `false` 或未设置;无法读取引用的环境文件会失败关闭,而不会把本机环境变量当作线上证据。
|
||||||
|
|
||||||
Android 人工验收每完成一个步骤,用 `.\scripts\capture-android-acceptance.ps1 -Serial <adb-serial> -Stage <stage>` 保存当前状态;脚本只读取包版本、前台 Activity、权限、UI、截图和退出记录,不执行安装、启动、清数据、授权或点击。证据仅写入已忽略的 `output/aug1-rc/android-acceptance/`,详细口径见 `docs/MOBILE_APP_DEVICE_ACCEPTANCE.md`。
|
Android 人工验收每完成一个步骤,用 `.\scripts\capture-android-acceptance.ps1 -Serial <adb-serial> -Stage <stage>` 保存当前状态;脚本只读取包版本、前台 Activity、权限、UI、截图和退出记录,不执行安装、启动、清数据、授权或点击。证据仅写入已忽略的 `output/aug1-rc/android-acceptance/`,详细口径见 `docs/MOBILE_APP_DEVICE_ACCEPTANCE.md`。
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,24 @@
|
|||||||
"tenantId": "000000",
|
"tenantId": "000000",
|
||||||
"candidateFile": "aug1-life-advisor-content-candidates-v0.1.json",
|
"candidateFile": "aug1-life-advisor-content-candidates-v0.1.json",
|
||||||
"candidateSha256": "148654e241e0fd74bcb29d71cd43518cb961f0128855b66af98a56bc841aea1d",
|
"candidateSha256": "148654e241e0fd74bcb29d71cd43518cb961f0128855b66af98a56bc841aea1d",
|
||||||
|
"expectedApkSignerSha256": "ab06ecc0de544de73c385f3f0690b22966ebc12887fb8bee1830cdbefdebef5d",
|
||||||
"releaseStatus": "PENDING",
|
"releaseStatus": "PENDING",
|
||||||
"formalContentVersion": "",
|
"formalContentVersion": "",
|
||||||
"sourceRegistry": [],
|
"sourceRegistry": [],
|
||||||
"scenarioApprovals": {},
|
"scenarioApprovals": {},
|
||||||
"policyQuestionApprovals": {},
|
"policyQuestionApprovals": {},
|
||||||
|
"channelApprovals": {},
|
||||||
|
"distributionApproval": {
|
||||||
|
"status": "PENDING",
|
||||||
|
"scope": "CONTROLLED_INTERNAL_TEST",
|
||||||
|
"legalDecision": "PENDING",
|
||||||
|
"signerDecision": "PENDING",
|
||||||
|
"signerSha256": "",
|
||||||
|
"distributionEvidenceRef": "",
|
||||||
|
"distributionEvidenceSha256": "",
|
||||||
|
"reviewedBy": "",
|
||||||
|
"reviewedAt": ""
|
||||||
|
},
|
||||||
"signoffs": {
|
"signoffs": {
|
||||||
"businessOwner": {
|
"businessOwner": {
|
||||||
"status": "PENDING",
|
"status": "PENDING",
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
| 构建 | `mobile-uni` H5、App-Plus、管理端生产构建通过;后端 `ruoyi-admin.jar` 重建通过;统一 RC 的前端、移动 H5 和后端 JAR 哈希均已写入冻结证据 |
|
| 构建 | `mobile-uni` H5、App-Plus、管理端生产构建通过;后端 `ruoyi-admin.jar` 重建通过;统一 RC 的前端、移动 H5 和后端 JAR 哈希均已写入冻结证据 |
|
||||||
| P0 后端语义 | `AihrSopSeedServiceTest` 39 项、`AihrPracticeSeedServiceTest` 102 项通过;`ruoyi-aihr` 700 项全量测试通过,0 失败、0 错误;新增 2 项覆盖有效主管身份与空身份失败关闭 |
|
| P0 后端语义 | `AihrSopSeedServiceTest` 39 项、`AihrPracticeSeedServiceTest` 102 项通过;`ruoyi-aihr` 700 项全量测试通过,0 失败、0 错误;新增 2 项覆盖有效主管身份与空身份失败关闭 |
|
||||||
| 内容候选门禁 | 已生成 5 个禁用场景候选和 30 道待审政策题,机器校验通过;正式可发布场景 `0`、正式有据标准答案 `0`,不得导入或启用 |
|
| 内容候选门禁 | 已生成 5 个禁用场景候选和 30 道待审政策题,机器校验通过;正式可发布场景 `0`、正式有据标准答案 `0`,不得导入或启用 |
|
||||||
| 最终业务就绪门禁 | `verify-aug1-formal-content.mjs --strict` 和 `verify-aug1-release-readiness.sh --execute` 已落地并覆盖正反例;当前机器签认准确返回场景 `0/5`、问题证据 `0/30`、负责人签认 `0/5` 并失败关闭。签认完成后还会只读核对生产固定码模式、五角色各至少两名有效处理人和五个已发布场景的版本/哈希/审核链/SOP 引用;不请求验证码、不登录、不发短信、不写数据库 |
|
| 最终业务就绪门禁 | `verify-aug1-formal-content.mjs --strict` 和 `verify-aug1-release-readiness.sh --execute` 已落地并覆盖正反例;当前机器签认准确返回场景 `0/5`、问题证据 `0/30`、通道验收 `0/5`、负责人签认 `0/5`、受控分发 `PENDING` 并失败关闭。签认完成后还会只读核对生产固定码模式、五角色各至少两名有效处理人和五个已发布场景的版本/哈希/审核链/SOP 引用;不请求验证码、不登录、不发短信、不写数据库 |
|
||||||
| App 资源门禁 | 正式法务 URL、登录页同意门禁、原生 `prompt=none`、API 35、图标和启动图检查通过 |
|
| App 资源门禁 | 正式法务 URL、登录页同意门禁、原生 `prompt=none`、API 35、图标和启动图检查通过 |
|
||||||
| 独立 APK | `com.yincheng.wygj`,`0.1.13 (113)`,`targetSdkVersion=35`,v2 测试签名,明文 HTTP 关闭且网络安全资源已进入 APK;大小 `33,921,543` 字节,SHA-256 `FA2D2A6DC84AE759714E6D16D146C1E1552A0F3F53C046069422012361D1D0C0` |
|
| 独立 APK | `com.yincheng.wygj`,`0.1.13 (113)`,`targetSdkVersion=35`,v2 测试签名,明文 HTTP 关闭且网络安全资源已进入 APK;大小 `33,921,543` 字节,SHA-256 `FA2D2A6DC84AE759714E6D16D146C1E1552A0F3F53C046069422012361D1D0C0` |
|
||||||
| 双提交交付包 | `output/aug1-release/bangdao-aug1-0.1.13-113-af8af2f6-ops9c53668c.zip`;运行时 `af8af2f6`、operations `9c53668c`,大小 `192,946,729` 字节,SHA-256 `B639C5933F8F3401EEEAB26E2BF2B98F5E5BA7E0BC65B7199469856B96906CEC`,20 个条目和内部哈希复核通过;新增机器签认清单、正式内容严格校验和生产业务就绪门禁,包内 Go/No-Go 已指向当前双提交包,旧 `ops63968f33` ZIP 只作历史追溯 |
|
| 双提交交付包 | `output/aug1-release/bangdao-aug1-0.1.13-113-af8af2f6-ops9c53668c.zip`;运行时 `af8af2f6`、operations `9c53668c`,大小 `192,946,729` 字节,SHA-256 `B639C5933F8F3401EEEAB26E2BF2B98F5E5BA7E0BC65B7199469856B96906CEC`,20 个条目和内部哈希复核通过;新增机器签认清单、正式内容严格校验和生产业务就绪门禁,包内 Go/No-Go 已指向当前双提交包,旧 `ops63968f33` ZIP 只作历史追溯 |
|
||||||
|
|||||||
@@ -23,6 +23,8 @@
|
|||||||
| 正式有据标准答案 | 0/30 | 来源、版本、适用范围与审核人齐全 |
|
| 正式有据标准答案 | 0/30 | 来源、版本、适用范围与审核人齐全 |
|
||||||
| 直通车通道 | 5/5 可见 | 每个通道至少绑定 1 名主处理人和 1 名替补 |
|
| 直通车通道 | 5/5 可见 | 每个通道至少绑定 1 名主处理人和 1 名替补 |
|
||||||
| 生产有效处理人绑定 | 5 个角色均为 0 | 绑定后完成本通道正例、跨通道反例和一次正式回复 |
|
| 生产有效处理人绑定 | 5 个角色均为 0 | 绑定后完成本通道正例、跨通道反例和一次正式回复 |
|
||||||
|
| 五通道验收证据 | 0/5 | 每通道的绑定、正例、跨通道拒绝和一次回复证据均有不可变引用与哈希 |
|
||||||
|
| 受控分发决定 | `PENDING` | 明确法务决定、签名决定、分发范围、证据哈希和负责人 |
|
||||||
|
|
||||||
候选事实源:
|
候选事实源:
|
||||||
|
|
||||||
@@ -107,6 +109,7 @@
|
|||||||
- [ ] 五个处理角色分别绑定主处理人和替补;
|
- [ ] 五个处理角色分别绑定主处理人和替补;
|
||||||
- [ ] 五通道正例、跨通道反例和一次正式回复全部通过;
|
- [ ] 五通道正例、跨通道反例和一次正式回复全部通过;
|
||||||
- [ ] 内容运营确认候选版本、导入版本和员工可见版本一致;
|
- [ ] 内容运营确认候选版本、导入版本和员工可见版本一致;
|
||||||
|
- [ ] 法务/合规与发布负责人明确接受受控内测范围、当前 APK 签名和分发名单;
|
||||||
- [ ] 发布负责人确认本单对应的后端、内容和权限变更已纳入回退点。
|
- [ ] 发布负责人确认本单对应的后端、内容和权限变更已纳入回退点。
|
||||||
|
|
||||||
| 签认角色 | 结论 | 签认人 | 日期 | 备注 |
|
| 签认角色 | 结论 | 签认人 | 日期 | 备注 |
|
||||||
@@ -115,6 +118,8 @@
|
|||||||
| 知识内容负责人 | `PENDING` | 待签认 | 待签认 | |
|
| 知识内容负责人 | `PENDING` | 待签认 | 待签认 | |
|
||||||
| 训练内容负责人 | `PENDING` | 待签认 | 待签认 | |
|
| 训练内容负责人 | `PENDING` | 待签认 | 待签认 | |
|
||||||
| 五通道业务负责人 | `PENDING` | 待签认 | 待签认 | |
|
| 五通道业务负责人 | `PENDING` | 待签认 | 待签认 | |
|
||||||
|
| 法务/合规确认人 | `PENDING` | 待签认 | 待签认 | 受控内测可填写正式批准或明确例外接受 |
|
||||||
|
| 受控分发负责人 | `PENDING` | 待签认 | 待签认 | 确认签名摘要、分发范围与名单证据 |
|
||||||
| 发布负责人 | `PENDING` | 待签认 | 待签认 | |
|
| 发布负责人 | `PENDING` | 待签认 | 待签认 | |
|
||||||
|
|
||||||
## 7. 机器门禁与回填方式
|
## 7. 机器门禁与回填方式
|
||||||
@@ -126,8 +131,10 @@
|
|||||||
1. 业务方提供正式文件,登记 `sourceRegistry`;不得把访谈或 AI 输出登记为 `formalPolicy=true`。
|
1. 业务方提供正式文件,登记 `sourceRegistry`;不得把访谈或 AI 输出登记为 `formalPolicy=true`。
|
||||||
2. 5 个场景在管理端完成内容录入和审核,高风险场景由不同人员二审;把最终生产 `scenarioCode`、`contentVersion`、`contentHash` 和证据引用回填到对应 `scenarioApprovals`。
|
2. 5 个场景在管理端完成内容录入和审核,高风险场景由不同人员二审;把最终生产 `scenarioCode`、`contentVersion`、`contentHash` 和证据引用回填到对应 `scenarioApprovals`。
|
||||||
3. 30 道问题逐项形成正式引用/拒答/越权回归证据,把证据文件引用和 SHA-256 回填到 `policyQuestionApprovals`;不把答案正文写入仓库。
|
3. 30 道问题逐项形成正式引用/拒答/越权回归证据,把证据文件引用和 SHA-256 回填到 `policyQuestionApprovals`;不把答案正文写入仓库。
|
||||||
4. 五类负责人签认后,将顶层 `releaseStatus` 改为 `APPROVED`。任何缺项、占位值、未知来源、证据哈希缺失或高风险同人二审都会失败关闭。
|
4. 五个通道分别完成绑定、正例访问、其他通道拒绝和一次正式回复验收,把四类证据引用与 SHA-256 回填到 `channelApprovals`;清单不记录处理人身份或反馈正文。
|
||||||
5. 系统管理员按本单绑定五通道主处理人和替补;机器门禁只输出每个角色的有效人数,不输出人员身份。
|
5. 受控分发负责人在 `distributionApproval` 中明确法务决定、DCloud 测试签名或企业签名决定、实际签名摘要和分发证据;只有受控内部测试范围允许填写明确的例外接受。
|
||||||
|
6. 五类业务负责人签认后,将顶层 `releaseStatus` 改为 `APPROVED`。任何缺项、占位值、未知来源、证据哈希缺失、高风险同人二审、通道验收不足或签名摘要不一致都会失败关闭。
|
||||||
|
7. 系统管理员按本单绑定五通道主处理人和替补;机器门禁只输出每个角色的有效人数,不输出人员身份。
|
||||||
|
|
||||||
先审计当前回填进度:
|
先审计当前回填进度:
|
||||||
|
|
||||||
@@ -147,4 +154,4 @@ node scripts/verify-aug1-formal-content.mjs --strict
|
|||||||
./scripts/verify-aug1-release-readiness.sh --execute
|
./scripts/verify-aug1-release-readiness.sh --execute
|
||||||
```
|
```
|
||||||
|
|
||||||
最终脚本不会请求或消费验证码,不会触发真实短信,不登录,不写数据库。它要求生产固定码模式已明确启用、五个角色各至少两名有效处理人,并核对 5 个已发布场景的版本、内容哈希、审核链和 SOP 引用与签认清单完全一致。
|
最终脚本不会请求或消费验证码,不会触发真实短信,不登录,不写数据库。它要求生产固定码模式已明确启用、五个角色各至少两名有效处理人、五通道验收证据和受控分发决定完整,并核对 5 个已发布场景的版本、内容哈希、审核链和 SOP 引用与签认清单完全一致。
|
||||||
|
|||||||
@@ -126,6 +126,9 @@ $releaseApprovalManifest = Get-Content -Raw -Encoding UTF8 -LiteralPath $release
|
|||||||
if ([string]$releaseApprovalManifest.candidateSha256 -ne $contentSha256) {
|
if ([string]$releaseApprovalManifest.candidateSha256 -ne $contentSha256) {
|
||||||
throw "Release approval candidate hash mismatch: $($releaseApprovalManifest.candidateSha256)"
|
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
|
& node (Join-Path $projectRoot 'scripts\verify-aug1-formal-content.mjs') $releaseApprovalPath | Out-Null
|
||||||
if ($LASTEXITCODE -ne 0) {
|
if ($LASTEXITCODE -ne 0) {
|
||||||
throw 'Release approval structure audit failed.'
|
throw 'Release approval structure audit failed.'
|
||||||
@@ -134,8 +137,11 @@ $approvedScenarioCount = @($releaseApprovalManifest.scenarioApprovals.PSObject.P
|
|||||||
Where-Object { [string]$_.Value.status -eq 'APPROVED' }).Count
|
Where-Object { [string]$_.Value.status -eq 'APPROVED' }).Count
|
||||||
$approvedQuestionCount = @($releaseApprovalManifest.policyQuestionApprovals.PSObject.Properties |
|
$approvedQuestionCount = @($releaseApprovalManifest.policyQuestionApprovals.PSObject.Properties |
|
||||||
Where-Object { [string]$_.Value.status -eq 'APPROVED' }).Count
|
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 |
|
$approvedSignoffCount = @($releaseApprovalManifest.signoffs.PSObject.Properties |
|
||||||
Where-Object { [string]$_.Value.status -eq 'APPROVED' }).Count
|
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')
|
$goNoGoCandidates = @(Get-ChildItem -LiteralPath (Join-Path $projectRoot 'docs') -File -Filter '*Android*Go-No-Go-20260728.md')
|
||||||
if ($goNoGoCandidates.Count -ne 1) {
|
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.',
|
'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.',
|
'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 schema: 64/64 and runtime schema bootstrap disabled/default.',
|
||||||
'- Production GET-only route probe: 20/20 (`1x200`, `6x401`, `13x405`).',
|
'- 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.',
|
'- 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.',
|
'- 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.',
|
'- 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`.'
|
'- 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)",
|
"- Policy-question candidates: $($evidence.contentCandidates.policyQuestionCandidates)",
|
||||||
"- Formally approved scenarios: $approvedScenarioCount/5",
|
"- Formally approved scenarios: $approvedScenarioCount/5",
|
||||||
"- Formally evidenced policy questions: $approvedQuestionCount/30",
|
"- Formally evidenced policy questions: $approvedQuestionCount/30",
|
||||||
|
"- Approved direct-channel acceptances: $approvedChannelCount/5",
|
||||||
"- Owner sign-offs: $approvedSignoffCount/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.',
|
'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
|
formalContentVersion = [string]$releaseApprovalManifest.formalContentVersion
|
||||||
approvedScenarios = $approvedScenarioCount
|
approvedScenarios = $approvedScenarioCount
|
||||||
approvedPolicyQuestions = $approvedQuestionCount
|
approvedPolicyQuestions = $approvedQuestionCount
|
||||||
|
approvedDirectChannels = $approvedChannelCount
|
||||||
approvedOwnerSignoffs = $approvedSignoffCount
|
approvedOwnerSignoffs = $approvedSignoffCount
|
||||||
|
controlledDistributionStatus = $distributionApprovalStatus
|
||||||
}
|
}
|
||||||
operations = @($operationSources.Keys)
|
operations = @($operationSources.Keys)
|
||||||
manualGatesRemaining = @($evidence.manualGatesRemaining)
|
manualGatesRemaining = @($evidence.manualGatesRemaining)
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ function validApproval() {
|
|||||||
secondReviewedAt: '2026-07-30T10:00:00+08:00',
|
secondReviewedAt: '2026-07-30T10:00:00+08:00',
|
||||||
} : {}),
|
} : {}),
|
||||||
businessEvidenceRef: `evidence/scenario-${index + 1}.pdf`,
|
businessEvidenceRef: `evidence/scenario-${index + 1}.pdf`,
|
||||||
|
businessEvidenceSha256: 'c'.repeat(64),
|
||||||
productionContentVersion: `aug1-v${index + 1}`,
|
productionContentVersion: `aug1-v${index + 1}`,
|
||||||
productionContentHash: String(index + 1).repeat(64),
|
productionContentHash: String(index + 1).repeat(64),
|
||||||
}]
|
}]
|
||||||
@@ -84,17 +85,51 @@ function validApproval() {
|
|||||||
reviewedBy: `${role}-reviewer`,
|
reviewedBy: `${role}-reviewer`,
|
||||||
reviewedAt: '2026-07-30T12:00:00+08:00',
|
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 {
|
return {
|
||||||
schemaVersion: '1.0',
|
schemaVersion: '1.0',
|
||||||
releaseTarget: '2026-08-01',
|
releaseTarget: '2026-08-01',
|
||||||
tenantId: '000000',
|
tenantId: '000000',
|
||||||
candidateFile: 'aug1-life-advisor-content-candidates-v0.1.json',
|
candidateFile: 'aug1-life-advisor-content-candidates-v0.1.json',
|
||||||
candidateSha256,
|
candidateSha256,
|
||||||
|
expectedApkSignerSha256,
|
||||||
releaseStatus: 'APPROVED',
|
releaseStatus: 'APPROVED',
|
||||||
formalContentVersion: 'aug1-formal-v1',
|
formalContentVersion: 'aug1-formal-v1',
|
||||||
sourceRegistry: [source],
|
sourceRegistry: [source],
|
||||||
scenarioApprovals,
|
scenarioApprovals,
|
||||||
policyQuestionApprovals,
|
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,
|
signoffs,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -123,6 +158,8 @@ test('audits the checked-in pending manifest without claiming release readiness'
|
|||||||
assert.equal(audit.status, 0, audit.stderr)
|
assert.equal(audit.status, 0, audit.stderr)
|
||||||
assert.match(audit.stdout, /approved scenarios: 0\/5/)
|
assert.match(audit.stdout, /approved scenarios: 0\/5/)
|
||||||
assert.match(audit.stdout, /approved policy questions: 0\/30/)
|
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], {
|
const strict = spawnSync(process.execPath, [verifierPath, '--strict', pendingApprovalPath], {
|
||||||
cwd: projectRoot,
|
cwd: projectRoot,
|
||||||
@@ -168,6 +205,18 @@ test('rejects incomplete or contradictory formal approvals', async (t) => {
|
|||||||
['missing owner sign-off', (approval) => {
|
['missing owner sign-off', (approval) => {
|
||||||
approval.signoffs.releaseOwner.status = 'PENDING'
|
approval.signoffs.releaseOwner.status = 'PENDING'
|
||||||
}, /releaseOwner: sign-off must be APPROVED/],
|
}, /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) {
|
for (const [name, mutate, expected] of cases) {
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ candidate.scenarios.forEach((item, index) => {
|
|||||||
secondReviewedAt: '2026-07-30T10:00:00+08:00',
|
secondReviewedAt: '2026-07-30T10:00:00+08:00',
|
||||||
} : {}),
|
} : {}),
|
||||||
businessEvidenceRef: `evidence/scenario-${index + 1}.pdf`,
|
businessEvidenceRef: `evidence/scenario-${index + 1}.pdf`,
|
||||||
|
businessEvidenceSha256: 'c'.repeat(64),
|
||||||
productionContentVersion: version,
|
productionContentVersion: version,
|
||||||
productionContentHash: hash,
|
productionContentHash: hash,
|
||||||
}
|
}
|
||||||
@@ -105,17 +106,51 @@ const signoffs = Object.fromEntries([
|
|||||||
reviewedBy: `${role}-reviewer`,
|
reviewedBy: `${role}-reviewer`,
|
||||||
reviewedAt: '2026-07-30T12:00:00+08:00',
|
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({
|
fs.writeFileSync(approvalPath, JSON.stringify({
|
||||||
schemaVersion: '1.0',
|
schemaVersion: '1.0',
|
||||||
releaseTarget: '2026-08-01',
|
releaseTarget: '2026-08-01',
|
||||||
tenantId: '000000',
|
tenantId: '000000',
|
||||||
candidateFile: 'aug1-life-advisor-content-candidates-v0.1.json',
|
candidateFile: 'aug1-life-advisor-content-candidates-v0.1.json',
|
||||||
candidateSha256: crypto.createHash('sha256').update(bytes).digest('hex'),
|
candidateSha256: crypto.createHash('sha256').update(bytes).digest('hex'),
|
||||||
|
expectedApkSignerSha256,
|
||||||
releaseStatus: 'APPROVED',
|
releaseStatus: 'APPROVED',
|
||||||
formalContentVersion: 'aug1-formal-v1',
|
formalContentVersion: 'aug1-formal-v1',
|
||||||
sourceRegistry: [source],
|
sourceRegistry: [source],
|
||||||
scenarioApprovals,
|
scenarioApprovals,
|
||||||
policyQuestionApprovals,
|
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,
|
signoffs,
|
||||||
}))
|
}))
|
||||||
fs.writeFileSync(rowsPath, rows.join('\n') + '\n')
|
fs.writeFileSync(rowsPath, rows.join('\n') + '\n')
|
||||||
@@ -177,6 +212,8 @@ run_mock_readiness() {
|
|||||||
ready_output="$(run_mock_readiness)"
|
ready_output="$(run_mock_readiness)"
|
||||||
grep -Fq 'AUG1_FIXED_CODE_NO_REAL_SMS_MODE=PASS' <<<"$ready_output"
|
grep -Fq 'AUG1_FIXED_CODE_NO_REAL_SMS_MODE=PASS' <<<"$ready_output"
|
||||||
grep -Fq 'AUG1_FORMAL_SCENARIOS=5/5' <<<"$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"
|
grep -Fq 'AUG1_RELEASE_READINESS=PASS' <<<"$ready_output"
|
||||||
|
|
||||||
handler_failure="$(
|
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 'production content hash does not match the approved snapshot' "$SCRIPT"
|
||||||
grep -Fq 'AUG1_FORMAL_SCENARIOS=5/5' "$SCRIPT"
|
grep -Fq 'AUG1_FORMAL_SCENARIOS=5/5' "$SCRIPT"
|
||||||
grep -Fq 'AUG1_FORMAL_POLICY_QUESTIONS=30/30' "$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
|
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
|
echo 'FAIL: August 1 release readiness verifier contains a mutating SQL verb' >&2
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ $requiredFragments = @(
|
|||||||
'Frozen backend JAR hash mismatch',
|
'Frozen backend JAR hash mismatch',
|
||||||
'Content candidate hash mismatch',
|
'Content candidate hash mismatch',
|
||||||
'Release approval candidate hash mismatch',
|
'Release approval candidate hash mismatch',
|
||||||
|
'Release approval APK signer mismatch',
|
||||||
"'operations/release-backend.sh'",
|
"'operations/release-backend.sh'",
|
||||||
"'operations/release-preflight.sh'",
|
"'operations/release-preflight.sh'",
|
||||||
"'operations/verify-aug1-production-api-readonly.sh'",
|
"'operations/verify-aug1-production-api-readonly.sh'",
|
||||||
@@ -34,7 +35,9 @@ $requiredFragments = @(
|
|||||||
'fixedCodeNoRealSmsConfirmed = $true',
|
'fixedCodeNoRealSmsConfirmed = $true',
|
||||||
'deployPlanSmsOrLoginTriggered = $false',
|
'deployPlanSmsOrLoginTriggered = $false',
|
||||||
"artifact = 'aug1-release-approval.json'",
|
"artifact = 'aug1-release-approval.json'",
|
||||||
'approvedPolicyQuestions = $approvedQuestionCount'
|
'approvedPolicyQuestions = $approvedQuestionCount',
|
||||||
|
'approvedDirectChannels = $approvedChannelCount',
|
||||||
|
'controlledDistributionStatus = $distributionApprovalStatus'
|
||||||
)
|
)
|
||||||
foreach ($fragment in $requiredFragments) {
|
foreach ($fragment in $requiredFragments) {
|
||||||
if (-not $source.Contains($fragment)) {
|
if (-not $source.Contains($fragment)) {
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ const candidateFileValid = nonBlank(approval.candidateFile)
|
|||||||
&& approval.candidateFile.endsWith('.json')
|
&& approval.candidateFile.endsWith('.json')
|
||||||
check(candidateFileValid, 'candidateFile must be a JSON basename beside the approval file')
|
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.candidateSha256 ?? ''), 'candidateSha256 must be lowercase SHA-256')
|
||||||
|
check(
|
||||||
|
sha256Pattern.test(approval.expectedApkSignerSha256 ?? ''),
|
||||||
|
'expectedApkSignerSha256 must be lowercase SHA-256',
|
||||||
|
)
|
||||||
|
|
||||||
if (!candidateFileValid) {
|
if (!candidateFileValid) {
|
||||||
for (const failure of failures) console.error(`FAIL: ${failure}`)
|
for (const failure of failures) console.error(`FAIL: ${failure}`)
|
||||||
@@ -110,6 +114,7 @@ for (const source of sourceRegistry) {
|
|||||||
|
|
||||||
const scenarioApprovals = approval.scenarioApprovals
|
const scenarioApprovals = approval.scenarioApprovals
|
||||||
const questionApprovals = approval.policyQuestionApprovals
|
const questionApprovals = approval.policyQuestionApprovals
|
||||||
|
const channelApprovals = approval.channelApprovals
|
||||||
check(
|
check(
|
||||||
scenarioApprovals && typeof scenarioApprovals === 'object' && !Array.isArray(scenarioApprovals),
|
scenarioApprovals && typeof scenarioApprovals === 'object' && !Array.isArray(scenarioApprovals),
|
||||||
'scenarioApprovals must be an object keyed by candidate ID',
|
'scenarioApprovals must be an object keyed by candidate ID',
|
||||||
@@ -118,14 +123,29 @@ check(
|
|||||||
questionApprovals && typeof questionApprovals === 'object' && !Array.isArray(questionApprovals),
|
questionApprovals && typeof questionApprovals === 'object' && !Array.isArray(questionApprovals),
|
||||||
'policyQuestionApprovals must be an object keyed by question ID',
|
'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 scenarioEntries = Object.entries(scenarioApprovals ?? {})
|
||||||
const questionEntries = Object.entries(questionApprovals ?? {})
|
const questionEntries = Object.entries(questionApprovals ?? {})
|
||||||
|
const channelEntries = Object.entries(channelApprovals ?? {})
|
||||||
for (const [candidateId] of scenarioEntries) {
|
for (const [candidateId] of scenarioEntries) {
|
||||||
check(expectedScenarios.has(candidateId), `${candidateId}: approval references an unknown scenario`)
|
check(expectedScenarios.has(candidateId), `${candidateId}: approval references an unknown scenario`)
|
||||||
}
|
}
|
||||||
for (const [questionId] of questionEntries) {
|
for (const [questionId] of questionEntries) {
|
||||||
check(expectedQuestions.has(questionId), `${questionId}: approval references an unknown question`)
|
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) => {
|
const resolveSourceRefs = (refs, label) => {
|
||||||
check(Array.isArray(refs) && refs.length > 0, `${label}: at least one formal source reference is required`)
|
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(entry.secondReviewedBy !== entry.reviewedBy, `${label}: high-risk reviewers must be different people`)
|
||||||
}
|
}
|
||||||
check(nonBlank(entry.businessEvidenceRef), `${label}: business evidence reference is required`)
|
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(nonBlank(entry.productionContentVersion), `${label}: production content version is required`)
|
||||||
check(sha256Pattern.test(entry.productionContentHash ?? ''), `${label}: production content hash is required`)
|
check(sha256Pattern.test(entry.productionContentHash ?? ''), `${label}: production content hash is required`)
|
||||||
}
|
}
|
||||||
@@ -208,6 +229,29 @@ if (strict) {
|
|||||||
requireReview(entry, label)
|
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 = [
|
const requiredSignoffs = [
|
||||||
'businessOwner',
|
'businessOwner',
|
||||||
'knowledgeOwner',
|
'knowledgeOwner',
|
||||||
@@ -224,6 +268,37 @@ if (strict) {
|
|||||||
check(signoff?.status === 'APPROVED', `${role}: sign-off must be APPROVED`)
|
check(signoff?.status === 'APPROVED', `${role}: sign-off must be APPROVED`)
|
||||||
if (signoff) requireReview(signoff, role)
|
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) {
|
if (failures.length > 0) {
|
||||||
@@ -243,12 +318,15 @@ if (scenarioSnapshots) {
|
|||||||
} else {
|
} else {
|
||||||
const approvedScenarios = scenarioEntries.filter(([, value]) => value?.status === 'APPROVED').length
|
const approvedScenarios = scenarioEntries.filter(([, value]) => value?.status === 'APPROVED').length
|
||||||
const approvedQuestions = questionEntries.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
|
const approvedSignoffs = Object.values(approval.signoffs ?? {}).filter((value) => value?.status === 'APPROVED').length
|
||||||
console.log('August 1 formal content approval audit passed')
|
console.log('August 1 formal content approval audit passed')
|
||||||
console.log(`- releaseStatus: ${approval.releaseStatus}`)
|
console.log(`- releaseStatus: ${approval.releaseStatus}`)
|
||||||
console.log(`- formal sources: ${sourceRegistry.length}`)
|
console.log(`- formal sources: ${sourceRegistry.length}`)
|
||||||
console.log(`- approved scenarios: ${approvedScenarios}/5`)
|
console.log(`- approved scenarios: ${approvedScenarios}/5`)
|
||||||
console.log(`- approved policy questions: ${approvedQuestions}/30`)
|
console.log(`- approved policy questions: ${approvedQuestions}/30`)
|
||||||
|
console.log(`- approved direct channels: ${approvedChannels}/5`)
|
||||||
console.log(`- approved owner sign-offs: ${approvedSignoffs}/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'}`)
|
console.log(`- strict release ready: ${strict ? 'true' : 'not requested'}`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -213,6 +213,8 @@ for snapshot in "${snapshots[@]}"; do
|
|||||||
done
|
done
|
||||||
echo "AUG1_FORMAL_SCENARIOS=5/5"
|
echo "AUG1_FORMAL_SCENARIOS=5/5"
|
||||||
echo "AUG1_FORMAL_POLICY_QUESTIONS=30/30"
|
echo "AUG1_FORMAL_POLICY_QUESTIONS=30/30"
|
||||||
|
echo "AUG1_CHANNEL_ACCEPTANCE=5/5"
|
||||||
echo "AUG1_OWNER_SIGNOFFS=5/5"
|
echo "AUG1_OWNER_SIGNOFFS=5/5"
|
||||||
|
echo "AUG1_DISTRIBUTION_APPROVAL=PASS"
|
||||||
echo "AUG1_RELEASE_READINESS=PASS"
|
echo "AUG1_RELEASE_READINESS=PASS"
|
||||||
REMOTE
|
REMOTE
|
||||||
|
|||||||
Reference in New Issue
Block a user