Skip to content

ci: 发布前统一校验版本号,四处版本只用脚本改 - #36

Merged
HarlonWang merged 2 commits into
mainfrom
fix/release-version-preflight
Oct 18, 2026
Merged

HarlonWang merged 2 commits into
mainfrom
fix/release-version-preflight

Conversation

@HarlonWang

@HarlonWang HarlonWang commented Oct 18, 2026 •

Copy link
Copy Markdown
Member

背景

0.11.0 的 tag 漏升 JS 包版本:npm job 的「tag 必须等于包版本」拦住了 npm,但 Maven job 并行、不做这个检查,已经发出。那个产物坐标 0.11.0、内嵌 TinyUI.version 却是 0.10.1,自相矛盾且 Maven Central 不可撤回(已用 0.11.1 补齐,宿主一律指向 0.11.1)。

版本号的设计本身是对的:packages/core/package.json 是真源,Kotlin 的 TinyUI.version 与页面包的 tinyui 下限都从它来,源码构建与产物一致(8c61063)。缺的是:校验只在一个 job 里、事后才跑;四处版本靠手改、PR 期不校验。

改动

  • publish.yml:新增 versions 前置 job,核对三个 package.json 与 core 的 VERSION 导出彼此一致、且等于 tag;maven 与 npm 都 needs 它,任一不符整次发布不出门。workflow_dispatch 时也跑,只校验彼此一致
  • build.yml:pnpm job 加 pnpm version:check,PR 期就发现不一致
  • packages/cli/scripts/version.ts:pnpm version:set <x.y.z> 一次改四处,pnpm version:check [expected] 校验
  • 删掉 tinyui-native 的 VERSION 常量:无文档、无消费方,纯粹多一处要改。core 的 VERSION 是 runtime-api.md 的公开导出,保留并由脚本维护
  • README 补「发布」步骤

验证

本地:check 四处一致通过;check 0.12.0 正确列出四处不符并退出 1;set 0.0.0-test 后 check 通过,set 0.11.1 还原后工作树无差异。合入后手动跑一次 publish.yml(dry-run)验证 versions job 接线。

🤖 Generated with Claude Code

Summary by Sourcery

Centralize version updates and enforce consistency checks throughout pull requests and publishing workflows.

New Features:

  • Add version management commands to synchronize all package and runtime version sources and regenerate embedded runtime bytecode.
  • Add a pre-publish workflow gate that verifies version consistency and tag alignment before Maven or npm publishing.

Bug Fixes:

  • Prevent releases with mismatched package, exported, generated, or Git tag versions.

Enhancements:

  • Run version consistency checks during pull request builds.
  • Document the unified versioning and publishing process.

CI:

  • Centralize release validation in a prerequisite workflow job shared by Maven and npm publishing.

Documentation:

  • Add release instructions covering version updates, tag creation, validation, and dry-run publishing.

0.11.0 的 tag 漏升 JS 包版本:npm job 的检查拦住了 npm,Maven 却已发出,
产物坐标 0.11.0、内嵌 TinyUI.version 0.10.1,自相矛盾且不可撤回。

- publish.yml 加 versions 前置 job,核对三个 package.json 与 core 的 VERSION 导出
  彼此一致、且等于 tag;maven 与 npm 都 needs 它,任一不符整次发布不出门
- build.yml 的 pnpm job 加 version:check,PR 期就发现四处不一致
- packages/cli/scripts/version.ts:version:set 一次改四处,version:check 校验
- 删掉 tinyui-native 的 VERSION 常量:无文档、无消费方,纯粹多一处要改
- README 补「发布」步骤与 dry-run 用法

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

本 PR 将版本号管理从人工同步和发布后检查改为脚本统一写入、构建期校验及发布前置拦截:四处版本镜像由 version:set 维护,version:check 在 PR 和 publish.yml 的 versions job 中执行,并阻止版本不一致时向 Maven Central 或 npm 发布。

Flow diagram for unified version validation and publishing

flowchart TD
    A["pnpm version:set <x.y.z>"] --> B["Update four version sites"]
    B --> C["PR build: pnpm version:check"]
    C -->|pass| D["Push matching git tag"]
    D --> E["publish.yml: versions job"]
    E -->|pass| F["Maven publish"]
    E -->|pass| G["npm publish"]
    E -->|mismatch| H["Block both publishes"]
Loading

File-Level Changes

Change Details Files
将版本一致性校验前移到发布前置阶段,并覆盖 PR、tag 发布和手动触发流程。
  • 新增 versions job 校验三个 package.json 与 core 的 VERSION;tag 发布时额外校验 tag 版本。
  • 让 Maven 和 npm 发布 job 依赖 versions,移除 npm 内重复的 tag 校验。
  • 在常规构建流程中执行 version:check,确保 PR 阶段暴露版本漂移。
