From 4d0c872e05a02ff18230880345b526fe13d26aaa Mon Sep 17 00:00:00 2001 From: heimanba <371510756@qq.com> Date: Mon, 20 Jul 2026 19:35:40 +0800 Subject: [PATCH 1/2] feat(release): simplify npm publishing Change-Id: Ie20ec6314bd82891009cf5b1ec7b2670fa45005d --- .github/workflows/prepare-beta.yml | 89 -------------------------- .github/workflows/release.yml | 21 +++++- docs/contributing/release.md | 26 ++++---- docs/contributing/release.zh-CN.md | 28 ++++---- scripts/open-source.test.ts | 5 +- scripts/release/channel.test.ts | 13 ++-- scripts/release/channel.ts | 26 ++------ scripts/release/consumer-smoke.test.ts | 2 + scripts/release/consumer-smoke.ts | 3 + scripts/release/publish.test.ts | 4 ++ scripts/release/publish.ts | 2 +- scripts/release/snapshot.test.ts | 13 ++++ scripts/release/snapshot.ts | 47 ++++++++++++++ 13 files changed, 127 insertions(+), 152 deletions(-) delete mode 100644 .github/workflows/prepare-beta.yml create mode 100644 scripts/release/snapshot.test.ts create mode 100644 scripts/release/snapshot.ts diff --git a/.github/workflows/prepare-beta.yml b/.github/workflows/prepare-beta.yml deleted file mode 100644 index 66970ee..0000000 --- a/.github/workflows/prepare-beta.yml +++ /dev/null @@ -1,89 +0,0 @@ -name: Prepare Beta Release - -on: - workflow_dispatch: - inputs: - version: - description: Stable SemVer series, for example 0.1.0 - required: true - type: string - -concurrency: - group: prepare-beta-${{ inputs.version }} - cancel-in-progress: false - -jobs: - prepare: - if: github.ref_name == 'main' - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 - with: - fetch-depth: 0 - - - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 - with: - bun-version: "1.3.5" - - - name: Validate requested version - env: - VERSION: ${{ inputs.version }} - run: | - if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "::error::version must be a stable SemVer such as 0.1.0" - exit 1 - fi - - - name: Prepare isolated beta branch - env: - VERSION: ${{ inputs.version }} - run: | - branch="release/${VERSION}-beta" - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git fetch origin main - if git ls-remote --exit-code --heads origin "${branch}" >/dev/null 2>&1; then - git fetch origin "${branch}" - git checkout -b "${branch}" --track "origin/${branch}" - git merge --no-edit origin/main - else - git checkout -b "${branch}" origin/main - fi - - - name: Install dependencies - run: bun install --frozen-lockfile - - - name: Enter prerelease mode for a new beta series - env: - VERSION: ${{ inputs.version }} - run: | - branch="release/${VERSION}-beta" - if ! git ls-remote --exit-code --heads origin "${branch}" >/dev/null 2>&1; then - bun run changeset -- pre enter beta - fi - - - name: Calculate next beta version - run: bun run release:version - - - name: Validate calculated beta version - id: release - run: >- - bun run scripts/release/channel.ts validate - --channel beta - --ref "release/${{ inputs.version }}-beta" - --expected "${{ inputs.version }}" - --output "$GITHUB_OUTPUT" - - - name: Commit and push beta branch - env: - VERSION: ${{ steps.release.outputs.version }} - run: | - if git diff --quiet && git diff --cached --quiet; then - echo "::error::No unreleased changesets are available for the next beta." - exit 1 - fi - git add --all - git commit --message "chore: prepare ${VERSION}" - git push origin HEAD diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 677eb17..1d19d05 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,6 +1,8 @@ name: Publish npm on: + push: + branches: [main] workflow_dispatch: inputs: channel: @@ -11,7 +13,7 @@ on: - beta - stable confirm: - description: Type PUBLISH to confirm + description: Type PUBLISH to confirm a manual beta or stable retry required: true type: string @@ -23,7 +25,10 @@ concurrency: jobs: preflight: - if: vars.NPM_RELEASE_ENABLED == 'true' && inputs.confirm == 'PUBLISH' + if: >- + vars.NPM_RELEASE_ENABLED == 'true' && + ((github.event_name == 'push' && contains(github.event.head_commit.message, 'chore: release packages')) || + (github.event_name == 'workflow_dispatch' && inputs.confirm == 'PUBLISH')) runs-on: ubuntu-latest outputs: channel: ${{ steps.release.outputs.channel }} @@ -53,11 +58,18 @@ jobs: - name: Install dependencies run: bun install --frozen-lockfile + - name: Prepare immutable beta snapshot version + if: github.event_name == 'workflow_dispatch' && inputs.channel == 'beta' + run: >- + bun run scripts/release/snapshot.ts apply + --run-id "${{ github.run_id }}" + --sha "${{ github.sha }}" + - name: Validate channel, branch, and package versions id: release run: >- bun run scripts/release/channel.ts validate - --channel "${{ inputs.channel }}" + --channel "${{ github.event_name == 'push' && 'stable' || inputs.channel }}" --ref "${{ github.ref_name }}" --output "$GITHUB_OUTPUT" @@ -94,6 +106,9 @@ jobs: - name: Build publishable packages run: | bun install --frozen-lockfile + if [ "${{ needs.preflight.outputs.channel }}" = beta ]; then + bun run scripts/release/snapshot.ts apply --version "${{ needs.preflight.outputs.version }}" + fi bun run build:packages - name: Publish packages with npm Trusted Publishing diff --git a/docs/contributing/release.md b/docs/contributing/release.md index a6b5372..0501a54 100644 --- a/docs/contributing/release.md +++ b/docs/contributing/release.md @@ -30,27 +30,25 @@ Add a changeset whenever a user-visible package change is made: bun run changeset ``` -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. +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. ## Publish a beta -1. In GitHub, open **Actions → Prepare Beta Release → Run workflow**. -2. Keep the workflow branch set to `main`, enter the target stable series (for example `0.1.0`), and run it. -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`. -4. Open **Actions → Publish npm → Run workflow**. -5. Select `release/0.1.0-beta` in the workflow branch dropdown, choose `beta`, type `PUBLISH`, and run it. -6. Approve the `npm-release` Environment deployment after reviewing the commit and job summary. +1. In GitHub, open **Actions → Publish npm → Run workflow**. +2. Keep the workflow branch set to `main`, choose `beta`, type `PUBLISH`, and run it. +3. Approve the `npm-release` Environment deployment after reviewing the commit, generated version, and job summary. -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. +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. -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. +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. ## Publish a stable release 1. Review and merge the automated `chore: release packages` PR. -2. Open **Actions → Publish npm → Run workflow**. -3. Select `main`, choose `stable`, type `PUBLISH`, and run it. -4. Approve the `npm-release` Environment deployment after reviewing the exact package version. +2. **Publish npm** starts automatically when the release commit reaches `main`. +3. Approve the `npm-release` Environment deployment after reviewing the exact package version. + +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. 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. @@ -93,7 +91,7 @@ npm install --global @openagentpack/cli npm install --global @openagentpack/cli@beta # Pin or test an exact version without a global install -npx @openagentpack/cli@0.1.0-beta.0 --version +npx @openagentpack/cli@0.0.0-beta.run-123456789.sha-a1b2c3d --version # SDK npm install @openagentpack/sdk @@ -108,4 +106,4 @@ After installing the CLI, run `agents --help`. A beta user returns to stable wit - 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. - 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. - If a version tag already points at another commit, stop. Tags are immutable; investigate the repository history instead of moving or deleting the tag. -- If **Prepare Beta Release** reports no unreleased changesets, add a changeset on `main` before preparing another beta. +- Beta publishing must be manually dispatched from `main`; the release identity check rejects other branches. diff --git a/docs/contributing/release.zh-CN.md b/docs/contributing/release.zh-CN.md index cce4447..7919859 100644 --- a/docs/contributing/release.zh-CN.md +++ b/docs/contributing/release.zh-CN.md @@ -30,27 +30,25 @@ OpenAgentPack 将 `@openagentpack/sdk`、`@openagentpack/playground` 和 `@opena bun run changeset ``` -选择 SemVer 影响范围并填写 changelog。PR 合并后,**Release PR** 工作流会在 `main` 上创建或更新稳定版版本 PR,但绝不会直接发布 npm 包。 +选择 SemVer 影响范围并填写 changelog。PR 合并后,**Release PR** 工作流会在 `main` 上创建或更新稳定版版本 PR。只有合并该 Release PR 才会自动启动正式发布,并且发布仍需 `npm-release` Environment 审批。 ## 发布 Beta -1. 打开 GitHub 的 **Actions → Prepare Beta Release → Run workflow**。 -2. workflow branch 保持 `main`,输入目标稳定版系列,例如 `0.1.0`,然后运行。 -3. 工作流会创建或更新 `release/0.1.0-beta`,消费现有 changeset,并提交下一个版本,例如 `0.1.0-beta.0`。 -4. 打开 **Actions → Publish npm → Run workflow**。 -5. 在 workflow branch 下拉框选择 `release/0.1.0-beta`,channel 选择 `beta`,输入 `PUBLISH`,然后运行。 -6. 检查提交和 job 信息后,批准 `npm-release` Environment deployment。 +1. 打开 GitHub 的 **Actions → Publish npm → Run workflow**。 +2. workflow branch 保持 `main`,channel 选择 `beta`,输入 `PUBLISH`,然后运行。 +3. 检查提交、自动生成的精确版本和 job 信息后,批准 `npm-release` Environment deployment。 -发布工作流会同时校验分支、包版本和 channel。通过后使用 npm 的 `beta` dist-tag 发布,并创建不可变的 `v0.1.0-beta.N` Git tag。随后,它会在 Linux、Windows、macOS 的 Node.js 22 和 24 环境中,从公共 npm registry 安装该精确版本。只有六个消费者 job 全部通过,才创建 GitHub prerelease。 +工作流根据 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。 -需要下一个 Beta 时,把修复及其 changeset 合并到 `main`,再对同一个版本系列运行 **Prepare Beta Release**。工作流会把 `main` 合入 Beta 分支并计算下一个 Beta。不要把 Beta 分支反向合并到 `main`;稳定版发布后删除它。 +需要下一个 Beta 时,把修复合并到 `main` 后再次运行 **Publish npm**。每次运行都会根据 Actions run ID 和提交生成新的不可变版本,不需要 Beta 分支,也不会消费 changeset。 ## 发布稳定版 1. 审核并合并自动生成的 `chore: release packages` PR。 -2. 打开 **Actions → Publish npm → Run workflow**。 -3. 选择 `main`,channel 选择 `stable`,输入 `PUBLISH`,然后运行。 -4. 检查准确版本后,批准 `npm-release` Environment deployment。 +2. 合并提交到达 `main` 后,**Publish npm** 会自动启动。 +3. 检查准确版本后,批准 `npm-release` Environment deployment。 + +如果发布中途失败,可在 **Actions → Publish npm → Run workflow** 中选择 `main`、channel `stable`,输入 `PUBLISH` 手动重跑。已经成功发布的精确包版本会被跳过。 只有 `main` 上不含预发布后缀的 `X.Y.Z` 才能发布到 npm 的 `latest`。发布后会创建对应的不可变 Git tag,等待三个包在公共 registry 中全部可见,再执行六个消费者 job。正式 GitHub Release 是最后的发布认证,只有矩阵全部通过后才创建。发布过程可安全重试:已经存在的精确包版本会被跳过。 @@ -92,8 +90,8 @@ npm install --global @openagentpack/cli # Beta CLI(npm beta) npm install --global @openagentpack/cli@beta -# 固定或临时体验某个精确版本 -npx @openagentpack/cli@0.1.0-beta.0 --version +# 固定或临时体验某个精确 Beta 版本 +npx @openagentpack/cli@0.0.0-beta.run-123456789.sha-a1b2c3d --version # SDK npm install @openagentpack/sdk @@ -108,4 +106,4 @@ npm install @openagentpack/sdk - 所有包都已发布,但发布后消费者 job 失败:保留不可变 tag,不执行 unpublish、不移动 tag,也不创建 GitHub Release。修复兼容性问题后发布新的 patch 版本;npm 上的版本不能被覆盖。 - registry 可见性会重试五分钟,之后才判定 release 失败。只有确认失败原因是 npm 同步延迟而不是包兼容性时,才从同一个提交重试。 - 版本 tag 已指向其他提交:立即停止。tag 必须保持不可变,应排查历史,不能移动或删除 tag。 -- **Prepare Beta Release** 提示没有未发布 changeset:先在 `main` 添加 changeset,再准备下一个 Beta。 +- Beta 发布必须从 `main` 手动触发;其他分支会被发布身份检查拒绝。 diff --git a/scripts/open-source.test.ts b/scripts/open-source.test.ts index feb9781..3cabe60 100644 --- a/scripts/open-source.test.ts +++ b/scripts/open-source.test.ts @@ -138,9 +138,10 @@ describe("open-source repository invariants", () => { expect(unpinned).toEqual([]); }); - test("npm publishing is manual, approval-gated, and cannot be cancelled mid-publish", () => { + test("npm publishing auto-starts stable releases, remains approval-gated, and cannot be cancelled", () => { const workflow = readFileSync(resolve(root, ".github/workflows/release.yml"), "utf8"); - expect(workflow).not.toMatch(/\npush:/); + expect(workflow).toMatch(/\n {2}push:\n {4}branches: \[main\]/); + expect(workflow).toContain("contains(github.event.head_commit.message, 'chore: release packages')"); expect(workflow).toContain("environment: npm-release"); expect(workflow).toContain("vars.NPM_RELEASE_ENABLED == 'true'"); expect(workflow).toContain("inputs.confirm == 'PUBLISH'"); diff --git a/scripts/release/channel.test.ts b/scripts/release/channel.test.ts index 19d6634..f96fd3f 100644 --- a/scripts/release/channel.test.ts +++ b/scripts/release/channel.test.ts @@ -12,17 +12,16 @@ describe("release channel guard", () => { expect(() => validateReleaseIdentity("stable", "main", "1.2.3-beta.0")).toThrow("X.Y.Z"); }); - test("binds beta versions to their isolated release branch", () => { - expect(validateReleaseIdentity("beta", "release/1.2.3-beta", "1.2.3-beta.4", "1.2.3")).toEqual({ + test("accepts deterministic beta snapshots only on main", () => { + expect(validateReleaseIdentity("beta", "main", "0.0.0-beta.run-123456789.sha-a1b2c3d")).toEqual({ channel: "beta", - version: "1.2.3-beta.4", + version: "0.0.0-beta.run-123456789.sha-a1b2c3d", distTag: "beta", }); - expect(() => validateReleaseIdentity("beta", "main", "1.2.3-beta.0")).toThrow("release/X.Y.Z-beta"); - expect(() => validateReleaseIdentity("beta", "release/1.2.3-beta", "1.2.4-beta.0")).toThrow("1.2.3-beta.N"); - expect(() => validateReleaseIdentity("beta", "release/1.2.3-beta", "1.2.3-beta.0", "2.0.0")).toThrow( - "does not match", + expect(() => validateReleaseIdentity("beta", "feature/test", "0.0.0-beta.run-123456789.sha-a1b2c3d")).toThrow( + "main", ); + expect(() => validateReleaseIdentity("beta", "main", "1.2.3-beta.0")).toThrow("unexpected format"); }); test("requires all fixed-group package versions to match", () => { diff --git a/scripts/release/channel.ts b/scripts/release/channel.ts index f79a1d2..899362b 100644 --- a/scripts/release/channel.ts +++ b/scripts/release/channel.ts @@ -30,27 +30,16 @@ export function commonReleaseVersion(versions: readonly string[]): string { return unique[0]; } -export function validateReleaseIdentity( - channel: ReleaseChannel, - ref: string, - version: string, - expectedBase?: string, -): ReleaseIdentity { +export function validateReleaseIdentity(channel: ReleaseChannel, ref: string, version: string): ReleaseIdentity { if (channel === "stable") { if (ref !== "main") throw new Error(`stable releases must run from main, not ${ref}`); if (!stableVersion.test(version)) throw new Error(`stable release version must be X.Y.Z; found ${version}`); return { channel, version, distTag: "latest" }; } - const branch = /^release\/([0-9]+\.[0-9]+\.[0-9]+)-beta$/.exec(ref); - if (!branch) throw new Error(`beta releases must run from release/X.Y.Z-beta, not ${ref}`); - const base = branch[1]; - if (expectedBase && base !== expectedBase) { - throw new Error(`beta branch series ${base} does not match requested ${expectedBase}`); - } - const betaVersion = /^([0-9]+\.[0-9]+\.[0-9]+)-beta\.[0-9]+$/.exec(version); - if (!betaVersion || betaVersion[1] !== base) { - throw new Error(`beta package version must match ${base}-beta.N; found ${version}`); + if (ref !== "main") throw new Error(`beta snapshots must run from main, not ${ref}`); + if (!/^0\.0\.0-beta\.run-[1-9]\d*\.sha-[0-9a-f]{7}$/.test(version)) { + throw new Error(`beta snapshot version has an unexpected format: ${version}`); } return { channel, version, distTag: "beta" }; } @@ -67,12 +56,7 @@ function main(): void { const ref = option("ref"); if (channel !== "beta" && channel !== "stable") throw new Error("--channel must be beta or stable"); if (!ref) throw new Error("--ref is required"); - const identity = validateReleaseIdentity( - channel, - ref, - commonReleaseVersion(releasePackageVersions()), - option("expected"), - ); + const identity = validateReleaseIdentity(channel, ref, commonReleaseVersion(releasePackageVersions())); const output = option("output"); if (output) { appendFileSync(output, `channel=${identity.channel}\nversion=${identity.version}\ndist-tag=${identity.distTag}\n`); diff --git a/scripts/release/consumer-smoke.test.ts b/scripts/release/consumer-smoke.test.ts index 77fcbf5..b6038ad 100644 --- a/scripts/release/consumer-smoke.test.ts +++ b/scripts/release/consumer-smoke.test.ts @@ -8,9 +8,11 @@ describe("published consumer smoke", () => { test("reads an exact registry version", () => { expect(registryVersion('"1.2.3"', "@openagentpack/sdk")).toBe("1.2.3"); + expect(registryVersion('["1.2.3"]', "@openagentpack/sdk")).toBe("1.2.3"); const invalidVersion = () => registryVersion("[]", "@openagentpack/sdk"); expect(invalidVersion).toThrow("invalid version for @openagentpack/sdk"); expect(invalidVersion).toThrow("received: []"); + expect(() => registryVersion('["1.2.3","1.2.4"]', "@openagentpack/sdk")).toThrow("invalid version"); }); test("requires the fixed release group to resolve one version", () => { diff --git a/scripts/release/consumer-smoke.ts b/scripts/release/consumer-smoke.ts index 49ec94a..ade4d79 100644 --- a/scripts/release/consumer-smoke.ts +++ b/scripts/release/consumer-smoke.ts @@ -22,6 +22,9 @@ export function registryVersion(raw: string, name: string): string { try { const parsed: unknown = JSON.parse(raw); if (typeof parsed === "string" && parsed.length > 0) return parsed; + if (Array.isArray(parsed) && parsed.length === 1 && typeof parsed[0] === "string" && parsed[0].length > 0) { + return parsed[0]; + } } catch { // Fall through to the stable error below. } diff --git a/scripts/release/publish.test.ts b/scripts/release/publish.test.ts index 4b7b9b2..1765842 100644 --- a/scripts/release/publish.test.ts +++ b/scripts/release/publish.test.ts @@ -55,6 +55,10 @@ describe("release publish recovery", () => { expect(() => assertPublishEnvironment(false, {})).toThrow("GitHub Actions"); expect(() => assertPublishEnvironment(false, { ...workflow, GITHUB_WORKFLOW: "CI" })).toThrow("GitHub Actions"); expect(() => assertPublishEnvironment(false, workflow)).not.toThrow(); + expect(() => assertPublishEnvironment(false, { ...workflow, GITHUB_EVENT_NAME: "push" })).not.toThrow(); + expect(() => assertPublishEnvironment(false, { ...workflow, GITHUB_EVENT_NAME: "pull_request" })).toThrow( + "GitHub Actions", + ); expect(() => assertPublishEnvironment(true, {})).not.toThrow(); }); }); diff --git a/scripts/release/publish.ts b/scripts/release/publish.ts index bab95ef..812e61c 100644 --- a/scripts/release/publish.ts +++ b/scripts/release/publish.ts @@ -46,7 +46,7 @@ export function assertPublishEnvironment( !dryRun && (environment.GITHUB_ACTIONS !== "true" || environment.GITHUB_WORKFLOW !== "Publish npm" || - environment.GITHUB_EVENT_NAME !== "workflow_dispatch") + !["workflow_dispatch", "push"].includes(environment.GITHUB_EVENT_NAME ?? "")) ) { throw new Error("Real npm publishing is allowed only from the GitHub Actions Publish npm workflow."); } diff --git a/scripts/release/snapshot.test.ts b/scripts/release/snapshot.test.ts new file mode 100644 index 0000000..e6a6544 --- /dev/null +++ b/scripts/release/snapshot.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, test } from "bun:test"; +import { snapshotVersion } from "./snapshot.ts"; + +describe("beta snapshot version", () => { + test("is deterministic and valid SemVer", () => { + expect(snapshotVersion("123456789", "A1B2C3D99887766")).toBe("0.0.0-beta.run-123456789.sha-a1b2c3d"); + }); + + test("rejects ambiguous inputs", () => { + expect(() => snapshotVersion("0", "a1b2c3d")).toThrow("run ID"); + expect(() => snapshotVersion("123456789", "not-a-sha")).toThrow("sha"); + }); +}); diff --git a/scripts/release/snapshot.ts b/scripts/release/snapshot.ts new file mode 100644 index 0000000..f0579b4 --- /dev/null +++ b/scripts/release/snapshot.ts @@ -0,0 +1,47 @@ +import { appendFileSync, readFileSync, writeFileSync } from "node:fs"; +import { resolve } from "node:path"; + +const root = resolve(import.meta.dirname, "../.."); +const releasePackages = ["sdk", "playground", "cli"] as const; + +export function snapshotVersion(runId: string, sha: string): string { + if (!/^[1-9]\d*$/.test(runId)) throw new Error("run ID must be a positive integer"); + if (!/^[0-9a-f]{7,40}$/i.test(sha)) throw new Error("sha must contain 7 to 40 hexadecimal characters"); + return `0.0.0-beta.run-${runId}.sha-${sha.slice(0, 7).toLowerCase()}`; +} + +export function applySnapshotVersion(version: string): void { + for (const pkg of releasePackages) { + const path = resolve(root, "packages", pkg, "package.json"); + const manifest = JSON.parse(readFileSync(path, "utf8")) as Record; + manifest.version = version; + writeFileSync(path, `${JSON.stringify(manifest, null, "\t")}\n`); + } +} + +function option(name: string): string | undefined { + const index = process.argv.indexOf(`--${name}`); + return index === -1 ? undefined : process.argv[index + 1]; +} + +function main(): void { + if (process.argv[2] !== "apply") { + throw new Error( + "usage: snapshot.ts apply (--version | --run-id --sha )", + ); + } + const suppliedVersion = option("version"); + const runId = option("run-id"); + const sha = option("sha"); + const version = suppliedVersion ?? (runId && sha ? snapshotVersion(runId, sha) : undefined); + if (!version) throw new Error("provide --version or both --run-id and --sha"); + if (!/^0\.0\.0-beta\.run-[1-9]\d*\.sha-[0-9a-f]{7}$/.test(version)) { + throw new Error(`invalid beta snapshot version: ${version}`); + } + applySnapshotVersion(version); + const output = option("output"); + if (output) appendFileSync(output, `version=${version}\n`); + console.log(`Prepared beta snapshot ${version}.`); +} + +if (import.meta.main) main(); From 6a2c07b8a49d45565e1a816755e5526237c01394 Mon Sep 17 00:00:00 2001 From: heimanba <371510756@qq.com> Date: Mon, 20 Jul 2026 19:43:03 +0800 Subject: [PATCH 2/2] ci: remove maintainer evidence gate Change-Id: If1bffe80c7c7d8e9bca1035cc7a32e9e975b5acf --- .github/workflows/ci.yml | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 520b9f8..38a20d1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,23 +51,6 @@ jobs: BASE: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }} run: bun scripts/verify.ts full --step "${{ matrix.step }}" - maintainer-evidence: - name: Maintainer evidence - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 - with: - fetch-depth: 0 - - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 - with: - bun-version: "1.3.5" - - name: Require evidence only for high-risk changes - env: - BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }} - HEAD_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} - PR_BODY: ${{ github.event.pull_request.body }} - run: bun scripts/pr-evidence.ts - package-compatibility: name: Package compatibility / Node ${{ matrix.node }} runs-on: ubuntu-latest @@ -91,14 +74,13 @@ jobs: gate: name: Gate runs-on: ubuntu-latest - needs: [prepare-verification, verify, maintainer-evidence, package-compatibility] + needs: [prepare-verification, verify, package-compatibility] if: always() steps: - name: Check results run: | if [ "${{ needs.prepare-verification.result }}" != "success" ] || \ [ "${{ needs.verify.result }}" != "success" ] || \ - [ "${{ needs.maintainer-evidence.result }}" != "success" ] || \ [ "${{ needs.package-compatibility.result }}" != "success" ]; then echo "One or more required checks failed." exit 1