diff --git a/.github/actions/restore-e2e-cli-artifact/action.yaml b/.github/actions/restore-e2e-cli-artifact/action.yaml new file mode 100644 index 00000000000..d3e7784e9bc --- /dev/null +++ b/.github/actions/restore-e2e-cli-artifact/action.yaml @@ -0,0 +1,174 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +name: restore-e2e-cli-artifact +description: Verify and restore the exact-commit CLI artifact for an E2E job. + +inputs: + provenance-json: + description: Exact producer artifact, candidate, and workflow provenance. + required: true + +runs: + using: composite + steps: + - name: Validate exact-commit CLI artifact identity + id: identity + env: + CALLER_WORKFLOW_SHA: ${{ github.workflow_sha }} + PROVENANCE_JSON: ${{ inputs.provenance-json }} + shell: bash + run: | + set -euo pipefail + jq -e ' + type == "object" and + (keys | sort) == [ + "artifactDigest", + "artifactId", + "artifactName", + "candidateRepository", + "candidateSha", + "kind", + "payloadSha256", + "runAttempt", + "runId", + "workflowSha" + ] and + .kind == "nemoclaw-e2e-cli-provenance-v1" and + (.artifactId | strings | test("^[1-9][0-9]*$")) and + (.artifactDigest | strings | test("^[a-f0-9]{64}$")) and + (.artifactName | strings) and + (.candidateRepository | strings | test("^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$")) and + (.candidateSha | strings | test("^[a-f0-9]{40}$")) and + (.payloadSha256 | strings | test("^[a-f0-9]{64}$")) and + (.workflowSha | strings | test("^[a-f0-9]{40}$")) and + (.runId | strings | test("^[1-9][0-9]*$")) and + (.runAttempt | strings | test("^[1-9][0-9]*$")) and + .artifactName == ("nemoclaw-cli-" + .candidateSha + "-" + .payloadSha256) + ' <<<"$PROVENANCE_JSON" >/dev/null || + { echo "::error::producer CLI artifact provenance is invalid"; exit 1; } + + artifact_id="$(jq -r '.artifactId' <<<"$PROVENANCE_JSON")" + candidate_repository="$(jq -r '.candidateRepository' <<<"$PROVENANCE_JSON")" + candidate_sha="$(jq -r '.candidateSha' <<<"$PROVENANCE_JSON")" + run_attempt="$(jq -r '.runAttempt' <<<"$PROVENANCE_JSON")" + run_id="$(jq -r '.runId' <<<"$PROVENANCE_JSON")" + workflow_sha="$(jq -r '.workflowSha' <<<"$PROVENANCE_JSON")" + [[ "$(git rev-parse --verify HEAD)" == "$candidate_sha" ]] || + { echo "::error::consumer checkout does not match the producer candidate SHA"; exit 1; } + [[ "$workflow_sha" == "$CALLER_WORKFLOW_SHA" ]] || + { echo "::error::consumer and producer workflow SHAs differ"; exit 1; } + [[ "$run_id" == "$GITHUB_RUN_ID" && "$run_attempt" == "$GITHUB_RUN_ATTEMPT" ]] || + { echo "::error::consumer and producer workflow run identities differ"; exit 1; } + + remote_url="$(git remote get-url origin)" + case "$remote_url" in + https://github.com/*) remote_repository="${remote_url#https://github.com/}" ;; + git@github.com:*) remote_repository="${remote_url#git@github.com:}" ;; + *) echo "::error::consumer checkout repository URL is invalid"; exit 1 ;; + esac + remote_repository="${remote_repository%.git}" + [[ "$remote_repository" == "$candidate_repository" ]] || + { echo "::error::consumer checkout repository does not match producer provenance"; exit 1; } + + jq -r ' + "artifact_digest=" + .artifactDigest, + "artifact_id=" + .artifactId, + "artifact_name=" + .artifactName, + "candidate_repository=" + .candidateRepository, + "candidate_sha=" + .candidateSha, + "payload_sha256=" + .payloadSha256, + "run_attempt=" + .runAttempt, + "run_id=" + .runId, + "workflow_sha=" + .workflowSha + ' <<<"$PROVENANCE_JSON" >>"$GITHUB_OUTPUT" + + - name: Download exact-commit CLI artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + artifact-ids: ${{ steps.identity.outputs.artifact_id }} + path: ${{ runner.temp }}/nemoclaw-cli-artifact + digest-mismatch: error + + - name: Verify and restore exact-commit CLI artifact + env: + ARTIFACT_NAME: ${{ steps.identity.outputs.artifact_name }} + CANDIDATE_REPOSITORY: ${{ steps.identity.outputs.candidate_repository }} + CANDIDATE_SHA: ${{ steps.identity.outputs.candidate_sha }} + PAYLOAD_SHA256: ${{ steps.identity.outputs.payload_sha256 }} + RUN_ATTEMPT: ${{ steps.identity.outputs.run_attempt }} + RUN_ID: ${{ steps.identity.outputs.run_id }} + WORKFLOW_SHA: ${{ steps.identity.outputs.workflow_sha }} + shell: bash + run: | + set -euo pipefail + artifact_dir="${RUNNER_TEMP}/nemoclaw-cli-artifact" + manifest="$artifact_dir/manifest.json" + payload="$artifact_dir/nemoclaw-cli.tar" + test -s "$manifest" && test -s "$payload" || + { echo "::error::exact-commit CLI artifact is incomplete"; exit 1; } + [[ "$(node --version)" =~ ^v22\.[0-9]+\.[0-9]+$ ]] || + { echo "::error::consumer must restore the CLI under the pinned Node 22 toolchain"; exit 1; } + source_tree="$(git rev-parse 'HEAD^{tree}')" + lockfile_sha256="$(sha256sum package-lock.json | awk '{print $1}')" + jq -e \ + --arg artifactName "$ARTIFACT_NAME" \ + --arg candidateRepository "$CANDIDATE_REPOSITORY" \ + --arg candidateSha "$CANDIDATE_SHA" \ + --arg lockfileSha256 "$lockfile_sha256" \ + --arg payloadSha256 "$PAYLOAD_SHA256" \ + --arg runAttempt "$RUN_ATTEMPT" \ + --arg runId "$RUN_ID" \ + --arg sourceTree "$source_tree" \ + --arg workflowSha "$WORKFLOW_SHA" \ + ' + .kind == "nemoclaw-e2e-cli-artifact-v1" and + .artifactName == $artifactName and + .candidate.repository == $candidateRepository and + .candidate.sha == $candidateSha and + .candidate.sourceTree == $sourceTree and + .candidate.lockfileSha256 == $lockfileSha256 and + .workflow.sha == $workflowSha and + .workflow.runId == $runId and + .workflow.runAttempt == $runAttempt and + (.toolchain.node | strings | test("^v22\\.[0-9]+\\.[0-9]+$")) and + (.toolchain.npm | strings | test("^[0-9]+\\.[0-9]+\\.[0-9]+$")) and + .toolchain.runnerOs == "Linux" and + .toolchain.runnerArch == "X64" and + .build.command == "npm run build:cli" and + .build.sourceRevision == $candidateSha and + .payload.file == "nemoclaw-cli.tar" and + .payload.sha256 == $payloadSha256 + ' "$manifest" >/dev/null || + { echo "::error::exact-commit CLI artifact provenance mismatch"; exit 1; } + actual_payload_sha256="$(sha256sum "$payload" | awk '{print $1}')" + [[ "$actual_payload_sha256" == "$PAYLOAD_SHA256" ]] || + { echo "::error::exact-commit CLI artifact payload digest mismatch"; exit 1; } + while IFS= read -r member; do + case "$member" in + dist | dist/*) ;; + *) echo "::error::CLI artifact contains an unsafe member: $member"; exit 1 ;; + esac + case "/$member/" in + *"/../"* | *"/./"*) echo "::error::CLI artifact contains traversal: $member"; exit 1 ;; + esac + done < <(tar -tf "$payload") + tar -tvf "$payload" | + awk 'substr($1, 1, 1) != "-" && substr($1, 1, 1) != "d" { exit 1 }' || + { echo "::error::CLI artifact contains a link or special file"; exit 1; } + [[ ! -e "$GITHUB_WORKSPACE/dist" && ! -L "$GITHUB_WORKSPACE/dist" ]] || + { echo "::error::consumer unexpectedly built dist before artifact restore"; exit 1; } + restore_dir="$(mktemp -d "${RUNNER_TEMP}/nemoclaw-cli-restore.XXXXXX")" + trap 'rm -rf -- "$restore_dir"' EXIT + tar --no-same-owner --no-same-permissions -xf "$payload" -C "$restore_dir" + test -s "$restore_dir/dist/nemoclaw.js" || + { echo "::error::restored CLI artifact is missing dist/nemoclaw.js"; exit 1; } + jq -e --arg candidateSha "$CANDIDATE_SHA" ' + type == "object" and + (keys | sort) == ["nemoclawVersion", "sourceRevision"] and + (.nemoclawVersion | strings | length > 0) and + .sourceRevision == $candidateSha + ' "$restore_dir/dist/build-identity.json" >/dev/null || + { echo "::error::restored CLI build identity does not match the candidate SHA"; exit 1; } + mv "$restore_dir/dist" "$GITHUB_WORKSPACE/dist" + node "$GITHUB_WORKSPACE/bin/nemoclaw.js" --version >/dev/null diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index c109380a4c4..8173c20f284 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -157,6 +157,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 outputs: + cli_artifact_provenance: ${{ steps.record_cli_artifact.outputs.provenance }} matrix: ${{ steps.controller_matrix.outputs.matrix || steps.matrix.outputs.matrix }} test_matrix: ${{ steps.matrix.outputs.test_matrix }} hermes_selected: ${{ steps.matrix.outputs.hermes_selected }} @@ -369,8 +370,169 @@ jobs: - name: Prepare E2E workspace uses: NVIDIA/NemoClaw/.github/actions/prepare-e2e@f6304bc25fc35bfaa441c8c2fbfee38f72805a75 - with: - build-cli: "false" + + - id: package_cli_artifact + name: Package exact-commit CLI + 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; } + test -s dist/nemoclaw.js || + { echo "::error::candidate CLI build did not produce dist/nemoclaw.js"; exit 1; } + test -s dist/build-identity.json || + { echo "::error::candidate CLI build did not produce dist/build-identity.json"; exit 1; } + 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 + 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 + 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 + 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 @@ -454,6 +616,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Verify retired selector replacements env: @@ -625,6 +794,13 @@ jobs: # 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: Restore exact-commit CLI artifact + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@4b911e40f3f41ce500d69330ca9280b79ceede0f + 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. @@ -781,6 +957,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Run tagged credential-free test env: @@ -825,6 +1008,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + 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 @@ -1092,6 +1282,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + 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') }} @@ -1217,6 +1414,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Install and verify cloudflared prerequisite env: @@ -1332,6 +1536,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + 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 @@ -1437,6 +1648,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Install OpenShell CLI # The migrated skill-agent lane invokes helper scripts that call @@ -1513,6 +1731,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Run OpenClaw skill CLI live test env: @@ -1551,6 +1776,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Install and verify cloudflared prerequisite # Keep the public HTTPS routing fixture on the same reviewed binary as @@ -1623,6 +1855,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Run cloud inference live test # The Vitest test @@ -1674,6 +1913,13 @@ jobs: - *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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Install OpenShell CLI run: bash scripts/install-openshell.sh - name: Run GPU Ollama live Vitest test @@ -2626,6 +2872,13 @@ jobs: - *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@4b911e40f3f41ce500d69330ca9280b79ceede0f + 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 @@ -2693,6 +2946,13 @@ jobs: - *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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Install OpenShell CLI run: bash scripts/install-openshell.sh - name: Run Kimi compatibility live Vitest test @@ -2773,6 +3033,13 @@ jobs: - *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@4b911e40f3f41ce500d69330ca9280b79ceede0f + 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 @@ -2830,6 +3097,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Install OpenShell CLI run: bash scripts/install-openshell.sh @@ -2930,6 +3204,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Install OpenShell CLI run: bash scripts/install-openshell.sh @@ -3000,6 +3281,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Install OpenShell CLI run: bash scripts/install-openshell.sh @@ -3059,6 +3347,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Run dashboard remote-bind live test env: @@ -3101,6 +3396,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Install OpenShell CLI run: | @@ -3159,6 +3461,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Run Hermes Slack live test # Preserves the @@ -3234,6 +3543,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + 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 == '' }} @@ -3372,6 +3688,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Reassert trusted Node runtime uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 @@ -3573,6 +3896,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + 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 == '' }} @@ -3656,6 +3986,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Install OpenShell # Runs without workflow tokens, Docker credentials, or NVIDIA_INFERENCE_API_KEY. @@ -3748,6 +4085,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + 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 == '' }} @@ -3905,6 +4249,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + 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 == '' }} @@ -3957,6 +4308,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Install OpenShell # Direct Vitest execution uses bin/nemoclaw.js instead of install.sh, @@ -4028,6 +4386,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Add swap for Hermes image rebuild shell: bash @@ -4143,6 +4508,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Add swap for Hermes image rebuild shell: bash @@ -4252,6 +4624,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Run overlayfs autofix live test env: @@ -4296,6 +4675,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Install OpenShell env: @@ -4356,6 +4742,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Install OpenShell CLI run: bash scripts/install-openshell.sh @@ -4434,6 +4827,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Verify Jetson GPU availability run: | @@ -4489,6 +4889,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Install OpenShell CLI run: bash scripts/install-openshell.sh @@ -4545,6 +4952,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + 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_INFERENCE_API_KEY -u COMPATIBLE_API_KEY -u GITHUB_TOKEN bash scripts/install-openshell.sh @@ -4600,6 +5014,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Install OpenShell CLI run: bash scripts/install-openshell.sh @@ -4787,6 +5208,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Install OpenShell CLI run: bash scripts/install-openshell.sh @@ -4878,6 +5306,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Install OpenShell CLI run: bash scripts/install-openshell.sh @@ -4932,6 +5367,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Install OpenShell CLI run: bash scripts/install-openshell.sh @@ -4987,6 +5429,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Install OpenShell CLI run: bash scripts/install-openshell.sh @@ -5040,6 +5489,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Run token rotation live test # Preserve the original runner class: ubuntu-latest with Docker/OpenShell @@ -5092,6 +5548,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Run messaging compatible endpoint live test # Preserves the fake OpenAI-compatible endpoint, Telegram messaging @@ -5201,6 +5664,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Run OpenShell gateway upgrade live Vitest test # Keep the original v0.0.36 fixture on x86_64 and validate the exact @@ -5252,6 +5722,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Run messaging providers live Vitest test # The test keeps @@ -5357,6 +5834,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Run Model Router provider-routed inference live test # Preserves the real provider-routed onboard, host model-router health, and @@ -5453,6 +5937,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Verify CLI launcher run: | @@ -5512,6 +6003,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Run sandbox survival live test # This intentionally @@ -5577,6 +6075,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Run OpenClaw custom-plugin release baseline live test run: | @@ -5645,6 +6150,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Run OpenClaw custom-plugin lifecycle and runtime-deps EXDEV live test run: | @@ -5717,6 +6229,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Run OpenClaw TUI chat correlation live test env: @@ -5779,6 +6298,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Install OpenShell CLI # This target invokes `bin/nemoclaw.js onboard` directly, so install @@ -5868,6 +6394,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Run OpenClaw inference switch live test # Preserves the real install.sh/OpenShell/Docker/managed-proxy boundary while @@ -5943,6 +6476,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Run Bedrock Runtime compatible Anthropic live test # Preserves the fake Bedrock Runtime endpoint, /etc/hosts mapping, @@ -5991,6 +6531,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Install OpenShell CLI run: bash scripts/install-openshell.sh @@ -6047,6 +6594,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Install OpenShell env: @@ -6117,6 +6671,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Install OpenShell env: @@ -6210,6 +6771,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + 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' }} @@ -6296,6 +6864,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Install OpenShell CLI run: | @@ -6361,6 +6936,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Install OpenShell CLI run: bash scripts/install-openshell.sh @@ -6419,6 +7001,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Install OpenShell CLI run: | @@ -6484,6 +7073,13 @@ jobs: - 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@4b911e40f3f41ce500d69330ca9280b79ceede0f + with: + provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Install and verify cloudflared prerequisite env: diff --git a/test/e2e/README.md b/test/e2e/README.md index 858cadcb0e8..bf1815f95b0 100644 --- a/test/e2e/README.md +++ b/test/e2e/README.md @@ -23,6 +23,98 @@ before those targets run; local runners must provide it themselves. ## CI execution shape +### Candidate CLI Artifact + +The candidate CLI comes from the source commit that an E2E run tests. +The `generate-matrix` job builds it once and publishes `dist/` as a content-addressed artifact. +The workflow has 62 artifact-using job definitions. +Each selected job execution restores the artifact instead of running `npm run build:cli`. +Each selected job still runs the pinned preparation action to install Node.js and project dependencies. +It sets `build-cli: "false"` so the preparation action does not rebuild the CLI. +The `managed-image-protected-runtime` qualification does not use this artifact. +It builds the CLI from the trusted workflow checkout and never executes or restores the candidate CLI. + +#### Artifact Identity + +For a pull request (PR) run, `checkout_sha` identifies the candidate source commit. +The trusted workflow runs from `github.workflow_sha`. +A scheduled or manual run uses `github.sha` when `checkout_sha` is empty. + +The artifact manifest records these values: + +- The candidate repository and commit SHA. +- The trusted workflow SHA, run ID, and attempt. +- The source tree and lockfile digests. +- The Node.js and npm versions, runner platform, and build command. +- The payload digest. + +The artifact name contains the candidate commit SHA and payload SHA-256 digest. +The `generate-matrix` job emits one `nemoclaw-e2e-cli-provenance-v1` JSON object through its `cli_artifact_provenance` output. +Each artifact-using job passes that object as the restore action's only `provenance-json` input. + +Each artifact-using job invokes the repository-owned `restore-e2e-cli-artifact` composite action at a full commit SHA. +The workflow does not load the action implementation from the candidate checkout. +Before download, the action rejects extra or missing provenance fields. +It also compares the candidate checkout, repository, workflow SHA, run ID, and attempt with the provenance object. +The action downloads the artifact by immutable ID and sets digest mismatch handling to `error`. + +Before the action restores `dist/` into the workspace, it verifies these conditions: + +- The upload digest is present and well formed. +- The candidate SHA matches the expected commit. +- The manifest matches the source, workflow run, toolchain contract, and payload. +- The archive contains no path traversal, links, special files, or files outside `dist/`. +- The candidate checkout has no preexisting `dist/` path. +- The staged `dist/build-identity.json` names the candidate commit SHA. + +If a pre-restore check fails, the action stops before it moves `dist/` into the workspace. +After the checks pass, the action moves `dist/` and runs `bin/nemoclaw.js --version`. +If the version command fails, the action stops before the live test runs. +This boundary keeps candidate source separate from the trusted workflow implementation. + +#### Timing Baseline + +The pre-change baseline uses GitHub Actions `Build CLI` step timings from these workflow runs: + +| Workflow run | Job | Tested candidate | `Build CLI` duration | +| --- | --- | --- | --- | +| [30574154335](https://github.com/NVIDIA/NemoClaw/actions/runs/30574154335) | `cloud-inference` | `385f598` | 18.740 seconds | +| [30574154335](https://github.com/NVIDIA/NemoClaw/actions/runs/30574154335) | `cloud-onboard` | `385f598` | 18.793 seconds | +| [30503498077](https://github.com/NVIDIA/NemoClaw/actions/runs/30503498077) | `Shared E2E (vllm-docker-storage)` | `d52d459` | 18.756 seconds | + +The three observed build steps have a median duration of 18.756 seconds. +This baseline measures only the replaced build step. +Artifact upload, download, validation, and the dependency on `generate-matrix` add runtime and can affect the workflow critical path. +Do not use the build-step median to claim savings in runner time or workflow elapsed time. + +A trusted PR E2E run tests candidate code but executes `.github/workflows/e2e.yaml` from `main`. +The PR run cannot measure this workflow change before merge. +After merge, use a passing `main` run and complete these steps: + +1. Match the job selection, runner labels, and first attempt to the baseline. +2. Record durations for the candidate build, artifact upload, artifact download, combined verification and restore step, job, and workflow. +3. Sum affected step durations for runner-time comparison. +4. Compare matched job and workflow elapsed times. +5. Identify each result by workflow run, tested commit SHA, trusted workflow SHA, and attempt. + +Do not substitute a theoretical value for post-change CI evidence. + +#### Historical Fixtures + +The historical fixtures retain these version boundaries: + +| Fixture | Required boundary | +| --- | --- | +| `openshell-gateway-upgrade` | Retain the historical installer commit and SHA-256 digest, sandbox image digest, and reviewed OpenClaw npm URL and SHA-512 integrity. Install the historical package before testing the candidate upgrade path. | +| `rebuild-openclaw` | Retain the reviewed old-base build in the target. Build and create the old sandbox before testing the candidate rebuild path. | + +These targets may restore the shared artifact for the candidate CLI. +They must not replace a historical installer, package, image, or version boundary with that artifact. +The gateway fixture already binds its remote historical inputs to immutable commits and cryptographic digests. +The workflow does not republish those inputs as artifacts. + +### Hermes Sandbox Image Artifact + The sandbox image workflow builds the Hermes production image in the dedicated 30-minute `build-hermes-sandbox-image` job. It uses full-SHA-pinned Buildx actions and a GitHub Actions cache scoped to the runner OS and architecture. @@ -351,8 +443,7 @@ larger runner. Each execution writes one bounded, ordered v2 time series to the canonical `runner-comparison.jsonl` ledger. It contains: -- an `initialize` endpoint after workspace preparation and any fixed-capacity - rebuild swap; +- an `initialize` endpoint after exact-commit artifact restoration, or after workspace preparation for `security-posture`; the rebuild jobs initialize after their fixed-capacity swap; - a distinct `scenario-start` for every test handled by the execution; - a `periodic` sample on an approximately 15-second fixed cadence for `rebuild-hermes` and `rebuild-hermes-stale-base`, and an approximately @@ -406,8 +497,12 @@ selection and ranking observation; `breakdown.vmRssKb` is the immediately following procfs observation and may differ when a live process changes memory. The finalizer validates the complete ledger before writing -`runner-comparison-summary.json`. The v2 summary reports the sampled -post-prepare window; CPU average and busiest interval; one-minute load; +`runner-comparison-summary.json`. The v2 summary reports the sampled window from +`initialize` until immediately before artifact scanning or upload. For artifact-using +jobs, initialization follows artifact restoration and any required rebuild swap. For +the Hermes `security-posture` shard, initialization follows workspace preparation, +so the window includes OpenShell installation and installer-backed NemoClaw setup. +The summary reports CPU average and busiest interval; one-minute load; available, cached, reclaimable, swap, root-cgroup current/peak/limit, and endpoint OOM-counter evidence; memory and I/O pressure; workspace bytes and inodes; Docker image, container, and build-cache usage; largest container diff --git a/test/e2e/support/cli-artifact-workflow-boundary.test.ts b/test/e2e/support/cli-artifact-workflow-boundary.test.ts new file mode 100644 index 00000000000..1015e91df8d --- /dev/null +++ b/test/e2e/support/cli-artifact-workflow-boundary.test.ts @@ -0,0 +1,538 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { execFileSync, spawnSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; +import { + CLI_ARTIFACT_PACKAGE_STEP, + CLI_ARTIFACT_PUBLISH_STEP, + CLI_ARTIFACT_RESTORE_STEP, + validateCliArtifactRestoreAction, + validateCliArtifactWorkflowBoundary, +} from "../../../tools/e2e/cli-artifact-workflow-boundary.mts"; +import { + type CompositeAction, + readRepoText, + readWorkflow, + readYaml, + type Workflow, +} from "../../helpers/e2e-workflow-contract"; + +const CANDIDATE_SHA = execFileSync("git", ["rev-parse", "HEAD"], { + encoding: "utf8", +}).trim(); +const PAYLOAD_SHA256 = "b".repeat(64); +const CONTENT_ADDRESSED_ARTIFACT_NAME = `artifact_name="nemoclaw-cli-\${CANDIDATE_SHA}-\${payload_sha256}"`; +const UNBOUND_ARTIFACT_NAME = `artifact_name="nemoclaw-cli-\${CANDIDATE_SHA}"`; + +function runIdentityValidation(overrides: Record = {}) { + const action = readYaml(".github/actions/restore-e2e-cli-artifact/action.yaml"); + const workflowSha = "d".repeat(40); + const outputDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "cli-artifact-identity-")); + try { + return spawnSync("bash", ["-c", action.runs.steps[0]!.run!], { + encoding: "utf8", + env: { + ...process.env, + CALLER_WORKFLOW_SHA: workflowSha, + GITHUB_OUTPUT: path.join(outputDirectory, "github-output"), + GITHUB_RUN_ATTEMPT: "1", + GITHUB_RUN_ID: "98765", + PROVENANCE_JSON: JSON.stringify({ + kind: "nemoclaw-e2e-cli-provenance-v1", + artifactDigest: "c".repeat(64), + artifactId: "12345", + artifactName: `nemoclaw-cli-${CANDIDATE_SHA}-${PAYLOAD_SHA256}`, + candidateRepository: "NVIDIA/NemoClaw", + candidateSha: CANDIDATE_SHA, + payloadSha256: PAYLOAD_SHA256, + runAttempt: "1", + runId: "98765", + workflowSha, + ...overrides, + }), + }, + }); + } finally { + fs.rmSync(outputDirectory, { force: true, recursive: true }); + } +} + +function workflowFixture(): Workflow { + return JSON.parse(JSON.stringify(readWorkflow())) as Workflow; +} + +type RestoreFixtureOptions = { + archive?: "valid" | "non-dist" | "link" | "traversal"; + buildIdentitySha?: string; + expectedPayloadSha256?: string; + manifestCandidateSha?: string; + preexistingDist?: "dangling-symlink" | "directory"; +}; + +type ArchiveFixtureContext = { + buildIdentitySha: string; + payload: string; + payloadRoot: string; +}; + +function sha256File(file: string): string { + return createHash("sha256").update(fs.readFileSync(file)).digest("hex"); +} + +function writeDistArchive( + context: ArchiveFixtureContext, + customizeDist: (dist: string) => void, +): void { + const dist = path.join(context.payloadRoot, "dist"); + fs.mkdirSync(dist); + fs.writeFileSync(path.join(dist, "nemoclaw.js"), 'console.log("nemoclaw v0.0.0");\n'); + fs.writeFileSync( + path.join(dist, "build-identity.json"), + `${JSON.stringify({ + nemoclawVersion: "0.0.0", + sourceRevision: context.buildIdentitySha, + })}\n`, + ); + customizeDist(dist); + execFileSync("tar", ["-cf", context.payload, "-C", context.payloadRoot, "dist"]); +} + +function writeValidArchive(context: ArchiveFixtureContext): void { + writeDistArchive(context, () => undefined); +} + +function writeLinkArchive(context: ArchiveFixtureContext): void { + writeDistArchive(context, (dist) => { + fs.symlinkSync("nemoclaw.js", path.join(dist, "linked-cli.js")); + }); +} + +function writeNonDistArchive(context: ArchiveFixtureContext): void { + fs.writeFileSync(path.join(context.payloadRoot, "outside.txt"), "outside dist\n"); + execFileSync("tar", ["-cf", context.payload, "-C", context.payloadRoot, "outside.txt"]); +} + +function writeTraversalArchive(context: ArchiveFixtureContext): void { + fs.writeFileSync(path.join(context.payloadRoot, "outside.txt"), "outside dist\n"); + const transform = + process.platform === "darwin" + ? ["-s", "|^outside.txt$|dist/../outside.txt|"] + : ["--transform=s|^outside.txt$|dist/../outside.txt|"]; + execFileSync("tar", [ + "-cf", + context.payload, + ...transform, + "-C", + context.payloadRoot, + "outside.txt", + ]); +} + +const ARCHIVE_FIXTURE_WRITERS = { + link: writeLinkArchive, + "non-dist": writeNonDistArchive, + traversal: writeTraversalArchive, + valid: writeValidArchive, +} satisfies Record< + NonNullable, + (context: ArchiveFixtureContext) => void +>; + +function writeDanglingDistSymlink(workspace: string): void { + const dist = path.join(workspace, "dist"); + fs.symlinkSync("missing-dist", dist); +} + +function writePreexistingDistDirectory(workspace: string): void { + const dist = path.join(workspace, "dist"); + fs.mkdirSync(dist); + fs.writeFileSync(path.join(workspace, "dist", "existing.txt"), "preserve\n"); +} + +const PREEXISTING_DIST_WRITERS = { + "dangling-symlink": writeDanglingDistSymlink, + directory: writePreexistingDistDirectory, + none: () => undefined, +} satisfies Record< + NonNullable | "none", + (workspace: string) => void +>; + +function runRestoreValidation(options: RestoreFixtureOptions = {}) { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "cli-artifact-restore-")); + const workspace = path.join(root, "workspace"); + const runnerTemp = path.join(root, "runner-temp"); + const artifactDirectory = path.join(runnerTemp, "nemoclaw-cli-artifact"); + const payloadRoot = path.join(root, "payload-root"); + const toolDirectory = path.join(root, "tools"); + fs.mkdirSync(path.join(workspace, "bin"), { recursive: true }); + fs.mkdirSync(artifactDirectory, { recursive: true }); + fs.mkdirSync(payloadRoot, { recursive: true }); + fs.mkdirSync(toolDirectory, { recursive: true }); + fs.writeFileSync(path.join(workspace, "package-lock.json"), '{"lockfileVersion":3}\n'); + fs.writeFileSync( + path.join(workspace, "bin", "nemoclaw.js"), + '#!/usr/bin/env node\nrequire("../dist/nemoclaw.js");\n', + { mode: 0o755 }, + ); + execFileSync("git", ["init", "--quiet"], { cwd: workspace }); + execFileSync("git", ["add", "."], { cwd: workspace }); + execFileSync( + "git", + [ + "-c", + "commit.gpgsign=false", + "-c", + "user.name=NemoClaw Test", + "-c", + "user.email=test@localhost", + "commit", + "--quiet", + "-m", + "fixture", + ], + { cwd: workspace }, + ); + const candidateSha = execFileSync("git", ["rev-parse", "HEAD"], { + cwd: workspace, + encoding: "utf8", + }).trim(); + const sourceTree = execFileSync("git", ["rev-parse", "HEAD^{tree}"], { + cwd: workspace, + encoding: "utf8", + }).trim(); + + const payload = path.join(artifactDirectory, "nemoclaw-cli.tar"); + ARCHIVE_FIXTURE_WRITERS[options.archive ?? "valid"]({ + buildIdentitySha: options.buildIdentitySha ?? candidateSha, + payload, + payloadRoot, + }); + + const actualPayloadSha256 = sha256File(payload); + const expectedPayloadSha256 = options.expectedPayloadSha256 ?? actualPayloadSha256; + const artifactName = `nemoclaw-cli-${candidateSha}-${expectedPayloadSha256}`; + const workflowSha = "d".repeat(40); + fs.writeFileSync( + path.join(artifactDirectory, "manifest.json"), + `${JSON.stringify({ + kind: "nemoclaw-e2e-cli-artifact-v1", + artifactName, + candidate: { + repository: "NVIDIA/NemoClaw", + sha: options.manifestCandidateSha ?? candidateSha, + sourceTree, + lockfileSha256: sha256File(path.join(workspace, "package-lock.json")), + }, + workflow: { sha: workflowSha, runId: "98765", runAttempt: "1" }, + toolchain: { + node: "v22.23.1", + npm: "10.9.2", + runnerOs: "Linux", + runnerArch: "X64", + }, + build: { command: "npm run build:cli", sourceRevision: candidateSha }, + payload: { file: "nemoclaw-cli.tar", sha256: expectedPayloadSha256 }, + })}\n`, + ); + + const nodeWrapper = path.join(toolDirectory, "node"); + fs.writeFileSync( + nodeWrapper, + `#!/usr/bin/env bash\nset -euo pipefail\nif [[ "$#" -eq 1 && "$1" == "--version" ]]; then\n echo v22.23.1\n exit 0\nfi\nexec ${JSON.stringify(process.execPath)} "$@"\n`, + { mode: 0o755 }, + ); + PREEXISTING_DIST_WRITERS[options.preexistingDist ?? "none"](workspace); + + const action = readYaml(".github/actions/restore-e2e-cli-artifact/action.yaml"); + const result = spawnSync("bash", ["-c", action.runs.steps[2]!.run!], { + cwd: workspace, + encoding: "utf8", + env: { + ...process.env, + ARTIFACT_NAME: artifactName, + CANDIDATE_REPOSITORY: "NVIDIA/NemoClaw", + CANDIDATE_SHA: candidateSha, + GITHUB_WORKSPACE: workspace, + PATH: `${toolDirectory}:${process.env.PATH ?? ""}`, + PAYLOAD_SHA256: expectedPayloadSha256, + RUN_ATTEMPT: "1", + RUN_ID: "98765", + RUNNER_TEMP: runnerTemp, + WORKFLOW_SHA: workflowSha, + }, + }); + return { + candidateSha, + cleanup: () => fs.rmSync(root, { force: true, recursive: true }), + output: `${result.stdout}${result.stderr}`, + result, + runnerTemp, + workspace, + }; +} + +function expectRestoreFailure(options: RestoreFixtureOptions, message: string): void { + const fixture = runRestoreValidation(options); + try { + expect(fixture.result.status, fixture.output).not.toBe(0); + expect(fixture.output).toContain(message); + expect(fs.existsSync(path.join(fixture.workspace, "dist"))).toBe(false); + } finally { + fixture.cleanup(); + } +} + +function requireStep(workflow: Workflow, jobName: string, stepName: string) { + const step = workflow.jobs[jobName]?.steps?.find((candidate) => candidate.name === stepName); + expect(step, `${jobName} must contain ${stepName}`).toBeDefined(); + return step!; +} + +describe("exact-commit CLI artifact workflow boundary", () => { + it("builds the candidate CLI once and requires every artifact-using job to restore it", () => { + expect(validateCliArtifactWorkflowBoundary(readWorkflow())).toEqual([]); + }); + + it("reports both an unreadable action and a missing producer", () => { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), "cli-artifact-missing-action-")); + try { + const workflow = workflowFixture(); + delete workflow.jobs["generate-matrix"]; + + expect( + validateCliArtifactWorkflowBoundary(workflow, path.join(directory, "missing-action.yaml")), + ).toEqual([ + "CLI artifact restore action file is missing or unreadable", + "workflow is missing CLI artifact producer generate-matrix", + ]); + } finally { + fs.rmSync(directory, { force: true, recursive: true }); + } + }); + + it("accepts matching artifact, candidate source, and workflow identities", () => { + const result = runIdentityValidation(); + expect(result.status, result.stderr).toBe(0); + }); + + it.each([ + ["empty artifact ID", { artifactId: "" }, "producer CLI artifact provenance is invalid"], + [ + "prefixed upload digest", + { artifactDigest: `sha256:${"c".repeat(64)}` }, + "producer CLI artifact provenance is invalid", + ], + [ + "malformed candidate SHA", + { candidateSha: "abc" }, + "producer CLI artifact provenance is invalid", + ], + [ + "different candidate SHA", + { candidateSha: "e".repeat(40) }, + "producer CLI artifact provenance is invalid", + ], + [ + "unbound artifact name", + { artifactName: `nemoclaw-cli-${CANDIDATE_SHA}` }, + "producer CLI artifact provenance is invalid", + ], + [ + "malformed payload digest", + { payloadSha256: "abc" }, + "producer CLI artifact provenance is invalid", + ], + [ + "malformed workflow SHA", + { workflowSha: "abc" }, + "producer CLI artifact provenance is invalid", + ], + [ + "unknown provenance field", + { unexpected: "value" }, + "producer CLI artifact provenance is invalid", + ], + ])("fails closed for %s", (_case, overrides, expectedError) => { + const result = runIdentityValidation(overrides); + expect(result.status, `${result.stdout}${result.stderr}`).not.toBe(0); + expect(`${result.stdout}${result.stderr}`).toContain(expectedError); + }); + + it.each([ + [ + "candidate repository", + { candidateRepository: "example/other-repository" }, + "consumer checkout repository does not match producer provenance", + ], + ["workflow SHA", { workflowSha: "e".repeat(40) }, "consumer and producer workflow SHAs differ"], + ["run ID", { runId: "98766" }, "consumer and producer workflow run identities differ"], + ["run attempt", { runAttempt: "2" }, "consumer and producer workflow run identities differ"], + ])("rejects a mismatched %s before artifact download", (_case, overrides, expectedError) => { + const result = runIdentityValidation(overrides); + expect(result.status, `${result.stdout}${result.stderr}`).not.toBe(0); + expect(`${result.stdout}${result.stderr}`).toContain(expectedError); + }); + + it("restores a payload whose compiled identity matches the candidate commit (#7915)", () => { + const fixture = runRestoreValidation(); + try { + expect(fixture.result.status, fixture.output).toBe(0); + expect( + JSON.parse( + fs.readFileSync(path.join(fixture.workspace, "dist", "build-identity.json"), "utf8"), + ), + ).toEqual({ nemoclawVersion: "0.0.0", sourceRevision: fixture.candidateSha }); + expect( + fs + .readdirSync(fixture.runnerTemp) + .filter((entry) => entry.startsWith("nemoclaw-cli-restore.")), + ).toEqual([]); + } finally { + fixture.cleanup(); + } + }); + + it("rejects manifest provenance before artifact extraction (#7915)", () => { + expectRestoreFailure( + { manifestCandidateSha: "e".repeat(40) }, + "exact-commit CLI artifact provenance mismatch", + ); + }); + + it("rejects a payload digest mismatch before artifact extraction (#7915)", () => { + expectRestoreFailure( + { expectedPayloadSha256: "f".repeat(64) }, + "exact-commit CLI artifact payload digest mismatch", + ); + }); + + it("rejects an archive member outside dist before artifact extraction (#7915)", () => { + expectRestoreFailure( + { archive: "non-dist" }, + "CLI artifact contains an unsafe member: outside.txt", + ); + }); + + it("rejects traversal through a dist-prefixed archive member before extraction (#7915)", () => { + expectRestoreFailure({ archive: "traversal" }, "CLI artifact contains traversal"); + }); + + it("rejects an archive link before artifact extraction (#7915)", () => { + expectRestoreFailure({ archive: "link" }, "CLI artifact contains a link or special file"); + }); + + it("does not overwrite a preexisting dist directory (#7915)", () => { + const fixture = runRestoreValidation({ preexistingDist: "directory" }); + try { + expect(fixture.result.status, fixture.output).not.toBe(0); + expect(fixture.output).toContain("consumer unexpectedly built dist before artifact restore"); + expect(fs.readFileSync(path.join(fixture.workspace, "dist", "existing.txt"), "utf8")).toBe( + "preserve\n", + ); + } finally { + fixture.cleanup(); + } + }); + + it("does not overwrite a dangling dist symlink (#7915)", () => { + const fixture = runRestoreValidation({ preexistingDist: "dangling-symlink" }); + try { + expect(fixture.result.status, fixture.output).not.toBe(0); + expect(fixture.output).toContain("consumer unexpectedly built dist before artifact restore"); + expect(fs.lstatSync(path.join(fixture.workspace, "dist")).isSymbolicLink()).toBe(true); + expect(fs.readlinkSync(path.join(fixture.workspace, "dist"))).toBe("missing-dist"); + } finally { + fixture.cleanup(); + } + }); + + it("rejects a compiled identity mismatch before artifact activation (#7915)", () => { + expectRestoreFailure( + { buildIdentitySha: "e".repeat(40) }, + "restored CLI build identity does not match the candidate SHA", + ); + }); + + it("rejects producer identity and content-addressing drift", () => { + const workflow = workflowFixture(); + const producer = workflow.jobs["generate-matrix"]; + producer.outputs!.cli_artifact_provenance = + "${{ steps.upload_cli_artifact.outputs.artifact-url }}"; + const packageStep = requireStep(workflow, "generate-matrix", CLI_ARTIFACT_PACKAGE_STEP); + packageStep.env!.WORKFLOW_SHA = "${{ inputs.checkout_sha }}"; + packageStep.run = packageStep.run!.replace( + CONTENT_ADDRESSED_ARTIFACT_NAME, + UNBOUND_ARTIFACT_NAME, + ); + const uploadStep = requireStep(workflow, "generate-matrix", CLI_ARTIFACT_PUBLISH_STEP); + uploadStep.uses = "actions/upload-artifact@v7"; + + expect(validateCliArtifactWorkflowBoundary(workflow)).toEqual( + expect.arrayContaining([ + "generate-matrix must expose exact cli_artifact_provenance provenance", + "CLI artifact package step must bind candidate and trusted workflow identities explicitly", + `CLI artifact package step must contain ${CONTENT_ADDRESSED_ARTIFACT_NAME}`, + "CLI artifact upload must use the immutable content-addressed upload contract", + ]), + ); + }); + + it("rejects incomplete consumer provenance and a mutable action reference", () => { + const workflow = workflowFixture(); + const restore = requireStep(workflow, "sandbox-operations", CLI_ARTIFACT_RESTORE_STEP); + restore.uses = "NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@main"; + restore.with = { "provenance-json": "${{ inputs.checkout_sha }}" }; + + expect(validateCliArtifactWorkflowBoundary(workflow)).toContain( + "sandbox-operations must use the immutable complete CLI artifact restore contract", + ); + }); + + it("rejects action implementation drift that weakens extraction or payload verification", () => { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), "cli-artifact-action-")); + try { + const actionPath = path.join(directory, "action.yaml"); + const source = readRepoText(".github/actions/restore-e2e-cli-artifact/action.yaml") + .replace("tar --no-same-owner --no-same-permissions", "tar") + .replace('[[ "$actual_payload_sha256" == "$PAYLOAD_SHA256" ]]', '[[ -s "$payload" ]]'); + fs.writeFileSync(actionPath, source); + + expect(validateCliArtifactRestoreAction(actionPath)).toEqual( + expect.arrayContaining([ + "CLI artifact restore action must match its immutable workflow pin", + 'CLI artifact payload verification must contain tar --no-same-owner --no-same-permissions -xf "$payload" -C "$restore_dir"', + 'CLI artifact payload verification must contain [[ "$actual_payload_sha256" == "$PAYLOAD_SHA256" ]]', + ]), + ); + } finally { + fs.rmSync(directory, { force: true, recursive: true }); + } + }); + + it("rejects missing consumer restoration", () => { + const workflow = workflowFixture(); + workflow.jobs["cloud-inference"].steps = workflow.jobs["cloud-inference"].steps!.filter( + (step) => step.name !== CLI_ARTIFACT_RESTORE_STEP, + ); + + expect(validateCliArtifactWorkflowBoundary(workflow)).toContain( + "cloud-inference must verify and restore the exact CLI artifact exactly once", + ); + }); + + it("excludes installer-backed jobs from the shared CLI artifact", () => { + const workflow = workflowFixture(); + const inheritedRestore = requireStep(workflow, "sandbox-operations", CLI_ARTIFACT_RESTORE_STEP); + workflow.jobs["security-posture"].steps!.push(inheritedRestore); + + expect(validateCliArtifactWorkflowBoundary(workflow)).toContain( + "security-posture must not consume the shared CLI artifact", + ); + }); +}); diff --git a/test/e2e/support/prepare-e2e-workflow-boundary.test.ts b/test/e2e/support/prepare-e2e-workflow-boundary.test.ts index c22d2af0038..c6891de04a9 100644 --- a/test/e2e/support/prepare-e2e-workflow-boundary.test.ts +++ b/test/e2e/support/prepare-e2e-workflow-boundary.test.ts @@ -27,7 +27,7 @@ type Workflow = { }; describe("prepare-e2e workflow boundary", () => { - it("keeps one canonical bootstrap invocation on every E2E execution job", () => { + it("requires one workspace preparation step per E2E job and one candidate CLI build in generate-matrix", () => { expect(validatePrepareE2eAction()).toEqual([]); expect(validatePrepareE2eInvocations(readWorkflow())).toEqual([]); }); @@ -89,10 +89,16 @@ describe("prepare-e2e workflow boundary", () => { it("rejects build-mode, duplicate-step, and ordering drift", () => { const workflow = readWorkflow() as Workflow; - const buildJob = workflow.jobs["sandbox-operations"]; - const buildPrepare = buildJob.steps!.find((step) => step.uses === PREPARE_E2E_ACTION)!; - buildPrepare.with = { "build-cli": "false" }; - buildJob.steps!.splice(buildJob.steps!.indexOf(buildPrepare), 0, { + const artifactProducer = workflow.jobs["generate-matrix"]; + const producerPrepare = artifactProducer.steps!.find( + (step) => step.uses === PREPARE_E2E_ACTION, + )!; + producerPrepare.with = { "build-cli": "false" }; + + const consumerJob = workflow.jobs["sandbox-operations"]; + const consumerPrepare = consumerJob.steps!.find((step) => step.uses === PREPARE_E2E_ACTION)!; + delete consumerPrepare.with; + consumerJob.steps!.splice(consumerJob.steps!.indexOf(consumerPrepare), 0, { name: "Build CLI", run: "npm run build:cli", }); @@ -103,7 +109,7 @@ describe("prepare-e2e workflow boundary", () => { const sharedJob = workflow.jobs["shared-e2e"]; const sharedPrepare = sharedJob.steps!.find((step) => step.uses === PREPARE_E2E_ACTION)!; - sharedPrepare.with = { "build-cli": "false" }; + delete sharedPrepare.with; sharedJob.env!.E2E_EXECUTION_PROFILE = "credential-free"; sharedJob.env!.E2E_JOB = "1"; @@ -120,14 +126,16 @@ describe("prepare-e2e workflow boundary", () => { expect(validatePrepareE2eInvocations(workflow)).toEqual( expect.arrayContaining([ - "sandbox-operations prepare-e2e must use the default CLI build", + "generate-matrix prepare-e2e must own the only default CLI build", + "generate-matrix prepare-e2e invocation must not override its canonical contract", + "sandbox-operations prepare-e2e must set build-cli to false", "sandbox-operations prepare-e2e invocation must not override its canonical contract", "sandbox-operations must not duplicate prepare-e2e step 'Build CLI'", "bootstrap-install-smoke prepare-e2e must set build-cli to false", "bootstrap-install-smoke prepare-e2e invocation must not override its canonical contract", "shared-e2e must not declare E2E_EXECUTION_PROFILE", "shared-e2e must not declare E2E_JOB", - "shared-e2e prepare-e2e must use the default CLI build", + "shared-e2e prepare-e2e must set build-cli to false", "shared-e2e prepare-e2e invocation must not override its canonical contract", "inference-routing must not load prepare-e2e from the target checkout", "inference-routing must use prepare-e2e exactly once", diff --git a/test/e2e/support/rebuild-hermes-workflow-boundary.test.ts b/test/e2e/support/rebuild-hermes-workflow-boundary.test.ts index b7c873169c8..86e69d0169f 100644 --- a/test/e2e/support/rebuild-hermes-workflow-boundary.test.ts +++ b/test/e2e/support/rebuild-hermes-workflow-boundary.test.ts @@ -31,7 +31,9 @@ function bootstrapJob(jobName: JobName): { { name: "Prepare E2E workspace", uses: "NVIDIA/NemoClaw/.github/actions/prepare-e2e@immutable", + with: { "build-cli": "false" }, }, + { name: "Restore exact-commit CLI artifact" }, { name: "Install OpenShell", run: INSTALL_OPENSHELL }, { name: runStepName, @@ -50,14 +52,26 @@ describe("Hermes rebuild bootstrap workflow boundary", () => { expect(validateRebuildHermesBootstrapBoundary(jobName, bootstrapJob(jobName))).toEqual([]); }); + it.each([ + "rebuild-hermes", + "rebuild-hermes-stale-base", + ] as const)("%s requires the exact-commit CLI restore step (#7144)", (jobName) => { + const job = bootstrapJob(jobName); + job.steps = job.steps.filter((step) => step.name !== "Restore exact-commit CLI artifact"); + + expect(validateRebuildHermesBootstrapBoundary(jobName, job)).toContain( + `${jobName} job missing step: Restore exact-commit CLI artifact`, + ); + }); + it.each([ "rebuild-hermes", "rebuild-hermes-stale-base", ] as const)("%s rejects bootstrap trust-boundary drift (#7144)", (jobName) => { const job = bootstrapJob(jobName); - const [prepare, install, run] = job.steps; + const [prepare, restore, install, run] = job.steps; job.env.NEMOCLAW_CLI_BIN = "${{ github.workspace }}/evil/bin/nemoclaw.js"; - prepare.with = { "build-cli": "false" }; + delete prepare.with; install.env = { NVIDIA_INFERENCE_API_KEY: "${{ secrets.NVIDIA_INFERENCE_API_KEY }}", }; @@ -68,12 +82,12 @@ describe("Hermes rebuild bootstrap workflow boundary", () => { }; run.run = "tools/e2e/live-vitest-invocation.mts run --test-path test/e2e/live/rebuild-hermes.test.ts"; - job.steps = [run, install, prepare]; + job.steps = [run, install, restore, prepare]; const errors = validateRebuildHermesBootstrapBoundary(jobName, job); expect(errors).toContain(`${jobName} job must point NEMOCLAW_CLI_BIN at the repo CLI`); expect(errors).toContain( - `${jobName} workspace preparation must use the default checked-out CLI build`, + `${jobName} workspace preparation must defer to the exact-commit CLI artifact`, ); expect(errors).toContain( "step 'Install OpenShell' run script must include env -u DOCKER_CONFIG", @@ -88,7 +102,7 @@ describe("Hermes rebuild bootstrap workflow boundary", () => { expect(errors).toContain(`${jobName} step '${run.name}' env must not include NVIDIA_API_KEY`); expect(errors).toContain(`step '${run.name}' run script must include OPENSHELL_BIN`); expect(errors).toContain( - `${jobName} must build the CLI before installing OpenShell and running Vitest`, + `${jobName} must restore the exact-commit CLI before installing OpenShell and running Vitest`, ); }); }); diff --git a/test/e2e/support/runner-comparison-workflow-boundary.test.ts b/test/e2e/support/runner-comparison-workflow-boundary.test.ts index 926b4108d41..e7df1699515 100644 --- a/test/e2e/support/runner-comparison-workflow-boundary.test.ts +++ b/test/e2e/support/runner-comparison-workflow-boundary.test.ts @@ -148,7 +148,9 @@ describe("runner comparison E2E workflow boundary (#7140)", () => { ); }); - it.each(JOBS)("keeps %s telemetry around the entire post-prepare job", (jobId) => { + it.each( + JOBS, + )("places %s telemetry after workspace preparation, artifact restore when used, and required rebuild swap, and before artifact scanning or upload", (jobId) => { const lateInitialize = loadWorkflow(); const lateSteps = lateInitialize.jobs[jobId]!.steps; const initializeIndex = lateSteps.indexOf( @@ -160,7 +162,9 @@ describe("runner comparison E2E workflow boundary (#7140)", () => { ]; const expectedInitializeError = REBUILD_JOBS.includes(jobId as (typeof REBUILD_JOBS)[number]) ? `${jobId} must establish rebuild swap before initializing runner comparison telemetry` - : `${jobId} must initialize runner comparison telemetry immediately after prepare-e2e`; + : jobId === "security-posture" + ? `${jobId} must initialize runner comparison telemetry immediately after workspace preparation` + : `${jobId} must initialize runner comparison telemetry immediately after CLI artifact restore`; expect(validateRunnerComparisonWorkflow(lateInitialize)).toContain(expectedInitializeError); const afterPublication = loadWorkflow(); diff --git a/tools/e2e/cli-artifact-workflow-boundary.mts b/tools/e2e/cli-artifact-workflow-boundary.mts new file mode 100644 index 00000000000..72272013c18 --- /dev/null +++ b/tools/e2e/cli-artifact-workflow-boundary.mts @@ -0,0 +1,367 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { createHash } from "node:crypto"; +import { readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { isDeepStrictEqual } from "node:util"; +import YAML from "yaml"; +import { + CLI_ARTIFACT_PRODUCER_JOB, + PREPARE_E2E_ACTION, + PREPARE_E2E_NO_BUILD_JOBS, + PREPARE_E2E_TRUSTED_BUILD_JOBS, +} from "./prepare-e2e-workflow-boundary.mts"; + +export const CLI_ARTIFACT_DOWNLOAD_ACTION = + "actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c"; +export const CLI_ARTIFACT_UPLOAD_ACTION = + "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a"; +export const CLI_ARTIFACT_RESTORE_ACTION = + "NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@4b911e40f3f41ce500d69330ca9280b79ceede0f"; +export const CLI_ARTIFACT_PACKAGE_STEP = "Package exact-commit CLI"; +export const CLI_ARTIFACT_PUBLISH_STEP = "Publish content-addressed CLI artifact"; +export const CLI_ARTIFACT_RESTORE_STEP = "Restore exact-commit CLI artifact"; + +const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", ".."); +const DEFAULT_RESTORE_ACTION_PATH = join( + REPO_ROOT, + ".github", + "actions", + "restore-e2e-cli-artifact", + "action.yaml", +); +const RESTORE_ACTION_CONTENT_SHA256 = + "bae2b6b99b46f7d007be9b792d7179fbef30c38d31623849e025d7341f179d0d"; +const CLI_ARTIFACT_DOWNLOAD_STEP = "Download exact-commit CLI artifact"; +const CLI_ARTIFACT_VERIFY_STEP = "Verify and restore exact-commit CLI artifact"; +const CLI_ARTIFACT_PROVENANCE_STEP = "Record CLI artifact provenance"; + +type WorkflowRecord = Record; +type WorkflowStep = WorkflowRecord & { + env?: WorkflowRecord; + id?: string; + name?: string; + run?: string; + uses?: string; + with?: WorkflowRecord; +}; + +function record(value: unknown): WorkflowRecord { + return value && typeof value === "object" && !Array.isArray(value) + ? (value as WorkflowRecord) + : {}; +} + +function steps(value: unknown): WorkflowStep[] { + return Array.isArray(value) ? (value as WorkflowStep[]) : []; +} + +function requireFragments( + errors: string[], + owner: string, + source: unknown, + fragments: readonly string[], +): void { + const script = typeof source === "string" ? source : ""; + for (const fragment of fragments) { + if (!script.includes(fragment)) errors.push(`${owner} must contain ${fragment}`); + } +} + +export function validateCliArtifactRestoreAction( + actionPath = DEFAULT_RESTORE_ACTION_PATH, +): string[] { + const errors: string[] = []; + let actionSource: string; + try { + actionSource = readFileSync(actionPath, "utf8"); + } catch { + return ["CLI artifact restore action file is missing or unreadable"]; + } + if (createHash("sha256").update(actionSource).digest("hex") !== RESTORE_ACTION_CONTENT_SHA256) { + errors.push("CLI artifact restore action must match its immutable workflow pin"); + } + const action = record(YAML.parse(actionSource)); + const inputNames = ["provenance-json"]; + const inputs = record(action.inputs); + if ( + !isDeepStrictEqual(Object.keys(inputs).sort(), inputNames) || + !inputNames.every((name) => record(inputs[name]).required === true) + ) { + errors.push("CLI artifact restore action must require the complete provenance input set"); + } + const actionSteps = steps(record(action.runs).steps); + if (record(action.runs).using !== "composite" || actionSteps.length !== 3) { + errors.push("CLI artifact restore action must keep the three-step composite boundary"); + return errors; + } + const [identity, download, restore] = actionSteps; + if ( + identity?.name !== "Validate exact-commit CLI artifact identity" || + identity?.id !== "identity" || + identity?.shell !== "bash" || + !isDeepStrictEqual(record(identity.env), { + CALLER_WORKFLOW_SHA: "${{ github.workflow_sha }}", + PROVENANCE_JSON: "${{ inputs.provenance-json }}", + }) + ) { + errors.push("CLI artifact restore action must validate identity before download"); + } + requireFragments(errors, "CLI artifact identity validation", identity?.run, [ + "(keys | sort) == [", + '.kind == "nemoclaw-e2e-cli-provenance-v1"', + '.artifactId | strings | test("^[1-9][0-9]*$")', + '.artifactDigest | strings | test("^[a-f0-9]{64}$")', + '.candidateSha | strings | test("^[a-f0-9]{40}$")', + '.payloadSha256 | strings | test("^[a-f0-9]{64}$")', + '.workflowSha | strings | test("^[a-f0-9]{40}$")', + '.artifactName == ("nemoclaw-cli-" + .candidateSha + "-" + .payloadSha256)', + 'git rev-parse --verify HEAD)" == "$candidate_sha"', + '[[ "$workflow_sha" == "$CALLER_WORKFLOW_SHA" ]]', + '[[ "$run_id" == "$GITHUB_RUN_ID" && "$run_attempt" == "$GITHUB_RUN_ATTEMPT" ]]', + '[[ "$remote_repository" == "$candidate_repository" ]]', + '<<<"$PROVENANCE_JSON" >>"$GITHUB_OUTPUT"', + ]); + if ( + download?.name !== CLI_ARTIFACT_DOWNLOAD_STEP || + download?.uses !== CLI_ARTIFACT_DOWNLOAD_ACTION || + !isDeepStrictEqual(record(download.with), { + "artifact-ids": "${{ steps.identity.outputs.artifact_id }}", + path: "${{ runner.temp }}/nemoclaw-cli-artifact", + "digest-mismatch": "error", + }) + ) { + errors.push("CLI artifact restore action must download by immutable ID and reject mismatch"); + } + if (restore?.name !== CLI_ARTIFACT_VERIFY_STEP || restore?.shell !== "bash") { + errors.push("CLI artifact restore action must verify the downloaded payload in bash"); + } + if ( + !isDeepStrictEqual(record(restore?.env), { + ARTIFACT_NAME: "${{ steps.identity.outputs.artifact_name }}", + CANDIDATE_REPOSITORY: "${{ steps.identity.outputs.candidate_repository }}", + CANDIDATE_SHA: "${{ steps.identity.outputs.candidate_sha }}", + PAYLOAD_SHA256: "${{ steps.identity.outputs.payload_sha256 }}", + RUN_ATTEMPT: "${{ steps.identity.outputs.run_attempt }}", + RUN_ID: "${{ steps.identity.outputs.run_id }}", + WORKFLOW_SHA: "${{ steps.identity.outputs.workflow_sha }}", + }) + ) { + errors.push("CLI artifact restore action must pass validated identity to payload verification"); + } + requireFragments(errors, "CLI artifact payload verification", restore?.run, [ + ".candidate.sha == $candidateSha", + ".candidate.sourceTree == $sourceTree", + ".candidate.lockfileSha256 == $lockfileSha256", + ".workflow.sha == $workflowSha", + ".workflow.runId == $runId", + ".workflow.runAttempt == $runAttempt", + ".build.sourceRevision == $candidateSha", + ".payload.sha256 == $payloadSha256", + '[[ "$actual_payload_sha256" == "$PAYLOAD_SHA256" ]]', + '*) echo "::error::CLI artifact contains an unsafe member', + "CLI artifact contains a link or special file", + '[[ ! -e "$GITHUB_WORKSPACE/dist" && ! -L "$GITHUB_WORKSPACE/dist" ]]', + 'restore_dir="$(mktemp -d', + 'tar --no-same-owner --no-same-permissions -xf "$payload" -C "$restore_dir"', + ".sourceRevision == $candidateSha", + 'mv "$restore_dir/dist" "$GITHUB_WORKSPACE/dist"', + 'node "$GITHUB_WORKSPACE/bin/nemoclaw.js" --version', + ]); + return errors; +} + +function validateProducer(errors: string[], producer: WorkflowRecord): void { + const outputs = record(producer.outputs); + const requiredOutputs = { + cli_artifact_provenance: "${{ steps.record_cli_artifact.outputs.provenance }}", + }; + for (const [name, value] of Object.entries(requiredOutputs)) { + if (outputs[name] !== value) { + errors.push(`${CLI_ARTIFACT_PRODUCER_JOB} must expose exact ${name} provenance`); + } + } + + const producerSteps = steps(producer.steps); + const packageSteps = producerSteps.filter((step) => step.name === CLI_ARTIFACT_PACKAGE_STEP); + const uploadSteps = producerSteps.filter((step) => step.name === CLI_ARTIFACT_PUBLISH_STEP); + const provenanceSteps = producerSteps.filter( + (step) => step.name === CLI_ARTIFACT_PROVENANCE_STEP, + ); + if (packageSteps.length !== 1) { + errors.push(`${CLI_ARTIFACT_PRODUCER_JOB} must package the CLI artifact exactly once`); + } + if (uploadSteps.length !== 1) { + errors.push(`${CLI_ARTIFACT_PRODUCER_JOB} must publish the CLI artifact exactly once`); + } + if (provenanceSteps.length !== 1) { + errors.push(`${CLI_ARTIFACT_PRODUCER_JOB} must record CLI artifact provenance exactly once`); + } + const packageStep = packageSteps[0]; + const uploadStep = uploadSteps[0]; + const provenanceStep = provenanceSteps[0]; + if (!packageStep || !uploadStep || !provenanceStep) return; + + if (packageStep.id !== "package_cli_artifact" || packageStep.shell !== "bash") { + errors.push("CLI artifact package step must use id package_cli_artifact and the Bash shell"); + } + if ( + !isDeepStrictEqual(record(packageStep.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 }}", + }) + ) { + errors.push( + "CLI artifact package step must bind candidate and trusted workflow identities explicitly", + ); + } + requireFragments(errors, "CLI artifact package step", packageStep.run, [ + 'git rev-parse --verify HEAD)" == "$CANDIDATE_SHA"', + "test -s dist/nemoclaw.js", + "test -s dist/build-identity.json", + ".sourceRevision == $candidateSha", + "candidate CLI build identity does not match the candidate commit SHA", + "--sort=name", + "--mtime=@0", + "source_tree=\"$(git rev-parse 'HEAD^{tree}')\"", + 'lockfile_sha256="$(sha256sum package-lock.json', + 'artifact_name="nemoclaw-cli-${CANDIDATE_SHA}-${payload_sha256}"', + 'kind: "nemoclaw-e2e-cli-artifact-v1"', + "sha: $candidateSha", + "sha: $workflowSha", + "sourceRevision: $candidateSha", + "sha256: $payloadSha256", + ]); + + if ( + uploadStep.id !== "upload_cli_artifact" || + uploadStep.uses !== CLI_ARTIFACT_UPLOAD_ACTION || + !isDeepStrictEqual(record(uploadStep.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, + }) + ) { + errors.push("CLI artifact upload must use the immutable content-addressed upload contract"); + } + if ( + provenanceStep.id !== "record_cli_artifact" || + !isDeepStrictEqual(record(provenanceStep.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 }}", + }) + ) { + errors.push("CLI artifact provenance step must consume the immutable upload outputs"); + } + requireFragments(errors, "CLI artifact provenance step", provenanceStep.run, [ + '[[ "$ARTIFACT_ID" =~ ^[1-9][0-9]*$ ]]', + '[[ "$ARTIFACT_DIGEST" =~ ^[a-f0-9]{64}$ ]]', + 'kind: "nemoclaw-e2e-cli-provenance-v1"', + "artifactDigest: $artifactDigest", + "candidateRepository: $candidateRepository", + "workflowSha: $workflowSha", + 'printf \'provenance=%s\\n\' "$provenance" >>"$GITHUB_OUTPUT"', + 'echo "- Candidate: \\`${CANDIDATE_SHA}\\`"', + 'echo "- Payload digest: \\`${PAYLOAD_SHA256}\\`"', + ]); + + const prepareIndex = producerSteps.findIndex((step) => step.uses === PREPARE_E2E_ACTION); + const packageIndex = producerSteps.indexOf(packageStep); + const uploadIndex = producerSteps.indexOf(uploadStep); + const provenanceIndex = producerSteps.indexOf(provenanceStep); + if ( + !( + prepareIndex >= 0 && + prepareIndex < packageIndex && + packageIndex < uploadIndex && + uploadIndex < provenanceIndex + ) + ) { + errors.push("CLI artifact producer must build, package, upload, then record provenance"); + } +} + +function validateConsumer( + errors: string[], + jobName: string, + job: WorkflowRecord, + jobSteps: WorkflowStep[], +): void { + if (job.needs !== CLI_ARTIFACT_PRODUCER_JOB) { + errors.push(`${jobName} must depend directly on the CLI artifact producer`); + } + const prepareIndex = jobSteps.findIndex((step) => step.uses === PREPARE_E2E_ACTION); + const restoreSteps = jobSteps.filter( + (step) => step.name === CLI_ARTIFACT_RESTORE_STEP || step.uses === CLI_ARTIFACT_RESTORE_ACTION, + ); + if (restoreSteps.length !== 1) { + errors.push(`${jobName} must verify and restore the exact CLI artifact exactly once`); + } + const restore = restoreSteps[0]; + if (!restore) return; + + if ( + restore.uses !== CLI_ARTIFACT_RESTORE_ACTION || + !isDeepStrictEqual(record(restore.with), { + "provenance-json": "${{ needs.generate-matrix.outputs.cli_artifact_provenance }}", + }) + ) { + errors.push(`${jobName} must use the immutable complete CLI artifact restore contract`); + } + const restoreIndex = jobSteps.indexOf(restore); + if (!(prepareIndex >= 0 && prepareIndex < restoreIndex)) { + errors.push(`${jobName} must prepare before restoring the CLI artifact`); + } +} + +export function validateCliArtifactWorkflowBoundary( + workflow: WorkflowRecord, + actionPath = DEFAULT_RESTORE_ACTION_PATH, +): string[] { + const errors = validateCliArtifactRestoreAction(actionPath); + const jobs = record(workflow.jobs); + const producer = record(jobs[CLI_ARTIFACT_PRODUCER_JOB]); + if (Object.keys(producer).length === 0) { + errors.push(`workflow is missing CLI artifact producer ${CLI_ARTIFACT_PRODUCER_JOB}`); + return errors; + } + validateProducer(errors, producer); + + for (const [jobName, value] of Object.entries(jobs)) { + const job = record(value); + const jobSteps = steps(job.steps); + const usesPrepare = jobSteps.some((step) => step.uses === PREPARE_E2E_ACTION); + const artifactSteps = jobSteps.filter( + (step) => + step.name === CLI_ARTIFACT_RESTORE_STEP || + (typeof step.uses === "string" && + step.uses.startsWith("NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@")), + ); + const shouldConsume = + usesPrepare && + jobName !== CLI_ARTIFACT_PRODUCER_JOB && + !PREPARE_E2E_NO_BUILD_JOBS.has(jobName) && + !PREPARE_E2E_TRUSTED_BUILD_JOBS.has(jobName); + if (shouldConsume) { + validateConsumer(errors, jobName, job, jobSteps); + } else if (artifactSteps.length > 0) { + errors.push(`${jobName} must not consume the shared CLI artifact`); + } + } + + return errors; +} diff --git a/tools/e2e/hermes-gpu-startup-workflow-boundary.mts b/tools/e2e/hermes-gpu-startup-workflow-boundary.mts index e4fee00e9d7..18495f9a776 100644 --- a/tools/e2e/hermes-gpu-startup-workflow-boundary.mts +++ b/tools/e2e/hermes-gpu-startup-workflow-boundary.mts @@ -7,6 +7,7 @@ import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import YAML from "yaml"; +import { CLI_ARTIFACT_RESTORE_STEP } from "./cli-artifact-workflow-boundary.mts"; /** * SOURCE_OF_TRUTH_REVIEW @@ -241,13 +242,15 @@ if ! @run restore`; } const run = stringValue(runStep.run); const pi = steps.findIndex((step) => step.name === "Prepare E2E workspace"); + const restoreI = steps.findIndex((step) => step.name === CLI_ARTIFACT_RESTORE_STEP); const ni = steps.findIndex((step) => step.name === "Reassert trusted Node runtime"); const node = steps[ni]; if ( runStep.shell !== BASH || !trustedEnv(runStep) || pi < 0 || - ni !== pi + 1 || + restoreI <= pi || + ni !== restoreI + 1 || ni + 1 !== steps.indexOf(runStep) || node?.uses !== "actions/setup-node@820762786026740c76f36085b0efc47a31fe5020" || asRecord(node?.with)["node-version"] !== "22" || diff --git a/tools/e2e/prepare-e2e-workflow-boundary.mts b/tools/e2e/prepare-e2e-workflow-boundary.mts index feb44a78d8b..53f3c3dc98a 100644 --- a/tools/e2e/prepare-e2e-workflow-boundary.mts +++ b/tools/e2e/prepare-e2e-workflow-boundary.mts @@ -21,11 +21,11 @@ export const PREPARE_E2E_ACTION = PREPARE_E2E_ACTION_PROVENANCE.reference; export const PREPARE_E2E_STEP = "Prepare E2E workspace"; const CHECKOUT_LOCAL_PREPARE_E2E_ACTION = "./.github/actions/prepare-e2e"; +export const CLI_ARTIFACT_PRODUCER_JOB = "generate-matrix"; const PREINSTALLED_E2E_JOBS = new Set(["staging-brev-launchable"]); const RETIRED_SELECTOR_COMPATIBILITY_JOB = "retired-selector-compatibility"; -const NO_BUILD_JOBS = new Set([ - "generate-matrix", +export const PREPARE_E2E_NO_BUILD_JOBS = new Set([ "bootstrap-install-smoke", "llama-cpp-dgx-spark-qualification", "managed-image-multiarch-startup", @@ -36,6 +36,8 @@ const NO_BUILD_JOBS = new Set([ "spark-install", ]); +export const PREPARE_E2E_TRUSTED_BUILD_JOBS = new Set(["managed-image-protected-runtime"]); + type WorkflowRecord = Record; type WorkflowStep = WorkflowRecord & { name?: string; @@ -118,7 +120,6 @@ export function validatePrepareE2eInvocations(workflow: WorkflowRecord): string[ }) .map(([jobName]) => jobName), ); - const sharedE2eJob = jobs[SHARED_E2E_JOB_ID]; if (sharedE2eJob === undefined) { errors.push(`prepare-e2e shared job is missing: ${SHARED_E2E_JOB_ID}`); @@ -153,9 +154,14 @@ export function validatePrepareE2eInvocations(workflow: WorkflowRecord): string[ errors.push(`${jobName} prepare-e2e step must be named '${PREPARE_E2E_STEP}'`); } const withInputs = record(prepare.with); - const shouldBuild = !NO_BUILD_JOBS.has(jobName); + const shouldBuild = + jobName === CLI_ARTIFACT_PRODUCER_JOB || PREPARE_E2E_TRUSTED_BUILD_JOBS.has(jobName); if (shouldBuild && Object.keys(withInputs).length !== 0) { - errors.push(`${jobName} prepare-e2e must use the default CLI build`); + errors.push( + jobName === CLI_ARTIFACT_PRODUCER_JOB + ? `${jobName} prepare-e2e must own the only default CLI build` + : `${jobName} prepare-e2e must use its default trusted CLI build`, + ); } if (!shouldBuild && !isDeepStrictEqual(withInputs, { "build-cli": "false" })) { errors.push(`${jobName} prepare-e2e must set build-cli to false`); @@ -181,9 +187,14 @@ export function validatePrepareE2eInvocations(workflow: WorkflowRecord): string[ } } - for (const jobName of NO_BUILD_JOBS) { + for (const jobName of PREPARE_E2E_NO_BUILD_JOBS) { if (!expectedJobs.has(jobName)) errors.push(`prepare-e2e no-build job is missing: ${jobName}`); } + for (const jobName of PREPARE_E2E_TRUSTED_BUILD_JOBS) { + if (!expectedJobs.has(jobName)) { + errors.push(`prepare-e2e trusted-build job is missing: ${jobName}`); + } + } return errors; } diff --git a/tools/e2e/runner-comparison-workflow-boundary.mts b/tools/e2e/runner-comparison-workflow-boundary.mts index 1b5a4bcacc0..9ada8d28f17 100644 --- a/tools/e2e/runner-comparison-workflow-boundary.mts +++ b/tools/e2e/runner-comparison-workflow-boundary.mts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { isDeepStrictEqual } from "node:util"; +import { CLI_ARTIFACT_RESTORE_STEP } from "./cli-artifact-workflow-boundary.mts"; import { PREPARE_E2E_STEP } from "./prepare-e2e-workflow-boundary.mts"; import { UPLOAD_E2E_ARTIFACTS_ACTION } from "./upload-e2e-artifacts-workflow-boundary.mts"; @@ -193,19 +194,28 @@ export function validateRunnerComparisonWorkflow(workflowValue: unknown): string if (!initialize || !finalize) continue; const prepare = jobSteps.findIndex((step) => step.name === PREPARE_E2E_STEP); + const restore = jobSteps.findIndex((step) => step.name === CLI_ARTIFACT_RESTORE_STEP); + // security-posture installs the CLI after workspace preparation and does not restore the artifact. + const bootstrapEnd = restore >= 0 ? restore : prepare; + const initializationBoundary = restore >= 0 ? "CLI artifact restore" : "workspace preparation"; const initializeIndex = jobSteps.indexOf(initialize); const finalizeIndex = jobSteps.indexOf(finalize); const publish = publicationIndex(jobSteps); if (HERMES_REBUILD_SWAP_JOBS.has(jobId)) { const swapIndex = jobSteps.findIndex((step) => step.name === HERMES_REBUILD_SWAP_STEP); - if (prepare < 0 || swapIndex !== prepare + 1 || initializeIndex !== swapIndex + 1) { + if ( + prepare < 0 || + bootstrapEnd < prepare || + swapIndex !== bootstrapEnd + 1 || + initializeIndex !== swapIndex + 1 + ) { errors.push( `${jobId} must establish rebuild swap before initializing runner comparison telemetry`, ); } - } else if (prepare < 0 || initializeIndex !== prepare + 1) { + } else if (prepare < 0 || bootstrapEnd < prepare || initializeIndex !== bootstrapEnd + 1) { errors.push( - `${jobId} must initialize runner comparison telemetry immediately after prepare-e2e`, + `${jobId} must initialize runner comparison telemetry immediately after ${initializationBoundary}`, ); } if (publish < 0 || finalizeIndex !== publish - 1) { diff --git a/tools/e2e/upload-e2e-artifacts-workflow-boundary.mts b/tools/e2e/upload-e2e-artifacts-workflow-boundary.mts index edb63d139c4..233e04b2c28 100644 --- a/tools/e2e/upload-e2e-artifacts-workflow-boundary.mts +++ b/tools/e2e/upload-e2e-artifacts-workflow-boundary.mts @@ -7,6 +7,10 @@ import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { isDeepStrictEqual } from "node:util"; import YAML from "yaml"; +import { + CLI_ARTIFACT_PUBLISH_STEP, + CLI_ARTIFACT_UPLOAD_ACTION, +} from "./cli-artifact-workflow-boundary.mts"; import { SHARED_E2E_JOB_ID } from "./credential-free-tests.mts"; const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", ".."); @@ -439,7 +443,11 @@ export function validateUploadE2eArtifactsInvocations(workflow: WorkflowRecord): if (uses.startsWith(CHECKOUT_LOCAL_UPLOAD_E2E_ARTIFACTS_ACTION)) { errors.push(`${jobName} must not load upload-e2e-artifacts from the target checkout`); } - if (uses.startsWith(UPLOAD_ARTIFACT_ACTION_PREFIX)) { + const isExactCommitCliArtifactUpload = + jobName === "generate-matrix" && + step.name === CLI_ARTIFACT_PUBLISH_STEP && + uses === CLI_ARTIFACT_UPLOAD_ACTION; + if (uses.startsWith(UPLOAD_ARTIFACT_ACTION_PREFIX) && !isExactCommitCliArtifactUpload) { errors.push(`${jobName} must not invoke actions/upload-artifact directly`); } if ( diff --git a/tools/e2e/workflow-boundary.mts b/tools/e2e/workflow-boundary.mts index 186fb4a2c5d..38d6b1796a2 100644 --- a/tools/e2e/workflow-boundary.mts +++ b/tools/e2e/workflow-boundary.mts @@ -42,7 +42,6 @@ import { type OperationsWorkflow, validateE2eOperationsWorkflow, } from "./operations-workflow-boundary.mts"; -import { validatePrepareE2eWorkflowBoundary } from "./prepare-e2e-workflow-boundary.mts"; import { validateRunnerComparisonWorkflowBoundary } from "./runner-comparison-workflow-boundary.mts"; import { validateRunnerPressureWorkflow } from "./runner-pressure-workflow-boundary.mts"; import { validateSandboxOperationsWorkflow } from "./sandbox-operations-workflow-boundary.mts"; @@ -56,6 +55,10 @@ import { UPLOAD_E2E_ARTIFACTS_ACTION, validateUploadE2eArtifactsWorkflowBoundary, } from "./upload-e2e-artifacts-workflow-boundary.mts"; +import { + CLI_ARTIFACT_RESTORE_STEP, + validateE2eWorkspaceBootstrapBoundary, +} from "./workspace-bootstrap-workflow-boundary.mts"; const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", ".."); const DEFAULT_E2E_WORKFLOW_PATH = join(REPO_ROOT, ".github", "workflows", "e2e.yaml"); @@ -1840,9 +1843,10 @@ export function validateRebuildHermesBootstrapBoundary( const steps = asSteps(job.steps); const prepareWorkspace = requireJobStep(errors, jobName, steps, "Prepare E2E workspace"); - if (Object.keys(asRecord(prepareWorkspace?.with)).length !== 0) { - errors.push(`${jobName} workspace preparation must use the default checked-out CLI build`); + if (!isDeepStrictEqual(asRecord(prepareWorkspace?.with), { "build-cli": "false" })) { + errors.push(`${jobName} workspace preparation must defer to the exact-commit CLI artifact`); } + const restoreCli = requireJobStep(errors, jobName, steps, CLI_ARTIFACT_RESTORE_STEP); const installOpenShell = requireJobStep(errors, jobName, steps, "Install OpenShell"); requireRunContains(errors, installOpenShell, "bash scripts/install-openshell.sh"); @@ -1894,14 +1898,18 @@ export function validateRebuildHermesBootstrapBoundary( if ( prepareWorkspace && + restoreCli && installOpenShell && runVitest && !( - steps.indexOf(prepareWorkspace) < steps.indexOf(installOpenShell) && + steps.indexOf(prepareWorkspace) < steps.indexOf(restoreCli) && + steps.indexOf(restoreCli) < steps.indexOf(installOpenShell) && steps.indexOf(installOpenShell) < steps.indexOf(runVitest) ) ) { - errors.push(`${jobName} must build the CLI before installing OpenShell and running Vitest`); + errors.push( + `${jobName} must restore the exact-commit CLI before installing OpenShell and running Vitest`, + ); } return errors; } @@ -4242,7 +4250,7 @@ function validateTrustedE2eDispatchReceipt( export function validateE2eWorkflow(workflowValue: unknown): string[] { const workflow = asRecord(workflowValue); const errors: string[] = []; - errors.push(...validatePrepareE2eWorkflowBoundary(workflow)); + errors.push(...validateE2eWorkspaceBootstrapBoundary(workflow)); errors.push(...validateUploadE2eArtifactsWorkflowBoundary(workflow)); errors.push(...validateHermesDashboardWorkflow(workflow as unknown as HermesDashboardWorkflow)); errors.push(...validateHermesGpuStartupWorkflow(workflow)); diff --git a/tools/e2e/workspace-bootstrap-workflow-boundary.mts b/tools/e2e/workspace-bootstrap-workflow-boundary.mts new file mode 100644 index 00000000000..de1af65c73c --- /dev/null +++ b/tools/e2e/workspace-bootstrap-workflow-boundary.mts @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + CLI_ARTIFACT_RESTORE_STEP, + validateCliArtifactWorkflowBoundary, +} from "./cli-artifact-workflow-boundary.mts"; +import { validatePrepareE2eWorkflowBoundary } from "./prepare-e2e-workflow-boundary.mts"; + +export { CLI_ARTIFACT_RESTORE_STEP }; + +type WorkflowRecord = Record; + +export function validateE2eWorkspaceBootstrapBoundary(workflow: WorkflowRecord): string[] { + return [ + ...validatePrepareE2eWorkflowBoundary(workflow), + ...validateCliArtifactWorkflowBoundary(workflow), + ]; +}