#!/usr/bin/env node // CI 专用: 校验版本号四处一致 + 与分支名一致 // // 背景: // 版本号只允许一个手工管理点 = package.json 的 version, 其余位置全部派生: // - APK / 桌面安装包 version = (裸版本号, 无后缀) // - OTA 热更包 version = -ota (多一个 -ota 标记) // - 前端运行时 APP_VERSION = // 任何一处漏改都会让线上版本号、APK 版本、OTA 判新三者对不上, 且很难在客户端发现。 // // 校验分两段: // A. 一致性 (任何分支都查): package.json ↔ tauri.conf.json ↔ Cargo.toml ↔ Cargo.lock // 并要求 package.json 的 version 不含 "-ota" (裸版本, OTA 后缀由打包脚本追加) // 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 } const sources = [ { file: 'package.json', version: String(readJson('package.json').version || '') }, { 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) let failed = false // ---- A. 四处一致性 + 裸版本号 ---- if (/-ota$/i.test(version)) { console.error('') console.error(`[check-version] package.json 的 version 不能带 "-ota": ${version}`) 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) { console.error('') console.error('============================================================') console.error('[check-version] 版本号不一致, 构建中止') console.error('============================================================') for (const s of mismatched) { console.error(` ${s.file}: ${s.version || '(缺失)'} ≠ package.json: ${version}`) } console.error('') console.error(' 规则: APK / 桌面包版本 = 裸版本号 (无后缀);') console.error(' OTA 热更包版本 = 同一个版本号 + "-ota"。') console.error(' 改版本时四处必须一起改 (见 readme「版本管理」)。') failed = true } // ---- B. 分支对应 ---- 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)) { console.error('') console.error(`[check-version] 版本与分支不一致: 分支 "${branch}" 要求版本以 ${want} 开头, 实际是 ${version}`) console.error('[check-version] 请修改 package.json 的 version, 并同步 tauri.conf.json / Cargo.toml / Cargo.lock') failed = true } else { console.log(`[check-version] 分支一致 (${branch} ↔ ${version})`) } } if (failed) process.exit(1) console.log('[check-version] 全部通过')