| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150 |
- import { test } from 'node:test'
- import assert from 'node:assert/strict'
- import fs from 'node:fs'
- import os from 'node:os'
- import path from 'node:path'
- import zlib from 'node:zlib'
- import { createHash } from 'node:crypto'
- import { execFileSync, spawnSync } from 'node:child_process'
- import { root } from '../version.mjs'
- const hash = bytes => createHash('sha256').update(bytes).digest('hex')
- const version = '0.1.0-preview.4'
- const publishConfig = { access: 'public', tag: 'preview', registry: 'https://registry.npmjs.org/' }
- function archive(files) {
- const rows = Object.entries(files).map(([name, text]) => {
- const body = Buffer.from(typeof text === 'object' ? JSON.stringify(text) : text)
- const header = Buffer.alloc(512)
- header.write(`package/${name}`)
- header.write(body.length.toString(8).padStart(11, '0'), 124)
- header.write('0', 156)
- return Buffer.concat([header, body, Buffer.alloc((512 - body.length % 512) % 512)])
- })
- return zlib.gzipSync(Buffer.concat([...rows, Buffer.alloc(1024)]))
- }
- const commit = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8', windowsHide: true }).trim()
- /** Build release assets whose bytes and manifest match what the publisher validates. */
- function writeAssets(directory) {
- const entries = [
- ['@linfengqaqtat/dsh-scriptor', version, `linfengqaqtat-dsh-scriptor-${version}.tgz`],
- ['webnovel-embedding-provider', '0.0.8', 'webnovel-embedding-provider-0.0.8.tgz'],
- ['@linfengqaqtat/dsh-scriptor-full', version, `linfengqaqtat-dsh-scriptor-full-${version}.tgz`],
- ]
- const packages = entries.map(([name, v, tarball]) => ({ name, version: v, tarball }))
- for (const [index, item] of packages.entries()) {
- const manifest = { name: item.name, version: item.version, license: 'GPL-3.0-only', publishConfig,
- repository: { url: 'https://github.com/lingfengQAQ/webnovel-writer.git' } }
- const files = { 'package.json': manifest, LICENSE: 'GNU GENERAL PUBLIC LICENSE', 'README.md': 'Example' }
- if (index === 1) Object.assign(files, { 'lib/index.js': '', 'lib/client.js': `window.__ModuleLoader__.load({ id: ${JSON.stringify(item.name)},`, 'cordis.patch.yml': '', 'MODEL_DIMENSIONS.md': '', 'THIRD_PARTY_NOTICES.md': 'Original notices' })
- if (index === 2) {
- manifest.dependencies = { '@linfengqaqtat/dsh-scriptor': version, 'webnovel-embedding-provider': '0.0.8' }
- manifest.dsh = { bundle: { patch: './cordis.patch.yml' } }
- files['cordis.patch.yml'] = fs.readFileSync(path.join(root, 'packages/meta/cordis.patch.yml'), 'utf8')
- }
- fs.writeFileSync(path.join(directory, item.tarball), archive(files))
- }
- const manifest = { schemaVersion: 1, packageName: packages[0].name, version, tag: `scriptor-v${version}`, publicCommit: commit, prerelease: true,
- filename: packages[0].tarball, optionalPackages: packages.slice(1),
- assets: packages.map(item => ({ file: item.tarball, sha256: hash(fs.readFileSync(path.join(directory, item.tarball))) })) }
- fs.writeFileSync(path.join(directory, 'release-manifest.json'), JSON.stringify(manifest))
- const files = [...manifest.assets, { file: 'release-manifest.json', sha256: hash(fs.readFileSync(path.join(directory, 'release-manifest.json'))) }]
- fs.writeFileSync(path.join(directory, 'SHA256SUMS'), files.map(item => `${item.sha256} ${item.file}`).join('\n') + '\n')
- return { manifest, packages }
- }
- const fixturePaths = {
- runner: path.join(root, 'scripts/release/tests/fixtures/publish-path-run.mjs'),
- publisher: path.join(root, 'scripts/release/publish-npm.mjs'),
- stub: path.join(root, 'scripts/release/tests/fixtures/stub-npm.mjs'),
- }
- /** Run the real publisher against the stub npm CLI and a first-publication registry. */
- function runPublisher({ assets, fixture, mode = '--publish', environment = {}, keepState = false }) {
- const result = spawnSync(process.execPath, [fixturePaths.runner, assets, fixture, ...mode.split(' ')], {
- cwd: root,
- encoding: 'utf8',
- windowsHide: true,
- timeout: 120000,
- env: {
- ...process.env,
- RELEASE_TAG: `scriptor-v${version}`,
- SCRIPTOR_PUBLISHER: fixturePaths.publisher,
- SCRIPTOR_STUB_NPM: fixturePaths.stub,
- ...(keepState ? { SCRIPTOR_KEEP_STATE: '1' } : {}),
- ...environment,
- },
- })
- assert.equal(result.status, 0, `publish-path fixture failed: ${result.stdout}${result.stderr}`)
- const report = JSON.parse(result.stdout.trim().split('\n').pop())
- const invocations = fs.readFileSync(path.join(fixture, 'invocations.jsonl'), 'utf8').trim().split('\n').filter(Boolean).map(line => JSON.parse(line))
- return { report, invocations }
- }
- function withFixture(callback) {
- const base = fs.mkdtempSync(path.join(os.tmpdir(), 'scriptor-publish-path-'))
- const assets = path.join(base, 'assets')
- const fixture = path.join(base, 'fixture')
- fs.mkdirSync(assets)
- const { manifest, packages } = writeAssets(assets)
- try {
- return callback({ base, assets, fixture, manifest, packages })
- } finally {
- fs.rmSync(base, { recursive: true, force: true })
- }
- }
- test('publish path preflights every package, then uploads the same tarballs in dependency order with provenance', () => withFixture(({ assets, fixture, packages }) => {
- const { invocations } = runPublisher({ assets, fixture })
- const dryRuns = invocations.filter(item => item.dryRun)
- const uploads = invocations.filter(item => !item.dryRun)
- assert.equal(dryRuns.length, 3, 'every package must be preflighted before any upload')
- assert.deepEqual(uploads.map(item => item.name), packages.map(item => item.name))
- for (const item of invocations) {
- assert.equal(item.command, 'publish')
- assert.ok(item.rest.includes('--access'), 'public access flag is required')
- assert.equal(item.rest[item.rest.indexOf('--access') + 1], 'public')
- assert.ok(item.rest.includes('--ignore-scripts'), 'lifecycle scripts must stay disabled')
- assert.equal(item.rest[item.rest.indexOf('--tag') + 1], 'preview', 'preview releases must not take latest')
- const target = item.rest.find(value => value.startsWith('--registry='))
- assert.ok(target, 'every npm invocation must name its registry explicitly')
- assert.ok(target === '--registry=https://registry.npmjs.org/' || /^--registry=http:\/\/127\.0\.0\.1:\d+\/$/.test(target), `unexpected registry target: ${target}`)
- assert.equal(item.registryConfigured, target.slice('--registry='.length), 'the stub must receive the same registry the publisher targets')
- }
- for (const item of uploads) assert.ok(item.provenance, `provenance is required for ${item.name}`)
- for (const item of dryRuns) assert.ok(!item.provenance, 'dry runs must not request provenance')
- for (const item of uploads) assert.equal(path.basename(item.rest[0]), item.tarball, 'uploads must name the release tarball')
- assert.deepEqual(uploads.map(item => path.basename(item.rest[0])), packages.map(item => item.tarball))
- const published = JSON.parse(fs.readFileSync(path.join(fixture, 'published.json'), 'utf8'))
- assert.deepEqual(Object.keys(published).sort(), packages.map(item => item.name).sort())
- for (const item of packages) assert.equal(published[item.name].tarball, item.tarball)
- for (const item of packages) assert.equal(published[item.name].distTag, 'preview')
- assert.ok(!Object.values(published).some(item => item.distTag === 'latest'), 'no preview upload may move latest')
- assert.deepEqual(uploads.slice(-1).map(item => item.integrity), [published[packages[2].name].integrity])
- }))
- test('publish retry skips a byte-identical version and never re-uploads it', () => withFixture(({ assets, fixture, packages }) => {
- runPublisher({ assets, fixture })
- const firstUploads = JSON.parse(fs.readFileSync(path.join(fixture, 'published.json'), 'utf8'))
- const { invocations } = runPublisher({ assets, fixture, keepState: true })
- const uploads = invocations.filter(item => !item.dryRun)
- assert.deepEqual(uploads, [], 'a re-run must not publish an existing name/version again')
- assert.equal(invocations.filter(item => item.dryRun).length, 3, 'the preflight still runs on a retry')
- assert.deepEqual(JSON.parse(fs.readFileSync(path.join(fixture, 'published.json'), 'utf8')), firstUploads)
- for (const item of packages) assert.equal(firstUploads[item.name].distTag, 'preview')
- }))
- test('publish requires the GitHub Actions environment before any upload', () => withFixture(({ assets, fixture }) => {
- const runner = spawnSync(process.execPath, [fixturePaths.runner, assets, fixture, '--publish'], {
- cwd: root,
- encoding: 'utf8',
- windowsHide: true,
- timeout: 120000,
- env: { ...process.env, RELEASE_TAG: `scriptor-v${version}`, SCRIPTOR_PUBLISHER: fixturePaths.publisher, SCRIPTOR_STUB_NPM: fixturePaths.stub, SCRIPTOR_STRIP_GITHUB: '1' },
- })
- const report = JSON.parse(runner.stdout.trim().split('\n').pop())
- assert.equal(runner.status, 0, 'the fixture itself must report its result')
- assert.notEqual(report.code, 0, 'publishing outside Actions must fail')
- const invocations = fs.readFileSync(path.join(fixture, 'invocations.jsonl'), 'utf8').trim().split('\n').filter(Boolean).map(line => JSON.parse(line))
- assert.deepEqual(invocations.filter(item => !item.dryRun), [], 'nothing may be uploaded without provenance authority')
- }))
|