From ecce62aeae85c9c1e8e0897ced086b5810a0fe36 Mon Sep 17 00:00:00 2001 From: Sawyer Hood Date: Fri, 4 Sep 2026 02:38:00 +0000 Subject: [PATCH] Cut CI setup and tarball smoke time: skip npm registry round trips Every CI job bootstrapped pnpm through pnpm/action-setup, which runs `npm ci` and `pnpm self-update` against the npm registry: 30 s at best, and 7 minutes on runs where the registry stalled, on every job of the run at once. Setup workspace ran at a median of 140-160 s per job across the last 30 runs, against 25 s for a job that got a fast registry. The tarball smoke, the critical path of nearly every run at a median of 382 s on Linux, spent most of that in `npx --package `: unlike the smoke's `npm install`, the npx calls never passed `--no-audit`, so each one blocked on the registry's bulk advisories endpoint. Locally that single call took 211 s of a 246 s smoke. - The setup action downloads the pinned pnpm release binary from GitHub Releases, verified against a committed sha256 list, instead of using pnpm/action-setup. - The smoke passes `--no-audit --no-fund` to both npx invocations. - The smoke prints per-stage timings so the next regression is attributable from the CI log instead of needing a local reproduction. Local smoke: 246 s before, 40 s after. Co-Authored-By: Claude Fable 5.1 --- .github/actions/setup-workspace/action.yml | 14 +- .../actions/setup-workspace/install-pnpm.sh | 63 ++++++++ .../setup-workspace/pnpm-9.15.0.sha256 | 4 + packages/bb-app/scripts/smoke-tarball.mjs | 137 ++++++++++++------ 4 files changed, 165 insertions(+), 53 deletions(-) create mode 100755 .github/actions/setup-workspace/install-pnpm.sh create mode 100644 .github/actions/setup-workspace/pnpm-9.15.0.sha256 diff --git a/.github/actions/setup-workspace/action.yml b/.github/actions/setup-workspace/action.yml index 9a677f81ed..e9b025aeb6 100644 --- a/.github/actions/setup-workspace/action.yml +++ b/.github/actions/setup-workspace/action.yml @@ -6,7 +6,7 @@ inputs: description: Node version to install required: true pnpm-version: - description: pnpm version to install + description: pnpm version to install. Needs a matching pnpm-.sha256 file beside this action. required: true cache-prefix: description: Names this job's Turbo cache entry (e.g. checks, test-server). Every job reads every prefix; this only decides which entry the job writes back. @@ -19,11 +19,15 @@ inputs: runs: using: composite steps: + # A pinned release binary instead of pnpm/action-setup: the action + # bootstraps through `npm ci` and `pnpm self-update`, two npm registry + # round trips that cost 30 s at best and stalled for 7 minutes on every + # job of a run when the registry was slow. See install-pnpm.sh. - name: Set up pnpm - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 - with: - version: ${{ inputs.pnpm-version }} - run_install: false + shell: bash + env: + PNPM_VERSION: ${{ inputs.pnpm-version }} + run: bash "${GITHUB_ACTION_PATH}/install-pnpm.sh" - name: Set up Node.js uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 diff --git a/.github/actions/setup-workspace/install-pnpm.sh b/.github/actions/setup-workspace/install-pnpm.sh new file mode 100755 index 0000000000..e255d097eb --- /dev/null +++ b/.github/actions/setup-workspace/install-pnpm.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# Installs the pinned pnpm release binary straight from GitHub Releases. +# +# pnpm/action-setup bootstraps pnpm with `npm ci` against the npm registry and +# then runs `pnpm self-update`, so every job paid two registry round trips +# before doing any work: 30 s on a good day, and 7 minutes on the runs where +# the registry stalled (the same stall hit every job of the run at once). A +# GitHub release asset is a single download from the CDN the runner already +# talks to for checkout, and the pinned sha256 gives the same integrity the +# action's committed lockfile did. +set -euo pipefail + +version="${PNPM_VERSION:?PNPM_VERSION is required}" +action_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +checksums="${action_dir}/pnpm-${version}.sha256" + +if [[ ! -f "${checksums}" ]]; then + echo "::error::No checksums for pnpm ${version}. Download the release assets from https://github.com/pnpm/pnpm/releases/tag/v${version}, run sha256sum on them, and commit the output as ${checksums}." + exit 1 +fi + +case "$(uname -s)-$(uname -m)" in + Linux-x86_64) asset="pnpm-linux-x64" ;; + Linux-aarch64 | Linux-arm64) asset="pnpm-linux-arm64" ;; + Darwin-arm64) asset="pnpm-macos-arm64" ;; + Darwin-x86_64) asset="pnpm-macos-x64" ;; + *) + echo "::error::Unsupported runner platform $(uname -s)-$(uname -m) for the pnpm release binary." + exit 1 + ;; +esac + +expected="$(awk -v asset="${asset}" '$2 == asset { print $1 }' "${checksums}")" +if [[ -z "${expected}" ]]; then + echo "::error::${checksums} has no entry for ${asset}." + exit 1 +fi + +install_dir="${RUNNER_TEMP:-/tmp}/pnpm-${version}" +mkdir -p "${install_dir}/bin" +binary="${install_dir}/bin/pnpm" + +url="https://github.com/pnpm/pnpm/releases/download/v${version}/${asset}" +curl --fail --silent --show-error --location \ + --retry 5 --retry-all-errors --retry-delay 2 \ + --connect-timeout 15 --max-time 180 \ + --output "${binary}" "${url}" + +if command -v sha256sum >/dev/null 2>&1; then + actual="$(sha256sum "${binary}" | awk '{ print $1 }')" +else + actual="$(shasum -a 256 "${binary}" | awk '{ print $1 }')" +fi +if [[ "${actual}" != "${expected}" ]]; then + echo "::error::sha256 mismatch for ${asset}: expected ${expected}, got ${actual}." + exit 1 +fi + +chmod +x "${binary}" +ln -sf pnpm "${install_dir}/bin/pnpx" +echo "${install_dir}/bin" >> "${GITHUB_PATH}" +echo "PNPM_HOME=${install_dir}/bin" >> "${GITHUB_ENV}" +echo "Installed pnpm $("${binary}" --version) from ${url}" diff --git a/.github/actions/setup-workspace/pnpm-9.15.0.sha256 b/.github/actions/setup-workspace/pnpm-9.15.0.sha256 new file mode 100644 index 0000000000..c9e60df752 --- /dev/null +++ b/.github/actions/setup-workspace/pnpm-9.15.0.sha256 @@ -0,0 +1,4 @@ +1f66770efc74835602642c37b8d31c635c0a90bdfc1401c273c6e957714b7561 pnpm-linux-arm64 +f389709623d29195c5555a32513d633d28aa457c30448d0bb8d87439c7b127dc pnpm-linux-x64 +cfc80f4a44d0d2e7bb581d5ee8436933df5f1d10794349eb52029c71c48534cb pnpm-macos-arm64 +c7f3ee5213019219303fd657197e63ccac292dc487f895f7d885cf1986a5add8 pnpm-macos-x64 diff --git a/packages/bb-app/scripts/smoke-tarball.mjs b/packages/bb-app/scripts/smoke-tarball.mjs index f66e70d379..4690ebb53f 100644 --- a/packages/bb-app/scripts/smoke-tarball.mjs +++ b/packages/bb-app/scripts/smoke-tarball.mjs @@ -60,6 +60,21 @@ const smokeProcessEnv = { BB_TELEMETRY: "false", }; +function formatElapsed(startedAt) { + return `${((performance.now() - startedAt) / 1000).toFixed(1)}s`; +} + +async function timed(label, run) { + const startedAt = performance.now(); + try { + return await run(); + } finally { + process.stdout.write( + `bb-app tarball smoke: ${label} ${formatElapsed(startedAt)}\n`, + ); + } +} + function delay(ms) { return new Promise((resolvePromise) => { setTimeout(resolvePromise, ms); @@ -287,11 +302,22 @@ async function smokeNpxEntrypoint(tarballPath) { // Keep one real invocation through the package's advertised npx path. Once // npx dispatches the bin, the installed-package smokes below cover the same // launcher without repeatedly charging npm startup to readiness budgets. - await runCommand({ - args: ["--yes", "--package", tarballPath, "--", "bb-app", "--help"], - command: "npx", - label: "bb-app npx help", - }); + await timed("npx install and help", () => + runCommand({ + args: [ + "--yes", + "--no-audit", + "--no-fund", + "--package", + tarballPath, + "--", + "bb-app", + "--help", + ], + command: "npx", + label: "bb-app npx help", + }), + ); } async function packTarball() { @@ -796,18 +822,20 @@ async function smokeSdkPackage(tarballPath) { join(sdkDir, "package.json"), JSON.stringify({ type: "module", private: true }, null, 2), ); - await runCommand({ - args: [ - "install", - "--ignore-scripts=false", - "--no-audit", - "--no-fund", - tarballPath, - ], - command: "npm", - cwd: sdkDir, - label: "install bb-app SDK smoke package", - }); + await timed("npm install tarball", () => + runCommand({ + args: [ + "install", + "--ignore-scripts=false", + "--no-audit", + "--no-fund", + tarballPath, + ], + command: "npm", + cwd: sdkDir, + label: "install bb-app SDK smoke package", + }), + ); await runCommand({ args: [ "--input-type=module", @@ -830,26 +858,30 @@ async function smokeSdkPackage(tarballPath) { "", ].join("\n"), ); - await runCommand({ - args: [ - "--yes", - "--package", - "typescript", - "--", - "tsc", - "--module", - "NodeNext", - "--moduleResolution", - "NodeNext", - "--target", - "ES2022", - "--noEmit", - "sdk-smoke.ts", - ], - command: "npx", - cwd: sdkDir, - label: "bb-app SDK TypeScript import", - }); + await timed("npx typescript check", () => + runCommand({ + args: [ + "--yes", + "--no-audit", + "--no-fund", + "--package", + "typescript", + "--", + "tsc", + "--module", + "NodeNext", + "--moduleResolution", + "NodeNext", + "--target", + "ES2022", + "--noEmit", + "sdk-smoke.ts", + ], + command: "npx", + cwd: sdkDir, + label: "bb-app SDK TypeScript import", + }), + ); return sdkDir; } @@ -1096,19 +1128,28 @@ async function smokeDaemonJoin(binDir) { } try { - const tarballPath = await packTarball(); - await smokeNpxEntrypoint(tarballPath); - const sdkDir = await smokeSdkPackage(tarballPath); + const smokeStartedAt = performance.now(); + const tarballPath = await timed("npm pack", () => packTarball()); + await timed("npx entrypoint", () => smokeNpxEntrypoint(tarballPath)); + const sdkDir = await timed("sdk package", () => smokeSdkPackage(tarballPath)); const installedBinDir = join(sdkDir, "node_modules", ".bin"); const installedPackageDir = join(sdkDir, "node_modules", "bb-app"); - await smokeHelpCommands(installedBinDir); - await smokeConfigCommand(installedBinDir); - await smokeInstalledRepack(installedPackageDir); - await smokeProviderBridgeBundles(installedPackageDir); - await smokePluginHostWorkerBundle(installedPackageDir); - await smokeFullStack(installedBinDir, sdkDir); - await smokeDaemonJoin(installedBinDir); - process.stdout.write("bb-app tarball smoke passed\n"); + await timed("help commands", () => smokeHelpCommands(installedBinDir)); + await timed("config command", () => smokeConfigCommand(installedBinDir)); + await timed("installed repack", () => + smokeInstalledRepack(installedPackageDir), + ); + await timed("provider bridge bundles", () => + smokeProviderBridgeBundles(installedPackageDir), + ); + await timed("plugin host worker bundle", () => + smokePluginHostWorkerBundle(installedPackageDir), + ); + await timed("full stack", () => smokeFullStack(installedBinDir, sdkDir)); + await timed("daemon join", () => smokeDaemonJoin(installedBinDir)); + process.stdout.write( + `bb-app tarball smoke passed in ${formatElapsed(smokeStartedAt)}\n`, + ); } finally { await rm(tempRoot, { force: true, recursive: true }); }