Files
Ncurator/scripts/release/r2/finalize-macos.mjs
T
hy 7cd3782f1e Refactor macOS release handling: remove x64 support and streamline metadata processing
- Removed support for macos-x64 in fragment definitions and related scripts.
- Updated finalize-macos script to only handle macos-arm64, simplifying signature checks and metadata writing.
- Deleted merge-mac-metadata script as it is no longer needed.
- Adjusted storage layout and release status checks to reflect the removal of x64.
- Updated tests to ensure they only validate macos-arm64 functionality.
2026-09-01 16:36:09 +08:00

194 lines
6.2 KiB
JavaScript

import { execFile } from 'node:child_process'
import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'
import process from 'node:process'
import { promisify } from 'node:util'
import { assertReleaseFragment } from './fragment-contract.mjs'
import { fetchWithRetry } from '../shared/http.mjs'
import { createReleaseStorageClient } from '../storage/client.mjs'
import { artifactKey, fixedDownloadKey, statusKey } from './storage-layout.mjs'
import { parseReleaseTag } from '../tag.mjs'
const execFileAsync = promisify(execFile)
const ROOT = process.cwd()
const args = process.argv.slice(2)
const tagIndex = args.indexOf('--tag')
const releaseTag = tagIndex >= 0 ? args[tagIndex + 1] || '' : ''
let releaseVersion = ''
try {
releaseVersion = parseReleaseTag(releaseTag, 'mac').version
} catch {
throw new Error('usage: node scripts/release/r2/finalize-macos.mjs --tag <mac-v1.2.3>')
}
const { aws, bucket, publicBaseUrl, listKeys, deleteKeys, label } =
await createReleaseStorageClient(ROOT)
const workDirectory = await mkdtemp(path.join(os.tmpdir(), `ncurator-macos-${releaseTag}-`))
const downloadAndVerifyFragment = async (platform) => {
const target = path.join(workDirectory, `${platform}.json`)
await aws([
's3',
'cp',
`s3://${bucket}/${statusKey(releaseTag, platform)}`,
target,
'--no-progress'
])
const fragment = JSON.parse(await readFile(target, 'utf8'))
assertReleaseFragment(fragment, { tag: releaseTag, platform })
for (const file of fragment.files) {
const key = artifactKey(fragment, file.storageName)
const remote = JSON.parse(
(await aws(['s3api', 'head-object', '--bucket', bucket, '--key', key, '--output', 'json']))
.stdout
)
if (Number(remote.ContentLength) !== file.size || remote.Metadata?.sha512 !== file.sha512) {
throw new Error(`${label} object does not match release fragment: ${key}`)
}
}
return fragment
}
try {
const arm64 = await downloadAndVerifyFragment('macos-arm64')
const metadataInput = path.join(workDirectory, 'metadata-input')
const metadataOutput = path.join(workDirectory, 'metadata-output')
await mkdir(metadataInput, { recursive: true })
await writeFile(path.join(metadataInput, 'latest-mac.yml'), arm64.metadata, 'utf8')
await execFileAsync(process.execPath, [
path.join(ROOT, 'scripts', 'release', 'r2', 'metadata.mjs'),
metadataInput,
metadataOutput,
releaseTag
])
await aws([
's3',
'cp',
path.join(metadataOutput, 'latest-mac.yml'),
`s3://${bucket}/updates/latest/latest-mac.yml`,
'--content-type',
'application/yaml',
'--cache-control',
'no-store',
'--no-progress'
])
const installer = arm64.files.find((file) => file.role === 'installer')
const sourceKey = artifactKey(arm64, installer.storageName)
const downloadKey = fixedDownloadKey(arm64.platform)
const downloadName = `Ncurator-${arm64.version}-macos-arm64.dmg`
await aws([
's3api',
'copy-object',
'--bucket',
bucket,
'--copy-source',
`${bucket}/${sourceKey}`,
'--key',
downloadKey,
'--content-type',
'application/octet-stream',
'--cache-control',
'no-store',
'--content-disposition',
`attachment; filename="${downloadName}"`,
'--metadata-directive',
'REPLACE',
'--metadata',
`sha512=${installer.sha512}`
])
const download = {
name: downloadName,
url: `${publicBaseUrl}/${downloadKey}`,
size: installer.size,
sha512: installer.sha512
}
const manifest = {
schemaVersion: 2,
platform: 'macos',
version: arm64.version,
tag: arm64.tag,
publishedAt: new Date().toISOString(),
downloads: { arm64: download }
}
const manifestPath = path.join(workDirectory, 'latest.json')
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8')
await aws([
's3',
'cp',
manifestPath,
`s3://${bucket}/releases/latest/macos.json`,
'--content-type',
'application/json',
'--cache-control',
'no-store',
'--no-progress'
])
const metadataResponse = await fetchWithRetry(
`${publicBaseUrl}/updates/latest/latest-mac.yml?check=${Date.now()}`,
{ cache: 'no-store' }
)
const metadata = metadataResponse.ok ? await metadataResponse.text() : ''
if (
!metadata.includes(`version: ${releaseVersion}`) ||
!metadata.includes(`artifacts/macos/${releaseTag}/Ncurator-mac-arm64.zip`)
) {
throw new Error('public latest-mac.yml does not match the Mac release')
}
const response = await fetchWithRetry(`${publicBaseUrl}/${downloadKey}?check=${Date.now()}`, {
method: 'HEAD',
cache: 'no-store'
})
if (
!response.ok ||
Number(response.headers.get('content-length')) !== installer.size ||
!String(response.headers.get('content-disposition') || '').includes(
`filename="${downloadName}"`
)
) {
throw new Error(`public fixed macOS download verification failed: ${downloadKey}`)
}
const removable = (await listKeys('artifacts/macos/')).filter(
(key) => !key.startsWith(`artifacts/macos/${releaseTag}/`)
)
const activeArtifactKeys = new Set(
arm64.files.map((file) => artifactKey(arm64, file.storageName))
)
removable.push(
...(await listKeys(`artifacts/macos/${releaseTag}/`)).filter(
(key) => !activeArtifactKeys.has(key)
)
)
removable.push(
...(await listKeys('release-status/macos/')).filter(
(key) => !key.startsWith(`release-status/macos/${releaseTag}/`)
)
)
const activeStatusKey = statusKey(releaseTag, arm64.platform)
removable.push(
...(await listKeys(`release-status/macos/${releaseTag}/`)).filter(
(key) => key !== activeStatusKey
)
)
removable.push(
...(await listKeys('downloads/latest/')).filter(
(key) => key.startsWith('downloads/latest/macos-') && key !== downloadKey
)
)
removable.push(
...(await listKeys('artifacts/v')).filter((key) => /\/Ncurator-mac-[^/]+$/u.test(key))
)
removable.push(
...(await listKeys('release-status/v')).filter((key) => /\/macos-arm64\.json$/u.test(key))
)
await deleteKeys(removable)
process.stdout.write(`[release] macOS arm64 ${releaseTag} is live\n`)
} finally {
await rm(workDirectory, { recursive: true, force: true })
}