Files

161 lines
5.5 KiB
JavaScript

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 { assertReleaseFragment } from './fragment-contract.mjs'
import { artifactKey, artifactPrefix, fragmentKey, legacyStatusPrefix } from './storage-layout.mjs'
import { createReleaseStorageClient } from '../storage/client.mjs'
import { fetchWithRetry } from '../shared/http.mjs'
import { parseReleaseTag } from '../tag.mjs'
import { isReleaseChannelCurrent } from '../shared/live-tag.mjs'
const ROOT = process.cwd()
const args = process.argv.slice(2)
const tagIndex = args.indexOf('--tag')
const releaseTag = tagIndex >= 0 ? args[tagIndex + 1] || '' : ''
try {
parseReleaseTag(releaseTag, 'windows')
} catch {
throw new Error('usage: node scripts/release/r2/finalize-windows.mjs --tag <windows-v1.2.3>')
}
const { aws, bucket, publicBaseUrl, listKeys, deleteKeys, label } =
await createReleaseStorageClient(ROOT)
const workDirectory = await mkdtemp(path.join(os.tmpdir(), `ncurator-windows-${releaseTag}-`))
try {
const fragmentPath = path.join(workDirectory, 'windows-x64.json')
await aws([
's3',
'cp',
`s3://${bucket}/${fragmentKey(releaseTag, 'windows-x64')}`,
fragmentPath,
'--no-progress'
])
const fragment = JSON.parse(await readFile(fragmentPath, 'utf8'))
assertReleaseFragment(fragment, { tag: releaseTag, platform: 'windows-x64' })
for (const file of fragment.files) {
const key = artifactKey(fragment, file.name)
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}`)
}
}
await isReleaseChannelCurrent(fragment, publicBaseUrl)
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.yml'), fragment.metadata, 'utf8')
const { execFile } = await import('node:child_process')
await new Promise((resolve, reject) => {
const child = execFile(process.execPath, [
path.join(ROOT, 'scripts', 'release', 'r2', 'metadata.mjs'),
metadataInput,
metadataOutput,
releaseTag,
fragmentPath
])
child.once('error', reject)
child.once('exit', (code) =>
code === 0 ? resolve() : reject(new Error(`metadata preparation exited with ${code}`))
)
})
await aws([
's3',
'cp',
path.join(metadataOutput, 'latest.yml'),
`s3://${bucket}/updates/latest/latest.yml`,
'--content-type',
'application/yaml',
'--cache-control',
'no-store',
'--no-progress'
])
const installer = fragment.files.find((file) => file.role === 'installer')
const installerKey = artifactKey(fragment, installer.name)
const installerUrl = `${publicBaseUrl}/${installerKey}`
const manifest = {
schemaVersion: 2,
platform: 'windows',
version: fragment.version,
tag: fragment.tag,
publishedAt: new Date().toISOString(),
downloads: {
x64: {
name: installer.name,
url: installerUrl,
size: installer.size,
sha512: installer.sha512
}
}
}
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/windows.json`,
'--content-type',
'application/json',
'--cache-control',
'no-store',
'--no-progress'
])
const [metadataResponse, installerResponse, manifestResponse] = await Promise.all([
fetchWithRetry(`${publicBaseUrl}/updates/latest/latest.yml?check=${Date.now()}`, {
cache: 'no-store'
}),
fetchWithRetry(`${installerUrl}?check=${Date.now()}`, {
method: 'HEAD',
cache: 'no-store'
}),
fetchWithRetry(`${publicBaseUrl}/releases/latest/windows.json?check=${Date.now()}`, {
cache: 'no-store'
})
])
if (
!metadataResponse.ok ||
!(await metadataResponse.text()).includes(`version: ${fragment.version}`)
) {
throw new Error('public Windows update metadata verification failed')
}
if (
!installerResponse.ok ||
Number(installerResponse.headers.get('content-length')) !== installer.size
) {
throw new Error('public Windows artifact download verification failed')
}
if (
!installerUrl.endsWith(`/${encodeURI(installer.name)}`) &&
!installerUrl.endsWith(`/${installer.name}`)
) {
throw new Error(`Windows installer URL must end with the user-visible name: ${installerUrl}`)
}
const publicManifest = manifestResponse.ok ? await manifestResponse.json() : undefined
if (
publicManifest?.version !== fragment.version ||
publicManifest?.downloads?.x64?.name !== installer.name ||
publicManifest?.downloads?.x64?.url !== installerUrl
) {
throw new Error('public Windows release manifest verification failed')
}
const removable = (await listKeys(artifactPrefix('windows'))).filter(
(key) => !key.startsWith(artifactPrefix('windows', releaseTag))
)
removable.push(...(await listKeys(legacyStatusPrefix('windows'))))
await deleteKeys(removable)
process.stdout.write(`[release] Windows ${releaseTag} is live at ${installerUrl}\n`)
} finally {
await rm(workDirectory, { recursive: true, force: true })
}