E2E / Nightly (advisor-4506-26625306200-1) #2302
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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. | |
| # messaging-providers-e2e Validates messaging credential provider/placeholder/L7-proxy chain | |
| # for Telegram + Discord + Slack. Uses fake tokens. Slack additionally | |
| # exercises OpenShell provider-shaped alias resolution (#2085 follow-up). | |
| # openclaw-slack-pairing-e2e | |
| # Validates hermetic Slack Socket Mode pairing request approval across | |
| # gateway and connect-shell OpenClaw state roots (#3730/#3737). | |
| # openclaw-discord-pairing-e2e | |
| # Validates hermetic Discord pairing request approval across | |
| # gateway and connect-shell OpenClaw state roots (#4061). | |
| # issue-4462-scope-upgrade-approval-e2e | |
| # Validates real CLI scope-upgrade approval and confirms | |
| # the approved agent run stays on gateway mode (#4462). | |
| # issue-4462-gateway-pinned-approval-characterization-e2e | |
| # Characterizes legacy gateway-pinned scope approval | |
| # against a real sandbox, then recovers with the fix. | |
| # messaging-compatible-endpoint-e2e | |
| # Validates Telegram + OpenAI-compatible endpoint inference routing | |
| # through inference.local with a hermetic local mock (#2766). | |
| # kimi-inference-compat-e2e | |
| # Validates Kimi K2.6 safe exec splitting through OpenClaw trajectories | |
| # with a hermetic OpenAI-compatible mock (#2620). | |
| # bedrock-runtime-compatible-anthropic-e2e | |
| # Validates the silent Bedrock Runtime custom Anthropic endpoint path | |
| # through a hermetic fake Bedrock Runtime host for OpenClaw and Hermes. | |
| # token-rotation-e2e Validates that rotating a messaging token and re-running onboard | |
| # propagates the new credential to the sandbox. Combined Telegram + | |
| # Discord + Slack coverage with cross-talk assertions. See issue #1903. | |
| # sandbox-survival-e2e Sandbox survival across gateway restarts (onboard, inference, | |
| # gateway stop/start, verify sandbox + workspace + inference). | |
| # openshell-gateway-upgrade-e2e | |
| # Validates real v0.0.36 curl install upgrade into | |
| # the current supported OpenShell with pre-upgrade backup, restored | |
| # agent state, and the same agent type running. | |
| # hermes-e2e Hermes Agent E2E — install → onboard --agent hermes → health | |
| # probe → live inference. Validates the multi-agent architecture. | |
| # hermes-dashboard-e2e Hermes Agent E2E with optional web dashboard enabled, | |
| # validating API/dashboard forwards and host reachability. | |
| # hermes-root-entrypoint-smoke-e2e | |
| # Builds the real Hermes image and verifies root entrypoint startup, | |
| # gateway-user execution, v0.14 layout repair, and PID migration. | |
| # openclaw-onboard-security-posture-e2e | |
| # Full OpenClaw onboard on a non-root host user | |
| # with trusted rc-file and runtime guard assertions. | |
| # hermes-onboard-security-posture-e2e | |
| # Full Hermes onboard on a non-root host user | |
| # with trusted rc-file and runtime guard assertions. | |
| # hermes-inference-switch-e2e | |
| # Switches a running Hermes sandbox with `nemohermes inference set` | |
| # and verifies route, config.yaml, hashes, and live requests. | |
| # hermes-discord-e2e Hermes Discord onboarding — validates the top-level Hermes | |
| # Discord schema plus OpenShell placeholder/token isolation. | |
| # hermes-slack-e2e Hermes Slack onboarding — validates the Hermes Slack policy, | |
| # Slack providers, and OpenShell credential rewrite path. | |
| # openclaw-inference-switch-e2e | |
| # Switches a running OpenClaw sandbox with `nemoclaw inference set` | |
| # and verifies route, openclaw.json, hashes, and live requests. | |
| # credential-migration-e2e Validates legacy ~/.nemoclaw/credentials.json migration to the | |
| # OpenShell gateway, secure zero-fill on unlink, allowlist filter | |
| # on non-credential env keys, and symlink-safe deletion. | |
| # launchable-smoke-e2e Community install path (brev-launchable-ci-cpu.sh) on ubuntu-latest. | |
| # gpu-e2e Local Ollama inference on an NVKS ephemeral GPU runner. | |
| # gpu-double-onboard-e2e Ollama proxy token consistency after re-onboard (#2553). | |
| # 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. | |
| # | |
| # NVIDIA_API_KEY for cloud-e2e: | |
| # - Repository secret: Settings → Secrets and variables → Actions → Repository secrets. | |
| # - Environment secret: only available if the job sets `environment: <that environment name>`. | |
| # (Storing the key under Environments / NVIDIA_API_KEY without `environment:` here leaves the | |
| # variable empty in the job — repository secrets and environment secrets are separate.) | |
| # Only runs on schedule and manual dispatch — never on PRs (secret protection). | |
| name: E2E / Nightly | |
| run-name: >- | |
| ${{ github.event_name == 'workflow_dispatch' && inputs.advisor_dispatch_id != '' && format('E2E / Nightly ({0})', inputs.advisor_dispatch_id) || 'E2E / Nightly' }} | |
| on: | |
| schedule: | |
| - cron: "0 0 * * *" | |
| workflow_dispatch: | |
| inputs: | |
| jobs: | |
| description: >- | |
| Comma-separated job names to run (empty = all). | |
| Valid: cloud-e2e, cloud-onboard-e2e, cloud-inference-e2e, | |
| skill-agent-e2e, docs-validation-e2e, messaging-providers-e2e, | |
| openclaw-slack-pairing-e2e, | |
| openclaw-tui-chat-correlation-e2e, | |
| issue-3600-gpu-proof-optional-e2e, | |
| openclaw-discord-pairing-e2e, | |
| issue-4462-scope-upgrade-approval-e2e, | |
| issue-4462-gateway-pinned-approval-characterization-e2e, | |
| messaging-compatible-endpoint-e2e, | |
| kimi-inference-compat-e2e, | |
| bedrock-runtime-compatible-anthropic-e2e, | |
| token-rotation-e2e, sandbox-survival-e2e, | |
| openshell-gateway-upgrade-e2e, | |
| issue-2478-crash-loop-recovery-e2e, hermes-e2e, | |
| hermes-dashboard-e2e, | |
| hermes-root-entrypoint-smoke-e2e, | |
| openclaw-onboard-security-posture-e2e, | |
| hermes-onboard-security-posture-e2e, | |
| hermes-inference-switch-e2e, hermes-discord-e2e, | |
| hermes-slack-e2e, sandbox-operations-e2e, inference-routing-e2e, | |
| openclaw-inference-switch-e2e, | |
| network-policy-e2e, state-backup-restore-e2e, tunnel-lifecycle-e2e, diagnostics-e2e, | |
| credential-migration-e2e, | |
| snapshot-commands-e2e, shields-config-e2e, | |
| vm-driver-privileged-exec-routing-e2e, rebuild-openclaw-e2e, | |
| upgrade-stale-sandbox-e2e, rebuild-hermes-e2e, | |
| rebuild-hermes-stale-base-e2e, double-onboard-e2e, | |
| onboard-repair-e2e, onboard-resume-e2e, onboard-negative-paths-e2e, | |
| runtime-overrides-e2e, | |
| credential-sanitization-e2e, telegram-injection-e2e, | |
| overlayfs-autofix-e2e, device-auth-health-e2e, | |
| launchable-smoke-e2e, gpu-e2e, gpu-double-onboard-e2e, | |
| channels-add-remove-e2e, channels-stop-start-e2e, brave-search-e2e | |
| required: false | |
| type: string | |
| default: "" | |
| target_ref: | |
| description: >- | |
| Optional branch, ref, or SHA to test. When empty, tests run against | |
| the workflow ref selected for the dispatch. Used by e2e-advisor | |
| auto-dispatch so the trusted main workflow can test a PR head SHA. | |
| required: false | |
| type: string | |
| default: "" | |
| pr_number: | |
| description: Optional PR number for selective-dispatch result comments. | |
| required: false | |
| type: string | |
| default: "" | |
| advisor_dispatch_id: | |
| description: Optional correlation ID from e2e-advisor auto-dispatch. | |
| required: false | |
| type: string | |
| default: "" | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: nightly-e2e-${{ github.event_name }}-${{ github.event_name == 'workflow_dispatch' && format('{0}-{1}', github.ref, inputs.pr_number || 'manual') || 'schedule' }} | |
| cancel-in-progress: true | |
| # Selective-dispatch contract: tools/e2e-advisor/dispatch.mts discovers | |
| # dispatchable jobs by looking for each job's exact predicate shape below: | |
| # github.event_name != 'workflow_dispatch' || inputs.jobs == '' || | |
| # contains(format(',{0},', inputs.jobs), ',<job-id>,') | |
| # Keep this predicate format in sync with test/e2e-advisor-dispatch.test.ts if | |
| # the workflow changes how individual jobs opt in to selective dispatch. | |
| jobs: | |
| cloud-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',cloud-e2e,')) | |
| uses: ./.github/workflows/e2e-script.yaml | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| script: test/e2e/test-full-e2e.sh | |
| artifact_name: "install-log" | |
| artifact_path: "/tmp/nemoclaw-e2e-install.log" | |
| env_json: '{"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE":"1","NEMOCLAW_NON_INTERACTIVE":"1","NEMOCLAW_RECREATE_SANDBOX":"1","NEMOCLAW_SANDBOX_NAME":"e2e-nightly"}' | |
| nvidia_api_key: true | |
| github_token: true | |
| secrets: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }} | |
| cloud-onboard-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',cloud-onboard-e2e,')) | |
| uses: ./.github/workflows/e2e-script.yaml | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| script: test/e2e/test-cloud-onboard-e2e.sh | |
| artifact_name: "install-log-cloud-onboard" | |
| artifact_path: "/tmp/nemoclaw-e2e-cloud-onboard-install.log" | |
| env_json: '{"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE":"1","NEMOCLAW_NON_INTERACTIVE":"1","NEMOCLAW_POLICY_MODE":"custom","NEMOCLAW_POLICY_PRESETS":"npm,pypi","NEMOCLAW_RECREATE_SANDBOX":"1","NEMOCLAW_SANDBOX_NAME":"e2e-cloud-onboard"}' | |
| checked_out_ref_env: "NEMOCLAW_PUBLIC_INSTALL_REF" | |
| nvidia_api_key: true | |
| github_token: true | |
| secrets: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }} | |
| cloud-inference-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',cloud-inference-e2e,')) | |
| uses: ./.github/workflows/e2e-script.yaml | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| script: test/e2e/test-cloud-inference-e2e.sh | |
| timeout_minutes: 30 | |
| artifact_name: "install-log-cloud-inference" | |
| artifact_path: "/tmp/nemoclaw-e2e-cloud-inference-install.log" | |
| env_json: '{"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE":"1","NEMOCLAW_NON_INTERACTIVE":"1","NEMOCLAW_RECREATE_SANDBOX":"1","NEMOCLAW_SANDBOX_NAME":"e2e-cloud-inference"}' | |
| nvidia_api_key: true | |
| secrets: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }} | |
| skill-agent-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',skill-agent-e2e,')) | |
| uses: ./.github/workflows/e2e-script.yaml | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| script: test/e2e/test-skill-agent-e2e.sh | |
| timeout_minutes: 30 | |
| artifact_name: "install-log-skill-agent" | |
| artifact_path: "/tmp/nemoclaw-e2e-skill-agent-install.log" | |
| env_json: '{"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE":"1","NEMOCLAW_NON_INTERACTIVE":"1","NEMOCLAW_RECREATE_SANDBOX":"1","NEMOCLAW_SANDBOX_NAME":"e2e-skill-agent"}' | |
| nvidia_api_key: true | |
| secrets: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }} | |
| docs-validation-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && | |
| (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',docs-validation-e2e,')) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| - name: Install NemoClaw | |
| env: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| NEMOCLAW_NON_INTERACTIVE: "1" | |
| NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1" | |
| run: bash install.sh --non-interactive --yes-i-accept-third-party-software | |
| - name: Run docs validation | |
| env: | |
| CHECK_DOC_LINKS_REMOTE: "0" | |
| run: | | |
| set -euo pipefail | |
| [ -f "$HOME/.bashrc" ] && source "$HOME/.bashrc" 2>/dev/null || true | |
| export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" | |
| [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" | |
| [ -d "$HOME/.local/bin" ] && [[ ":$PATH:" != *":$HOME/.local/bin:"* ]] && export PATH="$HOME/.local/bin:$PATH" | |
| bash test/e2e/test-docs-validation.sh | |
| # ── Messaging Providers E2E ────────────────────────────────── | |
| # Validates the full provider/placeholder/L7-proxy chain for token-backed | |
| # messaging credentials, and the QR-only WhatsApp config/policy/no-provider | |
| # path. Uses fake tokens by default — the L7 proxy rewrites placeholders and | |
| # the real API returns 401, proving the chain works. See: PR #1081 | |
| messaging-providers-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',messaging-providers-e2e,')) | |
| uses: ./.github/workflows/e2e-script.yaml | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| script: test/e2e/test-messaging-providers.sh | |
| timeout_minutes: 75 | |
| artifact_name: "install-log-messaging-providers" | |
| artifact_path: | | |
| /tmp/nemoclaw-e2e-install.log | |
| /tmp/nemoclaw-e2e-whatsapp-*.log | |
| env_json: '{"DISCORD_BOT_TOKEN":"test-fake-discord-token-e2e","NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE":"1","NEMOCLAW_NON_INTERACTIVE":"1","NEMOCLAW_POLICY_TIER":"open","NEMOCLAW_SANDBOX_NAME":"e2e-msg-provider","SLACK_APP_TOKEN":"xapp-fake-slack-app-token-e2e","SLACK_BOT_TOKEN":"xoxb-fake-slack-token-e2e","TELEGRAM_BOT_TOKEN":"test-fake-telegram-token-e2e"}' | |
| nvidia_api_key: true | |
| github_token: true | |
| secrets: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }} | |
| TELEGRAM_BOT_TOKEN_REAL: ${{ secrets.TELEGRAM_BOT_TOKEN_REAL }} | |
| TELEGRAM_CHAT_ID_E2E: ${{ secrets.TELEGRAM_CHAT_ID_E2E }} | |
| DISCORD_BOT_TOKEN_REAL: ${{ secrets.DISCORD_BOT_TOKEN_REAL }} | |
| DISCORD_CHANNEL_ID_E2E: ${{ secrets.DISCORD_CHANNEL_ID_E2E }} | |
| SLACK_BOT_TOKEN_REAL: ${{ secrets.SLACK_BOT_TOKEN_REAL }} | |
| SLACK_APP_TOKEN_REAL: ${{ secrets.SLACK_APP_TOKEN_REAL }} | |
| SLACK_CHANNEL_ID_E2E: ${{ secrets.SLACK_CHANNEL_ID_E2E }} | |
| openclaw-slack-pairing-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',openclaw-slack-pairing-e2e,')) | |
| uses: ./.github/workflows/e2e-script.yaml | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| script: test/e2e/test-openclaw-slack-pairing.sh | |
| artifact_name: "install-log-openclaw-slack-pairing" | |
| artifact_path: "/tmp/nemoclaw-e2e-openclaw-slack-pairing-install.log" | |
| env_json: '{"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE":"1","NEMOCLAW_NON_INTERACTIVE":"1","NEMOCLAW_POLICY_TIER":"open","NEMOCLAW_SANDBOX_NAME":"e2e-openclaw-slack-pairing","SLACK_APP_TOKEN":"xapp-fake-slack-pairing-e2e","SLACK_BOT_TOKEN":"xoxb-fake-slack-pairing-e2e"}' | |
| nvidia_api_key: true | |
| github_token: true | |
| secrets: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }} | |
| openclaw-tui-chat-correlation-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && | |
| (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',openclaw-tui-chat-correlation-e2e,')) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 75 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| - name: Resolve public install ref | |
| id: public_install_ref | |
| shell: bash | |
| run: | | |
| printf 'ref=%s\n' "$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" | |
| - name: Setup Node.js | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.0.0 | |
| with: | |
| node-version: "22" | |
| - name: Install test dependencies | |
| run: npm ci --include=dev | |
| - name: Run OpenClaw TUI chat correlation E2E test | |
| env: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| NEMOCLAW_NON_INTERACTIVE: "1" | |
| NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1" | |
| NEMOCLAW_RECREATE_SANDBOX: "1" | |
| NEMOCLAW_SANDBOX_NAME: "e2e-openclaw-tui-correlation" | |
| NEMOCLAW_PUBLIC_INSTALL_REF: ${{ steps.public_install_ref.outputs.ref }} | |
| GITHUB_TOKEN: ${{ github.token }} | |
| run: bash test/e2e/test-openclaw-tui-chat-correlation.sh | |
| - name: Upload install log on failure | |
| if: failure() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: install-log-openclaw-tui-chat-correlation | |
| path: /tmp/nemoclaw-e2e-openclaw-tui-correlation-install.log | |
| if-no-files-found: ignore | |
| # ── DGX Station GPU optional proof validation (#3600) ────────── | |
| # CI cannot emulate GB300, but this guards the release-blocker mitigation: | |
| # optional direct GPU proofs must not abort onboard before the fatal throw. | |
| issue-3600-gpu-proof-optional-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && | |
| (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',issue-3600-gpu-proof-optional-e2e,')) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| - name: Setup Node.js | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.0.0 | |
| with: | |
| node-version: "22" | |
| - name: Install test dependencies | |
| run: npm ci --include=dev | |
| - name: Verify optional GPU proof cannot abort onboard | |
| run: npx vitest run src/lib/onboard/sandbox-gpu-preflight.test.ts --pool=forks -t "direct sandbox GPU proof" | |
| # ── OpenClaw Discord Pairing E2E (#4061) ────────────────────── | |
| # Hermetic Discord Gateway placeholder rewrite proof, then connect-shell | |
| # `openclaw pairing approve discord <code>` against shared state. | |
| openclaw-discord-pairing-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',openclaw-discord-pairing-e2e,')) | |
| uses: ./.github/workflows/e2e-script.yaml | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| script: test/e2e/test-openclaw-discord-pairing.sh | |
| artifact_name: "install-log-openclaw-discord-pairing" | |
| artifact_path: "/tmp/nemoclaw-e2e-openclaw-discord-pairing-install.log" | |
| env_json: '{"DISCORD_BOT_TOKEN":"test-fake-discord-pairing-e2e","NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE":"1","NEMOCLAW_NON_INTERACTIVE":"1","NEMOCLAW_POLICY_TIER":"open","NEMOCLAW_SANDBOX_NAME":"e2e-openclaw-discord-pairing"}' | |
| nvidia_api_key: true | |
| github_token: true | |
| secrets: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }} | |
| # ── OpenClaw Scope-Upgrade Approval E2E (#4462) ──────────────── | |
| # Positive proof: in a real sandbox, accept either a visible pending CLI | |
| # scope upgrade or the fixed watcher's immediate approval, then confirm | |
| # openclaw agent still uses the gateway path rather than embedded fallback. | |
| issue-4462-scope-upgrade-approval-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',issue-4462-scope-upgrade-approval-e2e,')) | |
| uses: ./.github/workflows/e2e-script.yaml | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| script: test/e2e/test-issue-4462-scope-upgrade-approval.sh | |
| timeout_minutes: 60 | |
| artifact_name: "issue-4462-scope-upgrade-approval-logs" | |
| artifact_path: | | |
| /tmp/nemoclaw-e2e-issue-4462-scope-upgrade-install.log | |
| /tmp/nemoclaw-issue-4462-scope-upgrade-approval.log | |
| /tmp/nemoclaw-issue-4462-scope-upgrade-agent.log | |
| /tmp/nemoclaw-issue-4462-scope-upgrade-state.log | |
| env_json: '{"NEMOCLAW_4462_MODE":"approval","NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE":"1","NEMOCLAW_AUTO_PAIR_DEADLINE_SECS":"30","NEMOCLAW_AUTO_PAIR_FAST_DEADLINE_SECS":"3","NEMOCLAW_AUTO_PAIR_SLOW_INTERVAL_SECS":"600","NEMOCLAW_NON_INTERACTIVE":"1","NEMOCLAW_RECREATE_SANDBOX":"1","NEMOCLAW_SANDBOX_NAME":"e2e-issue-4462-scope-upgrade"}' | |
| nvidia_api_key: true | |
| github_token: false | |
| secrets: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }} | |
| # ── OpenClaw Gateway-Pinned Approval Characterization (#4462) ── | |
| # Diagnostic proof: in a real sandbox, wait for the fixed watcher to exit, | |
| # force the legacy gateway-pinned approve path, record the observed | |
| # OpenClaw outcome, and recover through the fixed proxy-env guard if needed. | |
| issue-4462-gateway-pinned-approval-characterization-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',issue-4462-gateway-pinned-approval-characterization-e2e,')) | |
| uses: ./.github/workflows/e2e-script.yaml | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| script: test/e2e/test-issue-4462-scope-upgrade-approval.sh | |
| timeout_minutes: 60 | |
| artifact_name: "issue-4462-gateway-pinned-approval-characterization-logs" | |
| artifact_path: | | |
| /tmp/nemoclaw-e2e-issue-4462-scope-upgrade-repro-install.log | |
| /tmp/nemoclaw-issue-4462-scope-upgrade-repro-approval.log | |
| /tmp/nemoclaw-issue-4462-scope-upgrade-repro-agent.log | |
| /tmp/nemoclaw-issue-4462-scope-upgrade-repro-state.log | |
| env_json: '{"NEMOCLAW_4462_AGENT_LOG":"/tmp/nemoclaw-issue-4462-scope-upgrade-repro-agent.log","NEMOCLAW_4462_APPROVAL_LOG":"/tmp/nemoclaw-issue-4462-scope-upgrade-repro-approval.log","NEMOCLAW_4462_AUTO_PAIR_DEADLINE_SECS":"12","NEMOCLAW_4462_AUTO_PAIR_FAST_DEADLINE_SECS":"1","NEMOCLAW_4462_AUTO_PAIR_RUN_TIMEOUT_SECS":"2","NEMOCLAW_4462_AUTO_PAIR_SLOW_INTERVAL_SECS":"1","NEMOCLAW_4462_INSTALL_LOG":"/tmp/nemoclaw-e2e-issue-4462-scope-upgrade-repro-install.log","NEMOCLAW_4462_MODE":"legacy-repro","NEMOCLAW_4462_STATE_LOG":"/tmp/nemoclaw-issue-4462-scope-upgrade-repro-state.log","NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE":"1","NEMOCLAW_NON_INTERACTIVE":"1","NEMOCLAW_RECREATE_SANDBOX":"1","NEMOCLAW_SANDBOX_NAME":"e2e-issue-4462-scope-upgrade-repro"}' | |
| nvidia_api_key: true | |
| github_token: false | |
| secrets: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }} | |
| messaging-compatible-endpoint-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',messaging-compatible-endpoint-e2e,')) | |
| uses: ./.github/workflows/e2e-script.yaml | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| script: test/e2e/test-messaging-compatible-endpoint.sh | |
| artifact_name: "install-log-messaging-compatible-endpoint" | |
| artifact_path: "/tmp/nemoclaw-e2e-messaging-compatible-endpoint-install.log" | |
| env_json: '{"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE":"1","NEMOCLAW_NON_INTERACTIVE":"1","NEMOCLAW_SANDBOX_NAME":"e2e-msg-compat","TELEGRAM_ALLOWED_IDS":"123456789","TELEGRAM_BOT_TOKEN":"test-fake-telegram-token-e2e"}' | |
| github_token: true | |
| secrets: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }} | |
| # ── Channels add/remove lifecycle E2E (#3462 Test 2) ──────────────── | |
| # Regression coverage for #3437 (channels add must auto-apply the matching | |
| # network policy preset so the bridge boots with egress to its upstream API) | |
| # and #3671 (channels remove must detach providers, un-apply the preset, | |
| # and survive a follow-up rebuild without being silently re-added from | |
| # shell env). Telegram-only — the other paste-token channels walk the same | |
| # KNOWN_CHANNELS + preset lookup code path. | |
| channels-add-remove-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',channels-add-remove-e2e,')) | |
| uses: ./.github/workflows/e2e-script.yaml | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| script: test/e2e/test-channels-add-remove.sh | |
| timeout_minutes: 75 | |
| artifact_name: "install-log-channels-add-remove" | |
| artifact_path: | | |
| /tmp/nemoclaw-e2e-install.log | |
| /tmp/nc-add.log | |
| /tmp/nc-remove.log | |
| /tmp/nc-rebuild-add.log | |
| /tmp/nc-rebuild-remove.log | |
| env_json: '{"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE":"1","NEMOCLAW_NON_INTERACTIVE":"1","NEMOCLAW_SANDBOX_NAME":"e2e-channels-add-remove","TELEGRAM_BOT_TOKEN":"test-fake-telegram-token-add-remove-e2e"}' | |
| nvidia_api_key: true | |
| github_token: true | |
| secrets: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }} | |
| # ── Channels stop/start lifecycle E2E (#3462) ─────────────────────── | |
| # Regression coverage for #3453 (stop must disable across rebuild), #3381 | |
| # (start must re-attach from cached credentials). | |
| # Exercises OpenClaw and Hermes across telegram, discord, wechat, slack, and whatsapp. | |
| channels-stop-start-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',channels-stop-start-e2e,')) | |
| uses: ./.github/workflows/e2e-script.yaml | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| script: test/e2e/test-channels-stop-start.sh | |
| timeout_minutes: 120 | |
| artifact_name: "install-log-channels-stop-start" | |
| artifact_path: | | |
| /tmp/nemoclaw-e2e-install.log | |
| /tmp/nemoclaw-e2e-channels-*-install.log | |
| /tmp/nc-channels-*.log | |
| env_json: '{"DISCORD_ALLOWED_IDS":"1005536447329222676","DISCORD_BOT_TOKEN":"test-fake-discord-token-stop-start-e2e","DISCORD_REQUIRE_MENTION":"0","DISCORD_SERVER_ID":"1491590992753590594","NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE":"1","NEMOCLAW_NON_INTERACTIVE":"1","NEMOCLAW_POLICY_TIER":"open","NEMOCLAW_SANDBOX_NAME":"e2e-channels-stop-start","SLACK_ALLOWED_USERS":"U0123456789,U09ABCDEFGH","SLACK_APP_TOKEN":"xapp-fake-slack-app-token-stop-start-e2e","SLACK_BOT_TOKEN":"xoxb-fake-slack-token-stop-start-e2e","TELEGRAM_ALLOWED_IDS":"123456789","TELEGRAM_BOT_TOKEN":"test-fake-telegram-token-stop-start-e2e","WECHAT_ACCOUNT_ID":"e2e-fake-account-stop-start","WECHAT_ALLOWED_IDS":"wxid_stopstart_operator","WECHAT_BASE_URL":"https://ilinkai-fake-stop-start.wechat.com","WECHAT_BOT_TOKEN":"test-fake-wechat-token-stop-start-e2e","WECHAT_USER_ID":"wxid_stopstart_operator"}' | |
| nvidia_api_key: true | |
| github_token: true | |
| secrets: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }} | |
| brave-search-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',brave-search-e2e,')) | |
| uses: ./.github/workflows/e2e-script.yaml | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| script: test/e2e/test-brave-search-e2e.sh | |
| artifact_name: "install-log-brave-search" | |
| artifact_path: "/tmp/nemoclaw-e2e-brave-search-onboard.log" | |
| env_json: '{"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE":"1","NEMOCLAW_NON_INTERACTIVE":"1","NEMOCLAW_SANDBOX_NAME":"e2e-brave-search"}' | |
| brave_api_key: true | |
| nvidia_api_key: true | |
| github_token: true | |
| secrets: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }} | |
| kimi-inference-compat-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && | |
| (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',kimi-inference-compat-e2e,')) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| - name: Run Kimi inference compatibility E2E test | |
| env: | |
| NEMOCLAW_NON_INTERACTIVE: "1" | |
| NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1" | |
| NEMOCLAW_SANDBOX_NAME: "e2e-kimi-compat" | |
| GITHUB_TOKEN: ${{ github.token }} | |
| run: bash test/e2e/test-kimi-inference-compat.sh | |
| - name: Upload onboard log on failure | |
| if: failure() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: install-log-kimi-inference-compat | |
| path: /tmp/nemoclaw-e2e-kimi-inference-compat-onboard.log | |
| if-no-files-found: ignore | |
| - name: Upload build/setup log on failure | |
| if: failure() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: build-log-kimi-inference-compat | |
| path: /tmp/nemoclaw-e2e-kimi-inference-compat-build.log | |
| if-no-files-found: ignore | |
| - name: Upload agent log on failure | |
| if: failure() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: agent-log-kimi-inference-compat | |
| path: /tmp/nemoclaw-e2e-kimi-inference-compat-agent.log | |
| if-no-files-found: ignore | |
| # ── Bedrock Runtime compatible Anthropic endpoint (#3767) ───── | |
| # Hermetic fake Bedrock Runtime endpoint path. The sandbox only sees | |
| # inference.local; the host-side OpenShell provider owns the hidden adapter | |
| # token and the upstream Bedrock bearer derived from the fake pasted key. | |
| bedrock-runtime-compatible-anthropic-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && | |
| (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',bedrock-runtime-compatible-anthropic-e2e,')) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| agent: [openclaw, hermes] | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| - name: Run Bedrock Runtime compatible Anthropic E2E test | |
| env: | |
| NEMOCLAW_NON_INTERACTIVE: "1" | |
| NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1" | |
| NEMOCLAW_RECREATE_SANDBOX: "1" | |
| NEMOCLAW_AGENT: ${{ matrix.agent }} | |
| NEMOCLAW_SANDBOX_NAME: e2e-bedrock-${{ matrix.agent }} | |
| GITHUB_TOKEN: ${{ github.token }} | |
| run: bash test/e2e/test-bedrock-runtime-compatible-anthropic.sh | |
| - name: Upload onboard log on failure | |
| if: failure() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: onboard-log-bedrock-runtime-compatible-anthropic-${{ matrix.agent }} | |
| path: /tmp/nemoclaw-e2e-bedrock-runtime-${{ matrix.agent }}-onboard.log | |
| if-no-files-found: ignore | |
| - name: Upload build/setup log on failure | |
| if: failure() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: build-log-bedrock-runtime-compatible-anthropic-${{ matrix.agent }} | |
| path: /tmp/nemoclaw-e2e-bedrock-runtime-${{ matrix.agent }}-build.log | |
| if-no-files-found: ignore | |
| - name: Upload fake Bedrock Runtime log on failure | |
| if: failure() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: mock-log-bedrock-runtime-compatible-anthropic-${{ matrix.agent }} | |
| path: /tmp/nemoclaw-e2e-bedrock-runtime-${{ matrix.agent }}-mock.log | |
| if-no-files-found: ignore | |
| - name: Upload Bedrock Runtime adapter log on failure | |
| if: failure() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: adapter-log-bedrock-runtime-compatible-anthropic-${{ matrix.agent }} | |
| path: ~/.nemoclaw/bedrock-runtime-adapter.log | |
| if-no-files-found: ignore | |
| # ── Token rotation (credential propagation to L7 proxy) ───── | |
| # Validates that rotating a messaging token and re-running onboard | |
| # propagates the new credential to the sandbox. Uses two fake tokens | |
| # per provider (Telegram + Discord) to prove the sandbox is rebuilt on | |
| # rotation and reused when unchanged. | |
| # See: issue #1903 | |
| token-rotation-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && | |
| (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',token-rotation-e2e,')) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| - name: Run token rotation E2E test | |
| env: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| NEMOCLAW_NON_INTERACTIVE: "1" | |
| NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1" | |
| NEMOCLAW_POLICY_TIER: "open" | |
| GITHUB_TOKEN: ${{ github.token }} | |
| TELEGRAM_BOT_TOKEN_A: "test-fake-token-A-rotation-e2e" | |
| TELEGRAM_BOT_TOKEN_B: "test-fake-token-B-rotation-e2e" | |
| DISCORD_BOT_TOKEN_A: "test-fake-discord-A-rotation-e2e" | |
| DISCORD_BOT_TOKEN_B: "test-fake-discord-B-rotation-e2e" | |
| SLACK_BOT_TOKEN_A: "xoxb-fake-A-rotation-e2e" | |
| SLACK_BOT_TOKEN_B: "xoxb-fake-B-rotation-e2e" | |
| SLACK_APP_TOKEN_A: "xapp-fake-A-rotation-e2e" | |
| SLACK_APP_TOKEN_B: "xapp-fake-B-rotation-e2e" | |
| run: bash test/e2e/test-token-rotation.sh | |
| - name: Upload install log on failure | |
| if: failure() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: install-log-token-rotation | |
| path: /tmp/nemoclaw-e2e-install.log | |
| if-no-files-found: ignore | |
| # ── Sandbox survival (gateway restart recovery) ────────────── | |
| sandbox-survival-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',sandbox-survival-e2e,')) | |
| uses: ./.github/workflows/e2e-script.yaml | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| script: test/e2e/test-sandbox-survival.sh | |
| timeout_minutes: 30 | |
| artifact_name: "sandbox-survival-install-log" | |
| artifact_path: "/tmp/nemoclaw-e2e-install.log" | |
| env_json: '{"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE":"1","NEMOCLAW_NON_INTERACTIVE":"1","NEMOCLAW_SANDBOX_NAME":"e2e-survival"}' | |
| nvidia_api_key: true | |
| github_token: true | |
| secrets: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }} | |
| issue-2478-crash-loop-recovery-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',issue-2478-crash-loop-recovery-e2e,')) | |
| uses: ./.github/workflows/e2e-script.yaml | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| script: test/e2e/test-issue-2478-crash-loop-recovery.sh | |
| timeout_minutes: 30 | |
| artifact_name: "issue-2478-crash-loop-recovery-install-log" | |
| artifact_path: "/tmp/nemoclaw-e2e-install.log" | |
| env_json: '{"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE":"1","NEMOCLAW_NON_INTERACTIVE":"1","NEMOCLAW_SANDBOX_NAME":"e2e-2478"}' | |
| nvidia_api_key: true | |
| github_token: true | |
| secrets: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }} | |
| hermes-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',hermes-e2e,')) | |
| uses: ./.github/workflows/e2e-script.yaml | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| script: test/e2e/test-hermes-e2e.sh | |
| timeout_minutes: 60 | |
| artifact_name: "hermes-e2e-install-log" | |
| artifact_path: "/tmp/nemoclaw-e2e-hermes-install.log" | |
| env_json: '{"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE":"1","NEMOCLAW_AGENT":"hermes","NEMOCLAW_NON_INTERACTIVE":"1","NEMOCLAW_RECREATE_SANDBOX":"1","NEMOCLAW_SANDBOX_NAME":"e2e-hermes"}' | |
| nvidia_api_key: true | |
| github_token: true | |
| secrets: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }} | |
| hermes-dashboard-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',hermes-dashboard-e2e,')) | |
| uses: ./.github/workflows/e2e-script.yaml | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| script: test/e2e/test-hermes-e2e.sh | |
| timeout_minutes: 60 | |
| artifact_name: "hermes-dashboard-e2e-install-log" | |
| artifact_path: "/tmp/nemoclaw-e2e-hermes-install.log" | |
| env_json: '{"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE":"1","NEMOCLAW_AGENT":"hermes","NEMOCLAW_E2E_HERMES_DASHBOARD":"1","NEMOCLAW_HERMES_DASHBOARD":"1","NEMOCLAW_NON_INTERACTIVE":"1","NEMOCLAW_RECREATE_SANDBOX":"1","NEMOCLAW_SANDBOX_NAME":"e2e-hermes-dashboard"}' | |
| nvidia_api_key: true | |
| github_token: true | |
| secrets: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }} | |
| hermes-root-entrypoint-smoke-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',hermes-root-entrypoint-smoke-e2e,')) | |
| uses: ./.github/workflows/e2e-script.yaml | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| script: test/e2e/test-hermes-root-entrypoint-smoke.sh | |
| timeout_minutes: 45 | |
| artifact_name: "hermes-root-entrypoint-smoke-log" | |
| artifact_path: "/tmp/nemoclaw-hermes-root-entrypoint-smoke.log" | |
| secrets: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }} | |
| openclaw-onboard-security-posture-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',openclaw-onboard-security-posture-e2e,')) | |
| uses: ./.github/workflows/e2e-script.yaml | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| script: test/e2e/test-full-e2e.sh | |
| timeout_minutes: 60 | |
| artifact_name: "openclaw-onboard-security-posture-install-log" | |
| artifact_path: "/tmp/nemoclaw-e2e-install.log" | |
| env_json: '{"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE":"1","NEMOCLAW_E2E_EXPECT_NON_ROOT_HOST":"1","NEMOCLAW_E2E_SECURITY_POSTURE":"1","NEMOCLAW_NON_INTERACTIVE":"1","NEMOCLAW_RECREATE_SANDBOX":"1","NEMOCLAW_SANDBOX_NAME":"e2e-openclaw-security-posture"}' | |
| nvidia_api_key: true | |
| github_token: true | |
| secrets: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }} | |
| hermes-onboard-security-posture-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',hermes-onboard-security-posture-e2e,')) | |
| uses: ./.github/workflows/e2e-script.yaml | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| script: test/e2e/test-hermes-e2e.sh | |
| timeout_minutes: 60 | |
| artifact_name: "hermes-onboard-security-posture-install-log" | |
| artifact_path: "/tmp/nemoclaw-e2e-hermes-install.log" | |
| env_json: '{"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE":"1","NEMOCLAW_AGENT":"hermes","NEMOCLAW_E2E_EXPECT_NON_ROOT_HOST":"1","NEMOCLAW_E2E_SECURITY_POSTURE":"1","NEMOCLAW_NON_INTERACTIVE":"1","NEMOCLAW_RECREATE_SANDBOX":"1","NEMOCLAW_SANDBOX_NAME":"e2e-hermes-security-posture"}' | |
| nvidia_api_key: true | |
| github_token: true | |
| secrets: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }} | |
| hermes-inference-switch-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',hermes-inference-switch-e2e,')) | |
| uses: ./.github/workflows/e2e-script.yaml | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| script: test/e2e/test-hermes-inference-switch.sh | |
| timeout_minutes: 60 | |
| artifact_name: "hermes-inference-switch-install-log" | |
| artifact_path: "/tmp/nemoclaw-e2e-hermes-inference-switch-install.log" | |
| env_json: '{"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE":"1","NEMOCLAW_AGENT":"hermes","NEMOCLAW_NON_INTERACTIVE":"1","NEMOCLAW_RECREATE_SANDBOX":"1","NEMOCLAW_SANDBOX_NAME":"e2e-hermes-inference-switch"}' | |
| nvidia_api_key: true | |
| github_token: true | |
| secrets: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }} | |
| hermes-discord-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',hermes-discord-e2e,')) | |
| uses: ./.github/workflows/e2e-script.yaml | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| script: test/e2e/test-hermes-discord-e2e.sh | |
| timeout_minutes: 60 | |
| artifact_name: "hermes-discord-e2e-install-log" | |
| artifact_path: "/tmp/nemoclaw-e2e-hermes-discord-install.log" | |
| env_json: '{"DISCORD_ALLOWED_IDS":"1005536447329222676","DISCORD_BOT_TOKEN":"test-fake-discord-token-hermes-e2e","DISCORD_REQUIRE_MENTION":"0","DISCORD_SERVER_IDS":"1491590992753590594","NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE":"1","NEMOCLAW_AGENT":"hermes","NEMOCLAW_NON_INTERACTIVE":"1","NEMOCLAW_POLICY_TIER":"open","NEMOCLAW_RECREATE_SANDBOX":"1","NEMOCLAW_SANDBOX_NAME":"e2e-hermes-discord"}' | |
| nvidia_api_key: true | |
| github_token: true | |
| secrets: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }} | |
| hermes-slack-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',hermes-slack-e2e,')) | |
| uses: ./.github/workflows/e2e-script.yaml | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| script: test/e2e/test-hermes-slack-e2e.sh | |
| runner: linux-amd64-cpu4 | |
| timeout_minutes: 60 | |
| artifact_name: "hermes-slack-e2e-install-log" | |
| artifact_path: "/tmp/nemoclaw-e2e-hermes-slack-install.log" | |
| env_json: '{"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE":"1","NEMOCLAW_AGENT":"hermes","NEMOCLAW_NON_INTERACTIVE":"1","NEMOCLAW_POLICY_TIER":"open","NEMOCLAW_RECREATE_SANDBOX":"1","NEMOCLAW_SANDBOX_NAME":"e2e-hermes-slack","SLACK_APP_TOKEN":"xapp-test-hermes-slack-app-token","SLACK_BOT_TOKEN":"xoxb-test-hermes-slack-token"}' | |
| nvidia_api_key: true | |
| github_token: true | |
| secrets: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }} | |
| sandbox-operations-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && | |
| (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',sandbox-operations-e2e,')) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| - name: Start gateway log streamer (background) | |
| run: | | |
| # Diagnostic for NVIDIA/NemoClaw#2484: container log driver in | |
| # openshell's k3s setup doesn't allow reading container stdio — | |
| # only working path to /tmp/gateway.log is via SSH, which | |
| # `nemoclaw <sandbox> logs` uses internally. | |
| # | |
| # Snapshot mode (not follow): every 10s, overwrite per-sandbox | |
| # log file with the latest gateway log content. Bounded output | |
| # (~62 lines per snapshot). When a sandbox is destroyed by the | |
| # test, the file holds the final pre-destroy snapshot. | |
| mkdir -p docker-logs | |
| nohup bash -c ' | |
| export PATH="$HOME/.local/bin:$PATH" | |
| # Strategy: every 5s, snapshot each live sandbox via | |
| # `docker exec openshell-cluster-nemoclaw kubectl ...`. This | |
| # bypasses both per-pod networking (which has had connection- | |
| # refused races for some sandboxes) and the host openshell | |
| # client (which loses gateway metadata after TC-SBX-06s | |
| # docker-kill). kubectl talks directly to k3s in the cluster | |
| # container. | |
| # | |
| # Snapshot mode (overwrite per iteration), not live tail-F: | |
| # the gateway-persistent.log file accumulates everything since | |
| # boot (mirrored from /tmp/gateway.log by nemoclaw-start.sh), | |
| # so a single full-cat at any point gives us complete history. | |
| # Each iteration is short-lived so transient connection issues | |
| # do not cause us to lose the entire stream. | |
| # | |
| # Also snapshot kubectl pod listing per iteration so we have | |
| # the actual pod naming convention even if the cluster is | |
| # destroyed by teardown later. | |
| while sleep 5; do | |
| if ! docker ps --format "{{.Names}}" 2>/dev/null | grep -q "^openshell-cluster-nemoclaw$"; then | |
| continue | |
| fi | |
| docker exec openshell-cluster-nemoclaw kubectl get pods -A --no-headers >docker-logs/_pods.txt 2>&1 | |
| registry="$HOME/.nemoclaw/sandboxes.json" | |
| [ -f "$registry" ] || continue | |
| live=$(jq -r ".sandboxes // {} | keys[]?" "$registry" 2>/dev/null) | |
| for name in $live; do | |
| case "$name" in | |
| *[!a-z0-9_-]*|"") continue ;; | |
| esac | |
| # Find pod by sandbox name. openshell uses the sandbox | |
| # name as the namespace and "agent" as the pod name. | |
| # Try a few common patterns. | |
| pod_match=$(awk -v n="$name" "\$1==n || \$2==n || \$1==\"sandbox-\" n || \$2==\"sandbox-\" n {print \$1\"/\"\$2; exit}" docker-logs/_pods.txt) | |
| if [ -z "$pod_match" ]; then | |
| # Fallback: any pod whose name contains the sandbox name | |
| pod_match=$(awk -v n="$name" "index(\$2,n)>0 {print \$1\"/\"\$2; exit}" docker-logs/_pods.txt) | |
| fi | |
| if [ -z "$pod_match" ]; then continue; fi | |
| pod_ns="${pod_match%%/*}" | |
| pod_name="${pod_match##*/}" | |
| docker exec openshell-cluster-nemoclaw kubectl exec -n "$pod_ns" "$pod_name" -- bash -c " | |
| for f in /sandbox/.openclaw/logs/gateway-persistent.log /tmp/gateway.log /tmp/openclaw-*/openclaw-*.log; do | |
| [ -f \"\$f\" ] || continue | |
| printf \"\\n----- %s (size=%s) -----\\n\" \"\$f\" \"\$(stat -c%s \"\$f\" 2>/dev/null || echo ?)\" | |
| cat -- \"\$f\" 2>/dev/null | |
| done | |
| " > "docker-logs/sandbox-${name}.log" 2>&1 | |
| done | |
| done | |
| ' >/dev/null 2>&1 & | |
| echo $! > /tmp/gateway-log-streamer.pid | |
| - name: Run sandbox operations E2E test | |
| env: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| NEMOCLAW_NON_INTERACTIVE: "1" | |
| NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1" | |
| NEMOCLAW_POLICY_TIER: "open" | |
| GITHUB_TOKEN: ${{ github.token }} | |
| # Override the 1800s default in test/e2e/e2e-timeout.sh. Sandbox | |
| # creation alone is ~14 min per sandbox in current CI conditions | |
| # (build+upload to k3s gateway), and the test creates two — leaving | |
| # the default 30-min budget completely consumed by setup with no | |
| # room for the actual TC-SBX cases. The job-level timeout (60 min, | |
| # set in `timeout-minutes` above) is the real upper bound. | |
| NEMOCLAW_E2E_TIMEOUT_SECONDS: "2700" | |
| run: bash test/e2e/test-sandbox-operations.sh | |
| - name: Stop gateway log streamer | |
| if: always() | |
| # Diagnostic step: never let `bash -e` kill the snapshot loop on a | |
| # single command failure (openshell ssh-config, nemoclaw logs, etc. | |
| # all routinely fail post-test depending on TC-SBX-06's docker-kill | |
| # state). We log the failures inline and continue. | |
| shell: bash --noprofile --norc -uo pipefail {0} | |
| run: | | |
| [ -f /tmp/gateway-log-streamer.pid ] && kill "$(cat /tmp/gateway-log-streamer.pid)" 2>/dev/null || true | |
| # Kill any per-sandbox SSH+tail followers spawned by the streamer. | |
| pkill -f 'tail -n \+1 -F /tmp/gateway.log' 2>/dev/null || true | |
| pkill -f 'ssh.*openshell-' 2>/dev/null || true | |
| sleep 2 | |
| # Final snapshot: tail -F glob expands once at start, so log files | |
| # for openclaw processes that ran as a different UID (creating new | |
| # /tmp/openclaw-<uid>/ dirs mid-test) get missed. Re-glob now and | |
| # append every openclaw log file from each live sandbox to the | |
| # per-sandbox docker-logs file. | |
| # | |
| # Use `nemoclaw <name> logs` (not raw openshell ssh-config + ssh) | |
| # because nemoclaw handles SSH key/host setup and is robust to | |
| # streamer race conditions. Tested working in TC-SBX-04. | |
| export PATH="$HOME/.local/bin:$PATH" | |
| echo "=== final-snapshot: PATH=$PATH" | |
| echo "=== final-snapshot: nemoclaw=$(command -v nemoclaw)" | |
| echo "=== final-snapshot: openshell=$(command -v openshell)" | |
| # TC-SBX-06's docker kill of the gateway pod can leave openshell | |
| # without an active gateway selected; re-select before the snapshot | |
| # so `nemoclaw <name> logs` and direct `openshell sandbox exec` both | |
| # have a target. The select is best-effort — failure (e.g., gateway | |
| # not yet recovered) just means we fall through to ssh-config-based | |
| # capture below. | |
| openshell gateway select nemoclaw 2>&1 | head -5 || true | |
| openshell gateway list 2>&1 | head -10 || true | |
| # NEW PATH: bypass the openshell client entirely. The | |
| # openshell-cluster-nemoclaw docker container runs k3s with | |
| # kubectl available inside. Even after TC-SBX-06's docker-kill, | |
| # docker auto-restarts the container and k3s state survives via | |
| # /var/lib/rancher/k3s. Use `docker exec ... kubectl` to read | |
| # the persistent log directly from each sandbox pod, with no | |
| # dependency on the host's openshell metadata. | |
| echo "=== final-snapshot: docker containers:" | |
| docker ps --format '{{.Names}}\t{{.Status}}' 2>&1 | head -10 | |
| echo "=== final-snapshot: cluster pods:" | |
| docker exec openshell-cluster-nemoclaw kubectl get pods -A --no-headers 2>&1 | head -20 | |
| if [ -f "$HOME/.nemoclaw/sandboxes.json" ]; then | |
| echo "=== final-snapshot: sandboxes.json contents:" | |
| cat "$HOME/.nemoclaw/sandboxes.json" 2>&1 | head -30 | |
| registry_keys=$(jq -r ".sandboxes // {} | keys[]?" "$HOME/.nemoclaw/sandboxes.json" 2>&1) | |
| echo "=== final-snapshot: sandbox names from jq: '$registry_keys'" | |
| for name in $registry_keys; do | |
| case "$name" in *[!a-z0-9_-]*|"") echo "=== final-snapshot: skipping invalid name '$name'"; continue ;; esac | |
| echo "=== final-snapshot: capturing logs for '$name'" | |
| { | |
| printf '\n\n===== FINAL SNAPSHOT: %s =====\n' "$name" | |
| # FIRST attempt: docker exec into the cluster container and | |
| # kubectl-exec into the sandbox pod. This works even when | |
| # the host openshell client is broken post-TC-SBX-06 because | |
| # docker (and k3s inside the cluster) survive the gateway | |
| # docker-kill via auto-restart + persistent k3s state. | |
| pod_ns_name=$(docker exec openshell-cluster-nemoclaw kubectl get pods -A --no-headers 2>/dev/null | awk -v n="$name" '$2==n {print $1"/"$2; exit}') | |
| if [ -n "$pod_ns_name" ]; then | |
| echo "(found pod $pod_ns_name for $name)" | |
| pod_ns="${pod_ns_name%%/*}" | |
| pod_name="${pod_ns_name##*/}" | |
| k_out=$(mktemp) | |
| docker exec openshell-cluster-nemoclaw kubectl exec -n "$pod_ns" "$pod_name" -- bash -c ' | |
| for f in /sandbox/.openclaw/logs/gateway-persistent.log /tmp/gateway.log /tmp/openclaw-*/openclaw-*.log; do | |
| [ -f "$f" ] || continue | |
| printf "\n----- %s (size=%s) -----\n" "$f" "$(stat -c%s "$f" 2>/dev/null || echo ?)" | |
| cat -- "$f" 2>/dev/null || true | |
| done | |
| ' >"$k_out" 2>&1 | |
| k_rc=$? | |
| echo "(kubectl exec rc=$k_rc size=$(wc -c <"$k_out"))" | |
| tail -c 500000 "$k_out" | |
| rm -f "$k_out" | |
| else | |
| echo "(no kubectl pod found matching '$name')" | |
| fi | |
| # Existing fallbacks (raw ssh + nemoclaw logs) preserved | |
| # below in case the docker/kubectl path also fails — they | |
| # provide complementary coverage during transient states. | |
| ssh_cfg="/tmp/sshcfg-final-${name}.tmp" | |
| if openshell sandbox ssh-config "$name" >"$ssh_cfg" 2>&1 && [ -s "$ssh_cfg" ]; then | |
| ssh_out=$(mktemp) | |
| ssh -F "$ssh_cfg" \ | |
| -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ | |
| -o ConnectTimeout=10 -o LogLevel=ERROR \ | |
| "openshell-${name}" \ | |
| 'for f in /sandbox/.openclaw/logs/gateway-persistent.log \ | |
| /tmp/gateway.log \ | |
| /tmp/openclaw-*/openclaw-*.log; do | |
| [ -f "$f" ] || continue | |
| printf "\n----- %s (size=%s) -----\n" "$f" "$(stat -c%s "$f" 2>/dev/null || echo ?)" | |
| cat -- "$f" 2>/dev/null || true | |
| done' >"$ssh_out" 2>&1 | |
| ssh_rc=$? | |
| tail -c 500000 "$ssh_out" | |
| rm -f "$ssh_out" | |
| [ "$ssh_rc" -eq 0 ] || echo "(direct ssh exited rc=$ssh_rc)" | |
| else | |
| echo "(openshell sandbox ssh-config failed for $name)" | |
| # Fallback to nemoclaw logs (less reliable, but try anything) | |
| if command -v nemoclaw >/dev/null 2>&1; then | |
| nm_out=$(mktemp) | |
| nemoclaw "$name" logs >"$nm_out" 2>&1 | |
| echo "(nemoclaw logs rc=$? size=$(wc -c <"$nm_out"))" | |
| tail -c 500000 "$nm_out" | |
| rm -f "$nm_out" | |
| fi | |
| fi | |
| rm -f "$ssh_cfg" | |
| } >> "docker-logs/sandbox-${name}.log" | |
| done | |
| else | |
| echo "=== final-snapshot: sandboxes.json not found at $HOME/.nemoclaw/sandboxes.json" | |
| fi | |
| # Cap each log file at 5MB by keeping only the last 5MB — useful | |
| # content (real gateway events) is mixed throughout, so tail-trim | |
| # is fine for diagnostic purposes. | |
| for f in docker-logs/*.log; do | |
| [ -f "$f" ] || continue | |
| sz=$(stat -c%s "$f" 2>/dev/null || stat -f%z "$f" 2>/dev/null || echo 0) | |
| if [ "$sz" -gt 5242880 ]; then | |
| tail -c 5242880 "$f" > "${f}.tail" && mv "${f}.tail" "$f" | |
| fi | |
| done | |
| ls -la docker-logs/ 2>&1 | head -20 || true | |
| du -sh docker-logs/ 2>&1 || true | |
| - name: Upload sandbox gateway logs on failure | |
| if: failure() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: sandbox-operations-docker-logs | |
| path: docker-logs/ | |
| if-no-files-found: ignore | |
| - name: Upload test log on failure | |
| if: failure() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: sandbox-operations-test-log | |
| path: test-sandbox-operations-*.log | |
| if-no-files-found: ignore | |
| # ── Inference routing (credential isolation + error classification) ── | |
| # TC-INF-05: real API key absent from sandbox env/process/filesystem | |
| # TC-INF-06: invalid API key → classified credential error (PR-safe) | |
| # TC-INF-07: unreachable endpoint → classified transport error (PR-safe) | |
| inference-routing-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',inference-routing-e2e,')) | |
| uses: ./.github/workflows/e2e-script.yaml | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| script: test/e2e/test-inference-routing.sh | |
| timeout_minutes: 30 | |
| artifact_name: "inference-routing-test-log" | |
| artifact_path: "test-inference-routing-*.log" | |
| env_json: '{"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE":"1","NEMOCLAW_NON_INTERACTIVE":"1","NEMOCLAW_POLICY_TIER":"open"}' | |
| nvidia_api_key: true | |
| secrets: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }} | |
| openclaw-inference-switch-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',openclaw-inference-switch-e2e,')) | |
| uses: ./.github/workflows/e2e-script.yaml | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| script: test/e2e/test-openclaw-inference-switch.sh | |
| artifact_name: "openclaw-inference-switch-install-log" | |
| artifact_path: "/tmp/nemoclaw-e2e-openclaw-inference-switch-install.log" | |
| env_json: '{"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE":"1","NEMOCLAW_NON_INTERACTIVE":"1","NEMOCLAW_RECREATE_SANDBOX":"1","NEMOCLAW_SANDBOX_NAME":"e2e-openclaw-inference-switch"}' | |
| nvidia_api_key: true | |
| github_token: true | |
| secrets: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }} | |
| network-policy-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',network-policy-e2e,')) | |
| uses: ./.github/workflows/e2e-script.yaml | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| script: test/e2e/test-network-policy.sh | |
| artifact_name: "network-policy-test-log" | |
| artifact_path: "test-network-policy-*.log" | |
| env_json: '{"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE":"1","NEMOCLAW_NON_INTERACTIVE":"1","NEMOCLAW_POLICY_TIER":"restricted","NEMOCLAW_RECREATE_SANDBOX":"1"}' | |
| nvidia_api_key: true | |
| secrets: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }} | |
| state-backup-restore-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',state-backup-restore-e2e,')) | |
| uses: ./.github/workflows/e2e-script.yaml | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| script: test/e2e/test-state-backup-restore.sh | |
| timeout_minutes: 60 | |
| artifact_name: "state-backup-restore-test-log" | |
| artifact_path: "test-state-backup-restore-*.log" | |
| env_json: '{"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE":"1","NEMOCLAW_NON_INTERACTIVE":"1"}' | |
| nvidia_api_key: true | |
| secrets: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }} | |
| tunnel-lifecycle-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',tunnel-lifecycle-e2e,')) | |
| uses: ./.github/workflows/e2e-script.yaml | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| script: test/e2e/test-tunnel-lifecycle.sh | |
| timeout_minutes: 60 | |
| artifact_name: "tunnel-lifecycle-test-log" | |
| artifact_path: "test-tunnel-lifecycle-*.log" | |
| env_json: '{"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE":"1","NEMOCLAW_NON_INTERACTIVE":"1"}' | |
| nvidia_api_key: true | |
| secrets: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }} | |
| diagnostics-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',diagnostics-e2e,')) | |
| uses: ./.github/workflows/e2e-script.yaml | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| script: test/e2e/test-diagnostics.sh | |
| artifact_name: "diagnostics-test-log" | |
| artifact_path: "test-diagnostics-*.log" | |
| env_json: '{"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE":"1","NEMOCLAW_NON_INTERACTIVE":"1","NEMOCLAW_RECREATE_SANDBOX":"1"}' | |
| nvidia_api_key: true | |
| secrets: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }} | |
| credential-migration-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',credential-migration-e2e,')) | |
| uses: ./.github/workflows/e2e-script.yaml | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| script: test/e2e/test-credential-migration.sh | |
| timeout_minutes: 30 | |
| artifact_name: "install-log-credential-migration" | |
| artifact_path: "/tmp/nemoclaw-e2e-install.log" | |
| env_json: '{"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE":"1","NEMOCLAW_NON_INTERACTIVE":"1","NEMOCLAW_RECREATE_SANDBOX":"1","NEMOCLAW_SANDBOX_NAME":"e2e-cred-migration"}' | |
| nvidia_api_key: true | |
| github_token: true | |
| secrets: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }} | |
| snapshot-commands-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',snapshot-commands-e2e,')) | |
| uses: ./.github/workflows/e2e-script.yaml | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| script: test/e2e/test-snapshot-commands.sh | |
| timeout_minutes: 30 | |
| artifact_name: "snapshot-commands-install-log" | |
| artifact_path: "/tmp/nemoclaw-e2e-install.log" | |
| env_json: '{"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE":"1","NEMOCLAW_NON_INTERACTIVE":"1","NEMOCLAW_SANDBOX_NAME":"e2e-snapshot"}' | |
| nvidia_api_key: true | |
| github_token: true | |
| secrets: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }} | |
| shields-config-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',shields-config-e2e,')) | |
| uses: ./.github/workflows/e2e-script.yaml | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| script: test/e2e/test-shields-config.sh | |
| timeout_minutes: 30 | |
| artifact_name: "shields-config-install-log" | |
| artifact_path: "/tmp/nemoclaw-e2e-shields-install.log" | |
| env_json: '{"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE":"1","NEMOCLAW_NON_INTERACTIVE":"1","NEMOCLAW_SANDBOX_NAME":"e2e-shields"}' | |
| nvidia_api_key: true | |
| github_token: true | |
| secrets: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }} | |
| vm-driver-privileged-exec-routing-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',vm-driver-privileged-exec-routing-e2e,')) | |
| uses: ./.github/workflows/e2e-script.yaml | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| script: test/e2e/test-vm-driver-privileged-exec-routing.sh | |
| timeout_minutes: 10 | |
| artifact_name: "vm-driver-privileged-exec-routing-log" | |
| artifact_path: "/tmp/nemoclaw-vm-driver-privileged-exec-routing-build.log" | |
| env_json: '{"NEMOCLAW_NON_INTERACTIVE":"1"}' | |
| secrets: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }} | |
| rebuild-openclaw-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',rebuild-openclaw-e2e,')) | |
| uses: ./.github/workflows/e2e-script.yaml | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| script: test/e2e/test-rebuild-openclaw.sh | |
| timeout_minutes: 60 | |
| artifact_name: "rebuild-openclaw-install-log" | |
| artifact_path: "/tmp/nemoclaw-e2e-install.log" | |
| env_json: '{"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE":"1","NEMOCLAW_NON_INTERACTIVE":"1","NEMOCLAW_SANDBOX_NAME":"e2e-rebuild-oc"}' | |
| nvidia_api_key: true | |
| github_token: true | |
| secrets: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }} | |
| upgrade-stale-sandbox-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',upgrade-stale-sandbox-e2e,')) | |
| uses: ./.github/workflows/e2e-script.yaml | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| script: test/e2e/test-upgrade-stale-sandbox.sh | |
| timeout_minutes: 60 | |
| artifact_name: "upgrade-stale-sandbox-logs" | |
| artifact_path: | | |
| /tmp/nemoclaw-e2e-old-install.log | |
| /tmp/nemoclaw-e2e-upgrade-install.log | |
| env_json: '{"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE":"1","NEMOCLAW_NON_INTERACTIVE":"1","NEMOCLAW_SANDBOX_NAME":"e2e-upgrade-stale"}' | |
| nvidia_api_key: true | |
| github_token: true | |
| secrets: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }} | |
| openshell-gateway-upgrade-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && | |
| (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',openshell-gateway-upgrade-e2e,')) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| - name: Setup Node | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.0.0 | |
| with: | |
| node-version: "22" | |
| - name: Run OpenShell gateway upgrade E2E test | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| NEMOCLAW_NON_INTERACTIVE: "1" | |
| NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1" | |
| run: bash test/e2e/test-openshell-gateway-upgrade.sh | |
| - name: Upload gateway upgrade logs on failure | |
| if: failure() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: openshell-gateway-upgrade-logs | |
| path: | | |
| /tmp/nemoclaw-e2e-openshell-gateway-upgrade.log | |
| /tmp/nemoclaw-e2e-openshell-gateway-install.log | |
| /tmp/nemoclaw-e2e-openshell-gateway-old-install.log | |
| /tmp/nemoclaw-e2e-openshell-gateway-current-install.log | |
| /tmp/nemoclaw-e2e-openshell-gateway-start.log | |
| /tmp/nemoclaw-e2e-openshell-gateway-process.log | |
| /tmp/nemoclaw-e2e-openshell-gateway-compatible-mock.log | |
| if-no-files-found: ignore | |
| # ── Hermes rebuild upgrade E2E ────────────────────────────── | |
| # Same upgrade scenario as OpenClaw but for Hermes Agent. | |
| rebuild-hermes-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',rebuild-hermes-e2e,')) | |
| uses: ./.github/workflows/e2e-script.yaml | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| script: test/e2e/test-rebuild-hermes.sh | |
| timeout_minutes: 60 | |
| artifact_name: "rebuild-hermes-install-log" | |
| artifact_path: "/tmp/nemoclaw-e2e-install.log" | |
| env_json: '{"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE":"1","NEMOCLAW_AGENT":"hermes","NEMOCLAW_NON_INTERACTIVE":"1","NEMOCLAW_SANDBOX_NAME":"e2e-rebuild-hm"}' | |
| nvidia_api_key: true | |
| github_token: true | |
| secrets: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }} | |
| rebuild-hermes-stale-base-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',rebuild-hermes-stale-base-e2e,')) | |
| uses: ./.github/workflows/e2e-script.yaml | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| script: test/e2e/test-rebuild-hermes.sh | |
| timeout_minutes: 60 | |
| artifact_name: "rebuild-hermes-stale-base-install-log" | |
| artifact_path: "/tmp/nemoclaw-e2e-install.log" | |
| env_json: '{"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE":"1","NEMOCLAW_AGENT":"hermes","NEMOCLAW_HERMES_STALE_BASE_REBUILD_E2E":"1","NEMOCLAW_NON_INTERACTIVE":"1","NEMOCLAW_SANDBOX_NAME":"e2e-rebuild-hm-base"}' | |
| nvidia_api_key: true | |
| github_token: true | |
| secrets: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }} | |
| double-onboard-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && | |
| (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',double-onboard-e2e,')) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 90 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| persist-credentials: false | |
| - name: Install NemoClaw | |
| env: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| NEMOCLAW_NON_INTERACTIVE: "1" | |
| NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1" | |
| run: bash install.sh --non-interactive --yes-i-accept-third-party-software | |
| - name: Run double onboard E2E test | |
| env: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| NEMOCLAW_NON_INTERACTIVE: "1" | |
| NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1" | |
| run: | | |
| [ -f "$HOME/.bashrc" ] && source "$HOME/.bashrc" 2>/dev/null || true | |
| export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" | |
| [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" | |
| [ -d "$HOME/.local/bin" ] && [[ ":$PATH:" != *":$HOME/.local/bin:"* ]] && export PATH="$HOME/.local/bin:$PATH" | |
| bash test/e2e/test-double-onboard.sh | |
| - name: Upload test log on failure | |
| if: failure() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: double-onboard-test-log | |
| path: test-double-onboard-*.log | |
| if-no-files-found: ignore | |
| # ── Onboard Repair E2E ───────────────────────────────────── | |
| onboard-repair-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && | |
| (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',onboard-repair-e2e,')) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| persist-credentials: false | |
| - name: Install NemoClaw | |
| env: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| NEMOCLAW_NON_INTERACTIVE: "1" | |
| NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1" | |
| run: bash install.sh --non-interactive --yes-i-accept-third-party-software | |
| - name: Run onboard repair E2E test | |
| env: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| NEMOCLAW_NON_INTERACTIVE: "1" | |
| NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1" | |
| run: | | |
| [ -f "$HOME/.bashrc" ] && source "$HOME/.bashrc" 2>/dev/null || true | |
| export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" | |
| [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" | |
| [ -d "$HOME/.local/bin" ] && [[ ":$PATH:" != *":$HOME/.local/bin:"* ]] && export PATH="$HOME/.local/bin:$PATH" | |
| bash test/e2e/test-onboard-repair.sh | |
| - name: Upload test log on failure | |
| if: failure() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: onboard-repair-test-log | |
| path: test-onboard-repair-*.log | |
| if-no-files-found: ignore | |
| # ── Onboard Resume E2E ───────────────────────────────────── | |
| onboard-resume-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && | |
| (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',onboard-resume-e2e,')) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| persist-credentials: false | |
| - name: Install NemoClaw | |
| env: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| NEMOCLAW_NON_INTERACTIVE: "1" | |
| NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1" | |
| run: bash install.sh --non-interactive --yes-i-accept-third-party-software | |
| - name: Run onboard resume E2E test | |
| env: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| NEMOCLAW_NON_INTERACTIVE: "1" | |
| NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1" | |
| run: | | |
| [ -f "$HOME/.bashrc" ] && source "$HOME/.bashrc" 2>/dev/null || true | |
| export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" | |
| [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" | |
| [ -d "$HOME/.local/bin" ] && [[ ":$PATH:" != *":$HOME/.local/bin:"* ]] && export PATH="$HOME/.local/bin:$PATH" | |
| bash test/e2e/test-onboard-resume.sh | |
| - name: Upload test log on failure | |
| if: failure() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: onboard-resume-test-log | |
| path: test-onboard-resume-*.log | |
| if-no-files-found: ignore | |
| # -- Onboard Negative Paths E2E ------------------------------- | |
| onboard-negative-paths-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && | |
| (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',onboard-negative-paths-e2e,')) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 75 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| persist-credentials: false | |
| - name: Install NemoClaw | |
| env: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| NEMOCLAW_NON_INTERACTIVE: "1" | |
| NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1" | |
| run: bash install.sh --non-interactive --yes-i-accept-third-party-software | |
| - name: Run onboard negative-path E2E test | |
| env: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| NEMOCLAW_NON_INTERACTIVE: "1" | |
| NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1" | |
| run: | | |
| set -euo pipefail | |
| [ -f "$HOME/.bashrc" ] && source "$HOME/.bashrc" 2>/dev/null || true | |
| export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" | |
| [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" | |
| [ -d "$HOME/.local/bin" ] && [[ ":$PATH:" != *":$HOME/.local/bin:"* ]] && export PATH="$HOME/.local/bin:$PATH" | |
| bash test/e2e/test-onboard-negative-paths.sh | |
| - name: Upload test log on failure | |
| if: failure() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: onboard-negative-paths-test-log | |
| path: /tmp/nemoclaw-e2e-onboard-negative-paths.log | |
| if-no-files-found: ignore | |
| # ── Runtime Overrides E2E ────────────────────────────────── | |
| runtime-overrides-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && | |
| (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',runtime-overrides-e2e,')) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| persist-credentials: false | |
| - name: Install NemoClaw | |
| env: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| NEMOCLAW_NON_INTERACTIVE: "1" | |
| NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1" | |
| run: bash install.sh --non-interactive --yes-i-accept-third-party-software | |
| - name: Run runtime overrides E2E test | |
| env: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| NEMOCLAW_NON_INTERACTIVE: "1" | |
| NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1" | |
| run: | | |
| [ -f "$HOME/.bashrc" ] && source "$HOME/.bashrc" 2>/dev/null || true | |
| export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" | |
| [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" | |
| [ -d "$HOME/.local/bin" ] && [[ ":$PATH:" != *":$HOME/.local/bin:"* ]] && export PATH="$HOME/.local/bin:$PATH" | |
| bash test/e2e/test-runtime-overrides.sh | |
| - name: Upload test log on failure | |
| if: failure() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: runtime-overrides-test-log | |
| path: test-runtime-overrides-*.log | |
| if-no-files-found: ignore | |
| # ── Credential Sanitization E2E ──────────────────────────── | |
| # Requires a running sandbox. Bootstraps via install.sh then runs tests. | |
| credential-sanitization-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && | |
| (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',credential-sanitization-e2e,')) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| - name: Install NemoClaw and onboard sandbox | |
| env: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| NEMOCLAW_NON_INTERACTIVE: "1" | |
| NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1" | |
| NEMOCLAW_SANDBOX_NAME: "e2e-test" | |
| run: bash install.sh --non-interactive --yes-i-accept-third-party-software | |
| - name: Run credential sanitization E2E test | |
| env: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| NEMOCLAW_NON_INTERACTIVE: "1" | |
| NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1" | |
| NEMOCLAW_SANDBOX_NAME: "e2e-test" | |
| run: | | |
| # shellcheck source=/dev/null | |
| [ -f "$HOME/.bashrc" ] && source "$HOME/.bashrc" 2>/dev/null || true | |
| export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" | |
| [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" | |
| [ -d "$HOME/.local/bin" ] && [[ ":$PATH:" != *":$HOME/.local/bin:"* ]] && export PATH="$HOME/.local/bin:$PATH" | |
| bash test/e2e/test-credential-sanitization.sh | |
| - name: Upload test log on failure | |
| if: failure() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: credential-sanitization-test-log | |
| path: test-credential-sanitization-*.log | |
| if-no-files-found: ignore | |
| # ── Telegram Injection E2E ───────────────────────────────── | |
| # Requires a running sandbox. Bootstraps via install.sh then runs tests. | |
| telegram-injection-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && | |
| (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',telegram-injection-e2e,')) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| - name: Install NemoClaw and onboard sandbox | |
| env: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| NEMOCLAW_NON_INTERACTIVE: "1" | |
| NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1" | |
| NEMOCLAW_SANDBOX_NAME: "e2e-test" | |
| run: bash install.sh --non-interactive --yes-i-accept-third-party-software | |
| - name: Run telegram injection E2E test | |
| env: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| NEMOCLAW_NON_INTERACTIVE: "1" | |
| NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1" | |
| NEMOCLAW_SANDBOX_NAME: "e2e-test" | |
| run: | | |
| # shellcheck source=/dev/null | |
| [ -f "$HOME/.bashrc" ] && source "$HOME/.bashrc" 2>/dev/null || true | |
| export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" | |
| [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" | |
| [ -d "$HOME/.local/bin" ] && [[ ":$PATH:" != *":$HOME/.local/bin:"* ]] && export PATH="$HOME/.local/bin:$PATH" | |
| bash test/e2e/test-telegram-injection.sh | |
| - name: Upload test log on failure | |
| if: failure() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: telegram-injection-test-log | |
| path: test-telegram-injection-*.log | |
| if-no-files-found: ignore | |
| # Remove this job — and the matching notify-on-failure entry — in the | |
| # same PR that deletes cluster-image-patch.ts when the OpenShell | |
| # roadmap migration off k3s (NVIDIA/OpenShell#873) lands. | |
| # ── Docker 26+ overlayfs nested-mount auto-fix (#2481) ────── | |
| # TEMPORARY: validates the auto-fix in src/lib/cluster-image-patch.ts. | |
| overlayfs-autofix-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',overlayfs-autofix-e2e,')) | |
| uses: ./.github/workflows/e2e-script.yaml | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| script: test/e2e/test-overlayfs-autofix.sh | |
| artifact_name: "overlayfs-autofix-logs" | |
| artifact_path: | | |
| /tmp/nemoclaw-e2e-install.log | |
| /tmp/nemoclaw-e2e-onboard-positive.log | |
| /tmp/nemoclaw-e2e-onboard-negative.log | |
| env_json: '{"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE":"1","NEMOCLAW_NON_INTERACTIVE":"1","NEMOCLAW_SANDBOX_NAME":"e2e-overlayfs"}' | |
| nvidia_api_key: true | |
| github_token: true | |
| secrets: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }} | |
| device-auth-health-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',device-auth-health-e2e,')) | |
| uses: ./.github/workflows/e2e-script.yaml | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| script: test/e2e/test-device-auth-health.sh | |
| timeout_minutes: 30 | |
| artifact_name: "device-auth-health-install-log" | |
| artifact_path: "/tmp/nemoclaw-e2e-health-install.log" | |
| env_json: '{"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE":"1","NEMOCLAW_NON_INTERACTIVE":"1","NEMOCLAW_RECREATE_SANDBOX":"1","NEMOCLAW_SANDBOX_NAME":"e2e-health-auth"}' | |
| nvidia_api_key: true | |
| github_token: true | |
| secrets: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }} | |
| launchable-smoke-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && | |
| (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',launchable-smoke-e2e,')) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| - name: Run launchable install-flow smoke test | |
| env: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| NEMOCLAW_NON_INTERACTIVE: "1" | |
| NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1" | |
| NEMOCLAW_SANDBOX_NAME: "e2e-launchable" | |
| NEMOCLAW_RECREATE_SANDBOX: "1" | |
| SKIP_DOCKER_PULL: "1" | |
| GITHUB_TOKEN: ${{ github.token }} | |
| run: bash test/e2e/test-launchable-smoke.sh | |
| - name: Upload install log on failure | |
| if: failure() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: launchable-smoke-install-log | |
| path: /tmp/nemoclaw-launchable-install.log | |
| if-no-files-found: ignore | |
| - name: Upload onboard log on failure | |
| if: failure() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: launchable-smoke-onboard-log | |
| path: /tmp/nemoclaw-launchable-onboard.log | |
| if-no-files-found: ignore | |
| - name: Upload test log on failure | |
| if: failure() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: launchable-smoke-test-log | |
| path: /tmp/nemoclaw-launchable-test.log | |
| if-no-files-found: ignore | |
| # ── GPU E2E (Ollama local inference) ────────────────────────── | |
| # Runs on an NVKS ephemeral GPU runner (RTX Pro 6000, 36 GB VRAM). | |
| # Each job gets a fresh VM — no state leakage between runs. | |
| gpu-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && | |
| vars.GPU_E2E_ENABLED == 'true' && | |
| (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',gpu-e2e,')) | |
| runs-on: linux-amd64-gpu-rtxpro6000-latest-1 | |
| timeout-minutes: 30 | |
| env: | |
| NEMOCLAW_NON_INTERACTIVE: "1" | |
| NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1" | |
| NEMOCLAW_SANDBOX_NAME: "e2e-gpu-ollama" | |
| NEMOCLAW_RECREATE_SANDBOX: "1" | |
| NEMOCLAW_PROVIDER: "ollama" | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| - 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: gpu-e2e-test-log | |
| path: /tmp/nemoclaw-gpu-e2e-test.log | |
| if-no-files-found: ignore | |
| # ── GPU Double-Onboard E2E (Ollama token consistency) ──────── | |
| # Reproduces issue #2553: re-onboard with Ollama must not leave the | |
| # proxy running with a different token than what's persisted to disk. | |
| # Runs on its own ephemeral VM — no dependency on gpu-e2e. | |
| gpu-double-onboard-e2e: | |
| if: >- | |
| github.repository == 'NVIDIA/NemoClaw' && | |
| vars.GPU_E2E_ENABLED == 'true' && | |
| (github.event_name != 'workflow_dispatch' || | |
| inputs.jobs == '' || | |
| contains(format(',{0},', inputs.jobs), ',gpu-double-onboard-e2e,')) | |
| runs-on: linux-amd64-gpu-rtxpro6000-latest-1 | |
| timeout-minutes: 30 | |
| env: | |
| NEMOCLAW_NON_INTERACTIVE: "1" | |
| NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1" | |
| NEMOCLAW_SANDBOX_NAME: "e2e-gpu-double-onboard" | |
| NEMOCLAW_RECREATE_SANDBOX: "1" | |
| NEMOCLAW_PROVIDER: "ollama" | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| ref: ${{ inputs.target_ref || github.ref }} | |
| - 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 double-onboard E2E test | |
| run: bash test/e2e/test-gpu-double-onboard.sh | |
| - name: Upload install log on failure | |
| if: failure() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: gpu-double-onboard-install-log | |
| path: /tmp/nemoclaw-gpu-double-onboard-install.log | |
| if-no-files-found: ignore | |
| - name: Upload re-onboard log on failure | |
| if: failure() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: gpu-double-onboard-reonboard-log | |
| path: /tmp/nemoclaw-gpu-double-onboard-reonboard.log | |
| if-no-files-found: ignore | |
| - name: Upload test log on failure | |
| if: failure() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: gpu-double-onboard-test-log | |
| path: /tmp/nemoclaw-gpu-double-onboard-test.log | |
| if-no-files-found: ignore | |
| notify-on-failure: | |
| runs-on: ubuntu-latest | |
| needs: | |
| [ | |
| cloud-e2e, | |
| cloud-onboard-e2e, | |
| cloud-inference-e2e, | |
| skill-agent-e2e, | |
| docs-validation-e2e, | |
| messaging-providers-e2e, | |
| openclaw-slack-pairing-e2e, | |
| openclaw-tui-chat-correlation-e2e, | |
| issue-3600-gpu-proof-optional-e2e, | |
| openclaw-discord-pairing-e2e, | |
| issue-4462-scope-upgrade-approval-e2e, | |
| issue-4462-gateway-pinned-approval-characterization-e2e, | |
| messaging-compatible-endpoint-e2e, | |
| channels-add-remove-e2e, | |
| channels-stop-start-e2e, | |
| brave-search-e2e, | |
| kimi-inference-compat-e2e, | |
| bedrock-runtime-compatible-anthropic-e2e, | |
| token-rotation-e2e, | |
| sandbox-survival-e2e, | |
| issue-2478-crash-loop-recovery-e2e, | |
| hermes-e2e, | |
| hermes-dashboard-e2e, | |
| hermes-root-entrypoint-smoke-e2e, | |
| openclaw-onboard-security-posture-e2e, | |
| hermes-onboard-security-posture-e2e, | |
| hermes-inference-switch-e2e, | |
| hermes-discord-e2e, | |
| hermes-slack-e2e, | |
| sandbox-operations-e2e, | |
| inference-routing-e2e, | |
| openclaw-inference-switch-e2e, | |
| network-policy-e2e, | |
| state-backup-restore-e2e, | |
| tunnel-lifecycle-e2e, | |
| diagnostics-e2e, | |
| credential-migration-e2e, | |
| snapshot-commands-e2e, | |
| shields-config-e2e, | |
| vm-driver-privileged-exec-routing-e2e, | |
| rebuild-openclaw-e2e, | |
| upgrade-stale-sandbox-e2e, | |
| openshell-gateway-upgrade-e2e, | |
| rebuild-hermes-e2e, | |
| rebuild-hermes-stale-base-e2e, | |
| double-onboard-e2e, | |
| onboard-repair-e2e, | |
| onboard-resume-e2e, | |
| onboard-negative-paths-e2e, | |
| runtime-overrides-e2e, | |
| credential-sanitization-e2e, | |
| telegram-injection-e2e, | |
| overlayfs-autofix-e2e, | |
| device-auth-health-e2e, | |
| launchable-smoke-e2e, | |
| gpu-e2e, | |
| gpu-double-onboard-e2e, | |
| ] | |
| if: ${{ always() && github.event_name == 'schedule' && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) }} | |
| permissions: | |
| issues: write | |
| steps: | |
| - name: Create or update failure issue | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; | |
| const title = 'Nightly E2E failed'; | |
| const needs = ${{ toJSON(needs) }}; | |
| const failed = Object.entries(needs).filter(([, v]) => v.result === 'failure').map(([k]) => k); | |
| const cancelled = Object.entries(needs).filter(([, v]) => v.result === 'cancelled').map(([k]) => k); | |
| const summary = [ | |
| failed.length ? `**Failed:** ${failed.join(', ')}` : '', | |
| cancelled.length ? `**Cancelled:** ${cancelled.join(', ')}` : '', | |
| ].filter(Boolean).join('\n'); | |
| 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${summary}\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${summary}\n**Artifacts:** Check the run artifacts for install/test logs (artifact names vary by job).`, | |
| labels: ['bug', 'CI/CD'], | |
| }); | |
| } | |
| report-to-pr: | |
| runs-on: ubuntu-latest | |
| needs: | |
| [ | |
| cloud-e2e, | |
| cloud-onboard-e2e, | |
| cloud-inference-e2e, | |
| skill-agent-e2e, | |
| docs-validation-e2e, | |
| messaging-providers-e2e, | |
| openclaw-slack-pairing-e2e, | |
| openclaw-tui-chat-correlation-e2e, | |
| issue-3600-gpu-proof-optional-e2e, | |
| openclaw-discord-pairing-e2e, | |
| issue-4462-scope-upgrade-approval-e2e, | |
| issue-4462-gateway-pinned-approval-characterization-e2e, | |
| messaging-compatible-endpoint-e2e, | |
| channels-add-remove-e2e, | |
| channels-stop-start-e2e, | |
| brave-search-e2e, | |
| kimi-inference-compat-e2e, | |
| bedrock-runtime-compatible-anthropic-e2e, | |
| token-rotation-e2e, | |
| sandbox-survival-e2e, | |
| issue-2478-crash-loop-recovery-e2e, | |
| hermes-e2e, | |
| hermes-dashboard-e2e, | |
| hermes-root-entrypoint-smoke-e2e, | |
| openclaw-onboard-security-posture-e2e, | |
| hermes-onboard-security-posture-e2e, | |
| hermes-inference-switch-e2e, | |
| hermes-discord-e2e, | |
| hermes-slack-e2e, | |
| sandbox-operations-e2e, | |
| inference-routing-e2e, | |
| openclaw-inference-switch-e2e, | |
| network-policy-e2e, | |
| state-backup-restore-e2e, | |
| tunnel-lifecycle-e2e, | |
| diagnostics-e2e, | |
| credential-migration-e2e, | |
| snapshot-commands-e2e, | |
| shields-config-e2e, | |
| vm-driver-privileged-exec-routing-e2e, | |
| rebuild-openclaw-e2e, | |
| upgrade-stale-sandbox-e2e, | |
| openshell-gateway-upgrade-e2e, | |
| rebuild-hermes-e2e, | |
| rebuild-hermes-stale-base-e2e, | |
| double-onboard-e2e, | |
| onboard-repair-e2e, | |
| onboard-resume-e2e, | |
| onboard-negative-paths-e2e, | |
| runtime-overrides-e2e, | |
| credential-sanitization-e2e, | |
| telegram-injection-e2e, | |
| overlayfs-autofix-e2e, | |
| device-auth-health-e2e, | |
| launchable-smoke-e2e, | |
| gpu-e2e, | |
| gpu-double-onboard-e2e, | |
| ] | |
| if: ${{ always() && github.event_name == 'workflow_dispatch' }} | |
| permissions: | |
| issues: write | |
| pull-requests: write | |
| steps: | |
| - name: Post E2E results to PR | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| const needs = ${{ toJSON(needs) }}; | |
| const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; | |
| const workflowBranch = context.ref.replace('refs/heads/', ''); | |
| const targetRef = ${{ toJSON(inputs.target_ref) }} || ''; | |
| const prNumberInput = ${{ toJSON(inputs.pr_number) }} || ''; | |
| const displayRef = targetRef || workflowBranch; | |
| const requestedJobs = ${{ toJSON(inputs.jobs) }} || ""; | |
| let prNumber = prNumberInput ? Number.parseInt(prNumberInput, 10) : undefined; | |
| if (!prNumber) { | |
| // Find open PR for this branch. This is the legacy manual-dispatch | |
| // path where the workflow itself is dispatched on the PR branch. | |
| const { data: prs } = await github.rest.pulls.list({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| head: `${context.repo.owner}:${workflowBranch}`, | |
| state: 'open', | |
| }); | |
| if (prs.length === 0) { | |
| core.info(`No open PR found for branch ${workflowBranch} — skipping comment.`); | |
| return; | |
| } | |
| prNumber = prs[0].number; | |
| } | |
| const requested = requestedJobs | |
| .split(',') | |
| .map((job) => job.trim()) | |
| .filter(Boolean); | |
| const requestedSet = new Set(requested); | |
| // Build results table. For selective dispatches, report only the | |
| // requested jobs; otherwise the comment is dominated by expected skips. | |
| const emoji = { success: '✅', failure: '❌', cancelled: '⚠️', skipped: '⏭️' }; | |
| const allEntries = Object.entries(needs).sort(([a], [b]) => a.localeCompare(b)); | |
| const missingRequested = requested.filter((job) => !(job in needs)); | |
| const reportedEntries = requested.length | |
| ? allEntries.filter(([name]) => requestedSet.has(name)) | |
| : allEntries; | |
| const rows = reportedEntries | |
| .sort(([a], [b]) => a.localeCompare(b)) | |
| .map(([name, { result }]) => `| ${name} | ${emoji[result] || '❓'} ${result} |`); | |
| for (const name of missingRequested) { | |
| rows.push(`| ${name} | ❓ not reported |`); | |
| } | |
| const ran = reportedEntries.filter(([, v]) => v.result !== 'skipped'); | |
| const passed = ran.filter(([, v]) => v.result === 'success'); | |
| const failed = ran.filter(([, v]) => v.result === 'failure'); | |
| const skipped = reportedEntries.filter(([, v]) => v.result === 'skipped'); | |
| const status = | |
| failed.length > 0 || missingRequested.length > 0 | |
| ? '❌ Some jobs failed' | |
| : skipped.length > 0 && passed.length === 0 | |
| ? '⚠️ No requested jobs ran' | |
| : '✅ All requested jobs passed'; | |
| const body = [ | |
| `### Selective E2E Results — ${status}`, | |
| '', | |
| `**Run:** [${context.runId}](${runUrl})`, | |
| `**Target ref:** \`${displayRef}\``, | |
| targetRef ? `**Workflow ref:** \`${workflowBranch}\`` : undefined, | |
| requestedJobs ? `**Requested jobs:** \`${requestedJobs}\`` : '**Requested jobs:** all (no filter)', | |
| `**Summary:** ${passed.length} passed, ${failed.length} failed, ${skipped.length} skipped`, | |
| '', | |
| '| Job | Result |', | |
| '|-----|--------|', | |
| ...rows, | |
| '', | |
| failed.length > 0 | |
| ? `> **Failed jobs:** ${failed.map(([k]) => k).join(', ')}. Check [run artifacts](${runUrl}) for logs.` | |
| : '', | |
| missingRequested.length > 0 | |
| ? `> **Missing requested jobs:** ${missingRequested.join(', ')}. The reporting workflow needs to include these jobs.` | |
| : '', | |
| ].filter((line) => line !== undefined).join('\n'); | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| body, | |
| }); | |
| # ── Nightly Scorecard ────────────────────────────────────────────────── | |
| # Aggregates overnight results into a scorecard published to | |
| # $GITHUB_STEP_SUMMARY. Identifies flaky jobs, computes pass/fail/cancel | |
| # breakdowns, and compares trends against the prior day. | |
| # Only runs on schedule (not workflow_dispatch — that uses report-to-pr). | |
| scorecard: | |
| runs-on: ubuntu-latest | |
| needs: | |
| [ | |
| cloud-e2e, | |
| cloud-onboard-e2e, | |
| cloud-inference-e2e, | |
| skill-agent-e2e, | |
| docs-validation-e2e, | |
| messaging-providers-e2e, | |
| openclaw-slack-pairing-e2e, | |
| openclaw-tui-chat-correlation-e2e, | |
| issue-3600-gpu-proof-optional-e2e, | |
| openclaw-discord-pairing-e2e, | |
| issue-4462-scope-upgrade-approval-e2e, | |
| issue-4462-gateway-pinned-approval-characterization-e2e, | |
| messaging-compatible-endpoint-e2e, | |
| channels-add-remove-e2e, | |
| channels-stop-start-e2e, | |
| brave-search-e2e, | |
| kimi-inference-compat-e2e, | |
| bedrock-runtime-compatible-anthropic-e2e, | |
| token-rotation-e2e, | |
| sandbox-survival-e2e, | |
| issue-2478-crash-loop-recovery-e2e, | |
| hermes-e2e, | |
| hermes-dashboard-e2e, | |
| hermes-root-entrypoint-smoke-e2e, | |
| openclaw-onboard-security-posture-e2e, | |
| hermes-onboard-security-posture-e2e, | |
| hermes-inference-switch-e2e, | |
| hermes-discord-e2e, | |
| hermes-slack-e2e, | |
| sandbox-operations-e2e, | |
| inference-routing-e2e, | |
| openclaw-inference-switch-e2e, | |
| network-policy-e2e, | |
| state-backup-restore-e2e, | |
| tunnel-lifecycle-e2e, | |
| diagnostics-e2e, | |
| credential-migration-e2e, | |
| snapshot-commands-e2e, | |
| shields-config-e2e, | |
| vm-driver-privileged-exec-routing-e2e, | |
| rebuild-openclaw-e2e, | |
| upgrade-stale-sandbox-e2e, | |
| openshell-gateway-upgrade-e2e, | |
| rebuild-hermes-e2e, | |
| rebuild-hermes-stale-base-e2e, | |
| double-onboard-e2e, | |
| onboard-repair-e2e, | |
| onboard-resume-e2e, | |
| onboard-negative-paths-e2e, | |
| runtime-overrides-e2e, | |
| credential-sanitization-e2e, | |
| telegram-injection-e2e, | |
| overlayfs-autofix-e2e, | |
| device-auth-health-e2e, | |
| launchable-smoke-e2e, | |
| gpu-e2e, | |
| gpu-double-onboard-e2e, | |
| ] | |
| if: ${{ always() && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') }} | |
| permissions: | |
| actions: read | |
| steps: | |
| - name: Generate nightly scorecard | |
| id: scorecard | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| // ── Config ────────────────────────────────────────────── | |
| const EXCLUDED_JOBS = new Set(['gpu-e2e', 'notify-on-failure', 'report-to-pr', 'scorecard']); | |
| // ── Helpers ───────────────────────────────────────────── | |
| function formatDate(date) { | |
| return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); | |
| } | |
| // ── Gather results from the current run's needs context ─ | |
| const needs = ${{ toJSON(needs) }}; | |
| const today = formatDate(new Date()); | |
| const isDispatch = context.eventName === 'workflow_dispatch'; | |
| const requestedJobsRaw = isDispatch ? '${{ inputs.jobs }}'.trim() : ''; | |
| const requestedJobs = requestedJobsRaw | |
| ? requestedJobsRaw.split(',').map((name) => name.trim()).filter(Boolean) | |
| : []; | |
| const isSelectiveDispatch = isDispatch && requestedJobs.length > 0; | |
| const runMode = isSelectiveDispatch | |
| ? 'Selective dispatch' | |
| : isDispatch | |
| ? 'Manual full run' | |
| : 'Scheduled full nightly'; | |
| const entries = Object.entries(needs).filter(([name]) => !EXCLUDED_JOBS.has(name)); | |
| let success = 0; | |
| let failure = 0; | |
| let cancelled = 0; | |
| let skipped = 0; | |
| for (const [, { result }] of entries) { | |
| if (result === 'success') success++; | |
| else if (result === 'failure') failure++; | |
| else if (result === 'cancelled') cancelled++; | |
| else if (result === 'skipped') skipped++; | |
| } | |
| const total = entries.length; | |
| const ran = total - skipped; | |
| const perfect = failure === 0 && cancelled === 0 && ran > 0; | |
| // ── Identify failed jobs ──────────────────────────────── | |
| const failedJobs = entries | |
| .filter(([, { result }]) => result === 'failure') | |
| .map(([name]) => name) | |
| .sort(); | |
| // ── Fetch prior-day run for trend comparison ──────────── | |
| let trendLine = ''; | |
| if (isSelectiveDispatch) { | |
| trendLine = 'Trend: ⊘ Not shown for selective dispatches'; | |
| } else { | |
| try { | |
| const WORKFLOW_FILE = 'nightly-e2e.yaml'; | |
| const now = new Date(); | |
| const since48h = new Date(now.getTime() - 48 * 60 * 60 * 1000).toISOString(); | |
| const since24h = new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString(); | |
| const priorRuns = []; | |
| for (let page = 1; page <= 10 && priorRuns.length === 0; page++) { | |
| const { data } = await github.rest.actions.listWorkflowRuns({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| workflow_id: WORKFLOW_FILE, | |
| created: `>=${since48h}`, | |
| per_page: 100, | |
| page, | |
| }); | |
| priorRuns.push( | |
| ...data.workflow_runs.filter(r => | |
| r.status === 'completed' && | |
| r.event === 'schedule' && | |
| new Date(r.created_at) < new Date(since24h) | |
| ) | |
| ); | |
| if (data.workflow_runs.length < 100) break; | |
| } | |
| if (priorRuns.length > 0) { | |
| // Check the most recent prior run | |
| const priorRun = priorRuns[0]; | |
| const priorPerfect = priorRun.conclusion === 'success'; | |
| if (perfect && priorPerfect) { | |
| trendLine = 'Trend: ➡️ Stable (perfect both days)'; | |
| } else if (perfect && !priorPerfect) { | |
| trendLine = 'Trend: ↗️ Improving (yesterday had failures → today perfect)'; | |
| } else if (!perfect && priorPerfect) { | |
| trendLine = 'Trend: ↘️ Degrading (yesterday perfect → today has failures)'; | |
| } else { | |
| trendLine = 'Trend: ➡️ Stable (failures both days)'; | |
| } | |
| } else { | |
| trendLine = 'Trend: ⊘ No prior-day data for comparison'; | |
| } | |
| } catch (e) { | |
| trendLine = `Trend: ⊘ Could not fetch prior-day data (${e.message})`; | |
| } | |
| } | |
| // ── Build scorecard ───────────────────────────────────── | |
| const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; | |
| const lines = [ | |
| `## 🌅 NemoClaw Nightly Scorecard — ${today}`, | |
| '', | |
| `**Run mode:** ${runMode}`, | |
| ]; | |
| if (isSelectiveDispatch) { | |
| lines.push(`**Requested jobs:** ${requestedJobs.map((name) => `\`${name}\``).join(', ')}`); | |
| } | |
| lines.push( | |
| `**Jobs run:** ${ran} of ${total}`, | |
| ` ✅ ${success} passed`, | |
| ` ❌ ${failure} failed`, | |
| ` ⊘ ${cancelled} cancelled`, | |
| ` ⏭️ ${skipped} skipped`, | |
| ); | |
| if (failedJobs.length > 0) { | |
| lines.push(''); | |
| lines.push('**Failed jobs:**'); | |
| for (const name of failedJobs) { | |
| lines.push(` - \`${name}\``); | |
| } | |
| } | |
| if (perfect) { | |
| lines.push(''); | |
| lines.push('🎉 **All jobs passed!**'); | |
| } | |
| lines.push(''); | |
| lines.push(trendLine); | |
| lines.push(''); | |
| lines.push(`🔗 [Full run details](${runUrl})`); | |
| const scorecard = lines.join('\n'); | |
| core.summary.addRaw(scorecard); | |
| await core.summary.write(); | |
| core.setOutput('scorecard', scorecard); | |
| # ── Optional Slack notification ──────────────────────────── | |
| - name: Post scorecard to Slack | |
| if: ${{ steps.scorecard.outputs.scorecard != '' }} | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| env: | |
| SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} | |
| SCORECARD_TEXT: ${{ steps.scorecard.outputs.scorecard }} | |
| with: | |
| script: | | |
| const webhookUrl = process.env.SLACK_WEBHOOK_URL; | |
| if (!webhookUrl) { | |
| core.info('SLACK_WEBHOOK_URL not configured — skipping Slack notification'); | |
| return; | |
| } | |
| const scorecard = process.env.SCORECARD_TEXT; | |
| // Strip markdown formatting for Slack plain-text rendering | |
| const slackText = scorecard | |
| .replace(/^## /gm, '') | |
| .replace(/\*\*/g, '*') | |
| .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<$2|$1>'); | |
| const resp = await fetch(webhookUrl, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ text: slackText }), | |
| }); | |
| if (!resp.ok) { | |
| core.warning(`Slack webhook returned ${resp.status}: ${await resp.text()}`); | |
| } else { | |
| core.info('Scorecard posted to Slack'); | |
| } |