Skip to content

Commit d82d1ef

Browse files
committed
feat: add approval-gated npm releases [policy]
Change-Id: Iae0db1a40fac3bbdaa4d1dfc2e0c41b0d05c4dba
1 parent 8924d8d commit d82d1ef

16 files changed

Lines changed: 511 additions & 179 deletions

File tree

.changeset/pre.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"mode": "pre",
2+
"mode": "exit",
33
"tag": "beta",
44
"initialVersions": {
55
"@openagentpack/server": "0.0.1",

.github/workflows/prepare-beta.yml

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
name: Prepare Beta Release
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
version:
7+
description: Stable SemVer series, for example 0.1.0
8+
required: true
9+
type: string
10+
11+
concurrency:
12+
group: prepare-beta-${{ inputs.version }}
13+
cancel-in-progress: false
14+
15+
jobs:
16+
prepare:
17+
if: github.ref_name == 'main'
18+
runs-on: ubuntu-latest
19+
permissions:
20+
contents: write
21+
steps:
22+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
23+
with:
24+
fetch-depth: 0
25+
26+
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
27+
with:
28+
bun-version: "1.3.5"
29+
30+
- name: Validate requested version
31+
env:
32+
VERSION: ${{ inputs.version }}
33+
run: |
34+
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
35+
echo "::error::version must be a stable SemVer such as 0.1.0"
36+
exit 1
37+
fi
38+
39+
- name: Prepare isolated beta branch
40+
env:
41+
VERSION: ${{ inputs.version }}
42+
run: |
43+
branch="release/${VERSION}-beta"
44+
git config user.name "github-actions[bot]"
45+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
46+
git fetch origin main
47+
if git ls-remote --exit-code --heads origin "${branch}" >/dev/null 2>&1; then
48+
git fetch origin "${branch}"
49+
git checkout --branch "${branch}" --track "origin/${branch}"
50+
git merge --no-edit origin/main
51+
else
52+
git checkout --branch "${branch}" origin/main
53+
bunx changeset pre enter beta
54+
fi
55+
56+
- name: Install dependencies
57+
run: bun install --frozen-lockfile
58+
59+
- name: Calculate next beta version
60+
run: bun run release:version
61+
62+
- name: Validate calculated beta version
63+
id: release
64+
run: >-
65+
bun run scripts/release/channel.ts validate
66+
--channel beta
67+
--ref "release/${{ inputs.version }}-beta"
68+
--expected "${{ inputs.version }}"
69+
--output "$GITHUB_OUTPUT"
70+
71+
- name: Commit and push beta branch
72+
env:
73+
VERSION: ${{ steps.release.outputs.version }}
74+
run: |
75+
if git diff --quiet && git diff --cached --quiet; then
76+
echo "::error::No unreleased changesets are available for the next beta."
77+
exit 1
78+
fi
79+
git add --all
80+
git commit --message "chore: prepare ${VERSION}"
81+
git push origin HEAD

.github/workflows/release-pr.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: Release PR
2+
3+
on:
4+
push:
5+
branches: [main]
6+
7+
concurrency:
8+
group: release-pr
9+
cancel-in-progress: false
10+
11+
jobs:
12+
version:
13+
runs-on: ubuntu-latest
14+
permissions:
15+
contents: write
16+
pull-requests: write
17+
steps:
18+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
19+
with:
20+
fetch-depth: 0
21+
22+
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
23+
with:
24+
bun-version: "1.3.5"
25+
26+
- name: Install dependencies
27+
run: bun install --frozen-lockfile
28+
29+
- name: Create or update stable Release PR
30+
uses: changesets/action@a45c4d594aa4e2c509dc14a9f2b3b67ba3780d0d # v1.9.0
31+
with:
32+
version: bun run release:version
33+
commit: "chore: release packages"
34+
title: "chore: release packages"
35+
env:
36+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.github/workflows/release.yml

Lines changed: 58 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,34 @@
1-
name: Release
1+
name: Publish npm
22

33
on:
44
workflow_dispatch:
5-
push:
6-
branches: [main]
5+
inputs:
6+
channel:
7+
description: npm channel to publish
8+
required: true
9+
type: choice
10+
options:
11+
- beta
12+
- stable
13+
confirm:
14+
description: Type PUBLISH to confirm
15+
required: true
16+
type: string
717

818
concurrency:
9-
group: ${{ github.workflow }}-${{ github.ref }}
1019
# Publishing three packages is not atomic. Never cancel a run after the first
11-
# package may already have reached npm; the publish script is also idempotent
12-
# so a failed run can safely be retried.
20+
# package may already have reached npm; exact versions are safe to retry.
21+
group: npm-publish
1322
cancel-in-progress: false
1423

1524
jobs:
16-
release:
17-
# Missing repository variables resolve to an empty string, so a newly
18-
# created repository cannot publish until maintainers explicitly opt in.
19-
if: vars.NPM_RELEASE_ENABLED == 'true'
25+
publish:
26+
if: vars.NPM_RELEASE_ENABLED == 'true' && inputs.confirm == 'PUBLISH'
2027
runs-on: ubuntu-latest
28+
environment: npm-release
2129
permissions:
2230
contents: write
2331
id-token: write
24-
pull-requests: write
2532
steps:
2633
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
2734
with:
@@ -43,22 +50,48 @@ jobs:
4350
- name: Install dependencies
4451
run: bun install --frozen-lockfile
4552

53+
- name: Validate channel, branch, and package versions
54+
id: release
55+
run: >-
56+
bun run scripts/release/channel.ts validate
57+
--channel "${{ inputs.channel }}"
58+
--ref "${{ github.ref_name }}"
59+
--output "$GITHUB_OUTPUT"
60+
4661
- name: Verify release
47-
env:
48-
BASE: ${{ github.event.before }}
4962
run: bun run verify:release
5063

51-
- name: Create Release PR or Publish
52-
uses: changesets/action@a45c4d594aa4e2c509dc14a9f2b3b67ba3780d0d # v1.9.0
53-
with:
54-
version: bun run release:version
55-
publish: bun run release:publish
56-
commit: "chore: release packages"
57-
title: "chore: release packages"
64+
- name: Publish packages with npm Trusted Publishing
5865
env:
59-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
60-
# Bootstrap only: set this secret for the first publication, then remove
61-
# and revoke it after npm Trusted Publishers are configured. With the
62-
# secret absent, npm authenticates through OIDC trusted publishing.
63-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
6466
NPM_CONFIG_PROVENANCE: "true"
67+
run: bun run release:publish
68+
69+
- name: Create immutable Git tag
70+
env:
71+
VERSION: ${{ steps.release.outputs.version }}
72+
run: |
73+
tag="v${VERSION}"
74+
if git rev-parse --verify --quiet "refs/tags/${tag}"; then
75+
test "$(git rev-list -n 1 "${tag}")" = "$GITHUB_SHA" || {
76+
echo "::error::${tag} already points to another commit"
77+
exit 1
78+
}
79+
else
80+
git tag --annotate "${tag}" --message "${tag}"
81+
git push origin "${tag}"
82+
fi
83+
84+
- name: Create GitHub Release
85+
env:
86+
GH_TOKEN: ${{ github.token }}
87+
VERSION: ${{ steps.release.outputs.version }}
88+
CHANNEL: ${{ steps.release.outputs.channel }}
89+
run: |
90+
tag="v${VERSION}"
91+
if gh release view "${tag}" >/dev/null 2>&1; then
92+
echo "GitHub Release ${tag} already exists; nothing to do."
93+
elif test "${CHANNEL}" = beta; then
94+
gh release create "${tag}" --verify-tag --generate-notes --prerelease --title "${tag}"
95+
else
96+
gh release create "${tag}" --verify-tag --generate-notes --title "${tag}"
97+
fi

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Thanks for your interest in improving OpenAgentPack. By participating you agree
88
|-------|-----|
99
| Dev setup, verification, package boundaries | [docs/contributing/development.md](./docs/contributing/development.md) |
1010
| Adding a new provider | [docs/contributing/provider-development.md](./docs/contributing/provider-development.md) |
11-
| npm release workflow | [docs/contributing/release.md](./docs/contributing/release.md) |
11+
| npm release workflow | [English](./docs/contributing/release.md) · [简体中文](./docs/contributing/release.zh-CN.md) |
1212
| Architecture and how it works | [docs/architecture/how-it-works.md](./docs/architecture/how-it-works.md) |
1313

1414
## Quick start

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ npm install -g @openagentpack/cli
136136
```
137137

138138
This provides the `agents` command. To run from source instead, see [Contributing](./CONTRIBUTING.md).
139+
Beta testers can install `@openagentpack/cli@beta`; see the [release guide](./docs/contributing/release.md#what-users-install) for version pinning and switching back to stable.
139140

140141
## Provider support
141142

README.zh-CN.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ npm install -g @openagentpack/cli
136136
```
137137

138138
安装后即可使用 `agents` 命令。若想从源码运行,见 [贡献指南](./CONTRIBUTING.md)。
139+
Beta 用户可以安装 `@openagentpack/cli@beta`;固定版本及切回稳定版的方法见 [发布指南](./docs/contributing/release.zh-CN.md#用户如何安装)。
139140

140141
## Provider 支持
141142

docs/contributing/release.md

Lines changed: 67 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,95 @@
1-
# Release
1+
# npm releases
22

3-
How `@openagentpack/sdk`, `@openagentpack/playground`, and `@openagentpack/cli` are published. This is a concise summary of the full procedure in [`packages/sdk/docs/release.md`](../../packages/sdk/docs/release.md).
3+
OpenAgentPack publishes `@openagentpack/sdk`, `@openagentpack/playground`, and `@openagentpack/cli` as one fixed version group. Real publishing happens only in GitHub Actions; local commands can build and dry-run the tarballs but cannot publish them.
44

5-
## Principles
5+
## One-time repository setup
66

7-
- npm packages publish **only** from GitHub Actions on `modelstudioai/OpenAgentPack`, via the `release.yml` workflow.
8-
- Publishing uses **npm Trusted Publishing** (OIDC) — no long-lived write `NPM_TOKEN` is kept.
9-
- Every release is gated by `bun run verify:release`, which runs the full check suite, package builds, `npm publish --dry-run`, and a packed-consumer smoke (importing every SDK entry point, running `agents --version`, and booting the Playground) on Node.js 20 and 24.
10-
- Packages publish in topological order: `sdk → playground → cli`.
7+
An organization owner must allow GitHub Actions to create pull requests in the organization Actions settings; then enable the matching option in **Repository Settings → Actions → General**. The Release PR workflow uses the repository `GITHUB_TOKEN`, not a personal token.
118

12-
## First-time bootstrap
9+
Create a GitHub Environment named `npm-release`. Add required reviewers so a maintainer must approve every npm publish job. Do not store an npm token in this environment. GitHub may expose reviewer protection only after the repository is public or on a qualifying private-repository plan.
1310

14-
npm only allows configuring a Trusted Publisher for a package that already exists. Before the first release:
11+
On npmjs.com, open the settings for each of the three packages and configure the same Trusted Publisher:
1512

16-
1. Create a short-lived granular npm token (allow creating public packages under `@openagentpack`) and save it as the GitHub Actions secret `NPM_TOKEN`.
17-
2. The repository Actions variable `NPM_RELEASE_ENABLED` is intentionally absent while open-source review is pending. After approval, set it to `true` and manually dispatch the Release workflow to publish the verified prereleases with the `beta` dist-tag.
18-
3. On each of the three npm packages, configure the Trusted Publisher: Provider `GitHub Actions`, org `modelstudioai`, repo `OpenAgentPack`, workflow `release.yml`, allowed action `npm publish`.
19-
4. Delete the `NPM_TOKEN` secret, revoke the bootstrap token, require 2FA, and disallow traditional token publishing on all three packages.
20-
5. Publish a subsequent beta through OIDC and verify the provenance link.
13+
| npm setting | Value |
14+
|---|---|
15+
| Provider | GitHub Actions |
16+
| Organization | `modelstudioai` |
17+
| Repository | `OpenAgentPack` |
18+
| Workflow filename | `release.yml` |
19+
| Environment | `npm-release` |
2120

22-
Requires npm CLI 11.5.1+ and Node.js 22.14+. The Release workflow uses Node.js 24, pins the npm CLI, and grants `id-token: write`.
21+
The workflow uses a GitHub-hosted runner, Node.js 24, npm 12, `id-token: write`, and provenance. No `NPM_TOKEN` or `NODE_AUTH_TOKEN` is required. The packages must already exist before npm lets you add a Trusted Publisher.
2322

24-
## Day-to-day releases
23+
Only after the Trusted Publishers and Environment reviewers are ready, create the repository Actions variable `NPM_RELEASE_ENABLED` with value `true`. Its absence is a kill switch: publish jobs are skipped even if somebody manually dispatches the workflow.
2524

26-
Create a changeset in a feature PR:
25+
## Changes required in every feature PR
26+
27+
Add a changeset whenever a user-visible package change is made:
2728

2829
```bash
2930
bun run changeset
3031
```
3132

32-
After merge to `main`, the Release workflow creates/updates a version PR, runs full verification, updates versions and changelogs, and publishes the three packages with OIDC and provenance. If a precise version already exists it is skipped, so a partially-failed release can be safely re-run.
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.
3334

34-
## Beta / pre-release
35+
## Publish a beta
3536

36-
```bash
37-
bunx changeset pre enter beta
38-
bun run changeset
39-
# ... merge version PR ...
40-
bunx changeset pre exit
41-
```
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.
4243

43-
Pre-release versions derive the npm dist-tag from the SemVer identifier (e.g. `1.0.1-beta.5``beta`), so `latest` is never occupied by a pre-release.
44+
The publish workflow validates that the selected branch, package version, and channel agree. It publishes with the npm `beta` dist-tag, creates an immutable `v0.1.0-beta.N` tag, and creates a GitHub prerelease.
4445

45-
## Local verification
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.
4647

47-
Local machines never perform a real publish — only verify what would be published:
48+
## Publish a stable release
49+
50+
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.
54+
55+
Only a clean `X.Y.Z` version on `main` can publish to npm's `latest` tag. A successful run creates the matching immutable Git tag and GitHub Release. Publishing is idempotent: packages whose exact version already exists are skipped, so a partially failed run can be retried.
56+
57+
## Local verification
4858

4959
```bash
5060
bun run verify:release
51-
# or check the packed packages directly:
61+
```
62+
63+
This runs the full repository checks, builds the packages, performs `npm publish --dry-run`, and installs the packed artifacts into clean Node.js 20 and 24 consumer projects. To inspect only the package tarballs:
64+
65+
```bash
5266
bun run build:packages
53-
bun run release:publish -- --dry-run --tag beta
67+
bun run release:publish -- --dry-run
5468
```
5569

56-
The publish script temporarily rewrites `./src/*.ts` exports to `./dist/*.js`, resolves `workspace:` dependencies to real versions, and drops the root Apache-2.0 `LICENSE` into each tarball — restoring the workspace in `finally`.
70+
The publish script blocks any real publish outside GitHub Actions.
71+
72+
## What users install
73+
74+
```bash
75+
# Stable CLI (npm latest)
76+
npm install --global @openagentpack/cli
77+
78+
# Beta CLI (npm beta)
79+
npm install --global @openagentpack/cli@beta
80+
81+
# Pin or test an exact version without a global install
82+
npx @openagentpack/cli@0.1.0-beta.0 --version
83+
84+
# SDK
85+
npm install @openagentpack/sdk
86+
```
5787

58-
## Troubleshooting
88+
After installing the CLI, run `agents --help`. A beta user returns to stable with `npm install --global @openagentpack/cli@latest`.
5989

60-
- `ENEEDAUTH` — for the first release, confirm the temporary `NPM_TOKEN` secret exists and allows creating a scoped public package. After bootstrap, confirm the Trusted Publisher org/repo/workflow match exactly and the workflow has `id-token: write`.
61-
- Incomplete package contents — run `bun run verify:release` and confirm the dry-run output includes `README.md`, `LICENSE`, `package.json`, and built `dist/`.
62-
- Leftover `package.json`/`LICENSE` changes — the publish script restores these on normal exit; if killed forcefully, check the workspace and remove only the temporary package-local `LICENSE`.
90+
## Recovery
6391

64-
See [`packages/sdk/docs/release.md`](../../packages/sdk/docs/release.md) for the full procedure and troubleshooting.
92+
- If npm authentication fails, compare the Trusted Publisher repository, workflow filename, and Environment character-for-character with the table above.
93+
- If a package published before another failed, rerun the same workflow from the same commit. Already-published exact versions are skipped.
94+
- If a version tag already points at another commit, stop. Tags are immutable; investigate the repository history instead of moving or deleting the tag.
95+
- If **Prepare Beta Release** reports no unreleased changesets, add a changeset on `main` before preparing another beta.

0 commit comments

Comments
 (0)