From 03780c3138db072a98bf43e09f9d8c8e67ccf98d Mon Sep 17 00:00:00 2001 From: Julie Yaunches Date: Mon, 30 Mar 2026 08:56:53 -0400 Subject: [PATCH 1/2] fix: improve setup.sh and brev-setup.sh for CI and non-interactive use setup.sh: - Add timestamped log output ([HH:MM:SS] prefix) for CI visibility - Support NEMOCLAW_SANDBOX_NAME env var for custom sandbox names - Add --no-tty flag to openshell sandbox create for non-interactive mode - Pre-build base image locally when GHCR image is unavailable (forks) - Add background progress reporter during sandbox build (heartbeat every 30s with Docker step info, filters secrets from output) - Show build elapsed time on completion brev-setup.sh: - Add timestamped log output matching setup.sh format - Support SKIP_VLLM=1 to skip vLLM install on CPU-only instances --- scripts/brev-setup.sh | 11 +++++---- scripts/setup.sh | 52 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 55 insertions(+), 8 deletions(-) diff --git a/scripts/brev-setup.sh b/scripts/brev-setup.sh index cc8701ba9..1d1367086 100755 --- a/scripts/brev-setup.sh +++ b/scripts/brev-setup.sh @@ -21,10 +21,11 @@ GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' -info() { echo -e "${GREEN}[brev]${NC} $1"; } -warn() { echo -e "${YELLOW}[brev]${NC} $1"; } +_ts() { date '+%H:%M:%S'; } +info() { echo -e "${GREEN}[$(_ts) brev]${NC} $1"; } +warn() { echo -e "${YELLOW}[$(_ts) brev]${NC} $1"; } fail() { - echo -e "${RED}[brev]${NC} $1" + echo -e "${RED}[$(_ts) brev]${NC} $1" exit 1 } @@ -120,7 +121,9 @@ fi # --- 4. vLLM (local inference, if GPU present) --- VLLM_MODEL="nvidia/nemotron-3-nano-30b-a3b" -if command -v nvidia-smi >/dev/null 2>&1; then +if [ "${SKIP_VLLM:-}" = "1" ]; then + info "Skipping vLLM install (SKIP_VLLM=1)" +elif command -v nvidia-smi >/dev/null 2>&1; then if ! python3 -c "import vllm" 2>/dev/null; then info "Installing vLLM..." if ! command -v pip3 >/dev/null 2>&1; then diff --git a/scripts/setup.sh b/scripts/setup.sh index 81bd7a2c2..b8df313a7 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -34,10 +34,11 @@ REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" # shellcheck source=./lib/runtime.sh . "$SCRIPT_DIR/lib/runtime.sh" -info() { echo -e "${GREEN}>>>${NC} $1"; } -warn() { echo -e "${YELLOW}>>>${NC} $1"; } +_ts() { date '+%H:%M:%S'; } +info() { echo -e "${GREEN}[$(_ts)]${NC} $1"; } +warn() { echo -e "${YELLOW}[$(_ts)]${NC} $1"; } fail() { - echo -e "${RED}>>>${NC} $1" + echo -e "${RED}[$(_ts)]${NC} $1" exit 1 } @@ -92,7 +93,7 @@ fi if [ "$CONTAINER_RUNTIME" != "unknown" ]; then info "Container runtime: $CONTAINER_RUNTIME" fi -SANDBOX_NAME="${1:-nemoclaw}" +SANDBOX_NAME="${1:-${NEMOCLAW_SANDBOX_NAME:-nemoclaw}}" info "Using sandbox name: ${SANDBOX_NAME}" OPEN_SHELL_VERSION_RAW="$(openshell -V 2>/dev/null || true)" @@ -192,6 +193,20 @@ openshell inference set --no-verify --provider nvidia-nim --model nvidia/nemotro info "Deleting old ${SANDBOX_NAME} sandbox (if any)..." openshell sandbox delete "$SANDBOX_NAME" >/dev/null 2>&1 || true +# Pre-build the base image if it's not available (GHCR image may not exist on +# forks or before the first base-image workflow run). This ensures the +# Dockerfile's `FROM ${BASE_IMAGE}` can resolve locally. +BASE_IMAGE="${BASE_IMAGE:-ghcr.io/nvidia/nemoclaw/sandbox-base:latest}" +if ! docker image inspect "$BASE_IMAGE" >/dev/null 2>&1 && ! docker pull "$BASE_IMAGE" 2>/dev/null; then + if [ -f "$REPO_DIR/Dockerfile.base" ]; then + info "Base image not in registry — building Dockerfile.base locally..." + docker build -f "$REPO_DIR/Dockerfile.base" -t "$BASE_IMAGE" "$REPO_DIR" 2>&1 | tail -5 + info "Local base image built" + else + warn "Dockerfile.base not found — sandbox build may fall back to full rebuild" + fi +fi + info "Building and creating NemoClaw sandbox (this takes a few minutes on first run)..." # Stage a clean build context (openshell doesn't honor .dockerignore) @@ -206,14 +221,43 @@ bash "$BUILD_CTX/scripts/clean-staged-tree.sh" "$BUILD_CTX/nemoclaw-blueprint" 2 # Capture full output to a temp file so we can filter for display but still # detect failures. The raw log is kept on failure for debugging. CREATE_LOG=$(mktemp /tmp/nemoclaw-create-XXXXXX.log) +SANDBOX_BUILD_START=$(date +%s) + +# Background progress reporter: tails the log for Docker build steps and +# prints a heartbeat every 30s so CI (and humans) can see what's happening. +( + while true; do + sleep 30 + if [ ! -f "$CREATE_LOG" ]; then break; fi + ELAPSED=$(($(date +%s) - SANDBOX_BUILD_START)) + LAST_STEP=$(grep -oE "^Step [0-9]+/[0-9]+" "$CREATE_LOG" 2>/dev/null | tail -1 || true) + LAST_LINE=$(tail -1 "$CREATE_LOG" 2>/dev/null | head -c 120 || true) + # Filter out lines that might contain secrets + if echo "$LAST_LINE" | grep -qi "API_KEY\|TOKEN\|SECRET\|CREDENTIAL"; then + LAST_LINE="[filtered]" + fi + echo -e "${GREEN}[$(_ts)]${NC} ⏳ Sandbox build ${ELAPSED}s elapsed${LAST_STEP:+ — $LAST_STEP}${LAST_LINE:+ — $LAST_LINE}" + done +) & +PROGRESS_PID=$! + set +e # NVIDIA_API_KEY is NOT passed into the sandbox. Inference is proxied through # the OpenShell gateway which injects the stored credential server-side. openshell sandbox create --from "$BUILD_CTX/Dockerfile" --name "$SANDBOX_NAME" \ --provider nvidia-nim \ + --no-tty -- true \ >"$CREATE_LOG" 2>&1 CREATE_RC=$? set -e + +# Stop progress reporter +kill "$PROGRESS_PID" 2>/dev/null || true +wait "$PROGRESS_PID" 2>/dev/null || true + +SANDBOX_BUILD_ELAPSED=$(($(date +%s) - SANDBOX_BUILD_START)) +info "Sandbox build finished in ${SANDBOX_BUILD_ELAPSED}s (exit code: $CREATE_RC)" + rm -rf "$BUILD_CTX" # Show progress lines (filter apt noise and env var dumps that contain NVIDIA_API_KEY) From 673554d3a4868aab30aeae9a8e159d4cd75fffcf Mon Sep 17 00:00:00 2001 From: Julie Yaunches Date: Mon, 30 Mar 2026 09:22:04 -0400 Subject: [PATCH 2/2] fix(test): update sandbox name test for NEMOCLAW_SANDBOX_NAME env var fallback --- test/setup-sandbox-name.test.js | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/test/setup-sandbox-name.test.js b/test/setup-sandbox-name.test.js index d9ace4ead..f3e122d7f 100644 --- a/test/setup-sandbox-name.test.js +++ b/test/setup-sandbox-name.test.js @@ -16,8 +16,9 @@ const ROOT = path.resolve(import.meta.dirname, ".."); describe("setup.sh sandbox name parameterization (#197)", () => { const content = fs.readFileSync(path.join(ROOT, "scripts/setup.sh"), "utf-8"); - it("accepts sandbox name as $1 with default", () => { - expect(content.includes('SANDBOX_NAME="${1:-nemoclaw}"')).toBeTruthy(); + it("accepts sandbox name as $1 with env var fallback and default", () => { + // $1 takes priority, then NEMOCLAW_SANDBOX_NAME env var, then "nemoclaw" + expect(content.includes('SANDBOX_NAME="${1:-${NEMOCLAW_SANDBOX_NAME:-nemoclaw}}"')).toBeTruthy(); }); it("sandbox create uses $SANDBOX_NAME, not hardcoded", () => { @@ -51,16 +52,24 @@ describe("setup.sh sandbox name parameterization (#197)", () => { it("$1 arg actually sets SANDBOX_NAME in bash", () => { const result = execSync( - 'bash -c \'SANDBOX_NAME="${1:-nemoclaw}"; echo "$SANDBOX_NAME"\' -- my-test-box', + 'bash -c \'SANDBOX_NAME="${1:-${NEMOCLAW_SANDBOX_NAME:-nemoclaw}}"; echo "$SANDBOX_NAME"\' -- my-test-box', { encoding: "utf-8" } ).trim(); expect(result).toBe("my-test-box"); }); - it("no arg defaults to nemoclaw in bash", () => { + it("NEMOCLAW_SANDBOX_NAME env var is used when no $1 arg", () => { const result = execSync( - 'bash -c \'SANDBOX_NAME="${1:-nemoclaw}"; echo "$SANDBOX_NAME"\'', - { encoding: "utf-8" } + 'bash -c \'SANDBOX_NAME="${1:-${NEMOCLAW_SANDBOX_NAME:-nemoclaw}}"; echo "$SANDBOX_NAME"\'', + { encoding: "utf-8", env: { ...process.env, NEMOCLAW_SANDBOX_NAME: "e2e-test" } } + ).trim(); + expect(result).toBe("e2e-test"); + }); + + it("no arg and no env var defaults to nemoclaw in bash", () => { + const result = execSync( + 'bash -c \'SANDBOX_NAME="${1:-${NEMOCLAW_SANDBOX_NAME:-nemoclaw}}"; echo "$SANDBOX_NAME"\'', + { encoding: "utf-8", env: { PATH: process.env.PATH } } ).trim(); expect(result).toBe("nemoclaw"); });