feat: ship skill and MCP workflows with signed updater for v0.0.7
This commit is contained in:
1 parent
1b52df0603
commit
a01085c656
136 files changed
+18646
-217
No files matched your search
@@ -0,0 +1,591 @@
|
||||
import crypto from 'node:crypto'
|
||||
import fsp from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import {
|
||||
DEFAULT_BASE_URL,
|
||||
DEFAULT_PRIVATE_KEY,
|
||||
DEFAULT_PUBLIC_KEY,
|
||||
DEFAULT_SSH_HOST,
|
||||
assert,
|
||||
assertOutsideRepo,
|
||||
compareVersions,
|
||||
createPrivateKey,
|
||||
createPublicKey,
|
||||
normalizeBaseUrl,
|
||||
parseVersion,
|
||||
verifySignedManifest
|
||||
} from './lib.mjs'
|
||||
|
||||
export const STABLE_VERSION_RE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/
|
||||
export const RELEASE_LOCK_RELATIVE = path.join('.tmp', 'updates', 'release.lock')
|
||||
export const MAX_HTTP_MANIFEST_BYTES = 1024 * 1024
|
||||
|
||||
const CLI_VALUE_OPTIONS = new Set([
|
||||
'version',
|
||||
'notes-zh',
|
||||
'notes-en',
|
||||
'ssh-host',
|
||||
'base-url',
|
||||
'private-key'
|
||||
])
|
||||
|
||||
export class ReleaseError extends Error {
|
||||
constructor(stage, message, options = {}) {
|
||||
super(message, options)
|
||||
this.name = 'ReleaseError'
|
||||
this.stage = stage
|
||||
}
|
||||
}
|
||||
|
||||
export function toReleaseError(stage, error) {
|
||||
if (error instanceof ReleaseError) return error
|
||||
return new ReleaseError(stage, String(error?.message || error), { cause: error })
|
||||
}
|
||||
|
||||
async function runStage(stage, action) {
|
||||
try {
|
||||
return await action()
|
||||
} catch (error) {
|
||||
throw toReleaseError(stage, error)
|
||||
}
|
||||
}
|
||||
|
||||
export function validateStableVersion(value) {
|
||||
const version = String(value ?? '')
|
||||
assert(
|
||||
STABLE_VERSION_RE.test(version),
|
||||
'Release version must be a stable x.y.z version without a leading zero, prerelease or build metadata'
|
||||
)
|
||||
parseVersion(version)
|
||||
for (const segment of version.split('.').map(BigInt)) {
|
||||
assert(segment <= 65_535n, 'Release version segments must be between 0 and 65535 for Windows EXE metadata')
|
||||
}
|
||||
return version
|
||||
}
|
||||
|
||||
export function defaultReleaseNotes(version) {
|
||||
validateStableVersion(version)
|
||||
return {
|
||||
zh: `YCode ${version} 更新`,
|
||||
en: `YCode ${version} update`
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveVersionChoice({ version, prompt }) {
|
||||
if (version !== undefined) return validateStableVersion(version)
|
||||
const answer = await prompt()
|
||||
if (!answer || !answer.trim()) return null
|
||||
return validateStableVersion(answer.trim())
|
||||
}
|
||||
|
||||
export function parseReleaseArgs(argv, { cwd = process.cwd() } = {}) {
|
||||
const raw = {}
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const argument = argv[index]
|
||||
if (!argument.startsWith('--')) throw new Error(`Unexpected argument: ${argument}`)
|
||||
const equals = argument.indexOf('=')
|
||||
const name = argument.slice(2, equals === -1 ? undefined : equals)
|
||||
if (name === 'help' || name === 'resume') {
|
||||
assert(equals === -1, `--${name} does not accept a value`)
|
||||
assert(!(name in raw), `Option --${name} was provided more than once`)
|
||||
raw[name] = true
|
||||
continue
|
||||
}
|
||||
assert(CLI_VALUE_OPTIONS.has(name), `Unknown option: --${name}`)
|
||||
assert(!(name in raw), `Option --${name} was provided more than once`)
|
||||
const value = equals === -1 ? argv[++index] : argument.slice(equals + 1)
|
||||
assert(value !== undefined && value !== '', `Missing value for --${name}`)
|
||||
raw[name] = value
|
||||
}
|
||||
|
||||
if (raw.help) {
|
||||
return { help: true }
|
||||
}
|
||||
if (raw.resume) {
|
||||
assert(raw.version, '--resume requires an explicit --version')
|
||||
assert(!raw['notes-zh'] && !raw['notes-en'], '--notes-zh and --notes-en cannot be used with --resume')
|
||||
}
|
||||
|
||||
const version = raw.version === undefined ? undefined : validateStableVersion(raw.version)
|
||||
const notes = {}
|
||||
if (raw['notes-zh'] !== undefined) notes.zh = raw['notes-zh'].trim()
|
||||
if (raw['notes-en'] !== undefined) notes.en = raw['notes-en'].trim()
|
||||
if ('zh' in notes) assert(notes.zh, '--notes-zh must not be empty')
|
||||
if ('en' in notes) assert(notes.en, '--notes-en must not be empty')
|
||||
|
||||
return {
|
||||
help: false,
|
||||
resume: raw.resume === true,
|
||||
version,
|
||||
notes,
|
||||
sshHost: raw['ssh-host'] || DEFAULT_SSH_HOST,
|
||||
baseUrl: normalizeBaseUrl(raw['base-url'] || DEFAULT_BASE_URL),
|
||||
privateKeyPath: path.resolve(cwd, raw['private-key'] || DEFAULT_PRIVATE_KEY),
|
||||
publicKeyPath: path.resolve(cwd, DEFAULT_PUBLIC_KEY)
|
||||
}
|
||||
}
|
||||
|
||||
export function finalizeReleaseRequest(parsed, version) {
|
||||
validateStableVersion(version)
|
||||
const defaults = defaultReleaseNotes(version)
|
||||
return {
|
||||
...parsed,
|
||||
version,
|
||||
notes: {
|
||||
zh: parsed.notes?.zh ?? defaults.zh,
|
||||
en: parsed.notes?.en ?? defaults.en
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function releasePaths(root, version) {
|
||||
validateStableVersion(version)
|
||||
const outputDir = path.join(root, 'release', 'publish', version)
|
||||
return {
|
||||
root,
|
||||
packagePath: path.join(root, 'package.json'),
|
||||
lockPath: path.join(root, RELEASE_LOCK_RELATIVE),
|
||||
outputDir,
|
||||
installerPath: path.join(outputDir, `YCode-${version}-Setup-x64.exe`),
|
||||
manifestPath: path.join(outputDir, 'latest.json'),
|
||||
builderCli: path.join(root, 'node_modules', 'electron-builder', 'cli.js'),
|
||||
prepareScript: path.join(root, 'scripts', 'updates', 'prepare-release.mjs'),
|
||||
deployScript: path.join(root, 'scripts', 'updates', 'deploy.mjs')
|
||||
}
|
||||
}
|
||||
|
||||
function isOutside(root, filename) {
|
||||
const relative = path.relative(path.resolve(root), path.resolve(filename))
|
||||
return relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)
|
||||
}
|
||||
|
||||
async function canonicalPrivateKeyPath({ root, filename, deps }) {
|
||||
const lexical = path.resolve(filename)
|
||||
assert(isOutside(root, lexical), `Private key must be outside the repository: ${lexical}`)
|
||||
await assertOutsideRepo(lexical, 'Private key')
|
||||
if (!(await deps.pathExists(lexical))) throw new Error(`Signing key not found: ${lexical}`)
|
||||
const canonical = await deps.realpath(lexical)
|
||||
assert(isOutside(root, canonical), `Private key resolves inside the repository: ${canonical}`)
|
||||
await assertOutsideRepo(canonical, 'Private key')
|
||||
return canonical
|
||||
}
|
||||
|
||||
export function assertKeyPairMatches(privatePem, publicPem) {
|
||||
const privateKey = createPrivateKey(privatePem)
|
||||
const derived = createPublicKey(privateKey).export({ type: 'spki', format: 'der' })
|
||||
const supplied = createPublicKey(publicPem).export({ type: 'spki', format: 'der' })
|
||||
assert(derived.equals(supplied), 'Signing private key does not match the update public key')
|
||||
}
|
||||
|
||||
function parseJson(text, label) {
|
||||
try {
|
||||
return JSON.parse(text)
|
||||
} catch (error) {
|
||||
throw new Error(`${label} is not valid JSON: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
function jsonFormat(text) {
|
||||
const newline = text.includes('\r\n') ? '\r\n' : '\n'
|
||||
const indentMatch = text.match(/\n([ \t]+)"/)
|
||||
return {
|
||||
indent: indentMatch?.[1] || ' ',
|
||||
newline,
|
||||
trailingNewline: text.endsWith('\n')
|
||||
}
|
||||
}
|
||||
|
||||
function rewriteJson(text, mutate) {
|
||||
const value = parseJson(text, 'JSON document')
|
||||
mutate(value)
|
||||
const format = jsonFormat(text)
|
||||
let serialized = JSON.stringify(value, null, format.indent).replace(/\n/g, format.newline)
|
||||
if (format.trailingNewline) serialized += format.newline
|
||||
return serialized
|
||||
}
|
||||
|
||||
export function updatePackageText(text, version) {
|
||||
validateStableVersion(version)
|
||||
return rewriteJson(text, (document) => {
|
||||
assert(document.name === 'ycode', 'package.json is not the YCode package')
|
||||
assert(document.build?.appId === 'com.ycode.app', 'package.json appId is not com.ycode.app')
|
||||
document.version = version
|
||||
})
|
||||
}
|
||||
|
||||
export function updateLockText(text, version) {
|
||||
validateStableVersion(version)
|
||||
return rewriteJson(text, (document) => {
|
||||
assert(document && typeof document === 'object', 'package-lock.json must be an object')
|
||||
assert(
|
||||
document.packages && typeof document.packages === 'object',
|
||||
'package-lock.json has no root package entry'
|
||||
)
|
||||
assert(
|
||||
document.packages[''] && typeof document.packages[''] === 'object',
|
||||
'package-lock.json has no packages[""] entry'
|
||||
)
|
||||
document.version = version
|
||||
document.packages[''].version = version
|
||||
})
|
||||
}
|
||||
|
||||
function packageVersions(packageText, lockText) {
|
||||
const pkg = parseJson(packageText, 'package.json')
|
||||
const lock = parseJson(lockText, 'package-lock.json')
|
||||
assert(pkg.name === 'ycode', 'package.json is not the YCode package')
|
||||
assert(pkg.build?.appId === 'com.ycode.app', 'package.json appId is not com.ycode.app')
|
||||
const version = validateStableVersion(pkg.version)
|
||||
assert(
|
||||
lock.version !== undefined && lock.packages?.['']?.version !== undefined,
|
||||
'package-lock.json must contain top-level and packages[""] versions'
|
||||
)
|
||||
assert(lock.version === version, 'package-lock.json top-level version does not match package.json')
|
||||
assert(lock.packages[''].version === version, 'package-lock.json root version does not match package.json')
|
||||
return { pkg, lock, version }
|
||||
}
|
||||
|
||||
function sameManifest(left, right) {
|
||||
return JSON.stringify(left) === JSON.stringify(right)
|
||||
}
|
||||
|
||||
function parseSignedManifest(text, publicKeyPem, baseUrl, label) {
|
||||
const document = parseJson(text, label)
|
||||
verifySignedManifest(document, publicKeyPem, { baseUrl })
|
||||
return document
|
||||
}
|
||||
|
||||
export async function preflightRelease({ request, paths, deps }) {
|
||||
const publicKeyPath = path.resolve(request.publicKeyPath)
|
||||
const privateKeyPath = await canonicalPrivateKeyPath({
|
||||
root: paths.root,
|
||||
filename: request.privateKeyPath,
|
||||
deps
|
||||
})
|
||||
|
||||
const privatePem = await deps.readPrivateKey(privateKeyPath)
|
||||
const publicPem = await deps.readPublicKey(publicKeyPath)
|
||||
assertKeyPairMatches(privatePem, publicPem)
|
||||
|
||||
const packageLockPath = path.join(paths.root, 'package-lock.json')
|
||||
for (const [label, filename] of [
|
||||
['package.json', paths.packagePath],
|
||||
['package-lock.json', packageLockPath]
|
||||
]) {
|
||||
assert(await deps.pathExists(filename), `${label} not found: ${filename}`)
|
||||
}
|
||||
|
||||
const packageText = await deps.readFile(paths.packagePath, 'utf8')
|
||||
const lockText = await deps.readFile(packageLockPath, 'utf8')
|
||||
const versions = packageVersions(packageText, lockText)
|
||||
|
||||
if (request.resume) {
|
||||
assert(
|
||||
versions.version === request.version,
|
||||
`--resume requires repository version ${request.version}; found ${versions.version}`
|
||||
)
|
||||
assert(await deps.pathExists(paths.outputDir), `Resume output directory not found: ${paths.outputDir}`)
|
||||
assert(await deps.pathExists(paths.installerPath), `Resume installer not found: ${paths.installerPath}`)
|
||||
assert(await deps.pathExists(paths.manifestPath), `Resume manifest not found: ${paths.manifestPath}`)
|
||||
} else {
|
||||
const compared = compareVersions(request.version, versions.version)
|
||||
assert(
|
||||
compared >= 0,
|
||||
`Refusing to release ${request.version}; repository version is newer (${versions.version})`
|
||||
)
|
||||
assert(
|
||||
!(await deps.pathExists(paths.outputDir)),
|
||||
`Refusing to build an existing release directory: ${paths.outputDir}`
|
||||
)
|
||||
}
|
||||
|
||||
if (deps.checkToolchain) await deps.checkToolchain(paths, { resume: request.resume })
|
||||
await deps.checkSsh(request.sshHost)
|
||||
const current = await deps.readLatest({
|
||||
host: request.sshHost,
|
||||
publicKeyPem: publicPem,
|
||||
baseUrl: request.baseUrl
|
||||
})
|
||||
|
||||
let currentVersion = null
|
||||
if (current?.document) {
|
||||
verifySignedManifest(current.document, publicPem, { baseUrl: request.baseUrl })
|
||||
currentVersion = current.document.payload.version
|
||||
const compared = compareVersions(request.version, currentVersion)
|
||||
if (request.resume) {
|
||||
assert(
|
||||
compared >= 0,
|
||||
`Refusing resume for ${request.version}; server latest is newer (${currentVersion})`
|
||||
)
|
||||
} else {
|
||||
assert(
|
||||
compared > 0,
|
||||
`Release version ${request.version} must be strictly newer than server latest ${currentVersion}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let localManifestText = null
|
||||
let localManifest = null
|
||||
if (request.resume) {
|
||||
localManifestText = await deps.readFile(paths.manifestPath, 'utf8')
|
||||
localManifest = parseSignedManifest(localManifestText, publicPem, request.baseUrl, 'Local latest.json')
|
||||
assert(
|
||||
localManifest.payload.version === request.version,
|
||||
`Resume manifest version ${localManifest.payload.version} does not match ${request.version}`
|
||||
)
|
||||
if (currentVersion === request.version) {
|
||||
assert(
|
||||
sameManifest(current.document, localManifest),
|
||||
`Server already has a different ${request.version} manifest`
|
||||
)
|
||||
}
|
||||
const bundle = await deps.validateBundle({
|
||||
manifestPath: paths.manifestPath,
|
||||
installerPath: paths.installerPath,
|
||||
publicKeyPath,
|
||||
baseUrl: request.baseUrl
|
||||
})
|
||||
assert(
|
||||
bundle?.document?.payload?.version === request.version,
|
||||
`Revalidated bundle version does not match ${request.version}`
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
repoVersion: versions.version,
|
||||
privateKeyPath,
|
||||
packageText,
|
||||
lockText,
|
||||
publicPem,
|
||||
currentVersion,
|
||||
currentRaw: current?.raw ?? null,
|
||||
localManifestText,
|
||||
localManifest
|
||||
}
|
||||
}
|
||||
|
||||
async function updateTargetVersions({ request, paths, preflight, deps }) {
|
||||
if (request.resume) return { changed: false }
|
||||
const nextPackageText = updatePackageText(preflight.packageText, request.version)
|
||||
const nextLockText = updateLockText(preflight.lockText, request.version)
|
||||
let changed = false
|
||||
if (nextPackageText !== preflight.packageText) {
|
||||
await deps.writeFileAtomic(paths.packagePath, nextPackageText)
|
||||
changed = true
|
||||
}
|
||||
if (nextLockText !== preflight.lockText) {
|
||||
await deps.writeFileAtomic(path.join(paths.root, 'package-lock.json'), nextLockText)
|
||||
changed = true
|
||||
}
|
||||
return { changed }
|
||||
}
|
||||
|
||||
export async function acquireReleaseLock(lockPath, { version } = {}) {
|
||||
await fsp.mkdir(path.dirname(lockPath), { recursive: true })
|
||||
const token = crypto.randomBytes(16).toString('hex')
|
||||
let handle
|
||||
let created = false
|
||||
try {
|
||||
handle = await fsp.open(lockPath, 'wx', 0o600)
|
||||
created = true
|
||||
await handle.writeFile(
|
||||
`${JSON.stringify({
|
||||
token,
|
||||
pid: process.pid,
|
||||
version: version || null,
|
||||
startedAt: new Date().toISOString()
|
||||
})}\n`,
|
||||
'utf8'
|
||||
)
|
||||
await handle.sync()
|
||||
} catch (error) {
|
||||
if (handle) await handle.close().catch(() => {})
|
||||
if (created) await fsp.rm(lockPath, { force: true }).catch(() => {})
|
||||
if (error?.code === 'EEXIST') {
|
||||
throw new Error(`Release lock already exists: ${lockPath}`)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
await handle.close()
|
||||
|
||||
let released = false
|
||||
return async function releaseLock() {
|
||||
if (released) return
|
||||
let raw
|
||||
try {
|
||||
raw = await fsp.readFile(lockPath, 'utf8')
|
||||
} catch (error) {
|
||||
if (error?.code === 'ENOENT') {
|
||||
released = true
|
||||
return
|
||||
}
|
||||
throw error
|
||||
}
|
||||
let record
|
||||
try {
|
||||
record = JSON.parse(raw)
|
||||
} catch {
|
||||
throw new Error(`Release lock was replaced or corrupted: ${lockPath}`)
|
||||
}
|
||||
assert(record.token === token, `Release lock ownership changed: ${lockPath}`)
|
||||
await fsp.rm(lockPath, { force: true })
|
||||
released = true
|
||||
}
|
||||
}
|
||||
|
||||
async function readResponseLimited(response, maxBytes) {
|
||||
const declared = Number(response.headers.get('content-length'))
|
||||
if (Number.isFinite(declared) && declared > maxBytes) {
|
||||
throw new Error(`latest.json exceeds ${maxBytes} bytes`)
|
||||
}
|
||||
if (!response.body) return Buffer.alloc(0)
|
||||
const reader = response.body.getReader()
|
||||
const chunks = []
|
||||
let total = 0
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
total += value.byteLength
|
||||
if (total > maxBytes) {
|
||||
await reader.cancel()
|
||||
throw new Error(`latest.json exceeds ${maxBytes} bytes`)
|
||||
}
|
||||
chunks.push(Buffer.from(value))
|
||||
}
|
||||
return Buffer.concat(chunks, total)
|
||||
}
|
||||
|
||||
export async function verifyPublishedManifest({
|
||||
baseUrl,
|
||||
expectedManifestText,
|
||||
publicKeyPem,
|
||||
version,
|
||||
fetchImpl = globalThis.fetch,
|
||||
timeoutMs = 30_000,
|
||||
maxBytes = MAX_HTTP_MANIFEST_BYTES
|
||||
}) {
|
||||
validateStableVersion(version)
|
||||
const normalizedBaseUrl = normalizeBaseUrl(baseUrl)
|
||||
assert(typeof fetchImpl === 'function', 'fetch is not available')
|
||||
const manifestUrl = new URL('latest.json', normalizedBaseUrl)
|
||||
const response = await fetchImpl(manifestUrl, {
|
||||
method: 'GET',
|
||||
cache: 'no-store',
|
||||
credentials: 'omit',
|
||||
redirect: 'error',
|
||||
referrerPolicy: 'no-referrer',
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
'cache-control': 'no-cache',
|
||||
pragma: 'no-cache'
|
||||
},
|
||||
signal: AbortSignal.timeout(timeoutMs)
|
||||
})
|
||||
assert(response.status === 200, `latest.json HTTP status is ${response.status}, expected 200`)
|
||||
const expected = Buffer.from(expectedManifestText, 'utf8')
|
||||
assert(expected.length <= maxBytes, 'Local signed manifest exceeds the HTTP verification limit')
|
||||
const actual = await readResponseLimited(response, maxBytes)
|
||||
assert(actual.equals(expected), 'HTTP latest.json does not exactly match the local signed manifest')
|
||||
const document = parseSignedManifest(actual.toString('utf8'), publicKeyPem, normalizedBaseUrl, 'HTTP latest.json')
|
||||
assert(document.payload.version === version, `HTTP latest.json version is ${document.payload.version}, expected ${version}`)
|
||||
return document
|
||||
}
|
||||
|
||||
function deployArgs({ request, paths }) {
|
||||
return [
|
||||
'--manifest',
|
||||
paths.manifestPath,
|
||||
'--installer',
|
||||
paths.installerPath,
|
||||
'--transport',
|
||||
'ssh',
|
||||
'--base-url',
|
||||
request.baseUrl,
|
||||
'--public-key',
|
||||
request.publicKeyPath,
|
||||
'--ssh-host',
|
||||
request.sshHost
|
||||
]
|
||||
}
|
||||
|
||||
export async function runReleaseWorkflow(request, deps) {
|
||||
validateStableVersion(request.version)
|
||||
const paths = releasePaths(deps.root, request.version)
|
||||
let releaseLock
|
||||
let primaryError = null
|
||||
|
||||
releaseLock = await runStage('lock', () => deps.acquireReleaseLock(paths.lockPath, { version: request.version }))
|
||||
assert(typeof releaseLock === 'function', 'Release lock must return a release function')
|
||||
|
||||
try {
|
||||
const preflight = await runStage('preflight', () => preflightRelease({ request, paths, deps }))
|
||||
if (!request.resume) {
|
||||
await runStage('version-update', () => updateTargetVersions({ request, paths, preflight, deps }))
|
||||
await runStage('test-updates', () => deps.runNpm(['run', 'test:updates']))
|
||||
await runStage('build', () => deps.runNpm(['run', 'build']))
|
||||
await runStage('package', () =>
|
||||
deps.runNode([
|
||||
paths.builderCli,
|
||||
'--win',
|
||||
'nsis',
|
||||
'--x64',
|
||||
'--publish',
|
||||
'never',
|
||||
`--config.directories.output=${path.relative(paths.root, paths.outputDir).replaceAll(path.sep, '/')}`
|
||||
])
|
||||
)
|
||||
await runStage('sign', () =>
|
||||
deps.runNode([
|
||||
paths.prepareScript,
|
||||
'--installer',
|
||||
paths.installerPath,
|
||||
'--version',
|
||||
request.version,
|
||||
'--notes-zh',
|
||||
request.notes.zh,
|
||||
'--notes-en',
|
||||
request.notes.en,
|
||||
'--base-url',
|
||||
request.baseUrl,
|
||||
'--private-key',
|
||||
preflight.privateKeyPath,
|
||||
'--public-key',
|
||||
request.publicKeyPath,
|
||||
'--output',
|
||||
paths.manifestPath
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
await runStage('deploy-dry-run', () => deps.runNode([paths.deployScript, ...deployArgs({ request, paths }), '--dry-run']))
|
||||
await runStage('deploy', () => deps.runNode([paths.deployScript, ...deployArgs({ request, paths })]))
|
||||
await runStage('verify-http', async () => {
|
||||
const expectedManifestText =
|
||||
preflight.localManifestText || (await deps.readFile(paths.manifestPath, 'utf8'))
|
||||
await deps.verifyHttp({
|
||||
baseUrl: request.baseUrl,
|
||||
expectedManifestText,
|
||||
publicKeyPem: preflight.publicPem,
|
||||
version: request.version
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
version: request.version,
|
||||
outputDir: paths.outputDir,
|
||||
installerPath: paths.installerPath,
|
||||
manifestPath: paths.manifestPath
|
||||
}
|
||||
} catch (error) {
|
||||
primaryError = toReleaseError('workflow', error)
|
||||
throw primaryError
|
||||
} finally {
|
||||
try {
|
||||
await releaseLock()
|
||||
} catch (error) {
|
||||
if (!primaryError) throw toReleaseError('lock-release', error)
|
||||
if (deps.logWarning) deps.logWarning(`Release lock cleanup failed: ${error.message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user