#!/usr/bin/env node // CI 专用: 校验版本号分组一致 + 前端版本与分支名对应 // // 背景 (2026-10-25 调整: 外壳与前端版本号解绑): // 前端 (Web/OTA) 与外壳 (Android/Windows 壳) 独立维护、独立发版, // 两组版本号不再要求相等, 只要求**组内**一致: // // 组1 前端: package.json .version // ├─ 前端运行时 APP_VERSION = (裸, 无后缀) // └─ OTA 热更包 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. 组内一致性 (任何分支都查) // B. 分支对应 (仅 dev-<版本> 分支, 针对前端组): // dev-. → version 必须以 . 开头 // dev- → version 必须以 . 开头 // main / master / 其它 → 跳过 // // 环境变量: // GITHUB_REF_NAME - Gitea Actions 提供的分支名 (本地运行可用 BRANCH 覆盖) // // 退出码: 0 通过 / 1 不一致 import fs from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const ROOT = path.resolve(__dirname, '..', '..') const branch = process.env.GITHUB_REF_NAME || process.env.BRANCH || '' function readJson(rel) { return JSON.parse(fs.readFileSync(path.join(ROOT, rel), 'utf8')) } // Cargo.toml 的 [package].version 位于文件开头, 取第一个行首 version = "x" function readCargoTomlVersion() { const text = fs.readFileSync(path.join(ROOT, 'src-tauri/Cargo.toml'), 'utf8') const m = /^version\s*=\s*"([^"]+)"/m.exec(text) return m ? m[1] : null } // Cargo.lock 里挑出 name = "iboard" 那个包的 version function readCargoLockVersion() { const text = fs.readFileSync(path.join(ROOT, 'src-tauri/Cargo.lock'), 'utf8') const blocks = text.split(/^\[\[package\]\]$/m) for (const b of blocks) { if (/^name\s*=\s*"iboard"$/m.test(b)) { const m = /^version\s*=\s*"([^"]+)"/m.exec(b) if (m) return m[1] } } return null } // 组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() }, ] console.log('[check-version] 分支: ', branch || '(未知)') 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 // ---- A1. 前端组: 裸版本号 ---- if (/-ota$/i.test(webVersion)) { console.error('') console.error(`[check-version] package.json 的 version 不能带 "-ota": ${webVersion}`) console.error('[check-version] OTA 后缀由 scripts/build-ota.mjs 自动追加, 请只写裸版本号 (如 0.2.0)') failed = true } // ---- A2. 外壳组内一致 ---- const shellBroken = shellSources.filter((s) => s.version !== shellSources[0].version || !s.version) if (shellBroken.length) { console.error('') console.error('============================================================') console.error('[check-version] 外壳版本号不一致 (tauri.conf.json ↔ Cargo.toml ↔ Cargo.lock), 构建中止') console.error('============================================================') for (const s of shellBroken) { console.error(` ${s.file}: ${s.version || '(缺失)'} ≠ tauri.conf.json: ${shellSources[0].version}`) } failed = true } // ---- 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 (!webVersion.startsWith(want)) { console.error('') console.error(`[check-version] 版本与分支不一致: 分支 "${branch}" 要求前端版本以 ${want} 开头, 实际是 ${webVersion}`) console.error('[check-version] 请修改 package.json 的 version') failed = true } else { console.log(`[check-version] 分支一致 (${branch} ↔ ${webVersion})`) } } if (failed) process.exit(1) console.log('[check-version] 全部通过')