diff --git a/.github/workflows/package-smoke.yml b/.github/workflows/package-smoke.yml deleted file mode 100644 index 17236918f..000000000 --- a/.github/workflows/package-smoke.yml +++ /dev/null @@ -1,69 +0,0 @@ -name: Package Smoke - -on: - pull_request: - paths: - - 'package.json' - - 'pnpm-lock.yaml' - - 'pnpm-workspace.yaml' - - 'packages/**' - - 'tsdown.config.ts' - - 'scripts/check-package.ts' - - 'scripts/build-android-helper.sh' - - 'scripts/build-xcuitest-apple.sh' - - 'scripts/package-android-helper.sh' - - 'scripts/package-apple-runner-source.mjs' - - 'scripts/patch-xcuitest-runner-icon.ts' - - 'scripts/sync-mcp-metadata.mjs' - - 'scripts/write-xcuitest-cache-metadata.mjs' - - 'apple/macos-helper/**' - - 'apple/runner/**' - - 'android/ime-helper/**' - - 'android/snapshot-helper/**' - - '.github/actions/setup-node-pnpm/**' - - '.github/workflows/package-smoke.yml' - push: - branches: - - main - paths: - - 'package.json' - - 'pnpm-lock.yaml' - - 'pnpm-workspace.yaml' - - 'packages/**' - - 'tsdown.config.ts' - - 'scripts/check-package.ts' - - 'scripts/build-android-helper.sh' - - 'scripts/build-xcuitest-apple.sh' - - 'scripts/package-android-helper.sh' - - 'scripts/package-apple-runner-source.mjs' - - 'scripts/patch-xcuitest-runner-icon.ts' - - 'scripts/sync-mcp-metadata.mjs' - - 'scripts/write-xcuitest-cache-metadata.mjs' - - 'apple/macos-helper/**' - - 'apple/runner/**' - - 'android/ime-helper/**' - - 'android/snapshot-helper/**' - - '.github/actions/setup-node-pnpm/**' - - '.github/workflows/package-smoke.yml' - -permissions: - contents: read - -concurrency: - group: package-smoke-${{ github.ref }} - cancel-in-progress: true - -jobs: - package: - name: Build npm package - runs-on: macos-26 - timeout-minutes: 30 - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Setup toolchain - uses: ./.github/actions/setup-node-pnpm - - - name: Build complete npm package - run: pnpm prepack diff --git a/package.json b/package.json index d6328db56..99035bf2f 100644 --- a/package.json +++ b/package.json @@ -103,7 +103,10 @@ "package:android-ime-helper:npm": "rm -rf android/ime-helper/dist && AGENT_DEVICE_ANDROID_HELPER=ime sh ./scripts/package-android-helper.sh $(node -p \"require('./package.json').version\") android/ime-helper/dist", "build:macos-helper": "swift build -c release --package-path apple/macos-helper", "build:macos-helper:clean": "swift package --package-path apple/macos-helper clean && pnpm build:macos-helper", - "package:npm": "pnpm build && pnpm build:xcuitest:ios && pnpm build:xcuitest:macos && pnpm build:xcuitest:tvos && pnpm build:xcuitest:visionos && pnpm build:macos-helper:clean && pnpm package:apple-runner:npm && pnpm build:android && pnpm check:package", + "build:package": "pnpm build && pnpm build:xcuitest:ios && pnpm build:xcuitest:macos && pnpm build:xcuitest:tvos && pnpm build:xcuitest:visionos && pnpm build:macos-helper:clean && pnpm package:apple-runner:npm && pnpm build:android", + "package:npm": "pnpm build:package && pnpm check:package", + "release:prepare": "rm -rf .tmp/release && pnpm check:mcp-metadata && pnpm build:package && pnpm check:package -- --pack-destination .tmp/release", + "release:publish": "pnpm release:prepare && npm publish --ignore-scripts .tmp/release/*.tgz", "ad": "node bin/agent-device.mjs", "bench:help-conformance": "node scripts/help-conformance-bench.mjs", "maestro:conformance": "node --experimental-strip-types --test packages/maestro/test/conformance/verify.test.ts packages/maestro/test/conformance/differential/run.test.ts packages/maestro/test/conformance/differential/invariants.test.ts", diff --git a/scripts/check-affected/checks.ts b/scripts/check-affected/checks.ts index 75775003e..2b50649b5 100644 --- a/scripts/check-affected/checks.ts +++ b/scripts/check-affected/checks.ts @@ -84,7 +84,7 @@ export const CHECK_CATALOG: readonly CheckSpec[] = [ id: 'package', label: 'Published package (publint, attw, clean-install resolution)', kind: { type: 'script', script: 'check:package' }, - ciJobs: ['Packaged CLI Node 22.12', 'Build npm package'], + ciJobs: ['Packaged CLI Node 22.12'], // Needs a `pnpm build` output and the npm registry, both of which local runs already have. localRunnable: true, }, diff --git a/scripts/check-package.ts b/scripts/check-package.ts index 19b6f6f98..e230d573c 100644 --- a/scripts/check-package.ts +++ b/scripts/check-package.ts @@ -33,9 +33,19 @@ type PackedManifest = PackedDependencies & { }; const repoRoot = path.resolve(import.meta.dirname, '..'); +const packDestinationFlag = '--pack-destination'; +const suppliedPackDestination = process.argv + .slice(2) + .find((arg, index, args) => (args[index - 1] === packDestinationFlag ? arg : undefined)); +if (process.argv.includes(packDestinationFlag) && !suppliedPackDestination) { + throw new Error(`${packDestinationFlag} requires a destination directory.`); +} // `npm install` resolves `file:` tarballs through the real path, and macOS `/var` is a symlink to // `/private/var`; resolving up front keeps the paths this script prints equal to the ones npm uses. const workDir = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'agent-device-package-')); +const packDestination = suppliedPackDestination + ? path.resolve(repoRoot, suppliedPackDestination) + : workDir; const consumerDir = path.join(workDir, 'consumer'); /** Stdout is captured for the callers that parse it; stderr passes through so failures are readable. */ @@ -70,10 +80,15 @@ function packTarball(): string { if (!fs.existsSync(path.join(repoRoot, 'dist', 'src'))) { throw new Error('No dist/src build found. Run `pnpm build` first.'); } + fs.mkdirSync(packDestination, { recursive: true }); const packed = JSON.parse( - run('npm', ['pack', '--ignore-scripts', '--json', '--pack-destination', workDir], repoRoot), + run( + 'npm', + ['pack', '--ignore-scripts', '--json', '--pack-destination', packDestination], + repoRoot, + ), ) as [{ filename: string }]; - return path.join(workDir, packed[0].filename); + return path.join(packDestination, packed[0].filename); } function lintTarball(tarball: string): void { diff --git a/src/__tests__/npm-package-scripts.test.ts b/src/__tests__/npm-package-scripts.test.ts index 6843aabf8..e46006cbd 100644 --- a/src/__tests__/npm-package-scripts.test.ts +++ b/src/__tests__/npm-package-scripts.test.ts @@ -12,10 +12,6 @@ const perfNightlyWorkflow = fs.readFileSync( path.join(repoRoot, '.github', 'workflows', 'perf-nightly.yml'), 'utf8', ); -const packageSmokeWorkflow = fs.readFileSync( - path.join(repoRoot, '.github', 'workflows', 'package-smoke.yml'), - 'utf8', -); const packagedCliWorkflow = fs.readFileSync( path.join(repoRoot, '.github', 'workflows', 'ci.yml'), 'utf8', @@ -31,17 +27,6 @@ test('prepack builds the complete package without stopping the development daemo assert.equal(script('prepack'), 'pnpm check:mcp-metadata && pnpm package:npm'); assert.doesNotMatch(script('package:npm'), /clean:daemon|rebuild:cli/); assert.equal(packageJson.scripts['build:dev'], undefined); - assert.match(packageSmokeWorkflow, /run: pnpm prepack/); - for (const input of [ - 'pnpm-workspace.yaml', - 'packages/**', - 'scripts/patch-xcuitest-runner-icon.ts', - 'scripts/sync-mcp-metadata.mjs', - 'scripts/write-xcuitest-cache-metadata.mjs', - '.github/actions/setup-node-pnpm/**', - ]) { - assert.ok(packageSmokeWorkflow.includes(`- '${input}'`)); - } }); test('perf uses one platform-selectable entry point', () => { @@ -59,10 +44,11 @@ test('Fallow exposes one changed-code gate and an explicit full-tree audit', () assert.equal(script('fallow:all'), 'fallow --summary'); }); -// `check:package` verifies the tarball, so it has to observe every build output the package ships — -// it runs last, after the Apple and Android payloads exist, not next to the JS build. -test('the npm package build covers every package-owned build output, then verifies the result', () => { - assert.deepEqual(script('package:npm').split(' && '), [ +// `check:package` verifies the tarball, so it has to observe every build output the package ships. +// Keep construction separate from verification so release preparation can publish the exact tarball +// that was installed and smoke-tested instead of rebuilding it during `npm publish`. +test('the npm package build covers every package-owned output before verification', () => { + assert.deepEqual(script('build:package').split(' && '), [ 'pnpm build', 'pnpm build:xcuitest:ios', 'pnpm build:xcuitest:macos', @@ -71,8 +57,8 @@ test('the npm package build covers every package-owned build output, then verifi 'pnpm build:macos-helper:clean', 'pnpm package:apple-runner:npm', 'pnpm build:android', - 'pnpm check:package', ]); + assert.equal(script('package:npm'), 'pnpm build:package && pnpm check:package'); assert.deepEqual(script('build:android').split(' && '), [ 'pnpm package:android-snapshot-helper:npm', @@ -80,10 +66,26 @@ test('the npm package build covers every package-owned build output, then verifi ]); }); -// Behavior of the closure audit is pinned by fixtures in -// scripts/__tests__/package-closure-audit.test.ts, which can fail a malformed package the way no -// test of the real gate can. That leaves the wiring: the audit and both runtime probes have to stay -// wired into the gate, or the fixtures would keep passing while the tarball went unchecked. +test('release publishing uploads the tarball that passed the package gate', () => { + assert.equal( + script('release:prepare'), + 'rm -rf .tmp/release && pnpm check:mcp-metadata && pnpm build:package && pnpm check:package -- --pack-destination .tmp/release', + ); + assert.equal( + script('release:publish'), + 'pnpm release:prepare && npm publish --ignore-scripts .tmp/release/*.tgz', + ); + assert.doesNotMatch(script('release:publish'), /prepack|package:npm/); +}); + +test('the package checker can retain the tarball it verifies for publishing', () => { + const gate = fs.readFileSync(path.join(repoRoot, 'scripts', 'check-package.ts'), 'utf8'); + assert.match(gate, /--pack-destination/); + assert.match(gate, /packDestination/); + assert.match(gate, /fs\.mkdirSync\(packDestination, \{ recursive: true \}\)/); + assert.match(gate, /fs\.rmSync\(workDir, \{ recursive: true, force: true \}\)/); +}); + test('the package gate runs the closure audit and both runtime probes', () => { const gate = fs.readFileSync(path.join(repoRoot, 'scripts', 'check-package.ts'), 'utf8'); for (const call of [