Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
3e3448a
test(e2e): migrate Hermes root entrypoint smoke
jyaunches Jun 11, 2026
f00d47c
Merge remote-tracking branch 'origin/main' into e2e-migrate/test-herm…
jyaunches Jun 11, 2026
da55fed
ci(e2e): allow Hermes root smoke dispatch
jyaunches Jun 11, 2026
cdf6199
Merge remote-tracking branch 'origin/main' into e2e-migrate/test-herm…
jyaunches Jun 11, 2026
2647e83
ci(e2e): align hermes-root-entrypoint-smoke-vitest dispatch
jyaunches Jun 11, 2026
f0d7eb4
Merge remote-tracking branch 'origin/main' into e2e-migrate/test-herm…
jyaunches Jun 12, 2026
38a4c9c
fix(e2e): harden hermes smoke workflow coverage
jyaunches Jun 12, 2026
52d4e1f
Merge remote-tracking branch 'origin/main' into e2e-migrate/test-herm…
jyaunches Jun 12, 2026
be59903
Merge remote-tracking branch 'origin/main' into e2e-migrate/test-herm…
jyaunches Jun 12, 2026
78390e9
Merge remote-tracking branch 'origin/main' into e2e-migrate/test-herm…
jyaunches Jun 12, 2026
186dda4
Merge remote-tracking branch 'origin/main' into e2e-migrate/test-herm…
jyaunches Jun 12, 2026
6027ed6
fix(e2e): isolate hermes smoke docker auth
jyaunches Jun 12, 2026
90fb61d
fix(e2e): route docker probe through env boundary
jyaunches Jun 12, 2026
d47a4f5
style(e2e): format hermes root smoke
jyaunches Jun 12, 2026
354a1fe
test(e2e): cover docker diagnostic redaction
jyaunches Jun 12, 2026
366edde
ci(e2e): avoid sandbox checkout credentials
jyaunches Jun 12, 2026
7368ec4
Merge remote-tracking branch 'origin/main' into e2e-migrate/test-herm…
jyaunches Jun 12, 2026
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
49 changes: 46 additions & 3 deletions .github/workflows/e2e-vitest-scenarios.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:
SCENARIOS: ${{ inputs.scenarios }}
run: |
set -euo pipefail
allowed_jobs="openshell-version-pin-vitest,onboard-negative-paths-vitest,inference-routing-vitest,credential-migration-vitest,runtime-overrides-vitest,hermes-e2e-vitest,network-policy-vitest,rebuild-openclaw-vitest,token-rotation-vitest,launchable-smoke-vitest,openclaw-tui-chat-correlation-vitest,gateway-guard-recovery,double-onboard-vitest,issue-4434-tui-unreachable-inference-vitest"
allowed_jobs="openshell-version-pin-vitest,onboard-negative-paths-vitest,inference-routing-vitest,credential-migration-vitest,runtime-overrides-vitest,hermes-e2e-vitest,hermes-root-entrypoint-smoke-vitest,network-policy-vitest,rebuild-openclaw-vitest,token-rotation-vitest,launchable-smoke-vitest,openclaw-tui-chat-correlation-vitest,gateway-guard-recovery,double-onboard-vitest,issue-4434-tui-unreachable-inference-vitest"
if [ -n "${JOBS}" ] && [ -n "${SCENARIOS}" ]; then
echo "::error::Use either scenarios or jobs, not both." >&2
exit 1
Expand Down Expand Up @@ -93,12 +93,12 @@ jobs:
SCENARIOS: ${{ inputs.scenarios }}
run: |
set -euo pipefail
allowed_jobs="openshell-version-pin-vitest,onboard-negative-paths-vitest,inference-routing-vitest,credential-migration-vitest,runtime-overrides-vitest,hermes-e2e-vitest,network-policy-vitest,rebuild-openclaw-vitest,token-rotation-vitest,launchable-smoke-vitest,openclaw-tui-chat-correlation-vitest,gateway-guard-recovery,double-onboard-vitest,issue-4434-tui-unreachable-inference-vitest"
allowed_jobs="openshell-version-pin-vitest,onboard-negative-paths-vitest,inference-routing-vitest,credential-migration-vitest,runtime-overrides-vitest,hermes-e2e-vitest,hermes-root-entrypoint-smoke-vitest,network-policy-vitest,rebuild-openclaw-vitest,token-rotation-vitest,launchable-smoke-vitest,openclaw-tui-chat-correlation-vitest,gateway-guard-recovery,double-onboard-vitest,issue-4434-tui-unreachable-inference-vitest"
args=(--emit-live-matrix)
matrix=""
hermes_selected=false
registry_scenarios=()
free_standing_scenarios=(openshell-version-pin onboard-negative-paths inference-routing runtime-overrides hermes-e2e network-policy rebuild-openclaw token-rotation openclaw-tui-chat-correlation double-onboard issue-4434-tui-unreachable-inference)
free_standing_scenarios=(openshell-version-pin onboard-negative-paths inference-routing runtime-overrides hermes-e2e hermes-root-entrypoint-smoke network-policy rebuild-openclaw token-rotation openclaw-tui-chat-correlation double-onboard issue-4434-tui-unreachable-inference)
is_free_standing_scenario() {
local id="$1"
local known
Expand Down Expand Up @@ -363,6 +363,48 @@ jobs:
if-no-files-found: ignore
retention-days: 14

hermes-root-entrypoint-smoke-vitest:
needs: [validate-jobs, generate-matrix]
if: ${{ needs.generate-matrix.result == 'success' && ((inputs.jobs == '' && inputs.scenarios == '') || contains(format(',{0},', inputs.jobs), ',hermes-root-entrypoint-smoke-vitest,') || contains(format(',{0},', inputs.scenarios), ',hermes-root-entrypoint-smoke,')) }}
runs-on: ubuntu-latest
Comment thread
coderabbitai[bot] marked this conversation as resolved.
timeout-minutes: 45
env:
E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts/vitest/hermes-root-entrypoint-smoke
NEMOCLAW_RUN_E2E_SCENARIOS: "1"
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false

- name: Set up Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.0.0
with:
node-version: 22
cache: npm

- name: Install root dependencies
run: npm ci --ignore-scripts

- name: Run Hermes root entrypoint smoke live test
# Migrated from test/e2e/test-hermes-root-entrypoint-smoke.sh. This
# builds the real Hermes image unless NEMOCLAW_HERMES_TEST_IMAGE points
# at a prebuilt image, then probes the root entrypoint via Docker.
run: |
set -euo pipefail
npx vitest run --project e2e-scenarios-live \
test/e2e-scenario/live/hermes-root-entrypoint-smoke.test.ts \
--silent=false --reporter=default

- name: Upload Hermes root entrypoint smoke artifacts
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: e2e-vitest-scenarios-hermes-root-entrypoint-smoke
path: e2e-artifacts/vitest/hermes-root-entrypoint-smoke/
include-hidden-files: false
if-no-files-found: ignore
retention-days: 14

inference-routing-vitest:
needs: [validate-jobs, generate-matrix]
if: ${{ (inputs.jobs == '' && inputs.scenarios == '') || contains(format(',{0},', inputs.jobs), ',inference-routing-vitest,') || contains(format(',{0},', inputs.scenarios), ',inference-routing,') }}
Expand Down Expand Up @@ -1262,6 +1304,7 @@ jobs:
credential-migration-vitest,
runtime-overrides-vitest,
hermes-e2e-vitest,
hermes-root-entrypoint-smoke-vitest,
network-policy-vitest,
rebuild-openclaw-vitest,
token-rotation-vitest,
Expand Down
43 changes: 36 additions & 7 deletions .github/workflows/sandbox-images-and-e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false

- name: Resolve sandbox base image
uses: ./.github/actions/resolve-sandbox-base-image
Expand Down Expand Up @@ -59,10 +61,12 @@ jobs:

build-hermes-sandbox-image:
runs-on: ubuntu-latest
timeout-minutes: 15
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false

- name: Resolve Hermes base image
uses: ./.github/actions/resolve-hermes-base-image
Expand Down Expand Up @@ -97,18 +101,35 @@ jobs:
path: /tmp/nemoclaw-hermes-sandbox-secret-boundary.log
if-no-files-found: ignore

- name: Run Hermes root entrypoint smoke
- name: Set up Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.0.0
with:
node-version: 22
cache: npm

- name: Install root dependencies
run: npm ci --ignore-scripts

- name: Run Hermes root entrypoint smoke Vitest test
env:
E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts/vitest/hermes-root-entrypoint-smoke
NEMOCLAW_HERMES_TEST_IMAGE: nemoclaw-hermes-production
run: bash test/e2e/test-hermes-root-entrypoint-smoke.sh
NEMOCLAW_RUN_E2E_SCENARIOS: "1"
run: |
set -euo pipefail
npx vitest run --project e2e-scenarios-live \
test/e2e-scenario/live/hermes-root-entrypoint-smoke.test.ts \
--silent=false --reporter=default

- name: Upload Hermes root entrypoint smoke log on failure
if: failure()
- name: Upload Hermes root entrypoint smoke artifacts
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: hermes-root-entrypoint-smoke-log
path: /tmp/nemoclaw-hermes-root-entrypoint-smoke.log
name: hermes-root-entrypoint-smoke-artifacts
path: e2e-artifacts/vitest/hermes-root-entrypoint-smoke/
include-hidden-files: false
if-no-files-found: ignore
retention-days: 14

build-sandbox-images-arm64:
if: inputs.run_arm64
Expand All @@ -117,6 +138,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false

- name: Resolve sandbox base image
uses: ./.github/actions/resolve-sandbox-base-image
Expand All @@ -134,6 +157,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false

- name: Download image artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
Expand All @@ -154,6 +179,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false

- name: Download image artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
Expand All @@ -174,6 +201,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false

- name: Download image artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
Expand Down
142 changes: 142 additions & 0 deletions test/e2e-scenario/fixtures/docker-probe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import {
spawnSync,
type SpawnSyncOptionsWithStringEncoding,
type SpawnSyncReturns,
} from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";

import type { ArtifactSink } from "./artifacts.ts";
import { buildChildEnv } from "./redaction.ts";
import type { SecretStore } from "./secrets.ts";

export type DockerCommandResult = {
command: string[];
exitCode: number | null;
signal: NodeJS.Signals | null;
stdout: string;
stderr: string;
error?: string;
};

export type DockerProbeRunner = (
command: string,
args: string[],
options: SpawnSyncOptionsWithStringEncoding,
) => SpawnSyncReturns<string>;

const DOCKER_ENV_ALLOWLIST = [
"DOCKER_HOST",
"DOCKER_CONTEXT",
"DOCKER_TLS_VERIFY",
"DOCKER_CERT_PATH",
"XDG_RUNTIME_DIR",
] as const;

function safeName(value: string): string {
return (
value
.trim()
.toLowerCase()
.replace(/[^a-z0-9._-]+/g, "-")
.replace(/^-+|-+$/g, "") || "docker"
);
}

export function buildDockerProbeEnv(
base: NodeJS.ProcessEnv,
dockerConfigDir: string,
): NodeJS.ProcessEnv {
return buildChildEnv(base, {
additionalAllowedEnv: DOCKER_ENV_ALLOWLIST,
fixtureOverlay: {
DOCKER_CONFIG: dockerConfigDir,
},
});
}

export function redactDockerProbeResult(
result: DockerCommandResult,
redact: SecretStore["redact"],
): DockerCommandResult {
return {
command: result.command.map((part) => redact(part)),
exitCode: result.exitCode,
signal: result.signal,
stdout: redact(result.stdout),
stderr: redact(result.stderr),
error: result.error ? redact(result.error) : undefined,
};
}

export function resultText(result: DockerCommandResult): string {
return [
`$ ${result.command.join(" ")}`,
result.stdout.trim(),
result.stderr.trim(),
result.error ? `error: ${result.error}` : "",
]
.filter(Boolean)
.join("\n");
}

export class DockerProbe {
private sequence = 0;
private readonly dockerConfigDir: string;

constructor(
private readonly artifacts: ArtifactSink,
private readonly redact: SecretStore["redact"],
private readonly runDocker: DockerProbeRunner = spawnSync,
) {
this.dockerConfigDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-docker-config-"));
}

async run(
args: string[],
options: { artifactName: string; timeoutMs?: number } = { artifactName: "docker" },
): Promise<DockerCommandResult> {
fs.mkdirSync(this.dockerConfigDir, { recursive: true });
const command = ["docker", ...args];
const result = this.runDocker("docker", args, {
cwd: path.resolve(import.meta.dirname, "../../.."),
encoding: "utf8",
env: buildDockerProbeEnv(process.env, this.dockerConfigDir),
maxBuffer: 10 * 1024 * 1024,
timeout: options.timeoutMs ?? 30_000,
});
const commandResult = redactDockerProbeResult(
{
command,
exitCode: result.status,
signal: result.signal,
stdout: result.stdout ?? "",
stderr: result.stderr ?? "",
error: result.error instanceof Error ? result.error.message : undefined,
},
this.redact,
);
const artifactBase = `docker/${String(++this.sequence).padStart(3, "0")}-${safeName(
options.artifactName,
)}`;
await this.artifacts.writeText(`${artifactBase}.stdout.txt`, commandResult.stdout);
await this.artifacts.writeText(`${artifactBase}.stderr.txt`, commandResult.stderr);
await this.artifacts.writeJson(`${artifactBase}.result.json`, commandResult);
return commandResult;
}

async expect(
args: string[],
options: { artifactName: string; timeoutMs?: number },
): Promise<DockerCommandResult> {
const result = await this.run(args, options);
if (result.exitCode !== 0) {
throw new Error(resultText(result));
}
return result;
}
}
Loading
Loading