2 Commits

Author SHA1 Message Date
laily c113f68bf4 feat: add draw-prototype-flow 2026-08-04 13:55:32 +08:00
laily 5ff8899b48 feat: update manage-release 2026-08-04 13:55:16 +08:00
15 changed files with 1143 additions and 80 deletions
+37
View File
@@ -0,0 +1,37 @@
# draw-prototype-flow
把产品想法或需求文档画成一份可编辑的 Draw.io 低保真原型,并同步表达用户动线、页面状态、角色权限和关键规则。
## 什么时候使用
- 想从零梳理 Web、移动端或后台系统的页面原型与用户流程。
- 已有 `.drawio` 原型,希望补齐跳转、异常态、权限或规则说明。
- 需要一张能用于产品评审、开发对齐和测试设计的产品说明板。
## 使用前准备
最好提供产品目标、目标用户、核心任务和已有需求材料。缺少细节时,Agent 会先列出必要假设;只有关键范围无法判断时才会询问。
可以指定「快速 / 标准 / 完整」档位;不指定时默认按产品评审所需的标准档交付。
## 使用示例
```text
请用 draw-prototype-flow 为一个企业访客预约系统画低保真原型和用户动线。
覆盖访客申请、员工审批、前台核验,以及驳回、过期和无权限状态,输出 editable.drawio。
```
```text
检查 current.drawio,把所有主按钮的去向、审批状态和错误恢复路径补完整,并保持现有视觉风格。
```
## Agent 会做什么
1. 整理角色、页面、动作、状态和规则。
2. 绘制可编辑页面、关键状态和带标签的用户动线。
3. 补充页面索引、权限矩阵或字段规则等必要规格。
4. 校验 Draw.io 结构与跨图引用,并说明假设和未决项。
## 如何判断完成
最终会得到可在 diagrams.net/Draw.io 打开的 `.drawio` 源文件。核心任务应能从入口走到结果,每个关键按钮有明确去向,异常状态有恢复动作,原型、动线和规格使用同一套页面与状态 ID。
+138
View File
@@ -0,0 +1,138 @@
---
name: draw-prototype-flow
description: >-
将产品想法、需求文档或现有系统整理成可编辑的低保真 Draw.io 原型、用户动线和产品规则说明板。
当用户提出画原型图、线框图、用户流程、用户动线、页面跳转、交互状态、wireframe、user flow、
UX flow、产品方案可视化,或要求检查和完善现有 .drawio 产品原型时使用。适合同时表达页面、弹窗、
异常态、角色权限、状态流转、字段规则与操作反馈;不用于像素级高保真 UI、纯技术架构图或单张插画。
---
# 绘制低保真原型与用户动线
把原型、动线和关键规则放进同一套可追踪的产品模型,交付原生可编辑的 `.drawio` 文件。
## 使用资源
- 快速单页原型复制 `assets/prototype-flow-template.drawio`,保留其画板尺寸、语义色和基础组件,再替换占位内容。
- 多页面原型复制 `assets/scaffold-spec.example.json`,填写页面清单和 `transitions`,再运行 `scripts/scaffold_drawio.py` 生成可编辑骨架与总览连线,禁止手工从空 XML 重复搭建外壳。
- 开始绘制前读取 `references/design-patterns.md`;遇到复杂业务、多个角色或超过 8 个页面时,再读取 `references/product-model.md`
- 完成后运行 `scripts/validate_drawio.py`。该脚本可检查常见的压缩或未压缩 Draw.io 文件;Agent 输出时仍设置 `compressed="false"`,便于审阅、差异比较和后续修改。
## 工作流
### 1. 确认交付边界
从用户材料中提取目标用户、核心任务、终端形态、必须覆盖的流程和输出路径。信息不全但不影响主流程时,采用合理假设并在交付说明中列出;只有会改变产品范围或关键流程时才询问。
默认交付:
- 原生可编辑 `.drawio`
- 一张端到端动线总览;
- 核心任务的页面与关键状态;
- 页面索引、状态流转和角色权限等必要规格;
- 可选的 PNG/SVG 预览,仅作为查看副本,不能代替源文件。
根据请求选择交付档位,避免把概念验证画成大而全的规格库:
- **快速**:1 条核心动线、3~8 个画板、最高风险的 1~2 个异常;适合早期讨论。
- **标准(默认)**:核心任务、8~20 个画板、动线总览、关键状态和必要的权限/字段规则;适合产品评审。
- **完整**:多角色、多业务域、系统异常、状态机、权限矩阵和详细规格;适合开发与测试对齐。
用户没有指定时,根据材料成熟度选择,先说明档位和不覆盖的内容。
### 2. 先建模,再画图
建立稳定 ID,避免用会随排序变化的显示序号充当引用:
- 角色:`R-APPLICANT``R-APPROVER`
- 页面:`P-LOGIN``P-ORDER-LIST`
- 状态:`S-DRAFT``S-PENDING`
- 动作:`A-SUBMIT``A-REJECT`
至少整理以下关系:
1. 角色 → 目标 → 入口;
2. 页面/弹窗 → 可见信息 → 可执行动作;
3. 动作 → 前置条件 → 成功去向 → 失败反馈;
4. 业务对象 → 状态 → 可编辑性 → 角色权限;
5. 字段 → 必填/值域/联动 → 校验反馈。
复杂任务使用 `references/product-model.md` 中的清单模板。先消除断头页面、无去向按钮和无入口状态,再开始排版。
### 3. 选择画布结构
- 不超过 12 个交互画板:可放在一个 Draw.io 页面,按「主流程 → 异常态 → 规格」排列。
- 超过 12 个交互画板:默认拆成多个 Draw.io 页面,例如 `00-总览``10-申请人``20-审批人``30-异常状态``40-规格`。总览页保留所有跨域跳转。
- 用户明确需要一张巨幅说明板时才使用单页矩阵;每行最多 5 个 1600×900 画板,横向间距 120,纵向间距 260,先放原型、再放流程和规格。
这里的“交互画板”只统计用户可进入的页面、弹窗和异常状态;动线总览、页面索引、字段字典和权限矩阵不参与 12 个阈值计算。生成多页骨架:
```bash
python3 <skill-dir>/scripts/scaffold_drawio.py scaffold-spec.json output.drawio
```
每个画板标题使用「显示序号 · 页面名 · 状态」;底部元信息写角色、入口、核心功能和下一步,但跨页引用使用稳定 ID,例如 `下一步:P-DETAIL`,不要手写易漂移的序号。
### 4. 绘制页面与状态
遵循 `references/design-patterns.md` 的视觉语法,并保持低保真:
- 使用一致的应用外壳、导航、标题层级、表单、表格、按钮和反馈组件;
- 主操作、成功、警告、危险和辅助跳转使用固定语义色;
- 按钮、字段、弹窗和表格单元格保持独立图元,禁止把整个页面栅格化为图片;
- 为核心任务画成功路径,也覆盖与业务相关的加载、空数据、校验失败、权限不足、并发冲突和不可逆确认;
- 弹窗作为触发页面的明确状态,必须同时画取消/返回路径和确认后的结果;
- 内容优先使用真实语义的示例数据,避免大段 Lorem Ipsum。
### 5. 绘制动线
在总览中按角色或阶段使用泳道。所有跳转必须使用 Draw.io edge,并设置真实的 `source``target`
- 节点写页面稳定 ID 与名称;
- 连线标签写用户动作或系统结果,如「提交审批」「保存失败」「自动完结」;
- 判断节点写条件,分支在线上写具体结果;
- 成功用绿、警告/可恢复异常用橙、破坏性操作用红、普通导航用蓝;
- 连线优先正交,绕开画板与文字,不用无来源或无目标的装饰箭头;
- 每个入口可达,每个非终态有去向,每个终态有清晰结果。
### 6. 补齐产品规格
原型之后至少提供页面与动作索引。按复杂度增加:
- 角色 × 页面 × 操作权限矩阵;
- 状态流转、触发动作、操作者和结果;
- 字段字典、值域、必填与联动规则;
- 异常反馈与恢复动作;
- 报表、导出或批处理口径。
不要为凑数量复制页面。规格应解释原型无法准确表达的规则,并使用与原型相同的稳定 ID。
### 7. 一致性检查
逐项核对:
1. 每个核心任务都能从入口走到明确结果;
2. 每个可点击主操作都在动线或规格中有去向;
3. 每条连线的源按钮、目标页面和标签语义一致;
4. 页面状态、角色权限、字段规则和按钮可编辑性不冲突;
5. 确认弹窗、失败反馈和恢复路径成对出现;
6. 所有引用使用稳定 ID,显示序号没有被当作唯一标识;
7. 文字没有明显截断,图元不重叠,连线不穿过主要内容。
运行结构校验:
```bash
python3 <skill-dir>/scripts/validate_drawio.py <output.drawio> --min-frames 1
```
存在两个以上交互画板时追加 `--min-edges 1`,确保至少有一条真实动线;正式交付按核心转移清单设置更高的最小连线数。
多页面或正式交付追加 `--strict`;如果环境有 Draw.io 导出工具,再导出 PNG/SVG 并目视检查全图和 2~3 个关键画板。不要把包含私有需求的文件上传到公共在线转换服务。
## 完成标准
- `.drawio` 能被 XML 解析并在 diagrams.net/Draw.io 打开;
- 图元和文字可单独编辑,关键跳转是有源和目标的连接线;
- 核心流程、关键分支、角色权限和业务状态相互一致;
- 校验无 error;正式交付在 `--strict` 下也无 warning,或逐条说明保留原因;
- 向用户列出产物路径、采用的假设、覆盖范围以及仍待产品决策的问题。
@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8"?>
<mxfile host="app.diagrams.net" agent="draw-prototype-flow" version="24.7.17" type="device" compressed="false">
<diagram id="prototype-template" name="00-原型与动线">
<mxGraphModel dx="1200" dy="800" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="0" pageScale="1" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="label-p01" value="01 · P-LIST · 任务列表" style="text;html=0;strokeColor=none;fillColor=none;fontColor=#10284B;fontSize=24;fontStyle=1;fontFamily=Microsoft YaHei;" vertex="1" parent="1"><mxGeometry x="0" y="15" width="1000" height="40" as="geometry"/></mxCell>
<mxCell id="p01-bg" value="" style="rounded=0;whiteSpace=wrap;html=0;fillColor=#F4F7FB;strokeColor=#D9E2EF;" vertex="1" parent="1"><mxGeometry x="0" y="70" width="1600" height="900" as="geometry"/></mxCell>
<mxCell id="p01-nav" value="产品名称&#xa;工作台&#xa;我的任务&#xa;数据概览&#xa;系统设置" style="rounded=0;whiteSpace=wrap;html=0;fillColor=#061B3A;strokeColor=#061B3A;fontColor=#C8D6EA;fontSize=16;fontFamily=Microsoft YaHei;align=left;verticalAlign=top;spacingTop=32;spacingLeft=32;" vertex="1" parent="1"><mxGeometry x="0" y="70" width="260" height="900" as="geometry"/></mxCell>
<mxCell id="p01-title" value="我的任务" style="text;html=0;strokeColor=none;fillColor=none;fontColor=#10284B;fontSize=30;fontStyle=1;fontFamily=Microsoft YaHei;align=left;" vertex="1" parent="1"><mxGeometry x="310" y="120" width="500" height="50" as="geometry"/></mxCell>
<mxCell id="p01-desc" value="查看进度、继续处理并创建新任务" style="text;html=0;strokeColor=none;fillColor=none;fontColor=#718198;fontSize=14;fontFamily=Microsoft YaHei;align=left;" vertex="1" parent="1"><mxGeometry x="310" y="170" width="600" height="30" as="geometry"/></mxCell>
<mxCell id="p01-filter" value="状态:全部 关键词:请输入名称 查询" style="rounded=1;whiteSpace=wrap;html=0;fillColor=#FFFFFF;strokeColor=#D9E2EF;fontColor=#40516A;fontSize=14;fontFamily=Microsoft YaHei;align=left;spacingLeft=18;arcSize=8;" vertex="1" parent="1"><mxGeometry x="310" y="225" width="1240" height="70" as="geometry"/></mxCell>
<mxCell id="p01-new" value="+ 创建任务" style="rounded=1;whiteSpace=wrap;html=0;fillColor=#1768E8;strokeColor=#1768E8;fontColor=#FFFFFF;fontSize=16;fontStyle=1;fontFamily=Microsoft YaHei;arcSize=10;" vertex="1" parent="1"><mxGeometry x="1330" y="130" width="220" height="48" as="geometry"/></mxCell>
<mxCell id="p01-table-head" value="编号 名称 状态 更新时间 操作" style="rounded=0;whiteSpace=wrap;html=0;fillColor=#EAF0F8;strokeColor=#D9E2EF;fontColor=#263750;fontSize=14;fontStyle=1;fontFamily=Microsoft YaHei;align=left;spacingLeft=18;" vertex="1" parent="1"><mxGeometry x="310" y="335" width="1240" height="54" as="geometry"/></mxCell>
<mxCell id="p01-table-row1" value="T-001 示例任务 A 待处理 2026-08-04 10:30 查看 编辑" style="rounded=0;whiteSpace=wrap;html=0;fillColor=#FFFFFF;strokeColor=#D9E2EF;fontColor=#40516A;fontSize=14;fontFamily=Microsoft YaHei;align=left;spacingLeft=18;" vertex="1" parent="1"><mxGeometry x="310" y="389" width="1240" height="62" as="geometry"/></mxCell>
<mxCell id="p01-table-row2" value="T-002 示例任务 B 已完成 2026-08-03 16:20 查看" style="rounded=0;whiteSpace=wrap;html=0;fillColor=#F7F9FC;strokeColor=#D9E2EF;fontColor=#40516A;fontSize=14;fontFamily=Microsoft YaHei;align=left;spacingLeft=18;" vertex="1" parent="1"><mxGeometry x="310" y="451" width="1240" height="62" as="geometry"/></mxCell>
<mxCell id="p01-meta" value="角色:R-USER|入口:登录后首页|功能:筛选、查看、创建|下一步:P-EDIT" style="text;html=0;strokeColor=none;fillColor=none;fontColor=#718198;fontSize=13;fontFamily=Microsoft YaHei;align=left;" vertex="1" parent="1"><mxGeometry x="310" y="915" width="1240" height="30" as="geometry"/></mxCell>
<mxCell id="label-p02" value="02 · P-EDIT · 创建任务 · 编辑中" style="text;html=0;strokeColor=none;fillColor=none;fontColor=#10284B;fontSize=24;fontStyle=1;fontFamily=Microsoft YaHei;" vertex="1" parent="1"><mxGeometry x="1720" y="15" width="1000" height="40" as="geometry"/></mxCell>
<mxCell id="p02-bg" value="" style="rounded=0;whiteSpace=wrap;html=0;fillColor=#F4F7FB;strokeColor=#D9E2EF;" vertex="1" parent="1"><mxGeometry x="1720" y="70" width="1600" height="900" as="geometry"/></mxCell>
<mxCell id="p02-nav" value="产品名称&#xa;工作台&#xa;我的任务&#xa;数据概览&#xa;系统设置" style="rounded=0;whiteSpace=wrap;html=0;fillColor=#061B3A;strokeColor=#061B3A;fontColor=#C8D6EA;fontSize=16;fontFamily=Microsoft YaHei;align=left;verticalAlign=top;spacingTop=32;spacingLeft=32;" vertex="1" parent="1"><mxGeometry x="1720" y="70" width="260" height="900" as="geometry"/></mxCell>
<mxCell id="p02-title" value="创建任务" style="text;html=0;strokeColor=none;fillColor=none;fontColor=#10284B;fontSize=30;fontStyle=1;fontFamily=Microsoft YaHei;align=left;" vertex="1" parent="1"><mxGeometry x="2030" y="120" width="500" height="50" as="geometry"/></mxCell>
<mxCell id="p02-form" value="" style="rounded=1;whiteSpace=wrap;html=0;fillColor=#FFFFFF;strokeColor=#D9E2EF;arcSize=8;" vertex="1" parent="1"><mxGeometry x="2030" y="210" width="1240" height="570" as="geometry"/></mxCell>
<mxCell id="p02-field1" value="任务名称 *&#xa;请输入任务名称" style="rounded=1;whiteSpace=wrap;html=0;fillColor=#FFFFFF;strokeColor=#CBD6E5;fontColor=#40516A;fontSize=14;fontFamily=Microsoft YaHei;align=left;spacingLeft=14;arcSize=8;" vertex="1" parent="1"><mxGeometry x="2080" y="270" width="540" height="74" as="geometry"/></mxCell>
<mxCell id="p02-field2" value="任务类型 *&#xa;请选择类型 ▾" style="rounded=1;whiteSpace=wrap;html=0;fillColor=#FFFFFF;strokeColor=#CBD6E5;fontColor=#40516A;fontSize=14;fontFamily=Microsoft YaHei;align=left;spacingLeft=14;arcSize=8;" vertex="1" parent="1"><mxGeometry x="2670" y="270" width="540" height="74" as="geometry"/></mxCell>
<mxCell id="p02-field3" value="任务说明&#xa;请输入必要的背景、范围和完成标准" style="rounded=1;whiteSpace=wrap;html=0;fillColor=#FFFFFF;strokeColor=#CBD6E5;fontColor=#40516A;fontSize=14;fontFamily=Microsoft YaHei;align=left;verticalAlign=top;spacingTop=14;spacingLeft=14;arcSize=8;" vertex="1" parent="1"><mxGeometry x="2080" y="390" width="1130" height="180" as="geometry"/></mxCell>
<mxCell id="p02-cancel" value="取消" style="rounded=1;whiteSpace=wrap;html=0;fillColor=#FFFFFF;strokeColor=#AFC1DA;fontColor=#27405F;fontSize=16;fontStyle=1;fontFamily=Microsoft YaHei;arcSize=10;" vertex="1" parent="1"><mxGeometry x="2720" y="700" width="200" height="50" as="geometry"/></mxCell>
<mxCell id="p02-submit" value="提交" style="rounded=1;whiteSpace=wrap;html=0;fillColor=#1768E8;strokeColor=#1768E8;fontColor=#FFFFFF;fontSize=16;fontStyle=1;fontFamily=Microsoft YaHei;arcSize=10;" vertex="1" parent="1"><mxGeometry x="2970" y="700" width="240" height="50" as="geometry"/></mxCell>
<mxCell id="p02-meta" value="角色:R-USER|入口:P-LIST / A-CREATE|状态:S-EDITING|下一步:P-CONFIRM" style="text;html=0;strokeColor=none;fillColor=none;fontColor=#718198;fontSize=13;fontFamily=Microsoft YaHei;align=left;" vertex="1" parent="1"><mxGeometry x="2030" y="915" width="1240" height="30" as="geometry"/></mxCell>
<mxCell id="label-p03" value="03 · P-CONFIRM · 提交确认" style="text;html=0;strokeColor=none;fillColor=none;fontColor=#10284B;fontSize=24;fontStyle=1;fontFamily=Microsoft YaHei;" vertex="1" parent="1"><mxGeometry x="3440" y="15" width="1000" height="40" as="geometry"/></mxCell>
<mxCell id="p03-bg" value="" style="rounded=0;whiteSpace=wrap;html=0;fillColor=#F4F7FB;strokeColor=#D9E2EF;" vertex="1" parent="1"><mxGeometry x="3440" y="70" width="1600" height="900" as="geometry"/></mxCell>
<mxCell id="p03-context" value="创建任务页面(保留用户已填写内容)" style="rounded=1;whiteSpace=wrap;html=0;fillColor=#FFFFFF;strokeColor=#D9E2EF;fontColor=#718198;fontSize=18;fontFamily=Microsoft YaHei;" vertex="1" parent="1"><mxGeometry x="3540" y="170" width="1400" height="650" as="geometry"/></mxCell>
<mxCell id="p03-overlay" value="" style="rounded=0;whiteSpace=wrap;html=0;fillColor=#10284B;strokeColor=none;opacity=25;" vertex="1" parent="1"><mxGeometry x="3440" y="70" width="1600" height="900" as="geometry"/></mxCell>
<mxCell id="p03-modal" value="" style="rounded=1;whiteSpace=wrap;html=0;fillColor=#FFFFFF;strokeColor=#CBD6E5;arcSize=10;" vertex="1" parent="1"><mxGeometry x="3750" y="300" width="980" height="420" as="geometry"/></mxCell>
<mxCell id="p03-title" value="确认提交任务?" style="text;html=0;strokeColor=none;fillColor=none;fontColor=#10284B;fontSize=26;fontStyle=1;fontFamily=Microsoft YaHei;align=left;" vertex="1" parent="1"><mxGeometry x="3820" y="355" width="600" height="45" as="geometry"/></mxCell>
<mxCell id="p03-desc" value="提交后将进入处理流程。你仍可在任务详情中查看进度。" style="text;html=0;strokeColor=none;fillColor=none;fontColor=#40516A;fontSize=16;fontFamily=Microsoft YaHei;align=left;whiteSpace=wrap;" vertex="1" parent="1"><mxGeometry x="3820" y="425" width="820" height="70" as="geometry"/></mxCell>
<mxCell id="p03-back" value="返回修改" style="rounded=1;whiteSpace=wrap;html=0;fillColor=#FFFFFF;strokeColor=#AFC1DA;fontColor=#27405F;fontSize=16;fontStyle=1;fontFamily=Microsoft YaHei;arcSize=10;" vertex="1" parent="1"><mxGeometry x="4080" y="590" width="220" height="52" as="geometry"/></mxCell>
<mxCell id="p03-confirm" value="确认提交" style="rounded=1;whiteSpace=wrap;html=0;fillColor=#18864B;strokeColor=#18864B;fontColor=#FFFFFF;fontSize=16;fontStyle=1;fontFamily=Microsoft YaHei;arcSize=10;" vertex="1" parent="1"><mxGeometry x="4340" y="590" width="260" height="52" as="geometry"/></mxCell>
<mxCell id="p03-meta" value="角色:R-USER|触发:P-EDIT / A-SUBMIT|取消:返回编辑|确认:S-SUBMITTED" style="text;html=0;strokeColor=none;fillColor=none;fontColor=#718198;fontSize=13;fontFamily=Microsoft YaHei;align=left;" vertex="1" parent="1"><mxGeometry x="3750" y="915" width="1240" height="30" as="geometry"/></mxCell>
<mxCell id="label-p04" value="04 · FLOW-OVERVIEW · 端到端用户动线" style="text;html=0;strokeColor=none;fillColor=none;fontColor=#10284B;fontSize=24;fontStyle=1;fontFamily=Microsoft YaHei;" vertex="1" parent="1"><mxGeometry x="0" y="1175" width="1200" height="40" as="geometry"/></mxCell>
<mxCell id="p04-bg" value="" style="rounded=0;whiteSpace=wrap;html=0;fillColor=#FFFFFF;strokeColor=#D9E2EF;" vertex="1" parent="1"><mxGeometry x="0" y="1230" width="1600" height="900" as="geometry"/></mxCell>
<mxCell id="p04-lane-user" value="R-USER 用户" style="swimlane;html=0;rounded=0;startSize=70;horizontal=0;fillColor=#EEF4FF;strokeColor=#9CBDF0;fontColor=#1768E8;fontSize=16;fontStyle=1;fontFamily=Microsoft YaHei;" vertex="1" parent="1"><mxGeometry x="70" y="1310" width="1460" height="330" as="geometry"/></mxCell>
<mxCell id="p04-lane-system" value="R-SYSTEM 系统" style="swimlane;html=0;rounded=0;startSize=70;horizontal=0;fillColor=#F4F7FB;strokeColor=#D9E2EF;fontColor=#40516A;fontSize=16;fontStyle=1;fontFamily=Microsoft YaHei;" vertex="1" parent="1"><mxGeometry x="70" y="1670" width="1460" height="330" as="geometry"/></mxCell>
<mxCell id="flow-start" value="开始" style="ellipse;whiteSpace=wrap;html=0;fillColor=#EEF4FF;strokeColor=#1768E8;fontColor=#1768E8;fontSize=14;fontStyle=1;fontFamily=Microsoft YaHei;" vertex="1" parent="1"><mxGeometry x="180" y="1430" width="120" height="70" as="geometry"/></mxCell>
<mxCell id="flow-list" value="P-LIST&#xa;任务列表" style="rounded=1;whiteSpace=wrap;html=0;fillColor=#FFFFFF;strokeColor=#1768E8;fontColor=#10284B;fontSize=14;fontStyle=1;fontFamily=Microsoft YaHei;arcSize=10;" vertex="1" parent="1"><mxGeometry x="410" y="1415" width="220" height="100" as="geometry"/></mxCell>
<mxCell id="flow-edit" value="P-EDIT&#xa;创建任务" style="rounded=1;whiteSpace=wrap;html=0;fillColor=#FFFFFF;strokeColor=#1768E8;fontColor=#10284B;fontSize=14;fontStyle=1;fontFamily=Microsoft YaHei;arcSize=10;" vertex="1" parent="1"><mxGeometry x="760" y="1415" width="220" height="100" as="geometry"/></mxCell>
<mxCell id="flow-confirm" value="P-CONFIRM&#xa;提交确认" style="rhombus;whiteSpace=wrap;html=0;fillColor=#FFF8E8;strokeColor=#D97706;fontColor=#604A16;fontSize=14;fontStyle=1;fontFamily=Microsoft YaHei;" vertex="1" parent="1"><mxGeometry x="1110" y="1395" width="220" height="140" as="geometry"/></mxCell>
<mxCell id="flow-process" value="S-SUBMITTED&#xa;创建处理记录" style="rounded=1;whiteSpace=wrap;html=0;fillColor=#EAF8F0;strokeColor=#89D3A8;fontColor=#18864B;fontSize=14;fontStyle=1;fontFamily=Microsoft YaHei;arcSize=10;" vertex="1" parent="1"><mxGeometry x="760" y="1785" width="260" height="100" as="geometry"/></mxCell>
<mxCell id="flow-end" value="完成" style="ellipse;whiteSpace=wrap;html=0;fillColor=#EAF8F0;strokeColor=#18864B;fontColor=#18864B;fontSize=14;fontStyle=1;fontFamily=Microsoft YaHei;" vertex="1" parent="1"><mxGeometry x="1190" y="1800" width="120" height="70" as="geometry"/></mxCell>
<mxCell id="flow-e1" value="进入" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=0;endArrow=classic;endFill=1;strokeWidth=2;strokeColor=#1768E8;fontColor=#40516A;" edge="1" parent="1" source="flow-start" target="flow-list"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="flow-e2" value="创建" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=0;endArrow=classic;endFill=1;strokeWidth=2;strokeColor=#1768E8;fontColor=#40516A;" edge="1" parent="1" source="flow-list" target="flow-edit"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="flow-e3" value="提交" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=0;endArrow=classic;endFill=1;strokeWidth=2;strokeColor=#D97706;fontColor=#40516A;" edge="1" parent="1" source="flow-edit" target="flow-confirm"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="flow-e4" value="确认" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=0;endArrow=classic;endFill=1;strokeWidth=2;strokeColor=#18864B;fontColor=#40516A;" edge="1" parent="1" source="flow-confirm" target="flow-process"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="flow-e5" value="处理成功" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=0;endArrow=classic;endFill=1;strokeWidth=2;strokeColor=#18864B;fontColor=#40516A;" edge="1" parent="1" source="flow-process" target="flow-end"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="label-p05" value="05 · SPEC-INDEX · 页面、状态与操作索引" style="text;html=0;strokeColor=none;fillColor=none;fontColor=#10284B;fontSize=24;fontStyle=1;fontFamily=Microsoft YaHei;" vertex="1" parent="1"><mxGeometry x="1720" y="1175" width="1200" height="40" as="geometry"/></mxCell>
<mxCell id="p05-bg" value="" style="rounded=0;whiteSpace=wrap;html=0;fillColor=#FFFFFF;strokeColor=#D9E2EF;" vertex="1" parent="1"><mxGeometry x="1720" y="1230" width="1600" height="900" as="geometry"/></mxCell>
<mxCell id="p05-title" value="页面与动作索引" style="text;html=0;strokeColor=none;fillColor=none;fontColor=#10284B;fontSize=30;fontStyle=1;fontFamily=Microsoft YaHei;align=left;" vertex="1" parent="1"><mxGeometry x="1810" y="1310" width="700" height="50" as="geometry"/></mxCell>
<mxCell id="p05-head" value="稳定 ID 页面/状态 角色 主操作 下一步" style="rounded=0;whiteSpace=wrap;html=0;fillColor=#EAF0F8;strokeColor=#D9E2EF;fontColor=#263750;fontSize=14;fontStyle=1;fontFamily=Microsoft YaHei;align=left;spacingLeft=18;" vertex="1" parent="1"><mxGeometry x="1810" y="1400" width="1420" height="56" as="geometry"/></mxCell>
<mxCell id="p05-row1" value="P-LIST 任务列表 R-USER A-CREATE P-EDIT" style="rounded=0;whiteSpace=wrap;html=0;fillColor=#FFFFFF;strokeColor=#D9E2EF;fontColor=#40516A;fontSize=14;fontFamily=Microsoft YaHei;align=left;spacingLeft=18;" vertex="1" parent="1"><mxGeometry x="1810" y="1456" width="1420" height="64" as="geometry"/></mxCell>
<mxCell id="p05-row2" value="P-EDIT 创建任务 R-USER A-SUBMIT P-CONFIRM" style="rounded=0;whiteSpace=wrap;html=0;fillColor=#F7F9FC;strokeColor=#D9E2EF;fontColor=#40516A;fontSize=14;fontFamily=Microsoft YaHei;align=left;spacingLeft=18;" vertex="1" parent="1"><mxGeometry x="1810" y="1520" width="1420" height="64" as="geometry"/></mxCell>
<mxCell id="p05-row3" value="P-CONFIRM 提交确认 R-USER A-CONFIRM S-SUBMITTED" style="rounded=0;whiteSpace=wrap;html=0;fillColor=#FFFFFF;strokeColor=#D9E2EF;fontColor=#40516A;fontSize=14;fontFamily=Microsoft YaHei;align=left;spacingLeft=18;" vertex="1" parent="1"><mxGeometry x="1810" y="1584" width="1420" height="64" as="geometry"/></mxCell>
<mxCell id="p05-note" value="规则:所有原型、动线、状态机与权限矩阵复用稳定 ID;显示序号只代表当前阅读顺序。" style="rounded=1;whiteSpace=wrap;html=0;fillColor=#FFF8E8;strokeColor=#E6BE65;fontColor=#604A16;fontSize=14;fontFamily=Microsoft YaHei;align=left;spacingLeft=18;arcSize=8;" vertex="1" parent="1"><mxGeometry x="1810" y="1730" width="1420" height="80" as="geometry"/></mxCell>
<mxCell id="cross-e1" value="创建任务" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=0;endArrow=classic;endFill=1;strokeWidth=2;strokeColor=#1768E8;fontColor=#40516A;" edge="1" parent="1" source="p01-new" target="p02-title"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="cross-e2" value="提交确认" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=0;endArrow=classic;endFill=1;strokeWidth=2;strokeColor=#18864B;fontColor=#40516A;" edge="1" parent="1" source="p02-submit" target="p03-title"><mxGeometry relative="1" as="geometry"/></mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
@@ -0,0 +1,68 @@
{
"title": "产品名称",
"columns": 3,
"transitions": [
{
"from": "P-LIST",
"to": "P-EDIT",
"label": "创建任务",
"kind": "primary"
},
{
"from": "P-EDIT",
"to": "P-LIST",
"label": "提交成功",
"kind": "success"
}
],
"pages": [
{
"name": "00-总览",
"kind": "flow",
"frames": [
{
"id": "FLOW-OVERVIEW",
"title": "端到端用户动线",
"role": "ALL",
"state": ""
}
]
},
{
"name": "10-核心页面",
"kind": "screen",
"frames": [
{
"id": "P-LIST",
"title": "任务列表",
"role": "R-USER",
"state": "S-DEFAULT"
},
{
"id": "P-EDIT",
"title": "创建任务",
"role": "R-USER",
"state": "S-EDITING"
}
]
},
{
"name": "40-规格",
"kind": "spec",
"frames": [
{
"id": "SPEC-PAGE-INDEX",
"title": "页面与动作索引",
"role": "ALL",
"state": ""
},
{
"id": "SPEC-PERMISSION",
"title": "角色权限矩阵",
"role": "ALL",
"state": ""
}
]
}
]
}
@@ -0,0 +1,93 @@
# 低保真产品说明板设计模式
## 1. 三层交付结构
一份可落地的产品原型同时包含三层:
1. **页面层**:用户看见什么、能做什么,包括页面、抽屉、弹窗和关键状态。
2. **动线层**:谁在什么条件下执行什么动作,从哪里到哪里,失败后如何恢复。
3. **规则层**:状态、权限、字段值域、联动、统计口径以及操作反馈。
只画页面会遗漏行为,只画流程会缺少界面上下文,只画规则又难以评审。三层必须复用同一套稳定 ID。
## 2. 画板与信息层级
默认桌面端画板为 1600×900;移动端改为统一设备宽高,但同一文件不要混用多个无说明尺寸。
每个画板从上到下包含:
- 外部编号标题:`03 · P-REQUEST-EDIT · 编辑申请 · 草稿`
- 应用外壳:产品名、当前角色、导航和环境信息;
- 页面标题与一句任务说明;
- 主要内容区;
- 主次操作;
- 底部元信息:角色、入口、关键规则和下一步稳定 ID。
画板之间保留足够留白,让跨画板连线走在间隙中。弹窗状态可单独复制一张触发页面并叠加 20%~30% 遮罩,避免脱离上下文的孤立弹窗。
## 3. 视觉语法
除非用户提供品牌规范,使用以下克制的基线:
| 语义 | 建议色 | 用途 |
| --- | --- | --- |
| 深色外壳 | `#061B3A` | 侧边栏、顶栏、产品框架 |
| 主操作 | `#1768E8` | 主要按钮、普通导航连线 |
| 成功 | `#18864B` | 成功反馈、通过、完成 |
| 警告 | `#D97706` | 可恢复异常、未保存、待处理 |
| 危险 | `#D92D20` | 删除、废弃、不可逆动作 |
| 辅助跳转 | `#5B6ABF` | 预览、详情、跨域辅助流程 |
| 主文字 | `#10284B` | 标题与正文重点 |
| 次文字 | `#718198` | 说明、占位和元信息 |
| 页面背景 | `#F4F7FB` | 内容画布 |
| 边框 | `#D9E2EF` | 卡片、表格、输入框 |
低保真仍应有层级,但不要添加装饰性插画、复杂阴影、渐变或像素级视觉润色。圆角统一为 8~10,正文 12~14,区块标题 18,页面标题 28~30。中文优先使用环境常见无衬线字体并提供回退。
## 4. 组件表达
- **按钮**:独立图元,文字使用动词;危险动作不用模糊文案。
- **输入框**:同时表达字段名、当前值/占位、必填和错误反馈,不能只画空矩形。
- **表格**:表头、至少一条典型数据、空态与行操作;大表无需填满整页。
- **状态标签**:颜色与文案双重编码,不能只靠颜色。
- **反馈**:就近错误用于字段校验,全局消息用于操作结果,阻断性问题使用弹窗或整页状态。
- **附件/媒体**:画出上传中、失败、预览、删除和权限差异中真正相关的状态。
## 5. 动线表达
总览使用角色或阶段泳道。页面节点使用稳定 ID;操作节点以动词开头;判断节点写可验证条件;开始和结束节点清楚区分。
连接线要求:
- 使用正交线并设置 `source``target`
- 线上的标签描述动作或结果,不重复节点名称;
- 同一语义使用同一颜色,不给每条线随机配色;
- 交叉多时拆分页面或增加局部子流程,不依赖长距离回折线;
- 页面内按钮到目标页的连线与总览中的转移必须一致。
## 6. 页面与状态覆盖
按业务风险选择,不机械地给每页复制全部状态:
- 核心成功路径;
- 表单校验与保存失败;
- 加载、空数据和加载失败;
- 无权限、账号异常或配置缺失;
- 未保存离开;
- 删除、撤回、废弃等不可逆确认;
- 审批或库存等并发冲突;
- 只读、已结束和审计视角。
## 7. 大型原型的默认优化
大型说明板容易出现两个问题:单页过大导致导航困难;显示序号变化导致页内引用失真。默认采用多页面结构与稳定 ID,显示序号只用于阅读顺序。
建议页签:
- `00-总览`:角色、核心任务、端到端动线、状态机;
- `10-核心页面`:主流程页面;
- `20-角色或子域`:按角色/业务域拆分;
- `30-异常状态`:跨页面异常与恢复;
- `40-规格`:索引、权限、字段与口径。
如果用户要求单页巨幅板,仍先在左上放导航索引,并限制每行画板数量。
@@ -0,0 +1,71 @@
# 产品模型与覆盖检查
复杂原型先用以下表格建立轻量 SSOT。可以放在工作笔记中,也可以作为 Draw.io 的规格页;不要让原型、动线和规则各自维护一套名称。
## 页面清单
| 稳定 ID | 页面/状态 | 角色 | 入口 | 用户目标 | 主操作 | 成功去向 | 关键异常 |
| --- | --- | --- | --- | --- | --- | --- | --- |
| P-LOGIN | 登录 | 访客 | 未登录 | 进入系统 | 登录 | P-HOME | S-AUTH-FAIL |
检查:每个页面都有入口;非终态至少有一个去向;弹窗写明触发页面。
## 转移清单
| 动作 ID | 来源 | 操作者 | 前置条件 | 动作 | 成功目标 | 失败目标/反馈 |
| --- | --- | --- | --- | --- | --- | --- |
| A-SUBMIT | P-EDIT | 申请人 | 校验通过 | 提交 | P-DETAIL / S-PENDING | P-EDIT / 保存失败消息 |
检查:每个主按钮有一条转移;每条连线对应真实操作;失败后能重试、返回或安全结束。
## 状态机
| 当前状态 | 可执行动作 | 角色 | 下一状态 | 字段可编辑性 | 记录/通知 |
| --- | --- | --- | --- | --- | --- |
| S-DRAFT | A-SUBMIT | 申请人 | S-PENDING | 全部可编辑 | 创建审批任务 |
检查:状态名称唯一;每次变化有触发者;终态不会意外回到可编辑状态;并发变化有刷新或冲突处理。
## 角色权限矩阵
| 页面/动作 | 申请人 | 审批人 | 管理员 | 无权限表现 |
| --- | --- | --- | --- | --- |
| P-DETAIL / 查看 | 本人数据 | 分配给自己的数据 | 全部 | 403 或无入口 |
| A-DELETE / 删除 | 仅草稿 | 无 | 按规则 | 隐藏按钮或阻断 |
检查:区分「看不见」「只读」「可操作」;数据范围和按钮权限同时表达;管理员操作注明审计要求。
## 字段规则
| 字段 ID | 页面 | 类型 | 必填 | 值域/格式 | 显示条件 | 联动 | 错误反馈 |
| --- | --- | --- | --- | --- | --- | --- | --- |
| F-AMOUNT | P-EDIT | 金额 | 是 | > 02 位小数 | 始终 | 影响合计 | 请输入有效金额 |
检查:原型中的标签与字段表一致;依赖字段有清空/保留规则;日期、金额、附件和枚举给出边界。
## 异常与恢复
| 异常 ID | 触发点 | 用户看到什么 | 数据是否保留 | 恢复动作 | 记录 |
| --- | --- | --- | --- | --- | --- |
| E-SAVE-TIMEOUT | A-SAVE | 保存超时消息 | 保留本地输入 | 重试 | 请求 ID |
检查:错误文案可行动;不可恢复问题给出返回路径;危险操作有二次确认;成功反馈不与实际状态矛盾。
## 端到端覆盖
每个核心任务至少验证一条成功路径和一条高风险分支:
```text
角色 → 入口 → 页面 → 动作 → 状态变化 → 下一角色/系统处理 → 终态 → 可见反馈
```
发布前抽查:
- 新用户第一次进入;
- 正常创建并完成;
- 中途保存和再次进入;
- 校验失败或服务失败;
- 无权限或配置缺失;
- 不可逆动作;
- 多角色交接;
- 已结束对象的只读/审计视角。
+259
View File
@@ -0,0 +1,259 @@
#!/usr/bin/env python3
"""Generate an editable multi-page Draw.io prototype skeleton from JSON."""
from __future__ import annotations
import argparse
import json
import re
import sys
import xml.etree.ElementTree as ET
from pathlib import Path
FRAME_W = 1600
FRAME_H = 900
GAP_X = 120
GAP_Y = 260
STYLE = {
"label": "text;html=0;strokeColor=none;fillColor=none;fontColor=#10284B;fontSize=24;fontStyle=1;fontFamily=Microsoft YaHei;",
"bg": "rounded=0;whiteSpace=wrap;html=0;fillColor=#F4F7FB;strokeColor=#D9E2EF;",
"nav": "rounded=0;whiteSpace=wrap;html=0;fillColor=#061B3A;strokeColor=#061B3A;fontColor=#C8D6EA;fontSize=16;fontFamily=Microsoft YaHei;align=left;verticalAlign=top;spacingTop=32;spacingLeft=32;",
"title": "text;html=0;strokeColor=none;fillColor=none;fontColor=#10284B;fontSize=30;fontStyle=1;fontFamily=Microsoft YaHei;align=left;",
"card": "rounded=1;whiteSpace=wrap;html=0;fillColor=#FFFFFF;strokeColor=#D9E2EF;fontColor=#718198;fontSize=16;fontFamily=Microsoft YaHei;arcSize=8;",
"primary": "rounded=1;whiteSpace=wrap;html=0;fillColor=#1768E8;strokeColor=#1768E8;fontColor=#FFFFFF;fontSize=16;fontStyle=1;fontFamily=Microsoft YaHei;arcSize=10;",
"secondary": "rounded=1;whiteSpace=wrap;html=0;fillColor=#FFFFFF;strokeColor=#AFC1DA;fontColor=#27405F;fontSize=16;fontStyle=1;fontFamily=Microsoft YaHei;arcSize=10;",
"meta": "text;html=0;strokeColor=none;fillColor=none;fontColor=#718198;fontSize=13;fontFamily=Microsoft YaHei;align=left;",
"table_head": "rounded=0;whiteSpace=wrap;html=0;fillColor=#EAF0F8;strokeColor=#D9E2EF;fontColor=#263750;fontSize=14;fontStyle=1;fontFamily=Microsoft YaHei;align=left;spacingLeft=18;",
"table_row": "rounded=0;whiteSpace=wrap;html=0;fillColor=#FFFFFF;strokeColor=#D9E2EF;fontColor=#40516A;fontSize=14;fontFamily=Microsoft YaHei;align=left;spacingLeft=18;",
}
EDGE_COLORS = {
"primary": "#1768E8",
"success": "#18864B",
"warning": "#D97706",
"danger": "#D92D20",
"secondary": "#5B6ABF",
}
def args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("spec", type=Path, help="JSON scaffold specification")
parser.add_argument("output", type=Path, help="Target .drawio path")
parser.add_argument("--force", action="store_true", help="Overwrite an existing output file")
return parser.parse_args()
def safe_id(value: str) -> str:
value = re.sub(r"[^A-Za-z0-9_-]+", "-", value).strip("-").lower()
return value or "frame"
def geometry(parent: ET.Element, x: int, y: int, width: int, height: int) -> None:
ET.SubElement(parent, "mxGeometry", {
"x": str(x), "y": str(y), "width": str(width), "height": str(height), "as": "geometry"
})
def cell(root: ET.Element, cell_id: str, value: str, style: str, x: int, y: int,
width: int, height: int) -> ET.Element:
node = ET.SubElement(root, "mxCell", {
"id": cell_id, "value": value, "style": style, "vertex": "1", "parent": "1"
})
geometry(node, x, y, width, height)
return node
def edge(root: ET.Element, edge_id: str, value: str, source: str, target: str, kind: str) -> None:
color = EDGE_COLORS.get(kind, EDGE_COLORS["primary"])
node = ET.SubElement(root, "mxCell", {
"id": edge_id,
"value": value,
"style": (
"edgeStyle=orthogonalEdgeStyle;rounded=1;html=0;endArrow=classic;endFill=1;"
f"strokeWidth=2;strokeColor={color};fontColor=#40516A;"
),
"edge": "1",
"parent": "1",
"source": source,
"target": target,
})
ET.SubElement(node, "mxGeometry", {"relative": "1", "as": "geometry"})
def screen_frame(root: ET.Element, prefix: str, number: int, frame: dict, x: int, y: int,
product: str) -> None:
stable_id = frame["id"]
title = frame["title"]
state = frame.get("state", "")
role = frame.get("role", "")
suffix = f" · {state}" if state else ""
cell(root, f"{prefix}-label", f"{number:02d} · {stable_id} · {title}{suffix}", STYLE["label"], x, y, 1300, 40)
top = y + 55
cell(root, f"{prefix}-bg", "", STYLE["bg"], x, top, FRAME_W, FRAME_H)
cell(root, f"{prefix}-nav", f"{product}\n工作台\n核心任务\n数据概览\n系统设置", STYLE["nav"], x, top, 260, FRAME_H)
cell(root, f"{prefix}-title", title, STYLE["title"], x + 310, top + 50, 700, 50)
cell(root, f"{prefix}-card", "在此绘制页面内容、字段、表格和反馈状态", STYLE["card"], x + 310, top + 150, 1240, 560)
cell(root, f"{prefix}-secondary", "次要操作", STYLE["secondary"], x + 1060, top + 745, 200, 50)
cell(root, f"{prefix}-primary", "主要操作", STYLE["primary"], x + 1310, top + 745, 240, 50)
cell(root, f"{prefix}-meta", f"角色:{role or '待定义'}|状态:{state or '待定义'}|稳定 ID{stable_id}|下一步:待定义", STYLE["meta"], x + 310, top + 845, 1240, 30)
def flow_frame(root: ET.Element, prefix: str, number: int, frame: dict, x: int, y: int) -> None:
stable_id, title = frame["id"], frame["title"]
cell(root, f"{prefix}-label", f"{number:02d} · {stable_id} · {title}", STYLE["label"], x, y, 1300, 40)
top = y + 55
cell(root, f"{prefix}-bg", "", STYLE["bg"], x, top, FRAME_W, FRAME_H)
cell(root, f"{prefix}-title", title, STYLE["title"], x + 80, top + 55, 900, 50)
cell(root, f"{prefix}-card", "按角色或阶段添加泳道;使用带 source/target 的正交连接线表达动作与结果", STYLE["card"], x + 80, top + 150, 1440, 620)
cell(root, f"{prefix}-meta", f"稳定 ID{stable_id}|覆盖:入口、判断、跨角色交接、成功与失败终态", STYLE["meta"], x + 80, top + 835, 1440, 30)
def flow_graph(root: ET.Element, prefix: str, transitions: list[dict], frames: dict[str, dict],
x: int, y: int) -> None:
ordered_ids: list[str] = []
for transition in transitions:
for stable_id in (transition["from"], transition["to"]):
if stable_id not in ordered_ids:
ordered_ids.append(stable_id)
node_ids: dict[str, str] = {}
for index, stable_id in enumerate(ordered_ids):
col, row = index % 5, index // 5
node_id = f"{prefix}-node-{safe_id(stable_id)}"
node_ids[stable_id] = node_id
frame = frames[stable_id]
cell(
root,
node_id,
f"{stable_id}\n{frame['title']}",
"rounded=1;whiteSpace=wrap;html=0;fillColor=#FFFFFF;strokeColor=#1768E8;"
"fontColor=#10284B;fontSize=14;fontStyle=1;fontFamily=Microsoft YaHei;arcSize=10;",
x + 110 + col * 280,
y + 245 + row * 155,
220,
82,
)
for index, transition in enumerate(transitions):
edge(
root,
f"{prefix}-edge-{index}",
transition["label"],
node_ids[transition["from"]],
node_ids[transition["to"]],
transition.get("kind", "primary"),
)
def spec_frame(root: ET.Element, prefix: str, number: int, frame: dict, x: int, y: int) -> None:
stable_id, title = frame["id"], frame["title"]
cell(root, f"{prefix}-label", f"{number:02d} · {stable_id} · {title}", STYLE["label"], x, y, 1300, 40)
top = y + 55
cell(root, f"{prefix}-bg", "", STYLE["bg"], x, top, FRAME_W, FRAME_H)
cell(root, f"{prefix}-title", title, STYLE["title"], x + 80, top + 55, 900, 50)
cell(root, f"{prefix}-head", "稳定 ID 名称/条件 角色 动作/规则 结果", STYLE["table_head"], x + 80, top + 150, 1440, 58)
for row in range(4):
cell(root, f"{prefix}-row-{row}", "待填写 待填写 待填写 待填写 待填写", STYLE["table_row"], x + 80, top + 208 + row * 66, 1440, 66)
cell(root, f"{prefix}-meta", f"规格 ID{stable_id}|所有引用使用页面/状态/动作稳定 ID", STYLE["meta"], x + 80, top + 835, 1440, 30)
def load_spec(path: Path) -> dict:
try:
data = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
raise ValueError(f"Cannot read spec: {exc}") from exc
if not isinstance(data.get("pages"), list) or not data["pages"]:
raise ValueError("spec.pages must be a non-empty list")
columns = data.get("columns", 3)
if not isinstance(columns, int) or not 1 <= columns <= 5:
raise ValueError("columns must be an integer from 1 to 5")
seen: set[str] = set()
for page in data["pages"]:
if page.get("kind") not in {"screen", "flow", "spec"}:
raise ValueError(f"Unsupported page kind: {page.get('kind')!r}")
if not page.get("name") or not isinstance(page.get("frames"), list) or not page["frames"]:
raise ValueError("each page needs name and a non-empty frames list")
for frame in page["frames"]:
if not frame.get("id") or not frame.get("title"):
raise ValueError("each frame needs id and title")
if frame["id"] in seen:
raise ValueError(f"duplicate stable frame id: {frame['id']}")
seen.add(frame["id"])
transitions = data.get("transitions", [])
if not isinstance(transitions, list):
raise ValueError("transitions must be a list")
for transition in transitions:
if not all(transition.get(key) for key in ("from", "to", "label")):
raise ValueError("each transition needs from, to and label")
if transition["from"] not in seen or transition["to"] not in seen:
raise ValueError(f"transition references unknown frame: {transition}")
if transition.get("kind", "primary") not in EDGE_COLORS:
raise ValueError(f"unsupported transition kind: {transition.get('kind')!r}")
return data
def build(spec: dict) -> ET.ElementTree:
mxfile = ET.Element("mxfile", {
"host": "app.diagrams.net", "agent": "draw-prototype-flow", "version": "24.7.17",
"type": "device", "compressed": "false", "pages": str(len(spec["pages"]))
})
product = spec.get("title", "产品名称")
columns = spec.get("columns", 3)
frames_by_id = {
frame["id"]: frame
for page in spec["pages"]
for frame in page["frames"]
}
number = 0
for page_index, page in enumerate(spec["pages"]):
diagram = ET.SubElement(mxfile, "diagram", {
"id": f"page-{page_index}-{safe_id(page['name'])}", "name": page["name"]
})
model = ET.SubElement(diagram, "mxGraphModel", {
"dx": "1200", "dy": "800", "grid": "1", "gridSize": "10", "guides": "1",
"tooltips": "1", "connect": "1", "arrows": "1", "fold": "1", "page": "0",
"pageScale": "1", "math": "0", "shadow": "0"
})
root = ET.SubElement(model, "root")
ET.SubElement(root, "mxCell", {"id": "0"})
ET.SubElement(root, "mxCell", {"id": "1", "parent": "0"})
for frame_index, frame in enumerate(page["frames"]):
number += 1
col, row = frame_index % columns, frame_index // columns
x, y = col * (FRAME_W + GAP_X), row * (FRAME_H + GAP_Y) + 15
prefix = f"p{page_index}-f{frame_index}-{safe_id(frame['id'])}"
if page["kind"] == "screen":
screen_frame(root, prefix, number, frame, x, y, product)
elif page["kind"] == "flow":
flow_frame(root, prefix, number, frame, x, y)
else:
spec_frame(root, prefix, number, frame, x, y)
if page["kind"] == "flow" and spec.get("transitions"):
flow_graph(root, f"p{page_index}-auto-flow", spec["transitions"], frames_by_id, 0, 15)
ET.indent(mxfile, space=" ")
return ET.ElementTree(mxfile)
def main() -> int:
options = args()
if options.output.exists() and not options.force:
print(f"Refusing to overwrite existing file: {options.output}", file=sys.stderr)
return 2
try:
spec = load_spec(options.spec)
tree = build(spec)
options.output.parent.mkdir(parents=True, exist_ok=True)
tree.write(options.output, encoding="utf-8", xml_declaration=True)
except (ValueError, OSError) as exc:
print(f"ERROR: {exc}", file=sys.stderr)
return 1
frame_count = sum(len(page["frames"]) for page in spec["pages"])
print(f"Created {options.output}: {len(spec['pages'])} page(s), {frame_count} frame(s)")
return 0
if __name__ == "__main__":
sys.exit(main())
+236
View File
@@ -0,0 +1,236 @@
#!/usr/bin/env python3
"""Validate the structure of an uncompressed Draw.io product prototype."""
from __future__ import annotations
import argparse
import base64
import html
import json
import re
import sys
import urllib.parse
import xml.etree.ElementTree as ET
import zlib
from dataclasses import asdict, dataclass
from pathlib import Path
FRAME_RE = re.compile(r"^(?:\d{2,3}|P-[A-Z0-9-]+)\s*(?:[·.\-—-]|\s)")
NUMERIC_REF_RE = re.compile(r"(?:下一步|关联|进入|见|去向)[^|\n]{0,40}(?<!\d)\d{2,3}(?!\d)")
FLOW_PAGE_RE = re.compile(r"(?:总览|流程|动线|flow)", re.I)
@dataclass
class Issue:
level: str
code: str
message: str
page: str | None = None
cell: str | None = None
def label_text(value: str | None) -> str:
value = html.unescape(value or "")
value = re.sub(r"<br\s*/?>", " ", value, flags=re.I)
value = re.sub(r"<[^>]+>", "", value)
return " ".join(value.replace("\xa0", " ").split())
def is_frame_title(cell: ET.Element) -> bool:
"""Recognize large external frame headings, not numbered table content."""
if not FRAME_RE.match(label_text(cell.get("value"))):
return False
geometry = cell.find("mxGeometry")
if geometry is None:
return False
try:
width = float(geometry.get("width", "0"))
height = float(geometry.get("height", "0"))
except ValueError:
return False
style = cell.get("style", "")
size_match = re.search(r"(?:^|;)fontSize=(\d+(?:\.\d+)?)(?:;|$)", style)
font_size = float(size_match.group(1)) if size_match else 0
return width >= 500 and height <= 80 and font_size >= 20
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("file", type=Path, help="Uncompressed .drawio file")
parser.add_argument("--min-frames", type=int, default=0)
parser.add_argument("--min-edges", type=int, default=0)
parser.add_argument("--strict", action="store_true", help="Treat warnings as failures")
parser.add_argument("--json", action="store_true", dest="as_json")
return parser.parse_args()
def graph_model(diagram: ET.Element) -> tuple[ET.Element | None, bool]:
"""Return a graph model and whether the page used Draw.io compression."""
model = diagram.find("mxGraphModel")
if model is not None:
return model, False
payload = (diagram.text or "").strip()
if not payload:
return None, False
try:
decoded = base64.b64decode(payload)
xml_text = urllib.parse.unquote(zlib.decompress(decoded, -15).decode("utf-8"))
model = ET.fromstring(xml_text)
except (ValueError, UnicodeDecodeError, zlib.error, ET.ParseError):
return None, True
return (model if model.tag == "mxGraphModel" else None), True
def validate(path: Path, min_frames: int, min_edges: int) -> tuple[dict, list[Issue]]:
issues: list[Issue] = []
try:
root = ET.parse(path).getroot()
except (OSError, ET.ParseError) as exc:
return {"file": str(path), "pages": []}, [Issue("error", "xml", str(exc))]
if root.tag != "mxfile":
issues.append(Issue("error", "root", f"Expected mxfile root, found {root.tag!r}"))
diagrams = root.findall("diagram")
if not diagrams:
issues.append(Issue("error", "pages", "No diagram pages found"))
names = [diagram.get("name", "") for diagram in diagrams]
for name in sorted(set(names)):
if name and names.count(name) > 1:
issues.append(Issue("warning", "duplicate-page-name", f"Duplicate page name: {name}"))
page_reports = []
total_frames = 0
total_edges = 0
for index, diagram in enumerate(diagrams, start=1):
page_name = diagram.get("name") or f"page-{index}"
model, compressed = graph_model(diagram)
if model is None:
issues.append(Issue(
"error",
"compressed-page",
"Page has no readable mxGraphModel",
page_name,
))
page_reports.append({"name": page_name, "vertices": 0, "edges": 0, "frames": 0})
continue
if compressed:
issues.append(Issue(
"warning",
"compressed-page",
"Compressed page is readable, but agent-generated output should use compressed=false",
page_name,
))
graph_root = model.find("root")
if graph_root is None:
issues.append(Issue("error", "graph-root", "Missing graph root", page_name))
continue
cells = graph_root.findall("mxCell")
by_id: dict[str, ET.Element] = {}
for cell in cells:
cell_id = cell.get("id")
if not cell_id:
issues.append(Issue("error", "missing-id", "mxCell has no id", page_name))
continue
if cell_id in by_id:
issues.append(Issue("error", "duplicate-id", f"Duplicate cell id: {cell_id}", page_name, cell_id))
by_id[cell_id] = cell
vertices = [cell for cell in cells if cell.get("vertex") == "1"]
edges = [cell for cell in cells if cell.get("edge") == "1"]
frame_cells = [cell for cell in vertices if is_frame_title(cell)]
for cell in cells:
cell_id = cell.get("id")
parent = cell.get("parent")
if parent and parent not in by_id:
issues.append(Issue("error", "dangling-parent", f"Unknown parent {parent}", page_name, cell_id))
if (cell.get("vertex") == "1" or cell.get("edge") == "1") and cell.find("mxGeometry") is None:
issues.append(Issue("error", "geometry", "Vertex/edge has no mxGeometry", page_name, cell_id))
if cell.get("vertex") == "1" and NUMERIC_REF_RE.search(label_text(cell.get("value"))):
issues.append(Issue(
"warning",
"numeric-cross-reference",
"Cross-reference appears to use a display number; use a stable page/state ID",
page_name,
cell_id,
))
for edge in edges:
edge_id = edge.get("id")
source = edge.get("source")
target = edge.get("target")
if not source or not target:
issues.append(Issue("error", "unbound-edge", "Edge needs both source and target", page_name, edge_id))
continue
if source not in by_id:
issues.append(Issue("error", "dangling-source", f"Unknown source {source}", page_name, edge_id))
if target not in by_id:
issues.append(Issue("error", "dangling-target", f"Unknown target {target}", page_name, edge_id))
if not label_text(edge.get("value")):
issues.append(Issue("warning", "unlabelled-edge", "User-flow edge has no action/result label", page_name, edge_id))
if len(frame_cells) > 15:
issues.append(Issue(
"warning",
"crowded-page",
f"{len(frame_cells)} numbered frames on one page; consider splitting into multiple pages",
page_name,
))
if FLOW_PAGE_RE.search(page_name) and not edges:
issues.append(Issue("warning", "no-flow", "Flow/overview page has no linked user-flow edges", page_name))
total_frames += len(frame_cells)
total_edges += len(edges)
page_reports.append({
"name": page_name,
"cells": len(cells),
"vertices": len(vertices),
"edges": len(edges),
"frames": len(frame_cells),
})
if total_frames < min_frames:
issues.append(Issue("error", "min-frames", f"Found {total_frames} frames; expected at least {min_frames}"))
if total_edges < min_edges:
issues.append(Issue("error", "min-edges", f"Found {total_edges} edges; expected at least {min_edges}"))
return {
"file": str(path),
"page_count": len(diagrams),
"frame_count": total_frames,
"edge_count": total_edges,
"pages": page_reports,
}, issues
def main() -> int:
args = parse_args()
report, issues = validate(args.file, args.min_frames, args.min_edges)
errors = sum(issue.level == "error" for issue in issues)
warnings = sum(issue.level == "warning" for issue in issues)
if args.as_json:
print(json.dumps({**report, "issues": [asdict(issue) for issue in issues]}, ensure_ascii=False, indent=2))
else:
print(f"{report['file']}: {report.get('page_count', 0)} page(s), "
f"{report.get('frame_count', 0)} frame(s), {report.get('edge_count', 0)} edge(s)")
for page in report.get("pages", []):
print(f" {page['name']}: {page.get('frames', 0)} frames, "
f"{page.get('vertices', 0)} vertices, {page.get('edges', 0)} edges")
for issue in issues:
where = " / ".join(value for value in (issue.page, issue.cell) if value)
suffix = f" ({where})" if where else ""
print(f" {issue.level.upper()} [{issue.code}] {issue.message}{suffix}")
print(f"Result: {errors} error(s), {warnings} warning(s)")
return 1 if errors or (args.strict and warnings) else 0
if __name__ == "__main__":
sys.exit(main())
+27 -15
View File
@@ -1,13 +1,13 @@
# manage-release # manage-release
`manage-release` 帮助 Agent 管理从开发版本到发布的 Git 流程,包括 worktree、分支、 `manage-release` 帮助 Agent 发布已经准备好的 Git commit,也能在发布前确有必要时管理
版本号、PR/MR、合并和 release tag。它可以只处理其中一个阶段,也可以从准备开发目录 版本文件、worktree、分支、PR/MR 和合并。普通发布优先走“更新说明 + annotated tag”的
一直执行到发布 短路径;只有发布要求修改仓库内容时,才进入准备版本的分支流程
## 使用前准备 ## 使用前准备
- 项目已经是 Git 仓库,并配置了可访问的远端。 - 项目已经是 Git 仓库,并配置了可访问的远端。
- 项目已有版本文件或发布约定;没有时可以让 Agent 先给出版本建议。 - 项目已有版本来源或 tag 发布约定;没有时可以让 Agent 先给出版本建议。
- 创建或合并 PR/MR、创建 Forge Release 时,准备好 GitHub、GitLab、Gitea 或 Forgejo - 创建或合并 PR/MR、创建 Forge Release 时,准备好 GitHub、GitLab、Gitea 或 Forgejo
对应的已认证 CLI。只发布 Git tag 时不要求 Forge CLI,但 Git remote 必须可读写。 对应的已认证 CLI。只发布 Git tag 时不要求 Forge CLI,但 Git remote 必须可读写。
- 请求里写清目标版本、基线分支,以及允许执行到哪一步。没有明确授权的远端写操作 - 请求里写清目标版本、基线分支,以及允许执行到哪一步。没有明确授权的远端写操作
@@ -54,11 +54,21 @@ Agent 只会在仓库要求全部满足后合并,不会使用管理员权限
### 为已合并版本发布 tag ### 为已合并版本发布 tag
```text ```text
使用 manage-release,为已经合并到 main 的 1.6.0 发布 v1.6.0 tag。确认 tag 指向包含该版本号的 merged commit不要创建 Forge Release。 使用 manage-release,为已经合并到 main 的 1.6.0 发布 v1.6.0 tag。根据 v1.5.0 到目标 commit 的实际变化生成更新说明,把说明写入 annotated tag,并验证远端 tag 的 commit 和说明。不要创建 Forge Release。
``` ```
适合依靠 Git tag 触发后续 CI 发布的项目。Agent 会分别验证版本文件、目标 commit 和 这是无需修改仓库文件时的默认短路径,适合依靠 Git tag 触发后续 CI 发布的项目。Agent
远端 tag,不会给未合并分支打正式 tag。 会分别验证目标 commit、更新说明和远端 tag,不会创建 worktree、发布分支或 PR,也不会
给未合并分支打正式 tag。
### 为已合并版本创建 Gitea Release
```text
使用 manage-release,发布已经合并到 main 的 1.6.0:生成更新说明,创建并验证附带说明的 v1.6.0 annotated tag,然后在 Gitea 创建使用同一份说明的 Release。
```
Agent 会先证明 tag 已存在、指向计划 commit 且包含更新说明,再创建 Gitea Release。
Release 创建失败时保留正确的 tag,从 Release 阶段恢复。
### 完成端到端发布 ### 完成端到端发布
@@ -66,8 +76,9 @@ Agent 只会在仓库要求全部满足后合并,不会使用管理员权限
使用 manage-release 完成 1.6.0 发布:从 main 创建 worktree 和分支,完成版本更新和验证,创建 PR,检查通过后合并,推送 v1.6.0 tag,并创建 Forge Release。 使用 manage-release 完成 1.6.0 发布:从 main 创建 worktree 和分支,完成版本更新和验证,创建 PR,检查通过后合并,推送 v1.6.0 tag,并创建 Forge Release。
``` ```
这条请求授权完整流程。遇到 review 未通过、CI 失败、版本冲突或已有同名 tag 时, 只有版本文件或仓库内 CHANGELOG 必须随发布修改时才使用这条长路径。tag 仍必须附带更新
Agent 会停止并说明卡在哪一步,不会绕过检查继续发布。 说明,Forge Release 复用同一份说明。遇到 review 未通过、CI 失败、版本冲突或已有同名
tag 时,Agent 会停止并说明卡在哪一步,不会绕过检查继续发布。
### 发布紧急修复版本 ### 发布紧急修复版本
@@ -89,18 +100,19 @@ Skill 会从 Git 和代码托管平台重新判断状态,再继续缺失的步
## Agent 会做什么 ## Agent 会做什么
Agent 会先发现项目自己的版本、分支和发布规则,再检查本地 Git 与远端状态。它只执行 Agent 会先发现项目自己的版本和发布规则、锁定远端目标 commit,再判断发布是否要求修改
请求中明确授权的阶段,并在合并、tag 或 release 条件不满足时停止。完整流程结束后 仓库内容。不需要修改时直接生成更新说明并发布 annotated tag;需要修改时才准备分支
结果中会分别列出 worktree、分支、版本、PR/MR、merged commit、tag 和 Forge Release 并只在隔离用户工作确有必要时创建 worktree。它只执行请求中明确授权的阶段,并在合并、
状态 tag 或 release 条件不满足时停止
## 如何判断完成 ## 如何判断完成
- 只分析版本时,结果包含建议版本及依据。 - 只分析版本时,结果包含建议版本及依据。
- 创建开发环境时,结果包含 worktree 路径、分支和基线 commit。 - 创建开发环境时,结果包含 worktree 路径、分支和基线 commit。
- 创建或合并 PR/MR 时,结果包含 URL、检查状态和 merged commit。 - 创建或合并 PR/MR 时,结果包含 URL、检查状态和 merged commit。
- 发布 tag 时,远端 tag 解引用后的 commit 与 merged commit 一致 - 发布 tag 时,远端 tag 解引用后的 commit 与目标 commit 一致,而且 tag object 包含经过
- 创建 Forge Release 时,结果包含可访问的 release URL 核对的更新说明
- 创建 Forge Release 时,结果包含可访问的 release URL,正文与 tag 更新说明一致。
- 中途停止时,结果说明停在哪一步、为什么停止,以及下次如何继续。 - 中途停止时,结果说明停在哪一步、为什么停止,以及下次如何继续。
## 不适用的场景 ## 不适用的场景
+76 -37
View File
@@ -1,13 +1,12 @@
--- ---
name: manage-release name: manage-release
description: >- description: >-
管理 Git 项目从开发版本发布的完整生命周期:发现并遵循项目分支与版本策略, 管理 Git 项目从已准备 commit 到版本发布的生命周期:发现并遵循项目版本与发布策略,
创建或复用隔离 worktree 和版本分支,确定并同步版本号,推送并创建、检查或合并 优先为远端已验证 commit 生成更新说明并创建 annotated release tag,按需创建 Forge
PR/MR,在合并后的准确提交上创建并推送 release tag,并按需创建 Forge Release、 Release;只有必须修改版本文件或仓库内发布说明时,才创建或复用版本分支、worktree
恢复中断流程安全清理。用户要求开始发布相关的新版本、为发布开 worktree 或分支、 和 PR/MR。也支持恢复中断流程安全清理。用户要求发布版本、打 release tag、创建
升版本、提交或合并发布 PR/MR、打 release tag、完成发版、处理 hotfix 或继续未完成 平台 Release、准备版本变更、处理 hotfix 或继续未完成发布时使用。只做普通编码、普通
发布时使用。只做普通编码、普通 worktree 或 PR/MR 操作、代码审查、构建或上传 worktree 或 PR/MR 操作、代码审查、构建或上传 DEB/Docker 等产物、管理仓库权限时不使用。
DEB/Docker 等产物、管理仓库权限时不使用。
--- ---
# Manage Release # Manage Release
@@ -40,9 +39,11 @@ description: >-
项目策略只描述仓库期望的做法,不能替用户授予 push、创建或合并 PR/MR、推送 tag、 项目策略只描述仓库期望的做法,不能替用户授予 push、创建或合并 PR/MR、推送 tag、
创建 Forge Release、删除分支等外部写权限。 创建 Forge Release、删除分支等外部写权限。
完整处理并发布 v1.2.3”可以授权从准备到验证远端 tag 的流程,不要在每一步重复询问; “发布 v1.2.3”允许为已经准备好的远端 commit 生成更新说明、创建并推送 annotated tag
它不自动包含 Forge Release 或分支清理。若版本、base、tag、远端或合并方式是推断所得, 再验证远端 tag 的 commit 和说明。“完整处理并发布 v1.2.3”还允许在仓库内容必须修改时
或者执行中目标发生变化,在第一次远端写操作前展示准确目标并获得确认。 走版本分支、PR/MR 和合并流程。两者都不自动包含 Forge Release 或分支清理。若版本、
base、目标 commit、tag、远端或合并方式是推断所得,或者执行中目标发生变化,在第一次
远端写操作前展示准确目标和更新说明并获得确认。
## 工作流 ## 工作流
@@ -70,25 +71,47 @@ description: >-
在修改前确定并展示: 在修改前确定并展示:
- base 分支及其远端 commit。 - base 分支、远端 commit,以及本次 tag 将指向的准确目标 commit。
- 当前版本、规范化后的目标版本、渲染出的分支名和 tag,以及升级依据。 - 当前版本、规范化后的目标版本、tag,以及升级依据;需要准备仓库修改时再渲染分支名
- 分支名与 worktree 路径 - 从上一个稳定 tag 到目标 commit 的更新说明范围、来源和拟发布内容
- 是否需要修改仓库内容;若需要,再列出分支名与 worktree 路径。
- 需要执行的本地验证和远端检查。 - 需要执行的本地验证和远端检查。
- 合并方式、tag、是否创建 Forge Release。 - 合并方式、tag、是否创建 Forge Release。
- 用户已授权的最远阶段和清理范围。 - 用户已授权的最远阶段和清理范围。
项目没有约定时,单一协调版本使用 SemVer`release/v<version>` 分支、 项目没有约定时,单一协调版本使用 SemVer`v<version>` annotated tag。只有发布前
`v<version>` annotated tag。仓库只启用一种合并方式时使用该方式;存在多种方式且没有 必须修改仓库内容时才使用 `release/v<version>` 分支;仓库只启用一种合并方式时使用该
项目规则时默认 squash。默认不删除远端分支,不自动创建 Forge Release。 方式,存在多种方式且没有项目规则时默认 squash。默认不删除远端分支,不自动创建
Forge Release。
把用户输入拆成“规范版本”和“展示名称”:默认 SemVer 的规范版本是无 `v` 前缀的 把用户输入拆成“规范版本”和“展示名称”:默认 SemVer 的规范版本是无 `v` 前缀的
`1.6.0`,版本文件和 `{version}` 都使用该值;分支与 tag 再分别按 pattern 渲染为 `1.6.0`,版本文件和 `{version}` 都使用该值;分支与 tag 再分别按 pattern 渲染为
`release/v1.6.0``v1.6.0`。用户输入 `v1.6.0` 时先规范化,不要产生双前缀。 `release/v1.6.0``v1.6.0`。用户输入 `v1.6.0` 时先规范化,不要产生双前缀。
### 3. 创建或复用隔离 worktree ### 3. 选择直接发布或准备版本
优先使用当前环境提供的 worktree 管理器;项目仓库中的管理脚本只有在可信 base 已声明 先计算发布所需的仓库内容差异,不要为了遵循固定模板而创建分支或 worktree。满足以下
且经过检查时才能使用,没有时使用原生 Git。创建前验证 条件时走直接发布路径,跳过第 4 至 7 节
- 目标 commit 已作为准确远端引用读取,并可从计划的 base 或项目允许的维护分支到达。
- 项目权威版本文件若存在,目标 commit 中已经是目标版本;或项目明确以 tag 作为版本来源。
- 项目不要求把本次 CHANGELOG 或发布说明提交回仓库。
- 项目要求的构建、测试和发布检查已经通过。
直接发布不 checkout 目标 commit,不创建分支、worktree、版本提交或 PR/MR。更新说明是
tag object 和可选 Forge Release 的内容,不因生成说明本身进入仓库修改路径。
只要版本文件、生成元数据或仓库内 CHANGELOG 必须修改,就进入准备版本路径。优先复用
身份准确、未被占用且适合本次发布的现有分支;没有时才创建版本分支。优先复用安全且
干净的现有 worktree;当前目录承载用户工作、目标分支已在别处使用或需要隔离时,才创建
linked worktree。用户只授权了直接发布时,在展示必要差异后停止,不要擅自扩大到修改、
分支或 PR/MR 流程。
### 4. 创建或复用准备版本的分支和 worktree
仅在第 3 节判定需要准备仓库修改时执行本节。优先使用当前环境提供的 worktree 管理器;
项目仓库中的管理脚本只有在可信 base 已声明且经过检查时才能使用,没有时使用原生 Git。
创建前验证:
- 目标分支和目录未被其他 worktree 占用。 - 目标分支和目录未被其他 worktree 占用。
- 目标目录不是 `/`、用户主目录、仓库根目录或已有非空目录。 - 目标目录不是 `/`、用户主目录、仓库根目录或已有非空目录。
@@ -105,7 +128,7 @@ worktree 占用的分支。
相对计划 base 的提交,确认是同一次发布后用 `git worktree add <path> <branch>` 挂载; 相对计划 base 的提交,确认是同一次发布后用 `git worktree add <path> <branch>` 挂载;
身份不匹配或来源不明时停止,不要用 `-b` 覆盖或另建同名分支。 身份不匹配或来源不明时停止,不要用 `-b` 覆盖或另建同名分支。
### 4. 开发并同步版本 ### 5. 修改并同步版本
在目标 worktree 内完成用户要求的修改。根据项目规则更新所有权威版本来源和 在目标 worktree 内完成用户要求的修改。根据项目规则更新所有权威版本来源和
CHANGELOG 或发布说明,保证它们进入同一个 PR/MR。运行项目声明的格式化、测试、构建 CHANGELOG 或发布说明,保证它们进入同一个 PR/MR。运行项目声明的格式化、测试、构建
@@ -119,7 +142,7 @@ CHANGELOG 或发布说明,保证它们进入同一个 PR/MR。运行项目声
提交前重新读取 HEAD 和完整工作区状态,只提交本次范围内的文件。不要在未合并分支上 提交前重新读取 HEAD 和完整工作区状态,只提交本次范围内的文件。不要在未合并分支上
创建正式 release tag。 创建正式 release tag。
### 5. 推送并创建 PR/MR ### 6. 推送并创建 PR/MR
执行远端写操作前再次确认 remote、base、head、版本、活动账号和授权范围,并重新读取 执行远端写操作前再次确认 remote、base、head、版本、活动账号和授权范围,并重新读取
唯一 fetch/push URL。Git 分支与 tag 通过已确认的 remote 读写;创建或操作 PR/MR 时使用 唯一 fetch/push URL。Git 分支与 tag 通过已确认的 remote 读写;创建或操作 PR/MR 时使用
@@ -130,7 +153,7 @@ CHANGELOG 或发布说明,保证它们进入同一个 PR/MR。运行项目声
PR/MR 内容至少说明目标版本、变更摘要、验证命令和结果、发布后续动作。用户只要求开 PR/MR 内容至少说明目标版本、变更摘要、验证命令和结果、发布后续动作。用户只要求开
PR/MR 时,停在这里并返回 URL、head/base、当前检查状态和阻塞项。 PR/MR 时,停在这里并返回 URL、head/base、当前检查状态和阻塞项。
### 6. 检查并合并 ### 7. 检查并合并
从托管平台重新读取 PR/MR 状态。仅在下列条件全部满足时合并: 从托管平台重新读取 PR/MR 状态。仅在下列条件全部满足时合并:
@@ -143,29 +166,40 @@ PR/MR 时,停在这里并返回 URL、head/base、当前检查状态和阻塞
不要使用管理员绕过、直接推送受保护 base,或为通过检查而修改保护规则。合并后获取 不要使用管理员绕过、直接推送受保护 base,或为通过检查而修改保护规则。合并后获取
平台确认的 merged commit,更新远端 base,并验证该 commit 可从远端 base 到达。 平台确认的 merged commit,更新远端 base,并验证该 commit 可从远端 base 到达。
### 7. 创建并发布 tag ### 8. 创建并发布带更新说明的 tag
在 merged commit 上核对目标版本后,再查询一次远端同名 tag。tag 不存在时创建 准备版本路径使用平台确认且可从远端 base 到达的 merged commit;直接发布路径使用计划
annotated tag;项目要求签名时,必须先确认签名工具和密钥可用,创建 signed tag,并在 中锁定且重新验证过的远端目标 commit。两者统一记为 release commit。核对目标版本后,
本地验证签名成功后才允许 push。签名不可用或验证失败时停止,不得降级为 unsigned tag。 再查询一次远端同名 tag。
显式指定 merged commit,并只推送这个 tag。
创建 tag 前,根据上一个稳定 tag 到 release commit 的实际差异生成更新说明;首次发布则
使用项目声明的发布基线,没有声明时核对完整可达历史并明确标记首次发布。说明概括实际
存在的新增、变更、修复、维护或文档变化;存在破坏性变化、迁移步骤或已知限制时必须明确
列出。每项内容都应能追溯到本次提交、PR/MR 或项目 CHANGELOG。空说明、只重复版本号、
模板占位文字或无法由实际变化支持的内容都不能发布。
tag 不存在时,使用完整更新说明创建 annotated tag;项目要求签名时,必须先确认签名
工具和密钥可用,创建 signed annotated tag,并在本地验证签名成功后才允许 push。使用
文件输入完整的多行说明,显式指定 release commit,并只推送这个 tag。禁止创建 lightweight
release tag。签名不可用或验证失败时停止,不得降级为 unsigned tag。
同名远端 tag 已存在且指向其他 commit 时立即停止。不要覆盖、删除或移动已经发布的 同名远端 tag 已存在且指向其他 commit 时立即停止。不要覆盖、删除或移动已经发布的
tag。推送后读取远端 tag,并将 annotated tag 解引用到 commit,确认它与 merged commit tag。同名 tag 即使指向 release commit,只要它是 lightweight tag、缺少更新说明或说明与
完全相同。 计划不一致,也视为身份不匹配并停止。推送后读取远端 tag object,确认它是 annotated
tag,将它解引用到 commit 并核对完整更新说明;commit 和说明必须都与发布计划完全一致。
仅当用户本次请求明确要求时创建 Forge Release;项目策略只能说明创建方式,不能授予 仅当用户本次请求明确要求时创建 Forge Release;项目策略只能说明创建方式,不能授予
外部写权限。必须引用已经存在并验证过的 tag,禁止让平台从默认分支隐式创建 tag。 外部写权限。必须引用已经存在并验证过的 tag,禁止让平台从默认分支隐式创建 tag。
发布说明中的每项变化都应能追溯到本次 PR/MR、提交或项目 CHANGELOG Forge Release 正文复用 tag 中经过验证的更新说明,不维护第二份相互独立的发布内容
### 8. 验证、恢复和清理 ### 9. 验证、恢复和清理
分别报告以下状态,不要用“发布成功”掩盖其中某一步未完成: 分别报告以下状态,不要用“发布成功”掩盖其中某一步未完成:
- 版本文件和本地验证。 - 版本文件和本地验证。
- PR/MR URL、合并状态和 merged commit。 - PR/MR URL、合并状态和 merged commit。
- 远端 tag 及其解引用后的 commit。 - 远端 tag 的对象类型、完整更新说明及其解引用后的 commit。
- Forge Release URL 和可见性,若本次要求创建。 - Forge Release URL、可见性及正文一致性,若本次要求创建。
- worktree、本地分支和远端分支是否保留。 - worktree、本地分支和远端分支是否保留。
清理时只移除干净且已确认不再使用的 linked worktree,不使用强制删除。删除本地或远端 清理时只移除干净且已确认不再使用的 linked worktree,不使用强制删除。删除本地或远端
@@ -176,12 +210,15 @@ tag。推送后读取远端 tag,并将 annotated tag 解引用到 commit,确
遇到以下任一情况时停止相应写操作并说明恢复路径: 遇到以下任一情况时停止相应写操作并说明恢复路径:
- base、目标版本、版本来源、remote,或请求阶段所需的托管平台无法唯一确定。 - base、目标版本、版本来源、remote,或请求阶段所需的托管平台无法唯一确定。
- 直接发布的目标 commit 无法从计划远端分支到达,或项目要求的发布检查未通过。
- 项目策略包含未知字段、错误类型、仓库外路径或不安全 ref。 - 项目策略包含未知字段、错误类型、仓库外路径或不安全 ref。
- 多个权威版本来源不一致。 - 多个权威版本来源不一致。
- 目标分支、worktree、PR/MR、tag 或 release 已存在但身份不匹配。 - 目标分支、worktree、PR/MR、tag 或 release 已存在但身份不匹配。
- required checks、review 或冲突状态不满足合并规则。 - required checks、review 或冲突状态不满足合并规则。
- 合并结果的准确 commit 无法从平台确认,或无法从远端 base 到达。 - 合并结果的准确 commit 无法从平台确认,或无法从远端 base 到达。
- 远端 tag 已指向其他 commit。 - 远端 tag 已指向其他 commit。
- 更新说明为空、只有版本号、包含占位内容、无法追溯到实际变化,或远端 tag 不是包含
计划说明的 annotated tag。
- 请求的 PR/MR、合并或 Forge Release 阶段所需的平台 CLI 缺失、未认证或无法读取状态。 - 请求的 PR/MR、合并或 Forge Release 阶段所需的平台 CLI 缺失、未认证或无法读取状态。
- 项目要求 tag 签名,但签名能力不可用或本地签名验证失败。 - 项目要求 tag 签名,但签名能力不可用或本地签名验证失败。
- 操作需要 force push、管理员绕过、移动已发布 tag 或覆盖用户工作。 - 操作需要 force push、管理员绕过、移动已发布 tag 或覆盖用户工作。
@@ -192,7 +229,9 @@ tag。推送后读取远端 tag,并将 annotated tag 解引用到 commit,确
## 完成标准 ## 完成标准
只分析时,给出当前状态、建议版本、依据和下一步。开始版本时,给出 worktree 路径、 只分析时,给出当前状态、建议版本、依据和下一步。直接发布时,给出目标 commit 和更新
分支和基线 commit。创建 PR/MR 时,给出 URL 与检查状态。合并时,给出 merged commit。 说明,不得虚构 worktree 或分支步骤。开始准备版本时,给出所用 worktree、分支和基线
发布 tag 时,证明远端 tag 解引用到该 commit。创Forge Release 时,给出 release URL commit;未新worktree 时明确说明复用了哪个安全工作区。创建 PR/MR 时,给出 URL
与可见性。任何部分未完成都要标明阻塞阶段和可恢复动作。 检查状态。合并时,给出 merged commit。发布 tag 时,证明远端 tag 是包含计划更新说明的
annotated tag,并解引用到 release commit。创建 Forge Release 时,再给出 release URL、
可见性与正文一致性。任何部分未完成都要标明阻塞阶段和可恢复动作。
@@ -68,14 +68,16 @@ commit 已进入远端 base。只看到本地 merge commit 或分支关闭不足
## 创建 Forge Release ## 创建 Forge Release
把 Git tag 和 Forge Release 当成两个独立状态。先创建、推送并验证 tag,再创建 release。 把 Git tag 和 Forge Release 当成两个独立状态。先创建、推送并验证包含完整更新说明的
annotated tag,再创建 release。Forge Release 正文复用已经冻结并验证的 tag 更新说明,
不要重新生成另一份内容。
- GitHub 创建 release 时使用能够拒绝缺失 tag 的选项,例如当前 CLI 支持的 - GitHub 创建 release 时使用能够拒绝缺失 tag 的选项,例如当前 CLI 支持的
`--verify-tag` `--verify-tag`
- GitLab、Gitea 或 Forgejo 创建 release 前,先用只读命令证明 tag 已存在并指向计划的 - GitLab、Gitea 或 Forgejo 创建 release 前,先用只读命令证明 tag 已存在并指向计划的
commit;不要使用会顺便创建 tag 的默认行为。 commit、tag object 包含计划的更新说明;不要使用会顺便创建 tag 的默认行为。
- 预发布版本按项目规则标记 prerelease,不要自动把它标为 latest 或 stable。 - 预发布版本按项目规则标记 prerelease,不要自动把它标为 latest 或 stable。
- 发布后重新读取 release URL、tag 和可见性。 - 发布后重新读取 release URL、tag、正文和可见性,确认正文与 tag 更新说明一致
Forge Release 创建失败但 tag 已成功推送时,保留 tag 并从 release 阶段恢复,不要重新 Forge Release 创建失败但 tag 已成功推送时,保留 tag 并从 release 阶段恢复,不要重新
合并或创建另一个 tag。 合并或创建另一个 tag。
@@ -28,9 +28,11 @@
- `schema`:配置结构版本,当前为 `1` - `schema`:配置结构版本,当前为 `1`
- `base_branch`:发布 PR/MR 的目标分支。 - `base_branch`:发布 PR/MR 的目标分支。
- `branch_pattern`:版本分支格式,支持无展示前缀的规范 `{version}` - `branch_pattern`需要准备仓库修改时使用的版本分支格式,支持无展示前缀的规范
`{version}`;直接发布不因此创建分支。
- `version.scheme``semver` 或项目已经使用的其他方案。 - `version.scheme``semver` 或项目已经使用的其他方案。
- `version.sources`:构建和运行实际读取的权威版本文件 - `version.sources`:构建和运行实际读取的权威版本文件;项目明确从 Git tag 派生版本时
可以省略,不要为了发布新增无消费方的版本文件。
- `tag_pattern`release tag 格式,支持无展示前缀的规范 `{version}` - `tag_pattern`release tag 格式,支持无展示前缀的规范 `{version}`
- `merge_method``squash``merge``rebase` - `merge_method``squash``merge``rebase`
- `forge_release`:项目是否建议在 tag 后创建 Forge Release;它不授予创建权限。 - `forge_release`:项目是否建议在 tag 后创建 Forge Release;它不授予创建权限。
@@ -54,18 +56,20 @@ symlink 后,目标必须仍在当前 worktree 内;权威来源通常还应
1. 从 remote HEAD 和平台信息确定默认 base,不能确定时询问。 1. 从 remote HEAD 和平台信息确定默认 base,不能确定时询问。
2. 从项目文档、CI 和构建入口找版本文件及更新方式。 2. 从项目文档、CI 和构建入口找版本文件及更新方式。
3. 从已有分支和已合并 PR/MR 识别命名与合并方式。 3. 仅当发布需要修改仓库内容时,从已有分支和已合并 PR/MR 识别命名与合并方式。
4. 从稳定 tag 识别前缀版本方案,只把 tag 当作交叉验证。 4. 从稳定 tag 识别前缀版本方案和更新说明范围;项目由 tag 派生版本时,把实际构建
5. 无项目约定时使用 `release/v<version>`、SemVer、`v<version>` annotated tag 和 入口和 tag 历史共同作为版本来源证据,否则只把 tag 当作交叉验证。
squash merge;默认不创建 Forge Release,不删除远端分支。 5. 无项目约定时使用 SemVer 和 `v<version>` annotated tag;需要准备仓库修改时再使用
`release/v<version>` 和 squash merge。默认不创建 Forge Release,不删除远端分支。
## 每次执行的计划快照 ## 每次执行的计划快照
在第一次写操作前列出以下事实: 在第一次写操作前列出以下事实:
- base 分支远端 commit。 - base 分支远端 commit 和 tag 将指向的目标 commit。
- 当前版本、目标版本、版本来源和升级依据。 - 当前版本、目标版本、版本来源和升级依据。
- worktree 路径与分支名 - 更新说明的提交范围、来源和拟发布内容
- 是否需要修改仓库内容;需要时再列出 worktree 路径与分支名。
- PR/MR 托管平台和合并方式。 - PR/MR 托管平台和合并方式。
- tag 与 Forge Release 计划。 - tag 与 Forge Release 计划。
- 用户授权的最远阶段和清理范围。 - 用户授权的最远阶段和清理范围。
+6 -3
View File
@@ -12,12 +12,15 @@ base、head、PR/MR、merged commit、tag 和 release,再执行唯一缺失的
| 当前状态 | 继续方式 | 禁止事项 | | 当前状态 | 继续方式 | 禁止事项 |
|---|---|---| |---|---|---|
| 远端 release commit 已准备,没有 tag | 核对版本与更新说明后直接创建 annotated tag | 不创建无必要的分支或 worktree |
| 本地 annotated tag 已创建但未推送 | 核对对象类型、commit 和完整说明后只推送该 tag | 不因重入重复创建或改写 tag |
| worktree 已创建,无改动 | 继续开发,或经授权移除干净 worktree | 不使用强制删除 | | worktree 已创建,无改动 | 继续开发,或经授权移除干净 worktree | 不使用强制删除 |
| 本地发布分支已存在,没有 worktree | 核对版本身份、tip、upstream 和 base 后挂载现有分支 | 不用 `-b` 覆盖分支 | | 本地发布分支已存在,没有 worktree | 核对版本身份、tip、upstream 和 base 后挂载现有分支 | 不用 `-b` 覆盖分支 |
| 分支已推送,没有 PR/MR | 确认 head/base 后创建一次 PR/MR | 不重复推送新分支 | | 分支已推送,没有 PR/MR | 确认 head/base 后创建一次 PR/MR | 不重复推送新分支 |
| PR/MR 已存在,未合并 | 复用 URL,刷新 checks、review 和冲突状态 | 不创建第二个 PR/MR | | PR/MR 已存在,未合并 | 复用 URL,刷新 checks、review 和冲突状态 | 不创建第二个 PR/MR |
| PR/MR 已合并,没有 tag | 获取 merged commit在该 commit 上继续发布 | 不重新合并 | | PR/MR 已合并,没有 tag | 获取 merged commit生成并确认更新说明后继续发布 | 不重新合并 |
| tag 已推送,没有 release | 验证 tag 后创建 Forge Release | 不创建替代 tag | | tag 已推送,没有 release | 验证 tag 的 commit 和更新说明后创建 Forge Release | 不创建替代 tag |
| tag 指向正确 commit 但为 lightweight 或说明不符 | 停止并交由维护者决定撤销或发布修订版本 | 不移动、覆盖或补写远端 tag |
| release 已创建,验证未完成 | 读回 release、tag 和可见性 | 不直接声称发布完成 | | release 已创建,验证未完成 | 读回 release、tag 和可见性 | 不直接声称发布完成 |
| 同名资源身份不匹配 | 停止并报告差异 | 不覆盖、关闭或删除未知资源 | | 同名资源身份不匹配 | 停止并报告差异 | 不覆盖、关闭或删除未知资源 |
@@ -35,7 +38,7 @@ merge queue 和 auto-merge。任何 force push 都是硬停止条件。
- 未推送的本地版本提交可以在用户授权下修改或放弃,但不要覆盖其他工作。 - 未推送的本地版本提交可以在用户授权下修改或放弃,但不要覆盖其他工作。
- 已推送但未合并的 PR/MR 可以关闭,分支默认保留。 - 已推送但未合并的 PR/MR 可以关闭,分支默认保留。
- 已合并变更通过新的 revert PR/MR 回滚,不重写 base 历史。 - 已合并变更通过新的 revert PR/MR 回滚,不重写 base 历史。
- 已发布 tag 默认不可变。tag 错误时停止,由维护者决定撤销发布或发布新版本。 - 已发布 tag 默认不可变。commit 或更新说明错误时停止,由维护者决定撤销发布或发布新版本。
- Forge Release 失败不回滚已经正确推送的 tag;从 release 阶段恢复。 - Forge Release 失败不回滚已经正确推送的 tag;从 release 阶段恢复。
## 清理规则 ## 清理规则
+40 -14
View File
@@ -5,15 +5,17 @@
按项目实际构建链路寻找版本来源,不要遍历到一个看起来像版本号的字符串就修改。优先级: 按项目实际构建链路寻找版本来源,不要遍历到一个看起来像版本号的字符串就修改。优先级:
1. 项目发布文档或 `.release-policy.yaml` 明确声明的文件。 1. 项目发布文档或 `.release-policy.yaml` 明确声明的文件。
2. 构建、打包或运行入口直接读取的清单,例如 `VERSION``package.json` 2. 项目明确由 Git tag 或 VCS metadata 派生构建版本时,以 tag 规则作为发布版本来源,
不要为了发布凭空新增或修改版本文件。
3. 构建、打包或运行入口直接读取的清单,例如 `VERSION``package.json`
`pyproject.toml``Cargo.toml` 或语言工具链的版本配置。 `pyproject.toml``Cargo.toml` 或语言工具链的版本配置。
3. 由权威文件生成的镜像文件、锁文件或发布元数据。 4. 由权威文件生成的镜像文件、锁文件或发布元数据。
4. 最近稳定 tag用于验证当前版本和发布历史。 5. 最近稳定 tag,用于确定当前已发布版本、更新说明范围和发布历史。
记录每个权威文件的当前值和更新方式。多个权威来源不一致时停止,不要选择修改时间最新 记录每个权威文件的当前值和更新方式。多个权威来源不一致时停止,不要选择修改时间最新
的文件,也不要只改其中一个。 的文件,也不要只改其中一个。
配置声明版本文件必须使用仓库相对路径。拒绝绝对路径、`..` 和解析后逃出目标 配置声明版本文件时,它们必须使用仓库相对路径。拒绝绝对路径、`..` 和解析后逃出目标
worktree 的 symlink;修改前确认规范化后的准确路径。不要用 glob 或模糊搜索结果执行 worktree 的 symlink;修改前确认规范化后的准确路径。不要用 glob 或模糊搜索结果执行
批量替换。 批量替换。
@@ -45,10 +47,19 @@ worktree 的 symlink;修改前确认规范化后的准确路径。不要用 gl
项目采用自定义前缀或非 SemVer 时,以项目版本来源定义规范值,以 branch/tag pattern 项目采用自定义前缀或非 SemVer 时,以项目版本来源定义规范值,以 branch/tag pattern
定义展示形式。规范化结果无法唯一确定时停止并让用户确认。 定义展示形式。规范化结果无法唯一确定时停止并让用户确认。
## 判断是否需要同步版本
直接读取计划 release commit 中的权威版本来源。版本文件已经是目标值,或者项目明确由
tag 派生版本,且发布说明不要求写回仓库时,不产生仓库修改,直接进入 tag 发布。
只有权威版本文件、生成文件或仓库内 CHANGELOG 必须变化时,才进入准备版本流程并使用
分支/PR。不要为了制造发布提交而触碰与构建、运行或项目发布规则无关的文件。
## 同步版本 ## 同步版本
在发布分支中一次性更新所有权威版本来源、生成文件和项目要求的 CHANGELOG。运行项目 仅在判断存在必要仓库差异后,在准备版本的分支中一次性更新所有权威版本来源、生成文件
自己的版本更新工具时,先检查它会修改哪些文件,避免隐式发布、提交或上传。 和项目要求的 CHANGELOG。运行项目自己的版本更新工具时,先检查它会修改哪些文件,避免
隐式发布、提交或上传。
提交前确认: 提交前确认:
@@ -57,23 +68,38 @@ worktree 的 symlink;修改前确认规范化后的准确路径。不要用 gl
- CHANGELOG 或发布说明描述的是本次实际变更。 - CHANGELOG 或发布说明描述的是本次实际变更。
- 项目构建和测试读取到了新版本。 - 项目构建和测试读取到了新版本。
## 验证 merged commit ## 生成更新说明
合并后以代码托管平台返回的 merged commit 为准。获取远端 base 后,确认该 commit 可从 以最近一个适用于当前发布线的稳定 tag 为起点,以 release commit 为终点,结合项目
远端 base 到达,并直接读取该 commit 中的版本文件。不要用仍停留在 feature worktree CHANGELOG、已合并 PR/MR 和实际 diff 生成更新说明。首次发布使用项目声明的发布基线;
中的文件证明已合并版本。 没有声明时核对完整可达历史,并在说明中明确这是首次发布。说明覆盖实际存在的新增、
变更、修复、维护或文档变化;破坏性变化、迁移步骤和已知限制存在时必须单独标明。没有
某一类别时可以省略该类别,不要生成空标题或模板占位。
每条说明都要能追溯到范围内的提交、PR/MR 或项目 CHANGELOG。提交消息和 PR/MR 文本只
作为待核对素材,不能覆盖实际 diff,也不能把范围外变化写入本次说明。tag 创建前冻结
完整多行说明;tag 已推送后不再修改。
## 验证 release commit
准备版本路径以代码托管平台返回的 merged commit 为准;直接发布路径以计划中锁定的远端
commit 为准。获取远端 base 或允许的维护分支后,确认 release commit 可从对应远端引用
到达,并直接读取该 commit 中的版本文件。不要用当前 checkout 或 feature worktree 中的
文件证明 release commit 内容。
正式 tag 必须满足: 正式 tag 必须满足:
- tag 名按项目格式由目标版本唯一生成。 - tag 名按项目格式由目标版本唯一生成。
- 远端没有同名 tag,或同名 tag 已经准确指向本次 commit。 - 远端没有同名 tag,或同名 tag 已经准确指向本次 commit。
- annotated tag 显式指向 merged commit。 - annotated tag 显式指向 release commit,并包含冻结后的完整更新说明;禁止 lightweight
release tag。
- 项目要求签名时,push 前已成功创建 signed tag,并在本地验证签名通过;不可用或失败 - 项目要求签名时,push 前已成功创建 signed tag,并在本地验证签名通过;不可用或失败
时停止,不得改用 unsigned tag。 时停止,不得改用 unsigned tag。
- 推送后从远端重新读取,并将 annotated tag 解引用到 commit。 - 推送后从远端重新读取 tag object,核对对象类型和完整说明,再将 annotated tag 解引用
到 commit。
展开 tag pattern 后用 `git check-ref-format refs/tags/<tag>` 校验。把 tag 和 commit 作为 展开 tag pattern 后用 `git check-ref-format refs/tags/<tag>` 校验。把 tag 和 commit 作为
独立 argv 传递,不拼接 shell 字符串。 独立 argv 传递,不拼接 shell 字符串。
tag 已在远端指向其他 commit 时停止。不要 force push、删除或移动已发布 tag;由维护者 tag 已在远端指向其他 commit、是 lightweight tag、缺少更新说明或说明不一致时停止。
决定撤销发布或创建新的修订版本。 不要 force push、删除或移动已发布 tag;由维护者决定撤销发布或创建新的修订版本。
@@ -2,10 +2,12 @@
# 此文件描述发布策略,不授予任何远端写操作权限。 # 此文件描述发布策略,不授予任何远端写操作权限。
schema: 1 schema: 1
base_branch: main base_branch: main
# 仅在发布必须修改仓库内容时使用;直接发布不会因此创建分支。
# {version} 是规范版本,例如 1.6.0;前缀由 pattern 添加。 # {version} 是规范版本,例如 1.6.0;前缀由 pattern 添加。
branch_pattern: release/v{version} branch_pattern: release/v{version}
version: version:
scheme: semver scheme: semver
# 项目明确从 Git tag 派生版本时可以省略 sources。
sources: sources:
- VERSION - VERSION
tag_pattern: v{version} tag_pattern: v{version}