E2E main #1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | |
| # SPDX-License-Identifier: Apache-2.0 | |
| name: E2E | |
| run-name: "${{ inputs.checkout_sha != '' && format('E2E PR #{0} ({1})', inputs.pr_number, inputs.correlation_id) || inputs.correlation_id != '' && format('E2E {0} ({1})', github.ref_name, inputs.correlation_id) || format('E2E {0}', github.ref_name) }}" | |
| on: | |
| push: | |
| branches: [main] | |
| workflow_dispatch: | |
| inputs: | |
| targets: | |
| description: "Optional comma-separated typed target IDs. On trusted main runs, empty jobs and targets select all supported registry E2E tests. PR revision runs use the trusted controller target matrix." | |
| required: false | |
| default: "" | |
| type: string | |
| jobs: | |
| description: "Optional comma-separated E2E test IDs. Empty selectors choose the default suite. Set include_staging_brev_launchable for Launchable. Jetson dispatch and DGX Spark require their opt-in flags. PR revisions use the trusted controller matrix." | |
| required: false | |
| default: "" | |
| type: string | |
| include_staging_brev_launchable: | |
| description: "Include Exact staging Brev Launchable in a full E2E run when jobs and targets are empty." | |
| required: false | |
| default: false | |
| type: boolean | |
| release_qualification_waived_jobs: | |
| description: "Admin-only comma-separated release-required E2E job IDs to waive. Leave empty to require every release E2E job." | |
| required: false | |
| default: "" | |
| type: string | |
| release_qualification_waiver_reason: | |
| description: "Admin-only reason for release qualification waived jobs. Required when release_qualification_waived_jobs is not empty." | |
| required: false | |
| default: "" | |
| type: string | |
| inference_mode: | |
| description: "Inference adapter mode for compatible Vitest E2E jobs: mock, internal-nvidia, or public-nvidia." | |
| required: false | |
| default: "mock" | |
| type: choice | |
| options: | |
| - mock | |
| - internal-nvidia | |
| - public-nvidia | |
| allow_jetson_dispatch: | |
| description: "Set true for a manual jetson-nvmap-gpu run. Main pushes dispatch it automatically. The operator-owned dispatch backend must be available, and JETSON_DISPATCH_URL must contain its verified HTTPS origin. Refer to test/e2e/docs/jetson-dispatch.md." | |
| required: false | |
| default: false | |
| type: boolean | |
| allow_dgx_spark_runner_queue: | |
| description: "Before setting true for llama-cpp-dgx-spark-qualification, obtain repository administrator confirmation of an online DGX Spark runner in the authoritative NVIDIA/NemoClaw Settings -> Actions -> Runners inventory; queued jobs do not honor timeout-minutes before assignment." | |
| required: false | |
| default: false | |
| type: boolean | |
| pr_number: | |
| description: Optional current PR number for manual exact-revision E2E. | |
| required: false | |
| type: string | |
| default: "" | |
| post_to_slack: | |
| description: Post a selective-dispatch scorecard to the preview Slack route. | |
| required: false | |
| default: false | |
| type: boolean | |
| checkout_sha: | |
| description: Optional lowercase 40-character latest PR commit SHA for manual E2E. | |
| required: false | |
| default: "" | |
| type: string | |
| checkout_repository: | |
| description: Optional PR source repository for manual E2E. | |
| required: false | |
| default: "" | |
| type: string | |
| review_reason: | |
| description: Optional review reason containing 10 to 500 printable characters. | |
| required: false | |
| default: "" | |
| type: string | |
| base_sha: | |
| description: Optional lowercase PR base SHA for manual exact-revision E2E. | |
| required: false | |
| default: "" | |
| type: string | |
| workflow_sha: | |
| description: Optional trusted main workflow SHA for manual E2E. | |
| required: false | |
| default: "" | |
| type: string | |
| correlation_id: | |
| description: Run correlation ID (UUIDv4). | |
| required: false | |
| default: "" | |
| type: string | |
| permissions: | |
| checks: read | |
| contents: read | |
| pull-requests: read | |
| concurrency: | |
| group: e2e-${{ github.ref }}-${{ inputs.checkout_sha != '' && format('pr-{0}', inputs.pr_number) || (inputs.include_staging_brev_launchable && inputs.jobs == '' && inputs.targets == '' && format('full-{0}', github.run_id)) || inputs.targets || 'supported' }}-${{ inputs.checkout_sha != '' && 'manual-pr' || inputs.jobs || 'all-jobs' }} | |
| cancel-in-progress: ${{ inputs.checkout_sha != '' && !inputs.allow_jetson_dispatch }} | |
| env: | |
| NEMOCLAW_E2E_EXPECTED_SHA: ${{ inputs.checkout_sha }} | |
| NEMOCLAW_E2E_CORRELATION_ID: ${{ inputs.correlation_id }} | |
| NEMOCLAW_E2E_SHARD: default | |
| jobs: | |
| base-image-publication: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 55 | |
| outputs: | |
| dcode_base_contract: ${{ steps.validate_dcode_base.outputs.contract }} | |
| dcode_base_ref: ${{ steps.validate_dcode_base.outputs.base_ref }} | |
| permissions: | |
| actions: read | |
| contents: read | |
| steps: | |
| - id: publication_mode | |
| name: Classify base-image publication requirement | |
| env: | |
| CHECKOUT_SHA: ${{ inputs.checkout_sha }} | |
| EVENT_NAME: ${{ github.event_name }} | |
| REF: ${{ github.ref }} | |
| REPOSITORY: ${{ github.repository }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| case "${REPOSITORY}:${REF}:${EVENT_NAME}:${CHECKOUT_SHA:+controller}" in | |
| NVIDIA/NemoClaw:refs/heads/main:push:|NVIDIA/NemoClaw:refs/heads/main:workflow_dispatch:) | |
| required=1 | |
| ;; | |
| NVIDIA/NemoClaw:refs/heads/main:workflow_dispatch:controller) | |
| required=1 | |
| ;; | |
| *) | |
| echo "::error::base-image publication mode is not trusted" >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| printf 'required=%s\n' "${required}" >> "${GITHUB_OUTPUT}" | |
| - name: Check out trusted E2E workflow | |
| if: ${{ steps.publication_mode.outputs.required == '1' }} | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ github.sha }} | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Set up Node for publication verification | |
| if: ${{ steps.publication_mode.outputs.required == '1' }} | |
| uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: 22 | |
| - id: publication | |
| name: Verify applicable base-image publication | |
| if: ${{ steps.publication_mode.outputs.required == '1' }} | |
| env: | |
| EXPECTED_SHA: ${{ github.sha }} | |
| GITHUB_TOKEN: ${{ github.token }} | |
| run: node --experimental-strip-types --no-warnings tools/e2e/base-image-publication.mts --wait-seconds 3000 --poll-seconds 30 | |
| - name: Download immutable Deep Agents Code base contract | |
| if: ${{ steps.publication_mode.outputs.required == '1' }} | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| PUBLICATION_HEAD_SHA: ${{ steps.publication.outputs.head_sha }} | |
| PUBLICATION_RUN_ATTEMPT: ${{ steps.publication.outputs.run_attempt }} | |
| PUBLICATION_RUN_ID: ${{ steps.publication.outputs.run_id }} | |
| run: node --experimental-strip-types --no-warnings tools/e2e/exact-artifact-download.mts "${RUNNER_TEMP}/dcode-base-contract" | |
| - id: validate_dcode_base | |
| name: Validate immutable Deep Agents Code base | |
| if: ${{ steps.publication_mode.outputs.required == '1' }} | |
| env: | |
| PUBLICATION_HEAD_SHA: ${{ steps.publication.outputs.head_sha }} | |
| PUBLICATION_RUN_ATTEMPT: ${{ steps.publication.outputs.run_attempt }} | |
| PUBLICATION_RUN_ID: ${{ steps.publication.outputs.run_id }} | |
| run: node --experimental-strip-types --no-warnings tools/e2e/dcode-base-image-contract.mts "${RUNNER_TEMP}/dcode-base-contract/contract.json" | |
| generate-matrix: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| outputs: | |
| cli_artifact_provenance: ${{ steps.record_cli_artifact.outputs.provenance }} | |
| e2e_credentials_allowed: ${{ steps.e2e_credentials.outputs.allowed }} | |
| matrix: ${{ steps.matrix.outputs.matrix }} | |
| test_matrix: ${{ steps.matrix.outputs.test_matrix }} | |
| hermes_selected: ${{ steps.matrix.outputs.hermes_selected }} | |
| explicit_only_jobs: ${{ steps.matrix.outputs.explicit_only_jobs }} | |
| release_required_jobs: ${{ steps.matrix.outputs.release_required_jobs }} | |
| release_qualification_waived_jobs: ${{ steps.matrix.outputs.release_qualification_waived_jobs }} | |
| selected_jobs: ${{ steps.matrix.outputs.selected_jobs }} | |
| selected_workflow_jobs: ${{ steps.matrix.outputs.selected_workflow_jobs }} | |
| catalogue_standard_matrix: ${{ steps.matrix.outputs.catalogue_standard_matrix }} | |
| catalogue_nvidia_api_matrix: ${{ steps.matrix.outputs.catalogue_nvidia_api_matrix }} | |
| catalogue_nvidia_inference_matrix: ${{ steps.matrix.outputs.catalogue_nvidia_inference_matrix }} | |
| catalogue_github_read_matrix: ${{ steps.matrix.outputs.catalogue_github_read_matrix }} | |
| catalogue_brave_nvidia_inference_matrix: ${{ steps.matrix.outputs.catalogue_brave_nvidia_inference_matrix }} | |
| runner_routing: ${{ steps.runner_routing.outputs.runner_routing }} | |
| steps: | |
| - id: controller_matrix | |
| name: Build trusted controller target matrix | |
| if: ${{ inputs.checkout_sha != '' }} | |
| env: | |
| JOBS: ${{ inputs.jobs }} | |
| TARGETS: ${{ inputs.targets }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| test_matrix='[]' | |
| case "${JOBS}:${TARGETS}" in | |
| :) | |
| matrix='[{"id":"ubuntu-policy-custom-missing-presets-negative","runner":"ubuntu-latest"},{"id":"ubuntu-repo-cloud-langchain-deepagents-code","runner":"ubuntu-latest"},{"id":"ubuntu-repo-cloud-openclaw","runner":"ubuntu-latest"},{"id":"ubuntu-repo-docker-post-reboot-recovery","runner":"ubuntu-latest"}]' | |
| test_matrix='[{"id":"onboard-managed-image-buildless-e2e","file":"test/onboard-managed-image-buildless-e2e.test.ts","project":"integration"},{"id":"vllm-docker-storage","file":"test/vllm-docker-storage.test.ts","project":"integration"}]' | |
| ;; | |
| inference-routing: | managed-image-protected-runtime: | native-runtime-qualification-producer: | :jetson-nvmap-gpu) | |
| matrix='[]' | |
| ;; | |
| :ubuntu-repo-cloud-langchain-deepagents-code) | |
| matrix='[{"id":"ubuntu-repo-cloud-langchain-deepagents-code","runner":"ubuntu-latest","label":"ubuntu-repo-cloud-langchain-deepagents-code"}]' | |
| ;; | |
| :ubuntu-repo-docker-post-reboot-recovery) | |
| matrix='[{"id":"ubuntu-repo-docker-post-reboot-recovery","runner":"ubuntu-latest","label":"ubuntu-repo-docker-post-reboot-recovery"}]' | |
| ;; | |
| :ubuntu-repo-cloud-langchain-deepagents-code,ubuntu-repo-docker-post-reboot-recovery) | |
| matrix='[{"id":"ubuntu-repo-cloud-langchain-deepagents-code","runner":"ubuntu-latest","label":"ubuntu-repo-cloud-langchain-deepagents-code"},{"id":"ubuntu-repo-docker-post-reboot-recovery","runner":"ubuntu-latest","label":"ubuntu-repo-docker-post-reboot-recovery"}]' | |
| ;; | |
| *) | |
| echo "::error::PR E2E target is not approved by the trusted controller" >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| printf 'matrix=%s\n' "${matrix}" >> "${GITHUB_OUTPUT}" | |
| printf 'test_matrix=%s\n' "${test_matrix}" >> "${GITHUB_OUTPUT}" | |
| - id: runner_routing | |
| name: Build trusted larger-runner routing | |
| env: | |
| CHECKOUT_SHA: ${{ inputs.checkout_sha }} | |
| LARGER_RUNNER_LABEL: ${{ vars.E2E_LARGER_RUNNER_LABEL }} | |
| REF: ${{ github.ref }} | |
| REPOSITORY: ${{ github.repository }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| larger_runner="ubuntu-latest" | |
| if [[ "${REPOSITORY}" == "NVIDIA/NemoClaw" && "${REF}" == "refs/heads/main" && -z "${CHECKOUT_SHA}" && -n "${LARGER_RUNNER_LABEL}" ]]; then | |
| if [[ ! "${LARGER_RUNNER_LABEL}" =~ ^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$ ]]; then | |
| echo "::error::E2E_LARGER_RUNNER_LABEL must be a 1-64 character workflow label using letters, digits, dots, underscores, or hyphens" >&2 | |
| exit 1 | |
| fi | |
| larger_runner="${LARGER_RUNNER_LABEL}" | |
| fi | |
| runner_routing="$(jq -cn --arg standard "ubuntu-latest" --arg larger "${larger_runner}" '{"channels-stop-start-hermes":$larger,"common-egress-agent":$larger,"hermes-discord":$larger,"hermes-e2e":$larger,"hermes-inference-switch":$larger,"hermes-shields-config":$larger,"mcp-bridge-deepagents":$larger,"mcp-bridge-hermes":$larger,"mcp-bridge-openclaw":$standard,"rebuild-hermes":$larger,"rebuild-hermes-stale-base":$larger,"security-posture-hermes":$larger}')" | |
| printf 'runner_routing=%s\n' "${runner_routing}" >> "${GITHUB_OUTPUT}" | |
| - name: Authenticate manual PR dispatch | |
| if: ${{ inputs.checkout_sha != '' }} | |
| env: | |
| ACTOR: ${{ github.actor }} | |
| ALLOW_JETSON_DISPATCH: ${{ inputs.allow_jetson_dispatch && 'true' || 'false' }} | |
| BASE_SHA: ${{ inputs.base_sha }} | |
| CHECKOUT_REPOSITORY: ${{ inputs.checkout_repository }} | |
| CHECKOUT_SHA: ${{ inputs.checkout_sha }} | |
| GITHUB_TOKEN: ${{ github.token }} | |
| EXPECTED_WORKFLOW_SHA: ${{ inputs.workflow_sha }} | |
| INCLUDE_LAUNCHABLE: ${{ inputs.include_staging_brev_launchable }} | |
| JOBS: ${{ inputs.jobs }} | |
| PR_NUMBER: ${{ inputs.pr_number }} | |
| REVIEW_REASON: ${{ inputs.review_reason }} | |
| RUN_ATTEMPT: ${{ github.run_attempt }} | |
| TARGETS: ${{ inputs.targets }} | |
| TRIGGERING_ACTOR: ${{ github.triggering_actor }} | |
| WORKFLOW_EVENT: ${{ github.event_name }} | |
| WORKFLOW_REF: ${{ github.ref }} | |
| WORKFLOW_SHA: ${{ github.workflow_sha }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| read_collaborator_permission() { | |
| local maintainer="$1" | |
| local attempt curl_exit failure http_status permission_file | |
| permission_file="$(mktemp "${RUNNER_TEMP:-/tmp}/nemoclaw-collaborator-permission.XXXXXX")" | |
| for attempt in 1 2 3; do | |
| : >"$permission_file" | |
| if http_status="$(curl --silent --proto '=https' --connect-timeout 10 --max-time 30 \ | |
| --output "$permission_file" --write-out "%{http_code}" \ | |
| --header "Authorization: Bearer ${GITHUB_TOKEN}" \ | |
| --header "Accept: application/vnd.github+json" \ | |
| --header "X-GitHub-Api-Version: 2022-11-28" \ | |
| "https://api.github.com/repos/${GITHUB_REPOSITORY}/collaborators/${maintainer}/permission" \ | |
| 2>/dev/null)"; then | |
| if [[ "$http_status" =~ ^2[0-9]{2}$ ]]; then | |
| if jq -e 'type == "object" and (.user.login | type == "string") and (.role_name | type == "string")' "$permission_file" >/dev/null 2>&1; then | |
| if (( attempt > 1 )); then | |
| echo "::notice::Collaborator permission read passed after retry on attempt ${attempt}/3" >&2 | |
| fi | |
| cat "$permission_file" | |
| rm -f "$permission_file" | |
| return 0 | |
| fi | |
| echo "::error::Collaborator permission read attempt ${attempt}/3 failed: malformed response" >&2 | |
| rm -f "$permission_file" | |
| return 1 | |
| fi | |
| if [[ "$http_status" =~ ^[0-9]{3}$ ]]; then | |
| failure="HTTP ${http_status}" | |
| case "$http_status" in | |
| 408 | 429 | 5??) ;; | |
| *) echo "::error::Collaborator permission read attempt ${attempt}/3 failed: ${failure}" >&2; rm -f "$permission_file"; return 1 ;; | |
| esac | |
| else | |
| echo "::error::Collaborator permission read attempt ${attempt}/3 failed: invalid HTTP status" >&2 | |
| rm -f "$permission_file" | |
| return 1 | |
| fi | |
| else | |
| curl_exit=$? | |
| case "$curl_exit" in | |
| 5 | 6 | 7 | 16 | 18 | 28 | 35 | 52 | 55 | 56 | 92 | 95 | 96) failure="transport" ;; | |
| *) echo "::error::Collaborator permission read attempt ${attempt}/3 failed: curl exit ${curl_exit}" >&2; rm -f "$permission_file"; return 1 ;; | |
| esac | |
| fi | |
| if (( attempt == 3 )); then | |
| echo "::error::Collaborator permission read exhausted after attempt ${attempt}/3: ${failure}" >&2 | |
| rm -f "$permission_file" | |
| return 1 | |
| fi | |
| echo "::warning::Collaborator permission read attempt ${attempt}/3 failed: ${failure}; retrying" >&2 | |
| sleep "$attempt" | |
| done | |
| } | |
| require_maintainer() { | |
| local maintainer="$1" | |
| [[ "$maintainer" =~ ^[A-Za-z0-9-]{1,39}$ && "$maintainer" != -* && "$maintainer" != *- ]] || { | |
| echo "::error::Manual PR E2E actor is invalid" >&2 | |
| exit 1 | |
| } | |
| local permission_json | |
| permission_json="$(read_collaborator_permission "$maintainer")" | |
| if [[ "$(jq -r '.user.login // ""' <<< "$permission_json" | tr '[:upper:]' '[:lower:]')" != "$(tr '[:upper:]' '[:lower:]' <<< "$maintainer")" ]]; then | |
| echo "::error::Manual PR E2E permission response did not match the actor" >&2 | |
| exit 1 | |
| fi | |
| case "$(jq -r '.role_name // ""' <<< "$permission_json")" in | |
| maintain | admin) ;; | |
| *) echo "::error::Manual PR E2E requires a repository maintainer or administrator" >&2; exit 1 ;; | |
| esac | |
| } | |
| [[ "$WORKFLOW_EVENT" == "workflow_dispatch" && "$WORKFLOW_REF" == "refs/heads/main" ]] || { | |
| echo "::error::Manual PR E2E must be dispatched from main" >&2 | |
| exit 1 | |
| } | |
| [[ "$RUN_ATTEMPT" == "1" ]] || { echo "::error::Manual PR E2E cannot be rerun" >&2; exit 1; } | |
| case "${JOBS}:${TARGETS}:${INCLUDE_LAUNCHABLE}:${ALLOW_JETSON_DISPATCH}" in | |
| ::false:false | inference-routing::false:false | managed-image-protected-runtime::false:false | native-runtime-qualification-producer::false:false | :jetson-nvmap-gpu:false:true) ;; | |
| *) | |
| echo "::error::Manual PR E2E accepts only empty selectors, inference-routing, managed-image-protected-runtime, native-runtime-qualification-producer, or jetson-nvmap-gpu with its dispatch flag" >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| [[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]] || { echo "::error::pr_number must be a positive integer" >&2; exit 1; } | |
| [[ "$CHECKOUT_REPOSITORY" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]] || { echo "::error::checkout_repository must be an owner/repository name" >&2; exit 1; } | |
| [[ "$CHECKOUT_SHA" =~ ^[a-f0-9]{40}$ ]] || { echo "::error::checkout_sha must be a lowercase 40-character SHA" >&2; exit 1; } | |
| [[ "$BASE_SHA" =~ ^[a-f0-9]{40}$ ]] || { echo "::error::base_sha must be a lowercase 40-character SHA" >&2; exit 1; } | |
| [[ "$REVIEW_REASON" =~ ^[[:print:]]+$ ]] && | |
| (( ${#REVIEW_REASON} >= 10 && ${#REVIEW_REASON} <= 500 )) || | |
| { echo "::error::review_reason must contain 10 to 500 printable characters" >&2; exit 1; } | |
| [[ "$EXPECTED_WORKFLOW_SHA" =~ ^[a-f0-9]{40}$ && "$EXPECTED_WORKFLOW_SHA" == "$WORKFLOW_SHA" ]] || { echo "::error::workflow_sha must match the trusted main workflow SHA" >&2; exit 1; } | |
| require_maintainer "$ACTOR" | |
| if [[ "$(printf '%s' "$TRIGGERING_ACTOR" | tr '[:upper:]' '[:lower:]')" != "$(printf '%s' "$ACTOR" | tr '[:upper:]' '[:lower:]')" ]]; then | |
| require_maintainer "$TRIGGERING_ACTOR" | |
| fi | |
| pull_json="$(curl --fail --silent --show-error --proto '=https' \ | |
| --header "Authorization: Bearer ${GITHUB_TOKEN}" \ | |
| --header "Accept: application/vnd.github+json" \ | |
| --header "X-GitHub-Api-Version: 2022-11-28" \ | |
| "https://api.github.com/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}")" | |
| [[ "$(jq -r '.state' <<< "$pull_json")" == "open" ]] || { echo "::error::pull request must be open" >&2; exit 1; } | |
| [[ "$(jq -r '.head.repo.full_name // ""' <<< "$pull_json")" == "$CHECKOUT_REPOSITORY" ]] || { echo "::error::checkout_repository must match the PR source repository" >&2; exit 1; } | |
| [[ "$(jq -r '.head.sha' <<< "$pull_json")" == "$CHECKOUT_SHA" ]] || { echo "::error::checkout_sha must match the latest PR commit SHA" >&2; exit 1; } | |
| [[ "$(jq -r '.base.sha' <<< "$pull_json")" == "$BASE_SHA" ]] || { echo "::error::base_sha must match the PR base SHA" >&2; exit 1; } | |
| - name: Authorize release qualification waiver | |
| if: ${{ github.event_name == 'workflow_dispatch' && (inputs.release_qualification_waived_jobs != '' || inputs.release_qualification_waiver_reason != '') }} | |
| env: | |
| ACTOR: ${{ github.actor }} | |
| ALLOW_DGX_SPARK_RUNNER_QUEUE: ${{ inputs.allow_dgx_spark_runner_queue && 'true' || 'false' }} | |
| ALLOW_JETSON_DISPATCH: ${{ inputs.allow_jetson_dispatch && 'true' || 'false' }} | |
| CHECKOUT_SHA: ${{ inputs.checkout_sha }} | |
| GITHUB_TOKEN: ${{ github.token }} | |
| INCLUDE_LAUNCHABLE: ${{ inputs.include_staging_brev_launchable && 'true' || 'false' }} | |
| JOBS: ${{ inputs.jobs }} | |
| TARGETS: ${{ inputs.targets }} | |
| TRIGGERING_ACTOR: ${{ github.triggering_actor }} | |
| WAIVED_JOBS: ${{ inputs.release_qualification_waived_jobs }} | |
| WAIVER_REASON: ${{ inputs.release_qualification_waiver_reason }} | |
| WORKFLOW_REF: ${{ github.ref }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| read_collaborator_permission() { | |
| local administrator="$1" | |
| local attempt curl_exit failure http_status permission_file | |
| permission_file="$(mktemp "${RUNNER_TEMP:-/tmp}/nemoclaw-collaborator-permission.XXXXXX")" | |
| for attempt in 1 2 3; do | |
| : >"$permission_file" | |
| if http_status="$(curl --silent --proto '=https' --connect-timeout 10 --max-time 30 \ | |
| --output "$permission_file" --write-out "%{http_code}" \ | |
| --header "Authorization: Bearer ${GITHUB_TOKEN}" \ | |
| --header "Accept: application/vnd.github+json" \ | |
| --header "X-GitHub-Api-Version: 2022-11-28" \ | |
| "https://api.github.com/repos/${GITHUB_REPOSITORY}/collaborators/${administrator}/permission" \ | |
| 2>/dev/null)"; then | |
| if [[ "$http_status" =~ ^2[0-9]{2}$ ]]; then | |
| if jq -e 'type == "object" and (.user.login | type == "string") and (.role_name | type == "string")' "$permission_file" >/dev/null 2>&1; then | |
| if (( attempt > 1 )); then | |
| echo "::notice::Collaborator permission read passed after retry on attempt ${attempt}/3" >&2 | |
| fi | |
| cat "$permission_file" | |
| rm -f "$permission_file" | |
| return 0 | |
| fi | |
| echo "::error::Collaborator permission read attempt ${attempt}/3 failed: malformed response" >&2 | |
| rm -f "$permission_file" | |
| return 1 | |
| fi | |
| if [[ "$http_status" =~ ^[0-9]{3}$ ]]; then | |
| failure="HTTP ${http_status}" | |
| case "$http_status" in | |
| 408 | 429 | 5??) ;; | |
| *) echo "::error::Collaborator permission read attempt ${attempt}/3 failed: ${failure}" >&2; rm -f "$permission_file"; return 1 ;; | |
| esac | |
| else | |
| echo "::error::Collaborator permission read attempt ${attempt}/3 failed: invalid HTTP status" >&2 | |
| rm -f "$permission_file" | |
| return 1 | |
| fi | |
| else | |
| curl_exit=$? | |
| case "$curl_exit" in | |
| 5 | 6 | 7 | 16 | 18 | 28 | 35 | 52 | 55 | 56 | 92 | 95 | 96) failure="transport" ;; | |
| *) echo "::error::Collaborator permission read attempt ${attempt}/3 failed: curl exit ${curl_exit}" >&2; rm -f "$permission_file"; return 1 ;; | |
| esac | |
| fi | |
| if (( attempt == 3 )); then | |
| echo "::error::Collaborator permission read exhausted after attempt ${attempt}/3: ${failure}" >&2 | |
| rm -f "$permission_file" | |
| return 1 | |
| fi | |
| echo "::warning::Collaborator permission read attempt ${attempt}/3 failed: ${failure}; retrying" >&2 | |
| sleep "$attempt" | |
| done | |
| } | |
| require_admin() { | |
| local administrator="$1" | |
| if [[ ! "$administrator" =~ ^[A-Za-z0-9-]{1,39}$ || "$administrator" == -* || "$administrator" == *- ]]; then | |
| echo "::error::Release qualification waiver actor is invalid" >&2 | |
| exit 1 | |
| fi | |
| local permission_json | |
| permission_json="$(read_collaborator_permission "$administrator")" | |
| if [[ "$(jq -r '.user.login // ""' <<< "$permission_json" | tr '[:upper:]' '[:lower:]')" != "$(tr '[:upper:]' '[:lower:]' <<< "$administrator")" ]]; then | |
| echo "::error::Release qualification waiver permission response did not match the actor" >&2 | |
| exit 1 | |
| fi | |
| [[ "$(jq -r '.role_name // ""' <<< "$permission_json")" == "admin" ]] || { | |
| echo "::error::Release qualification waiver requires a repository administrator" >&2 | |
| exit 1 | |
| } | |
| } | |
| [[ "$WORKFLOW_REF" == "refs/heads/main" ]] || { | |
| echo "::error::Release qualification waiver must be dispatched from main" >&2 | |
| exit 1 | |
| } | |
| [[ -z "$CHECKOUT_SHA" && -z "$JOBS" && -z "$TARGETS" ]] || { | |
| echo "::error::Release qualification waiver requires an exact main release run with empty selectors" >&2 | |
| exit 1 | |
| } | |
| [[ "$INCLUDE_LAUNCHABLE" == "true" && "$ALLOW_JETSON_DISPATCH" == "false" && "$ALLOW_DGX_SPARK_RUNNER_QUEUE" == "false" ]] || { | |
| echo "::error::Release qualification waiver conflicts with another release-run override" >&2 | |
| exit 1 | |
| } | |
| waived_jobs_pattern='^[a-z0-9]+(-[a-z0-9]+)*(,[a-z0-9]+(-[a-z0-9]+)*)*$' | |
| [[ "$WAIVED_JOBS" =~ $waived_jobs_pattern ]] || { | |
| echo "::error::release_qualification_waived_jobs must contain comma-separated E2E job IDs" >&2 | |
| exit 1 | |
| } | |
| waiver_reason_pattern="^[A-Za-z0-9][A-Za-z0-9 .,:;/_()'-]*$" | |
| [[ ${#WAIVER_REASON} -ge 10 && ${#WAIVER_REASON} -le 500 && "$WAIVER_REASON" =~ $waiver_reason_pattern ]] || { | |
| echo "::error::release_qualification_waiver_reason must start with a letter or number and contain 10 to 500 allowed ASCII characters" >&2 | |
| exit 1 | |
| } | |
| require_admin "$ACTOR" | |
| if [[ "$(printf '%s' "$TRIGGERING_ACTOR" | tr '[:upper:]' '[:lower:]')" != "$(printf '%s' "$ACTOR" | tr '[:upper:]' '[:lower:]')" ]]; then | |
| require_admin "$TRIGGERING_ACTOR" | |
| fi | |
| - name: Record trusted E2E dispatch receipt | |
| if: ${{ github.event_name == 'workflow_dispatch' }} | |
| env: | |
| ACTOR: ${{ github.actor }} | |
| ALLOW_DGX_SPARK_RUNNER_QUEUE: ${{ inputs.allow_dgx_spark_runner_queue && 'true' || 'false' }} | |
| ALLOW_JETSON_DISPATCH: ${{ inputs.allow_jetson_dispatch && 'true' || 'false' }} | |
| ALLOW_JETSON_RUNNER_QUEUE: "false" | |
| BASE_SHA: ${{ inputs.checkout_sha != '' && inputs.base_sha || github.sha }} | |
| CANDIDATE_REPOSITORY: ${{ inputs.checkout_repository || github.repository }} | |
| CANDIDATE_SHA: ${{ inputs.checkout_sha || github.sha }} | |
| DISPATCH_JOBS: ${{ inputs.jobs }} | |
| DISPATCH_RECEIPT_DIR: ${{ runner.temp }}/nemoclaw-e2e-dispatch | |
| DISPATCH_TARGETS: ${{ inputs.targets }} | |
| EVENT_NAME: ${{ github.event_name }} | |
| INCLUDE_STAGING_BREV_LAUNCHABLE: ${{ inputs.include_staging_brev_launchable && 'true' || 'false' }} | |
| PR_NUMBER: ${{ inputs.checkout_sha != '' && inputs.pr_number || '' }} | |
| REPOSITORY: ${{ github.repository }} | |
| RUN_ATTEMPT: ${{ github.run_attempt }} | |
| RUN_ID: ${{ github.run_id }} | |
| RELEASE_QUALIFICATION_WAIVED_JOBS: ${{ inputs.release_qualification_waived_jobs }} | |
| RELEASE_QUALIFICATION_WAIVER_REASON: ${{ inputs.release_qualification_waiver_reason }} | |
| TRIGGERING_ACTOR: ${{ github.triggering_actor }} | |
| WORKFLOW_SHA: ${{ github.workflow_sha }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| install -d -m 0700 "$DISPATCH_RECEIPT_DIR" | |
| jq -n \ | |
| --arg actor "$ACTOR" \ | |
| --arg baseSha "$BASE_SHA" \ | |
| --arg candidateRepository "$CANDIDATE_REPOSITORY" \ | |
| --arg candidateSha "$CANDIDATE_SHA" \ | |
| --arg eventName "$EVENT_NAME" \ | |
| --arg jobs "$DISPATCH_JOBS" \ | |
| --arg prNumber "$PR_NUMBER" \ | |
| --arg repository "$REPOSITORY" \ | |
| --arg releaseQualificationWaivedJobs "$RELEASE_QUALIFICATION_WAIVED_JOBS" \ | |
| --arg releaseQualificationWaiverReason "$RELEASE_QUALIFICATION_WAIVER_REASON" \ | |
| --arg targets "$DISPATCH_TARGETS" \ | |
| --arg triggeringActor "$TRIGGERING_ACTOR" \ | |
| --arg workflowRunId "$RUN_ID" \ | |
| --arg workflowSha "$WORKFLOW_SHA" \ | |
| --argjson allowDgxSparkRunnerQueue "$ALLOW_DGX_SPARK_RUNNER_QUEUE" \ | |
| --argjson allowJetsonDispatch "$ALLOW_JETSON_DISPATCH" \ | |
| --argjson allowJetsonRunnerQueue "$ALLOW_JETSON_RUNNER_QUEUE" \ | |
| --argjson includeStagingBrevLaunchable "$INCLUDE_STAGING_BREV_LAUNCHABLE" \ | |
| --argjson workflowRunAttempt "$RUN_ATTEMPT" \ | |
| '{ | |
| kind: "nemoclaw-e2e-dispatch-v2", | |
| actor: $actor, | |
| repository: $repository, | |
| prNumber: (if $prNumber == "" then null else ($prNumber | tonumber) end), | |
| candidateRepository: $candidateRepository, | |
| candidateSha: $candidateSha, | |
| baseSha: $baseSha, | |
| workflowSha: $workflowSha, | |
| workflowRunId: $workflowRunId, | |
| workflowRunAttempt: $workflowRunAttempt, | |
| eventName: $eventName, | |
| jobs: $jobs, | |
| targets: $targets, | |
| allowDgxSparkRunnerQueue: $allowDgxSparkRunnerQueue, | |
| allowJetsonDispatch: $allowJetsonDispatch, | |
| allowJetsonRunnerQueue: $allowJetsonRunnerQueue, | |
| includeStagingBrevLaunchable: $includeStagingBrevLaunchable, | |
| releaseQualificationWaivedJobs: (if $releaseQualificationWaivedJobs == "" then [] else ($releaseQualificationWaivedJobs | split(",")) end), | |
| releaseQualificationWaiverReason: (if $releaseQualificationWaiverReason == "" then null else $releaseQualificationWaiverReason end), | |
| triggeringActor: $triggeringActor, | |
| emptySelectors: ($jobs == "" and $targets == "") | |
| }' >"$DISPATCH_RECEIPT_DIR/dispatch.json" | |
| - name: Upload trusted E2E dispatch receipt | |
| if: ${{ github.event_name == 'workflow_dispatch' }} | |
| uses: NVIDIA/NemoClaw/.github/actions/upload-e2e-artifacts@7768e15eb90d3ee2d33432f481dfe8747e4f6d57 | |
| with: | |
| name: e2e-dispatch-${{ github.run_id }}-${{ github.run_attempt }} | |
| path: ${{ runner.temp }}/nemoclaw-e2e-dispatch/dispatch.json | |
| - name: Authorize Launchable E2E maintainer dispatch | |
| if: ${{ github.event_name == 'workflow_dispatch' && ((inputs.jobs == 'staging-brev-launchable' && inputs.targets == '') || (inputs.include_staging_brev_launchable && inputs.jobs == '' && inputs.targets == '')) }} | |
| env: | |
| ACTOR: ${{ github.actor }} | |
| GITHUB_TOKEN: ${{ github.token }} | |
| TRIGGERING_ACTOR: ${{ github.triggering_actor }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| read_collaborator_permission() { | |
| local maintainer="$1" | |
| local attempt curl_exit failure http_status permission_file | |
| permission_file="$(mktemp "${RUNNER_TEMP:-/tmp}/nemoclaw-collaborator-permission.XXXXXX")" | |
| for attempt in 1 2 3; do | |
| : >"$permission_file" | |
| if http_status="$(curl --silent --proto '=https' --connect-timeout 10 --max-time 30 \ | |
| --output "$permission_file" --write-out "%{http_code}" \ | |
| --header "Authorization: Bearer ${GITHUB_TOKEN}" \ | |
| --header "Accept: application/vnd.github+json" \ | |
| --header "X-GitHub-Api-Version: 2022-11-28" \ | |
| "https://api.github.com/repos/${GITHUB_REPOSITORY}/collaborators/${maintainer}/permission" \ | |
| 2>/dev/null)"; then | |
| if [[ "$http_status" =~ ^2[0-9]{2}$ ]]; then | |
| if jq -e 'type == "object" and (.user.login | type == "string") and (.role_name | type == "string")' "$permission_file" >/dev/null 2>&1; then | |
| if (( attempt > 1 )); then | |
| echo "::notice::Collaborator permission read passed after retry on attempt ${attempt}/3" >&2 | |
| fi | |
| cat "$permission_file" | |
| rm -f "$permission_file" | |
| return 0 | |
| fi | |
| echo "::error::Collaborator permission read attempt ${attempt}/3 failed: malformed response" >&2 | |
| rm -f "$permission_file" | |
| return 1 | |
| fi | |
| if [[ "$http_status" =~ ^[0-9]{3}$ ]]; then | |
| failure="HTTP ${http_status}" | |
| case "$http_status" in | |
| 408 | 429 | 5??) ;; | |
| *) echo "::error::Collaborator permission read attempt ${attempt}/3 failed: ${failure}" >&2; rm -f "$permission_file"; return 1 ;; | |
| esac | |
| else | |
| echo "::error::Collaborator permission read attempt ${attempt}/3 failed: invalid HTTP status" >&2 | |
| rm -f "$permission_file" | |
| return 1 | |
| fi | |
| else | |
| curl_exit=$? | |
| case "$curl_exit" in | |
| 5 | 6 | 7 | 16 | 18 | 28 | 35 | 52 | 55 | 56 | 92 | 95 | 96) failure="transport" ;; | |
| *) echo "::error::Collaborator permission read attempt ${attempt}/3 failed: curl exit ${curl_exit}" >&2; rm -f "$permission_file"; return 1 ;; | |
| esac | |
| fi | |
| if (( attempt == 3 )); then | |
| echo "::error::Collaborator permission read exhausted after attempt ${attempt}/3: ${failure}" >&2 | |
| rm -f "$permission_file" | |
| return 1 | |
| fi | |
| echo "::warning::Collaborator permission read attempt ${attempt}/3 failed: ${failure}; retrying" >&2 | |
| sleep "$attempt" | |
| done | |
| } | |
| require_maintainer() { | |
| local maintainer="$1" | |
| if [[ ! "$maintainer" =~ ^[A-Za-z0-9-]{1,39}$ || "$maintainer" == -* || "$maintainer" == *- ]]; then | |
| echo "::error::Launchable E2E actor is invalid" >&2 | |
| exit 1 | |
| fi | |
| local permission_json | |
| permission_json="$(read_collaborator_permission "$maintainer")" | |
| if [[ "$(jq -r '.user.login // ""' <<< "$permission_json" | tr '[:upper:]' '[:lower:]')" != "$(tr '[:upper:]' '[:lower:]' <<< "$maintainer")" ]]; then | |
| echo "::error::Launchable E2E permission response did not match the actor" >&2 | |
| exit 1 | |
| fi | |
| case "$(jq -r '.role_name // ""' <<< "$permission_json")" in | |
| maintain | admin) ;; | |
| *) | |
| echo "::error::Launchable E2E requires a repository maintainer or administrator" >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| } | |
| require_maintainer "$ACTOR" | |
| if [[ "$(printf '%s' "$TRIGGERING_ACTOR" | tr '[:upper:]' '[:lower:]')" != "$(printf '%s' "$ACTOR" | tr '[:upper:]' '[:lower:]')" ]]; then | |
| require_maintainer "$TRIGGERING_ACTOR" | |
| fi | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| if: ${{ inputs.checkout_sha == '' || inputs.jobs != 'native-runtime-qualification-producer' || inputs.targets != '' }} | |
| with: | |
| repository: ${{ inputs.checkout_repository || github.repository }} | |
| ref: ${{ inputs.checkout_sha || github.sha }} | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Validate manual PR checkout | |
| if: ${{ inputs.checkout_sha != '' && (inputs.jobs != 'native-runtime-qualification-producer' || inputs.targets != '') }} | |
| env: | |
| BASE_SHA: ${{ inputs.base_sha }} | |
| CHECKOUT_REPOSITORY: ${{ inputs.checkout_repository }} | |
| CHECKOUT_SHA: ${{ inputs.checkout_sha }} | |
| GITHUB_TOKEN: ${{ github.token }} | |
| PR_NUMBER: ${{ inputs.pr_number }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| [[ "$(git rev-parse --verify HEAD)" == "$CHECKOUT_SHA" ]] || { echo "::error::checked-out commit does not match checkout_sha" >&2; exit 1; } | |
| pull_json="$(curl --fail --silent --show-error --proto '=https' \ | |
| --header "Authorization: Bearer ${GITHUB_TOKEN}" \ | |
| --header "Accept: application/vnd.github+json" \ | |
| --header "X-GitHub-Api-Version: 2022-11-28" \ | |
| "https://api.github.com/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}")" | |
| [[ "$(jq -r '.state' <<< "$pull_json")" == "open" ]] || { echo "::error::pull request must still be open" >&2; exit 1; } | |
| [[ "$(jq -r '.head.repo.full_name // ""' <<< "$pull_json")" == "$CHECKOUT_REPOSITORY" ]] || { echo "::error::checkout_repository changed before execution" >&2; exit 1; } | |
| [[ "$(jq -r '.head.sha' <<< "$pull_json")" == "$CHECKOUT_SHA" ]] || { echo "::error::checkout_sha changed before execution" >&2; exit 1; } | |
| [[ "$(jq -r '.base.sha' <<< "$pull_json")" == "$BASE_SHA" ]] || { echo "::error::base_sha changed before execution" >&2; exit 1; } | |
| - id: e2e_credentials | |
| name: Authorize E2E credentials | |
| if: ${{ inputs.checkout_sha != '' && (inputs.jobs != 'native-runtime-qualification-producer' || inputs.targets != '') }} | |
| env: | |
| CHECKOUT_REPOSITORY: ${{ inputs.checkout_repository }} | |
| CHECKOUT_SHA: ${{ inputs.checkout_sha }} | |
| EVENT_NAME: ${{ github.event_name }} | |
| EXPECTED_WORKFLOW_SHA: ${{ inputs.workflow_sha }} | |
| REF: ${{ github.ref }} | |
| WORKFLOW_REPOSITORY: ${{ github.repository }} | |
| WORKFLOW_SHA: ${{ github.workflow_sha }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| credentials_allowed=false | |
| if [[ "$WORKFLOW_REPOSITORY" == "NVIDIA/NemoClaw" && | |
| "$CHECKOUT_REPOSITORY" == "$WORKFLOW_REPOSITORY" && | |
| "$EVENT_NAME" == "workflow_dispatch" && | |
| "$REF" == "refs/heads/main" && | |
| "$CHECKOUT_SHA" =~ ^[a-f0-9]{40}$ && | |
| "$WORKFLOW_SHA" =~ ^[a-f0-9]{40}$ && | |
| "$EXPECTED_WORKFLOW_SHA" == "$WORKFLOW_SHA" && | |
| "$(git rev-parse --verify HEAD)" == "$CHECKOUT_SHA" ]]; then | |
| credentials_allowed=true | |
| fi | |
| printf 'allowed=%s\n' "$credentials_allowed" >> "$GITHUB_OUTPUT" | |
| - name: Prepare E2E workspace | |
| if: ${{ inputs.checkout_sha == '' || inputs.jobs != 'native-runtime-qualification-producer' || inputs.targets != '' }} | |
| uses: NVIDIA/NemoClaw/.github/actions/prepare-e2e@f6304bc25fc35bfaa441c8c2fbfee38f72805a75 | |
| - id: package_cli_artifact | |
| name: Package exact-commit CLI | |
| if: ${{ inputs.checkout_sha == '' || inputs.jobs != 'native-runtime-qualification-producer' || inputs.targets != '' }} | |
| env: | |
| CANDIDATE_REPOSITORY: ${{ inputs.checkout_repository || github.repository }} | |
| CANDIDATE_SHA: ${{ inputs.checkout_sha || github.sha }} | |
| RUN_ATTEMPT: ${{ github.run_attempt }} | |
| RUN_ID: ${{ github.run_id }} | |
| WORKFLOW_SHA: ${{ github.workflow_sha }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| [[ "$CANDIDATE_REPOSITORY" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]] || | |
| { echo "::error::candidate repository is invalid"; exit 1; } | |
| [[ "$CANDIDATE_SHA" =~ ^[a-f0-9]{40}$ ]] || | |
| { echo "::error::candidate SHA must be a lowercase 40-character SHA"; exit 1; } | |
| [[ "$WORKFLOW_SHA" =~ ^[a-f0-9]{40}$ ]] || | |
| { echo "::error::workflow SHA must be a lowercase 40-character SHA"; exit 1; } | |
| [[ "$(git rev-parse --verify HEAD)" == "$CANDIDATE_SHA" ]] || | |
| { echo "::error::checked-out commit does not match the artifact candidate SHA"; exit 1; } | |
| [[ "$RUN_ATTEMPT" =~ ^[1-9][0-9]*$ && "$RUN_ID" =~ ^[1-9][0-9]*$ ]] || | |
| { echo "::error::workflow run identity is invalid"; exit 1; } | |
| for required_file in dist/nemoclaw.js dist/build-identity.json; do | |
| [[ -f "$required_file" && ! -L "$required_file" && -s "$required_file" ]] || | |
| { echo "::error::candidate CLI build output is missing or is not a nonempty regular file: $required_file"; exit 1; } | |
| done | |
| for boundary in \ | |
| openshell-policy-boundary.cjs \ | |
| sandbox-name.cjs \ | |
| snapshot-sanitizer-boundary.cjs; do | |
| boundary_path="nemoclaw/dist/shared/$boundary" | |
| [[ -f "$boundary_path" && ! -L "$boundary_path" && -s "$boundary_path" ]] || | |
| { echo "::error::candidate CLI build shared module is missing or is not a nonempty regular file: $boundary"; exit 1; } | |
| done | |
| jq -e --arg candidateSha "$CANDIDATE_SHA" ' | |
| type == "object" and | |
| (keys | sort) == ["nemoclawVersion", "sourceRevision"] and | |
| (.nemoclawVersion | strings | length > 0) and | |
| .sourceRevision == $candidateSha | |
| ' dist/build-identity.json >/dev/null || | |
| { echo "::error::candidate CLI build identity does not match the candidate commit SHA"; exit 1; } | |
| artifact_dir="${RUNNER_TEMP}/nemoclaw-cli-artifact" | |
| install -d -m 0700 "$artifact_dir" | |
| payload="$artifact_dir/nemoclaw-cli.tar" | |
| manifest="$artifact_dir/manifest.json" | |
| tar \ | |
| --sort=name \ | |
| --mtime=@0 \ | |
| --owner=0 \ | |
| --group=0 \ | |
| --numeric-owner \ | |
| -cf "$payload" \ | |
| dist \ | |
| nemoclaw/dist/shared | |
| payload_sha256="$(sha256sum "$payload" | awk '{print $1}')" | |
| source_tree="$(git rev-parse 'HEAD^{tree}')" | |
| lockfile_sha256="$(sha256sum package-lock.json | awk '{print $1}')" | |
| node_version="$(node --version)" | |
| npm_version="$(npm --version)" | |
| artifact_name="nemoclaw-cli-${CANDIDATE_SHA}-${payload_sha256}" | |
| jq -n \ | |
| --arg artifactName "$artifact_name" \ | |
| --arg buildCommand "npm run build:cli" \ | |
| --arg candidateRepository "$CANDIDATE_REPOSITORY" \ | |
| --arg candidateSha "$CANDIDATE_SHA" \ | |
| --arg lockfileSha256 "$lockfile_sha256" \ | |
| --arg nodeVersion "$node_version" \ | |
| --arg npmVersion "$npm_version" \ | |
| --arg payloadFile "nemoclaw-cli.tar" \ | |
| --arg payloadSha256 "$payload_sha256" \ | |
| --arg runAttempt "$RUN_ATTEMPT" \ | |
| --arg runId "$RUN_ID" \ | |
| --arg runnerArch "$RUNNER_ARCH" \ | |
| --arg runnerOs "$RUNNER_OS" \ | |
| --arg sourceTree "$source_tree" \ | |
| --arg workflowSha "$WORKFLOW_SHA" \ | |
| '{ | |
| kind: "nemoclaw-e2e-cli-artifact-v1", | |
| artifactName: $artifactName, | |
| candidate: { | |
| repository: $candidateRepository, | |
| sha: $candidateSha, | |
| sourceTree: $sourceTree, | |
| lockfileSha256: $lockfileSha256 | |
| }, | |
| workflow: { | |
| sha: $workflowSha, | |
| runId: $runId, | |
| runAttempt: $runAttempt | |
| }, | |
| toolchain: { | |
| node: $nodeVersion, | |
| npm: $npmVersion, | |
| runnerOs: $runnerOs, | |
| runnerArch: $runnerArch | |
| }, | |
| build: { | |
| command: $buildCommand, | |
| sourceRevision: $candidateSha | |
| }, | |
| payload: { | |
| file: $payloadFile, | |
| sha256: $payloadSha256 | |
| } | |
| }' >"$manifest" | |
| chmod 0600 "$manifest" "$payload" | |
| printf 'artifact_name=%s\n' "$artifact_name" >>"$GITHUB_OUTPUT" | |
| printf 'candidate_sha=%s\n' "$CANDIDATE_SHA" >>"$GITHUB_OUTPUT" | |
| printf 'payload_sha256=%s\n' "$payload_sha256" >>"$GITHUB_OUTPUT" | |
| - id: upload_cli_artifact | |
| name: Publish content-addressed CLI artifact | |
| if: ${{ inputs.checkout_sha == '' || inputs.jobs != 'native-runtime-qualification-producer' || inputs.targets != '' }} | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: ${{ steps.package_cli_artifact.outputs.artifact_name }} | |
| path: ${{ runner.temp }}/nemoclaw-cli-artifact/ | |
| if-no-files-found: error | |
| retention-days: 3 | |
| compression-level: 0 | |
| - id: record_cli_artifact | |
| name: Record CLI artifact provenance | |
| if: ${{ inputs.checkout_sha == '' || inputs.jobs != 'native-runtime-qualification-producer' || inputs.targets != '' }} | |
| env: | |
| ARTIFACT_DIGEST: ${{ steps.upload_cli_artifact.outputs.artifact-digest }} | |
| ARTIFACT_ID: ${{ steps.upload_cli_artifact.outputs.artifact-id }} | |
| ARTIFACT_NAME: ${{ steps.package_cli_artifact.outputs.artifact_name }} | |
| CANDIDATE_REPOSITORY: ${{ inputs.checkout_repository || github.repository }} | |
| CANDIDATE_SHA: ${{ steps.package_cli_artifact.outputs.candidate_sha }} | |
| PAYLOAD_SHA256: ${{ steps.package_cli_artifact.outputs.payload_sha256 }} | |
| RUN_ATTEMPT: ${{ github.run_attempt }} | |
| RUN_ID: ${{ github.run_id }} | |
| WORKFLOW_SHA: ${{ github.workflow_sha }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| [[ "$ARTIFACT_ID" =~ ^[1-9][0-9]*$ ]] || | |
| { echo "::error::artifact upload did not return an immutable artifact ID"; exit 1; } | |
| [[ "$ARTIFACT_DIGEST" =~ ^[a-f0-9]{64}$ ]] || | |
| { echo "::error::artifact upload did not return a SHA-256 digest"; exit 1; } | |
| provenance="$(jq -cn \ | |
| --arg artifactDigest "$ARTIFACT_DIGEST" \ | |
| --arg artifactId "$ARTIFACT_ID" \ | |
| --arg artifactName "$ARTIFACT_NAME" \ | |
| --arg candidateRepository "$CANDIDATE_REPOSITORY" \ | |
| --arg candidateSha "$CANDIDATE_SHA" \ | |
| --arg payloadSha256 "$PAYLOAD_SHA256" \ | |
| --arg runAttempt "$RUN_ATTEMPT" \ | |
| --arg runId "$RUN_ID" \ | |
| --arg workflowSha "$WORKFLOW_SHA" \ | |
| '{ | |
| kind: "nemoclaw-e2e-cli-provenance-v1", | |
| artifactDigest: $artifactDigest, | |
| artifactId: $artifactId, | |
| artifactName: $artifactName, | |
| candidateRepository: $candidateRepository, | |
| candidateSha: $candidateSha, | |
| payloadSha256: $payloadSha256, | |
| workflowSha: $workflowSha, | |
| runId: $runId, | |
| runAttempt: $runAttempt | |
| }')" | |
| printf 'provenance=%s\n' "$provenance" >>"$GITHUB_OUTPUT" | |
| { | |
| echo "## Exact-commit CLI artifact" | |
| echo | |
| echo "- Candidate: \`${CANDIDATE_SHA}\`" | |
| echo "- Artifact: \`${ARTIFACT_NAME}\` (ID \`${ARTIFACT_ID}\`)" | |
| echo "- GitHub archive digest: \`${ARTIFACT_DIGEST}\`" | |
| echo "- Payload digest: \`${PAYLOAD_SHA256}\`" | |
| } >>"$GITHUB_STEP_SUMMARY" | |
| - id: matrix | |
| name: Generate E2E target matrix | |
| env: | |
| CHECKOUT_SHA: ${{ inputs.checkout_sha }} | |
| CONTROLLER_MATRIX: ${{ steps.controller_matrix.outputs.matrix }} | |
| CONTROLLER_TEST_MATRIX: ${{ steps.controller_matrix.outputs.test_matrix }} | |
| INFERENCE_MODE: ${{ inputs.inference_mode || 'mock' }} | |
| JOBS: ${{ inputs.jobs }} | |
| TARGETS: ${{ inputs.targets }} | |
| EVENT_NAME: ${{ github.event_name }} | |
| BEFORE_SHA: ${{ github.event.before }} | |
| CANDIDATE_SHA: ${{ github.sha }} | |
| RELEASE_QUALIFICATION_WAIVED_JOBS: ${{ inputs.release_qualification_waived_jobs }} | |
| run: | | |
| set -euo pipefail | |
| if [[ "${JOBS:-}" == "native-runtime-qualification-producer" && -z "${TARGETS:-}" ]]; then | |
| { | |
| echo 'matrix=[]' | |
| echo 'test_matrix=[]' | |
| echo 'catalogue_standard_matrix=[]' | |
| echo 'catalogue_nvidia_api_matrix=[]' | |
| echo 'catalogue_nvidia_inference_matrix=[]' | |
| echo 'catalogue_github_read_matrix=[]' | |
| echo 'catalogue_brave_nvidia_inference_matrix=[]' | |
| echo 'selected_jobs=["native-runtime-qualification-producer"]' | |
| echo 'selected_workflow_jobs=["native-runtime-qualification-producer"]' | |
| echo 'hermes_selected=false' | |
| echo 'explicit_only_jobs=native-runtime-qualification-producer' | |
| echo 'release_qualification_waived_jobs=[]' | |
| echo 'release_required_jobs=[]' | |
| } >>"$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| if [ "${EVENT_NAME:-}" = "push" ]; then | |
| [[ "${BEFORE_SHA}" =~ ^[a-f0-9]{40}$ && "${CANDIDATE_SHA}" =~ ^[a-f0-9]{40}$ ]] || { | |
| echo "::error::Main push E2E requires exact before and candidate commit SHAs" >&2 | |
| exit 1 | |
| } | |
| CHANGED_FILES="$(git diff --name-only --diff-filter=ACMRD "${BEFORE_SHA}" "${CANDIDATE_SHA}")" | |
| export CHANGED_FILES | |
| fi | |
| npx tsx tools/e2e/workflow-plan.mts --ci-output | |
| if [ -n "${CHECKOUT_SHA}" ]; then | |
| matrix="$(sed -n 's/^matrix=//p' "${GITHUB_OUTPUT}")" | |
| expected_controller_matrix="$(jq -c 'map({id, runner}) | sort_by(.id)' <<< "${CONTROLLER_MATRIX}")" | |
| actual_controller_matrix="$(jq -c 'map({id, runner}) | sort_by(.id)' <<< "${matrix}")" | |
| test_matrix="$(sed -n 's/^test_matrix=//p' "${GITHUB_OUTPUT}")" | |
| expected_controller_test_matrix="$(jq -c 'map({id, file, project}) | sort_by(.id)' <<< "${CONTROLLER_TEST_MATRIX}")" | |
| actual_controller_test_matrix="$(jq -c 'map({id, file, project}) | sort_by(.id)' <<< "${test_matrix}")" | |
| if [ "${actual_controller_matrix}" != "${expected_controller_matrix}" ] || | |
| [ "${actual_controller_test_matrix}" != "${expected_controller_test_matrix}" ]; then | |
| : > "${GITHUB_OUTPUT}" | |
| echo "::error::E2E planner matrix does not match controller-selected targets or shared tests" >&2 | |
| exit 1 | |
| fi | |
| fi | |
| native-runtime-qualification-producer-plan: | |
| needs: generate-matrix | |
| if: ${{ github.event_name == 'workflow_dispatch' && github.repository == 'NVIDIA/NemoClaw' && github.ref == 'refs/heads/main' && inputs.checkout_sha != '' && inputs.jobs == 'native-runtime-qualification-producer' && inputs.targets == '' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| actions: read | |
| contents: read | |
| pull-requests: read | |
| outputs: | |
| matrix: ${{ steps.plan.outputs.matrix }} | |
| steps: | |
| - name: Check out the trusted qualification producer | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ github.workflow_sha }} | |
| persist-credentials: false | |
| sparse-checkout: | | |
| src/lib/onboard/runtime-provider/native-qualification-authority.ts | |
| test/e2e/registry/native-runtime-qualification.ts | |
| tools/e2e/native-runtime-qualification-producer-plan.mts | |
| sparse-checkout-cone-mode: false | |
| - name: Check out the candidate for source authentication | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| repository: ${{ inputs.checkout_repository || github.repository }} | |
| ref: ${{ inputs.checkout_sha || github.sha }} | |
| path: .candidate-source | |
| fetch-depth: 1 | |
| persist-credentials: false | |
| - name: Set up Node for trusted qualification planning | |
| uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: 22 | |
| - id: source | |
| name: Authenticate the candidate and dispatch artifact | |
| env: | |
| BASE_SHA: ${{ inputs.base_sha }} | |
| CANDIDATE_REPOSITORY: ${{ inputs.checkout_repository }} | |
| CANDIDATE_SHA: ${{ inputs.checkout_sha }} | |
| GH_TOKEN: ${{ github.token }} | |
| PR_NUMBER: ${{ inputs.pr_number }} | |
| PRODUCER_RUN_ATTEMPT: ${{ github.run_attempt }} | |
| PRODUCER_RUN_ID: ${{ github.run_id }} | |
| WORKFLOW_SHA: ${{ github.workflow_sha }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| [[ "$CANDIDATE_REPOSITORY" == "NVIDIA/NemoClaw" ]] || { | |
| echo "::error::Native runtime qualification accepts only a branch in NVIDIA/NemoClaw" >&2 | |
| exit 1 | |
| } | |
| [[ "$PRODUCER_RUN_ATTEMPT" == "1" ]] || { | |
| echo "::error::Native runtime qualification producer runs cannot be rerun" >&2 | |
| exit 1 | |
| } | |
| [[ "$BASE_SHA" == "$WORKFLOW_SHA" && "$CANDIDATE_SHA" != "$WORKFLOW_SHA" ]] || { | |
| echo "::error::Native runtime qualification requires base_sha to equal workflow_sha and checkout_sha to name a different commit" >&2 | |
| exit 1 | |
| } | |
| [[ "$(git -C .candidate-source rev-parse --verify 'HEAD^{commit}')" == "$CANDIDATE_SHA" ]] || { | |
| echo "::error::Candidate checkout does not match the authenticated candidate commit" >&2 | |
| exit 1 | |
| } | |
| pull="$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}")" | |
| jq -e \ | |
| --arg baseSha "$BASE_SHA" \ | |
| --arg candidateRepository "$CANDIDATE_REPOSITORY" \ | |
| --arg candidateSha "$CANDIDATE_SHA" ' | |
| .state == "open" and | |
| .head.repo.full_name == $candidateRepository and | |
| .head.sha == $candidateSha and | |
| .base.repo.full_name == "NVIDIA/NemoClaw" and | |
| .base.ref == "main" and | |
| .base.sha == $baseSha | |
| ' <<<"$pull" >/dev/null || { | |
| echo "::error::Pull request identity changed before qualification planning" >&2 | |
| exit 1 | |
| } | |
| artifact_name="e2e-dispatch-${PRODUCER_RUN_ID}-${PRODUCER_RUN_ATTEMPT}" | |
| artifacts="$(gh api --method GET \ | |
| "repos/${GITHUB_REPOSITORY}/actions/runs/${PRODUCER_RUN_ID}/artifacts" \ | |
| -f name="$artifact_name" \ | |
| -f per_page=100)" | |
| artifact="$(jq -ce \ | |
| --arg name "$artifact_name" \ | |
| --arg runId "$PRODUCER_RUN_ID" \ | |
| --arg workflowSha "$WORKFLOW_SHA" ' | |
| select(.total_count == 1 and (.artifacts | length) == 1) | | |
| .artifacts[0] | | |
| select(.name == $name and .expired == false) | | |
| select(.size_in_bytes > 0 and .size_in_bytes <= 1048576) | | |
| select((.id | tostring | test("^[1-9][0-9]{0,19}$"))) | | |
| select((.digest // "") | test("^sha256:[a-f0-9]{64}$")) | | |
| select((.workflow_run.id | tostring) == $runId) | | |
| select(.workflow_run.head_sha == $workflowSha) | |
| ' <<<"$artifacts")" || { | |
| echo "::error::Trusted dispatch artifact is missing or ambiguous" >&2 | |
| exit 1 | |
| } | |
| installer_sha256="$(sha256sum .candidate-source/scripts/install.sh | awk '{print $1}')" | |
| [[ "$installer_sha256" =~ ^[a-f0-9]{64}$ ]] || { | |
| echo "::error::Candidate installer SHA-256 is invalid" >&2 | |
| exit 1 | |
| } | |
| printf 'artifact_id=%s\n' "$(jq -r '.id | tostring' <<<"$artifact")" >>"$GITHUB_OUTPUT" | |
| printf 'artifact_name=%s\n' "$artifact_name" >>"$GITHUB_OUTPUT" | |
| printf 'artifact_digest=%s\n' "$(jq -r '.digest' <<<"$artifact")" >>"$GITHUB_OUTPUT" | |
| printf 'artifact_size=%s\n' "$(jq -r '.size_in_bytes' <<<"$artifact")" >>"$GITHUB_OUTPUT" | |
| printf 'installer_sha256=%s\n' "$installer_sha256" >>"$GITHUB_OUTPUT" | |
| - id: plan | |
| name: Compile the trusted qualification producer matrix | |
| env: | |
| BASE_SHA: ${{ inputs.base_sha }} | |
| CANDIDATE_REPOSITORY: ${{ inputs.checkout_repository }} | |
| CANDIDATE_SHA: ${{ inputs.checkout_sha }} | |
| DISPATCH_ARTIFACT_DIGEST: ${{ steps.source.outputs.artifact_digest }} | |
| DISPATCH_ARTIFACT_ID: ${{ steps.source.outputs.artifact_id }} | |
| DISPATCH_ARTIFACT_NAME: ${{ steps.source.outputs.artifact_name }} | |
| DISPATCH_ARTIFACT_SIZE: ${{ steps.source.outputs.artifact_size }} | |
| INSTALLER_SHA256: ${{ steps.source.outputs.installer_sha256 }} | |
| NATIVE_RUNTIME_ARM64_GPU_RUNNER_LABEL: ${{ vars.NATIVE_RUNTIME_ARM64_GPU_RUNNER_LABEL }} | |
| PR_NUMBER: ${{ inputs.pr_number }} | |
| PRODUCER_RUN_ATTEMPT: ${{ github.run_attempt }} | |
| PRODUCER_RUN_ID: ${{ github.run_id }} | |
| WORKFLOW_SHA: ${{ github.workflow_sha }} | |
| run: node --experimental-strip-types --no-warnings tools/e2e/native-runtime-qualification-producer-plan.mts --ci-output | |
| native-runtime-qualification-podman-toolchain: | |
| name: Build pinned native Podman toolchain / ${{ matrix.architecture }} | |
| needs: [generate-matrix, native-runtime-qualification-producer-plan] | |
| if: ${{ needs.native-runtime-qualification-producer-plan.result == 'success' && contains(fromJSON(needs.generate-matrix.outputs.selected_jobs), 'native-runtime-qualification-producer') }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - architecture: amd64 | |
| runner: ubuntu-24.04 | |
| - architecture: arm64 | |
| runner: ubuntu-24.04-arm | |
| runs-on: ${{ matrix.runner }} | |
| timeout-minutes: 45 | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Check out the pinned Podman source | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| repository: podman-container-tools/podman | |
| ref: cade97a52ebdf9dbf9e81de8009015776837a074 # v6.1.0 | |
| path: .podman-source | |
| fetch-depth: 1 | |
| persist-credentials: false | |
| - name: Check out the pinned Netavark source | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| repository: containers/netavark | |
| ref: 8e91ad1d947ed325327b638f0cb906bea1f7d0ab # v2.1.0 | |
| path: .netavark-source | |
| fetch-depth: 1 | |
| persist-credentials: false | |
| - name: Check out the pinned Aardvark DNS source | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| repository: containers/aardvark-dns | |
| ref: cd7417681229219059939bdd9f0b3bd9ac9abb08 # v2.1.0 | |
| path: .aardvark-source | |
| fetch-depth: 1 | |
| persist-credentials: false | |
| - name: Set up pinned Go for the Podman build | |
| uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 | |
| with: | |
| go-version: 1.25.9 | |
| cache: false | |
| - name: Set up pinned Rust for the network helper builds | |
| uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0 | |
| with: | |
| toolchain: 1.88.0 | |
| cache: false | |
| rustflags: "" | |
| - name: Install build dependencies from the signed runner OS repository | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| sudo env DEBIAN_FRONTEND=noninteractive /usr/bin/apt-get update | |
| sudo env DEBIAN_FRONTEND=noninteractive /usr/bin/apt-get install \ | |
| --yes --no-install-recommends \ | |
| curl gcc git libapparmor-dev libbtrfs-dev libc6-dev \ | |
| libdevmapper-dev libglib2.0-dev \ | |
| libprotobuf-c-dev libprotobuf-dev libseccomp-dev libselinux1-dev \ | |
| libsqlite3-dev libsystemd-dev make pkg-config protobuf-compiler | |
| - name: Build and package the pinned native toolchain | |
| env: | |
| AARDVARK_SOURCE_SHA: cd7417681229219059939bdd9f0b3bd9ac9abb08 | |
| EXPECTED_ARCHITECTURE: ${{ matrix.architecture }} | |
| NETAVARK_SOURCE_SHA: 8e91ad1d947ed325327b638f0cb906bea1f7d0ab | |
| PASTA_SOURCE_ARCHIVE_SHA256: 54fc6a3b39b0fcb13182078662886a629032852e186e47a371fd9d7fd20d3958 | |
| PASTA_SOURCE_SHA: f8df3f1b228fe19a74a269334fdfe6cc7d0605ce | |
| PASTA_VERSION: 2026_07_28.f8df3f1 | |
| PODMAN_SOURCE_SHA: cade97a52ebdf9dbf9e81de8009015776837a074 | |
| TOOLCHAIN_DIRECTORY: ${{ runner.temp }}/native-runtime-podman-toolchain | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| [[ "$(dpkg --print-architecture)" == "$EXPECTED_ARCHITECTURE" ]] | |
| [[ "$(git -C .podman-source rev-parse --verify 'HEAD^{commit}')" == "$PODMAN_SOURCE_SHA" ]] | |
| [[ "$(git -C .netavark-source rev-parse --verify 'HEAD^{commit}')" == "$NETAVARK_SOURCE_SHA" ]] | |
| [[ "$(git -C .aardvark-source rev-parse --verify 'HEAD^{commit}')" == "$AARDVARK_SOURCE_SHA" ]] | |
| [[ ! -e .passt-source && ! -L .passt-source ]] | |
| pasta_source_archive="${RUNNER_TEMP}/passt-${PASTA_SOURCE_SHA}.tar.gz" | |
| [[ ! -e "$pasta_source_archive" && ! -L "$pasta_source_archive" ]] | |
| /usr/bin/curl \ | |
| --fail --location --proto '=https' --proto-redir '=https' \ | |
| --retry 3 --show-error --silent --tlsv1.2 \ | |
| --output "$pasta_source_archive" \ | |
| "https://passt.top/passt/snapshot/passt-${PASTA_SOURCE_SHA}.tar.gz" | |
| [[ -f "$pasta_source_archive" && ! -L "$pasta_source_archive" ]] | |
| [[ "$(sha256sum "$pasta_source_archive" | cut -d' ' -f1)" == "$PASTA_SOURCE_ARCHIVE_SHA256" ]] | |
| mkdir .passt-source | |
| tar \ | |
| --extract --gzip --file="$pasta_source_archive" \ | |
| --directory=.passt-source --strip-components=1 \ | |
| --no-same-owner --no-same-permissions | |
| [[ -f .passt-source/Makefile && ! -L .passt-source/Makefile ]] | |
| [[ -f .passt-source/passt.c && ! -L .passt-source/passt.c ]] | |
| [[ ! -e .passt-source/passt && ! -L .passt-source/passt ]] | |
| for source in .podman-source .netavark-source .aardvark-source; do | |
| [[ -z "$(git -C "$source" status --porcelain --untracked-files=no)" ]] | |
| done | |
| [[ "$(go version)" == go\ version\ go1.25.9\ * ]] | |
| [[ "$(rustc --version)" == rustc\ 1.88.0\ * ]] | |
| SOURCE_DATE_EPOCH=1786554266 \ | |
| BUILD_ORIGIN="NVIDIA/NemoClaw native runtime qualification" \ | |
| EXTRA_BUILDTAGS=containers_image_openpgp \ | |
| make --directory=.podman-source --jobs=2 \ | |
| podman rootlessport PREFIX=/usr/local | |
| SOURCE_DATE_EPOCH=1785940686 CI=1 \ | |
| make --directory=.netavark-source --jobs=2 build | |
| SOURCE_DATE_EPOCH=1785940850 CI=1 \ | |
| make --directory=.aardvark-source --jobs=2 build | |
| SOURCE_DATE_EPOCH=1785255008 \ | |
| make --directory=.passt-source --jobs=2 VERSION="$PASTA_VERSION" passt | |
| podman_dependencies="$(ldd .podman-source/bin/podman)" | |
| printf '%s\n' "$podman_dependencies" | |
| if grep -F "not found" <<<"$podman_dependencies"; then | |
| echo "::error::Pinned Podman build has an unresolved runtime dependency" >&2 | |
| exit 1 | |
| fi | |
| if grep -E "libgpgme|libsubid" <<<"$podman_dependencies"; then | |
| echo "::error::Pinned Podman build must not require an optional host ABI" >&2 | |
| exit 1 | |
| fi | |
| install -D -m 0755 .podman-source/bin/podman "$TOOLCHAIN_DIRECTORY/bin/podman" | |
| [[ -f .passt-source/passt && ! -L .passt-source/passt ]] | |
| install -D -m 0755 .passt-source/passt "$TOOLCHAIN_DIRECTORY/bin/pasta" | |
| install -D -m 0755 .podman-source/bin/rootlessport \ | |
| "$TOOLCHAIN_DIRECTORY/libexec/podman/rootlessport" | |
| install -D -m 0755 .netavark-source/bin/netavark \ | |
| "$TOOLCHAIN_DIRECTORY/libexec/podman/netavark" | |
| install -D -m 0755 .aardvark-source/bin/aardvark-dns \ | |
| "$TOOLCHAIN_DIRECTORY/libexec/podman/aardvark-dns" | |
| install -D -m 0644 \ | |
| .podman-source/vendor/go.podman.io/common/pkg/config/containers.conf \ | |
| "$TOOLCHAIN_DIRECTORY/share/containers/containers.conf" | |
| [[ "$("$TOOLCHAIN_DIRECTORY/bin/podman" --version)" == "podman version 6.1.0" ]] | |
| pasta_version_output="$("$TOOLCHAIN_DIRECTORY/bin/pasta" --version)" | |
| [[ "${pasta_version_output%%$'\n'*}" == "pasta $PASTA_VERSION" ]] || { | |
| echo "::error::Pinned qualification pasta version is invalid" >&2 | |
| exit 1 | |
| } | |
| [[ "$("$TOOLCHAIN_DIRECTORY/libexec/podman/netavark" --version)" == "netavark 2.1.0" ]] | |
| [[ "$("$TOOLCHAIN_DIRECTORY/libexec/podman/aardvark-dns" --version)" == "aardvark-dns 2.1.0" ]] | |
| jq -n \ | |
| --arg architecture "$EXPECTED_ARCHITECTURE" \ | |
| --arg aardvarkDnsSourceSha "$AARDVARK_SOURCE_SHA" \ | |
| --arg netavarkSourceSha "$NETAVARK_SOURCE_SHA" \ | |
| --arg pastaSourceArchiveSha256 "$PASTA_SOURCE_ARCHIVE_SHA256" \ | |
| --arg pastaSourceSha "$PASTA_SOURCE_SHA" \ | |
| --arg pastaVersion "$PASTA_VERSION" \ | |
| --arg podmanSourceSha "$PODMAN_SOURCE_SHA" ' | |
| { | |
| schemaVersion: 1, | |
| kind: "nemoclaw-native-podman-toolchain-v1", | |
| architecture: $architecture, | |
| podmanVersion: "6.1.0", | |
| podmanSourceSha: $podmanSourceSha, | |
| netavarkVersion: "2.1.0", | |
| netavarkSourceSha: $netavarkSourceSha, | |
| aardvarkDnsVersion: "2.1.0", | |
| aardvarkDnsSourceSha: $aardvarkDnsSourceSha, | |
| pastaVersion: $pastaVersion, | |
| pastaSourceArchiveSha256: $pastaSourceArchiveSha256, | |
| pastaSourceSha: $pastaSourceSha, | |
| goVersion: "1.25.9", | |
| rustVersion: "1.88.0" | |
| } | |
| ' >"$TOOLCHAIN_DIRECTORY/manifest.json" | |
| ( | |
| cd "$TOOLCHAIN_DIRECTORY" | |
| sha256sum \ | |
| bin/pasta \ | |
| bin/podman \ | |
| libexec/podman/aardvark-dns \ | |
| libexec/podman/netavark \ | |
| libexec/podman/rootlessport \ | |
| manifest.json \ | |
| share/containers/containers.conf >SHA256SUMS | |
| ) | |
| - name: Upload the pinned native Podman toolchain | |
| if: success() | |
| uses: NVIDIA/NemoClaw/.github/actions/upload-e2e-artifacts@7768e15eb90d3ee2d33432f481dfe8747e4f6d57 | |
| with: | |
| name: native-runtime-podman-toolchain-${{ matrix.architecture }} | |
| path: ${{ runner.temp }}/native-runtime-podman-toolchain/ | |
| native-runtime-qualification-producer: | |
| name: ${{ matrix.jobName }} | |
| needs: | |
| - generate-matrix | |
| - native-runtime-qualification-podman-toolchain | |
| - native-runtime-qualification-producer-plan | |
| if: ${{ needs.native-runtime-qualification-producer-plan.result == 'success' && contains(fromJSON(needs.generate-matrix.outputs.selected_jobs), 'native-runtime-qualification-producer') }} | |
| strategy: | |
| fail-fast: false | |
| matrix: ${{ fromJSON(needs.native-runtime-qualification-producer-plan.outputs.matrix) }} | |
| runs-on: ${{ matrix.runner }} | |
| timeout-minutes: 120 | |
| permissions: | |
| contents: read | |
| env: | |
| NEMOCLAW_RUN_LIVE_E2E: "1" | |
| QUALIFICATION_ROW: ${{ toJSON(matrix) }} | |
| steps: | |
| - name: Require an ephemeral qualification runner | |
| env: | |
| REVIEWED_EPHEMERAL_POOL: ${{ vars.NATIVE_RUNTIME_EPHEMERAL_RUNNER_POOL }} | |
| RUNNER_ENVIRONMENT: ${{ runner.environment }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if [[ "$RUNNER_ENVIRONMENT" != "github-hosted" && "$REVIEWED_EPHEMERAL_POOL" != "enabled" ]]; then | |
| echo "::error::Self-hosted native runtime qualification requires the reviewed ephemeral runner pool" >&2 | |
| exit 1 | |
| fi | |
| - name: Check out the trusted qualification harness | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ matrix.source.workflowSha }} | |
| path: .qualification-workflow | |
| persist-credentials: false | |
| sparse-checkout: | | |
| src/lib/onboard/runtime-provider/native-qualification-authority.ts | |
| scripts/checks/run-native-runtime-installer-qualification.sh | |
| test/e2e/registry/native-runtime-qualification.ts | |
| tools/e2e/native-runtime-qualification-producer-plan.mts | |
| tools/e2e/native-runtime-qualification-producer-evidence.mts | |
| sparse-checkout-cone-mode: false | |
| - name: Check out the candidate commit | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| repository: ${{ matrix.source.candidateRepository }} | |
| ref: ${{ matrix.source.candidateSha }} | |
| path: .candidate-runtime | |
| fetch-depth: 1 | |
| persist-credentials: false | |
| - name: Set up Node for candidate execution | |
| uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: 22.19.0 | |
| - name: Require a reviewed Ubuntu runtime host | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| [[ -r /etc/os-release ]] || { | |
| echo "::error::Protected runner does not expose an OS release identity" >&2 | |
| exit 1 | |
| } | |
| # shellcheck disable=SC1091 | |
| source /etc/os-release | |
| [[ "${ID:-}" == "ubuntu" ]] || { | |
| echo "::error::Protected runner must use a reviewed Ubuntu image" >&2 | |
| exit 1 | |
| } | |
| [[ "${VERSION_ID:-}" == "24.04" || "${VERSION_ID:-}" == "26.04" ]] || { | |
| echo "::error::Protected runner Ubuntu release is not reviewed for Podman qualification" >&2 | |
| exit 1 | |
| } | |
| - name: Download the pinned native Podman toolchain | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: native-runtime-podman-toolchain-${{ matrix.case.architecture }} | |
| path: ${{ runner.temp }}/native-runtime-podman-toolchain | |
| - name: Install the pinned native Podman toolchain and rootless prerequisites | |
| env: | |
| EXPECTED_ARCHITECTURE: ${{ matrix.case.architecture }} | |
| TOOLCHAIN_DIRECTORY: ${{ runner.temp }}/native-runtime-podman-toolchain | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| [[ -x /usr/bin/apt-get ]] || { | |
| echo "::error::Protected runner cannot install rootless Podman prerequisites from its signed OS repository" >&2 | |
| exit 1 | |
| } | |
| sudo env DEBIAN_FRONTEND=noninteractive /usr/bin/apt-get update | |
| sudo env DEBIAN_FRONTEND=noninteractive /usr/bin/apt-get install \ | |
| --yes --no-install-recommends \ | |
| acl apparmor btrfs-progs conmon \ | |
| golang-github-containers-common iptables nftables runc slirp4netns uidmap | |
| [[ -d "$TOOLCHAIN_DIRECTORY" && ! -L "$TOOLCHAIN_DIRECTORY" ]] | |
| [[ -z "$(find -P "$TOOLCHAIN_DIRECTORY" -type l -print -quit)" ]] | |
| mapfile -t actual_files < <( | |
| cd "$TOOLCHAIN_DIRECTORY" | |
| find . -type f -print | LC_ALL=C sort | |
| ) | |
| expected_files=( | |
| ./SHA256SUMS | |
| ./bin/pasta | |
| ./bin/podman | |
| ./libexec/podman/aardvark-dns | |
| ./libexec/podman/netavark | |
| ./libexec/podman/rootlessport | |
| ./manifest.json | |
| ./share/containers/containers.conf | |
| ) | |
| [[ "${actual_files[*]}" == "${expected_files[*]}" ]] || { | |
| echo "::error::Downloaded native Podman toolchain contains unexpected files" >&2 | |
| exit 1 | |
| } | |
| ( | |
| cd "$TOOLCHAIN_DIRECTORY" | |
| sha256sum --check --strict SHA256SUMS | |
| ) | |
| jq -e \ | |
| --arg architecture "$EXPECTED_ARCHITECTURE" ' | |
| type == "object" and | |
| keys == [ | |
| "aardvarkDnsSourceSha", | |
| "aardvarkDnsVersion", | |
| "architecture", | |
| "goVersion", | |
| "kind", | |
| "netavarkSourceSha", | |
| "netavarkVersion", | |
| "pastaSourceArchiveSha256", | |
| "pastaSourceSha", | |
| "pastaVersion", | |
| "podmanSourceSha", | |
| "podmanVersion", | |
| "rustVersion", | |
| "schemaVersion" | |
| ] and | |
| .schemaVersion == 1 and | |
| .kind == "nemoclaw-native-podman-toolchain-v1" and | |
| .architecture == $architecture and | |
| .podmanVersion == "6.1.0" and | |
| .podmanSourceSha == "cade97a52ebdf9dbf9e81de8009015776837a074" and | |
| .netavarkVersion == "2.1.0" and | |
| .netavarkSourceSha == "8e91ad1d947ed325327b638f0cb906bea1f7d0ab" and | |
| .aardvarkDnsVersion == "2.1.0" and | |
| .aardvarkDnsSourceSha == "cd7417681229219059939bdd9f0b3bd9ac9abb08" and | |
| .pastaVersion == "2026_07_28.f8df3f1" and | |
| .pastaSourceArchiveSha256 == "54fc6a3b39b0fcb13182078662886a629032852e186e47a371fd9d7fd20d3958" and | |
| .pastaSourceSha == "f8df3f1b228fe19a74a269334fdfe6cc7d0605ce" and | |
| .goVersion == "1.25.9" and | |
| .rustVersion == "1.88.0" | |
| ' "$TOOLCHAIN_DIRECTORY/manifest.json" >/dev/null | |
| for target in \ | |
| /usr/local/bin/podman \ | |
| /usr/local/libexec/podman/aardvark-dns \ | |
| /usr/local/libexec/podman/netavark \ | |
| /usr/local/libexec/podman/rootlessport \ | |
| /usr/share/containers/containers.conf; do | |
| [[ ! -L "$target" ]] || { | |
| echo "::error::Native Podman toolchain target must not be a symlink: $target" >&2 | |
| exit 1 | |
| } | |
| done | |
| sudo install --owner=root --group=root --mode=0755 \ | |
| "$TOOLCHAIN_DIRECTORY/bin/podman" /usr/local/bin/podman | |
| for helper in aardvark-dns netavark rootlessport; do | |
| sudo install -D --owner=root --group=root --mode=0755 \ | |
| "$TOOLCHAIN_DIRECTORY/libexec/podman/$helper" \ | |
| "/usr/local/libexec/podman/$helper" | |
| done | |
| sudo install --owner=root --group=root --mode=0644 \ | |
| "$TOOLCHAIN_DIRECTORY/share/containers/containers.conf" \ | |
| /usr/share/containers/containers.conf | |
| [[ "$(command -v podman)" == "/usr/local/bin/podman" ]] | |
| conmon_version="$(conmon --version | awk 'NR == 1 { print $NF }')" | |
| runc_version="$(runc --version | awk 'NR == 1 { print $NF }')" | |
| dpkg --compare-versions "$conmon_version" ge 2.1.7 | |
| dpkg --compare-versions "$runc_version" ge 1.1.11 | |
| [[ "$(/usr/local/libexec/podman/netavark --version)" == "netavark 2.1.0" ]] | |
| [[ "$(/usr/local/libexec/podman/aardvark-dns --version)" == "aardvark-dns 2.1.0" ]] | |
| version="$(podman --version)" | |
| [[ "$version" == "podman version 6.1.0" ]] | |
| - name: Prepare the credential-free execution account and disable Docker | |
| id: boundary | |
| env: | |
| CANDIDATE_DIRECTORY: ${{ github.workspace }}/.candidate-runtime | |
| TOOLCHAIN_DIRECTORY: ${{ runner.temp }}/native-runtime-podman-toolchain | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| for command in apparmor_parser awk cat curl getent git grep groupdel id jq node npm pgrep podman setfacl sha256sum stat systemctl tee unlink useradd userdel usermod; do | |
| command -v "$command" >/dev/null || { | |
| echo "::error::Protected runner is missing required command: $command" >&2 | |
| exit 1 | |
| } | |
| done | |
| [[ "$(podman --version)" == "podman version 6.1.0" ]] || { | |
| echo "::error::Protected runner must provide Podman 6.1.0" >&2 | |
| exit 1 | |
| } | |
| sudo systemctl stop docker.service docker.socket 2>/dev/null || true | |
| sudo systemctl mask --runtime docker.service docker.socket 2>/dev/null || true | |
| sudo pkill -TERM -x dockerd 2>/dev/null || true | |
| sudo rm -f /var/run/docker.sock /run/docker.sock | |
| ! systemctl is-active --quiet docker.service | |
| ! systemctl is-active --quiet docker.socket | |
| ! pgrep -x dockerd >/dev/null | |
| [[ ! -S /var/run/docker.sock && ! -S /run/docker.sock ]] | |
| account="nemoclawq" | |
| ownership_marker="/run/nemoclaw-native-runtime-owner-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" | |
| if getent passwd "$account" >/dev/null || getent group "$account" >/dev/null || grep -q "^${account}:" /etc/subuid /etc/subgid; then | |
| echo "::error::Qualification account, group, or subordinate-ID authorization already exists" >&2 | |
| exit 1 | |
| fi | |
| [[ ! -e "$ownership_marker" && ! -L "$ownership_marker" ]] || { | |
| echo "::error::Qualification account ownership marker already exists" >&2 | |
| exit 1 | |
| } | |
| account_created_without_marker=0 | |
| rollback_unmarked_account() { | |
| local result="$?" | |
| trap - EXIT | |
| if ((result != 0 && account_created_without_marker == 1)); then | |
| sudo userdel --remove "$account" 2>/dev/null || true | |
| sudo groupdel "$account" 2>/dev/null || true | |
| sudo rm -f -- "$ownership_marker" | |
| if getent passwd "$account" >/dev/null || getent group "$account" >/dev/null || grep -q "^${account}:" /etc/subuid /etc/subgid; then | |
| echo "::error::Partially created qualification account could not be rolled back" >&2 | |
| exit 1 | |
| fi | |
| fi | |
| exit "$result" | |
| } | |
| trap rollback_unmarked_account EXIT | |
| sudo useradd --create-home --shell /usr/sbin/nologin --user-group "$account" | |
| account_created_without_marker=1 | |
| uid="$(id -u "$account")" | |
| gid="$(id -g "$account")" | |
| home="$(getent passwd "$account" | cut -d: -f6)" | |
| group_entry="$(getent group "$account")" | |
| [[ "$uid" =~ ^[0-9]+$ && "$gid" =~ ^[0-9]+$ && "$home" == "/home/${account}" && -d "$home" && ! -L "$home" ]] || { | |
| echo "::error::Qualification account identity is missing or invalid" >&2 | |
| exit 1 | |
| } | |
| [[ "$group_entry" == "${account}:x:${gid}:" ]] || { | |
| echo "::error::Qualification private group identity is missing or invalid" >&2 | |
| exit 1 | |
| } | |
| printf '%s:%s:%s\n' "$account" "$uid" "$gid" | sudo tee "$ownership_marker" >/dev/null | |
| sudo chown root:root "$ownership_marker" | |
| sudo chmod 0400 "$ownership_marker" | |
| [[ -f "$ownership_marker" && ! -L "$ownership_marker" && "$(stat -c '%u:%g:%a' "$ownership_marker")" == "0:0:400" ]] || { | |
| echo "::error::Qualification account ownership marker is invalid" >&2 | |
| exit 1 | |
| } | |
| account_created_without_marker=0 | |
| trap - EXIT | |
| ensure_subordinate_range() { | |
| local file="$1" | |
| local option="$2" | |
| local range_start=100000 | |
| local range_end | |
| local conflict_end | |
| [[ -f "$file" && ! -L "$file" ]] || { | |
| echo "::error::Rootless Podman subordinate-ID file is missing or invalid: $file" >&2 | |
| exit 1 | |
| } | |
| if awk -F: -v account="$account" ' | |
| $1 == account && $2 ~ /^[0-9]+$/ && $3 ~ /^[0-9]+$/ && $3 >= 65536 { found = 1 } | |
| END { exit found ? 0 : 1 } | |
| ' "$file"; then | |
| return | |
| fi | |
| while :; do | |
| ((range_start <= 4294901760)) || { | |
| echo "::error::Protected runner has no free subordinate-ID range for rootless Podman" >&2 | |
| exit 1 | |
| } | |
| range_end=$((range_start + 65535)) | |
| conflict_end="$(awk -F: -v start="$range_start" -v end="$range_end" ' | |
| $2 ~ /^[0-9]+$/ && $3 ~ /^[0-9]+$/ { | |
| current_end = $2 + $3 - 1 | |
| if ($2 <= end && current_end >= start && current_end > maximum) maximum = current_end | |
| } | |
| END { if (maximum != "") print maximum } | |
| ' "$file")" | |
| [[ -n "$conflict_end" ]] || break | |
| range_start=$((conflict_end + 1)) | |
| done | |
| range_end=$((range_start + 65535)) | |
| sudo usermod "$option" "${range_start}-${range_end}" "$account" | |
| awk -F: -v account="$account" ' | |
| $1 == account && $2 ~ /^[0-9]+$/ && $3 ~ /^[0-9]+$/ && $3 >= 65536 { found = 1 } | |
| END { exit found ? 0 : 1 } | |
| ' "$file" || { | |
| echo "::error::Protected runner did not provision rootless Podman subordinate IDs" >&2 | |
| exit 1 | |
| } | |
| } | |
| ensure_subordinate_range /etc/subuid --add-subuids | |
| ensure_subordinate_range /etc/subgid --add-subgids | |
| printf 'account=%s\n' "$account" >>"$GITHUB_OUTPUT" | |
| printf 'account_created=true\n' >>"$GITHUB_OUTPUT" | |
| printf 'uid=%s\n' "$uid" >>"$GITHUB_OUTPUT" | |
| printf 'gid=%s\n' "$gid" >>"$GITHUB_OUTPUT" | |
| runtime_dir="/run/user/${uid}" | |
| runtime_directory_unit="user-runtime-dir@${uid}.service" | |
| user_manager_unit="user@${uid}.service" | |
| user_manager_dropin_directory="/run/systemd/system/${user_manager_unit}.d" | |
| user_manager_dropin="${user_manager_dropin_directory}/50-nemoclaw-native-runtime.conf" | |
| trusted_user_unit_path="/usr/lib/systemd/user:/lib/systemd/user" | |
| [[ ! -e "$user_manager_dropin_directory" && ! -L "$user_manager_dropin_directory" ]] || { | |
| echo "::error::Qualification systemd user-manager drop-in directory already exists" >&2 | |
| exit 1 | |
| } | |
| sudo install -d --owner=root --group=root --mode=0755 "$user_manager_dropin_directory" | |
| printf '[Service]\nEnvironment="SYSTEMD_UNIT_PATH=%s"\n' "$trusted_user_unit_path" | | |
| sudo tee "$user_manager_dropin" >/dev/null | |
| sudo chown root:root "$user_manager_dropin" | |
| sudo chmod 0444 "$user_manager_dropin" | |
| [[ -f "$user_manager_dropin" && ! -L "$user_manager_dropin" && "$(stat -c '%u:%g:%a:%h' "$user_manager_dropin")" == "0:0:444:1" ]] || { | |
| echo "::error::Qualification systemd user-manager drop-in is invalid" >&2 | |
| exit 1 | |
| } | |
| sudo systemctl daemon-reload | |
| verify_user_manager_unit_path() { | |
| local environment | |
| environment="$(sudo -u "$1" env -i \ | |
| HOME="$2" \ | |
| LANG=C.UTF-8 \ | |
| PATH=/usr/bin:/bin \ | |
| XDG_RUNTIME_DIR="$3" \ | |
| /usr/bin/systemctl --user show-environment)" | |
| tr ' ' '\n' <<<"$environment" | grep -Fx -- "SYSTEMD_UNIT_PATH=$trusted_user_unit_path" >/dev/null || { | |
| echo "::error::Qualification systemd user manager did not inherit the trusted unit path" >&2 | |
| return 1 | |
| } | |
| } | |
| verify_user_bus() { | |
| local execution_account="$1" | |
| local expected_uid="$2" | |
| local bus="$3" | |
| local context="$4" | |
| sudo /usr/bin/test -S "$bus" && | |
| sudo /usr/bin/test ! -L "$bus" && | |
| [[ "$(sudo stat -c '%u' -- "$bus")" == "$expected_uid" ]] && | |
| sudo -u "$execution_account" /usr/bin/test -S "$bus" || { | |
| echo "::error::Qualification systemd user bus $context" >&2 | |
| return 1 | |
| } | |
| } | |
| sudo systemctl start "$user_manager_unit" | |
| systemctl is-active --quiet "$runtime_directory_unit" || { | |
| echo "::error::Qualification systemd runtime-directory unit is not active" >&2 | |
| exit 1 | |
| } | |
| [[ -d "$runtime_dir" && ! -L "$runtime_dir" && "$(stat -c '%u:%g:%a' "$runtime_dir")" == "${uid}:${gid}:700" ]] || { | |
| echo "::error::Qualification runtime directory is missing or invalid" >&2 | |
| exit 1 | |
| } | |
| systemctl is-active --quiet "$user_manager_unit" || { | |
| echo "::error::Qualification systemd user manager is not active" >&2 | |
| exit 1 | |
| } | |
| sudo -u "$account" env -i \ | |
| HOME="$home" \ | |
| LANG=C.UTF-8 \ | |
| PATH=/usr/bin:/bin \ | |
| XDG_RUNTIME_DIR="$runtime_dir" \ | |
| /usr/bin/systemctl --user start dbus.socket | |
| sudo -u "$account" env -i \ | |
| HOME="$home" \ | |
| LANG=C.UTF-8 \ | |
| PATH=/usr/bin:/bin \ | |
| XDG_RUNTIME_DIR="$runtime_dir" \ | |
| /usr/bin/systemctl --user is-active --quiet dbus.socket || { | |
| echo "::error::Qualification systemd user bus socket unit is not active" >&2 | |
| exit 1 | |
| } | |
| verify_user_bus "$account" "$uid" "$runtime_dir/bus" \ | |
| "is invalid or inaccessible to the execution account" | |
| verify_user_manager_unit_path "$account" "$home" "$runtime_dir" | |
| storage_config_directory="/run/nemoclaw-native-runtime-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${uid}" | |
| storage_config="${storage_config_directory}/storage.conf" | |
| containers_config="${storage_config_directory}/containers.conf" | |
| podman_executable="/nemoclaw-native-runtime-podman-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${uid}" | |
| apparmor_profile="${storage_config_directory}/podman.apparmor" | |
| apparmor_profile_name="nemoclaw-native-podman-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${uid}" | |
| helper_directory="/nemoclaw-native-runtime-helpers-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${uid}" | |
| pasta_executable="${helper_directory}/pasta" | |
| pasta_apparmor_profile="${storage_config_directory}/pasta.apparmor" | |
| pasta_apparmor_profile_name="nemoclaw-native-pasta-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${uid}" | |
| [[ ! -e "$storage_config_directory" && ! -L "$storage_config_directory" ]] || { | |
| echo "::error::Qualification storage configuration directory already exists" >&2 | |
| exit 1 | |
| } | |
| [[ ! -e "$helper_directory" && ! -L "$helper_directory" ]] || { | |
| echo "::error::Qualification helper directory already exists" >&2 | |
| exit 1 | |
| } | |
| sudo install -d -o root -g root -m 0755 "$storage_config_directory" | |
| [[ -f "$TOOLCHAIN_DIRECTORY/bin/podman" && ! -L "$TOOLCHAIN_DIRECTORY/bin/podman" ]] || { | |
| echo "::error::Pinned qualification Podman executable source is missing or invalid" >&2 | |
| exit 1 | |
| } | |
| [[ ! -e "$podman_executable" && ! -L "$podman_executable" ]] || { | |
| echo "::error::Run-owned qualification Podman executable already exists" >&2 | |
| exit 1 | |
| } | |
| sudo install --owner=root --group=root --mode=0555 \ | |
| "$TOOLCHAIN_DIRECTORY/bin/podman" "$podman_executable" | |
| [[ -f "$podman_executable" && ! -L "$podman_executable" && "$(stat -c '%u:%g:%a' "$podman_executable")" == "0:0:555" ]] || { | |
| echo "::error::Run-owned qualification Podman executable is invalid" >&2 | |
| exit 1 | |
| } | |
| [[ "$(sha256sum "$podman_executable" | cut -d' ' -f1)" == "$(sha256sum "$TOOLCHAIN_DIRECTORY/bin/podman" | cut -d' ' -f1)" ]] || { | |
| echo "::error::Run-owned qualification Podman executable digest changed during installation" >&2 | |
| exit 1 | |
| } | |
| [[ -f "$TOOLCHAIN_DIRECTORY/bin/pasta" && ! -L "$TOOLCHAIN_DIRECTORY/bin/pasta" ]] || { | |
| echo "::error::Pinned qualification pasta executable source is missing or invalid" >&2 | |
| exit 1 | |
| } | |
| sudo install -d --owner=root --group=root --mode=0555 "$helper_directory" | |
| sudo install --owner=root --group=root --mode=0555 \ | |
| "$TOOLCHAIN_DIRECTORY/bin/pasta" "$pasta_executable" | |
| [[ -d "$helper_directory" && ! -L "$helper_directory" && "$(stat -c '%u:%g:%a' "$helper_directory")" == "0:0:555" ]] || { | |
| echo "::error::Run-owned qualification helper directory is invalid" >&2 | |
| exit 1 | |
| } | |
| [[ -f "$pasta_executable" && ! -L "$pasta_executable" && "$(stat -c '%u:%g:%a' "$pasta_executable")" == "0:0:555" ]] || { | |
| echo "::error::Run-owned qualification pasta executable is invalid" >&2 | |
| exit 1 | |
| } | |
| [[ "$(sha256sum "$pasta_executable" | cut -d' ' -f1)" == "$(sha256sum "$TOOLCHAIN_DIRECTORY/bin/pasta" | cut -d' ' -f1)" ]] || { | |
| echo "::error::Run-owned qualification pasta executable digest changed during installation" >&2 | |
| exit 1 | |
| } | |
| printf '%s\n' \ | |
| '[storage]' \ | |
| 'driver = "overlay"' \ | |
| "runroot = \"${home}/.local/share/containers/runroot\"" \ | |
| "graphroot = \"${home}/.local/share/containers/storage\"" \ | |
| "rootless_storage_path = \"${home}/.local/share/containers/storage\"" | sudo tee "$storage_config" >/dev/null | |
| sudo chown root:root "$storage_config" | |
| sudo chmod 0444 "$storage_config" | |
| [[ -f "$storage_config" && ! -L "$storage_config" && "$(stat -c '%u:%g:%a' "$storage_config")" == "0:0:444" ]] || { | |
| echo "::error::Qualification storage configuration is not root-owned and read-only" >&2 | |
| exit 1 | |
| } | |
| printf '%s\n' \ | |
| '[network]' \ | |
| 'firewall_driver = "nftables"' | sudo tee "$containers_config" >/dev/null | |
| sudo chown root:root "$containers_config" | |
| sudo chmod 0444 "$containers_config" | |
| [[ -f "$containers_config" && ! -L "$containers_config" && "$(stat -c '%u:%g:%a' "$containers_config")" == "0:0:444" ]] || { | |
| echo "::error::Qualification containers configuration is not root-owned and read-only" >&2 | |
| exit 1 | |
| } | |
| if [[ -r /sys/module/apparmor/parameters/enabled ]] && grep -q '^Y' /sys/module/apparmor/parameters/enabled; then | |
| printf '%s\n' \ | |
| '# This ephemeral profile grants user namespaces only to the pinned qualification Podman binary.' \ | |
| '' \ | |
| 'abi <abi/4.0>,' \ | |
| 'include <tunables/global>' \ | |
| '' \ | |
| "profile ${apparmor_profile_name} ${podman_executable} flags=(unconfined) {" \ | |
| ' userns,' \ | |
| '}' | sudo tee "$apparmor_profile" >/dev/null | |
| sudo chown root:root "$apparmor_profile" | |
| sudo chmod 0444 "$apparmor_profile" | |
| [[ -f "$apparmor_profile" && ! -L "$apparmor_profile" && "$(stat -c '%u:%g:%a' "$apparmor_profile")" == "0:0:444" ]] || { | |
| echo "::error::Qualification AppArmor profile is not root-owned and read-only" >&2 | |
| exit 1 | |
| } | |
| sudo apparmor_parser -r "$apparmor_profile" | |
| printf '%s\n' \ | |
| '# This ephemeral profile is limited to the immutable run-owned pasta helper.' \ | |
| '' \ | |
| 'abi <abi/4.0>,' \ | |
| 'include <tunables/global>' \ | |
| '' \ | |
| "profile ${pasta_apparmor_profile_name} ${pasta_executable} flags=(unconfined) {" \ | |
| ' userns,' \ | |
| '}' | sudo tee "$pasta_apparmor_profile" >/dev/null | |
| sudo chown root:root "$pasta_apparmor_profile" | |
| sudo chmod 0444 "$pasta_apparmor_profile" | |
| [[ -f "$pasta_apparmor_profile" && ! -L "$pasta_apparmor_profile" && "$(stat -c '%u:%g:%a' "$pasta_apparmor_profile")" == "0:0:444" ]] || { | |
| echo "::error::Qualification pasta AppArmor profile is not root-owned and read-only" >&2 | |
| exit 1 | |
| } | |
| sudo apparmor_parser -r "$pasta_apparmor_profile" | |
| fi | |
| ancestor="$(dirname "$CANDIDATE_DIRECTORY")" | |
| while [[ "$ancestor" != "/home" ]]; do | |
| [[ ("$ancestor" == "/home/runner" || "$ancestor" == /home/runner/*) && -d "$ancestor" && ! -L "$ancestor" ]] || { | |
| echo "::error::Candidate checkout ancestor is outside the reviewed runner workspace" >&2 | |
| exit 1 | |
| } | |
| sudo setfacl --modify "u:${account}:--x" "$ancestor" | |
| ancestor="$(dirname "$ancestor")" | |
| done | |
| sudo chown -R "$uid:$gid" "$CANDIDATE_DIRECTORY" | |
| node_directory="$(dirname "$(command -v node)")" | |
| [[ "$node_directory" == /* && -x "$node_directory/node" && -x "$node_directory/npm" ]] || { | |
| echo "::error::Pinned Node toolchain path is invalid" >&2 | |
| exit 1 | |
| } | |
| guard_dir="${RUNNER_TEMP}/native-runtime-docker-guard" | |
| install -d -m 0755 "$guard_dir" | |
| printf '%s\n' '#!/usr/bin/env bash' 'exit 97' >"$guard_dir/docker" | |
| chmod 0555 "$guard_dir/docker" | |
| podman_info="$(sudo -u "$account" env -i \ | |
| CONTAINERS_CONF="$containers_config" \ | |
| CONTAINERS_STORAGE_CONF="$storage_config" \ | |
| HOME="$home" \ | |
| LANG=C.UTF-8 \ | |
| PATH="$guard_dir:$helper_directory:/usr/local/bin:/usr/bin:/bin" \ | |
| XDG_RUNTIME_DIR="$runtime_dir" \ | |
| "$podman_executable" info --format json)" || { | |
| echo "::error::Credential-free rootless Podman readiness failed" >&2 | |
| exit 1 | |
| } | |
| jq -e ' | |
| .host.security.rootless == true and | |
| .store.graphDriverName == "overlay" and | |
| (((.store.graphOptions // {})["overlay.mount_program"].Executable? // "") == "") | |
| ' <<<"$podman_info" >/dev/null || { | |
| echo "::error::Qualification requires native rootless overlay storage" >&2 | |
| exit 1 | |
| } | |
| printf 'home=%s\n' "$home" >>"$GITHUB_OUTPUT" | |
| printf 'runtime_dir=%s\n' "$runtime_dir" >>"$GITHUB_OUTPUT" | |
| printf 'guard_dir=%s\n' "$guard_dir" >>"$GITHUB_OUTPUT" | |
| printf 'helper_dir=%s\n' "$helper_directory" >>"$GITHUB_OUTPUT" | |
| printf 'node_dir=%s\n' "$node_directory" >>"$GITHUB_OUTPUT" | |
| printf 'podman_executable=%s\n' "$podman_executable" >>"$GITHUB_OUTPUT" | |
| printf 'containers_config=%s\n' "$containers_config" >>"$GITHUB_OUTPUT" | |
| printf 'runtime_directory_unit=%s\n' "$runtime_directory_unit" >>"$GITHUB_OUTPUT" | |
| printf 'storage_config=%s\n' "$storage_config" >>"$GITHUB_OUTPUT" | |
| printf 'user_manager_unit=%s\n' "$user_manager_unit" >>"$GITHUB_OUTPUT" | |
| - name: Prepare GPU resources with the NVIDIA API key | |
| id: gpu_resources | |
| env: | |
| ACCOUNT: ${{ steps.boundary.outputs.account }} | |
| ACCOUNT_GID: ${{ steps.boundary.outputs.gid }} | |
| ACCOUNT_UID: ${{ steps.boundary.outputs.uid }} | |
| ACCELERATION: ${{ matrix.case.acceleration }} | |
| ARCHITECTURE: ${{ matrix.case.architecture }} | |
| CONTAINERS_CONFIG: ${{ steps.boundary.outputs.containers_config }} | |
| GUARD_DIRECTORY: ${{ steps.boundary.outputs.guard_dir }} | |
| HELPER_DIRECTORY: ${{ steps.boundary.outputs.helper_dir }} | |
| INFERENCE: ${{ matrix.case.inference }} | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| PODMAN_EXECUTABLE: ${{ steps.boundary.outputs.podman_executable }} | |
| QUALIFICATION_HOME: ${{ steps.boundary.outputs.home }} | |
| RUNTIME_DIRECTORY: ${{ steps.boundary.outputs.runtime_dir }} | |
| STORAGE_CONFIG: ${{ steps.boundary.outputs.storage_config }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| umask 077 | |
| if [[ "$ACCELERATION" != "nvidia-gpu" ]]; then | |
| printf 'runner_contract=\n' >>"$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| [[ -n "$NVIDIA_API_KEY" ]] || { | |
| echo "::error::Native runtime GPU preparation requires the NVIDIA_API_KEY repository secret" >&2 | |
| exit 1 | |
| } | |
| uid="$(id -u "$ACCOUNT")" | |
| gid="$(id -g "$ACCOUNT")" | |
| [[ "$uid" == "$ACCOUNT_UID" && "$gid" == "$ACCOUNT_GID" ]] || { | |
| echo "::error::Qualification account identity changed before GPU resource preparation" >&2 | |
| exit 1 | |
| } | |
| storage_config_directory="$(dirname "$STORAGE_CONFIG")" | |
| runner_contract="${storage_config_directory}/runner-contract.json" | |
| registry_auth_directory="${storage_config_directory}/registry-auth" | |
| registry_auth_file="${registry_auth_directory}/auth.json" | |
| resource_directory="/var/tmp/nemoclaw-native-runtime-resources-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${uid}" | |
| model_directory="${resource_directory}/model" | |
| model_revision="7ae557604adf67be50417f59c2c2f167def9a775" | |
| model_name="Qwen/Qwen2.5-0.5B-Instruct" | |
| case "$ARCHITECTURE" in | |
| amd64) | |
| probe_image="nvcr.io/nvidia/k8s/cuda-sample@sha256:9855f4c8500addf185360474184b9efdaf4384284779aa9173dcd70164a4ae6f" | |
| nim_image="nvcr.io/nim/nvidia/model-free-nim@sha256:a0fdbecdf51792dadc48d284fde3199a58d5a2067007ad5b80319975fe81ce93" | |
| vllm_image="nvcr.io/nvidia/vllm@sha256:7be6c2f676c36059a494fe17254e69ae5c677535ba6191044e5fc8e42a91c773" | |
| ;; | |
| arm64) | |
| probe_image="nvcr.io/nvidia/k8s/cuda-sample@sha256:a54fdceac3bc2a8d177f07db942defc2f7237e18d07fca2ff00718ba5aee4940" | |
| nim_image="nvcr.io/nim/nvidia/model-free-nim@sha256:8342257b9744e9bc23a02e0f45badad6b88474727473965b4dc83e8fee45956a" | |
| vllm_image="nvcr.io/nvidia/vllm@sha256:447995cbb57e6c7cf792cab95e9852e5f62b5fb6d2f39e030fa4eda9a54eadb4" | |
| ;; | |
| *) | |
| echo "::error::GPU resource architecture is unsupported" >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| [[ ! -e "$runner_contract" && ! -L "$runner_contract" ]] || { | |
| echo "::error::Run-owned GPU runner contract already exists" >&2 | |
| exit 1 | |
| } | |
| [[ ! -e "$registry_auth_directory" && ! -L "$registry_auth_directory" ]] || { | |
| echo "::error::Run-owned registry authentication directory already exists" >&2 | |
| exit 1 | |
| } | |
| sudo install -d --owner="$uid" --group="$gid" --mode=0700 "$registry_auth_directory" | |
| cleanup_registry_auth() { | |
| local result="$?" | |
| trap - EXIT | |
| if sudo test -e "$registry_auth_file" || sudo test -L "$registry_auth_file"; then | |
| sudo test -f "$registry_auth_file" && | |
| sudo test ! -L "$registry_auth_file" && | |
| [[ "$(sudo stat -c '%u:%g:%a:%h' -- "$registry_auth_file")" == "${uid}:${gid}:600:1" ]] || { | |
| echo "::error::Run-owned registry authentication file cleanup target is invalid" >&2 | |
| return 1 | |
| } | |
| sudo unlink "$registry_auth_file" | |
| fi | |
| sudo rmdir "$registry_auth_directory" || { | |
| echo "::error::Run-owned registry authentication directory was not empty" >&2 | |
| return 1 | |
| } | |
| return "$result" | |
| } | |
| trap cleanup_registry_auth EXIT | |
| printf '%s' "$NVIDIA_API_KEY" | sudo -u "$ACCOUNT" env -i \ | |
| CONTAINERS_CONF="$CONTAINERS_CONFIG" \ | |
| CONTAINERS_STORAGE_CONF="$STORAGE_CONFIG" \ | |
| HOME="$QUALIFICATION_HOME" \ | |
| LANG=C.UTF-8 \ | |
| PATH="$HELPER_DIRECTORY:/usr/local/bin:/usr/bin:/bin" \ | |
| REGISTRY_AUTH_FILE="$registry_auth_file" \ | |
| XDG_RUNTIME_DIR="$RUNTIME_DIRECTORY" \ | |
| "$PODMAN_EXECUTABLE" login nvcr.io --username '$oauthtoken' --password-stdin >/dev/null | |
| sudo test -f "$registry_auth_file" && | |
| sudo test ! -L "$registry_auth_file" && | |
| [[ "$(sudo stat -c '%u:%g:%h' -- "$registry_auth_file")" == "${uid}:${gid}:1" ]] || { | |
| echo "::error::Run-owned registry authentication file is invalid" >&2 | |
| exit 1 | |
| } | |
| sudo chmod 0600 -- "$registry_auth_file" | |
| [[ "$(sudo stat -c '%u:%g:%a:%h' -- "$registry_auth_file")" == "${uid}:${gid}:600:1" ]] || { | |
| echo "::error::Run-owned registry authentication file permissions are invalid" >&2 | |
| exit 1 | |
| } | |
| image_to_pull="$probe_image" | |
| if [[ "$INFERENCE" == "nim" ]]; then | |
| image_to_pull="$nim_image" | |
| elif [[ "$INFERENCE" == "vllm" ]]; then | |
| image_to_pull="$vllm_image" | |
| fi | |
| images=("$probe_image") | |
| [[ "$image_to_pull" == "$probe_image" ]] || images+=("$image_to_pull") | |
| for image in "${images[@]}"; do | |
| sudo -u "$ACCOUNT" env -i \ | |
| CONTAINERS_CONF="$CONTAINERS_CONFIG" \ | |
| CONTAINERS_STORAGE_CONF="$STORAGE_CONFIG" \ | |
| HOME="$QUALIFICATION_HOME" \ | |
| LANG=C.UTF-8 \ | |
| PATH="$GUARD_DIRECTORY:$HELPER_DIRECTORY:/usr/local/bin:/usr/bin:/bin" \ | |
| REGISTRY_AUTH_FILE="$registry_auth_file" \ | |
| XDG_RUNTIME_DIR="$RUNTIME_DIRECTORY" \ | |
| "$PODMAN_EXECUTABLE" pull "$image" | |
| done | |
| cleanup_registry_auth | |
| trap - EXIT | |
| unset NVIDIA_API_KEY | |
| if [[ "$INFERENCE" == "nim" || "$INFERENCE" == "vllm" ]]; then | |
| [[ ! -e "$resource_directory" && ! -L "$resource_directory" ]] || { | |
| echo "::error::Run-owned GPU model resource already exists" >&2 | |
| exit 1 | |
| } | |
| sudo install -d --owner=root --group=root --mode=0711 "$resource_directory" | |
| sudo install -d --owner="$uid" --group="$gid" --mode=0700 "$model_directory" | |
| download_model_file() { | |
| local file="$1" | |
| local size="$2" | |
| local algorithm="$3" | |
| local digest="$4" | |
| local target="${model_directory}/${file}" | |
| sudo -u "$ACCOUNT" env -i \ | |
| HOME="$QUALIFICATION_HOME" \ | |
| LANG=C.UTF-8 \ | |
| /usr/bin/curl \ | |
| --fail --location --proto '=https' --retry 3 --show-error --silent --tlsv1.2 \ | |
| --output "$target" \ | |
| "https://huggingface.co/Qwen/Qwen2.5-0.5B-Instruct/resolve/${model_revision}/${file}?download=true" | |
| sudo test -f "$target" && | |
| sudo test ! -L "$target" && | |
| [[ "$(sudo stat -c '%u:%g:%h:%s' -- "$target")" == "${uid}:${gid}:1:${size}" ]] || { | |
| echo "::error::Downloaded GPU model file metadata is invalid: $file" >&2 | |
| exit 1 | |
| } | |
| sudo chmod 0600 -- "$target" | |
| [[ "$(sudo stat -c '%u:%g:%a:%h:%s' -- "$target")" == "${uid}:${gid}:600:1:${size}" ]] || { | |
| echo "::error::Downloaded GPU model file permissions are invalid: $file" >&2 | |
| exit 1 | |
| } | |
| if [[ "$algorithm" == "sha256" ]]; then | |
| [[ "$(sudo sha256sum -- "$target" | cut -d' ' -f1)" == "$digest" ]] | |
| else | |
| [[ "$(sudo git hash-object --no-filters -- "$target")" == "$digest" ]] | |
| fi || { | |
| echo "::error::Downloaded GPU model file digest is invalid: $file" >&2 | |
| exit 1 | |
| } | |
| } | |
| download_model_file config.json 659 sha1 0dbb161213629a23f0fc00ef286e6b1e366d180f | |
| download_model_file generation_config.json 242 sha1 dfc11073787daf1b0f9c0f1499487ab5f4c93738 | |
| download_model_file merges.txt 1671839 sha1 20024bfe7c83998e9aeaf98a0cd6a2ce6306c2f0 | |
| download_model_file model.safetensors 988097824 sha256 fdf756fa7fcbe7404d5c60e26bff1a0c8b8aa1f72ced49e7dd0210fe288fb7fe | |
| download_model_file tokenizer.json 7031645 sha1 443909a61d429dff23010e5bddd28ff530edda00 | |
| download_model_file tokenizer_config.json 7305 sha1 07bfe0640cb5a0037f9322287fbfc682806cf672 | |
| download_model_file vocab.json 2776833 sha1 4783fe10ac3adce15ac8f358ef5462739852c569 | |
| for file in config.json generation_config.json merges.txt model.safetensors tokenizer.json tokenizer_config.json vocab.json; do | |
| sudo chown root:root "${model_directory}/${file}" | |
| sudo chmod 0444 "${model_directory}/${file}" | |
| done | |
| sudo chown root:root "$model_directory" | |
| sudo chmod 0555 "$model_directory" | |
| sudo chmod 0555 "$resource_directory" | |
| fi | |
| jq -n \ | |
| --arg architecture "$ARCHITECTURE" \ | |
| --arg gpuProbeImageRef "$probe_image" \ | |
| --arg model "$model_name" \ | |
| --arg modelPath "$model_directory" \ | |
| --arg modelRevision "$model_revision" \ | |
| --arg nimImageRef "$nim_image" \ | |
| --arg vllmImageRef "$vllm_image" ' | |
| { | |
| schemaVersion: 1, | |
| kind: "nemoclaw-native-runtime-qualification-runner-v1", | |
| architecture: $architecture, | |
| gpuProbeImageRef: $gpuProbeImageRef, | |
| nim: { | |
| imageRef: $nimImageRef, | |
| model: $model, | |
| modelPath: $modelPath, | |
| modelRevision: $modelRevision | |
| }, | |
| vllm: { | |
| imageRef: $vllmImageRef, | |
| model: $model, | |
| modelPath: $modelPath, | |
| modelRevision: $modelRevision | |
| } | |
| } | |
| ' | sudo tee "$runner_contract" >/dev/null | |
| sudo chown root:root "$runner_contract" | |
| sudo chmod 0444 "$runner_contract" | |
| [[ -f "$runner_contract" && ! -L "$runner_contract" && "$(stat -c '%u:%g:%a' "$runner_contract")" == "0:0:444" ]] || { | |
| echo "::error::Run-owned GPU runner contract is invalid" >&2 | |
| exit 1 | |
| } | |
| printf 'runner_contract=%s\n' "$runner_contract" >>"$GITHUB_OUTPUT" | |
| - name: Install locked candidate test dependencies without scripts | |
| env: | |
| ACCOUNT: ${{ steps.boundary.outputs.account }} | |
| CANDIDATE_DIRECTORY: ${{ github.workspace }}/.candidate-runtime | |
| GUARD_DIRECTORY: ${{ steps.boundary.outputs.guard_dir }} | |
| HELPER_DIRECTORY: ${{ steps.boundary.outputs.helper_dir }} | |
| NODE_DIRECTORY: ${{ steps.boundary.outputs.node_dir }} | |
| QUALIFICATION_HOME: ${{ steps.boundary.outputs.home }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| sudo -u "$ACCOUNT" env -i \ | |
| HOME="$QUALIFICATION_HOME" \ | |
| LANG=C.UTF-8 \ | |
| PATH="$GUARD_DIRECTORY:$HELPER_DIRECTORY:$NODE_DIRECTORY:/usr/local/bin:/usr/bin:/bin" \ | |
| /bin/bash --noprofile --norc -c ' | |
| set -euo pipefail | |
| cd "$1" | |
| for file in package.json package-lock.json; do | |
| [[ -f "$file" && ! -L "$file" && -O "$file" ]] || { | |
| echo "Candidate dependency manifest is missing or invalid: $file" >&2 | |
| exit 1 | |
| } | |
| done | |
| exec npm --prefix "$1" ci --ignore-scripts | |
| ' bash "$CANDIDATE_DIRECTORY" | |
| - name: Run the authenticated installer qualification | |
| env: | |
| ACCOUNT: ${{ steps.boundary.outputs.account }} | |
| ACCOUNT_GID: ${{ steps.boundary.outputs.gid }} | |
| ACCOUNT_UID: ${{ steps.boundary.outputs.uid }} | |
| ARCHITECTURE: ${{ matrix.case.architecture }} | |
| CANDIDATE_DIRECTORY: ${{ github.workspace }}/.candidate-runtime | |
| CANDIDATE_SHA: ${{ matrix.source.candidateSha }} | |
| CONTAINERS_CONFIG: ${{ steps.boundary.outputs.containers_config }} | |
| GUARD_DIRECTORY: ${{ steps.boundary.outputs.guard_dir }} | |
| HELPER_DIRECTORY: ${{ steps.boundary.outputs.helper_dir }} | |
| INSTALLER_RECEIPT_PARENT: ${{ runner.temp }}/native-runtime-installer | |
| INSTALLER_SHA256: ${{ matrix.installerSha256 }} | |
| QUALIFICATION_HOME: ${{ steps.boundary.outputs.home }} | |
| RUNTIME_DIRECTORY: ${{ steps.boundary.outputs.runtime_dir }} | |
| RUNTIME_DIRECTORY_UNIT: ${{ steps.boundary.outputs.runtime_directory_unit }} | |
| STORAGE_CONFIG: ${{ steps.boundary.outputs.storage_config }} | |
| TRUSTED_USER_UNIT_PATH: /usr/lib/systemd/user:/lib/systemd/user | |
| USER_MANAGER_UNIT: ${{ steps.boundary.outputs.user_manager_unit }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| install -d -m 0700 "$INSTALLER_RECEIPT_PARENT" | |
| sudo chown "$ACCOUNT_UID:$ACCOUNT_GID" "$INSTALLER_RECEIPT_PARENT" | |
| sudo -u "$ACCOUNT" env -i \ | |
| CONTAINERS_CONF="$CONTAINERS_CONFIG" \ | |
| CONTAINERS_STORAGE_CONF="$STORAGE_CONFIG" \ | |
| HOME="$QUALIFICATION_HOME" \ | |
| LANG=C.UTF-8 \ | |
| PATH="$GUARD_DIRECTORY:$HELPER_DIRECTORY:/usr/local/bin:/usr/bin:/bin" \ | |
| XDG_RUNTIME_DIR="$RUNTIME_DIRECTORY" \ | |
| bash .qualification-workflow/scripts/checks/run-native-runtime-installer-qualification.sh \ | |
| --candidate-checkout "$CANDIDATE_DIRECTORY" \ | |
| --candidate-sha "$CANDIDATE_SHA" \ | |
| --installer-sha256 "$INSTALLER_SHA256" \ | |
| --architecture "$ARCHITECTURE" \ | |
| --artifact-dir "$INSTALLER_RECEIPT_PARENT/receipts" | |
| sudo systemctl stop "$USER_MANAGER_UNIT" "$RUNTIME_DIRECTORY_UNIT" | |
| ! systemctl is-active --quiet "$USER_MANAGER_UNIT" && ! systemctl is-active --quiet "$RUNTIME_DIRECTORY_UNIT" || { | |
| echo "::error::Qualification systemd user lifecycle remained active after installer isolation" >&2 | |
| exit 1 | |
| } | |
| sudo pkill -KILL -u "$ACCOUNT_UID" 2>/dev/null || true | |
| sudo systemctl start "$USER_MANAGER_UNIT" | |
| systemctl is-active --quiet "$USER_MANAGER_UNIT" && systemctl is-active --quiet "$RUNTIME_DIRECTORY_UNIT" || { | |
| echo "::error::Qualification systemd user lifecycle did not restart after installer isolation" >&2 | |
| exit 1 | |
| } | |
| sudo -u "$ACCOUNT" env -i \ | |
| HOME="$QUALIFICATION_HOME" \ | |
| LANG=C.UTF-8 \ | |
| PATH=/usr/bin:/bin \ | |
| XDG_RUNTIME_DIR="$RUNTIME_DIRECTORY" \ | |
| /usr/bin/systemctl --user start dbus.socket | |
| sudo -u "$ACCOUNT" env -i \ | |
| HOME="$QUALIFICATION_HOME" \ | |
| LANG=C.UTF-8 \ | |
| PATH=/usr/bin:/bin \ | |
| XDG_RUNTIME_DIR="$RUNTIME_DIRECTORY" \ | |
| /usr/bin/systemctl --user is-active --quiet dbus.socket || { | |
| echo "::error::Qualification systemd user bus socket unit did not restart after installer isolation" >&2 | |
| exit 1 | |
| } | |
| sudo /usr/bin/test -S "$RUNTIME_DIRECTORY/bus" && | |
| sudo /usr/bin/test ! -L "$RUNTIME_DIRECTORY/bus" && | |
| [[ "$(sudo stat -c '%u' -- "$RUNTIME_DIRECTORY/bus")" == "$ACCOUNT_UID" ]] && | |
| sudo -u "$ACCOUNT" /usr/bin/test -S "$RUNTIME_DIRECTORY/bus" || { | |
| echo "::error::Qualification systemd user bus is invalid or inaccessible after installer isolation" >&2 | |
| exit 1 | |
| } | |
| manager_environment="$(sudo -u "$ACCOUNT" env -i \ | |
| HOME="$QUALIFICATION_HOME" \ | |
| LANG=C.UTF-8 \ | |
| PATH=/usr/bin:/bin \ | |
| XDG_RUNTIME_DIR="$RUNTIME_DIRECTORY" \ | |
| /usr/bin/systemctl --user show-environment)" | |
| tr ' ' '\n' <<<"$manager_environment" | grep -Fx -- "SYSTEMD_UNIT_PATH=$TRUSTED_USER_UNIT_PATH" >/dev/null || { | |
| echo "::error::Qualification systemd user manager lost the trusted unit path after installer isolation" >&2 | |
| exit 1 | |
| } | |
| sudo test -d "$INSTALLER_RECEIPT_PARENT/receipts" || { | |
| echo "::error::Installer receipt directory is missing or invalid" >&2 | |
| exit 1 | |
| } | |
| sudo test ! -L "$INSTALLER_RECEIPT_PARENT/receipts" || { | |
| echo "::error::Installer receipt directory must not be a symlink" >&2 | |
| exit 1 | |
| } | |
| - name: Execute the candidate qualification case without credentials | |
| env: | |
| ACCOUNT: ${{ steps.boundary.outputs.account }} | |
| ACCOUNT_GID: ${{ steps.boundary.outputs.gid }} | |
| ACCOUNT_UID: ${{ steps.boundary.outputs.uid }} | |
| CANDIDATE_DIRECTORY: ${{ github.workspace }}/.candidate-runtime | |
| CONTAINERS_CONFIG: ${{ steps.boundary.outputs.containers_config }} | |
| GUARD_DIRECTORY: ${{ steps.boundary.outputs.guard_dir }} | |
| HELPER_DIRECTORY: ${{ steps.boundary.outputs.helper_dir }} | |
| NODE_DIRECTORY: ${{ steps.boundary.outputs.node_dir }} | |
| PODMAN_EXECUTABLE: ${{ steps.boundary.outputs.podman_executable }} | |
| QUALIFICATION_HOME: ${{ steps.boundary.outputs.home }} | |
| RUNNER_CONTRACT: ${{ steps.gpu_resources.outputs.runner_contract }} | |
| RUNTIME_DIRECTORY: ${{ steps.boundary.outputs.runtime_dir }} | |
| RUNTIME_DIRECTORY_UNIT: ${{ steps.boundary.outputs.runtime_directory_unit }} | |
| STORAGE_CONFIG: ${{ steps.boundary.outputs.storage_config }} | |
| USER_MANAGER_UNIT: ${{ steps.boundary.outputs.user_manager_unit }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| live_test="test/e2e/live/native-runtime-qualification-case.test.ts" | |
| [[ -f "$CANDIDATE_DIRECTORY/$live_test" && ! -L "$CANDIDATE_DIRECTORY/$live_test" ]] || { | |
| echo "::error::Candidate commit does not provide the native runtime qualification case executor" >&2 | |
| exit 1 | |
| } | |
| receipt_directory="${RUNNER_TEMP}/native-runtime-case" | |
| install -d -m 0700 "$receipt_directory" | |
| sudo chown "$ACCOUNT_UID:$ACCOUNT_GID" "$receipt_directory" | |
| cd "$CANDIDATE_DIRECTORY" | |
| sudo -u "$ACCOUNT" env -i \ | |
| CI=true \ | |
| CONTAINERS_CONF="$CONTAINERS_CONFIG" \ | |
| CONTAINERS_STORAGE_CONF="$STORAGE_CONFIG" \ | |
| E2E_DEFAULT_ENABLED=0 \ | |
| E2E_JOB=1 \ | |
| HOME="$QUALIFICATION_HOME" \ | |
| LANG=C.UTF-8 \ | |
| NEMOCLAW_NATIVE_RUNTIME_QUALIFICATION_RECEIPT="$receipt_directory/execution.json" \ | |
| NEMOCLAW_NATIVE_RUNTIME_QUALIFICATION_ROW="$QUALIFICATION_ROW" \ | |
| NEMOCLAW_NATIVE_RUNTIME_QUALIFICATION_PODMAN_EXECUTABLE="$PODMAN_EXECUTABLE" \ | |
| NEMOCLAW_NATIVE_RUNTIME_QUALIFICATION_RUNNER_CONTRACT="$RUNNER_CONTRACT" \ | |
| NEMOCLAW_RUN_LIVE_E2E=1 \ | |
| PATH="$GUARD_DIRECTORY:$HELPER_DIRECTORY:$NODE_DIRECTORY:/usr/local/bin:/usr/bin:/bin" \ | |
| XDG_RUNTIME_DIR="$RUNTIME_DIRECTORY" \ | |
| "$CANDIDATE_DIRECTORY/node_modules/.bin/vitest" run \ | |
| --config "$CANDIDATE_DIRECTORY/vitest.config.ts" \ | |
| --project e2e-live \ | |
| "$live_test" | |
| sudo systemctl stop "$USER_MANAGER_UNIT" "$RUNTIME_DIRECTORY_UNIT" | |
| ! systemctl is-active --quiet "$USER_MANAGER_UNIT" && ! systemctl is-active --quiet "$RUNTIME_DIRECTORY_UNIT" || { | |
| echo "::error::Qualification systemd user lifecycle remained active after candidate execution" >&2 | |
| exit 1 | |
| } | |
| sudo pkill -KILL -u "$ACCOUNT_UID" 2>/dev/null || true | |
| - name: Verify Docker stayed unavailable | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| ! systemctl is-active --quiet docker.service | |
| ! systemctl is-active --quiet docker.socket | |
| ! pgrep -x dockerd >/dev/null | |
| [[ ! -S /var/run/docker.sock && ! -S /run/docker.sock ]] | |
| - name: Validate receipts and emit bounded evidence | |
| env: | |
| EVIDENCE_DIRECTORY: ${{ runner.temp }}/native-runtime-evidence | |
| EXECUTION_RECEIPT_PATH: ${{ runner.temp }}/native-runtime-case/execution.json | |
| INSTALLER_RECEIPT_DIRECTORY: ${{ runner.temp }}/native-runtime-installer/receipts | |
| NODE_DIRECTORY: ${{ steps.boundary.outputs.node_dir }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| sudo --preserve-env=EVIDENCE_DIRECTORY,EXECUTION_RECEIPT_PATH,INSTALLER_RECEIPT_DIRECTORY,QUALIFICATION_ROW \ | |
| "$NODE_DIRECTORY/node" --experimental-strip-types --no-warnings \ | |
| .qualification-workflow/tools/e2e/native-runtime-qualification-producer-evidence.mts | |
| sudo chown -R "$(id -u):$(id -g)" "$EVIDENCE_DIRECTORY" | |
| - name: Remove qualification resources | |
| if: always() | |
| env: | |
| ACCOUNT: ${{ steps.boundary.outputs.account }} | |
| ACCOUNT_CREATED: ${{ steps.boundary.outputs.account_created }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| reported_account="${ACCOUNT:-}" | |
| reported_created="${ACCOUNT_CREATED:-}" | |
| ownership_marker="/run/nemoclaw-native-runtime-owner-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" | |
| account="" | |
| uid="" | |
| gid="" | |
| if [[ -e "$ownership_marker" || -L "$ownership_marker" ]]; then | |
| [[ -f "$ownership_marker" && ! -L "$ownership_marker" && "$(stat -c '%u:%g:%a' "$ownership_marker")" == "0:0:400" ]] || { | |
| echo "::error::Qualification account ownership marker cleanup target is invalid" >&2 | |
| exit 1 | |
| } | |
| ownership="$(sudo cat "$ownership_marker")" | |
| [[ "$ownership" =~ ^nemoclawq:([0-9]+):([0-9]+)$ ]] || { | |
| echo "::error::Qualification account ownership marker content is invalid" >&2 | |
| exit 1 | |
| } | |
| account="nemoclawq" | |
| uid="${BASH_REMATCH[1]}" | |
| gid="${BASH_REMATCH[2]}" | |
| [[ -z "$reported_account" || "$reported_account" == "$account" ]] || { | |
| echo "::error::Qualification account output does not match its ownership marker" >&2 | |
| exit 1 | |
| } | |
| if getent passwd "$uid" >/dev/null && ! getent passwd "$account" >/dev/null; then | |
| echo "::error::Qualification account UID belongs to a different host account" >&2 | |
| exit 1 | |
| fi | |
| if getent passwd "$account" >/dev/null; then | |
| [[ "$(id -u "$account")" == "$uid" && "$(id -g "$account")" == "$gid" ]] || { | |
| echo "::error::Qualification account identity changed before cleanup" >&2 | |
| exit 1 | |
| } | |
| fi | |
| if getent group "$account" >/dev/null; then | |
| [[ "$(getent group "$account")" == "${account}:x:${gid}:" ]] || { | |
| echo "::error::Qualification private group identity changed before cleanup" >&2 | |
| exit 1 | |
| } | |
| elif getent passwd "$account" >/dev/null; then | |
| echo "::error::Qualification private group disappeared before cleanup" >&2 | |
| exit 1 | |
| fi | |
| runtime_dir="/run/user/${uid}" | |
| runtime_directory_unit="user-runtime-dir@${uid}.service" | |
| user_manager_unit="user@${uid}.service" | |
| user_manager_dropin_directory="/run/systemd/system/${user_manager_unit}.d" | |
| user_manager_dropin="${user_manager_dropin_directory}/50-nemoclaw-native-runtime.conf" | |
| trusted_user_unit_path="/usr/lib/systemd/user:/lib/systemd/user" | |
| storage_config_directory="/run/nemoclaw-native-runtime-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${uid}" | |
| podman_executable="/nemoclaw-native-runtime-podman-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${uid}" | |
| apparmor_profile="${storage_config_directory}/podman.apparmor" | |
| pasta_apparmor_profile="${storage_config_directory}/pasta.apparmor" | |
| helper_directory="/nemoclaw-native-runtime-helpers-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${uid}" | |
| pasta_executable="${helper_directory}/pasta" | |
| registry_auth_directory="${storage_config_directory}/registry-auth" | |
| registry_auth_file="${registry_auth_directory}/auth.json" | |
| runner_contract="${storage_config_directory}/runner-contract.json" | |
| resource_directory="/var/tmp/nemoclaw-native-runtime-resources-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${uid}" | |
| model_directory="${resource_directory}/model" | |
| sudo systemctl stop "$user_manager_unit" "$runtime_directory_unit" 2>/dev/null || true | |
| ! systemctl is-active --quiet "$user_manager_unit" && ! systemctl is-active --quiet "$runtime_directory_unit" || { | |
| echo "::error::Qualification systemd user lifecycle remained active during cleanup" >&2 | |
| exit 1 | |
| } | |
| if getent passwd "$account" >/dev/null; then | |
| sudo pkill -KILL -u "$uid" 2>/dev/null || true | |
| fi | |
| if [[ -e "$user_manager_dropin_directory" || -L "$user_manager_dropin_directory" ]]; then | |
| [[ -d "$user_manager_dropin_directory" && ! -L "$user_manager_dropin_directory" && "$(stat -c '%u:%g:%a' "$user_manager_dropin_directory")" == "0:0:755" ]] || { | |
| echo "::error::Qualification systemd user-manager drop-in directory cleanup target is invalid" >&2 | |
| exit 1 | |
| } | |
| [[ -f "$user_manager_dropin" && ! -L "$user_manager_dropin" && "$(stat -c '%u:%g:%a:%h' "$user_manager_dropin")" == "0:0:444:1" ]] || { | |
| echo "::error::Qualification systemd user-manager drop-in cleanup target is invalid" >&2 | |
| exit 1 | |
| } | |
| expected_user_manager_dropin="$(printf '[Service]\nEnvironment="SYSTEMD_UNIT_PATH=%s"' "$trusted_user_unit_path")" | |
| [[ "$(cat "$user_manager_dropin")" == "$expected_user_manager_dropin" ]] || { | |
| echo "::error::Qualification systemd user-manager drop-in content changed before cleanup" >&2 | |
| exit 1 | |
| } | |
| sudo unlink "$user_manager_dropin" | |
| sudo rmdir "$user_manager_dropin_directory" | |
| sudo systemctl daemon-reload | |
| fi | |
| if [[ -e "$storage_config_directory" || -L "$storage_config_directory" ]]; then | |
| [[ -d "$storage_config_directory" && ! -L "$storage_config_directory" ]] || { | |
| echo "::error::Qualification storage configuration cleanup target is invalid" >&2 | |
| exit 1 | |
| } | |
| if [[ -e "$apparmor_profile" || -L "$apparmor_profile" ]]; then | |
| [[ -f "$apparmor_profile" && ! -L "$apparmor_profile" && "$(stat -c '%u:%g:%a' "$apparmor_profile")" == "0:0:444" ]] || { | |
| echo "::error::Qualification AppArmor profile cleanup target is invalid" >&2 | |
| exit 1 | |
| } | |
| sudo apparmor_parser -R "$apparmor_profile" | |
| sudo rm -f -- "$apparmor_profile" | |
| fi | |
| if [[ -e "$pasta_apparmor_profile" || -L "$pasta_apparmor_profile" ]]; then | |
| [[ -f "$pasta_apparmor_profile" && ! -L "$pasta_apparmor_profile" && "$(stat -c '%u:%g:%a' "$pasta_apparmor_profile")" == "0:0:444" ]] || { | |
| echo "::error::Qualification pasta AppArmor profile cleanup target is invalid" >&2 | |
| exit 1 | |
| } | |
| sudo apparmor_parser -R "$pasta_apparmor_profile" | |
| sudo rm -f -- "$pasta_apparmor_profile" | |
| fi | |
| if [[ -e "$registry_auth_directory" || -L "$registry_auth_directory" ]]; then | |
| [[ -d "$registry_auth_directory" && ! -L "$registry_auth_directory" && "$(stat -c '%u:%g:%a' "$registry_auth_directory")" == "${uid}:${gid}:700" ]] || { | |
| echo "::error::Qualification registry authentication directory cleanup target is invalid" >&2 | |
| exit 1 | |
| } | |
| if sudo test -e "$registry_auth_file" || sudo test -L "$registry_auth_file"; then | |
| sudo test -f "$registry_auth_file" && | |
| sudo test ! -L "$registry_auth_file" && | |
| [[ "$(sudo stat -c '%u:%g:%a:%h' -- "$registry_auth_file")" == "${uid}:${gid}:600:1" ]] || { | |
| echo "::error::Qualification registry authentication file cleanup target is invalid" >&2 | |
| exit 1 | |
| } | |
| sudo unlink "$registry_auth_file" | |
| fi | |
| sudo rmdir "$registry_auth_directory" | |
| fi | |
| if [[ -e "$runner_contract" || -L "$runner_contract" ]]; then | |
| [[ -f "$runner_contract" && ! -L "$runner_contract" && "$(stat -c '%u:%g:%a' "$runner_contract")" == "0:0:444" ]] || { | |
| echo "::error::Qualification GPU runner contract cleanup target is invalid" >&2 | |
| exit 1 | |
| } | |
| sudo unlink "$runner_contract" | |
| fi | |
| if [[ -e "$storage_config_directory/containers.conf" || -L "$storage_config_directory/containers.conf" ]]; then | |
| [[ -f "$storage_config_directory/containers.conf" && ! -L "$storage_config_directory/containers.conf" && "$(stat -c '%u:%g:%a' "$storage_config_directory/containers.conf")" == "0:0:444" ]] || { | |
| echo "::error::Qualification containers configuration cleanup target is invalid" >&2 | |
| exit 1 | |
| } | |
| sudo unlink "$storage_config_directory/containers.conf" | |
| fi | |
| sudo rm -f -- "$storage_config_directory/storage.conf" | |
| sudo rmdir "$storage_config_directory" | |
| fi | |
| if [[ -e "$resource_directory" || -L "$resource_directory" ]]; then | |
| [[ -d "$resource_directory" && ! -L "$resource_directory" && ("$(stat -c '%u:%g:%a' "$resource_directory")" == "0:0:711" || "$(stat -c '%u:%g:%a' "$resource_directory")" == "0:0:555") ]] || { | |
| echo "::error::Qualification GPU resource directory cleanup target is invalid" >&2 | |
| exit 1 | |
| } | |
| if sudo test -e "$model_directory" || sudo test -L "$model_directory"; then | |
| model_mode="$(sudo stat -c '%u:%g:%a' -- "$model_directory")" | |
| sudo test -d "$model_directory" && | |
| sudo test ! -L "$model_directory" && | |
| [[ "$model_mode" == "${uid}:${gid}:700" || "$model_mode" == "0:0:555" ]] || { | |
| echo "::error::Qualification GPU model directory cleanup target is invalid" >&2 | |
| exit 1 | |
| } | |
| for file in config.json generation_config.json merges.txt model.safetensors tokenizer.json tokenizer_config.json vocab.json; do | |
| target="${model_directory}/${file}" | |
| if sudo test -e "$target" || sudo test -L "$target"; then | |
| file_mode="$(sudo stat -c '%u:%g:%a:%h' -- "$target")" | |
| sudo test -f "$target" && | |
| sudo test ! -L "$target" && | |
| [[ "$file_mode" == "${uid}:${gid}:600:1" || "$file_mode" == "0:0:444:1" ]] || { | |
| echo "::error::Qualification GPU model file cleanup target is invalid: $file" >&2 | |
| exit 1 | |
| } | |
| sudo unlink "$target" | |
| fi | |
| done | |
| sudo rmdir "$model_directory" || { | |
| echo "::error::Qualification GPU model directory contains unexpected entries" >&2 | |
| exit 1 | |
| } | |
| fi | |
| sudo rmdir "$resource_directory" || { | |
| echo "::error::Qualification GPU resource directory contains unexpected entries" >&2 | |
| exit 1 | |
| } | |
| fi | |
| if [[ -e "$podman_executable" || -L "$podman_executable" ]]; then | |
| [[ -f "$podman_executable" && ! -L "$podman_executable" && "$(stat -c '%u:%g:%a' "$podman_executable")" == "0:0:555" ]] || { | |
| echo "::error::Qualification Podman executable cleanup target is invalid" >&2 | |
| exit 1 | |
| } | |
| sudo rm -f -- "$podman_executable" | |
| fi | |
| if [[ -e "$helper_directory" || -L "$helper_directory" ]]; then | |
| [[ -d "$helper_directory" && ! -L "$helper_directory" && "$(stat -c '%u:%g:%a' "$helper_directory")" == "0:0:555" ]] || { | |
| echo "::error::Qualification helper directory cleanup target is invalid" >&2 | |
| exit 1 | |
| } | |
| [[ -f "$pasta_executable" && ! -L "$pasta_executable" && "$(stat -c '%u:%g:%a' "$pasta_executable")" == "0:0:555" ]] || { | |
| echo "::error::Qualification pasta executable cleanup target is invalid" >&2 | |
| exit 1 | |
| } | |
| sudo rm -f -- "$pasta_executable" | |
| sudo rmdir "$helper_directory" | |
| fi | |
| if [[ -e "$runtime_dir" || -L "$runtime_dir" ]]; then | |
| echo "::error::Qualification runtime directory remains after its systemd cleanup" >&2 | |
| exit 1 | |
| fi | |
| if getent passwd "$account" >/dev/null; then | |
| sudo userdel --remove "$account" | |
| fi | |
| if getent group "$account" >/dev/null; then | |
| sudo groupdel "$account" | |
| fi | |
| if getent passwd "$account" >/dev/null; then | |
| echo "::error::Qualification account still exists after cleanup" >&2 | |
| exit 1 | |
| fi | |
| if getent group "$account" >/dev/null; then | |
| echo "::error::Qualification private group still exists after cleanup" >&2 | |
| exit 1 | |
| fi | |
| if grep -q "^${account}:" /etc/subuid /etc/subgid; then | |
| echo "::error::Qualification subordinate-ID authorization remains after cleanup" >&2 | |
| exit 1 | |
| fi | |
| [[ ! -e "/run/user/${uid}" && ! -L "/run/user/${uid}" ]] || { | |
| echo "::error::Qualification runtime directory remains after cleanup" >&2 | |
| exit 1 | |
| } | |
| storage_config_directory="/run/nemoclaw-native-runtime-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${uid}" | |
| [[ ! -e "$storage_config_directory" && ! -L "$storage_config_directory" ]] || { | |
| echo "::error::Qualification storage configuration remains after cleanup" >&2 | |
| exit 1 | |
| } | |
| [[ ! -e "$podman_executable" && ! -L "$podman_executable" ]] || { | |
| echo "::error::Qualification Podman executable remains after cleanup" >&2 | |
| exit 1 | |
| } | |
| [[ ! -e "$helper_directory" && ! -L "$helper_directory" ]] || { | |
| echo "::error::Qualification helper directory remains after cleanup" >&2 | |
| exit 1 | |
| } | |
| [[ ! -e "$resource_directory" && ! -L "$resource_directory" ]] || { | |
| echo "::error::Qualification GPU resource directory remains after cleanup" >&2 | |
| exit 1 | |
| } | |
| sudo rm -f -- "$ownership_marker" | |
| elif [[ -n "$reported_account" || "$reported_created" == "true" ]]; then | |
| echo "::error::Qualification account output exists without its ownership marker" >&2 | |
| exit 1 | |
| fi | |
| [[ ! -e "$ownership_marker" && ! -L "$ownership_marker" ]] || { | |
| echo "::error::Qualification account ownership marker remains after cleanup" >&2 | |
| exit 1 | |
| } | |
| - name: Upload the qualification case evidence | |
| if: success() | |
| uses: NVIDIA/NemoClaw/.github/actions/upload-e2e-artifacts@7768e15eb90d3ee2d33432f481dfe8747e4f6d57 | |
| with: | |
| name: ${{ matrix.artifactName }} | |
| path: ${{ runner.temp }}/native-runtime-evidence/ | |
| native-runtime-qualification-producer-aggregate: | |
| name: Aggregate native runtime qualification evidence | |
| needs: | |
| [ | |
| generate-matrix, | |
| native-runtime-qualification-producer-plan, | |
| native-runtime-qualification-producer, | |
| ] | |
| if: ${{ always() && needs.native-runtime-qualification-producer-plan.result == 'success' && needs.native-runtime-qualification-producer.result == 'success' && contains(fromJSON(needs.generate-matrix.outputs.selected_jobs), 'native-runtime-qualification-producer') }} | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| permissions: | |
| actions: read | |
| contents: read | |
| pull-requests: read | |
| steps: | |
| - name: Check out the qualification aggregator | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| repository: ${{ github.repository }} | |
| ref: ${{ github.workflow_sha }} | |
| path: .qualification-aggregate | |
| persist-credentials: false | |
| sparse-checkout: | | |
| src/lib/onboard/runtime-provider/native-qualification-authority.ts | |
| test/e2e/registry/native-runtime-qualification.ts | |
| tools/e2e/native-runtime-qualification-producer-plan.mts | |
| tools/e2e/native-runtime-qualification-producer-aggregate.mts | |
| sparse-checkout-cone-mode: false | |
| - name: Download the exact case evidence cohort | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| pattern: native-runtime-qualification-evidence-${{ inputs.checkout_sha }}-* | |
| path: ${{ runner.temp }}/native-runtime-case-artifacts | |
| merge-multiple: false | |
| - name: Resolve this aggregate job identity | |
| id: aggregate-job | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| PRODUCER_RUN_ATTEMPT: ${{ github.run_attempt }} | |
| PRODUCER_RUN_ID: ${{ github.run_id }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| jobs="$(gh api --method GET \ | |
| "repos/${GITHUB_REPOSITORY}/actions/runs/${PRODUCER_RUN_ID}/attempts/${PRODUCER_RUN_ATTEMPT}/jobs" \ | |
| -f per_page=100)" | |
| total_count="$(jq -er '.total_count | select(type == "number" and . >= 1)' <<<"$jobs")" || { | |
| echo "::error::Aggregate job lookup returned an invalid job count" >&2 | |
| exit 1 | |
| } | |
| (( total_count <= 100 )) || { | |
| echo "::error::Aggregate job lookup exceeds the bounded 100-job page" >&2 | |
| exit 1 | |
| } | |
| job_id="$(jq -er \ | |
| --arg name 'Aggregate native runtime qualification evidence' \ | |
| --argjson runId "$PRODUCER_RUN_ID" \ | |
| --argjson attempt "$PRODUCER_RUN_ATTEMPT" ' | |
| [.jobs[] | select( | |
| .name == $name and | |
| .run_id == $runId and | |
| .run_attempt == $attempt and | |
| .status == "in_progress" | |
| )] | | |
| select(length == 1) | | |
| .[0].id | |
| ' <<<"$jobs")" || { | |
| echo "::error::Could not resolve one in-progress aggregate job identity" >&2 | |
| exit 1 | |
| } | |
| [[ "$job_id" =~ ^[1-9][0-9]{0,19}$ ]] || { | |
| echo "::error::Aggregate job identity is invalid" >&2 | |
| exit 1 | |
| } | |
| printf 'job_id=%s\n' "$job_id" >>"$GITHUB_OUTPUT" | |
| - name: Set up Node for qualification aggregation | |
| uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: 22.19.0 | |
| - name: Validate and aggregate all 24 case receipts | |
| working-directory: .qualification-aggregate | |
| env: | |
| AGGREGATE_JOB_ID: ${{ steps.aggregate-job.outputs.job_id }} | |
| CASE_ARTIFACT_ROOT: ${{ runner.temp }}/native-runtime-case-artifacts | |
| EVIDENCE_DIRECTORY: ${{ runner.temp }}/native-runtime-aggregate | |
| QUALIFICATION_PLAN: ${{ needs.native-runtime-qualification-producer-plan.outputs.matrix }} | |
| run: >- | |
| node --experimental-strip-types --no-warnings | |
| tools/e2e/native-runtime-qualification-producer-aggregate.mts | |
| - name: Upload aggregate evidence | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: native-runtime-qualification-${{ inputs.checkout_sha }} | |
| path: ${{ runner.temp }}/native-runtime-aggregate/ | |
| if-no-files-found: error | |
| retention-days: 30 | |
| compression-level: 9 | |
| retired-selector-compatibility: | |
| needs: generate-matrix | |
| if: ${{ inputs.checkout_sha != '' && (contains(format(',{0},', inputs.jobs), ',credential-migration,') || contains(format(',{0},', inputs.jobs), ',credential-sanitization,') || contains(format(',{0},', inputs.jobs), ',diagnostics,') || contains(format(',{0},', inputs.jobs), ',docs-validation,') || contains(format(',{0},', inputs.jobs), ',gateway-drift-preflight,') || contains(format(',{0},', inputs.jobs), ',gateway-health-honest,') || contains(format(',{0},', inputs.jobs), ',onboard-negative-paths,') || contains(format(',{0},', inputs.jobs), ',openshell-version-pin,') || contains(format(',{0},', inputs.jobs), ',sandbox-rebuild,') || contains(format(',{0},', inputs.jobs), ',ubuntu-repo-cli-smoke,') || contains(format(',{0},', inputs.jobs), ',upgrade-stale-sandbox,') || contains(format(',{0},', inputs.targets), ',sandbox-rebuild,') || contains(format(',{0},', inputs.targets), ',upgrade-stale-sandbox,')) }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| env: | |
| E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts/live/retired-selector-compatibility | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| repository: ${{ inputs.checkout_repository || github.repository }} | |
| ref: ${{ inputs.checkout_sha || github.sha }} | |
| persist-credentials: false | |
| - name: Prepare E2E workspace | |
| uses: NVIDIA/NemoClaw/.github/actions/prepare-e2e@f6304bc25fc35bfaa441c8c2fbfee38f72805a75 | |
| with: | |
| build-cli: "false" | |
| - name: Restore exact-commit CLI artifact | |
| uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@c246409193a31133cab10c8a3589001cc0d59eb3 | |
| with: | |
| provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} | |
| - name: Verify retired selector replacements | |
| env: | |
| JOBS: ${{ inputs.jobs }} | |
| TARGETS: ${{ inputs.targets }} | |
| run: npx tsx tools/e2e/retired-selector-compatibility.mts | |
| - name: Upload retired selector compatibility evidence | |
| if: always() | |
| uses: NVIDIA/NemoClaw/.github/actions/upload-e2e-artifacts@7768e15eb90d3ee2d33432f481dfe8747e4f6d57 | |
| with: | |
| name: e2e-retired-selector-compatibility | |
| path: e2e-artifacts/live/retired-selector-compatibility/ | |
| staging-brev-launchable: | |
| name: Exact staging Brev Launchable | |
| needs: generate-matrix | |
| if: ${{ github.repository == 'NVIDIA/NemoClaw' && github.ref == 'refs/heads/main' && github.event_name == 'workflow_dispatch' && ((inputs.jobs == 'staging-brev-launchable' && inputs.targets == '') || (inputs.include_staging_brev_launchable && inputs.jobs == '' && inputs.targets == '')) }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 180 | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: staging-brev-launchable-cpu | |
| queue: max | |
| cancel-in-progress: false | |
| env: | |
| CANDIDATE_SHA: ${{ inputs.checkout_sha || github.sha }} | |
| E2E_JOB: "1" | |
| INSTANCE_NAME: nclaw-e2e-${{ github.run_id }}-${{ github.run_attempt }} | |
| steps: | |
| - name: Checkout trusted Launchable lane | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ github.workflow_sha }} | |
| persist-credentials: false | |
| sparse-checkout: | | |
| tools/e2e/brev-launchable-e2e.sh | |
| sparse-checkout-cone-mode: false | |
| - id: workspace | |
| name: Prepare the trusted lane | |
| env: | |
| BREV_API_KEY: ${{ github.repository == 'NVIDIA/NemoClaw' && github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && secrets.BREV_API_KEY || '' }} | |
| BREV_CLI_SHA256: d4aa49db1716f10308a6587778a676a0c0076bd48a212d86a421ab9550bc8f32 | |
| BREV_CLI_VERSION: 0.6.334 | |
| BREV_ORG_ID: ${{ github.repository == 'NVIDIA/NemoClaw' && github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && secrets.BREV_ORG_ID || '' }} | |
| run: | | |
| set -euo pipefail | |
| work_dir="$(mktemp -d "${RUNNER_TEMP}/nemoclaw-launchable-e2e.XXXXXX")" | |
| chmod 700 "$work_dir" | |
| archive="${RUNNER_TEMP}/brev-cli.tar.gz" | |
| curl -fsSL -o "$archive" "https://github.com/brevdev/brev-cli/releases/download/v${BREV_CLI_VERSION}/brev-cli_${BREV_CLI_VERSION}_linux_amd64.tar.gz" | |
| printf '%s %s\n' "$BREV_CLI_SHA256" "$archive" | sha256sum -c - | |
| tar -xzf "$archive" -C "${RUNNER_TEMP}" brev && sudo install -m 0755 "${RUNNER_TEMP}/brev" /usr/local/bin/brev | |
| brev login --api-key "$BREV_API_KEY" --org-id "$BREV_ORG_ID" | |
| printf 'work_dir=%s\n' "$work_dir" >> "$GITHUB_OUTPUT" | |
| - name: Build, deploy, verify, test, and clean up | |
| env: | |
| BREV_LAUNCHABLE_ID: ${{ vars.NEMOCLAW_STAGING_LAUNCHABLE_ID }} | |
| GH_TOKEN: ${{ github.repository == 'NVIDIA/NemoClaw' && github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && secrets.NEMOCLAW_IMAGE_DISPATCH_TOKEN || '' }} | |
| NVIDIA_INFERENCE_API_KEY: ${{ github.repository == 'NVIDIA/NemoClaw' && github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && secrets.NVIDIA_INFERENCE_API_KEY || '' }} | |
| WORK_DIR: ${{ steps.workspace.outputs.work_dir }} | |
| run: tools/e2e/brev-launchable-e2e.sh | |
| - name: Upload Launchable evidence | |
| if: ${{ always() && steps.workspace.outputs.work_dir != '' }} | |
| uses: NVIDIA/NemoClaw/.github/actions/upload-e2e-artifacts@7768e15eb90d3ee2d33432f481dfe8747e4f6d57 | |
| with: | |
| name: staging-brev-launchable-${{ env.CANDIDATE_SHA }}-${{ github.run_id }}-${{ github.run_attempt }} | |
| path: | | |
| ${{ steps.workspace.outputs.work_dir }}/lane.log | |
| ${{ steps.workspace.outputs.work_dir }}/launchable-e2e.json | |
| ${{ steps.workspace.outputs.work_dir }}/full-e2e.log | |
| ${{ steps.workspace.outputs.work_dir }}/cleanup.json | |
| live: | |
| needs: [base-image-publication, generate-matrix] | |
| if: ${{ needs.generate-matrix.outputs.matrix != '[]' }} | |
| runs-on: ${{ matrix.runner }} | |
| timeout-minutes: 45 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: ${{ fromJSON(needs.generate-matrix.outputs.matrix) }} | |
| env: | |
| E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts/live | |
| NEMOCLAW_LANGCHAIN_DEEPAGENTS_CODE_SANDBOX_BASE_IMAGE_REF: ${{ needs.base-image-publication.outputs.dcode_base_ref }} | |
| NEMOCLAW_CLI_BIN: ${{ github.workspace }}/bin/nemoclaw.js | |
| NEMOCLAW_RUN_LIVE_E2E: "1" | |
| NEMOCLAW_E2E_USE_HOSTED_INFERENCE: "1" | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| repository: ${{ inputs.checkout_repository || github.repository }} | |
| ref: ${{ inputs.checkout_sha || github.sha }} | |
| persist-credentials: false | |
| # Keep only the credential-bearing step anchored. Cleanup mappings stay | |
| # explicit because strict YAML decoders reject 100 or more aliases here. | |
| - &dockerhub-auth | |
| name: Authenticate to Docker Hub | |
| uses: NVIDIA/NemoClaw/.github/actions/docker-auth-setup@05fa6b810017752ab21148cb7e9d82d12a88c92f | |
| with: | |
| auth-required: ${{ github.repository == 'NVIDIA/NemoClaw' && github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && inputs.checkout_sha == '' && '1' || '0' }} | |
| username: ${{ github.repository == 'NVIDIA/NemoClaw' && github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && inputs.checkout_sha == '' && secrets.DOCKERHUB_USERNAME || '' }} | |
| token: ${{ github.repository == 'NVIDIA/NemoClaw' && github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && inputs.checkout_sha == '' && secrets.DOCKERHUB_TOKEN || '' }} | |
| - name: Configure live E2E trace directory | |
| env: | |
| TARGET_ID: ${{ matrix.id }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| printf 'NEMOCLAW_TRACE_DIR=%s\n' "${RUNNER_TEMP}/nemoclaw-e2e-traces/${TARGET_ID}" >> "${GITHUB_ENV}" | |
| # invalidState: the selected PR-modifiable TUI check needs a PTY driver, | |
| # but the fixed GitHub-hosted runner image does not provide expect. | |
| # sourceBoundary: privileged host setup runs from the first-party | |
| # host-dependency-setup action pinned to an immutable full SHA, never the | |
| # PR-controlled target ref; the check only consumes expect without privilege. | |
| # whyNotSourceFix: GitHub-hosted jobs cannot use a repository-owned host | |
| # image, and caching privileged dpkg state between clean runners is not | |
| # supported. | |
| # regressionTest: the workflow-boundary suite pins this target, condition, | |
| # ordering, action provenance, and package mapping. | |
| # removalCondition: remove the install when the hosted runner supplies | |
| # expect or the acceptance check no longer requires a PTY. | |
| - name: Install Deep Agents Code TUI host dependencies | |
| if: ${{ matrix.id == 'ubuntu-repo-cloud-langchain-deepagents-code' }} | |
| uses: NVIDIA/NemoClaw/.github/actions/host-dependency-setup@4def1501b34ce586f83b91af50a66b5d22b31d75 | |
| with: | |
| packages: expect | |
| # Configure NEMOCLAW_TRACE_DIR before workspace prep so every child | |
| # command writes raw traces under runner temp, never under upload roots. | |
| - name: Prepare E2E workspace | |
| uses: NVIDIA/NemoClaw/.github/actions/prepare-e2e@f6304bc25fc35bfaa441c8c2fbfee38f72805a75 | |
| with: | |
| build-cli: "false" | |
| - name: Record immutable Deep Agents Code base evidence | |
| if: ${{ matrix.id == 'ubuntu-repo-cloud-langchain-deepagents-code' }} | |
| env: | |
| BASE_CONTRACT: ${{ needs.base-image-publication.outputs.dcode_base_contract }} | |
| CANDIDATE_SHA: ${{ inputs.checkout_sha || github.sha }} | |
| TARGET_ID: ${{ matrix.id }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| evidence_dir="${E2E_ARTIFACT_DIR}/${TARGET_ID}" | |
| install -d -m 0700 "${evidence_dir}" | |
| jq -e -n \ | |
| --arg candidateSha "${CANDIDATE_SHA}" \ | |
| --argjson baseContract "${BASE_CONTRACT}" \ | |
| '{contractVersion: 1, candidateSha: $candidateSha, base: $baseContract} | |
| | select( | |
| (.candidateSha | test("^[0-9a-f]{40}$")) | |
| and (.base.reference | test("@sha256:[0-9a-f]{64}$")) | |
| )' >"${evidence_dir}/dcode-base-image.json" | |
| - name: Restore exact-commit CLI artifact | |
| uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@c246409193a31133cab10c8a3589001cc0d59eb3 | |
| with: | |
| provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} | |
| # invalidState: a profile plugin installed with --no-deps can import even | |
| # when an incomplete base image omitted its required upstream packages. | |
| # sourceBoundary: this trusted workflow scopes the repo-owned stripped-base | |
| # build to the exact DCode target; the production Dockerfile must reject it | |
| # at the isolated import gate before its later dependency-consistency check. | |
| # whyNotSourceFix: dependency completeness belongs to the hash-locked base; | |
| # resolving dependencies during local plugin install would duplicate that | |
| # trust boundary, so the regression constructs the invalid input instead. | |
| # regressionTest: workflow-boundary tests pin the target, script, and | |
| # ordering; build-gate tests pin the base build and failure contract. | |
| # removalCondition: remove only if package installation no longer uses | |
| # --no-deps or an equivalent earlier build gate proves both imports. | |
| - name: Verify DCode profile import gate rejects missing base dependencies | |
| if: ${{ matrix.id == 'ubuntu-repo-cloud-langchain-deepagents-code' }} | |
| shell: bash | |
| run: bash scripts/check-dcode-profile-import-gate.sh | |
| - name: Run live E2E tests | |
| env: | |
| E2E_TARGET_ID: ${{ matrix.id }} | |
| NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} | |
| TARGET_ID: ${{ matrix.id }} | |
| run: | | |
| set -euo pipefail | |
| npx tsx tools/e2e/live-vitest-invocation.mts run --test-path test/e2e/live/registry-targets.test.ts --selector "^${TARGET_ID}$" | |
| # The sanitizer reads raw traces only after checking the workflow-owned | |
| # runner-temp path, then writes the timing-only file into upload roots. | |
| - name: Build trusted live E2E timing summary | |
| if: always() | |
| env: | |
| TARGET_ID: ${{ matrix.id }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| expected_trace_dir="${RUNNER_TEMP}/nemoclaw-e2e-traces/${TARGET_ID}" | |
| if [ -z "${RUNNER_TEMP}" ] || [ "${NEMOCLAW_TRACE_DIR}" != "${expected_trace_dir}" ]; then | |
| echo "::error::Refusing to sanitize unexpected raw trace path" >&2 | |
| exit 1 | |
| fi | |
| python3 scripts/e2e/sanitize-trace-timing.py \ | |
| "${NEMOCLAW_TRACE_DIR}" \ | |
| "${E2E_ARTIFACT_DIR}/${TARGET_ID}" | |
| # Cleanup intentionally runs after sanitization and before upload so raw | |
| # trace JSON never becomes part of the uploaded artifact surface. | |
| - name: Delete raw live E2E traces | |
| if: always() | |
| env: | |
| TARGET_ID: ${{ matrix.id }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| expected_trace_dir="${RUNNER_TEMP}/nemoclaw-e2e-traces/${TARGET_ID}" | |
| if [ -z "${RUNNER_TEMP}" ] || [ "${NEMOCLAW_TRACE_DIR}" != "${expected_trace_dir}" ]; then | |
| echo "::error::Refusing to delete unexpected raw trace path" >&2 | |
| exit 1 | |
| fi | |
| rm -rf -- "${NEMOCLAW_TRACE_DIR}" | |
| - name: Summarize artifacts | |
| if: always() | |
| env: | |
| TARGET_ID: ${{ matrix.id }} | |
| TARGET_LABEL: ${{ matrix.label }} | |
| run: | | |
| python3 - <<'PY' >> "$GITHUB_STEP_SUMMARY" | |
| import json | |
| import os | |
| from pathlib import Path | |
| root = Path(os.environ["E2E_ARTIFACT_DIR"]) / os.environ["TARGET_ID"] | |
| plan_path = root / "run-plan.json" | |
| print("## E2E Targets") | |
| print() | |
| print("- Project: `e2e-live`") | |
| print(f"- Target: `{os.environ['TARGET_ID']}`") | |
| print(f"- Label: `{os.environ['TARGET_LABEL']}`") | |
| print(f"- Artifact root: `{root}`") | |
| print() | |
| print("| Target | Manifest | Expected state | Suites | Phases |") | |
| print("| --- | --- | --- | --- | --- |") | |
| if plan_path.exists(): | |
| plan = json.loads(plan_path.read_text(encoding="utf-8")) | |
| suites = ", ".join(plan.get("suiteIds") or []) or "(none)" | |
| phases = ", ".join(plan.get("phases") or []) or "(none)" | |
| print( | |
| "| " | |
| f"`{plan.get('targetId') or os.environ['TARGET_ID']}` | " | |
| f"`{plan.get('manifestPath') or 'not-yet-defined'}` | " | |
| f"`{plan.get('expectedStateId') or 'not-yet-defined'}` | " | |
| f"{suites} | {phases} |" | |
| ) | |
| else: | |
| print( | |
| "| " | |
| f"`{os.environ['TARGET_ID']}` | `(missing run-plan.json)` | " | |
| "`(missing run-plan.json)` | `(missing)` | `(missing)` |" | |
| ) | |
| PY | |
| - name: Upload E2E artifacts | |
| if: always() | |
| uses: NVIDIA/NemoClaw/.github/actions/upload-e2e-artifacts@7768e15eb90d3ee2d33432f481dfe8747e4f6d57 | |
| with: | |
| name: e2e-${{ matrix.id }} | |
| path: | | |
| e2e-artifacts/live/${{ matrix.id }}/run-plan.json | |
| e2e-artifacts/live/${{ matrix.id }}/target.json | |
| e2e-artifacts/live/${{ matrix.id }}/target-result.json | |
| e2e-artifacts/live/${{ matrix.id }}/test-progress.json | |
| e2e-artifacts/live/${{ matrix.id }}/environment.result.json | |
| e2e-artifacts/live/${{ matrix.id }}/onboarding.result.json | |
| e2e-artifacts/live/${{ matrix.id }}/state-validation.result.json | |
| e2e-artifacts/live/${{ matrix.id }}/dcode-base-image.json | |
| e2e-artifacts/live/${{ matrix.id }}/cloud-onboard-trace-timing-summary.json | |
| e2e-artifacts/live/risk-signal.json | |
| e2e-artifacts/live/${{ matrix.id }}/actions/ | |
| e2e-artifacts/live/${{ matrix.id }}/logs/ | |
| e2e-artifacts/live/${{ matrix.id }}/shell/ | |
| - name: Clean up Docker auth | |
| if: always() | |
| shell: bash | |
| run: bash .github/scripts/docker-auth-cleanup.sh | |
| # Credential-free tests opt in with a tag beside the test. Discovery supplies | |
| # only a validated test ID, file, and Vitest project; this E2E workflow owns | |
| # the shared job's runner, setup, timeout, permissions, and artifact policy. | |
| shared-e2e: | |
| name: Shared E2E (${{ matrix.id }}) | |
| needs: generate-matrix | |
| if: ${{ needs.generate-matrix.outputs.test_matrix != '[]' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: ${{ fromJSON(needs.generate-matrix.outputs.test_matrix) }} | |
| env: | |
| CHECK_DOC_LINKS_REMOTE: "0" | |
| E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts/live/${{ matrix.id }} | |
| E2E_TARGET_ID: ${{ matrix.id }} | |
| NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1" | |
| NEMOCLAW_CLI_BIN: ${{ github.workspace }}/bin/nemoclaw.js | |
| NEMOCLAW_NON_INTERACTIVE: "1" | |
| NEMOCLAW_RUN_LIVE_E2E: "1" | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| repository: ${{ inputs.checkout_repository || github.repository }} | |
| ref: ${{ inputs.checkout_sha || github.sha }} | |
| persist-credentials: false | |
| - name: Prepare E2E workspace | |
| uses: NVIDIA/NemoClaw/.github/actions/prepare-e2e@f6304bc25fc35bfaa441c8c2fbfee38f72805a75 | |
| with: | |
| build-cli: "false" | |
| - name: Restore exact-commit CLI artifact | |
| uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@c246409193a31133cab10c8a3589001cc0d59eb3 | |
| with: | |
| provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} | |
| - name: Run tagged credential-free test | |
| env: | |
| TEST_FILE: ${{ matrix.file }} | |
| TEST_PROJECT: ${{ matrix.project }} | |
| run: | | |
| set -euo pipefail | |
| npx vitest run --project "${TEST_PROJECT}" "${TEST_FILE}" \ | |
| --tags-filter=e2e/credential-free \ | |
| --silent=false --reporter=default --reporter=test/e2e/risk-signal-reporter.ts | |
| - name: Upload test artifacts | |
| if: always() | |
| uses: NVIDIA/NemoClaw/.github/actions/upload-e2e-artifacts@7768e15eb90d3ee2d33432f481dfe8747e4f6d57 | |
| catalogue-standard: | |
| name: ${{ matrix.display_name }} | |
| needs: generate-matrix | |
| if: ${{ needs.generate-matrix.outputs.catalogue_standard_matrix != '[]' }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: ${{ fromJSON(needs.generate-matrix.outputs.catalogue_standard_matrix) }} | |
| uses: ./.github/workflows/e2e-standard-profile.yaml | |
| with: | |
| candidate_repository: ${{ inputs.checkout_repository || github.repository }} | |
| candidate_sha: ${{ inputs.checkout_sha || github.sha }} | |
| risk_signal_expected_sha: ${{ github.event_name == 'workflow_dispatch' && inputs.checkout_sha != '' && inputs.checkout_sha || '' }} | |
| risk_signal_correlation_id: ${{ github.event_name == 'workflow_dispatch' && inputs.checkout_sha != '' && inputs.correlation_id || '' }} | |
| cli_artifact_provenance: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} | |
| credential_boundary: no provider credential | |
| target_id: ${{ matrix.target_id }} | |
| catalogue_id: ${{ matrix.id }} | |
| runner: ${{ matrix.runner_key != '' && fromJSON(needs.generate-matrix.outputs.runner_routing)[matrix.runner_key] || matrix.runner }} | |
| checkout_sha: ${{ inputs.checkout_sha }} | |
| workflow_sha: ${{ inputs.workflow_sha }} | |
| test_file: ${{ matrix.test_file }} | |
| timeout_minutes: ${{ matrix.timeout_minutes }} | |
| install_mode: ${{ matrix.install_mode }} | |
| install_non_interactive: ${{ matrix.install_non_interactive }} | |
| restore_cli: ${{ matrix.restore_cli }} | |
| cloudflared: ${{ matrix.cloudflared }} | |
| host_packages: ${{ matrix.host_packages }} | |
| host_preparation: ${{ matrix.host_preparation }} | |
| runner_comparison: ${{ matrix.runner_comparison }} | |
| compatible_api_key: ${{ matrix.compatible_api_key }} | |
| github_token: false | |
| shard: ${{ matrix.shard }} | |
| artifact_layout: ${{ matrix.artifact_layout }} | |
| trusted_main: ${{ github.repository == 'NVIDIA/NemoClaw' && github.ref == 'refs/heads/main' && (inputs.checkout_sha == '' || needs.generate-matrix.outputs.e2e_credentials_allowed == 'true') }} | |
| secrets: | |
| DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} | |
| DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} | |
| catalogue-nvidia-api: | |
| name: ${{ matrix.display_name }} | |
| needs: generate-matrix | |
| if: ${{ needs.generate-matrix.outputs.catalogue_nvidia_api_matrix != '[]' }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: ${{ fromJSON(needs.generate-matrix.outputs.catalogue_nvidia_api_matrix) }} | |
| uses: ./.github/workflows/e2e-standard-profile.yaml | |
| with: | |
| candidate_repository: ${{ inputs.checkout_repository || github.repository }} | |
| candidate_sha: ${{ inputs.checkout_sha || github.sha }} | |
| risk_signal_expected_sha: ${{ github.event_name == 'workflow_dispatch' && inputs.checkout_sha != '' && inputs.checkout_sha || '' }} | |
| risk_signal_correlation_id: ${{ github.event_name == 'workflow_dispatch' && inputs.checkout_sha != '' && inputs.correlation_id || '' }} | |
| cli_artifact_provenance: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} | |
| credential_boundary: NVIDIA API key | |
| target_id: ${{ matrix.target_id }} | |
| catalogue_id: ${{ matrix.id }} | |
| runner: ${{ matrix.runner_key != '' && fromJSON(needs.generate-matrix.outputs.runner_routing)[matrix.runner_key] || matrix.runner }} | |
| checkout_sha: ${{ inputs.checkout_sha }} | |
| workflow_sha: ${{ inputs.workflow_sha }} | |
| test_file: ${{ matrix.test_file }} | |
| timeout_minutes: ${{ matrix.timeout_minutes }} | |
| install_mode: ${{ matrix.install_mode }} | |
| install_non_interactive: ${{ matrix.install_non_interactive }} | |
| restore_cli: ${{ matrix.restore_cli }} | |
| cloudflared: ${{ matrix.cloudflared }} | |
| host_packages: ${{ matrix.host_packages }} | |
| host_preparation: ${{ matrix.host_preparation }} | |
| runner_comparison: ${{ matrix.runner_comparison }} | |
| compatible_api_key: ${{ matrix.compatible_api_key }} | |
| github_token: false | |
| shard: ${{ matrix.shard }} | |
| artifact_layout: ${{ matrix.artifact_layout }} | |
| trusted_main: ${{ github.repository == 'NVIDIA/NemoClaw' && github.ref == 'refs/heads/main' && (inputs.checkout_sha == '' || needs.generate-matrix.outputs.e2e_credentials_allowed == 'true') }} | |
| secrets: | |
| DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} | |
| DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| catalogue-nvidia-inference: | |
| name: ${{ matrix.display_name }} | |
| needs: generate-matrix | |
| if: ${{ needs.generate-matrix.outputs.catalogue_nvidia_inference_matrix != '[]' }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: ${{ fromJSON(needs.generate-matrix.outputs.catalogue_nvidia_inference_matrix) }} | |
| uses: ./.github/workflows/e2e-standard-profile.yaml | |
| with: | |
| candidate_repository: ${{ inputs.checkout_repository || github.repository }} | |
| candidate_sha: ${{ inputs.checkout_sha || github.sha }} | |
| risk_signal_expected_sha: ${{ github.event_name == 'workflow_dispatch' && inputs.checkout_sha != '' && inputs.checkout_sha || '' }} | |
| risk_signal_correlation_id: ${{ github.event_name == 'workflow_dispatch' && inputs.checkout_sha != '' && inputs.correlation_id || '' }} | |
| cli_artifact_provenance: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} | |
| credential_boundary: NVIDIA inference API key | |
| target_id: ${{ matrix.target_id }} | |
| catalogue_id: ${{ matrix.id }} | |
| runner: ${{ matrix.runner_key != '' && fromJSON(needs.generate-matrix.outputs.runner_routing)[matrix.runner_key] || matrix.runner }} | |
| checkout_sha: ${{ inputs.checkout_sha }} | |
| workflow_sha: ${{ inputs.workflow_sha }} | |
| test_file: ${{ matrix.test_file }} | |
| timeout_minutes: ${{ matrix.timeout_minutes }} | |
| install_mode: ${{ matrix.install_mode }} | |
| install_non_interactive: ${{ matrix.install_non_interactive }} | |
| restore_cli: ${{ matrix.restore_cli }} | |
| cloudflared: ${{ matrix.cloudflared }} | |
| host_packages: ${{ matrix.host_packages }} | |
| host_preparation: ${{ matrix.host_preparation }} | |
| runner_comparison: ${{ matrix.runner_comparison }} | |
| compatible_api_key: ${{ matrix.compatible_api_key }} | |
| github_token: false | |
| shard: ${{ matrix.shard }} | |
| artifact_layout: ${{ matrix.artifact_layout }} | |
| trusted_main: ${{ github.repository == 'NVIDIA/NemoClaw' && github.ref == 'refs/heads/main' && (inputs.checkout_sha == '' || needs.generate-matrix.outputs.e2e_credentials_allowed == 'true') }} | |
| secrets: | |
| DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} | |
| DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} | |
| NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} | |
| catalogue-github-read: | |
| name: ${{ matrix.display_name }} | |
| needs: generate-matrix | |
| if: ${{ needs.generate-matrix.outputs.catalogue_github_read_matrix != '[]' }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: ${{ fromJSON(needs.generate-matrix.outputs.catalogue_github_read_matrix) }} | |
| uses: ./.github/workflows/e2e-standard-profile.yaml | |
| with: | |
| candidate_repository: ${{ inputs.checkout_repository || github.repository }} | |
| candidate_sha: ${{ inputs.checkout_sha || github.sha }} | |
| risk_signal_expected_sha: ${{ github.event_name == 'workflow_dispatch' && inputs.checkout_sha != '' && inputs.checkout_sha || '' }} | |
| risk_signal_correlation_id: ${{ github.event_name == 'workflow_dispatch' && inputs.checkout_sha != '' && inputs.correlation_id || '' }} | |
| cli_artifact_provenance: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} | |
| credential_boundary: GitHub read token | |
| target_id: ${{ matrix.target_id }} | |
| catalogue_id: ${{ matrix.id }} | |
| runner: ${{ matrix.runner_key != '' && fromJSON(needs.generate-matrix.outputs.runner_routing)[matrix.runner_key] || matrix.runner }} | |
| checkout_sha: ${{ inputs.checkout_sha }} | |
| workflow_sha: ${{ inputs.workflow_sha }} | |
| test_file: ${{ matrix.test_file }} | |
| timeout_minutes: ${{ matrix.timeout_minutes }} | |
| install_mode: ${{ matrix.install_mode }} | |
| install_non_interactive: ${{ matrix.install_non_interactive }} | |
| restore_cli: ${{ matrix.restore_cli }} | |
| cloudflared: ${{ matrix.cloudflared }} | |
| host_packages: ${{ matrix.host_packages }} | |
| host_preparation: ${{ matrix.host_preparation }} | |
| runner_comparison: ${{ matrix.runner_comparison }} | |
| compatible_api_key: ${{ matrix.compatible_api_key }} | |
| github_token: true | |
| shard: ${{ matrix.shard }} | |
| artifact_layout: ${{ matrix.artifact_layout }} | |
| trusted_main: ${{ github.repository == 'NVIDIA/NemoClaw' && github.ref == 'refs/heads/main' && (inputs.checkout_sha == '' || needs.generate-matrix.outputs.e2e_credentials_allowed == 'true') }} | |
| secrets: | |
| DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} | |
| DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} | |
| catalogue-brave-nvidia-inference: | |
| name: ${{ matrix.display_name }} | |
| needs: generate-matrix | |
| if: ${{ needs.generate-matrix.outputs.catalogue_brave_nvidia_inference_matrix != '[]' }} | |
| strategy: | |
| fail-fast: false | |
| max-parallel: 2 | |
| matrix: | |
| include: ${{ fromJSON(needs.generate-matrix.outputs.catalogue_brave_nvidia_inference_matrix) }} | |
| uses: ./.github/workflows/e2e-standard-profile.yaml | |
| with: | |
| candidate_repository: ${{ inputs.checkout_repository || github.repository }} | |
| candidate_sha: ${{ inputs.checkout_sha || github.sha }} | |
| risk_signal_expected_sha: ${{ github.event_name == 'workflow_dispatch' && inputs.checkout_sha != '' && inputs.checkout_sha || '' }} | |
| risk_signal_correlation_id: ${{ github.event_name == 'workflow_dispatch' && inputs.checkout_sha != '' && inputs.correlation_id || '' }} | |
| cli_artifact_provenance: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} | |
| credential_boundary: Brave and NVIDIA inference API keys | |
| target_id: ${{ matrix.target_id }} | |
| catalogue_id: ${{ matrix.id }} | |
| runner: ${{ matrix.runner_key != '' && fromJSON(needs.generate-matrix.outputs.runner_routing)[matrix.runner_key] || matrix.runner }} | |
| checkout_sha: ${{ inputs.checkout_sha }} | |
| workflow_sha: ${{ inputs.workflow_sha }} | |
| test_file: ${{ matrix.test_file }} | |
| timeout_minutes: ${{ matrix.timeout_minutes }} | |
| install_mode: ${{ matrix.install_mode }} | |
| install_non_interactive: ${{ matrix.install_non_interactive }} | |
| restore_cli: ${{ matrix.restore_cli }} | |
| cloudflared: ${{ matrix.cloudflared }} | |
| host_packages: ${{ matrix.host_packages }} | |
| host_preparation: ${{ matrix.host_preparation }} | |
| runner_comparison: ${{ matrix.runner_comparison }} | |
| compatible_api_key: ${{ matrix.compatible_api_key }} | |
| github_token: false | |
| shard: ${{ matrix.shard }} | |
| artifact_layout: ${{ matrix.artifact_layout }} | |
| trusted_main: ${{ github.repository == 'NVIDIA/NemoClaw' && github.ref == 'refs/heads/main' && (inputs.checkout_sha == '' || needs.generate-matrix.outputs.e2e_credentials_allowed == 'true') }} | |
| secrets: | |
| DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} | |
| DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} | |
| BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }} | |
| NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} | |
| openshell-gateway-auth-contract: | |
| needs: generate-matrix | |
| if: ${{ contains(fromJSON(needs.generate-matrix.outputs.selected_jobs), 'openshell-gateway-auth-contract') }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| env: | |
| E2E_JOB: "1" | |
| E2E_TARGET_ID: "openshell-gateway-auth-contract" | |
| E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts/live/openshell-gateway-auth-contract | |
| NEMOCLAW_RUN_LIVE_E2E: "1" | |
| NEMOCLAW_NON_INTERACTIVE: "1" | |
| NEMOCLAW_OPENSHELL_PIN_VERSION: "0.0.101" | |
| DOCKER_GRPC_PROBE_IMAGE: "node:22-trixie-slim@sha256:db8a96a63e5264607ada2d206758876ebbed6a12be2ada7517793cbfb0c2a29c" | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| repository: ${{ inputs.checkout_repository || github.repository }} | |
| ref: ${{ inputs.checkout_sha || github.sha }} | |
| persist-credentials: false | |
| - *dockerhub-auth | |
| - name: Prepare E2E workspace | |
| uses: NVIDIA/NemoClaw/.github/actions/prepare-e2e@f6304bc25fc35bfaa441c8c2fbfee38f72805a75 | |
| with: | |
| build-cli: "false" | |
| - name: Restore exact-commit CLI artifact | |
| uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@c246409193a31133cab10c8a3589001cc0d59eb3 | |
| with: | |
| provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} | |
| - name: Install OpenShell CLI | |
| run: 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: Pre-pull pinned gateway auth probe image | |
| run: docker pull "$DOCKER_GRPC_PROBE_IMAGE" | |
| - name: Run OpenShell gateway auth contract live test | |
| run: | | |
| set -euo pipefail | |
| export PATH="$HOME/.local/bin:$HOME/.npm-global/bin:$PATH" | |
| if command -v openshell-gateway >/dev/null 2>&1; then | |
| OPENSHELL_GATEWAY_BIN="$(command -v openshell-gateway)" | |
| elif [ -x "$HOME/.local/bin/openshell-gateway" ]; then | |
| OPENSHELL_GATEWAY_BIN="$HOME/.local/bin/openshell-gateway" | |
| else | |
| echo "::error::OpenShell gateway binary not found after install" | |
| ls -la /usr/local/bin/openshell-gateway "$HOME/.local/bin/openshell-gateway" 2>&1 || true | |
| exit 1 | |
| fi | |
| export OPENSHELL_GATEWAY_BIN | |
| echo "Using OPENSHELL_GATEWAY_BIN=$OPENSHELL_GATEWAY_BIN" | |
| "$OPENSHELL_GATEWAY_BIN" --version | |
| npx tsx tools/e2e/live-vitest-invocation.mts run --test-path test/e2e/live/openshell-gateway-auth-source-contract.test.ts | |
| - id: artifact_safety | |
| name: Validate final OpenShell gateway auth contract artifacts | |
| if: always() | |
| run: node --experimental-strip-types --no-warnings tools/e2e/openshell-gateway-auth-artifact-safety.mts "$E2E_ARTIFACT_DIR" | |
| - name: Upload OpenShell gateway auth contract artifacts | |
| if: ${{ always() && steps.artifact_safety.outcome == 'success' && steps.artifact_safety.outputs.approved_path != '' }} | |
| uses: NVIDIA/NemoClaw/.github/actions/upload-e2e-artifacts@7768e15eb90d3ee2d33432f481dfe8747e4f6d57 | |
| with: | |
| name: e2e-openshell-gateway-auth-contract | |
| path: ${{ steps.artifact_safety.outputs.approved_path }} | |
| - name: Clean up Docker auth | |
| if: always() | |
| shell: bash | |
| run: bash .github/scripts/docker-auth-cleanup.sh | |
| mcp-bridge: | |
| needs: generate-matrix | |
| if: ${{ contains(fromJSON(needs.generate-matrix.outputs.selected_jobs), 'mcp-bridge') }} | |
| runs-on: ${{ fromJSON(needs.generate-matrix.outputs.runner_routing)[format('mcp-bridge-{0}', matrix.agent)] }} | |
| permissions: | |
| contents: read | |
| # Keep each destructive agent lifecycle on a fresh runner. This bounds the | |
| # blast radius of runner loss and gives every adapter independent evidence. | |
| timeout-minutes: 90 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| agent: [openclaw, hermes, deepagents] | |
| env: | |
| E2E_JOB: "1" | |
| E2E_TARGET_ID: "mcp-bridge" | |
| E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts/live/mcp-bridge/${{ matrix.agent }} | |
| NEMOCLAW_CLI_BIN: ${{ github.workspace }}/bin/nemoclaw.js | |
| NEMOCLAW_E2E_SHARD: ${{ matrix.agent }} | |
| NEMOCLAW_MCP_BRIDGE_AGENT: ${{ matrix.agent }} | |
| NEMOCLAW_OPENSHELL_CHANNEL: stable | |
| NEMOCLAW_OPENSHELL_EXACT_MAIN_PROOF: "1" | |
| NEMOCLAW_RUN_LIVE_E2E: "1" | |
| OPENSHELL_DOCKER_SUPERVISOR_IMAGE: ghcr.io/nvidia/openshell/supervisor@sha256:b58be5e40c788977ffa0e8305a8cad9c656efdf1a3fe182582a00ca870bb0edb | |
| steps: | |
| - id: trusted_hermes_swap | |
| name: Provision trusted Hermes E2E swap | |
| if: ${{ github.repository == 'NVIDIA/NemoClaw' && github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && matrix.agent == 'hermes' }} | |
| shell: /bin/bash --noprofile --norc -e -o pipefail {0} | |
| env: | |
| BASH_ENV: /dev/null | |
| CHECKOUT_SHA: ${{ inputs.checkout_sha }} | |
| DISPATCH_SHA: ${{ github.sha }} | |
| ENV: /dev/null | |
| EVENT_NAME: ${{ github.event_name }} | |
| EXPECTED_WORKFLOW_SHA: ${{ inputs.workflow_sha }} | |
| LC_ALL: C | |
| REF: ${{ github.ref }} | |
| REPOSITORY: ${{ github.repository }} | |
| RUNNER_ARCH_KIND: ${{ runner.arch }} | |
| RUNNER_ENVIRONMENT_KIND: ${{ runner.environment }} | |
| RUNNER_OS_KIND: ${{ runner.os }} | |
| WORKFLOW_SHA: ${{ github.workflow_sha }} | |
| run: &trusted-hermes-e2e-swap | | |
| set -euo pipefail | |
| readonly swap_dir="/mnt/nemoclaw-hermes-e2e-swap" | |
| readonly required_swap_bytes=34359738368 | |
| readonly swap_file_bytes=34359742464 | |
| readonly reserve_bytes=17179869184 | |
| readonly activation_observation_attempts=5 | |
| readonly activation_observation_delay_seconds=1 | |
| swap_file="" | |
| swap_activation_succeeded=0 | |
| fail() { | |
| printf 'Trusted Hermes E2E swap setup failed: %s\n' "$1" >&2 | |
| exit 1 | |
| } | |
| if [[ "${REPOSITORY}" != "NVIDIA/NemoClaw" || "${REF}" != "refs/heads/main" ]]; then | |
| fail "workflow must run from NVIDIA/NemoClaw main" | |
| fi | |
| if [[ "${EVENT_NAME}" != "push" && "${EVENT_NAME}" != "workflow_dispatch" ]]; then | |
| fail "workflow event must be push or workflow_dispatch" | |
| fi | |
| # PR E2E mode: maintainer-dispatched PR commit. | |
| if [[ "${EVENT_NAME}" == "workflow_dispatch" && -n "${CHECKOUT_SHA}" ]]; then | |
| if [[ ! "${CHECKOUT_SHA}" =~ ^[0-9a-f]{40}$ ]]; then | |
| fail "checkout SHA must be lowercase 40-hex" | |
| fi | |
| if [[ ! "${EXPECTED_WORKFLOW_SHA}" =~ ^[0-9a-f]{40}$ || "${WORKFLOW_SHA}" != "${EXPECTED_WORKFLOW_SHA}" || "${WORKFLOW_SHA}" != "${DISPATCH_SHA}" ]]; then | |
| fail "workflow source must match the trusted dispatch revision" | |
| fi | |
| else | |
| # Direct-main mode: push or manual trigger on main. | |
| if [[ -n "${CHECKOUT_SHA}" || -n "${EXPECTED_WORKFLOW_SHA}" ]]; then | |
| fail "direct main runs must not request an alternate checkout or workflow revision" | |
| fi | |
| if [[ ! "${WORKFLOW_SHA}" =~ ^[0-9a-f]{40}$ || "${WORKFLOW_SHA}" != "${DISPATCH_SHA}" ]]; then | |
| fail "direct main workflow source must match the run revision" | |
| fi | |
| fi | |
| if [[ "${RUNNER_ENVIRONMENT_KIND}" != "github-hosted" || "${RUNNER_OS_KIND}" != "Linux" || "${RUNNER_ARCH_KIND}" != "X64" ]]; then | |
| fail "swap fallback requires an ephemeral GitHub-hosted Linux x64 runner" | |
| fi | |
| mnt_metadata="$(/usr/bin/stat -c "%F:%u:%g" -- /mnt)" | |
| if [[ "${mnt_metadata}" != "directory:0:0" ]]; then | |
| fail "/mnt must be a root-owned directory" | |
| fi | |
| read_active_swap_bytes() { | |
| /usr/bin/sudo -n /usr/sbin/swapon --show=SIZE --bytes --noheadings | | |
| /usr/bin/awk '{ total += $1 } END { printf "%.0f", total }' | |
| } | |
| active_swap_bytes="$(read_active_swap_bytes)" | |
| active_swap_bytes="${active_swap_bytes:-0}" | |
| if [[ ! "${active_swap_bytes}" =~ ^[0-9]+$ ]]; then | |
| fail "unable to determine active swap capacity" | |
| fi | |
| if (( active_swap_bytes >= required_swap_bytes )); then | |
| printf 'Hermes E2E swap is already sufficient: %s bytes active\n' "${active_swap_bytes}" | |
| exit 0 | |
| fi | |
| available_bytes="$(/usr/bin/df --block-size=1 --output=avail /mnt | /usr/bin/tail -n 1 | /usr/bin/tr -d "[:space:]")" | |
| if [[ ! "${available_bytes}" =~ ^[0-9]+$ ]]; then | |
| fail "unable to determine available disk capacity under /mnt" | |
| fi | |
| required_disk_bytes=$((swap_file_bytes + reserve_bytes)) | |
| if (( available_bytes < required_disk_bytes )); then | |
| fail "insufficient disk capacity: ${available_bytes} bytes available, ${required_disk_bytes} required" | |
| fi | |
| if /usr/bin/sudo -n /usr/bin/test -e "${swap_dir}" || /usr/bin/sudo -n /usr/bin/test -L "${swap_dir}"; then | |
| fail "refusing unexpected pre-existing swap path" | |
| fi | |
| directory_created=0 | |
| cleanup_partial_swap() { | |
| status="$?" | |
| if (( status != 0 && directory_created == 1 )); then | |
| if active_swap_names="$(/usr/bin/sudo -n /usr/sbin/swapon --show=NAME --noheadings --raw 2>/dev/null)"; then | |
| fixed_swap_active=0 | |
| while IFS= read -r active_swap_name; do | |
| if [[ -n "${swap_file}" && "${active_swap_name}" == "${swap_file}" ]]; then | |
| fixed_swap_active=1 | |
| break | |
| fi | |
| done <<< "${active_swap_names}" | |
| if (( fixed_swap_active == 1 || swap_activation_succeeded == 1 )); then | |
| if /usr/bin/sudo -n /usr/sbin/swapoff "${swap_file}" 2>/dev/null; then | |
| /usr/bin/sudo -n /usr/bin/rm -f -- "${swap_file}" || true | |
| /usr/bin/sudo -n /usr/bin/rmdir -- "${swap_dir}" || true | |
| else | |
| printf 'Preserving active Hermes E2E swap after setup failure: %s\n' "${swap_file}" >&2 | |
| fi | |
| else | |
| if [[ -n "${swap_file}" ]]; then | |
| /usr/bin/sudo -n /usr/bin/rm -f -- "${swap_file}" || true | |
| fi | |
| /usr/bin/sudo -n /usr/bin/rmdir -- "${swap_dir}" || true | |
| fi | |
| else | |
| printf 'Preserving Hermes E2E swap because active swap could not be queried: %s\n' "${swap_file}" >&2 | |
| fi | |
| fi | |
| trap - EXIT | |
| exit "${status}" | |
| } | |
| trap cleanup_partial_swap EXIT | |
| /usr/bin/sudo -n /usr/bin/mkdir -m 0700 -- "${swap_dir}" | |
| directory_created=1 | |
| directory_metadata="$(/usr/bin/sudo -n /usr/bin/stat -c "%F:%u:%g:%a" -- "${swap_dir}")" | |
| if [[ "${directory_metadata}" != "directory:0:0:700" ]]; then | |
| fail "swap directory must be a root-owned mode-0700 directory" | |
| fi | |
| swap_file="$(/usr/bin/sudo -n /usr/bin/mktemp --tmpdir="${swap_dir}" nemoclaw-hermes.XXXXXXXX.swap)" | |
| if ! /usr/bin/sudo -n /usr/bin/test -f "${swap_file}" || /usr/bin/sudo -n /usr/bin/test -L "${swap_file}"; then | |
| fail "swap file must be a regular non-symlink" | |
| fi | |
| file_metadata="$(/usr/bin/sudo -n /usr/bin/stat -c "%u:%g:%a" -- "${swap_file}")" | |
| if [[ "${file_metadata}" != "0:0:600" ]]; then | |
| fail "swap file must be root-owned mode 0600" | |
| fi | |
| /usr/bin/sudo -n /usr/bin/fallocate -l "${swap_file_bytes}" "${swap_file}" | |
| file_size_bytes="$(/usr/bin/sudo -n /usr/bin/stat -c "%s" -- "${swap_file}")" | |
| if [[ ! "${file_size_bytes}" =~ ^[0-9]+$ || "${file_size_bytes}" -ne "${swap_file_bytes}" ]]; then | |
| fail "swap file size does not match the fixed backing allocation" | |
| fi | |
| remaining_bytes="$(/usr/bin/df --block-size=1 --output=avail /mnt | /usr/bin/tail -n 1 | /usr/bin/tr -d "[:space:]")" | |
| if [[ ! "${remaining_bytes}" =~ ^[0-9]+$ || "${remaining_bytes}" -lt "${reserve_bytes}" ]]; then | |
| fail "swap allocation did not preserve the required disk reserve" | |
| fi | |
| /usr/bin/sudo -n /usr/sbin/mkswap --quiet "${swap_file}" | |
| /usr/bin/sudo -n /usr/sbin/swapon "${swap_file}" | |
| swap_activation_succeeded=1 | |
| observe_provisioned_swap() { | |
| activation_observation_attempt=1 | |
| while (( activation_observation_attempt <= activation_observation_attempts )); do | |
| provisioned_swap_active=0 | |
| if active_swap_names="$(/usr/bin/sudo -n /usr/sbin/swapon --show=NAME --noheadings --raw 2>/dev/null)"; then | |
| while IFS= read -r active_swap_name; do | |
| if [[ "${active_swap_name}" == "${swap_file}" ]]; then | |
| provisioned_swap_active=1 | |
| break | |
| fi | |
| done <<< "${active_swap_names}" | |
| fi | |
| if observed_swap_bytes="$(read_active_swap_bytes 2>/dev/null)"; then | |
| observed_swap_bytes="${observed_swap_bytes:-0}" | |
| if [[ "${observed_swap_bytes}" =~ ^[0-9]+$ ]] && | |
| (( provisioned_swap_active == 1 && observed_swap_bytes >= required_swap_bytes )); then | |
| active_swap_bytes="${observed_swap_bytes}" | |
| return 0 | |
| fi | |
| fi | |
| if (( activation_observation_attempt < activation_observation_attempts )); then | |
| /usr/bin/sleep "${activation_observation_delay_seconds}" | |
| fi | |
| activation_observation_attempt=$((activation_observation_attempt + 1)) | |
| done | |
| return 1 | |
| } | |
| if ! observe_provisioned_swap; then | |
| fail "unable to verify the required active swap capacity after bounded observation" | |
| fi | |
| trap - EXIT | |
| printf 'Hermes E2E swap ready: %s bytes active\n' "${active_swap_bytes}" | |
| /usr/bin/sudo -n /usr/sbin/swapon --show | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| repository: ${{ inputs.checkout_repository || github.repository }} | |
| ref: ${{ inputs.checkout_sha || github.sha }} | |
| persist-credentials: false | |
| - *dockerhub-auth | |
| - name: Prepare E2E workspace | |
| uses: NVIDIA/NemoClaw/.github/actions/prepare-e2e@f6304bc25fc35bfaa441c8c2fbfee38f72805a75 | |
| with: | |
| build-cli: "false" | |
| - name: Restore exact-commit CLI artifact | |
| uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@c246409193a31133cab10c8a3589001cc0d59eb3 | |
| with: | |
| provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} | |
| - name: Initialize runner comparison telemetry | |
| if: ${{ github.repository == 'NVIDIA/NemoClaw' && github.ref == 'refs/heads/main' && inputs.checkout_sha == '' && (matrix.agent == 'hermes' || matrix.agent == 'deepagents') }} | |
| continue-on-error: true | |
| shell: bash | |
| run: npx tsx tools/e2e/runner-comparison.mts initialize | |
| - name: Install and verify cloudflared prerequisite | |
| # Update posture: maintainers review upstream cloudflared releases and | |
| # update the version and reviewed SHA256 together in both explicit MCP | |
| # lanes; mutable package repositories and unreviewed latest releases | |
| # are intentionally rejected by the workflow-contract tests. | |
| env: | |
| CLOUDFLARED_VERSION: "2026.6.1" | |
| CLOUDFLARED_DEB_SHA256: "ccd02ec216c62bfa573395d8f72cb2e91e95cbdf8726a8acc06b3e2d9aa31526" | |
| run: | | |
| set -euo pipefail | |
| cloudflared_deb="${RUNNER_TEMP}/cloudflared-${CLOUDFLARED_VERSION}-linux-amd64.deb" | |
| curl -fL "https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64.deb" -o "${cloudflared_deb}" | |
| printf '%s %s\n' "${CLOUDFLARED_DEB_SHA256}" "${cloudflared_deb}" | sha256sum -c - | |
| package="$(dpkg-deb -f "${cloudflared_deb}" Package)" | |
| version="$(dpkg-deb -f "${cloudflared_deb}" Version)" | |
| architecture="$(dpkg-deb -f "${cloudflared_deb}" Architecture)" | |
| if [[ "${package}" != "cloudflared" || "${version}" != "${CLOUDFLARED_VERSION}" || "${architecture}" != "amd64" ]]; then | |
| printf 'Unexpected cloudflared package metadata: package=%s version=%s architecture=%s\n' "${package}" "${version}" "${architecture}" >&2 | |
| exit 1 | |
| fi | |
| sudo dpkg -i "${cloudflared_deb}" | |
| cloudflared --version | grep -F "cloudflared version ${CLOUDFLARED_VERSION}" | |
| - name: Generate MCP test TLS | |
| run: bash test/e2e/setup-mcp-test-tls.sh | |
| - name: Install OpenShell CLI | |
| env: | |
| NEMOCLAW_OPENSHELL_FORCE_INSTALL: "1" | |
| run: | | |
| set -euo pipefail | |
| bash scripts/install-openshell.sh | |
| if [[ "$NEMOCLAW_MCP_BRIDGE_AGENT" == "deepagents" ]]; then | |
| mkdir -p "$E2E_ARTIFACT_DIR/mcp-bridge-deepagents" | |
| jq -n '{ | |
| schemaVersion: 1, | |
| sourceRepository: "NVIDIA/OpenShell", | |
| releaseTag: "v0.0.101", | |
| sourceSha: "8ddd98c3dff62619a3963f99ba1e055b67650e72", | |
| artifacts: { | |
| cli: {binarySha256: "1ad48efd5e1de8f3f017a81b3a7177872f350343a1a8d8074c7e844bca4801e9"}, | |
| gateway: {binarySha256: "a6a5d754605a2144b148637b85a09291d2eeb77e08a4ee34b83685c6920448f5"}, | |
| standaloneSandbox: {binarySha256: "a2704babbb468fd0a359bfdd9844de71095b730758541b4ca8cbab77d4018920"} | |
| } | |
| }' > "$E2E_ARTIFACT_DIR/mcp-bridge-deepagents/openshell-exact-main-provenance.json" | |
| fi | |
| - name: Run MCP OpenShell provider live test | |
| run: | | |
| set -euo pipefail | |
| export PATH="$HOME/.local/bin:$HOME/.npm-global/bin:$PATH" | |
| if command -v openshell >/dev/null 2>&1; then | |
| OPENSHELL_BIN="$(command -v openshell)" | |
| elif [ -x "$HOME/.local/bin/openshell" ]; then | |
| OPENSHELL_BIN="$HOME/.local/bin/openshell" | |
| else | |
| echo "::error::OpenShell CLI not found after install" | |
| exit 1 | |
| fi | |
| export OPENSHELL_BIN | |
| "$OPENSHELL_BIN" --version | |
| npx tsx tools/e2e/live-vitest-invocation.mts run --test-path test/e2e/live/mcp-bridge.test.ts | |
| - name: Finalize runner comparison telemetry | |
| if: ${{ always() && github.repository == 'NVIDIA/NemoClaw' && github.ref == 'refs/heads/main' && inputs.checkout_sha == '' && (matrix.agent == 'hermes' || matrix.agent == 'deepagents') }} | |
| continue-on-error: true | |
| shell: bash | |
| run: npx tsx tools/e2e/runner-comparison.mts finalize | |
| - id: mcp_artifact_secret_scan | |
| name: Scan MCP artifacts for fixture credentials | |
| if: always() | |
| run: >- | |
| npx tsx tools/e2e/assert-mcp-artifact-secrets-absent.mts | |
| e2e-artifacts/live/mcp-bridge/${{ matrix.agent }} | |
| - name: Upload MCP server artifacts | |
| if: ${{ always() && steps.mcp_artifact_secret_scan.outcome == 'success' }} | |
| uses: NVIDIA/NemoClaw/.github/actions/upload-e2e-artifacts@7768e15eb90d3ee2d33432f481dfe8747e4f6d57 | |
| with: | |
| name: e2e-mcp-bridge-${{ matrix.agent }} | |
| path: e2e-artifacts/live/mcp-bridge/${{ matrix.agent }}/ | |
| - name: Clean up Docker auth | |
| if: always() | |
| shell: bash | |
| run: bash .github/scripts/docker-auth-cleanup.sh | |
| openshell-credential-generation-window: | |
| needs: generate-matrix | |
| if: ${{ contains(fromJSON(needs.generate-matrix.outputs.selected_jobs), 'openshell-credential-generation-window') }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| # Keep the credential-generation lifecycle on a fresh runner so it can | |
| # execute in parallel with, and fail independently from, the Deep Agents | |
| # MCP lifecycle without sharing destructive sandbox state. | |
| timeout-minutes: 90 | |
| env: | |
| E2E_JOB: "1" | |
| E2E_TARGET_ID: "openshell-credential-generation-window" | |
| E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts/live/openshell-credential-generation-window | |
| NEMOCLAW_CLI_BIN: ${{ github.workspace }}/bin/nemoclaw.js | |
| NEMOCLAW_OPENSHELL_CHANNEL: stable | |
| NEMOCLAW_OPENSHELL_EXACT_MAIN_PROOF: "1" | |
| NEMOCLAW_RUN_LIVE_E2E: "1" | |
| OPENSHELL_DOCKER_SUPERVISOR_IMAGE: ghcr.io/nvidia/openshell/supervisor@sha256:b58be5e40c788977ffa0e8305a8cad9c656efdf1a3fe182582a00ca870bb0edb | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| repository: ${{ inputs.checkout_repository || github.repository }} | |
| ref: ${{ inputs.checkout_sha || github.sha }} | |
| persist-credentials: false | |
| - *dockerhub-auth | |
| - name: Prepare E2E workspace | |
| uses: NVIDIA/NemoClaw/.github/actions/prepare-e2e@f6304bc25fc35bfaa441c8c2fbfee38f72805a75 | |
| with: | |
| build-cli: "false" | |
| - name: Restore exact-commit CLI artifact | |
| uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@c246409193a31133cab10c8a3589001cc0d59eb3 | |
| with: | |
| provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} | |
| - name: Install and verify cloudflared prerequisite | |
| env: | |
| CLOUDFLARED_VERSION: "2026.6.1" | |
| CLOUDFLARED_DEB_SHA256: "ccd02ec216c62bfa573395d8f72cb2e91e95cbdf8726a8acc06b3e2d9aa31526" | |
| run: | | |
| set -euo pipefail | |
| cloudflared_deb="${RUNNER_TEMP}/cloudflared-${CLOUDFLARED_VERSION}-linux-amd64.deb" | |
| curl -fL "https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64.deb" -o "${cloudflared_deb}" | |
| printf '%s %s\n' "${CLOUDFLARED_DEB_SHA256}" "${cloudflared_deb}" | sha256sum -c - | |
| package="$(dpkg-deb -f "${cloudflared_deb}" Package)" | |
| version="$(dpkg-deb -f "${cloudflared_deb}" Version)" | |
| architecture="$(dpkg-deb -f "${cloudflared_deb}" Architecture)" | |
| if [[ "${package}" != "cloudflared" || "${version}" != "${CLOUDFLARED_VERSION}" || "${architecture}" != "amd64" ]]; then | |
| printf 'Unexpected cloudflared package metadata: package=%s version=%s architecture=%s\n' "${package}" "${version}" "${architecture}" >&2 | |
| exit 1 | |
| fi | |
| sudo dpkg -i "${cloudflared_deb}" | |
| cloudflared --version | grep -F "cloudflared version ${CLOUDFLARED_VERSION}" | |
| - name: Generate MCP test TLS | |
| run: bash test/e2e/setup-mcp-test-tls.sh | |
| - name: Install OpenShell CLI | |
| env: | |
| NEMOCLAW_OPENSHELL_FORCE_INSTALL: "1" | |
| run: | | |
| set -euo pipefail | |
| bash scripts/install-openshell.sh | |
| mkdir -p "$E2E_ARTIFACT_DIR/openshell-credential-generation-window" | |
| jq -n '{ | |
| schemaVersion: 1, | |
| sourceRepository: "NVIDIA/OpenShell", | |
| releaseTag: "v0.0.101", | |
| sourceSha: "8ddd98c3dff62619a3963f99ba1e055b67650e72", | |
| artifacts: { | |
| cli: {binarySha256: "1ad48efd5e1de8f3f017a81b3a7177872f350343a1a8d8074c7e844bca4801e9"}, | |
| gateway: {binarySha256: "a6a5d754605a2144b148637b85a09291d2eeb77e08a4ee34b83685c6920448f5"}, | |
| standaloneSandbox: {binarySha256: "a2704babbb468fd0a359bfdd9844de71095b730758541b4ca8cbab77d4018920"} | |
| } | |
| }' > "$E2E_ARTIFACT_DIR/openshell-credential-generation-window/openshell-exact-main-provenance.json" | |
| - name: Run OpenShell credential generation-window live test | |
| run: | | |
| set -euo pipefail | |
| export PATH="$HOME/.local/bin:$HOME/.npm-global/bin:$PATH" | |
| if command -v openshell >/dev/null 2>&1; then | |
| OPENSHELL_BIN="$(command -v openshell)" | |
| elif [ -x "$HOME/.local/bin/openshell" ]; then | |
| OPENSHELL_BIN="$HOME/.local/bin/openshell" | |
| else | |
| echo "::error::OpenShell CLI not found after install" | |
| exit 1 | |
| fi | |
| export OPENSHELL_BIN | |
| "$OPENSHELL_BIN" --version | |
| npx vitest run --project e2e-live \ | |
| test/e2e/live/openshell-credential-generation-window.test.ts \ | |
| -t '^openshell-credential-generation-window$' \ | |
| --no-file-parallelism \ | |
| --silent=false --reporter=default --reporter=test/e2e/risk-signal-reporter.ts | |
| - id: credential_window_artifact_secret_scan | |
| name: Scan credential-window artifacts for fixture credentials | |
| if: always() | |
| run: >- | |
| npx tsx tools/e2e/assert-mcp-artifact-secrets-absent.mts | |
| e2e-artifacts/live/openshell-credential-generation-window | |
| - name: Upload credential-window artifacts | |
| if: ${{ always() && steps.credential_window_artifact_secret_scan.outcome == 'success' }} | |
| uses: NVIDIA/NemoClaw/.github/actions/upload-e2e-artifacts@7768e15eb90d3ee2d33432f481dfe8747e4f6d57 | |
| with: | |
| name: e2e-openshell-credential-generation-window | |
| path: e2e-artifacts/live/openshell-credential-generation-window/ | |
| - name: Clean up Docker auth | |
| if: always() | |
| shell: bash | |
| run: bash .github/scripts/docker-auth-cleanup.sh | |
| openshell-dev-artifact: | |
| needs: generate-matrix | |
| if: ${{ contains(fromJSON(needs.generate-matrix.outputs.selected_jobs), 'mcp-bridge-dev') }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| timeout-minutes: 15 | |
| outputs: | |
| artifact_name: ${{ steps.resolve_openshell_dev_artifact.outputs.artifact_name }} | |
| source_commit: ${{ steps.resolve_openshell_dev_artifact.outputs.source_commit }} | |
| manifest_sha256: ${{ steps.resolve_openshell_dev_artifact.outputs.manifest_sha256 }} | |
| steps: | |
| - name: Checkout trusted OpenShell dev tooling | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| repository: ${{ github.repository }} | |
| ref: ${{ inputs.workflow_sha || github.workflow_sha }} | |
| path: .trusted-openshell-dev-artifact | |
| persist-credentials: false | |
| sparse-checkout: | | |
| scripts/install-openshell.sh | |
| tools/e2e/openshell-dev-artifact.mts | |
| - name: Set up Node for OpenShell dev artifact resolution | |
| uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: 22 | |
| - id: resolve_openshell_dev_artifact | |
| name: Resolve immutable OpenShell dev artifact | |
| run: >- | |
| node --experimental-strip-types --no-warnings | |
| "${{ github.workspace }}/.trusted-openshell-dev-artifact/tools/e2e/openshell-dev-artifact.mts" resolve | |
| "${{ runner.temp }}/openshell-dev-artifact" | |
| - name: Upload OpenShell dev artifact resolution | |
| if: ${{ always() }} | |
| uses: NVIDIA/NemoClaw/.github/actions/upload-e2e-artifacts@7768e15eb90d3ee2d33432f481dfe8747e4f6d57 | |
| with: | |
| name: ${{ steps.resolve_openshell_dev_artifact.outputs.artifact_name || format('openshell-dev-infrastructure-failure-{0}-{1}', github.run_id, github.run_attempt) }} | |
| path: ${{ runner.temp }}/openshell-dev-artifact/ | |
| mcp-bridge-dev: | |
| needs: [generate-matrix, openshell-dev-artifact] | |
| if: ${{ contains(fromJSON(needs.generate-matrix.outputs.selected_jobs), 'mcp-bridge-dev') }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| timeout-minutes: 90 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| agent: [openclaw, hermes, deepagents] | |
| env: | |
| E2E_JOB: "1" | |
| E2E_TARGET_ID: "mcp-bridge-dev" | |
| E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts/live/mcp-bridge-dev/${{ matrix.agent }} | |
| NEMOCLAW_CLI_BIN: ${{ github.workspace }}/bin/nemoclaw.js | |
| NEMOCLAW_E2E_SHARD: ${{ matrix.agent }} | |
| NEMOCLAW_MCP_BRIDGE_AGENT: ${{ matrix.agent }} | |
| NEMOCLAW_OPENSHELL_CHANNEL: dev | |
| NEMOCLAW_RUN_LIVE_E2E: "1" | |
| steps: | |
| # setup-node probes package managers in the workspace. | |
| # Run it before candidate checkout with automatic caching disabled. | |
| - name: Set up Node.js for trusted OpenShell verification | |
| uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: 22 | |
| package-manager-cache: false | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| repository: ${{ inputs.checkout_repository || github.repository }} | |
| ref: ${{ inputs.checkout_sha || github.sha }} | |
| persist-credentials: false | |
| - *dockerhub-auth | |
| - name: Checkout trusted OpenShell dev tooling | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| repository: ${{ github.repository }} | |
| ref: ${{ inputs.workflow_sha || github.workflow_sha }} | |
| path: .trusted-openshell-dev-artifact | |
| persist-credentials: false | |
| sparse-checkout: | | |
| .github/scripts/docker-auth-cleanup.sh | |
| scripts/install-openshell.sh | |
| tools/e2e/openshell-dev-artifact.mts | |
| - name: Restore immutable OpenShell dev artifact | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: ${{ needs.openshell-dev-artifact.outputs.artifact_name }} | |
| path: ${{ runner.temp }}/openshell-dev-artifact | |
| digest-mismatch: error | |
| - name: Verify immutable OpenShell dev artifact | |
| env: | |
| OPENSHELL_DEV_ARTIFACT_DIR: ${{ runner.temp }}/openshell-dev-artifact | |
| OPENSHELL_DEV_EXPECTED_MANIFEST_SHA256: ${{ needs.openshell-dev-artifact.outputs.manifest_sha256 }} | |
| OPENSHELL_DEV_EXPECTED_SOURCE_COMMIT: ${{ needs.openshell-dev-artifact.outputs.source_commit }} | |
| run: >- | |
| node --experimental-strip-types --no-warnings | |
| "${{ github.workspace }}/.trusted-openshell-dev-artifact/tools/e2e/openshell-dev-artifact.mts" verify | |
| "$OPENSHELL_DEV_ARTIFACT_DIR" | |
| "$OPENSHELL_DEV_EXPECTED_SOURCE_COMMIT" | |
| "$OPENSHELL_DEV_EXPECTED_MANIFEST_SHA256" | |
| - name: Revoke Docker auth before OpenShell development tooling | |
| shell: bash | |
| run: >- | |
| bash | |
| "${{ github.workspace }}/.trusted-openshell-dev-artifact/.github/scripts/docker-auth-cleanup.sh" | |
| - name: Install immutable OpenShell dev artifact | |
| env: | |
| NEMOCLAW_ACCEPT_DEV_UNVERIFIED_INSTALL: "1" | |
| NEMOCLAW_OPENSHELL_FORCE_INSTALL: "1" | |
| OPENSHELL_DEV_ASSET_DIR: ${{ runner.temp }}/openshell-dev-artifact/assets | |
| run: | | |
| set -euo pipefail | |
| shim_dir="$(mktemp -d)" | |
| trap 'rm -rf "$shim_dir"' EXIT | |
| cat >"$shim_dir/gh" <<'EOF' | |
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| if [[ "$#" -ne 10 || "$1" != "release" || "$2" != "download" || "$3" != "dev" || "$4" != "--repo" || "$5" != "NVIDIA/OpenShell" || "$6" != "--pattern" || "$8" != "--dir" || "${10}" != "--clobber" ]]; then | |
| printf 'Unsupported gh invocation for retained OpenShell assets.\n' >&2 | |
| exit 64 | |
| fi | |
| asset="$7" | |
| destination="$9" | |
| case "$asset" in | |
| openshell-x86_64-unknown-linux-musl.tar.gz | openshell-checksums-sha256.txt | openshell-gateway-x86_64-unknown-linux-gnu.tar.gz | openshell-gateway-checksums-sha256.txt | openshell-sandbox-x86_64-unknown-linux-gnu.tar.gz | openshell-sandbox-checksums-sha256.txt) ;; | |
| *) | |
| printf 'Unsupported retained OpenShell asset: %s\n' "$asset" >&2 | |
| exit 64 | |
| ;; | |
| esac | |
| source_asset="${OPENSHELL_DEV_ASSET_DIR}/${asset}" | |
| [[ -f "$source_asset" && ! -L "$source_asset" && "$destination" = /* && -d "$destination" && ! -L "$destination" ]] | |
| cp -- "$source_asset" "$destination/$asset" | |
| EOF | |
| cat >"$shim_dir/curl" <<'EOF' | |
| #!/usr/bin/env bash | |
| printf 'Network fallback is disabled for retained OpenShell assets.\n' >&2 | |
| exit 1 | |
| EOF | |
| chmod 700 "$shim_dir/gh" "$shim_dir/curl" | |
| PATH="$shim_dir:$PATH" \ | |
| bash "${{ github.workspace }}/.trusted-openshell-dev-artifact/scripts/install-openshell.sh" | |
| # Dependency preparation can read candidate project configuration. Keep | |
| # it after the trusted installer has consumed the verified OpenShell | |
| # assets and Docker credentials have been revoked. | |
| - name: Prepare E2E workspace | |
| uses: NVIDIA/NemoClaw/.github/actions/prepare-e2e@f6304bc25fc35bfaa441c8c2fbfee38f72805a75 | |
| with: | |
| build-cli: "false" | |
| # The restore action executes the candidate CLI for its final identity | |
| # check. Candidate-controlled state starts with dependency preparation. | |
| - name: Restore exact-commit CLI artifact | |
| uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@c246409193a31133cab10c8a3589001cc0d59eb3 | |
| with: | |
| provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} | |
| - name: Install and verify cloudflared prerequisite | |
| # Update posture: keep this dev compatibility lane on the same reviewed | |
| # version/SHA256 pair as the stable lane; workflow-contract tests fail | |
| # if the pins diverge or installation becomes mutable. | |
| env: | |
| CLOUDFLARED_VERSION: "2026.6.1" | |
| CLOUDFLARED_DEB_SHA256: "ccd02ec216c62bfa573395d8f72cb2e91e95cbdf8726a8acc06b3e2d9aa31526" | |
| run: | | |
| set -euo pipefail | |
| cloudflared_deb="${RUNNER_TEMP}/cloudflared-${CLOUDFLARED_VERSION}-linux-amd64.deb" | |
| curl -fL "https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64.deb" -o "${cloudflared_deb}" | |
| printf '%s %s\n' "${CLOUDFLARED_DEB_SHA256}" "${cloudflared_deb}" | sha256sum -c - | |
| package="$(dpkg-deb -f "${cloudflared_deb}" Package)" | |
| version="$(dpkg-deb -f "${cloudflared_deb}" Version)" | |
| architecture="$(dpkg-deb -f "${cloudflared_deb}" Architecture)" | |
| if [[ "${package}" != "cloudflared" || "${version}" != "${CLOUDFLARED_VERSION}" || "${architecture}" != "amd64" ]]; then | |
| printf 'Unexpected cloudflared package metadata: package=%s version=%s architecture=%s\n' "${package}" "${version}" "${architecture}" >&2 | |
| exit 1 | |
| fi | |
| sudo dpkg -i "${cloudflared_deb}" | |
| cloudflared --version | grep -F "cloudflared version ${CLOUDFLARED_VERSION}" | |
| - name: Generate MCP test TLS | |
| run: bash test/e2e/setup-mcp-test-tls.sh | |
| - id: mcp_runtime_compatibility | |
| name: Classify OpenShell credential-boundary compatibility | |
| run: | | |
| set -euo pipefail | |
| export PATH="$HOME/.local/bin:$HOME/.npm-global/bin:$PATH" | |
| npx tsx tools/e2e/mcp-bridge-runtime-compatibility.mts | |
| - name: Run MCP OpenShell provider live test | |
| if: ${{ steps.mcp_runtime_compatibility.outputs.mode == 'full-lifecycle' }} | |
| run: | | |
| set -euo pipefail | |
| export PATH="$HOME/.local/bin:$HOME/.npm-global/bin:$PATH" | |
| if command -v openshell >/dev/null 2>&1; then | |
| OPENSHELL_BIN="$(command -v openshell)" | |
| elif [ -x "$HOME/.local/bin/openshell" ]; then | |
| OPENSHELL_BIN="$HOME/.local/bin/openshell" | |
| else | |
| echo "::error::OpenShell CLI not found after install" | |
| exit 1 | |
| fi | |
| export OPENSHELL_BIN | |
| "$OPENSHELL_BIN" --version | |
| npx tsx tools/e2e/live-vitest-invocation.mts run --test-path test/e2e/live/mcp-bridge.test.ts | |
| - id: mcp_artifact_secret_scan | |
| name: Scan MCP artifacts for fixture credentials | |
| if: always() | |
| run: >- | |
| npx tsx tools/e2e/assert-mcp-artifact-secrets-absent.mts | |
| e2e-artifacts/live/mcp-bridge-dev/${{ matrix.agent }} | |
| - name: Upload MCP server artifacts | |
| if: ${{ always() && steps.mcp_artifact_secret_scan.outcome == 'success' }} | |
| uses: NVIDIA/NemoClaw/.github/actions/upload-e2e-artifacts@7768e15eb90d3ee2d33432f481dfe8747e4f6d57 | |
| with: | |
| name: e2e-mcp-bridge-dev-${{ matrix.agent }} | |
| path: e2e-artifacts/live/mcp-bridge-dev/${{ matrix.agent }}/ | |
| - name: Clean up Docker auth | |
| if: always() | |
| shell: bash | |
| run: bash .github/scripts/docker-auth-cleanup.sh | |
| # Manual PR qualification also requires the exact candidate activation contract. | |
| managed-image-multiarch-startup: | |
| name: Protected managed-image startup (${{ matrix.platform }}) | |
| needs: generate-matrix | |
| if: ${{ github.repository == 'NVIDIA/NemoClaw' && github.ref == 'refs/heads/main' && (contains(fromJSON(needs.generate-matrix.outputs.selected_jobs), 'managed-image-multiarch-startup') || contains(fromJSON(needs.generate-matrix.outputs.selected_jobs), 'managed-image-protected-runtime')) }} | |
| runs-on: ${{ matrix.runner }} | |
| timeout-minutes: 210 | |
| permissions: | |
| contents: read | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - platform: linux/amd64 | |
| runner: ubuntu-24.04 | |
| shard: linux-amd64 | |
| - platform: linux/arm64 | |
| runner: ubuntu-24.04-arm | |
| shard: linux-arm64 | |
| env: | |
| E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts/live/managed-image-multiarch-startup/${{ matrix.shard }} | |
| E2E_JOB: "1" | |
| E2E_TARGET_ID: "managed-image-multiarch-startup" | |
| RELEASE_E2E_ACTIVATION_PATH: ci/protected-managed-image-multiarch-activation-v1.json | |
| NEMOCLAW_E2E_EXPECTED_SHA: ${{ inputs.checkout_sha }} | |
| NEMOCLAW_PROTECTED_MANAGED_IMAGE_HEAD_SHA: ${{ inputs.checkout_sha || github.sha }} | |
| NEMOCLAW_E2E_SHARD: ${{ matrix.shard }} | |
| NEMOCLAW_PROTECTED_MANAGED_IMAGE_BASE_SHA: ${{ inputs.base_sha || github.event.before || github.sha }} | |
| NEMOCLAW_PROTECTED_MANAGED_IMAGE_BUILD_CACHE: ${{ github.workspace }}/.protected-managed-image-build-cache/${{ matrix.shard }} | |
| NEMOCLAW_PROTECTED_MANAGED_IMAGE_BUILD_CACHE_ARTIFACT: protected-managed-image-build-cache-${{ github.run_id }}-${{ inputs.checkout_sha || github.sha }} | |
| NEMOCLAW_PROTECTED_MANAGED_IMAGE_COHORT: protected-${{ github.run_id }}-${{ github.run_attempt }} | |
| NEMOCLAW_PROTECTED_MANAGED_IMAGE_CONTRACT: ${{ github.workspace }}/e2e-artifacts/live/managed-image-multiarch-startup/${{ matrix.shard }}/contracts.json | |
| NEMOCLAW_PROTECTED_MANAGED_IMAGE_EVIDENCE: ${{ github.workspace }}/e2e-artifacts/live/managed-image-multiarch-startup/${{ matrix.shard }}/evidence.json | |
| NEMOCLAW_PROTECTED_MANAGED_IMAGE_PLATFORM: ${{ matrix.platform }} | |
| NEMOCLAW_PROTECTED_MANAGED_IMAGE_WORKFLOW_SHA: ${{ inputs.workflow_sha || github.workflow_sha }} | |
| NEMOCLAW_PROTECTED_REGISTRY_NAME: nemoclaw-managed-${{ matrix.shard }}-${{ github.run_id }}-${{ github.run_attempt }} | |
| NEMOCLAW_RUN_LIVE_E2E: "1" | |
| steps: | |
| - name: Validate protected exact-head dispatch | |
| env: | |
| BASE_SHA: ${{ inputs.base_sha || github.event.before || github.sha }} | |
| CHECKOUT_SHA: ${{ inputs.checkout_sha || github.sha }} | |
| EVENT_NAME: ${{ github.event_name }} | |
| EXPECTED_WORKFLOW_SHA: ${{ inputs.workflow_sha || github.workflow_sha }} | |
| PLATFORM: ${{ matrix.platform }} | |
| REF: ${{ github.ref }} | |
| REPOSITORY: ${{ github.repository }} | |
| RUNNER_ARCH_KIND: ${{ runner.arch }} | |
| WORKFLOW_SHA: ${{ github.workflow_sha }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| [[ "$REPOSITORY" == "NVIDIA/NemoClaw" && "$REF" == "refs/heads/main" && ( "$EVENT_NAME" == "push" || "$EVENT_NAME" == "workflow_dispatch" ) ]] || { | |
| echo "::error::Protected managed-image startup must run from trusted NVIDIA/NemoClaw main" >&2 | |
| exit 1 | |
| } | |
| [[ "$CHECKOUT_SHA" =~ ^[a-f0-9]{40}$ && "$BASE_SHA" =~ ^[a-f0-9]{40}$ ]] || { | |
| echo "::error::Protected managed-image startup requires candidate and base SHAs" >&2 | |
| exit 1 | |
| } | |
| [[ "$EXPECTED_WORKFLOW_SHA" =~ ^[a-f0-9]{40}$ && "$WORKFLOW_SHA" == "$EXPECTED_WORKFLOW_SHA" ]] || { | |
| echo "::error::Protected managed-image startup requires the exact trusted workflow SHA" >&2 | |
| exit 1 | |
| } | |
| case "${PLATFORM}:${RUNNER_ARCH_KIND}" in | |
| linux/amd64:X64 | linux/arm64:ARM64) ;; | |
| *) | |
| echo "::error::Protected managed-image startup requires a native ${PLATFORM} runner" >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| repository: ${{ inputs.checkout_repository || github.repository }} | |
| ref: ${{ inputs.checkout_sha || github.sha }} | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - *dockerhub-auth | |
| - name: Set up protected managed-image Buildx | |
| uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 | |
| with: | |
| driver-opts: network=host | |
| buildkitd-config-inline: | | |
| [registry."localhost:5000"] | |
| http = true | |
| - name: Prepare E2E workspace | |
| uses: NVIDIA/NemoClaw/.github/actions/prepare-e2e@f6304bc25fc35bfaa441c8c2fbfee38f72805a75 | |
| with: | |
| build-cli: "false" | |
| - name: Validate candidate activation contract | |
| env: | |
| CHECKOUT_SHA: ${{ inputs.checkout_sha || github.sha }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| activation="ci/protected-managed-image-multiarch-activation-v1.json" | |
| [[ "$(git rev-parse --verify HEAD)" == "$CHECKOUT_SHA" ]] || { | |
| echo "::error::Protected managed-image checkout does not match the candidate SHA" >&2 | |
| exit 1 | |
| } | |
| [[ -f "$activation" && ! -L "$activation" ]] || { | |
| echo "::error::Protected managed-image activation contract is absent" >&2 | |
| exit 1 | |
| } | |
| jq -e ' | |
| (keys | sort) == ["agents", "contractVersion", "jobId", "platforms"] and | |
| .contractVersion == 1 and | |
| .jobId == "managed-image-multiarch-startup" and | |
| .agents == ["openclaw", "hermes", "langchain-deepagents-code"] and | |
| .platforms == ["linux/amd64", "linux/arm64"] | |
| ' "$activation" >/dev/null || { | |
| echo "::error::Protected managed-image activation contract is invalid" >&2 | |
| exit 1 | |
| } | |
| install -d -m 0700 "$E2E_ARTIFACT_DIR" | |
| - id: bases | |
| name: Resolve exact platform base images | |
| env: | |
| PLATFORM: ${{ matrix.platform }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| arch="${PLATFORM#linux/}" | |
| work_dir="$(mktemp -d "${RUNNER_TEMP}/nemoclaw-protected-bases.XXXXXX")" | |
| trap 'rm -rf -- "$work_dir"' EXIT | |
| resolve_base() { | |
| local output_name="$1" | |
| local alias="$2" | |
| local repository="$3" | |
| local alias_raw="$work_dir/${output_name}-alias.raw" | |
| local exact_raw="$work_dir/${output_name}-exact.raw" | |
| docker buildx imagetools inspect "$alias" --raw > "$alias_raw" | |
| local digest | |
| digest="$( | |
| jq -er --arg arch "$arch" ' | |
| if ( | |
| .mediaType == "application/vnd.oci.image.index.v1+json" or | |
| .mediaType == "application/vnd.docker.distribution.manifest.list.v2+json" | |
| ) then | |
| [.manifests[] | select(.platform.os == "linux" and .platform.architecture == $arch)] | |
| | if length == 1 then .[0].digest else error("not one exact platform descriptor") end | |
| else | |
| error("base alias is not a platform index") | |
| end | |
| ' "$alias_raw" | |
| )" | |
| [[ "$digest" =~ ^sha256:[a-f0-9]{64}$ ]] || { | |
| echo "::error::${output_name} base alias returned an invalid digest" >&2 | |
| exit 1 | |
| } | |
| local reference="${repository}@${digest}" | |
| docker buildx imagetools inspect "$reference" --raw > "$exact_raw" | |
| [[ "sha256:$(sha256sum "$exact_raw" | awk '{print $1}')" == "$digest" ]] || { | |
| echo "::error::${output_name} exact base bytes do not match the selected digest" >&2 | |
| exit 1 | |
| } | |
| printf '%s=%s\n' "$output_name" "$reference" >> "$GITHUB_OUTPUT" | |
| } | |
| resolve_base openclaw \ | |
| ghcr.io/nvidia/nemoclaw/sandbox-base:latest \ | |
| ghcr.io/nvidia/nemoclaw/sandbox-base | |
| resolve_base hermes \ | |
| ghcr.io/nvidia/nemoclaw/hermes-sandbox-base:latest \ | |
| ghcr.io/nvidia/nemoclaw/hermes-sandbox-base | |
| resolve_base dcode \ | |
| ghcr.io/nvidia/nemoclaw/langchain-deepagents-code-sandbox-base:latest \ | |
| ghcr.io/nvidia/nemoclaw/langchain-deepagents-code-sandbox-base | |
| - name: Start isolated protected managed-image registry | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if docker container inspect "$NEMOCLAW_PROTECTED_REGISTRY_NAME" >/dev/null 2>&1; then | |
| echo "::error::Protected registry name already exists" >&2 | |
| exit 1 | |
| fi | |
| if curl --fail --silent --show-error http://127.0.0.1:5000/v2/ >/dev/null 2>&1; then | |
| echo "::error::Refusing to reuse an existing localhost:5000 registry" >&2 | |
| exit 1 | |
| fi | |
| docker run --detach \ | |
| --name "$NEMOCLAW_PROTECTED_REGISTRY_NAME" \ | |
| --label "io.nvidia.nemoclaw.e2e-owner=${NEMOCLAW_PROTECTED_MANAGED_IMAGE_COHORT}" \ | |
| --label "io.nvidia.nemoclaw.e2e-platform=${NEMOCLAW_PROTECTED_MANAGED_IMAGE_PLATFORM}" \ | |
| --publish 127.0.0.1:5000:5000 \ | |
| docker.io/library/registry@sha256:a3d8aaa63ed8681a604f1dea0aa03f100d5895b6a58ace528858a7b332415373 | |
| for _ in $(seq 1 30); do | |
| if curl --fail --silent --show-error http://127.0.0.1:5000/v2/ >/dev/null; then | |
| exit 0 | |
| fi | |
| sleep 1 | |
| done | |
| docker logs "$NEMOCLAW_PROTECTED_REGISTRY_NAME" >&2 | |
| exit 1 | |
| - name: Build exact all-agent protected managed images | |
| env: | |
| BASE_DCODE: ${{ steps.bases.outputs.dcode }} | |
| BASE_HERMES: ${{ steps.bases.outputs.hermes }} | |
| BASE_OPENCLAW: ${{ steps.bases.outputs.openclaw }} | |
| CHECKOUT_SHA: ${{ inputs.checkout_sha || github.sha }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| cache_args=() | |
| if [[ "$NEMOCLAW_PROTECTED_MANAGED_IMAGE_PLATFORM" == "linux/amd64" ]]; then | |
| cache_args=(--cache-to "$NEMOCLAW_PROTECTED_MANAGED_IMAGE_BUILD_CACHE") | |
| fi | |
| scripts/checks/build-protected-managed-images.sh \ | |
| --output "$NEMOCLAW_PROTECTED_MANAGED_IMAGE_CONTRACT" \ | |
| --revision "$CHECKOUT_SHA" \ | |
| --cohort "$NEMOCLAW_PROTECTED_MANAGED_IMAGE_COHORT" \ | |
| --platform "$NEMOCLAW_PROTECTED_MANAGED_IMAGE_PLATFORM" \ | |
| --openclaw-base "$BASE_OPENCLAW" \ | |
| --hermes-base "$BASE_HERMES" \ | |
| --dcode-base "$BASE_DCODE" \ | |
| "${cache_args[@]}" | |
| - name: Run every exact managed-image contract directly | |
| env: | |
| BASE_SHA: ${{ inputs.base_sha || github.event.before || github.sha }} | |
| CHECKOUT_SHA: ${{ inputs.checkout_sha || github.sha }} | |
| WORKFLOW_SHA: ${{ inputs.workflow_sha || github.workflow_sha }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| direct_runs="${RUNNER_TEMP}/protected-managed-image-direct-runs.jsonl" | |
| : > "$direct_runs" | |
| while IFS= read -r entry; do | |
| agent="$(jq -er '.agent' <<< "$entry")" | |
| digest="$(jq -er '.digest' <<< "$entry")" | |
| platform="$(jq -er '.platform' <<< "$entry")" | |
| reference="$(jq -er '.reference' <<< "$entry")" | |
| [[ "$platform" == "$NEMOCLAW_PROTECTED_MANAGED_IMAGE_PLATFORM" ]] || { | |
| echo "::error::Protected managed-image contract changed platform" >&2 | |
| exit 1 | |
| } | |
| npx --no-install tsx scripts/checks/run-managed-image-direct-e2e.ts \ | |
| --agent "$agent" \ | |
| --image "$reference" \ | |
| --platform "$platform" | |
| jq -nc \ | |
| --arg agent "$agent" \ | |
| --arg digest "$digest" \ | |
| --arg platform "$platform" \ | |
| --arg reference "$reference" \ | |
| '{agent: $agent, digest: $digest, platform: $platform, reference: $reference}' \ | |
| >> "$direct_runs" | |
| done < <(jq -c '.[]' "$NEMOCLAW_PROTECTED_MANAGED_IMAGE_CONTRACT") | |
| contract_sha="sha256:$(sha256sum "$NEMOCLAW_PROTECTED_MANAGED_IMAGE_CONTRACT" | awk '{print $1}')" | |
| jq -n \ | |
| --arg baseSha "$BASE_SHA" \ | |
| --arg cohort "$NEMOCLAW_PROTECTED_MANAGED_IMAGE_COHORT" \ | |
| --arg contractSha256 "$contract_sha" \ | |
| --arg headSha "$CHECKOUT_SHA" \ | |
| --arg platform "$NEMOCLAW_PROTECTED_MANAGED_IMAGE_PLATFORM" \ | |
| --arg workflowSha "$WORKFLOW_SHA" \ | |
| --argjson runAttempt "$GITHUB_RUN_ATTEMPT" \ | |
| --argjson runId "$GITHUB_RUN_ID" \ | |
| --slurpfile contracts "$NEMOCLAW_PROTECTED_MANAGED_IMAGE_CONTRACT" \ | |
| --slurpfile directRuns "$direct_runs" \ | |
| '{ | |
| kind: "nemoclaw-protected-managed-image-multiarch-v1", | |
| headSha: $headSha, | |
| baseSha: $baseSha, | |
| workflowSha: $workflowSha, | |
| platform: $platform, | |
| cohort: $cohort, | |
| contractSha256: $contractSha256, | |
| contracts: $contracts[0], | |
| directRuns: $directRuns, | |
| run: {id: $runId, attempt: $runAttempt} | |
| }' > "${NEMOCLAW_PROTECTED_MANAGED_IMAGE_EVIDENCE}.tmp" | |
| mv \ | |
| "${NEMOCLAW_PROTECTED_MANAGED_IMAGE_EVIDENCE}.tmp" \ | |
| "$NEMOCLAW_PROTECTED_MANAGED_IMAGE_EVIDENCE" | |
| - name: Remove isolated protected managed-image registry | |
| if: always() | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if docker container inspect "$NEMOCLAW_PROTECTED_REGISTRY_NAME" >/dev/null 2>&1; then | |
| owner="$( | |
| docker container inspect \ | |
| --format '{{index .Config.Labels "io.nvidia.nemoclaw.e2e-owner"}}' \ | |
| "$NEMOCLAW_PROTECTED_REGISTRY_NAME" | |
| )" | |
| [[ "$owner" == "$NEMOCLAW_PROTECTED_MANAGED_IMAGE_COHORT" ]] || { | |
| echo "::error::Refusing to remove a registry not owned by this protected shard" >&2 | |
| exit 1 | |
| } | |
| docker rm -f "$NEMOCLAW_PROTECTED_REGISTRY_NAME" >/dev/null | |
| fi | |
| if docker container inspect "$NEMOCLAW_PROTECTED_REGISTRY_NAME" >/dev/null 2>&1; then | |
| echo "::error::Protected managed-image registry container remained after cleanup" >&2 | |
| exit 1 | |
| fi | |
| if curl --fail --silent --show-error http://127.0.0.1:5000/v2/ >/dev/null 2>&1; then | |
| echo "::error::Protected managed-image registry listener remained after cleanup" >&2 | |
| exit 1 | |
| fi | |
| - name: Validate protected managed-image evidence | |
| shell: bash | |
| run: >- | |
| npx tsx tools/e2e/live-vitest-invocation.mts run | |
| --test-path test/e2e/live/managed-image-multiarch-startup.test.ts | |
| - name: Publish exact amd64 protected runtime build cache | |
| if: ${{ matrix.platform == 'linux/amd64' }} | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: ${{ env.NEMOCLAW_PROTECTED_MANAGED_IMAGE_BUILD_CACHE_ARTIFACT }} | |
| path: ${{ env.NEMOCLAW_PROTECTED_MANAGED_IMAGE_BUILD_CACHE }}/ | |
| if-no-files-found: error | |
| retention-days: 1 | |
| compression-level: 0 | |
| overwrite: true | |
| - name: Upload protected managed-image evidence | |
| if: always() | |
| uses: NVIDIA/NemoClaw/.github/actions/upload-e2e-artifacts@7768e15eb90d3ee2d33432f481dfe8747e4f6d57 | |
| with: | |
| name: e2e-managed-image-multiarch-startup-${{ matrix.shard }} | |
| path: e2e-artifacts/live/managed-image-multiarch-startup/${{ matrix.shard }}/ | |
| - name: Clean up Docker auth | |
| if: always() | |
| shell: bash | |
| run: bash .github/scripts/docker-auth-cleanup.sh | |
| # The trusted plan compiler runs on a standard runner before the protected | |
| # job can be assigned. Candidate workflow code is never evaluated; trusted | |
| # main compiles the exact candidate's declarative YAML into bounded outputs. | |
| llama-cpp-dgx-spark-plan: | |
| name: Compile protected llama.cpp DGX Spark plan | |
| needs: generate-matrix | |
| if: ${{ inputs.allow_dgx_spark_runner_queue && github.repository == 'NVIDIA/NemoClaw' && github.ref == 'refs/heads/main' && contains(fromJSON(needs.generate-matrix.outputs.selected_jobs), 'llama-cpp-dgx-spark-qualification') }} | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| permissions: | |
| contents: read | |
| outputs: | |
| agent_qualification_execution: ${{ steps.plan.outputs.agent_qualification_execution }} | |
| environment: ${{ steps.plan.outputs.environment }} | |
| execution: ${{ steps.plan.outputs.execution }} | |
| model_host_path: ${{ steps.plan.outputs.model_host_path }} | |
| plan: ${{ steps.plan.outputs.plan }} | |
| plan_sha256: ${{ steps.plan.outputs.plan_sha256 }} | |
| qualification: ${{ steps.plan.outputs.qualification }} | |
| runner: ${{ steps.plan.outputs.runner }} | |
| steps: | |
| - name: Validate trusted llama.cpp plan dispatch | |
| env: | |
| BASE_SHA: ${{ inputs.base_sha || github.event.before || github.sha }} | |
| CHECKOUT_SHA: ${{ inputs.checkout_sha || github.sha }} | |
| EVENT_NAME: ${{ github.event_name }} | |
| EXPECTED_WORKFLOW_SHA: ${{ inputs.workflow_sha || github.workflow_sha }} | |
| REF: ${{ github.ref }} | |
| REPOSITORY: ${{ github.repository }} | |
| WORKFLOW_SHA: ${{ github.workflow_sha }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| [[ "$REPOSITORY" == "NVIDIA/NemoClaw" && "$REF" == "refs/heads/main" && ( "$EVENT_NAME" == "push" || "$EVENT_NAME" == "workflow_dispatch" ) ]] || { | |
| echo "::error::Protected llama.cpp planning must run from trusted NVIDIA/NemoClaw main" >&2 | |
| exit 1 | |
| } | |
| [[ "$CHECKOUT_SHA" =~ ^[a-f0-9]{40}$ && "$BASE_SHA" =~ ^[a-f0-9]{40}$ ]] || { | |
| echo "::error::Protected llama.cpp planning requires candidate and base SHAs" >&2 | |
| exit 1 | |
| } | |
| [[ "$EXPECTED_WORKFLOW_SHA" =~ ^[a-f0-9]{40}$ && "$WORKFLOW_SHA" == "$EXPECTED_WORKFLOW_SHA" ]] || { | |
| echo "::error::Protected llama.cpp planning requires the exact trusted workflow SHA" >&2 | |
| exit 1 | |
| } | |
| - name: Checkout trusted llama.cpp plan compiler | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| repository: ${{ github.repository }} | |
| ref: ${{ inputs.workflow_sha || github.workflow_sha }} | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Checkout exact llama.cpp candidate configuration | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| repository: ${{ inputs.checkout_repository || github.repository }} | |
| ref: ${{ inputs.checkout_sha || github.sha }} | |
| path: .candidate-llama-cpp | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Set up Node for trusted llama.cpp plan compilation | |
| uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: 22 | |
| cache: npm | |
| - name: Install trusted llama.cpp plan dependencies | |
| run: npm ci --ignore-scripts | |
| - id: plan | |
| name: Compile exact candidate llama.cpp qualification plan | |
| env: | |
| CANDIDATE_ROOT: ${{ github.workspace }}/.candidate-llama-cpp | |
| CHECKOUT_SHA: ${{ inputs.checkout_sha || github.sha }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| [[ "$(git -C "$CANDIDATE_ROOT" rev-parse --verify HEAD)" == "$CHECKOUT_SHA" ]] || { | |
| echo "::error::Protected llama.cpp candidate checkout does not match the candidate SHA" >&2 | |
| exit 1 | |
| } | |
| node --experimental-strip-types --no-warnings \ | |
| scripts/checks/export-llama-cpp-dgx-spark-qualification-plan.mts \ | |
| --source-root "$CANDIDATE_ROOT" | |
| # The opt-in flag prevents assignment to an unavailable DGX Spark runner. | |
| # Qualification runs only when the trusted plan reports execution=enabled. | |
| llama-cpp-dgx-spark-qualification: | |
| name: Protected llama.cpp on NVIDIA DGX Spark | |
| needs: [generate-matrix, llama-cpp-dgx-spark-plan] | |
| if: ${{ inputs.allow_dgx_spark_runner_queue && github.repository == 'NVIDIA/NemoClaw' && github.ref == 'refs/heads/main' && contains(fromJSON(needs.generate-matrix.outputs.selected_jobs), 'llama-cpp-dgx-spark-qualification') && needs.llama-cpp-dgx-spark-plan.outputs.execution == 'enabled' }} | |
| runs-on: ${{ needs.llama-cpp-dgx-spark-plan.outputs.runner }} | |
| environment: | |
| name: ${{ needs.llama-cpp-dgx-spark-plan.outputs.environment }} | |
| timeout-minutes: 300 | |
| permissions: | |
| contents: read | |
| env: | |
| E2E_DEFAULT_ENABLED: "0" | |
| E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts/live/llama-cpp-dgx-spark-qualification | |
| E2E_JOB: "1" | |
| E2E_TARGET_ID: "llama-cpp-dgx-spark-qualification" | |
| NEMOCLAW_E2E_EXPECTED_SHA: ${{ inputs.checkout_sha }} | |
| NEMOCLAW_E2E_SHARD: linux-arm64-gpu-dgx-spark-gb10 | |
| NEMOCLAW_LLAMA_CPP_QUALIFICATION_BASE_SHA: ${{ inputs.base_sha || github.event.before || github.sha }} | |
| NEMOCLAW_LLAMA_CPP_QUALIFICATION_HEAD_SHA: ${{ inputs.checkout_sha || github.sha }} | |
| NEMOCLAW_LLAMA_CPP_QUALIFICATION_CANDIDATE_ROOT: ${{ github.workspace }}/.candidate-llama-cpp | |
| NEMOCLAW_LLAMA_CPP_QUALIFICATION_EVIDENCE: ${{ github.workspace }}/e2e-artifacts/live/llama-cpp-dgx-spark-qualification/evidence.json | |
| NEMOCLAW_LLAMA_CPP_QUALIFICATION_PLAN: ${{ github.workspace }}/.llama-cpp-qualification/plan.json | |
| NEMOCLAW_LLAMA_CPP_QUALIFICATION_PLAN_SHA256: ${{ needs.llama-cpp-dgx-spark-plan.outputs.plan_sha256 }} | |
| NEMOCLAW_LLAMA_CPP_QUALIFICATION_REGISTRY: nemoclaw-llama-cpp-${{ github.run_id }}-${{ github.run_attempt }} | |
| NEMOCLAW_LLAMA_CPP_QUALIFICATION_WORKFLOW_SHA: ${{ inputs.workflow_sha || github.workflow_sha }} | |
| NEMOCLAW_RUN_LIVE_E2E: "1" | |
| RELEASE_E2E_ACTIVATION_PATH: ci/llama-cpp-dgx-spark-qualification-v1.yaml | |
| steps: | |
| - name: Validate protected llama.cpp exact-head dispatch | |
| env: | |
| BASE_SHA: ${{ inputs.base_sha || github.event.before || github.sha }} | |
| CHECKOUT_SHA: ${{ inputs.checkout_sha || github.sha }} | |
| EVENT_NAME: ${{ github.event_name }} | |
| EXPECTED_WORKFLOW_SHA: ${{ inputs.workflow_sha || github.workflow_sha }} | |
| REF: ${{ github.ref }} | |
| REPOSITORY: ${{ github.repository }} | |
| RUNNER_ARCH_KIND: ${{ runner.arch }} | |
| WORKFLOW_SHA: ${{ github.workflow_sha }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| [[ "$REPOSITORY" == "NVIDIA/NemoClaw" && "$REF" == "refs/heads/main" && ( "$EVENT_NAME" == "push" || "$EVENT_NAME" == "workflow_dispatch" ) ]] || { | |
| echo "::error::Protected llama.cpp qualification must run from trusted NVIDIA/NemoClaw main" >&2 | |
| exit 1 | |
| } | |
| [[ "$CHECKOUT_SHA" =~ ^[a-f0-9]{40}$ && "$BASE_SHA" =~ ^[a-f0-9]{40}$ ]] || { | |
| echo "::error::Protected llama.cpp qualification requires candidate and base SHAs" >&2 | |
| exit 1 | |
| } | |
| [[ "$EXPECTED_WORKFLOW_SHA" =~ ^[a-f0-9]{40}$ && "$WORKFLOW_SHA" == "$EXPECTED_WORKFLOW_SHA" ]] || { | |
| echo "::error::Protected llama.cpp qualification requires the exact trusted workflow SHA" >&2 | |
| exit 1 | |
| } | |
| [[ "$RUNNER_ARCH_KIND" == "ARM64" ]] || { | |
| echo "::error::Protected llama.cpp qualification requires native Linux ARM64" >&2 | |
| exit 1 | |
| } | |
| - name: Checkout trusted llama.cpp qualification | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| repository: ${{ github.repository }} | |
| ref: ${{ inputs.workflow_sha || github.workflow_sha }} | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Checkout exact llama.cpp qualification candidate | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| repository: ${{ inputs.checkout_repository || github.repository }} | |
| ref: ${{ inputs.checkout_sha || github.sha }} | |
| path: .candidate-llama-cpp | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - *dockerhub-auth | |
| - name: Set up protected llama.cpp Buildx | |
| uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 | |
| with: | |
| driver: docker | |
| - name: Prepare E2E workspace | |
| uses: NVIDIA/NemoClaw/.github/actions/prepare-e2e@f6304bc25fc35bfaa441c8c2fbfee38f72805a75 | |
| with: | |
| build-cli: "false" | |
| - name: Install OpenShell CLI for declarative OpenClaw qualification | |
| if: ${{ needs.llama-cpp-dgx-spark-plan.outputs.agent_qualification_execution == 'enabled' }} | |
| run: 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: Materialize trusted llama.cpp qualification plan | |
| env: | |
| CANDIDATE_ROOT: ${{ github.workspace }}/.candidate-llama-cpp | |
| CHECKOUT_SHA: ${{ inputs.checkout_sha || github.sha }} | |
| PLAN: ${{ needs.llama-cpp-dgx-spark-plan.outputs.plan }} | |
| PLAN_SHA256: ${{ needs.llama-cpp-dgx-spark-plan.outputs.plan_sha256 }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| [[ "$(git -C "$CANDIDATE_ROOT" rev-parse --verify HEAD)" == "$CHECKOUT_SHA" ]] || { | |
| echo "::error::Protected llama.cpp candidate checkout does not match the candidate SHA" >&2 | |
| exit 1 | |
| } | |
| install -d -m 0700 "$E2E_ARTIFACT_DIR" | |
| install -d -m 0700 "$(dirname "$NEMOCLAW_LLAMA_CPP_QUALIFICATION_PLAN")" | |
| umask 077 | |
| printf '%s' "$PLAN" > "$NEMOCLAW_LLAMA_CPP_QUALIFICATION_PLAN" | |
| [[ "sha256:$(sha256sum "$NEMOCLAW_LLAMA_CPP_QUALIFICATION_PLAN" | awk '{print $1}')" == "$PLAN_SHA256" ]] || { | |
| echo "::error::Protected llama.cpp qualification plan digest changed" >&2 | |
| exit 1 | |
| } | |
| - id: qualify | |
| name: Build and qualify exact llama.cpp candidate | |
| env: | |
| BASE_SHA: ${{ inputs.base_sha || github.event.before || github.sha }} | |
| CANDIDATE_ROOT: ${{ github.workspace }}/.candidate-llama-cpp | |
| CHECKOUT_SHA: ${{ inputs.checkout_sha || github.sha }} | |
| MODEL_HOST_PATH: ${{ needs.llama-cpp-dgx-spark-plan.outputs.model_host_path }} | |
| WORKFLOW_SHA: ${{ inputs.workflow_sha || github.workflow_sha }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| [[ "$(git rev-parse --verify HEAD)" == "$WORKFLOW_SHA" ]] || { | |
| echo "::error::Protected llama.cpp qualification must execute trusted workflow code" >&2 | |
| exit 1 | |
| } | |
| npx --no-install tsx scripts/checks/run-llama-cpp-dgx-spark-qualification.mts \ | |
| --base-sha "$BASE_SHA" \ | |
| --candidate-root "$CANDIDATE_ROOT" \ | |
| --head-sha "$CHECKOUT_SHA" \ | |
| --model-host-path "$MODEL_HOST_PATH" \ | |
| --output "$NEMOCLAW_LLAMA_CPP_QUALIFICATION_EVIDENCE" \ | |
| --plan "$NEMOCLAW_LLAMA_CPP_QUALIFICATION_PLAN" \ | |
| --plan-sha256 "$NEMOCLAW_LLAMA_CPP_QUALIFICATION_PLAN_SHA256" \ | |
| --registry-name "$NEMOCLAW_LLAMA_CPP_QUALIFICATION_REGISTRY" \ | |
| --run-attempt "$GITHUB_RUN_ATTEMPT" \ | |
| --run-id "$GITHUB_RUN_ID" \ | |
| --workflow-sha "$WORKFLOW_SHA" | |
| - name: Remove protected llama.cpp qualification resources | |
| if: always() | |
| shell: bash | |
| run: >- | |
| npx --no-install tsx scripts/checks/run-llama-cpp-dgx-spark-qualification.mts | |
| --cleanup-only | |
| --registry-name "$NEMOCLAW_LLAMA_CPP_QUALIFICATION_REGISTRY" | |
| --run-attempt "$GITHUB_RUN_ATTEMPT" | |
| --run-id "$GITHUB_RUN_ID" | |
| - name: Validate protected llama.cpp evidence | |
| shell: bash | |
| run: >- | |
| npx --no-install tsx tools/e2e/live-vitest-invocation.mts run | |
| --test-path test/e2e/live/llama-cpp-dgx-spark-qualification.test.ts | |
| - name: Upload protected llama.cpp evidence | |
| if: always() | |
| uses: NVIDIA/NemoClaw/.github/actions/upload-e2e-artifacts@7768e15eb90d3ee2d33432f481dfe8747e4f6d57 | |
| with: | |
| name: e2e-llama-cpp-dgx-spark-qualification | |
| path: e2e-artifacts/live/llama-cpp-dgx-spark-qualification/ | |
| - name: Clean up Docker auth | |
| if: always() | |
| shell: bash | |
| run: bash .github/scripts/docker-auth-cleanup.sh | |
| # This lane runs after managed-image startup succeeds. Manual PR qualification | |
| # requires the exact candidate activation contract. Its | |
| # hosted amd64 prerequisite exports the exact candidate build cache. | |
| # This GPU lane imports the exact candidate build cache and its complete | |
| # locked npm cache seed, then rebuilds with BuildKit networking disabled. | |
| # invalidState: Cold managed-image builds on the protected GPU runner can | |
| # stall in npm and terminate with "Exit handler never called!" before any | |
| # GPU, Ollama, NIM, or vLLM assertion executes. | |
| # sourceBoundary: The hosted amd64 lane builds the exact candidate. It adds | |
| # every reachable Linux x64 glibc registry archive from the candidate | |
| # lockfile to the same-run cache artifact. The GPU lane verifies the manifest, | |
| # lock digest, archive set, and | |
| # each SHA-512 integrity before the candidate build can consume the seed. | |
| # BuildKit records can accelerate the rebuild, while the seed makes a missing | |
| # npm layer reproducible without weakening the fail-closed network boundary. | |
| # whyNotBuildCacheOnly: Protected runs 31156103003, 31158136234, and | |
| # 31159822518 proved that network-key drift and incomplete local cache exports | |
| # can miss the npm layer before runtime qualification. | |
| # regressionTest: The materializer and build-script behavior tests reject an | |
| # incomplete, substituted, unexpected, or symlinked archive handoff and pin | |
| # exact --cache-from/--network none arguments. Workflow boundary tests pin the | |
| # same-run cache artifact. | |
| # lifecycleDecision: PR #8261 owns removal after three consecutive protected | |
| # GPU cold builds of all three images reach GPU, Ollama, NIM, and vLLM | |
| # assertions without the hosted cache. | |
| managed-image-protected-runtime: | |
| name: Protected managed-image GPU and local inference | |
| needs: [generate-matrix, managed-image-multiarch-startup] | |
| if: ${{ always() && github.repository == 'NVIDIA/NemoClaw' && github.ref == 'refs/heads/main' && needs['generate-matrix'].result == 'success' && needs['managed-image-multiarch-startup'].result == 'success' && contains(fromJSON(needs.generate-matrix.outputs.selected_jobs), 'managed-image-protected-runtime') }} | |
| runs-on: linux-amd64-gpu-rtxpro6000-latest-1 | |
| timeout-minutes: 300 | |
| permissions: | |
| contents: read | |
| env: | |
| E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts/live/managed-image-protected-runtime | |
| E2E_JOB: "1" | |
| E2E_TARGET_ID: "managed-image-protected-runtime" | |
| E2E_WORKLOAD_SOURCE: "managed-image" | |
| RELEASE_E2E_ACTIVATION_PATH: ci/protected-managed-image-runtime-activation-v1.json | |
| NEMOCLAW_CLI_BIN: ${{ github.workspace }}/bin/nemoclaw.js | |
| NEMOCLAW_E2E_EXPECTED_SHA: ${{ inputs.checkout_sha }} | |
| NEMOCLAW_E2E_SHARD: linux-amd64-gpu | |
| NEMOCLAW_E2E_TESTED_ROOT: ${{ github.workspace }}/.candidate-runtime | |
| NEMOCLAW_NON_INTERACTIVE: "1" | |
| NEMOCLAW_PROTECTED_MANAGED_IMAGE_BASE_SHA: ${{ inputs.base_sha || github.event.before || github.sha }} | |
| NEMOCLAW_PROTECTED_MANAGED_IMAGE_BUILD_CACHE: ${{ github.workspace }}/.protected-managed-image-build-cache/linux-amd64 | |
| NEMOCLAW_PROTECTED_MANAGED_IMAGE_BUILD_CACHE_ARTIFACT: protected-managed-image-build-cache-${{ github.run_id }}-${{ inputs.checkout_sha || github.sha }} | |
| NEMOCLAW_PROTECTED_MANAGED_IMAGE_COHORT: protected-${{ github.run_id }}-${{ github.run_attempt }} | |
| NEMOCLAW_PROTECTED_MANAGED_IMAGE_CONTRACT: ${{ github.workspace }}/e2e-artifacts/live/managed-image-protected-runtime/contracts.json | |
| NEMOCLAW_PROTECTED_MANAGED_IMAGE_PLATFORM: linux/amd64 | |
| NEMOCLAW_PROTECTED_MANAGED_IMAGE_WORKFLOW_SHA: ${{ inputs.workflow_sha || github.workflow_sha }} | |
| NEMOCLAW_PROTECTED_REGISTRY_NAME: nemoclaw-managed-runtime-${{ github.run_id }}-${{ github.run_attempt }} | |
| NEMOCLAW_RUN_LIVE_E2E: "1" | |
| OPENSHELL_GATEWAY: nemoclaw | |
| steps: | |
| - name: Validate protected runtime exact-head dispatch | |
| env: | |
| BASE_SHA: ${{ inputs.base_sha || github.event.before || github.sha }} | |
| CHECKOUT_SHA: ${{ inputs.checkout_sha || github.sha }} | |
| EVENT_NAME: ${{ github.event_name }} | |
| EXPECTED_WORKFLOW_SHA: ${{ inputs.workflow_sha || github.workflow_sha }} | |
| REF: ${{ github.ref }} | |
| REPOSITORY: ${{ github.repository }} | |
| RUNNER_ARCH_KIND: ${{ runner.arch }} | |
| WORKFLOW_SHA: ${{ github.workflow_sha }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| [[ "$REPOSITORY" == "NVIDIA/NemoClaw" && "$REF" == "refs/heads/main" && ( "$EVENT_NAME" == "push" || "$EVENT_NAME" == "workflow_dispatch" ) ]] || { | |
| echo "::error::Protected managed-image runtime must run from trusted NVIDIA/NemoClaw main" >&2 | |
| exit 1 | |
| } | |
| [[ "$CHECKOUT_SHA" =~ ^[a-f0-9]{40}$ && "$BASE_SHA" =~ ^[a-f0-9]{40}$ ]] || { | |
| echo "::error::Protected managed-image runtime requires candidate and base SHAs" >&2 | |
| exit 1 | |
| } | |
| [[ "$EXPECTED_WORKFLOW_SHA" =~ ^[a-f0-9]{40}$ && "$WORKFLOW_SHA" == "$EXPECTED_WORKFLOW_SHA" ]] || { | |
| echo "::error::Protected managed-image runtime requires the exact trusted workflow SHA" >&2 | |
| exit 1 | |
| } | |
| [[ "$RUNNER_ARCH_KIND" == "X64" ]] || { | |
| echo "::error::Protected managed-image runtime requires a native linux/amd64 GPU runner" >&2 | |
| exit 1 | |
| } | |
| - name: Checkout trusted protected runtime qualification | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| repository: ${{ github.repository }} | |
| ref: ${{ inputs.workflow_sha || github.workflow_sha }} | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Checkout exact protected runtime candidate source | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| repository: ${{ inputs.checkout_repository || github.repository }} | |
| ref: ${{ inputs.checkout_sha || github.sha }} | |
| path: .candidate-runtime | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Download exact protected runtime build cache | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: ${{ env.NEMOCLAW_PROTECTED_MANAGED_IMAGE_BUILD_CACHE_ARTIFACT }} | |
| path: ${{ env.NEMOCLAW_PROTECTED_MANAGED_IMAGE_BUILD_CACHE }} | |
| - *dockerhub-auth | |
| - name: Set up protected runtime Buildx | |
| uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 | |
| with: | |
| driver-opts: network=host | |
| buildkitd-config-inline: | | |
| [registry."localhost:5000"] | |
| http = true | |
| - name: Prepare E2E workspace | |
| uses: NVIDIA/NemoClaw/.github/actions/prepare-e2e@f6304bc25fc35bfaa441c8c2fbfee38f72805a75 | |
| - name: Validate protected runtime activation contract | |
| env: | |
| CHECKOUT_SHA: ${{ inputs.checkout_sha || github.sha }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| candidate_root=".candidate-runtime" | |
| activation="$candidate_root/ci/protected-managed-image-runtime-activation-v1.json" | |
| [[ "$(git -C "$candidate_root" rev-parse --verify HEAD)" == "$CHECKOUT_SHA" ]] || { | |
| echo "::error::Protected managed-image runtime checkout does not match the candidate SHA" >&2 | |
| exit 1 | |
| } | |
| [[ -f "$activation" && ! -L "$activation" ]] || { | |
| echo "::error::Protected managed-image runtime activation contract is absent" >&2 | |
| exit 1 | |
| } | |
| jq -e ' | |
| (keys | sort) == ["agents", "contractVersion", "jobId", "platform", "providers"] and | |
| .contractVersion == 1 and | |
| .jobId == "managed-image-protected-runtime" and | |
| .agents == ["openclaw", "hermes", "langchain-deepagents-code"] and | |
| .platform == "linux/amd64" and | |
| .providers == ["ollama", "nim", "vllm"] | |
| ' "$activation" >/dev/null || { | |
| echo "::error::Protected managed-image runtime activation contract is invalid" >&2 | |
| exit 1 | |
| } | |
| install -d -m 0700 "$E2E_ARTIFACT_DIR" | |
| - id: runtime-bases | |
| name: Resolve exact amd64 runtime base images | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| work_dir="$(mktemp -d "${RUNNER_TEMP}/nemoclaw-runtime-bases.XXXXXX")" | |
| trap 'rm -rf -- "$work_dir"' EXIT | |
| resolve_base() { | |
| local output_name="$1" | |
| local alias="$2" | |
| local repository="$3" | |
| local alias_raw="$work_dir/${output_name}-alias.raw" | |
| local exact_raw="$work_dir/${output_name}-exact.raw" | |
| docker buildx imagetools inspect "$alias" --raw > "$alias_raw" | |
| local digest | |
| digest="$( | |
| jq -er ' | |
| if ( | |
| .mediaType == "application/vnd.oci.image.index.v1+json" or | |
| .mediaType == "application/vnd.docker.distribution.manifest.list.v2+json" | |
| ) then | |
| [.manifests[] | select(.platform.os == "linux" and .platform.architecture == "amd64")] | |
| | if length == 1 then .[0].digest else error("not one exact amd64 descriptor") end | |
| else | |
| error("base alias is not a platform index") | |
| end | |
| ' "$alias_raw" | |
| )" | |
| [[ "$digest" =~ ^sha256:[a-f0-9]{64}$ ]] || { | |
| echo "::error::${output_name} base alias returned an invalid digest" >&2 | |
| exit 1 | |
| } | |
| local reference="${repository}@${digest}" | |
| docker buildx imagetools inspect "$reference" --raw > "$exact_raw" | |
| [[ "sha256:$(sha256sum "$exact_raw" | awk '{print $1}')" == "$digest" ]] || { | |
| echo "::error::${output_name} exact base bytes do not match the selected digest" >&2 | |
| exit 1 | |
| } | |
| printf '%s=%s\n' "$output_name" "$reference" >> "$GITHUB_OUTPUT" | |
| } | |
| resolve_base openclaw \ | |
| ghcr.io/nvidia/nemoclaw/sandbox-base:latest \ | |
| ghcr.io/nvidia/nemoclaw/sandbox-base | |
| resolve_base hermes \ | |
| ghcr.io/nvidia/nemoclaw/hermes-sandbox-base:latest \ | |
| ghcr.io/nvidia/nemoclaw/hermes-sandbox-base | |
| resolve_base dcode \ | |
| ghcr.io/nvidia/nemoclaw/langchain-deepagents-code-sandbox-base:latest \ | |
| ghcr.io/nvidia/nemoclaw/langchain-deepagents-code-sandbox-base | |
| - name: Start isolated protected runtime registry | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if docker container inspect "$NEMOCLAW_PROTECTED_REGISTRY_NAME" >/dev/null 2>&1; then | |
| echo "::error::Protected runtime registry name already exists" >&2 | |
| exit 1 | |
| fi | |
| if curl --fail --silent --show-error http://127.0.0.1:5000/v2/ >/dev/null 2>&1; then | |
| echo "::error::Refusing to reuse an existing localhost:5000 registry" >&2 | |
| exit 1 | |
| fi | |
| docker run --detach \ | |
| --name "$NEMOCLAW_PROTECTED_REGISTRY_NAME" \ | |
| --label "io.nvidia.nemoclaw.e2e-owner=${NEMOCLAW_PROTECTED_MANAGED_IMAGE_COHORT}" \ | |
| --label "io.nvidia.nemoclaw.e2e-platform=linux/amd64" \ | |
| --publish 127.0.0.1:5000:5000 \ | |
| docker.io/library/registry@sha256:a3d8aaa63ed8681a604f1dea0aa03f100d5895b6a58ace528858a7b332415373 | |
| for _ in $(seq 1 30); do | |
| if curl --fail --silent --show-error http://127.0.0.1:5000/v2/ >/dev/null; then | |
| exit 0 | |
| fi | |
| sleep 1 | |
| done | |
| docker logs "$NEMOCLAW_PROTECTED_REGISTRY_NAME" >&2 | |
| exit 1 | |
| - name: Build exact all-agent protected runtime images | |
| env: | |
| BASE_DCODE: ${{ steps.runtime-bases.outputs.dcode }} | |
| BASE_HERMES: ${{ steps.runtime-bases.outputs.hermes }} | |
| BASE_OPENCLAW: ${{ steps.runtime-bases.outputs.openclaw }} | |
| CHECKOUT_SHA: ${{ inputs.checkout_sha || github.sha }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| scripts/checks/build-protected-managed-images.sh \ | |
| --output "$NEMOCLAW_PROTECTED_MANAGED_IMAGE_CONTRACT" \ | |
| --revision "$CHECKOUT_SHA" \ | |
| --cohort "$NEMOCLAW_PROTECTED_MANAGED_IMAGE_COHORT" \ | |
| --platform linux/amd64 \ | |
| --source-root "$GITHUB_WORKSPACE/.candidate-runtime" \ | |
| --cache-from "$NEMOCLAW_PROTECTED_MANAGED_IMAGE_BUILD_CACHE" \ | |
| --openclaw-base "$BASE_OPENCLAW" \ | |
| --hermes-base "$BASE_HERMES" \ | |
| --dcode-base "$BASE_DCODE" | |
| - name: Install OpenShell CLI | |
| shell: bash | |
| run: 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: Run all-agent GPU, local inference, rollback, and cleanup qualification | |
| env: | |
| NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| [[ "$(git rev-parse --verify HEAD)" == "$NEMOCLAW_PROTECTED_MANAGED_IMAGE_WORKFLOW_SHA" ]] || { | |
| echo "::error::Protected NIM qualification must execute trusted workflow code" >&2 | |
| exit 1 | |
| } | |
| export PATH="$HOME/.local/bin:$HOME/.npm-global/bin:$PATH" | |
| export OPENSHELL_BIN="$(command -v openshell)" | |
| "$OPENSHELL_BIN" --version | |
| npx tsx tools/e2e/live-vitest-invocation.mts run --test-path test/e2e/live/managed-image-protected-runtime.test.ts | |
| - name: Remove isolated protected runtime registry | |
| if: always() | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if docker container inspect "$NEMOCLAW_PROTECTED_REGISTRY_NAME" >/dev/null 2>&1; then | |
| owner="$( | |
| docker container inspect \ | |
| --format '{{index .Config.Labels "io.nvidia.nemoclaw.e2e-owner"}}' \ | |
| "$NEMOCLAW_PROTECTED_REGISTRY_NAME" | |
| )" | |
| [[ "$owner" == "$NEMOCLAW_PROTECTED_MANAGED_IMAGE_COHORT" ]] || { | |
| echo "::error::Refusing to remove a runtime registry not owned by this protected job" >&2 | |
| exit 1 | |
| } | |
| docker rm -f "$NEMOCLAW_PROTECTED_REGISTRY_NAME" >/dev/null | |
| fi | |
| if docker container inspect "$NEMOCLAW_PROTECTED_REGISTRY_NAME" >/dev/null 2>&1; then | |
| echo "::error::Protected managed-image runtime registry remained after cleanup" >&2 | |
| exit 1 | |
| fi | |
| if curl --fail --silent --show-error http://127.0.0.1:5000/v2/ >/dev/null 2>&1; then | |
| echo "::error::Protected managed-image runtime registry listener remained after cleanup" >&2 | |
| exit 1 | |
| fi | |
| - name: Upload protected managed-image runtime artifacts | |
| if: always() | |
| uses: NVIDIA/NemoClaw/.github/actions/upload-e2e-artifacts@7768e15eb90d3ee2d33432f481dfe8747e4f6d57 | |
| with: | |
| name: e2e-managed-image-protected-runtime | |
| path: e2e-artifacts/live/managed-image-protected-runtime/ | |
| - name: Clean up Docker auth | |
| if: always() | |
| shell: bash | |
| run: bash .github/scripts/docker-auth-cleanup.sh | |
| hermes-e2e: | |
| needs: generate-matrix | |
| if: ${{ needs.generate-matrix.outputs.hermes_selected == 'true' }} | |
| runs-on: ${{ fromJSON(needs.generate-matrix.outputs.runner_routing)['hermes-e2e'] }} | |
| timeout-minutes: 85 | |
| env: | |
| E2E_JOB: "1" | |
| E2E_TARGET_ID: "hermes-e2e" | |
| E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts/live/hermes-e2e | |
| NEMOCLAW_CLI_BIN: ${{ github.workspace }}/bin/nemoclaw.js | |
| NEMOCLAW_RUN_LIVE_E2E: "1" | |
| NEMOCLAW_E2E_INFERENCE_MODE: ${{ inputs.inference_mode || 'mock' }} | |
| NEMOCLAW_E2E_HERMES_DASHBOARD: "1" | |
| NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1" | |
| NEMOCLAW_AGENT: hermes | |
| NEMOCLAW_NON_INTERACTIVE: "1" | |
| NEMOCLAW_RECREATE_SANDBOX: "1" | |
| NEMOCLAW_SANDBOX_NAME: e2e-hermes | |
| NEMOCLAW_ONBOARD_VALIDATION_TIMEOUT_SECONDS: "60" | |
| steps: | |
| - id: trusted_hermes_swap | |
| name: Provision trusted Hermes E2E swap | |
| if: ${{ github.repository == 'NVIDIA/NemoClaw' && github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.event_name == 'push' || inputs.checkout_sha == '' || (github.event_name == 'workflow_dispatch' && inputs.checkout_sha != '' && (contains(format(',{0},', inputs.jobs), ',hermes-e2e,') || contains(format(',{0},', inputs.targets), ',hermes-e2e,') || contains(format(',{0},', inputs.jobs), ',hermes-dashboard,') || contains(format(',{0},', inputs.targets), ',hermes-dashboard,')))) }} | |
| shell: /bin/bash --noprofile --norc -e -o pipefail {0} | |
| env: | |
| BASH_ENV: /dev/null | |
| CHECKOUT_SHA: ${{ inputs.checkout_sha }} | |
| DISPATCH_SHA: ${{ github.sha }} | |
| ENV: /dev/null | |
| EVENT_NAME: ${{ github.event_name }} | |
| EXPECTED_WORKFLOW_SHA: ${{ inputs.workflow_sha }} | |
| LC_ALL: C | |
| REF: ${{ github.ref }} | |
| REPOSITORY: ${{ github.repository }} | |
| RUNNER_ARCH_KIND: ${{ runner.arch }} | |
| RUNNER_ENVIRONMENT_KIND: ${{ runner.environment }} | |
| RUNNER_OS_KIND: ${{ runner.os }} | |
| WORKFLOW_SHA: ${{ github.workflow_sha }} | |
| run: *trusted-hermes-e2e-swap | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| repository: ${{ inputs.checkout_repository || github.repository }} | |
| ref: ${{ inputs.checkout_sha || github.sha }} | |
| persist-credentials: false | |
| - *dockerhub-auth | |
| - name: Prepare E2E workspace | |
| uses: NVIDIA/NemoClaw/.github/actions/prepare-e2e@f6304bc25fc35bfaa441c8c2fbfee38f72805a75 | |
| with: | |
| build-cli: "false" | |
| - name: Restore exact-commit CLI artifact | |
| uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@c246409193a31133cab10c8a3589001cc0d59eb3 | |
| with: | |
| provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} | |
| - name: Initialize runner comparison telemetry | |
| if: ${{ github.repository == 'NVIDIA/NemoClaw' && github.ref == 'refs/heads/main' && inputs.checkout_sha == '' }} | |
| continue-on-error: true | |
| shell: bash | |
| run: npx tsx tools/e2e/runner-comparison.mts initialize | |
| - name: Run Hermes live Vitest test | |
| env: | |
| NVIDIA_INFERENCE_API_KEY: ${{ github.repository == 'NVIDIA/NemoClaw' && github.ref == 'refs/heads/main' && github.event_name == 'workflow_dispatch' && inputs.checkout_sha == '' && (inputs.inference_mode || 'mock') != 'mock' && secrets.NVIDIA_INFERENCE_API_KEY || '' }} | |
| run: | | |
| set -euo pipefail | |
| npx tsx tools/e2e/live-vitest-invocation.mts run --test-path test/e2e/live/hermes-e2e.test.ts | |
| - name: Finalize runner comparison telemetry | |
| if: ${{ always() && github.repository == 'NVIDIA/NemoClaw' && github.ref == 'refs/heads/main' && inputs.checkout_sha == '' }} | |
| continue-on-error: true | |
| shell: bash | |
| run: npx tsx tools/e2e/runner-comparison.mts finalize | |
| - name: Upload Hermes live Vitest artifacts | |
| if: always() | |
| uses: NVIDIA/NemoClaw/.github/actions/upload-e2e-artifacts@7768e15eb90d3ee2d33432f481dfe8747e4f6d57 | |
| - name: Clean up Docker auth | |
| if: always() | |
| shell: bash | |
| run: bash .github/scripts/docker-auth-cleanup.sh | |
| hermes-gpu-startup: | |
| needs: generate-matrix | |
| if: ${{ contains(fromJSON(needs.generate-matrix.outputs.selected_jobs), 'hermes-gpu-startup') }} | |
| runs-on: linux-amd64-gpu-rtxpro6000-latest-1 | |
| timeout-minutes: 90 | |
| strategy: | |
| fail-fast: false | |
| max-parallel: 1 | |
| matrix: | |
| include: | |
| - scenario: native | |
| sandbox_name: e2e-hgpu-native | |
| - scenario: fallback | |
| sandbox_name: e2e-hgpu-fallback | |
| - scenario: compatibility-only | |
| sandbox_name: e2e-hgpu-compat | |
| env: | |
| E2E_JOB: "1" | |
| E2E_TARGET_ID: "hermes-gpu-startup" | |
| E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts/live/hermes-gpu-startup/${{ matrix.scenario }} | |
| E2E_HERMES_GPU_STARTUP_SCENARIO: ${{ matrix.scenario }} | |
| NEMOCLAW_E2E_SHARD: ${{ matrix.scenario }} | |
| NEMOCLAW_CLI_BIN: ${{ github.workspace }}/bin/nemoclaw.js | |
| NEMOCLAW_RUN_LIVE_E2E: "1" | |
| NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1" | |
| NEMOCLAW_AGENT: hermes | |
| NEMOCLAW_NON_INTERACTIVE: "1" | |
| NEMOCLAW_RECREATE_SANDBOX: "1" | |
| NEMOCLAW_SANDBOX_GPU: "1" | |
| NEMOCLAW_SANDBOX_NAME: ${{ matrix.sandbox_name }} | |
| NEMOCLAW_ONBOARD_VALIDATION_TIMEOUT_SECONDS: "60" | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| repository: ${{ inputs.checkout_repository || github.repository }} | |
| ref: ${{ inputs.checkout_sha || github.sha }} | |
| persist-credentials: false | |
| - name: Checkout trusted Hermes GPU runtime fixture | |
| if: ${{ matrix.scenario == 'fallback' }} | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| repository: NVIDIA/NemoClaw | |
| ref: ${{ github.workflow_sha }} | |
| path: .trusted-hermes-gpu-fixture-${{ github.run_id }}-${{ github.run_attempt }} | |
| sparse-checkout: tools/e2e/hermes-gpu-docker-runtime-fixture.sh | |
| sparse-checkout-cone-mode: false | |
| persist-credentials: false | |
| - name: Install trusted Hermes GPU runtime fixture | |
| if: ${{ matrix.scenario == 'fallback' }} | |
| shell: /bin/bash --noprofile --norc -e -o pipefail {0} | |
| env: | |
| BASH_ENV: /dev/null | |
| E2E_HERMES_GPU_STARTUP_SCENARIO: ${{ matrix.scenario }} | |
| ENV: /dev/null | |
| TRUSTED_DISPATCH_SHA: ${{ github.sha }} | |
| TRUSTED_FIXTURE_SHA256: e273c4baa7fe89546d64517cf56eafec30aeda7b355971263605ab1327fade02 | |
| TRUSTED_WORKFLOW_SHA: ${{ github.workflow_sha }} | |
| run: | | |
| set -euo pipefail | |
| trusted_checkout="$GITHUB_WORKSPACE/.trusted-hermes-gpu-fixture-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" | |
| trusted_source="$trusted_checkout/tools/e2e/hermes-gpu-docker-runtime-fixture.sh" | |
| trusted_fixture="/usr/local/libexec/nemoclaw/hermes-gpu-docker-runtime-fixture.${GITHUB_RUN_ID}.${GITHUB_RUN_ATTEMPT}.${E2E_HERMES_GPU_STARTUP_SCENARIO}" | |
| trusted_state_root=/var/lib/nemoclaw-e2e | |
| run_trusted_fixture() { | |
| /usr/bin/sudo -n /usr/bin/env -i \ | |
| PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \ | |
| /bin/bash "$trusted_fixture" "$@" | |
| } | |
| [[ "$TRUSTED_WORKFLOW_SHA" =~ ^[a-f0-9]{40}$ ]] \ | |
| || { echo "Trusted workflow SHA must be an immutable commit" >&2; exit 1; } | |
| [[ "$TRUSTED_DISPATCH_SHA" = "$TRUSTED_WORKFLOW_SHA" ]] \ | |
| || { echo "Trusted fixture must match the dispatched main workflow" >&2; exit 1; } | |
| [[ "$TRUSTED_FIXTURE_SHA256" =~ ^[a-f0-9]{64}$ ]] \ | |
| || { echo "Trusted fixture SHA-256 must be pinned" >&2; exit 1; } | |
| [ "$(/usr/bin/git -C "$trusted_checkout" rev-parse HEAD)" = "$TRUSTED_WORKFLOW_SHA" ] \ | |
| || { echo "Trusted fixture checkout does not match the workflow SHA" >&2; exit 1; } | |
| [ -f "$trusted_source" ] && [ ! -L "$trusted_source" ] \ | |
| || { echo "Trusted Docker fixture must be a regular non-symlink file" >&2; exit 1; } | |
| /usr/bin/sudo /usr/bin/install -d -o root -g root -m 0755 /usr/local/libexec/nemoclaw | |
| /usr/bin/sudo /usr/bin/install -o root -g root -m 0500 \ | |
| "$trusted_source" "$trusted_fixture" | |
| [ "$(/usr/bin/sudo /usr/bin/stat -c '%a %u %g' "$trusted_fixture")" = "500 0 0" ] \ | |
| || { echo "Trusted Docker fixture ownership or mode is invalid" >&2; exit 1; } | |
| printf '%s %s\n' "$TRUSTED_FIXTURE_SHA256" "$trusted_fixture" \ | |
| | /usr/bin/sudo /usr/bin/sha256sum -c - | |
| /usr/bin/sudo /usr/bin/cmp -s "$trusted_source" "$trusted_fixture" \ | |
| || { echo "Installed Docker fixture does not match trusted workflow code" >&2; exit 1; } | |
| /usr/bin/sudo /usr/bin/install -d -o root -g root -m 0700 "$trusted_state_root" | |
| # Recover any root-owned snapshot left by a hard-cancelled earlier run before | |
| # PR-controlled build or test code executes on this persistent GPU runner. | |
| if ! /usr/bin/sudo /usr/bin/find "$trusted_state_root" -mindepth 1 -maxdepth 1 \ | |
| -type d -name 'hermes-gpu-fallback-docker-runtime.*' -print0 \ | |
| | while IFS= read -r -d '' stale_state_dir; do | |
| if ! run_trusted_fixture restore \ | |
| "$stale_state_dir" /etc/docker/daemon.json >/dev/null; then | |
| exit 1 | |
| fi | |
| done; then | |
| echo "Could not recover stale Docker fallback state" >&2 | |
| exit 1 | |
| fi | |
| - *dockerhub-auth | |
| - name: Prepare E2E workspace | |
| uses: NVIDIA/NemoClaw/.github/actions/prepare-e2e@f6304bc25fc35bfaa441c8c2fbfee38f72805a75 | |
| with: | |
| build-cli: "false" | |
| - name: Restore exact-commit CLI artifact | |
| uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@c246409193a31133cab10c8a3589001cc0d59eb3 | |
| with: | |
| provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} | |
| - name: Reassert trusted Node runtime | |
| uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| env: | |
| BASH_ENV: /dev/null | |
| E2E_HERMES_GPU_STARTUP_SCENARIO: ${{ matrix.scenario }} | |
| ENV: /dev/null | |
| NODE_OPTIONS: "" | |
| with: | |
| node-version: "22" | |
| - name: Run Hermes GPU startup live Vitest test | |
| shell: /bin/bash --noprofile --norc -e -o pipefail {0} | |
| env: | |
| BASH_ENV: /dev/null | |
| E2E_HERMES_GPU_STARTUP_SCENARIO: ${{ matrix.scenario }} | |
| ENV: /dev/null | |
| run: | | |
| set -euo pipefail | |
| if [ "$E2E_HERMES_GPU_STARTUP_SCENARIO" = fallback ]; then | |
| umask 077 | |
| daemon_json=/etc/docker/daemon.json | |
| trusted_fixture="/usr/local/libexec/nemoclaw/hermes-gpu-docker-runtime-fixture.${GITHUB_RUN_ID}.${GITHUB_RUN_ATTEMPT}.${E2E_HERMES_GPU_STARTUP_SCENARIO}" | |
| trusted_state_root=/var/lib/nemoclaw-e2e | |
| run_trusted_fixture() { | |
| /usr/bin/sudo -n /usr/bin/env -i \ | |
| PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \ | |
| /bin/bash "$trusted_fixture" "$@" | |
| } | |
| mkdir -p "$E2E_ARTIFACT_DIR" | |
| [ "$(/usr/bin/sudo /usr/bin/stat -c '%a %u %g' "$trusted_fixture")" = "500 0 0" ] \ | |
| || { echo "Trusted Docker fixture ownership or mode changed" >&2; exit 1; } | |
| /usr/bin/sudo /usr/bin/install -d -o root -g root -m 0700 "$trusted_state_root" | |
| state_dir="$(/usr/bin/sudo /usr/bin/mktemp -d \ | |
| "$trusted_state_root/hermes-gpu-fallback-docker-runtime.${GITHUB_RUN_ID}.${GITHUB_RUN_ATTEMPT}.fallback.XXXXXX")" | |
| /usr/bin/sudo /usr/bin/chown root:root "$state_dir" | |
| /usr/bin/sudo /usr/bin/chmod 0700 "$state_dir" | |
| # SOURCE_OF_TRUTH_REVIEW (Hermes GPU fallback fixture; #6110): | |
| # invalidState: a cancelled fallback test leaves the runner-global Docker default | |
| # runtime changed, contaminating later OpenShell jobs on this self-hosted runner. | |
| # sourceBoundary: daemon mutation uses only immutable root-owned workflow_sha | |
| # code; the PR test cannot replace the helper or its root-owned snapshot state. | |
| # whyNotSourceFix: production must fail closed when GPU attachment is ambiguous; | |
| # this fixture needs a real no-GPU partial container on an nvidia-default runner. | |
| # regressionTest: hermes-gpu-startup-workflow-boundary requires this same-step trap. | |
| # removalCondition: remove the daemon fixture when OpenShell can create a provably | |
| # GPU-unattached partial sandbox without consulting Docker's default runtime. | |
| restore_docker_default_runtime() { | |
| local command_status=$? | |
| local restore_status=0 | |
| local restored_runtime="" | |
| trap - EXIT INT TERM | |
| set +e | |
| restored_runtime="$(run_trusted_fixture restore "$state_dir" "$daemon_json")" | |
| restore_status=$? | |
| if [ "$restore_status" -eq 0 ] && [ -n "$restored_runtime" ]; then | |
| printf '%s\n' "$restored_runtime" \ | |
| >"$E2E_ARTIFACT_DIR/docker-default-runtime-restored.txt" \ | |
| || restore_status=1 | |
| fi | |
| if [ "$restore_status" -ne 0 ]; then | |
| exit 1 | |
| fi | |
| exit "$command_status" | |
| } | |
| # OpenShell leaves Runtime unset after the wrapper strips --gpu, so Docker | |
| # otherwise substitutes its daemon default. Force runc only within this | |
| # trap-guarded process so production can prove the partial container has no GPU. | |
| trap restore_docker_default_runtime EXIT | |
| trap 'exit 130' INT | |
| trap 'exit 143' TERM | |
| original_runtime="$(run_trusted_fixture capture "$state_dir" "$daemon_json")" | |
| printf '%s\n' "$original_runtime" \ | |
| >"$E2E_ARTIFACT_DIR/docker-default-runtime-before.txt" | |
| selected_runtime="$(run_trusted_fixture select-runc "$state_dir" "$daemon_json")" | |
| printf '%s\n' "$selected_runtime" \ | |
| >"$E2E_ARTIFACT_DIR/docker-default-runtime-during.txt" | |
| fi | |
| npx tsx tools/e2e/live-vitest-invocation.mts run --test-path test/e2e/live/hermes-gpu-startup.test.ts | |
| - name: Recover Docker daemon after Hermes GPU fallback fixture | |
| if: always() | |
| shell: /bin/bash --noprofile --norc -e -o pipefail {0} | |
| env: | |
| BASH_ENV: /dev/null | |
| E2E_HERMES_GPU_STARTUP_SCENARIO: ${{ matrix.scenario }} | |
| ENV: /dev/null | |
| run: | | |
| set -euo pipefail | |
| if [ "$E2E_HERMES_GPU_STARTUP_SCENARIO" != fallback ]; then | |
| exit 0 | |
| fi | |
| recovery_failed=0 | |
| restored_runtime="" | |
| trusted_fixture="/usr/local/libexec/nemoclaw/hermes-gpu-docker-runtime-fixture.${GITHUB_RUN_ID}.${GITHUB_RUN_ATTEMPT}.${E2E_HERMES_GPU_STARTUP_SCENARIO}" | |
| trusted_state_root=/var/lib/nemoclaw-e2e | |
| state_prefix="hermes-gpu-fallback-docker-runtime.${GITHUB_RUN_ID}.${GITHUB_RUN_ATTEMPT}.fallback." | |
| run_trusted_fixture() { | |
| /usr/bin/sudo -n /usr/bin/env -i \ | |
| PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \ | |
| /bin/bash "$trusted_fixture" "$@" | |
| } | |
| if ! /usr/bin/sudo /usr/bin/find "$trusted_state_root" -mindepth 1 -maxdepth 1 \ | |
| -type d -name "${state_prefix}*" -print0 \ | |
| | while IFS= read -r -d '' state_dir; do | |
| if ! restored_runtime="$(run_trusted_fixture restore \ | |
| "$state_dir" /etc/docker/daemon.json)"; then | |
| exit 1 | |
| fi | |
| if [ -n "$restored_runtime" ]; then | |
| printf '%s\n' "$restored_runtime" \ | |
| >"$E2E_ARTIFACT_DIR/docker-default-runtime-restored.txt" | |
| fi | |
| done; then | |
| recovery_failed=1 | |
| fi | |
| if [ "$recovery_failed" -ne 0 ]; then | |
| echo "Independent Docker daemon recovery could not prove restoration" >&2 | |
| exit 1 | |
| fi | |
| - name: Remove trusted Hermes GPU runtime fixture | |
| if: ${{ always() && matrix.scenario == 'fallback' }} | |
| shell: /bin/bash --noprofile --norc -e -o pipefail {0} | |
| env: | |
| BASH_ENV: /dev/null | |
| E2E_HERMES_GPU_STARTUP_SCENARIO: ${{ matrix.scenario }} | |
| ENV: /dev/null | |
| run: | | |
| set -euo pipefail | |
| trusted_fixture="/usr/local/libexec/nemoclaw/hermes-gpu-docker-runtime-fixture.${GITHUB_RUN_ID}.${GITHUB_RUN_ATTEMPT}.${E2E_HERMES_GPU_STARTUP_SCENARIO}" | |
| /usr/bin/sudo /usr/bin/rm -f -- "$trusted_fixture" | |
| - name: Upload Hermes GPU startup artifacts | |
| if: always() | |
| uses: NVIDIA/NemoClaw/.github/actions/upload-e2e-artifacts@7768e15eb90d3ee2d33432f481dfe8747e4f6d57 | |
| with: | |
| name: e2e-hermes-gpu-startup-${{ matrix.scenario }} | |
| path: e2e-artifacts/live/hermes-gpu-startup/${{ matrix.scenario }}/ | |
| - name: Clean up Docker auth | |
| if: always() | |
| shell: bash | |
| run: bash .github/scripts/docker-auth-cleanup.sh | |
| jetson-nvmap-gpu: | |
| needs: generate-matrix | |
| if: ${{ github.repository == 'NVIDIA/NemoClaw' && github.ref == 'refs/heads/main' && (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.allow_jetson_dispatch && (inputs.checkout_repository == '' || inputs.checkout_repository == github.repository) && ((inputs.jobs == '' && inputs.targets == '') || contains(fromJSON(needs.generate-matrix.outputs.selected_jobs), 'jetson-nvmap-gpu')))) }} | |
| concurrency: | |
| group: jetson-nvmap-gpu-dispatch | |
| queue: max | |
| cancel-in-progress: false | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| permissions: | |
| contents: read | |
| id-token: write | |
| steps: | |
| - name: Check out trusted Jetson controller | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| repository: NVIDIA/NemoClaw | |
| ref: ${{ github.workflow_sha }} | |
| persist-credentials: false | |
| - name: Set up Node for Jetson controller | |
| uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: 22 | |
| - name: Dispatch exact commit to Jetson through operator backend | |
| env: | |
| E2E_ARTIFACT_DIR: ${{ runner.temp }}/e2e-artifacts/live/jetson-nvmap-gpu | |
| JETSON_DISPATCH_CANDIDATE_SHA: ${{ inputs.checkout_sha || github.sha }} | |
| JETSON_DISPATCH_URL: ${{ vars.JETSON_DISPATCH_URL }} | |
| run: node --experimental-strip-types --no-warnings tools/e2e/jetson-dispatch-client.mts | |
| - name: Upload Jetson nvmap GPU artifacts | |
| if: always() | |
| uses: NVIDIA/NemoClaw/.github/actions/upload-e2e-artifacts@7768e15eb90d3ee2d33432f481dfe8747e4f6d57 | |
| with: | |
| name: e2e-jetson-nvmap-gpu | |
| path: ${{ runner.temp }}/e2e-artifacts/live/jetson-nvmap-gpu/ | |
| cloud-onboard: | |
| needs: generate-matrix | |
| if: ${{ contains(fromJSON(needs.generate-matrix.outputs.selected_jobs), 'cloud-onboard') }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 70 | |
| env: | |
| E2E_JOB: "1" | |
| E2E_TARGET_ID: "cloud-onboard" | |
| E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts/live/cloud-onboard | |
| NEMOCLAW_CLI_BIN: ${{ github.workspace }}/bin/nemoclaw.js | |
| NEMOCLAW_RUN_LIVE_E2E: "1" | |
| NEMOCLAW_E2E_USE_HOSTED_INFERENCE: "1" | |
| NEMOCLAW_NON_INTERACTIVE: "1" | |
| NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1" | |
| NEMOCLAW_SANDBOX_NAME: "e2e-cloud-onboard" | |
| NEMOCLAW_PUBLIC_INSTALL_REF: "${{ inputs.checkout_sha || github.sha }}" | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| repository: ${{ inputs.checkout_repository || github.repository }} | |
| ref: ${{ inputs.checkout_sha || github.sha }} | |
| persist-credentials: false | |
| - *dockerhub-auth | |
| - name: Configure cloud-onboard trace directory | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| printf 'NEMOCLAW_TRACE_DIR=%s\n' "${RUNNER_TEMP}/nemoclaw-cloud-onboard-traces" >> "${GITHUB_ENV}" | |
| # invalidState: the cloud-onboard DCode TUI check requires a PTY driver, | |
| # but the fixed GitHub-hosted runner image does not provide expect. | |
| # sourceBoundary: privileged host setup runs from the first-party | |
| # host-dependency-setup action pinned to an immutable full SHA, never the | |
| # repository target ref; the check only consumes expect without privilege. | |
| # whyNotSourceFix: GitHub-hosted jobs cannot use a repository-owned host | |
| # image, and caching privileged dpkg state between clean runners is not | |
| # supported. | |
| # regressionTest: workflow-boundary tests pin the ordering, action | |
| # provenance, and package mapping. | |
| # removalCondition: remove when the hosted runner supplies expect or the | |
| # cloud-onboard acceptance check no longer requires a PTY. | |
| - name: Install cloud-onboard DCode TUI host dependencies | |
| uses: NVIDIA/NemoClaw/.github/actions/host-dependency-setup@4def1501b34ce586f83b91af50a66b5d22b31d75 | |
| with: | |
| packages: expect | |
| - name: Prepare E2E workspace | |
| uses: NVIDIA/NemoClaw/.github/actions/prepare-e2e@f6304bc25fc35bfaa441c8c2fbfee38f72805a75 | |
| with: | |
| build-cli: "false" | |
| - name: Restore exact-commit CLI artifact | |
| uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@c246409193a31133cab10c8a3589001cc0d59eb3 | |
| with: | |
| provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} | |
| - name: Install OpenShell CLI | |
| run: bash scripts/install-openshell.sh | |
| - 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" | |
| if command -v openshell >/dev/null 2>&1; then | |
| OPENSHELL_BIN="$(command -v openshell)" | |
| elif [ -x "$HOME/.local/bin/openshell" ]; then | |
| OPENSHELL_BIN="$HOME/.local/bin/openshell" | |
| else | |
| echo "::error::OpenShell CLI not found after install" | |
| exit 1 | |
| fi | |
| export OPENSHELL_BIN | |
| "$OPENSHELL_BIN" --version | |
| npx tsx tools/e2e/live-vitest-invocation.mts run --test-path test/e2e/live/cloud-onboard.test.ts | |
| - name: Build trusted cloud-onboard timing summary | |
| if: always() | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| expected_trace_dir="${RUNNER_TEMP}/nemoclaw-cloud-onboard-traces" | |
| if [ -z "${RUNNER_TEMP}" ] || [ "${NEMOCLAW_TRACE_DIR}" != "${expected_trace_dir}" ]; then | |
| echo "::error::Refusing to sanitize unexpected raw trace path" >&2 | |
| exit 1 | |
| fi | |
| python3 scripts/e2e/sanitize-trace-timing.py \ | |
| "${NEMOCLAW_TRACE_DIR}" \ | |
| "${E2E_ARTIFACT_DIR}" | |
| # The target process must emit full local traces for diagnosis, but those | |
| # traces may contain prompts, environment data, and credential material. | |
| # Keep cleanup as a separate always() step so a sanitizer failure cannot | |
| # bypass it. A runner-level termination is contained by the ephemeral | |
| # GitHub-hosted runner.temp boundary. Remove this step only when the trace | |
| # producer itself emits the allowlisted timing-only schema. | |
| - name: Delete raw cloud-onboard traces | |
| if: always() | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| expected_trace_dir="${RUNNER_TEMP}/nemoclaw-cloud-onboard-traces" | |
| if [ -z "${RUNNER_TEMP}" ] || [ "${NEMOCLAW_TRACE_DIR}" != "${expected_trace_dir}" ]; then | |
| echo "::error::Refusing to delete unexpected raw trace path" >&2 | |
| exit 1 | |
| fi | |
| rm -rf -- "${NEMOCLAW_TRACE_DIR}" | |
| - name: Upload cloud-onboard artifacts | |
| if: always() | |
| uses: NVIDIA/NemoClaw/.github/actions/upload-e2e-artifacts@7768e15eb90d3ee2d33432f481dfe8747e4f6d57 | |
| - name: Clean up Docker auth | |
| if: always() | |
| shell: bash | |
| run: bash .github/scripts/docker-auth-cleanup.sh | |
| messaging-providers: | |
| needs: generate-matrix | |
| if: ${{ contains(fromJSON(needs.generate-matrix.outputs.selected_jobs), 'messaging-providers') }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 90 | |
| env: | |
| E2E_JOB: "1" | |
| E2E_TARGET_ID: "messaging-providers" | |
| E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts/live/messaging-providers | |
| NEMOCLAW_CLI_BIN: ${{ github.workspace }}/bin/nemoclaw.js | |
| NEMOCLAW_RUN_LIVE_E2E: "1" | |
| NEMOCLAW_E2E_USE_HOSTED_INFERENCE: "1" | |
| NEMOCLAW_NON_INTERACTIVE: "1" | |
| NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1" | |
| OPENSHELL_GATEWAY: "nemoclaw" | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| repository: ${{ inputs.checkout_repository || github.repository }} | |
| ref: ${{ inputs.checkout_sha || github.sha }} | |
| persist-credentials: false | |
| - *dockerhub-auth | |
| - name: Prepare E2E workspace | |
| uses: NVIDIA/NemoClaw/.github/actions/prepare-e2e@f6304bc25fc35bfaa441c8c2fbfee38f72805a75 | |
| with: | |
| build-cli: "false" | |
| - name: Restore exact-commit CLI artifact | |
| uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@c246409193a31133cab10c8a3589001cc0d59eb3 | |
| with: | |
| provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} | |
| - name: Run messaging providers live Vitest test | |
| # The test keeps | |
| # the legacy fake-token defaults, optional _REAL secret overrides, | |
| # provider placeholder redaction checks, and WhatsApp QR-only parity. | |
| env: | |
| NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_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 }} | |
| SLACK_APP_TOKEN_REAL: ${{ secrets.SLACK_APP_TOKEN_REAL }} | |
| TELEGRAM_ALLOWED_IDS: ${{ secrets.TELEGRAM_ALLOWED_IDS }} | |
| TELEGRAM_AUTHORIZED_CHAT_IDS: ${{ secrets.TELEGRAM_AUTHORIZED_CHAT_IDS }} | |
| TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }} | |
| TELEGRAM_CHAT_ID_E2E: ${{ secrets.TELEGRAM_CHAT_ID_E2E }} | |
| DISCORD_CHANNEL_ID_E2E: ${{ secrets.DISCORD_CHANNEL_ID_E2E }} | |
| SLACK_CHANNEL_ID_E2E: ${{ secrets.SLACK_CHANNEL_ID_E2E }} | |
| run: | | |
| set -euo pipefail | |
| npx tsx tools/e2e/live-vitest-invocation.mts run --test-path test/e2e/live/messaging-providers.test.ts | |
| - name: Upload messaging providers artifacts | |
| if: always() | |
| uses: NVIDIA/NemoClaw/.github/actions/upload-e2e-artifacts@7768e15eb90d3ee2d33432f481dfe8747e4f6d57 | |
| - name: Clean up Docker auth | |
| if: always() | |
| shell: bash | |
| run: bash .github/scripts/docker-auth-cleanup.sh | |
| openclaw-plugin-runtime-exdev-release: | |
| needs: generate-matrix | |
| if: ${{ contains(fromJSON(needs.generate-matrix.outputs.selected_jobs), 'openclaw-plugin-runtime-exdev-release') }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| timeout-minutes: 55 | |
| env: | |
| E2E_JOB: "1" | |
| E2E_TARGET_ID: "openclaw-plugin-runtime-exdev-release" | |
| E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts/live/openclaw-plugin-runtime-exdev-release | |
| NEMOCLAW_CLI_BIN: ${{ github.workspace }}/bin/nemoclaw.js | |
| NEMOCLAW_RUN_LIVE_E2E: "1" | |
| NEMOCLAW_NON_INTERACTIVE: "1" | |
| NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1" | |
| NEMOCLAW_SANDBOX_NAME: "e2e-oc-exdev-rel" | |
| OPENSHELL_GATEWAY: "nemoclaw" | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| repository: ${{ inputs.checkout_repository || github.repository }} | |
| ref: ${{ inputs.checkout_sha || github.sha }} | |
| persist-credentials: false | |
| - *dockerhub-auth | |
| - name: Pre-pull release-matched Docker Hub builder image | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| docker pull node:22-trixie-slim@sha256:2d9f5c76c8f4dd36e8f253bee5d828a83a6c09f36188f0b0414325232e0b175d | |
| - name: Remove Docker auth before release-pinned fixture | |
| if: always() | |
| uses: NVIDIA/NemoClaw/.github/actions/docker-auth-cleanup@d5f37099766ca82a4516e7d8f0de117cda197fe3 | |
| - name: Prepare E2E workspace | |
| uses: NVIDIA/NemoClaw/.github/actions/prepare-e2e@f6304bc25fc35bfaa441c8c2fbfee38f72805a75 | |
| with: | |
| build-cli: "false" | |
| - name: Restore exact-commit CLI artifact | |
| uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@c246409193a31133cab10c8a3589001cc0d59eb3 | |
| with: | |
| provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} | |
| - name: Run OpenClaw custom-plugin release baseline live test | |
| run: | | |
| set -euo pipefail | |
| test -n "${DOCKER_CONFIG:-}" | |
| test ! -e "${DOCKER_CONFIG}" | |
| test -z "${DOCKERHUB_USERNAME:-}" | |
| test -z "${DOCKERHUB_TOKEN:-}" | |
| env -u DOCKER_CONFIG -u DOCKERHUB_USERNAME -u DOCKERHUB_TOKEN \ | |
| npx tsx tools/e2e/live-vitest-invocation.mts run \ | |
| --test-path test/e2e/live/openclaw-plugin-runtime-exdev.test.ts \ | |
| --selector release-baseline | |
| - name: Upload OpenClaw plugin release baseline artifacts | |
| if: always() | |
| uses: NVIDIA/NemoClaw/.github/actions/upload-e2e-artifacts@7768e15eb90d3ee2d33432f481dfe8747e4f6d57 | |
| - name: Clean up Docker auth | |
| if: always() | |
| shell: bash | |
| run: bash .github/scripts/docker-auth-cleanup.sh | |
| # Main-push current-lifecycle coverage for #6108 / #3513 / #3127. This | |
| # proves restart/rebuild persistence and target-side runtime-dependency | |
| # replacement across devices without waiting for the release baseline. | |
| openclaw-plugin-runtime-exdev: | |
| needs: generate-matrix | |
| if: ${{ contains(fromJSON(needs.generate-matrix.outputs.selected_jobs), 'openclaw-plugin-runtime-exdev') }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| # Two bounded 25-minute onboards plus the 20-minute rebuild and 15-minute | |
| # Vitest buffer need 85 minutes; allow 20 more for setup and teardown. | |
| timeout-minutes: 105 | |
| env: | |
| E2E_JOB: "1" | |
| E2E_TARGET_ID: "openclaw-plugin-runtime-exdev" | |
| E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts/live/openclaw-plugin-runtime-exdev | |
| NEMOCLAW_CLI_BIN: ${{ github.workspace }}/bin/nemoclaw.js | |
| NEMOCLAW_RUN_LIVE_E2E: "1" | |
| NEMOCLAW_NON_INTERACTIVE: "1" | |
| NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1" | |
| NEMOCLAW_SANDBOX_NAME: "e2e-oc-exdev" | |
| OPENSHELL_GATEWAY: "nemoclaw" | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| repository: ${{ inputs.checkout_repository || github.repository }} | |
| ref: ${{ inputs.checkout_sha || github.sha }} | |
| persist-credentials: false | |
| - *dockerhub-auth | |
| - name: Pre-pull release-matched Docker Hub builder image | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| docker pull node:22-trixie-slim@sha256:db8a96a63e5264607ada2d206758876ebbed6a12be2ada7517793cbfb0c2a29c | |
| - name: Remove Docker auth before release-pinned fixture | |
| if: always() | |
| uses: NVIDIA/NemoClaw/.github/actions/docker-auth-cleanup@d5f37099766ca82a4516e7d8f0de117cda197fe3 | |
| - name: Prepare E2E workspace | |
| uses: NVIDIA/NemoClaw/.github/actions/prepare-e2e@f6304bc25fc35bfaa441c8c2fbfee38f72805a75 | |
| with: | |
| build-cli: "false" | |
| - name: Restore exact-commit CLI artifact | |
| uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@c246409193a31133cab10c8a3589001cc0d59eb3 | |
| with: | |
| provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} | |
| - name: Run OpenClaw custom-plugin lifecycle and runtime-deps EXDEV live test | |
| run: | | |
| set -euo pipefail | |
| test -n "${DOCKER_CONFIG:-}" | |
| test ! -e "${DOCKER_CONFIG}" | |
| test -z "${DOCKERHUB_USERNAME:-}" | |
| test -z "${DOCKERHUB_TOKEN:-}" | |
| env -u DOCKER_CONFIG -u DOCKERHUB_USERNAME -u DOCKERHUB_TOKEN \ | |
| npx tsx tools/e2e/live-vitest-invocation.mts run \ | |
| --test-path test/e2e/live/openclaw-plugin-runtime-exdev.test.ts \ | |
| --selector current-lifecycle | |
| - name: Upload OpenClaw plugin runtime-deps EXDEV artifacts | |
| if: always() | |
| uses: NVIDIA/NemoClaw/.github/actions/upload-e2e-artifacts@7768e15eb90d3ee2d33432f481dfe8747e4f6d57 | |
| - name: Clean up Docker auth | |
| if: always() | |
| shell: bash | |
| run: bash .github/scripts/docker-auth-cleanup.sh | |
| report-to-pr: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| # This entire workflow is dispatch-only. Keeping selective jobs in `needs` | |
| # makes the report wait for and record any requested job without adding | |
| # skipped checks to the normal pull_request workflow. | |
| needs: &e2e-result-jobs | |
| [ | |
| base-image-publication, | |
| generate-matrix, | |
| retired-selector-compatibility, | |
| staging-brev-launchable, | |
| live, | |
| shared-e2e, | |
| catalogue-standard, | |
| catalogue-nvidia-api, | |
| catalogue-nvidia-inference, | |
| catalogue-github-read, | |
| catalogue-brave-nvidia-inference, | |
| openshell-gateway-auth-contract, | |
| mcp-bridge, | |
| openshell-credential-generation-window, | |
| openshell-dev-artifact, | |
| mcp-bridge-dev, | |
| managed-image-multiarch-startup, | |
| llama-cpp-dgx-spark-plan, | |
| llama-cpp-dgx-spark-qualification, | |
| managed-image-protected-runtime, | |
| hermes-e2e, | |
| hermes-gpu-startup, | |
| messaging-providers, | |
| jetson-nvmap-gpu, | |
| cloud-onboard, | |
| openclaw-plugin-runtime-exdev-release, | |
| openclaw-plugin-runtime-exdev, | |
| native-runtime-qualification-producer, | |
| native-runtime-qualification-producer-aggregate, | |
| ] | |
| if: ${{ always() && github.event_name == 'workflow_dispatch' && inputs.checkout_sha == '' }} | |
| permissions: | |
| actions: read | |
| contents: read | |
| # The issue-comment endpoint accepts pull request write permission for PR comments. | |
| # Keep issues: write absent so this job cannot restore general issue routing. | |
| pull-requests: write | |
| steps: | |
| - name: Check out the trusted E2E reporting helper | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ github.workflow_sha }} | |
| persist-credentials: false | |
| sparse-checkout: | | |
| tools/e2e/report-e2e-results.mts | |
| tools/e2e/selector-aliases.mts | |
| sparse-checkout-cone-mode: false | |
| - name: Post E2E target results to PR | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| env: | |
| EXPLICIT_ONLY_JOBS: ${{ needs.generate-matrix.outputs.explicit_only_jobs }} | |
| TEST_MATRIX: ${{ needs.generate-matrix.outputs.test_matrix }} | |
| JOB_PR_NUMBER: ${{ inputs.pr_number }} | |
| JOB_TARGETS: ${{ inputs.targets }} | |
| JOBS: ${{ inputs.jobs }} | |
| NEEDS_JSON: ${{ toJSON(needs) }} | |
| with: | |
| script: | | |
| const path = require('node:path'); | |
| const { pathToFileURL } = require('node:url'); | |
| const { resolveReportPr, loadReportJobs, renderE2eReport } = await import( | |
| pathToFileURL(path.join(process.env.GITHUB_WORKSPACE, 'tools/e2e/report-e2e-results.mts')).href | |
| ); | |
| const needs = JSON.parse(process.env.NEEDS_JSON || '{}'); | |
| const prNumber = await resolveReportPr({ github, context, core, env: process.env }); | |
| if (prNumber === undefined) return; | |
| const { apiJobs, loaded } = await loadReportJobs({ github, context, core }); | |
| const report = renderE2eReport({ | |
| needs, | |
| env: process.env, | |
| apiJobs, | |
| apiJobsLoaded: loaded, | |
| context, | |
| }); | |
| if (report.fatal) { | |
| core.setFailed(report.fatal); | |
| return; | |
| } | |
| for (const warning of report.warnings) core.warning(warning); | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| body: report.body, | |
| }); | |
| relevant-e2e: | |
| name: Relevant E2E | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| needs: *e2e-result-jobs | |
| if: ${{ always() && github.repository == 'NVIDIA/NemoClaw' && github.ref == 'refs/heads/main' && github.event_name == 'push' }} | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Check out the E2E result evaluator | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ github.workflow_sha }} | |
| persist-credentials: false | |
| sparse-checkout: tools/e2e/release-qualification.mts | |
| sparse-checkout-cone-mode: false | |
| - name: Require every selected E2E result | |
| env: | |
| NEEDS_JSON: ${{ toJSON(needs) }} | |
| RELEASE_REQUIRED_JOBS: ${{ needs.generate-matrix.outputs.selected_workflow_jobs }} | |
| run: node --experimental-strip-types --no-warnings tools/e2e/release-qualification.mts | |
| - name: Summarize selected E2E | |
| env: | |
| SELECTED_WORKFLOW_JOBS: ${{ needs.generate-matrix.outputs.selected_workflow_jobs }} | |
| run: | | |
| set -euo pipefail | |
| if [ "${SELECTED_WORKFLOW_JOBS}" = "[]" ]; then | |
| echo "No E2E target owns a file changed by this push." >> "${GITHUB_STEP_SUMMARY}" | |
| else | |
| { | |
| echo "The selected E2E workflow jobs passed:" | |
| echo | |
| jq -r '.[] | "- `\(.)`"' <<< "${SELECTED_WORKFLOW_JOBS}" | |
| } >> "${GITHUB_STEP_SUMMARY}" | |
| fi | |
| release-qualification: | |
| name: Release qualification | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| needs: *e2e-result-jobs | |
| if: ${{ always() && github.repository == 'NVIDIA/NemoClaw' && github.ref == 'refs/heads/main' && github.event_name == 'workflow_dispatch' && inputs.checkout_sha == '' && inputs.jobs == '' && inputs.targets == '' && inputs.include_staging_brev_launchable && !inputs.allow_jetson_dispatch && !inputs.allow_dgx_spark_runner_queue }} | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Check out the qualification evaluator | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ github.workflow_sha }} | |
| persist-credentials: false | |
| sparse-checkout: tools/e2e/release-qualification.mts | |
| sparse-checkout-cone-mode: false | |
| - name: Require every release E2E result | |
| env: | |
| NEEDS_JSON: ${{ toJSON(needs) }} | |
| RELEASE_QUALIFICATION_WAIVED_JOBS: ${{ needs.generate-matrix.outputs.release_qualification_waived_jobs }} | |
| RELEASE_REQUIRED_JOBS: ${{ needs.generate-matrix.outputs.release_required_jobs }} | |
| run: node --experimental-strip-types --no-warnings tools/e2e/release-qualification.mts | |
| - name: Record release qualification waiver | |
| if: ${{ inputs.release_qualification_waived_jobs != '' }} | |
| env: | |
| ACTOR: ${{ github.actor }} | |
| CANDIDATE_SHA: ${{ github.sha }} | |
| NEEDS_JSON: ${{ toJSON(needs) }} | |
| RUN_ATTEMPT: ${{ github.run_attempt }} | |
| RUN_ID: ${{ github.run_id }} | |
| TRIGGERING_ACTOR: ${{ github.triggering_actor }} | |
| WAIVED_JOBS: ${{ needs.generate-matrix.outputs.release_qualification_waived_jobs }} | |
| WAIVER_REASON: ${{ inputs.release_qualification_waiver_reason }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| evidence_dir="${RUNNER_TEMP}/release-qualification-waiver" | |
| install -d -m 0700 "$evidence_dir" | |
| jq -n \ | |
| --arg actor "$ACTOR" \ | |
| --arg candidateSha "$CANDIDATE_SHA" \ | |
| --arg reason "$WAIVER_REASON" \ | |
| --arg triggeringActor "$TRIGGERING_ACTOR" \ | |
| --argjson needs "$NEEDS_JSON" \ | |
| --argjson waived "$WAIVED_JOBS" \ | |
| --argjson workflowRunAttempt "$RUN_ATTEMPT" \ | |
| --argjson workflowRunId "$RUN_ID" \ | |
| '{ | |
| schemaVersion: 1, | |
| kind: "nemoclaw-release-qualification-waiver-v1", | |
| candidateSha: $candidateSha, | |
| workflowRunId: $workflowRunId, | |
| workflowRunAttempt: $workflowRunAttempt, | |
| actor: $actor, | |
| triggeringActor: $triggeringActor, | |
| reason: $reason, | |
| jobs: [$waived[] as $job | {id: $job, result: $needs[$job].result}] | |
| }' >"$evidence_dir/waiver.json" | |
| { | |
| echo "## Release qualification waiver" | |
| echo | |
| echo "A repository administrator waived release-required E2E jobs." | |
| echo | |
| printf -- '- Candidate commit: `%s`\n' "$CANDIDATE_SHA" | |
| printf -- '- Dispatch actor: `%s`\n' "$ACTOR" | |
| printf -- '- Triggering actor: `%s`\n' "$TRIGGERING_ACTOR" | |
| printf -- '- Reason: %s\n' "$WAIVER_REASON" | |
| echo '- Waived jobs:' | |
| jq -r --argjson waived "$WAIVED_JOBS" ' | |
| . as $needs | | |
| $waived[] | " - `\(.)`: `\($needs[.].result // \"missing\")`" | |
| ' <<< "$NEEDS_JSON" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Upload release qualification waiver evidence | |
| if: ${{ inputs.release_qualification_waived_jobs != '' }} | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: release-qualification-waiver-${{ github.run_id }}-${{ github.run_attempt }} | |
| path: ${{ runner.temp }}/release-qualification-waiver/waiver.json | |
| if-no-files-found: error | |
| retention-days: 30 | |
| # ── Push/manual scorecard ───────────────────────────────────────────────── | |
| scorecard: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| needs: *e2e-result-jobs | |
| if: ${{ always() && (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.checkout_sha == '')) }} | |
| permissions: | |
| actions: read | |
| contents: read | |
| steps: | |
| - name: Checkout scorecard builders | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| repository: ${{ inputs.checkout_repository || github.repository }} | |
| ref: ${{ inputs.checkout_sha || github.sha }} | |
| persist-credentials: false | |
| sparse-checkout: | | |
| ci/onboard-performance-budget.json | |
| scripts/audit-test-runtime.mts | |
| scripts/scorecard | |
| sparse-checkout-cone-mode: false | |
| - name: Download E2E progress artifacts | |
| continue-on-error: true | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| path: ${{ runner.temp }}/e2e-runtime-audit | |
| pattern: e2e-* | |
| - name: Generate E2E scorecard | |
| id: scorecard | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| env: | |
| EXPLICIT_ONLY_JOBS: ${{ needs.generate-matrix.outputs.explicit_only_jobs }} | |
| JOBS: ${{ inputs.jobs }} | |
| RUNTIME_ARTIFACTS: ${{ runner.temp }}/e2e-runtime-audit | |
| RUNTIME_SUMMARY_FILE: ${{ runner.temp }}/e2e-runtime-summary.json | |
| TARGETS: ${{ inputs.targets }} | |
| NEEDS_JSON: ${{ toJSON(needs) }} | |
| with: | |
| script: | | |
| const path = require('path'); | |
| const { buildScorecard } = require( | |
| path.join(process.env.GITHUB_WORKSPACE, 'scripts/scorecard/coordinate-scorecard.mts'), | |
| ); | |
| const scorecardJobs = require( | |
| path.join(process.env.GITHUB_WORKSPACE, 'scripts/scorecard/summarize-jobs.mts'), | |
| ); | |
| const traceTiming = require( | |
| path.join(process.env.GITHUB_WORKSPACE, 'scripts/scorecard/analyze-trace-timing.mts'), | |
| ); | |
| const runtimeAudit = require( | |
| path.join(process.env.GITHUB_WORKSPACE, 'scripts/audit-test-runtime.mts'), | |
| ); | |
| const runtimeHistory = require( | |
| path.join(process.env.GITHUB_WORKSPACE, 'scripts/scorecard/analyze-runtime-history.mts'), | |
| ); | |
| const firstTurnLatency = require( | |
| path.join(process.env.GITHUB_WORKSPACE, 'scripts/scorecard/analyze-first-turn-latency.mts'), | |
| ); | |
| const needs = JSON.parse(process.env.NEEDS_JSON || '{}'); | |
| // GitHub's jobs API is the canonical source because `needs.live` | |
| // collapses every matrix target into one result and has no job URL. | |
| // The typed helper owns and tests the degraded `needs` fallback. | |
| const apiJobs = await scorecardJobs.loadWorkflowRunJobs({ github, context, core }); | |
| let runtimeSummaryMarkdown; | |
| let runtimeHistoryRows = null; | |
| try { | |
| const runtimeRows = runtimeAudit.auditTestRuntime([process.env.RUNTIME_ARTIFACTS]); | |
| runtimeSummaryMarkdown = runtimeAudit.formatRuntimeAuditSummary(runtimeRows); | |
| runtimeHistoryRows = runtimeAudit.collectRuntimeHistorySamples([ | |
| process.env.RUNTIME_ARTIFACTS, | |
| ]); | |
| } catch { | |
| core.warning('E2E test phase runtime summary unavailable: invalid progress artifact'); | |
| runtimeSummaryMarkdown = [ | |
| '## E2E Test Phase Runtime', | |
| '', | |
| 'The summary is unavailable because a `test-progress.json` artifact was invalid.', | |
| '', | |
| ].join('\n'); | |
| } | |
| const runtimeHistoryMarkdown = runtimeHistoryRows === null | |
| ? [ | |
| '## E2E Push Runtime Trend', | |
| '', | |
| 'The trend is unavailable because a `test-progress.json` artifact was invalid.', | |
| '', | |
| ].join('\n') | |
| : await runtimeHistory.buildRuntimeHistory( | |
| { github, context, core }, | |
| runtimeHistoryRows, | |
| process.env.RUNTIME_SUMMARY_FILE, | |
| { | |
| currentFirstTurnLatency: | |
| firstTurnLatency.readCurrentFirstTurnLatencySample( | |
| process.env.RUNTIME_ARTIFACTS, | |
| ), | |
| loadPriorPushSummaries: runtimeHistory.loadPriorPushSummaries, | |
| }, | |
| ); | |
| const trace = await traceTiming.buildTraceTimingResult({ github, context, core }); | |
| if (trace.budgetWarningMessage) core.warning(trace.budgetWarningMessage); | |
| const { summaryMarkdown, scorecardData, slackData } = buildScorecard({ | |
| eventName: context.eventName, | |
| actor: context.actor || '', | |
| serverUrl: context.serverUrl, | |
| repo: context.repo, | |
| runId: context.runId, | |
| rawJobs: process.env.JOBS || '', | |
| rawTargets: process.env.TARGETS || '', | |
| rawExplicitOnly: process.env.EXPLICIT_ONLY_JOBS || '', | |
| needs, | |
| apiJobs, | |
| trace, | |
| today: new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric' }), | |
| }); | |
| await core.summary | |
| .addRaw(`${summaryMarkdown}\n\n${runtimeSummaryMarkdown}\n${runtimeHistoryMarkdown}`) | |
| .write(); | |
| core.setOutput('scorecardData', JSON.stringify(scorecardData)); | |
| core.setOutput('slackData', JSON.stringify(slackData)); | |
| - name: Post scorecard to Slack | |
| # Webhook secrets never enter branch-dispatched runs. The payload is | |
| # computed in the preceding no-secret step; this fixed publisher does | |
| # not load code from the checked-out workflow ref. | |
| if: ${{ steps.scorecard.outputs.slackData != '' && github.ref == 'refs/heads/main' }} | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| env: | |
| SLACK_WEBHOOK_URL_DAILY: ${{ secrets.SLACK_WEBHOOK_URL_DAILY }} | |
| SLACK_WEBHOOK_URL_FULLRUN: ${{ secrets.SLACK_WEBHOOK_URL_FULLRUN }} | |
| SLACK_WEBHOOK_URL_PREVIEW: ${{ secrets.SLACK_WEBHOOK_URL_PREVIEW }} | |
| SLACK_DATA: ${{ steps.scorecard.outputs.slackData }} | |
| POST_TO_SLACK: ${{ inputs.post_to_slack }} | |
| with: | |
| script: | | |
| const data = JSON.parse(process.env.SLACK_DATA); | |
| const envByChannel = { | |
| daily: 'SLACK_WEBHOOK_URL_DAILY', | |
| fullrun: 'SLACK_WEBHOOK_URL_FULLRUN', | |
| preview: 'SLACK_WEBHOOK_URL_PREVIEW', | |
| }; | |
| const channel = data?.channel; | |
| const payload = data?.payload; | |
| const attachments = payload?.attachments; | |
| const validPayload = | |
| Object.hasOwn(envByChannel, channel) && | |
| payload !== null && | |
| typeof payload === 'object' && | |
| typeof payload.text === 'string' && | |
| Array.isArray(attachments) && | |
| attachments.every( | |
| (attachment) => | |
| attachment !== null && | |
| typeof attachment === 'object' && | |
| typeof attachment.color === 'string' && | |
| Array.isArray(attachment.blocks), | |
| ); | |
| if (!validPayload) { | |
| core.setFailed('Invalid precomputed Slack payload'); | |
| return; | |
| } | |
| if (channel === 'preview' && process.env.POST_TO_SLACK !== 'true') { | |
| core.info('Selective dispatch without post_to_slack — skipping'); | |
| return; | |
| } | |
| const webhookUrl = process.env[envByChannel[channel]]; | |
| if (!webhookUrl) { | |
| core.info(`Slack webhook for "${channel}" not configured — skipping`); | |
| return; | |
| } | |
| const response = await fetch(webhookUrl, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(data.payload), | |
| }); | |
| if (!response.ok) { | |
| core.setFailed(`Slack webhook returned ${response.status}`); | |
| } | |
| - name: Upload E2E runtime summary | |
| if: ${{ always() && github.event_name == 'push' }} | |
| uses: NVIDIA/NemoClaw/.github/actions/upload-e2e-artifacts@7768e15eb90d3ee2d33432f481dfe8747e4f6d57 | |
| with: | |
| name: e2e-runtime-summary | |
| path: ${{ runner.temp }}/e2e-runtime-summary.json |