Version checks and downloads previously depended on GitHub Release. About & Updates now offers GitHub or a validated mirror source, keeps the selector beneath the startup-check switch, and disables it when startup checks are off. The website resolves platform downloads from a bounded OSS release index with a GitHub fallback. Tagged releases publish and verify immutable OSS assets through OIDC before switching the latest-version index; deployment requires the configured Alibaba Cloud environment variables and role. Release note: “关于与更新”新增 GitHub 与镜像节点选择,启动检查、手动检查和下载页使用同一可信来源;官网下载也可按系统、架构和安装包类型直接选择。
204 lines
5.3 KiB
JavaScript
204 lines
5.3 KiB
JavaScript
const {
|
|
mkdirSync,
|
|
readFileSync,
|
|
writeFileSync
|
|
} = require('node:fs')
|
|
const { dirname, resolve } = require('node:path')
|
|
|
|
const supportedTargets = new Map([
|
|
['windows-x64', ['nsis', 'portable']],
|
|
['windows-arm64', ['nsis', 'portable']],
|
|
['macos-x64', ['dmg', 'zip']],
|
|
['macos-arm64', ['dmg', 'zip']],
|
|
['linux-x64', ['AppImage', 'deb']],
|
|
['linux-arm64', ['AppImage', 'deb']]
|
|
])
|
|
|
|
function parseArguments(argv) {
|
|
const options = {}
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
const argument = argv[index]
|
|
if (
|
|
argument === '--manifest' ||
|
|
argument === '--base-url' ||
|
|
argument === '--output'
|
|
) {
|
|
const value = argv[index + 1]
|
|
if (!value) {
|
|
throw new Error(`${argument} 缺少值`)
|
|
}
|
|
options[argument.slice(2)] = value
|
|
index += 1
|
|
} else {
|
|
throw new Error(`未知参数:${argument}`)
|
|
}
|
|
}
|
|
if (!options.manifest || !options.baseUrl || !options.output) {
|
|
throw new Error('必须指定 --manifest、--base-url 和 --output')
|
|
}
|
|
return {
|
|
manifest: resolve(options.manifest),
|
|
baseUrl: options.baseUrl,
|
|
output: resolve(options.output)
|
|
}
|
|
}
|
|
|
|
function validateBaseUrl(value) {
|
|
let url
|
|
try {
|
|
url = new URL(value)
|
|
} catch {
|
|
throw new Error(`OSS 基础地址无效:${value}`)
|
|
}
|
|
if (
|
|
url.protocol !== 'https:' ||
|
|
url.username ||
|
|
url.password ||
|
|
url.search ||
|
|
url.hash
|
|
) {
|
|
throw new Error('OSS 基础地址必须是无凭据、查询参数和片段的 HTTPS 地址')
|
|
}
|
|
if (!url.pathname.endsWith('/')) {
|
|
url.pathname += '/'
|
|
}
|
|
return url
|
|
}
|
|
|
|
function assertFile(file, targetKey) {
|
|
if (
|
|
!file ||
|
|
typeof file.name !== 'string' ||
|
|
!/^GoodBuddy-[A-Za-z0-9._-]+$/u.test(file.name) ||
|
|
!Number.isSafeInteger(file.size) ||
|
|
file.size < 1 ||
|
|
typeof file.sha256 !== 'string' ||
|
|
!/^[a-f0-9]{64}$/u.test(file.sha256)
|
|
) {
|
|
throw new Error(`发布文件元数据无效:${targetKey}`)
|
|
}
|
|
}
|
|
|
|
function formatForFile(fileName, platform) {
|
|
if (platform === 'windows') {
|
|
if (/-setup\.exe$/u.test(fileName)) {
|
|
return 'nsis'
|
|
}
|
|
if (/-portable\.zip$/u.test(fileName)) {
|
|
return 'portable'
|
|
}
|
|
} else if (platform === 'macos') {
|
|
if (fileName.endsWith('.dmg')) {
|
|
return 'dmg'
|
|
}
|
|
if (fileName.endsWith('.zip')) {
|
|
return 'zip'
|
|
}
|
|
} else if (platform === 'linux') {
|
|
if (fileName.endsWith('.AppImage')) {
|
|
return 'AppImage'
|
|
}
|
|
if (fileName.endsWith('.deb')) {
|
|
return 'deb'
|
|
}
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
function createSiteRelease(manifest, baseUrlValue) {
|
|
if (
|
|
!manifest ||
|
|
manifest.formatVersion !== 1 ||
|
|
manifest.productName !== 'GoodBuddy' ||
|
|
typeof manifest.version !== 'string' ||
|
|
!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u.test(manifest.version) ||
|
|
!Array.isArray(manifest.targets)
|
|
) {
|
|
throw new Error('聚合发布 manifest 元数据无效')
|
|
}
|
|
const baseUrl = validateBaseUrl(baseUrlValue)
|
|
const seenTargets = new Set()
|
|
const targets = {}
|
|
for (const target of manifest.targets) {
|
|
const key = `${target?.platform}-${target?.arch}`
|
|
const expectedFormats = supportedTargets.get(key)
|
|
if (
|
|
!expectedFormats ||
|
|
seenTargets.has(key) ||
|
|
!Array.isArray(target.formats) ||
|
|
!Array.isArray(target.files) ||
|
|
target.formats.length !== expectedFormats.length ||
|
|
!expectedFormats.every(
|
|
(format, index) => target.formats[index] === format
|
|
) ||
|
|
target.files.length !== expectedFormats.length
|
|
) {
|
|
throw new Error(`发布目标元数据无效:${key}`)
|
|
}
|
|
const files = {}
|
|
for (const file of target.files) {
|
|
assertFile(file, key)
|
|
const format = formatForFile(file.name, target.platform)
|
|
if (!format || !expectedFormats.includes(format) || files[format]) {
|
|
throw new Error(`发布文件格式无效:${file.name}`)
|
|
}
|
|
files[format] = {
|
|
name: file.name,
|
|
size: file.size,
|
|
sha256: file.sha256,
|
|
url: new URL(encodeURIComponent(file.name), baseUrl).href
|
|
}
|
|
}
|
|
if (expectedFormats.some((format) => !files[format])) {
|
|
throw new Error(`发布目标文件不完整:${key}`)
|
|
}
|
|
targets[key] = {
|
|
platform: target.platform,
|
|
arch: target.arch,
|
|
files
|
|
}
|
|
seenTargets.add(key)
|
|
}
|
|
if (seenTargets.size !== supportedTargets.size) {
|
|
throw new Error(
|
|
`发布目标数量错误:期望 ${supportedTargets.size},实际 ${seenTargets.size}`
|
|
)
|
|
}
|
|
return {
|
|
formatVersion: 1,
|
|
productName: manifest.productName,
|
|
version: manifest.version,
|
|
targets,
|
|
checksumUrl: new URL('SHA256SUMS', baseUrl).href,
|
|
fallbackUrl: 'https://github.com/mesalogo/goodbuddy/releases/latest'
|
|
}
|
|
}
|
|
|
|
function main(argv = process.argv.slice(2)) {
|
|
const options = parseArguments(argv)
|
|
const manifest = JSON.parse(readFileSync(options.manifest, 'utf8'))
|
|
const siteRelease = createSiteRelease(manifest, options.baseUrl)
|
|
mkdirSync(dirname(options.output), { recursive: true })
|
|
writeFileSync(
|
|
options.output,
|
|
`${JSON.stringify(siteRelease, null, 2)}\n`,
|
|
'utf8'
|
|
)
|
|
console.log(`官网发布索引已生成:${options.output}`)
|
|
}
|
|
|
|
module.exports = {
|
|
createSiteRelease,
|
|
parseArguments,
|
|
validateBaseUrl
|
|
}
|
|
|
|
if (require.main === module) {
|
|
try {
|
|
main()
|
|
} catch (error) {
|
|
console.error(error)
|
|
process.exitCode = 1
|
|
}
|
|
}
|