.github/workflows/publish.yml
.github/workflows/build.yml
提供统一维护四处版本镜像的脚本,并将其接入 pnpm 命令。
  • 新增 version:set,一次更新 core、native、cli 的 package.json 及 core 的 VERSION 导出。
  • 新增 version:check,支持校验当前镜像一致性或与指定版本一致,并以非零状态报告不一致。
  • 在根 package.json 暴露 version:set 和 version:check 命令。
packages/cli/scripts/version.ts
package.json
移除无实际消费方的 native 版本常量,明确 core 版本为公开真源及唯一维护入口。
  • 删除 tinyui-native 的 VERSION 导出,减少手工维护点。
  • 补充发布文档,说明版本修改、PR 校验、tag 发布和手动 dry-run 流程。
packages/native/src/index.ts
README.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 8a9a4594-8db3-4e2e-b931-040ebc00858c
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codeseerbot codeseerbot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CodeSeer 审查

结论:批准(附 1 条建议)

摘要

本次改动将版本一致性检查前置到构建与发布流程,并通过脚本集中更新三个包的版本和 core 的 VERSION。发布任务现在会在 Maven 与 npm 之前执行统一校验,整体降低了两种产物版本不一致的风险。主要剩余风险是脚本对版本格式的校验不是严格的 SemVer,仍可能让无效版本通过前置检查并触发部分发布。

整个 PR 至 689cce6 · gpt-5.6-terra

