NVIDIA/NemoClaw
9 workflows · maturity 33% · 5 patterns · GitHub ↗
Practices
○ Matrix✓ Permissions○ Security scan○ AI review○ Cache✓ Concurrency○ Reusable workflows
Detected patterns
Security dimensions
Workflows (9)
base-image perms .github/workflows/base-image.yaml
View raw YAML
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
# Build and push the sandbox base image to GHCR.
#
# Triggers:
# - Push to main when Dockerfile.base changes (new apt pkgs, openclaw bump, etc.)
# - Manual dispatch for ad-hoc rebuilds
#
# The base image contains the expensive, rarely-changing layers (apt, gosu,
# user setup, openclaw CLI). The production Dockerfile layers PR-specific
# code on top via: FROM ghcr.io/nvidia/nemoclaw/sandbox-base:<tag>
name: base-image
on:
push:
branches: [main]
paths:
- "Dockerfile.base"
workflow_dispatch:
permissions:
contents: read
packages: write
concurrency:
group: base-image
cancel-in-progress: true
env:
REGISTRY: ghcr.io
IMAGE_NAME: nvidia/nemoclaw/sandbox-base
jobs:
build-and-push:
if: github.repository == 'NVIDIA/NemoClaw'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Set up QEMU (arm64 emulation)
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=latest
type=sha,prefix=,format=short
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile.base
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
commit-lint perms .github/workflows/commit-lint.yaml
View raw YAML
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
name: commit-lint
on:
pull_request:
types: [opened, synchronize, reopened, edited]
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
commit-lint:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: "22"
cache: npm
- name: Install dependencies
run: npm install --ignore-scripts
- name: Lint PR title
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: printf '%s\n' "$PR_TITLE" | npx commitlint --verbose
docker-pin-check perms .github/workflows/docker-pin-check.yaml
View raw YAML
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Weekly check that the pinned Dockerfile base-image digest is still current.
# Fails with an actionable message when a newer node:22-slim is available.
name: docker-pin-check
on:
schedule:
# Every Monday at 09:00 UTC
- cron: "0 9 * * 1"
workflow_dispatch:
permissions:
contents: read
jobs:
check-pin:
if: github.repository == 'NVIDIA/NemoClaw'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Check Dockerfile base-image pin
run: |
bash scripts/update-docker-pin.sh --check
DOCKERFILE=Dockerfile.base bash scripts/update-docker-pin.sh --check
docs-preview-deploy perms .github/workflows/docs-preview-deploy.yaml
View raw YAML
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
# Step 2 of 2: Deploy (or clean up) the docs preview built by docs-preview-pr.yaml.
# Runs via workflow_run so it always has write access to gh-pages.
# Only deploys previews for same-repo PRs to prevent fork PRs from
# publishing arbitrary content to the GitHub Pages domain.
name: Docs PR Preview Deploy
on:
workflow_run:
workflows: ["Docs PR Preview"]
types: [completed]
permissions:
contents: write
pull-requests: write
actions: read
jobs:
deploy:
if: github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
# Checkout first so subsequent artifact downloads are not clobbered.
- name: Checkout repository
uses: actions/checkout@v6
- name: Download PR metadata
uses: actions/download-artifact@v8
with:
name: docs-preview-metadata
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Read PR metadata
id: meta
run: |
echo "pr-number=$(cat pr-number.txt)" >> "$GITHUB_OUTPUT"
if [ -f pr-action.txt ]; then
echo "action=$(cat pr-action.txt)" >> "$GITHUB_OUTPUT"
else
echo "action=deploy" >> "$GITHUB_OUTPUT"
fi
if [ -f same-repo.txt ]; then
echo "same-repo=$(cat same-repo.txt)" >> "$GITHUB_OUTPUT"
else
echo "same-repo=false" >> "$GITHUB_OUTPUT"
fi
- name: Skip fork PRs
if: steps.meta.outputs.action != 'closed' && steps.meta.outputs.same-repo != 'true'
run: |
echo "::notice::Skipping preview deploy for fork PR #${{ steps.meta.outputs.pr-number }}"
exit 0
- name: Download docs artifact
if: steps.meta.outputs.action != 'closed' && steps.meta.outputs.same-repo == 'true'
uses: actions/download-artifact@v8
with:
name: docs-preview
path: docs-preview-html/
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Deploy preview
if: steps.meta.outputs.action != 'closed' && steps.meta.outputs.same-repo == 'true'
uses: rossjrw/pr-preview-action@ffa7509e91a3ec8dfc2e5536c4d5c1acdf7a6de9 # v1
with:
source-dir: ./docs-preview-html/
preview-branch: gh-pages
umbrella-dir: pr-preview
action: deploy
pr-number: ${{ steps.meta.outputs.pr-number }}
comment: false
- name: Post preview comment
if: steps.meta.outputs.action != 'closed' && steps.meta.outputs.same-repo == 'true'
uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2
with:
header: pr-preview
number: ${{ steps.meta.outputs.pr-number }}
message: |
:rocket: **Docs preview ready!**
https://NVIDIA.github.io/NemoClaw/pr-preview/pr-${{ steps.meta.outputs.pr-number }}/
- name: Remove preview
if: steps.meta.outputs.action == 'closed'
uses: rossjrw/pr-preview-action@ffa7509e91a3ec8dfc2e5536c4d5c1acdf7a6de9 # v1
with:
preview-branch: gh-pages
umbrella-dir: pr-preview
action: remove
pr-number: ${{ steps.meta.outputs.pr-number }}
comment: false
- name: Remove preview comment
if: steps.meta.outputs.action == 'closed'
uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2
with:
header: pr-preview
number: ${{ steps.meta.outputs.pr-number }}
delete: true
docs-preview-pr perms .github/workflows/docs-preview-pr.yaml
View raw YAML
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
# Step 1 of 2: Build docs on the PR and upload as an artifact.
# Step 2 (deploy) runs in docs-preview-deploy.yaml via workflow_run,
# which has write access to gh-pages regardless of PR origin.
name: Docs PR Preview
on:
pull_request:
branches: [main]
types: [opened, reopened, synchronize, closed]
paths:
- "docs/**"
- "README.md"
- "pyproject.toml"
- "uv.lock"
- ".github/workflows/docs-preview-pr.yaml"
- ".github/workflows/docs-preview-deploy.yaml"
concurrency:
group: preview-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
build:
if: github.event.action != 'closed'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.11"
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Install doc dependencies
run: uv sync --group docs
- name: Build documentation
run: uv run --group docs sphinx-build -W -b html docs docs/_build/html
- name: Clean build artifacts
run: |
find docs/_build -name .doctrees -prune -exec rm -rf {} \;
find docs/_build -name .buildinfo -exec rm {} \;
touch docs/_build/html/.nojekyll
- name: Upload preview artifact
uses: actions/upload-artifact@v7
with:
name: docs-preview
path: docs/_build/html/
retention-days: 3
- name: Save PR metadata
run: |
echo "${{ github.event.pull_request.number }}" > pr-number.txt
if [ "${{ github.event.pull_request.head.repo.full_name }}" = "${{ github.repository }}" ]; then
echo "true" > same-repo.txt
else
echo "false" > same-repo.txt
fi
- name: Upload PR metadata
uses: actions/upload-artifact@v7
with:
name: docs-preview-metadata
path: |
pr-number.txt
same-repo.txt
retention-days: 3
# On PR close, trigger the deploy workflow to clean up the preview.
close:
if: github.event.action == 'closed'
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- name: Save PR metadata
run: |
echo "${{ github.event.pull_request.number }}" > pr-number.txt
echo "closed" > pr-action.txt
- name: Upload PR metadata
uses: actions/upload-artifact@v7
with:
name: docs-preview-metadata
path: |
pr-number.txt
pr-action.txt
retention-days: 3
e2e-brev perms .github/workflows/e2e-brev.yaml
View raw YAML
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
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)"
required: false
default: ""
test_suite:
description: "Test suite to run"
required: true
default: "full"
type: choice
options:
- full
- credential-sanitization
- all
keep_alive:
description: "Keep Brev instance alive after tests (for SSH debugging)"
required: false
type: boolean
default: true
workflow_call:
inputs:
branch:
required: true
type: string
pr_number:
required: false
type: string
default: ""
test_suite:
required: false
type: string
default: "full"
keep_alive:
required: false
type: boolean
default: true
secrets:
BREV_API_TOKEN:
required: true
NVIDIA_API_KEY:
required: true
permissions:
contents: read
checks: write
pull-requests: write
concurrency:
group: e2e-brev-${{ inputs.pr_number || github.run_id }}
cancel-in-progress: true
jobs:
e2e-brev:
if: github.repository == 'NVIDIA/NemoClaw'
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- name: Checkout target branch
uses: actions/checkout@v6
with:
ref: ${{ inputs.branch }}
- name: Create check run (pending)
if: inputs.pr_number != ''
env:
GH_TOKEN: ${{ github.token }}
run: |
PR_SHA=$(gh pr view ${{ inputs.pr_number }} --json headRefOid -q .headRefOid)
CHECK_RUN_ID=$(gh api repos/${{ github.repository }}/check-runs \
-f name="Brev E2E (${{ inputs.test_suite }})" \
-f head_sha="$PR_SHA" \
-f status="in_progress" \
-f "output[title]=Running on ephemeral Brev instance" \
-f "output[summary]=Tests in progress..." \
--jq '.id')
echo "CHECK_RUN_ID=$CHECK_RUN_ID" >> "$GITHUB_ENV"
echo "PR_SHA=$PR_SHA" >> "$GITHUB_ENV"
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: "22"
cache: npm
- name: Install Brev CLI
run: |
# Pin to v0.6.310 — v0.6.322 removed --cpu flag and defaults to GPU instances
curl -fsSL -o /tmp/brev.tar.gz "https://github.com/brevdev/brev-cli/releases/download/v0.6.310/brev-cli_0.6.310_linux_amd64.tar.gz"
tar -xzf /tmp/brev.tar.gz -C /usr/local/bin brev
chmod +x /usr/local/bin/brev
- name: Install dependencies
run: npm install --ignore-scripts
- name: Run ephemeral Brev E2E
env:
BREV_API_TOKEN: ${{ secrets.BREV_API_TOKEN }}
NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }}
GITHUB_TOKEN: ${{ github.token }}
INSTANCE_NAME: e2e-pr-${{ inputs.pr_number || github.run_id }}
TEST_SUITE: ${{ inputs.test_suite }}
KEEP_ALIVE: ${{ inputs.keep_alive }}
run: npx vitest run --project e2e-brev --reporter=verbose
- name: Update check run (completed)
if: always() && inputs.pr_number != '' && env.CHECK_RUN_ID != ''
env:
GH_TOKEN: ${{ github.token }}
run: |
CONCLUSION=${{ job.status == 'success' && 'success' || 'failure' }}
gh api repos/${{ github.repository }}/check-runs/${{ env.CHECK_RUN_ID }} \
-X PATCH \
-f status="completed" \
-f conclusion="$CONCLUSION" \
-f "output[title]=Brev E2E (${{ inputs.test_suite }}): ${CONCLUSION}" \
-f "output[summary]=See workflow run for details."
- name: Post PR comment
if: always() && inputs.pr_number != ''
env:
GH_TOKEN: ${{ github.token }}
run: |
if [ "${{ job.status }}" = "success" ]; then
EMOJI="✅"
STATUS="PASSED"
else
EMOJI="❌"
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 }})"
if [ "${{ inputs.keep_alive }}" = "true" ]; then
BODY="${BODY}
> **Instance \`${INSTANCE}\` is still running.** To SSH in:
> \`\`\`
> brev refresh && ssh ${INSTANCE}
> \`\`\`
> When done, delete it: \`brev delete ${INSTANCE}\`"
fi
gh pr comment ${{ inputs.pr_number }} --repo ${{ github.repository }} --body "$BODY"
- name: Upload test logs
if: failure()
uses: actions/upload-artifact@v4
with:
name: e2e-brev-logs
path: /tmp/brev-e2e-*.log
if-no-files-found: ignore
nightly-e2e perms .github/workflows/nightly-e2e.yaml
View raw YAML
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Nightly E2E tests:
#
# cloud-e2e Cloud inference (NVIDIA Endpoint API) on ubuntu-latest.
# cloud-experimental-e2e Experimental cloud inference test.
# gpu-e2e Local Ollama inference on a GPU self-hosted runner.
# Controlled by the GPU_E2E_ENABLED repository variable.
# Set vars.GPU_E2E_ENABLED to "true" in repo settings to enable.
# notify-on-failure Auto-creates a GitHub issue when any E2E job fails.
#
# Runs directly on the runner (not inside Docker) because OpenShell bootstraps
# a K3s cluster inside a privileged Docker container — nesting would break networking.
#
# Requires NVIDIA_API_KEY repository secret (for cloud-e2e and cloud-experimental-e2e).
# Only runs on schedule and manual dispatch — never on PRs (secret protection).
name: nightly-e2e
on:
schedule:
- cron: "0 0 * * *"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: nightly-e2e
cancel-in-progress: true
jobs:
cloud-e2e:
if: github.repository == 'NVIDIA/NemoClaw'
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Run cloud E2E test
env:
NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }}
NEMOCLAW_NON_INTERACTIVE: "1"
NEMOCLAW_SANDBOX_NAME: "e2e-nightly"
NEMOCLAW_RECREATE_SANDBOX: "1"
GITHUB_TOKEN: ${{ github.token }}
run: bash test/e2e/test-full-e2e.sh
- name: Upload install log on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: install-log
path: /tmp/nemoclaw-e2e-install.log
if-no-files-found: ignore
cloud-experimental-e2e:
if: github.repository == 'NVIDIA/NemoClaw'
runs-on: ubuntu-latest
environment: NVIDIA_API_KEY
timeout-minutes: 45
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Run cloud-experimental E2E test
env:
NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }}
GITHUB_TOKEN: ${{ github.token }}
# Non-interactive install (expect-driven Phase 3 optional). Runner has no expect; Phase 5e TUI skips if expect is absent.
RUN_E2E_CLOUD_EXPERIMENTAL_INTERACTIVE_INSTALL: "0"
NEMOCLAW_NON_INTERACTIVE: "1"
NEMOCLAW_RECREATE_SANDBOX: "1"
NEMOCLAW_POLICY_MODE: "custom"
NEMOCLAW_POLICY_PRESETS: "npm,pypi"
run: bash test/e2e/test-e2e-cloud-experimental.sh
- name: Upload install log on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: install-log-cloud-experimental
path: /tmp/nemoclaw-e2e-install.log
if-no-files-found: ignore
# ── GPU E2E (Ollama local inference) ──────────────────────────
# Enable by setting repository variable GPU_E2E_ENABLED=true
# (Settings → Secrets and variables → Actions → Variables)
#
# Runner labels: using 'self-hosted' for now. Refine to
# [self-hosted, linux, x64, gpu] once NVIDIA runner labels are confirmed.
gpu-e2e:
if: github.repository == 'NVIDIA/NemoClaw' && vars.GPU_E2E_ENABLED == 'true'
runs-on: self-hosted
timeout-minutes: 60
env:
NEMOCLAW_NON_INTERACTIVE: "1"
NEMOCLAW_SANDBOX_NAME: "e2e-gpu-ollama"
NEMOCLAW_RECREATE_SANDBOX: "1"
NEMOCLAW_PROVIDER: "ollama"
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Verify GPU availability
run: |
echo "=== GPU Info ==="
nvidia-smi
echo ""
echo "=== VRAM ==="
nvidia-smi --query-gpu=name,memory.total --format=csv,noheader
echo ""
echo "=== Docker ==="
docker info --format '{{.ServerVersion}}'
- name: Run GPU E2E test (Ollama local inference)
run: bash test/e2e/test-gpu-e2e.sh
- name: Upload install log on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: gpu-e2e-install-log
path: /tmp/nemoclaw-gpu-e2e-install.log
if-no-files-found: ignore
- name: Upload test log on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: gpu-e2e-test-log
path: /tmp/nemoclaw-gpu-e2e-test.log
if-no-files-found: ignore
notify-on-failure:
runs-on: ubuntu-latest
needs: [cloud-e2e, cloud-experimental-e2e, gpu-e2e]
if: ${{ always() && (needs.cloud-e2e.result == 'failure' || needs.cloud-experimental-e2e.result == 'failure' || needs.gpu-e2e.result == 'failure') }}
permissions:
issues: write
steps:
- name: Create or update failure issue
uses: actions/github-script@v7
with:
script: |
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
const title = 'Nightly E2E failed';
const { data: existing } = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
labels: 'CI/CD',
per_page: 100,
});
const match = existing.find(i => !i.pull_request && i.title.startsWith(title));
if (match) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: match.number,
body: `Failed again on ${new Date().toISOString().split('T')[0]}.\n\n**Run:** ${runUrl}\n**Artifacts:** Check the run artifacts for install/test logs (artifact names vary by job).`,
});
} else {
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: `${title} — ${new Date().toISOString().split('T')[0]}`,
body: `The nightly E2E pipeline failed.\n\n**Run:** ${runUrl}\n**Artifacts:** Check the run artifacts for install/test logs (artifact names vary by job).`,
labels: ['bug', 'CI/CD'],
});
}
pr perms .github/workflows/pr.yaml
View raw YAML
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
name: pr
on:
pull_request:
types: [opened, synchronize, reopened]
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
lint:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: "22"
cache: npm
- name: Install hadolint
run: |
HADOLINT_URL="https://github.com/hadolint/hadolint/releases/latest/download/hadolint-Linux-x86_64"
curl -fsSL -o /usr/local/bin/hadolint "$HADOLINT_URL"
EXPECTED=$(curl -fsSL "${HADOLINT_URL}.sha256" | awk '{print $1}')
ACTUAL=$(sha256sum /usr/local/bin/hadolint | awk '{print $1}')
[ "$EXPECTED" = "$ACTUAL" ] || { echo "::error::hadolint checksum mismatch"; exit 1; }
chmod +x /usr/local/bin/hadolint
- name: Install dependencies
run: |
npm install --ignore-scripts
cd nemoclaw && npm install
- name: Build TypeScript plugin
run: cd nemoclaw && npm run build
- name: Run all hooks (pre-commit + pre-push)
run: npx prek run --all-files --stage pre-push
test-unit:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: "22"
cache: npm
- name: Install dependencies
run: |
npm install --ignore-scripts
cd nemoclaw && npm install
- name: Build TypeScript plugin
run: cd nemoclaw && npm run build
- name: Run all unit tests with coverage
run: npx vitest run --coverage
- name: Check coverage ratchet
run: npx tsx scripts/check-coverage-ratchet.ts
build-sandbox-images:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Pull base image from GHCR (fall back to local build)
run: |
if docker pull ghcr.io/nvidia/nemoclaw/sandbox-base:latest 2>/dev/null; then
echo "BASE_IMAGE=ghcr.io/nvidia/nemoclaw/sandbox-base:latest" >> "$GITHUB_ENV"
else
echo "::warning::GHCR base image not available, building locally"
docker build -f Dockerfile.base -t nemoclaw-sandbox-base-local .
echo "BASE_IMAGE=nemoclaw-sandbox-base-local" >> "$GITHUB_ENV"
fi
- name: Build production image
run: docker build --build-arg BASE_IMAGE=${{ env.BASE_IMAGE }} -t nemoclaw-production .
- name: Build sandbox test image (fixtures layered on production)
run: docker build -f test/Dockerfile.sandbox --build-arg BASE_IMAGE=nemoclaw-production -t nemoclaw-sandbox-test .
- name: Save images to tarballs
run: |
docker save nemoclaw-sandbox-test | gzip > /tmp/sandbox-test-image.tar.gz
docker save nemoclaw-production | gzip > /tmp/isolation-image.tar.gz
- name: Upload sandbox test image
uses: actions/upload-artifact@v4
with:
name: sandbox-test-image
path: /tmp/sandbox-test-image.tar.gz
retention-days: 1
- name: Upload isolation image
uses: actions/upload-artifact@v4
with:
name: isolation-image
path: /tmp/isolation-image.tar.gz
retention-days: 1
build-sandbox-images-arm64:
runs-on: ubuntu-24.04-arm
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Pull base image from GHCR (fall back to local build)
run: |
if docker pull ghcr.io/nvidia/nemoclaw/sandbox-base:latest 2>/dev/null; then
echo "BASE_IMAGE=ghcr.io/nvidia/nemoclaw/sandbox-base:latest" >> "$GITHUB_ENV"
else
echo "::warning::GHCR base image not available, building locally"
docker build -f Dockerfile.base -t nemoclaw-sandbox-base-local .
echo "BASE_IMAGE=nemoclaw-sandbox-base-local" >> "$GITHUB_ENV"
fi
- name: Build production image on arm64
run: docker build --build-arg BASE_IMAGE=${{ env.BASE_IMAGE }} -t nemoclaw-production-arm64 .
- name: Build sandbox test image on arm64
run: docker build -f test/Dockerfile.sandbox --build-arg BASE_IMAGE=nemoclaw-production-arm64 -t nemoclaw-sandbox-test-arm64 .
test-e2e-sandbox:
runs-on: ubuntu-latest
timeout-minutes: 15
needs: build-sandbox-images
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Download image artifact
uses: actions/download-artifact@v4
with:
name: sandbox-test-image
path: /tmp
- name: Load image
run: gunzip -c /tmp/sandbox-test-image.tar.gz | docker load
- name: Run sandbox E2E tests
run: docker run --rm -v "${{ github.workspace }}/test:/opt/test" nemoclaw-sandbox-test /opt/test/e2e-test.sh
test-e2e-gateway-isolation:
runs-on: ubuntu-latest
timeout-minutes: 15
needs: build-sandbox-images
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Download image artifact
uses: actions/download-artifact@v4
with:
name: isolation-image
path: /tmp
- name: Load image
run: gunzip -c /tmp/isolation-image.tar.gz | docker load
- name: Run gateway isolation E2E tests
run: NEMOCLAW_TEST_IMAGE=nemoclaw-production bash test/e2e-gateway-isolation.sh
pr-limit perms .github/workflows/pr-limit.yaml
View raw YAML
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
name: pr-limit
# pull_request_target runs in the base repo context, giving the token write
# access even for fork PRs. This is safe here because this workflow never
# checks out or executes code from the PR — it only counts open PRs and
# closes excess ones. Do NOT add a checkout step or run PR-sourced code
# in this workflow to prevent injection attacks.
on:
pull_request_target:
types: [opened, reopened]
permissions:
pull-requests: write
jobs:
check-pr-limit:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Check open PR count for author
env:
GH_TOKEN: ${{ github.token }}
AUTHOR: ${{ github.event.pull_request.user.login }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
run: |
# Core maintainers are exempt from the PR limit.
EXEMPT="ericksoa kjw3 jacobtomlinson cv"
for user in $EXEMPT; do
if [ "$AUTHOR" = "$user" ]; then
echo "Author $AUTHOR is a core maintainer — exempt from PR limit"
exit 0
fi
done
OPEN_COUNT=$(gh pr list --repo "$REPO" --author "$AUTHOR" --state open --json number --jq 'length')
echo "Author $AUTHOR has $OPEN_COUNT open PR(s)"
if [ "$OPEN_COUNT" -gt 10 ]; then
gh pr comment "$PR_NUMBER" --repo "$REPO" --body \
"This repository limits contributors to 10 open pull requests. Please close or merge existing PRs before opening new ones."
gh pr close "$PR_NUMBER" --repo "$REPO"
echo "::error::PR closed — author $AUTHOR exceeds the 10 open PR limit"
exit 1
fi