Skip to content

Commit 65ccfac

Browse files
authored
feat(release): simplify npm publishing (#40)
* feat(release): simplify npm publishing Change-Id: Ie20ec6314bd82891009cf5b1ec7b2670fa45005d * ci: remove maintainer evidence gate Change-Id: If1bffe80c7c7d8e9bca1035cc7a32e9e975b5acf
1 parent 0236e73 commit 65ccfac

14 files changed

Lines changed: 128 additions & 171 deletions

.github/workflows/ci.yml

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -51,23 +51,6 @@ jobs:
5151
BASE: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }}
5252
run: bun scripts/verify.ts full --step "${{ matrix.step }}"
5353

54-
maintainer-evidence:
55-
name: Maintainer evidence
56-
runs-on: ubuntu-latest
57-
steps:
58-
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
59-
with:
60-
fetch-depth: 0
61-
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
62-
with:
63-
bun-version: "1.3.5"
64-
- name: Require evidence only for high-risk changes
65-
env:
66-
BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }}
67-
HEAD_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
68-
PR_BODY: ${{ github.event.pull_request.body }}
69-
run: bun scripts/pr-evidence.ts
70-
7154
package-compatibility:
7255
name: Package compatibility / Node ${{ matrix.node }}
7356
runs-on: ubuntu-latest
@@ -91,14 +74,13 @@ jobs:
9174
gate:
9275
name: Gate
9376
runs-on: ubuntu-latest
94-
needs: [prepare-verification, verify, maintainer-evidence, package-compatibility]
77+
needs: [prepare-verification, verify, package-compatibility]
9578
if: always()
9679
steps:
9780
- name: Check results
9881
run: |
9982
if [ "${{ needs.prepare-verification.result }}" != "success" ] || \
10083
[ "${{ needs.verify.result }}" != "success" ] || \
101-
[ "${{ needs.maintainer-evidence.result }}" != "success" ] || \
10284
[ "${{ needs.package-compatibility.result }}" != "success" ]; then
10385
echo "One or more required checks failed."
10486
exit 1

.github/workflows/prepare-beta.yml

Lines changed: 0 additions & 89 deletions
This file was deleted.

.github/workflows/release.yml

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
name: Publish npm
22

33
on:
4+
push:
5+
branches: [main]
46
workflow_dispatch:
57
inputs:
68
channel:
@@ -11,7 +13,7 @@ on:
1113
- beta
1214
- stable
1315
confirm:
14-
description: Type PUBLISH to confirm
16+
description: Type PUBLISH to confirm a manual beta or stable retry
1517
required: true
1618
type: string
1719

@@ -23,7 +25,10 @@ concurrency:
2325

2426
jobs:
2527
preflight:
26-
if: vars.NPM_RELEASE_ENABLED == 'true' && inputs.confirm == 'PUBLISH'
28+
if: >-
29+
vars.NPM_RELEASE_ENABLED == 'true' &&
30+
((github.event_name == 'push' && contains(github.event.head_commit.message, 'chore: release packages')) ||
31+
(github.event_name == 'workflow_dispatch' && inputs.confirm == 'PUBLISH'))
2732
runs-on: ubuntu-latest
2833
outputs:
2934
channel: ${{ steps.release.outputs.channel }}
@@ -53,11 +58,18 @@ jobs:
5358
- name: Install dependencies
5459
run: bun install --frozen-lockfile
5560

61+
- name: Prepare immutable beta snapshot version
62+
if: github.event_name == 'workflow_dispatch' && inputs.channel == 'beta'
63+
run: >-
64+
bun run scripts/release/snapshot.ts apply
65+
--run-id "${{ github.run_id }}"
66+
--sha "${{ github.sha }}"
67+
5668
- name: Validate channel, branch, and package versions
5769
id: release
5870
run: >-
5971
bun run scripts/release/channel.ts validate
60-
--channel "${{ inputs.channel }}"
72+
--channel "${{ github.event_name == 'push' && 'stable' || inputs.channel }}"
6173
--ref "${{ github.ref_name }}"
6274
--output "$GITHUB_OUTPUT"
6375
@@ -94,6 +106,9 @@ jobs:
94106
- name: Build publishable packages
95107
run: |
96108
bun install --frozen-lockfile
109+
if [ "${{ needs.preflight.outputs.channel }}" = beta ]; then
110+
bun run scripts/release/snapshot.ts apply --version "${{ needs.preflight.outputs.version }}"
111+
fi
97112
bun run build:packages
98113
99114
- name: Publish packages with npm Trusted Publishing