{ file: "packages/cli/package.json", pattern: /("version":\s*")([^"]+)(")/ },
{ file: "packages/core/src/version.ts", pattern: /(export const VERSION = ")([^"]+)(")/ },
];
const VERSION = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[建议] VERSION 不是严格的 SemVer 校验,例如 01.2.3、1.2.3-a..b 都会通过,但它们不是合法的 npm 包版本。这样四处内容和 tag 即使一致也会通过 versions job,随后 Maven 与 npm 并行发布时,npm 可能因非法版本拒绝而 Maven 已经发布,重新引入部分发布的风险。建议使用严格的 SemVer 校验(特别是禁止主版本前导零、空 prerelease identifier 及 prerelease 数字前导零),或在不安装依赖的前提下实现等价校验。

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 5 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path=".github/workflows/publish.yml" line_range="21" />
<code_context>
+      - uses: actions/setup-node@v5
+        with:
+          node-version: 22
+      - run: node packages/cli/scripts/version.ts check "${{ github.event_name == 'push' && github.ref_name || '' }}"
+
   maven:
</code_context>
<issue_to_address>
**Tags execute commands in CI**

When a matching pushed tag contains shell syntax such as command substitution, the interpolated `github.ref_name` is parsed as part of the `run` script, so commands in the tag execute in the `versions` job before the checker runs. The job has checkout credentials available, and a crafted tag can still let the release jobs proceed.

Pass the tag through an environment variable and quote the variable in the shell command instead of interpolating the tag into the `run` script.
</issue_to_address>

### Comment 2
<location path="packages/native/src/index.ts" line_range="1" />
<code_context>
-export const VERSION = "0.11.1";
 export * as navigation from "./navigation.ts";
 export * as store from "./store.ts";
</code_context>
<issue_to_address>
**Native VERSION imports fail**

When the native test suite runs or a consumer imports `VERSION` from `tinyui-native`, the removed export makes the native version test fail during module loading, blocking `pnpm test`, builds, and npm publishing; consumers also lose the documented public API without a package-version bump.

Restore or re-export `VERSION` from the native entry point.
</issue_to_address>

### Comment 3
<location path="packages/cli/scripts/version.ts" line_range="14" />
<code_context>
+    { file: "packages/cli/package.json", pattern: /("version":\s*")([^"]+)(")/ },
+    { file: "packages/core/src/version.ts", pattern: /(export const VERSION = ")([^"]+)(")/ },
+];
+const VERSION = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/;
+
+const [mode, arg] = process.argv.slice(2);
</code_context>
<issue_to_address>
**Invalid versions reach release jobs**

When a release version matches this regex but violates SemVer, such as a leading-zero component or malformed prerelease, `VERSION.test` accepts the invalid value, so `set` and `check` pass it. Maven can publish a coordinate that npm rejects, and Kotlin's `SemVer.parse` rejects it in compatibility checks, leaving a partial or unusable release.

Validate versions with a strict SemVer parser before allowing `set` or `check` to succeed.
</issue_to_address>

### Comment 4
<location path="packages/cli/scripts/version.ts" line_range="18-22" />
<code_context>
+
+const [mode, arg] = process.argv.slice(2);
+if (mode === "set" && arg && VERSION.test(arg)) {
+    for (const { file, pattern } of SITES) {
+        const path = join(root, file);
+        const text = await readFile(path, "utf8");
+        if (!pattern.test(text)) throw new Error(`${file}: no version found`);
+        await writeFile(path, text.replace(pattern, `$1${arg}$3`));
+    }
+    process.stdout.write(`${arg} written to ${SITES.length} files\n`);
</code_context>
<issue_to_address>
**Failed setter leaves versions mixed**

When a later target file is missing, unreadable, does not match its pattern, or cannot be written, the loop writes earlier sites before a later read, pattern check, or write fails, then exits with an inconsistent worktree that `version:check` rejects.

Prepare and validate every update before writing, then apply the changes with rollback or another all-or-nothing update strategy.
</issue_to_address>

### Comment 5
<location path="packages/cli/scripts/version.ts" line_range="18-23" />
<code_context>
+
+const [mode, arg] = process.argv.slice(2);
+if (mode === "set" && arg && VERSION.test(arg)) {
+    for (const { file, pattern } of SITES) {
+        const path = join(root, file);
+        const text = await readFile(path, "utf8");
+        if (!pattern.test(text)) throw new Error(`${file}: no version found`);
+        await writeFile(path, text.replace(pattern, `$1${arg}$3`));
+    }
+    process.stdout.write(`${arg} written to ${SITES.length} files\n`);
+} else if (mode === "check" && (arg === undefined || arg === "")) {
</code_context>
<issue_to_address>
**Concurrent updates mix versions**

When two `version:set` commands for different versions run at the same time in one worktree, two `version:set` processes for different versions write the `SITES` files independently without a lock, so their writes can interleave and leave a mixed-version worktree that `version:check` rejects.

Serialize `version:set` invocations with a lock, or make updates derive from a single source of truth.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 5 findings to address first, and a tag name is interpolated directly into a quoted shell command, so a crafted tag could break out of the quotes and execute commands in the versions job. Reverting removes the injection point, but it cannot undo any runner data or credentials accessed before the revert.

Blocking findings: .github/workflows/publish.yml:21, packages/native/src/index.ts:1, packages/cli/scripts/version.ts:14, packages/cli/scripts/version.ts:22, packages/cli/scripts/version.ts:23


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread .github/workflows/publish.yml Outdated
- uses: actions/setup-node@v5
with:
node-version: 22
- run: node packages/cli/scripts/version.ts check "${{ github.event_name == 'push' && github.ref_name || '' }}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Critical · Tags execute commands in CI

When a matching pushed tag contains shell syntax such as command substitution, the interpolated github.ref_name is parsed as part of the run script, so commands in the tag execute in the versions job before the checker runs. The job has checkout credentials available, and a crafted tag can still let the release jobs proceed.

Pass the tag through an environment variable and quote the variable in the shell command instead of interpolating the tag into the run script.

Prompt for AI agents
In `.github/workflows/publish.yml` at line 21:

**Tags execute commands in CI**

When a matching pushed tag contains shell syntax such as command substitution, the interpolated `github.ref_name` is parsed as part of the `run` script, so commands in the tag execute in the `versions` job before the checker runs. The job has checkout credentials available, and a crafted tag can still let the release jobs proceed.

Pass the tag through an environment variable and quote the variable in the shell command instead of interpolating the tag into the `run` script.

Comment thread packages/native/src/index.ts
{ file: "packages/cli/package.json", pattern: /("version":\s*")([^"]+)(")/ },
{ file: "packages/core/src/version.ts", pattern: /(export const VERSION = ")([^"]+)(")/ },
];
const VERSION = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High · Invalid versions reach release jobs

When a release version matches this regex but violates SemVer, such as a leading-zero component or malformed prerelease, VERSION.test accepts the invalid value, so set and check pass it. Maven can publish a coordinate that npm rejects, and Kotlin's SemVer.parse rejects it in compatibility checks, leaving a partial or unusable release.

Validate versions with a strict SemVer parser before allowing set or check to succeed.

Prompt for AI agents
In `packages/cli/scripts/version.ts` at line 14:

**Invalid versions reach release jobs**

When a release version matches this regex but violates SemVer, such as a leading-zero component or malformed prerelease, `VERSION.test` accepts the invalid value, so `set` and `check` pass it. Maven can publish a coordinate that npm rejects, and Kotlin's `SemVer.parse` rejects it in compatibility checks, leaving a partial or unusable release.

Validate versions with a strict SemVer parser before allowing `set` or `check` to succeed.

Comment on lines +18 to +22
for (const { file, pattern } of SITES) {
const path = join(root, file);
const text = await readFile(path, "utf8");
if (!pattern.test(text)) throw new Error(`${file}: no version found`);
await writeFile(path, text.replace(pattern, `$1${arg}$3`));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Failed setter leaves versions mixed

When a later target file is missing, unreadable, does not match its pattern, or cannot be written, the loop writes earlier sites before a later read, pattern check, or write fails, then exits with an inconsistent worktree that version:check rejects.

Prepare and validate every update before writing, then apply the changes with rollback or another all-or-nothing update strategy.

Prompt for AI agents
In `packages/cli/scripts/version.ts` at lines 18-22:

**Failed setter leaves versions mixed**

When a later target file is missing, unreadable, does not match its pattern, or cannot be written, the loop writes earlier sites before a later read, pattern check, or write fails, then exits with an inconsistent worktree that `version:check` rejects.

Prepare and validate every update before writing, then apply the changes with rollback or another all-or-nothing update strategy.

Comment on lines +18 to +23
for (const { file, pattern } of SITES) {
const path = join(root, file);
const text = await readFile(path, "utf8");
if (!pattern.test(text)) throw new Error(`${file}: no version found`);
await writeFile(path, text.replace(pattern, `$1${arg}$3`));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Concurrent updates mix versions

When two version:set commands for different versions run at the same time in one worktree, two version:set processes for different versions write the SITES files independently without a lock, so their writes can interleave and leave a mixed-version worktree that version:check rejects.

Serialize version:set invocations with a lock, or make updates derive from a single source of truth.

Prompt for AI agents
In `packages/cli/scripts/version.ts` at lines 18-23:

**Concurrent updates mix versions**

When two `version:set` commands for different versions run at the same time in one worktree, two `version:set` processes for different versions write the `SITES` files independently without a lock, so their writes can interleave and leave a mixed-version worktree that `version:check` rejects.

Serialize `version:set` invocations with a lock, or make updates derive from a single source of truth.

- 保留 tinyui-native 的 VERSION:core / native 各有测试要求它等于 package.json,是有意的对称设计
- core 的 VERSION 会编进 core.jsb,升版本必须重生成 RuntimeBytecode.kt;0.11.1 那次漏了,main 的
  runtime:check 自 d562719 起失败,本提交补上。version:set 改完五处后自动 build + runtime
- publish.yml 的 versions 前置 job 加 pnpm install / build / runtime:check,字节码过期同样不发
- README 更正为五处

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@codeseerbot codeseerbot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CodeSeer 审查

结论:批准(附 1 条建议)

上轮意见:1 条,已处理 0 条,待处理 1 条

  • packages/cli/scripts/version.ts:14 [建议] VERSION 不是严格的 SemVer 校验,例如 01.2.3、1.2.3-a..b 都会通过,但它们不是合法的 npm 包版…

摘要

本次增量将发布前置校验扩展为安装依赖、构建并校验运行时字节码,同时版本脚本会同步 native 的 VERSION 并重生成字节码。生成的 RuntimeBytecode.kt 已随版本号更新。主要剩余问题是 README 保留了旧的发布步骤,和新的五处校验说明相互矛盾。

增量 689cce6..6aa41bb · gpt-5.6-terra

Comment thread README.md
## 发布

版本号的真源是 `packages/core/package.json`:Kotlin 库的 `TinyUI.version` 由它生成,页面包的 `tinyui` 下限由它写入,所以源码构建与 Maven 产物报同一个数(docs/updates.md §1.1)。它另有四处镜像(native、cli 的 `package.json`,core 与 native 的 `VERSION` 导出),而 core 的 `VERSION` 会编进库内嵌的运行时字节码,所以改版本号一定伴随 `RuntimeBytecode.kt` 的重生成。只用脚本改:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[细节] 这里保留了旧的“pnpm version:set 改四处、发布时核对四处”的两步说明,紧接着第 37、38 行又给出了改五处并校验字节码的新说明。读者会看到两套相互矛盾且重复编号的发布流程。应删除旧的第 1、2 步,仅保留与当前脚本和 CI 一致的五处版本及字节码校验说明。

@HarlonWang
HarlonWang merged commit 8f59bd0 into main Oct 18, 2026
5 checks passed
@HarlonWang
HarlonWang deleted the fix/release-version-preflight branch October 18, 2026 08:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant