diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index b2f7289794..6b8fbdce64 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -496,11 +496,37 @@ jobs: # Clean-room install of the generated npm packages under Node: bare-install # CLI startup, optional-peer metadata, extension package loading, and a real # starter dev-server render (see scripts/test/npm-install-smoke.sh). + npm-smoke-node-versions: + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} + runs-on: ubuntu-latest + timeout-minutes: 10 + name: npm smoke Node version contract + outputs: + node_versions: ${{ steps.versions.outputs.node_versions }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: ./.github/actions/setup-deno + timeout-minutes: 5 + - name: Read npm smoke Node versions + id: versions + run: | + deno eval ' + import { NPM_SMOKE_NODE_VERSIONS } from "./scripts/build/runtime-support.ts"; + console.log(`node_versions=${JSON.stringify(NPM_SMOKE_NODE_VERSIONS)}`); + ' >> "$GITHUB_OUTPUT" + tests-npm-install-smoke: if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} + needs: npm-smoke-node-versions runs-on: ubuntu-latest timeout-minutes: 30 - name: tests (npm install smoke) + name: "tests (npm install smoke: Node ${{ matrix.node-version }})" + strategy: + fail-fast: false + matrix: + node-version: ${{ fromJSON(needs.npm-smoke-node-versions.outputs.node_versions) }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -511,7 +537,7 @@ jobs: warm-cache: "true" - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v6 with: - node-version: "24" + node-version: ${{ matrix.node-version }} package-manager-cache: false - name: Install TypeScript toolchain run: npm --prefix storybook ci --ignore-scripts diff --git a/deno.json b/deno.json index f995da72a3..6a626efe29 100644 --- a/deno.json +++ b/deno.json @@ -559,7 +559,7 @@ "lint:render-mode-defaults": "deno run --allow-read scripts/lint/audit-render-mode-defaults.ts", "lint:dnt-meta-properties": "deno run --config=scripts/test.deno.json --frozen --allow-read scripts/build/dnt-meta-property-safety.ts", "test:layout": "deno run --config=scripts/test.deno.json --no-check --allow-read --allow-run=git --allow-env=TEST_LAYOUT_MIGRATION_BASE_REF scripts/test/test-layout.ts", - "test:scripts": "deno test --config=scripts/test.deno.json --no-check --allow-read --allow-write --allow-env=DENO_DIR,HOME,XDG_CACHE_HOME,LOCALAPPDATA,USERPROFILE --allow-run scripts/lint/check-deno-version.test.ts scripts/ci/automated-review-gate.test.ts scripts/ci/prepare-rc-build.test.ts scripts/ci/publish-npm-packages.test.ts scripts/ci/setup-deno-workflow.test.ts scripts/test/coverage-ci.test.ts scripts/test/run-suite.test.ts scripts/test/run-test-file.test.ts scripts/test/suites.test.ts scripts/test/test-layout.test.ts scripts/build/compile-binary.test.ts scripts/build/dnt-jsx-runtime.test.ts scripts/build/dnt-meta-property-safety.test.ts scripts/build/dnt-polyfill.test.ts scripts/build/generate-sbom.test.ts scripts/build/generated-artifact-checks.test.ts scripts/build/npm-dependency-sources.test.ts scripts/build/npm-extension-package-metadata.test.ts scripts/build/npm-package-metadata.test.ts scripts/build/npm-react-shims.test.ts scripts/build/npm-runtime-helper-contract.test.ts scripts/build/prepare-framework-sources.test.ts scripts/build/report-artifact-sizes.test.ts scripts/build/run-generate.test.ts scripts/docs/docs-coverage.test.ts scripts/docs/generate-api-reference.test.ts scripts/docs/guide-validation.test.ts scripts/docs/validate-tracked-docs.test.ts scripts/lint/audit-chat-composability.test.ts scripts/lint/audit-core-deps.test.ts scripts/lint/audit-cwd-relative-test-reads.test.ts scripts/lint/audit-cross-runtime-jsr.test.ts scripts/lint/audit-dependency-boundaries.test.ts scripts/lint/audit-extension-capabilities.test.ts scripts/lint/audit-extension-contracts.test.ts scripts/lint/audit-deps.test.ts scripts/lint/check-module-boundaries.test.ts scripts/lint/lint-config.test.ts scripts/lint/ban-test-only.test.ts scripts/lint/check-sanitizer-baseline.test.ts scripts/lint/check-skipped-tests-baseline.test.ts scripts/lint/audit-anti-slop.test.ts scripts/lint/audit-render-mode-defaults.test.ts scripts/lint/check-test-typecheck-baseline.test.ts scripts/lint/check-coverage.test.ts scripts/security/audit-npm.test.ts scripts/security/secret-scanning-config.test.ts scripts/security/submit-dependency-snapshot.test.ts scripts/test/template-runtime-e2e.test.ts scripts/test/runtime-inference-critical-flow.test.ts && deno task test:tool-search-live", + "test:scripts": "deno test --config=scripts/test.deno.json --no-check --allow-read --allow-write --allow-env=DENO_DIR,HOME,XDG_CACHE_HOME,LOCALAPPDATA,USERPROFILE --allow-run scripts/lint/check-deno-version.test.ts scripts/ci/automated-review-gate.test.ts scripts/ci/prepare-rc-build.test.ts scripts/ci/publish-npm-packages.test.ts scripts/ci/setup-deno-workflow.test.ts scripts/test/coverage-ci.test.ts scripts/test/run-suite.test.ts scripts/test/run-test-file.test.ts scripts/test/suites.test.ts scripts/test/test-layout.test.ts scripts/build/compile-binary.test.ts scripts/build/dnt-jsx-runtime.test.ts scripts/build/dnt-meta-property-safety.test.ts scripts/build/dnt-polyfill.test.ts scripts/build/generate-sbom.test.ts scripts/build/generated-artifact-checks.test.ts scripts/build/npm-dependency-sources.test.ts scripts/build/npm-extension-package-metadata.test.ts scripts/build/npm-package-metadata.test.ts scripts/build/npm-react-shims.test.ts scripts/build/npm-runtime-helper-contract.test.ts scripts/build/prepare-framework-sources.test.ts scripts/build/report-artifact-sizes.test.ts scripts/build/runtime-support.test.ts scripts/build/run-generate.test.ts scripts/docs/docs-coverage.test.ts scripts/docs/generate-api-reference.test.ts scripts/docs/guide-validation.test.ts scripts/docs/validate-tracked-docs.test.ts scripts/lint/audit-chat-composability.test.ts scripts/lint/audit-core-deps.test.ts scripts/lint/audit-cwd-relative-test-reads.test.ts scripts/lint/audit-cross-runtime-jsr.test.ts scripts/lint/audit-dependency-boundaries.test.ts scripts/lint/audit-extension-capabilities.test.ts scripts/lint/audit-extension-contracts.test.ts scripts/lint/audit-deps.test.ts scripts/lint/check-module-boundaries.test.ts scripts/lint/lint-config.test.ts scripts/lint/ban-test-only.test.ts scripts/lint/check-sanitizer-baseline.test.ts scripts/lint/check-skipped-tests-baseline.test.ts scripts/lint/audit-anti-slop.test.ts scripts/lint/audit-render-mode-defaults.test.ts scripts/lint/check-test-typecheck-baseline.test.ts scripts/lint/check-coverage.test.ts scripts/security/audit-npm.test.ts scripts/security/secret-scanning-config.test.ts scripts/security/submit-dependency-snapshot.test.ts scripts/test/template-runtime-e2e.test.ts scripts/test/runtime-inference-critical-flow.test.ts && deno task test:tool-search-live", "test:sentry-runtime-packages": "deno test --config=scripts/test.deno.json --no-check --no-lock --allow-read --allow-write --allow-run --allow-env=DENO_DIR,HOME,XDG_CACHE_HOME,LOCALAPPDATA,USERPROFILE scripts/build/sentry-runtime-packages.test.ts", "test:tool-search-live": "VF_DISABLE_LRU_INTERVAL=1 deno test --no-check -A tests/agent/verify-tool-search-live.test.ts", "test:cross-runtime": "deno run --allow-all src/platform/compat/cross-runtime.test.ts", diff --git a/scripts/build/runtime-support.test.ts b/scripts/build/runtime-support.test.ts new file mode 100644 index 0000000000..989ccb47b8 --- /dev/null +++ b/scripts/build/runtime-support.test.ts @@ -0,0 +1,117 @@ +import { assert, assertEquals } from "#veryfront/testing/assert.ts"; +import { describe, it } from "#veryfront/testing/bdd.ts"; +import { parse } from "#std/yaml/parse"; +import { + CURRENT_NPM_SMOKE_NODE_RELEASE_LINE, + MINIMUM_NODE_RELEASE_LINE, + MINIMUM_NODE_VERSION, + NPM_SMOKE_NODE_VERSIONS, +} from "./runtime-support.ts"; + +const WORKFLOW_PATH = new URL( + "../../.github/workflows/cicd.yml", + import.meta.url, +); + +function record(value: unknown, label: string): Record { + if (typeof value !== "object" || value === null || Array.isArray(value)) { + throw new TypeError(`${label} must be a record`); + } + return value as Record; +} + +function steps( + job: Record, + label: string, +): Array> { + if (!Array.isArray(job.steps)) { + throw new TypeError(`${label} steps must be an array`); + } + return job.steps.map((step, index) => record(step, `${label} step ${index}`)); +} + +describe("npm smoke Node support contract", () => { + it("runs the packed npm smoke on the oldest supported and current release lines", async () => { + const workflow = record( + parse(await Deno.readTextFile(WORKFLOW_PATH)), + "CI workflow", + ); + const jobs = record(workflow.jobs, "CI workflow jobs"); + const versionsJob = record( + jobs["npm-smoke-node-versions"], + "npm smoke Node versions job", + ); + const versionsStep = steps(versionsJob, "npm smoke Node versions job").find( + (step) => step.id === "versions", + ); + assert( + versionsStep, + "The version contract job must publish its Node matrix", + ); + assertEquals( + record(versionsJob.outputs, "npm smoke Node versions outputs") + .node_versions, + "${{ steps.versions.outputs.node_versions }}", + ); + assertEquals( + typeof versionsStep.run === "string" && + versionsStep.run.includes("NPM_SMOKE_NODE_VERSIONS"), + true, + "The workflow must derive the matrix from the runtime support module", + ); + + const smokeJob = record( + jobs["tests-npm-install-smoke"], + "npm install smoke job", + ); + assertEquals(smokeJob.needs, "npm-smoke-node-versions"); + assertEquals( + record( + record(smokeJob.strategy, "npm smoke strategy").matrix, + "npm smoke matrix", + )[ + "node-version" + ], + "${{ fromJSON(needs.npm-smoke-node-versions.outputs.node_versions) }}", + ); + assertEquals( + smokeJob.name, + "tests (npm install smoke: Node ${{ matrix.node-version }})", + ); + + const smokeSteps = steps(smokeJob, "npm install smoke job"); + const setupNode = smokeSteps.find((step) => + step.uses === + "actions/setup-node@820762786026740c76f36085b0efc47a31fe5020" + ); + assert(setupNode, "The npm smoke job must install its matrix Node release"); + assertEquals( + record(setupNode.with, "npm smoke setup-node inputs")["node-version"], + "${{ matrix.node-version }}", + ); + assert( + smokeSteps.some((step) => + step.name === "Clean-room install/import smoke" && + step.run === "bash scripts/test/npm-install-smoke.sh" + ), + "Every matrix leg must run the existing packed clean-room smoke", + ); + + for (const jobName of ["prerelease", "release"]) { + const releaseJob = record(jobs[jobName], `${jobName} job`); + assert( + Array.isArray(releaseJob.needs) && + releaseJob.needs.includes("tests-npm-install-smoke"), + `${jobName} must wait for every npm install smoke matrix leg`, + ); + } + + assertEquals( + NPM_SMOKE_NODE_VERSIONS, + [MINIMUM_NODE_RELEASE_LINE, CURRENT_NPM_SMOKE_NODE_RELEASE_LINE], + ); + assertEquals(MINIMUM_NODE_RELEASE_LINE, "22"); + assertEquals(MINIMUM_NODE_VERSION, "22.3.0"); + assertEquals(CURRENT_NPM_SMOKE_NODE_RELEASE_LINE, "24"); + }); +}); diff --git a/scripts/build/runtime-support.ts b/scripts/build/runtime-support.ts index 2c4c577f11..f71ef44d55 100644 --- a/scripts/build/runtime-support.ts +++ b/scripts/build/runtime-support.ts @@ -1,6 +1,30 @@ /** Minimum Node.js release supported by the root and first-party npm packages. */ export const MINIMUM_NODE_VERSION = "22.3.0"; +/** Current Node.js release line used by the clean-room npm smoke. */ +export const CURRENT_NPM_SMOKE_NODE_RELEASE_LINE = "24"; + +/** Oldest supported Node.js release line, exercised at its latest patch. */ +export const MINIMUM_NODE_RELEASE_LINE = MINIMUM_NODE_VERSION.slice( + 0, + MINIMUM_NODE_VERSION.indexOf("."), +); + +/** + * Node.js releases that exercise the clean-room npm artifact boundary. + * + * These are release lines, so `setup-node` resolves each to its newest patch. + * That covers the lines, not the exact `MINIMUM_NODE_VERSION` the published + * `engines.node` accepts: the packed artifact fails from 22.3.0 through + * 22.12.0, 22.13.x is unverified, and 22.14.0 is the first observed passing + * version. Pin the exact floor here once veryfront-issue-inbox#748 either fixes + * that range or raises the floor. + */ +export const NPM_SMOKE_NODE_VERSIONS = Object.freeze([ + MINIMUM_NODE_RELEASE_LINE, + CURRENT_NPM_SMOKE_NODE_RELEASE_LINE, +]); + /** npm `engines.node` range derived from the single runtime-version contract. */ export const NPM_NODE_ENGINE = `>=${MINIMUM_NODE_VERSION}`;