From 29c5faf45be667f19c521b483854ccda56800962 Mon Sep 17 00:00:00 2001 From: jdalton Date: Sat, 11 Jul 2026 20:15:39 -0400 Subject: [PATCH 01/11] ci: inline the fleet-canonical CI workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the Layer-3 reusable ci.yml + _local-not-for-reuse-ci.yml wrapper pair with the fleet-canonical inlined workflow (verbatim wheelhouse template block, including the 'pnpm run test --all' fix โ€” bare 'pnpm test' resolves to the modified scope and skips everything on a clean CI checkout). Repo-owned jobs live below the end marker, matching every other fleet member. Old @sha pins to the removed reusable keep resolving (SHA-pinned refs are immutable). Required-checks note: job names change from 'Run CI Pipeline / ...' to '๐Ÿ”Ž Check', '๐Ÿงช Test', '๐Ÿงช Test NPM Packages'. --- .github/workflows/_local-not-for-reuse-ci.yml | 52 --- .github/workflows/ci.yml | 299 ++++-------------- 2 files changed, 70 insertions(+), 281 deletions(-) delete mode 100644 .github/workflows/_local-not-for-reuse-ci.yml diff --git a/.github/workflows/_local-not-for-reuse-ci.yml b/.github/workflows/_local-not-for-reuse-ci.yml deleted file mode 100644 index 341b381f8..000000000 --- a/.github/workflows/_local-not-for-reuse-ci.yml +++ /dev/null @@ -1,52 +0,0 @@ -# Layer 4 โ€” `_local` wrapper. NOT reused externally; this is -# socket-registry's own CI entry point. References Layer 3 (ci.yml) -# AND inline Layer 1 (cache-npm-packages) + Layer 2b (setup-and-install) -# for the additional jobs it runs alongside the reusable workflow. -# Both ref kinds bump to the PROPAGATION SHA during cascade. See -# updating-workflows skill โ€” Phase 5 (Layer 4) sweeps both ref shapes. - -name: ๐Ÿš€ CI - -# Dependencies: -# - SocketDev/socket-registry/.github/workflows/ci.yml - -on: - push: - branches: [main] - tags: ['*'] - pull_request: - branches: [main] - workflow_dispatch: - -permissions: - contents: read - -jobs: - ci: - name: Run CI Pipeline - uses: SocketDev/socket-registry/.github/workflows/ci.yml@f686e4e3b7554d83a4597f1e60514ea70ce09ed8 # main (2026-07-10) - with: - test-script: 'pnpm run test --all --fast' - node-versions: '["24"]' - os-versions: '["ubuntu-latest", "windows-latest"]' - secrets: - SOCKET_API_TOKEN: ${{ secrets.SOCKET_API_TOKEN }} - - test-npm-packages: - name: Test NPM Packages - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 (2026-05-15) - with: - fetch-depth: 1 - persist-credentials: false - - uses: ./.github/actions/fleet/setup-and-install - with: - node-version: 24 - - - name: Build and test npm packages - shell: bash - run: | - pnpm run build - pnpm run test:npm diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d66fae191..ee232c73d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,185 +1,33 @@ -# Layer 3 โ€” reusable workflow. Each job checks out the CALLER's repo -# (actions/checkout) and runs its local fleet composites -# (./.github/actions/fleet/{setup-and-install,run-script}) โ€” every fleet -# repo carries them via the cascade. When this file bumps, the merge SHA -# becomes the PROPAGATION SHA: Layer 4 (`_local-not-for-reuse-ci.yml`) -# pins to it, and external repos pin their `uses: ...ci.yml@` refs -# to it. See updating-workflows skill at .claude/skills/updating-workflows/. - -name: ๐Ÿ”— CI - -# CI orchestrator that runs checks and tests. -# -# By default runs `pnpm run check --all` (lint + type) as a single job. -# Set `run-check: false` and enable `run-lint`/`run-type-check` to split them. -# -# Dependencies (resolved in the caller's checkout): -# - ./.github/actions/fleet/setup-and-install -# - ./.github/actions/fleet/run-script +# +# Managed by socket-wheelhouse. Don't edit this block locally โ€” edit +# template/base/.github/workflows/ci.yml upstream and re-cascade via +# `pnpm run sync`. Fleet CI runs check + test using the LOCAL composite actions +# under .github/actions/ (inlined โ€” no cross-repo reusable, no `uses:@sha` for +# first-party). Repo-owned jobs go BELOW the END marker (still under the +# top-level `jobs:` key). See scripts/repo/sync-scaffolding/checks/workflow-fleet-block.mts. +name: โšก CI on: - workflow_call: - inputs: - check-node-version: - description: 'Node.js version for checks' - required: false - type: string - default: '26.3.0' - check-script: - description: 'Check command โ€” runs lint + type checks (e.g., "pnpm run check --all")' - required: false - type: string - default: 'pnpm run check --all' - check-setup-script: - description: 'Setup script before checks' - required: false - type: string - default: '' - check-timeout-minutes: - description: 'Timeout for check job in minutes' - required: false - type: number - default: 10 - debug: - description: 'Enable debug output (e.g., "1" for verbose logging)' - required: false - type: string - default: '0' - fail-fast: - description: 'Cancel all matrix jobs if one fails' - required: false - type: boolean - default: false - lint-node-version: - description: 'Node.js version for linting' - required: false - type: string - default: '26.3.0' - lint-script: - description: 'Lint command (e.g., "pnpm run lint --all")' - required: false - type: string - default: 'pnpm run lint --all' - lint-setup-script: - description: 'Setup script before linting' - required: false - type: string - default: '' - lint-timeout-minutes: - description: 'Timeout for lint job in minutes' - required: false - type: number - default: 10 - max-parallel: - description: 'Maximum parallel test jobs' - required: false - type: number - default: 4 - node-versions: - description: 'Node.js versions for testing (e.g., ''["22", "24"]'')' - required: false - type: string - default: '["26.3.0"]' - os-versions: - description: 'Operating systems for testing (e.g., ''["ubuntu-latest", "windows-latest"]'')' - required: false - type: string - default: '["ubuntu-latest", "macos-latest", "windows-latest"]' - run-check: - description: 'Include combined check job (lint + type)' - required: false - type: boolean - default: true - run-lint: - description: 'Include standalone lint job' - required: false - type: boolean - default: false - run-test: - description: 'Include test matrix job' - required: false - type: boolean - default: true - setup-go: - description: | - Run setup-go-toolchain after install. Required for repos whose - test/build steps invoke `go` on macOS/Windows (where the - toolcache go is not always on PATH after sfw setup). - required: false - type: boolean - default: false - setup-go-version: - description: 'Major.minor Go version to ensure when setup-go is true' - required: false - type: string - default: '' - run-type-check: - description: 'Include standalone type check job' - required: false - type: boolean - default: false - test-script: - description: 'Test command (e.g., "pnpm test")' - required: false - type: string - default: 'pnpm test' - test-setup-script: - description: 'Setup script before tests (e.g., "pnpm run build")' - required: false - type: string - default: 'pnpm run build' - test-timeout-minutes: - description: 'Timeout for test jobs in minutes' - required: false - type: number - default: 15 - type-check-node-version: - description: 'Node.js version for type checking' - required: false - type: string - default: '26.3.0' - type-check-script: - description: 'Type check command (e.g., "pnpm run type")' - required: false - type: string - default: 'pnpm run type' - type-check-setup-script: - description: 'Setup script before type checking' - required: false - type: string - default: '' - type-check-timeout-minutes: - description: 'Timeout for type check job in minutes' - required: false - type: number - default: 10 - working-directory: - description: 'Working directory (e.g., "packages/core" for monorepos)' - required: false - type: string - default: '.' - secrets: - SOCKET_API_TOKEN: - description: 'Socket API token โ€” when provided, uses sfw-enterprise instead of sfw-free' - required: false - SOCKET_API_KEY: - description: 'DEPRECATED alias of SOCKET_API_TOKEN. Pass `SOCKET_API_TOKEN:` instead. Tracked for one cycle so existing callers keep working.' - required: false + push: + branches: [main] + tags: ['*'] + pull_request: + branches: [main] + workflow_dispatch: permissions: contents: read concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true -# Fleet no-phone-home posture, matched to dev machines (setup-security-tools -# persists these knobs to the shell-rc). CI runners don't source that, so -# npm/pnpm would run their update-notifier registry check โ€” and the -# telemetry-env-is-disabled + package-manager-auto-update-is-disabled gates -# (run under `check --all`) fail. Set every FLEET_ENV knob workflow-level so -# each job + step in this reusable CI inherits it. Lockstep source: -# .claude/hooks/fleet/_shared/fleet-env.mts (FLEET_ENV). +# Fleet no-phone-home posture: CI runners don't source the shell-rc that dev +# machines get from setup-security-tools, so set every FLEET_ENV knob +# workflow-level or the telemetry-env-is-disabled + +# package-manager-auto-update-is-disabled gates (under `check --all`) fail. +# Lockstep source: .claude/hooks/fleet/_shared/fleet-env.mts (FLEET_ENV) โ€” +# the telemetry-env-is-disabled check asserts each knob at CI runtime. env: CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1' COREPACK_ENABLE_PROJECT_SPEC: '0' @@ -188,11 +36,16 @@ env: NO_UPDATE_NOTIFIER: '1' jobs: + # First step of every job is the third-party actions/checkout (GitHub fetches + # it independently) to populate the workspace so the LOCAL `./.github/actions/*` + # composites resolve. setup-and-install then re-checks-out at its deeper + # default (fetch-depth 25 โ€” covers CI's git operations) and + # runs the zizmor Actions audit (its own `strategy.job-total < 2` skip runs it + # in the non-matrix check job, skips it in the test matrix). check: - if: inputs.run-check name: ๐Ÿ”Ž Check runs-on: ubuntu-latest - timeout-minutes: ${{ inputs.check-timeout-minutes }} + timeout-minutes: 10 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 (2026-05-15) with: @@ -200,22 +53,20 @@ jobs: persist-credentials: false - uses: ./.github/actions/fleet/setup-and-install with: - debug: ${{ inputs.debug }} - node-version: ${{ inputs.check-node-version }} - socket-api-token: ${{ secrets.SOCKET_API_TOKEN || secrets.SOCKET_API_KEY }} - working-directory: ${{ inputs.working-directory }} - + socket-api-token: ${{ secrets.SOCKET_API_TOKEN_FOR_CLI_AND_SFW }} - uses: ./.github/actions/fleet/run-script with: - main-script: ${{ inputs.check-script }} - setup-script: ${{ inputs.check-setup-script }} - working-directory: ${{ inputs.working-directory }} + main-script: pnpm run check --all - lint: - if: inputs.run-lint - name: ๐Ÿงน Lint - runs-on: ubuntu-latest - timeout-minutes: ${{ inputs.lint-timeout-minutes }} + test: + name: ๐Ÿงช Test + strategy: + fail-fast: false + max-parallel: 4 + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + timeout-minutes: 15 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 (2026-05-15) with: @@ -223,22 +74,26 @@ jobs: persist-credentials: false - uses: ./.github/actions/fleet/setup-and-install with: - debug: ${{ inputs.debug }} - node-version: ${{ inputs.lint-node-version }} - socket-api-token: ${{ secrets.SOCKET_API_TOKEN || secrets.SOCKET_API_KEY }} - working-directory: ${{ inputs.working-directory }} - + socket-api-token: ${{ secrets.SOCKET_API_TOKEN_FOR_CLI_AND_SFW }} - uses: ./.github/actions/fleet/run-script + env: + # Authenticate build-time GitHub API reads (release listings, + # prebuilt-artifact downloads). Unauthenticated calls share the + # hosted runner's IP-scoped rate limit and 403 under load. + GH_TOKEN: ${{ github.token }} with: - main-script: ${{ inputs.lint-script }} - setup-script: ${{ inputs.lint-setup-script }} - working-directory: ${{ inputs.working-directory }} + setup-script: pnpm run build + main-script: pnpm run test --all + # - type-check: - if: inputs.run-type-check - name: ๐Ÿท๏ธ Type Check + # โ”€โ”€ Repo-owned jobs (below the fleet-canonical block) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + # socket-registry's npm-override packages get their own build + vitest + # pass; `pnpm test` above covers the repo's unit suite, this covers the + # 131 packages/npm overrides (scripts/npm/run-vitest-npm.mts). + test-npm-packages: + name: ๐Ÿงช Test NPM Packages runs-on: ubuntu-latest - timeout-minutes: ${{ inputs.type-check-timeout-minutes }} + timeout-minutes: 15 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 (2026-05-15) with: @@ -246,28 +101,22 @@ jobs: persist-credentials: false - uses: ./.github/actions/fleet/setup-and-install with: - debug: ${{ inputs.debug }} - node-version: ${{ inputs.type-check-node-version }} - socket-api-token: ${{ secrets.SOCKET_API_TOKEN || secrets.SOCKET_API_KEY }} - working-directory: ${{ inputs.working-directory }} - + socket-api-token: ${{ secrets.SOCKET_API_TOKEN_FOR_CLI_AND_SFW }} - uses: ./.github/actions/fleet/run-script with: - main-script: ${{ inputs.type-check-script }} - setup-script: ${{ inputs.type-check-setup-script }} - working-directory: ${{ inputs.working-directory }} + setup-script: pnpm run build + main-script: pnpm run test:npm - test: - if: inputs.run-test - name: ๐Ÿงช Test Matrix - strategy: - fail-fast: ${{ inputs.fail-fast }} - max-parallel: ${{ inputs.max-parallel }} - matrix: - node-version: ${{ fromJson(inputs.node-versions) }} - os: ${{ fromJson(inputs.os-versions) }} - runs-on: ${{ matrix.os }} - timeout-minutes: ${{ inputs.test-timeout-minutes }} + # Every override declares `engines.node: >=24`, but the jobs above run a + # newer current-line Node โ€” nothing executes the FLOOR, so a too-new + # syntax/API in an override stays green here and only breaks in a consumer + # pinned to the floor. This job side-installs the exact floor release + # (sha256-verified against nodejs.org's published SHASUMS256 before + # extraction) and dynamic-imports every override's node entry under it. + floor-node-smoke: + name: ๐Ÿงช NPM Overrides on Floor Node + runs-on: ubuntu-latest + timeout-minutes: 10 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 (2026-05-15) with: @@ -275,15 +124,7 @@ jobs: persist-credentials: false - uses: ./.github/actions/fleet/setup-and-install with: - debug: ${{ inputs.debug }} - node-version: ${{ matrix.node-version }} - setup-go: ${{ inputs.setup-go && 'true' || 'false' }} - setup-go-version: ${{ inputs.setup-go-version }} - socket-api-token: ${{ secrets.SOCKET_API_TOKEN || secrets.SOCKET_API_KEY }} - working-directory: ${{ inputs.working-directory }} - + socket-api-token: ${{ secrets.SOCKET_API_TOKEN_FOR_CLI_AND_SFW }} - uses: ./.github/actions/fleet/run-script with: - main-script: ${{ inputs.test-script }} - setup-script: ${{ inputs.test-setup-script }} - working-directory: ${{ inputs.working-directory }} + main-script: pnpm run test:npm:floor From 40f9475e922c740a768c8dc0d26dd69a85cbd251 Mon Sep 17 00:00:00 2001 From: jdalton Date: Sat, 11 Jul 2026 20:15:39 -0400 Subject: [PATCH 02/11] test(npm): gate every override on the exact engines floor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All 131 overrides declare engines.node >=24, but CI only ever ran a newer current-line Node โ€” nothing proved the floor. The new floor-node-smoke gate side-installs Node 24.0.0 (pinned version + sha256 verified against nodejs.org's SHASUMS256 before extraction; the binary never touches PATH) and dynamic-imports every override's node entry under it, including the JSON-module entries (date, es-iterator-helpers) via 'with { type: json }'. Verified locally: 131 imported, 0 failed. --- package.json | 1 + scripts/npm/floor-node-smoke.mts | 214 +++++++++++++++++++++++++++++++ 2 files changed, 215 insertions(+) create mode 100644 scripts/npm/floor-node-smoke.mts diff --git a/package.json b/package.json index 88aa972ac..e9bc87645 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "release-npm": "node scripts/npm/release-npm-packages.mts", "test": "node scripts/fleet/test.mts", "test:npm": "node scripts/npm/run-vitest-npm.mts", + "test:npm:floor": "node scripts/npm/floor-node-smoke.mts", "type": "tsgo --noEmit -p tsconfig.check.json", "update": "node scripts/fleet/update.mts", "update:external-tools": "node scripts/update-external-tools.mts", diff --git a/scripts/npm/floor-node-smoke.mts b/scripts/npm/floor-node-smoke.mts new file mode 100644 index 000000000..31084a8cb --- /dev/null +++ b/scripts/npm/floor-node-smoke.mts @@ -0,0 +1,214 @@ +#!/usr/bin/env node +/** + * @file Prove every npm override runs on the FLOOR Node its engines declare. + * All packages/npm overrides ship `engines.node: >=24`, but regular CI runs + * a newer current-line Node โ€” nothing executes the floor, so an override + * that quietly uses newer-than-floor syntax/APIs stays green here and only + * breaks inside a consumer pinned to the floor. This gate side-installs the + * EXACT floor release (pinned version + sha256, verified before extraction โ€” + * a mismatched download hard-fails) and dynamic-imports every override's + * node entry under that binary. + * The floor binary is a side install: it never touches PATH โ€” the harness + * keeps running on the repo's own Node and spawns the floor binary + * explicitly, so the two runtimes can't be confused. + * Usage: node scripts/npm/floor-node-smoke.mts + * Env: FLOOR_NODE_DIR overrides the install dir (default: RUNNER_TEMP, + * falling back to os.tmpdir()). + */ +import { + existsSync, + mkdirSync, + readdirSync, + readFileSync, + rmSync, +} from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import process from 'node:process' +import { fileURLToPath, pathToFileURL } from 'node:url' + +import { httpDownload } from '@socketsecurity/lib-stable/http-request/download' +import { getDefaultLogger } from '@socketsecurity/lib-stable/logger/default' +import { spawnSync } from '@socketsecurity/lib-stable/process/spawn/child' + +const logger = getDefaultLogger() + +// The floor is the low edge of packages/npm/*'s `engines.node: >=24` range. +// Bump BOTH the version and every sha256 together when the engines floor +// moves โ€” hashes come from https://nodejs.org/dist/v/SHASUMS256.txt. +const FLOOR_NODE_VERSION = '24.0.0' +const FLOOR_NODE_PLATFORMS: Record = + { + 'darwin-arm64': { + asset: `node-v${FLOOR_NODE_VERSION}-darwin-arm64.tar.gz`, + sha256: + '194e2f3dd3ec8c2adcaa713ed40f44c5ca38467880e160974ceac1659be60121', + }, + 'darwin-x64': { + asset: `node-v${FLOOR_NODE_VERSION}-darwin-x64.tar.gz`, + sha256: + 'f716b3ce14a7e37a6cbf97c9de10d444d7da07ef833cd8da81dd944d111e6a4a', + }, + 'linux-x64': { + asset: `node-v${FLOOR_NODE_VERSION}-linux-x64.tar.xz`, + sha256: + '59b8af617dccd7f9f68cc8451b2aee1e86d6bd5cb92cd51dd6216a31b707efd7', + }, + 'win-x64': { + asset: `node-v${FLOOR_NODE_VERSION}-win-x64.zip`, + sha256: + '3d0fff80c87bb9a8d7f49f2f27832aa34a1477d137af46f5b14df5498be81304', + }, + } + +const repoRoot = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '../..', +) +const npmPackagesPath = path.join(repoRoot, 'packages/npm') + +/** + * Download + verify + extract the floor Node, returning its binary path. + * `httpDownload` streams to a temp sibling, checks the pinned sha256, and + * atomic-renames on success โ€” a tampered or truncated download never + * materializes at the archive path, let alone extracts. + */ +async function ensureFloorNode(): Promise { + const platKey = `${process.platform}-${process.arch}` + const pin = FLOOR_NODE_PLATFORMS[platKey] + if (!pin) { + throw new Error( + `no floor-node pin for ${platKey} โ€” add its asset + sha256 (from ` + + `https://nodejs.org/dist/v${FLOOR_NODE_VERSION}/SHASUMS256.txt) to FLOOR_NODE_PLATFORMS`, + ) + } + const baseDir = + process.env['FLOOR_NODE_DIR'] || process.env['RUNNER_TEMP'] || os.tmpdir() + // Strip the archive suffix (.tar.gz / .tar.xz / .zip) to get the top-level + // directory name the tarball extracts to (node-v--). + const extractDir = path.join( + baseDir, + pin.asset.replace(/\.(tar\.(gz|xz)|zip)$/, ''), + ) + const binPath = + process.platform === 'win32' + ? path.join(extractDir, 'node.exe') + : path.join(extractDir, 'bin/node') + if (existsSync(binPath)) { + return binPath + } + const url = `https://nodejs.org/dist/v${FLOOR_NODE_VERSION}/${pin.asset}` + logger.log(`downloading floor node ${FLOOR_NODE_VERSION} (${pin.asset})`) + mkdirSync(baseDir, { recursive: true }) + const archivePath = path.join(baseDir, pin.asset) + await httpDownload(url, archivePath, { sha256: pin.sha256 }) + // `tar -xf` auto-detects gz/xz; bsdtar (macOS + Windows runners) also + // extracts zip through the same flag. + const tar = spawnSync('tar', ['-xf', archivePath, '-C', baseDir], { + stdio: 'inherit', + }) + rmSync(archivePath, { force: true }) + if (tar.status !== 0 || !existsSync(binPath)) { + rmSync(extractDir, { force: true, recursive: true }) + throw new Error(`floor node extract failed (${pin.asset})`) + } + return binPath +} + +/** + * Resolve a package's node-runtime entry from its `exports` map (the + * overrides ship no `main`). Condition preference mirrors Node's own + * resolution for a `node` consumer; `types` never matches by construction. + */ +function resolveNodeEntry(exp: unknown): string | undefined { + if (typeof exp === 'string') { + return exp + } + if (!exp || typeof exp !== 'object') { + return undefined + } + const conditions = exp as Record + const conditionOrder = ['node', 'import', 'require', 'default'] + for (let i = 0, { length } = conditionOrder; i < length; i += 1) { + const key = conditionOrder[i]! + if (key in conditions) { + const resolved = resolveNodeEntry(conditions[key]) + if (resolved) { + return resolved + } + } + } + return undefined +} + +async function main(): Promise { + const floorNode = await ensureFloorNode() + const version = spawnSync(floorNode, ['--version'], { encoding: 'utf8' }) + logger.log( + `floor node ready: ${String(version.stdout).trim()} at ${floorNode}`, + ) + + const failures: string[] = [] + let imported = 0 + let skipped = 0 + const dirents = readdirSync(npmPackagesPath, { withFileTypes: true }) + for (let i = 0, { length } = dirents; i < length; i += 1) { + const dirent = dirents[i]! + if (!dirent.isDirectory()) { + continue + } + const pkgDir = path.join(npmPackagesPath, dirent.name) + const pkgJsonPath = path.join(pkgDir, 'package.json') + if (!existsSync(pkgJsonPath)) { + continue + } + const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf8')) + const entryRel = pkgJson.exports + ? resolveNodeEntry(pkgJson.exports['.'] ?? pkgJson.exports) + : pkgJson.main + if (!entryRel) { + skipped += 1 + logger.log( + `skip ${dirent.name}: no node entry (bin-only or asset package)`, + ) + continue + } + const entryUrl = pathToFileURL(path.join(pkgDir, entryRel)).href + // Dynamic import loads every entry kind a floor-pinned ESM consumer can + // reach: .cjs, .js/ESM, and .json (which requires the import attribute + // on every Node โ€” `date` and `es-iterator-helpers` ship JSON entries). + const run = spawnSync( + floorNode, + [ + '--input-type=module', + '-e', + "const s = process.argv[1]; await (s.endsWith('.json') ? import(s, { with: { type: 'json' } }) : import(s))", + entryUrl, + ], + { encoding: 'utf8' }, + ) + if (run.status === 0) { + imported += 1 + } else { + failures.push( + `${dirent.name}: ${String(run.stderr).trim().split('\n')[0]}`, + ) + } + } + + logger.log( + `floor-node smoke: ${imported} imported, ${skipped} skipped, ${failures.length} failed ` + + `(node ${FLOOR_NODE_VERSION})`, + ) + if (failures.length > 0) { + for (let i = 0, { length } = failures; i < length; i += 1) { + logger.error(`FAIL ${failures[i]}`) + } + process.exitCode = 1 + } +} + +main().catch((e: unknown) => { + logger.error(e) + process.exitCode = 1 +}) From bcae77a563560376f28535c28254c19aaca944ab Mon Sep 17 00:00:00 2001 From: jdalton Date: Sat, 11 Jul 2026 22:45:55 -0400 Subject: [PATCH 03/11] ci: env-indirect agentics record-output inputs (zizmor template-injection) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two pre-existing high findings โ€” inputs.operation / inputs.run_url expanded directly inside run: โ€” fail the setup-and-install zizmor audit in every CI job, on main too (main CI is currently red on them). Route both through step env vars. --- .github/workflows/agentics-maintenance.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/agentics-maintenance.yml b/.github/workflows/agentics-maintenance.yml index 1c7a3edbb..46df1d643 100644 --- a/.github/workflows/agentics-maintenance.yml +++ b/.github/workflows/agentics-maintenance.yml @@ -195,7 +195,9 @@ jobs: - name: Record outputs id: record - run: echo "operation=${{ inputs.operation }}" >> "$GITHUB_OUTPUT" + env: + OPERATION: ${{ inputs.operation }} + run: echo "operation=$OPERATION" >> "$GITHUB_OUTPUT" update_pull_request_branches: if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'update_pull_request_branches' && (!(github.event.repository.fork)) }} @@ -280,7 +282,9 @@ jobs: - name: Record outputs id: record - run: echo "run_url=${{ inputs.run_url }}" >> "$GITHUB_OUTPUT" + env: + RUN_URL: ${{ inputs.run_url }} + run: echo "run_url=$RUN_URL" >> "$GITHUB_OUTPUT" create_labels: if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'create_labels' && (!(github.event.repository.fork)) }} From 16a3e30d9997a52db1ec39c2ac003eac8c8bb4d9 Mon Sep 17 00:00:00 2001 From: jdalton Date: Sat, 11 Jul 2026 23:02:58 -0400 Subject: [PATCH 04/11] docs: retire ci.yml from the Layer-3 architecture docs and hooks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ci.yml is now the fleet-canonical inlined workflow, so: - README badges point at ci.yml (the _local wrapper is gone) - shared-workflow-cascade hook watches the real Layer-3 reusables (weekly-update / publish-npm / get-green) instead of ci.yml and the never-existent provenance.yml โ€” an inlined-ci edit no longer demands a cross-repo cascade - updating-workflows reference, repo architecture doc, pin-guard header, and the fleet cleaning-ci/agent-ci skills describe the current split: inlined fleet block for CI, @sha pins only for the surviving reusables (also fixes the stale provenance.yml name โ€” the file is publish-npm.yml) Audit verdict on the rest: weekly-update.yml + publish-npm.yml + get-green.yml stay workflow_call BY DESIGN (the wheelhouse template itself delegates to them); .lock.yml files are gh-aw-owned; agentics-maintenance.yml's workflow_call trigger has no callers but is dormant and harmless โ€” left as-is. --- .../no-hand-edit-registry-pin-guard/index.mts | 2 +- .../repo/shared-workflow-cascade/index.mts | 16 ++++++++------ .claude/skills/fleet/agent-ci/reference.md | 2 +- .claude/skills/fleet/cleaning-ci/SKILL.md | 2 +- .../repo/updating-workflows/reference.md | 21 ++++++++++--------- README.md | 2 +- docs/agents.md/repo/architecture.md | 2 +- registry/README.md | 2 +- 8 files changed, 27 insertions(+), 22 deletions(-) diff --git a/.claude/hooks/fleet/no-hand-edit-registry-pin-guard/index.mts b/.claude/hooks/fleet/no-hand-edit-registry-pin-guard/index.mts index 3b1781b0d..ca615dff3 100644 --- a/.claude/hooks/fleet/no-hand-edit-registry-pin-guard/index.mts +++ b/.claude/hooks/fleet/no-hand-edit-registry-pin-guard/index.mts @@ -4,7 +4,7 @@ // BLOCKS an Edit/Write that changes a SocketDev/socket-registry shared // workflow/action SHA pin by hand: // -// uses: SocketDev/socket-registry/.github/workflows/ci.yml@<40-hex> +// uses: SocketDev/socket-registry/.github/workflows/weekly-update.yml@<40-hex> // uses: SocketDev/socket-registry/.github/actions/setup-and-install@<40-hex> // // Those pins are OWNED by the cascade: `cascade-workflows.mts` (in diff --git a/.claude/hooks/repo/shared-workflow-cascade/index.mts b/.claude/hooks/repo/shared-workflow-cascade/index.mts index 662a64093..8105487fa 100644 --- a/.claude/hooks/repo/shared-workflow-cascade/index.mts +++ b/.claude/hooks/repo/shared-workflow-cascade/index.mts @@ -2,8 +2,10 @@ // Claude Code PostToolUse hook โ€” shared-workflow-cascade (repo-specific). // // TRIGGER: After any Bash tool call that commits or pushes a change to -// a shared workflow file in socket-registry (.github/workflows/ci.yml, -// .github/workflows/provenance.yml, or any _local-not-for-reuse-*.yml). +// a shared workflow file in socket-registry (.github/workflows/ +// weekly-update.yml / publish-npm.yml / get-green.yml, or any +// _local-not-for-reuse-*.yml). ci.yml is NOT shared anymore โ€” it is the +// fleet-canonical INLINED workflow cascaded from socket-wheelhouse. // // WHAT IT DOES: When a shared workflow file changes on main, the // consuming fleet repos have stale SHA pins. This hook emits a REMINDER @@ -19,8 +21,9 @@ // // DETECTION: inspects the latest commit on the current branch for // changes to the canonical shared-workflow paths: -// - .github/workflows/ci.yml (Layer 3 reusable workflow) -// - .github/workflows/provenance.yml (Layer 3 reusable workflow) +// - .github/workflows/weekly-update.yml (Layer 3 reusable workflow) +// - .github/workflows/publish-npm.yml (Layer 3 reusable workflow) +// - .github/workflows/get-green.yml (Layer 3 reusable workflow) // - .github/workflows/_local-not-for-reuse-*.yml (Layer 4 โ€” signals cascade complete) // // EXIT 2 = block (with error message to stderr). @@ -42,8 +45,9 @@ const BYPASS_PHRASE = 'Allow workflow-cascade bypass' // Shared workflow files whose change triggers the cascade obligation. const SHARED_WORKFLOW_PATTERNS: readonly RegExp[] = [ - /\.github\/workflows\/ci\.yml$/, - /\.github\/workflows\/provenance\.yml$/, + /\.github\/workflows\/weekly-update\.yml$/, + /\.github\/workflows\/publish-npm\.yml$/, + /\.github\/workflows\/get-green\.yml$/, /\.github\/workflows\/_local-not-for-reuse-.*\.yml$/, ] diff --git a/.claude/skills/fleet/agent-ci/reference.md b/.claude/skills/fleet/agent-ci/reference.md index e174b12f3..7a6beaa53 100644 --- a/.claude/skills/fleet/agent-ci/reference.md +++ b/.claude/skills/fleet/agent-ci/reference.md @@ -32,7 +32,7 @@ When stdout is not a TTY (piped, redirected, captured by a parent process), the ## Requirements rationale - **Docker.** agent-ci executes each workflow job inside a container, the same way GitHub's runners do. It connects via `AGENT_CI_DOCKER_HOST` (default `unix:///var/run/docker.sock`) โ€” **not** the standard `DOCKER_HOST` (setting `DOCKER_HOST` makes agent-ci exit with a rename error; use `AGENT_CI_DOCKER_HOST` for a remote `ssh://`/`tcp://` daemon). Without a running daemon the run cannot start; it fails fast with a dangling-socket message and exit 1. On macOS the fleet provider is **OrbStack** (`open -a OrbStack`, then `docker info` to confirm). There is no degraded mode; if you can't start a daemon, use `greening-ci` (push and watch remote CI) instead. -- **Remote reusable workflows.** A fleet `ci.yml` doesn't contain the jobs โ€” it `uses:` a `SocketDev/socket-registry/.github/workflows/ci.yml@` reusable workflow. agent-ci fetches that over the network, which needs `--github-token` (bare flag โ†’ `gh auth token`, or `AGENT_CI_GITHUB_TOKEN`). Without it the reusable workflow can't resolve and the run can't assemble the job graph. +- **Remote reusable workflows.** A fleet `ci.yml` is INLINED (fleet-canonical block cascaded from socket-wheelhouse) โ€” no remote fetch needed for it. But other workflows still `uses:` remote reusables (e.g. the `SocketDev/socket-registry` `weekly-update.yml` / `publish-npm.yml` delegators); running those needs `--github-token` (bare flag โ†’ `gh auth token`, or `AGENT_CI_GITHUB_TOKEN`) or the run can't assemble the job graph. - **macOS jobs.** `runs-on: macos-*` jobs run in a real throwaway macOS VM via `tart` (Apple Silicon only) with `sshpass`. Missing either tool, or on Linux/Intel, those jobs **skip with a reason** rather than failing the run; the Linux/container jobs still execute. VM concurrency caps at `AGENT_CI_MACOS_VM_CONCURRENCY` (default 2 โ€” tart's free tier). Windows jobs (`runs-on: windows-*`) always skip (unsupported). - **Missing tools in the runner image.** Jobs run in `ghcr.io/actions/actions-runner:latest`, which ships node/git/curl/jq/unzip but **not** build toolchains, `python3`, or `xz`. A job failing on a missing tool isn't your code โ€” add a `.github/agent-ci.Dockerfile` (`FROM ghcr.io/actions/actions-runner:latest` + `apt-get install`); agent-ci picks it up automatically and caches by content hash. - **Install.** `@redwoodjs/agent-ci` is a fleet devDependency declared as `catalog:` in every repo's `package.json`, pinned in the wheelhouse `pnpm-workspace.yaml` catalog. `pnpm install` provisions it. The published package is a self-contained Node CLI (`dist/cli.js`) โ€” it has no platform-binary dependencies and its `ssh2` native build scripts are declined in the fleet's `allowBuilds`/`allowScripts` (the CLI runs without them). diff --git a/.claude/skills/fleet/cleaning-ci/SKILL.md b/.claude/skills/fleet/cleaning-ci/SKILL.md index ce3449144..7867a1f7b 100644 --- a/.claude/skills/fleet/cleaning-ci/SKILL.md +++ b/.claude/skills/fleet/cleaning-ci/SKILL.md @@ -12,7 +12,7 @@ context: fork Audit + clean redundant CI surface on a Socket fleet repo. Three target classes: -1. **Orphan workflow YAML files**: `lint.yml`, `check.yml`, `type.yml`, `test.yml`. The fleet consolidated those into the shared `ci.yml` (via `SocketDev/socket-registry/.github/workflows/ci.yml`) long ago. Any per-repo file with those names is a leftover from pre-consolidation days. Delete them. +1. **Orphan workflow YAML files**: `lint.yml`, `check.yml`, `type.yml`, `test.yml`. The fleet consolidated those into `ci.yml` long ago (today the fleet-canonical INLINED ci.yml cascaded from socket-wheelhouse; historically the `SocketDev/socket-registry` reusable). Any per-repo file with those names is a leftover from pre-consolidation days. Delete them. 2. **GitHub-Dependabot automated security PRs**: the fleet pattern is to handle vulnerability fixes via `/updating-security` (pnpm `overrides:` for transitive deps), not via auto-PRs from Dependabot. The `dependabot.yml` no-op file (`open-pull-requests-limit: 0`) suppresses version-update PRs but does NOT suppress security PRs. Those flow from a separate repo-settings toggle (`automated-security-fixes`). Disable via `gh api -X DELETE /repos/{owner}/{repo}/automated-security-fixes`. diff --git a/.claude/skills/repo/updating-workflows/reference.md b/.claude/skills/repo/updating-workflows/reference.md index d9ccb3175..eb5cb209b 100644 --- a/.claude/skills/repo/updating-workflows/reference.md +++ b/.claude/skills/repo/updating-workflows/reference.md @@ -30,8 +30,9 @@ Layer 2b โ€” setup-and-install (references Layer 1 + 2a): setup-and-install -> refs: checkout, setup, install Layer 3 โ€” Shared reusable workflows (reference Layer 2): - ci.yml -> refs: setup-and-install, run-script - provenance.yml -> refs: setup-and-install + (ci.yml LEFT this layer: it is now the fleet-canonical INLINED workflow + cascaded from socket-wheelhouse โ€” no consumers pin it anymore.) + publish-npm.yml -> refs: setup-and-install weekly-update.yml -> thin delegator consumers pin; refs (uses:) weekly-update.lock.yml. Hand-authored, cascaded. weekly-update.lock.yml -> gh-aw compiled from weekly-update.md; runs the @@ -45,8 +46,7 @@ Layer 3 โ€” Shared reusable workflows (reference Layer 2): escalation worker); same gh-aw-owned rules. Layer 4 โ€” _local workflows (reference Layer 3, not reused externally): - _local-not-for-reuse-ci.yml -> refs: ci.yml, setup-and-install, cache-npm-packages - _local-not-for-reuse-provenance.yml -> refs: provenance.yml + _local-not-for-reuse-provenance.yml -> refs: publish-npm.yml _local-not-for-reuse-weekly-update.yml -> refs: weekly-update.yml (the delegator, via uses) ``` @@ -74,9 +74,9 @@ substrate + testing story (`gh aw trial`, not Agent CI). ## Propagation SHA -The **propagation SHA** is the Layer 3 merge SHA โ€” the one where `ci.yml`, -`provenance.yml`, `weekly-update.yml` (the delegator), or `weekly-update.lock.yml` -were updated. +The **propagation SHA** is the Layer 3 merge SHA โ€” the one where +`publish-npm.yml`, `weekly-update.yml` (the delegator), or +`weekly-update.lock.yml` were updated. - Layer 4 (`_local-not-for-reuse-*`) pins to the propagation SHA - External repos pin to the propagation SHA @@ -94,7 +94,7 @@ Starting from the layer **above** the change, create a PR for each layer. ``` 1. PR: Update Layer 2a pins (setup) -> merge -> get SHA 2. PR: Update Layer 2b pins (setup-and-install) -> merge -> get SHA -3. PR: Update Layer 3 pins (ci.yml, provenance.yml) -> merge -> get SHA <-- PROPAGATION SHA +3. PR: Update Layer 3 pins (publish-npm, weekly-update) -> merge -> get SHA <-- PROPAGATION SHA 4. PR: Update Layer 4 pins (_local workflows) -> merge 5. Propagate the Layer 3 SHA to all consuming repos ``` @@ -113,8 +113,9 @@ pinning during the cascade: line that delegates to a Layer 3 reusable workflow. 2. Inline `uses: SocketDev/socket-registry/.github/actions/@` steps inside additional jobs the wrapper runs (e.g. - `_local-not-for-reuse-ci.yml`'s `test-npm-packages` job has its own - `setup-and-install` step on top of delegating to `ci.yml`). + a wrapper running extra jobs alongside its delegation โ€” historically + `_local-not-for-reuse-ci.yml`'s `test-npm-packages`, whose jobs now live + below the inlined `ci.yml`'s fleet-canonical block). Both go to the propagation SHA. Verify with: diff --git a/README.md b/README.md index 8238b869d..642cc3dbb 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # @socketregistry [![Socket Badge](https://socket.dev/api/badge/npm/package/@socketsecurity/registry)](https://socket.dev/npm/package/@socketsecurity/registry) -[![CI](https://github.com/SocketDev/socket-registry/actions/workflows/_local-not-for-reuse-ci.yml/badge.svg)](https://github.com/SocketDev/socket-registry/actions/workflows/_local-not-for-reuse-ci.yml) +[![CI](https://github.com/SocketDev/socket-registry/actions/workflows/ci.yml/badge.svg)](https://github.com/SocketDev/socket-registry/actions/workflows/ci.yml) ![Coverage](https://img.shields.io/badge/coverage-99.10%25-brightgreen) [![Follow @SocketSecurity](https://img.shields.io/twitter/follow/SocketSecurity?style=social)](https://twitter.com/SocketSecurity) diff --git a/docs/agents.md/repo/architecture.md b/docs/agents.md/repo/architecture.md index 3be4bc9eb..a3825013d 100644 --- a/docs/agents.md/repo/architecture.md +++ b/docs/agents.md/repo/architecture.md @@ -53,7 +53,7 @@ Quick orientation: ## CI testing -- **Mandatory**: invoke `SocketDev/socket-registry/.github/workflows/ci.yml@` with a full 40-char commit SHA. +- `ci.yml` is the fleet-canonical INLINED workflow (cascaded from socket-wheelhouse); repo-owned jobs go below its end marker. The @ pin rule still applies to the surviving registry reusables (`weekly-update.yml`, `publish-npm.yml`, `get-green.yml`). - Matrix: Node 22 and 24, cross-platform (Linux + macOS + Windows where applicable). - CI-script naming: `lint-ci`, `test-ci`, `type-ci`. No watch or fix modes in CI. diff --git a/registry/README.md b/registry/README.md index e73a88f42..6318737af 100644 --- a/registry/README.md +++ b/registry/README.md @@ -1,7 +1,7 @@ # @socketsecurity/registry [![Socket Badge](https://socket.dev/api/badge/npm/package/@socketsecurity/registry)](https://socket.dev/npm/package/@socketsecurity/registry) -[![CI](https://github.com/SocketDev/socket-registry/actions/workflows/_local-not-for-reuse-ci.yml/badge.svg)](https://github.com/SocketDev/socket-registry/actions/workflows/_local-not-for-reuse-ci.yml) +[![CI](https://github.com/SocketDev/socket-registry/actions/workflows/ci.yml/badge.svg)](https://github.com/SocketDev/socket-registry/actions/workflows/ci.yml) [![Follow @SocketSecurity](https://img.shields.io/twitter/follow/SocketSecurity?style=social)](https://twitter.com/SocketSecurity) [![Follow @socket.dev on Bluesky](https://img.shields.io/badge/Follow-@socket.dev-1DA1F2?style=social&logo=bluesky)](https://bsky.app/profile/socket.dev) From ad4a42a8fbcceb349054862c3e4042eba06ac820 Mon Sep 17 00:00:00 2001 From: jdalton Date: Sat, 11 Jul 2026 23:16:52 -0400 Subject: [PATCH 05/11] fix(check): deepen shallow clones before declaring pins unreachable; repin weekly-update wrapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit action-pins-are-current tests reachability with merge-base --is-ancestor, but CI's fetch-depth-25 checkout truncates history โ€” any pin older than ~25 commits reported UNREACHABLE as phantom rot (the June propagation-SHA pins aged out as main advanced). The check now deepens once (--unshallow) on a shallow repo and retests before failing. Also lands the one genuinely stale pin's --fix repin. --- .../_local-not-for-reuse-weekly-update.yml | 2 +- scripts/fleet/check/action-pins-are-current.mts | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/workflows/_local-not-for-reuse-weekly-update.yml b/.github/workflows/_local-not-for-reuse-weekly-update.yml index 010a49979..8241179b1 100644 --- a/.github/workflows/_local-not-for-reuse-weekly-update.yml +++ b/.github/workflows/_local-not-for-reuse-weekly-update.yml @@ -28,7 +28,7 @@ permissions: jobs: weekly-update: - uses: SocketDev/socket-registry/.github/workflows/weekly-update.yml@f686e4e3b7554d83a4597f1e60514ea70ce09ed8 # main (2026-07-10) + uses: SocketDev/socket-registry/.github/workflows/weekly-update.yml@121653d21199adf489fa222506e41b5aeb2b86d2 # main (2026-07-11) with: test-setup-script: '' test-script: 'pnpm test' diff --git a/scripts/fleet/check/action-pins-are-current.mts b/scripts/fleet/check/action-pins-are-current.mts index ea0c57ec4..aaa195cfc 100644 --- a/scripts/fleet/check/action-pins-are-current.mts +++ b/scripts/fleet/check/action-pins-are-current.mts @@ -513,7 +513,19 @@ const cliGit: GitRunner = { return Number.isNaN(n) ? 0 : n }, isReachable(sha, base) { - return git(['merge-base', '--is-ancestor', sha, base]).status === 0 + if (git(['merge-base', '--is-ancestor', sha, base]).status === 0) { + return true + } + // A shallow CI clone (setup-and-install checks out at fetch-depth 25) + // truncates history, so an older-but-genuine ancestor looks unreachable + // and the check reports phantom stale pins. Deepen once and retest + // before declaring the pin unreachable; after --unshallow the repo is + // no longer shallow, so later pins skip the fetch. + if (git(['rev-parse', '--is-shallow-repository']).stdout.trim() === 'true') { + git(['fetch', '--unshallow', 'origin']) + return git(['merge-base', '--is-ancestor', sha, base]).status === 0 + } + return false }, resolve(ref) { return git(['rev-parse', ref]).stdout.trim() From 607a07b370efec1873edc2465af86a4171ad5d1a Mon Sep 17 00:00:00 2001 From: jdalton Date: Sat, 11 Jul 2026 23:28:10 -0400 Subject: [PATCH 06/11] fix(types): repair sync-npm-overrides + packages util type errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pre-existing on main since the sync-overrides commit landed (Check never ran far enough to report them until the agentics and shallow-pin layers were fixed): getPackageVersionSpec called with 1 arg while its options param was required; fetchPackageManifest returns unknown, so manifest?.version failed; unused __dirname/_ecosystem bindings; and the null-proto spread idiom trips excess-property freshness checks when destructured directly โ€” split through an intermediate so the idiom keeps its Object-pollution guard and satisfies tsc. --- scripts/npm/sync-npm-overrides.mts | 4 +++- scripts/repo/util/packages.mts | 33 +++++++++++++++++------------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/scripts/npm/sync-npm-overrides.mts b/scripts/npm/sync-npm-overrides.mts index 65926eb83..3fe537d17 100644 --- a/scripts/npm/sync-npm-overrides.mts +++ b/scripts/npm/sync-npm-overrides.mts @@ -161,7 +161,9 @@ async function fetchLatestVersions( await pEach( pins, async pin => { - const manifest = await fetchPackageManifest(`${pin.upstreamName}@latest`) + const manifest = (await fetchPackageManifest( + `${pin.upstreamName}@latest`, + )) as { version?: string | undefined } | null latest.set(pin.upstreamName, manifest?.version) }, { concurrency: DEFAULT_CONCURRENCY }, diff --git a/scripts/repo/util/packages.mts b/scripts/repo/util/packages.mts index 086dec2e7..eb6218dd5 100644 --- a/scripts/repo/util/packages.mts +++ b/scripts/repo/util/packages.mts @@ -4,15 +4,16 @@ import { existsSync, readFileSync } from 'node:fs' import path from 'node:path' -import { fileURLToPath } from 'node:url' import { hasOwn } from '@socketsecurity/lib-stable/objects/predicates' import { PACKAGE_JSON, TEST_NPM_PATH } from '../../constants/paths.mts' -const __dirname = path.dirname(fileURLToPath(import.meta.url)) +type TestNpmPackageJson = { + devDependencies?: Record | undefined +} -let cachedTestNpmPackageJson +let cachedTestNpmPackageJson: TestNpmPackageJson | undefined /** * Get cached test npm package.json. @@ -32,11 +33,12 @@ export function getTestNpmPackageJson() { /** * Get the version spec for a package from test package.json. */ -export function getPackageVersionSpec(packageName, options) { - const { testNpmPackageJson = getTestNpmPackageJson() } = { - __proto__: null, - ...options, - } +export function getPackageVersionSpec( + packageName: string, + options: { testNpmPackageJson?: TestNpmPackageJson | undefined } = {}, +) { + const opts = { __proto__: null, ...options } + const { testNpmPackageJson = getTestNpmPackageJson() } = opts return testNpmPackageJson?.devDependencies?.[packageName] || undefined } @@ -46,12 +48,15 @@ export function getPackageVersionSpec(packageName, options) { * * @throws {Error} When unable to determine test status. */ -export function shouldSkipTests(packageName, options) { - const { - _ecosystem = 'npm', - testNpmPackageJson = getTestNpmPackageJson(), - testPath, - } = { __proto__: null, ...options } +export function shouldSkipTests( + packageName: string, + options: { + testNpmPackageJson?: TestNpmPackageJson | undefined + testPath: string + }, +) { + const opts = { __proto__: null, ...options } + const { testNpmPackageJson = getTestNpmPackageJson(), testPath } = opts // Check if package has test file. const testFilePath = path.join(testPath, `${packageName}.test.mts`) From f2fd7cbdcaa26bf817b060a8a7b0490e46f9f5c2 Mon Sep 17 00:00:00 2001 From: jdalton Date: Sun, 12 Jul 2026 00:15:21 -0400 Subject: [PATCH 07/11] ci: recompile gh-aw locks drifted by main's md-only edit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged origin/main (its a90a4365f edited get-green.md and weekly-update.md without running gh aw compile, so the committed .lock.yml body_hash no longer matched โ€” main's Check is red on it, and PR merge refs inherit it). Recompiled both locks with gh-aw v0.79.6 and committed the refreshed actions-lock entries. Type, lint, pin, and gh-aw drift gates all pass on the merged tree. --- .github/aw/actions-lock.json | 5 +++++ .github/workflows/get-green.lock.yml | 2 +- .github/workflows/weekly-update.lock.yml | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json index 34cc08a15..ce334f17d 100644 --- a/.github/aw/actions-lock.json +++ b/.github/aw/actions-lock.json @@ -1,5 +1,10 @@ { "entries": { + "actions/checkout@v5.0.0": { + "repo": "actions/checkout", + "version": "v5.0.0", + "sha": "08c6903cd8c0fde910a37f88322edcfb5dd907a8" + }, "actions/github-script@v9.0.0": { "repo": "actions/github-script", "version": "v9.0.0", diff --git a/.github/workflows/get-green.lock.yml b/.github/workflows/get-green.lock.yml index 3c700f342..1c6b182e2 100644 --- a/.github/workflows/get-green.lock.yml +++ b/.github/workflows/get-green.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"b4bcb078440fcfb787ce4ee8af919689d1e89a923be8e9092816cec426c5c4b5","body_hash":"7c9a005ce081585d0986b21748c2625dc5915c56e6f313e59e41541a265689e7","compiler_version":"v0.79.6","strict":true,"agent_id":"claude","agent_model":"claude-sonnet-4-5","engine_versions":{"claude":"2.1.168"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"b4bcb078440fcfb787ce4ee8af919689d1e89a923be8e9092816cec426c5c4b5","body_hash":"0e28f931b93337e00798b4f054a2f7add9470f4c667610dcd4563a0e166ea49f","compiler_version":"v0.79.6","strict":true,"agent_id":"claude","agent_model":"claude-sonnet-4-5","engine_versions":{"claude":"2.1.168"}} # gh-aw-manifest: {"version":1,"secrets":["ANTHROPIC_API_KEY","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"08c6903cd8c0fde910a37f88322edcfb5dd907a8","version":"v5.0.0"},{"repo":"actions/checkout","sha":"df4cb1c069e1874edd31b4311f1884172cec0e10","version":"v6.0.3"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"5c2fe865bb4dc46e1450f6ee0d0541d759aea73a","version":"v0.79.6"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2","digest":"sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2","digest":"sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2@sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2","digest":"sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2@sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.25","digest":"sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.25@sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa"},{"image":"ghcr.io/github/github-mcp-server:v1.1.2","digest":"sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c","pinned_image":"ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c"}]} # ___ _ _ # / _ \ | | (_) diff --git a/.github/workflows/weekly-update.lock.yml b/.github/workflows/weekly-update.lock.yml index ec2903c1f..bddbb0176 100644 --- a/.github/workflows/weekly-update.lock.yml +++ b/.github/workflows/weekly-update.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"72dcf20b08668f0690e5664a0cbf65ce530998ff8dbd30877645ebe2c9ac4b1d","body_hash":"d2e18d98a0c71e73c2465a483cb711fc90523762006c890be6028ce74958a9ce","compiler_version":"v0.79.6","strict":true,"agent_id":"claude","agent_model":"claude-haiku-4-5","engine_versions":{"claude":"2.1.168"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"72dcf20b08668f0690e5664a0cbf65ce530998ff8dbd30877645ebe2c9ac4b1d","body_hash":"e1f4db2a5f313c5a2a5c72aa99c3f3907d9554b136dc39fc56ef06055da49b91","compiler_version":"v0.79.6","strict":true,"agent_id":"claude","agent_model":"claude-haiku-4-5","engine_versions":{"claude":"2.1.168"}} # gh-aw-manifest: {"version":1,"secrets":["ANTHROPIC_API_KEY","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"08c6903cd8c0fde910a37f88322edcfb5dd907a8","version":"v5.0.0"},{"repo":"actions/checkout","sha":"df4cb1c069e1874edd31b4311f1884172cec0e10","version":"v6.0.3"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"5c2fe865bb4dc46e1450f6ee0d0541d759aea73a","version":"v0.79.6"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2","digest":"sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2","digest":"sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2@sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2","digest":"sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2@sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.25","digest":"sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.25@sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa"},{"image":"ghcr.io/github/github-mcp-server:v1.1.2","digest":"sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c","pinned_image":"ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c"}]} # ___ _ _ # / _ \ | | (_) From 1a8f0a6fb4e3b5c8ce7cc1ef339ffacdec18fce7 Mon Sep 17 00:00:00 2001 From: jdalton Date: Sun, 12 Jul 2026 01:11:40 -0400 Subject: [PATCH 08/11] feat(npm): sync es-iterator-helpers override to upstream 1.3.3 --- .../Iterator.prototype.includes/auto.d.ts | 1 + .../Iterator.prototype.includes/auto.js | 3 + .../implementation.d.ts | 3 + .../implementation.js | 84 +++++++++++++ .../Iterator.prototype.includes/index.d.ts | 10 ++ .../Iterator.prototype.includes/index.js | 23 ++++ .../Iterator.prototype.includes/polyfill.d.ts | 3 + .../Iterator.prototype.includes/polyfill.js | 7 ++ .../Iterator.prototype.includes/shim.d.ts | 3 + .../Iterator.prototype.includes/shim.js | 18 +++ .../Iterator.prototype/implementation.d.ts | 1 + packages/npm/es-iterator-helpers/index.json | 1 + packages/npm/es-iterator-helpers/package.json | 20 +++ packages/npm/es-iterator-helpers/shim.js | 2 + pnpm-lock.yaml | 12 +- test/npm/es-iterator-helpers.test.mts | 114 +++++++++++++++++- test/npm/package.json | 2 +- 17 files changed, 298 insertions(+), 9 deletions(-) create mode 100644 packages/npm/es-iterator-helpers/Iterator.prototype.includes/auto.d.ts create mode 100644 packages/npm/es-iterator-helpers/Iterator.prototype.includes/auto.js create mode 100644 packages/npm/es-iterator-helpers/Iterator.prototype.includes/implementation.d.ts create mode 100644 packages/npm/es-iterator-helpers/Iterator.prototype.includes/implementation.js create mode 100644 packages/npm/es-iterator-helpers/Iterator.prototype.includes/index.d.ts create mode 100644 packages/npm/es-iterator-helpers/Iterator.prototype.includes/index.js create mode 100644 packages/npm/es-iterator-helpers/Iterator.prototype.includes/polyfill.d.ts create mode 100644 packages/npm/es-iterator-helpers/Iterator.prototype.includes/polyfill.js create mode 100644 packages/npm/es-iterator-helpers/Iterator.prototype.includes/shim.d.ts create mode 100644 packages/npm/es-iterator-helpers/Iterator.prototype.includes/shim.js diff --git a/packages/npm/es-iterator-helpers/Iterator.prototype.includes/auto.d.ts b/packages/npm/es-iterator-helpers/Iterator.prototype.includes/auto.d.ts new file mode 100644 index 000000000..40a8c178f --- /dev/null +++ b/packages/npm/es-iterator-helpers/Iterator.prototype.includes/auto.d.ts @@ -0,0 +1 @@ +/* empty */ diff --git a/packages/npm/es-iterator-helpers/Iterator.prototype.includes/auto.js b/packages/npm/es-iterator-helpers/Iterator.prototype.includes/auto.js new file mode 100644 index 000000000..67d3d96da --- /dev/null +++ b/packages/npm/es-iterator-helpers/Iterator.prototype.includes/auto.js @@ -0,0 +1,3 @@ +'use strict' + +require('./shim')() diff --git a/packages/npm/es-iterator-helpers/Iterator.prototype.includes/implementation.d.ts b/packages/npm/es-iterator-helpers/Iterator.prototype.includes/implementation.d.ts new file mode 100644 index 000000000..4780d824c --- /dev/null +++ b/packages/npm/es-iterator-helpers/Iterator.prototype.includes/implementation.d.ts @@ -0,0 +1,3 @@ +import EsIterator from '../Iterator.prototype/implementation' +declare const Impl: EsIterator['includes'] +export = Impl diff --git a/packages/npm/es-iterator-helpers/Iterator.prototype.includes/implementation.js b/packages/npm/es-iterator-helpers/Iterator.prototype.includes/implementation.js new file mode 100644 index 000000000..56574e440 --- /dev/null +++ b/packages/npm/es-iterator-helpers/Iterator.prototype.includes/implementation.js @@ -0,0 +1,84 @@ +'use strict' + +const { + IteratorPrototype, + NumberIsNaN, + RangeErrorCtor, + ReflectApply, + TypeErrorCtor, + ensureObject, + getIteratorDirect, + ifAbruptCloseIterator, + iteratorClose, +} = require('../shared') + +const { isFinite: NumberIsFinite, isInteger: NumberIsInteger } = Number +const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER + +const { includes: IteratorProtoIncludes } = IteratorPrototype + +// Based on https://tc39.es/ecma262/#sec-iterator.prototype.includes. +module.exports = + typeof IteratorProtoIncludes === 'function' + ? IteratorProtoIncludes + : function includes(searchElement) { + // Built-in function objects that are not identified as constructors do + // not implement the [[Construct]] internal method unless otherwise + // specified in the description of a particular function. + // https://tc39.es/ecma262/#sec-ecmascript-standard-built-in-objects + if (new.target) { + throw new TypeErrorCtor('`includes` is not a constructor') + } + // If O is not an Object, throw a TypeError exception. + ensureObject(this) + // Let toSkip be 0; if a second argument was passed, validate and use + // it as the number of elements to skip. + const hasSkippedElements = arguments.length > 1 + const skippedElements = hasSkippedElements ? arguments[1] : undefined + let toSkip = 0 + if (hasSkippedElements) { + if ( + typeof skippedElements !== 'number' || + NumberIsNaN(skippedElements) || + (NumberIsFinite(skippedElements) && + !NumberIsInteger(skippedElements)) + ) { + ifAbruptCloseIterator( + this, + new TypeErrorCtor( + '`skippedElements` must be an integral Number, +Infinity, or -Infinity', + ), + ) + } + toSkip = skippedElements + } + if (toSkip < 0) { + ifAbruptCloseIterator( + this, + new RangeErrorCtor('`skippedElements` must be >= 0'), + ) + } + if (NumberIsFinite(toSkip) && toSkip > MAX_SAFE_INTEGER) { + ifAbruptCloseIterator( + this, + new RangeErrorCtor('`skippedElements` must be <= 2 ** 53 - 1'), + ) + } + const { iterator, next: nextMethod } = getIteratorDirect(this) + let skipped = 0 + while (true) { + const result = ReflectApply(nextMethod, iterator, []) + if (result.done) { + return false + } + const { value } = result + if (skipped < toSkip) { + skipped += 1 + } else if ( + value === searchElement || + (NumberIsNaN(value) && NumberIsNaN(searchElement)) + ) { + return iteratorClose(iterator, true) + } + } + } diff --git a/packages/npm/es-iterator-helpers/Iterator.prototype.includes/index.d.ts b/packages/npm/es-iterator-helpers/Iterator.prototype.includes/index.d.ts new file mode 100644 index 000000000..67fd5666c --- /dev/null +++ b/packages/npm/es-iterator-helpers/Iterator.prototype.includes/index.d.ts @@ -0,0 +1,10 @@ +import Impl from './implementation' +declare const { + x: EsIteratorProtoIncludes, +}: { + x: typeof Impl & { + getPolyfill(): typeof Impl + shim(): typeof Impl + } +} +export = EsIteratorProtoIncludes diff --git a/packages/npm/es-iterator-helpers/Iterator.prototype.includes/index.js b/packages/npm/es-iterator-helpers/Iterator.prototype.includes/index.js new file mode 100644 index 000000000..9a71fbab6 --- /dev/null +++ b/packages/npm/es-iterator-helpers/Iterator.prototype.includes/index.js @@ -0,0 +1,23 @@ +'use strict' + +const impl = require('./implementation') + +function desc(value) { + return { + __proto__: null, + configurable: true, + value, + writable: true, + } +} + +module.exports = Object.defineProperties( + function includes(thisArg, ...args) { + return new.target ? new impl() : Reflect.apply(impl, thisArg, args) + }, + { + getPolyfill: desc(require('./polyfill')), + implementation: desc(impl), + shim: desc(require('./shim')), + }, +) diff --git a/packages/npm/es-iterator-helpers/Iterator.prototype.includes/polyfill.d.ts b/packages/npm/es-iterator-helpers/Iterator.prototype.includes/polyfill.d.ts new file mode 100644 index 000000000..284d093fa --- /dev/null +++ b/packages/npm/es-iterator-helpers/Iterator.prototype.includes/polyfill.d.ts @@ -0,0 +1,3 @@ +import Impl from './implementation' +declare function getPolyfill(): () => typeof Impl +export = getPolyfill diff --git a/packages/npm/es-iterator-helpers/Iterator.prototype.includes/polyfill.js b/packages/npm/es-iterator-helpers/Iterator.prototype.includes/polyfill.js new file mode 100644 index 000000000..ff68a60c8 --- /dev/null +++ b/packages/npm/es-iterator-helpers/Iterator.prototype.includes/polyfill.js @@ -0,0 +1,7 @@ +'use strict' + +const impl = require('./implementation') + +module.exports = function getPolyfill() { + return impl +} diff --git a/packages/npm/es-iterator-helpers/Iterator.prototype.includes/shim.d.ts b/packages/npm/es-iterator-helpers/Iterator.prototype.includes/shim.d.ts new file mode 100644 index 000000000..b2a727d37 --- /dev/null +++ b/packages/npm/es-iterator-helpers/Iterator.prototype.includes/shim.d.ts @@ -0,0 +1,3 @@ +import Impl from './implementation' +declare function shim(): () => typeof Impl +export = shim diff --git a/packages/npm/es-iterator-helpers/Iterator.prototype.includes/shim.js b/packages/npm/es-iterator-helpers/Iterator.prototype.includes/shim.js new file mode 100644 index 000000000..3766bac5c --- /dev/null +++ b/packages/npm/es-iterator-helpers/Iterator.prototype.includes/shim.js @@ -0,0 +1,18 @@ +'use strict' + +const getPolyfill = require('./polyfill') +const { IteratorPrototype, ObjectDefineProperty } = require('../shared') + +module.exports = function shimIteratorProtoIncludes() { + const polyfill = getPolyfill() + if (IteratorPrototype.includes !== polyfill) { + ObjectDefineProperty(IteratorPrototype, 'includes', { + __proto__: null, + configurable: true, + enumerable: false, + value: polyfill, + writable: true, + }) + } + return polyfill +} diff --git a/packages/npm/es-iterator-helpers/Iterator.prototype/implementation.d.ts b/packages/npm/es-iterator-helpers/Iterator.prototype/implementation.d.ts index 9daf31a4e..163b3bd4e 100644 --- a/packages/npm/es-iterator-helpers/Iterator.prototype/implementation.d.ts +++ b/packages/npm/es-iterator-helpers/Iterator.prototype/implementation.d.ts @@ -16,6 +16,7 @@ interface EsIterator { ): EsIterator | undefined flatMap(callbackFn: (value: T, index: number) => unknown): U forEach(callbackFn: (value: T, index: number) => void): void + includes(searchElement: T, fromIndex?: number): boolean map(callbackFn: (value: T, index: number) => U): EsIterator reduce( callbackfn: (previousValue: T, currentValue: T, currentIndex: number) => T, diff --git a/packages/npm/es-iterator-helpers/index.json b/packages/npm/es-iterator-helpers/index.json index 0c30f56cf..4244fa258 100644 --- a/packages/npm/es-iterator-helpers/index.json +++ b/packages/npm/es-iterator-helpers/index.json @@ -12,6 +12,7 @@ "Iterator.prototype.find", "Iterator.prototype.flatMap", "Iterator.prototype.forEach", + "Iterator.prototype.includes", "Iterator.prototype.map", "Iterator.prototype.reduce", "Iterator.prototype.some", diff --git a/packages/npm/es-iterator-helpers/package.json b/packages/npm/es-iterator-helpers/package.json index 3690a9607..5aab43963 100644 --- a/packages/npm/es-iterator-helpers/package.json +++ b/packages/npm/es-iterator-helpers/package.json @@ -289,6 +289,26 @@ "types": "./Iterator.prototype.forEach/shim.d.ts", "default": "./Iterator.prototype.forEach/shim.js" }, + "./Iterator.prototype.includes": { + "types": "./Iterator.prototype.includes/index.d.ts", + "default": "./Iterator.prototype.includes/index.js" + }, + "./Iterator.prototype.includes/auto": { + "types": "./Iterator.prototype.includes/auto.d.ts", + "default": "./Iterator.prototype.includes/auto.js" + }, + "./Iterator.prototype.includes/polyfill": { + "types": "./Iterator.prototype.includes/polyfill.d.ts", + "default": "./Iterator.prototype.includes/polyfill.js" + }, + "./Iterator.prototype.includes/implementation": { + "types": "./Iterator.prototype.includes/implementation.d.ts", + "default": "./Iterator.prototype.includes/implementation.js" + }, + "./Iterator.prototype.includes/shim": { + "types": "./Iterator.prototype.includes/shim.d.ts", + "default": "./Iterator.prototype.includes/shim.js" + }, "./Iterator.prototype.map": { "types": "./Iterator.prototype.map/index.d.ts", "default": "./Iterator.prototype.map/index.js" diff --git a/packages/npm/es-iterator-helpers/shim.js b/packages/npm/es-iterator-helpers/shim.js index 36db9c76e..cb737494c 100644 --- a/packages/npm/es-iterator-helpers/shim.js +++ b/packages/npm/es-iterator-helpers/shim.js @@ -11,6 +11,7 @@ const shimIteratorProtoFilter = require('./Iterator.prototype.filter/shim') const shimIteratorProtoFind = require('./Iterator.prototype.find/shim') const shimIteratorProtoFlatMap = require('./Iterator.prototype.flatMap/shim') const shimIteratorProtoForEach = require('./Iterator.prototype.forEach/shim') +const shimIteratorProtoIncludes = require('./Iterator.prototype.includes/shim') const shimIteratorProtoMap = require('./Iterator.prototype.map/shim') const shimIteratorProtoReduce = require('./Iterator.prototype.reduce/shim') const shimIteratorProtoSome = require('./Iterator.prototype.some/shim') @@ -33,6 +34,7 @@ module.exports = function shimIteratorHelpers() { shimIteratorProtoFind() shimIteratorProtoFlatMap() shimIteratorProtoForEach() + shimIteratorProtoIncludes() shimIteratorProtoMap() shimIteratorProtoReduce() shimIteratorProtoSome() diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 286565309..ef8e040c6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1376,8 +1376,8 @@ importers: specifier: 1.1.3 version: 1.1.3 es-iterator-helpers: - specifier: 1.2.1 - version: 1.2.1 + specifier: 1.3.3 + version: 1.3.3 es-set-tostringtag: specifier: npm:@socketregistry/es-set-tostringtag@1.0.10 version: '@socketregistry/es-set-tostringtag@1.0.10' @@ -3498,8 +3498,8 @@ packages: es-get-iterator@1.1.3: resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} - es-iterator-helpers@1.2.1: - resolution: {integrity: sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==} + es-iterator-helpers@1.3.3: + resolution: {integrity: sha512-0PuBxFi+4uPanB97iDxCLWuHeYud2FALrw5HFZGtAF38UpJDbDC8frwp2cnDyae692CQ0dou60UwWfhgsa4U/g==} engines: {node: '>= 0.4'} es-module-lexer@2.3.0: @@ -7568,7 +7568,7 @@ snapshots: isarray: 2.0.5 stop-iteration-iterator: 1.1.0 - es-iterator-helpers@1.2.1: + es-iterator-helpers@1.3.3: dependencies: call-bind: 1.0.9 call-bound: 1.0.4 @@ -7585,7 +7585,7 @@ snapshots: has-symbols: '@socketregistry/has-symbols@1.0.7' internal-slot: 1.1.0 iterator.prototype: 1.1.5 - safe-array-concat: 1.1.4 + math-intrinsics: 1.1.0 es-module-lexer@2.3.0: {} diff --git a/test/npm/es-iterator-helpers.test.mts b/test/npm/es-iterator-helpers.test.mts index e1608d810..60590c8dc 100644 --- a/test/npm/es-iterator-helpers.test.mts +++ b/test/npm/es-iterator-helpers.test.mts @@ -1,7 +1,7 @@ /** * @file Tests for es-iterator-helpers NPM package override. Ported 1:1 from - * upstream v1.3.2 (1a9241c3): - * https://github.com/es-shims/iterator-helpers/blob/1a9241c33779dce25110474feedb2c4a1b15c7ff/test/Iterator.js. + * upstream v1.3.3: + * https://github.com/es-shims/iterator-helpers/blob/v1.3.3/test/Iterator.js. */ import path from 'node:path' @@ -41,6 +41,7 @@ const iteratorEvery = loadSub('Iterator.prototype.every') const iteratorFind = loadSub('Iterator.prototype.find') const iteratorReduce = loadSub('Iterator.prototype.reduce') const iteratorFlatMap = loadSub('Iterator.prototype.flatMap') +const iteratorIncludes = loadSub('Iterator.prototype.includes') describe(`${eco} > ${sockRegPkgName}`, { skip }, () => { it('exports expected methods', () => { @@ -214,4 +215,113 @@ describe(`${eco} > ${sockRegPkgName}`, { skip }, () => { expect(result).toEqual([1, 10, 2, 20, 3, 30]) }) }) + + describe('Iterator.prototype.includes', () => { + it( + 'returns true when the value is present', + { skip: !iteratorIncludes }, + () => { + const arr = [1, 2, 3] + const iter = arr[Symbol.iterator]() + expect(iteratorIncludes(iter, 2)).toBe(true) + }, + ) + + it( + 'returns false when the value is absent', + { skip: !iteratorIncludes }, + () => { + const arr = [1, 2, 3] + const iter = arr[Symbol.iterator]() + expect(iteratorIncludes(iter, 4)).toBe(false) + }, + ) + + it( + 'treats NaN as equal to NaN (SameValueZero)', + { skip: !iteratorIncludes }, + () => { + const arr = [1, Number.NaN, 3] + const iter = arr[Symbol.iterator]() + expect(iteratorIncludes(iter, Number.NaN)).toBe(true) + }, + ) + + it( + 'treats +0 and -0 as equal (SameValueZero)', + { skip: !iteratorIncludes }, + () => { + const arr = [-0] + const iter = arr[Symbol.iterator]() + expect(iteratorIncludes(iter, 0)).toBe(true) + }, + ) + + it( + 'skips the given number of elements before searching', + { skip: !iteratorIncludes }, + () => { + const arr = [1, 2, 3] + const iter = arr[Symbol.iterator]() + expect(iteratorIncludes(iter, 1, 1)).toBe(false) + }, + ) + + it( + 'returns false for an empty iterator', + { skip: !iteratorIncludes }, + () => { + const iter = [][Symbol.iterator]() + expect(iteratorIncludes(iter, 1)).toBe(false) + }, + ) + + it( + 'throws a TypeError for a non-integral skip count', + { skip: !iteratorIncludes }, + () => { + const arr = [1, 2, 3] + const iter = arr[Symbol.iterator]() + expect(() => iteratorIncludes(iter, 1, 1.5)).toThrow(TypeError) + }, + ) + + it( + 'throws a TypeError for a non-numeric skip count', + { skip: !iteratorIncludes }, + () => { + const arr = [1, 2, 3] + const iter = arr[Symbol.iterator]() + expect(() => iteratorIncludes(iter, 1, 'nope')).toThrow(TypeError) + }, + ) + + it( + 'throws a RangeError for a negative skip count', + { skip: !iteratorIncludes }, + () => { + const arr = [1, 2, 3] + const iter = arr[Symbol.iterator]() + expect(() => iteratorIncludes(iter, 1, -1)).toThrow(RangeError) + }, + ) + + it( + 'allows +Infinity as a skip count, exhausting the iterator', + { skip: !iteratorIncludes }, + () => { + const arr = [1, 2, 3] + const iter = arr[Symbol.iterator]() + expect(iteratorIncludes(iter, 1, Number.POSITIVE_INFINITY)).toBe(false) + }, + ) + + it( + 'throws a TypeError when called on a non-object', + { skip: !iteratorIncludes }, + () => { + expect(() => iteratorIncludes(null, 1)).toThrow(TypeError) + }, + ) + }) }) diff --git a/test/npm/package.json b/test/npm/package.json index b8fff4d60..d5d487164 100644 --- a/test/npm/package.json +++ b/test/npm/package.json @@ -36,7 +36,7 @@ "es-aggregate-error": "1.0.14", "es-define-property": "1.0.1", "es-get-iterator": "1.1.3", - "es-iterator-helpers": "1.2.1", + "es-iterator-helpers": "1.3.3", "es-set-tostringtag": "2.1.0", "es6-object-assign": "https://github.com/rubennorte/es6-object-assign/archive/77eb94482832b642aefb1b875f2921a13f6003c6.tar.gz", "es6-symbol": "https://github.com/medikoo/es6-symbol/archive/c57f8d88070cec913ff4e3e0ed5192b2330373b2.tar.gz", From 3d0729db8d682d3bb4382c9842320b4e937c499a Mon Sep 17 00:00:00 2001 From: jdalton Date: Sun, 12 Jul 2026 01:11:50 -0400 Subject: [PATCH 09/11] fix(check): judge override currency against newest soak-cleared upstream --- scripts/npm/sync-npm-overrides.mts | 163 +++++++++++++++--- .../repo/check/npm-overrides-are-current.mts | 23 ++- test/scripts/npm/sync-npm-overrides.test.mts | 99 +++++++++++ .../check/npm-overrides-are-current.test.mts | 18 +- 4 files changed, 270 insertions(+), 33 deletions(-) diff --git a/scripts/npm/sync-npm-overrides.mts b/scripts/npm/sync-npm-overrides.mts index e40cfc579..ee72fd145 100644 --- a/scripts/npm/sync-npm-overrides.mts +++ b/scripts/npm/sync-npm-overrides.mts @@ -1,8 +1,11 @@ #!/usr/bin/env node /** * @file Keep packages/npm/* overrides in lock-step with their upstreams. - * Detects overridden upstream packages that have published a newer version - * than the pin in test/npm/package.json and, with --apply, drives an AI + * Detects overridden upstream packages whose newest SOAK-CLEARED release + * (pnpm-workspace.yaml minimumReleaseAge โ€” a `latest` still inside the + * window is never demanded, so the currency law can't contradict the + * install policy) is newer than the pin in test/npm/package.json and, with + * --apply, drives an AI * agent to update the override implementation and its unit tests TOGETHER, * per docs/agents.md/fleet/test-layout.md (tests cascade in lock-step with * the code they cover) and docs/agents.md/fleet/code-is-law.md (tests are @@ -34,13 +37,14 @@ import { AI_PROFILE } from '@socketsecurity/lib-stable/ai/profiles' import { spawnAiAgent } from '@socketsecurity/lib-stable/ai/spawn' import { parseArgs } from '@socketsecurity/lib-stable/argv/parse' import { getDefaultLogger } from '@socketsecurity/lib-stable/logger/default' -import { fetchPackageManifest } from '@socketsecurity/lib-stable/packages/manifest' +import { fetchPackagePackument } from '@socketsecurity/lib-stable/packages/manifest' import { resolveOriginalPackageName } from '@socketsecurity/lib-stable/packages/normalize' import { pEach } from '@socketsecurity/lib-stable/promises/iterate' import { DEFAULT_CONCURRENCY } from '../constants/core.mts' import { ROOT_PATH, TEST_NPM_PATH } from '../constants/paths.mts' import { getNpmPackageNames } from '../constants/testing.mts' +import { isSoakExcluded, readSoakRules } from '../fleet/soak-rules.mts' import { runCommandQuietStrict } from '../fleet/util/run-command.mts' import { getPackageVersionSpec } from '../repo/util/packages.mts' @@ -62,6 +66,17 @@ export interface OverridePin { } export interface OverrideDrift extends OverridePin { + /** + * The newest upstream version the install policy actually allows: the + * highest release at or below the `latest` dist-tag that has soaked past + * `minimumReleaseAge` (or is bypassed by a `minimumReleaseAgeExclude` + * entry). This is what `stale` is judged against and what --apply pins. + */ + targetVersion: string | undefined + /** + * The raw `latest` dist-tag; differs from `targetVersion` only while the + * newest release is still inside the soak window. + */ latestVersion: string | undefined status: OverrideStatus } @@ -74,13 +89,15 @@ export function isExactSemver(spec: unknown): spec is string { } /** - * Classify one override's pin against the upstream's latest published + * Classify one override's pin against the upstream's newest soak-cleared * version. Pure: fetching happens in the caller so this stays unit-testable - * offline (lock-step tests never touch the network). + * offline (lock-step tests never touch the network). `latestVersion` + * defaults to the target so pre-soak callers/tests stay valid. */ export function classifyOverride( pin: OverridePin, - latestVersion: string | undefined, + targetVersion: string | undefined, + latestVersion: string | undefined = targetVersion, ): OverrideDrift { let status: OverrideStatus if (pin.pinnedSpec === undefined) { @@ -89,14 +106,78 @@ export function classifyOverride( // GitHub tarball URLs and ranges have no single upstream version to // track; surfaced for a human, never auto-synced. status = 'unpinnable-spec' - } else if (!isExactSemver(latestVersion)) { + } else if (!isExactSemver(targetVersion)) { status = 'unresolved' - } else if (semver.gte(pin.pinnedSpec, latestVersion)) { + } else if (semver.gte(pin.pinnedSpec, targetVersion)) { status = 'current' } else { status = 'stale' } - return { ...pin, latestVersion, status } + return { ...pin, targetVersion, latestVersion, status } +} + +export interface SoakTargetOptions { + exclude: readonly string[] + nowMs: number + soakMinutes: number + upstreamName: string +} + +/** + * The newest upstream version an install is actually allowed to use: the + * highest release at or below the `latest` dist-tag that has either soaked + * past `minimumReleaseAge` or is bypassed by a `minimumReleaseAgeExclude` + * entry. Targeting raw `latest` would deadlock the currency law against the + * soak policy โ€” the check demanding a version `pnpm install` refuses for up + * to the whole soak window. Prereleases below `latest` never win (an rc of + * an unsoaked stable is not a sync target). Falls back to `latest` when the + * packument carries no usable `time` data or nothing has cleared yet, so + * degraded metadata behaves like the pre-soak-aware check instead of hiding + * drift. + */ +export function newestSoakClearedVersion( + latestVersion: string | undefined, + times: Record | undefined, + options: SoakTargetOptions, +): string | undefined { + const { exclude, nowMs, soakMinutes, upstreamName } = { + __proto__: null, + ...options, + } as SoakTargetOptions + if (!isExactSemver(latestVersion)) { + return latestVersion + } + if ( + soakMinutes <= 0 || + !times || + isSoakExcluded(upstreamName, latestVersion, exclude) + ) { + return latestVersion + } + const cutoffMs = nowMs - soakMinutes * 60_000 + let best: string | undefined + const entries = Object.entries(times) + for (let i = 0, { length } = entries; i < length; i += 1) { + const { 0: version, 1: published } = entries[i]! + // `time` also carries non-version keys (`created`, `modified`). + if (!isExactSemver(version) || semver.gt(version, latestVersion)) { + continue + } + if (semver.prerelease(version) !== null && version !== latestVersion) { + continue + } + const publishedMs = Date.parse(published) + const cleared = + (Number.isFinite(publishedMs) && publishedMs <= cutoffMs) || + isSoakExcluded(upstreamName, version, exclude) + if (!cleared) { + continue + } + if (best === undefined || semver.gt(version, best)) { + best = version + } + } + return best ?? latestVersion } /** @@ -154,29 +235,63 @@ async function collectOverridePins( }) } -async function fetchLatestVersions( +interface UpstreamVersions { + latestVersion: string | undefined + targetVersion: string | undefined +} + +async function fetchUpstreamVersions( pins: OverridePin[], -): Promise> { - const latest = new Map() +): Promise> { + // The fixture installs in the root workspace, so the root workspace file's + // soak policy is the one `pnpm install` will actually enforce. + const rules = readSoakRules(path.join(ROOT_PATH, 'pnpm-workspace.yaml')) + const nowMs = Date.now() + const versions = new Map() await pEach( pins, async pin => { - const manifest = (await fetchPackageManifest( - `${pin.upstreamName}@latest`, - )) as { version?: string | undefined } | null - latest.set(pin.upstreamName, manifest?.version) + // fullMetadata: abbreviated packuments omit the `time` map the soak + // cutoff is computed from. + const packument = (await fetchPackagePackument(pin.upstreamName, { + fullMetadata: true, + })) as { + 'dist-tags'?: Record | undefined + time?: Record | undefined + } | null + const latestVersion = packument?.['dist-tags']?.['latest'] + versions.set(pin.upstreamName, { + latestVersion, + targetVersion: newestSoakClearedVersion( + latestVersion, + packument?.time, + { + exclude: rules.exclude, + nowMs, + soakMinutes: rules.minutes, + upstreamName: pin.upstreamName, + }, + ), + }) }, { concurrency: DEFAULT_CONCURRENCY }, ) - return latest + return versions } export async function collectOverrideDrift( packageFilter: string[] = [], ): Promise { const pins = await collectOverridePins(packageFilter) - const latest = await fetchLatestVersions(pins) - return pins.map(pin => classifyOverride(pin, latest.get(pin.upstreamName))) + const upstream = await fetchUpstreamVersions(pins) + return pins.map(pin => { + const versions = upstream.get(pin.upstreamName) + return classifyOverride( + pin, + versions?.targetVersion, + versions?.latestVersion, + ) + }) } async function writeUpstreamPin( @@ -212,7 +327,7 @@ async function applyOne( ): Promise<'synced' | 'soak-blocked' | 'failed'> { const { effort, model } = { __proto__: null, ...options } as typeof options const fromVersion = drift.pinnedSpec as string - const toVersion = drift.latestVersion as string + const toVersion = drift.targetVersion as string logger.log( `${drift.socketPkgName}: syncing ${drift.upstreamName} ` + @@ -295,9 +410,13 @@ async function main(): Promise { if (!quiet) { for (let i = 0, { length } = stale; i < length; i += 1) { const d = stale[i]! + const soaking = + d.latestVersion !== d.targetVersion + ? `; latest ${d.latestVersion} still soaking` + : '' logger.log( `stale: ${d.socketPkgName} (${d.upstreamName} ` + - `${d.pinnedSpec} -> ${d.latestVersion})`, + `${d.pinnedSpec} -> ${d.targetVersion}${soaking})`, ) } for (let i = 0, { length } = unpinnable; i < length; i += 1) { @@ -316,7 +435,7 @@ async function main(): Promise { if (!stale.length) { if (!quiet) { logger.success( - `All ${drift.length} overrides track their upstream latest.`, + `All ${drift.length} overrides track their newest soak-cleared upstream.`, ) } return diff --git a/scripts/repo/check/npm-overrides-are-current.mts b/scripts/repo/check/npm-overrides-are-current.mts index 12e4f9884..c458847b8 100644 --- a/scripts/repo/check/npm-overrides-are-current.mts +++ b/scripts/repo/check/npm-overrides-are-current.mts @@ -1,11 +1,15 @@ #!/usr/bin/env node /** - * @file Every packages/npm/* override tracks its upstream's latest release. - * An override that lags upstream ships stale behavior under the upstream's - * name โ€” the override's implementation and unit tests must move in - * lock-step with upstream releases (docs/agents.md/fleet/test-layout.md, + * @file Every packages/npm/* override tracks its upstream's newest + * soak-cleared release. An override that lags upstream ships stale + * behavior under the upstream's name โ€” the override's implementation and + * unit tests must move in lock-step with upstream releases + * (docs/agents.md/fleet/test-layout.md, * docs/agents.md/fleet/code-is-law.md). Fails when a pinned upstream in - * test/npm/package.json is behind that upstream's npm latest; the remedy + * test/npm/package.json is behind the newest upstream release that has + * cleared the pnpm-workspace.yaml minimumReleaseAge soak โ€” a `latest` + * still inside the window is never demanded, so this law can't contradict + * the install policy it would need to violate to go green. The remedy * is the sync driver, which updates implementation + tests together: * node scripts/npm/sync-npm-overrides.mts --apply [--package ] * Network-dependent by nature; an unreachable registry degrades to a @@ -34,10 +38,13 @@ export function formatStaleReport(drift: OverrideDrift[]): string | undefined { const lines = stale.map( d => ` ${d.socketPkgName}: ${d.upstreamName} ${d.pinnedSpec} -> ` + - `${d.latestVersion}`, + `${d.targetVersion}` + + (d.latestVersion !== d.targetVersion + ? ` (latest ${d.latestVersion} still soaking)` + : ''), ) return [ - `${stale.length} npm override(s) behind their upstream latest:`, + `${stale.length} npm override(s) behind their newest soak-cleared upstream:`, ...lines, 'Sync implementation + tests in lock-step:', ' node scripts/npm/sync-npm-overrides.mts --apply [--package ]', @@ -59,7 +66,7 @@ async function main(): Promise { const report = formatStaleReport(drift) if (report === undefined) { if (!quiet) { - logger.success('npm overrides track their upstream latest') + logger.success('npm overrides track their newest soak-cleared upstream') } return 0 } diff --git a/test/scripts/npm/sync-npm-overrides.test.mts b/test/scripts/npm/sync-npm-overrides.test.mts index 4fda7eb9a..5eb7ea9fa 100644 --- a/test/scripts/npm/sync-npm-overrides.test.mts +++ b/test/scripts/npm/sync-npm-overrides.test.mts @@ -11,6 +11,7 @@ import { buildSyncPrompt, classifyOverride, isExactSemver, + newestSoakClearedVersion, } from '../../../scripts/npm/sync-npm-overrides.mts' function pin(pinnedSpec: string | undefined) { @@ -75,6 +76,104 @@ describe('scripts/npm/sync-npm-overrides', () => { 'unresolved', ) }) + + it('judges staleness against the soak-cleared target, not raw latest', () => { + const drift = classifyOverride(pin('7.0.1'), '7.0.1', '7.1.0') + expect(drift.status).toBe('current') + expect(drift.targetVersion).toBe('7.0.1') + expect(drift.latestVersion).toBe('7.1.0') + }) + + it('defaults latestVersion to the target when not supplied', () => { + const drift = classifyOverride(pin('7.0.0'), '7.0.1') + expect(drift.latestVersion).toBe('7.0.1') + }) + }) + + describe('newestSoakClearedVersion', () => { + const DAY_MS = 24 * 60 * 60 * 1000 + const nowMs = Date.parse('2026-07-12T00:00:00.000Z') + const soak = { + exclude: [] as readonly string[], + nowMs, + soakMinutes: 7 * 24 * 60, + upstreamName: 'is-number', + } + const at = (daysAgo: number) => + new Date(nowMs - daysAgo * DAY_MS).toISOString() + + it('returns latest when it has cleared the soak window', () => { + expect( + newestSoakClearedVersion( + '7.1.0', + { '7.0.0': at(400), '7.1.0': at(30) }, + soak, + ), + ).toBe('7.1.0') + }) + + it('falls back to the newest cleared version while latest soaks', () => { + expect( + newestSoakClearedVersion( + '7.2.0', + { '7.0.0': at(400), '7.1.0': at(30), '7.2.0': at(2) }, + soak, + ), + ).toBe('7.1.0') + }) + + it('never targets a prerelease below latest', () => { + expect( + newestSoakClearedVersion( + '7.1.0', + { '7.0.0': at(400), '7.1.0-rc.1': at(30), '7.1.0': at(2) }, + soak, + ), + ).toBe('7.0.0') + }) + + it('ignores versions above latest and non-version time keys', () => { + expect( + newestSoakClearedVersion( + '7.1.0', + { + created: at(500), + modified: at(1), + '7.1.0': at(30), + '8.0.0-next.0': at(20), + }, + soak, + ), + ).toBe('7.1.0') + }) + + it('lets a minimumReleaseAgeExclude entry bypass the soak', () => { + const times = { '7.0.0': at(400), '7.1.0': at(1) } + expect( + newestSoakClearedVersion('7.1.0', times, { + ...soak, + exclude: ['is-number@7.1.0'], + }), + ).toBe('7.1.0') + expect( + newestSoakClearedVersion('7.1.0', times, { + ...soak, + exclude: ['is-number'], + }), + ).toBe('7.1.0') + }) + + it('returns latest verbatim when soak is off or metadata is degraded', () => { + const times = { '7.1.0': at(1) } + expect( + newestSoakClearedVersion('7.1.0', times, { ...soak, soakMinutes: 0 }), + ).toBe('7.1.0') + expect(newestSoakClearedVersion('7.1.0', undefined, soak)).toBe('7.1.0') + // Nothing cleared at all โ†’ surface latest for a human rather than + // hiding the drift. + expect(newestSoakClearedVersion('7.1.0', times, soak)).toBe('7.1.0') + expect(newestSoakClearedVersion(undefined, times, soak)).toBeUndefined() + }) }) describe('buildSyncPrompt', () => { diff --git a/test/scripts/repo/check/npm-overrides-are-current.test.mts b/test/scripts/repo/check/npm-overrides-are-current.test.mts index 7bddc9c25..4ad024707 100644 --- a/test/scripts/repo/check/npm-overrides-are-current.test.mts +++ b/test/scripts/repo/check/npm-overrides-are-current.test.mts @@ -12,12 +12,16 @@ import { formatStaleReport } from '../../../../scripts/repo/check/npm-overrides- import type { OverrideDrift } from '../../../../scripts/npm/sync-npm-overrides.mts' -function drift(status: OverrideDrift['status']): OverrideDrift { +function drift( + status: OverrideDrift['status'], + latestVersion = '2.2.4', +): OverrideDrift { return { socketPkgName: '@socketregistry/deep-equal', upstreamName: 'deep-equal', pinnedSpec: '2.2.3', - latestVersion: '2.2.4', + targetVersion: '2.2.4', + latestVersion, status, } } @@ -41,9 +45,17 @@ describe('scripts/repo/check/npm-overrides-are-current', () => { it('reports stale overrides with versions and the remedy command', () => { const report = formatStaleReport([drift('stale'), drift('current')]) expect(report).toBeDefined() - expect(report).toContain('1 npm override(s) behind their upstream latest') + expect(report).toContain( + '1 npm override(s) behind their newest soak-cleared upstream', + ) expect(report).toContain('@socketregistry/deep-equal') expect(report).toContain('2.2.3 -> 2.2.4') + expect(report).not.toContain('still soaking') expect(report).toContain('node scripts/npm/sync-npm-overrides.mts --apply') }) + + it('notes when a newer latest exists but is still inside the soak', () => { + const report = formatStaleReport([drift('stale', '2.3.0')]) + expect(report).toContain('2.2.3 -> 2.2.4 (latest 2.3.0 still soaking)') + }) }) From c4608de70be3a83358c8b031dff9f2b138c686f7 Mon Sep 17 00:00:00 2001 From: jdalton Date: Sun, 12 Jul 2026 04:55:00 -0400 Subject: [PATCH 10/11] feat(check): lock floor-node pin to the overrides' declared engines floor --- scripts/npm/floor-node-smoke.mts | 17 ++- .../check/floor-node-pin-matches-engines.mts | 123 ++++++++++++++++++ .../floor-node-pin-matches-engines.test.mts | 68 ++++++++++ 3 files changed, 203 insertions(+), 5 deletions(-) create mode 100644 scripts/repo/check/floor-node-pin-matches-engines.mts create mode 100644 test/scripts/repo/check/floor-node-pin-matches-engines.test.mts diff --git a/scripts/npm/floor-node-smoke.mts b/scripts/npm/floor-node-smoke.mts index 31084a8cb..d93d58709 100644 --- a/scripts/npm/floor-node-smoke.mts +++ b/scripts/npm/floor-node-smoke.mts @@ -36,7 +36,10 @@ const logger = getDefaultLogger() // The floor is the low edge of packages/npm/*'s `engines.node: >=24` range. // Bump BOTH the version and every sha256 together when the engines floor // moves โ€” hashes come from https://nodejs.org/dist/v/SHASUMS256.txt. -const FLOOR_NODE_VERSION = '24.0.0' +// floor-node-pin-matches-engines (check --all) enforces the version half of +// that lock-step: it fails when any override's engines floor drifts from +// this pin. +export const FLOOR_NODE_VERSION = '24.0.0' const FLOOR_NODE_PLATFORMS: Record = { 'darwin-arm64': { @@ -208,7 +211,11 @@ async function main(): Promise { } } -main().catch((e: unknown) => { - logger.error(e) - process.exitCode = 1 -}) +// Entrypoint-guarded: floor-node-pin-matches-engines (check --all) imports +// FLOOR_NODE_VERSION from this module, which must not trigger a download. +if (process.argv[1] && import.meta.url === `file://${process.argv[1]}`) { + main().catch((e: unknown) => { + logger.error(e) + process.exitCode = 1 + }) +} diff --git a/scripts/repo/check/floor-node-pin-matches-engines.mts b/scripts/repo/check/floor-node-pin-matches-engines.mts new file mode 100644 index 000000000..262e42bfc --- /dev/null +++ b/scripts/repo/check/floor-node-pin-matches-engines.mts @@ -0,0 +1,123 @@ +#!/usr/bin/env node +/** + * @file The floor-node smoke gate tests the Node version the overrides + * actually declare. The gate side-installs FLOOR_NODE_VERSION and imports + * every packages/npm/* entry under it โ€” its whole premise is that the pin + * IS the low edge of every override's `engines.node` range. That + * lock-step used to be a comment ("bump BOTH together"); this law + * enforces the version half: every override must declare `engines.node`, + * and each range's minimum satisfying version must equal the pin. An + * engines bump without a pin bump (or vice versa) would otherwise leave + * the gate green while proving the WRONG floor โ€” the exact silent drift + * it exists to prevent. (The sha256 half of the lock-step is enforced at + * runtime: httpDownload hard-fails on a hash mismatch.) + * Usage: node scripts/repo/check/floor-node-pin-matches-engines.mts. + */ + +import { existsSync, readdirSync, readFileSync } from 'node:fs' +import path from 'node:path' +import process from 'node:process' +// oxlint-disable-next-line socket/prefer-stable-external-semver -- @socketsecurity/lib-stable has no ./external/semver export at the pinned version; semver is a devDependency (scripts/tests only, not bundled). +import semver from 'semver' + +import { getDefaultLogger } from '@socketsecurity/lib-stable/logger/default' + +import { FLOOR_NODE_VERSION } from '../../npm/floor-node-smoke.mts' + +const logger = getDefaultLogger() + +export interface OverrideEngines { + name: string + enginesNode: string | undefined +} + +/** + * Classify each override's declared Node floor against the smoke gate's + * pin. Pure: directory reading happens in the caller so this stays + * unit-testable on synthetic fixtures. + */ +export function findFloorDrift( + overrides: OverrideEngines[], + floorVersion: string, +): string[] { + const findings: string[] = [] + for (let i = 0, { length } = overrides; i < length; i += 1) { + const { enginesNode, name } = overrides[i]! + if (!enginesNode) { + findings.push( + `${name}: no engines.node โ€” the floor gate's premise (every ` + + 'override declares its floor) does not hold for it', + ) + continue + } + let min: semver.SemVer | undefined + try { + // semver throws on a malformed range instead of returning null. + min = semver.minVersion(enginesNode, { loose: true }) ?? undefined + } catch { + min = undefined + } + if (!min) { + findings.push( + `${name}: engines.node ${JSON.stringify(enginesNode)} has no ` + + 'resolvable minimum version', + ) + continue + } + if (min.version !== floorVersion) { + findings.push( + `${name}: engines.node ${JSON.stringify(enginesNode)} floors at ` + + `${min.version}, but the smoke gate proves ${floorVersion}`, + ) + } + } + return findings +} + +function collectOverrideEngines(npmPackagesPath: string): OverrideEngines[] { + const out: OverrideEngines[] = [] + const dirents = readdirSync(npmPackagesPath, { withFileTypes: true }) + for (let i = 0, { length } = dirents; i < length; i += 1) { + const dirent = dirents[i]! + if (!dirent.isDirectory()) { + continue + } + const pkgJsonPath = path.join(npmPackagesPath, dirent.name, 'package.json') + if (!existsSync(pkgJsonPath)) { + continue + } + const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf8')) as { + engines?: { node?: string | undefined } | undefined + } + out.push({ name: dirent.name, enginesNode: pkgJson.engines?.node }) + } + return out +} + +async function main(): Promise { + const repoRoot = path.resolve(import.meta.dirname, '../../..') + const overrides = collectOverrideEngines(path.join(repoRoot, 'packages/npm')) + const findings = findFloorDrift(overrides, FLOOR_NODE_VERSION) + if (findings.length) { + logger.fail( + [ + `${findings.length} override(s) out of lock-step with the floor-node pin (${FLOOR_NODE_VERSION}):`, + ...findings.map(f => ` ${f}`), + 'Fix: move engines.node and FLOOR_NODE_VERSION together โ€”', + ' scripts/npm/floor-node-smoke.mts (bump the pin + every sha256 from', + ' https://nodejs.org/dist/v/SHASUMS256.txt), or correct the', + ' drifted engines range.', + ].join('\n'), + ) + process.exitCode = 1 + return 1 + } + logger.success( + `all ${overrides.length} overrides floor at ${FLOOR_NODE_VERSION} โ€” the smoke gate proves the declared floor`, + ) + return 0 +} + +if (process.argv[1] && import.meta.url === `file://${process.argv[1]}`) { + void main() +} diff --git a/test/scripts/repo/check/floor-node-pin-matches-engines.test.mts b/test/scripts/repo/check/floor-node-pin-matches-engines.test.mts new file mode 100644 index 000000000..bd976f387 --- /dev/null +++ b/test/scripts/repo/check/floor-node-pin-matches-engines.test.mts @@ -0,0 +1,68 @@ +/** + * @file Unit tests for the floor-pin โ†” engines lock-step law's pure core: + * both arms of each classification (in-step, drifted range, missing or + * unresolvable engines). Directory scanning stays in the caller, so the + * suite runs on synthetic fixtures with no repo coupling. + */ + +import { describe, expect, it } from 'vitest' + +import { findFloorDrift } from '../../../../scripts/repo/check/floor-node-pin-matches-engines.mts' + +const FLOOR = '24.0.0' + +describe('scripts/repo/check/floor-node-pin-matches-engines', () => { + it('passes when every override floors exactly at the pin', () => { + expect( + findFloorDrift( + [ + { name: 'a', enginesNode: '>=24' }, + { name: 'b', enginesNode: '>=24.0.0' }, + { name: 'c', enginesNode: '^24.0.0 || >=25' }, + ], + FLOOR, + ), + ).toEqual([]) + }) + + it('flags a range whose floor drifted above or below the pin', () => { + const above = findFloorDrift([{ name: 'a', enginesNode: '>=26' }], FLOOR) + expect(above).toHaveLength(1) + expect(above[0]).toContain('floors at 26.0.0') + expect(above[0]).toContain('proves 24.0.0') + const below = findFloorDrift([{ name: 'b', enginesNode: '>=18.19' }], FLOOR) + expect(below).toHaveLength(1) + expect(below[0]).toContain('floors at 18.19.0') + }) + + it('flags a missing engines.node โ€” the gate premise breaks silently', () => { + const findings = findFloorDrift( + [{ name: 'a', enginesNode: undefined }], + FLOOR, + ) + expect(findings).toHaveLength(1) + expect(findings[0]).toContain('no engines.node') + }) + + it('flags an unresolvable range instead of crashing', () => { + const findings = findFloorDrift( + [{ name: 'a', enginesNode: 'not-a-range' }], + FLOOR, + ) + expect(findings).toHaveLength(1) + expect(findings[0]).toContain('no resolvable minimum') + }) + + it('reports every drifted override, not just the first', () => { + expect( + findFloorDrift( + [ + { name: 'a', enginesNode: '>=26' }, + { name: 'b', enginesNode: '>=24' }, + { name: 'c', enginesNode: undefined }, + ], + FLOOR, + ), + ).toHaveLength(2) + }) +}) From 7490f09f418c3de9ce568a38b9ec41ef41154a8a Mon Sep 17 00:00:00 2001 From: jdalton Date: Sun, 12 Jul 2026 11:25:12 -0400 Subject: [PATCH 11/11] =?UTF-8?q?fix(registry):=20drop=20the=20bare=20vite?= =?UTF-8?q?st=20test=20script=20=E2=80=94=20the=20root=20wrapper=20owns=20?= =?UTF-8?q?test=20invocation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pnpm-lock.yaml | 3 --- registry/package.json | 2 -- 2 files changed, 5 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ef8e040c6..f6b101660 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1276,9 +1276,6 @@ importers: typescript: specifier: 'catalog:' version: 7.0.2 - vitest: - specifier: 'catalog:' - version: 4.1.9(@types/node@26.1.0)(@vitest/coverage-v8@4.1.9)(@vitest/ui@4.1.9)(vite@8.1.0(@types/node@26.1.0)(jiti@2.7.0)(yaml@2.9.0)) yoctocolors-cjs: specifier: 'catalog:' version: 2.1.3 diff --git a/registry/package.json b/registry/package.json index ec3d616e2..900bd389e 100644 --- a/registry/package.json +++ b/registry/package.json @@ -55,7 +55,6 @@ "clean": "del-cli dist '**/*.tsbuildinfo'", "dev": "pnpm run build:watch", "prepublishOnly": "pnpm run build", - "test": "vitest", "type-ci": "pnpm run check" }, "devDependencies": { @@ -65,7 +64,6 @@ "fast-glob": "catalog:", "rolldown": "catalog:", "typescript": "catalog:", - "vitest": "catalog:", "yoctocolors-cjs": "catalog:" }, "peerDependencies": {