Files
Ncurator/scripts/release/r2/finalize-macos.mjs
T
Yoan Huang d4974286ef refactor: update test cases to improve structure and clarity
- Remove obsolete end-to-end test for system notification session navigation.
- Enhance workspace scan progress tests to verify UI state restoration.
- Update web prepare progress tests to ensure correct UI behavior after navigation.
- Refactor unit tests for AI persistence and model configuration to improve naming consistency.
- Modify release tests to use localized product names and ensure correct artifact naming.
- Adjust vector and workspace backend tests to reflect unit test categorization.
- Improve error handling in web crawler test utilities.
- Update Electron app launch environment to support additional environment variables.
- Refactor end-to-end test cases for workbench to improve readability and maintainability.
2026-07-25 11:22:23 +08:00

203 lines
6.8 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 { createR2Client } from './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 } = await createR2Client(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(`R2 object does not match release fragment: ${key}`)
}
}
return fragment
}
try {
const [arm64, x64] = await Promise.all([
downloadAndVerifyFragment('macos-arm64'),
downloadAndVerifyFragment('macos-x64')
])
if (!arm64.signature || arm64.signature !== x64.signature) {
throw new Error('macOS arm64 and x64 release signatures do not match')
}
const metadataInput = path.join(workDirectory, 'metadata-input')
const metadataOutput = path.join(workDirectory, 'metadata-output')
await mkdir(path.join(metadataInput, 'mac-arm64'), { recursive: true })
await mkdir(path.join(metadataInput, 'mac-x64'), { recursive: true })
await writeFile(path.join(metadataInput, 'mac-arm64', 'latest-mac.yml'), arm64.metadata, 'utf8')
await writeFile(path.join(metadataInput, 'mac-x64', 'latest-mac.yml'), x64.metadata, 'utf8')
await execFileAsync(process.execPath, [
path.join(ROOT, 'scripts', 'release', 'r2', 'merge-mac-metadata.mjs'),
metadataInput
])
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 installers = new Map()
for (const fragment of [arm64, x64]) {
const installer = fragment.files.find((file) => file.role === 'installer')
const sourceKey = artifactKey(fragment, installer.storageName)
const downloadKey = fixedDownloadKey(fragment.platform)
const downloadName =
fragment.platform === 'macos-arm64'
? `Ncurator-${fragment.version}-macos-arm64.dmg`
: `Ncurator-${fragment.version}-macos-x64.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}`
])
installers.set(fragment.platform, { installer, downloadKey, downloadName })
}
const toDownload = (platform) => {
const { installer, downloadKey, downloadName } = installers.get(platform)
return {
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: toDownload('macos-arm64'), x64: toDownload('macos-x64') }
}
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`) ||
!metadata.includes(`artifacts/macos/${releaseTag}/Ncurator-mac-x64.zip`)
) {
throw new Error('public latest-mac.yml does not match the Mac release')
}
for (const { installer, downloadKey, downloadName } of installers.values()) {
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}/`)
)
removable.push(
...(await listKeys('release-status/macos/')).filter(
(key) => !key.startsWith(`release-status/macos/${releaseTag}/`)
)
)
removable.push(
...(await listKeys('artifacts/v')).filter((key) => /\/Ncurator-mac-[^/]+$/u.test(key))
)
removable.push(
...(await listKeys('release-status/v')).filter((key) =>
/\/macos-(?:arm64|x64)\.json$/u.test(key)
)
)
await deleteKeys(removable)
process.stdout.write(
`[release] macOS ${releaseTag} is live on independent fixed download channels\n`
)
} finally {
await rm(workDirectory, { recursive: true, force: true })
}