Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
7d85bc5
ci: add Flavor 1 startup script for CI-Ready CPU launchable (#1327)
jyaunches Apr 2, 2026
99f4d7e
ci: add launchable support to Brev E2E test harness (#1327)
jyaunches Apr 2, 2026
03007f5
ci: set Flavor 1 launchable ID (env-3BoRsC1YMHNLmu82xvIike1Nh6E)
jyaunches Apr 2, 2026
ad12a34
fix(ci): use --setup-script instead of --env for brev start (#1327)
jyaunches Apr 2, 2026
81ab345
fix(ci): use brev create --startup-script for v0.6.322 compatibility …
jyaunches Apr 2, 2026
93b10eb
fix(ci): delete leftover instance before creating to avoid duplicate …
jyaunches Apr 2, 2026
e5ddf47
fix(ci): handle brev create unexpected EOF by checking if instance ex…
jyaunches Apr 2, 2026
998eab9
fix(ci): sudo npm link, sg docker, and Docker socket permissions (#1327)
jyaunches Apr 2, 2026
72a7a86
fix(ci): remove sg docker wrappers that cause onboard process tree hang
jyaunches Apr 2, 2026
ab06775
ci: resolve branch from PR number in e2e-brev workflow
jyaunches Apr 2, 2026
363ffbc
fix(ci): fully detach onboard background process to prevent SSH timeout
jyaunches Apr 2, 2026
d6dee31
fix(ci): use printf instead of heredoc for registry write over SSH
jyaunches Apr 2, 2026
0c35022
fix(ci): wrap pkill in try/catch — it kills its own SSH connection
jyaunches Apr 2, 2026
aa27899
ci: re-enable repository guard for e2e-brev workflow
jyaunches Apr 2, 2026
25026f0
fix(ci): address CodeRabbit review feedback (#1362)
jyaunches Apr 2, 2026
741111a
fix(ci): chown repo dir after sudo npm link to fix permission errors
jyaunches Apr 3, 2026
d807c0f
ci: simplify workflow form — remove branch and setup_script_url inputs
jyaunches Apr 3, 2026
d9172d5
Merge branch 'main' into feat/brev-ci-launchables
ericksoa Apr 3, 2026
03f2ab0
ci: temporarily disable repo guard for fork testing (re-enable before…
jyaunches Apr 3, 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
42 changes: 32 additions & 10 deletions .github/workflows/e2e-brev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,8 @@ name: e2e-brev
on:
workflow_dispatch:
inputs:
branch:
description: "Branch to test"
required: true
default: "main"
pr_number:
description: "PR number (for status reporting, optional)"
description: "PR number (resolves branch automatically)"
required: false
default: ""
test_suite:
Expand All @@ -46,11 +42,16 @@ on:
- credential-sanitization
- telegram-injection
- all
use_launchable:
description: "Use CI launchable (true) or bare brev create + brev-setup.sh (false)"
required: false
type: boolean
default: true
keep_alive:
description: "Keep Brev instance alive after tests (for SSH debugging)"
required: false
type: boolean
default: true
default: false
brev_token:
description: "Brev refresh token (overrides BREV_API_TOKEN secret if provided)"
required: false
Expand All @@ -68,6 +69,14 @@ on:
required: false
type: string
default: "full"
use_launchable:
required: false
type: boolean
default: true
setup_script_url:
required: false
type: string
default: ""
keep_alive:
required: false
type: boolean
Expand All @@ -89,14 +98,23 @@ concurrency:

jobs:
e2e-brev:
if: github.repository == 'NVIDIA/NemoClaw'
# if: github.repository == 'NVIDIA/NemoClaw' # Disabled for fork testing — re-enable before merge
runs-on: ubuntu-latest
timeout-minutes: 90
steps:
- name: Resolve branch from PR number
if: inputs.pr_number != ''
env:
GH_TOKEN: ${{ github.token }}
run: |
BRANCH=$(gh pr view ${{ inputs.pr_number }} --repo ${{ github.repository }} --json headRefName -q .headRefName)
echo "Resolved PR #${{ inputs.pr_number }} → branch: $BRANCH"
echo "RESOLVED_BRANCH=$BRANCH" >> "$GITHUB_ENV"

- name: Checkout target branch
uses: actions/checkout@v6
with:
ref: ${{ inputs.branch }}
ref: ${{ env.RESOLVED_BRANCH || inputs.branch || 'main' }}

- name: Create check run (pending)
if: inputs.pr_number != ''
Expand All @@ -122,7 +140,8 @@ jobs:

- name: Install Brev CLI
run: |
# Use latest Brev CLI (v0.6.322+) — CPU instances require `brev search cpu | brev create`
# Brev CLI v0.6.322+ — CPU instances use `brev search cpu | brev create`
# Startup scripts use `brev create --startup-script @file` (not brev start --cpu)
curl -fsSL -o /tmp/brev.tar.gz "https://github.com/brevdev/brev-cli/releases/download/v0.6.322/brev-cli_0.6.322_linux_amd64.tar.gz"
tar -xzf /tmp/brev.tar.gz -C /usr/local/bin brev
chmod +x /usr/local/bin/brev
Expand All @@ -137,6 +156,8 @@ jobs:
GITHUB_TOKEN: ${{ github.token }}
INSTANCE_NAME: e2e-pr-${{ inputs.pr_number || github.run_id }}
TEST_SUITE: ${{ inputs.test_suite }}
USE_LAUNCHABLE: ${{ inputs.use_launchable && '1' || '0' }}
LAUNCHABLE_SETUP_SCRIPT: ${{ inputs.setup_script_url || '' }}
KEEP_ALIVE: ${{ inputs.keep_alive }}
run: npx vitest run --project e2e-brev --reporter=verbose

Expand Down Expand Up @@ -166,7 +187,8 @@ jobs:
STATUS="FAILED"
fi
INSTANCE="e2e-pr-${{ inputs.pr_number || github.run_id }}"
BODY="${EMOJI} **Brev E2E** (${{ inputs.test_suite }}): **${STATUS}** on branch \`${{ inputs.branch }}\` — [See logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
BRANCH="${RESOLVED_BRANCH:-${{ inputs.branch || 'main' }}}"
BODY="${EMOJI} **Brev E2E** (${{ inputs.test_suite }}): **${STATUS}** on branch \`${BRANCH}\` — [See logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
if [ "${{ inputs.keep_alive }}" = "true" ]; then
BODY="${BODY}

Expand Down
251 changes: 251 additions & 0 deletions scripts/brev-launchable-ci-cpu.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
#!/usr/bin/env bash
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Brev launchable startup script — CI-Ready CPU
#
# Pre-bakes a VM with everything needed for NemoClaw E2E tests so that
# CI runs only need to: rsync branch code → npm ci → nemoclaw onboard → test.
#
# What this installs:
# 1. Docker (docker.io) — enabled and running
# 2. Node.js 22 (nodesource)
# 3. OpenShell CLI binary (pinned release)
# 4. NemoClaw repo cloned with npm deps installed and TS plugin built
# 5. Docker images pre-pulled (sandbox-base, openshell/cluster, node:22-slim)
#
# What this does NOT install (intentionally):
# - code-server (not needed for automated CI)
# - VS Code themes/extensions
# - NVIDIA Container Toolkit (see brev-launchable-ci-gpu.sh for GPU flavor)
# - Ollama / vLLM
#
# Readiness detection:
# Writes /var/run/nemoclaw-launchable-ready when complete.
# Also writes "=== Ready ===" to /tmp/launch-plugin.log for backward compat.
#
# Usage (Brev launchable startup script — one-liner that curls this):
# curl -fsSL https://raw.githubusercontent.com/NVIDIA/NemoClaw/<ref>/scripts/brev-launchable-ci-cpu.sh | bash
#
# Environment overrides:
# OPENSHELL_VERSION — OpenShell CLI release tag (default: v0.0.20)
# NEMOCLAW_REF — NemoClaw git ref to clone (default: main)
# NEMOCLAW_CLONE_DIR — Where to clone NemoClaw (default: ~/NemoClaw)
# SKIP_DOCKER_PULL — Set to 1 to skip Docker image pre-pulls
#
# Related:
# - Epic: https://github.com/NVIDIA/NemoClaw/issues/1326
# - Issue: https://github.com/NVIDIA/NemoClaw/issues/1327

set -euo pipefail

# ── Configuration ────────────────────────────────────────────────────
OPENSHELL_VERSION="${OPENSHELL_VERSION:-v0.0.20}"
NEMOCLAW_REF="${NEMOCLAW_REF:-main}"
TARGET_USER="${SUDO_USER:-$(id -un)}"
TARGET_HOME="$(getent passwd "$TARGET_USER" | cut -d: -f6)"
NEMOCLAW_CLONE_DIR="${NEMOCLAW_CLONE_DIR:-${TARGET_HOME}/NemoClaw}"

LAUNCH_LOG="${LAUNCH_LOG:-/tmp/launch-plugin.log}"
SENTINEL="/var/run/nemoclaw-launchable-ready"

# Docker images to pre-pull. These are the expensive layers that cause
# timeouts when pulled during CI runs.
DOCKER_IMAGES=(
"ghcr.io/nvidia/nemoclaw/sandbox-base:latest"
"node:22-slim"
)

# ── Suppress apt noise ───────────────────────────────────────────────
export DEBIAN_FRONTEND=noninteractive
export NEEDRESTART_MODE=a

# ── Logging ──────────────────────────────────────────────────────────
mkdir -p "$(dirname "$LAUNCH_LOG")"
exec > >(tee -a "$LAUNCH_LOG") 2>&1

_ts() { date '+%H:%M:%S'; }
info() { printf '\033[0;32m[%s ci-cpu]\033[0m %s\n' "$(_ts)" "$1"; }
warn() { printf '\033[1;33m[%s ci-cpu]\033[0m %s\n' "$(_ts)" "$1"; }
fail() {
printf '\033[0;31m[%s ci-cpu]\033[0m %s\n' "$(_ts)" "$1"
exit 1
}

# ── Retry helper ─────────────────────────────────────────────────────
# Usage: retry 3 10 "description" command arg1 arg2
retry() {
local max_attempts="$1" sleep_sec="$2" desc="$3"
shift 3
local attempt=1
while true; do
if "$@"; then
return 0
fi
if ((attempt >= max_attempts)); then
warn "Failed after $max_attempts attempts: $desc"
return 1
fi
info "Retry $attempt/$max_attempts for: $desc (sleeping ${sleep_sec}s)"
sleep "$sleep_sec"
((attempt++))
done
}

# ── Wait for apt locks ───────────────────────────────────────────────
# Brev VMs sometimes have unattended-upgrades running at boot.
wait_for_apt_lock() {
local max_wait=120 elapsed=0
while fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1 \
|| fuser /var/lib/apt/lists/lock >/dev/null 2>&1; do
if ((elapsed >= max_wait)); then
warn "apt lock not released after ${max_wait}s — proceeding anyway"
return 0
fi
if ((elapsed % 15 == 0)); then
info "Waiting for apt lock to be released... (${elapsed}s)"
fi
sleep 5
((elapsed += 5))
done
}

# ══════════════════════════════════════════════════════════════════════
# 1. System packages
# ══════════════════════════════════════════════════════════════════════
info "Installing system packages..."
wait_for_apt_lock
retry 3 10 "apt-get update" sudo apt-get update -qq
retry 3 10 "apt-get install" sudo apt-get install -y -qq \
ca-certificates curl git jq tar >/dev/null 2>&1
info "System packages installed"

# ══════════════════════════════════════════════════════════════════════
# 2. Docker
# ══════════════════════════════════════════════════════════════════════
if command -v docker >/dev/null 2>&1; then
info "Docker already installed"
else
info "Installing Docker..."
wait_for_apt_lock
retry 3 10 "install docker" sudo apt-get install -y -qq docker.io >/dev/null 2>&1
info "Docker installed"
fi
sudo systemctl enable --now docker
sudo usermod -aG docker "$TARGET_USER" 2>/dev/null || true
# Make the socket world-accessible so SSH sessions (which don't pick up the
# new docker group until re-login) can use Docker immediately. This is a
# short-lived CI VM — socket security is not a concern.
sudo chmod 666 /var/run/docker.sock
info "Docker enabled ($(docker --version 2>/dev/null | head -c 40))"

# ══════════════════════════════════════════════════════════════════════
# 3. Node.js 22
# ══════════════════════════════════════════════════════════════════════
node_major=""
if command -v node >/dev/null 2>&1; then
node_major="$(node -p 'process.versions.node.split(".")[0]' 2>/dev/null || true)"
fi

if command -v npm >/dev/null 2>&1 && [[ -n "$node_major" ]] && ((node_major >= 22)); then
info "Node.js already installed: $(node --version)"
else
info "Installing Node.js 22..."
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - >/dev/null 2>&1
wait_for_apt_lock
retry 3 10 "install nodejs" sudo apt-get install -y -qq nodejs >/dev/null 2>&1
info "Node.js $(node --version) installed"
fi

# ══════════════════════════════════════════════════════════════════════
# 4. OpenShell CLI
# ══════════════════════════════════════════════════════════════════════
if command -v openshell >/dev/null 2>&1; then
info "OpenShell CLI already installed: $(openshell --version 2>&1 || echo unknown)"
else
info "Installing OpenShell CLI ${OPENSHELL_VERSION}..."
ARCH="$(uname -m)"
case "$ARCH" in
x86_64 | amd64) ASSET="openshell-x86_64-unknown-linux-musl.tar.gz" ;;
aarch64 | arm64) ASSET="openshell-aarch64-unknown-linux-musl.tar.gz" ;;
*) fail "Unsupported architecture: $ARCH" ;;
esac
tmpdir="$(mktemp -d)"
retry 3 10 "download openshell" \
curl -fsSL -o "$tmpdir/$ASSET" \
"https://github.com/NVIDIA/OpenShell/releases/download/${OPENSHELL_VERSION}/${ASSET}"
tar xzf "$tmpdir/$ASSET" -C "$tmpdir"
sudo install -m 755 "$tmpdir/openshell" /usr/local/bin/openshell
rm -rf "$tmpdir"
info "OpenShell CLI installed: $(openshell --version 2>&1 || echo unknown)"
fi

# ══════════════════════════════════════════════════════════════════════
# 5. Clone NemoClaw and install deps
# ══════════════════════════════════════════════════════════════════════
if [[ -d "$NEMOCLAW_CLONE_DIR/.git" ]]; then
info "NemoClaw repo exists at $NEMOCLAW_CLONE_DIR — refreshing"
git -C "$NEMOCLAW_CLONE_DIR" fetch origin "$NEMOCLAW_REF"
git -C "$NEMOCLAW_CLONE_DIR" checkout "$NEMOCLAW_REF"
git -C "$NEMOCLAW_CLONE_DIR" pull --ff-only origin "$NEMOCLAW_REF" || true
else
info "Cloning NemoClaw (ref: $NEMOCLAW_REF)..."
git clone --branch "$NEMOCLAW_REF" --depth 1 \
"https://github.com/NVIDIA/NemoClaw.git" "$NEMOCLAW_CLONE_DIR"
fi

info "Installing npm dependencies..."
cd "$NEMOCLAW_CLONE_DIR"
npm install --ignore-scripts 2>&1 | tail -3
info "Root deps installed"

info "Building TypeScript plugin..."
cd "$NEMOCLAW_CLONE_DIR/nemoclaw"
npm install 2>&1 | tail -3
npm run build 2>&1 | tail -3
cd "$NEMOCLAW_CLONE_DIR"
info "Plugin built"

# ══════════════════════════════════════════════════════════════════════
# 6. Pre-pull Docker images
# ══════════════════════════════════════════════════════════════════════
if [[ "${SKIP_DOCKER_PULL:-0}" == "1" ]]; then
info "Skipping Docker image pre-pulls (SKIP_DOCKER_PULL=1)"
else
info "Pre-pulling Docker images (this saves 3-5 min per CI run)..."

# Use sg docker to ensure docker group is active without re-login
for image in "${DOCKER_IMAGES[@]}"; do
info " Pulling $image..."
sg docker -c "docker pull $image" 2>&1 | tail -1 \
|| warn " Failed to pull $image (will be pulled at test time)"
done

# The openshell/cluster image tag should match the CLI version.
# Try the pinned version first, fall back to latest.
CLUSTER_TAG="${OPENSHELL_VERSION#v}" # v0.0.20 → 0.0.20
CLUSTER_IMAGE="ghcr.io/nvidia/openshell/cluster:${CLUSTER_TAG}"
info " Pulling $CLUSTER_IMAGE..."
if ! sg docker -c "docker pull $CLUSTER_IMAGE" 2>&1 | tail -1; then
warn " Could not pull $CLUSTER_IMAGE — trying :latest"
sg docker -c "docker pull ghcr.io/nvidia/openshell/cluster:latest" 2>&1 | tail -1 \
|| warn " Failed to pull openshell/cluster (will be pulled at test time)"
fi

info "Docker images pre-pulled"
fi

# ══════════════════════════════════════════════════════════════════════
# 7. Readiness sentinel
# ══════════════════════════════════════════════════════════════════════
sudo touch "$SENTINEL"
echo "=== Ready ===" | sudo tee -a "$LAUNCH_LOG" >/dev/null

info "════════════════════════════════════════════════════"
info " CI-Ready CPU launchable setup complete"
info " NemoClaw: $NEMOCLAW_CLONE_DIR (ref: $NEMOCLAW_REF)"
info " OpenShell: $(openshell --version 2>&1 || echo unknown)"
info " Node.js: $(node --version)"
info " Docker: $(docker --version 2>/dev/null | head -c 40)"
info " Sentinel: $SENTINEL"
info "════════════════════════════════════════════════════"
Loading
Loading