From 7c8a68d48b29501bcc4ef164d84bc5c5a39c0607 Mon Sep 17 00:00:00 2001 From: Prekshi Vyas Date: Thu, 10 Sep 2026 16:36:47 -0700 Subject: [PATCH] fix(e2e): select reviewed SDK from candidate lock Signed-off-by: Prekshi Vyas --- .github/workflows/e2e-standard-profile.yaml | 11 +- .github/workflows/e2e.yaml | 14 +- .../checks/package-openshell-sdk-for-pr.mts | 26 +- test/e2e/README.md | 8 +- .../e2e/support/openshell-sdk-install.test.ts | 235 +++++++++++------- ...standard-profile-workflow-boundary.test.ts | 14 ++ .../package-openshell-sdk-for-pr.test.ts | 35 ++- .../standard-profile-workflow-boundary.mts | 20 +- 8 files changed, 243 insertions(+), 120 deletions(-) diff --git a/.github/workflows/e2e-standard-profile.yaml b/.github/workflows/e2e-standard-profile.yaml index a1e71519a77..63842ab98fe 100644 --- a/.github/workflows/e2e-standard-profile.yaml +++ b/.github/workflows/e2e-standard-profile.yaml @@ -431,10 +431,13 @@ jobs: shell: bash run: | set -euo pipefail - mapfile -t archives < <(find "$RUNNER_TEMP/openshell-sdk" -maxdepth 1 -type f -name '*.tgz' -print) - test "${#archives[@]}" -eq 1 - env -u NODE_AUTH_TOKEN -u GITHUB_TOKEN -u GH_TOKEN \ - npm cache add "${archives[0]}" --offline --ignore-scripts + mapfile -t archives < <(find "$RUNNER_TEMP/openshell-sdk" -maxdepth 1 -type f -name '*.tgz' -print | sort) + test "${#archives[@]}" -ge 1 + test "${#archives[@]}" -le 2 + for archive in "${archives[@]}"; do + env -u NODE_AUTH_TOKEN -u GITHUB_TOKEN -u GH_TOKEN \ + npm cache add "$archive" --offline --ignore-scripts + done env -u NODE_AUTH_TOKEN -u GITHUB_TOKEN -u GH_TOKEN \ npm ci --ignore-scripts --prefer-offline --no-audit --no-fund env -u NODE_AUTH_TOKEN -u GITHUB_TOKEN -u GH_TOKEN \ diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 4b107468508..6dd7cceb396 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -147,9 +147,10 @@ jobs: scope: "@nvidia" - id: package - name: Download and verify exact OpenShell SDK package + name: Download and verify reviewed OpenShell SDK packages env: NEMOCLAW_OPEN_SHELL_SDK_OUTPUT_DIRECTORY: ${{ runner.temp }}/openshell-sdk + NEMOCLAW_OPEN_SHELL_SDK_INCLUDE_AVAILABLE_REPLACEMENT: "1" NODE_AUTH_TOKEN: ${{ github.token }} run: | set -euo pipefail @@ -3422,10 +3423,13 @@ jobs: - name: Install reviewed OpenShell SDK archive without package credentials run: | set -euo pipefail - mapfile -t archives < <(find "$RUNNER_TEMP/openshell-sdk" -maxdepth 1 -type f -name '*.tgz' -print) - test "${#archives[@]}" -eq 1 - env -u NODE_AUTH_TOKEN -u GITHUB_TOKEN -u GH_TOKEN \ - npm cache add "${archives[0]}" --offline --ignore-scripts + mapfile -t archives < <(find "$RUNNER_TEMP/openshell-sdk" -maxdepth 1 -type f -name '*.tgz' -print | sort) + test "${#archives[@]}" -ge 1 + test "${#archives[@]}" -le 2 + for archive in "${archives[@]}"; do + env -u NODE_AUTH_TOKEN -u GITHUB_TOKEN -u GH_TOKEN \ + npm cache add "$archive" --offline --ignore-scripts + done env -u NODE_AUTH_TOKEN -u GITHUB_TOKEN -u GH_TOKEN \ npm ci --ignore-scripts --prefer-offline --no-audit --no-fund env -u NODE_AUTH_TOKEN -u GITHUB_TOKEN -u GH_TOKEN \ diff --git a/scripts/checks/package-openshell-sdk-for-pr.mts b/scripts/checks/package-openshell-sdk-for-pr.mts index 5792ff682dd..69e24e37a67 100755 --- a/scripts/checks/package-openshell-sdk-for-pr.mts +++ b/scripts/checks/package-openshell-sdk-for-pr.mts @@ -13,7 +13,7 @@ const TRUSTED_REPOSITORY_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), export function packageReviewedOpenShellSdk( outputDirectory: string, - includeReplacement = false, + replacementMode: "exclude" | "require" | "if-present" = "exclude", dependencies: Readonly<{ pack?: typeof packReviewedNpmArchive; readAuditConfig?: () => string; @@ -27,11 +27,11 @@ export function packageReviewedOpenShellSdk( dependencies.readAuditConfig?.() ?? readFileSync(join(TRUSTED_REPOSITORY_ROOT, "ci/reviewed-npm-audit.json"), "utf8"), ); - if (includeReplacement && !config.sourceRegistryPackageReplacement) { + if (replacementMode === "require" && !config.sourceRegistryPackageReplacement) { throw new Error("reviewed OpenShell SDK replacement metadata is required"); } const reviewedPackages = - includeReplacement && config.sourceRegistryPackageReplacement + replacementMode !== "exclude" && config.sourceRegistryPackageReplacement ? [config.sourceRegistryPackage, config.sourceRegistryPackageReplacement] : [config.sourceRegistryPackage]; const archives: ReturnType[] = []; @@ -65,14 +65,28 @@ if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1] process.exit(1); } const includeReplacementValue = process.env.NEMOCLAW_OPEN_SHELL_SDK_INCLUDE_REPLACEMENT; + const includeAvailableReplacementValue = + process.env.NEMOCLAW_OPEN_SHELL_SDK_INCLUDE_AVAILABLE_REPLACEMENT; if (includeReplacementValue !== undefined && includeReplacementValue !== "1") { console.error("NEMOCLAW_OPEN_SHELL_SDK_INCLUDE_REPLACEMENT must be 1 when set"); process.exit(1); } + if (includeAvailableReplacementValue !== undefined && includeAvailableReplacementValue !== "1") { + console.error("NEMOCLAW_OPEN_SHELL_SDK_INCLUDE_AVAILABLE_REPLACEMENT must be 1 when set"); + process.exit(1); + } + if (includeReplacementValue !== undefined && includeAvailableReplacementValue !== undefined) { + console.error("OpenShell SDK replacement modes are mutually exclusive"); + process.exit(1); + } try { - process.stdout.write( - `${packageReviewedOpenShellSdk(outputDirectory, includeReplacementValue === "1")}\n`, - ); + const replacementMode = + includeReplacementValue === "1" + ? "require" + : includeAvailableReplacementValue === "1" + ? "if-present" + : "exclude"; + process.stdout.write(`${packageReviewedOpenShellSdk(outputDirectory, replacementMode)}\n`); } catch (error) { console.error(error instanceof Error ? error.message : String(error)); process.exit(1); diff --git a/test/e2e/README.md b/test/e2e/README.md index 6c867bb6a2d..6930e6a217c 100644 --- a/test/e2e/README.md +++ b/test/e2e/README.md @@ -359,10 +359,10 @@ and transport. It also stops the gateway and removes its temporary state. ## Catalogue Targets -Every catalogue profile installs the reviewed OpenShell SDK archive before restoring the candidate CLI. -The shared package job downloads and verifies the pinned SDK with package-read permission. -Catalogue jobs receive the run-scoped archive without package credentials and reject a missing or ambiguous archive. -Catalogue and external-gateway health jobs add the archive to npm's cache, then reinstall dependencies from the lockfile with package scripts disabled. +Every catalogue profile installs a lockfile-selected reviewed OpenShell SDK archive before restoring the candidate CLI. +The shared package job downloads and verifies the active SDK and any approved transition replacement with package-read permission. +Catalogue jobs receive the run-scoped archives without package credentials and reject a missing artifact or more than one approved transition pair. +Catalogue and external-gateway health jobs add each reviewed archive to npm's cache, then reinstall dependencies from the lockfile with package scripts disabled. This preserves the locked dependency versions and avoids npm resolving a new peer dependency graph during SDK installation. Both jobs verify that the SDK connection API loads before running tests. This keeps the private optional dependency available for SDK-backed commands such as configuration export. diff --git a/test/e2e/support/openshell-sdk-install.test.ts b/test/e2e/support/openshell-sdk-install.test.ts index dc6d6b314ba..710b10602c4 100644 --- a/test/e2e/support/openshell-sdk-install.test.ts +++ b/test/e2e/support/openshell-sdk-install.test.ts @@ -29,14 +29,15 @@ function writePackageArchive( root: string, name: string, dependencies: Record = {}, + version = "1.0.0", ) { - const source = path.join(root, name.replaceAll("/", "-")); + const source = path.join(root, `${name.replaceAll("/", "-")}-${version}`); fs.mkdirSync(source); fs.writeFileSync( path.join(source, "package.json"), JSON.stringify({ name, - version: "1.0.0", + version, type: "module", exports: "./index.js", dependencies, @@ -49,7 +50,7 @@ function writePackageArchive( path.join(source, "index.js"), name === "@nvidia/openshell-sdk" ? 'import { version } from "fixture-transport"; if (process.env.NODE_AUTH_TOKEN || process.env.GITHUB_TOKEN || process.env.GH_TOKEN) throw new Error("Unexpected credential"); export class OpenShellClient { static connect() { return version; } }' - : 'export const version = "1.0.0";', + : `export const version = ${JSON.stringify(version)};`, ); const packed = JSON.parse( execFileSync( @@ -71,7 +72,7 @@ function writePackageArchive( return { archive, lock: { - version: "1.0.0", + version, hasInstallScript: true, resolved: `https://registry.example.invalid/${path.basename(archive)}`, integrity: `sha512-${createHash("sha512").update(fs.readFileSync(archive)).digest("base64")}`, @@ -98,97 +99,135 @@ fs.writeFileSync(directory + "/index.js", process.env.SDK_SOURCE); describe("catalogue OpenShell SDK installation", () => { it.each([ - { name: "catalogue", script: installScript }, - { name: "external gateway", script: externalGatewayInstallScript }, - ])("installs the SDK and locked dependencies offline for $name", ({ script }) => { - const root = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-sdk-real-npm-")); - try { - const sdk = writePackageArchive(root, "@nvidia/openshell-sdk", { - "fixture-transport": "^1.0.0", - }); - const transport = writePackageArchive(root, "fixture-transport"); - const sibling = writePackageArchive(root, "fixture-sibling"); - const workspace = path.join(root, "workspace"); - fs.mkdirSync(workspace); - const manifest = JSON.stringify({ - name: "sdk-install-fixture", - version: "1.0.0", - dependencies: { "fixture-sibling": "^1.0.0" }, - optionalDependencies: { "@nvidia/openshell-sdk": "1.0.0" }, - scripts: { - preinstall: "node -e \"require('node:fs').writeFileSync('lifecycle-ran', 'yes')\"", - }, - }); - const lock = JSON.stringify({ - name: "sdk-install-fixture", - version: "1.0.0", - lockfileVersion: 3, - requires: true, - packages: { - "": JSON.parse(manifest), - "node_modules/@nvidia/openshell-sdk": { ...sdk.lock, optional: true }, - "node_modules/fixture-transport": { ...transport.lock, optional: true }, - "node_modules/fixture-sibling": sibling.lock, - }, - }); - fs.writeFileSync(path.join(workspace, "package.json"), manifest); - fs.writeFileSync(path.join(workspace, "package-lock.json"), lock); - const env = { - PATH: process.env.PATH, - HOME: root, - NPM_CONFIG_CACHE: path.join(root, "cache"), - NPM_CONFIG_OFFLINE: "true", - NPM_CONFIG_AUDIT: "false", - NPM_CONFIG_FUND: "false", - NPM_CONFIG_UPDATE_NOTIFIER: "false", - RUNNER_TEMP: root, - }; - const runNpm = (args: string[]) => - execFileSync("npm", args, { + { name: "catalogue active SDK", script: installScript, lockedSdkVersion: "0.9.0" }, + { name: "catalogue replacement SDK", script: installScript, lockedSdkVersion: "1.0.0" }, + { + name: "external gateway active SDK", + script: externalGatewayInstallScript, + lockedSdkVersion: "0.9.0", + }, + { + name: "external gateway replacement SDK", + script: externalGatewayInstallScript, + lockedSdkVersion: "1.0.0", + }, + ])( + "installs the lock-selected SDK and dependencies offline for $name", + ({ lockedSdkVersion, script }) => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-sdk-real-npm-")); + try { + const sdk = writePackageArchive(root, "@nvidia/openshell-sdk", { + "fixture-transport": "^1.0.0", + }); + const previousSdk = writePackageArchive( + root, + "@nvidia/openshell-sdk", + { "fixture-transport": "^1.0.0" }, + "0.9.0", + ); + const selectedSdk = lockedSdkVersion === "1.0.0" ? sdk : previousSdk; + const transport = writePackageArchive(root, "fixture-transport"); + const sibling = writePackageArchive(root, "fixture-sibling"); + const workspace = path.join(root, "workspace"); + fs.mkdirSync(workspace); + const manifest = JSON.stringify({ + name: "sdk-install-fixture", + version: "1.0.0", + dependencies: { "fixture-sibling": "^1.0.0" }, + optionalDependencies: { "@nvidia/openshell-sdk": lockedSdkVersion }, + scripts: { + preinstall: "node -e \"require('node:fs').writeFileSync('lifecycle-ran', 'yes')\"", + }, + }); + const lock = JSON.stringify({ + name: "sdk-install-fixture", + version: "1.0.0", + lockfileVersion: 3, + requires: true, + packages: { + "": JSON.parse(manifest), + "node_modules/@nvidia/openshell-sdk": { ...selectedSdk.lock, optional: true }, + "node_modules/fixture-transport": { ...transport.lock, optional: true }, + "node_modules/fixture-sibling": sibling.lock, + }, + }); + fs.writeFileSync(path.join(workspace, "package.json"), manifest); + fs.writeFileSync(path.join(workspace, "package-lock.json"), lock); + const env = { + PATH: process.env.PATH, + HOME: root, + NPM_CONFIG_CACHE: path.join(root, "cache"), + NPM_CONFIG_OFFLINE: "true", + NPM_CONFIG_AUDIT: "false", + NPM_CONFIG_FUND: "false", + NPM_CONFIG_UPDATE_NOTIFIER: "false", + RUNNER_TEMP: root, + }; + const runNpm = (args: string[]) => + execFileSync("npm", args, { + cwd: workspace, + encoding: "utf8", + env, + timeout: 10_000, + }); + runNpm([ + "cache", + "add", + transport.archive, + sibling.archive, + "--offline", + "--ignore-scripts", + ]); + runNpm(["ci", "--ignore-scripts"]); + expect(fs.existsSync(path.join(workspace, "node_modules/@nvidia/openshell-sdk"))).toBe( + false, + ); + fs.mkdirSync(path.join(root, "openshell-sdk")); + fs.copyFileSync(sdk.archive, path.join(root, "openshell-sdk", "sdk.tgz")); + fs.copyFileSync(previousSdk.archive, path.join(root, "openshell-sdk", "previous-sdk.tgz")); + + const result = spawnSync("bash", ["-c", script], { cwd: workspace, encoding: "utf8", env, - timeout: 10_000, + timeout: 20_000, }); - runNpm(["cache", "add", transport.archive, sibling.archive, "--offline", "--ignore-scripts"]); - runNpm(["ci", "--ignore-scripts"]); - expect(fs.existsSync(path.join(workspace, "node_modules/@nvidia/openshell-sdk"))).toBe(false); - fs.mkdirSync(path.join(root, "openshell-sdk")); - fs.copyFileSync(sdk.archive, path.join(root, "openshell-sdk", "sdk.tgz")); - - const result = spawnSync("bash", ["-c", script], { - cwd: workspace, - encoding: "utf8", - env, - timeout: 20_000, - }); - expect(result.error).toBeUndefined(); - expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0); - const observed = execFileSync( - process.execPath, - [ - "--input-type=module", - "-e", - 'import { OpenShellClient } from "@nvidia/openshell-sdk"; import { version } from "fixture-sibling"; console.log(JSON.stringify([OpenShellClient.connect(), version]));', - ], - { cwd: workspace, encoding: "utf8", env }, - ); - expect(JSON.parse(observed)).toEqual(["1.0.0", "1.0.0"]); - expect(fs.existsSync(path.join(workspace, "lifecycle-ran"))).toBe(false); - expect( - fs.existsSync(path.join(workspace, "node_modules/@nvidia/openshell-sdk/lifecycle-ran")), - ).toBe(false); - expect( - fs.existsSync(path.join(workspace, "node_modules/fixture-transport/lifecycle-ran")), - ).toBe(false); - expect( - fs.existsSync(path.join(workspace, "node_modules/fixture-sibling/lifecycle-ran")), - ).toBe(false); - } finally { - fs.rmSync(root, { recursive: true, force: true }); - } - }); + expect(result.error).toBeUndefined(); + expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0); + const observed = execFileSync( + process.execPath, + [ + "--input-type=module", + "-e", + 'import { OpenShellClient } from "@nvidia/openshell-sdk"; import { version } from "fixture-sibling"; console.log(JSON.stringify([OpenShellClient.connect(), version]));', + ], + { cwd: workspace, encoding: "utf8", env }, + ); + expect(JSON.parse(observed)).toEqual(["1.0.0", "1.0.0"]); + expect( + JSON.parse( + fs.readFileSync( + path.join(workspace, "node_modules/@nvidia/openshell-sdk/package.json"), + "utf8", + ), + ).version, + ).toBe(lockedSdkVersion); + expect(fs.existsSync(path.join(workspace, "lifecycle-ran"))).toBe(false); + expect( + fs.existsSync(path.join(workspace, "node_modules/@nvidia/openshell-sdk/lifecycle-ran")), + ).toBe(false); + expect( + fs.existsSync(path.join(workspace, "node_modules/fixture-transport/lifecycle-ran")), + ).toBe(false); + expect( + fs.existsSync(path.join(workspace, "node_modules/fixture-sibling/lifecycle-ran")), + ).toBe(false); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } + }, + ); it.each([ { @@ -201,8 +240,16 @@ describe("catalogue OpenShell SDK installation", () => { }, { name: "no archive", archives: [], sdk: "", status: 1, calls: 0, failure: "" }, { - name: "ambiguous archives", + name: "one approved transition pair", archives: ["first.tgz", "second.tgz"], + sdk: "export class OpenShellClient { static connect() {} }", + status: 0, + calls: 3, + failure: "", + }, + { + name: "more than one transition pair", + archives: ["first.tgz", "second.tgz", "third.tgz"], sdk: "", status: 1, calls: 0, @@ -269,16 +316,16 @@ describe("catalogue OpenShell SDK installation", () => { expect(result.signal).toBeNull(); expect(result.status, result.stderr).toBe(status); const expectedCalls = [ - { + ...archives.map((archive) => ({ args: [ "cache", "add", - path.join(archiveDirectory, archives[0] ?? ""), + path.join(archiveDirectory, archive), "--offline", "--ignore-scripts", ], auth: [null, null, null], - }, + })), { args: ["ci", "--ignore-scripts", "--prefer-offline", "--no-audit", "--no-fund"], auth: [null, null, null], diff --git a/test/e2e/support/standard-profile-workflow-boundary.test.ts b/test/e2e/support/standard-profile-workflow-boundary.test.ts index b9c1283412e..48fc97c842c 100644 --- a/test/e2e/support/standard-profile-workflow-boundary.test.ts +++ b/test/e2e/support/standard-profile-workflow-boundary.test.ts @@ -136,6 +136,20 @@ describe("standard E2E execution profile", () => { ); }); + it("requires the SDK producer to include an available reviewed transition replacement", () => { + const workflow = readWorkflow() as { + jobs: Record; name?: string }> }>; + }; + const packageStep = workflow.jobs["package-openshell-sdk"]!.steps.find( + (step) => step.name === "Download and verify reviewed OpenShell SDK packages", + )!; + delete packageStep.env!.NEMOCLAW_OPEN_SHELL_SDK_INCLUDE_AVAILABLE_REPLACEMENT; + + expect(validateStandardProfileWorkflowBoundary(workflow)).toContain( + "catalogue SDK packaging must include an available reviewed transition replacement", + ); + }); + it("rejects a catalogue caller that does not consume its SDK artifact", () => { const workflow = readWorkflow() as { jobs: Record }> }; workflow.jobs["catalogue-nvidia-inference"]!.with.openshell_sdk_artifact_name = "unrelated"; diff --git a/test/repository/package-openshell-sdk-for-pr.test.ts b/test/repository/package-openshell-sdk-for-pr.test.ts index 399c59ff087..822e4e676cf 100644 --- a/test/repository/package-openshell-sdk-for-pr.test.ts +++ b/test/repository/package-openshell-sdk-for-pr.test.ts @@ -41,7 +41,7 @@ describe("reviewed OpenShell SDK transition packaging", () => { delete config.sourceRegistryPackageReplacement; expect(() => - packageReviewedOpenShellSdk(source.output, true, { + packageReviewedOpenShellSdk(source.output, "require", { ...source, readAuditConfig: () => JSON.stringify(config), }), @@ -52,7 +52,7 @@ describe("reviewed OpenShell SDK transition packaging", () => { it("keeps the default package result to the active SDK archive", () => { const source = fixture(); - const artifact = packageReviewedOpenShellSdk(source.output, false, source); + const artifact = packageReviewedOpenShellSdk(source.output, "exclude", source); expect(path.basename(artifact)).toBe("nvidia-openshell-sdk-0.0.106.tgz"); expect(source.requests.map(({ packageSpec }) => packageSpec)).toEqual([ @@ -64,7 +64,7 @@ describe("reviewed OpenShell SDK transition packaging", () => { it("packages exactly the active and replacement identities for PR selection", () => { const source = fixture(); - const artifactDirectory = packageReviewedOpenShellSdk(source.output, true, source); + const artifactDirectory = packageReviewedOpenShellSdk(source.output, "require", source); expect(artifactDirectory).toBe(source.output); expect(fs.readdirSync(artifactDirectory).sort()).toEqual([ @@ -77,4 +77,33 @@ describe("reviewed OpenShell SDK transition packaging", () => { ]); expect(source.remove).toHaveBeenCalledTimes(2); }); + + it("packages the replacement when available without requiring transition metadata", () => { + const source = fixture(); + + const artifactDirectory = packageReviewedOpenShellSdk(source.output, "if-present", source); + + expect(artifactDirectory).toBe(source.output); + expect(source.requests.map(({ packageSpec }) => packageSpec)).toEqual([ + "@nvidia/openshell-sdk@0.0.106", + "@nvidia/openshell-sdk@0.0.116", + ]); + }); + + it("keeps the active archive when optional transition metadata is absent", () => { + const source = fixture(); + const configPath = path.resolve("ci/reviewed-npm-audit.json"); + const config = JSON.parse(fs.readFileSync(configPath, "utf8")) as Record; + delete config.sourceRegistryPackageReplacement; + + const artifact = packageReviewedOpenShellSdk(source.output, "if-present", { + ...source, + readAuditConfig: () => JSON.stringify(config), + }); + + expect(path.basename(artifact)).toBe("nvidia-openshell-sdk-0.0.106.tgz"); + expect(source.requests.map(({ packageSpec }) => packageSpec)).toEqual([ + "@nvidia/openshell-sdk@0.0.106", + ]); + }); }); diff --git a/tools/e2e/standard-profile-workflow-boundary.mts b/tools/e2e/standard-profile-workflow-boundary.mts index 7ddc0f3d567..7e128d72878 100644 --- a/tools/e2e/standard-profile-workflow-boundary.mts +++ b/tools/e2e/standard-profile-workflow-boundary.mts @@ -87,10 +87,13 @@ const PROFILE_JOBS = { const SDK_INSTALL_SCRIPT = [ "set -euo pipefail", - "mapfile -t archives < <(find \"$RUNNER_TEMP/openshell-sdk\" -maxdepth 1 -type f -name '*.tgz' -print)", - 'test "${#archives[@]}" -eq 1', - "env -u NODE_AUTH_TOKEN -u GITHUB_TOKEN -u GH_TOKEN \\", - ' npm cache add "${archives[0]}" --offline --ignore-scripts', + "mapfile -t archives < <(find \"$RUNNER_TEMP/openshell-sdk\" -maxdepth 1 -type f -name '*.tgz' -print | sort)", + 'test "${#archives[@]}" -ge 1', + 'test "${#archives[@]}" -le 2', + 'for archive in "${archives[@]}"; do', + " env -u NODE_AUTH_TOKEN -u GITHUB_TOKEN -u GH_TOKEN \\", + ' npm cache add "$archive" --offline --ignore-scripts', + "done", "env -u NODE_AUTH_TOKEN -u GITHUB_TOKEN -u GH_TOKEN \\", " npm ci --ignore-scripts --prefer-offline --no-audit --no-fund", "env -u NODE_AUTH_TOKEN -u GITHUB_TOKEN -u GH_TOKEN \\", @@ -136,6 +139,15 @@ function validateProfileCallers(errors: string[], workflow: WorkflowRecord): voi "catalogue profiles require SDK packaging for every E2E run with package-read permission", ); } + const sdkPackageStep = namedStep( + steps(sdkPackage.steps), + "Download and verify reviewed OpenShell SDK packages", + ); + if (record(sdkPackageStep?.env).NEMOCLAW_OPEN_SHELL_SDK_INCLUDE_AVAILABLE_REPLACEMENT !== "1") { + errors.push( + "catalogue SDK packaging must include an available reviewed transition replacement", + ); + } for (const profile of E2E_EXECUTION_PROFILES) { const contract = PROFILE_JOBS[profile]; const job = record(jobs[contract.job]);