mirror of
https://github.com/hansjone/netxops.git
synced 2026-10-20 13:30:46 +08:00
Harden public package installation and release validation
This commit is contained in:
parent
9271494b37
commit
9b2166e5b6
41 changed files with 994 additions and 363 deletions
94
scripts/verify-release.mjs
Normal file
94
scripts/verify-release.mjs
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
/** Publisher check. No install lifecycle invokes this on a user's machine. */
|
||||
import assert from 'node:assert/strict'
|
||||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import { isBuiltin } from 'node:module'
|
||||
import { resolve } from 'node:path'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import vm from 'node:vm'
|
||||
import { load } from 'js-yaml'
|
||||
import ts from 'typescript'
|
||||
import { checkBuilds, root } from './build-integrity.mjs'
|
||||
|
||||
export function verifyRelease(directory = root) {
|
||||
const read = path => readFileSync(resolve(directory, path), 'utf8')
|
||||
const pkg = JSON.parse(read('package.json'))
|
||||
const outputs = checkBuilds(directory)
|
||||
assert.equal(pkg.name, 'dsh-netxops')
|
||||
assert.match(pkg.version, /^\d+\.\d+\.\d+$/)
|
||||
assert.equal(pkg.type, 'module')
|
||||
for (const script of ['preinstall', 'install', 'postinstall', 'prepare', 'prepack', 'postpack', 'prepublish']) {
|
||||
assert.ok(!pkg.scripts?.[script], `User installs must not run ${script}.`)
|
||||
}
|
||||
assert.deepEqual(pkg.dependencies, {}, 'Runtime third-party dependencies must be bundled.')
|
||||
for (const [name, range] of Object.entries(pkg.peerDependencies)) {
|
||||
assert.equal(pkg.peerDependenciesMeta[name]?.optional, true, `${name} must be supplied by Harness.`)
|
||||
assert.doesNotMatch(range, /workspace:|file:|link:|github:/)
|
||||
}
|
||||
for (const path of Object.values(pkg.exports)) {
|
||||
assert.ok(path.startsWith('./') && !path.includes('..'), `Unsafe export: ${path}`)
|
||||
if (!path.includes('*')) assert.ok(existsSync(resolve(directory, path)), `Missing export: ${path}`)
|
||||
}
|
||||
assert.ok(read(pkg.icon).includes('<svg'), 'Missing plugin icon.')
|
||||
for (const lang of ['en', 'zh']) {
|
||||
const locale = JSON.parse(read(`locale/${lang}.json`))
|
||||
assert.ok(locale.meta.title && locale.meta.description, `Missing ${lang} plugin metadata.`)
|
||||
}
|
||||
assert.equal(pkg.dsh.client.immediately, false)
|
||||
const patch = load(read(pkg.dsh.bundle.patch))
|
||||
assert.ok(Array.isArray(patch) && patch.length > 0)
|
||||
const hosts = patch.flatMap(row => row.insert ?? []).filter(row => row.name === pkg.name)
|
||||
assert.equal(hosts.length, 1, 'Bundle must insert exactly one Host bridge.')
|
||||
assert.equal(hosts[0].id, 'netxops')
|
||||
const declared = new Set(Object.keys(pkg.peerDependencies))
|
||||
for (const output of outputs) {
|
||||
const text = read(output)
|
||||
const parsed = spawnSync(process.execPath, ['--check', resolve(directory, output)], { encoding: 'utf8', windowsHide: true })
|
||||
assert.equal(parsed.status, 0, `Invalid JS in ${output}: ${parsed.stderr}`)
|
||||
assert.doesNotMatch(text, /\bBun\.|react\/jsx-dev-runtime|agent-tools-shared|(?:from|import\()\s*["']\.\//)
|
||||
if (output !== 'lib/client.js') {
|
||||
const source = ts.createSourceFile(output, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS)
|
||||
const visit = node => {
|
||||
const specifier = ts.isImportDeclaration(node) || ts.isExportDeclaration(node)
|
||||
? node.moduleSpecifier
|
||||
: ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword ? node.arguments[0] : undefined
|
||||
if (specifier && ts.isStringLiteral(specifier)) {
|
||||
assert.ok(isBuiltin(specifier.text) || declared.has(specifier.text), `${output} needs undeclared dependency ${specifier.text}`)
|
||||
}
|
||||
ts.forEachChild(node, visit)
|
||||
}
|
||||
visit(source)
|
||||
}
|
||||
}
|
||||
let client
|
||||
vm.runInNewContext(read('lib/client.js'), { window: { __ModuleLoader__: { load(value) { client = value } } } })
|
||||
assert.equal(client.id, pkg.name)
|
||||
assert.equal(typeof client.factory, 'function')
|
||||
const clientExports = client.factory(name => {
|
||||
assert.ok(['react', 'react/jsx-runtime'].includes(name), `Unseeded browser dependency: ${name}`)
|
||||
return {}
|
||||
})
|
||||
assert.equal(typeof clientExports.apply, 'function')
|
||||
assert.deepEqual(Array.from(clientExports.inject), ['slots', 'locale'])
|
||||
for (const group of ['ops/netx-ops', 'topology/netx-topology', 'biz-monitor/netx-biz-monitor']) {
|
||||
const skill = read(`presets/netxops/skills/${group}/SKILL.md`).replace(/\r\n/g, '\n')
|
||||
const frontmatter = /^---\n([\s\S]*?)\n---/.exec(skill)
|
||||
assert.ok(frontmatter, `Missing skill frontmatter: ${group}`)
|
||||
const parsed = load(frontmatter[1])
|
||||
assert.equal(typeof parsed.name, 'string')
|
||||
assert.equal(typeof parsed.description, 'string')
|
||||
assert.ok(skill.slice(frontmatter[0].length).trim())
|
||||
}
|
||||
for (const path of ['presets/netxops/PERSONA.md', 'presets/netxops/preset.yml']) assert.ok(read(path).trim())
|
||||
return { version: pkg.version, outputs }
|
||||
}
|
||||
|
||||
if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
|
||||
try {
|
||||
const result = verifyRelease()
|
||||
console.log(`Release ${result.version}: ${result.outputs.length} JS artifacts, bundle and skills verified.`)
|
||||
} catch (error) {
|
||||
console.error(error.message)
|
||||
process.exitCode = 1
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue