207 行
8.0 KiB
JavaScript
207 行
8.0 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { existsSync, readFileSync } from 'node:fs'
|
|
import { createHash } from 'node:crypto'
|
|
import path from 'node:path'
|
|
import { createInterface } from 'node:readline'
|
|
import process from 'node:process'
|
|
import {
|
|
SUPPORTED_PLATFORMS,
|
|
bundleOutputDirectory,
|
|
parseModuleOptions,
|
|
readConfig,
|
|
resolveHostPackageId,
|
|
resolveVersionedModules,
|
|
selectModules,
|
|
} from './xuqm-config.mjs'
|
|
import {
|
|
BUGCOLLECT_ARTIFACT_UPLOAD_PATH,
|
|
createArtifactUploadRequest,
|
|
createRnSourceMapForm,
|
|
} from './artifact-upload.mjs'
|
|
import { validateReleaseConfig } from './release-config.mjs'
|
|
import { readReleasePackageIdentity } from './release-package.mjs'
|
|
import { parseBugCollectOption } from './package-options.mjs'
|
|
|
|
const root = process.cwd()
|
|
const rawArgs = process.argv.slice(2)
|
|
|
|
function takeOption(name) {
|
|
const index = rawArgs.indexOf(name)
|
|
if (index < 0) return undefined
|
|
const value = rawArgs[index + 1]
|
|
if (!value || value.startsWith('--')) throw new Error(`${name} requires a value`)
|
|
rawArgs.splice(index, 2)
|
|
return value
|
|
}
|
|
|
|
const platform = takeOption('--platform') ?? 'android'
|
|
const noteOption = takeOption('--note')
|
|
const parsedBugCollect = parseBugCollectOption(
|
|
rawArgs,
|
|
process.env.XUQM_BUGCOLLECT_MODE ?? 'platform',
|
|
)
|
|
const bugCollectMode = parsedBugCollect.bugCollectMode
|
|
rawArgs.splice(0, rawArgs.length, ...parsedBugCollect.remaining)
|
|
const publishOptionIndex = rawArgs.indexOf('--publish')
|
|
const publishOption = publishOptionIndex >= 0
|
|
if (publishOption) rawArgs.splice(publishOptionIndex, 1)
|
|
const { moduleIds, remaining } = parseModuleOptions(rawArgs)
|
|
if (remaining.length) throw new Error(`unknown option(s): ${remaining.join(' ')}`)
|
|
if (!SUPPORTED_PLATFORMS.has(platform)) throw new Error('platform must be android or ios')
|
|
if (!['platform', 'disabled'].includes(bugCollectMode)) {
|
|
throw new Error('--bugcollect must be platform or disabled')
|
|
}
|
|
|
|
const config = readConfig(root)
|
|
const hostPackageId = resolveHostPackageId(config, platform)
|
|
const versioned = resolveVersionedModules(config, root, process.env.XUQM_APP_VERSION)
|
|
const selectedModules = selectModules({ ...config, modules: versioned.modules }, moduleIds)
|
|
if (moduleIds.length > 0 && selectedModules.some(module => module.type === 'startup')) {
|
|
throw new Error('startup is embedded in the host APK and cannot be published independently')
|
|
}
|
|
const modules = selectedModules.filter(module => module.type !== 'startup')
|
|
if (modules.length === 0) throw new Error('no publishable common/app/buz module selected')
|
|
const serverUrl = process.env.XUQM_SERVER_URL ?? config.release?.serverUrl
|
|
const appKey = process.env.XUQM_APP_KEY ?? config.release?.appKey
|
|
// API Token 只允许由 CI/当前进程注入,禁止把可用凭据写进会进入版本库的插件清单。
|
|
const apiToken = process.env.XUQM_API_TOKEN
|
|
const bugCollectUrl = process.env.XUQM_BUGCOLLECT_URL
|
|
if (!serverUrl || !appKey || !apiToken) {
|
|
throw new Error(
|
|
'publish requires XUQM_API_TOKEN and XUQM_SERVER_URL/XUQM_APP_KEY (environment or non-secret release config)',
|
|
)
|
|
}
|
|
|
|
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
const ask = question => new Promise(resolve => rl.question(`\x1b[36m${question}\x1b[0m`, resolve))
|
|
const confirm = async question => /^y/i.test(((await ask(`${question} [y/N]: `)) ?? '').trim())
|
|
|
|
function apiHeaders() {
|
|
return { Authorization: `Bearer ${apiToken}` }
|
|
}
|
|
|
|
async function validateReleaseConfigOnline() {
|
|
await validateReleaseConfig({
|
|
projectRoot: root,
|
|
packageName: hostPackageId,
|
|
})
|
|
}
|
|
|
|
function releasePackage(module) {
|
|
const file = path.join(
|
|
bundleOutputDirectory(root, config, platform, module.id),
|
|
`${module.id}.${platform}.xuqm.zip`,
|
|
)
|
|
if (!existsSync(file)) throw new Error(`plugin package is missing: ${file}`)
|
|
const identity = readReleasePackageIdentity(file, {
|
|
packageName: hostPackageId,
|
|
moduleId: module.id,
|
|
platform,
|
|
moduleVersion: module.moduleVersion,
|
|
appVersion: versioned.appVersion,
|
|
})
|
|
return { file, identity }
|
|
}
|
|
|
|
async function uploadBundle(module, release, note) {
|
|
const { file, identity } = release
|
|
const form = new FormData()
|
|
form.append('appKey', appKey)
|
|
form.append('moduleId', module.id)
|
|
form.append('platform', platform.toUpperCase())
|
|
form.append('version', identity.moduleVersion)
|
|
form.append('appVersionRange', identity.appVersionRange)
|
|
form.append('builtAgainstNativeBaselineId', identity.nativeBaselineId)
|
|
form.append('buildId', identity.buildId)
|
|
form.append('bundleFormat', identity.bundleFormat)
|
|
form.append('commonVersionRange', identity.commonVersionRange ?? '')
|
|
form.append('minNativeApiLevel', String(identity.minNativeApiLevel))
|
|
form.append('note', note)
|
|
form.append('packageName', identity.packageName)
|
|
form.append('bundle', new Blob([readFileSync(file)]), `${module.id}.${platform}.xuqm.zip`)
|
|
const response = await fetch(`${serverUrl}/api/v1/rn/upload`, {
|
|
method: 'POST',
|
|
headers: apiHeaders(),
|
|
body: form,
|
|
})
|
|
if (!response.ok) throw new Error(`upload failed for ${module.id}: ${await response.text()}`)
|
|
return response.json()
|
|
}
|
|
|
|
async function uploadSourceMap(module, release) {
|
|
const bundleDirectory = bundleOutputDirectory(root, config, platform, module.id)
|
|
const bundleFile = path.join(bundleDirectory, `${module.id}.${platform}.bundle`)
|
|
const sourceMapFile = `${bundleFile}.map`
|
|
if (!existsSync(sourceMapFile)) throw new Error(`Source Map is missing: ${sourceMapFile}`)
|
|
const sourceMapBundleHash = createHash('sha256').update(readFileSync(bundleFile)).digest('hex')
|
|
if (sourceMapBundleHash !== release.identity.bundleHash) {
|
|
throw new Error(
|
|
`Source Map Bundle identity mismatch for ${module.id}; rebuild before publishing`,
|
|
)
|
|
}
|
|
if (bugCollectMode === 'disabled') {
|
|
console.log(` ${module.id}/${platform} Source Map archived locally (BugCollect disabled)`)
|
|
return
|
|
}
|
|
if (!bugCollectUrl) {
|
|
throw new Error('XUQM_BUGCOLLECT_URL is required when BugCollect Source Map upload is enabled')
|
|
}
|
|
const form = createRnSourceMapForm({
|
|
appKey,
|
|
platform,
|
|
appVersion: versioned.appVersion,
|
|
buildId: release.identity.buildId,
|
|
moduleId: module.id,
|
|
moduleVersion: module.moduleVersion,
|
|
bundleHash: release.identity.bundleHash,
|
|
fileBytes: readFileSync(sourceMapFile),
|
|
fileName: `${module.id}.${platform}.map`,
|
|
})
|
|
const response = await fetch(
|
|
`${bugCollectUrl.replace(/\/$/, '')}${BUGCOLLECT_ARTIFACT_UPLOAD_PATH}`,
|
|
createArtifactUploadRequest(apiToken, form),
|
|
)
|
|
if (!response.ok) {
|
|
throw new Error(`Source Map upload failed for ${module.id}: ${await response.text()}`)
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
await validateReleaseConfigOnline()
|
|
const note = noteOption ?? (await ask('Release notes: '))
|
|
const publishImmediately = publishOption || config.release?.publishImmediately === true
|
|
console.log('\n\x1b[36m--- Xuqm RN plugin publish ---\x1b[0m')
|
|
console.log(` Platform: ${platform}`)
|
|
console.log(
|
|
` Modules: ${modules.map(module => `${module.id}@${module.moduleVersion}`).join(', ')}`,
|
|
)
|
|
console.log(` Publish immediately: ${publishImmediately}`)
|
|
console.log(` BugCollect: ${bugCollectMode}`)
|
|
if (!(await confirm('Proceed?'))) return
|
|
|
|
for (const module of modules) {
|
|
const release = releasePackage(module)
|
|
const result = await uploadBundle(module, release, note)
|
|
const bundleId = result.data?.id
|
|
console.log(`\x1b[32m✓ ${module.id}/${platform} uploaded, ID: ${bundleId}\x1b[0m`)
|
|
// Source Map 是正式发布硬门禁,必须在插件进入 published 状态前完成。
|
|
await uploadSourceMap(module, release)
|
|
if (publishImmediately) {
|
|
const response = await fetch(`${serverUrl}/api/v1/rn/${bundleId}/publish`, {
|
|
method: 'POST',
|
|
headers: apiHeaders(),
|
|
})
|
|
if (!response.ok) throw new Error(`publish failed for ${module.id}: ${await response.text()}`)
|
|
console.log(` ${module.id}/${platform} published`)
|
|
}
|
|
}
|
|
}
|
|
|
|
main()
|
|
.catch(error => {
|
|
console.error(`\n\x1b[31m${error.message}\x1b[0m`)
|
|
process.exitCode = 1
|
|
})
|
|
.finally(() => rl.close())
|