Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions .github/workflows/e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3509,24 +3509,34 @@ jobs:
name: OpenShell gateway upgrade (${{ matrix.legacy.id }})
needs: generate-matrix
if: ${{ (github.event_name != 'workflow_dispatch' || (inputs.jobs == '' && inputs.targets == '')) || contains(format(',{0},', inputs.jobs), ',openshell-gateway-upgrade,') || contains(format(',{0},', inputs.targets), ',openshell-gateway-upgrade,') }}
runs-on: ubuntu-latest
runs-on: ${{ matrix.legacy.runner }}
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
legacy:
- id: v0.0.36
- id: v0.0.36-x86_64
runner: ubuntu-latest
nemoclaw_ref: v0.0.36
nemoclaw_commit: "3351fbdd4eb7d9b80ec471545083956327da2b10"
installer_sha256: "0c42400a0d3867739f1d75d612e069967be4506e169974bbbebf14b7af39144f"
sandbox_base_image_ref: "ghcr.io/nvidia/nemoclaw/sandbox-base@sha256:104151ffadc2ff0b6c815e3c95c2783ced61aee0d0f83fc327cc02be9b7e14e6"
openshell_version: 0.0.36
openclaw_version: 2026.4.24
- id: v0.0.55
- id: v0.0.55-x86_64
runner: ubuntu-latest
nemoclaw_ref: v0.0.55
nemoclaw_commit: "95d483fe2b6569d68e59493c60f19df09a068e8f"
installer_sha256: "ff8cf448e4d17b00421545a1f333262b615b1b0aa236d0cc5aeaf4e2cae2d897"
sandbox_base_image_ref: "ghcr.io/nvidia/nemoclaw/sandbox-base@sha256:104151ffadc2ff0b6c815e3c95c2783ced61aee0d0f83fc327cc02be9b7e14e6"
sandbox_base_image_ref: "ghcr.io/nvidia/nemoclaw/sandbox-base@sha256:10433a8cd2f2b809dd0fdf983514679e04c0f8aa1ff5bbff675029046033b108"
openshell_version: 0.0.44
openclaw_version: 2026.5.22
- id: v0.0.55-aarch64
runner: ubuntu-24.04-arm
nemoclaw_ref: v0.0.55
nemoclaw_commit: "95d483fe2b6569d68e59493c60f19df09a068e8f"
installer_sha256: "ff8cf448e4d17b00421545a1f333262b615b1b0aa236d0cc5aeaf4e2cae2d897"
sandbox_base_image_ref: "ghcr.io/nvidia/nemoclaw/sandbox-base@sha256:10433a8cd2f2b809dd0fdf983514679e04c0f8aa1ff5bbff675029046033b108"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
openshell_version: 0.0.44
openclaw_version: 2026.5.22
env:
Expand Down Expand Up @@ -3557,9 +3567,8 @@ jobs:
uses: NVIDIA/NemoClaw/.github/actions/prepare-e2e@50281ee84c4a6fc759da95ea28fc0b7d9c378a28

