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,169 @@
|
||||
import { spawn } from 'node:child_process'
|
||||
import fs from 'node:fs'
|
||||
import fsp from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { createInterface } from 'node:readline/promises'
|
||||
import {
|
||||
DEFAULT_BASE_URL,
|
||||
REPO_ROOT,
|
||||
readPrivateKey,
|
||||
readPublicKey,
|
||||
runSsh,
|
||||
writeFileAtomic
|
||||
} from './lib.mjs'
|
||||
import {
|
||||
ReleaseError,
|
||||
acquireReleaseLock,
|
||||
finalizeReleaseRequest,
|
||||
parseReleaseArgs,
|
||||
resolveVersionChoice,
|
||||
runReleaseWorkflow,
|
||||
toReleaseError,
|
||||
verifyPublishedManifest
|
||||
} from './release-workflow.mjs'
|
||||
import { readLatestViaSsh, validateReleaseBundle } from './transport.mjs'
|
||||
|
||||
const usage = `Usage:
|
||||
npm run release
|
||||
npm run release -- --version <x.y.z> [--notes-zh <text>] [--notes-en <text>]
|
||||
npm run release -- --resume --version <x.y.z>
|
||||
npm run release -- --help
|
||||
|
||||
Options:
|
||||
--version <x.y.z> Stable Windows x64 version. Prompted when omitted.
|
||||
--notes-zh <text> Chinese release notes. Defaults to "YCode <version> 更新".
|
||||
--notes-en <text> English release notes. Defaults to "YCode <version> update".
|
||||
--resume Reuse an existing installer and signed manifest.
|
||||
--ssh-host <host> SSH host, default local001.
|
||||
--base-url <url> Release base URL, default ${DEFAULT_BASE_URL}.
|
||||
--private-key <path> Signing key, default in the current user profile.
|
||||
--help Show this help.
|
||||
|
||||
The workflow runs tests, builds, signs, deploys, and verifies latest.json.
|
||||
It does not create keys, roll back the server, or clean stale release locks.
|
||||
`
|
||||
|
||||
async function pathExists(filename) {
|
||||
try {
|
||||
await fsp.access(filename)
|
||||
return true
|
||||
} catch (error) {
|
||||
if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR') return false
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
function runExecutable(command, args, { cwd = REPO_ROOT } = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(command, args, {
|
||||
cwd,
|
||||
env: process.env,
|
||||
shell: false,
|
||||
stdio: 'inherit',
|
||||
windowsHide: true
|
||||
})
|
||||
child.once('error', reject)
|
||||
child.once('exit', (code, signal) => {
|
||||
if (code === 0) {
|
||||
resolve()
|
||||
} else {
|
||||
reject(new Error(`${path.basename(command)} exited with ${signal ? `signal ${signal}` : `code ${code}`}`))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function resolveNpmCli() {
|
||||
const candidates = [
|
||||
path.join(path.dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npm-cli.js'),
|
||||
process.env.npm_execpath
|
||||
].filter((candidate) => candidate && path.basename(candidate).toLowerCase() === 'npm-cli.js')
|
||||
const npmCli = candidates.find((candidate) => fs.existsSync(candidate))
|
||||
if (!npmCli) throw new Error('Unable to locate npm-cli.js; run this command through npm run release')
|
||||
return npmCli
|
||||
}
|
||||
|
||||
async function promptForVersion() {
|
||||
const readline = createInterface({ input: process.stdin, output: process.stdout })
|
||||
try {
|
||||
return await readline.question('Release version (stable x.y.z): ')
|
||||
} finally {
|
||||
readline.close()
|
||||
}
|
||||
}
|
||||
|
||||
function productionDependencies() {
|
||||
const npmCli = resolveNpmCli()
|
||||
return {
|
||||
root: REPO_ROOT,
|
||||
pathExists,
|
||||
realpath: fsp.realpath,
|
||||
readFile: fsp.readFile,
|
||||
writeFileAtomic,
|
||||
readPrivateKey,
|
||||
readPublicKey,
|
||||
validateBundle: validateReleaseBundle,
|
||||
acquireReleaseLock,
|
||||
checkToolchain: async (paths, { resume }) => {
|
||||
const required = [['deploy.mjs', paths.deployScript]]
|
||||
if (!resume) {
|
||||
required.unshift(
|
||||
['npm-cli.js', npmCli],
|
||||
['electron-builder CLI', paths.builderCli],
|
||||
['prepare-release.mjs', paths.prepareScript]
|
||||
)
|
||||
}
|
||||
for (const [label, filename] of required) {
|
||||
if (!fs.existsSync(filename)) throw new Error(`${label} not found: ${filename}`)
|
||||
}
|
||||
},
|
||||
checkSsh: async (host) => {
|
||||
runSsh(host, 'true\n')
|
||||
},
|
||||
readLatest: ({ host, publicKeyPem, baseUrl }) => readLatestViaSsh(host, publicKeyPem, baseUrl),
|
||||
runNpm: (args) => runExecutable(process.execPath, [npmCli, ...args]),
|
||||
runNode: (args) => runExecutable(process.execPath, args),
|
||||
verifyHttp: verifyPublishedManifest,
|
||||
logWarning: (message) => console.warn(message)
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
let parsed
|
||||
try {
|
||||
parsed = parseReleaseArgs(process.argv.slice(2))
|
||||
} catch (error) {
|
||||
throw toReleaseError('input', error)
|
||||
}
|
||||
if (parsed.help) {
|
||||
process.stdout.write(usage)
|
||||
return
|
||||
}
|
||||
|
||||
let version
|
||||
try {
|
||||
version = await resolveVersionChoice({ version: parsed.version, prompt: promptForVersion })
|
||||
} catch (error) {
|
||||
throw toReleaseError('prompt', error)
|
||||
}
|
||||
if (!version) {
|
||||
console.log('Release cancelled.')
|
||||
return
|
||||
}
|
||||
let request
|
||||
try {
|
||||
request = finalizeReleaseRequest(parsed, version)
|
||||
} catch (error) {
|
||||
throw toReleaseError('input', error)
|
||||
}
|
||||
const result = await runReleaseWorkflow(request, productionDependencies())
|
||||
console.log(`Release ${result.version} published and verified.`)
|
||||
console.log(`Installer: ${result.installerPath}`)
|
||||
console.log(`Manifest: ${result.manifestPath}`)
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
const releaseError = error instanceof ReleaseError ? error : toReleaseError('input', error)
|
||||
console.error(`Release failed during ${releaseError.stage}: ${releaseError.message}`)
|
||||
process.exitCode = 1
|
||||
})
|
||||
Reference in new issue
Block a user