Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,18 @@ python scripts/verify_release_readiness.py --repository-root . --allow-pending
python scripts/verify_release_readiness.py --repository-root . --tag v0.1.0
```

六个人工 gate 的首项证据必须是场景完整、执行人与复核人分离的本地 JSON 包;模板与单包校验命令如下:

```powershell
python scripts/verify_readiness_evidence.py init-manual --gate windows_admin_matrix --release-version 0.1.0 --output path/to/new-evidence.json
python scripts/verify_readiness_evidence.py verify-manual --repository-root . --evidence path/to/completed-evidence.json
```

Beta 参与者从应用 Metrics 页主动导出本机聚合值;Release Owner 使用版本化策略离线去重和汇总,原始文件不进入仓库:

```powershell
python scripts/aggregate_beta_metrics.py --policy release/beta-metrics-policy.v0.1.json --input-directory path/to/reviewed-exports --output path/to/new-beta-report.json --require-ready
python scripts/verify_readiness_evidence.py verify-beta --repository-root . --evidence path/to/new-beta-report.json
```

流程与最小样本量见 [`docs/beta-evidence.html`](./docs/beta-evidence.html)。
Expand Down
7 changes: 5 additions & 2 deletions docs/beta-evidence.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,18 @@ <h2 id="aggregate">聚合</h2>
--policy release/beta-metrics-policy.v0.1.json \
--input-directory path/to/reviewed-exports \
--output path/to/new-beta-report.json \
--require-ready</code></pre>
--require-ready
python scripts/verify_readiness_evidence.py verify-beta \
--repository-root . \
--evidence path/to/new-beta-report.json</code></pre>
<p>工具拒绝重复样本、重复 JSON key、symlink、超限文件、错误版本、非目标平台、未知字段、不安全隐私声明、伪造率和违反子集关系的计数。输出必须是新文件。</p>

<h2 id="review">审核</h2>
<ol>
<li>确认输入目录只含每位参与者最新且主动提交的一份导出。</li>
<li>保存聚合命令、策略版本、最终报告与受控原始证据位置。</li>
<li>由 Release Owner 审核三个 gate;未达标时继续取样或修复产品。</li>
<li>通过后仅将无样本 ID 的报告加入 readiness evidence;原始文件不进入仓库。</li>
<li>通过后将无样本 ID 的报告作为三个 Beta gate 的首项 readiness evidence;原始文件不进入仓库。</li>
</ol>

<footer class="doc-footer"><a href="release.html">← 签名发布</a><a href="progress.html">下一篇:进度跟踪 →</a></footer>
Expand Down
2 changes: 1 addition & 1 deletion docs/incident-response.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ <h2 id="evidence">证据边界</h2>
<ul><li>使用 <a href="support.html">支持包验证器</a>生成摘要;不得把原始 Prompt、Raw、私钥或日志详情写入工单。</li><li>发布事故保存 workflow URL、tag、manifest、文件哈希和签名结果。</li><li>数据库问题先复制加密文件并限制访问;不得要求用户提供 OS 凭据库中的 key。</li><li>证据到期按案件策略删除;安全事件只保留完成复盘所需的最小集合。</li></ul>

<h2 id="drill">GA 演练</h2>
<ol><li>在隔离 Windows 与 macOS 设备分别模拟 sidecar 强杀、应用强杀和 owner 断连,确认网络恢复。</li><li>使用签名候选版本模拟迁移失败,确认只读历史、搜索和脱敏导出。</li><li>模拟 updater manifest 哈希或签名不匹配,确认不会公开或安装。</li><li>记录负责人、时间线、检测/缓解时间、证据链接和改进项;由非执行者复核。</li><li>将演练证据写入 <code>release/readiness.v0.1.json</code> 后,发布门禁才可通过。</li></ol>
<ol><li>在隔离 Windows 与 macOS 设备分别模拟 sidecar 强杀、应用强杀和 owner 断连,确认网络恢复。</li><li>使用签名候选版本模拟迁移失败,确认只读历史、搜索和脱敏导出。</li><li>模拟 updater manifest 哈希或签名不匹配,确认不会公开或安装。</li><li>记录负责人、时间线、检测/缓解时间、证据链接和改进项;由非执行者复核。</li><li> <code>verify_readiness_evidence.py init-manual --gate incident_response_drill</code> 创建场景包;完成并校验后作为该 gate 的首项证据。</li></ol>
<footer class="doc-footer"><a href="support.html">← 支持处理</a><a href="progress.html">下一篇:进度跟踪 →</a></footer>
</main><aside class="toc"><strong>本页</strong><a href="#severity">分级</a><a href="#flow">响应链</a><a href="#playbooks">Playbook</a><a href="#evidence">证据边界</a><a href="#drill">GA 演练</a></aside></div>
</body></html>
4 changes: 2 additions & 2 deletions docs/progress.html
Original file line number Diff line number Diff line change
Expand Up @@ -131,14 +131,14 @@ <h2 id="backlog">6. 后续迭代承诺</h2>
<tr><td>S6</td><td>PAR-005~007、APP-005</td><td><span class="status done">Done</span></td><td>四厂商适配器、token/成本/指纹、全文搜索和结构/文本 Compare 全部完成</td></tr>
<tr><td>S7</td><td>TST-004、性能、可访问性、诊断与保留</td><td><span class="status active">In progress</span></td><td>axe、键盘与焦点、双主题截图、千条列表预算及 Gateway P95 门禁完成;继续屏幕阅读器和 Beta 真实环境测量</td></tr>
<tr><td>S8</td><td>SEC-004、REL-001/002、安全评审</td><td><span class="status active">In progress</span></td><td>仓库供应链门禁、失败关闭的跨平台签名发布流水线、应用内更新、预更新恢复快照、只读历史恢复与支持处理闭环已接入;正式凭据、首批制品、真实回滚与安全评审待推进</td></tr>
<tr><td>S9</td><td>REL-003、完整回归与发布演练</td><td><span class="status active">In progress</span></td><td>兼容矩阵、事故响应、版本说明与失败关闭 readiness 门禁完成;九项真实环境/人工 gate 待关闭</td></tr>
<tr><td>S9</td><td>REL-003、完整回归与发布演练</td><td><span class="status active">In progress</span></td><td>兼容矩阵、事故响应、版本说明、结构化人工/Beta 证据校验与失败关闭 readiness 门禁完成;九项真实环境/人工 gate 待关闭</td></tr>
</tbody></table></div>

<h2 id="blockers">7. 阻塞项</h2>
<div class="table-wrap"><table><thead><tr><th>ID</th><th>阻塞</th><th>影响</th><th>Owner</th><th>开始</th><th>解除条件</th><th>状态</th></tr></thead><tbody>
<tr><td class="task-id">B-001</td><td>核心团队与正式投入未确认</td><td>无法建立可靠的人力与交付日期基线</td><td>TBD</td><td>2026-07-14</td><td>角色、投入比例与正式启动日确定</td><td><span class="status blocked">Open</span></td></tr>
<tr><td class="task-id">B-002</td><td>正式平台证书、updater key 与 release environment 未配置</td><td>无法生成首批可分发签名制品</td><td>Release Owner TBD</td><td>2026-07-21</td><td>受保护环境完成密钥托管并通过候选构建</td><td><span class="status blocked">Open</span></td></tr>
<tr><td class="task-id">B-003</td><td>九项 GA readiness 人工/真实环境证据未完成</td><td>签名发布工作流按设计拒绝公开 Release</td><td>Release / QA / Security TBD</td><td>2026-07-21</td><td><code>release/readiness.v0.1.json</code> 全部 gate 为 passed 且证据可验证</td><td><span class="status blocked">Open</span></td></tr>
<tr><td class="task-id">B-003</td><td>九项 GA readiness 人工/真实环境证据未完成</td><td>签名发布工作流按设计拒绝公开 Release</td><td>Release / QA / Security TBD</td><td>2026-07-21</td><td>六个人工场景包和 Beta 聚合报告通过校验,<code>release/readiness.v0.1.json</code> 零 pending</td><td><span class="status blocked">Open</span></td></tr>
<tr><td colspan="7">新增阻塞项时在此追加;解除后保留记录并将状态改为 Closed。</td></tr>
</tbody></table></div>

Expand Down
5 changes: 4 additions & 1 deletion docs/release.html
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,15 @@ <h2 id="secrets">发布环境</h2>
<h2 id="trigger">触发与证据</h2>
<ol>
<li>同步 <code>tauri.conf.json</code>、桌面 <code>package.json</code> 和 Tauri <code>Cargo.toml</code> 的版本。</li>
<li>提交 <code>release/notes/v&lt;version&gt;.md</code>,并在 <code>release/readiness.v0.1.json</code> 中附上全部人工验收证据。</li>
<li>提交 <code>release/notes/v&lt;version&gt;.md</code>;人工 gate 的首项证据使用 <code>verify_readiness_evidence.py init-manual</code> 生成并完成,Beta gate 引用已通过策略的聚合报告。</li>
<li>签名 RC 可保留 pending 人工 gate,并以 GitHub prerelease 发布供隔离设备演练;稳定版必须零 pending。</li>
<li>确认 main CI 全绿后创建并推送 <code>v&lt;semver&gt;</code> tag,或手动选择已存在 tag 运行 <strong>Signed release</strong>。</li>
<li>审批 <code>release</code> environment;工作流会拒绝不在 main 中的 tag。</li>
<li>保存工作流链接、<code>release-manifest.v0.1.json</code>、<code>latest.json</code>、安装包和 `.sig` 作为发布证据。</li>
</ol>
<pre><code>python scripts/verify_readiness_evidence.py verify-manual --repository-root . --evidence path/to/completed-evidence.json
python scripts/verify_readiness_evidence.py verify-beta --repository-root . --evidence path/to/beta-report.json
python scripts/verify_release_readiness.py --repository-root . --tag v0.1.0</code></pre>

<h2 id="rollback">回滚演练</h2>
<ul class="checklist">
Expand Down
122 changes: 118 additions & 4 deletions scripts/tests/test_prepare_release.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
verify_release,
)
from verify_release_readiness import REQUIRED_GATE_IDS
from verify_readiness_evidence import BETA_GATE_IDS, MANUAL_GATE_IDS, build_manual_template


class ReleasePreparationTests(unittest.TestCase):
Expand All @@ -40,6 +41,103 @@ def repository(self, root: Path, version: str = "1.2.3") -> Path:
notes = repository / "release" / "notes" / f"v{version}.md"
notes.parent.mkdir(parents=True)
notes.write_text(f"# CodeIsCheap {version}\n", encoding="utf-8")
(repository / "release/beta-metrics-policy.v0.1.json").write_text(
json.dumps(
{
"schemaVersion": "0.1",
"releaseVersion": version,
"allowedPlatforms": ["macos", "windows"],
"minimums": {
"contributors": 3,
"firstCaptureSamples": 3,
"supportedCaptures": 6,
"completedSessions": 6,
},
"thresholds": {
"firstCaptureP50MaxMs": 500,
"endpointParseRateMinBasisPointsExclusive": 9500,
"crashFreeRateMinBasisPointsExclusive": 9950,
},
}
),
encoding="utf-8",
)
structured: dict[str, str] = {}
if "-" not in version:
evidence_directory = repository / "evidence"
evidence_directory.mkdir()
for gate_id in MANUAL_GATE_IDS:
packet = build_manual_template(gate_id, version)
packet.update(
{
"completedAt": "2026-07-20T00:00:00Z",
"executor": "qa-executor",
"reviewer": "release-owner",
"summary": f"{gate_id} acceptance completed.",
}
)
for scenario in packet["scenarios"]:
attachment = evidence_directory / f'{gate_id}-{scenario["id"]}.txt'
attachment.write_text("reviewed result", encoding="utf-8")
scenario.update(
{
"architecture": "cross-platform",
"environment": "isolated acceptance environment",
"status": "passed",
"evidence": [f"evidence/{attachment.name}"],
"notes": "Expected behavior observed.",
}
)
packet_path = evidence_directory / f"{gate_id}.json"
packet_path.write_text(json.dumps(packet), encoding="utf-8")
structured[gate_id] = f"evidence/{packet_path.name}"
beta_report = {
"schemaVersion": "0.1",
"releaseVersion": version,
"generatedAtUnixMs": 1,
"privacy": {
"sourceContentIncluded": False,
"sampleIdsIncluded": False,
"automaticUpload": False,
},
"cohort": {
"contributorCount": 3,
"platformCounts": {"macos": 1, "windows": 2},
"sourceSha256": [f"{value:064x}" for value in range(1, 4)],
},
"metrics": {
"firstCaptureSampleCount": 3,
"firstCaptureP50Ms": 200,
"supportedCaptureCount": 100,
"parsedCaptureCount": 99,
"parseRateBasisPoints": 9900,
"completedSessionCount": 1000,
"uncleanSessionCount": 0,
"crashFreeRateBasisPoints": 10000,
},
"gates": {
"firstCaptureP50": {
"status": "passed",
"actual": 200,
"requirement": "under 500 ms",
},
"endpointParseRate": {
"status": "passed",
"actual": 9900,
"requirement": "over 9500 basis points",
},
"crashFreeSessions": {
"status": "passed",
"actual": 10000,
"requirement": "over 9950 basis points",
},
},
"ready": True,
}
beta_path = evidence_directory / "beta-report.json"
beta_path.write_text(json.dumps(beta_report), encoding="utf-8")
for gate_id in BETA_GATE_IDS:
structured[gate_id] = "evidence/beta-report.json"
readiness = {
"schemaVersion": "0.1",
"releaseVersion": version,
Expand All @@ -48,10 +146,26 @@ def repository(self, root: Path, version: str = "1.2.3") -> Path:
"gates": [
{
"id": gate_id,
"status": "passed",
"reviewer": "release-owner",
"completedAt": "2026-07-21T00:00:00Z",
"evidence": ["docs/release-evidence.html"],
"status": (
"pending"
if "-" in version and gate_id in MANUAL_GATE_IDS | BETA_GATE_IDS
else "passed"
),
"reviewer": (
None
if "-" in version and gate_id in MANUAL_GATE_IDS | BETA_GATE_IDS
else "release-owner"
),
"completedAt": (
None
if "-" in version and gate_id in MANUAL_GATE_IDS | BETA_GATE_IDS
else "2026-07-21T00:00:00Z"
),
"evidence": (
[]
if "-" in version and gate_id in MANUAL_GATE_IDS | BETA_GATE_IDS
else [structured.get(gate_id, "docs/release-evidence.html")]
),
}
for gate_id in sorted(REQUIRED_GATE_IDS)
],
Expand Down
Loading
Loading