diff --git a/.github/actions/export-e2e-hosted-inference/action.yaml b/.github/actions/export-e2e-hosted-inference/action.yaml deleted file mode 100644 index 84202ee8917..00000000000 --- a/.github/actions/export-e2e-hosted-inference/action.yaml +++ /dev/null @@ -1,39 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -name: Export E2E hosted inference environment -description: Canonicalize NemoClaw hosted inference secrets and export the shared E2E env contract. -inputs: - nvidia-inference-api-key: - description: Canonical repository-hosted NVIDIA inference credential. - required: false - default: "" - nvidia-api-key: - description: >- - Temporary compatibility alias for legacy E2E lanes that still read - NVIDIA_API_KEY directly. The source-owned inventories and removal - conditions live in tools/e2e-scenarios/hosted-inference-legacy-alias.mts. - required: false - default: "" - export-nvidia-api-key: - description: >- - Emit NVIDIA_API_KEY only for documented legacy consumers that still need - the compatibility alias. Defaults to false so hosted CI inference exports - only NVIDIA_INFERENCE_API_KEY and COMPATIBLE_API_KEY. - required: false - default: "false" - require-hosted-inference: - description: Fail when no nvapi-prefixed hosted inference credential is available. - required: false - default: "true" -runs: - using: composite - steps: - - name: Export hosted inference env - shell: bash - env: - INPUT_NVIDIA_INFERENCE_API_KEY: ${{ inputs.nvidia-inference-api-key }} - INPUT_NVIDIA_API_KEY: ${{ inputs.nvidia-api-key }} - INPUT_EXPORT_NVIDIA_API_KEY: ${{ inputs.export-nvidia-api-key }} - INPUT_REQUIRE_HOSTED_INFERENCE: ${{ inputs.require-hosted-inference }} - run: bash "${GITHUB_ACTION_PATH}/export.sh" diff --git a/.github/actions/export-e2e-hosted-inference/export.sh b/.github/actions/export-e2e-hosted-inference/export.sh deleted file mode 100755 index 50a501a8314..00000000000 --- a/.github/actions/export-e2e-hosted-inference/export.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env bash -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -set -euo pipefail - -canonical="${INPUT_NVIDIA_INFERENCE_API_KEY:-}" -public_alias="${INPUT_NVIDIA_API_KEY:-}" - -# Keep the hosted E2E contract canonical: NVIDIA_INFERENCE_API_KEY is the -# hosted CI source secret and COMPATIBLE_API_KEY is the custom endpoint -# credential. NVIDIA_API_KEY is emitted only when the caller explicitly opts in -# for documented legacy consumers that still assert the older env name. -for value in "${canonical}" "${public_alias}"; do - if [[ "${value}" == *$'\n'* || "${value}" == *$'\r'* ]]; then - echo "::error::Hosted inference credentials must be single-line values." >&2 - exit 1 - fi -done - -if [[ "${canonical}" != nvapi-* && "${public_alias}" == nvapi-* ]]; then - canonical="${public_alias}" -fi -if [[ "${public_alias}" != nvapi-* && "${canonical}" == nvapi-* ]]; then - public_alias="${canonical}" -fi - -if [[ "${INPUT_REQUIRE_HOSTED_INFERENCE:-true}" == "true" ]]; then - if [[ -z "${canonical}" ]]; then - echo "::error::NVIDIA_INFERENCE_API_KEY secret is required for hosted CI inference." >&2 - exit 1 - fi - if [[ "${canonical}" != nvapi-* ]]; then - echo "::error::NVIDIA_INFERENCE_API_KEY must be nvapi-prefixed for hosted CI inference." >&2 - exit 1 - fi -fi - -{ - printf 'NEMOCLAW_E2E_USE_HOSTED_INFERENCE=1\n' - printf 'NEMOCLAW_PROVIDER=custom\n' - printf 'NEMOCLAW_ENDPOINT_URL=https://inference-api.nvidia.com/v1\n' - printf 'NEMOCLAW_MODEL=nvidia/nvidia/nemotron-3-super-v3\n' - printf 'NEMOCLAW_COMPAT_MODEL=nvidia/nvidia/nemotron-3-super-v3\n' - printf 'NEMOCLAW_PREFERRED_API=openai-completions\n' - if [[ -n "${canonical}" ]]; then - printf 'NVIDIA_INFERENCE_API_KEY=%s\n' "${canonical}" - printf 'COMPATIBLE_API_KEY=%s\n' "${canonical}" - fi - if [[ "${INPUT_EXPORT_NVIDIA_API_KEY:-false}" == "true" && -n "${public_alias}" ]]; then - printf 'NVIDIA_API_KEY=%s\n' "${public_alias}" - fi -} >>"${GITHUB_ENV}" diff --git a/.github/workflows/e2e-script.yaml b/.github/workflows/e2e-script.yaml index 3714d942e66..0b729fead78 100644 --- a/.github/workflows/e2e-script.yaml +++ b/.github/workflows/e2e-script.yaml @@ -62,11 +62,6 @@ on: required: false type: boolean default: false - nvidia_api_key_alias: - description: Emit NVIDIA_API_KEY compatibility alias for documented legacy shell consumers. - required: false - type: boolean - default: false brave_api_key: description: Pass the BRAVE_API_KEY secret to the script. required: false @@ -85,8 +80,6 @@ on: secrets: NVIDIA_INFERENCE_API_KEY: required: false - NVIDIA_API_KEY: - required: false BRAVE_API_KEY: required: false DOCKERHUB_USERNAME: @@ -150,7 +143,6 @@ jobs: with: ref: ${{ github.ref }} sparse-checkout: | - .github/actions/export-e2e-hosted-inference .github/actions/run-e2e-script path: workflow-actions persist-credentials: false @@ -222,11 +214,26 @@ jobs: - name: Export hosted CI inference environment if: ${{ inputs.nvidia_api_key }} - uses: ./workflow-actions/.github/actions/export-e2e-hosted-inference - with: - nvidia-inference-api-key: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} - nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }} - export-nvidia-api-key: ${{ inputs.nvidia_api_key_alias }} + env: + NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} + shell: bash + run: | + set -euo pipefail + + if [ -z "${NVIDIA_INFERENCE_API_KEY:-}" ]; then + echo "::error::NVIDIA_INFERENCE_API_KEY secret is required for hosted CI inference; it is withheld for workflow_dispatch target_ref runs." >&2 + exit 1 + fi + + { + printf 'NEMOCLAW_E2E_USE_HOSTED_INFERENCE=1\n' + printf 'NEMOCLAW_PROVIDER=custom\n' + printf 'NEMOCLAW_ENDPOINT_URL=https://inference-api.nvidia.com/v1\n' + printf 'NEMOCLAW_MODEL=nvidia/nvidia/nemotron-3-super-v3\n' + printf 'NEMOCLAW_COMPAT_MODEL=nvidia/nvidia/nemotron-3-super-v3\n' + printf 'NEMOCLAW_PREFERRED_API=openai-completions\n' + printf 'COMPATIBLE_API_KEY=%s\n' "${NVIDIA_INFERENCE_API_KEY}" + } >> "$GITHUB_ENV" - name: Run E2E script uses: ./workflow-actions/.github/actions/run-e2e-script @@ -241,6 +248,7 @@ jobs: env: BRAVE_API_KEY: ${{ inputs.brave_api_key && secrets.BRAVE_API_KEY || '' }} GITHUB_TOKEN: ${{ inputs.github_token && github.token || '' }} + NVIDIA_INFERENCE_API_KEY: ${{ inputs.nvidia_api_key && secrets.NVIDIA_INFERENCE_API_KEY || '' }} TELEGRAM_BOT_TOKEN_REAL: ${{ inputs.messaging_live_secrets && secrets.TELEGRAM_BOT_TOKEN_REAL || '' }} TELEGRAM_CHAT_ID_E2E: ${{ inputs.messaging_live_secrets && secrets.TELEGRAM_CHAT_ID_E2E || '' }} DISCORD_BOT_TOKEN_REAL: ${{ inputs.messaging_live_secrets && secrets.DISCORD_BOT_TOKEN_REAL || '' }} diff --git a/.github/workflows/e2e-vitest-scenarios.yaml b/.github/workflows/e2e-vitest-scenarios.yaml index 03b0313576f..a1e0551e91f 100644 --- a/.github/workflows/e2e-vitest-scenarios.yaml +++ b/.github/workflows/e2e-vitest-scenarios.yaml @@ -245,13 +245,9 @@ jobs: - name: Build CLI run: npm run build:cli - - name: Export hosted inference env - uses: ./.github/actions/export-e2e-hosted-inference - with: - nvidia-inference-api-key: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} - nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }} - name: Run Vitest live E2E scenarios env: + NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} SCENARIO_ID: ${{ matrix.id }} run: | set -euo pipefail @@ -475,12 +471,9 @@ jobs: # available to both Vitest and the bash helpers. run: bash scripts/install-openshell.sh - - name: Export hosted inference env - uses: ./.github/actions/export-e2e-hosted-inference - with: - nvidia-inference-api-key: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} - nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }} - name: Run skill-agent live test + env: + NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} run: | set -euo pipefail export PATH="$HOME/.local/bin:$HOME/.npm-global/bin:$PATH" @@ -577,13 +570,9 @@ jobs: - name: Build CLI run: npm run build:cli - - name: Export hosted inference env - uses: ./.github/actions/export-e2e-hosted-inference - with: - nvidia-inference-api-key: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} - nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }} - export-nvidia-api-key: "true" - name: Run OpenClaw skill CLI live test + env: + NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} run: | set -euo pipefail npx vitest run --project e2e-scenarios-live \ @@ -774,17 +763,13 @@ jobs: - name: Build CLI run: npm run build:cli - - name: Export hosted inference env - uses: ./.github/actions/export-e2e-hosted-inference - with: - nvidia-inference-api-key: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} - nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }} - export-nvidia-api-key: "true" - name: Run cloud inference live test # Migrated from test/e2e/test-cloud-inference-e2e.sh. The Vitest test # preserves the legacy install.sh/onboard path, posts a live PONG # request through sandbox inference.local, and reuses the skill # filesystem validators from the retained bash helpers. + env: + NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} run: | set -euo pipefail npx vitest run --project e2e-scenarios-live \ @@ -888,12 +873,9 @@ jobs: run: npm run build:cli - name: Install OpenShell CLI run: bash scripts/install-openshell.sh - - name: Export hosted inference env - uses: ./.github/actions/export-e2e-hosted-inference - with: - nvidia-inference-api-key: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} - nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }} - name: Run agent turn latency live Vitest test + env: + NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} run: | set -euo pipefail export PATH="$HOME/.local/bin:$HOME/.npm-global/bin:$PATH" @@ -997,12 +979,9 @@ jobs: run: npm run build:cli - name: Install OpenShell CLI run: bash scripts/install-openshell.sh - - name: Export hosted inference env - uses: ./.github/actions/export-e2e-hosted-inference - with: - nvidia-inference-api-key: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} - nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }} - name: Run Hermes inference switch live Vitest test + env: + NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} run: | set -euo pipefail export PATH="$HOME/.local/bin:$HOME/.npm-global/bin:$PATH" @@ -1053,14 +1032,10 @@ jobs: - name: Install OpenShell CLI run: bash scripts/install-openshell.sh - - name: Export hosted inference env - uses: ./.github/actions/export-e2e-hosted-inference - with: - nvidia-inference-api-key: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} - nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }} - name: Run Brave search live Vitest test env: BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }} + NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} run: | set -euo pipefail export PATH="$HOME/.local/bin:$HOME/.npm-global/bin:$PATH" @@ -1163,12 +1138,9 @@ jobs: - name: Install OpenShell CLI run: bash scripts/install-openshell.sh - - name: Export hosted inference env - uses: ./.github/actions/export-e2e-hosted-inference - with: - nvidia-inference-api-key: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} - nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }} - name: Run cron preflight inference.local live Vitest test + env: + NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} run: | set -euo pipefail export PATH="$HOME/.local/bin:$HOME/.npm-global/bin:$PATH" @@ -1262,12 +1234,9 @@ jobs: - name: Install OpenShell CLI run: bash scripts/install-openshell.sh - - name: Export hosted inference env - uses: ./.github/actions/export-e2e-hosted-inference - with: - nvidia-inference-api-key: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} - nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }} - name: Run issue #4434 TUI unreachable inference live test + env: + NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} run: | set -euo pipefail export PATH="$HOME/.local/bin:$HOME/.npm-global/bin:$PATH" @@ -1353,16 +1322,13 @@ jobs: - name: Build CLI run: npm run build:cli - - name: Export hosted inference env - uses: ./.github/actions/export-e2e-hosted-inference - with: - nvidia-inference-api-key: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} - nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }} - name: Run credential sanitization live test # Migrated from test/e2e/test-credential-sanitization.sh. Preserves the # same ubuntu-latest + Docker/OpenShell + NVIDIA_INFERENCE_API_KEY lane by running # install.sh, onboarding a real sandbox, and probing sandbox state from # Vitest while fixture redaction owns evidence logs. + env: + NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} run: | set -euo pipefail npx vitest run --project e2e-scenarios-live \ @@ -1439,18 +1405,19 @@ jobs: - name: Build CLI run: npm run build:cli - - name: Export hosted inference env - uses: ./.github/actions/export-e2e-hosted-inference - with: - nvidia-inference-api-key: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} - nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }} - export-nvidia-api-key: "true" - name: Run credential migration live test # Migrated from test/e2e/test-credential-migration.sh. This live test # stages NVIDIA_INFERENCE_API_KEY through legacy credentials.json as the # custom provider's COMPATIBLE_API_KEY. The hosted service behind this # repo-scoped secret is inference-api.nvidia.com, not Build/NVIDIA # Endpoints, so the test must exercise the compatible-provider route. + env: + NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} + NEMOCLAW_PROVIDER: custom + NEMOCLAW_ENDPOINT_URL: https://inference-api.nvidia.com/v1 + NEMOCLAW_MODEL: nvidia/nvidia/nemotron-3-super-v3 + NEMOCLAW_COMPAT_MODEL: nvidia/nvidia/nemotron-3-super-v3 + NEMOCLAW_PREFERRED_API: openai-completions run: | set -euo pipefail npx vitest run --project e2e-scenarios-live \ @@ -1529,17 +1496,13 @@ jobs: env -u DOCKER_CONFIG -u DOCKERHUB_USERNAME -u DOCKERHUB_TOKEN -u NVIDIA_API_KEY -u GITHUB_TOKEN \ bash scripts/install-openshell.sh - - name: Export hosted inference env - uses: ./.github/actions/export-e2e-hosted-inference - with: - nvidia-inference-api-key: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} - nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }} - export-nvidia-api-key: "true" - name: Run sessions/agents CLI live test # Migrated from test/e2e/test-sessions-agents-cli.sh. This stays as a # host-side CLI live test because the contract is argv translation, # OpenClaw gateway dispatch, pending pairing/scope approval, and JSON # envelope handling for sessions/agents commands. + env: + NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} run: | set -euo pipefail export PATH="$HOME/.local/bin:$HOME/.npm-global/bin:$PATH" @@ -1666,16 +1629,12 @@ jobs: - name: Build CLI run: npm run build:cli - - name: Export hosted inference env - uses: ./.github/actions/export-e2e-hosted-inference - with: - nvidia-inference-api-key: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} - nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }} - name: Run Hermes Slack live test # Migrated from test/e2e/test-hermes-slack-e2e.sh. Preserves the # linux-amd64-cpu4 Docker/OpenShell/Hermes Slack policy, placeholder, # provider, secret-boundary, and Python Slack egress contracts. env: + NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} SLACK_BOT_TOKEN: xoxb-test-hermes-slack-token SLACK_APP_TOKEN: xapp-test-hermes-slack-app-token run: | @@ -1736,12 +1695,9 @@ jobs: - name: Build CLI run: npm run build:cli - - name: Export hosted inference env - uses: ./.github/actions/export-e2e-hosted-inference - with: - nvidia-inference-api-key: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} - nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }} - name: Run Hermes live Vitest test + env: + NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} run: | set -euo pipefail npx vitest run --project e2e-scenarios-live \ @@ -1819,16 +1775,12 @@ jobs: - name: Build CLI run: npm run build:cli - - name: Export hosted inference env - uses: ./.github/actions/export-e2e-hosted-inference - with: - nvidia-inference-api-key: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} - nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }} - name: Run Hermes Discord live test # Migrated from test/e2e/test-hermes-discord-e2e.sh. Preserves the # ubuntu-latest Docker/OpenShell/Hermes Discord schema, provider, # placeholder isolation, native gateway rewrite, and rebuild contracts. env: + NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} DISCORD_BOT_TOKEN: test-fake-discord-token-hermes-e2e DISCORD_SERVER_IDS: "1491590992753590594" DISCORD_ALLOWED_IDS: "1005536447329222676" @@ -1898,15 +1850,12 @@ jobs: set -euo pipefail env -u DOCKER_CONFIG -u DOCKERHUB_USERNAME -u DOCKERHUB_TOKEN -u NVIDIA_INFERENCE_API_KEY -u GITHUB_TOKEN bash scripts/install-openshell.sh - - name: Export hosted inference env - uses: ./.github/actions/export-e2e-hosted-inference - with: - nvidia-inference-api-key: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} - nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }} - name: Run network-policy live test # Migrated from test/e2e/test-network-policy.sh. Free-standing anchor # for live network policy allow/deny probes; shell retirement remains # deferred to #5098 Phase 11. + env: + NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} run: | set -euo pipefail npx vitest run --project e2e-scenarios-live \ @@ -1964,16 +1913,12 @@ jobs: set -euo pipefail env -u DOCKER_CONFIG -u DOCKERHUB_USERNAME -u DOCKERHUB_TOKEN -u NVIDIA_API_KEY -u GITHUB_TOKEN bash scripts/install-openshell.sh - - name: Export hosted inference env - uses: ./.github/actions/export-e2e-hosted-inference - with: - nvidia-inference-api-key: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} - nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }} - export-nvidia-api-key: "true" - name: Run common-egress agent live test # Migrated from test/e2e/test-common-egress-agent-e2e.sh. Preserves # OpenClaw balanced/open and Hermes open common-egress agent-turn # coverage against real sandbox policy and live hosted inference. + env: + NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} run: | set -euo pipefail export PATH="$HOME/.local/bin:$HOME/.npm-global/bin:$PATH" @@ -2057,15 +2002,12 @@ jobs: - name: Install root dependencies run: npm ci --ignore-scripts - - name: Export hosted inference env - uses: ./.github/actions/export-e2e-hosted-inference - with: - nvidia-inference-api-key: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} - nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }} - name: Run shields-config live test # Migrated from test/e2e/test-shields-config.sh. The Vitest test runs # bash install.sh to preserve installer/onboard fidelity, then probes # real shields/config behavior against the live sandbox. + env: + NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} run: | set -euo pipefail npx vitest run --project e2e-scenarios-live \ @@ -2147,12 +2089,9 @@ jobs: set -euo pipefail env -u DOCKER_CONFIG -u DOCKERHUB_USERNAME -u DOCKERHUB_TOKEN -u NVIDIA_INFERENCE_API_KEY -u GITHUB_TOKEN bash scripts/install-openshell.sh - - name: Export hosted inference env - uses: ./.github/actions/export-e2e-hosted-inference - with: - nvidia-inference-api-key: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} - nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }} - name: Run OpenClaw rebuild live test + env: + NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} run: | set -euo pipefail export PATH="$HOME/.local/bin:$HOME/.npm-global/bin:$PATH" @@ -2241,15 +2180,12 @@ jobs: - name: Install root dependencies run: npm ci --ignore-scripts - - name: Export hosted inference env - uses: ./.github/actions/export-e2e-hosted-inference - with: - nvidia-inference-api-key: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} - nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }} - name: Run Hermes rebuild live test # Migrated from test/e2e/test-rebuild-hermes.sh. Preserves the real # install.sh, Docker/OpenShell, Hermes base-image rebuild, registry, # messaging-placeholder, and backup hygiene boundaries. + env: + NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} run: | set -euo pipefail npx vitest run --project e2e-scenarios-live \ @@ -2327,15 +2263,12 @@ jobs: - name: Install root dependencies run: npm ci --ignore-scripts - - name: Export hosted inference env - uses: ./.github/actions/export-e2e-hosted-inference - with: - nvidia-inference-api-key: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} - nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }} - name: Run Hermes stale-base rebuild live test # Migrated from test/e2e/test-rebuild-hermes.sh with # NEMOCLAW_HERMES_STALE_BASE_REBUILD_E2E=1, preserving issue #3025's # stale cached base-image regression boundary. + env: + NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} run: | set -euo pipefail npx vitest run --project e2e-scenarios-live \ @@ -2416,12 +2349,9 @@ jobs: set -euo pipefail env -u DOCKER_CONFIG -u DOCKERHUB_USERNAME -u DOCKERHUB_TOKEN -u NVIDIA_INFERENCE_API_KEY -u GITHUB_TOKEN bash scripts/install-openshell.sh - - name: Export hosted inference env - uses: ./.github/actions/export-e2e-hosted-inference - with: - nvidia-inference-api-key: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} - nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }} - name: Run sandbox rebuild live test + env: + NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} run: | set -euo pipefail export PATH="$HOME/.local/bin:$HOME/.npm-global/bin:$PATH" @@ -2514,12 +2444,9 @@ jobs: - name: Build CLI run: npm run build:cli - - name: Export hosted inference env - uses: ./.github/actions/export-e2e-hosted-inference - with: - nvidia-inference-api-key: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} - nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }} - name: Run overlayfs autofix live test + env: + NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} run: | set -euo pipefail npx vitest run --project e2e-scenarios-live \ @@ -2605,13 +2532,9 @@ jobs: set -euo pipefail env -u DOCKER_CONFIG -u DOCKERHUB_USERNAME -u DOCKERHUB_TOKEN -u NVIDIA_API_KEY -u GITHUB_TOKEN bash scripts/install-openshell.sh - - name: Export hosted inference env - uses: ./.github/actions/export-e2e-hosted-inference - with: - nvidia-inference-api-key: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} - nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }} - export-nvidia-api-key: "true" - name: Run state backup restore live test + env: + NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} run: | set -euo pipefail export PATH="$HOME/.local/bin:$HOME/.npm-global/bin:$PATH" @@ -2709,12 +2632,9 @@ jobs: env -u DOCKER_CONFIG -u DOCKERHUB_USERNAME -u DOCKERHUB_TOKEN -u NVIDIA_INFERENCE_API_KEY -u NVIDIA_API_KEY -u GITHUB_TOKEN \ bash scripts/install-openshell.sh - - name: Export hosted inference env - uses: ./.github/actions/export-e2e-hosted-inference - with: - nvidia-inference-api-key: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} - nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }} - name: Run upgrade stale sandbox live Vitest test + env: + NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} run: | set -euo pipefail export PATH="$HOME/.local/bin:$HOME/.npm-global/bin:$PATH" @@ -3069,12 +2989,10 @@ jobs: - name: Install OpenShell CLI run: bash scripts/install-openshell.sh - - name: Export hosted inference env - uses: ./.github/actions/export-e2e-hosted-inference - with: - nvidia-inference-api-key: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} - nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }} - name: Run onboard-resume live Vitest test + env: + NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} + COMPATIBLE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} run: | set -euo pipefail export PATH="$HOME/.local/bin:$HOME/.npm-global/bin:$PATH" @@ -3136,12 +3054,9 @@ jobs: - name: Install OpenShell CLI run: bash scripts/install-openshell.sh - - name: Export hosted inference env - uses: ./.github/actions/export-e2e-hosted-inference - with: - nvidia-inference-api-key: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} - nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }} - name: Run full-e2e live Vitest test + env: + NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} run: | set -euo pipefail export PATH="$HOME/.local/bin:$HOME/.npm-global/bin:$PATH" @@ -3204,12 +3119,9 @@ jobs: - name: Install OpenShell CLI run: bash scripts/install-openshell.sh - - name: Export hosted inference env - uses: ./.github/actions/export-e2e-hosted-inference - with: - nvidia-inference-api-key: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} - nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }} - name: Run cloud-onboard live Vitest test + env: + NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} run: | set -euo pipefail export PATH="$HOME/.local/bin:$HOME/.npm-global/bin:$PATH" @@ -3397,12 +3309,9 @@ jobs: - name: Install OpenShell CLI run: bash scripts/install-openshell.sh - - name: Export hosted inference env - uses: ./.github/actions/export-e2e-hosted-inference - with: - nvidia-inference-api-key: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} - nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }} - name: Run issue-4462-scope-upgrade-approval live Vitest test + env: + NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} run: | set -euo pipefail export PATH="$HOME/.local/bin:$HOME/.npm-global/bin:$PATH" @@ -3719,17 +3628,12 @@ jobs: - name: Build CLI run: npm run build:cli - - name: Export hosted inference env - uses: ./.github/actions/export-e2e-hosted-inference - with: - nvidia-inference-api-key: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} - nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }} - export-nvidia-api-key: "true" - name: Run messaging providers live Vitest test # Migrated from test/e2e/test-messaging-providers.sh. The test keeps # the legacy fake-token defaults, optional _REAL secret overrides, # provider placeholder redaction checks, and WhatsApp QR-only parity. env: + NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} TELEGRAM_BOT_TOKEN_REAL: ${{ secrets.TELEGRAM_BOT_TOKEN_REAL }} DISCORD_BOT_TOKEN_REAL: ${{ secrets.DISCORD_BOT_TOKEN_REAL }} SLACK_BOT_TOKEN_REAL: ${{ secrets.SLACK_BOT_TOKEN_REAL }} @@ -3809,12 +3713,9 @@ jobs: - name: Install root dependencies run: npm ci --ignore-scripts - - name: Export hosted inference env - uses: ./.github/actions/export-e2e-hosted-inference - with: - nvidia-inference-api-key: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} - nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }} - name: Run launchable smoke live test + env: + NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} run: | set -euo pipefail npx vitest run --project e2e-scenarios-live \ @@ -3894,17 +3795,14 @@ jobs: - name: Build CLI run: npm run build:cli - - name: Export hosted inference env - uses: ./.github/actions/export-e2e-hosted-inference - with: - nvidia-inference-api-key: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} - nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }} - name: Run Model Router provider-routed inference live test # Direct Vitest coverage for # test/e2e/test-model-router-provider-routed-inference.sh. It preserves # the real provider-routed onboard, host model-router health, and # sandbox inference.local completion boundaries without adding registry # or migration-ledger wiring. + env: + NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} run: | set -euo pipefail npx vitest run --project e2e-scenarios-live \ @@ -3987,16 +3885,12 @@ jobs: - name: Install root dependencies run: npm ci --ignore-scripts - - name: Export hosted inference env - uses: ./.github/actions/export-e2e-hosted-inference - with: - nvidia-inference-api-key: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} - nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }} - export-nvidia-api-key: "true" - name: Run snapshot commands live test # Migrated from test/e2e/test-snapshot-commands.sh. Preserves the # original ubuntu-latest + Docker/OpenShell + NVIDIA_API_KEY runner # contract by driving install.sh and the real snapshot CLI lifecycle. + env: + NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} run: | set -euo pipefail npx vitest run --project e2e-scenarios-live \ @@ -4080,16 +3974,13 @@ jobs: - name: Build CLI run: npm run build:cli - - name: Export hosted inference env - uses: ./.github/actions/export-e2e-hosted-inference - with: - nvidia-inference-api-key: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} - nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }} - name: Run sandbox survival live test # Migrated from test/e2e/test-sandbox-survival.sh. This intentionally # runs install.sh from Vitest to preserve installer/OpenShell/onboard # fidelity before exercising gateway restart, state survival, and live # inference.local before and after restart. + env: + NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} run: | set -euo pipefail npx vitest run --project e2e-scenarios-live \ @@ -4145,17 +4036,13 @@ jobs: - name: Build CLI run: npm run build:cli - - name: Export hosted inference env - uses: ./.github/actions/export-e2e-hosted-inference - with: - nvidia-inference-api-key: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} - nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }} - export-nvidia-api-key: "true" - name: Run diagnostics live test # Migrated from test/e2e/test-diagnostics.sh. This preserves the # ubuntu-latest + Docker/OpenShell + NVIDIA_API_KEY lane by running # debug archives, install.sh/onboard, sandbox exec/status, and # gateway-backed credentials list/reset from Vitest. + env: + NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} run: | set -euo pipefail npx vitest run --project e2e-scenarios-live test/e2e-scenario/live/diagnostics.test.ts --silent=false --reporter=default @@ -4275,12 +4162,9 @@ jobs: - name: Build CLI run: npm run build:cli - - name: Export hosted inference env - uses: ./.github/actions/export-e2e-hosted-inference - with: - nvidia-inference-api-key: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} - nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }} - name: Run OpenClaw TUI chat correlation live test + env: + NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} run: | set -euo pipefail npx vitest run --project e2e-scenarios-live \ @@ -4353,12 +4237,9 @@ jobs: # `maybe_install_openshell_during_install` does in install.sh. run: bash scripts/install-openshell.sh - - name: Export hosted inference env - uses: ./.github/actions/export-e2e-hosted-inference - with: - nvidia-inference-api-key: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} - nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }} - name: Run Vitest gateway-guard-recovery scenario + env: + NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} run: | set -euo pipefail # OpenShell installs to /usr/local/bin on GitHub-hosted runners @@ -4454,17 +4335,13 @@ jobs: - name: Build CLI run: npm run build:cli - - name: Export hosted inference env - uses: ./.github/actions/export-e2e-hosted-inference - with: - nvidia-inference-api-key: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} - nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }} - export-nvidia-api-key: "true" - name: Run OpenClaw inference switch live test # Migrated from test/e2e/test-openclaw-inference-switch.sh. Preserves # the real install.sh/OpenShell/Docker/live-provider boundary while # Vitest owns the route, config, registry, inference.local, and agent # assertions. + env: + NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} run: | set -euo pipefail npx vitest run --project e2e-scenarios-live \ @@ -4711,12 +4588,9 @@ jobs: - name: Install OpenShell CLI run: bash scripts/install-openshell.sh - - name: Export hosted inference env - uses: ./.github/actions/export-e2e-hosted-inference - with: - nvidia-inference-api-key: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} - nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }} - name: Run device auth health live Vitest test + env: + NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} run: | set -euo pipefail export PATH="$HOME/.local/bin:$HOME/.npm-global/bin:$PATH" @@ -4818,18 +4692,13 @@ jobs: set -euo pipefail env -u DOCKER_CONFIG -u DOCKERHUB_USERNAME -u DOCKERHUB_TOKEN -u NVIDIA_API_KEY -u GITHUB_TOKEN bash scripts/install-openshell.sh - - name: Export hosted inference env - uses: ./.github/actions/export-e2e-hosted-inference - with: - nvidia-inference-api-key: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} - nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }} - export-nvidia-api-key: "true" - name: Run channels add/remove live test # Migrated from test/e2e/test-channels-add-remove.sh. Preserves the # real OpenClaw + Docker/OpenShell boundary for onboard-empty, # channels add, rebuild, gateway credential reuse, policy-list, and # channels remove cleanup. env: + NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} TELEGRAM_BOT_TOKEN: "test-fake-telegram-token-add-remove-e2e" TELEGRAM_ALLOWED_IDS: "123456789" TELEGRAM_REQUIRE_MENTION: "0" @@ -4938,15 +4807,12 @@ jobs: set -euo pipefail env -u DOCKER_CONFIG -u DOCKERHUB_USERNAME -u DOCKERHUB_TOKEN -u NVIDIA_API_KEY -u NVIDIA_INFERENCE_API_KEY -u GITHUB_TOKEN bash scripts/install-openshell.sh - - name: Export hosted inference env - uses: ./.github/actions/export-e2e-hosted-inference - with: - nvidia-inference-api-key: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} - nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }} - name: Run Telegram injection live test # Migrated from test/e2e/test-telegram-injection.sh. Preserves the # real OpenShell sandbox boundary for shell metacharacter payloads, # process-table leak checks, and validateName rejection probes. + env: + NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} run: | set -euo pipefail export PATH="$HOME/.local/bin:$HOME/.npm-global/bin:$PATH" @@ -5043,16 +4909,12 @@ jobs: set -euo pipefail env -u DOCKER_CONFIG -u DOCKERHUB_USERNAME -u DOCKERHUB_TOKEN -u NVIDIA_API_KEY -u GITHUB_TOKEN bash scripts/install-openshell.sh - - name: Export hosted inference env - uses: ./.github/actions/export-e2e-hosted-inference - with: - nvidia-inference-api-key: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} - nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }} - name: Run channels stop/start live test # Migrated from test/e2e/test-channels-stop-start.sh. Preserves the # OpenClaw/Hermes messaging channel stop/start, rebuild, provider # reuse, registry, policy-list, and in-sandbox config contracts. env: + NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} TELEGRAM_BOT_TOKEN: test-fake-telegram-token-stop-start-${{ matrix.agent }} DISCORD_BOT_TOKEN: test-fake-discord-token-stop-start-${{ matrix.agent }} SLACK_BOT_TOKEN: xoxb-fake-slack-token-stop-start-${{ matrix.agent }} @@ -5150,15 +5012,11 @@ jobs: set -euo pipefail env -u DOCKER_CONFIG -u DOCKERHUB_USERNAME -u DOCKERHUB_TOKEN -u NVIDIA_API_KEY -u NVIDIA_INFERENCE_API_KEY -u GITHUB_TOKEN bash scripts/install-openshell.sh - - name: Export hosted inference env - uses: ./.github/actions/export-e2e-hosted-inference - with: - nvidia-inference-api-key: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} - nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }} - name: Run OpenClaw Slack pairing live test # Migrated from test/e2e/test-openclaw-slack-pairing.sh. Preserves # fake Slack Socket Mode/REST token rewrite and connect-shell approval. env: + NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} SLACK_BOT_TOKEN: xoxb-fake-slack-pairing-e2e SLACK_APP_TOKEN: xapp-fake-slack-pairing-e2e run: | @@ -5368,15 +5226,11 @@ jobs: set -euo pipefail env -u DOCKER_CONFIG -u DOCKERHUB_USERNAME -u DOCKERHUB_TOKEN -u NVIDIA_API_KEY -u NVIDIA_INFERENCE_API_KEY -u GITHUB_TOKEN bash scripts/install-openshell.sh - - name: Export hosted inference env - uses: ./.github/actions/export-e2e-hosted-inference - with: - nvidia-inference-api-key: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} - nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }} - name: Run OpenClaw Discord pairing live test # Migrated from test/e2e/test-openclaw-discord-pairing.sh. Preserves # fake Discord Gateway token rewrite and connect-shell pairing approval. env: + NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} DISCORD_BOT_TOKEN: test-fake-discord-pairing-e2e run: | set -euo pipefail @@ -5499,16 +5353,13 @@ jobs: sudo apt-get install -y "cloudflared=${cf_version}" cloudflared --version - - name: Export hosted inference env - uses: ./.github/actions/export-e2e-hosted-inference - with: - nvidia-inference-api-key: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} - nvidia-api-key: ${{ secrets.NVIDIA_API_KEY }} - name: Run tunnel lifecycle live test # Migrated from test/e2e/test-tunnel-lifecycle.sh. This preserves the # real Docker/OpenShell onboard, host cloudflared quick-tunnel, # local-dashboard readiness, public tunnel probe, and stop/status # cleanup boundaries under Vitest. + env: + NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} run: | set -euo pipefail npx vitest run --project e2e-scenarios-live \ diff --git a/.github/workflows/nightly-e2e.yaml b/.github/workflows/nightly-e2e.yaml index 8bf4eafb8df..db76a3cf1bf 100644 --- a/.github/workflows/nightly-e2e.yaml +++ b/.github/workflows/nightly-e2e.yaml @@ -243,12 +243,7 @@ jobs: 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_INFERENCE_API_KEY: ${{ (github.event_name != 'workflow_dispatch' || inputs.target_ref == '') && secrets.NVIDIA_INFERENCE_API_KEY || '' }} - NVIDIA_API_KEY: ${{ (github.event_name != 'workflow_dispatch' || inputs.target_ref == '') && secrets.NVIDIA_API_KEY || '' }} - BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }} - DOCKERHUB_USERNAME: ${{ (github.event_name != 'workflow_dispatch' || inputs.target_ref == '') && secrets.DOCKERHUB_USERNAME || '' }} - DOCKERHUB_TOKEN: ${{ (github.event_name != 'workflow_dispatch' || inputs.target_ref == '') && secrets.DOCKERHUB_TOKEN || '' }} + secrets: *nightly-e2e-default-secrets cron-preflight-inference-local-e2e: if: >- github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'workflow_dispatch' || @@ -381,12 +376,10 @@ jobs: /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 - nvidia_api_key_alias: true github_token: true messaging_live_secrets: ${{ github.event_name != 'workflow_dispatch' || inputs.target_ref == '' }} secrets: NVIDIA_INFERENCE_API_KEY: ${{ (github.event_name != 'workflow_dispatch' || inputs.target_ref == '') && secrets.NVIDIA_INFERENCE_API_KEY || '' }} - NVIDIA_API_KEY: ${{ (github.event_name != 'workflow_dispatch' || inputs.target_ref == '') && secrets.NVIDIA_API_KEY || '' }} BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }} DOCKERHUB_USERNAME: ${{ (github.event_name != 'workflow_dispatch' || inputs.target_ref == '') && secrets.DOCKERHUB_USERNAME || '' }} DOCKERHUB_TOKEN: ${{ (github.event_name != 'workflow_dispatch' || inputs.target_ref == '') && secrets.DOCKERHUB_TOKEN || '' }} @@ -1203,14 +1196,8 @@ jobs: 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 - nvidia_api_key_alias: true github_token: true - secrets: - NVIDIA_INFERENCE_API_KEY: ${{ (github.event_name != 'workflow_dispatch' || inputs.target_ref == '') && secrets.NVIDIA_INFERENCE_API_KEY || '' }} - NVIDIA_API_KEY: ${{ (github.event_name != 'workflow_dispatch' || inputs.target_ref == '') && secrets.NVIDIA_API_KEY || '' }} - BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }} - DOCKERHUB_USERNAME: ${{ (github.event_name != 'workflow_dispatch' || inputs.target_ref == '') && secrets.DOCKERHUB_USERNAME || '' }} - DOCKERHUB_TOKEN: ${{ (github.event_name != 'workflow_dispatch' || inputs.target_ref == '') && secrets.DOCKERHUB_TOKEN || '' }} + secrets: *nightly-e2e-default-secrets hermes-slack-e2e: if: >- github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'workflow_dispatch' || diff --git a/test/e2e-hosted-inference-action.test.ts b/test/e2e-hosted-inference-action.test.ts deleted file mode 100644 index 02915509d7a..00000000000 --- a/test/e2e-hosted-inference-action.test.ts +++ /dev/null @@ -1,103 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { mkdtempSync, readFileSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; -import path from "node:path"; -import { spawnSync } from "node:child_process"; - -import { describe, expect, it } from "vitest"; - -const exportScript = ".github/actions/export-e2e-hosted-inference/export.sh"; - -function runExportAction(env: Record) { - const tmp = mkdtempSync(path.join(tmpdir(), "e2e-hosted-export-")); - const githubEnv = path.join(tmp, "github-env"); - const result = spawnSync("bash", [exportScript], { - cwd: process.cwd(), - encoding: "utf8", - env: { - ...process.env, - GITHUB_ENV: githubEnv, - INPUT_REQUIRE_HOSTED_INFERENCE: "true", - ...env, - }, - }); - const exported = result.status === 0 ? readFileSync(githubEnv, "utf8") : ""; - rmSync(tmp, { recursive: true, force: true }); - return { ...result, exported }; -} - -describe("export-e2e-hosted-inference action", () => { - it("rejects multiline credentials before writing GITHUB_ENV", () => { - const result = runExportAction({ - INPUT_NVIDIA_INFERENCE_API_KEY: "nvapi-good\nMALICIOUS=1", - }); - - expect(result.status).toBe(1); - expect(result.stderr).toContain("Hosted inference credentials must be single-line values"); - expect(result.exported).toBe(""); - }); - - it("promotes the legacy-only NVIDIA_API_KEY input to canonical hosted exports", () => { - const result = runExportAction({ - INPUT_NVIDIA_API_KEY: "nvapi-test-legacy-credential", - }); - - expect(result.status).toBe(0); - expect(result.exported).toContain("NVIDIA_INFERENCE_API_KEY=nvapi-test-legacy-credential\n"); - expect(result.exported).toContain("COMPATIBLE_API_KEY=nvapi-test-legacy-credential\n"); - expect(result.exported).not.toContain("NVIDIA_API_KEY=nvapi-test-legacy-credential\n"); - }); - - it("exports NVIDIA_API_KEY only for explicit legacy alias callers", () => { - const result = runExportAction({ - INPUT_NVIDIA_INFERENCE_API_KEY: "nvapi-test-hosted-credential", - INPUT_EXPORT_NVIDIA_API_KEY: "true", - }); - - expect(result.status).toBe(0); - expect(result.exported).toContain("NVIDIA_INFERENCE_API_KEY=nvapi-test-hosted-credential\n"); - expect(result.exported).toContain("COMPATIBLE_API_KEY=nvapi-test-hosted-credential\n"); - expect(result.exported).toContain("NVIDIA_API_KEY=nvapi-test-hosted-credential\n"); - }); - - it("falls back from a non-hosted canonical secret to the hosted legacy alias", () => { - const result = runExportAction({ - INPUT_NVIDIA_INFERENCE_API_KEY: "not-hosted-secret", - INPUT_NVIDIA_API_KEY: "nvapi-test-legacy-credential", - INPUT_EXPORT_NVIDIA_API_KEY: "true", - }); - - expect(result.status).toBe(0); - expect(result.exported).toContain("NVIDIA_INFERENCE_API_KEY=nvapi-test-legacy-credential\n"); - expect(result.exported).toContain("COMPATIBLE_API_KEY=nvapi-test-legacy-credential\n"); - expect(result.exported).toContain("NVIDIA_API_KEY=nvapi-test-legacy-credential\n"); - }); - - it("normalizes a non-hosted NVIDIA_API_KEY alias to the canonical hosted credential", () => { - const result = runExportAction({ - INPUT_NVIDIA_INFERENCE_API_KEY: "nvapi-test-hosted-credential", - INPUT_NVIDIA_API_KEY: "not-hosted-secret", - INPUT_EXPORT_NVIDIA_API_KEY: "true", - }); - - expect(result.status).toBe(0); - expect(result.exported).toContain("NVIDIA_INFERENCE_API_KEY=nvapi-test-hosted-credential\n"); - expect(result.exported).toContain("COMPATIBLE_API_KEY=nvapi-test-hosted-credential\n"); - expect(result.exported).toContain("NVIDIA_API_KEY=nvapi-test-hosted-credential\n"); - expect(result.exported).not.toContain("not-hosted-secret"); - }); - - it("exports hosted inference aliases from the canonical credential", () => { - const result = runExportAction({ - INPUT_NVIDIA_INFERENCE_API_KEY: "nvapi-test-hosted-credential", - }); - - expect(result.status).toBe(0); - expect(result.exported).toContain("NVIDIA_INFERENCE_API_KEY=nvapi-test-hosted-credential\n"); - expect(result.exported).toContain("COMPATIBLE_API_KEY=nvapi-test-hosted-credential\n"); - expect(result.exported).not.toContain("NVIDIA_API_KEY=nvapi-test-hosted-credential\n"); - expect(result.exported).toContain("NEMOCLAW_PROVIDER=custom\n"); - }); -}); diff --git a/test/e2e-scenario/fixtures/hosted-inference.ts b/test/e2e-scenario/fixtures/hosted-inference.ts index fc588e7e1bb..f46b446bf71 100644 --- a/test/e2e-scenario/fixtures/hosted-inference.ts +++ b/test/e2e-scenario/fixtures/hosted-inference.ts @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 const HOSTED_INFERENCE_SECRET = "NVIDIA_INFERENCE_API_KEY"; -const HOSTED_INFERENCE_PUBLIC_FALLBACK_SECRET = "NVIDIA_API_KEY"; const HOSTED_INFERENCE_CREDENTIAL_ENV = "COMPATIBLE_API_KEY"; const HOSTED_INFERENCE_PROVIDER = "custom"; const HOSTED_INFERENCE_PROVIDER_NAME = "compatible-endpoint"; @@ -10,18 +9,16 @@ const DEFAULT_HOSTED_INFERENCE_BASE_URL = "https://inference-api.nvidia.com/v1"; const DEFAULT_HOSTED_INFERENCE_MODEL = "nvidia/nvidia/nemotron-3-super-v3"; export interface HostedInferenceSecrets { - optional?(name: string): string | undefined; required(name: string): string; } export interface HostedInferenceOptions { model?: string; - preferredApi?: string; } export interface HostedInferenceConfig { apiKey: string; - sourceSecretName: typeof HOSTED_INFERENCE_SECRET | typeof HOSTED_INFERENCE_PUBLIC_FALLBACK_SECRET; + sourceSecretName: typeof HOSTED_INFERENCE_SECRET; credentialEnv: typeof HOSTED_INFERENCE_CREDENTIAL_ENV; provider: typeof HOSTED_INFERENCE_PROVIDER; providerName: typeof HOSTED_INFERENCE_PROVIDER_NAME; @@ -36,29 +33,16 @@ export function requireHostedInferenceConfig( env: NodeJS.ProcessEnv = process.env, options: HostedInferenceOptions = {}, ): HostedInferenceConfig { - const canonicalApiKey = secrets.required(HOSTED_INFERENCE_SECRET); - const publicFallbackApiKey = secrets.optional?.(HOSTED_INFERENCE_PUBLIC_FALLBACK_SECRET); - const hostedPublicFallbackApiKey = publicFallbackApiKey?.startsWith("nvapi-") - ? publicFallbackApiKey - : undefined; - const apiKey = canonicalApiKey.startsWith("nvapi-") - ? canonicalApiKey - : hostedPublicFallbackApiKey || canonicalApiKey; - const sourceSecretName = canonicalApiKey.startsWith("nvapi-") - ? HOSTED_INFERENCE_SECRET - : hostedPublicFallbackApiKey - ? HOSTED_INFERENCE_PUBLIC_FALLBACK_SECRET - : HOSTED_INFERENCE_SECRET; + const apiKey = secrets.required(HOSTED_INFERENCE_SECRET); const endpointUrl = env.NEMOCLAW_ENDPOINT_URL || DEFAULT_HOSTED_INFERENCE_BASE_URL; const model = env.NEMOCLAW_MODEL || env.NEMOCLAW_COMPAT_MODEL || options.model || DEFAULT_HOSTED_INFERENCE_MODEL; - const preferredApi = options.preferredApi || env.NEMOCLAW_PREFERRED_API || "openai-completions"; return { apiKey, - sourceSecretName, + sourceSecretName: HOSTED_INFERENCE_SECRET, credentialEnv: HOSTED_INFERENCE_CREDENTIAL_ENV, provider: HOSTED_INFERENCE_PROVIDER, providerName: HOSTED_INFERENCE_PROVIDER_NAME, @@ -69,7 +53,6 @@ export function requireHostedInferenceConfig( NEMOCLAW_ENDPOINT_URL: endpointUrl, NEMOCLAW_MODEL: model, NEMOCLAW_COMPAT_MODEL: model, - NEMOCLAW_PREFERRED_API: preferredApi, [HOSTED_INFERENCE_CREDENTIAL_ENV]: apiKey, }, contractLabel: "NVIDIA_INFERENCE_API_KEY is staged as the compatible endpoint credential", diff --git a/test/e2e-scenario/live/agent-turn-latency-helpers.ts b/test/e2e-scenario/live/agent-turn-latency-helpers.ts index 07e340613bc..9fde9c79fc5 100644 --- a/test/e2e-scenario/live/agent-turn-latency-helpers.ts +++ b/test/e2e-scenario/live/agent-turn-latency-helpers.ts @@ -12,7 +12,6 @@ import { } from "../fixtures/clients/sandbox.ts"; import { expect } from "../fixtures/e2e-test.ts"; import type { ShellProbeResult } from "../fixtures/shell-probe.ts"; -import { installShOnboardArgs } from "./install-sh-onboard.ts"; import { isTransientProviderValidationFailure } from "./network-policy-transient-provider.ts"; export const REPO_ROOT = path.resolve(import.meta.dirname, "../../.."); @@ -221,13 +220,17 @@ export async function installSandbox( ): Promise { let install: ShellProbeResult | undefined; for (let attempt = 1; attempt <= INSTALL_ATTEMPTS; attempt += 1) { - install = await host.command("bash", installShOnboardArgs(), { - artifactName: `${agent}-install-attempt-${attempt}`, - cwd: REPO_ROOT, - env: env(sandboxName, agent, apiKey), - redactionValues: [apiKey], - timeoutMs: 30 * 60_000, - }); + install = await host.command( + "bash", + ["install.sh", "--non-interactive", "--yes-i-accept-third-party-software"], + { + artifactName: `${agent}-install-attempt-${attempt}`, + cwd: REPO_ROOT, + env: env(sandboxName, agent, apiKey), + redactionValues: [apiKey], + timeoutMs: 30 * 60_000, + }, + ); const retry = install.exitCode !== 0 && isTransientProviderValidationFailure(install) && diff --git a/test/e2e-scenario/live/credential-migration.test.ts b/test/e2e-scenario/live/credential-migration.test.ts index 5ed6d1ecaf9..4068840372a 100644 --- a/test/e2e-scenario/live/credential-migration.test.ts +++ b/test/e2e-scenario/live/credential-migration.test.ts @@ -9,6 +9,7 @@ import { buildAvailabilityProbeEnv } from "../fixtures/availability-env.ts"; import type { HostCliClient } from "../fixtures/clients/host.ts"; import { validateSandboxName } from "../fixtures/clients/sandbox.ts"; import { expect, test } from "../fixtures/e2e-test.ts"; +import { requireHostedInferenceConfig } from "../fixtures/hosted-inference.ts"; import { shouldRunLiveE2EScenarios } from "../fixtures/live-project-gate.ts"; // Migrated from test/e2e/test-credential-migration.sh. This is a focused live @@ -17,10 +18,10 @@ import { shouldRunLiveE2EScenarios } from "../fixtures/live-project-gate.ts"; // a successful real onboard registers the migrated value with the OpenShell // gateway, the plaintext file is removed after success, credentials list reads // from the gateway, and secure unlink removes a planted symlink without touching -// its target. The legacy shell lane stages NVIDIA_INFERENCE_API_KEY and lets -// non-interactive onboarding use the default NVIDIA Endpoints provider; this -// Vitest migration keeps that credential migration contract intact instead of -// layering hosted-compatible endpoint selection onto the migration path. +// its target. The repository secret is named NVIDIA_INFERENCE_API_KEY, but the +// hosted E2E service is the OpenAI-compatible inference-api.nvidia.com endpoint, +// so the migration contract stages that value as COMPATIBLE_API_KEY and expects +// the compatible-endpoint gateway provider. const REPO_ROOT = path.resolve(import.meta.dirname, "../../.."); const CLI_ENTRYPOINT = path.join(REPO_ROOT, "bin", "nemoclaw.js"); @@ -28,8 +29,7 @@ const DIST_CREDENTIAL_STORE = path.join(REPO_ROOT, "dist", "lib", "credentials", const ONBOARD_TIMEOUT_MS = 30 * 60_000; const INSTALL_TIMEOUT_MS = 10 * 60_000; const SANDBOX_NAME = process.env.NEMOCLAW_SANDBOX_NAME ?? `e2e-cred-migration-${process.pid}`; -const MIGRATION_CREDENTIAL_ENV = "NVIDIA_INFERENCE_API_KEY"; -const MIGRATION_PROVIDER_NAME = "nvidia-prod"; +const CREDENTIAL_MIGRATION_MODEL = "openai/gpt-oss-120b"; validateSandboxName(SANDBOX_NAME); const runCredentialMigrationTest = shouldRunLiveE2EScenarios() ? test : test.skip; @@ -146,11 +146,14 @@ runCredentialMigrationTest( // onboard child env below deliberately does not receive that credential, so // the only source is ~/.nemoclaw/credentials.json — matching the retired // shell lane's migration contract. - const migratedCredentialValue = secrets.required(MIGRATION_CREDENTIAL_ENV); - expect( - migratedCredentialValue.startsWith("nvapi-"), - `${MIGRATION_CREDENTIAL_ENV} must start with nvapi-`, - ).toBe(true); + const hostedInference = requireHostedInferenceConfig(secrets, process.env, { + model: CREDENTIAL_MIGRATION_MODEL, + }); + const migratedCredentialValue = hostedInference.apiKey; + const { + [hostedInference.credentialEnv]: _omittedCredential, + ...hostedInferenceEnvWithoutCredential + } = hostedInference.env; expect(fs.existsSync(CLI_ENTRYPOINT), "bin/nemoclaw.js missing").toBe(true); expect( fs.existsSync(DIST_CREDENTIAL_STORE), @@ -187,9 +190,9 @@ runCredentialMigrationTest( sandboxName: SANDBOX_NAME, contracts: [ "legacy credentials.json stages allowlisted provider keys into onboard env", - `successful onboard registers the migrated value with the ${MIGRATION_PROVIDER_NAME} OpenShell gateway provider`, - `${MIGRATION_CREDENTIAL_ENV} is migrated into the gateway provider credential`, - "onboard uses the default NVIDIA Endpoints non-interactive provider path", + `successful onboard registers the migrated value with the ${hostedInference.providerName} OpenShell gateway provider`, + `${hostedInference.sourceSecretName} is migrated into the ${hostedInference.credentialEnv} provider credential`, + `onboard uses the ${hostedInference.provider} provider and ${hostedInference.endpointUrl} endpoint path`, "successful onboard removes plaintext credentials.json", "tampered non-credential keys do not become gateway providers", "credentials list reads providers from the gateway, not disk", @@ -206,7 +209,7 @@ runCredentialMigrationTest( legacyFile, JSON.stringify( { - [MIGRATION_CREDENTIAL_ENV]: migratedCredentialValue, + [hostedInference.credentialEnv]: migratedCredentialValue, OPENSHELL_GATEWAY: "evil-gw-from-tampered-file", NODE_OPTIONS: "--require=/tmp/evil.js", }, @@ -219,6 +222,7 @@ runCredentialMigrationTest( const onboard = await host.command("node", [CLI_ENTRYPOINT, "onboard", "--non-interactive"], { artifactName: "onboard-from-legacy-credentials", env: testEnv(home, { + ...hostedInferenceEnvWithoutCredential, NEMOCLAW_SANDBOX_NAME: SANDBOX_NAME, NEMOCLAW_RECREATE_SANDBOX: "1", }), @@ -249,14 +253,10 @@ runCredentialMigrationTest( .split(/\r?\n/) .map((line) => line.trim()) .filter((line) => /^[a-zA-Z][a-zA-Z0-9_-]*$/.test(line)); - expect( - providerNames.length, - `expected a migrated provider\n${providersText}`, - ).toBeGreaterThanOrEqual(1); expect( providerNames, - `expected migrated ${MIGRATION_PROVIDER_NAME} provider\n${providersText}`, - ).toContain(MIGRATION_PROVIDER_NAME); + `expected migrated ${hostedInference.providerName} provider\n${providersText}`, + ).toContain(hostedInference.providerName); expect(providerNames).not.toContain("OPENSHELL_GATEWAY"); expect(providerNames).not.toContain("NODE_OPTIONS"); @@ -299,8 +299,9 @@ runCredentialMigrationTest( await artifacts.writeJson("scenario-result.json", { id: "credential-migration", sandboxName: SANDBOX_NAME, - provider: MIGRATION_PROVIDER_NAME, - credentialEnv: MIGRATION_CREDENTIAL_ENV, + model: hostedInference.model || CREDENTIAL_MIGRATION_MODEL, + provider: hostedInference.providerName, + credentialEnv: hostedInference.credentialEnv, providerNames, assertions: { onboardSucceeded: onboard.exitCode === 0, @@ -308,7 +309,7 @@ runCredentialMigrationTest( "Staged 1 legacy credential(s) for migration to the OpenShell gateway.", ), legacyFileRemovedAfterOnboard: !fs.existsSync(legacyFile), - migratedProviderRegistered: providerNames.includes(MIGRATION_PROVIDER_NAME), + migratedProviderRegistered: providerNames.includes(hostedInference.providerName), tamperedKeysExcluded: !providerNames.includes("OPENSHELL_GATEWAY") && !providerNames.includes("NODE_OPTIONS"), credentialsListReadsGateway: credentialsText.includes( diff --git a/test/e2e-scenario/live/cron-preflight-inference-local.test.ts b/test/e2e-scenario/live/cron-preflight-inference-local.test.ts index 73ccb2ef1d3..a31658092b3 100644 --- a/test/e2e-scenario/live/cron-preflight-inference-local.test.ts +++ b/test/e2e-scenario/live/cron-preflight-inference-local.test.ts @@ -19,7 +19,6 @@ import { type SandboxClient, validateSandboxName } from "../fixtures/clients/san import { expect, test } from "../fixtures/e2e-test.ts"; import { shouldRunLiveE2EScenarios } from "../fixtures/live-project-gate.ts"; import type { ShellProbeResult } from "../fixtures/shell-probe.ts"; -import { installShOnboardArgs } from "./install-sh-onboard.ts"; import { isTransientProviderValidationFailure } from "./network-policy-transient-provider.ts"; const REPO_ROOT = path.resolve(import.meta.dirname, "../../.."); @@ -266,16 +265,20 @@ test.skipIf(!shouldRunLiveE2EScenarios())( let install: ShellProbeResult | undefined; for (let attempt = 1; attempt <= INSTALL_ATTEMPTS; attempt += 1) { - install = await host.command("bash", installShOnboardArgs(), { - artifactName: - attempt === 1 - ? "phase-1-install-cron-preflight" - : `phase-1-install-cron-preflight-attempt-${attempt}`, - cwd: REPO_ROOT, - env: commandEnv(apiKey), - redactionValues: [apiKey], - timeoutMs: 20 * 60_000, - }); + install = await host.command( + "bash", + ["install.sh", "--non-interactive", "--yes-i-accept-third-party-software"], + { + artifactName: + attempt === 1 + ? "phase-1-install-cron-preflight" + : `phase-1-install-cron-preflight-attempt-${attempt}`, + cwd: REPO_ROOT, + env: commandEnv(apiKey), + redactionValues: [apiKey], + timeoutMs: 20 * 60_000, + }, + ); if (install.exitCode === 0) break; if (isTransientProviderValidationFailure(install) && attempt < INSTALL_ATTEMPTS) { await new Promise((resolve) => setTimeout(resolve, 10_000 * attempt)); diff --git a/test/e2e-scenario/live/install-sh-onboard.ts b/test/e2e-scenario/live/install-sh-onboard.ts deleted file mode 100644 index beca9c5e41b..00000000000 --- a/test/e2e-scenario/live/install-sh-onboard.ts +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -/** - * Canonical install.sh arguments for live Vitest scenarios that auto-onboard. - * - * Live scenario retries must not inherit a failed persisted onboarding session - * from an earlier failed attempt. `--fresh` makes install.sh bypass the failed - * session classifier and lets the CLI clear stale session state before creating - * the new session. - */ -export function installShOnboardArgs(): string[] { - return ["install.sh", "--non-interactive", "--yes-i-accept-third-party-software", "--fresh"]; -} diff --git a/test/e2e-scenario/support-tests/e2e-scenarios-workflow.test.ts b/test/e2e-scenario/support-tests/e2e-scenarios-workflow.test.ts index 26f81052f4b..37c1a21f5f5 100644 --- a/test/e2e-scenario/support-tests/e2e-scenarios-workflow.test.ts +++ b/test/e2e-scenario/support-tests/e2e-scenarios-workflow.test.ts @@ -976,7 +976,7 @@ jobs: "live-scenarios job must run on the matrix runner", "live-scenarios job env must not include NVIDIA_INFERENCE_API_KEY", "step 'Run Vitest live E2E scenarios' run script must not interpolate dispatch inputs directly", - "Vitest step must consume the shared hosted inference export action", + "Vitest step must receive NVIDIA_INFERENCE_API_KEY from secrets", "artifact upload must set include-hidden-files: false", "upload-artifact action must be pinned to a full commit SHA", "openshell-version-pin-vitest job must use the shared jobs selector condition", @@ -1263,8 +1263,7 @@ jobs: "step 'Install root dependencies' run script must include npm ci --ignore-scripts", "step 'Install OpenShell' run script must include env -u DOCKER_CONFIG", "channels-stop-start-vitest step 'Run channels stop/start live test' env must not include NVIDIA_API_KEY", - "channels-stop-start-vitest step must receive NVIDIA_INFERENCE_API_KEY from hosted inference secrets", - "channels-stop-start-vitest step must receive COMPATIBLE_API_KEY from hosted inference secrets", + "channels-stop-start-vitest step must receive NVIDIA_INFERENCE_API_KEY from secrets", "channels-stop-start-vitest step must set the fake Telegram token", "step 'Run channels stop/start live test' run script must include test/e2e-scenario/live/channels-stop-start.test.ts", "channels-stop-start-vitest upload-artifact action must be pinned to a full commit SHA", diff --git a/test/e2e-scenario/support-tests/hosted-inference-workflow-env.test.ts b/test/e2e-scenario/support-tests/hosted-inference-workflow-env.test.ts deleted file mode 100644 index b4037418aa9..00000000000 --- a/test/e2e-scenario/support-tests/hosted-inference-workflow-env.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -import fs from "node:fs"; -import path from "node:path"; - -import { describe, expect, it } from "vitest"; -import YAML from "yaml"; - -function readWorkflow(): Record { - return YAML.parse( - fs.readFileSync( - path.join(process.cwd(), ".github/workflows/e2e-vitest-scenarios.yaml"), - "utf-8", - ), - ) as Record; -} - -describe("hosted inference workflow env", () => { - it("uses the shared export action instead of duplicated run-step hosted env", () => { - const jobs = (readWorkflow().jobs ?? {}) as Record< - string, - { steps?: Array> } - >; - const hostedKeys = - /^(NVIDIA_INFERENCE_API_KEY|COMPATIBLE_API_KEY|NEMOCLAW_(E2E_USE_HOSTED_INFERENCE|PROVIDER|ENDPOINT_URL|MODEL|COMPAT_MODEL|PREFERRED_API))$/u; - const steps = Object.entries(jobs).flatMap(([jobName, job]) => - (job.steps ?? []).map((step) => ({ jobName, step })), - ); - - expect( - steps.filter(({ step }) => step.uses === "./.github/actions/export-e2e-hosted-inference") - .length, - ).toBeGreaterThan(0); - for (const { jobName, step } of steps.filter(({ step }) => - String(step.run).includes("npx vitest run"), - )) { - expect( - Object.keys((step.env ?? {}) as Record).filter((key) => - hostedKeys.test(key), - ), - `${jobName} ${String(step.name)}`, - ).toEqual([]); - } - }); -}); diff --git a/test/e2e-scenario/support-tests/hosted-inference.test.ts b/test/e2e-scenario/support-tests/hosted-inference.test.ts index 50bb29ce621..8a21df80667 100644 --- a/test/e2e-scenario/support-tests/hosted-inference.test.ts +++ b/test/e2e-scenario/support-tests/hosted-inference.test.ts @@ -20,7 +20,6 @@ const COMPAT_HELPER = path.join( function secrets(values: Record) { return { - optional: (name: string) => values[name], required: (name: string) => { const value = values[name]; if (!value) throw new Error(`missing ${name}`); @@ -104,48 +103,6 @@ describe("hosted inference E2E config", () => { expect(cfg.credentialEnv).toBe("COMPATIBLE_API_KEY"); }); - it("prefers the hosted public fallback secret when the canonical secret is not hosted", () => { - const cfg = requireHostedInferenceConfig( - secrets({ - NVIDIA_INFERENCE_API_KEY: "build-only-key", - NVIDIA_API_KEY: "nvapi-hosted-key", - }), - {}, - ); - - expect(cfg.apiKey).toBe("nvapi-hosted-key"); - expect(cfg.sourceSecretName).toBe("NVIDIA_API_KEY"); - expect(cfg.env.COMPATIBLE_API_KEY).toBe("nvapi-hosted-key"); - }); - - it("keeps the canonical hosted secret when both hosted aliases are available", () => { - const cfg = requireHostedInferenceConfig( - secrets({ - NVIDIA_INFERENCE_API_KEY: "nvapi-canonical-hosted-key", - NVIDIA_API_KEY: "nvapi-public-hosted-key", - }), - {}, - ); - - expect(cfg.apiKey).toBe("nvapi-canonical-hosted-key"); - expect(cfg.sourceSecretName).toBe("NVIDIA_INFERENCE_API_KEY"); - expect(cfg.env.COMPATIBLE_API_KEY).toBe("nvapi-canonical-hosted-key"); - }); - - it("ignores a non-nvapi public fallback alias", () => { - const cfg = requireHostedInferenceConfig( - secrets({ - NVIDIA_INFERENCE_API_KEY: "sk-compatible-key", - NVIDIA_API_KEY: "not-hosted-public-alias", - }), - {}, - ); - - expect(cfg.apiKey).toBe("sk-compatible-key"); - expect(cfg.sourceSecretName).toBe("NVIDIA_INFERENCE_API_KEY"); - expect(cfg.env.COMPATIBLE_API_KEY).toBe("sk-compatible-key"); - }); - it("uses a lightweight compatible reachability probe without API or auth requests", () => { const { result, calls } = runHostedProbe({ env: { @@ -201,39 +158,7 @@ describe("hosted inference E2E config", () => { NEMOCLAW_ENDPOINT_URL: "https://inference-api.nvidia.com/v1", NEMOCLAW_MODEL: "nvidia/custom-model", NEMOCLAW_COMPAT_MODEL: "nvidia/custom-model", - NEMOCLAW_PREFERRED_API: "openai-completions", COMPATIBLE_API_KEY: "repo-hosted-key", }); }); - - it("preserves workflow-hosted model defaults ahead of fixture fallbacks", () => { - const cfg = requireHostedInferenceConfig( - secrets({ NVIDIA_INFERENCE_API_KEY: "repo-hosted-key" }), - { NEMOCLAW_MODEL: "nvidia/workflow-model" }, - { model: "nvidia/scenario-model" }, - ); - - expect(cfg.model).toBe("nvidia/workflow-model"); - expect(cfg.env.NEMOCLAW_MODEL).toBe("nvidia/workflow-model"); - expect(cfg.env.NEMOCLAW_COMPAT_MODEL).toBe("nvidia/workflow-model"); - }); - - it("preserves an explicit hosted inference API preference", () => { - const cfg = requireHostedInferenceConfig( - secrets({ NVIDIA_INFERENCE_API_KEY: "repo-hosted-key" }), - { NEMOCLAW_PREFERRED_API: "openai-responses" }, - ); - - expect(cfg.env.NEMOCLAW_PREFERRED_API).toBe("openai-responses"); - }); - - it("prefers an explicit fixture API preference over workflow defaults", () => { - const cfg = requireHostedInferenceConfig( - secrets({ NVIDIA_INFERENCE_API_KEY: "repo-hosted-key" }), - { NEMOCLAW_PREFERRED_API: "openai-responses" }, - { preferredApi: "openai-completions" }, - ); - - expect(cfg.env.NEMOCLAW_PREFERRED_API).toBe("openai-completions"); - }); }); diff --git a/test/e2e-scenario/support-tests/install-sh-onboard.test.ts b/test/e2e-scenario/support-tests/install-sh-onboard.test.ts deleted file mode 100644 index 3631881279b..00000000000 --- a/test/e2e-scenario/support-tests/install-sh-onboard.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -import fs from "node:fs"; -import path from "node:path"; - -import { describe, expect, it } from "vitest"; - -import { installShOnboardArgs } from "../live/install-sh-onboard.ts"; - -const RETRY_SENSITIVE_LIVE_TESTS = [ - "test/e2e-scenario/live/agent-turn-latency-helpers.ts", - "test/e2e-scenario/live/cron-preflight-inference-local.test.ts", -]; - -describe("live install.sh onboarding arguments", () => { - it("force a fresh onboarding session so retries discard failed session state", () => { - expect(installShOnboardArgs()).toEqual([ - "install.sh", - "--non-interactive", - "--yes-i-accept-third-party-software", - "--fresh", - ]); - }); - - it("routes retry-sensitive live install.sh callers through the fresh-session helper", () => { - for (const relativePath of RETRY_SENSITIVE_LIVE_TESTS) { - const source = fs.readFileSync(path.join(process.cwd(), relativePath), "utf8"); - expect(source, relativePath).toContain("installShOnboardArgs()"); - expect(source, relativePath).not.toContain( - '["install.sh", "--non-interactive", "--yes-i-accept-third-party-software"]', - ); - } - }); -}); diff --git a/test/e2e-script-workflow.test.ts b/test/e2e-script-workflow.test.ts index 4f0d1b7dc8c..1da46728c7b 100644 --- a/test/e2e-script-workflow.test.ts +++ b/test/e2e-script-workflow.test.ts @@ -9,12 +9,6 @@ import path from "node:path"; import { describe, expect, it } from "vitest"; -import { - HOSTED_INFERENCE_LEGACY_NVIDIA_API_KEY_SCRIPT_JOBS, - HOSTED_INFERENCE_LEGACY_NVIDIA_API_KEY_SCRIPT_JOB_SET, - HOSTED_INFERENCE_PUBLIC_NVIDIA_FALLBACK_SCRIPT_JOB_SET, -} from "../tools/e2e-scenarios/hosted-inference-legacy-alias.mts"; - import { loadE2eWorkflowContract, readYaml, @@ -55,7 +49,6 @@ const TRUSTED_REF_GUARD = "github.event_name != 'workflow_dispatch' || inputs.ta const GUARDED_HOSTED_INFERENCE_SECRET = `\${{ (${TRUSTED_REF_GUARD}) && secrets.NVIDIA_INFERENCE_API_KEY || '' }}`; const GUARDED_PUBLIC_NVIDIA_SECRET = `\${{ (${TRUSTED_REF_GUARD}) && secrets.NVIDIA_API_KEY || '' }}`; const RAW_HOSTED_INFERENCE_SECRET = "${{ secrets.NVIDIA_INFERENCE_API_KEY }}"; -const RAW_PUBLIC_NVIDIA_SECRET = "${{ secrets.NVIDIA_API_KEY }}"; function timingSummary( phases: Record = { "nemoclaw.onboard.phase.preflight": 1000 }, @@ -409,9 +402,6 @@ describe("E2E reusable workflow contract", () => { DOCKERHUB_TOKEN: "${{ (github.event_name != 'workflow_dispatch' || inputs.target_ref == '') && secrets.DOCKERHUB_TOKEN || '' }}", }; - const legacyAliasSecret = { - NVIDIA_API_KEY: GUARDED_PUBLIC_NVIDIA_SECRET, - }; const messagingLiveSecrets = { TELEGRAM_BOT_TOKEN_REAL: `\${{ (${TRUSTED_REF_GUARD}) && secrets.TELEGRAM_BOT_TOKEN_REAL || '' }}`, TELEGRAM_CHAT_ID_E2E: `\${{ (${TRUSTED_REF_GUARD}) && secrets.TELEGRAM_CHAT_ID_E2E || '' }}`, @@ -425,13 +415,9 @@ describe("E2E reusable workflow contract", () => { expect(reusableJobs.length).toBeGreaterThan(20); for (const [name, job] of reusableJobs) { const expectsLiveMessaging = name === "messaging-providers-e2e"; - const expectsPublicNvidiaFallback = - HOSTED_INFERENCE_PUBLIC_NVIDIA_FALLBACK_SCRIPT_JOB_SET.has(name); const expectedSecrets = expectsLiveMessaging - ? { ...defaultSecrets, ...legacyAliasSecret, ...messagingLiveSecrets } - : expectsPublicNvidiaFallback - ? { ...defaultSecrets, ...legacyAliasSecret } - : defaultSecrets; + ? { ...defaultSecrets, ...messagingLiveSecrets } + : defaultSecrets; expect(job.secrets, name).toEqual(expectedSecrets); expect(job.with?.messaging_live_secrets ?? false, name).toBe( expectsLiveMessaging @@ -933,48 +919,21 @@ describe("E2E reusable workflow contract", () => { type: "boolean", default: false, }); - expect(workflowCall?.inputs?.nvidia_api_key_alias).toMatchObject({ - required: false, - type: "boolean", - default: false, - }); expect(workflowCall?.inputs?.nvidia_secret_as_compatible_api_key).toBeUndefined(); expect(exportStep?.if).toBe("${{ inputs.nvidia_api_key }}"); - expect(exportStep?.uses).toBe("./workflow-actions/.github/actions/export-e2e-hosted-inference"); - expect(exportStep?.with?.["nvidia-inference-api-key"]).toBe(RAW_HOSTED_INFERENCE_SECRET); - expect(exportStep?.with?.["nvidia-api-key"]).toBe(RAW_PUBLIC_NVIDIA_SECRET); - expect(exportStep?.with?.["export-nvidia-api-key"]).toBe("${{ inputs.nvidia_api_key_alias }}"); - - const runStep = runnerWorkflow.jobs.run.steps.find((step) => step.name === "Run E2E script"); - expect(runStep?.env?.NVIDIA_INFERENCE_API_KEY).toBeUndefined(); - expect(runStep?.env?.COMPATIBLE_API_KEY).toBeUndefined(); - expect(runStep?.env?.NVIDIA_API_KEY).toBeUndefined(); + expect(exportStep?.env?.NVIDIA_INFERENCE_API_KEY).toBe(RAW_HOSTED_INFERENCE_SECRET); + expect(exportStep?.run).toContain("withheld for workflow_dispatch target_ref runs"); + expect(exportStep?.run).toContain("NEMOCLAW_E2E_USE_HOSTED_INFERENCE=1"); + expect(exportStep?.run).toContain("NEMOCLAW_PROVIDER=custom"); + expect(exportStep?.run).toContain("NEMOCLAW_ENDPOINT_URL=https://inference-api.nvidia.com/v1"); + expect(exportStep?.run).toContain("NEMOCLAW_MODEL=nvidia/nvidia/nemotron-3-super-v3"); + expect(exportStep?.run).toContain("NEMOCLAW_COMPAT_MODEL=nvidia/nvidia/nemotron-3-super-v3"); + expect(exportStep?.run).toContain("NEMOCLAW_PREFERRED_API=openai-completions"); + expect(exportStep?.run).toContain("COMPATIBLE_API_KEY=%s"); expect(hostedJobs.length).toBeGreaterThan(20); for (const [name, job] of hostedJobs) { expect(job.with?.nvidia_secret_as_compatible_api_key, name).toBeUndefined(); - expect(String(job.with?.nvidia_api_key_alias) === "true", name).toBe( - HOSTED_INFERENCE_LEGACY_NVIDIA_API_KEY_SCRIPT_JOB_SET.has(name), - ); - } - }); - - it("exposes the reusable shell NVIDIA_API_KEY alias only to documented legacy consumers", () => { - const aliasJobs = reusableNightlyJobs(nightlyWorkflow) - .filter(([, job]) => String(job.with?.nvidia_api_key_alias) === "true") - .map(([name]) => name) - .sort(); - - expect(aliasJobs).toEqual([...HOSTED_INFERENCE_LEGACY_NVIDIA_API_KEY_SCRIPT_JOBS].sort()); - const shellScriptJobs = reusableNightlyJobs(nightlyWorkflow) - .map(([name, job]) => ({ name, job, script: String(job.with?.script ?? "") })) - .filter(({ script }) => script.startsWith("test/e2e/") && existsSync(script)); - - for (const { name, job, script } of shellScriptJobs) { - const readsLegacyAlias = readFileSync(script, "utf8").includes("NVIDIA_API_KEY"); - expect(HOSTED_INFERENCE_LEGACY_NVIDIA_API_KEY_SCRIPT_JOB_SET.has(name), name).toBe( - readsLegacyAlias && String(job.with?.nvidia_api_key) === "true", - ); } }); diff --git a/tools/e2e-scenarios/hosted-inference-legacy-alias.mts b/tools/e2e-scenarios/hosted-inference-legacy-alias.mts deleted file mode 100644 index 5f7c1597a15..00000000000 --- a/tools/e2e-scenarios/hosted-inference-legacy-alias.mts +++ /dev/null @@ -1,73 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -/** - * Source-owned inventories for the temporary NVIDIA_API_KEY compatibility - * alias. - * - * Invalid state: several Vitest live lanes and a small number of reusable shell - * E2E scripts still read NVIDIA_API_KEY directly even though hosted CI inference - * is now canonicalized as NVIDIA_INFERENCE_API_KEY and COMPATIBLE_API_KEY. Until - * those lanes migrate, only the jobs listed here may receive the compatibility - * alias. All other hosted-inference steps should receive only - * NVIDIA_INFERENCE_API_KEY and COMPATIBLE_API_KEY. - * - * Removal condition: delete these inventories, the nvidia-api-key and - * export-nvidia-api-key action inputs, e2e-script.yaml's nvidia_api_key_alias - * input, and workflow NVIDIA_API_KEY exports after every listed lane either - * migrates to NVIDIA_INFERENCE_API_KEY or documents an explicit public-endpoint - * credential requirement that is not a compatibility alias. - */ -export const HOSTED_INFERENCE_LEGACY_NVIDIA_API_KEY_JOBS = [ - "openclaw-skill-cli-vitest", - "cloud-inference-vitest", - "sessions-agents-cli-vitest", - "common-egress-agent-vitest", - "state-backup-restore-vitest", - "messaging-providers-vitest", - "snapshot-commands-vitest", - "diagnostics-vitest", - "openclaw-inference-switch-vitest", - "channels-add-remove-vitest", -] as const; - -export type HostedInferenceLegacyNvidiaApiKeyJob = - (typeof HOSTED_INFERENCE_LEGACY_NVIDIA_API_KEY_JOBS)[number]; - -export const HOSTED_INFERENCE_LEGACY_NVIDIA_API_KEY_JOB_SET: ReadonlySet = new Set( - HOSTED_INFERENCE_LEGACY_NVIDIA_API_KEY_JOBS, -); - -export const HOSTED_INFERENCE_LEGACY_NVIDIA_API_KEY_SCRIPT_JOBS = [ - "messaging-providers-e2e", - "hermes-discord-e2e", -] as const; - -export type HostedInferenceLegacyNvidiaApiKeyScriptJob = - (typeof HOSTED_INFERENCE_LEGACY_NVIDIA_API_KEY_SCRIPT_JOBS)[number]; - -export const HOSTED_INFERENCE_LEGACY_NVIDIA_API_KEY_SCRIPT_JOB_SET: ReadonlySet = new Set( - HOSTED_INFERENCE_LEGACY_NVIDIA_API_KEY_SCRIPT_JOBS, -); - -export const HOSTED_INFERENCE_PUBLIC_NVIDIA_FALLBACK_VITEST_JOBS = [ - "credential-migration-vitest", -] as const; - -export type HostedInferencePublicNvidiaFallbackVitestJob = - (typeof HOSTED_INFERENCE_PUBLIC_NVIDIA_FALLBACK_VITEST_JOBS)[number]; - -export const HOSTED_INFERENCE_PUBLIC_NVIDIA_FALLBACK_VITEST_JOB_SET: ReadonlySet = - new Set(HOSTED_INFERENCE_PUBLIC_NVIDIA_FALLBACK_VITEST_JOBS); - -export const HOSTED_INFERENCE_PUBLIC_NVIDIA_FALLBACK_SCRIPT_JOBS = [ - "cloud-inference-e2e", - ...HOSTED_INFERENCE_LEGACY_NVIDIA_API_KEY_SCRIPT_JOBS, -] as const; - -export type HostedInferencePublicNvidiaFallbackScriptJob = - (typeof HOSTED_INFERENCE_PUBLIC_NVIDIA_FALLBACK_SCRIPT_JOBS)[number]; - -export const HOSTED_INFERENCE_PUBLIC_NVIDIA_FALLBACK_SCRIPT_JOB_SET: ReadonlySet = new Set( - HOSTED_INFERENCE_PUBLIC_NVIDIA_FALLBACK_SCRIPT_JOBS, -); diff --git a/tools/e2e-scenarios/workflow-boundary.mts b/tools/e2e-scenarios/workflow-boundary.mts index f998ab34718..8f24e15caec 100644 --- a/tools/e2e-scenarios/workflow-boundary.mts +++ b/tools/e2e-scenarios/workflow-boundary.mts @@ -6,12 +6,6 @@ import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import YAML from "yaml"; -import { - HOSTED_INFERENCE_LEGACY_NVIDIA_API_KEY_JOB_SET, - HOSTED_INFERENCE_LEGACY_NVIDIA_API_KEY_JOBS, - HOSTED_INFERENCE_PUBLIC_NVIDIA_FALLBACK_VITEST_JOB_SET, -} from "./hosted-inference-legacy-alias.mts"; - const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", ".."); const DEFAULT_VITEST_WORKFLOW_PATH = join( REPO_ROOT, @@ -52,22 +46,6 @@ const COMMON_SECRET_ENV_NAMES = [ "DOCKERHUB_TOKEN", "GITHUB_TOKEN", ]; -const HOSTED_INFERENCE_SECRET_ENV_NAMES = ["NVIDIA_INFERENCE_API_KEY", "COMPATIBLE_API_KEY"]; -const HOSTED_INFERENCE_SECRET_EXPR = - "${{ startsWith(secrets.NVIDIA_INFERENCE_API_KEY, 'nvapi-') && secrets.NVIDIA_INFERENCE_API_KEY || (startsWith(secrets.NVIDIA_API_KEY, 'nvapi-') && secrets.NVIDIA_API_KEY || '') }}"; -const HOSTED_NVIDIA_API_KEY_EXPR = - "${{ startsWith(secrets.NVIDIA_API_KEY, 'nvapi-') && secrets.NVIDIA_API_KEY || (startsWith(secrets.NVIDIA_INFERENCE_API_KEY, 'nvapi-') && secrets.NVIDIA_INFERENCE_API_KEY || '') }}"; -const HOSTED_INFERENCE_DEFAULT_ENV = { - NEMOCLAW_E2E_USE_HOSTED_INFERENCE: "1", - NEMOCLAW_PROVIDER: "custom", - NEMOCLAW_ENDPOINT_URL: "https://inference-api.nvidia.com/v1", - NEMOCLAW_MODEL: "nvidia/nvidia/nemotron-3-super-v3", - NEMOCLAW_COMPAT_MODEL: "nvidia/nvidia/nemotron-3-super-v3", - NEMOCLAW_PREFERRED_API: "openai-completions", -} as const; -const HOSTED_INFERENCE_EXPORT_ACTION = "./.github/actions/export-e2e-hosted-inference"; -let currentWorkflowJobs: WorkflowRecord = {}; -const DIRECT_PUBLIC_NVIDIA_API_KEY_JOBS = new Set(["spark-install-vitest"]); const FREE_STANDING_SELECTOR_SPECIAL_CASES = new Set([ "hermes-e2e-vitest", "hermes-root-entrypoint-smoke-vitest", @@ -106,7 +84,9 @@ function deriveFreeStandingJobsInventoryFromJobs(jobs: WorkflowRecord): { if (!hasJobMarker && !hasScenarioMarker) continue; if (!SELECTOR_ID_PATTERN.test(jobId)) { - errors.push(`free-standing workflow metadata contains invalid job id: ${jobId}`); + errors.push( + `free-standing workflow metadata contains invalid job id: ${jobId}`, + ); } if (!hasJobMarker) { errors.push( @@ -124,7 +104,9 @@ function deriveFreeStandingJobsInventoryFromJobs(jobs: WorkflowRecord): { const scenario = env[FREE_STANDING_SCENARIO_MARKER]; if (typeof scenario !== "string" || !SELECTOR_ID_PATTERN.test(scenario)) { - errors.push(`${jobId} job ${FREE_STANDING_SCENARIO_MARKER} must be a selector id`); + errors.push( + `${jobId} job ${FREE_STANDING_SCENARIO_MARKER} must be a selector id`, + ); continue; } freeStandingScenarios.push(scenario); @@ -132,13 +114,17 @@ function deriveFreeStandingJobsInventoryFromJobs(jobs: WorkflowRecord): { } if (allowedJobs.length === 0) { - errors.push("free-standing workflow metadata must declare at least one job"); + errors.push( + "free-standing workflow metadata must declare at least one job", + ); } for (const duplicate of findDuplicates(allowedJobs)) { errors.push(`free-standing workflow metadata repeats job id: ${duplicate}`); } for (const duplicate of findDuplicates(freeStandingScenarios)) { - errors.push(`free-standing workflow metadata repeats scenario id: ${duplicate}`); + errors.push( + `free-standing workflow metadata repeats scenario id: ${duplicate}`, + ); } return { @@ -151,7 +137,10 @@ function deriveFreeStandingJobsInventoryFromJobs(jobs: WorkflowRecord): { }; } -const freeStandingJobsInventoryCache = new Map(); +const freeStandingJobsInventoryCache = new Map< + string, + CachedFreeStandingJobsInventory +>(); function readWorkflowRecord(workflowPath: string): WorkflowRecord { return asRecord(YAML.parse(readFileSync(workflowPath, "utf-8"))); @@ -171,7 +160,8 @@ export function validateFreeStandingWorkflowInventory( workflowPath = DEFAULT_VITEST_WORKFLOW_PATH, ): string[] { const workflow = readWorkflowRecord(workflowPath); - return deriveFreeStandingJobsInventoryFromJobs(asRecord(workflow.jobs)).errors; + return deriveFreeStandingJobsInventoryFromJobs(asRecord(workflow.jobs)) + .errors; } export function readFreeStandingJobsInventory( @@ -179,14 +169,22 @@ export function readFreeStandingJobsInventory( ): FreeStandingJobsInventory { const stats = statSync(workflowPath); const cached = freeStandingJobsInventoryCache.get(workflowPath); - if (cached && cached.mtimeMs === stats.mtimeMs && cached.size === stats.size) { + if ( + cached && + cached.mtimeMs === stats.mtimeMs && + cached.size === stats.size + ) { return cloneFreeStandingJobsInventory(cached.inventory); } const workflow = readWorkflowRecord(workflowPath); - const { errors, inventory } = deriveFreeStandingJobsInventoryFromJobs(asRecord(workflow.jobs)); + const { errors, inventory } = deriveFreeStandingJobsInventoryFromJobs( + asRecord(workflow.jobs), + ); if (errors.length > 0) { - throw new Error(`Invalid free-standing workflow inventory:\n${errors.join("\n")}`); + throw new Error( + `Invalid free-standing workflow inventory:\n${errors.join("\n")}`, + ); } freeStandingJobsInventoryCache.set(workflowPath, { mtimeMs: stats.mtimeMs, @@ -310,12 +308,20 @@ export function evaluateE2eVitestWorkflowDispatchSelectors(input: { }; } -function namedStep(steps: readonly WorkflowStep[], name: string): WorkflowStep | undefined { +function namedStep( + steps: readonly WorkflowStep[], + name: string, +): WorkflowStep | undefined { return steps.find((step) => step.name === name); } -function requireInput(errors: string[], inputs: WorkflowRecord, name: string): void { - if (!Object.hasOwn(inputs, name)) errors.push(`workflow_dispatch missing input: ${name}`); +function requireInput( + errors: string[], + inputs: WorkflowRecord, + name: string, +): void { + if (!Object.hasOwn(inputs, name)) + errors.push(`workflow_dispatch missing input: ${name}`); } function requireStep( @@ -346,7 +352,9 @@ function requireRunContains( ): void { if (!step) return; if (!stringValue(step.run).includes(expected)) { - errors.push(`step '${step.name ?? ""}' run script must include ${expected}`); + errors.push( + `step '${step.name ?? ""}' run script must include ${expected}`, + ); } } @@ -357,11 +365,17 @@ function requireRunDoesNotContain( ): void { if (!step) return; if (stringValue(step.run).includes(forbidden)) { - errors.push(`step '${step.name ?? ""}' run script must not include ${forbidden}`); + errors.push( + `step '${step.name ?? ""}' run script must not include ${forbidden}`, + ); } } -function requireUploadPathContains(errors: string[], uploadPath: string, expected: string): void { +function requireUploadPathContains( + errors: string[], + uploadPath: string, + expected: string, +): void { if (!uploadPath.includes(expected)) { errors.push(`artifact upload path must include ${expected}`); } @@ -378,111 +392,28 @@ function requireEnvDoesNotExposeSecret( } } -function hasHostedInferenceCredentialEnv(env: WorkflowRecord): boolean { - return HOSTED_INFERENCE_SECRET_ENV_NAMES.some((secret) => Object.hasOwn(env, secret)); -} - -function requireNoHostedInferenceCredentialEnv( - errors: string[], - owner: string, - env: WorkflowRecord, -): void { - for (const secret of HOSTED_INFERENCE_SECRET_ENV_NAMES) { - requireEnvDoesNotExposeSecret(errors, owner, env, secret); - } -} - -function hasHostedInferenceExportAction( - jobName: string | undefined, - allowLegacyNvidiaApiKey: boolean, -): boolean { - if (!jobName) return false; - const steps = asSteps(asRecord(currentWorkflowJobs[jobName]).steps); - return steps.some((step) => { - if (step.uses !== HOSTED_INFERENCE_EXPORT_ACTION) return false; - const actionWith = asRecord(step.with); - if (actionWith["nvidia-inference-api-key"] !== "${{ secrets.NVIDIA_INFERENCE_API_KEY }}") { - return false; - } - if (actionWith["nvidia-api-key"] !== "${{ secrets.NVIDIA_API_KEY }}") { - return false; - } - return allowLegacyNvidiaApiKey - ? actionWith["export-nvidia-api-key"] === "true" - : actionWith["export-nvidia-api-key"] !== "true"; - }); -} - -function requireHostedInferenceCredentialEnv( - errors: string[], - owner: string, - env: WorkflowRecord, - options: { jobName?: string; allowLegacyNvidiaApiKey?: boolean } = {}, -): void { - const allowLegacyNvidiaApiKey = - options.allowLegacyNvidiaApiKey ?? - (options.jobName - ? HOSTED_INFERENCE_LEGACY_NVIDIA_API_KEY_JOB_SET.has(options.jobName) || - HOSTED_INFERENCE_PUBLIC_NVIDIA_FALLBACK_VITEST_JOB_SET.has(options.jobName) - : false); - const hostedEnvKeys = new Set([ - ...HOSTED_INFERENCE_SECRET_ENV_NAMES, - "NVIDIA_API_KEY", - ...Object.keys(HOSTED_INFERENCE_DEFAULT_ENV), - ]); - const hasDirectHostedEnv = [...hostedEnvKeys].some((name) => Object.hasOwn(env, name)); - - if (!hasDirectHostedEnv) { - if (!hasHostedInferenceExportAction(options.jobName, allowLegacyNvidiaApiKey)) { - errors.push(`${owner} must consume the shared hosted inference export action`); - } - return; - } - - if (env.NVIDIA_INFERENCE_API_KEY !== HOSTED_INFERENCE_SECRET_EXPR) { - errors.push(`${owner} must receive NVIDIA_INFERENCE_API_KEY from hosted inference secrets`); - } - if (allowLegacyNvidiaApiKey) { - if (env.NVIDIA_API_KEY !== HOSTED_NVIDIA_API_KEY_EXPR) { - errors.push(`${owner} must receive NVIDIA_API_KEY from hosted inference secrets`); - } - } else if (Object.hasOwn(env, "NVIDIA_API_KEY")) { - errors.push( - `${owner} must not receive NVIDIA_API_KEY unless listed as a legacy alias consumer`, - ); - } - if (env.COMPATIBLE_API_KEY !== HOSTED_INFERENCE_SECRET_EXPR) { - errors.push(`${owner} must receive COMPATIBLE_API_KEY from hosted inference secrets`); - } - for (const [name, expected] of Object.entries(HOSTED_INFERENCE_DEFAULT_ENV)) { - if (env[name] !== expected) { - errors.push(`${owner} must opt in to hosted inference with ${name}`); - } - } -} - -function requireNoHostedInferenceDefaultEnv( +function requireWorkflowDispatch( errors: string[], - owner: string, - env: WorkflowRecord, -): void { - for (const name of Object.keys(HOSTED_INFERENCE_DEFAULT_ENV)) { - if (Object.hasOwn(env, name)) { - errors.push(`${owner} env must not include hosted inference default ${name}`); - } - } -} - -function requireWorkflowDispatch(errors: string[], triggers: WorkflowRecord): WorkflowRecord { + triggers: WorkflowRecord, +): WorkflowRecord { const workflowDispatch = asRecord(triggers.workflow_dispatch); if (Object.keys(workflowDispatch).length === 0) errors.push("workflow must support workflow_dispatch"); return workflowDispatch; } -function rejectAutomaticTriggers(errors: string[], triggers: WorkflowRecord): void { - for (const unsafe of ["push", "pull_request", "pull_request_target", "schedule"]) { - if (Object.hasOwn(triggers, unsafe)) errors.push(`workflow must not run on ${unsafe}`); +function rejectAutomaticTriggers( + errors: string[], + triggers: WorkflowRecord, +): void { + for (const unsafe of [ + "push", + "pull_request", + "pull_request_target", + "schedule", + ]) { + if (Object.hasOwn(triggers, unsafe)) + errors.push(`workflow must not run on ${unsafe}`); } } @@ -492,9 +423,7 @@ function requireFullShaAction( description: string, ): void { if (!step) return; - const uses = stringValue(step.uses); - if (uses.startsWith("./")) return; - if (!/@[0-9a-f]{40}$/i.test(uses)) { + if (!/@[0-9a-f]{40}$/i.test(stringValue(step.uses))) { errors.push(`${description} action must be pinned to a full commit SHA`); } } @@ -503,7 +432,8 @@ function requireNoDispatchInputInterpolation( errors: string[], steps: readonly WorkflowStep[], ): void { - const expressionPattern = /\$\{\{\s*(?:inputs|github\.event\.inputs)\s*(?:\.|\[)/; + const expressionPattern = + /\$\{\{\s*(?:inputs|github\.event\.inputs)\s*(?:\.|\[)/; for (const step of steps) { if (expressionPattern.test(stringValue(step.run))) { errors.push( @@ -549,66 +479,34 @@ function validateFreeStandingInventoryBoundary( if (Object.keys(job).length === 0) continue; if (!FREE_STANDING_SELECTOR_SPECIAL_CASES.has(jobName)) { - validateFreeStandingJobSelector(errors, jobs, jobName, scenarioByJob.get(jobName)); + validateFreeStandingJobSelector( + errors, + jobs, + jobName, + scenarioByJob.get(jobName), + ); } const jobEnv = asRecord(job.env); for (const secret of COMMON_SECRET_ENV_NAMES) { requireEnvDoesNotExposeSecret(errors, `${jobName} job`, jobEnv, secret); } - requireNoHostedInferenceCredentialEnv(errors, `${jobName} job`, jobEnv); const steps = asSteps(job.steps); requireNoDispatchInputInterpolation(errors, steps); for (const step of steps) { if (step.uses) { - requireFullShaAction(errors, step, `${jobName} step '${step.name ?? step.uses}'`); + requireFullShaAction( + errors, + step, + `${jobName} step '${step.name ?? step.uses}'`, + ); } if (/\$\{\{\s*secrets\./.test(stringValue(step.run))) { errors.push( `${jobName} step '${step.name ?? step.uses ?? ""}' run script must not interpolate secrets directly`, ); } - const stepEnv = asRecord(step.env); - if (hasHostedInferenceCredentialEnv(stepEnv)) { - const run = stringValue(step.run); - if (!run.includes("npx vitest run") || !run.includes("--project e2e-scenarios-live")) { - errors.push( - `${jobName} step '${step.name ?? step.uses ?? ""}' must not receive hosted inference credential env`, - ); - } - } - if ( - Object.hasOwn(stepEnv, "NVIDIA_API_KEY") && - !HOSTED_INFERENCE_LEGACY_NVIDIA_API_KEY_JOB_SET.has(jobName) && - !HOSTED_INFERENCE_PUBLIC_NVIDIA_FALLBACK_VITEST_JOB_SET.has(jobName) && - !DIRECT_PUBLIC_NVIDIA_API_KEY_JOBS.has(jobName) - ) { - errors.push( - `${jobName} step '${step.name ?? step.uses ?? ""}' must not receive NVIDIA_API_KEY unless listed as a legacy alias consumer`, - ); - } - } - } -} - -function validateHostedInferenceLegacyAliasInventoryBoundary( - errors: string[], - jobs: WorkflowRecord, -): void { - for (const jobName of HOSTED_INFERENCE_LEGACY_NVIDIA_API_KEY_JOBS) { - const job = asRecord(jobs[jobName]); - if (Object.keys(job).length === 0) continue; - const vitestSteps = asSteps(job.steps).filter((step) => - stringValue(step.run).includes("npx vitest run"), - ); - if ( - !vitestSteps.some( - (step) => asRecord(step.env).NVIDIA_API_KEY === HOSTED_NVIDIA_API_KEY_EXPR, - ) && - !hasHostedInferenceExportAction(jobName, true) - ) { - errors.push(`${jobName} legacy alias consumer must receive NVIDIA_API_KEY`); } } } @@ -629,7 +527,9 @@ function validateFreeStandingInventoryCoverage( } for (const [scenario, jobId] of inventory.scenarioToJob) { if (!inventory.allowedJobs.includes(jobId)) { - errors.push(`free-standing inventory maps ${scenario} to unknown job ${jobId}`); + errors.push( + `free-standing inventory maps ${scenario} to unknown job ${jobId}`, + ); continue; } const job = asRecord(jobs[jobId]); @@ -647,7 +547,10 @@ function validateFreeStandingInventoryCoverage( } } -function validateOpenShellVersionPinVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateOpenShellVersionPinVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "openshell-version-pin-vitest"; const job = asRecord(jobs[jobName]); if (Object.keys(job).length === 0) { @@ -658,14 +561,22 @@ function validateOpenShellVersionPinVitestJob(errors: string[], jobs: WorkflowRe if (job["runs-on"] !== "ubuntu-latest") { errors.push("openshell-version-pin-vitest job must run on ubuntu-latest"); } - validateFreeStandingJobSelector(errors, jobs, jobName, "openshell-version-pin"); + validateFreeStandingJobSelector( + errors, + jobs, + jobName, + "openshell-version-pin", + ); const jobEnv = asRecord(job.env); if (jobEnv.NEMOCLAW_RUN_E2E_SCENARIOS !== "1") { - errors.push("openshell-version-pin-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); + errors.push( + "openshell-version-pin-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1", + ); } if ( - jobEnv.E2E_ARTIFACT_DIR !== "${{ github.workspace }}/e2e-artifacts/vitest/openshell-version-pin" + jobEnv.E2E_ARTIFACT_DIR !== + "${{ github.workspace }}/e2e-artifacts/vitest/openshell-version-pin" ) { errors.push( "openshell-version-pin-vitest job must write artifacts under e2e-artifacts/vitest/openshell-version-pin", @@ -689,16 +600,30 @@ function validateOpenShellVersionPinVitestJob(errors: string[], jobs: WorkflowRe ); } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); - if (!checkout) errors.push("openshell-version-pin-vitest job missing checkout step"); - requireFullShaAction(errors, checkout, "openshell-version-pin-vitest checkout"); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); + if (!checkout) + errors.push("openshell-version-pin-vitest job missing checkout step"); + requireFullShaAction( + errors, + checkout, + "openshell-version-pin-vitest checkout", + ); if (asRecord(checkout?.with)["persist-credentials"] !== false) { - errors.push("openshell-version-pin-vitest checkout step must set persist-credentials=false"); + errors.push( + "openshell-version-pin-vitest checkout step must set persist-credentials=false", + ); } const setupNode = namedStep(steps, "Set up Node"); - if (!setupNode) errors.push("openshell-version-pin-vitest job missing step: Set up Node"); - requireFullShaAction(errors, setupNode, "openshell-version-pin-vitest setup-node"); + if (!setupNode) + errors.push("openshell-version-pin-vitest job missing step: Set up Node"); + requireFullShaAction( + errors, + setupNode, + "openshell-version-pin-vitest setup-node", + ); const installRootDependencies = requireJobStep( errors, @@ -706,20 +631,52 @@ function validateOpenShellVersionPinVitestJob(errors: string[], jobs: WorkflowRe steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); - const runVitest = requireJobStep(errors, jobName, steps, "Run OpenShell version-pin live test"); - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/openshell-version-pin.test.ts"); + const runVitest = requireJobStep( + errors, + jobName, + steps, + "Run OpenShell version-pin live test", + ); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/openshell-version-pin.test.ts", + ); - const upload = requireJobStep(errors, jobName, steps, "Upload OpenShell version-pin artifacts"); - requireFullShaAction(errors, upload, "openshell-version-pin-vitest upload-artifact"); + const upload = requireJobStep( + errors, + jobName, + steps, + "Upload OpenShell version-pin artifacts", + ); + requireFullShaAction( + errors, + upload, + "openshell-version-pin-vitest upload-artifact", + ); const uploadWith = asRecord(upload?.with); if (uploadWith.name !== "e2e-vitest-scenarios-openshell-version-pin") { - errors.push("openshell-version-pin-vitest artifact upload name must be stable"); + errors.push( + "openshell-version-pin-vitest artifact upload name must be stable", + ); } const uploadPath = stringValue(uploadWith.path); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/openshell-version-pin/"); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/openshell-version-pin/", + ); if (uploadWith["include-hidden-files"] !== false) { errors.push( "openshell-version-pin-vitest artifact upload must set include-hidden-files: false", @@ -731,11 +688,16 @@ function validateOpenShellVersionPinVitestJob(errors: string[], jobs: WorkflowRe ); } if (uploadWith["retention-days"] !== 14) { - errors.push("openshell-version-pin-vitest artifact upload retention-days must be 14"); + errors.push( + "openshell-version-pin-vitest artifact upload retention-days must be 14", + ); } } -function validateSkillAgentVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateSkillAgentVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "skill-agent-vitest"; const job = asRecord(jobs[jobName]); if (Object.keys(job).length === 0) { @@ -752,13 +714,18 @@ function validateSkillAgentVitestJob(errors: string[], jobs: WorkflowRecord): vo if (jobEnv.NEMOCLAW_RUN_E2E_SCENARIOS !== "1") { errors.push("skill-agent-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); } - if (jobEnv.E2E_ARTIFACT_DIR !== "${{ github.workspace }}/e2e-artifacts/vitest/skill-agent") { + if ( + jobEnv.E2E_ARTIFACT_DIR !== + "${{ github.workspace }}/e2e-artifacts/vitest/skill-agent" + ) { errors.push( "skill-agent-vitest job must write artifacts under e2e-artifacts/vitest/skill-agent", ); } if (!stringValue(jobEnv.NEMOCLAW_CLI_BIN).includes("bin/nemoclaw.js")) { - errors.push("skill-agent-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI"); + errors.push( + "skill-agent-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI", + ); } requireEnvDoesNotExposeSecret( errors, @@ -780,15 +747,20 @@ function validateSkillAgentVitestJob(errors: string[], jobs: WorkflowRecord): vo } } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); if (!checkout) errors.push("skill-agent-vitest job missing checkout step"); requireFullShaAction(errors, checkout, "skill-agent-vitest checkout"); if (asRecord(checkout?.with)["persist-credentials"] !== false) { - errors.push("skill-agent-vitest checkout step must set persist-credentials=false"); + errors.push( + "skill-agent-vitest checkout step must set persist-credentials=false", + ); } const setupNode = namedStep(steps, "Set up Node"); - if (!setupNode) errors.push("skill-agent-vitest job missing step: Set up Node"); + if (!setupNode) + errors.push("skill-agent-vitest job missing step: Set up Node"); requireFullShaAction(errors, setupNode, "skill-agent-vitest setup-node"); const installRootDependencies = requireJobStep( @@ -797,28 +769,70 @@ function validateSkillAgentVitestJob(errors: string[], jobs: WorkflowRecord): vo steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); const buildCli = requireJobStep(errors, jobName, steps, "Build CLI"); requireRunContains(errors, buildCli, "npm run build:cli"); - const installOpenShell = requireJobStep(errors, jobName, steps, "Install OpenShell CLI"); - requireRunContains(errors, installOpenShell, "bash scripts/install-openshell.sh"); + const installOpenShell = requireJobStep( + errors, + jobName, + steps, + "Install OpenShell CLI", + ); + requireRunContains( + errors, + installOpenShell, + "bash scripts/install-openshell.sh", + ); - const runVitest = requireJobStep(errors, jobName, steps, "Run skill-agent live test"); + const runVitest = requireJobStep( + errors, + jobName, + steps, + "Run skill-agent live test", + ); const runEnv = asRecord(runVitest?.env); - requireHostedInferenceCredentialEnv(errors, "skill-agent-vitest run step", runEnv, { jobName }); + if ( + runEnv.NVIDIA_INFERENCE_API_KEY !== + "${{ secrets.NVIDIA_INFERENCE_API_KEY }}" + ) { + errors.push( + "skill-agent-vitest run step must receive NVIDIA_INFERENCE_API_KEY from secrets", + ); + } requireRunContains( errors, runVitest, 'export PATH="$HOME/.local/bin:$HOME/.npm-global/bin:$PATH"', ); - requireRunContains(errors, runVitest, 'OPENSHELL_BIN="$(command -v openshell)"'); + requireRunContains( + errors, + runVitest, + 'OPENSHELL_BIN="$(command -v openshell)"', + ); requireRunContains(errors, runVitest, "export OPENSHELL_BIN"); - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/skill-agent.test.ts"); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/skill-agent.test.ts", + ); - const upload = requireJobStep(errors, jobName, steps, "Upload skill-agent artifacts"); + const upload = requireJobStep( + errors, + jobName, + steps, + "Upload skill-agent artifacts", + ); requireFullShaAction(errors, upload, "skill-agent-vitest upload-artifact"); const uploadWith = asRecord(upload?.with); if (uploadWith.name !== "e2e-vitest-scenarios-skill-agent") { @@ -845,17 +859,24 @@ function validateSkillAgentVitestJob(errors: string[], jobs: WorkflowRecord): vo } } if (uploadWith["include-hidden-files"] !== false) { - errors.push("skill-agent-vitest artifact upload must set include-hidden-files: false"); + errors.push( + "skill-agent-vitest artifact upload must set include-hidden-files: false", + ); } if (uploadWith["if-no-files-found"] !== "ignore") { - errors.push("skill-agent-vitest artifact upload must ignore missing fixture artifacts"); + errors.push( + "skill-agent-vitest artifact upload must ignore missing fixture artifacts", + ); } if (uploadWith["retention-days"] !== 14) { errors.push("skill-agent-vitest artifact upload retention-days must be 14"); } } -function validateNetworkPolicyVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateNetworkPolicyVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "network-policy-vitest"; const job = asRecord(jobs[jobName]); if (Object.keys(job).length === 0) { @@ -876,18 +897,27 @@ function validateNetworkPolicyVitestJob(errors: string[], jobs: WorkflowRecord): const jobEnv = asRecord(job.env); if (jobEnv.NEMOCLAW_RUN_E2E_SCENARIOS !== "1") { - errors.push("network-policy-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); + errors.push( + "network-policy-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1", + ); } - if (jobEnv.E2E_ARTIFACT_DIR !== "${{ github.workspace }}/e2e-artifacts/vitest/network-policy") { + if ( + jobEnv.E2E_ARTIFACT_DIR !== + "${{ github.workspace }}/e2e-artifacts/vitest/network-policy" + ) { errors.push( "network-policy-vitest job must write artifacts under e2e-artifacts/vitest/network-policy", ); } if (!stringValue(jobEnv.NEMOCLAW_CLI_BIN).includes("bin/nemoclaw.js")) { - errors.push("network-policy-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI"); + errors.push( + "network-policy-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI", + ); } if (jobEnv.OPENSHELL_GATEWAY !== "nemoclaw") { - errors.push("network-policy-vitest job must force OPENSHELL_GATEWAY=nemoclaw"); + errors.push( + "network-policy-vitest job must force OPENSHELL_GATEWAY=nemoclaw", + ); } for (const secret of [ "NVIDIA_INFERENCE_API_KEY", @@ -895,7 +925,12 @@ function validateNetworkPolicyVitestJob(errors: string[], jobs: WorkflowRecord): "DOCKERHUB_TOKEN", "GITHUB_TOKEN", ]) { - requireEnvDoesNotExposeSecret(errors, "network-policy-vitest job", jobEnv, secret); + requireEnvDoesNotExposeSecret( + errors, + "network-policy-vitest job", + jobEnv, + secret, + ); } const steps = asSteps(job.steps); @@ -933,15 +968,20 @@ function validateNetworkPolicyVitestJob(errors: string[], jobs: WorkflowRecord): ); } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); if (!checkout) errors.push("network-policy-vitest job missing checkout step"); requireFullShaAction(errors, checkout, "network-policy-vitest checkout"); if (asRecord(checkout?.with)["persist-credentials"] !== false) { - errors.push("network-policy-vitest checkout step must set persist-credentials=false"); + errors.push( + "network-policy-vitest checkout step must set persist-credentials=false", + ); } const setupNode = namedStep(steps, "Set up Node"); - if (!setupNode) errors.push("network-policy-vitest job missing step: Set up Node"); + if (!setupNode) + errors.push("network-policy-vitest job missing step: Set up Node"); requireFullShaAction(errors, setupNode, "network-policy-vitest setup-node"); const installRootDependencies = requireJobStep( @@ -950,51 +990,102 @@ function validateNetworkPolicyVitestJob(errors: string[], jobs: WorkflowRecord): steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); const buildCli = requireJobStep(errors, jobName, steps, "Build CLI"); requireRunContains(errors, buildCli, "npm run build:cli"); if (namedStep(steps, "Authenticate to Docker Hub")) { - errors.push("network-policy-vitest must not include unused Docker Hub authentication"); + errors.push( + "network-policy-vitest must not include unused Docker Hub authentication", + ); } - const installOpenShell = requireJobStep(errors, jobName, steps, "Install OpenShell"); - requireRunContains(errors, installOpenShell, "bash scripts/install-openshell.sh"); + const installOpenShell = requireJobStep( + errors, + jobName, + steps, + "Install OpenShell", + ); + requireRunContains( + errors, + installOpenShell, + "bash scripts/install-openshell.sh", + ); requireRunContains(errors, installOpenShell, "env -u DOCKER_CONFIG"); requireRunContains(errors, installOpenShell, "-u DOCKERHUB_USERNAME"); requireRunContains(errors, installOpenShell, "-u DOCKERHUB_TOKEN"); requireRunContains(errors, installOpenShell, "-u NVIDIA_INFERENCE_API_KEY"); requireRunContains(errors, installOpenShell, "-u GITHUB_TOKEN"); - const runVitest = requireJobStep(errors, jobName, steps, "Run network-policy live test"); - const runVitestEnv = asRecord(runVitest?.env); - requireHostedInferenceCredentialEnv(errors, "network-policy-vitest Vitest step", runVitestEnv, { + const runVitest = requireJobStep( + errors, jobName, - }); - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/network-policy.test.ts"); - - const upload = requireJobStep(errors, jobName, steps, "Upload network-policy artifacts"); - requireFullShaAction(errors, upload, "network-policy-vitest upload-artifact"); - const uploadWith = asRecord(upload?.with); - if (uploadWith.name !== "e2e-vitest-scenarios-network-policy") { - errors.push("network-policy-vitest artifact upload name must be stable"); - } - const uploadPath = stringValue(uploadWith.path); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/network-policy/"); - if (uploadWith["include-hidden-files"] !== false) { - errors.push("network-policy-vitest artifact upload must set include-hidden-files: false"); + steps, + "Run network-policy live test", + ); + const runVitestEnv = asRecord(runVitest?.env); + if ( + runVitestEnv.NVIDIA_INFERENCE_API_KEY !== + "${{ secrets.NVIDIA_INFERENCE_API_KEY }}" + ) { + errors.push( + "network-policy-vitest Vitest step must receive NVIDIA_INFERENCE_API_KEY from secrets", + ); + } + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/network-policy.test.ts", + ); + + const upload = requireJobStep( + errors, + jobName, + steps, + "Upload network-policy artifacts", + ); + requireFullShaAction(errors, upload, "network-policy-vitest upload-artifact"); + const uploadWith = asRecord(upload?.with); + if (uploadWith.name !== "e2e-vitest-scenarios-network-policy") { + errors.push("network-policy-vitest artifact upload name must be stable"); + } + const uploadPath = stringValue(uploadWith.path); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/network-policy/", + ); + if (uploadWith["include-hidden-files"] !== false) { + errors.push( + "network-policy-vitest artifact upload must set include-hidden-files: false", + ); } if (uploadWith["if-no-files-found"] !== "ignore") { - errors.push("network-policy-vitest artifact upload must ignore missing fixture artifacts"); + errors.push( + "network-policy-vitest artifact upload must ignore missing fixture artifacts", + ); } if (uploadWith["retention-days"] !== 14) { - errors.push("network-policy-vitest artifact upload retention-days must be 14"); + errors.push( + "network-policy-vitest artifact upload retention-days must be 14", + ); } } -function validateCommonEgressAgentVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateCommonEgressAgentVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "common-egress-agent-vitest"; const job = asRecord(jobs[jobName]); if (Object.keys(job).length === 0) { @@ -1007,34 +1098,49 @@ function validateCommonEgressAgentVitestJob(errors: string[], jobs: WorkflowReco } validateFreeStandingJobSelector(errors, jobs, jobName, "common-egress-agent"); if (job["timeout-minutes"] !== 120) { - errors.push("common-egress-agent-vitest job must keep the legacy 120 minute timeout"); + errors.push( + "common-egress-agent-vitest job must keep the legacy 120 minute timeout", + ); } const jobEnv = asRecord(job.env); if (jobEnv.NEMOCLAW_RUN_E2E_SCENARIOS !== "1") { - errors.push("common-egress-agent-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); + errors.push( + "common-egress-agent-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1", + ); } if ( - jobEnv.E2E_ARTIFACT_DIR !== "${{ github.workspace }}/e2e-artifacts/vitest/common-egress-agent" + jobEnv.E2E_ARTIFACT_DIR !== + "${{ github.workspace }}/e2e-artifacts/vitest/common-egress-agent" ) { errors.push( "common-egress-agent-vitest job must write artifacts under e2e-artifacts/vitest/common-egress-agent", ); } if (!stringValue(jobEnv.NEMOCLAW_CLI_BIN).includes("bin/nemoclaw.js")) { - errors.push("common-egress-agent-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI"); + errors.push( + "common-egress-agent-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI", + ); } if (jobEnv.NEMOCLAW_NON_INTERACTIVE !== "1") { - errors.push("common-egress-agent-vitest job must set NEMOCLAW_NON_INTERACTIVE=1"); + errors.push( + "common-egress-agent-vitest job must set NEMOCLAW_NON_INTERACTIVE=1", + ); } if (jobEnv.NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE !== "1") { - errors.push("common-egress-agent-vitest job must set NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1"); + errors.push( + "common-egress-agent-vitest job must set NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1", + ); } if (jobEnv.NEMOCLAW_RECREATE_SANDBOX !== "1") { - errors.push("common-egress-agent-vitest job must set NEMOCLAW_RECREATE_SANDBOX=1"); + errors.push( + "common-egress-agent-vitest job must set NEMOCLAW_RECREATE_SANDBOX=1", + ); } if (jobEnv.OPENSHELL_GATEWAY !== "nemoclaw") { - errors.push("common-egress-agent-vitest job must force OPENSHELL_GATEWAY=nemoclaw"); + errors.push( + "common-egress-agent-vitest job must force OPENSHELL_GATEWAY=nemoclaw", + ); } for (const secret of [ "NVIDIA_API_KEY", @@ -1042,7 +1148,12 @@ function validateCommonEgressAgentVitestJob(errors: string[], jobs: WorkflowReco "DOCKERHUB_TOKEN", "GITHUB_TOKEN", ]) { - requireEnvDoesNotExposeSecret(errors, "common-egress-agent-vitest job", jobEnv, secret); + requireEnvDoesNotExposeSecret( + errors, + "common-egress-agent-vitest job", + jobEnv, + secret, + ); } const steps = asSteps(job.steps); @@ -1058,7 +1169,11 @@ function validateCommonEgressAgentVitestJob(errors: string[], jobs: WorkflowReco "NVIDIA_API_KEY", ); } - for (const secret of ["DOCKERHUB_USERNAME", "DOCKERHUB_TOKEN", "GITHUB_TOKEN"]) { + for (const secret of [ + "DOCKERHUB_USERNAME", + "DOCKERHUB_TOKEN", + "GITHUB_TOKEN", + ]) { requireEnvDoesNotExposeSecret( errors, `common-egress-agent-vitest step '${stepName}'`, @@ -1068,16 +1183,26 @@ function validateCommonEgressAgentVitestJob(errors: string[], jobs: WorkflowReco } } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); - if (!checkout) errors.push("common-egress-agent-vitest job missing checkout step"); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); + if (!checkout) + errors.push("common-egress-agent-vitest job missing checkout step"); requireFullShaAction(errors, checkout, "common-egress-agent-vitest checkout"); if (asRecord(checkout?.with)["persist-credentials"] !== false) { - errors.push("common-egress-agent-vitest checkout step must set persist-credentials=false"); + errors.push( + "common-egress-agent-vitest checkout step must set persist-credentials=false", + ); } const setupNode = namedStep(steps, "Set up Node"); - if (!setupNode) errors.push("common-egress-agent-vitest job missing step: Set up Node"); - requireFullShaAction(errors, setupNode, "common-egress-agent-vitest setup-node"); + if (!setupNode) + errors.push("common-egress-agent-vitest job missing step: Set up Node"); + requireFullShaAction( + errors, + setupNode, + "common-egress-agent-vitest setup-node", + ); const installRootDependencies = requireJobStep( errors, @@ -1085,48 +1210,100 @@ function validateCommonEgressAgentVitestJob(errors: string[], jobs: WorkflowReco steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); const buildCli = requireJobStep(errors, jobName, steps, "Build CLI"); requireRunContains(errors, buildCli, "npm run build:cli"); - const installOpenShell = requireJobStep(errors, jobName, steps, "Install OpenShell"); - requireRunContains(errors, installOpenShell, "bash scripts/install-openshell.sh"); + const installOpenShell = requireJobStep( + errors, + jobName, + steps, + "Install OpenShell", + ); + requireRunContains( + errors, + installOpenShell, + "bash scripts/install-openshell.sh", + ); requireRunContains(errors, installOpenShell, "env -u DOCKER_CONFIG"); requireRunContains(errors, installOpenShell, "-u DOCKERHUB_USERNAME"); requireRunContains(errors, installOpenShell, "-u DOCKERHUB_TOKEN"); requireRunContains(errors, installOpenShell, "-u NVIDIA_API_KEY"); requireRunContains(errors, installOpenShell, "-u GITHUB_TOKEN"); - const runVitest = requireJobStep(errors, jobName, steps, "Run common-egress agent live test"); - const runVitestEnv = asRecord(runVitest?.env); - requireHostedInferenceCredentialEnv(errors, "common-egress-agent-vitest step", runVitestEnv, { + const runVitest = requireJobStep( + errors, jobName, - }); + steps, + "Run common-egress agent live test", + ); + const runVitestEnv = asRecord(runVitest?.env); + if (runVitestEnv.NVIDIA_API_KEY !== "${{ secrets.NVIDIA_API_KEY }}") { + errors.push( + "common-egress-agent-vitest step must receive NVIDIA_API_KEY from secrets", + ); + } requireRunContains(errors, runVitest, "OPENSHELL_BIN"); - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/common-egress-agent.test.ts"); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/common-egress-agent.test.ts", + ); - const upload = requireJobStep(errors, jobName, steps, "Upload common-egress agent artifacts"); - requireFullShaAction(errors, upload, "common-egress-agent-vitest upload-artifact"); + const upload = requireJobStep( + errors, + jobName, + steps, + "Upload common-egress agent artifacts", + ); + requireFullShaAction( + errors, + upload, + "common-egress-agent-vitest upload-artifact", + ); const uploadWith = asRecord(upload?.with); if (uploadWith.name !== "e2e-vitest-scenarios-common-egress-agent") { - errors.push("common-egress-agent-vitest artifact upload name must be stable"); + errors.push( + "common-egress-agent-vitest artifact upload name must be stable", + ); } const uploadPath = stringValue(uploadWith.path); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/common-egress-agent/"); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/common-egress-agent/", + ); if (uploadWith["include-hidden-files"] !== false) { - errors.push("common-egress-agent-vitest artifact upload must set include-hidden-files: false"); + errors.push( + "common-egress-agent-vitest artifact upload must set include-hidden-files: false", + ); } if (uploadWith["if-no-files-found"] !== "ignore") { - errors.push("common-egress-agent-vitest artifact upload must ignore missing fixture artifacts"); + errors.push( + "common-egress-agent-vitest artifact upload must ignore missing fixture artifacts", + ); } if (uploadWith["retention-days"] !== 14) { - errors.push("common-egress-agent-vitest artifact upload retention-days must be 14"); + errors.push( + "common-egress-agent-vitest artifact upload retention-days must be 14", + ); } } -function validateShieldsConfigVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateShieldsConfigVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "shields-config-vitest"; const job = asRecord(jobs[jobName]); if (Object.keys(job).length === 0) { @@ -1139,28 +1316,43 @@ function validateShieldsConfigVitestJob(errors: string[], jobs: WorkflowRecord): } validateFreeStandingJobSelector(errors, jobs, jobName, "shields-config"); if (job["timeout-minutes"] !== 45) { - errors.push("shields-config-vitest job must keep the legacy 45 minute timeout"); + errors.push( + "shields-config-vitest job must keep the legacy 45 minute timeout", + ); } const jobEnv = asRecord(job.env); if (jobEnv.NEMOCLAW_RUN_E2E_SCENARIOS !== "1") { - errors.push("shields-config-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); + errors.push( + "shields-config-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1", + ); } - if (jobEnv.E2E_ARTIFACT_DIR !== "${{ github.workspace }}/e2e-artifacts/vitest/shields-config") { + if ( + jobEnv.E2E_ARTIFACT_DIR !== + "${{ github.workspace }}/e2e-artifacts/vitest/shields-config" + ) { errors.push( "shields-config-vitest job must write artifacts under e2e-artifacts/vitest/shields-config", ); } if (jobEnv.OPENSHELL_GATEWAY !== "nemoclaw") { - errors.push("shields-config-vitest job must force OPENSHELL_GATEWAY=nemoclaw"); + errors.push( + "shields-config-vitest job must force OPENSHELL_GATEWAY=nemoclaw", + ); } if (jobEnv.NEMOCLAW_NON_INTERACTIVE !== "1") { - errors.push("shields-config-vitest job must set NEMOCLAW_NON_INTERACTIVE=1"); + errors.push( + "shields-config-vitest job must set NEMOCLAW_NON_INTERACTIVE=1", + ); } if (jobEnv.NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE !== "1") { - errors.push("shields-config-vitest job must set NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1"); + errors.push( + "shields-config-vitest job must set NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1", + ); } if (jobEnv.NEMOCLAW_SANDBOX_NAME !== "e2e-shields") { - errors.push("shields-config-vitest job must set NEMOCLAW_SANDBOX_NAME=e2e-shields"); + errors.push( + "shields-config-vitest job must set NEMOCLAW_SANDBOX_NAME=e2e-shields", + ); } requireEnvDoesNotExposeSecret( errors, @@ -1168,8 +1360,18 @@ function validateShieldsConfigVitestJob(errors: string[], jobs: WorkflowRecord): jobEnv, "NVIDIA_INFERENCE_API_KEY", ); - requireEnvDoesNotExposeSecret(errors, "shields-config-vitest job", jobEnv, "DOCKERHUB_USERNAME"); - requireEnvDoesNotExposeSecret(errors, "shields-config-vitest job", jobEnv, "DOCKERHUB_TOKEN"); + requireEnvDoesNotExposeSecret( + errors, + "shields-config-vitest job", + jobEnv, + "DOCKERHUB_USERNAME", + ); + requireEnvDoesNotExposeSecret( + errors, + "shields-config-vitest job", + jobEnv, + "DOCKERHUB_TOKEN", + ); const steps = asSteps(job.steps); requireNoDispatchInputInterpolation(errors, steps); @@ -1200,14 +1402,23 @@ function validateShieldsConfigVitestJob(errors: string[], jobs: WorkflowRecord): } } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); if (!checkout) errors.push("shields-config-vitest job missing checkout step"); requireFullShaAction(errors, checkout, "shields-config-vitest checkout"); if (asRecord(checkout?.with)["persist-credentials"] !== false) { - errors.push("shields-config-vitest checkout step must set persist-credentials=false"); + errors.push( + "shields-config-vitest checkout step must set persist-credentials=false", + ); } - const dockerHubAuth = requireJobStep(errors, jobName, steps, "Authenticate to Docker Hub"); + const dockerHubAuth = requireJobStep( + errors, + jobName, + steps, + "Authenticate to Docker Hub", + ); const dockerHubEnv = asRecord(dockerHubAuth?.env); if (dockerHubEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}") { errors.push( @@ -1215,12 +1426,15 @@ function validateShieldsConfigVitestJob(errors: string[], jobs: WorkflowRecord): ); } if (dockerHubEnv.DOCKERHUB_TOKEN !== "${{ secrets.DOCKERHUB_TOKEN }}") { - errors.push("shields-config-vitest Docker Hub auth must receive DOCKERHUB_TOKEN from secrets"); + errors.push( + "shields-config-vitest Docker Hub auth must receive DOCKERHUB_TOKEN from secrets", + ); } requireRunContains(errors, dockerHubAuth, "docker login docker.io"); const setupNode = namedStep(steps, "Set up Node"); - if (!setupNode) errors.push("shields-config-vitest job missing step: Set up Node"); + if (!setupNode) + errors.push("shields-config-vitest job missing step: Set up Node"); requireFullShaAction(errors, setupNode, "shields-config-vitest setup-node"); const installRootDependencies = requireJobStep( @@ -1229,37 +1443,81 @@ function validateShieldsConfigVitestJob(errors: string[], jobs: WorkflowRecord): steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); - const runVitest = requireJobStep(errors, jobName, steps, "Run shields-config live test"); - const runVitestEnv = asRecord(runVitest?.env); - requireHostedInferenceCredentialEnv(errors, "shields-config-vitest step", runVitestEnv, { + const runVitest = requireJobStep( + errors, jobName, - }); - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/shields-config.test.ts"); + steps, + "Run shields-config live test", + ); + const runVitestEnv = asRecord(runVitest?.env); + if ( + runVitestEnv.NVIDIA_INFERENCE_API_KEY !== + "${{ secrets.NVIDIA_INFERENCE_API_KEY }}" + ) { + errors.push( + "shields-config-vitest step must receive NVIDIA_INFERENCE_API_KEY from secrets", + ); + } + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/shields-config.test.ts", + ); - const upload = requireJobStep(errors, jobName, steps, "Upload shields-config artifacts"); + const upload = requireJobStep( + errors, + jobName, + steps, + "Upload shields-config artifacts", + ); requireFullShaAction(errors, upload, "shields-config-vitest upload-artifact"); const uploadWith = asRecord(upload?.with); if (uploadWith.name !== "e2e-vitest-scenarios-shields-config") { errors.push("shields-config-vitest artifact upload name must be stable"); } const uploadPath = stringValue(uploadWith.path); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/shields-config/"); - requireUploadPathContains(errors, uploadPath, "/tmp/nemoclaw-e2e-shields-install.log"); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/shields-config/", + ); + requireUploadPathContains( + errors, + uploadPath, + "/tmp/nemoclaw-e2e-shields-install.log", + ); if (uploadWith["include-hidden-files"] !== false) { - errors.push("shields-config-vitest artifact upload must set include-hidden-files: false"); + errors.push( + "shields-config-vitest artifact upload must set include-hidden-files: false", + ); } if (uploadWith["if-no-files-found"] !== "ignore") { - errors.push("shields-config-vitest artifact upload must ignore missing fixture artifacts"); + errors.push( + "shields-config-vitest artifact upload must ignore missing fixture artifacts", + ); } if (uploadWith["retention-days"] !== 14) { - errors.push("shields-config-vitest artifact upload retention-days must be 14"); + errors.push( + "shields-config-vitest artifact upload retention-days must be 14", + ); } } -function validateRebuildOpenClawVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateRebuildOpenClawVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "rebuild-openclaw-vitest"; const job = asRecord(jobs[jobName]); if (Object.keys(job).length === 0) { @@ -1272,19 +1530,28 @@ function validateRebuildOpenClawVitestJob(errors: string[], jobs: WorkflowRecord } validateFreeStandingJobSelector(errors, jobs, jobName, "rebuild-openclaw"); if (job["timeout-minutes"] !== 130) { - errors.push("rebuild-openclaw-vitest job must keep the legacy 130 minute timeout"); + errors.push( + "rebuild-openclaw-vitest job must keep the legacy 130 minute timeout", + ); } const jobEnv = asRecord(job.env); if (jobEnv.NEMOCLAW_RUN_E2E_SCENARIOS !== "1") { - errors.push("rebuild-openclaw-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); + errors.push( + "rebuild-openclaw-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1", + ); } - if (jobEnv.E2E_ARTIFACT_DIR !== "${{ github.workspace }}/e2e-artifacts/vitest/rebuild-openclaw") { + if ( + jobEnv.E2E_ARTIFACT_DIR !== + "${{ github.workspace }}/e2e-artifacts/vitest/rebuild-openclaw" + ) { errors.push( "rebuild-openclaw-vitest job must write artifacts under e2e-artifacts/vitest/rebuild-openclaw", ); } if (!stringValue(jobEnv.NEMOCLAW_CLI_BIN).includes("bin/nemoclaw.js")) { - errors.push("rebuild-openclaw-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI"); + errors.push( + "rebuild-openclaw-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI", + ); } requireEnvDoesNotExposeSecret( errors, @@ -1306,14 +1573,24 @@ function validateRebuildOpenClawVitestJob(errors: string[], jobs: WorkflowRecord } } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); - if (!checkout) errors.push("rebuild-openclaw-vitest job missing checkout step"); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); + if (!checkout) + errors.push("rebuild-openclaw-vitest job missing checkout step"); requireFullShaAction(errors, checkout, "rebuild-openclaw-vitest checkout"); if (asRecord(checkout?.with)["persist-credentials"] !== false) { - errors.push("rebuild-openclaw-vitest checkout step must set persist-credentials=false"); + errors.push( + "rebuild-openclaw-vitest checkout step must set persist-credentials=false", + ); } - const dockerHubAuth = requireJobStep(errors, jobName, steps, "Authenticate to Docker Hub"); + const dockerHubAuth = requireJobStep( + errors, + jobName, + steps, + "Authenticate to Docker Hub", + ); const dockerHubEnv = asRecord(dockerHubAuth?.env); if (dockerHubEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}") { errors.push( @@ -1328,7 +1605,8 @@ function validateRebuildOpenClawVitestJob(errors: string[], jobs: WorkflowRecord requireRunContains(errors, dockerHubAuth, "docker login docker.io"); const setupNode = namedStep(steps, "Set up Node"); - if (!setupNode) errors.push("rebuild-openclaw-vitest job missing step: Set up Node"); + if (!setupNode) + errors.push("rebuild-openclaw-vitest job missing step: Set up Node"); requireFullShaAction(errors, setupNode, "rebuild-openclaw-vitest setup-node"); const installRootDependencies = requireJobStep( @@ -1337,50 +1615,100 @@ function validateRebuildOpenClawVitestJob(errors: string[], jobs: WorkflowRecord steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); const buildCli = requireJobStep(errors, jobName, steps, "Build CLI"); requireRunContains(errors, buildCli, "npm run build:cli"); - const installOpenShell = requireJobStep(errors, jobName, steps, "Install OpenShell"); + const installOpenShell = requireJobStep( + errors, + jobName, + steps, + "Install OpenShell", + ); requireEnvDoesNotExposeSecret( errors, "rebuild-openclaw-vitest step 'Install OpenShell'", asRecord(installOpenShell?.env), "GITHUB_TOKEN", ); - requireRunContains(errors, installOpenShell, "bash scripts/install-openshell.sh"); + requireRunContains( + errors, + installOpenShell, + "bash scripts/install-openshell.sh", + ); requireRunContains(errors, installOpenShell, "env -u DOCKER_CONFIG"); requireRunContains(errors, installOpenShell, "-u DOCKERHUB_USERNAME"); requireRunContains(errors, installOpenShell, "-u DOCKERHUB_TOKEN"); requireRunContains(errors, installOpenShell, "-u NVIDIA_INFERENCE_API_KEY"); requireRunContains(errors, installOpenShell, "-u GITHUB_TOKEN"); - const runVitest = requireJobStep(errors, jobName, steps, "Run OpenClaw rebuild live test"); - const runVitestEnv = asRecord(runVitest?.env); - requireHostedInferenceCredentialEnv(errors, "rebuild-openclaw-vitest step", runVitestEnv, { + const runVitest = requireJobStep( + errors, jobName, - }); - requireRunContains(errors, runVitest, "OPENSHELL_BIN"); - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/rebuild-openclaw.test.ts"); - - const upload = requireJobStep(errors, jobName, steps, "Upload OpenClaw rebuild artifacts"); - requireFullShaAction(errors, upload, "rebuild-openclaw-vitest upload-artifact"); - const uploadWith = asRecord(upload?.with); - if (uploadWith.name !== "e2e-vitest-scenarios-rebuild-openclaw") { - errors.push("rebuild-openclaw-vitest artifact upload name must be stable"); + steps, + "Run OpenClaw rebuild live test", + ); + const runVitestEnv = asRecord(runVitest?.env); + if ( + runVitestEnv.NVIDIA_INFERENCE_API_KEY !== + "${{ secrets.NVIDIA_INFERENCE_API_KEY }}" + ) { + errors.push( + "rebuild-openclaw-vitest step must receive NVIDIA_INFERENCE_API_KEY from secrets", + ); + } + requireRunContains(errors, runVitest, "OPENSHELL_BIN"); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/rebuild-openclaw.test.ts", + ); + + const upload = requireJobStep( + errors, + jobName, + steps, + "Upload OpenClaw rebuild artifacts", + ); + requireFullShaAction( + errors, + upload, + "rebuild-openclaw-vitest upload-artifact", + ); + const uploadWith = asRecord(upload?.with); + if (uploadWith.name !== "e2e-vitest-scenarios-rebuild-openclaw") { + errors.push("rebuild-openclaw-vitest artifact upload name must be stable"); } const uploadPath = stringValue(uploadWith.path); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/rebuild-openclaw/"); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/rebuild-openclaw/", + ); if (uploadWith["include-hidden-files"] !== false) { - errors.push("rebuild-openclaw-vitest artifact upload must set include-hidden-files: false"); + errors.push( + "rebuild-openclaw-vitest artifact upload must set include-hidden-files: false", + ); } if (uploadWith["if-no-files-found"] !== "ignore") { - errors.push("rebuild-openclaw-vitest artifact upload must ignore missing fixture artifacts"); + errors.push( + "rebuild-openclaw-vitest artifact upload must ignore missing fixture artifacts", + ); } if (uploadWith["retention-days"] !== 14) { - errors.push("rebuild-openclaw-vitest artifact upload retention-days must be 14"); + errors.push( + "rebuild-openclaw-vitest artifact upload retention-days must be 14", + ); } } @@ -1389,8 +1717,12 @@ function validateRebuildHermesVitestJob( jobs: WorkflowRecord, options: { staleBase: boolean }, ): void { - const jobName = options.staleBase ? "rebuild-hermes-stale-base-vitest" : "rebuild-hermes-vitest"; - const scenarioName = options.staleBase ? "rebuild-hermes-stale-base" : "rebuild-hermes"; + const jobName = options.staleBase + ? "rebuild-hermes-stale-base-vitest" + : "rebuild-hermes-vitest"; + const scenarioName = options.staleBase + ? "rebuild-hermes-stale-base" + : "rebuild-hermes"; const job = asRecord(jobs[jobName]); if (Object.keys(job).length === 0) { errors.push(`workflow missing ${jobName} job`); @@ -1418,7 +1750,9 @@ function validateRebuildHermesVitestJob( errors.push(`${jobName} job must set NEMOCLAW_AGENT=hermes`); } if (jobEnv.NEMOCLAW_PROVIDER !== "custom") { - errors.push(`${jobName} job must use the hosted compatible endpoint provider`); + errors.push( + `${jobName} job must use the hosted compatible endpoint provider`, + ); } if (jobEnv.NEMOCLAW_ENDPOINT_URL !== "https://inference-api.nvidia.com/v1") { errors.push(`${jobName} job must target hosted CI inference endpoint`); @@ -1434,13 +1768,19 @@ function validateRebuildHermesVitestJob( } if (options.staleBase) { if (jobEnv.NEMOCLAW_HERMES_STALE_BASE_REBUILD_E2E !== "1") { - errors.push(`${jobName} job must enable NEMOCLAW_HERMES_STALE_BASE_REBUILD_E2E=1`); + errors.push( + `${jobName} job must enable NEMOCLAW_HERMES_STALE_BASE_REBUILD_E2E=1`, + ); } if (jobEnv.NEMOCLAW_SANDBOX_NAME !== "e2e-rebuild-hermes-base") { - errors.push(`${jobName} job must set NEMOCLAW_SANDBOX_NAME=e2e-rebuild-hermes-base`); + errors.push( + `${jobName} job must set NEMOCLAW_SANDBOX_NAME=e2e-rebuild-hermes-base`, + ); } } else if (jobEnv.NEMOCLAW_SANDBOX_NAME !== "e2e-rebuild-hermes") { - errors.push(`${jobName} job must set NEMOCLAW_SANDBOX_NAME=e2e-rebuild-hermes`); + errors.push( + `${jobName} job must set NEMOCLAW_SANDBOX_NAME=e2e-rebuild-hermes`, + ); } for (const secret of [ "NVIDIA_INFERENCE_API_KEY", @@ -1457,30 +1797,56 @@ function validateRebuildHermesVitestJob( const stepName = `${jobName} step '${step.name ?? step.uses ?? ""}'`; const stepEnv = asRecord(step.env); if (!step.name?.startsWith("Run Hermes")) { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_INFERENCE_API_KEY"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "NVIDIA_INFERENCE_API_KEY", + ); } if (step.name !== "Authenticate to Docker Hub") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_USERNAME"); - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_TOKEN"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_USERNAME", + ); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_TOKEN", + ); requireNoDockerHubAuthInRun(errors, stepName, stringValue(step.run)); } requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "GITHUB_TOKEN"); } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); if (!checkout) errors.push(`${jobName} job missing checkout step`); requireFullShaAction(errors, checkout, `${jobName} checkout`); if (asRecord(checkout?.with)["persist-credentials"] !== false) { errors.push(`${jobName} checkout step must set persist-credentials=false`); } - const dockerHubAuth = requireJobStep(errors, jobName, steps, "Authenticate to Docker Hub"); + const dockerHubAuth = requireJobStep( + errors, + jobName, + steps, + "Authenticate to Docker Hub", + ); const dockerHubEnv = asRecord(dockerHubAuth?.env); if (dockerHubEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}") { - errors.push(`${jobName} Docker Hub auth must receive DOCKERHUB_USERNAME from secrets`); + errors.push( + `${jobName} Docker Hub auth must receive DOCKERHUB_USERNAME from secrets`, + ); } if (dockerHubEnv.DOCKERHUB_TOKEN !== "${{ secrets.DOCKERHUB_TOKEN }}") { - errors.push(`${jobName} Docker Hub auth must receive DOCKERHUB_TOKEN from secrets`); + errors.push( + `${jobName} Docker Hub auth must receive DOCKERHUB_TOKEN from secrets`, + ); } requireRunContains(errors, dockerHubAuth, "docker login docker.io"); requireRunContains(errors, dockerHubAuth, "continuing with anonymous pulls"); @@ -1495,18 +1861,39 @@ function validateRebuildHermesVitestJob( steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); const runVitest = requireJobStep( errors, jobName, steps, - options.staleBase ? "Run Hermes stale-base rebuild live test" : "Run Hermes rebuild live test", + options.staleBase + ? "Run Hermes stale-base rebuild live test" + : "Run Hermes rebuild live test", ); const runVitestEnv = asRecord(runVitest?.env); - requireHostedInferenceCredentialEnv(errors, `${jobName} step`, runVitestEnv, { jobName }); - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/rebuild-hermes.test.ts"); + if ( + runVitestEnv.NVIDIA_INFERENCE_API_KEY !== + "${{ secrets.NVIDIA_INFERENCE_API_KEY }}" + ) { + errors.push( + `${jobName} step must receive NVIDIA_INFERENCE_API_KEY from secrets`, + ); + } + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/rebuild-hermes.test.ts", + ); const upload = requireJobStep( errors, @@ -1533,17 +1920,24 @@ function validateRebuildHermesVitestJob( : "e2e-artifacts/vitest/rebuild-hermes/", ); if (uploadWith["include-hidden-files"] !== false) { - errors.push(`${jobName} artifact upload must set include-hidden-files: false`); + errors.push( + `${jobName} artifact upload must set include-hidden-files: false`, + ); } if (uploadWith["if-no-files-found"] !== "ignore") { - errors.push(`${jobName} artifact upload must ignore missing fixture artifacts`); + errors.push( + `${jobName} artifact upload must ignore missing fixture artifacts`, + ); } if (uploadWith["retention-days"] !== 14) { errors.push(`${jobName} artifact upload retention-days must be 14`); } } -function validateSandboxRebuildVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateSandboxRebuildVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "sandbox-rebuild-vitest"; const scenarioName = "sandbox-rebuild"; const job = asRecord(jobs[jobName]); @@ -1557,22 +1951,33 @@ function validateSandboxRebuildVitestJob(errors: string[], jobs: WorkflowRecord) } validateFreeStandingJobSelector(errors, jobs, jobName, scenarioName); if (job["timeout-minutes"] !== 90) { - errors.push("sandbox-rebuild-vitest job must keep the legacy 90 minute timeout"); + errors.push( + "sandbox-rebuild-vitest job must keep the legacy 90 minute timeout", + ); } const jobEnv = asRecord(job.env); if (jobEnv.NEMOCLAW_RUN_E2E_SCENARIOS !== "1") { - errors.push("sandbox-rebuild-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); + errors.push( + "sandbox-rebuild-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1", + ); } - if (jobEnv.E2E_ARTIFACT_DIR !== "${{ github.workspace }}/e2e-artifacts/vitest/sandbox-rebuild") { + if ( + jobEnv.E2E_ARTIFACT_DIR !== + "${{ github.workspace }}/e2e-artifacts/vitest/sandbox-rebuild" + ) { errors.push( "sandbox-rebuild-vitest job must write artifacts under e2e-artifacts/vitest/sandbox-rebuild", ); } if (jobEnv.NEMOCLAW_CLI_BIN !== "${{ github.workspace }}/bin/nemoclaw.js") { - errors.push("sandbox-rebuild-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI"); + errors.push( + "sandbox-rebuild-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI", + ); } if (jobEnv.OPENSHELL_GATEWAY !== "nemoclaw") { - errors.push("sandbox-rebuild-vitest job must force OPENSHELL_GATEWAY=nemoclaw"); + errors.push( + "sandbox-rebuild-vitest job must force OPENSHELL_GATEWAY=nemoclaw", + ); } for (const secret of [ "NVIDIA_INFERENCE_API_KEY", @@ -1580,7 +1985,12 @@ function validateSandboxRebuildVitestJob(errors: string[], jobs: WorkflowRecord) "DOCKERHUB_TOKEN", "GITHUB_TOKEN", ]) { - requireEnvDoesNotExposeSecret(errors, "sandbox-rebuild-vitest job", jobEnv, secret); + requireEnvDoesNotExposeSecret( + errors, + "sandbox-rebuild-vitest job", + jobEnv, + secret, + ); } const steps = asSteps(job.steps); @@ -1589,24 +1999,49 @@ function validateSandboxRebuildVitestJob(errors: string[], jobs: WorkflowRecord) const stepName = `sandbox-rebuild-vitest step '${step.name ?? step.uses ?? ""}'`; const stepEnv = asRecord(step.env); if (step.name !== "Run sandbox rebuild live test") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_INFERENCE_API_KEY"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "NVIDIA_INFERENCE_API_KEY", + ); } if (step.name !== "Authenticate to Docker Hub") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_USERNAME"); - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_TOKEN"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_USERNAME", + ); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_TOKEN", + ); requireNoDockerHubAuthInRun(errors, stepName, stringValue(step.run)); } requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "GITHUB_TOKEN"); } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); - if (!checkout) errors.push("sandbox-rebuild-vitest job missing checkout step"); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); + if (!checkout) + errors.push("sandbox-rebuild-vitest job missing checkout step"); requireFullShaAction(errors, checkout, "sandbox-rebuild-vitest checkout"); if (asRecord(checkout?.with)["persist-credentials"] !== false) { - errors.push("sandbox-rebuild-vitest checkout step must set persist-credentials=false"); + errors.push( + "sandbox-rebuild-vitest checkout step must set persist-credentials=false", + ); } - const dockerHubAuth = requireJobStep(errors, jobName, steps, "Authenticate to Docker Hub"); + const dockerHubAuth = requireJobStep( + errors, + jobName, + steps, + "Authenticate to Docker Hub", + ); const dockerHubEnv = asRecord(dockerHubAuth?.env); if (dockerHubEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}") { errors.push( @@ -1614,13 +2049,16 @@ function validateSandboxRebuildVitestJob(errors: string[], jobs: WorkflowRecord) ); } if (dockerHubEnv.DOCKERHUB_TOKEN !== "${{ secrets.DOCKERHUB_TOKEN }}") { - errors.push("sandbox-rebuild-vitest Docker Hub auth must receive DOCKERHUB_TOKEN from secrets"); + errors.push( + "sandbox-rebuild-vitest Docker Hub auth must receive DOCKERHUB_TOKEN from secrets", + ); } requireRunContains(errors, dockerHubAuth, "docker login docker.io"); requireRunContains(errors, dockerHubAuth, "continuing with anonymous pulls"); const setupNode = namedStep(steps, "Set up Node"); - if (!setupNode) errors.push("sandbox-rebuild-vitest job missing step: Set up Node"); + if (!setupNode) + errors.push("sandbox-rebuild-vitest job missing step: Set up Node"); requireFullShaAction(errors, setupNode, "sandbox-rebuild-vitest setup-node"); const installRootDependencies = requireJobStep( @@ -1629,48 +2067,101 @@ function validateSandboxRebuildVitestJob(errors: string[], jobs: WorkflowRecord) steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); const buildCli = requireJobStep(errors, jobName, steps, "Build CLI"); requireRunContains(errors, buildCli, "npm run build:cli"); - const installOpenShell = requireJobStep(errors, jobName, steps, "Install OpenShell"); - requireRunContains(errors, installOpenShell, "bash scripts/install-openshell.sh"); + const installOpenShell = requireJobStep( + errors, + jobName, + steps, + "Install OpenShell", + ); + requireRunContains( + errors, + installOpenShell, + "bash scripts/install-openshell.sh", + ); requireRunContains(errors, installOpenShell, "env -u DOCKER_CONFIG"); requireRunContains(errors, installOpenShell, "-u DOCKERHUB_USERNAME"); requireRunContains(errors, installOpenShell, "-u DOCKERHUB_TOKEN"); requireRunContains(errors, installOpenShell, "-u NVIDIA_INFERENCE_API_KEY"); requireRunContains(errors, installOpenShell, "-u GITHUB_TOKEN"); - const runVitest = requireJobStep(errors, jobName, steps, "Run sandbox rebuild live test"); - const runVitestEnv = asRecord(runVitest?.env); - requireHostedInferenceCredentialEnv(errors, "sandbox-rebuild-vitest step", runVitestEnv, { + const runVitest = requireJobStep( + errors, jobName, - }); + steps, + "Run sandbox rebuild live test", + ); + const runVitestEnv = asRecord(runVitest?.env); + if ( + runVitestEnv.NVIDIA_INFERENCE_API_KEY !== + "${{ secrets.NVIDIA_INFERENCE_API_KEY }}" + ) { + errors.push( + "sandbox-rebuild-vitest step must receive NVIDIA_INFERENCE_API_KEY from secrets", + ); + } requireRunContains(errors, runVitest, "OPENSHELL_BIN"); - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/sandbox-rebuild.test.ts"); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/sandbox-rebuild.test.ts", + ); - const upload = requireJobStep(errors, jobName, steps, "Upload sandbox rebuild artifacts"); - requireFullShaAction(errors, upload, "sandbox-rebuild-vitest upload-artifact"); + const upload = requireJobStep( + errors, + jobName, + steps, + "Upload sandbox rebuild artifacts", + ); + requireFullShaAction( + errors, + upload, + "sandbox-rebuild-vitest upload-artifact", + ); const uploadWith = asRecord(upload?.with); if (uploadWith.name !== "e2e-vitest-scenarios-sandbox-rebuild") { errors.push("sandbox-rebuild-vitest artifact upload name must be stable"); } const uploadPath = stringValue(uploadWith.path); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/sandbox-rebuild/"); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/sandbox-rebuild/", + ); if (uploadWith["include-hidden-files"] !== false) { - errors.push("sandbox-rebuild-vitest artifact upload must set include-hidden-files: false"); + errors.push( + "sandbox-rebuild-vitest artifact upload must set include-hidden-files: false", + ); } if (uploadWith["if-no-files-found"] !== "ignore") { - errors.push("sandbox-rebuild-vitest artifact upload must ignore missing fixture artifacts"); + errors.push( + "sandbox-rebuild-vitest artifact upload must ignore missing fixture artifacts", + ); } if (uploadWith["retention-days"] !== 14) { - errors.push("sandbox-rebuild-vitest artifact upload retention-days must be 14"); + errors.push( + "sandbox-rebuild-vitest artifact upload retention-days must be 14", + ); } } -function validateStateBackupRestoreVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateStateBackupRestoreVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "state-backup-restore-vitest"; const scenarioName = "state-backup-restore"; const job = asRecord(jobs[jobName]); @@ -1684,33 +2175,48 @@ function validateStateBackupRestoreVitestJob(errors: string[], jobs: WorkflowRec } validateFreeStandingJobSelector(errors, jobs, jobName, scenarioName); if (job["timeout-minutes"] !== 60) { - errors.push("state-backup-restore-vitest job must keep the legacy 60 minute timeout"); + errors.push( + "state-backup-restore-vitest job must keep the legacy 60 minute timeout", + ); } const jobEnv = asRecord(job.env); if (jobEnv.NEMOCLAW_RUN_E2E_SCENARIOS !== "1") { - errors.push("state-backup-restore-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); + errors.push( + "state-backup-restore-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1", + ); } if ( - jobEnv.E2E_ARTIFACT_DIR !== "${{ github.workspace }}/e2e-artifacts/vitest/state-backup-restore" + jobEnv.E2E_ARTIFACT_DIR !== + "${{ github.workspace }}/e2e-artifacts/vitest/state-backup-restore" ) { errors.push( "state-backup-restore-vitest job must write artifacts under e2e-artifacts/vitest/state-backup-restore", ); } if (jobEnv.NEMOCLAW_CLI_BIN !== "${{ github.workspace }}/bin/nemoclaw.js") { - errors.push("state-backup-restore-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI"); + errors.push( + "state-backup-restore-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI", + ); } if (jobEnv.OPENSHELL_GATEWAY !== "nemoclaw") { - errors.push("state-backup-restore-vitest job must force OPENSHELL_GATEWAY=nemoclaw"); + errors.push( + "state-backup-restore-vitest job must force OPENSHELL_GATEWAY=nemoclaw", + ); } if (jobEnv.NEMOCLAW_NON_INTERACTIVE !== "1") { - errors.push("state-backup-restore-vitest job must set NEMOCLAW_NON_INTERACTIVE=1"); + errors.push( + "state-backup-restore-vitest job must set NEMOCLAW_NON_INTERACTIVE=1", + ); } if (jobEnv.NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE !== "1") { - errors.push("state-backup-restore-vitest job must set NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1"); + errors.push( + "state-backup-restore-vitest job must set NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1", + ); } if (jobEnv.NEMOCLAW_SANDBOX_NAME !== "e2e-state-backup") { - errors.push("state-backup-restore-vitest job must set NEMOCLAW_SANDBOX_NAME=e2e-state-backup"); + errors.push( + "state-backup-restore-vitest job must set NEMOCLAW_SANDBOX_NAME=e2e-state-backup", + ); } for (const secret of [ "NVIDIA_API_KEY", @@ -1718,7 +2224,12 @@ function validateStateBackupRestoreVitestJob(errors: string[], jobs: WorkflowRec "DOCKERHUB_TOKEN", "GITHUB_TOKEN", ]) { - requireEnvDoesNotExposeSecret(errors, "state-backup-restore-vitest job", jobEnv, secret); + requireEnvDoesNotExposeSecret( + errors, + "state-backup-restore-vitest job", + jobEnv, + secret, + ); } const steps = asSteps(job.steps); @@ -1727,24 +2238,53 @@ function validateStateBackupRestoreVitestJob(errors: string[], jobs: WorkflowRec const stepName = `state-backup-restore-vitest step '${step.name ?? step.uses ?? ""}'`; const stepEnv = asRecord(step.env); if (step.name !== "Run state backup restore live test") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_API_KEY"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "NVIDIA_API_KEY", + ); } if (step.name !== "Authenticate to Docker Hub") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_USERNAME"); - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_TOKEN"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_USERNAME", + ); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_TOKEN", + ); requireNoDockerHubAuthInRun(errors, stepName, stringValue(step.run)); } requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "GITHUB_TOKEN"); } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); - if (!checkout) errors.push("state-backup-restore-vitest job missing checkout step"); - requireFullShaAction(errors, checkout, "state-backup-restore-vitest checkout"); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); + if (!checkout) + errors.push("state-backup-restore-vitest job missing checkout step"); + requireFullShaAction( + errors, + checkout, + "state-backup-restore-vitest checkout", + ); if (asRecord(checkout?.with)["persist-credentials"] !== false) { - errors.push("state-backup-restore-vitest checkout step must set persist-credentials=false"); + errors.push( + "state-backup-restore-vitest checkout step must set persist-credentials=false", + ); } - const dockerHubAuth = requireJobStep(errors, jobName, steps, "Authenticate to Docker Hub"); + const dockerHubAuth = requireJobStep( + errors, + jobName, + steps, + "Authenticate to Docker Hub", + ); const dockerHubEnv = asRecord(dockerHubAuth?.env); if (dockerHubEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}") { errors.push( @@ -1760,8 +2300,13 @@ function validateStateBackupRestoreVitestJob(errors: string[], jobs: WorkflowRec requireRunContains(errors, dockerHubAuth, "continuing with anonymous pulls"); const setupNode = namedStep(steps, "Set up Node"); - if (!setupNode) errors.push("state-backup-restore-vitest job missing step: Set up Node"); - requireFullShaAction(errors, setupNode, "state-backup-restore-vitest setup-node"); + if (!setupNode) + errors.push("state-backup-restore-vitest job missing step: Set up Node"); + requireFullShaAction( + errors, + setupNode, + "state-backup-restore-vitest setup-node", + ); const installRootDependencies = requireJobStep( errors, @@ -1769,38 +2314,83 @@ function validateStateBackupRestoreVitestJob(errors: string[], jobs: WorkflowRec steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); const buildCli = requireJobStep(errors, jobName, steps, "Build CLI"); requireRunContains(errors, buildCli, "npm run build:cli"); - const installOpenShell = requireJobStep(errors, jobName, steps, "Install OpenShell"); - requireRunContains(errors, installOpenShell, "bash scripts/install-openshell.sh"); - requireRunContains(errors, installOpenShell, "env -u DOCKER_CONFIG"); - requireRunContains(errors, installOpenShell, "-u DOCKERHUB_USERNAME"); - requireRunContains(errors, installOpenShell, "-u DOCKERHUB_TOKEN"); + const installOpenShell = requireJobStep( + errors, + jobName, + steps, + "Install OpenShell", + ); + requireRunContains( + errors, + installOpenShell, + "bash scripts/install-openshell.sh", + ); + requireRunContains(errors, installOpenShell, "env -u DOCKER_CONFIG"); + requireRunContains(errors, installOpenShell, "-u DOCKERHUB_USERNAME"); + requireRunContains(errors, installOpenShell, "-u DOCKERHUB_TOKEN"); requireRunContains(errors, installOpenShell, "-u NVIDIA_API_KEY"); requireRunContains(errors, installOpenShell, "-u GITHUB_TOKEN"); - const runVitest = requireJobStep(errors, jobName, steps, "Run state backup restore live test"); - const runVitestEnv = asRecord(runVitest?.env); - requireHostedInferenceCredentialEnv(errors, "state-backup-restore-vitest step", runVitestEnv, { + const runVitest = requireJobStep( + errors, jobName, - }); + steps, + "Run state backup restore live test", + ); + const runVitestEnv = asRecord(runVitest?.env); + if (runVitestEnv.NVIDIA_API_KEY !== "${{ secrets.NVIDIA_API_KEY }}") { + errors.push( + "state-backup-restore-vitest step must receive NVIDIA_API_KEY from secrets", + ); + } requireRunContains(errors, runVitest, "OPENSHELL_BIN"); - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/state-backup-restore.test.ts"); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/state-backup-restore.test.ts", + ); - const upload = requireJobStep(errors, jobName, steps, "Upload state backup restore artifacts"); - requireFullShaAction(errors, upload, "state-backup-restore-vitest upload-artifact"); + const upload = requireJobStep( + errors, + jobName, + steps, + "Upload state backup restore artifacts", + ); + requireFullShaAction( + errors, + upload, + "state-backup-restore-vitest upload-artifact", + ); const uploadWith = asRecord(upload?.with); if (uploadWith.name !== "e2e-vitest-scenarios-state-backup-restore") { - errors.push("state-backup-restore-vitest artifact upload name must be stable"); + errors.push( + "state-backup-restore-vitest artifact upload name must be stable", + ); } const uploadPath = stringValue(uploadWith.path); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/state-backup-restore/"); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/state-backup-restore/", + ); if (uploadWith["include-hidden-files"] !== false) { - errors.push("state-backup-restore-vitest artifact upload must set include-hidden-files: false"); + errors.push( + "state-backup-restore-vitest artifact upload must set include-hidden-files: false", + ); } if (uploadWith["if-no-files-found"] !== "ignore") { errors.push( @@ -1808,11 +2398,16 @@ function validateStateBackupRestoreVitestJob(errors: string[], jobs: WorkflowRec ); } if (uploadWith["retention-days"] !== 14) { - errors.push("state-backup-restore-vitest artifact upload retention-days must be 14"); + errors.push( + "state-backup-restore-vitest artifact upload retention-days must be 14", + ); } } -function validateUpgradeStaleSandboxVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateUpgradeStaleSandboxVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "upgrade-stale-sandbox-vitest"; const scenarioName = "upgrade-stale-sandbox"; const job = asRecord(jobs[jobName]); @@ -1826,25 +2421,34 @@ function validateUpgradeStaleSandboxVitestJob(errors: string[], jobs: WorkflowRe } validateFreeStandingJobSelector(errors, jobs, jobName, scenarioName); if (job["timeout-minutes"] !== 55) { - errors.push("upgrade-stale-sandbox-vitest job must keep the legacy 55 minute timeout"); + errors.push( + "upgrade-stale-sandbox-vitest job must keep the legacy 55 minute timeout", + ); } const jobEnv = asRecord(job.env); if (jobEnv.NEMOCLAW_RUN_E2E_SCENARIOS !== "1") { - errors.push("upgrade-stale-sandbox-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); + errors.push( + "upgrade-stale-sandbox-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1", + ); } if ( - jobEnv.E2E_ARTIFACT_DIR !== "${{ github.workspace }}/e2e-artifacts/vitest/upgrade-stale-sandbox" + jobEnv.E2E_ARTIFACT_DIR !== + "${{ github.workspace }}/e2e-artifacts/vitest/upgrade-stale-sandbox" ) { errors.push( "upgrade-stale-sandbox-vitest job must write artifacts under e2e-artifacts/vitest/upgrade-stale-sandbox", ); } if (jobEnv.NEMOCLAW_CLI_BIN !== "${{ github.workspace }}/bin/nemoclaw.js") { - errors.push("upgrade-stale-sandbox-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI"); + errors.push( + "upgrade-stale-sandbox-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI", + ); } if (jobEnv.OPENSHELL_GATEWAY !== "nemoclaw") { - errors.push("upgrade-stale-sandbox-vitest job must force OPENSHELL_GATEWAY=nemoclaw"); + errors.push( + "upgrade-stale-sandbox-vitest job must force OPENSHELL_GATEWAY=nemoclaw", + ); } if (jobEnv.NEMOCLAW_SANDBOX_NAME !== "e2e-upgrade-stale") { errors.push( @@ -1852,10 +2456,20 @@ function validateUpgradeStaleSandboxVitestJob(errors: string[], jobs: WorkflowRe ); } if ("DOCKER_CONFIG" in jobEnv) { - errors.push("upgrade-stale-sandbox-vitest job must not set DOCKER_CONFIG at job level"); + errors.push( + "upgrade-stale-sandbox-vitest job must not set DOCKER_CONFIG at job level", + ); } - for (const secret of ["NVIDIA_INFERENCE_API_KEY", ...COMMON_SECRET_ENV_NAMES]) { - requireEnvDoesNotExposeSecret(errors, "upgrade-stale-sandbox-vitest job", jobEnv, secret); + for (const secret of [ + "NVIDIA_INFERENCE_API_KEY", + ...COMMON_SECRET_ENV_NAMES, + ]) { + requireEnvDoesNotExposeSecret( + errors, + "upgrade-stale-sandbox-vitest job", + jobEnv, + secret, + ); } const steps = asSteps(job.steps); @@ -1864,22 +2478,51 @@ function validateUpgradeStaleSandboxVitestJob(errors: string[], jobs: WorkflowRe const stepName = `upgrade-stale-sandbox-vitest step '${step.name ?? step.uses ?? ""}'`; const stepEnv = asRecord(step.env); if (step.name !== "Run upgrade stale sandbox live Vitest test") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_INFERENCE_API_KEY"); - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_API_KEY"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "NVIDIA_INFERENCE_API_KEY", + ); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "NVIDIA_API_KEY", + ); } if (step.name !== "Authenticate to Docker Hub") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_USERNAME"); - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_TOKEN"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_USERNAME", + ); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_TOKEN", + ); requireNoDockerHubAuthInRun(errors, stepName, stringValue(step.run)); } requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "GITHUB_TOKEN"); } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); - if (!checkout) errors.push("upgrade-stale-sandbox-vitest job missing checkout step"); - requireFullShaAction(errors, checkout, "upgrade-stale-sandbox-vitest checkout"); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); + if (!checkout) + errors.push("upgrade-stale-sandbox-vitest job missing checkout step"); + requireFullShaAction( + errors, + checkout, + "upgrade-stale-sandbox-vitest checkout", + ); if (asRecord(checkout?.with)["persist-credentials"] !== false) { - errors.push("upgrade-stale-sandbox-vitest checkout step must set persist-credentials=false"); + errors.push( + "upgrade-stale-sandbox-vitest checkout step must set persist-credentials=false", + ); } const configureDockerAuth = requireJobStep( @@ -1894,7 +2537,12 @@ function validateUpgradeStaleSandboxVitestJob(errors: string[], jobs: WorkflowRe 'echo "DOCKER_CONFIG=${RUNNER_TEMP}/docker-config-upgrade-stale-sandbox" >> "$GITHUB_ENV"', ); - const dockerHubAuth = requireJobStep(errors, jobName, steps, "Authenticate to Docker Hub"); + const dockerHubAuth = requireJobStep( + errors, + jobName, + steps, + "Authenticate to Docker Hub", + ); const dockerHubEnv = asRecord(dockerHubAuth?.env); if (dockerHubEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}") { errors.push( @@ -1912,8 +2560,13 @@ function validateUpgradeStaleSandboxVitestJob(errors: string[], jobs: WorkflowRe requireRunContains(errors, dockerHubAuth, "continuing with anonymous pulls"); const setupNode = namedStep(steps, "Set up Node"); - if (!setupNode) errors.push("upgrade-stale-sandbox-vitest job missing step: Set up Node"); - requireFullShaAction(errors, setupNode, "upgrade-stale-sandbox-vitest setup-node"); + if (!setupNode) + errors.push("upgrade-stale-sandbox-vitest job missing step: Set up Node"); + requireFullShaAction( + errors, + setupNode, + "upgrade-stale-sandbox-vitest setup-node", + ); const installRootDependencies = requireJobStep( errors, @@ -1921,13 +2574,26 @@ function validateUpgradeStaleSandboxVitestJob(errors: string[], jobs: WorkflowRe steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); const buildCli = requireJobStep(errors, jobName, steps, "Build CLI"); requireRunContains(errors, buildCli, "npm run build:cli"); - const installOpenShell = requireJobStep(errors, jobName, steps, "Install OpenShell CLI"); - requireRunContains(errors, installOpenShell, "bash scripts/install-openshell.sh"); + const installOpenShell = requireJobStep( + errors, + jobName, + steps, + "Install OpenShell CLI", + ); + requireRunContains( + errors, + installOpenShell, + "bash scripts/install-openshell.sh", + ); requireRunContains(errors, installOpenShell, "env -u DOCKER_CONFIG"); requireRunContains(errors, installOpenShell, "-u DOCKERHUB_USERNAME"); requireRunContains(errors, installOpenShell, "-u DOCKERHUB_TOKEN"); @@ -1942,21 +2608,49 @@ function validateUpgradeStaleSandboxVitestJob(errors: string[], jobs: WorkflowRe "Run upgrade stale sandbox live Vitest test", ); const runVitestEnv = asRecord(runVitest?.env); - requireHostedInferenceCredentialEnv(errors, "upgrade-stale-sandbox-vitest step", runVitestEnv, { - jobName, - }); + if ( + runVitestEnv.NVIDIA_INFERENCE_API_KEY !== + "${{ secrets.NVIDIA_INFERENCE_API_KEY }}" + ) { + errors.push( + "upgrade-stale-sandbox-vitest step must receive NVIDIA_INFERENCE_API_KEY from secrets", + ); + } requireRunContains(errors, runVitest, "OPENSHELL_BIN"); - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/upgrade-stale-sandbox.test.ts"); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/upgrade-stale-sandbox.test.ts", + ); - const upload = requireJobStep(errors, jobName, steps, "Upload upgrade stale sandbox artifacts"); - requireFullShaAction(errors, upload, "upgrade-stale-sandbox-vitest upload-artifact"); + const upload = requireJobStep( + errors, + jobName, + steps, + "Upload upgrade stale sandbox artifacts", + ); + requireFullShaAction( + errors, + upload, + "upgrade-stale-sandbox-vitest upload-artifact", + ); const uploadWith = asRecord(upload?.with); if (uploadWith.name !== "e2e-vitest-scenarios-upgrade-stale-sandbox") { - errors.push("upgrade-stale-sandbox-vitest artifact upload name must be stable"); + errors.push( + "upgrade-stale-sandbox-vitest artifact upload name must be stable", + ); } const uploadPath = stringValue(uploadWith.path); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/upgrade-stale-sandbox/"); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/upgrade-stale-sandbox/", + ); if (uploadWith["include-hidden-files"] !== false) { errors.push( "upgrade-stale-sandbox-vitest artifact upload must set include-hidden-files: false", @@ -1968,15 +2662,25 @@ function validateUpgradeStaleSandboxVitestJob(errors: string[], jobs: WorkflowRe ); } if (uploadWith["retention-days"] !== 14) { - errors.push("upgrade-stale-sandbox-vitest artifact upload retention-days must be 14"); + errors.push( + "upgrade-stale-sandbox-vitest artifact upload retention-days must be 14", + ); } - const cleanup = requireJobStep(errors, jobName, steps, "Clean up Docker auth"); + const cleanup = requireJobStep( + errors, + jobName, + steps, + "Clean up Docker auth", + ); requireRunContains(errors, cleanup, "docker logout docker.io"); requireRunContains(errors, cleanup, 'rm -rf "${DOCKER_CONFIG}"'); } -function validateTokenRotationVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateTokenRotationVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "token-rotation-vitest"; const job = asRecord(jobs[jobName]); if (Object.keys(job).length === 0) { @@ -1989,19 +2693,28 @@ function validateTokenRotationVitestJob(errors: string[], jobs: WorkflowRecord): } validateFreeStandingJobSelector(errors, jobs, jobName, "token-rotation"); if (job["timeout-minutes"] !== 45) { - errors.push("token-rotation-vitest job must keep the legacy 45 minute timeout"); + errors.push( + "token-rotation-vitest job must keep the legacy 45 minute timeout", + ); } const jobEnv = asRecord(job.env); if (jobEnv.NEMOCLAW_RUN_E2E_SCENARIOS !== "1") { - errors.push("token-rotation-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); + errors.push( + "token-rotation-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1", + ); } - if (jobEnv.E2E_ARTIFACT_DIR !== "${{ github.workspace }}/e2e-artifacts/vitest/token-rotation") { + if ( + jobEnv.E2E_ARTIFACT_DIR !== + "${{ github.workspace }}/e2e-artifacts/vitest/token-rotation" + ) { errors.push( "token-rotation-vitest job must write artifacts under e2e-artifacts/vitest/token-rotation", ); } if (!stringValue(jobEnv.NEMOCLAW_CLI_BIN).includes("bin/nemoclaw.js")) { - errors.push("token-rotation-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI"); + errors.push( + "token-rotation-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI", + ); } requireEnvDoesNotExposeSecret( errors, @@ -2023,14 +2736,23 @@ function validateTokenRotationVitestJob(errors: string[], jobs: WorkflowRecord): } } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); if (!checkout) errors.push("token-rotation-vitest job missing checkout step"); requireFullShaAction(errors, checkout, "token-rotation-vitest checkout"); if (asRecord(checkout?.with)["persist-credentials"] !== false) { - errors.push("token-rotation-vitest checkout step must set persist-credentials=false"); + errors.push( + "token-rotation-vitest checkout step must set persist-credentials=false", + ); } - const dockerHubAuth = requireJobStep(errors, jobName, steps, "Authenticate to Docker Hub"); + const dockerHubAuth = requireJobStep( + errors, + jobName, + steps, + "Authenticate to Docker Hub", + ); const dockerHubEnv = asRecord(dockerHubAuth?.env); if (dockerHubEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}") { errors.push( @@ -2038,12 +2760,15 @@ function validateTokenRotationVitestJob(errors: string[], jobs: WorkflowRecord): ); } if (dockerHubEnv.DOCKERHUB_TOKEN !== "${{ secrets.DOCKERHUB_TOKEN }}") { - errors.push("token-rotation-vitest Docker Hub auth must receive DOCKERHUB_TOKEN from secrets"); + errors.push( + "token-rotation-vitest Docker Hub auth must receive DOCKERHUB_TOKEN from secrets", + ); } requireRunContains(errors, dockerHubAuth, "docker login docker.io"); const setupNode = namedStep(steps, "Set up Node"); - if (!setupNode) errors.push("token-rotation-vitest job missing step: Set up Node"); + if (!setupNode) + errors.push("token-rotation-vitest job missing step: Set up Node"); requireFullShaAction(errors, setupNode, "token-rotation-vitest setup-node"); const installRootDependencies = requireJobStep( @@ -2052,12 +2777,21 @@ function validateTokenRotationVitestJob(errors: string[], jobs: WorkflowRecord): steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); const buildCli = requireJobStep(errors, jobName, steps, "Build CLI"); requireRunContains(errors, buildCli, "npm run build:cli"); - const runVitest = requireJobStep(errors, jobName, steps, "Run token rotation live test"); + const runVitest = requireJobStep( + errors, + jobName, + steps, + "Run token rotation live test", + ); const runVitestEnv = asRecord(runVitest?.env); requireEnvDoesNotExposeSecret( errors, @@ -2066,7 +2800,9 @@ function validateTokenRotationVitestJob(errors: string[], jobs: WorkflowRecord): "NVIDIA_INFERENCE_API_KEY", ); if (runVitestEnv.GITHUB_TOKEN !== "${{ github.token }}") { - errors.push("token-rotation-vitest step must receive GITHUB_TOKEN from github.token"); + errors.push( + "token-rotation-vitest step must receive GITHUB_TOKEN from github.token", + ); } for (const tokenName of [ "TELEGRAM_BOT_TOKEN_A", @@ -2087,25 +2823,48 @@ function validateTokenRotationVitestJob(errors: string[], jobs: WorkflowRecord): errors.push(`token-rotation-vitest step must set ${tokenName}`); } } - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/token-rotation.test.ts"); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/token-rotation.test.ts", + ); - const upload = requireJobStep(errors, jobName, steps, "Upload token rotation artifacts"); + const upload = requireJobStep( + errors, + jobName, + steps, + "Upload token rotation artifacts", + ); requireFullShaAction(errors, upload, "token-rotation-vitest upload-artifact"); const uploadWith = asRecord(upload?.with); if (uploadWith.name !== "e2e-vitest-scenarios-token-rotation") { errors.push("token-rotation-vitest artifact upload name must be stable"); } const uploadPath = stringValue(uploadWith.path); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/token-rotation/"); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/token-rotation/", + ); if (uploadWith["include-hidden-files"] !== false) { - errors.push("token-rotation-vitest artifact upload must set include-hidden-files: false"); + errors.push( + "token-rotation-vitest artifact upload must set include-hidden-files: false", + ); } if (uploadWith["if-no-files-found"] !== "ignore") { - errors.push("token-rotation-vitest artifact upload must ignore missing fixture artifacts"); + errors.push( + "token-rotation-vitest artifact upload must ignore missing fixture artifacts", + ); } if (uploadWith["retention-days"] !== 14) { - errors.push("token-rotation-vitest artifact upload retention-days must be 14"); + errors.push( + "token-rotation-vitest artifact upload retention-days must be 14", + ); } } @@ -2121,11 +2880,20 @@ function validateMessagingCompatibleEndpointVitestJob( } if (job["runs-on"] !== "ubuntu-latest") { - errors.push("messaging-compatible-endpoint-vitest job must run on ubuntu-latest"); + errors.push( + "messaging-compatible-endpoint-vitest job must run on ubuntu-latest", + ); } - validateFreeStandingJobSelector(errors, jobs, jobName, "messaging-compatible-endpoint"); + validateFreeStandingJobSelector( + errors, + jobs, + jobName, + "messaging-compatible-endpoint", + ); if (job["timeout-minutes"] !== 45) { - errors.push("messaging-compatible-endpoint-vitest job must keep the legacy 45 minute timeout"); + errors.push( + "messaging-compatible-endpoint-vitest job must keep the legacy 45 minute timeout", + ); } const jobEnv = asRecord(job.env); @@ -2143,13 +2911,19 @@ function validateMessagingCompatibleEndpointVitestJob( ); } if (jobEnv.NEMOCLAW_RUN_E2E_SCENARIOS !== "1") { - errors.push("messaging-compatible-endpoint-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); + errors.push( + "messaging-compatible-endpoint-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1", + ); } if (jobEnv.NEMOCLAW_SANDBOX_NAME !== "e2e-msg-compat") { - errors.push("messaging-compatible-endpoint-vitest job must pin the legacy sandbox name"); + errors.push( + "messaging-compatible-endpoint-vitest job must pin the legacy sandbox name", + ); } if (jobEnv.OPENSHELL_GATEWAY !== "nemoclaw") { - errors.push("messaging-compatible-endpoint-vitest job must force OPENSHELL_GATEWAY=nemoclaw"); + errors.push( + "messaging-compatible-endpoint-vitest job must force OPENSHELL_GATEWAY=nemoclaw", + ); } requireEnvDoesNotExposeSecret( errors, @@ -2206,9 +2980,18 @@ function validateMessagingCompatibleEndpointVitestJob( ); } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); - if (!checkout) errors.push("messaging-compatible-endpoint-vitest job missing checkout step"); - requireFullShaAction(errors, checkout, "messaging-compatible-endpoint-vitest checkout"); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); + if (!checkout) + errors.push( + "messaging-compatible-endpoint-vitest job missing checkout step", + ); + requireFullShaAction( + errors, + checkout, + "messaging-compatible-endpoint-vitest checkout", + ); if (asRecord(checkout?.with)["persist-credentials"] !== false) { errors.push( "messaging-compatible-endpoint-vitest checkout step must set persist-credentials=false", @@ -2216,8 +2999,15 @@ function validateMessagingCompatibleEndpointVitestJob( } const setupNode = namedStep(steps, "Set up Node"); - if (!setupNode) errors.push("messaging-compatible-endpoint-vitest job missing step: Set up Node"); - requireFullShaAction(errors, setupNode, "messaging-compatible-endpoint-vitest setup-node"); + if (!setupNode) + errors.push( + "messaging-compatible-endpoint-vitest job missing step: Set up Node", + ); + requireFullShaAction( + errors, + setupNode, + "messaging-compatible-endpoint-vitest setup-node", + ); const installRootDependencies = requireJobStep( errors, @@ -2225,7 +3015,11 @@ function validateMessagingCompatibleEndpointVitestJob( steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); const buildCli = requireJobStep(errors, jobName, steps, "Build CLI"); requireRunContains(errors, buildCli, "npm run build:cli"); @@ -2249,28 +3043,44 @@ function validateMessagingCompatibleEndpointVitestJob( ); } if (runVitestEnv.TELEGRAM_BOT_TOKEN !== "test-fake-telegram-token-e2e") { - errors.push("messaging-compatible-endpoint-vitest step must set a fake Telegram token"); + errors.push( + "messaging-compatible-endpoint-vitest step must set a fake Telegram token", + ); } if (runVitestEnv.TELEGRAM_ALLOWED_IDS !== "123456789") { - errors.push("messaging-compatible-endpoint-vitest step must set fake Telegram allowed ids"); + errors.push( + "messaging-compatible-endpoint-vitest step must set fake Telegram allowed ids", + ); } - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); requireRunContains( errors, runVitest, - "test/e2e-scenario/live/messaging-compatible-endpoint.test.ts", + "npx vitest run --project e2e-scenarios-live", ); - + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/messaging-compatible-endpoint.test.ts", + ); + const upload = requireJobStep( errors, jobName, steps, "Upload messaging compatible endpoint artifacts", ); - requireFullShaAction(errors, upload, "messaging-compatible-endpoint-vitest upload-artifact"); + requireFullShaAction( + errors, + upload, + "messaging-compatible-endpoint-vitest upload-artifact", + ); const uploadWith = asRecord(upload?.with); - if (uploadWith.name !== "e2e-vitest-scenarios-messaging-compatible-endpoint") { - errors.push("messaging-compatible-endpoint-vitest artifact upload name must be stable"); + if ( + uploadWith.name !== "e2e-vitest-scenarios-messaging-compatible-endpoint" + ) { + errors.push( + "messaging-compatible-endpoint-vitest artifact upload name must be stable", + ); } const uploadPath = stringValue(uploadWith.path); requireUploadPathContains( @@ -2289,11 +3099,16 @@ function validateMessagingCompatibleEndpointVitestJob( ); } if (uploadWith["retention-days"] !== 14) { - errors.push("messaging-compatible-endpoint-vitest artifact upload retention-days must be 14"); + errors.push( + "messaging-compatible-endpoint-vitest artifact upload retention-days must be 14", + ); } } -function validateOnboardNegativePathsVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateOnboardNegativePathsVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "onboard-negative-paths-vitest"; const job = asRecord(jobs[jobName]); if (Object.keys(job).length === 0) { @@ -2304,11 +3119,18 @@ function validateOnboardNegativePathsVitestJob(errors: string[], jobs: WorkflowR if (job["runs-on"] !== "ubuntu-latest") { errors.push("onboard-negative-paths-vitest job must run on ubuntu-latest"); } - validateFreeStandingJobSelector(errors, jobs, jobName, "onboard-negative-paths"); + validateFreeStandingJobSelector( + errors, + jobs, + jobName, + "onboard-negative-paths", + ); const jobEnv = asRecord(job.env); if (jobEnv.NEMOCLAW_RUN_E2E_SCENARIOS !== "1") { - errors.push("onboard-negative-paths-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); + errors.push( + "onboard-negative-paths-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1", + ); } if ( jobEnv.E2E_ARTIFACT_DIR !== @@ -2336,16 +3158,30 @@ function validateOnboardNegativePathsVitestJob(errors: string[], jobs: WorkflowR ); } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); - if (!checkout) errors.push("onboard-negative-paths-vitest job missing checkout step"); - requireFullShaAction(errors, checkout, "onboard-negative-paths-vitest checkout"); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); + if (!checkout) + errors.push("onboard-negative-paths-vitest job missing checkout step"); + requireFullShaAction( + errors, + checkout, + "onboard-negative-paths-vitest checkout", + ); if (asRecord(checkout?.with)["persist-credentials"] !== false) { - errors.push("onboard-negative-paths-vitest checkout step must set persist-credentials=false"); + errors.push( + "onboard-negative-paths-vitest checkout step must set persist-credentials=false", + ); } const setupNode = namedStep(steps, "Set up Node"); - if (!setupNode) errors.push("onboard-negative-paths-vitest job missing step: Set up Node"); - requireFullShaAction(errors, setupNode, "onboard-negative-paths-vitest setup-node"); + if (!setupNode) + errors.push("onboard-negative-paths-vitest job missing step: Set up Node"); + requireFullShaAction( + errors, + setupNode, + "onboard-negative-paths-vitest setup-node", + ); const installRootDependencies = requireJobStep( errors, @@ -2353,23 +3189,55 @@ function validateOnboardNegativePathsVitestJob(errors: string[], jobs: WorkflowR steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); const buildCli = requireJobStep(errors, jobName, steps, "Build CLI"); requireRunContains(errors, buildCli, "npm run build:cli"); - const runVitest = requireJobStep(errors, jobName, steps, "Run onboard negative-paths live test"); - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/onboard-negative-paths.test.ts"); + const runVitest = requireJobStep( + errors, + jobName, + steps, + "Run onboard negative-paths live test", + ); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/onboard-negative-paths.test.ts", + ); - const upload = requireJobStep(errors, jobName, steps, "Upload onboard negative-paths artifacts"); - requireFullShaAction(errors, upload, "onboard-negative-paths-vitest upload-artifact"); + const upload = requireJobStep( + errors, + jobName, + steps, + "Upload onboard negative-paths artifacts", + ); + requireFullShaAction( + errors, + upload, + "onboard-negative-paths-vitest upload-artifact", + ); const uploadWith = asRecord(upload?.with); if (uploadWith.name !== "e2e-vitest-scenarios-onboard-negative-paths") { - errors.push("onboard-negative-paths-vitest artifact upload name must be stable"); + errors.push( + "onboard-negative-paths-vitest artifact upload name must be stable", + ); } const uploadPath = stringValue(uploadWith.path); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/onboard-negative-paths/"); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/onboard-negative-paths/", + ); if (uploadWith["include-hidden-files"] !== false) { errors.push( "onboard-negative-paths-vitest artifact upload must set include-hidden-files: false", @@ -2381,11 +3249,16 @@ function validateOnboardNegativePathsVitestJob(errors: string[], jobs: WorkflowR ); } if (uploadWith["retention-days"] !== 14) { - errors.push("onboard-negative-paths-vitest artifact upload retention-days must be 14"); + errors.push( + "onboard-negative-paths-vitest artifact upload retention-days must be 14", + ); } } -function validateCloudInferenceVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateCloudInferenceVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "cloud-inference-vitest"; const job = asRecord(jobs[jobName]); if (Object.keys(job).length === 0) { @@ -2402,24 +3275,40 @@ function validateCloudInferenceVitestJob(errors: string[], jobs: WorkflowRecord) } const jobEnv = asRecord(job.env); - if (jobEnv.E2E_ARTIFACT_DIR !== "${{ github.workspace }}/e2e-artifacts/vitest/cloud-inference") { + if ( + jobEnv.E2E_ARTIFACT_DIR !== + "${{ github.workspace }}/e2e-artifacts/vitest/cloud-inference" + ) { errors.push( "cloud-inference-vitest job must write artifacts under e2e-artifacts/vitest/cloud-inference", ); } if (jobEnv.NEMOCLAW_CLI_BIN !== "${{ github.workspace }}/bin/nemoclaw.js") { - errors.push("cloud-inference-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI"); + errors.push( + "cloud-inference-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI", + ); } if (jobEnv.NEMOCLAW_RUN_E2E_SCENARIOS !== "1") { - errors.push("cloud-inference-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); + errors.push( + "cloud-inference-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1", + ); } if (jobEnv.NEMOCLAW_SANDBOX_NAME !== "e2e-cloud-inference") { - errors.push("cloud-inference-vitest job must set NEMOCLAW_SANDBOX_NAME=e2e-cloud-inference"); + errors.push( + "cloud-inference-vitest job must set NEMOCLAW_SANDBOX_NAME=e2e-cloud-inference", + ); } if (jobEnv.OPENSHELL_GATEWAY !== "nemoclaw") { - errors.push("cloud-inference-vitest job must force OPENSHELL_GATEWAY=nemoclaw"); + errors.push( + "cloud-inference-vitest job must force OPENSHELL_GATEWAY=nemoclaw", + ); } - requireEnvDoesNotExposeSecret(errors, "cloud-inference-vitest job", jobEnv, "NVIDIA_API_KEY"); + requireEnvDoesNotExposeSecret( + errors, + "cloud-inference-vitest job", + jobEnv, + "NVIDIA_API_KEY", + ); const steps = asSteps(job.steps); requireNoDispatchInputInterpolation(errors, steps); @@ -2434,15 +3323,21 @@ function validateCloudInferenceVitestJob(errors: string[], jobs: WorkflowRecord) } } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); - if (!checkout) errors.push("cloud-inference-vitest job missing checkout step"); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); + if (!checkout) + errors.push("cloud-inference-vitest job missing checkout step"); requireFullShaAction(errors, checkout, "cloud-inference-vitest checkout"); if (asRecord(checkout?.with)["persist-credentials"] !== false) { - errors.push("cloud-inference-vitest checkout step must set persist-credentials=false"); + errors.push( + "cloud-inference-vitest checkout step must set persist-credentials=false", + ); } const setupNode = namedStep(steps, "Set up Node"); - if (!setupNode) errors.push("cloud-inference-vitest job missing step: Set up Node"); + if (!setupNode) + errors.push("cloud-inference-vitest job missing step: Set up Node"); requireFullShaAction(errors, setupNode, "cloud-inference-vitest setup-node"); const installRootDependencies = requireJobStep( @@ -2451,48 +3346,96 @@ function validateCloudInferenceVitestJob(errors: string[], jobs: WorkflowRecord) steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); const buildCli = requireJobStep(errors, jobName, steps, "Build CLI"); requireRunContains(errors, buildCli, "npm run build:cli"); - const runVitest = requireJobStep(errors, jobName, steps, "Run cloud inference live test"); - const runVitestEnv = asRecord(runVitest?.env); - requireHostedInferenceCredentialEnv(errors, "cloud-inference-vitest run step", runVitestEnv, { + const runVitest = requireJobStep( + errors, jobName, - }); - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/cloud-inference.test.ts"); + steps, + "Run cloud inference live test", + ); + const runVitestEnv = asRecord(runVitest?.env); + if (runVitestEnv.NVIDIA_API_KEY !== "${{ secrets.NVIDIA_API_KEY }}") { + errors.push( + "cloud-inference-vitest run step must receive NVIDIA_API_KEY from secrets", + ); + } + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/cloud-inference.test.ts", + ); - const upload = requireJobStep(errors, jobName, steps, "Upload cloud inference artifacts"); - requireFullShaAction(errors, upload, "cloud-inference-vitest upload-artifact"); + const upload = requireJobStep( + errors, + jobName, + steps, + "Upload cloud inference artifacts", + ); + requireFullShaAction( + errors, + upload, + "cloud-inference-vitest upload-artifact", + ); const uploadWith = asRecord(upload?.with); if (uploadWith.name !== "e2e-vitest-scenarios-cloud-inference") { errors.push("cloud-inference-vitest artifact upload name must be stable"); } const uploadPath = stringValue(uploadWith.path); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/cloud-inference/"); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/cloud-inference/", + ); if (uploadWith["include-hidden-files"] !== false) { - errors.push("cloud-inference-vitest artifact upload must set include-hidden-files: false"); + errors.push( + "cloud-inference-vitest artifact upload must set include-hidden-files: false", + ); } if (uploadWith["if-no-files-found"] !== "ignore") { - errors.push("cloud-inference-vitest artifact upload must ignore missing fixture artifacts"); + errors.push( + "cloud-inference-vitest artifact upload must ignore missing fixture artifacts", + ); } if (uploadWith["retention-days"] !== 14) { - errors.push("cloud-inference-vitest artifact upload retention-days must be 14"); + errors.push( + "cloud-inference-vitest artifact upload retention-days must be 14", + ); } } -function requireNoDockerHubAuthInRun(errors: string[], owner: string, runScript: string): void { +function requireNoDockerHubAuthInRun( + errors: string[], + owner: string, + runScript: string, +): void { if (!runScript) return; const usesDockerLogin = /\bdocker\s+login\b/i.test(runScript); - const referencesSecret = /\bsecrets\.[A-Za-z0-9_]+\b|\$\{\{\s*secrets\.[^}]+\}\}/.test(runScript); + const referencesSecret = + /\bsecrets\.[A-Za-z0-9_]+\b|\$\{\{\s*secrets\.[^}]+\}\}/.test(runScript); if (usesDockerLogin || referencesSecret) { - errors.push(`${owner} run script must not use docker login or inline secret interpolation`); + errors.push( + `${owner} run script must not use docker login or inline secret interpolation`, + ); } } -function validateDoubleOnboardVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateDoubleOnboardVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "double-onboard-vitest"; const job = asRecord(jobs[jobName]); if (Object.keys(job).length === 0) { @@ -2507,12 +3450,19 @@ function validateDoubleOnboardVitestJob(errors: string[], jobs: WorkflowRecord): const jobEnv = asRecord(job.env); if (jobEnv.NEMOCLAW_RUN_E2E_SCENARIOS !== "1") { - errors.push("double-onboard-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); + errors.push( + "double-onboard-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1", + ); } if (jobEnv.NEMOCLAW_CLI_BIN !== "${{ github.workspace }}/bin/nemoclaw.js") { - errors.push("double-onboard-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI"); + errors.push( + "double-onboard-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI", + ); } - if (jobEnv.E2E_ARTIFACT_DIR !== "${{ github.workspace }}/e2e-artifacts/vitest/double-onboard") { + if ( + jobEnv.E2E_ARTIFACT_DIR !== + "${{ github.workspace }}/e2e-artifacts/vitest/double-onboard" + ) { errors.push( "double-onboard-vitest job must write artifacts under e2e-artifacts/vitest/double-onboard", ); @@ -2523,7 +3473,12 @@ function validateDoubleOnboardVitestJob(errors: string[], jobs: WorkflowRecord): jobEnv, "NVIDIA_INFERENCE_API_KEY", ); - requireEnvDoesNotExposeSecret(errors, "double-onboard-vitest job", jobEnv, "DOCKERHUB_TOKEN"); + requireEnvDoesNotExposeSecret( + errors, + "double-onboard-vitest job", + jobEnv, + "DOCKERHUB_TOKEN", + ); const steps = asSteps(job.steps); requireNoDispatchInputInterpolation(errors, steps); @@ -2544,28 +3499,42 @@ function validateDoubleOnboardVitestJob(errors: string[], jobs: WorkflowRecord): ); } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); if (!checkout) errors.push("double-onboard-vitest job missing checkout step"); requireFullShaAction(errors, checkout, "double-onboard-vitest checkout"); if (asRecord(checkout?.with)["persist-credentials"] !== false) { - errors.push("double-onboard-vitest checkout step must set persist-credentials=false"); + errors.push( + "double-onboard-vitest checkout step must set persist-credentials=false", + ); } - const dockerLogin = requireJobStep(errors, jobName, steps, "Authenticate to Docker Hub"); + const dockerLogin = requireJobStep( + errors, + jobName, + steps, + "Authenticate to Docker Hub", + ); const dockerLoginEnv = asRecord(dockerLogin?.env); - if (dockerLoginEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}") { + if ( + dockerLoginEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}" + ) { errors.push( "double-onboard-vitest Docker login step must read DOCKERHUB_USERNAME from secrets", ); } if (dockerLoginEnv.DOCKERHUB_TOKEN !== "${{ secrets.DOCKERHUB_TOKEN }}") { - errors.push("double-onboard-vitest Docker login step must read DOCKERHUB_TOKEN from secrets"); + errors.push( + "double-onboard-vitest Docker login step must read DOCKERHUB_TOKEN from secrets", + ); } requireRunContains(errors, dockerLogin, "docker login docker.io"); requireRunContains(errors, dockerLogin, "continuing with anonymous pulls"); const setupNode = namedStep(steps, "Set up Node"); - if (!setupNode) errors.push("double-onboard-vitest job missing step: Set up Node"); + if (!setupNode) + errors.push("double-onboard-vitest job missing step: Set up Node"); requireFullShaAction(errors, setupNode, "double-onboard-vitest setup-node"); const installRootDependencies = requireJobStep( @@ -2574,38 +3543,78 @@ function validateDoubleOnboardVitestJob(errors: string[], jobs: WorkflowRecord): steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); const buildCli = requireJobStep(errors, jobName, steps, "Build CLI"); requireRunContains(errors, buildCli, "npm run build:cli"); - const installTools = requireJobStep(errors, jobName, steps, "Install OpenShell CLI"); + const installTools = requireJobStep( + errors, + jobName, + steps, + "Install OpenShell CLI", + ); requireRunContains(errors, installTools, "bash scripts/install-openshell.sh"); - const runVitest = requireJobStep(errors, jobName, steps, "Run double-onboard live Vitest test"); + const runVitest = requireJobStep( + errors, + jobName, + steps, + "Run double-onboard live Vitest test", + ); requireRunContains(errors, runVitest, "OPENSHELL_BIN"); - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/double-onboard.test.ts"); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/double-onboard.test.ts", + ); - const upload = requireJobStep(errors, jobName, steps, "Upload double-onboard Vitest artifacts"); + const upload = requireJobStep( + errors, + jobName, + steps, + "Upload double-onboard Vitest artifacts", + ); requireFullShaAction(errors, upload, "double-onboard-vitest upload-artifact"); const uploadWith = asRecord(upload?.with); if (uploadWith.name !== "e2e-vitest-scenarios-double-onboard") { errors.push("double-onboard-vitest artifact upload name must be stable"); } const uploadPath = stringValue(uploadWith.path); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/double-onboard/"); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/double-onboard/", + ); if (uploadWith["include-hidden-files"] !== false) { - errors.push("double-onboard-vitest artifact upload must set include-hidden-files: false"); + errors.push( + "double-onboard-vitest artifact upload must set include-hidden-files: false", + ); } if (uploadWith["if-no-files-found"] !== "ignore") { - errors.push("double-onboard-vitest artifact upload must ignore missing fixture artifacts"); + errors.push( + "double-onboard-vitest artifact upload must ignore missing fixture artifacts", + ); } if (uploadWith["retention-days"] !== 14) { - errors.push("double-onboard-vitest artifact upload retention-days must be 14"); + errors.push( + "double-onboard-vitest artifact upload retention-days must be 14", + ); } } -function validateRuntimeOverridesVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateRuntimeOverridesVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "runtime-overrides-vitest"; const job = asRecord(jobs[jobName]); if (Object.keys(job).length === 0) { @@ -2620,10 +3629,13 @@ function validateRuntimeOverridesVitestJob(errors: string[], jobs: WorkflowRecor const jobEnv = asRecord(job.env); if (jobEnv.NEMOCLAW_RUN_E2E_SCENARIOS !== "1") { - errors.push("runtime-overrides-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); + errors.push( + "runtime-overrides-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1", + ); } if ( - jobEnv.E2E_ARTIFACT_DIR !== "${{ github.workspace }}/e2e-artifacts/vitest/runtime-overrides" + jobEnv.E2E_ARTIFACT_DIR !== + "${{ github.workspace }}/e2e-artifacts/vitest/runtime-overrides" ) { errors.push( "runtime-overrides-vitest job must write artifacts under e2e-artifacts/vitest/runtime-overrides", @@ -2641,29 +3653,54 @@ function validateRuntimeOverridesVitestJob(errors: string[], jobs: WorkflowRecor jobEnv, "DOCKERHUB_USERNAME", ); - requireEnvDoesNotExposeSecret(errors, "runtime-overrides-vitest job", jobEnv, "DOCKERHUB_TOKEN"); + requireEnvDoesNotExposeSecret( + errors, + "runtime-overrides-vitest job", + jobEnv, + "DOCKERHUB_TOKEN", + ); const steps = asSteps(job.steps); requireNoDispatchInputInterpolation(errors, steps); for (const step of steps) { const stepName = `runtime-overrides-vitest step '${step.name ?? step.uses ?? ""}'`; const stepEnv = asRecord(step.env); - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_INFERENCE_API_KEY"); - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_USERNAME"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "NVIDIA_INFERENCE_API_KEY", + ); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_USERNAME", + ); requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_TOKEN"); requireNoDockerHubAuthInRun(errors, stepName, stringValue(step.run)); } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); - if (!checkout) errors.push("runtime-overrides-vitest job missing checkout step"); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); + if (!checkout) + errors.push("runtime-overrides-vitest job missing checkout step"); requireFullShaAction(errors, checkout, "runtime-overrides-vitest checkout"); if (asRecord(checkout?.with)["persist-credentials"] !== false) { - errors.push("runtime-overrides-vitest checkout step must set persist-credentials=false"); + errors.push( + "runtime-overrides-vitest checkout step must set persist-credentials=false", + ); } const setupNode = namedStep(steps, "Set up Node"); - if (!setupNode) errors.push("runtime-overrides-vitest job missing step: Set up Node"); - requireFullShaAction(errors, setupNode, "runtime-overrides-vitest setup-node"); + if (!setupNode) + errors.push("runtime-overrides-vitest job missing step: Set up Node"); + requireFullShaAction( + errors, + setupNode, + "runtime-overrides-vitest setup-node", + ); const installRootDependencies = requireJobStep( errors, @@ -2671,32 +3708,71 @@ function validateRuntimeOverridesVitestJob(errors: string[], jobs: WorkflowRecor steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); - - const runVitest = requireJobStep(errors, jobName, steps, "Run runtime overrides live test"); - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/runtime-overrides.test.ts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); - const upload = requireJobStep(errors, jobName, steps, "Upload runtime overrides artifacts"); - requireFullShaAction(errors, upload, "runtime-overrides-vitest upload-artifact"); - const uploadWith = asRecord(upload?.with); - if (uploadWith.name !== "e2e-vitest-scenarios-runtime-overrides") { + const runVitest = requireJobStep( + errors, + jobName, + steps, + "Run runtime overrides live test", + ); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/runtime-overrides.test.ts", + ); + + const upload = requireJobStep( + errors, + jobName, + steps, + "Upload runtime overrides artifacts", + ); + requireFullShaAction( + errors, + upload, + "runtime-overrides-vitest upload-artifact", + ); + const uploadWith = asRecord(upload?.with); + if (uploadWith.name !== "e2e-vitest-scenarios-runtime-overrides") { errors.push("runtime-overrides-vitest artifact upload name must be stable"); } const uploadPath = stringValue(uploadWith.path); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/runtime-overrides/"); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/runtime-overrides/", + ); if (uploadWith["include-hidden-files"] !== false) { - errors.push("runtime-overrides-vitest artifact upload must set include-hidden-files: false"); + errors.push( + "runtime-overrides-vitest artifact upload must set include-hidden-files: false", + ); } if (uploadWith["if-no-files-found"] !== "ignore") { - errors.push("runtime-overrides-vitest artifact upload must ignore missing fixture artifacts"); + errors.push( + "runtime-overrides-vitest artifact upload must ignore missing fixture artifacts", + ); } if (uploadWith["retention-days"] !== 14) { - errors.push("runtime-overrides-vitest artifact upload retention-days must be 14"); + errors.push( + "runtime-overrides-vitest artifact upload retention-days must be 14", + ); } } -function validateHermesE2EVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateHermesE2EVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "hermes-e2e-vitest"; const job = asRecord(jobs[jobName]); if (Object.keys(job).length === 0) { @@ -2708,13 +3784,21 @@ function validateHermesE2EVitestJob(errors: string[], jobs: WorkflowRecord): voi errors.push("hermes-e2e-vitest job must run on ubuntu-latest"); } if (job.needs !== "generate-matrix") { - errors.push("hermes-e2e-vitest job must depend on generate-matrix validation"); + errors.push( + "hermes-e2e-vitest job must depend on generate-matrix validation", + ); } - if (job.if !== "${{ needs.generate-matrix.outputs.hermes_selected == 'true' }}") { - errors.push("hermes-e2e-vitest job must use validated hermes_selected output"); + if ( + job.if !== "${{ needs.generate-matrix.outputs.hermes_selected == 'true' }}" + ) { + errors.push( + "hermes-e2e-vitest job must use validated hermes_selected output", + ); } if (stringValue(job.if).includes("inputs.scenarios")) { - errors.push("hermes-e2e-vitest job must not inspect raw workflow dispatch scenarios"); + errors.push( + "hermes-e2e-vitest job must not inspect raw workflow dispatch scenarios", + ); } const jobEnv = asRecord(job.env); @@ -2722,10 +3806,17 @@ function validateHermesE2EVitestJob(errors: string[], jobs: WorkflowRecord): voi errors.push("hermes-e2e-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); } if (jobEnv.NEMOCLAW_CLI_BIN !== "${{ github.workspace }}/bin/nemoclaw.js") { - errors.push("hermes-e2e-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI"); + errors.push( + "hermes-e2e-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI", + ); } - if (jobEnv.E2E_ARTIFACT_DIR !== "${{ github.workspace }}/e2e-artifacts/vitest/hermes-e2e") { - errors.push("hermes-e2e-vitest job must write artifacts under e2e-artifacts/vitest/hermes-e2e"); + if ( + jobEnv.E2E_ARTIFACT_DIR !== + "${{ github.workspace }}/e2e-artifacts/vitest/hermes-e2e" + ) { + errors.push( + "hermes-e2e-vitest job must write artifacts under e2e-artifacts/vitest/hermes-e2e", + ); } if (jobEnv.NEMOCLAW_AGENT !== "hermes") { errors.push("hermes-e2e-vitest job must set NEMOCLAW_AGENT=hermes"); @@ -2734,7 +3825,9 @@ function validateHermesE2EVitestJob(errors: string[], jobs: WorkflowRecord): voi errors.push("hermes-e2e-vitest job must pin the CI-safe Hermes model"); } if (jobEnv.NEMOCLAW_ONBOARD_VALIDATION_TIMEOUT_SECONDS !== "60") { - errors.push("hermes-e2e-vitest job must give hosted endpoint validation a CI-safe timeout"); + errors.push( + "hermes-e2e-vitest job must give hosted endpoint validation a CI-safe timeout", + ); } requireEnvDoesNotExposeSecret( errors, @@ -2756,15 +3849,20 @@ function validateHermesE2EVitestJob(errors: string[], jobs: WorkflowRecord): voi } } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); if (!checkout) errors.push("hermes-e2e-vitest job missing checkout step"); requireFullShaAction(errors, checkout, "hermes-e2e-vitest checkout"); if (asRecord(checkout?.with)["persist-credentials"] !== false) { - errors.push("hermes-e2e-vitest checkout step must set persist-credentials=false"); + errors.push( + "hermes-e2e-vitest checkout step must set persist-credentials=false", + ); } const setupNode = namedStep(steps, "Set up Node"); - if (!setupNode) errors.push("hermes-e2e-vitest job missing step: Set up Node"); + if (!setupNode) + errors.push("hermes-e2e-vitest job missing step: Set up Node"); requireFullShaAction(errors, setupNode, "hermes-e2e-vitest setup-node"); const installRootDependencies = requireJobStep( @@ -2773,40 +3871,78 @@ function validateHermesE2EVitestJob(errors: string[], jobs: WorkflowRecord): voi steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); const buildCli = requireJobStep(errors, jobName, steps, "Build CLI"); requireRunContains(errors, buildCli, "npm run build:cli"); - const runVitest = requireJobStep(errors, jobName, steps, "Run Hermes live Vitest test"); - const runVitestEnv = asRecord(runVitest?.env); - requireHostedInferenceCredentialEnv(errors, "hermes-e2e-vitest Vitest step", runVitestEnv, { + const runVitest = requireJobStep( + errors, jobName, - }); - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/hermes-e2e.test.ts"); + steps, + "Run Hermes live Vitest test", + ); + const runVitestEnv = asRecord(runVitest?.env); + if ( + runVitestEnv.NVIDIA_INFERENCE_API_KEY !== + "${{ secrets.NVIDIA_INFERENCE_API_KEY }}" + ) { + errors.push( + "hermes-e2e-vitest Vitest step must receive NVIDIA_INFERENCE_API_KEY from secrets", + ); + } + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/hermes-e2e.test.ts", + ); requireRunDoesNotContain(errors, runVitest, "${{ inputs."); - const upload = requireJobStep(errors, jobName, steps, "Upload Hermes live Vitest artifacts"); + const upload = requireJobStep( + errors, + jobName, + steps, + "Upload Hermes live Vitest artifacts", + ); requireFullShaAction(errors, upload, "hermes-e2e-vitest upload-artifact"); const uploadWith = asRecord(upload?.with); if (uploadWith.name !== "e2e-vitest-scenarios-hermes-e2e") { errors.push("hermes-e2e-vitest artifact upload name must be stable"); } const uploadPath = stringValue(uploadWith.path); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/hermes-e2e/"); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/hermes-e2e/", + ); if (uploadWith["include-hidden-files"] !== false) { - errors.push("hermes-e2e-vitest artifact upload must set include-hidden-files: false"); + errors.push( + "hermes-e2e-vitest artifact upload must set include-hidden-files: false", + ); } if (uploadWith["if-no-files-found"] !== "ignore") { - errors.push("hermes-e2e-vitest artifact upload must ignore missing fixture artifacts"); + errors.push( + "hermes-e2e-vitest artifact upload must ignore missing fixture artifacts", + ); } if (uploadWith["retention-days"] !== 14) { errors.push("hermes-e2e-vitest artifact upload retention-days must be 14"); } } -function validateHermesRootEntrypointSmokeVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateHermesRootEntrypointSmokeVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "hermes-root-entrypoint-smoke-vitest"; const job = asRecord(jobs[jobName]); if (Object.keys(job).length === 0) { @@ -2815,10 +3951,14 @@ function validateHermesRootEntrypointSmokeVitestJob(errors: string[], jobs: Work } if (job["runs-on"] !== "ubuntu-latest") { - errors.push("hermes-root-entrypoint-smoke-vitest job must run on ubuntu-latest"); + errors.push( + "hermes-root-entrypoint-smoke-vitest job must run on ubuntu-latest", + ); } if (job.needs !== "generate-matrix") { - errors.push("hermes-root-entrypoint-smoke-vitest job must depend on generate-matrix"); + errors.push( + "hermes-root-entrypoint-smoke-vitest job must depend on generate-matrix", + ); } const expectedIf = "${{ needs.generate-matrix.result == 'success' && ((inputs.jobs == '' && inputs.scenarios == '') || contains(format(',{0},', inputs.jobs), ',hermes-root-entrypoint-smoke-vitest,') || contains(format(',{0},', inputs.scenarios), ',hermes-root-entrypoint-smoke,')) }}"; @@ -2828,12 +3968,16 @@ function validateHermesRootEntrypointSmokeVitestJob(errors: string[], jobs: Work ); } if (job["timeout-minutes"] !== 45) { - errors.push("hermes-root-entrypoint-smoke-vitest job must keep the 45 minute timeout"); + errors.push( + "hermes-root-entrypoint-smoke-vitest job must keep the 45 minute timeout", + ); } const jobEnv = asRecord(job.env); if (jobEnv.NEMOCLAW_RUN_E2E_SCENARIOS !== "1") { - errors.push("hermes-root-entrypoint-smoke-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); + errors.push( + "hermes-root-entrypoint-smoke-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1", + ); } if ( jobEnv.E2E_ARTIFACT_DIR !== @@ -2898,9 +4042,18 @@ function validateHermesRootEntrypointSmokeVitestJob(errors: string[], jobs: Work ); } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); - if (!checkout) errors.push("hermes-root-entrypoint-smoke-vitest job missing checkout step"); - requireFullShaAction(errors, checkout, "hermes-root-entrypoint-smoke-vitest checkout"); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); + if (!checkout) + errors.push( + "hermes-root-entrypoint-smoke-vitest job missing checkout step", + ); + requireFullShaAction( + errors, + checkout, + "hermes-root-entrypoint-smoke-vitest checkout", + ); if (asRecord(checkout?.with)["persist-credentials"] !== false) { errors.push( "hermes-root-entrypoint-smoke-vitest checkout step must set persist-credentials=false", @@ -2908,8 +4061,15 @@ function validateHermesRootEntrypointSmokeVitestJob(errors: string[], jobs: Work } const setupNode = namedStep(steps, "Set up Node"); - if (!setupNode) errors.push("hermes-root-entrypoint-smoke-vitest job missing step: Set up Node"); - requireFullShaAction(errors, setupNode, "hermes-root-entrypoint-smoke-vitest setup-node"); + if (!setupNode) + errors.push( + "hermes-root-entrypoint-smoke-vitest job missing step: Set up Node", + ); + requireFullShaAction( + errors, + setupNode, + "hermes-root-entrypoint-smoke-vitest setup-node", + ); const installRootDependencies = requireJobStep( errors, @@ -2917,7 +4077,11 @@ function validateHermesRootEntrypointSmokeVitestJob(errors: string[], jobs: Work steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); const runVitest = requireJobStep( errors, @@ -2925,7 +4089,11 @@ function validateHermesRootEntrypointSmokeVitestJob(errors: string[], jobs: Work steps, "Run Hermes root entrypoint smoke live test", ); - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); requireRunContains( errors, runVitest, @@ -2939,10 +4107,16 @@ function validateHermesRootEntrypointSmokeVitestJob(errors: string[], jobs: Work steps, "Upload Hermes root entrypoint smoke artifacts", ); - requireFullShaAction(errors, upload, "hermes-root-entrypoint-smoke-vitest upload-artifact"); + requireFullShaAction( + errors, + upload, + "hermes-root-entrypoint-smoke-vitest upload-artifact", + ); const uploadWith = asRecord(upload?.with); if (uploadWith.name !== "e2e-vitest-scenarios-hermes-root-entrypoint-smoke") { - errors.push("hermes-root-entrypoint-smoke-vitest artifact upload name must be stable"); + errors.push( + "hermes-root-entrypoint-smoke-vitest artifact upload name must be stable", + ); } const uploadPath = stringValue(uploadWith.path); requireUploadPathContains( @@ -2961,7 +4135,9 @@ function validateHermesRootEntrypointSmokeVitestJob(errors: string[], jobs: Work ); } if (uploadWith["retention-days"] !== 14) { - errors.push("hermes-root-entrypoint-smoke-vitest artifact upload retention-days must be 14"); + errors.push( + "hermes-root-entrypoint-smoke-vitest artifact upload retention-days must be 14", + ); } } @@ -3093,7 +4269,11 @@ function validateHermesSandboxSecretBoundaryVitestJob( } } -function validateDiagnosticsVitestJob(errors: string[], jobs: WorkflowRecord): void { + +function validateDiagnosticsVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "diagnostics-vitest"; const scenarioName = "diagnostics"; const job = asRecord(jobs[jobName]); @@ -3112,15 +4292,22 @@ function validateDiagnosticsVitestJob(errors: string[], jobs: WorkflowRecord): v const jobEnv = asRecord(job.env); if ("DOCKER_CONFIG" in jobEnv) { - errors.push("diagnostics-vitest job must not expose Docker auth to branch-controlled steps"); + errors.push( + "diagnostics-vitest job must not expose Docker auth to branch-controlled steps", + ); } - if (jobEnv.E2E_ARTIFACT_DIR !== "${{ github.workspace }}/e2e-artifacts/vitest/diagnostics") { + if ( + jobEnv.E2E_ARTIFACT_DIR !== + "${{ github.workspace }}/e2e-artifacts/vitest/diagnostics" + ) { errors.push( "diagnostics-vitest job must write artifacts under e2e-artifacts/vitest/diagnostics", ); } if (jobEnv.NEMOCLAW_CLI_BIN !== "${{ github.workspace }}/bin/nemoclaw.js") { - errors.push("diagnostics-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI"); + errors.push( + "diagnostics-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI", + ); } if (jobEnv.NEMOCLAW_RUN_E2E_SCENARIOS !== "1") { errors.push("diagnostics-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); @@ -3129,10 +4316,14 @@ function validateDiagnosticsVitestJob(errors: string[], jobs: WorkflowRecord): v errors.push("diagnostics-vitest job must set NEMOCLAW_NON_INTERACTIVE=1"); } if (jobEnv.NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE !== "1") { - errors.push("diagnostics-vitest job must set NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1"); + errors.push( + "diagnostics-vitest job must set NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1", + ); } if (jobEnv.NEMOCLAW_SANDBOX_NAME !== "e2e-diag") { - errors.push("diagnostics-vitest job must use the stable e2e-diag sandbox name"); + errors.push( + "diagnostics-vitest job must use the stable e2e-diag sandbox name", + ); } if (jobEnv.OPENSHELL_GATEWAY !== "nemoclaw") { errors.push("diagnostics-vitest job must force OPENSHELL_GATEWAY=nemoclaw"); @@ -3143,7 +4334,12 @@ function validateDiagnosticsVitestJob(errors: string[], jobs: WorkflowRecord): v "DOCKERHUB_TOKEN", "GITHUB_TOKEN", ]) { - requireEnvDoesNotExposeSecret(errors, "diagnostics-vitest job", jobEnv, secret); + requireEnvDoesNotExposeSecret( + errors, + "diagnostics-vitest job", + jobEnv, + secret, + ); } const steps = asSteps(job.steps); @@ -3152,9 +4348,19 @@ function validateDiagnosticsVitestJob(errors: string[], jobs: WorkflowRecord): v const stepName = `diagnostics-vitest step '${step.name ?? step.uses ?? ""}'`; const stepEnv = asRecord(step.env); if (step.name !== "Run diagnostics live test") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_API_KEY"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "NVIDIA_API_KEY", + ); } - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_USERNAME"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_USERNAME", + ); requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_TOKEN"); requireNoDockerHubAuthInRun(errors, stepName, stringValue(step.run)); requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "GITHUB_TOKEN"); @@ -3166,15 +4372,20 @@ function validateDiagnosticsVitestJob(errors: string[], jobs: WorkflowRecord): v ); } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); if (!checkout) errors.push("diagnostics-vitest job missing checkout step"); requireFullShaAction(errors, checkout, "diagnostics-vitest checkout"); if (asRecord(checkout?.with)["persist-credentials"] !== false) { - errors.push("diagnostics-vitest checkout step must set persist-credentials=false"); + errors.push( + "diagnostics-vitest checkout step must set persist-credentials=false", + ); } const setupNode = namedStep(steps, "Set up Node"); - if (!setupNode) errors.push("diagnostics-vitest job missing step: Set up Node"); + if (!setupNode) + errors.push("diagnostics-vitest job missing step: Set up Node"); requireFullShaAction(errors, setupNode, "diagnostics-vitest setup-node"); const installRootDependencies = requireJobStep( @@ -3183,40 +4394,75 @@ function validateDiagnosticsVitestJob(errors: string[], jobs: WorkflowRecord): v steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); const buildCli = requireJobStep(errors, jobName, steps, "Build CLI"); requireRunContains(errors, buildCli, "npm run build:cli"); - const runVitest = requireJobStep(errors, jobName, steps, "Run diagnostics live test"); - const runVitestEnv = asRecord(runVitest?.env); - requireHostedInferenceCredentialEnv(errors, "diagnostics-vitest Vitest step", runVitestEnv, { + const runVitest = requireJobStep( + errors, jobName, - }); - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/diagnostics.test.ts"); + steps, + "Run diagnostics live test", + ); + const runVitestEnv = asRecord(runVitest?.env); + if (runVitestEnv.NVIDIA_API_KEY !== "${{ secrets.NVIDIA_API_KEY }}") { + errors.push( + "diagnostics-vitest Vitest step must receive NVIDIA_API_KEY from secrets", + ); + } + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/diagnostics.test.ts", + ); requireRunDoesNotContain(errors, runVitest, "${{ inputs."); - const upload = requireJobStep(errors, jobName, steps, "Upload diagnostics artifacts"); + const upload = requireJobStep( + errors, + jobName, + steps, + "Upload diagnostics artifacts", + ); requireFullShaAction(errors, upload, "diagnostics-vitest upload-artifact"); const uploadWith = asRecord(upload?.with); if (uploadWith.name !== "e2e-vitest-scenarios-diagnostics") { errors.push("diagnostics-vitest artifact upload name must be stable"); } const uploadPath = stringValue(uploadWith.path); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/diagnostics/"); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/diagnostics/", + ); if (uploadWith["include-hidden-files"] !== false) { - errors.push("diagnostics-vitest artifact upload must set include-hidden-files: false"); + errors.push( + "diagnostics-vitest artifact upload must set include-hidden-files: false", + ); } if (uploadWith["if-no-files-found"] !== "ignore") { - errors.push("diagnostics-vitest artifact upload must ignore missing fixture artifacts"); + errors.push( + "diagnostics-vitest artifact upload must ignore missing fixture artifacts", + ); } if (uploadWith["retention-days"] !== 14) { errors.push("diagnostics-vitest artifact upload retention-days must be 14"); } } -function validateSparkInstallVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateSparkInstallVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "spark-install-vitest"; const scenarioName = "spark-install"; const job = asRecord(jobs[jobName]); @@ -3234,22 +4480,31 @@ function validateSparkInstallVitestJob(errors: string[], jobs: WorkflowRecord): validateFreeStandingJobSelector(errors, jobs, jobName, scenarioName); const jobEnv = asRecord(job.env); - if (jobEnv.E2E_ARTIFACT_DIR !== "${{ github.workspace }}/e2e-artifacts/vitest/spark-install") { + if ( + jobEnv.E2E_ARTIFACT_DIR !== + "${{ github.workspace }}/e2e-artifacts/vitest/spark-install" + ) { errors.push( "spark-install-vitest job must write artifacts under e2e-artifacts/vitest/spark-install", ); } if (jobEnv.NEMOCLAW_CLI_BIN !== "${{ github.workspace }}/bin/nemoclaw.js") { - errors.push("spark-install-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI"); + errors.push( + "spark-install-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI", + ); } if (jobEnv.NEMOCLAW_RUN_E2E_SCENARIOS !== "1") { - errors.push("spark-install-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); + errors.push( + "spark-install-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1", + ); } if (jobEnv.NEMOCLAW_NON_INTERACTIVE !== "1") { errors.push("spark-install-vitest job must set NEMOCLAW_NON_INTERACTIVE=1"); } if (jobEnv.NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE !== "1") { - errors.push("spark-install-vitest job must accept third-party software non-interactively"); + errors.push( + "spark-install-vitest job must accept third-party software non-interactively", + ); } if (jobEnv.NEMOCLAW_FRESH !== "1") { errors.push("spark-install-vitest job must set NEMOCLAW_FRESH=1"); @@ -3263,10 +4518,17 @@ function validateSparkInstallVitestJob(errors: string[], jobs: WorkflowRecord): errors.push("spark-install-vitest job must use the cloud provider"); } if (jobEnv.OPENSHELL_GATEWAY !== "nemoclaw") { - errors.push("spark-install-vitest job must force OPENSHELL_GATEWAY=nemoclaw"); + errors.push( + "spark-install-vitest job must force OPENSHELL_GATEWAY=nemoclaw", + ); } for (const secret of COMMON_SECRET_ENV_NAMES) { - requireEnvDoesNotExposeSecret(errors, "spark-install-vitest job", jobEnv, secret); + requireEnvDoesNotExposeSecret( + errors, + "spark-install-vitest job", + jobEnv, + secret, + ); } const steps = asSteps(job.steps); @@ -3275,20 +4537,34 @@ function validateSparkInstallVitestJob(errors: string[], jobs: WorkflowRecord): const stepName = `spark-install-vitest step '${step.name ?? step.uses ?? ""}'`; const stepEnv = asRecord(step.env); if (step.name !== "Run Spark install live test") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_API_KEY"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "NVIDIA_API_KEY", + ); } - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_USERNAME"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_USERNAME", + ); requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_TOKEN"); requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "GITHUB_TOKEN"); } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); if (!checkout) { errors.push("spark-install-vitest job missing checkout step"); } requireFullShaAction(errors, checkout, "spark-install-vitest checkout"); if (asRecord(checkout?.with)["persist-credentials"] !== false) { - errors.push("spark-install-vitest checkout step must set persist-credentials=false"); + errors.push( + "spark-install-vitest checkout step must set persist-credentials=false", + ); } const setupNode = namedStep(steps, "Set up Node"); @@ -3303,37 +4579,74 @@ function validateSparkInstallVitestJob(errors: string[], jobs: WorkflowRecord): steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); - const runVitest = requireJobStep(errors, jobName, steps, "Run Spark install live test"); - const runVitestEnv = asRecord(runVitest?.env); + const runVitest = requireJobStep( + errors, + jobName, + steps, + "Run Spark install live test", + ); + const runVitestEnv = asRecord(runVitest?.env); if (runVitestEnv.NVIDIA_API_KEY !== "${{ secrets.NVIDIA_API_KEY }}") { - errors.push("spark-install-vitest Vitest step must receive NVIDIA_API_KEY from secrets"); + errors.push( + "spark-install-vitest Vitest step must receive NVIDIA_API_KEY from secrets", + ); } requireRunContains(errors, runVitest, "set -euo pipefail"); - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/spark-install.test.ts"); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/spark-install.test.ts", + ); - const upload = requireJobStep(errors, jobName, steps, "Upload Spark install artifacts"); + const upload = requireJobStep( + errors, + jobName, + steps, + "Upload Spark install artifacts", + ); requireFullShaAction(errors, upload, "spark-install-vitest upload-artifact"); const uploadWith = asRecord(upload?.with); if (uploadWith.name !== "e2e-vitest-scenarios-spark-install") { errors.push("spark-install-vitest artifact upload name must be stable"); } const uploadPath = stringValue(uploadWith.path); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/spark-install/"); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/spark-install/", + ); if (uploadWith["include-hidden-files"] !== false) { - errors.push("spark-install-vitest artifact upload must set include-hidden-files: false"); + errors.push( + "spark-install-vitest artifact upload must set include-hidden-files: false", + ); } if (uploadWith["if-no-files-found"] !== "ignore") { - errors.push("spark-install-vitest artifact upload must ignore missing fixture artifacts"); + errors.push( + "spark-install-vitest artifact upload must ignore missing fixture artifacts", + ); } if (uploadWith["retention-days"] !== 14) { - errors.push("spark-install-vitest artifact upload retention-days must be 14"); + errors.push( + "spark-install-vitest artifact upload retention-days must be 14", + ); } } -function validateSnapshotCommandsVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateSnapshotCommandsVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "snapshot-commands-vitest"; const scenarioName = "snapshot-commands"; const job = asRecord(jobs[jobName]); @@ -3352,29 +4665,42 @@ function validateSnapshotCommandsVitestJob(errors: string[], jobs: WorkflowRecor const jobEnv = asRecord(job.env); if ("DOCKER_CONFIG" in jobEnv) { - errors.push("snapshot-commands-vitest job must not set DOCKER_CONFIG at job level"); + errors.push( + "snapshot-commands-vitest job must not set DOCKER_CONFIG at job level", + ); } if ( - jobEnv.E2E_ARTIFACT_DIR !== "${{ github.workspace }}/e2e-artifacts/vitest/snapshot-commands" + jobEnv.E2E_ARTIFACT_DIR !== + "${{ github.workspace }}/e2e-artifacts/vitest/snapshot-commands" ) { errors.push( "snapshot-commands-vitest job must write artifacts under e2e-artifacts/vitest/snapshot-commands", ); } if (jobEnv.NEMOCLAW_RUN_E2E_SCENARIOS !== "1") { - errors.push("snapshot-commands-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); + errors.push( + "snapshot-commands-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1", + ); } if (jobEnv.NEMOCLAW_NON_INTERACTIVE !== "1") { - errors.push("snapshot-commands-vitest job must set NEMOCLAW_NON_INTERACTIVE=1"); + errors.push( + "snapshot-commands-vitest job must set NEMOCLAW_NON_INTERACTIVE=1", + ); } if (jobEnv.NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE !== "1") { - errors.push("snapshot-commands-vitest job must accept third-party software non-interactively"); + errors.push( + "snapshot-commands-vitest job must accept third-party software non-interactively", + ); } if (jobEnv.NEMOCLAW_SANDBOX_NAME !== "e2e-snapshot") { - errors.push("snapshot-commands-vitest job must use the stable e2e-snapshot sandbox name"); + errors.push( + "snapshot-commands-vitest job must use the stable e2e-snapshot sandbox name", + ); } if (jobEnv.OPENSHELL_GATEWAY !== "nemoclaw") { - errors.push("snapshot-commands-vitest job must force OPENSHELL_GATEWAY=nemoclaw"); + errors.push( + "snapshot-commands-vitest job must force OPENSHELL_GATEWAY=nemoclaw", + ); } for (const secret of [ "NVIDIA_API_KEY", @@ -3382,7 +4708,12 @@ function validateSnapshotCommandsVitestJob(errors: string[], jobs: WorkflowRecor "DOCKERHUB_TOKEN", "GITHUB_TOKEN", ]) { - requireEnvDoesNotExposeSecret(errors, "snapshot-commands-vitest job", jobEnv, secret); + requireEnvDoesNotExposeSecret( + errors, + "snapshot-commands-vitest job", + jobEnv, + secret, + ); } const steps = asSteps(job.steps); @@ -3391,23 +4722,42 @@ function validateSnapshotCommandsVitestJob(errors: string[], jobs: WorkflowRecor const stepName = `snapshot-commands-vitest step '${step.name ?? step.uses ?? ""}'`; const stepEnv = asRecord(step.env); if (step.name !== "Run snapshot commands live test") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_API_KEY"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "NVIDIA_API_KEY", + ); } if (step.name !== "Authenticate to Docker Hub") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_USERNAME"); - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_TOKEN"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_USERNAME", + ); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_TOKEN", + ); requireNoDockerHubAuthInRun(errors, stepName, stringValue(step.run)); } requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "GITHUB_TOKEN"); } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); if (!checkout) { errors.push("snapshot-commands-vitest job missing checkout step"); } requireFullShaAction(errors, checkout, "snapshot-commands-vitest checkout"); if (asRecord(checkout?.with)["persist-credentials"] !== false) { - errors.push("snapshot-commands-vitest checkout step must set persist-credentials=false"); + errors.push( + "snapshot-commands-vitest checkout step must set persist-credentials=false", + ); } const configureDockerAuth = requireJobStep( @@ -3423,9 +4773,16 @@ function validateSnapshotCommandsVitestJob(errors: string[], jobs: WorkflowRecor ); requireRunDoesNotContain(errors, configureDockerAuth, "${{ runner.temp }}"); - const dockerLogin = requireJobStep(errors, jobName, steps, "Authenticate to Docker Hub"); + const dockerLogin = requireJobStep( + errors, + jobName, + steps, + "Authenticate to Docker Hub", + ); const dockerLoginEnv = asRecord(dockerLogin?.env); - if (dockerLoginEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}") { + if ( + dockerLoginEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}" + ) { errors.push( "snapshot-commands-vitest Docker Hub auth must receive DOCKERHUB_USERNAME from secrets", ); @@ -3445,7 +4802,11 @@ function validateSnapshotCommandsVitestJob(errors: string[], jobs: WorkflowRecor if (!setupNode) { errors.push("snapshot-commands-vitest job missing step: Set up Node"); } - requireFullShaAction(errors, setupNode, "snapshot-commands-vitest setup-node"); + requireFullShaAction( + errors, + setupNode, + "snapshot-commands-vitest setup-node", + ); const installRootDependencies = requireJobStep( errors, @@ -3453,38 +4814,78 @@ function validateSnapshotCommandsVitestJob(errors: string[], jobs: WorkflowRecor steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); - const runVitest = requireJobStep(errors, jobName, steps, "Run snapshot commands live test"); + const runVitest = requireJobStep( + errors, + jobName, + steps, + "Run snapshot commands live test", + ); const runVitestEnv = asRecord(runVitest?.env); - requireHostedInferenceCredentialEnv( + if (runVitestEnv.NVIDIA_API_KEY !== "${{ secrets.NVIDIA_API_KEY }}") { + errors.push( + "snapshot-commands-vitest Vitest step must receive NVIDIA_API_KEY from secrets", + ); + } + requireRunContains( errors, - "snapshot-commands-vitest Vitest step", - runVitestEnv, - { jobName }, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/snapshot-commands.test.ts", ); - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/snapshot-commands.test.ts"); - const upload = requireJobStep(errors, jobName, steps, "Upload snapshot commands artifacts"); - requireFullShaAction(errors, upload, "snapshot-commands-vitest upload-artifact"); + const upload = requireJobStep( + errors, + jobName, + steps, + "Upload snapshot commands artifacts", + ); + requireFullShaAction( + errors, + upload, + "snapshot-commands-vitest upload-artifact", + ); const uploadWith = asRecord(upload?.with); if (uploadWith.name !== "e2e-vitest-scenarios-snapshot-commands") { errors.push("snapshot-commands-vitest artifact upload name must be stable"); } const uploadPath = stringValue(uploadWith.path); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/snapshot-commands/"); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/snapshot-commands/", + ); if (uploadWith["include-hidden-files"] !== false) { - errors.push("snapshot-commands-vitest artifact upload must set include-hidden-files: false"); + errors.push( + "snapshot-commands-vitest artifact upload must set include-hidden-files: false", + ); } if (uploadWith["if-no-files-found"] !== "ignore") { - errors.push("snapshot-commands-vitest artifact upload must ignore missing fixture artifacts"); + errors.push( + "snapshot-commands-vitest artifact upload must ignore missing fixture artifacts", + ); } if (uploadWith["retention-days"] !== 14) { - errors.push("snapshot-commands-vitest artifact upload retention-days must be 14"); + errors.push( + "snapshot-commands-vitest artifact upload retention-days must be 14", + ); } - const cleanup = requireJobStep(errors, jobName, steps, "Clean up Docker auth"); + const cleanup = requireJobStep( + errors, + jobName, + steps, + "Clean up Docker auth", + ); if (cleanup?.if !== "always()") { errors.push("snapshot-commands-vitest Docker auth cleanup must always run"); } @@ -3500,12 +4901,16 @@ function validateModelRouterProviderRoutedInferenceVitestJob( const scenarioName = "model-router-provider-routed-inference"; const job = asRecord(jobs[jobName]); if (Object.keys(job).length === 0) { - errors.push("workflow missing model-router-provider-routed-inference-vitest job"); + errors.push( + "workflow missing model-router-provider-routed-inference-vitest job", + ); return; } if (job["runs-on"] !== "ubuntu-latest") { - errors.push("model-router-provider-routed-inference-vitest job must run on ubuntu-latest"); + errors.push( + "model-router-provider-routed-inference-vitest job must run on ubuntu-latest", + ); } validateFreeStandingJobSelector(errors, jobs, jobName, scenarioName); @@ -3558,21 +4963,44 @@ function validateModelRouterProviderRoutedInferenceVitestJob( const stepName = `model-router-provider-routed-inference-vitest step '${step.name ?? step.uses ?? ""}'`; const stepEnv = asRecord(step.env); if (step.name !== "Run Model Router provider-routed inference live test") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_INFERENCE_API_KEY"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "NVIDIA_INFERENCE_API_KEY", + ); } if (step.name !== "Authenticate to Docker Hub") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_USERNAME"); - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_TOKEN"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_USERNAME", + ); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_TOKEN", + ); requireNoDockerHubAuthInRun(errors, stepName, stringValue(step.run)); } requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "GITHUB_TOKEN"); } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); if (!checkout) { - errors.push("model-router-provider-routed-inference-vitest job missing checkout step"); + errors.push( + "model-router-provider-routed-inference-vitest job missing checkout step", + ); } - requireFullShaAction(errors, checkout, "model-router-provider-routed-inference-vitest checkout"); + requireFullShaAction( + errors, + checkout, + "model-router-provider-routed-inference-vitest checkout", + ); if (asRecord(checkout?.with)["persist-credentials"] !== false) { errors.push( "model-router-provider-routed-inference-vitest checkout step must set persist-credentials=false", @@ -3592,9 +5020,16 @@ function validateModelRouterProviderRoutedInferenceVitestJob( ); requireRunDoesNotContain(errors, configureDockerAuth, "${{ runner.temp }}"); - const dockerLogin = requireJobStep(errors, jobName, steps, "Authenticate to Docker Hub"); + const dockerLogin = requireJobStep( + errors, + jobName, + steps, + "Authenticate to Docker Hub", + ); const dockerLoginEnv = asRecord(dockerLogin?.env); - if (dockerLoginEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}") { + if ( + dockerLoginEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}" + ) { errors.push( "model-router-provider-routed-inference-vitest Docker Hub auth must receive DOCKERHUB_USERNAME from secrets", ); @@ -3612,7 +5047,9 @@ function validateModelRouterProviderRoutedInferenceVitestJob( const setupNode = namedStep(steps, "Set up Node"); if (!setupNode) { - errors.push("model-router-provider-routed-inference-vitest job missing step: Set up Node"); + errors.push( + "model-router-provider-routed-inference-vitest job missing step: Set up Node", + ); } requireFullShaAction( errors, @@ -3626,7 +5063,11 @@ function validateModelRouterProviderRoutedInferenceVitestJob( steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); const buildCli = requireJobStep(errors, jobName, steps, "Build CLI"); requireRunContains(errors, buildCli, "npm run build:cli"); @@ -3638,13 +5079,19 @@ function validateModelRouterProviderRoutedInferenceVitestJob( "Run Model Router provider-routed inference live test", ); const runVitestEnv = asRecord(runVitest?.env); - requireHostedInferenceCredentialEnv( + if ( + runVitestEnv.NVIDIA_INFERENCE_API_KEY !== + "${{ secrets.NVIDIA_INFERENCE_API_KEY }}" + ) { + errors.push( + "model-router-provider-routed-inference-vitest Vitest step must receive NVIDIA_INFERENCE_API_KEY from secrets", + ); + } + requireRunContains( errors, - "model-router-provider-routed-inference-vitest Vitest step", - runVitestEnv, - { jobName }, + runVitest, + "npx vitest run --project e2e-scenarios-live", ); - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); requireRunContains( errors, runVitest, @@ -3663,7 +5110,10 @@ function validateModelRouterProviderRoutedInferenceVitestJob( "model-router-provider-routed-inference-vitest upload-artifact", ); const uploadWith = asRecord(upload?.with); - if (uploadWith.name !== "e2e-vitest-scenarios-model-router-provider-routed-inference") { + if ( + uploadWith.name !== + "e2e-vitest-scenarios-model-router-provider-routed-inference" + ) { errors.push( "model-router-provider-routed-inference-vitest artifact upload name must be stable", ); @@ -3690,7 +5140,12 @@ function validateModelRouterProviderRoutedInferenceVitestJob( ); } - const cleanup = requireJobStep(errors, jobName, steps, "Clean up Docker auth"); + const cleanup = requireJobStep( + errors, + jobName, + steps, + "Clean up Docker auth", + ); if (cleanup?.if !== "always()") { errors.push( "model-router-provider-routed-inference-vitest Docker auth cleanup must always run", @@ -3706,7 +5161,10 @@ function runContainsCloudflaredAptInstall(run: string): boolean { ); } -function validateTunnelLifecycleVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateTunnelLifecycleVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "tunnel-lifecycle-vitest"; const scenarioName = "tunnel-lifecycle"; const job = asRecord(jobs[jobName]); @@ -3725,19 +5183,29 @@ function validateTunnelLifecycleVitestJob(errors: string[], jobs: WorkflowRecord const jobEnv = asRecord(job.env); if ("DOCKER_CONFIG" in jobEnv) { - errors.push("tunnel-lifecycle-vitest job must not set DOCKER_CONFIG at job level"); + errors.push( + "tunnel-lifecycle-vitest job must not set DOCKER_CONFIG at job level", + ); } if (jobEnv.NEMOCLAW_CLI_BIN !== "${{ github.workspace }}/bin/nemoclaw.js") { - errors.push("tunnel-lifecycle-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI"); + errors.push( + "tunnel-lifecycle-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI", + ); } if (jobEnv.FREE_STANDING_VITEST_JOB !== "1") { - errors.push("tunnel-lifecycle-vitest job must set FREE_STANDING_VITEST_JOB=1"); + errors.push( + "tunnel-lifecycle-vitest job must set FREE_STANDING_VITEST_JOB=1", + ); } if (jobEnv.FREE_STANDING_SCENARIO_ID !== scenarioName) { - errors.push(`tunnel-lifecycle-vitest job must set FREE_STANDING_SCENARIO_ID=${scenarioName}`); + errors.push( + `tunnel-lifecycle-vitest job must set FREE_STANDING_SCENARIO_ID=${scenarioName}`, + ); } if (jobEnv.NEMOCLAW_RUN_E2E_SCENARIOS !== "1") { - errors.push("tunnel-lifecycle-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); + errors.push( + "tunnel-lifecycle-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1", + ); } requireEnvDoesNotExposeSecret( errors, @@ -3753,23 +5221,47 @@ function validateTunnelLifecycleVitestJob(errors: string[], jobs: WorkflowRecord const stepEnv = asRecord(step.env); requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "GITHUB_TOKEN"); if (step.name !== "Run tunnel lifecycle live test") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_INFERENCE_API_KEY"); - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_API_KEY"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "NVIDIA_INFERENCE_API_KEY", + ); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "NVIDIA_API_KEY", + ); } if (step.name !== "Authenticate to Docker Hub") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_USERNAME"); - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_TOKEN"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_USERNAME", + ); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_TOKEN", + ); requireNoDockerHubAuthInRun(errors, stepName, stringValue(step.run)); } } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); if (!checkout) { errors.push("tunnel-lifecycle-vitest job missing checkout step"); } requireFullShaAction(errors, checkout, "tunnel-lifecycle-vitest checkout"); if (asRecord(checkout?.with)["persist-credentials"] !== false) { - errors.push("tunnel-lifecycle-vitest checkout step must set persist-credentials=false"); + errors.push( + "tunnel-lifecycle-vitest checkout step must set persist-credentials=false", + ); } const configureDockerAuth = requireJobStep( @@ -3784,11 +5276,22 @@ function validateTunnelLifecycleVitestJob(errors: string[], jobs: WorkflowRecord 'echo "DOCKER_CONFIG=${RUNNER_TEMP}/docker-config-tunnel-lifecycle" >> "$GITHUB_ENV"', ); requireRunDoesNotContain(errors, configureDockerAuth, "${{ runner.temp }}"); - requireRunDoesNotContain(errors, configureDockerAuth, "${{ github.workspace }}"); + requireRunDoesNotContain( + errors, + configureDockerAuth, + "${{ github.workspace }}", + ); - const dockerLogin = requireJobStep(errors, jobName, steps, "Authenticate to Docker Hub"); + const dockerLogin = requireJobStep( + errors, + jobName, + steps, + "Authenticate to Docker Hub", + ); const dockerLoginEnv = asRecord(dockerLogin?.env); - if (dockerLoginEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}") { + if ( + dockerLoginEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}" + ) { errors.push( "tunnel-lifecycle-vitest Docker Hub auth must receive DOCKERHUB_USERNAME from secrets", ); @@ -3816,7 +5319,11 @@ function validateTunnelLifecycleVitestJob(errors: string[], jobs: WorkflowRecord steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); const buildCli = requireJobStep(errors, jobName, steps, "Build CLI"); requireRunContains(errors, buildCli, "npm run build:cli"); @@ -3841,42 +5348,88 @@ function validateTunnelLifecycleVitestJob(errors: string[], jobs: WorkflowRecord "NVIDIA_API_KEY", ); requireRunContains(errors, cloudflaredPrereq, "cloudflared --version"); - requireRunContains(errors, cloudflaredPrereq, "test/e2e/lib/cloudflared-version-resolver.sh"); + requireRunContains( + errors, + cloudflaredPrereq, + "test/e2e/lib/cloudflared-version-resolver.sh", + ); requireRunContains(errors, cloudflaredPrereq, "sudo apt-get install -y"); requireRunContains(errors, cloudflaredPrereq, "cloudflared=${cf_version}"); - const runVitest = requireJobStep(errors, jobName, steps, "Run tunnel lifecycle live test"); - const runVitestEnv = asRecord(runVitest?.env); - requireHostedInferenceCredentialEnv(errors, "tunnel-lifecycle-vitest Vitest step", runVitestEnv, { + const runVitest = requireJobStep( + errors, jobName, - }); + steps, + "Run tunnel lifecycle live test", + ); + const runVitestEnv = asRecord(runVitest?.env); + if ( + runVitestEnv.NVIDIA_INFERENCE_API_KEY !== + "${{ secrets.NVIDIA_INFERENCE_API_KEY }}" + ) { + errors.push( + "tunnel-lifecycle-vitest Vitest step must receive NVIDIA_INFERENCE_API_KEY from secrets", + ); + } if (runContainsCloudflaredAptInstall(stringValue(runVitest?.run))) { errors.push( "tunnel-lifecycle-vitest Vitest step must not run cloudflared APT installation with NVIDIA_INFERENCE_API_KEY in scope", ); } - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/tunnel-lifecycle.test.ts"); - - const upload = requireJobStep(errors, jobName, steps, "Upload tunnel lifecycle artifacts"); - requireFullShaAction(errors, upload, "tunnel-lifecycle-vitest upload-artifact"); - const uploadWith = asRecord(upload?.with); - if (uploadWith.name !== "e2e-vitest-scenarios-tunnel-lifecycle") { - errors.push("tunnel-lifecycle-vitest artifact upload name must be stable"); - } - const uploadPath = stringValue(uploadWith.path); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/tunnel-lifecycle/"); - if (uploadWith["include-hidden-files"] !== false) { - errors.push("tunnel-lifecycle-vitest artifact upload must set include-hidden-files: false"); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/tunnel-lifecycle.test.ts", + ); + + const upload = requireJobStep( + errors, + jobName, + steps, + "Upload tunnel lifecycle artifacts", + ); + requireFullShaAction( + errors, + upload, + "tunnel-lifecycle-vitest upload-artifact", + ); + const uploadWith = asRecord(upload?.with); + if (uploadWith.name !== "e2e-vitest-scenarios-tunnel-lifecycle") { + errors.push("tunnel-lifecycle-vitest artifact upload name must be stable"); + } + const uploadPath = stringValue(uploadWith.path); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/tunnel-lifecycle/", + ); + if (uploadWith["include-hidden-files"] !== false) { + errors.push( + "tunnel-lifecycle-vitest artifact upload must set include-hidden-files: false", + ); } if (uploadWith["if-no-files-found"] !== "ignore") { - errors.push("tunnel-lifecycle-vitest artifact upload must ignore missing fixture artifacts"); + errors.push( + "tunnel-lifecycle-vitest artifact upload must ignore missing fixture artifacts", + ); } if (uploadWith["retention-days"] !== 14) { - errors.push("tunnel-lifecycle-vitest artifact upload retention-days must be 14"); + errors.push( + "tunnel-lifecycle-vitest artifact upload retention-days must be 14", + ); } - const cleanup = requireJobStep(errors, jobName, steps, "Clean up Docker auth"); + const cleanup = requireJobStep( + errors, + jobName, + steps, + "Clean up Docker auth", + ); if (cleanup?.if !== "always()") { errors.push("tunnel-lifecycle-vitest Docker auth cleanup must always run"); } @@ -3884,7 +5437,10 @@ function validateTunnelLifecycleVitestJob(errors: string[], jobs: WorkflowRecord requireRunContains(errors, cleanup, 'rm -rf "${DOCKER_CONFIG}"'); } -function validateIssue2478CrashLoopRecoveryVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateIssue2478CrashLoopRecoveryVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "issue-2478-crash-loop-recovery-vitest"; const scenarioName = "issue-2478-crash-loop-recovery"; const job = asRecord(jobs[jobName]); @@ -3894,10 +5450,14 @@ function validateIssue2478CrashLoopRecoveryVitestJob(errors: string[], jobs: Wor } if (job["runs-on"] !== "ubuntu-latest") { - errors.push("issue-2478-crash-loop-recovery-vitest job must run on ubuntu-latest"); + errors.push( + "issue-2478-crash-loop-recovery-vitest job must run on ubuntu-latest", + ); } if (job["timeout-minutes"] !== 30) { - errors.push("issue-2478-crash-loop-recovery-vitest job must keep the 30 minute timeout"); + errors.push( + "issue-2478-crash-loop-recovery-vitest job must keep the 30 minute timeout", + ); } validateFreeStandingJobSelector(errors, jobs, jobName, scenarioName); @@ -3910,7 +5470,8 @@ function validateIssue2478CrashLoopRecoveryVitestJob(errors: string[], jobs: Wor const expectedEnv: Record = { FREE_STANDING_VITEST_JOB: "1", FREE_STANDING_SCENARIO_ID: scenarioName, - E2E_ARTIFACT_DIR: "${{ github.workspace }}/e2e-artifacts/vitest/issue-2478-crash-loop-recovery", + E2E_ARTIFACT_DIR: + "${{ github.workspace }}/e2e-artifacts/vitest/issue-2478-crash-loop-recovery", NEMOCLAW_CLI_BIN: "${{ github.workspace }}/bin/nemoclaw.js", NEMOCLAW_RUN_E2E_SCENARIOS: "1", NEMOCLAW_NON_INTERACTIVE: "1", @@ -3920,10 +5481,15 @@ function validateIssue2478CrashLoopRecoveryVitestJob(errors: string[], jobs: Wor }; for (const [key, value] of Object.entries(expectedEnv)) { if (jobEnv[key] !== value) { - errors.push(`issue-2478-crash-loop-recovery-vitest job env ${key} must be ${value}`); + errors.push( + `issue-2478-crash-loop-recovery-vitest job env ${key} must be ${value}`, + ); } } - for (const secret of ["NVIDIA_INFERENCE_API_KEY", ...COMMON_SECRET_ENV_NAMES]) { + for (const secret of [ + "NVIDIA_INFERENCE_API_KEY", + ...COMMON_SECRET_ENV_NAMES, + ]) { requireEnvDoesNotExposeSecret( errors, "issue-2478-crash-loop-recovery-vitest job", @@ -3937,21 +5503,44 @@ function validateIssue2478CrashLoopRecoveryVitestJob(errors: string[], jobs: Wor for (const step of steps) { const stepName = `issue-2478-crash-loop-recovery-vitest step '${step.name ?? step.uses ?? ""}'`; const stepEnv = asRecord(step.env); - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_INFERENCE_API_KEY"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "NVIDIA_INFERENCE_API_KEY", + ); requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_API_KEY"); if (step.name !== "Authenticate to Docker Hub") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_USERNAME"); - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_TOKEN"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_USERNAME", + ); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_TOKEN", + ); requireNoDockerHubAuthInRun(errors, stepName, stringValue(step.run)); } requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "GITHUB_TOKEN"); } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); if (!checkout) { - errors.push("issue-2478-crash-loop-recovery-vitest job missing checkout step"); + errors.push( + "issue-2478-crash-loop-recovery-vitest job missing checkout step", + ); } - requireFullShaAction(errors, checkout, "issue-2478-crash-loop-recovery-vitest checkout"); + requireFullShaAction( + errors, + checkout, + "issue-2478-crash-loop-recovery-vitest checkout", + ); if (asRecord(checkout?.with)["persist-credentials"] !== false) { errors.push( "issue-2478-crash-loop-recovery-vitest checkout step must set persist-credentials=false", @@ -3970,11 +5559,22 @@ function validateIssue2478CrashLoopRecoveryVitestJob(errors: string[], jobs: Wor 'echo "DOCKER_CONFIG=${RUNNER_TEMP}/docker-config-issue-2478-crash-loop-recovery" >> "$GITHUB_ENV"', ); requireRunDoesNotContain(errors, configureDockerAuth, "${{ runner.temp }}"); - requireRunDoesNotContain(errors, configureDockerAuth, "${{ github.workspace }}"); + requireRunDoesNotContain( + errors, + configureDockerAuth, + "${{ github.workspace }}", + ); - const dockerLogin = requireJobStep(errors, jobName, steps, "Authenticate to Docker Hub"); + const dockerLogin = requireJobStep( + errors, + jobName, + steps, + "Authenticate to Docker Hub", + ); const dockerLoginEnv = asRecord(dockerLogin?.env); - if (dockerLoginEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}") { + if ( + dockerLoginEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}" + ) { errors.push( "issue-2478-crash-loop-recovery-vitest Docker Hub auth must receive DOCKERHUB_USERNAME from secrets", ); @@ -3992,9 +5592,15 @@ function validateIssue2478CrashLoopRecoveryVitestJob(errors: string[], jobs: Wor const setupNode = namedStep(steps, "Set up Node"); if (!setupNode) { - errors.push("issue-2478-crash-loop-recovery-vitest job missing step: Set up Node"); + errors.push( + "issue-2478-crash-loop-recovery-vitest job missing step: Set up Node", + ); } - requireFullShaAction(errors, setupNode, "issue-2478-crash-loop-recovery-vitest setup-node"); + requireFullShaAction( + errors, + setupNode, + "issue-2478-crash-loop-recovery-vitest setup-node", + ); const installRootDependencies = requireJobStep( errors, @@ -4002,13 +5608,26 @@ function validateIssue2478CrashLoopRecoveryVitestJob(errors: string[], jobs: Wor steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); const buildCli = requireJobStep(errors, jobName, steps, "Build CLI"); requireRunContains(errors, buildCli, "npm run build:cli"); - const installOpenShell = requireJobStep(errors, jobName, steps, "Install OpenShell CLI"); - requireRunContains(errors, installOpenShell, "bash scripts/install-openshell.sh"); + const installOpenShell = requireJobStep( + errors, + jobName, + steps, + "Install OpenShell CLI", + ); + requireRunContains( + errors, + installOpenShell, + "bash scripts/install-openshell.sh", + ); const runVitest = requireJobStep( errors, @@ -4023,7 +5642,11 @@ function validateIssue2478CrashLoopRecoveryVitestJob(errors: string[], jobs: Wor runVitestEnv, "NVIDIA_INFERENCE_API_KEY", ); - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); requireRunContains( errors, runVitest, @@ -4036,10 +5659,18 @@ function validateIssue2478CrashLoopRecoveryVitestJob(errors: string[], jobs: Wor steps, "Upload issue #2478 crash-loop recovery artifacts", ); - requireFullShaAction(errors, upload, "issue-2478-crash-loop-recovery-vitest upload-artifact"); + requireFullShaAction( + errors, + upload, + "issue-2478-crash-loop-recovery-vitest upload-artifact", + ); const uploadWith = asRecord(upload?.with); - if (uploadWith.name !== "e2e-vitest-scenarios-issue-2478-crash-loop-recovery") { - errors.push("issue-2478-crash-loop-recovery-vitest artifact upload name must be stable"); + if ( + uploadWith.name !== "e2e-vitest-scenarios-issue-2478-crash-loop-recovery" + ) { + errors.push( + "issue-2478-crash-loop-recovery-vitest artifact upload name must be stable", + ); } const uploadPath = stringValue(uploadWith.path); requireUploadPathContains( @@ -4058,18 +5689,30 @@ function validateIssue2478CrashLoopRecoveryVitestJob(errors: string[], jobs: Wor ); } if (uploadWith["retention-days"] !== 14) { - errors.push("issue-2478-crash-loop-recovery-vitest artifact upload retention-days must be 14"); + errors.push( + "issue-2478-crash-loop-recovery-vitest artifact upload retention-days must be 14", + ); } - const cleanup = requireJobStep(errors, jobName, steps, "Clean up Docker auth"); + const cleanup = requireJobStep( + errors, + jobName, + steps, + "Clean up Docker auth", + ); if (cleanup?.if !== "always()") { - errors.push("issue-2478-crash-loop-recovery-vitest Docker auth cleanup must always run"); + errors.push( + "issue-2478-crash-loop-recovery-vitest Docker auth cleanup must always run", + ); } requireRunContains(errors, cleanup, "docker logout docker.io"); requireRunContains(errors, cleanup, 'rm -rf "${DOCKER_CONFIG}"'); } -function validateChannelsAddRemoveVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateChannelsAddRemoveVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "channels-add-remove-vitest"; const scenarioName = "channels-add-remove"; const job = asRecord(jobs[jobName]); @@ -4083,21 +5726,28 @@ function validateChannelsAddRemoveVitestJob(errors: string[], jobs: WorkflowReco } validateFreeStandingJobSelector(errors, jobs, jobName, scenarioName); if (job["timeout-minutes"] !== 75) { - errors.push("channels-add-remove-vitest job must keep the legacy 75 minute timeout"); + errors.push( + "channels-add-remove-vitest job must keep the legacy 75 minute timeout", + ); } const jobEnv = asRecord(job.env); if (jobEnv.NEMOCLAW_RUN_E2E_SCENARIOS !== "1") { - errors.push("channels-add-remove-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); + errors.push( + "channels-add-remove-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1", + ); } if ( - jobEnv.E2E_ARTIFACT_DIR !== "${{ github.workspace }}/e2e-artifacts/vitest/channels-add-remove" + jobEnv.E2E_ARTIFACT_DIR !== + "${{ github.workspace }}/e2e-artifacts/vitest/channels-add-remove" ) { errors.push( "channels-add-remove-vitest job must write artifacts under e2e-artifacts/vitest/channels-add-remove", ); } if (jobEnv.NEMOCLAW_CLI_BIN !== "${{ github.workspace }}/bin/nemoclaw.js") { - errors.push("channels-add-remove-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI"); + errors.push( + "channels-add-remove-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI", + ); } if (jobEnv.NEMOCLAW_SANDBOX_NAME !== "e2e-channels-add-remove") { errors.push( @@ -4105,13 +5755,19 @@ function validateChannelsAddRemoveVitestJob(errors: string[], jobs: WorkflowReco ); } if (jobEnv.NEMOCLAW_NON_INTERACTIVE !== "1") { - errors.push("channels-add-remove-vitest job must set NEMOCLAW_NON_INTERACTIVE=1"); + errors.push( + "channels-add-remove-vitest job must set NEMOCLAW_NON_INTERACTIVE=1", + ); } if (jobEnv.NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE !== "1") { - errors.push("channels-add-remove-vitest job must set NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1"); + errors.push( + "channels-add-remove-vitest job must set NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1", + ); } if (jobEnv.OPENSHELL_GATEWAY !== "nemoclaw") { - errors.push("channels-add-remove-vitest job must force OPENSHELL_GATEWAY=nemoclaw"); + errors.push( + "channels-add-remove-vitest job must force OPENSHELL_GATEWAY=nemoclaw", + ); } for (const secret of [ "NVIDIA_API_KEY", @@ -4119,7 +5775,12 @@ function validateChannelsAddRemoveVitestJob(errors: string[], jobs: WorkflowReco "DOCKERHUB_TOKEN", "GITHUB_TOKEN", ]) { - requireEnvDoesNotExposeSecret(errors, "channels-add-remove-vitest job", jobEnv, secret); + requireEnvDoesNotExposeSecret( + errors, + "channels-add-remove-vitest job", + jobEnv, + secret, + ); } const steps = asSteps(job.steps); @@ -4128,24 +5789,49 @@ function validateChannelsAddRemoveVitestJob(errors: string[], jobs: WorkflowReco const stepName = `channels-add-remove-vitest step '${step.name ?? step.uses ?? ""}'`; const stepEnv = asRecord(step.env); if (step.name !== "Run channels add/remove live test") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_API_KEY"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "NVIDIA_API_KEY", + ); } if (step.name !== "Authenticate to Docker Hub") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_USERNAME"); - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_TOKEN"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_USERNAME", + ); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_TOKEN", + ); requireNoDockerHubAuthInRun(errors, stepName, stringValue(step.run)); } requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "GITHUB_TOKEN"); } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); - if (!checkout) errors.push("channels-add-remove-vitest job missing checkout step"); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); + if (!checkout) + errors.push("channels-add-remove-vitest job missing checkout step"); requireFullShaAction(errors, checkout, "channels-add-remove-vitest checkout"); if (asRecord(checkout?.with)["persist-credentials"] !== false) { - errors.push("channels-add-remove-vitest checkout step must set persist-credentials=false"); + errors.push( + "channels-add-remove-vitest checkout step must set persist-credentials=false", + ); } - const dockerHubAuth = requireJobStep(errors, jobName, steps, "Authenticate to Docker Hub"); + const dockerHubAuth = requireJobStep( + errors, + jobName, + steps, + "Authenticate to Docker Hub", + ); const dockerHubEnv = asRecord(dockerHubAuth?.env); if (dockerHubEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}") { errors.push( @@ -4161,8 +5847,13 @@ function validateChannelsAddRemoveVitestJob(errors: string[], jobs: WorkflowReco requireRunContains(errors, dockerHubAuth, "continuing with anonymous pulls"); const setupNode = namedStep(steps, "Set up Node"); - if (!setupNode) errors.push("channels-add-remove-vitest job missing step: Set up Node"); - requireFullShaAction(errors, setupNode, "channels-add-remove-vitest setup-node"); + if (!setupNode) + errors.push("channels-add-remove-vitest job missing step: Set up Node"); + requireFullShaAction( + errors, + setupNode, + "channels-add-remove-vitest setup-node", + ); const installRootDependencies = requireJobStep( errors, @@ -4170,57 +5861,118 @@ function validateChannelsAddRemoveVitestJob(errors: string[], jobs: WorkflowReco steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); const buildCli = requireJobStep(errors, jobName, steps, "Build CLI"); requireRunContains(errors, buildCli, "npm run build:cli"); - const installOpenShell = requireJobStep(errors, jobName, steps, "Install OpenShell"); - requireRunContains(errors, installOpenShell, "bash scripts/install-openshell.sh"); + const installOpenShell = requireJobStep( + errors, + jobName, + steps, + "Install OpenShell", + ); + requireRunContains( + errors, + installOpenShell, + "bash scripts/install-openshell.sh", + ); requireRunContains(errors, installOpenShell, "env -u DOCKER_CONFIG"); requireRunContains(errors, installOpenShell, "-u DOCKERHUB_USERNAME"); requireRunContains(errors, installOpenShell, "-u DOCKERHUB_TOKEN"); requireRunContains(errors, installOpenShell, "-u NVIDIA_API_KEY"); requireRunContains(errors, installOpenShell, "-u GITHUB_TOKEN"); - const runVitest = requireJobStep(errors, jobName, steps, "Run channels add/remove live test"); - const runVitestEnv = asRecord(runVitest?.env); - requireHostedInferenceCredentialEnv(errors, "channels-add-remove-vitest step", runVitestEnv, { + const runVitest = requireJobStep( + errors, jobName, - }); - if (runVitestEnv.TELEGRAM_BOT_TOKEN !== "test-fake-telegram-token-add-remove-e2e") { - errors.push("channels-add-remove-vitest step must set the fake Telegram token"); + steps, + "Run channels add/remove live test", + ); + const runVitestEnv = asRecord(runVitest?.env); + if (runVitestEnv.NVIDIA_API_KEY !== "${{ secrets.NVIDIA_API_KEY }}") { + errors.push( + "channels-add-remove-vitest step must receive NVIDIA_API_KEY from secrets", + ); + } + if ( + runVitestEnv.TELEGRAM_BOT_TOKEN !== + "test-fake-telegram-token-add-remove-e2e" + ) { + errors.push( + "channels-add-remove-vitest step must set the fake Telegram token", + ); } if (runVitestEnv.TELEGRAM_ALLOWED_IDS !== "123456789") { - errors.push("channels-add-remove-vitest step must set TELEGRAM_ALLOWED_IDS"); + errors.push( + "channels-add-remove-vitest step must set TELEGRAM_ALLOWED_IDS", + ); } if (runVitestEnv.TELEGRAM_REQUIRE_MENTION !== "0") { - errors.push("channels-add-remove-vitest step must set TELEGRAM_REQUIRE_MENTION"); + errors.push( + "channels-add-remove-vitest step must set TELEGRAM_REQUIRE_MENTION", + ); } requireRunContains(errors, runVitest, "OPENSHELL_BIN"); - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/channels-add-remove.test.ts"); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/channels-add-remove.test.ts", + ); - const upload = requireJobStep(errors, jobName, steps, "Upload channels add/remove artifacts"); - requireFullShaAction(errors, upload, "channels-add-remove-vitest upload-artifact"); + const upload = requireJobStep( + errors, + jobName, + steps, + "Upload channels add/remove artifacts", + ); + requireFullShaAction( + errors, + upload, + "channels-add-remove-vitest upload-artifact", + ); const uploadWith = asRecord(upload?.with); if (uploadWith.name !== "e2e-vitest-scenarios-channels-add-remove") { - errors.push("channels-add-remove-vitest artifact upload name must be stable"); + errors.push( + "channels-add-remove-vitest artifact upload name must be stable", + ); } const uploadPath = stringValue(uploadWith.path); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/channels-add-remove/"); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/channels-add-remove/", + ); if (uploadWith["include-hidden-files"] !== false) { - errors.push("channels-add-remove-vitest artifact upload must set include-hidden-files: false"); + errors.push( + "channels-add-remove-vitest artifact upload must set include-hidden-files: false", + ); } if (uploadWith["if-no-files-found"] !== "ignore") { - errors.push("channels-add-remove-vitest artifact upload must ignore missing fixture artifacts"); + errors.push( + "channels-add-remove-vitest artifact upload must ignore missing fixture artifacts", + ); } if (uploadWith["retention-days"] !== 14) { - errors.push("channels-add-remove-vitest artifact upload retention-days must be 14"); + errors.push( + "channels-add-remove-vitest artifact upload retention-days must be 14", + ); } } -function validateOpenClawDiscordPairingVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateOpenClawDiscordPairingVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "openclaw-discord-pairing-vitest"; const scenarioName = "openclaw-discord-pairing"; const job = asRecord(jobs[jobName]); @@ -4230,19 +5982,34 @@ function validateOpenClawDiscordPairingVitestJob(errors: string[], jobs: Workflo } if (job["runs-on"] !== "ubuntu-latest") { - errors.push("openclaw-discord-pairing-vitest job must run on ubuntu-latest"); + errors.push( + "openclaw-discord-pairing-vitest job must run on ubuntu-latest", + ); } if (job["timeout-minutes"] !== 60) { - errors.push("openclaw-discord-pairing-vitest job must keep the 60 minute timeout"); + errors.push( + "openclaw-discord-pairing-vitest job must keep the 60 minute timeout", + ); } validateFreeStandingJobSelector(errors, jobs, jobName, scenarioName); const jobEnv = asRecord(job.env); if ("DOCKER_CONFIG" in jobEnv) { - errors.push("openclaw-discord-pairing-vitest job must not set DOCKER_CONFIG at job level"); + errors.push( + "openclaw-discord-pairing-vitest job must not set DOCKER_CONFIG at job level", + ); } - for (const secret of ["NVIDIA_INFERENCE_API_KEY", "NVIDIA_API_KEY", ...COMMON_SECRET_ENV_NAMES]) { - requireEnvDoesNotExposeSecret(errors, "openclaw-discord-pairing-vitest job", jobEnv, secret); + for (const secret of [ + "NVIDIA_INFERENCE_API_KEY", + "NVIDIA_API_KEY", + ...COMMON_SECRET_ENV_NAMES, + ]) { + requireEnvDoesNotExposeSecret( + errors, + "openclaw-discord-pairing-vitest job", + jobEnv, + secret, + ); } const steps = asSteps(job.steps); @@ -4251,27 +6018,58 @@ function validateOpenClawDiscordPairingVitestJob(errors: string[], jobs: Workflo const stepName = `openclaw-discord-pairing-vitest step '${step.name ?? step.uses ?? ""}'`; const stepEnv = asRecord(step.env); if (step.name !== "Run OpenClaw Discord pairing live test") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_INFERENCE_API_KEY"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "NVIDIA_INFERENCE_API_KEY", + ); } requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_API_KEY"); if (step.name !== "Authenticate to Docker Hub") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_USERNAME"); - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_TOKEN"); - requireNoDockerHubAuthInRun(errors, stepName, stringValue(step.run)); - } + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_USERNAME", + ); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_TOKEN", + ); + requireNoDockerHubAuthInRun(errors, stepName, stringValue(step.run)); + } requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "GITHUB_TOKEN"); } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); - if (!checkout) errors.push("openclaw-discord-pairing-vitest job missing checkout step"); - requireFullShaAction(errors, checkout, "openclaw-discord-pairing-vitest checkout"); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); + if (!checkout) + errors.push("openclaw-discord-pairing-vitest job missing checkout step"); + requireFullShaAction( + errors, + checkout, + "openclaw-discord-pairing-vitest checkout", + ); if (asRecord(checkout?.with)["persist-credentials"] !== false) { - errors.push("openclaw-discord-pairing-vitest checkout step must set persist-credentials=false"); + errors.push( + "openclaw-discord-pairing-vitest checkout step must set persist-credentials=false", + ); } const setupNode = namedStep(steps, "Set up Node"); - if (!setupNode) errors.push("openclaw-discord-pairing-vitest job missing step: Set up Node"); - requireFullShaAction(errors, setupNode, "openclaw-discord-pairing-vitest setup-node"); + if (!setupNode) + errors.push( + "openclaw-discord-pairing-vitest job missing step: Set up Node", + ); + requireFullShaAction( + errors, + setupNode, + "openclaw-discord-pairing-vitest setup-node", + ); const installRootDependencies = requireJobStep( errors, @@ -4279,7 +6077,11 @@ function validateOpenClawDiscordPairingVitestJob(errors: string[], jobs: Workflo steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); const buildCli = requireJobStep(errors, jobName, steps, "Build CLI"); requireRunContains(errors, buildCli, "npm run build:cli"); @@ -4296,11 +6098,22 @@ function validateOpenClawDiscordPairingVitestJob(errors: string[], jobs: Workflo 'echo "DOCKER_CONFIG=${RUNNER_TEMP}/docker-config-openclaw-discord-pairing" >> "$GITHUB_ENV"', ); requireRunDoesNotContain(errors, configureDockerAuth, "${{ runner.temp }}"); - requireRunDoesNotContain(errors, configureDockerAuth, "${{ github.workspace }}"); + requireRunDoesNotContain( + errors, + configureDockerAuth, + "${{ github.workspace }}", + ); - const dockerLogin = requireJobStep(errors, jobName, steps, "Authenticate to Docker Hub"); + const dockerLogin = requireJobStep( + errors, + jobName, + steps, + "Authenticate to Docker Hub", + ); const dockerLoginEnv = asRecord(dockerLogin?.env); - if (dockerLoginEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}") { + if ( + dockerLoginEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}" + ) { errors.push( "openclaw-discord-pairing-vitest Docker Hub auth must receive DOCKERHUB_USERNAME from secrets", ); @@ -4315,8 +6128,17 @@ function validateOpenClawDiscordPairingVitestJob(errors: string[], jobs: Workflo requireRunContains(errors, dockerLogin, "docker login docker.io"); requireRunContains(errors, dockerLogin, "--password-stdin"); - const installOpenShell = requireJobStep(errors, jobName, steps, "Install OpenShell CLI"); - requireRunContains(errors, installOpenShell, "bash scripts/install-openshell.sh"); + const installOpenShell = requireJobStep( + errors, + jobName, + steps, + "Install OpenShell CLI", + ); + requireRunContains( + errors, + installOpenShell, + "bash scripts/install-openshell.sh", + ); requireRunContains(errors, installOpenShell, "env -u DOCKER_CONFIG"); requireRunContains(errors, installOpenShell, "-u DOCKERHUB_USERNAME"); requireRunContains(errors, installOpenShell, "-u DOCKERHUB_TOKEN"); @@ -4331,17 +6153,29 @@ function validateOpenClawDiscordPairingVitestJob(errors: string[], jobs: Workflo "Run OpenClaw Discord pairing live test", ); const runVitestEnv = asRecord(runVitest?.env); - requireHostedInferenceCredentialEnv( - errors, - "openclaw-discord-pairing-vitest step", - runVitestEnv, - { jobName }, - ); + if ( + runVitestEnv.NVIDIA_INFERENCE_API_KEY !== + "${{ secrets.NVIDIA_INFERENCE_API_KEY }}" + ) { + errors.push( + "openclaw-discord-pairing-vitest step must receive NVIDIA_INFERENCE_API_KEY from secrets", + ); + } if (runVitestEnv.DISCORD_BOT_TOKEN !== "test-fake-discord-pairing-e2e") { - errors.push("openclaw-discord-pairing-vitest step must use fake Discord token"); + errors.push( + "openclaw-discord-pairing-vitest step must use fake Discord token", + ); } - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/openclaw-discord-pairing.test.ts"); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/openclaw-discord-pairing.test.ts", + ); const upload = requireJobStep( errors, @@ -4349,10 +6183,18 @@ function validateOpenClawDiscordPairingVitestJob(errors: string[], jobs: Workflo steps, "Upload OpenClaw Discord pairing artifacts", ); - requireFullShaAction(errors, upload, "openclaw-discord-pairing-vitest upload-artifact"); + requireFullShaAction( + errors, + upload, + "openclaw-discord-pairing-vitest upload-artifact", + ); const uploadWith = asRecord(upload?.with); const uploadPath = stringValue(uploadWith.path); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/openclaw-discord-pairing/"); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/openclaw-discord-pairing/", + ); if (uploadWith["include-hidden-files"] !== false) { errors.push( "openclaw-discord-pairing-vitest artifact upload must set include-hidden-files: false", @@ -4364,18 +6206,30 @@ function validateOpenClawDiscordPairingVitestJob(errors: string[], jobs: Workflo ); } if (uploadWith["retention-days"] !== 14) { - errors.push("openclaw-discord-pairing-vitest artifact upload retention-days must be 14"); + errors.push( + "openclaw-discord-pairing-vitest artifact upload retention-days must be 14", + ); } - const cleanup = requireJobStep(errors, jobName, steps, "Clean up Docker auth"); + const cleanup = requireJobStep( + errors, + jobName, + steps, + "Clean up Docker auth", + ); if (cleanup?.if !== "always()") { - errors.push("openclaw-discord-pairing-vitest Docker auth cleanup must always run"); + errors.push( + "openclaw-discord-pairing-vitest Docker auth cleanup must always run", + ); } requireRunContains(errors, cleanup, "docker logout docker.io"); requireRunContains(errors, cleanup, 'rm -rf "${DOCKER_CONFIG}"'); } -function validateOpenClawSlackPairingVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateOpenClawSlackPairingVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "openclaw-slack-pairing-vitest"; const scenarioName = "openclaw-slack-pairing"; const job = asRecord(jobs[jobName]); @@ -4388,16 +6242,29 @@ function validateOpenClawSlackPairingVitestJob(errors: string[], jobs: WorkflowR errors.push("openclaw-slack-pairing-vitest job must run on ubuntu-latest"); } if (job["timeout-minutes"] !== 60) { - errors.push("openclaw-slack-pairing-vitest job must keep the 60 minute timeout"); + errors.push( + "openclaw-slack-pairing-vitest job must keep the 60 minute timeout", + ); } validateFreeStandingJobSelector(errors, jobs, jobName, scenarioName); const jobEnv = asRecord(job.env); if ("DOCKER_CONFIG" in jobEnv) { - errors.push("openclaw-slack-pairing-vitest job must not set DOCKER_CONFIG at job level"); + errors.push( + "openclaw-slack-pairing-vitest job must not set DOCKER_CONFIG at job level", + ); } - for (const secret of ["NVIDIA_INFERENCE_API_KEY", "NVIDIA_API_KEY", ...COMMON_SECRET_ENV_NAMES]) { - requireEnvDoesNotExposeSecret(errors, "openclaw-slack-pairing-vitest job", jobEnv, secret); + for (const secret of [ + "NVIDIA_INFERENCE_API_KEY", + "NVIDIA_API_KEY", + ...COMMON_SECRET_ENV_NAMES, + ]) { + requireEnvDoesNotExposeSecret( + errors, + "openclaw-slack-pairing-vitest job", + jobEnv, + secret, + ); } const steps = asSteps(job.steps); @@ -4406,27 +6273,56 @@ function validateOpenClawSlackPairingVitestJob(errors: string[], jobs: WorkflowR const stepName = `openclaw-slack-pairing-vitest step '${step.name ?? step.uses ?? ""}'`; const stepEnv = asRecord(step.env); if (step.name !== "Run OpenClaw Slack pairing live test") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_INFERENCE_API_KEY"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "NVIDIA_INFERENCE_API_KEY", + ); } requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_API_KEY"); if (step.name !== "Authenticate to Docker Hub") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_USERNAME"); - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_TOKEN"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_USERNAME", + ); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_TOKEN", + ); requireNoDockerHubAuthInRun(errors, stepName, stringValue(step.run)); } requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "GITHUB_TOKEN"); } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); - if (!checkout) errors.push("openclaw-slack-pairing-vitest job missing checkout step"); - requireFullShaAction(errors, checkout, "openclaw-slack-pairing-vitest checkout"); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); + if (!checkout) + errors.push("openclaw-slack-pairing-vitest job missing checkout step"); + requireFullShaAction( + errors, + checkout, + "openclaw-slack-pairing-vitest checkout", + ); if (asRecord(checkout?.with)["persist-credentials"] !== false) { - errors.push("openclaw-slack-pairing-vitest checkout step must set persist-credentials=false"); + errors.push( + "openclaw-slack-pairing-vitest checkout step must set persist-credentials=false", + ); } const setupNode = namedStep(steps, "Set up Node"); - if (!setupNode) errors.push("openclaw-slack-pairing-vitest job missing step: Set up Node"); - requireFullShaAction(errors, setupNode, "openclaw-slack-pairing-vitest setup-node"); + if (!setupNode) + errors.push("openclaw-slack-pairing-vitest job missing step: Set up Node"); + requireFullShaAction( + errors, + setupNode, + "openclaw-slack-pairing-vitest setup-node", + ); const installRootDependencies = requireJobStep( errors, @@ -4434,7 +6330,11 @@ function validateOpenClawSlackPairingVitestJob(errors: string[], jobs: WorkflowR steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); const buildCli = requireJobStep(errors, jobName, steps, "Build CLI"); requireRunContains(errors, buildCli, "npm run build:cli"); @@ -4451,11 +6351,22 @@ function validateOpenClawSlackPairingVitestJob(errors: string[], jobs: WorkflowR 'echo "DOCKER_CONFIG=${RUNNER_TEMP}/docker-config-openclaw-slack-pairing" >> "$GITHUB_ENV"', ); requireRunDoesNotContain(errors, configureDockerAuth, "${{ runner.temp }}"); - requireRunDoesNotContain(errors, configureDockerAuth, "${{ github.workspace }}"); + requireRunDoesNotContain( + errors, + configureDockerAuth, + "${{ github.workspace }}", + ); - const dockerLogin = requireJobStep(errors, jobName, steps, "Authenticate to Docker Hub"); + const dockerLogin = requireJobStep( + errors, + jobName, + steps, + "Authenticate to Docker Hub", + ); const dockerLoginEnv = asRecord(dockerLogin?.env); - if (dockerLoginEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}") { + if ( + dockerLoginEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}" + ) { errors.push( "openclaw-slack-pairing-vitest Docker Hub auth must receive DOCKERHUB_USERNAME from secrets", ); @@ -4470,8 +6381,17 @@ function validateOpenClawSlackPairingVitestJob(errors: string[], jobs: WorkflowR requireRunContains(errors, dockerLogin, "docker login docker.io"); requireRunContains(errors, dockerLogin, "--password-stdin"); - const installOpenShell = requireJobStep(errors, jobName, steps, "Install OpenShell CLI"); - requireRunContains(errors, installOpenShell, "bash scripts/install-openshell.sh"); + const installOpenShell = requireJobStep( + errors, + jobName, + steps, + "Install OpenShell CLI", + ); + requireRunContains( + errors, + installOpenShell, + "bash scripts/install-openshell.sh", + ); requireRunContains(errors, installOpenShell, "env -u DOCKER_CONFIG"); requireRunContains(errors, installOpenShell, "-u DOCKERHUB_USERNAME"); requireRunContains(errors, installOpenShell, "-u DOCKERHUB_TOKEN"); @@ -4479,25 +6399,60 @@ function validateOpenClawSlackPairingVitestJob(errors: string[], jobs: WorkflowR requireRunContains(errors, installOpenShell, "-u NVIDIA_INFERENCE_API_KEY"); requireRunContains(errors, installOpenShell, "-u GITHUB_TOKEN"); - const runVitest = requireJobStep(errors, jobName, steps, "Run OpenClaw Slack pairing live test"); - const runVitestEnv = asRecord(runVitest?.env); - requireHostedInferenceCredentialEnv(errors, "openclaw-slack-pairing-vitest step", runVitestEnv, { + const runVitest = requireJobStep( + errors, jobName, - }); + steps, + "Run OpenClaw Slack pairing live test", + ); + const runVitestEnv = asRecord(runVitest?.env); + if ( + runVitestEnv.NVIDIA_INFERENCE_API_KEY !== + "${{ secrets.NVIDIA_INFERENCE_API_KEY }}" + ) { + errors.push( + "openclaw-slack-pairing-vitest step must receive NVIDIA_INFERENCE_API_KEY from secrets", + ); + } if (runVitestEnv.SLACK_BOT_TOKEN !== "xoxb-fake-slack-pairing-e2e") { - errors.push("openclaw-slack-pairing-vitest step must use fake Slack bot token"); + errors.push( + "openclaw-slack-pairing-vitest step must use fake Slack bot token", + ); } if (runVitestEnv.SLACK_APP_TOKEN !== "xapp-fake-slack-pairing-e2e") { - errors.push("openclaw-slack-pairing-vitest step must use fake Slack app token"); + errors.push( + "openclaw-slack-pairing-vitest step must use fake Slack app token", + ); } - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/openclaw-slack-pairing.test.ts"); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/openclaw-slack-pairing.test.ts", + ); - const upload = requireJobStep(errors, jobName, steps, "Upload OpenClaw Slack pairing artifacts"); - requireFullShaAction(errors, upload, "openclaw-slack-pairing-vitest upload-artifact"); + const upload = requireJobStep( + errors, + jobName, + steps, + "Upload OpenClaw Slack pairing artifacts", + ); + requireFullShaAction( + errors, + upload, + "openclaw-slack-pairing-vitest upload-artifact", + ); const uploadWith = asRecord(upload?.with); const uploadPath = stringValue(uploadWith.path); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/openclaw-slack-pairing/"); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/openclaw-slack-pairing/", + ); if (uploadWith["include-hidden-files"] !== false) { errors.push( "openclaw-slack-pairing-vitest artifact upload must set include-hidden-files: false", @@ -4509,18 +6464,30 @@ function validateOpenClawSlackPairingVitestJob(errors: string[], jobs: WorkflowR ); } if (uploadWith["retention-days"] !== 14) { - errors.push("openclaw-slack-pairing-vitest artifact upload retention-days must be 14"); + errors.push( + "openclaw-slack-pairing-vitest artifact upload retention-days must be 14", + ); } - const cleanup = requireJobStep(errors, jobName, steps, "Clean up Docker auth"); + const cleanup = requireJobStep( + errors, + jobName, + steps, + "Clean up Docker auth", + ); if (cleanup?.if !== "always()") { - errors.push("openclaw-slack-pairing-vitest Docker auth cleanup must always run"); + errors.push( + "openclaw-slack-pairing-vitest Docker auth cleanup must always run", + ); } requireRunContains(errors, cleanup, "docker logout docker.io"); requireRunContains(errors, cleanup, 'rm -rf "${DOCKER_CONFIG}"'); } -function validateChannelsStopStartVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateChannelsStopStartVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "channels-stop-start-vitest"; const scenarioName = "channels-stop-start"; const job = asRecord(jobs[jobName]); @@ -4534,20 +6501,29 @@ function validateChannelsStopStartVitestJob(errors: string[], jobs: WorkflowReco } validateFreeStandingJobSelector(errors, jobs, jobName, scenarioName); if (job["timeout-minutes"] !== 90) { - errors.push("channels-stop-start-vitest job must keep the 90 minute timeout"); + errors.push( + "channels-stop-start-vitest job must keep the 90 minute timeout", + ); } const strategy = asRecord(job.strategy); if (strategy["fail-fast"] !== false) { errors.push("channels-stop-start-vitest strategy.fail-fast must be false"); } const matrix = asRecord(strategy.matrix); - if (!Array.isArray(matrix.agent) || matrix.agent.join(",") !== "openclaw,hermes") { - errors.push("channels-stop-start-vitest matrix.agent must be openclaw,hermes"); + if ( + !Array.isArray(matrix.agent) || + matrix.agent.join(",") !== "openclaw,hermes" + ) { + errors.push( + "channels-stop-start-vitest matrix.agent must be openclaw,hermes", + ); } const jobEnv = asRecord(job.env); if (jobEnv.NEMOCLAW_RUN_E2E_SCENARIOS !== "1") { - errors.push("channels-stop-start-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); + errors.push( + "channels-stop-start-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1", + ); } if ( jobEnv.E2E_ARTIFACT_DIR !== @@ -4558,15 +6534,22 @@ function validateChannelsStopStartVitestJob(errors: string[], jobs: WorkflowReco ); } if (jobEnv.NEMOCLAW_CLI_BIN !== "${{ github.workspace }}/bin/nemoclaw.js") { - errors.push("channels-stop-start-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI"); + errors.push( + "channels-stop-start-vitest job must point NEMOCLAW_CLI_BIN at the repo CLI", + ); } - if (jobEnv.NEMOCLAW_SANDBOX_NAME !== "e2e-channels-stop-start-${{ matrix.agent }}") { + if ( + jobEnv.NEMOCLAW_SANDBOX_NAME !== + "e2e-channels-stop-start-${{ matrix.agent }}" + ) { errors.push( "channels-stop-start-vitest job must derive NEMOCLAW_SANDBOX_NAME from matrix.agent with the e2e-channels-stop-start- prefix", ); } if (jobEnv.NEMOCLAW_AGENT !== "${{ matrix.agent }}") { - errors.push("channels-stop-start-vitest job must pass matrix.agent through NEMOCLAW_AGENT"); + errors.push( + "channels-stop-start-vitest job must pass matrix.agent through NEMOCLAW_AGENT", + ); } if (jobEnv.NEMOCLAW_CHANNELS_STOP_START_AGENT !== "${{ matrix.agent }}") { errors.push( @@ -4574,19 +6557,27 @@ function validateChannelsStopStartVitestJob(errors: string[], jobs: WorkflowReco ); } if (jobEnv.NEMOCLAW_NON_INTERACTIVE !== "1") { - errors.push("channels-stop-start-vitest job must set NEMOCLAW_NON_INTERACTIVE=1"); + errors.push( + "channels-stop-start-vitest job must set NEMOCLAW_NON_INTERACTIVE=1", + ); } if (jobEnv.NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE !== "1") { - errors.push("channels-stop-start-vitest job must set NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1"); + errors.push( + "channels-stop-start-vitest job must set NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1", + ); } if (jobEnv.OPENSHELL_GATEWAY !== "nemoclaw") { - errors.push("channels-stop-start-vitest job must force OPENSHELL_GATEWAY=nemoclaw"); + errors.push( + "channels-stop-start-vitest job must force OPENSHELL_GATEWAY=nemoclaw", + ); } if ( jobEnv.DOCKER_CONFIG !== "${{ github.workspace }}/.docker-config-channels-stop-start-${{ matrix.agent }}" ) { - errors.push("channels-stop-start-vitest job must isolate Docker auth by matrix agent"); + errors.push( + "channels-stop-start-vitest job must isolate Docker auth by matrix agent", + ); } for (const secret of [ "NVIDIA_INFERENCE_API_KEY", @@ -4595,7 +6586,12 @@ function validateChannelsStopStartVitestJob(errors: string[], jobs: WorkflowReco "DOCKERHUB_TOKEN", "GITHUB_TOKEN", ]) { - requireEnvDoesNotExposeSecret(errors, "channels-stop-start-vitest job", jobEnv, secret); + requireEnvDoesNotExposeSecret( + errors, + "channels-stop-start-vitest job", + jobEnv, + secret, + ); } const steps = asSteps(job.steps); @@ -4604,25 +6600,55 @@ function validateChannelsStopStartVitestJob(errors: string[], jobs: WorkflowReco const stepName = `channels-stop-start-vitest step '${step.name ?? step.uses ?? ""}'`; const stepEnv = asRecord(step.env); if (step.name !== "Run channels stop/start live test") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_INFERENCE_API_KEY"); - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_API_KEY"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "NVIDIA_INFERENCE_API_KEY", + ); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "NVIDIA_API_KEY", + ); } if (step.name !== "Authenticate to Docker Hub") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_USERNAME"); - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_TOKEN"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_USERNAME", + ); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_TOKEN", + ); requireNoDockerHubAuthInRun(errors, stepName, stringValue(step.run)); } requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "GITHUB_TOKEN"); } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); - if (!checkout) errors.push("channels-stop-start-vitest job missing checkout step"); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); + if (!checkout) + errors.push("channels-stop-start-vitest job missing checkout step"); requireFullShaAction(errors, checkout, "channels-stop-start-vitest checkout"); if (asRecord(checkout?.with)["persist-credentials"] !== false) { - errors.push("channels-stop-start-vitest checkout step must set persist-credentials=false"); + errors.push( + "channels-stop-start-vitest checkout step must set persist-credentials=false", + ); } - const dockerHubAuth = requireJobStep(errors, jobName, steps, "Authenticate to Docker Hub"); + const dockerHubAuth = requireJobStep( + errors, + jobName, + steps, + "Authenticate to Docker Hub", + ); const dockerHubEnv = asRecord(dockerHubAuth?.env); if (dockerHubEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}") { errors.push( @@ -4641,8 +6667,13 @@ function validateChannelsStopStartVitestJob(errors: string[], jobs: WorkflowReco requireRunContains(errors, dockerHubAuth, "continuing with anonymous pulls"); const setupNode = namedStep(steps, "Set up Node"); - if (!setupNode) errors.push("channels-stop-start-vitest job missing step: Set up Node"); - requireFullShaAction(errors, setupNode, "channels-stop-start-vitest setup-node"); + if (!setupNode) + errors.push("channels-stop-start-vitest job missing step: Set up Node"); + requireFullShaAction( + errors, + setupNode, + "channels-stop-start-vitest setup-node", + ); const installRootDependencies = requireJobStep( errors, @@ -4650,20 +6681,38 @@ function validateChannelsStopStartVitestJob(errors: string[], jobs: WorkflowReco steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); const buildCli = requireJobStep(errors, jobName, steps, "Build CLI"); requireRunContains(errors, buildCli, "npm run build:cli"); - const installOpenShell = requireJobStep(errors, jobName, steps, "Install OpenShell"); - requireRunContains(errors, installOpenShell, "bash scripts/install-openshell.sh"); + const installOpenShell = requireJobStep( + errors, + jobName, + steps, + "Install OpenShell", + ); + requireRunContains( + errors, + installOpenShell, + "bash scripts/install-openshell.sh", + ); requireRunContains(errors, installOpenShell, "env -u DOCKER_CONFIG"); requireRunContains(errors, installOpenShell, "-u DOCKERHUB_USERNAME"); requireRunContains(errors, installOpenShell, "-u DOCKERHUB_TOKEN"); requireRunContains(errors, installOpenShell, "-u NVIDIA_API_KEY"); requireRunContains(errors, installOpenShell, "-u GITHUB_TOKEN"); - const runVitest = requireJobStep(errors, jobName, steps, "Run channels stop/start live test"); + const runVitest = requireJobStep( + errors, + jobName, + steps, + "Run channels stop/start live test", + ); const runVitestEnv = asRecord(runVitest?.env); requireEnvDoesNotExposeSecret( errors, @@ -4671,35 +6720,85 @@ function validateChannelsStopStartVitestJob(errors: string[], jobs: WorkflowReco runVitestEnv, "NVIDIA_API_KEY", ); - requireHostedInferenceCredentialEnv(errors, "channels-stop-start-vitest step", runVitestEnv, { - jobName, - }); if ( - runVitestEnv.TELEGRAM_BOT_TOKEN !== "test-fake-telegram-token-stop-start-${{ matrix.agent }}" + runVitestEnv.NVIDIA_INFERENCE_API_KEY !== + "${{ secrets.NVIDIA_INFERENCE_API_KEY }}" ) { - errors.push("channels-stop-start-vitest step must set the fake Telegram token"); + errors.push( + "channels-stop-start-vitest step must receive NVIDIA_INFERENCE_API_KEY from secrets", + ); } - if (runVitestEnv.DISCORD_BOT_TOKEN !== "test-fake-discord-token-stop-start-${{ matrix.agent }}") { - errors.push("channels-stop-start-vitest step must set the fake Discord token"); + if ( + runVitestEnv.TELEGRAM_BOT_TOKEN !== + "test-fake-telegram-token-stop-start-${{ matrix.agent }}" + ) { + errors.push( + "channels-stop-start-vitest step must set the fake Telegram token", + ); } - if (runVitestEnv.SLACK_BOT_TOKEN !== "xoxb-fake-slack-token-stop-start-${{ matrix.agent }}") { - errors.push("channels-stop-start-vitest step must set the fake Slack bot token"); + if ( + runVitestEnv.DISCORD_BOT_TOKEN !== + "test-fake-discord-token-stop-start-${{ matrix.agent }}" + ) { + errors.push( + "channels-stop-start-vitest step must set the fake Discord token", + ); } - if (runVitestEnv.SLACK_APP_TOKEN !== "xapp-fake-slack-token-stop-start-${{ matrix.agent }}") { - errors.push("channels-stop-start-vitest step must set the fake Slack app token"); + if ( + runVitestEnv.SLACK_BOT_TOKEN !== + "xoxb-fake-slack-token-stop-start-${{ matrix.agent }}" + ) { + errors.push( + "channels-stop-start-vitest step must set the fake Slack bot token", + ); + } + if ( + runVitestEnv.SLACK_APP_TOKEN !== + "xapp-fake-slack-token-stop-start-${{ matrix.agent }}" + ) { + errors.push( + "channels-stop-start-vitest step must set the fake Slack app token", + ); } - if (runVitestEnv.WECHAT_BOT_TOKEN !== "test-fake-wechat-token-stop-start-${{ matrix.agent }}") { - errors.push("channels-stop-start-vitest step must set the fake WeChat token"); + if ( + runVitestEnv.WECHAT_BOT_TOKEN !== + "test-fake-wechat-token-stop-start-${{ matrix.agent }}" + ) { + errors.push( + "channels-stop-start-vitest step must set the fake WeChat token", + ); } requireRunContains(errors, runVitest, "OPENSHELL_BIN"); - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/channels-stop-start.test.ts"); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/channels-stop-start.test.ts", + ); - const upload = requireJobStep(errors, jobName, steps, "Upload channels stop/start artifacts"); - requireFullShaAction(errors, upload, "channels-stop-start-vitest upload-artifact"); + const upload = requireJobStep( + errors, + jobName, + steps, + "Upload channels stop/start artifacts", + ); + requireFullShaAction( + errors, + upload, + "channels-stop-start-vitest upload-artifact", + ); const uploadWith = asRecord(upload?.with); - if (uploadWith.name !== "e2e-vitest-scenarios-channels-stop-start-${{ matrix.agent }}") { - errors.push("channels-stop-start-vitest artifact upload name must include matrix.agent"); + if ( + uploadWith.name !== + "e2e-vitest-scenarios-channels-stop-start-${{ matrix.agent }}" + ) { + errors.push( + "channels-stop-start-vitest artifact upload name must include matrix.agent", + ); } const uploadPath = stringValue(uploadWith.path); requireUploadPathContains( @@ -4708,24 +6807,40 @@ function validateChannelsStopStartVitestJob(errors: string[], jobs: WorkflowReco "e2e-artifacts/vitest/channels-stop-start/${{ matrix.agent }}/", ); if (uploadWith["include-hidden-files"] !== false) { - errors.push("channels-stop-start-vitest artifact upload must set include-hidden-files: false"); + errors.push( + "channels-stop-start-vitest artifact upload must set include-hidden-files: false", + ); } if (uploadWith["if-no-files-found"] !== "ignore") { - errors.push("channels-stop-start-vitest artifact upload must ignore missing fixture artifacts"); + errors.push( + "channels-stop-start-vitest artifact upload must ignore missing fixture artifacts", + ); } if (uploadWith["retention-days"] !== 14) { - errors.push("channels-stop-start-vitest artifact upload retention-days must be 14"); + errors.push( + "channels-stop-start-vitest artifact upload retention-days must be 14", + ); } - const cleanup = requireJobStep(errors, jobName, steps, "Clean up Docker auth"); + const cleanup = requireJobStep( + errors, + jobName, + steps, + "Clean up Docker auth", + ); if (cleanup?.if !== "always()") { - errors.push("channels-stop-start-vitest Docker auth cleanup must always run"); + errors.push( + "channels-stop-start-vitest Docker auth cleanup must always run", + ); } requireRunContains(errors, cleanup, "docker logout docker.io"); requireRunContains(errors, cleanup, 'rm -rf "${DOCKER_CONFIG}"'); } -function validateTelegramInjectionVitestJob(errors: string[], jobs: WorkflowRecord): void { +function validateTelegramInjectionVitestJob( + errors: string[], + jobs: WorkflowRecord, +): void { const jobName = "telegram-injection-vitest"; const scenarioName = "telegram-injection"; const job = asRecord(jobs[jobName]); @@ -4738,16 +6853,29 @@ function validateTelegramInjectionVitestJob(errors: string[], jobs: WorkflowReco errors.push("telegram-injection-vitest job must run on ubuntu-latest"); } if (job["timeout-minutes"] !== 45) { - errors.push("telegram-injection-vitest job must keep the 45 minute timeout"); + errors.push( + "telegram-injection-vitest job must keep the 45 minute timeout", + ); } validateFreeStandingJobSelector(errors, jobs, jobName, scenarioName); const jobEnv = asRecord(job.env); if ("DOCKER_CONFIG" in jobEnv) { - errors.push("telegram-injection-vitest job must not set DOCKER_CONFIG at job level"); + errors.push( + "telegram-injection-vitest job must not set DOCKER_CONFIG at job level", + ); } - for (const secret of ["NVIDIA_INFERENCE_API_KEY", "NVIDIA_API_KEY", ...COMMON_SECRET_ENV_NAMES]) { - requireEnvDoesNotExposeSecret(errors, "telegram-injection-vitest job", jobEnv, secret); + for (const secret of [ + "NVIDIA_INFERENCE_API_KEY", + "NVIDIA_API_KEY", + ...COMMON_SECRET_ENV_NAMES, + ]) { + requireEnvDoesNotExposeSecret( + errors, + "telegram-injection-vitest job", + jobEnv, + secret, + ); } const steps = asSteps(job.steps); @@ -4756,12 +6884,32 @@ function validateTelegramInjectionVitestJob(errors: string[], jobs: WorkflowReco const stepName = `telegram-injection-vitest step '${step.name ?? step.uses ?? ""}'`; const stepEnv = asRecord(step.env); if (step.name !== "Run Telegram injection live test") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_INFERENCE_API_KEY"); - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_API_KEY"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "NVIDIA_INFERENCE_API_KEY", + ); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "NVIDIA_API_KEY", + ); } if (step.name !== "Authenticate to Docker Hub") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_USERNAME"); - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_TOKEN"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_USERNAME", + ); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_TOKEN", + ); requireNoDockerHubAuthInRun(errors, stepName, stringValue(step.run)); } requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "GITHUB_TOKEN"); @@ -4779,11 +6927,22 @@ function validateTelegramInjectionVitestJob(errors: string[], jobs: WorkflowReco 'echo "DOCKER_CONFIG=${RUNNER_TEMP}/docker-config-telegram-injection" >> "$GITHUB_ENV"', ); requireRunDoesNotContain(errors, configureDockerAuth, "${{ runner.temp }}"); - requireRunDoesNotContain(errors, configureDockerAuth, "${{ github.workspace }}"); + requireRunDoesNotContain( + errors, + configureDockerAuth, + "${{ github.workspace }}", + ); - const dockerLogin = requireJobStep(errors, jobName, steps, "Authenticate to Docker Hub"); + const dockerLogin = requireJobStep( + errors, + jobName, + steps, + "Authenticate to Docker Hub", + ); const dockerLoginEnv = asRecord(dockerLogin?.env); - if (dockerLoginEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}") { + if ( + dockerLoginEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}" + ) { errors.push( "telegram-injection-vitest Docker Hub auth must receive DOCKERHUB_USERNAME from secrets", ); @@ -4798,8 +6957,17 @@ function validateTelegramInjectionVitestJob(errors: string[], jobs: WorkflowReco requireRunContains(errors, dockerLogin, "docker login docker.io"); requireRunContains(errors, dockerLogin, "--password-stdin"); - const installOpenShell = requireJobStep(errors, jobName, steps, "Install OpenShell"); - requireRunContains(errors, installOpenShell, "bash scripts/install-openshell.sh"); + const installOpenShell = requireJobStep( + errors, + jobName, + steps, + "Install OpenShell", + ); + requireRunContains( + errors, + installOpenShell, + "bash scripts/install-openshell.sh", + ); requireRunContains(errors, installOpenShell, "env -u DOCKER_CONFIG"); requireRunContains(errors, installOpenShell, "-u DOCKERHUB_USERNAME"); requireRunContains(errors, installOpenShell, "-u DOCKERHUB_TOKEN"); @@ -4807,31 +6975,71 @@ function validateTelegramInjectionVitestJob(errors: string[], jobs: WorkflowReco requireRunContains(errors, installOpenShell, "-u NVIDIA_INFERENCE_API_KEY"); requireRunContains(errors, installOpenShell, "-u GITHUB_TOKEN"); - const runVitest = requireJobStep(errors, jobName, steps, "Run Telegram injection live test"); - const runVitestEnv = asRecord(runVitest?.env); - requireHostedInferenceCredentialEnv(errors, "telegram-injection-vitest step", runVitestEnv, { + const runVitest = requireJobStep( + errors, jobName, - }); - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/telegram-injection.test.ts"); + steps, + "Run Telegram injection live test", + ); + const runVitestEnv = asRecord(runVitest?.env); + if ( + runVitestEnv.NVIDIA_INFERENCE_API_KEY !== + "${{ secrets.NVIDIA_INFERENCE_API_KEY }}" + ) { + errors.push( + "telegram-injection-vitest step must receive NVIDIA_INFERENCE_API_KEY from secrets", + ); + } + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/telegram-injection.test.ts", + ); - const upload = requireJobStep(errors, jobName, steps, "Upload Telegram injection artifacts"); + const upload = requireJobStep( + errors, + jobName, + steps, + "Upload Telegram injection artifacts", + ); const uploadWith = asRecord(upload?.with); const uploadPath = stringValue(uploadWith.path); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/telegram-injection/"); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/telegram-injection/", + ); if (uploadWith["include-hidden-files"] !== false) { - errors.push("telegram-injection-vitest artifact upload must set include-hidden-files: false"); + errors.push( + "telegram-injection-vitest artifact upload must set include-hidden-files: false", + ); } if (uploadWith["if-no-files-found"] !== "ignore") { - errors.push("telegram-injection-vitest artifact upload must ignore missing fixture artifacts"); + errors.push( + "telegram-injection-vitest artifact upload must ignore missing fixture artifacts", + ); } if (uploadWith["retention-days"] !== 14) { - errors.push("telegram-injection-vitest artifact upload retention-days must be 14"); + errors.push( + "telegram-injection-vitest artifact upload retention-days must be 14", + ); } - const cleanup = requireJobStep(errors, jobName, steps, "Clean up Docker auth"); + const cleanup = requireJobStep( + errors, + jobName, + steps, + "Clean up Docker auth", + ); if (cleanup?.if !== "always()") { - errors.push("telegram-injection-vitest Docker auth cleanup must always run"); + errors.push( + "telegram-injection-vitest Docker auth cleanup must always run", + ); } requireRunContains(errors, cleanup, "docker logout docker.io"); requireRunContains(errors, cleanup, 'rm -rf "${DOCKER_CONFIG}"'); @@ -4845,25 +7053,38 @@ function validateBedrockRuntimeCompatibleAnthropicVitestJob( const scenarioName = "bedrock-runtime-compatible-anthropic"; const job = asRecord(jobs[jobName]); if (Object.keys(job).length === 0) { - errors.push("workflow missing bedrock-runtime-compatible-anthropic-vitest job"); + errors.push( + "workflow missing bedrock-runtime-compatible-anthropic-vitest job", + ); return; } if (job["runs-on"] !== "ubuntu-latest") { - errors.push("bedrock-runtime-compatible-anthropic-vitest job must run on ubuntu-latest"); + errors.push( + "bedrock-runtime-compatible-anthropic-vitest job must run on ubuntu-latest", + ); } if (job["timeout-minutes"] !== 60) { - errors.push("bedrock-runtime-compatible-anthropic-vitest timeout-minutes must be 60"); + errors.push( + "bedrock-runtime-compatible-anthropic-vitest timeout-minutes must be 60", + ); } validateFreeStandingJobSelector(errors, jobs, jobName, scenarioName); const strategy = asRecord(job.strategy); if (strategy["fail-fast"] !== false) { - errors.push("bedrock-runtime-compatible-anthropic-vitest strategy.fail-fast must be false"); + errors.push( + "bedrock-runtime-compatible-anthropic-vitest strategy.fail-fast must be false", + ); } const matrix = asRecord(strategy.matrix); - if (!Array.isArray(matrix.agent) || matrix.agent.join(",") !== "openclaw,hermes") { - errors.push("bedrock-runtime-compatible-anthropic-vitest matrix.agent must be openclaw,hermes"); + if ( + !Array.isArray(matrix.agent) || + matrix.agent.join(",") !== "openclaw,hermes" + ) { + errors.push( + "bedrock-runtime-compatible-anthropic-vitest matrix.agent must be openclaw,hermes", + ); } const jobEnv = asRecord(job.env); @@ -4942,17 +7163,35 @@ function validateBedrockRuntimeCompatibleAnthropicVitestJob( requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "NVIDIA_API_KEY"); requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "GITHUB_TOKEN"); if (step.name !== "Authenticate to Docker Hub") { - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_USERNAME"); - requireEnvDoesNotExposeSecret(errors, stepName, stepEnv, "DOCKERHUB_TOKEN"); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_USERNAME", + ); + requireEnvDoesNotExposeSecret( + errors, + stepName, + stepEnv, + "DOCKERHUB_TOKEN", + ); requireNoDockerHubAuthInRun(errors, stepName, stringValue(step.run)); } } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); if (!checkout) { - errors.push("bedrock-runtime-compatible-anthropic-vitest job missing checkout step"); + errors.push( + "bedrock-runtime-compatible-anthropic-vitest job missing checkout step", + ); } - requireFullShaAction(errors, checkout, "bedrock-runtime-compatible-anthropic-vitest checkout"); + requireFullShaAction( + errors, + checkout, + "bedrock-runtime-compatible-anthropic-vitest checkout", + ); if (asRecord(checkout?.with)["persist-credentials"] !== false) { errors.push( "bedrock-runtime-compatible-anthropic-vitest checkout step must set persist-credentials=false", @@ -4972,9 +7211,16 @@ function validateBedrockRuntimeCompatibleAnthropicVitestJob( ); requireRunDoesNotContain(errors, configureDockerAuth, "${{ runner.temp }}"); - const dockerLogin = requireJobStep(errors, jobName, steps, "Authenticate to Docker Hub"); + const dockerLogin = requireJobStep( + errors, + jobName, + steps, + "Authenticate to Docker Hub", + ); const dockerLoginEnv = asRecord(dockerLogin?.env); - if (dockerLoginEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}") { + if ( + dockerLoginEnv.DOCKERHUB_USERNAME !== "${{ secrets.DOCKERHUB_USERNAME }}" + ) { errors.push( "bedrock-runtime-compatible-anthropic-vitest Docker Hub auth must receive DOCKERHUB_USERNAME from secrets", ); @@ -4992,9 +7238,15 @@ function validateBedrockRuntimeCompatibleAnthropicVitestJob( const setupNode = namedStep(steps, "Set up Node"); if (!setupNode) { - errors.push("bedrock-runtime-compatible-anthropic-vitest job missing step: Set up Node"); + errors.push( + "bedrock-runtime-compatible-anthropic-vitest job missing step: Set up Node", + ); } - requireFullShaAction(errors, setupNode, "bedrock-runtime-compatible-anthropic-vitest setup-node"); + requireFullShaAction( + errors, + setupNode, + "bedrock-runtime-compatible-anthropic-vitest setup-node", + ); const installRootDependencies = requireJobStep( errors, @@ -5002,7 +7254,11 @@ function validateBedrockRuntimeCompatibleAnthropicVitestJob( steps, "Install root dependencies", ); - requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); + requireRunContains( + errors, + installRootDependencies, + "npm ci --ignore-scripts", + ); const buildCli = requireJobStep(errors, jobName, steps, "Build CLI"); requireRunContains(errors, buildCli, "npm run build:cli"); @@ -5013,7 +7269,11 @@ function validateBedrockRuntimeCompatibleAnthropicVitestJob( steps, "Run Bedrock Runtime compatible Anthropic live test", ); - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); requireRunContains( errors, runVitest, @@ -5063,9 +7323,16 @@ function validateBedrockRuntimeCompatibleAnthropicVitestJob( ); } - const cleanup = requireJobStep(errors, jobName, steps, "Clean up Docker auth"); + const cleanup = requireJobStep( + errors, + jobName, + steps, + "Clean up Docker auth", + ); if (cleanup?.if !== "always()") { - errors.push("bedrock-runtime-compatible-anthropic-vitest Docker auth cleanup must always run"); + errors.push( + "bedrock-runtime-compatible-anthropic-vitest Docker auth cleanup must always run", + ); } requireRunContains(errors, cleanup, "docker logout docker.io"); requireRunContains(errors, cleanup, 'rm -rf "${DOCKER_CONFIG}"'); @@ -5089,21 +7356,17 @@ export function validateE2eVitestScenariosWorkflowBoundary( } const permissions = asRecord(workflow.permissions); - if (permissions.contents !== "read") errors.push("workflow permissions.contents must be read"); - - const workflowEnv = asRecord(workflow.env); - requireNoHostedInferenceCredentialEnv(errors, "workflow", workflowEnv); - requireNoHostedInferenceDefaultEnv(errors, "workflow", workflowEnv); + if (permissions.contents !== "read") + errors.push("workflow permissions.contents must be read"); const jobs = asRecord(workflow.jobs); - currentWorkflowJobs = jobs; const { errors: inventoryErrors, inventory: freeStandingInventory } = deriveFreeStandingJobsInventoryFromJobs(jobs); errors.push(...inventoryErrors); validateFreeStandingInventoryBoundary(errors, jobs, freeStandingInventory); - validateHostedInferenceLegacyAliasInventoryBoundary(errors, jobs); const generateMatrix = asRecord(jobs["generate-matrix"]); - if (Object.keys(generateMatrix).length === 0) errors.push("workflow missing generate-matrix job"); + if (Object.keys(generateMatrix).length === 0) + errors.push("workflow missing generate-matrix job"); if (generateMatrix["runs-on"] !== "ubuntu-latest") { errors.push("generate-matrix job must run on ubuntu-latest"); } @@ -5111,7 +7374,10 @@ export function validateE2eVitestScenariosWorkflowBoundary( if (generateOutputs.matrix !== "${{ steps.matrix.outputs.matrix }}") { errors.push("generate-matrix job must expose matrix output"); } - if (generateOutputs.hermes_selected !== "${{ steps.matrix.outputs.hermes_selected }}") { + if ( + generateOutputs.hermes_selected !== + "${{ steps.matrix.outputs.hermes_selected }}" + ) { errors.push("generate-matrix job must expose hermes_selected output"); } const generateSteps = asSteps(generateMatrix.steps); @@ -5119,21 +7385,31 @@ export function validateE2eVitestScenariosWorkflowBoundary( const generateCheckout = generateSteps.find((step) => stringValue(step.uses).startsWith("actions/checkout@"), ); - if (!generateCheckout) errors.push("generate-matrix job missing checkout step"); + if (!generateCheckout) + errors.push("generate-matrix job missing checkout step"); requireFullShaAction(errors, generateCheckout, "generate-matrix checkout"); if (asRecord(generateCheckout?.with)["persist-credentials"] !== false) { - errors.push("generate-matrix checkout step must set persist-credentials=false"); + errors.push( + "generate-matrix checkout step must set persist-credentials=false", + ); } const generateSetupNode = namedStep(generateSteps, "Set up Node"); - if (!generateSetupNode) errors.push("generate-matrix job missing step: Set up Node"); + if (!generateSetupNode) + errors.push("generate-matrix job missing step: Set up Node"); requireFullShaAction(errors, generateSetupNode, "generate-matrix setup-node"); - const generate = requireStep(errors, generateSteps, "Generate Vitest scenario matrix"); + const generate = requireStep( + errors, + generateSteps, + "Generate Vitest scenario matrix", + ); const generateEnv = asRecord(generate?.env); if (generateEnv.JOBS !== "${{ inputs.jobs }}") { errors.push("matrix generation step must pass jobs through JOBS env"); } if (generateEnv.SCENARIOS !== "${{ inputs.scenarios }}") { - errors.push("matrix generation step must pass scenarios through SCENARIOS env"); + errors.push( + "matrix generation step must pass scenarios through SCENARIOS env", + ); } requireRunContains(errors, generate, FREE_STANDING_WORKFLOW_INVENTORY_SCRIPT); requireRunContains( @@ -5146,18 +7422,42 @@ export function validateE2eVitestScenariosWorkflowBoundary( generate, "free_standing_scenarios_csv must match scenario mapping keys", ); - requireRunContains(errors, generate, "Free-standing scenario maps to unknown job"); - requireRunContains(errors, generate, "Use either scenarios or jobs, not both"); + requireRunContains( + errors, + generate, + "Free-standing scenario maps to unknown job", + ); + requireRunContains( + errors, + generate, + "Use either scenarios or jobs, not both", + ); requireRunContains(errors, generate, "Unknown free-standing Vitest job"); requireRunContains(errors, generate, 'matrix="[]"'); - requireRunContains(errors, generate, "npx tsx test/e2e-scenario/scenarios/run.ts"); + requireRunContains( + errors, + generate, + "npx tsx test/e2e-scenario/scenarios/run.ts", + ); requireRunContains(errors, generate, "--emit-live-matrix"); requireRunContains(errors, generate, "--scenarios"); requireRunContains(errors, generate, "^[A-Za-z0-9_-]+(,[A-Za-z0-9_-]+)*$"); - requireRunContains(errors, generate, "Invalid scenario input; use comma-separated scenario ids"); - requireRunContains(errors, generate, "Invalid jobs input; use comma-separated job ids"); + requireRunContains( + errors, + generate, + "Invalid scenario input; use comma-separated scenario ids", + ); + requireRunContains( + errors, + generate, + "Invalid jobs input; use comma-separated job ids", + ); requireRunDoesNotContain(errors, generate, "Invalid jobs input: ${JOBS}"); - requireRunDoesNotContain(errors, generate, "Invalid scenario input: ${SCENARIOS}"); + requireRunDoesNotContain( + errors, + generate, + "Invalid scenario input: ${SCENARIOS}", + ); requireRunDoesNotContain(errors, generate, "^[A-Za-z0-9._-]+"); requireRunContains(errors, generate, "hermes_selected=false"); requireRunContains(errors, generate, "hermes_selected=true"); @@ -5170,7 +7470,8 @@ export function validateE2eVitestScenariosWorkflowBoundary( requireRunContains(errors, generate, "| Scenario | Runner | Label |"); const liveScenarios = asRecord(jobs["live-scenarios"]); - if (Object.keys(liveScenarios).length === 0) errors.push("workflow missing live-scenarios job"); + if (Object.keys(liveScenarios).length === 0) + errors.push("workflow missing live-scenarios job"); if (liveScenarios["runs-on"] !== "${{ matrix.runner }}") { errors.push("live-scenarios job must run on the matrix runner"); } @@ -5178,17 +7479,24 @@ export function validateE2eVitestScenariosWorkflowBoundary( errors.push("live-scenarios job must depend on generate-matrix"); } if ( - liveScenarios.if !== "${{ inputs.jobs == '' && needs.generate-matrix.outputs.matrix != '[]' }}" + liveScenarios.if !== + "${{ inputs.jobs == '' && needs.generate-matrix.outputs.matrix != '[]' }}" ) { - errors.push("live-scenarios job must not run when a free-standing jobs selector is supplied"); + errors.push( + "live-scenarios job must not run when a free-standing jobs selector is supplied", + ); } const strategy = asRecord(liveScenarios.strategy); if (strategy["fail-fast"] !== false) { errors.push("live-scenarios strategy.fail-fast must be false"); } const matrix = asRecord(strategy.matrix); - if (matrix.include !== "${{ fromJSON(needs.generate-matrix.outputs.matrix) }}") { - errors.push("live-scenarios matrix.include must come from generate-matrix output"); + if ( + matrix.include !== "${{ fromJSON(needs.generate-matrix.outputs.matrix) }}" + ) { + errors.push( + "live-scenarios matrix.include must come from generate-matrix output", + ); } const jobEnv = asRecord(liveScenarios.env); @@ -5196,15 +7504,26 @@ export function validateE2eVitestScenariosWorkflowBoundary( errors.push("live-scenarios job must set NEMOCLAW_RUN_E2E_SCENARIOS=1"); } if (!stringValue(jobEnv.E2E_ARTIFACT_DIR).includes("e2e-artifacts/vitest")) { - errors.push("live-scenarios job must write artifacts under e2e-artifacts/vitest"); + errors.push( + "live-scenarios job must write artifacts under e2e-artifacts/vitest", + ); } if (stringValue(jobEnv.E2E_ARTIFACT_DIR).includes("${{ matrix.id }}")) { - errors.push("live-scenarios job E2E_ARTIFACT_DIR must be the Vitest artifact parent"); + errors.push( + "live-scenarios job E2E_ARTIFACT_DIR must be the Vitest artifact parent", + ); } if (!stringValue(jobEnv.NEMOCLAW_CLI_BIN).includes("bin/nemoclaw.js")) { - errors.push("live-scenarios job must point NEMOCLAW_CLI_BIN at the repo CLI"); + errors.push( + "live-scenarios job must point NEMOCLAW_CLI_BIN at the repo CLI", + ); } - requireEnvDoesNotExposeSecret(errors, "live-scenarios job", jobEnv, "NVIDIA_INFERENCE_API_KEY"); + requireEnvDoesNotExposeSecret( + errors, + "live-scenarios job", + jobEnv, + "NVIDIA_INFERENCE_API_KEY", + ); const steps = asSteps(liveScenarios.steps); requireNoDispatchInputInterpolation(errors, steps); @@ -5219,7 +7538,9 @@ export function validateE2eVitestScenariosWorkflowBoundary( } } - const checkout = steps.find((step) => stringValue(step.uses).startsWith("actions/checkout@")); + const checkout = steps.find((step) => + stringValue(step.uses).startsWith("actions/checkout@"), + ); if (!checkout) errors.push("live-scenarios job missing checkout step"); requireFullShaAction(errors, checkout, "checkout"); if (asRecord(checkout?.with)["persist-credentials"] !== false) { @@ -5238,11 +7559,24 @@ export function validateE2eVitestScenariosWorkflowBoundary( if (runVitestEnv.SCENARIO_ID !== "${{ matrix.id }}") { errors.push("Vitest step must pass matrix.id through SCENARIO_ID env"); } - requireHostedInferenceCredentialEnv(errors, "Vitest step", runVitestEnv, { - jobName: "live-scenarios", - }); - requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); - requireRunContains(errors, runVitest, "test/e2e-scenario/live/registry-scenarios.test.ts"); + if ( + runVitestEnv.NVIDIA_INFERENCE_API_KEY !== + "${{ secrets.NVIDIA_INFERENCE_API_KEY }}" + ) { + errors.push( + "Vitest step must receive NVIDIA_INFERENCE_API_KEY from secrets", + ); + } + requireRunContains( + errors, + runVitest, + "npx vitest run --project e2e-scenarios-live", + ); + requireRunContains( + errors, + runVitest, + "test/e2e-scenario/live/registry-scenarios.test.ts", + ); requireRunContains(errors, runVitest, '"^${SCENARIO_ID}$"'); const summary = requireStep(errors, steps, "Summarize artifacts"); @@ -5251,7 +7585,9 @@ export function validateE2eVitestScenariosWorkflowBoundary( errors.push("summary step must pass matrix.id through SCENARIO_ID env"); } if (summaryEnv.SCENARIO_LABEL !== "${{ matrix.label }}") { - errors.push("summary step must pass matrix.label through SCENARIO_LABEL env"); + errors.push( + "summary step must pass matrix.label through SCENARIO_LABEL env", + ); } requireRunContains(errors, summary, "run-plan.json"); requireRunContains( @@ -5259,7 +7595,11 @@ export function validateE2eVitestScenariosWorkflowBoundary( summary, 'Path(os.environ["E2E_ARTIFACT_DIR"]) / os.environ["SCENARIO_ID"]', ); - requireRunContains(errors, summary, "| Scenario | Manifest | Expected state | Suites | Phases |"); + requireRunContains( + errors, + summary, + "| Scenario | Manifest | Expected state | Suites | Phases |", + ); requireRunContains(errors, summary, "SCENARIO_ID"); const upload = requireStep(errors, steps, "Upload Vitest E2E artifacts"); @@ -5299,12 +7639,26 @@ export function validateE2eVitestScenariosWorkflowBoundary( uploadPath, "e2e-artifacts/vitest/${{ matrix.id }}/state-validation.result.json", ); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/${{ matrix.id }}/actions/"); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/${{ matrix.id }}/logs/"); - requireUploadPathContains(errors, uploadPath, "e2e-artifacts/vitest/${{ matrix.id }}/shell/"); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/${{ matrix.id }}/actions/", + ); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/${{ matrix.id }}/logs/", + ); + requireUploadPathContains( + errors, + uploadPath, + "e2e-artifacts/vitest/${{ matrix.id }}/shell/", + ); for (const line of uploadPath.split("\n")) { if (line.trim() === "e2e-artifacts/vitest/${{ matrix.id }}/") { - errors.push("artifact upload path must not list the whole matrix artifact directory"); + errors.push( + "artifact upload path must not list the whole matrix artifact directory", + ); } } if (uploadWith["include-hidden-files"] !== false) { @@ -5327,7 +7681,12 @@ export function validateE2eVitestScenariosWorkflowBoundary( "sessions-agents-cli-vitest", "sessions-agents-cli", ); - validateFreeStandingJobSelector(errors, jobs, "inference-routing-vitest", "inference-routing"); + validateFreeStandingJobSelector( + errors, + jobs, + "inference-routing-vitest", + "inference-routing", + ); validateCloudInferenceVitestJob(errors, jobs); validateRuntimeOverridesVitestJob(errors, jobs); validateDoubleOnboardVitestJob(errors, jobs); @@ -5411,9 +7770,15 @@ export function validateE2eVitestScenariosWorkflowBoundary( } else { const needs = Array.isArray(reportToPr.needs) ? reportToPr.needs : []; for (const required of ["generate-matrix", "live-scenarios"]) { - if (!needs.includes(required)) errors.push(`report-to-pr job must wait for ${required}`); + if (!needs.includes(required)) + errors.push(`report-to-pr job must wait for ${required}`); } - validateFreeStandingInventoryCoverage(errors, jobs, needs, freeStandingInventory); + validateFreeStandingInventoryCoverage( + errors, + jobs, + needs, + freeStandingInventory, + ); const reportSteps = asSteps(reportToPr.steps); const report = requireJobStep( errors, @@ -5426,12 +7791,18 @@ export function validateE2eVitestScenariosWorkflowBoundary( errors.push("report-to-pr step must pass jobs through JOBS env"); } if (reportEnv.JOB_PR_NUMBER !== "${{ inputs.pr_number }}") { - errors.push("report-to-pr step must pass pr_number through JOB_PR_NUMBER env"); + errors.push( + "report-to-pr step must pass pr_number through JOB_PR_NUMBER env", + ); } if (reportEnv.JOB_SCENARIOS !== "${{ inputs.scenarios }}") { - errors.push("report-to-pr step must pass scenarios through JOB_SCENARIOS env"); + errors.push( + "report-to-pr step must pass scenarios through JOB_SCENARIOS env", + ); } - const reportScript = stringValue(asRecord(report?.with).script ?? report?.run); + const reportScript = stringValue( + asRecord(report?.with).script ?? report?.run, + ); if (!reportScript.includes("process.env.JOBS")) { errors.push( "step 'Post Vitest scenario results to PR' run script must include process.env.JOBS", @@ -5468,7 +7839,9 @@ export function validateE2eVitestScenariosWorkflowBoundary( ); } if (!reportScript.includes("cancelled")) { - errors.push("step 'Post Vitest scenario results to PR' run script must count cancelled jobs"); + errors.push( + "step 'Post Vitest scenario results to PR' run script must count cancelled jobs", + ); } if (!reportScript.includes("**Requested jobs:**")) { errors.push( @@ -5480,7 +7853,10 @@ export function validateE2eVitestScenariosWorkflowBoundary( "step 'Post Vitest scenario results to PR' run script must include **Requested scenarios:**", ); } - for (const forbidden of ["toJSON(inputs.pr_number)", "toJSON(inputs.scenarios)"]) { + for (const forbidden of [ + "toJSON(inputs.pr_number)", + "toJSON(inputs.scenarios)", + ]) { if (reportScript.includes(forbidden)) { errors.push( `step 'Post Vitest scenario results to PR' run script must not include ${forbidden}`,