- name: Run OpenShell gateway upgrade live Vitest test
# This preserves the legacy ubuntu-latest + Docker/OpenShell runner
# lane while covering both the original v0.0.36 fixture and the exact
# v0.0.55/OpenShell 0.0.44 source shape from upgrade regression #6114.
# Keep the original v0.0.36 fixture on x86_64 and validate the exact
# v0.0.55/OpenShell 0.0.44 regression shape on x86_64 and arm64.
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
Expand Down
7 changes: 7 additions & 0 deletions scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2201,6 +2201,13 @@ preinstall_backup_and_retire_legacy_gateway() {
fi
_PREEXISTING_SANDBOX_COUNT="$sandbox_count"
[ "$sandbox_count" -gt 0 ] 2>/dev/null || return 0
if ! command_exists openshell; then
# NemoClaw v0.0.55's OpenShell 0.0.44 layout could install this binary
# without persisting ~/.local/bin on PATH. Retain this fallback while direct
# v0.0.55 upgrades are supported; remove it only after support for that
# source version and its regression fixture are retired together.
prefer_user_local_openshell
fi
command_exists openshell || return 0

if [[ "${NEMOCLAW_SINGLE_SESSION:-}" == "1" ]]; then
Expand Down
11 changes: 7 additions & 4 deletions test/e2e/live/openshell-gateway-upgrade-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import { shellQuote } from "../fixtures/clients/command.ts";

const COMMON_INSTALLER_ARGS = ["--non-interactive", "--yes-i-accept-third-party-software"];
const NON_INTERACTIVE_INSTALLER_ARGS = ["--non-interactive", "--yes-i-accept-third-party-software"];
const GATEWAY_VOLUME_PREFIX = "openshell-cluster-nemoclaw";

export interface LegacyGatewayUpgradeFixture {
Expand Down Expand Up @@ -47,11 +47,14 @@ export function validateLegacyGatewayUpgradeFixture(fixture: LegacyGatewayUpgrad
}

export function oldGatewayUpgradeInstallerArgs(installer: string): string[] {
return [installer, ...COMMON_INSTALLER_ARGS, "--fresh"];
return [installer, ...NON_INTERACTIVE_INSTALLER_ARGS, "--fresh"];
}

export function currentGatewayUpgradeInstallerArgs(installer: string): string[] {
return [installer, ...COMMON_INSTALLER_ARGS];
export function currentGatewayUpgradeInstallerArgs(
installer: string,
options: { interactive?: boolean } = {},
): string[] {
return options.interactive ? [installer] : [installer, ...NON_INTERACTIVE_INSTALLER_ARGS];
}

export function upgradeGatewayStateCleanupScript(pidFile: string): string {
Expand Down
103 changes: 85 additions & 18 deletions test/e2e/live/openshell-gateway-upgrade.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,13 @@ function liveEnv(extra: NodeJS.ProcessEnv = {}): NodeJS.ProcessEnv {
};
}

function shellLoginPrefix(): string {
return [
function withoutEnvKeys(env: NodeJS.ProcessEnv, keys: readonly string[]): NodeJS.ProcessEnv {
const excluded = new Set(keys);
return Object.fromEntries(Object.entries(env).filter(([key]) => !excluded.has(key)));
}

function shellLoginPrefix(hideUserLocalOpenShell = false): string {
const lines = [
"set -euo pipefail",
'if [ -f "$HOME/.bashrc" ]; then',
" # shellcheck source=/dev/null",
Expand All @@ -118,8 +123,22 @@ function shellLoginPrefix(): string {
" # shellcheck source=/dev/null",
' . "$NVM_DIR/nvm.sh"',
"fi",
'export PATH="$HOME/.local/bin:$PATH"',
].join("\n");
];
lines.push(
...(hideUserLocalOpenShell
? [
'_path_without_user_local=""',
"while IFS= read -r _path_entry; do",
' [ "$_path_entry" = "$HOME/.local/bin" ] && continue',
' _path_without_user_local="${_path_without_user_local:+${_path_without_user_local}:}${_path_entry}"',
'done < <(tr ":" "\\n" <<<"$PATH")',
'export PATH="$_path_without_user_local"',
"unset _path_without_user_local _path_entry",
"hash -r",
]
: ['export PATH="$HOME/.local/bin:$PATH"']),
);
return lines.join("\n");
}

function expectOutputContains(result: ShellProbeResult, value: string, label: string): void {
Expand Down Expand Up @@ -147,16 +166,21 @@ async function bash(
env?: NodeJS.ProcessEnv;
timeoutMs?: number;
cwd?: string;
hideUserLocalOpenShell?: boolean;
redactionValues?: string[];
},
): Promise<ShellProbeResult> {
return host.command("bash", ["-lc", `${shellLoginPrefix()}\n${script}`], {
cwd: options.cwd ?? REPO_ROOT,
artifactName: options.artifactName,
env: options.env ?? liveEnv(),
redactionValues: options.redactionValues,
timeoutMs: options.timeoutMs ?? OPENSHELL_TIMEOUT_MS,
});
return host.command(
"bash",
["-lc", `${shellLoginPrefix(options.hideUserLocalOpenShell)}\n${script}`],
{
cwd: options.cwd ?? REPO_ROOT,
artifactName: options.artifactName,
env: options.env ?? liveEnv(),
redactionValues: options.redactionValues,
timeoutMs: options.timeoutMs ?? OPENSHELL_TIMEOUT_MS,
},
);
}

// The frozen release installers are the source of truth, but their embedded
Expand Down Expand Up @@ -368,15 +392,33 @@ async function runInstallerPayload(
logFile: string,
env: NodeJS.ProcessEnv,
redactionValues: string[] = [],
options: { hideUserLocalOpenShell?: boolean; interactiveInput?: string } = {},
): Promise<ShellProbeResult> {
const quotedInstallerArgs = installerArgs.map(shellQuote).join(" ");
const installerCommand = `bash ${quotedInstallerArgs} >${shellQuote(logFile)} 2>&1`;
// The live command runner closes stdin. util-linux `script` supplies the
// /dev/tty that the ordinary curl|bash confirmation path expects.
const installerInvocation = options.interactiveInput
? `printf '%s\\n' ${shellQuote(options.interactiveInput)} | script --quiet --return --command ${shellQuote(installerCommand)} /dev/null`
: installerCommand;
const hiddenOpenShellPreflight = options.hideUserLocalOpenShell
? [
'test -x "$HOME/.local/bin/openshell"',
"if command -v openshell >/dev/null 2>&1; then",
' echo "Expected the v0.0.55 user-local OpenShell binary to be absent from PATH" >&2',
" exit 1",
"fi",
].join("\n")
: "";
const result = await bash(
host,
`rm -f ${shellQuote(logFile)}
bash ${quotedInstallerArgs} >${shellQuote(logFile)} 2>&1`,
`${hiddenOpenShellPreflight}
rm -f ${shellQuote(logFile)}
${installerInvocation}`,
{
artifactName: `${label.replace(/[^a-z0-9_.-]+/gi, "-")}-installer`,
env,
hideUserLocalOpenShell: options.hideUserLocalOpenShell,
redactionValues,
timeoutMs: INSTALL_TIMEOUT_MS,
},
Expand Down Expand Up @@ -576,11 +618,10 @@ async function installCurrentNemoclawUpgrade(
expectExitZero(currentRefResult, "resolve current NemoClaw ref");
const resolvedRef = currentRefResult.stdout.trim();
expect(resolvedRef.length).toBeGreaterThan(0);
const currentEnv = liveEnv({
const exerciseOrdinaryUpgrade = OLD_NEMOCLAW_REF === "v0.0.55";
const baseCurrentEnv = liveEnv({
COMPATIBLE_API_KEY: "dummy",
GITHUB_TOKEN: process.env.GITHUB_TOKEN ?? "",
NEMOCLAW_ACCEPT_EXPERIMENTAL_OPENSHELL_UPGRADE: "1",
NEMOCLAW_CONFIRM_LEGACY_MANAGED_RECREATE: JSON.stringify([SURVIVOR_SANDBOX]),
NEMOCLAW_BOOTSTRAP_PAYLOAD: "1",
NEMOCLAW_INSTALL_REF: resolvedRef,
NEMOCLAW_INSTALL_TAG: resolvedRef,
Expand All @@ -592,18 +633,44 @@ async function installCurrentNemoclawUpgrade(
NEMOCLAW_DASHBOARD_PORT: "",
CHAT_UI_URL: "",
});
const currentEnv = exerciseOrdinaryUpgrade
? withoutEnvKeys(baseCurrentEnv, [
"ACCEPT_THIRD_PARTY_SOFTWARE",
"NON_INTERACTIVE",
"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE",
"NEMOCLAW_NON_INTERACTIVE",
"NEMOCLAW_ACCEPT_EXPERIMENTAL_OPENSHELL_UPGRADE",
"NEMOCLAW_CONFIRM_LEGACY_MANAGED_RECREATE",
])
: {
...baseCurrentEnv,
NEMOCLAW_ACCEPT_EXPERIMENTAL_OPENSHELL_UPGRADE: "1",
NEMOCLAW_CONFIRM_LEGACY_MANAGED_RECREATE: JSON.stringify([SURVIVOR_SANDBOX]),
};
const redactionValues = [process.env.GITHUB_TOKEN ?? ""].filter(Boolean);
await runInstallerPayload(
host,
`current-${resolvedRef.slice(0, 12)}`,
currentGatewayUpgradeInstallerArgs(path.join(REPO_ROOT, "scripts", "install.sh")),
currentGatewayUpgradeInstallerArgs(path.join(REPO_ROOT, "scripts", "install.sh"), {
interactive: exerciseOrdinaryUpgrade,
}),
currentInstallLog,
currentEnv,
redactionValues,
{
hideUserLocalOpenShell: exerciseOrdinaryUpgrade,
// One answer covers a changed usage notice, when present, and the other
// confirms the legacy managed-image recovery prompt.
interactiveInput: exerciseOrdinaryUpgrade ? "yes\nyes" : undefined,
},
);

const currentLog = fs.readFileSync(currentInstallLog, "utf8");
expect(currentLog).toContain("Confirmed 1 exact pre-fingerprint sandbox name(s)");
expect(currentLog).toContain(
exerciseOrdinaryUpgrade
? "Confirmed legacy managed-image recovery"
: "Confirmed 1 exact pre-fingerprint sandbox name(s)",
);
expect(currentLog).toContain("Pre-upgrade backup: 1 backed up, 0 failed, 0 skipped");
expect(currentLog).toContain("Existing sandboxes recovered; skipping generic onboarding");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import {
readOpenShellGatewayUpgradeWorkflow,
validateOpenShellGatewayUpgradeWorkflow,
} from "../../../tools/e2e/openshell-gateway-upgrade-workflow-boundary.mts";
import {
validateE2eWorkflow,
validateE2eWorkflowBoundary,
} from "../../../tools/e2e/workflow-boundary.mts";
import {
currentGatewayUpgradeInstallerArgs,
oldGatewayUpgradeInstallerArgs,
Expand All @@ -14,6 +22,34 @@ import {
} from "../live/openshell-gateway-upgrade-helpers.ts";

describe("OpenShell gateway upgrade workflow boundary", () => {
it("pins the v0.0.55 x86_64 and arm64 fixtures to the canonical live test (#6114)", () => {
const workflow = readOpenShellGatewayUpgradeWorkflow();
expect(validateOpenShellGatewayUpgradeWorkflow(workflow)).toEqual([]);
expect(validateE2eWorkflowBoundary()).toEqual([]);

const job = (workflow.jobs as Record<string, Record<string, unknown>>)[
"openshell-gateway-upgrade"
];
job["runs-on"] = "ubuntu-latest";
const strategy = job.strategy as Record<string, Record<string, unknown>>;
const legacy = strategy.matrix.legacy as Array<Record<string, unknown>>;
legacy.find((fixture) => fixture.id === "v0.0.55-x86_64")!.sandbox_base_image_ref =
"ghcr.io/nvidia/nemoclaw/sandbox-base@sha256:104151ffadc2ff0b6c815e3c95c2783ced61aee0d0f83fc327cc02be9b7e14e6";
legacy.find((fixture) => fixture.id === "v0.0.55-aarch64")!.runner = "ubuntu-latest";
const run = (job.steps as Array<Record<string, unknown>>).find(
(step) => step.name === "Run OpenShell gateway upgrade live Vitest test",
)!;
run.run = "npx vitest run --project e2e-live unrelated.test.ts";

expect(validateE2eWorkflow(workflow)).toEqual(
expect.arrayContaining([
"openshell-gateway-upgrade must run on ${{ matrix.legacy.runner }}",
"openshell-gateway-upgrade v0.0.55 matrix must pin x86_64 and arm64 upgrade fixtures",
"openshell-gateway-upgrade step 'Run OpenShell gateway upgrade live Vitest test' must run: npx tsx tools/e2e/live-vitest-invocation.mts run --test-path test/e2e/live/openshell-gateway-upgrade.test.ts",
]),
);
});

it("freshens only the retryable old fixture install", () => {
expect(oldGatewayUpgradeInstallerArgs("old-install.sh")).toEqual([
"old-install.sh",
Expand All @@ -26,6 +62,9 @@ describe("OpenShell gateway upgrade workflow boundary", () => {
"--non-interactive",
"--yes-i-accept-third-party-software",
]);
expect(currentGatewayUpgradeInstallerArgs("current-install.sh", { interactive: true })).toEqual(
["current-install.sh"],
);
});

it("rejects mutable or injectable historical fixture inputs before use (#6114)", () => {
Expand All @@ -35,11 +74,11 @@ describe("OpenShell gateway upgrade workflow boundary", () => {
installerSha256: "ff8cf448e4d17b00421545a1f333262b615b1b0aa236d0cc5aeaf4e2cae2d897",
openclawVersion: "2026.5.22",
sandboxBaseImageRef:
"ghcr.io/nvidia/nemoclaw/sandbox-base@sha256:104151ffadc2ff0b6c815e3c95c2783ced61aee0d0f83fc327cc02be9b7e14e6",
"ghcr.io/nvidia/nemoclaw/sandbox-base@sha256:10433a8cd2f2b809dd0fdf983514679e04c0f8aa1ff5bbff675029046033b108",
};

expect(validateLegacyGatewayUpgradeFixture(fixture)).toEqual({
sandboxBaseDigest: "104151ffadc2ff0b6c815e3c95c2783ced61aee0d0f83fc327cc02be9b7e14e6",
sandboxBaseDigest: "10433a8cd2f2b809dd0fdf983514679e04c0f8aa1ff5bbff675029046033b108",
});
expect(() =>
validateLegacyGatewayUpgradeFixture({
Expand Down
Loading
Loading