From a9ed9c6453f8a5dfa232c6fb04ae96a85a7e6b96 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Fri, 4 Sep 2026 00:07:58 +0000 Subject: [PATCH 1/3] chore(ci): remove the release audit gate Delete scripts/audit-packed-release.mjs, its npm-cli helper, and release-audit.test.ts; drop the script from audit:release and the packed pool; remove the EATTESTATIONVERIFY retry policy from docs/local-ci.md. --- docs/local-ci.md | 28 +- package.json | 2 +- packages/agent-bundle/README.md | 3 +- .../agent-bundle/tests/release-audit.test.ts | 225 --------------- .../agent-bundle/tests/support/shared-pack.ts | 8 +- rstest.integration-tests.ts | 1 - scripts/audit-packed-release.mjs | 259 ------------------ scripts/npm-cli.mjs | 25 -- 8 files changed, 10 insertions(+), 541 deletions(-) delete mode 100644 packages/agent-bundle/tests/release-audit.test.ts delete mode 100644 scripts/audit-packed-release.mjs delete mode 100644 scripts/npm-cli.mjs diff --git a/docs/local-ci.md b/docs/local-ci.md index 829d647ff..0d833a08c 100644 --- a/docs/local-ci.md +++ b/docs/local-ci.md @@ -189,29 +189,11 @@ attempt, so one edit is exactly one build. ## Infrastructure failures and their retry policy -Two failure shapes in the hosted Release gates are registry or runner -infrastructure, not the tree under test. Neither gets a code-level retry, -and neither is a reason to weaken the gate; the policy is to re-run the job -once the cause has cleared, then treat a repeat as a real signal. - -- **`npm audit signatures` → `EATTESTATIONVERIFY`** (from - `scripts/audit-packed-release.mjs`, reached through `pnpm audit:release`). - Example (CI run 33584654855, 2026-09-02, Release gates on Node 22.19): - `@modelcontextprotocol/server@2.0.0 failed to verify attestation: - Unexpected end of JSON input`. npm fetched a truncated attestation bundle - from `registry.npmjs.org` for a dependency this repository does not - publish; the same pinned version and integrity verify on every later run - of the same gate without any lockfile change. The audit deliberately - installs against live registry metadata (no `--prefer-offline`), so a - registry-side transient reaches it unfiltered. Policy: read the JSON in the - step log first; if the `invalid` entry names a third-party package with an - unchanged pinned version and a parse-shaped message (`Unexpected end of - JSON input`, `Unexpected token`, a 5xx), re-run the failed job (`gh run - rerun --failed`). If the same package fails twice in a row, or the - message is a genuine signature mismatch (`EATTESTATIONSIGNATURE`, - `EINTEGRITY`), stop and investigate the dependency before merging: that is - the supply-chain check doing its job. Do not add retries around the audit - command and do not relax `--json` parsing to tolerate the error. +One failure shape in the hosted Release gates is registry or runner +infrastructure, not the tree under test. It gets no code-level retry, and it +is not a reason to weaken the gate; the policy is to re-run the job once the +cause has cleared, then treat a repeat as a real signal. + - **Runner network stalls during `npm install`** in the packed pool. The pool's consumer installs are cache-backed per worker (`rstest.worker-isolation.ts`): each worker pays for one cold download of diff --git a/package.json b/package.json index 6daf3b7c2..dac3c50d0 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "release": "pnpm check:release && changeset publish", "preview:publish": "pkg-pr-new publish --previewVersion --peerDeps --no-compact --no-template './packages/agent-bundle' './packages/rsc-runtime' './packages/create-agent-bundle'", "pack:dry-run": "pnpm build && npm pack ./packages/agent-bundle --dry-run --json", - "audit:release": "pnpm lint:package && attw --pack --profile esm-only packages/agent-bundle && node scripts/audit-packed-release.mjs", + "audit:release": "pnpm lint:package && attw --pack --profile esm-only packages/agent-bundle", "check:release": "pnpm pack:dry-run && pnpm audit:release && pnpm test:packed:release", "check:release:ci": "pnpm pack:dry-run && pnpm audit:release && pnpm test:packed", "example:hooks": "pnpm build && pnpm --filter @agent-bundle-example/hooks-and-scripts dev", diff --git a/packages/agent-bundle/README.md b/packages/agent-bundle/README.md index 5bc75b549..6fc664203 100644 --- a/packages/agent-bundle/README.md +++ b/packages/agent-bundle/README.md @@ -1010,5 +1010,4 @@ Run the complete local delivery gate with `pnpm check && pnpm check:release`. Native Claude/Codex smokes stay intentionally opt-in and skipped in ordinary CI. npm publishing is deferred until the release owner picks the final package name/scope; pkg.pr.new previews are the interim channel, and the first npm release will use npm -package provenance (`publishConfig.provenance` is already set). `pnpm audit:release` fails if any -publishable tarball lacks `LICENSE`, `NOTICE`, or the `"license": "Apache-2.0"` manifest field. +package provenance (`publishConfig.provenance` is already set). diff --git a/packages/agent-bundle/tests/release-audit.test.ts b/packages/agent-bundle/tests/release-audit.test.ts deleted file mode 100644 index d88bedfb3..000000000 --- a/packages/agent-bundle/tests/release-audit.test.ts +++ /dev/null @@ -1,225 +0,0 @@ -import { execFile as executeFile } from 'node:child_process'; -import { mkdtemp, readFile, realpath, rm, writeFile } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; -import { promisify } from 'node:util'; - -import { expect, it } from '@rstest/core'; - -import { isolatedCommandEnvironment } from '../../../rstest.worker-isolation.ts'; -import { npmInstallArguments, sharedPackedTarball } from './support/shared-pack.ts'; - -const execFile = promisify(executeFile); -const workspaceRoot = process.cwd(); -const packageRoot = join(workspaceRoot, 'packages', 'agent-bundle'); - -const releaseEnvironment = (): NodeJS.ProcessEnv => isolatedCommandEnvironment({ ...process.env, NODE_ENV: 'production' }); - -it('audits an externally installed production tarball and generates its CycloneDX SBOM', async () => { - const { stdout } = await execFile(process.execPath, ['scripts/audit-packed-release.mjs'], { - cwd: workspaceRoot, - env: releaseEnvironment(), - }); - const sbom = JSON.parse(stdout) as { - readonly bomFormat?: string; - readonly components?: readonly { - readonly 'bom-ref'?: string; - readonly name?: string; - readonly scope?: string; - readonly version?: string; - readonly properties?: readonly { readonly name?: string; readonly value?: string }[]; - }[]; - readonly dependencies?: readonly { readonly dependsOn?: readonly string[]; readonly ref?: string }[]; - readonly metadata?: { - readonly component?: { readonly 'bom-ref'?: string; readonly name?: string; readonly version?: string }; - }; - readonly specVersion?: string; - }; - const sourceManifest = JSON.parse(await readFile(join(packageRoot, 'package.json'), 'utf8')) as { - readonly name: string; - readonly version: string; - }; - const components = sbom.components ?? []; - const root = sbom.metadata?.component; - const product = components.find((component) => ( - component.name === sourceManifest.name && component.version === sourceManifest.version - )); - const componentReferences = new Set(components.flatMap((component) => ( - component['bom-ref'] === undefined ? [] : [component['bom-ref']] - ))); - const rootReference = root?.['bom-ref']; - expect(rootReference).toBeDefined(); - const dependencyReferences = new Set([ - ...(rootReference === undefined ? [] : [rootReference]), - ...componentReferences, - ]); - const declaredDependencyReferences = new Set((sbom.dependencies ?? []).flatMap((dependency) => ( - dependency.ref === undefined ? [] : [dependency.ref] - ))); - const rootDependencies = (sbom.dependencies ?? []).find((dependency) => dependency.ref === root?.['bom-ref']); - const productDependencies = (sbom.dependencies ?? []).find((dependency) => dependency.ref === product?.['bom-ref']); - - expect(sbom.bomFormat).toBe('CycloneDX'); - expect(sbom.specVersion).toMatch(/^1\./u); - expect(root).toMatchObject({ version: '1.0.0' }); - expect(root?.name).not.toBe(sourceManifest.name); - expect(product).toBeDefined(); - expect(rootDependencies?.dependsOn).toContain(product?.['bom-ref']); - expect(productDependencies?.dependsOn?.length).toBeGreaterThan(0); - expect([...dependencyReferences].every((reference) => declaredDependencyReferences.has(reference))).toBe(true); - expect((sbom.dependencies ?? []).every((dependency) => ( - dependency.ref !== undefined - && dependencyReferences.has(dependency.ref) - && (dependency.dependsOn ?? []).every((reference) => dependencyReferences.has(reference)) - ))).toBe(true); - expect(components.some((component) => component.scope === 'development')).toBe(false); - expect(components.every((component) => { - const path = component.properties?.find((property) => property.name === 'cdx:npm:package:path')?.value; - // npm >= 11 omits the property; the audit script then verifies components against the installed tree instead. - if (path === undefined) return true; - return path.startsWith('node_modules/') - && !path.includes('node_modules/.pnpm/') - && !path.includes('/packages/'); - })).toBe(true); -}, 120_000); - -it('ships repository and support metadata that matches the verified origin', async () => { - const tarballRoot = await mkdtemp(join(tmpdir(), 'agent-bundle-package-metadata-')); - - try { - const { tarball } = await sharedPackedTarball('agent-bundle'); - await execFile('tar', ['--extract', '--file', tarball, '--directory', tarballRoot]); - const manifest = JSON.parse(await readFile(join(tarballRoot, 'package', 'package.json'), 'utf8')) as { - readonly bugs?: { readonly url?: string }; - readonly description?: string; - readonly homepage?: string; - readonly keywords?: readonly string[]; - readonly repository?: { readonly type?: string; readonly url?: string }; - }; - - expect(manifest).toMatchObject({ - bugs: { url: 'https://github.com/ScriptedAlchemy/agent-bundle/issues' }, - homepage: 'https://github.com/ScriptedAlchemy/agent-bundle#readme', - repository: { type: 'git', url: 'git+https://github.com/ScriptedAlchemy/agent-bundle.git' }, - }); - expect(manifest.keywords).toEqual(expect.arrayContaining(['agent', 'claude-code', 'codex', 'mcp'])); - } finally { - await rm(tarballRoot, { force: true, recursive: true }); - } -}); - -it('packs generated Workbench legal companion files', async () => { - const { packOutput } = await sharedPackedTarball('agent-bundle'); - const productManifest = await readFile(join(packageRoot, 'package.json'), 'utf8'); - - expect(packOutput.files.map((file) => file.path)).toContainEqual( - expect.stringMatching(/^dist\/workbench\/.*\.LICENSE\.txt$/u), - ); - expect(packOutput.files.some(({ path }) => path.startsWith('examples/'))).toBe(false); - expect(productManifest).not.toContain('workspace:'); -}, 120_000); - -it('packs the root LICENSE and NOTICE into every publishable tarball', async () => { - const [license, notice] = await Promise.all([ - readFile(join(workspaceRoot, 'LICENSE'), 'utf8'), - readFile(join(workspaceRoot, 'NOTICE'), 'utf8'), - ]); - - for (const packageName of ['agent-bundle', 'runtime', 'create-agent-bundle'] as const) { - const tarballRoot = await mkdtemp(join(tmpdir(), `agent-bundle-license-${packageName}-`)); - try { - const { packOutput, tarball } = await sharedPackedTarball(packageName); - const paths = packOutput.files.map((file) => file.path); - expect(paths, `${packageName} tarball listing`).toEqual(expect.arrayContaining(['LICENSE', 'NOTICE'])); - - await execFile('tar', ['--extract', '--file', tarball, '--directory', tarballRoot]); - const manifest = JSON.parse(await readFile(join(tarballRoot, 'package', 'package.json'), 'utf8')) as { - readonly license?: string; - }; - expect(manifest.license, `${packageName} license field`).toBe('Apache-2.0'); - await expect(readFile(join(tarballRoot, 'package', 'LICENSE'), 'utf8')).resolves.toBe(license); - await expect(readFile(join(tarballRoot, 'package', 'NOTICE'), 'utf8')).resolves.toBe(notice); - } finally { - await rm(tarballRoot, { force: true, recursive: true }); - } - } -}, 120_000); - -it('installs public entrypoints and an externally resolved CLI for production consumers', async () => { - const consumerRoot = await mkdtemp(join(tmpdir(), 'agent-bundle-release-consumer-')); - - try { - const { tarball } = await sharedPackedTarball('agent-bundle'); - await writeFile(join(consumerRoot, 'package.json'), '{"private":true,"type":"module"}\n'); - await execFile('npm', [ - 'install', '--omit=dev', ...npmInstallArguments, tarball, - ], { cwd: consumerRoot, env: releaseEnvironment() }); - - const installedPackageRoot = await realpath(join(consumerRoot, 'node_modules', 'agent-bundle')); - expect(installedPackageRoot.startsWith(workspaceRoot)).toBe(false); - const manifest = JSON.parse(await readFile(join(installedPackageRoot, 'package.json'), 'utf8')) as { - readonly dependencies?: Readonly>; - }; - expect(manifest.dependencies?.commander).toBe('15.0.0'); - expect(manifest.dependencies?.['agent-bundle']).toBeUndefined(); - expect(JSON.stringify(manifest)).not.toContain('workspace:'); - await Promise.all([ - writeFile(join(consumerRoot, 'canonical-contract.mts'), [ - "import { type CompareEvalsOptions, type RunEvalsOptions } from 'agent-bundle/api';", - "import { defineConfig, type AgentBundleConfig } from 'agent-bundle/config';", - "import { defineEvalSuite, expectExitCode, normalizeEvalConfig, type EvalConfigInput, type EvalSuiteInput } from 'agent-bundle/eval';", - '', - "const config = defineConfig({ plugin: { name: 'canonical-consumer', version: '1.0.0' }, targets: ['portable'] } satisfies AgentBundleConfig);", - "const evalConfig = { include: ['evals/**/*.eval.ts'], runsDir: '.agent-bundle/runs' } satisfies EvalConfigInput;", - "const evalSuite = { cases: [{ assertions: [expectExitCode(0)], fixture: '.', hosts: { claude: { model: 'claude-sonnet-4-5' } }, id: 'canonical-case', invocation: { mode: 'none' }, prompt: 'Verify the canonical contract.' }], name: 'canonical-consumer' } satisfies EvalSuiteInput;", - "const runOptions = { harness: 'claude', root: '.', trials: 1 } satisfies RunEvalsOptions;", - "const comparisonOptions = { baseRunId: 'baseline', candidateRunId: 'candidate', root: '.' } satisfies CompareEvalsOptions;", - '', - 'void [config, normalizeEvalConfig(evalConfig), defineEvalSuite(evalSuite), runOptions, comparisonOptions];', - '', - ].join('\n')), - writeFile(join(consumerRoot, 'canonical-contract.mjs'), [ - "import { compareEvals, runEvals } from 'agent-bundle/api';", - "import { defineConfig } from 'agent-bundle/config';", - "import { defineEvalSuite, expectExitCode, normalizeEvalConfig } from 'agent-bundle/eval';", - '', - "const config = defineConfig({ plugin: { name: 'canonical-consumer', version: '1.0.0' }, targets: ['portable'] });", - "const evalConfig = normalizeEvalConfig({ include: ['evals/**/*.eval.ts'], runsDir: '.agent-bundle/runs' });", - "const evalSuite = defineEvalSuite({ cases: [{ assertions: [expectExitCode(0)], fixture: '.', hosts: { claude: { model: 'claude-sonnet-4-5' } }, id: 'canonical-case', invocation: { mode: 'none' }, prompt: 'Verify the canonical contract.' }], name: 'canonical-consumer' });", - 'const canonicalValues = { config, evalConfig, evalSuite };', - 'const serialized = JSON.stringify(canonicalValues);', - "if (serialized.includes('\"schemaVersion\"') || serialized.includes('\"version\":1')) throw new Error('Canonical public values must not emit schemaVersion or version:1.');", - "if (typeof compareEvals !== 'function' || typeof runEvals !== 'function') throw new Error('Expected public Eval API entrypoints.');", - "process.stdout.write('canonical public contract\\n');", - '', - ].join('\n')), - ]); - await expect(execFile(join(workspaceRoot, 'node_modules', '.bin', 'tsc'), [ - '--module', 'NodeNext', - '--moduleResolution', 'NodeNext', - '--noEmit', - '--skipLibCheck', - '--strict', - '--target', 'ES2024', - 'canonical-contract.mts', - ], { cwd: consumerRoot, env: releaseEnvironment() })).resolves.toMatchObject({ stderr: '', stdout: '' }); - await expect(execFile(process.execPath, ['canonical-contract.mjs'], { - cwd: consumerRoot, - env: releaseEnvironment(), - })).resolves.toMatchObject({ stderr: '', stdout: 'canonical public contract\n' }); - await expect(execFile(process.execPath, [ - '--input-type=module', - '--eval', - "await Promise.all(['agent-bundle', 'agent-bundle/api', 'agent-bundle/config', 'agent-bundle/eval'].map((specifier) => import(specifier)));", - ], { cwd: consumerRoot, env: releaseEnvironment() })).resolves.toMatchObject({ stderr: '', stdout: '' }); - const cli = join(consumerRoot, 'node_modules', '.bin', 'agent-bundle'); - await expect(execFile(cli, ['--help'], { cwd: consumerRoot, env: releaseEnvironment() })).resolves.toMatchObject({ - stderr: '', - stdout: expect.stringContaining('Usage: agent-bundle'), - }); - await rm(join(consumerRoot, 'node_modules', 'commander'), { force: true, recursive: true }); - await expect(execFile(cli, ['--help'], { cwd: consumerRoot, env: releaseEnvironment() })).rejects.toThrow(); - } finally { - await rm(consumerRoot, { force: true, recursive: true }); - } -}, 120_000); diff --git a/packages/agent-bundle/tests/support/shared-pack.ts b/packages/agent-bundle/tests/support/shared-pack.ts index ebd59c2b3..4cf21e3ca 100644 --- a/packages/agent-bundle/tests/support/shared-pack.ts +++ b/packages/agent-bundle/tests/support/shared-pack.ts @@ -43,11 +43,9 @@ export const installedEnvironment = (): NodeJS.ProcessEnv => isolatedCommandEnvi * Canonical flags for installing a packed tarball into a consumer fixture. * They keep npm's default metadata staleness checks, so the install resolves * the tree a consumer would get today. That is the point of the proofs that - * stand in for a real consumer — release-audit's production entrypoint walk, - * the scaffolder template matrix, the native host smoke — and exact direct - * pins do not make it free: transitive ranges still move underneath them, and - * scripts/audit-packed-release.mjs audits the installed tree without ever - * exercising it. + * stand in for a real consumer — the scaffolder template matrix, the native + * host smoke — and exact direct pins do not make it free: transitive ranges + * still move underneath them. */ export const npmInstallArguments = ['--ignore-scripts', '--no-audit', '--no-fund'] as const; diff --git a/rstest.integration-tests.ts b/rstest.integration-tests.ts index ff3a0f66f..f59bc91ec 100644 --- a/rstest.integration-tests.ts +++ b/rstest.integration-tests.ts @@ -140,7 +140,6 @@ export const packedTestFiles: readonly string[] = [ 'packages/agent-bundle/tests/packed-native-smoke.test.ts', 'packages/agent-bundle/tests/packed-stdio-projection.test.ts', 'packages/agent-bundle/tests/public-api-packed.test.ts', - 'packages/agent-bundle/tests/release-audit.test.ts', 'packages/agent-bundle/tests/rsc-runtime-optional-packaging.test.ts', 'packages/create-agent-bundle/tests/scaffold-packed.e2e.test.ts', 'packages/workbench/tests/packed-release.e2e.test.ts', diff --git a/scripts/audit-packed-release.mjs b/scripts/audit-packed-release.mjs deleted file mode 100644 index 36152fd4d..000000000 --- a/scripts/audit-packed-release.mjs +++ /dev/null @@ -1,259 +0,0 @@ -import { execFile as executeFile } from 'node:child_process'; -import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; -import { dirname, join, resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { promisify } from 'node:util'; - -import { npmCliInvocation } from './npm-cli.mjs'; -import { packOutputFromJson as sharedPackOutputFromJson } from './npm-pack-json.mjs'; -import { licenseFiles, publishablePackageDirectories } from './sync-license-files.mjs'; - -const execFile = promisify(executeFile); -const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); -const packageRoot = join(repositoryRoot, 'packages', 'agent-bundle'); -const projectLicense = 'Apache-2.0'; -const { NODE_PATH: _nodePath, ...productionEnvironment } = process.env; -const npmCli = npmCliInvocation(productionEnvironment); -const execNpm = (args, options) => execFile(npmCli.command, [...npmCli.args, ...args], options); - -const fail = (message) => { - throw new Error(`Invalid packed release audit: ${message}`); -}; - -const asRecord = (value, message) => { - if (value === null || typeof value !== 'object' || Array.isArray(value)) fail(message); - return value; -}; - -const asString = (value, message) => { - if (typeof value !== 'string' || value.length === 0) fail(message); - return value; -}; - -const packOutputFromJson = (stdout, packageName) => { - try { - return sharedPackOutputFromJson(stdout, packageName); - } catch (error) { - fail(error instanceof Error ? error.message : String(error)); - } -}; - -/** npm package name declared by a workspace package directory. */ -const packageNameOf = async (packageDirectory) => asString( - asRecord( - JSON.parse(await readFile(join(repositoryRoot, packageDirectory, 'package.json'), 'utf8')), - `${packageDirectory}/package.json is not an object`, - ).name, - `${packageDirectory}/package.json has no name`, -); - -/** Every name -> Set(version) reachable under the consumer's own node_modules tree. */ -const collectInstalledPackages = async (nodeModulesRoot) => { - const installed = new Map(); - const walk = async (directory) => { - let entries; - try { - entries = await readdir(directory, { withFileTypes: true }); - } catch { - return; - } - for (const entry of entries) { - if (!entry.isDirectory() || entry.name === '.bin' || entry.name === '.cache') continue; - const child = join(directory, entry.name); - if (entry.name.startsWith('@')) { - await walk(child); - continue; - } - try { - const manifest = JSON.parse(await readFile(join(child, 'package.json'), 'utf8')); - if (typeof manifest.name === 'string' && typeof manifest.version === 'string') { - const versions = installed.get(manifest.name) ?? new Set(); - versions.add(manifest.version); - installed.set(manifest.name, versions); - } - } catch { - // A directory without a readable manifest is not an installed package. - } - await walk(join(child, 'node_modules')); - } - }; - await walk(nodeModulesRoot); - return installed; -}; - -const validateSbom = (sbom, productManifest, installedPackages) => { - const document = asRecord(sbom, 'document must be an object'); - if (document.bomFormat !== 'CycloneDX') fail('bomFormat must be CycloneDX'); - asString(document.specVersion, 'specVersion is required'); - - const metadata = asRecord(document.metadata, 'metadata is required'); - const root = asRecord(metadata.component, 'metadata.component is required'); - if (root.name === productManifest.name || root.version !== '1.0.0') { - fail('metadata.component must describe the external production consumer'); - } - const rootReference = asString(root['bom-ref'], 'metadata.component bom-ref is required'); - - if (!Array.isArray(document.components)) fail('components must be an array'); - const components = document.components.map((component, index) => asRecord(component, `component ${index} must be an object`)); - const componentReferences = new Set(components.map((component, index) => ( - asString(component['bom-ref'], `component ${index} bom-ref is required`) - ))); - const product = components.find((component) => ( - component.name === productManifest.name && component.version === productManifest.version - )); - if (product === undefined) fail(`missing installed ${productManifest.name}@${productManifest.version} component`); - const productReference = asString(product['bom-ref'], 'installed product bom-ref is required'); - - for (const [index, component] of components.entries()) { - if (component.scope === 'development') fail(`component ${index} has development scope`); - if (!Array.isArray(component.properties)) fail(`component ${index} is missing npm package path metadata`); - const packagePath = component.properties.find((property) => ( - asRecord(property, `component ${index} property must be an object`).name === 'cdx:npm:package:path' - ))?.value; - if (packagePath !== undefined) { - const path = asString(packagePath, `component ${index} has an invalid npm package path`); - if (!path.startsWith('node_modules/') || path.includes('node_modules/.pnpm/') || path.includes('/packages/')) { - fail(`component ${index} is not installed from the external production consumer`); - } - continue; - } - // npm >= 11 omits cdx:npm:package:path, so the same guarantee is checked - // against the packages physically installed in the consumer's node_modules. - const name = asString(component.name, `component ${index} name is required`); - const version = asString(component.version, `component ${index} version is required`); - if (installedPackages.get(name)?.has(version) !== true) { - fail(`component ${index} (${name}@${version}) is not installed in the external production consumer`); - } - } - - if (!Array.isArray(document.dependencies)) fail('dependencies must be an array'); - const knownReferences = new Set([rootReference, ...componentReferences]); - const dependencies = document.dependencies.map((dependency, index) => asRecord(dependency, `dependency ${index} must be an object`)); - const declaredReferences = new Set(); - for (const [index, dependency] of dependencies.entries()) { - const reference = asString(dependency.ref, `dependency ${index} ref is required`); - if (!knownReferences.has(reference)) fail(`dependency ${index} has an unknown ref`); - declaredReferences.add(reference); - if (dependency.dependsOn !== undefined && !Array.isArray(dependency.dependsOn)) { - fail(`dependency ${index} dependsOn must be an array`); - } - for (const dependencyReference of dependency.dependsOn ?? []) { - if (!knownReferences.has(asString(dependencyReference, `dependency ${index} contains an invalid dependency ref`))) { - fail(`dependency ${index} contains an unknown dependency ref`); - } - } - } - for (const reference of knownReferences) { - if (!declaredReferences.has(reference)) fail(`dependency closure is missing ${reference}`); - } - - const rootDependencies = dependencies.find((dependency) => dependency.ref === rootReference); - if (!rootDependencies?.dependsOn?.includes(productReference)) { - fail('external production consumer must depend on the packed product'); - } - const productDependencies = dependencies.find((dependency) => dependency.ref === productReference); - if (productDependencies?.dependsOn?.length === 0 || productDependencies === undefined) { - fail('packed product dependency closure is missing'); - } -}; - -/** - * Every publishable tarball must declare the project license and carry the - * root LICENSE and NOTICE byte-for-byte. Like the rest of this audit (attw and - * the SBOM install both pack `dist`), it inspects build output and never - * regenerates it: the package copies are written by each package's `build` - * (scripts/sync-license-files.mjs), and syncing here would hide a build step - * that stopped producing them. - */ -const validateLicenseFiles = async (packOutput, packageDirectory) => { - const manifest = JSON.parse(await readFile(join(repositoryRoot, packageDirectory, 'package.json'), 'utf8')); - if (manifest.license !== projectLicense) { - fail(`${packageDirectory} package.json must declare "license": "${projectLicense}"`); - } - if (!Array.isArray(packOutput.files)) fail(`${packageDirectory} npm pack did not list tarball files`); - const packedPaths = new Set(packOutput.files.map((file) => asRecord(file, 'pack file must be an object').path)); - for (const file of licenseFiles) { - const actual = await readFile(join(repositoryRoot, packageDirectory, file), 'utf8').catch(() => undefined); - if (actual === undefined) { - fail(`${packageDirectory}/${file} is missing; this audit inspects build output, so run \`pnpm build\` (which runs scripts/sync-license-files.mjs) first`); - } - if (!packedPaths.has(file)) fail(`${packageDirectory} tarball is missing ${file}`); - if (actual !== await readFile(join(repositoryRoot, file), 'utf8')) { - fail(`${packageDirectory}/${file} differs from the repository root ${file}`); - } - } -}; - -const auditLicenseFiles = async () => { - for (const packageDirectory of publishablePackageDirectories) { - const packOutput = packOutputFromJson((await execNpm(['pack', '--dry-run', '--json'], { - cwd: join(repositoryRoot, packageDirectory), - env: productionEnvironment, - })).stdout, await packageNameOf(packageDirectory)); - await validateLicenseFiles(packOutput, packageDirectory); - } -}; - -const auditPackedRelease = async () => { - await auditLicenseFiles(); - const auditRoot = await mkdtemp(join(tmpdir(), 'agent-bundle-release-audit-')); - - try { - const tarballs = join(auditRoot, 'tarballs'); - const consumer = join(auditRoot, 'consumer'); - await Promise.all([mkdir(tarballs), mkdir(consumer)]); - await writeFile(join(consumer, 'package.json'), JSON.stringify({ - name: 'agent-bundle-release-sbom-consumer', - private: true, - version: '1.0.0', - }) + '\n'); - - const { filename } = packOutputFromJson((await execNpm([ - 'pack', - '--json', - '--pack-destination', tarballs, - ], { cwd: packageRoot, env: productionEnvironment })).stdout, 'agent-bundle'); - const tarball = join(tarballs, asString(filename, 'npm pack did not produce a tarball filename')); - // No `--prefer-offline` here (unlike the packed pool's - // cachedNpmInstallArguments): the tree this install produces is what - // `npm audit`, `npm audit signatures`, and `npm sbom` below report on, so - // it has to resolve against live registry metadata rather than whatever - // the cache last saw. - await execNpm([ - 'install', - '--omit=dev', - '--ignore-scripts', - '--no-audit', - '--no-fund', - tarball, - ], { cwd: consumer, env: productionEnvironment }); - - await execFile('npm', ['ls', '--omit=dev', '--json'], { - cwd: consumer, - env: productionEnvironment, - }); - await execFile('npm', ['audit', '--omit=dev', '--json'], { - cwd: consumer, - env: productionEnvironment, - }); - await execFile('npm', ['audit', 'signatures', '--json'], { - cwd: consumer, - env: productionEnvironment, - }); - - const productManifest = JSON.parse(await readFile(join(packageRoot, 'package.json'), 'utf8')); - const sbom = JSON.parse((await execNpm([ - 'sbom', - '--omit=dev', - '--sbom-format', 'cyclonedx', - ], { cwd: consumer, env: productionEnvironment })).stdout); - const installedPackages = await collectInstalledPackages(join(consumer, 'node_modules')); - validateSbom(sbom, productManifest, installedPackages); - process.stdout.write(`${JSON.stringify(sbom)}\n`); - } finally { - await rm(auditRoot, { force: true, recursive: true }); - } -}; - -await auditPackedRelease(); diff --git a/scripts/npm-cli.mjs b/scripts/npm-cli.mjs deleted file mode 100644 index d6a44a3d3..000000000 --- a/scripts/npm-cli.mjs +++ /dev/null @@ -1,25 +0,0 @@ -import { existsSync } from 'node:fs'; -import { dirname, join } from 'node:path'; - -export const npmCliInvocation = (environment = process.env) => { - const entrypoint = environment.npm_execpath; - const command = environment.npm_node_execpath ?? process.execPath; - if (typeof command !== 'string' || command.length === 0) { - throw new Error('A Node.js executable is required to launch npm.'); - } - if (typeof entrypoint !== 'string' || entrypoint.length === 0) { - const nodeDirectory = dirname(command); - const candidates = [ - join(nodeDirectory, 'node_modules', 'npm', 'bin', 'npm-cli.js'), - join(nodeDirectory, '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'), - ]; - const npmCli = candidates.find((candidate) => existsSync(candidate)); - if (npmCli === undefined) throw new Error('The npm CLI could not be resolved from the Node.js installation.'); - return Object.freeze({ args: Object.freeze([npmCli]), command }); - } - const packageManager = environment.npm_config_user_agent?.split('/')[0]; - const args = packageManager === 'pnpm' - ? [entrypoint, 'exec', 'npm'] - : [entrypoint]; - return Object.freeze({ args: Object.freeze(args), command }); -}; From a074fcca3cf9a1110fba32702c03f6b35c5f7517 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Fri, 4 Sep 2026 00:14:14 +0000 Subject: [PATCH 2/3] chore(changeset): record the release audit removal for the packaged README --- .changeset/remove-release-audit-gate.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/remove-release-audit-gate.md diff --git a/.changeset/remove-release-audit-gate.md b/.changeset/remove-release-audit-gate.md new file mode 100644 index 000000000..be5ebe373 --- /dev/null +++ b/.changeset/remove-release-audit-gate.md @@ -0,0 +1,5 @@ +--- +"agent-bundle": patch +--- + +Remove the release audit gate: `pnpm audit:release` no longer runs `scripts/audit-packed-release.mjs` (consumer install, `npm audit`, `npm audit signatures`, CycloneDX SBOM, and LICENSE/NOTICE tarball checks), and the packaged README no longer states that it fails on missing license files; `audit:release` is now publint plus attw only (#487) From aa783429b46b3def1f9ef879b6895ed182f8794a Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Fri, 4 Sep 2026 00:48:25 +0000 Subject: [PATCH 3/3] chore(release): rename audit:release to lint:release and drop remaining audit references --- .changeset/remove-release-audit-gate.md | 2 +- ...26-08-24-public-examples-pnpm-workspace.md | 47 +++++-------------- .../2026-08-25-capability-aware-workbench.md | 2 +- ...26-08-24-examples-pnpm-workspace-design.md | 5 +- package.json | 6 +-- packages/agent-bundle/README.md | 2 +- .../guide/distribution/preview-packages.mdx | 2 +- .../guide/distribution/preview-packages.mdx | 2 +- 8 files changed, 22 insertions(+), 46 deletions(-) diff --git a/.changeset/remove-release-audit-gate.md b/.changeset/remove-release-audit-gate.md index be5ebe373..a403445de 100644 --- a/.changeset/remove-release-audit-gate.md +++ b/.changeset/remove-release-audit-gate.md @@ -2,4 +2,4 @@ "agent-bundle": patch --- -Remove the release audit gate: `pnpm audit:release` no longer runs `scripts/audit-packed-release.mjs` (consumer install, `npm audit`, `npm audit signatures`, CycloneDX SBOM, and LICENSE/NOTICE tarball checks), and the packaged README no longer states that it fails on missing license files; `audit:release` is now publint plus attw only (#487) +Remove the release audit gate: the `audit:release` script that ran `scripts/audit-packed-release.mjs` (external consumer install, npm advisory and signature checks, CycloneDX SBOM, LICENSE/NOTICE tarball checks) is replaced by `lint:release`, which runs publint and attw only, and the packaged README no longer states that the release gate fails on missing license files (#487) diff --git a/docs/superpowers/plans/2026-08-24-public-examples-pnpm-workspace.md b/docs/superpowers/plans/2026-08-24-public-examples-pnpm-workspace.md index 773f272ce..98ca6bce4 100644 --- a/docs/superpowers/plans/2026-08-24-public-examples-pnpm-workspace.md +++ b/docs/superpowers/plans/2026-08-24-public-examples-pnpm-workspace.md @@ -37,9 +37,7 @@ - `.github/workflows/ci.yml`: Corepack/frozen pnpm install, product gates, and example gate. - `.github/workflows/package-preview.yml`: pnpm build with pkg.pr.new publishing only the product package. - `.github/workflows/native-host-smoke.yml`: pnpm setup and unchanged opt-in native-host commands. -- `scripts/audit-packed-release.mjs`: create one external npm consumer, install the packed tarball, then run npm dependency, audit, signature, and CycloneDX checks. -- `scripts/audit-packed-sbom.mjs`: removed after its behavior is folded into `audit-packed-release.mjs`. -- `packages/agent-bundle/tests/release-audit.test.ts`: prove the package tarball installs outside the workspace and excludes examples/workspace dependencies. +- packed consumer tests: prove the package tarball installs outside the workspace and excludes examples/workspace dependencies. - `packages/agent-bundle/tests/workspace-contract.test.ts`: prove pnpm selects both product packages and all private examples. ### Public examples @@ -64,20 +62,16 @@ **Files:** - Create: `pnpm-workspace.yaml` - Create: `pnpm-lock.yaml` -- Create: `scripts/audit-packed-release.mjs` - Create: `packages/agent-bundle/tests/workspace-contract.test.ts` - Modify: `package.json` - Modify: `.github/workflows/ci.yml` - Modify: `.github/workflows/package-preview.yml` - Modify: `.github/workflows/native-host-smoke.yml` -- Modify: `packages/agent-bundle/tests/release-audit.test.ts` - Delete: `package-lock.json` -- Delete: `scripts/audit-packed-sbom.mjs` **Interfaces:** -- Consumes: current root npm scripts, existing package workspaces, and the installed-tarball assertions in `release-audit.test.ts`. +- Consumes: current root npm scripts, existing package workspaces, and the installed-tarball assertions in the packed consumer tests. - Produces: canonical root commands `pnpm build`, `pnpm test`, `pnpm check`, `pnpm check:release`, and a workspace containing `packages/*` plus future `examples/*`. -- Produces: `scripts/audit-packed-release.mjs` that exits nonzero if any external npm production check fails and prints the validated CycloneDX document as JSON on stdout. - [ ] **Step 1: Add the failing workspace membership test** @@ -132,32 +126,15 @@ In `package.json`, remove `workspaces`, add `"packageManager": "pnpm@11.23.0"`, "test:packed:native:claude": "pnpm build && AGENT_BUNDLE_PACKED_NATIVE_CLAUDE_SMOKE=1 pnpm test:packed:native", "test:packed:native:codex": "pnpm build && AGENT_BUNDLE_PACKED_NATIVE_CODEX_SMOKE=1 pnpm test:packed:native", "pack:dry-run": "pnpm build && npm pack ./packages/agent-bundle --dry-run --json", - "audit:release": "pnpm lint:package && attw --pack --profile esm-only packages/agent-bundle && node scripts/audit-packed-release.mjs", - "check:release": "pnpm pack:dry-run && pnpm audit:release && pnpm test:packed" + "lint:release": "pnpm lint:package && attw --pack --profile esm-only packages/agent-bundle", + "check:release": "pnpm pack:dry-run && pnpm lint:release && pnpm test:packed" } } ``` -- [ ] **Step 4: Move every npm production check into one external consumer** +- [ ] **Step 4: Tighten the tarball regression** -Implement `scripts/audit-packed-release.mjs` by retaining the current temporary-directory, pack, install, and SBOM validation logic from `audit-packed-sbom.mjs`, then run these commands with `cwd` set to the temporary consumer: - -```js -await execFile('npm', ['ls', '--omit=dev', '--json'], { cwd: consumerRoot }); -await execFile('npm', ['audit', '--omit=dev', '--json'], { cwd: consumerRoot }); -await execFile('npm', ['audit', 'signatures', '--json'], { cwd: consumerRoot }); -const { stdout } = await execFile( - 'npm', - ['sbom', '--omit=dev', '--sbom-format', 'cyclonedx'], - { cwd: consumerRoot, maxBuffer: 32 * 1024 * 1024 }, -); -``` - -Keep the existing checks for CycloneDX format, root component, installed `agent-bundle` dependency closure, and absence of workspace/`.pnpm` paths. Always remove the temp root in `finally`. Print only the validated SBOM JSON so the existing test can parse stdout deterministically. - -- [ ] **Step 5: Tighten the tarball regression** - -In `packages/agent-bundle/tests/release-audit.test.ts`, change repository-owned root commands to `corepack pnpm ...`, keep `npm pack` and the temporary consumer's `npm install`, and add: +In the packed consumer tests, change repository-owned root commands to `corepack pnpm ...`, keep `npm pack` and the temporary consumer's `npm install`, and add: ```ts expect(files.some(({ path }) => path.startsWith('examples/'))).toBe(false); @@ -165,7 +142,7 @@ expect(packageManifest.dependencies?.['agent-bundle']).toBeUndefined(); expect(JSON.stringify(packageManifest)).not.toContain('workspace:'); ``` -- [ ] **Step 6: Migrate GitHub Actions to the pinned manager** +- [ ] **Step 5: Migrate GitHub Actions to the pinned manager** In all three workflows, add `corepack enable` before install and replace `npm ci` with: @@ -176,7 +153,7 @@ In all three workflows, add `corepack enable` before install and replace `npm ci Replace repository script invocations with `pnpm