feat:移除了弹窗,服务器添加sls
This commit is contained in:
327
scripts/axolotl/cnb-release.mjs
Normal file
327
scripts/axolotl/cnb-release.mjs
Normal file
@ -0,0 +1,327 @@
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import fs from 'node:fs'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
|
||||
const [command, tag, outputDirectory] = process.argv.slice(2)
|
||||
const apiEndpoint = (process.env.CNB_API_ENDPOINT || 'https://api.cnb.cool').replace(/\/$/, '')
|
||||
const repo = process.env.CNB_REPO_SLUG || 'axlmc/Axolotl'
|
||||
const token = process.env.CNB_TOKEN
|
||||
const tokenUser = process.env.CNB_TOKEN_USER_NAME || 'cnb'
|
||||
const repoUrl = process.env.CNB_REPO_URL_HTTPS || `https://cnb.cool/${repo}.git`
|
||||
const githubReleaseBaseUrl = (
|
||||
process.env.GITHUB_RELEASE_BASE_URL || 'https://github.com/Mystic-Stars/Axolotl/releases/download'
|
||||
).replace(/\/$/, '')
|
||||
const githubApiBaseUrl = (
|
||||
process.env.GITHUB_API_BASE_URL || 'https://api.github.com/repos/Mystic-Stars/Axolotl'
|
||||
).replace(/\/$/, '')
|
||||
const configuredAssetMirrorConcurrency = Number(process.env.ASSET_MIRROR_CONCURRENCY || 4)
|
||||
const assetMirrorConcurrency =
|
||||
Number.isSafeInteger(configuredAssetMirrorConcurrency) && configuredAssetMirrorConcurrency > 0
|
||||
? configuredAssetMirrorConcurrency
|
||||
: 4
|
||||
|
||||
if (command !== 'finalize' || !tag || !outputDirectory || !token) {
|
||||
throw new Error('Usage: node cnb-release.mjs finalize <tag> <output-dir>; CNB_TOKEN is required')
|
||||
}
|
||||
|
||||
const apiHeaders = {
|
||||
Accept: 'application/vnd.cnb.api+json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
}
|
||||
const githubApiHeaders = {
|
||||
Accept: 'application/vnd.github+json',
|
||||
'User-Agent': 'Axolotl-CNB-Release',
|
||||
...(process.env.GITHUB_TOKEN ? { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` } : {}),
|
||||
}
|
||||
|
||||
async function apiRequest(url, options = {}, allowedStatuses = []) {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
...apiHeaders,
|
||||
...options.headers,
|
||||
},
|
||||
})
|
||||
if (!response.ok && !allowedStatuses.includes(response.status)) {
|
||||
throw new Error(
|
||||
`${options.method || 'GET'} ${url} failed (${response.status}): ${await response.text()}`,
|
||||
)
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
async function getRelease() {
|
||||
const response = await apiRequest(
|
||||
`${apiEndpoint}/${repo}/-/releases/tags/${encodeURIComponent(tag)}`,
|
||||
{},
|
||||
[404],
|
||||
)
|
||||
return response.status === 404 ? null : await response.json()
|
||||
}
|
||||
|
||||
async function ensureRelease(githubRelease) {
|
||||
const existing = await getRelease()
|
||||
if (existing) {
|
||||
return existing
|
||||
}
|
||||
|
||||
const prerelease = tag.includes('-')
|
||||
const response = await apiRequest(
|
||||
`${apiEndpoint}/${repo}/-/releases`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
tag_name: tag,
|
||||
target_commitish: process.env.CNB_COMMIT || tag,
|
||||
name:
|
||||
process.env.CNB_TAG_RELEASE_TITLE ||
|
||||
githubRelease.name ||
|
||||
`Axolotl Launcher ${tag}`,
|
||||
body: process.env.CNB_TAG_RELEASE_DESC || githubRelease.body || '',
|
||||
draft: true,
|
||||
prerelease,
|
||||
make_latest: 'false',
|
||||
}),
|
||||
},
|
||||
[409],
|
||||
)
|
||||
if (response.status !== 409) {
|
||||
return await response.json()
|
||||
}
|
||||
|
||||
for (let attempt = 0; attempt < 10; attempt++) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
const release = await getRelease()
|
||||
if (release) {
|
||||
return release
|
||||
}
|
||||
}
|
||||
throw new Error(`Release ${tag} was created concurrently but could not be loaded`)
|
||||
}
|
||||
|
||||
async function createAssetUpload(release, assetName, size) {
|
||||
const uploadResponse = await apiRequest(
|
||||
`${apiEndpoint}/${repo}/-/releases/${release.id}/asset-upload-url`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ asset_name: assetName, size, overwrite: true, ttl: 0 }),
|
||||
},
|
||||
)
|
||||
return await uploadResponse.json()
|
||||
}
|
||||
|
||||
async function verifyAssetUpload(upload) {
|
||||
const verifyUrl = new URL(upload.verify_url, apiEndpoint).toString()
|
||||
await apiRequest(`${verifyUrl}${verifyUrl.includes('?') ? '&' : '?'}ttl=0`, { method: 'POST' })
|
||||
}
|
||||
|
||||
async function uploadAsset(release, filePath) {
|
||||
const assetName = path.basename(filePath)
|
||||
const size = fs.statSync(filePath).size
|
||||
const upload = await createAssetUpload(release, assetName, size)
|
||||
const fileResponse = await fetch(upload.upload_url, {
|
||||
method: 'PUT',
|
||||
body: fs.createReadStream(filePath),
|
||||
duplex: 'half',
|
||||
headers: {
|
||||
'Content-Length': String(size),
|
||||
'Content-Type': 'application/octet-stream',
|
||||
},
|
||||
})
|
||||
if (!fileResponse.ok) {
|
||||
throw new Error(
|
||||
`Uploading ${assetName} failed (${fileResponse.status}): ${await fileResponse.text()}`,
|
||||
)
|
||||
}
|
||||
await verifyAssetUpload(upload)
|
||||
console.log(`Uploaded ${assetName}`)
|
||||
}
|
||||
|
||||
async function loadGithubManifest() {
|
||||
const manifestUrl = `${githubReleaseBaseUrl}/${encodeURIComponent(tag)}/latest.json`
|
||||
const response = await fetch(manifestUrl)
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Downloading GitHub manifest failed (${response.status}): ${await response.text()}`,
|
||||
)
|
||||
}
|
||||
const manifest = await response.json()
|
||||
if (manifest.version !== tag.replace(/^v/, '')) {
|
||||
throw new Error(`GitHub manifest version ${manifest.version} does not match ${tag}`)
|
||||
}
|
||||
return manifest
|
||||
}
|
||||
|
||||
async function loadGithubRelease(url) {
|
||||
const response = await fetch(url, {
|
||||
headers: githubApiHeaders,
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error(`Loading GitHub release failed (${response.status}): ${await response.text()}`)
|
||||
}
|
||||
return await response.json()
|
||||
}
|
||||
|
||||
async function getGithubRelease() {
|
||||
return await loadGithubRelease(
|
||||
`${githubApiBaseUrl}/releases/tags/${encodeURIComponent(tag)}`,
|
||||
)
|
||||
}
|
||||
|
||||
async function getLatestGithubRelease() {
|
||||
return await loadGithubRelease(`${githubApiBaseUrl}/releases/latest`)
|
||||
}
|
||||
|
||||
async function mirrorGithubAsset(release, asset) {
|
||||
const upload = await createAssetUpload(release, asset.name, asset.size)
|
||||
const response = await fetch(asset.browser_download_url)
|
||||
if (!response.ok || !response.body) {
|
||||
throw new Error(
|
||||
`Downloading ${asset.name} failed (${response.status}): ${await response.text()}`,
|
||||
)
|
||||
}
|
||||
const uploadResponse = await fetch(upload.upload_url, {
|
||||
method: 'PUT',
|
||||
body: response.body,
|
||||
duplex: 'half',
|
||||
headers: {
|
||||
'Content-Length': String(asset.size),
|
||||
'Content-Type': 'application/octet-stream',
|
||||
},
|
||||
})
|
||||
if (!uploadResponse.ok) {
|
||||
throw new Error(
|
||||
`Uploading ${asset.name} failed (${uploadResponse.status}): ${await uploadResponse.text()}`,
|
||||
)
|
||||
}
|
||||
await verifyAssetUpload(upload)
|
||||
console.log(`Mirrored ${asset.name}`)
|
||||
}
|
||||
|
||||
async function mirrorGithubAssets(release, githubRelease) {
|
||||
const assets = (githubRelease.assets || []).filter((asset) => asset.name !== 'latest.json')
|
||||
const mirroredNames = new Set(assets.map((asset) => asset.name))
|
||||
if (mirroredNames.size !== assets.length) {
|
||||
throw new Error('GitHub release contains duplicate asset names')
|
||||
}
|
||||
for (const asset of assets) {
|
||||
if (
|
||||
!asset.name ||
|
||||
!asset.browser_download_url ||
|
||||
!Number.isSafeInteger(asset.size) ||
|
||||
asset.size < 0
|
||||
) {
|
||||
throw new Error(`Invalid or duplicate GitHub release asset: ${asset.name}`)
|
||||
}
|
||||
}
|
||||
|
||||
let nextAsset = 0
|
||||
const workers = Array.from(
|
||||
{ length: Math.min(assetMirrorConcurrency, assets.length) },
|
||||
async () => {
|
||||
while (nextAsset < assets.length) {
|
||||
const asset = assets[nextAsset++]
|
||||
await mirrorGithubAsset(release, asset)
|
||||
}
|
||||
},
|
||||
)
|
||||
await Promise.all(workers)
|
||||
return mirroredNames
|
||||
}
|
||||
|
||||
function createCnbManifest(githubManifest, mirroredNames) {
|
||||
const requiredPlatforms = [
|
||||
'darwin-aarch64',
|
||||
'darwin-x86_64',
|
||||
'linux-aarch64',
|
||||
'linux-x86_64',
|
||||
'windows-x86_64',
|
||||
]
|
||||
const platforms = {}
|
||||
for (const platform of requiredPlatforms) {
|
||||
const update = githubManifest.platforms?.[platform]
|
||||
if (!update || typeof update.signature !== 'string' || update.signature.trim().length < 32) {
|
||||
throw new Error(`Missing signed GitHub update for ${platform}`)
|
||||
}
|
||||
const filename = decodeURIComponent(path.posix.basename(new URL(update.url).pathname))
|
||||
if (!mirroredNames.has(filename)) {
|
||||
throw new Error(`GitHub release is missing updater artifact ${filename} for ${platform}`)
|
||||
}
|
||||
platforms[platform] = {
|
||||
...update,
|
||||
url: `https://cnb.cool/${repo}/-/releases/download/${encodeURIComponent(tag)}/${encodeURIComponent(filename)}`,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...githubManifest,
|
||||
version: tag.replace(/^v/, ''),
|
||||
platforms,
|
||||
}
|
||||
}
|
||||
|
||||
function publishUpdateBranch(manifestPath) {
|
||||
if (tag.includes('-')) {
|
||||
return
|
||||
}
|
||||
|
||||
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'axolotl-cnb-update-'))
|
||||
const auth = Buffer.from(`${tokenUser}:${token}`).toString('base64')
|
||||
const git = (...args) => execFileSync('git', args, { cwd: directory, stdio: 'inherit' })
|
||||
git('init')
|
||||
git('config', 'user.name', 'Axolotl CNB Release')
|
||||
git('config', 'user.email', 'build@cnb.cool')
|
||||
git('checkout', '--orphan', 'update')
|
||||
fs.copyFileSync(manifestPath, path.join(directory, 'latest.json'))
|
||||
git('add', 'latest.json')
|
||||
git('commit', '-m', `Publish ${tag}`)
|
||||
git('remote', 'add', 'origin', repoUrl)
|
||||
git(
|
||||
'-c',
|
||||
`http.extraHeader=Authorization: Basic ${auth}`,
|
||||
'push',
|
||||
'--force',
|
||||
'origin',
|
||||
'HEAD:update',
|
||||
)
|
||||
}
|
||||
|
||||
async function finalizeRelease() {
|
||||
fs.mkdirSync(outputDirectory, { recursive: true })
|
||||
const [githubManifest, githubRelease, latestGithubRelease] = await Promise.all([
|
||||
loadGithubManifest(),
|
||||
getGithubRelease(),
|
||||
getLatestGithubRelease(),
|
||||
])
|
||||
const isLatestRelease = githubRelease.tag_name === latestGithubRelease.tag_name
|
||||
const release = await ensureRelease(githubRelease)
|
||||
const mirroredNames = await mirrorGithubAssets(release, githubRelease)
|
||||
const manifest = createCnbManifest(githubManifest, mirroredNames)
|
||||
const manifestPath = path.join(outputDirectory, 'latest.json')
|
||||
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`)
|
||||
await uploadAsset(release, manifestPath)
|
||||
|
||||
const prerelease = tag.includes('-')
|
||||
await apiRequest(`${apiEndpoint}/${repo}/-/releases/${release.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: githubRelease.name || `Axolotl Launcher ${tag}`,
|
||||
body: githubRelease.body || '',
|
||||
draft: false,
|
||||
prerelease,
|
||||
make_latest: isLatestRelease ? 'true' : 'false',
|
||||
}),
|
||||
})
|
||||
if (isLatestRelease) {
|
||||
publishUpdateBranch(manifestPath)
|
||||
} else {
|
||||
console.log(`Skipped update branch for non-latest GitHub release ${tag}`)
|
||||
}
|
||||
console.log(`Published CNB release ${tag}`)
|
||||
}
|
||||
|
||||
await finalizeRelease()
|
||||
Reference in New Issue
Block a user