> ## Documentation Index
> Fetch the complete documentation index at: https://cloud-docs.orbi.build/llms.txt
> Use this file to discover all available pages before exploring further.

# 发版

> 怎么发一个版本，Orbi 在打 tag 之前会检查什么?

你在状态页的**发个版本**里填一个版本号，Orbi 从那里开始按确定的流程把版本发出去。

发版永远不是自动的。Orbi 不会替你判断「这些工作可以发了」，也永远不会替你递增版本号 —— 下一个版本是什么，是产品决策。你把版本号定下来之后，Orbi 做的是每次都一样的事：过门禁，逐条核验 scope，bump 版本号，等 CI，打 tag，发布，关闭 milestone。

发版不是一次开发交付。没有模型会话，也没有拉取请求，它跑的是一个固定的状态机。

## 发一个版本

在状态页的**绑定的仓库** (Connected repository) 卡片里找到**发个版本（版本号）**。

<Frame caption="状态页上的发个版本表单，含版本号输入框，下方显示基础分支与版本文件">
  <img src="https://mintcdn.com/orbi-cloud/Ya1dzVX1ins06kld/images/zh/status-active.png?fit=max&auto=format&n=Ya1dzVX1ins06kld&q=85&s=4b803af92c4a5fe1f7d2f5d8384d8581" alt="状态页上的发个版本表单，含版本号输入框，下方显示基础分支与版本文件" width="1920" height="4850" data-path="images/zh/status-active.png" />
</Frame>

1. 填版本号，形如 `v1.2.3` —— 开头一个 `v`，后面是点号分隔的数字。其他形状会在创建 Issue **之前**就被拒绝。
2. 点**发个版本**。

Orbi 会在你连接的仓库里建一张 Release Issue，打上 `ai-ready` 和 `ai-release` 标签，然后直接把你带到这张 Issue。之后引擎会像认领其他工作一样认领它。

表单下方会显示这次发版要用的东西：**基础分支**，以及 Orbi 在你仓库里探测到的**版本文件**。

### milestone 是必需的

发版的 scope 来自**标题与你填的版本号完全一致的那个 milestone** —— 填 `v1.2.3` 就需要一个叫 `v1.2.3` 的 milestone。

Orbi 不会替你创建这个 milestone。没有它，发版会停在 scope 推导这一步并说明原因。发版之前先建好 milestone，并把它覆盖的 Issue 和 PR 放进去。

### 表单报错时

| 提示        | 原因                    | 怎么修                             |
| --------- | --------------------- | ------------------------------- |
| 版本号无效     | 不是 `vN.N…` 的形状        | 重填，例如 `v1.2.3`                  |
| 没有仓库      | 没有生效中的仓库              | 先[连接一个](/zh/connect-repository) |
| 版本文件探测失败  | Orbi 无法判断你的项目用什么声明版本号 | 见下                              |
| GitHub 错误 | GitHub 不可达，或 App 权限不足 | 重试；持续失败就重装 App                  |

### 版本文件

Orbi 会探测你的项目用什么声明版本号，按这个顺序找：

`pyproject.toml`、`package.json`、`pom.xml`、`build.gradle`、`build.gradle.kts`、`gradle.properties`、`Cargo.toml`、`composer.json`、`pubspec.yaml`

一个都没有时，这次发版是**只打 tag** —— 不 bump 版本号，只有 tag 和 GitHub Release。Orbi 拒绝猜：与其默认回退到 `pyproject.toml` 然后改错文件，它宁可报告探测失败。

## 自己手写 Release Issue

表单只是个便利入口。你也可以手动建这张 Issue —— 打上 `ai-ready` 和 `ai-release`，并写一段 `## Release`：

```text theme={null}
## Release

- version: v1.2.3
- base_branch: main
- version_file: package.json
- scope_from_milestone: v1.2.3
```

这一段是严格解析的：字段缺失、重复或出现未知字段都会立即失败。除了 `scope_from_milestone`，你也可以用 `#N` 的形式把 scope 逐条列出来。

## 打 tag 之前 Orbi 检查什么

门禁按顺序执行，**查不了的门禁算没过**。

**先冻结 base。** 发版 commit 就是认领那一刻你基础分支的 tip。此后所有判断都针对这一个 commit。

**这个 milestone 里不能有未完成的工作。** 只要里面还有 Issue 处于 `ai-in-progress`、`ai-pr-opened` 或 `ai-fix-needed`，发版就停。开着的 PR **故意不算**门禁 —— 一个开着的 PR 是队列状态，不代表这个版本的任何结论。

**发版 commit 上的 CI 必须是绿的。** 这就是测试验收，也是这里最值得搞明白的一条：

> **你的 CI 决定了 Orbi 保证什么。** 门禁读的是**你仓库**在发版 commit 上的 GitHub Actions check runs。你把单元测试放进去，发版就保证你的代码逻辑；你把业务流程 e2e 放进去，它就保证这条流程能走通。**完全不跑 CI 也是一种选择**：没有 check runs 时门禁带着明确的「无 check runs」证据通过，版本照样发出去 —— 只是少了它唯一的测试验收。

pending 的 check 不算失败：门禁会等最终结论，默认最多等 30 分钟，等待期间持续上报进度。等待超时会如实报成超时，绝不会伪装成 CI 失败。

**scope 逐条实时核验。** 每一条要么是已合并的 PR，要么是以 completed 关闭的 Issue。Issue 正文里的勾选框**完全不看** —— 核验走 GitHub API。以「not planned」关闭的 Issue 会被记为排除，不进 changelog。

## 产出什么

* 在发版 commit 上创建一个**带注释的 tag**，用普通 push 推上去 —— 永远不用 `--force`。如果 tag 已存在，它必须**恰好**指向这个 commit，否则发版失败，而不是去移动它。
* 发布一个 **GitHub Release**，release notes 里带完整证据：版本号、tag、发版 commit、逐条 scope 证据、门禁证据和测试证据。
* **关闭 milestone**。

每一步都是幂等的，所以在门禁上失败的发版，修掉原因后可以重试 —— 它会接着跑而不是从头来，也永远不会移动或覆盖一个已经存在的 tag。

## 发版失败时

CI 红了或超时会走正常的失败路径，Issue 被标记 `ai-blocked` 并写明原因。

这时版本号的 bump 可能已经落在你的基础分支上了，这是一个可接受的状态：bump 是幂等的，修好 CI 重跑一次会安全地把同一个版本再准备一遍。

## 为什么发版保持手动

release 标签是 Orbi 唯一不会自己打的开关。循环里其他环节可以自动，是因为错了能收回来 —— 一个坏 PR 关掉就是了，一张 `ai-blocked` 的 Issue 重新派发就是了。而一个已经发布的 tag 和 GitHub Release 是公开的，收回来代价大得多，所以这个决定留给人。

<Note>
  发版状态机属于引擎，不属于 Cloud。它的完整参考 —— 每一道门禁、接续行为、写下的证据 —— 在 [docs.orbi.build/zh/workflow](https://docs.orbi.build/zh/workflow)。
</Note>