docs/contributing/release.md

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,27 +30,25 @@ Add a changeset whenever a user-visible package change is made:
3030
bun run changeset
3131
```
3232

33-
Choose the SemVer impact and describe the change for the generated changelog. Once merged, the **Release PR** workflow creates or updates a stable version PR on `main`. It never publishes npm packages.
33+
Choose the SemVer impact and describe the change for the generated changelog. Once merged, the **Release PR** workflow creates or updates a stable version PR on `main`. Only merging that Release PR starts stable publishing, which still waits for approval on the `npm-release` Environment.
3434

3535
## Publish a beta
3636

37-
1. In GitHub, open **Actions → Prepare Beta Release → Run workflow**.
38-
2. Keep the workflow branch set to `main`, enter the target stable series (for example `0.1.0`), and run it.
39-
3. The workflow creates or updates `release/0.1.0-beta`, consumes the available changesets, and commits the next version such as `0.1.0-beta.0`.
40-
4. Open **Actions → Publish npm → Run workflow**.
41-
5. Select `release/0.1.0-beta` in the workflow branch dropdown, choose `beta`, type `PUBLISH`, and run it.
42-
6. Approve the `npm-release` Environment deployment after reviewing the commit and job summary.
37+
1. In GitHub, open **Actions → Publish npm → Run workflow**.
38+
2. Keep the workflow branch set to `main`, choose `beta`, type `PUBLISH`, and run it.
39+
3. Approve the `npm-release` Environment deployment after reviewing the commit, generated version, and job summary.
4340

44-
The publish workflow validates that the selected branch, package version, and channel agree. It publishes with the npm `beta` dist-tag and creates the immutable `v0.1.0-beta.N` tag. It then installs that exact version from the public npm registry on Linux, Windows, and macOS under Node.js 22 and 24. The GitHub prerelease is created only after all six consumer jobs pass.
41+
The workflow derives an immutable version from the GitHub Actions run ID and current `main` commit without changing Git history, for example `0.0.0-beta.run-123456789.sha-a1b2c3d`. It publishes with the npm `beta` dist-tag and creates the matching immutable Git tag. It then installs that exact version from the public npm registry on Linux, Windows, and macOS under Node.js 22 and 24. The GitHub prerelease is created only after all six consumer jobs pass.
4542

46-
For another beta, merge fixes and their changesets into `main`, then rerun **Prepare Beta Release** for the same series. The workflow merges `main` into the beta branch and calculates the next beta. Never merge the beta branch back into `main`; delete it after the stable release.
43+
For another beta, merge fixes into `main` and run **Publish npm** again. Every run gets a new Actions-run-and-commit-derived version; no beta branch is created and changesets are not consumed.
4744

4845
## Publish a stable release
4946

5047
1. Review and merge the automated `chore: release packages` PR.
51-
2. Open **Actions → Publish npm → Run workflow**.
52-
3. Select `main`, choose `stable`, type `PUBLISH`, and run it.
53-
4. Approve the `npm-release` Environment deployment after reviewing the exact package version.
48+
2. **Publish npm** starts automatically when the release commit reaches `main`.
49+
3. Approve the `npm-release` Environment deployment after reviewing the exact package version.
50+
51+
If publishing fails partway through, manually rerun it from **Actions → Publish npm → Run workflow** with branch `main`, channel `stable`, and confirmation `PUBLISH`. Exact package versions already published are skipped.
5452

5553
Only a clean `X.Y.Z` version on `main` can publish to npm's `latest` tag. Publishing creates the matching immutable Git tag, waits for all three packages to become visible in the public registry, and runs the six-job consumer matrix. The GitHub Release is the final certification step and is created only after that matrix passes. Publishing is idempotent: packages whose exact version already exists are skipped, so a partially failed publish can be retried from the same commit.
5654

@@ -93,7 +91,7 @@ npm install --global @openagentpack/cli
9391
npm install --global @openagentpack/cli@beta
9492

9593
# Pin or test an exact version without a global install
96-
npx @openagentpack/cli@0.1.0-beta.0 --version
94+
npx @openagentpack/cli@0.0.0-beta.run-123456789.sha-a1b2c3d --version
9795

9896
# SDK
9997
npm install @openagentpack/sdk
@@ -108,4 +106,4 @@ After installing the CLI, run `agents --help`. A beta user returns to stable wit
108106
- If all packages published but a post-release consumer job fails, keep the immutable tag, do not unpublish or move the tag, and do not create the GitHub Release. Fix the compatibility issue and publish a new patch version; npm package versions cannot be overwritten.
109107
- Registry visibility is retried for five minutes before it is classified as a release failure. Retry the same workflow only when npm propagation, rather than package compatibility, was the cause.
110108
- If a version tag already points at another commit, stop. Tags are immutable; investigate the repository history instead of moving or deleting the tag.
111-
- If **Prepare Beta Release** reports no unreleased changesets, add a changeset on `main` before preparing another beta.
109+
- Beta publishing must be manually dispatched from `main`; the release identity check rejects other branches.

docs/contributing/release.zh-CN.md

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -30,27 +30,25 @@ OpenAgentPack 将 `@openagentpack/sdk`、`@openagentpack/playground` 和 `@opena
3030
bun run changeset
3131
```
3232

33-
选择 SemVer 影响范围并填写 changelog。PR 合并后,**Release PR** 工作流会在 `main` 上创建或更新稳定版版本 PR,但绝不会直接发布 npm
33+
选择 SemVer 影响范围并填写 changelog。PR 合并后,**Release PR** 工作流会在 `main` 上创建或更新稳定版版本 PR。只有合并该 Release PR 才会自动启动正式发布,并且发布仍需 `npm-release` Environment 审批
3434

3535
## 发布 Beta
3636

37-
1. 打开 GitHub 的 **Actions → Prepare Beta Release → Run workflow**
38-
2. workflow branch 保持 `main`,输入目标稳定版系列,例如 `0.1.0`,然后运行。
39-
3. 工作流会创建或更新 `release/0.1.0-beta`,消费现有 changeset,并提交下一个版本,例如 `0.1.0-beta.0`
40-
4. 打开 **Actions → Publish npm → Run workflow**
41-
5. 在 workflow branch 下拉框选择 `release/0.1.0-beta`,channel 选择 `beta`,输入 `PUBLISH`,然后运行。
42-
6. 检查提交和 job 信息后,批准 `npm-release` Environment deployment。
37+
1. 打开 GitHub 的 **Actions → Publish npm → Run workflow**
38+
2. workflow branch 保持 `main`,channel 选择 `beta`,输入 `PUBLISH`,然后运行。
39+
3. 检查提交、自动生成的精确版本和 job 信息后,批准 `npm-release` Environment deployment。
4340

44-
发布工作流会同时校验分支、包版本和 channel。通过后使用 npm 的 `beta` dist-tag 发布,并创建不可变的 `v0.1.0-beta.N` Git tag。随后,它会在 Linux、Windows、macOS 的 Node.js 22 和 24 环境中,从公共 npm registry 安装该精确版本。只有六个消费者 job 全部通过,才创建 GitHub prerelease。
41+
工作流根据 GitHub Actions run ID 和 `main` 当前提交生成不修改 Git 历史的版本,例如 `0.0.0-beta.run-123456789.sha-a1b2c3d`。通过后使用 npm 的 `beta` dist-tag 发布,并创建对应的不可变 Git tag。随后,它会在 Linux、Windows、macOS 的 Node.js 22 和 24 环境中,从公共 npm registry 安装该精确版本。只有六个消费者 job 全部通过,才创建 GitHub prerelease。
4542

46-
需要下一个 Beta 时,把修复及其 changeset 合并到 `main`,再对同一个版本系列运行 **Prepare Beta Release**工作流会把 `main` 合入 Beta 分支并计算下一个 Beta。不要把 Beta 分支反向合并到 `main`;稳定版发布后删除它
43+
需要下一个 Beta 时,把修复合并到 `main` 后再次运行 **Publish npm**每次运行都会根据 Actions run ID 和提交生成新的不可变版本,不需要 Beta 分支,也不会消费 changeset
4744

4845
## 发布稳定版
4946

5047
1. 审核并合并自动生成的 `chore: release packages` PR。
51-
2. 打开 **Actions → Publish npm → Run workflow**
52-
3. 选择 `main`,channel 选择 `stable`,输入 `PUBLISH`,然后运行。
53-
4. 检查准确版本后,批准 `npm-release` Environment deployment。
48+
2. 合并提交到达 `main` 后,**Publish npm** 会自动启动。
49+
3. 检查准确版本后,批准 `npm-release` Environment deployment。
50+
51+
如果发布中途失败,可在 **Actions → Publish npm → Run workflow** 中选择 `main`、channel `stable`,输入 `PUBLISH` 手动重跑。已经成功发布的精确包版本会被跳过。
5452

5553
只有 `main` 上不含预发布后缀的 `X.Y.Z` 才能发布到 npm 的 `latest`。发布后会创建对应的不可变 Git tag,等待三个包在公共 registry 中全部可见,再执行六个消费者 job。正式 GitHub Release 是最后的发布认证,只有矩阵全部通过后才创建。发布过程可安全重试:已经存在的精确包版本会被跳过。
5654

@@ -92,8 +90,8 @@ npm install --global @openagentpack/cli
9290
# Beta CLI(npm beta)
9391
npm install --global @openagentpack/cli@beta
9492

95-
# 固定或临时体验某个精确版本
96-
npx @openagentpack/cli@0.1.0-beta.0 --version
93+
# 固定或临时体验某个精确 Beta 版本
94+
npx @openagentpack/cli@0.0.0-beta.run-123456789.sha-a1b2c3d --version
9795

9896
# SDK
9997
npm install @openagentpack/sdk
@@ -108,4 +106,4 @@ npm install @openagentpack/sdk
108106
- 所有包都已发布,但发布后消费者 job 失败:保留不可变 tag,不执行 unpublish、不移动 tag,也不创建 GitHub Release。修复兼容性问题后发布新的 patch 版本;npm 上的版本不能被覆盖。
109107
- registry 可见性会重试五分钟,之后才判定 release 失败。只有确认失败原因是 npm 同步延迟而不是包兼容性时,才从同一个提交重试。
110108
- 版本 tag 已指向其他提交:立即停止。tag 必须保持不可变,应排查历史,不能移动或删除 tag。
111-
- **Prepare Beta Release** 提示没有未发布 changeset:先在 `main` 添加 changeset,再准备下一个 Beta
109+
- Beta 发布必须从 `main` 手动触发;其他分支会被发布身份检查拒绝

scripts/open-source.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,9 +138,10 @@ describe("open-source repository invariants", () => {
138138
expect(unpinned).toEqual([]);
139139
});
140140

141-
test("npm publishing is manual, approval-gated, and cannot be cancelled mid-publish", () => {
141+
test("npm publishing auto-starts stable releases, remains approval-gated, and cannot be cancelled", () => {
142142
const workflow = readFileSync(resolve(root, ".github/workflows/release.yml"), "utf8");
143-
expect(workflow).not.toMatch(/\npush:/);
143+
expect(workflow).toMatch(/\n {2}push:\n {4}branches: \[main\]/);
144+
expect(workflow).toContain("contains(github.event.head_commit.message, 'chore: release packages')");
144145
expect(workflow).toContain("environment: npm-release");
145146
expect(workflow).toContain("vars.NPM_RELEASE_ENABLED == 'true'");
146147
expect(workflow).toContain("inputs.confirm == 'PUBLISH'");

scripts/release/channel.test.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,16 @@ describe("release channel guard", () => {
1212
expect(() => validateReleaseIdentity("stable", "main", "1.2.3-beta.0")).toThrow("X.Y.Z");
1313
});
1414

15-
test("binds beta versions to their isolated release branch", () => {
16-
expect(validateReleaseIdentity("beta", "release/1.2.3-beta", "1.2.3-beta.4", "1.2.3")).toEqual({
15+
test("accepts deterministic beta snapshots only on main", () => {
16+
expect(validateReleaseIdentity("beta", "main", "0.0.0-beta.run-123456789.sha-a1b2c3d")).toEqual({
1717
channel: "beta",
18-
version: "1.2.3-beta.4",
18+
version: "0.0.0-beta.run-123456789.sha-a1b2c3d",
1919
distTag: "beta",
2020
});
21-
expect(() => validateReleaseIdentity("beta", "main", "1.2.3-beta.0")).toThrow("release/X.Y.Z-beta");
22-
expect(() => validateReleaseIdentity("beta", "release/1.2.3-beta", "1.2.4-beta.0")).toThrow("1.2.3-beta.N");
23-
expect(() => validateReleaseIdentity("beta", "release/1.2.3-beta", "1.2.3-beta.0", "2.0.0")).toThrow(
24-
"does not match",
21+
expect(() => validateReleaseIdentity("beta", "feature/test", "0.0.0-beta.run-123456789.sha-a1b2c3d")).toThrow(
22+
"main",
2523
);
24+
expect(() => validateReleaseIdentity("beta", "main", "1.2.3-beta.0")).toThrow("unexpected format");
2625
});
2726

2827
test("requires all fixed-group package versions to match", () => {

0 commit comments

Comments
 (0)