feat(V0.2): 版本方案落地 —— 前端/外壳解绑, build 标识统一, 外壳版本展示
Docker Build and Push / build-image (push) Successful in 1m5s

- check-version: 分组校验 (前端 package.json / 外壳 tauri.conf+Cargo),
  两组不再要求相等; 分支前缀校验只针对前端版本
- build-ota: build_id 改为 YYYYMMDDHHmmss (北京时间, 与前端 BUILD_ID
  同格式); sequence 保持毫秒仅用于判新
- 我的页: 当前版本恢复 v0.2.0 · build <原始BUILD_ID>; 新版本附 OTA
  build_id; 底部替换为外壳版本 (Tauri getVersion, Web 端 —)
- README: 合并遗留 readme 全部内容并按决议重写版本管理章节;
  新增 CHANGELOG.md (PATCH 仅明确提出时自增)
This commit is contained in:
jif2.zhang
2026-10-25 21:28:06 +08:00
parent 06aa48f6ad
commit 67be8d8f7b
18 changed files with 700 additions and 338 deletions
+7 -2
View File
@@ -123,8 +123,13 @@ const manifest = {
commit: COMMIT || undefined,
pub_date: new Date().toISOString(),
mandatory: false,
// 内部用: 前端 web 模式比对 build_id
build_id: 'seq-' + SEQUENCE,
// 内部用: 前端 web 模式比对 build_id。
// 格式与前端 vite BUILD_ID 一致 (YYYYMMDDHHmmss, 北京时间); sequence 保持毫秒单调递增
build_id: (() => {
const d = new Date(Date.now() + 8 * 3600 * 1000)
const p = (n, w) => String(n).padStart(w || 2, '0')
return `${d.getUTCFullYear()}${p(d.getUTCMonth() + 1)}${p(d.getUTCDate())}${p(d.getUTCHours())}${p(d.getUTCMinutes())}${p(d.getUTCSeconds())}`
})(),
}
fs.writeFileSync(path.join(OUT_DIR, 'latest.json'), JSON.stringify(manifest, null, 2))
+41 -36
View File
@@ -1,17 +1,24 @@
#!/usr/bin/env node
// CI 专用: 校验版本号四处一致 + 与分支名一致
// CI 专用: 校验版本号分组一致 + 前端版本与分支名对应
//
// 背景:
// 版本号只允许一个手工管理点 = package.json 的 version, 其余位置全部派生:
// - APK / 桌面安装包 version = <version> (裸版本号, 无后缀)
// - OTA 热更包 version = <version>-ota (多一个 -ota 标记)
// - 前端运行时 APP_VERSION = <version>
// 任何一处漏改都会让线上版本号、APK 版本、OTA 判新三者对不上, 且很难在客户端发现。
// 背景 (2026-10-25 调整: 外壳与前端版本号解绑):
// 前端 (Web/OTA) 与外壳 (Android/Windows 壳) 独立维护、独立发版,
// 两组版本号不再要求相等, 只要求**组内**一致:
//
// 组1 前端: package.json .version
// ├─ 前端运行时 APP_VERSION = <version> (裸, 无后缀)
// └─ OTA 热更包 version = <version>-ota (打包脚本追加)
//
// 组2 外壳: src-tauri/tauri.conf.json .version (= APK versionName)
// ├─ src-tauri/Cargo.toml [package].version
// └─ src-tauri/Cargo.lock (iboard 条目)
// 外壳随 android-*/桌面发布独立演进, 与 package.json 无关。
//
// 另: package.json 的 version 不允许带 "-ota" (OTA 后缀由打包脚本追加)。
//
// 校验分两段:
// A. 一致性 (任何分支都查): package.json ↔ tauri.conf.json ↔ Cargo.toml ↔ Cargo.lock
// 并要求 package.json 的 version 不含 "-ota" (裸版本, OTA 后缀由打包脚本追加)
// B. 分支对应 (仅 dev-<版本> 分支):
// A. 组内一致性 (任何分支都查)
// B. 分支对应 (仅 dev-<版本> 分支, 针对前端组):
// dev-<major>.<minor> → version 必须以 <major>.<minor> 开头
// dev-<major> → version 必须以 <major>. 开头
// main / master / 其它 → 跳过
@@ -53,61 +60,59 @@ function readCargoLockVersion() {
return null
}
const sources = [
{ file: 'package.json', version: String(readJson('package.json').version || '') },
// 组1 前端 (唯一手动管理点 = package.json)
const webVersion = String(readJson('package.json').version || '')
const webSources = [{ file: 'package.json', version: webVersion }]
const otaVersion = webVersion + '-ota'
// 组2 外壳 (独立维护; 三处必须组内一致)
const shellSources = [
{ file: 'src-tauri/tauri.conf.json', version: String(readJson('src-tauri/tauri.conf.json').version || '') },
{ file: 'src-tauri/Cargo.toml', version: readCargoTomlVersion() },
{ file: 'src-tauri/Cargo.lock (iboard)', version: readCargoLockVersion() },
]
const version = sources[0].version
const otaVersion = version + '-ota'
console.log('[check-version] 分支: ', branch || '(未知)')
for (const s of sources) {
console.log(`[check-version] ${s.file.padEnd(28)} ${s.version || '(缺失)'}`)
}
console.log('[check-version] 产物版本: APK =', version, '/ OTA =', otaVersion)
for (const s of webSources) console.log(`[check-version] [前端] ${s.file.padEnd(28)} ${s.version || '(缺失)'}`)
for (const s of shellSources) console.log(`[check-version] [外壳] ${s.file.padEnd(28)} ${s.version || '(缺失)'}`)
console.log('[check-version] 产物版本: OTA =', otaVersion, '/ 外壳(APK versionName) =', shellSources[0].version)
let failed = false
// ---- A. 四处一致性 + 裸版本号 ----
if (/-ota$/i.test(version)) {
// ---- A1. 前端组: 裸版本号 ----
if (/-ota$/i.test(webVersion)) {
console.error('')
console.error(`[check-version] package.json 的 version 不能带 "-ota": ${version}`)
console.error(`[check-version] package.json 的 version 不能带 "-ota": ${webVersion}`)
console.error('[check-version] OTA 后缀由 scripts/build-ota.mjs 自动追加, 请只写裸版本号 (如 0.2.0)')
failed = true
}
const mismatched = sources.filter((s) => s.version !== version)
if (mismatched.length) {
// ---- A2. 外壳组内一致 ----
const shellBroken = shellSources.filter((s) => s.version !== shellSources[0].version || !s.version)
if (shellBroken.length) {
console.error('')
console.error('============================================================')
console.error('[check-version] 版本号不一致, 构建中止')
console.error('[check-version] 外壳版本号不一致 (tauri.conf.json ↔ Cargo.toml ↔ Cargo.lock), 构建中止')
console.error('============================================================')
for (const s of mismatched) {
console.error(` ${s.file}: ${s.version || '(缺失)'} ≠ package.json: ${version}`)
for (const s of shellBroken) {
console.error(` ${s.file}: ${s.version || '(缺失)'} ≠ tauri.conf.json: ${shellSources[0].version}`)
}
console.error('')
console.error(' 规则: APK / 桌面包版本 = 裸版本号 (无后缀);')
console.error(' OTA 热更包版本 = 同一个版本号 + "-ota"。')
console.error(' 改版本时四处必须一起改 (见 readme「版本管理」)。')
failed = true
}
// ---- B. 分支对应 ----
// ---- B. 分支对应 (仅前端组; 外壳在 android-* 分支独立演进, 不参与) ----
const m = /^dev-(\d+)(?:\.(\d+))?$/.exec(branch)
if (!m) {
console.log('[check-version] 非 dev-<版本> 分支, 跳过分支对应校验')
} else {
const want = m[2] === undefined ? `${m[1]}.` : `${m[1]}.${m[2]}`
if (!version.startsWith(want)) {
if (!webVersion.startsWith(want)) {
console.error('')
console.error(`[check-version] 版本与分支不一致: 分支 "${branch}" 要求版本以 ${want} 开头, 实际是 ${version}`)
console.error('[check-version] 请修改 package.json 的 version, 并同步 tauri.conf.json / Cargo.toml / Cargo.lock')
console.error(`[check-version] 版本与分支不一致: 分支 "${branch}" 要求前端版本以 ${want} 开头, 实际是 ${webVersion}`)
console.error('[check-version] 请修改 package.json 的 version')
failed = true
} else {
console.log(`[check-version] 分支一致 (${branch} ↔ ${version})`)
console.log(`[check-version] 分支一致 (${branch} ↔ ${webVersion})`)
}
}