import { createHash } from 'node:crypto' import { mkdirSync, readFileSync, writeFileSync } from 'node:fs' import path from 'node:path' const MAX_ANDROID_VERSION_CODE = 2_100_000_000 function readPreviousVersionCode(cacheFile) { try { return Number(JSON.parse(readFileSync(cacheFile, 'utf8')).versionCode) || 0 } catch { return 0 } } /** * 为同一 versionName 的连续测试包生成可安装的新构建身份。 * * 本地缓存只负责同一工作区内的单调递增;CI 若需要跨工作区全局唯一,必须通过 * XUQM_VERSION_CODE 注入流水线唯一编号。 */ export function createAndroidReleaseIdentity({ projectRoot, environment = process.env, now = Date.now(), entropy = `${process.pid}:${Math.random()}`, }) { const cacheFile = path.join(projectRoot, '.xuqm-cache', 'release-build.json') const previousVersionCode = readPreviousVersionCode(cacheFile) const requestedText = environment.XUQM_VERSION_CODE const requested = requestedText === undefined ? undefined : Number(requestedText) if ( requestedText !== undefined && (!Number.isInteger(requested) || requested <= 0 || requested > MAX_ANDROID_VERSION_CODE) ) { throw new Error( `XUQM_VERSION_CODE must be an integer between 1 and ${MAX_ANDROID_VERSION_CODE}`, ) } const epochSeconds = Math.floor(now / 1_000) const versionCode = requested ?? Math.max(epochSeconds, previousVersionCode + 1) if (versionCode > MAX_ANDROID_VERSION_CODE) { throw new Error('Generated Android versionCode exceeds limit') } const buildId = environment.XUQM_BUILD_ID ?? createHash('sha256').update(`${versionCode}:${now}:${entropy}`).digest('hex').slice(0, 32) if (!/^[A-Za-z0-9._-]{1,128}$/.test(buildId)) { throw new Error('XUQM_BUILD_ID must contain only letters, numbers, dot, underscore or hyphen') } mkdirSync(path.dirname(cacheFile), { recursive: true }) writeFileSync( cacheFile, `${JSON.stringify( { versionCode, buildId, generatedAt: new Date(now).toISOString(), }, null, 2, )}\n`, ) return { versionCode, buildId } }