diff --git a/.github/actions/export-e2e-hosted-inference/action.yaml b/.github/actions/export-e2e-hosted-inference/action.yaml new file mode 100644 index 00000000000..84202ee8917 --- /dev/null +++ b/.github/actions/export-e2e-hosted-inference/action.yaml @@ -0,0 +1,39 @@ +# 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 new file mode 100755 index 00000000000..50a501a8314 --- /dev/null +++ b/.github/actions/export-e2e-hosted-inference/export.sh @@ -0,0 +1,53 @@ +#!/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 0b729fead78..3714d942e66 100644 --- a/.github/workflows/e2e-script.yaml +++ b/.github/workflows/e2e-script.yaml @@ -62,6 +62,11 @@ 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 @@ -80,6 +85,8 @@ on: secrets: NVIDIA_INFERENCE_API_KEY: required: false + NVIDIA_API_KEY: + required: false BRAVE_API_KEY: required: false DOCKERHUB_USERNAME: @@ -143,6 +150,7 @@ jobs: with: ref: ${{ github.ref }} sparse-checkout: | + .github/actions/export-e2e-hosted-inference .github/actions/run-e2e-script path: workflow-actions persist-credentials: false @@ -214,26 +222,11 @@ jobs: - name: Export hosted CI inference environment if: ${{ inputs.nvidia_api_key }} - 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" + 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 }} - name: Run E2E script uses: ./workflow-actions/.github/actions/run-e2e-script @@ -248,7 +241,6 @@ 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 a1e0551e91f..03b0313576f 100644 --- a/.github/workflows/e2e-vitest-scenarios.yaml +++ b/.github/workflows/e2e-vitest-scenarios.yaml @@ -245,9 +245,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 Vitest live E2E scenarios env: - NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} SCENARIO_ID: ${{ matrix.id }} run: | set -euo pipefail @@ -471,9 +475,12 @@ 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" @@ -570,9 +577,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 skill CLI live test - env: - NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} run: | set -euo pipefail npx vitest run --project e2e-scenarios-live \ @@ -763,13 +774,17 @@ 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 \ @@ -873,9 +888,12 @@ 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" @@ -979,9 +997,12 @@ 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" @@ -1032,10 +1053,14 @@ 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" @@ -1138,9 +1163,12 @@ 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" @@ -1234,9 +1262,12 @@ 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" @@ -1322,13 +1353,16 @@ 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 \ @@ -1405,19 +1439,18 @@ 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 \ @@ -1496,13 +1529,17 @@ 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" @@ -1629,12 +1666,16 @@ 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: | @@ -1695,9 +1736,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 live Vitest test - env: - NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} run: | set -euo pipefail npx vitest run --project e2e-scenarios-live \ @@ -1775,12 +1819,16 @@ 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" @@ -1850,12 +1898,15 @@ 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 \ @@ -1913,12 +1964,16 @@ 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" @@ -2002,12 +2057,15 @@ 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 \ @@ -2089,9 +2147,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 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" @@ -2180,12 +2241,15 @@ 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 \ @@ -2263,12 +2327,15 @@ 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 \ @@ -2349,9 +2416,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 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" @@ -2444,9 +2514,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 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 \ @@ -2532,9 +2605,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 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" @@ -2632,9 +2709,12 @@ 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" @@ -2989,10 +3069,12 @@ 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" @@ -3054,9 +3136,12 @@ 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" @@ -3119,9 +3204,12 @@ 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" @@ -3309,9 +3397,12 @@ 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" @@ -3628,12 +3719,17 @@ 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 }} @@ -3713,9 +3809,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 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 \ @@ -3795,14 +3894,17 @@ 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 \ @@ -3885,12 +3987,16 @@ 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 \ @@ -3974,13 +4080,16 @@ 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 \ @@ -4036,13 +4145,17 @@ 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 @@ -4162,9 +4275,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 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 \ @@ -4237,9 +4353,12 @@ 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 @@ -4335,13 +4454,17 @@ 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 \ @@ -4588,9 +4711,12 @@ 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" @@ -4692,13 +4818,18 @@ 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" @@ -4807,12 +4938,15 @@ 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" @@ -4909,12 +5043,16 @@ 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 }} @@ -5012,11 +5150,15 @@ 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: | @@ -5226,11 +5368,15 @@ 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 @@ -5353,13 +5499,16 @@ 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 db76a3cf1bf..8bf4eafb8df 100644 --- a/.github/workflows/nightly-e2e.yaml +++ b/.github/workflows/nightly-e2e.yaml @@ -243,7 +243,12 @@ 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: *nightly-e2e-default-secrets + 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 || '' }} cron-preflight-inference-local-e2e: if: >- github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'workflow_dispatch' || @@ -376,10 +381,12 @@ 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 || '' }} @@ -1196,8 +1203,14 @@ 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: *nightly-e2e-default-secrets + 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 || '' }} 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 new file mode 100644 index 00000000000..02915509d7a --- /dev/null +++ b/test/e2e-hosted-inference-action.test.ts @@ -0,0 +1,103 @@ +// 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 f46b446bf71..fc588e7e1bb 100644 --- a/test/e2e-scenario/fixtures/hosted-inference.ts +++ b/test/e2e-scenario/fixtures/hosted-inference.ts @@ -2,6 +2,7 @@ // 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"; @@ -9,16 +10,18 @@ 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; + sourceSecretName: typeof HOSTED_INFERENCE_SECRET | typeof HOSTED_INFERENCE_PUBLIC_FALLBACK_SECRET; credentialEnv: typeof HOSTED_INFERENCE_CREDENTIAL_ENV; provider: typeof HOSTED_INFERENCE_PROVIDER; providerName: typeof HOSTED_INFERENCE_PROVIDER_NAME; @@ -33,16 +36,29 @@ export function requireHostedInferenceConfig( env: NodeJS.ProcessEnv = process.env, options: HostedInferenceOptions = {}, ): HostedInferenceConfig { - const apiKey = secrets.required(HOSTED_INFERENCE_SECRET); + 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 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: HOSTED_INFERENCE_SECRET, + sourceSecretName, credentialEnv: HOSTED_INFERENCE_CREDENTIAL_ENV, provider: HOSTED_INFERENCE_PROVIDER, providerName: HOSTED_INFERENCE_PROVIDER_NAME, @@ -53,6 +69,7 @@ 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 9fde9c79fc5..07e340613bc 100644 --- a/test/e2e-scenario/live/agent-turn-latency-helpers.ts +++ b/test/e2e-scenario/live/agent-turn-latency-helpers.ts @@ -12,6 +12,7 @@ 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, "../../.."); @@ -220,17 +221,13 @@ export async function installSandbox( ): Promise { let install: ShellProbeResult | undefined; for (let attempt = 1; attempt <= INSTALL_ATTEMPTS; attempt += 1) { - 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, - }, - ); + 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, + }); 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 4068840372a..5ed6d1ecaf9 100644 --- a/test/e2e-scenario/live/credential-migration.test.ts +++ b/test/e2e-scenario/live/credential-migration.test.ts @@ -9,7 +9,6 @@ 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 @@ -18,10 +17,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 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. +// 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. const REPO_ROOT = path.resolve(import.meta.dirname, "../../.."); const CLI_ENTRYPOINT = path.join(REPO_ROOT, "bin", "nemoclaw.js"); @@ -29,7 +28,8 @@ 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 CREDENTIAL_MIGRATION_MODEL = "openai/gpt-oss-120b"; +const MIGRATION_CREDENTIAL_ENV = "NVIDIA_INFERENCE_API_KEY"; +const MIGRATION_PROVIDER_NAME = "nvidia-prod"; validateSandboxName(SANDBOX_NAME); const runCredentialMigrationTest = shouldRunLiveE2EScenarios() ? test : test.skip; @@ -146,14 +146,11 @@ 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 hostedInference = requireHostedInferenceConfig(secrets, process.env, { - model: CREDENTIAL_MIGRATION_MODEL, - }); - const migratedCredentialValue = hostedInference.apiKey; - const { - [hostedInference.credentialEnv]: _omittedCredential, - ...hostedInferenceEnvWithoutCredential - } = hostedInference.env; + const migratedCredentialValue = secrets.required(MIGRATION_CREDENTIAL_ENV); + expect( + migratedCredentialValue.startsWith("nvapi-"), + `${MIGRATION_CREDENTIAL_ENV} must start with nvapi-`, + ).toBe(true); expect(fs.existsSync(CLI_ENTRYPOINT), "bin/nemoclaw.js missing").toBe(true); expect( fs.existsSync(DIST_CREDENTIAL_STORE), @@ -190,9 +187,9 @@ runCredentialMigrationTest( sandboxName: SANDBOX_NAME, contracts: [ "legacy credentials.json stages allowlisted provider keys into onboard env", - `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 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 removes plaintext credentials.json", "tampered non-credential keys do not become gateway providers", "credentials list reads providers from the gateway, not disk", @@ -209,7 +206,7 @@ runCredentialMigrationTest( legacyFile, JSON.stringify( { - [hostedInference.credentialEnv]: migratedCredentialValue, + [MIGRATION_CREDENTIAL_ENV]: migratedCredentialValue, OPENSHELL_GATEWAY: "evil-gw-from-tampered-file", NODE_OPTIONS: "--require=/tmp/evil.js", }, @@ -222,7 +219,6 @@ 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", }), @@ -253,10 +249,14 @@ 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 ${hostedInference.providerName} provider\n${providersText}`, - ).toContain(hostedInference.providerName); + `expected migrated ${MIGRATION_PROVIDER_NAME} provider\n${providersText}`, + ).toContain(MIGRATION_PROVIDER_NAME); expect(providerNames).not.toContain("OPENSHELL_GATEWAY"); expect(providerNames).not.toContain("NODE_OPTIONS"); @@ -299,9 +299,8 @@ runCredentialMigrationTest( await artifacts.writeJson("scenario-result.json", { id: "credential-migration", sandboxName: SANDBOX_NAME, - model: hostedInference.model || CREDENTIAL_MIGRATION_MODEL, - provider: hostedInference.providerName, - credentialEnv: hostedInference.credentialEnv, + provider: MIGRATION_PROVIDER_NAME, + credentialEnv: MIGRATION_CREDENTIAL_ENV, providerNames, assertions: { onboardSucceeded: onboard.exitCode === 0, @@ -309,7 +308,7 @@ runCredentialMigrationTest( "Staged 1 legacy credential(s) for migration to the OpenShell gateway.", ), legacyFileRemovedAfterOnboard: !fs.existsSync(legacyFile), - migratedProviderRegistered: providerNames.includes(hostedInference.providerName), + migratedProviderRegistered: providerNames.includes(MIGRATION_PROVIDER_NAME), 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 a31658092b3..73ccb2ef1d3 100644 --- a/test/e2e-scenario/live/cron-preflight-inference-local.test.ts +++ b/test/e2e-scenario/live/cron-preflight-inference-local.test.ts @@ -19,6 +19,7 @@ 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, "../../.."); @@ -265,20 +266,16 @@ test.skipIf(!shouldRunLiveE2EScenarios())( let install: ShellProbeResult | undefined; for (let attempt = 1; attempt <= INSTALL_ATTEMPTS; attempt += 1) { - 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, - }, - ); + 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, + }); 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 new file mode 100644 index 00000000000..beca9c5e41b --- /dev/null +++ b/test/e2e-scenario/live/install-sh-onboard.ts @@ -0,0 +1,14 @@ +// 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 37c1a21f5f5..26f81052f4b 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 receive NVIDIA_INFERENCE_API_KEY from secrets", + "Vitest step must consume the shared hosted inference export action", "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,7 +1263,8 @@ 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 secrets", + "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 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 new file mode 100644 index 00000000000..b4037418aa9 --- /dev/null +++ b/test/e2e-scenario/support-tests/hosted-inference-workflow-env.test.ts @@ -0,0 +1,46 @@ +// 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 8a21df80667..50bb29ce621 100644 --- a/test/e2e-scenario/support-tests/hosted-inference.test.ts +++ b/test/e2e-scenario/support-tests/hosted-inference.test.ts @@ -20,6 +20,7 @@ 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}`); @@ -103,6 +104,48 @@ 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: { @@ -158,7 +201,39 @@ 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 new file mode 100644 index 00000000000..3631881279b --- /dev/null +++ b/test/e2e-scenario/support-tests/install-sh-onboard.test.ts @@ -0,0 +1,35 @@ +// 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 1da46728c7b..4f0d1b7dc8c 100644 --- a/test/e2e-script-workflow.test.ts +++ b/test/e2e-script-workflow.test.ts @@ -9,6 +9,12 @@ 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, @@ -49,6 +55,7 @@ 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 }, @@ -402,6 +409,9 @@ 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 || '' }}`, @@ -415,9 +425,13 @@ 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, ...messagingLiveSecrets } - : defaultSecrets; + ? { ...defaultSecrets, ...legacyAliasSecret, ...messagingLiveSecrets } + : expectsPublicNvidiaFallback + ? { ...defaultSecrets, ...legacyAliasSecret } + : defaultSecrets; expect(job.secrets, name).toEqual(expectedSecrets); expect(job.with?.messaging_live_secrets ?? false, name).toBe( expectsLiveMessaging @@ -919,21 +933,48 @@ 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?.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(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(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 new file mode 100644 index 00000000000..5f7c1597a15 --- /dev/null +++ b/tools/e2e-scenarios/hosted-inference-legacy-alias.mts @@ -0,0 +1,73 @@ +// 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 8f24e15caec..f998ab34718 100644 --- a/tools/e2e-scenarios/workflow-boundary.mts +++ b/tools/e2e-scenarios/workflow-boundary.mts @@ -6,6 +6,12 @@ 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, @@ -46,6 +52,22 @@ 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", @@ -84,9 +106,7 @@ 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( @@ -104,9 +124,7 @@ 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); @@ -114,17 +132,13 @@ 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 { @@ -137,10 +151,7 @@ function deriveFreeStandingJobsInventoryFromJobs(jobs: WorkflowRecord): { }; } -const freeStandingJobsInventoryCache = new Map< - string, - CachedFreeStandingJobsInventory ->(); +const freeStandingJobsInventoryCache = new Map(); function readWorkflowRecord(workflowPath: string): WorkflowRecord { return asRecord(YAML.parse(readFileSync(workflowPath, "utf-8"))); @@ -160,8 +171,7 @@ 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( @@ -169,22 +179,14 @@ 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, @@ -308,20 +310,12 @@ 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( @@ -352,9 +346,7 @@ 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}`); } } @@ -365,17 +357,11 @@ 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}`); } @@ -392,28 +378,111 @@ function requireEnvDoesNotExposeSecret( } } -function requireWorkflowDispatch( +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( errors: string[], - triggers: WorkflowRecord, -): WorkflowRecord { + 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 { 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}`); } } @@ -423,7 +492,9 @@ function requireFullShaAction( description: string, ): void { if (!step) return; - if (!/@[0-9a-f]{40}$/i.test(stringValue(step.uses))) { + const uses = stringValue(step.uses); + if (uses.startsWith("./")) return; + if (!/@[0-9a-f]{40}$/i.test(uses)) { errors.push(`${description} action must be pinned to a full commit SHA`); } } @@ -432,8 +503,7 @@ 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( @@ -479,34 +549,66 @@ 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`); } } } @@ -527,9 +629,7 @@ 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]); @@ -547,10 +647,7 @@ 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) { @@ -561,22 +658,14 @@ function validateOpenShellVersionPinVitestJob( 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", @@ -600,30 +689,16 @@ function validateOpenShellVersionPinVitestJob( ); } - 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, @@ -631,52 +706,20 @@ function validateOpenShellVersionPinVitestJob( 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", @@ -688,16 +731,11 @@ function validateOpenShellVersionPinVitestJob( ); } 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) { @@ -714,18 +752,13 @@ function validateSkillAgentVitestJob( 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, @@ -747,20 +780,15 @@ function validateSkillAgentVitestJob( } } - 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( @@ -769,70 +797,28 @@ function validateSkillAgentVitestJob( 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); - 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", - ); - } + requireHostedInferenceCredentialEnv(errors, "skill-agent-vitest run step", runEnv, { jobName }); 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") { @@ -859,24 +845,17 @@ function validateSkillAgentVitestJob( } } 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) { @@ -897,27 +876,18 @@ function validateNetworkPolicyVitestJob( 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", @@ -925,12 +895,7 @@ function validateNetworkPolicyVitestJob( "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); @@ -968,20 +933,15 @@ function validateNetworkPolicyVitestJob( ); } - 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( @@ -990,102 +950,51 @@ function validateNetworkPolicyVitestJob( 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 runVitest = requireJobStep(errors, jobName, 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, + requireHostedInferenceCredentialEnv(errors, "network-policy-vitest Vitest step", runVitestEnv, { jobName, - steps, - "Upload network-policy artifacts", - ); + }); + 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/", - ); + 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", - ); + 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) { @@ -1098,49 +1007,34 @@ function validateCommonEgressAgentVitestJob( } 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", @@ -1148,12 +1042,7 @@ function validateCommonEgressAgentVitestJob( "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); @@ -1169,11 +1058,7 @@ function validateCommonEgressAgentVitestJob( "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}'`, @@ -1183,26 +1068,16 @@ function validateCommonEgressAgentVitestJob( } } - 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, @@ -1210,100 +1085,48 @@ function validateCommonEgressAgentVitestJob( 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 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", - ); - } + requireHostedInferenceCredentialEnv(errors, "common-egress-agent-vitest step", runVitestEnv, { + jobName, + }); 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) { @@ -1316,43 +1139,28 @@ function validateShieldsConfigVitestJob( } 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, @@ -1360,18 +1168,8 @@ function validateShieldsConfigVitestJob( 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); @@ -1402,23 +1200,14 @@ function validateShieldsConfigVitestJob( } } - 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( @@ -1426,15 +1215,12 @@ function validateShieldsConfigVitestJob( ); } 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( @@ -1443,81 +1229,37 @@ function validateShieldsConfigVitestJob( 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 runVitest = requireJobStep(errors, jobName, 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, + requireHostedInferenceCredentialEnv(errors, "shields-config-vitest step", runVitestEnv, { jobName, - steps, - "Upload shields-config artifacts", - ); + }); + 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"); 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) { @@ -1530,28 +1272,19 @@ function validateRebuildOpenClawVitestJob( } 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, @@ -1573,24 +1306,14 @@ function validateRebuildOpenClawVitestJob( } } - 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( @@ -1605,8 +1328,7 @@ function validateRebuildOpenClawVitestJob( 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( @@ -1615,100 +1337,50 @@ function validateRebuildOpenClawVitestJob( 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 runVitest = requireJobStep(errors, jobName, 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", - ); - } + requireHostedInferenceCredentialEnv(errors, "rebuild-openclaw-vitest step", runVitestEnv, { + 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", - ); + 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 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"); } } @@ -1717,12 +1389,8 @@ 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`); @@ -1750,9 +1418,7 @@ 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`); @@ -1768,19 +1434,13 @@ 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", @@ -1797,56 +1457,30 @@ 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"); @@ -1861,39 +1495,18 @@ 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); - 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", - ); + 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"); const upload = requireJobStep( errors, @@ -1920,24 +1533,17 @@ 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]); @@ -1951,33 +1557,22 @@ function validateSandboxRebuildVitestJob( } 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", @@ -1985,12 +1580,7 @@ function validateSandboxRebuildVitestJob( "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); @@ -1999,49 +1589,24 @@ function validateSandboxRebuildVitestJob( 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( @@ -2049,16 +1614,13 @@ function validateSandboxRebuildVitestJob( ); } 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( @@ -2067,101 +1629,48 @@ function validateSandboxRebuildVitestJob( 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 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", - ); - } + requireHostedInferenceCredentialEnv(errors, "sandbox-rebuild-vitest step", runVitestEnv, { + jobName, + }); 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]); @@ -2175,48 +1684,33 @@ function validateStateBackupRestoreVitestJob( } 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", @@ -2224,12 +1718,7 @@ function validateStateBackupRestoreVitestJob( "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); @@ -2238,53 +1727,24 @@ function validateStateBackupRestoreVitestJob( 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( @@ -2300,13 +1760,8 @@ function validateStateBackupRestoreVitestJob( 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, @@ -2314,100 +1769,50 @@ function validateStateBackupRestoreVitestJob( 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 state backup restore live test", - ); + 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 }}") { + requireHostedInferenceCredentialEnv(errors, "state-backup-restore-vitest step", runVitestEnv, { + jobName, + }); + 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"); + + 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"); + } + const uploadPath = stringValue(uploadWith.path); + 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"); + } + if (uploadWith["if-no-files-found"] !== "ignore") { 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", - ); - - 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", - ); - } - const uploadPath = stringValue(uploadWith.path); - 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", - ); - } - if (uploadWith["if-no-files-found"] !== "ignore") { - errors.push( - "state-backup-restore-vitest artifact upload must ignore missing fixture artifacts", + "state-backup-restore-vitest artifact upload must ignore missing fixture artifacts", ); } 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]); @@ -2421,34 +1826,25 @@ function validateUpgradeStaleSandboxVitestJob( } 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( @@ -2456,20 +1852,10 @@ function validateUpgradeStaleSandboxVitestJob( ); } 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); @@ -2478,51 +1864,22 @@ function validateUpgradeStaleSandboxVitestJob( 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( @@ -2537,12 +1894,7 @@ function validateUpgradeStaleSandboxVitestJob( '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( @@ -2560,13 +1912,8 @@ function validateUpgradeStaleSandboxVitestJob( 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, @@ -2574,26 +1921,13 @@ function validateUpgradeStaleSandboxVitestJob( 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"); @@ -2608,49 +1942,21 @@ function validateUpgradeStaleSandboxVitestJob( "Run upgrade stale sandbox live Vitest test", ); const runVitestEnv = asRecord(runVitest?.env); - 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", - ); - } + requireHostedInferenceCredentialEnv(errors, "upgrade-stale-sandbox-vitest step", runVitestEnv, { + jobName, + }); 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", @@ -2662,25 +1968,15 @@ function validateUpgradeStaleSandboxVitestJob( ); } 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) { @@ -2693,28 +1989,19 @@ function validateTokenRotationVitestJob( } 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, @@ -2736,23 +2023,14 @@ function validateTokenRotationVitestJob( } } - 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( @@ -2760,15 +2038,12 @@ function validateTokenRotationVitestJob( ); } 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( @@ -2777,21 +2052,12 @@ function validateTokenRotationVitestJob( 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, @@ -2800,9 +2066,7 @@ function validateTokenRotationVitestJob( "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", @@ -2823,48 +2087,25 @@ function validateTokenRotationVitestJob( 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"); } } @@ -2880,20 +2121,11 @@ 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); @@ -2911,19 +2143,13 @@ 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, @@ -2980,18 +2206,9 @@ 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", @@ -2999,15 +2216,8 @@ 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, @@ -3015,11 +2225,7 @@ 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"); @@ -3043,20 +2249,12 @@ 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, "npx vitest run --project e2e-scenarios-live"); requireRunContains( errors, runVitest, @@ -3069,18 +2267,10 @@ function validateMessagingCompatibleEndpointVitestJob( 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( @@ -3099,16 +2289,11 @@ 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) { @@ -3119,18 +2304,11 @@ function validateOnboardNegativePathsVitestJob( 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 !== @@ -3158,30 +2336,16 @@ function validateOnboardNegativePathsVitestJob( ); } - 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, @@ -3189,55 +2353,23 @@ function validateOnboardNegativePathsVitestJob( 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", @@ -3249,16 +2381,11 @@ function validateOnboardNegativePathsVitestJob( ); } 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) { @@ -3275,40 +2402,24 @@ function validateCloudInferenceVitestJob( } 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); @@ -3323,21 +2434,15 @@ function validateCloudInferenceVitestJob( } } - 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( @@ -3346,96 +2451,48 @@ function validateCloudInferenceVitestJob( 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 runVitest = requireJobStep(errors, jobName, 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, + requireHostedInferenceCredentialEnv(errors, "cloud-inference-vitest run step", runVitestEnv, { jobName, - steps, - "Upload cloud inference artifacts", - ); - requireFullShaAction( - errors, - upload, - "cloud-inference-vitest upload-artifact", - ); + }); + 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 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) { @@ -3450,19 +2507,12 @@ function validateDoubleOnboardVitestJob( 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", ); @@ -3473,12 +2523,7 @@ function validateDoubleOnboardVitestJob( 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); @@ -3499,42 +2544,28 @@ function validateDoubleOnboardVitestJob( ); } - 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( @@ -3543,78 +2574,38 @@ function validateDoubleOnboardVitestJob( 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) { @@ -3629,13 +2620,10 @@ function validateRuntimeOverridesVitestJob( 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", @@ -3653,54 +2641,29 @@ function validateRuntimeOverridesVitestJob( 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, @@ -3708,71 +2671,32 @@ function validateRuntimeOverridesVitestJob( 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 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 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 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) { @@ -3784,21 +2708,13 @@ function validateHermesE2EVitestJob( 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); @@ -3806,17 +2722,10 @@ function validateHermesE2EVitestJob( 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"); @@ -3825,9 +2734,7 @@ function validateHermesE2EVitestJob( 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, @@ -3849,20 +2756,15 @@ function validateHermesE2EVitestJob( } } - 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( @@ -3871,78 +2773,40 @@ function validateHermesE2EVitestJob( 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 runVitest = requireJobStep(errors, jobName, 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", - ); + requireHostedInferenceCredentialEnv(errors, "hermes-e2e-vitest Vitest step", runVitestEnv, { + jobName, + }); + 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) { @@ -3951,14 +2815,10 @@ function validateHermesRootEntrypointSmokeVitestJob( } 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,')) }}"; @@ -3968,16 +2828,12 @@ function validateHermesRootEntrypointSmokeVitestJob( ); } 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 !== @@ -4042,18 +2898,9 @@ function validateHermesRootEntrypointSmokeVitestJob( ); } - 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", @@ -4061,15 +2908,8 @@ function validateHermesRootEntrypointSmokeVitestJob( } 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, @@ -4077,11 +2917,7 @@ function validateHermesRootEntrypointSmokeVitestJob( steps, "Install root dependencies", ); - requireRunContains( - errors, - installRootDependencies, - "npm ci --ignore-scripts", - ); + requireRunContains(errors, installRootDependencies, "npm ci --ignore-scripts"); const runVitest = requireJobStep( errors, @@ -4089,11 +2925,7 @@ function validateHermesRootEntrypointSmokeVitestJob( 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, @@ -4107,16 +2939,10 @@ function validateHermesRootEntrypointSmokeVitestJob( 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( @@ -4135,9 +2961,7 @@ function validateHermesRootEntrypointSmokeVitestJob( ); } 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"); } } @@ -4269,11 +3093,7 @@ 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]); @@ -4292,22 +3112,15 @@ function validateDiagnosticsVitestJob( 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"); @@ -4316,14 +3129,10 @@ function validateDiagnosticsVitestJob( 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"); @@ -4334,12 +3143,7 @@ function validateDiagnosticsVitestJob( "DOCKERHUB_TOKEN", "GITHUB_TOKEN", ]) { - requireEnvDoesNotExposeSecret( - errors, - "diagnostics-vitest job", - jobEnv, - secret, - ); + requireEnvDoesNotExposeSecret(errors, "diagnostics-vitest job", jobEnv, secret); } const steps = asSteps(job.steps); @@ -4348,19 +3152,9 @@ function validateDiagnosticsVitestJob( 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"); @@ -4372,20 +3166,15 @@ function validateDiagnosticsVitestJob( ); } - 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( @@ -4394,75 +3183,40 @@ function validateDiagnosticsVitestJob( 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 runVitest = requireJobStep(errors, jobName, 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", - ); + requireHostedInferenceCredentialEnv(errors, "diagnostics-vitest Vitest step", runVitestEnv, { + jobName, + }); + 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]); @@ -4480,31 +3234,22 @@ function validateSparkInstallVitestJob( 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"); @@ -4518,17 +3263,10 @@ function validateSparkInstallVitestJob( 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); @@ -4537,34 +3275,20 @@ function validateSparkInstallVitestJob( 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"); @@ -4579,74 +3303,37 @@ function validateSparkInstallVitestJob( 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 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]); @@ -4665,42 +3352,29 @@ function validateSnapshotCommandsVitestJob( 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", @@ -4708,12 +3382,7 @@ function validateSnapshotCommandsVitestJob( "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); @@ -4722,42 +3391,23 @@ function validateSnapshotCommandsVitestJob( 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( @@ -4773,16 +3423,9 @@ function validateSnapshotCommandsVitestJob( ); 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", ); @@ -4802,11 +3445,7 @@ function validateSnapshotCommandsVitestJob( 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, @@ -4814,78 +3453,38 @@ function validateSnapshotCommandsVitestJob( 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); - 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, - runVitest, - "npx vitest run --project e2e-scenarios-live", - ); - requireRunContains( + requireHostedInferenceCredentialEnv( errors, - runVitest, - "test/e2e-scenario/live/snapshot-commands.test.ts", + "snapshot-commands-vitest Vitest step", + runVitestEnv, + { jobName }, ); + 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"); } @@ -4901,16 +3500,12 @@ 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); @@ -4963,44 +3558,21 @@ 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", @@ -5020,16 +3592,9 @@ 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", ); @@ -5047,9 +3612,7 @@ 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, @@ -5063,11 +3626,7 @@ 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"); @@ -5079,19 +3638,13 @@ function validateModelRouterProviderRoutedInferenceVitestJob( "Run Model Router provider-routed inference live test", ); const runVitestEnv = asRecord(runVitest?.env); - 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( + requireHostedInferenceCredentialEnv( errors, - runVitest, - "npx vitest run --project e2e-scenarios-live", + "model-router-provider-routed-inference-vitest Vitest step", + runVitestEnv, + { jobName }, ); + requireRunContains(errors, runVitest, "npx vitest run --project e2e-scenarios-live"); requireRunContains( errors, runVitest, @@ -5110,10 +3663,7 @@ 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", ); @@ -5140,12 +3690,7 @@ 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", @@ -5161,10 +3706,7 @@ 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]); @@ -5183,29 +3725,19 @@ function validateTunnelLifecycleVitestJob( 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, @@ -5221,47 +3753,23 @@ function validateTunnelLifecycleVitestJob( 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( @@ -5276,22 +3784,11 @@ function validateTunnelLifecycleVitestJob( '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", ); @@ -5319,11 +3816,7 @@ function validateTunnelLifecycleVitestJob( 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"); @@ -5348,88 +3841,42 @@ function validateTunnelLifecycleVitestJob( "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 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", - ); - } + requireHostedInferenceCredentialEnv(errors, "tunnel-lifecycle-vitest Vitest step", runVitestEnv, { + jobName, + }); 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", - ); + 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 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/", - ); + 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", - ); + 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"); } @@ -5437,10 +3884,7 @@ function validateTunnelLifecycleVitestJob( 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]); @@ -5450,14 +3894,10 @@ function validateIssue2478CrashLoopRecoveryVitestJob( } 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); @@ -5470,8 +3910,7 @@ function validateIssue2478CrashLoopRecoveryVitestJob( 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", @@ -5481,15 +3920,10 @@ function validateIssue2478CrashLoopRecoveryVitestJob( }; 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", @@ -5503,44 +3937,21 @@ function validateIssue2478CrashLoopRecoveryVitestJob( 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", @@ -5559,22 +3970,11 @@ function validateIssue2478CrashLoopRecoveryVitestJob( '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", ); @@ -5592,15 +3992,9 @@ function validateIssue2478CrashLoopRecoveryVitestJob( 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, @@ -5608,26 +4002,13 @@ function validateIssue2478CrashLoopRecoveryVitestJob( 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, @@ -5642,11 +4023,7 @@ function validateIssue2478CrashLoopRecoveryVitestJob( 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, @@ -5659,18 +4036,10 @@ function validateIssue2478CrashLoopRecoveryVitestJob( 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( @@ -5689,30 +4058,18 @@ function validateIssue2478CrashLoopRecoveryVitestJob( ); } 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]); @@ -5726,28 +4083,21 @@ function validateChannelsAddRemoveVitestJob( } 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( @@ -5755,19 +4105,13 @@ function validateChannelsAddRemoveVitestJob( ); } 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", @@ -5775,12 +4119,7 @@ function validateChannelsAddRemoveVitestJob( "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); @@ -5789,49 +4128,24 @@ function validateChannelsAddRemoveVitestJob( 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( @@ -5847,13 +4161,8 @@ function validateChannelsAddRemoveVitestJob( 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, @@ -5861,118 +4170,57 @@ function validateChannelsAddRemoveVitestJob( 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 runVitest = requireJobStep(errors, jobName, 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", - ); + requireHostedInferenceCredentialEnv(errors, "channels-add-remove-vitest step", runVitestEnv, { + 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"); } 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]); @@ -5982,34 +4230,19 @@ function validateOpenClawDiscordPairingVitestJob( } 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); @@ -6018,58 +4251,27 @@ function validateOpenClawDiscordPairingVitestJob( 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", - ); + 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, @@ -6077,11 +4279,7 @@ function validateOpenClawDiscordPairingVitestJob( 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"); @@ -6098,22 +4296,11 @@ function validateOpenClawDiscordPairingVitestJob( '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", ); @@ -6128,17 +4315,8 @@ function validateOpenClawDiscordPairingVitestJob( 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"); @@ -6153,29 +4331,17 @@ function validateOpenClawDiscordPairingVitestJob( "Run OpenClaw Discord pairing live test", ); const runVitestEnv = asRecord(runVitest?.env); - 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", - ); - } - requireRunContains( - errors, - runVitest, - "npx vitest run --project e2e-scenarios-live", - ); - requireRunContains( + requireHostedInferenceCredentialEnv( errors, - runVitest, - "test/e2e-scenario/live/openclaw-discord-pairing.test.ts", + "openclaw-discord-pairing-vitest step", + runVitestEnv, + { jobName }, ); + if (runVitestEnv.DISCORD_BOT_TOKEN !== "test-fake-discord-pairing-e2e") { + 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"); const upload = requireJobStep( errors, @@ -6183,18 +4349,10 @@ function validateOpenClawDiscordPairingVitestJob( 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", @@ -6206,30 +4364,18 @@ function validateOpenClawDiscordPairingVitestJob( ); } 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]); @@ -6242,29 +4388,16 @@ function validateOpenClawSlackPairingVitestJob( 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); @@ -6273,56 +4406,27 @@ function validateOpenClawSlackPairingVitestJob( 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, @@ -6330,11 +4434,7 @@ function validateOpenClawSlackPairingVitestJob( 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"); @@ -6351,22 +4451,11 @@ function validateOpenClawSlackPairingVitestJob( '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", ); @@ -6381,17 +4470,8 @@ function validateOpenClawSlackPairingVitestJob( 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"); @@ -6399,60 +4479,25 @@ function validateOpenClawSlackPairingVitestJob( 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 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", - ); - } + requireHostedInferenceCredentialEnv(errors, "openclaw-slack-pairing-vitest step", runVitestEnv, { + jobName, + }); 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", @@ -6464,30 +4509,18 @@ function validateOpenClawSlackPairingVitestJob( ); } 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]); @@ -6501,29 +4534,20 @@ function validateChannelsStopStartVitestJob( } 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 !== @@ -6534,22 +4558,15 @@ function validateChannelsStopStartVitestJob( ); } 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( @@ -6557,27 +4574,19 @@ function validateChannelsStopStartVitestJob( ); } 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", @@ -6586,12 +4595,7 @@ function validateChannelsStopStartVitestJob( "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); @@ -6600,55 +4604,25 @@ function validateChannelsStopStartVitestJob( 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( @@ -6667,13 +4641,8 @@ function validateChannelsStopStartVitestJob( 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, @@ -6681,38 +4650,20 @@ function validateChannelsStopStartVitestJob( 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, @@ -6720,85 +4671,35 @@ function validateChannelsStopStartVitestJob( runVitestEnv, "NVIDIA_API_KEY", ); + requireHostedInferenceCredentialEnv(errors, "channels-stop-start-vitest step", runVitestEnv, { + jobName, + }); if ( - runVitestEnv.NVIDIA_INFERENCE_API_KEY !== - "${{ secrets.NVIDIA_INFERENCE_API_KEY }}" - ) { - errors.push( - "channels-stop-start-vitest step must receive NVIDIA_INFERENCE_API_KEY from secrets", - ); - } - if ( - runVitestEnv.TELEGRAM_BOT_TOKEN !== - "test-fake-telegram-token-stop-start-${{ matrix.agent }}" + 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", - ); + errors.push("channels-stop-start-vitest step must set the fake Telegram 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.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_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_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.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( @@ -6807,40 +4708,24 @@ function validateChannelsStopStartVitestJob( "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", - ); - } - - const cleanup = requireJobStep( - errors, - jobName, - steps, - "Clean up Docker auth", - ); + if (uploadWith["retention-days"] !== 14) { + errors.push("channels-stop-start-vitest artifact upload retention-days must be 14"); + } + + 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]); @@ -6853,29 +4738,16 @@ function validateTelegramInjectionVitestJob( 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); @@ -6884,32 +4756,12 @@ function validateTelegramInjectionVitestJob( 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"); @@ -6927,22 +4779,11 @@ function validateTelegramInjectionVitestJob( '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", ); @@ -6957,17 +4798,8 @@ function validateTelegramInjectionVitestJob( 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"); @@ -6975,71 +4807,31 @@ function validateTelegramInjectionVitestJob( 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 runVitest = requireJobStep(errors, jobName, 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, + requireHostedInferenceCredentialEnv(errors, "telegram-injection-vitest step", runVitestEnv, { jobName, - steps, - "Upload Telegram injection artifacts", - ); + }); + 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 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}"'); @@ -7053,38 +4845,25 @@ 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); @@ -7163,35 +4942,17 @@ 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", @@ -7211,16 +4972,9 @@ 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", ); @@ -7238,15 +4992,9 @@ 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, @@ -7254,11 +5002,7 @@ 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"); @@ -7269,11 +5013,7 @@ 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, @@ -7323,16 +5063,9 @@ 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}"'); @@ -7356,17 +5089,21 @@ export function validateE2eVitestScenariosWorkflowBoundary( } const permissions = asRecord(workflow.permissions); - if (permissions.contents !== "read") - errors.push("workflow permissions.contents must be read"); + 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); 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"); } @@ -7374,10 +5111,7 @@ 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); @@ -7385,31 +5119,21 @@ 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( @@ -7422,42 +5146,18 @@ 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"); @@ -7470,8 +5170,7 @@ 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"); } @@ -7479,24 +5178,17 @@ 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); @@ -7504,26 +5196,15 @@ 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); @@ -7538,9 +5219,7 @@ 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) { @@ -7559,24 +5238,11 @@ export function validateE2eVitestScenariosWorkflowBoundary( if (runVitestEnv.SCENARIO_ID !== "${{ matrix.id }}") { errors.push("Vitest step must pass matrix.id through SCENARIO_ID env"); } - 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", - ); + 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"); requireRunContains(errors, runVitest, '"^${SCENARIO_ID}$"'); const summary = requireStep(errors, steps, "Summarize artifacts"); @@ -7585,9 +5251,7 @@ 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( @@ -7595,11 +5259,7 @@ 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"); @@ -7639,26 +5299,12 @@ 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) { @@ -7681,12 +5327,7 @@ 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); @@ -7770,15 +5411,9 @@ 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, @@ -7791,18 +5426,12 @@ 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", @@ -7839,9 +5468,7 @@ 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( @@ -7853,10 +5480,7 @@ 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}`,