diff --git a/.github/workflows/e2e-vitest-scenarios.yaml b/.github/workflows/e2e-vitest-scenarios.yaml index a1e0551e91f..afd930af581 100644 --- a/.github/workflows/e2e-vitest-scenarios.yaml +++ b/.github/workflows/e2e-vitest-scenarios.yaml @@ -12,7 +12,7 @@ on: default: "" type: string jobs: - description: "Optional comma-separated free-standing live Vitest job ids. Empty runs all jobs only when scenarios is also empty." + description: "Optional comma-separated free-standing live Vitest job ids. Empty runs default-enabled jobs only when scenarios is also empty; explicit-only jobs such as jetson-nvmap-gpu-vitest are skipped unless selected." required: false default: "" type: string @@ -2761,7 +2761,10 @@ jobs: jetson-nvmap-gpu-vitest: needs: generate-matrix - if: ${{ (inputs.jobs == '' && inputs.scenarios == '') || contains(format(',{0},', inputs.jobs), ',jetson-nvmap-gpu-vitest,') || contains(format(',{0},', inputs.scenarios), ',jetson-nvmap-gpu,') }} + # Explicit-only until a stable Jetson runner is available; otherwise full-suite dispatches remain queued forever. + # Required validation path: dispatch with jobs=jetson-nvmap-gpu-vitest or scenarios=jetson-nvmap-gpu. + # Re-enable default dispatch only after a stable Jetson runner exists, then remove this job from FULL_SUITE_EXCLUDED_FREE_STANDING_JOBS. + if: ${{ contains(format(',{0},', inputs.jobs), ',jetson-nvmap-gpu-vitest,') || contains(format(',{0},', inputs.scenarios), ',jetson-nvmap-gpu,') }} runs-on: ${{ vars.JETSON_E2E_RUNNER_LABEL || 'linux-arm64-gpu-jetson-orin-latest-1' }} timeout-minutes: 60 env: @@ -5541,6 +5544,13 @@ jobs: const selectorValidationPassed = needs['generate-matrix']?.result === 'success'; const requestedScenarios = selectorValidationPassed ? rawRequestedScenarios : ''; const requestedJobs = selectorValidationPassed ? rawRequestedJobs : ''; + const explicitOnlySkippedJobs = [ + { + job: 'jetson-nvmap-gpu-vitest', + scenario: 'jetson-nvmap-gpu', + reason: 'default dispatch excludes Jetson until a stable Jetson runner is available', + }, + ]; const scenariosRejected = rawRequestedScenarios && !selectorValidationPassed; const jobsRejected = rawRequestedJobs && !selectorValidationPassed; @@ -5599,7 +5609,7 @@ jobs: ? '✅ All requested jobs passed' : selectiveDispatch ? '✅ All selected jobs passed' - : '✅ All jobs passed'; + : '✅ All default jobs passed'; const status = failed.length > 0 || missingRequested.length > 0 ? '❌ Some jobs failed' @@ -5625,13 +5635,22 @@ jobs: ? '**Requested jobs:** _(selector rejected by workflow validation)_' : requestedJobs ? `**Requested jobs:** \`${requestedJobs}\`` - : '**Requested jobs:** _(default — all free-standing when no scenarios are requested)_', + : '**Requested jobs:** _(default — all default-enabled free-standing jobs; explicit-only jobs such as `jetson-nvmap-gpu-vitest` are skipped unless selected)_', `**Summary:** ${passed.length} passed, ${failed.length} failed, ${cancelled.length} cancelled, ${skipped.length} skipped`, '', '| Job | Result |', '|-----|--------|', ...rows, ]; + if (!selectiveDispatch) { + const skippedJobHints = explicitOnlySkippedJobs + .map( + ({ job, scenario, reason }) => + `\`${job}\` (${reason}; validate with \`jobs=${job}\` or \`scenarios=${scenario}\`)`, + ) + .join(', '); + lines.push('', `> **Explicit-only jobs skipped:** ${skippedJobHints}.`); + } if (failed.length > 0) { const failedNames = failed.map(([name]) => name).join(', '); lines.push('', `> **Failed jobs:** ${failedNames}. Check [run artifacts](${runUrl}) for logs.`); diff --git a/test/e2e-scenario/support-tests/jetson-workflow-boundary.test.ts b/test/e2e-scenario/support-tests/jetson-workflow-boundary.test.ts new file mode 100644 index 00000000000..c1ed8db75c5 --- /dev/null +++ b/test/e2e-scenario/support-tests/jetson-workflow-boundary.test.ts @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it } from "vitest"; +import { + evaluateE2eVitestWorkflowDispatchSelectors, + readFreeStandingJobsInventory, + validateE2eVitestScenariosWorkflowBoundary, +} from "../../../tools/e2e-scenarios/workflow-boundary.mts"; + +describe("Jetson nvmap GPU Vitest workflow boundary", () => { + it("keeps Jetson selectable but excluded from full-suite dispatch", () => { + const inventory = readFreeStandingJobsInventory(); + expect(validateE2eVitestScenariosWorkflowBoundary()).toEqual([]); + expect(inventory.allowedJobs).toContain("jetson-nvmap-gpu-vitest"); + expect(inventory.scenarioToJob.get("jetson-nvmap-gpu")).toBe("jetson-nvmap-gpu-vitest"); + expect(evaluateE2eVitestWorkflowDispatchSelectors({}).selectedFreeStandingJobs).not.toContain( + "jetson-nvmap-gpu-vitest", + ); + }); + + it("runs Jetson only when explicitly selected", () => { + for (const selector of [ + { scenarios: "jetson-nvmap-gpu" }, + { jobs: "jetson-nvmap-gpu-vitest" }, + ]) { + expect(evaluateE2eVitestWorkflowDispatchSelectors(selector)).toMatchObject({ + valid: true, + liveScenariosRuns: false, + selectedFreeStandingJobs: ["jetson-nvmap-gpu-vitest"], + registryScenarios: [], + }); + } + }); + + it("reports default jobs without claiming explicit-only Jetson ran", () => { + expect(validateE2eVitestScenariosWorkflowBoundary()).toEqual([]); + }); +}); diff --git a/tools/e2e-scenarios/workflow-boundary.mts b/tools/e2e-scenarios/workflow-boundary.mts index 8f24e15caec..7fc5008d37d 100644 --- a/tools/e2e-scenarios/workflow-boundary.mts +++ b/tools/e2e-scenarios/workflow-boundary.mts @@ -49,7 +49,9 @@ const COMMON_SECRET_ENV_NAMES = [ const FREE_STANDING_SELECTOR_SPECIAL_CASES = new Set([ "hermes-e2e-vitest", "hermes-root-entrypoint-smoke-vitest", + "jetson-nvmap-gpu-vitest", ]); +const FULL_SUITE_EXCLUDED_FREE_STANDING_JOBS = new Set(["jetson-nvmap-gpu-vitest"]); function asRecord(value: unknown): WorkflowRecord { return value && typeof value === "object" && !Array.isArray(value) @@ -275,7 +277,9 @@ export function evaluateE2eVitestWorkflowDispatchSelectors(input: { return { valid: true, errors: [], - selectedFreeStandingJobs: [...freeStandingVitestJobIds].sort(), + selectedFreeStandingJobs: freeStandingVitestJobIds + .filter((job) => !FULL_SUITE_EXCLUDED_FREE_STANDING_JOBS.has(job)) + .sort(), registryScenarios: [], liveScenariosRuns: true, }; @@ -319,9 +323,12 @@ function requireInput( errors: string[], inputs: WorkflowRecord, name: string, -): void { - if (!Object.hasOwn(inputs, name)) +): WorkflowRecord { + if (!Object.hasOwn(inputs, name)) { errors.push(`workflow_dispatch missing input: ${name}`); + return {}; + } + return asRecord(inputs[name]); } function requireStep( @@ -450,6 +457,13 @@ function freeStandingJobIf(jobName: string, scenarioName?: string): string { return `\${{ (inputs.jobs == '' && inputs.scenarios == '') || contains(format(',{0},', inputs.jobs), ',${jobName},')${scenarioSelector} }}`; } +function explicitOnlyFreeStandingJobIf(jobName: string, scenarioName?: string): string { + const scenarioSelector = scenarioName + ? ` || contains(format(',{0},', inputs.scenarios), ',${scenarioName},')` + : ""; + return `\${{ contains(format(',{0},', inputs.jobs), ',${jobName},')${scenarioSelector} }}`; +} + function validateFreeStandingJobSelector( errors: string[], jobs: WorkflowRecord, @@ -7350,7 +7364,18 @@ export function validateE2eVitestScenariosWorkflowBoundary( const dispatchInputs = asRecord(workflowDispatch.inputs); requireInput(errors, dispatchInputs, "scenarios"); - requireInput(errors, dispatchInputs, "jobs"); + const jobsInput = requireInput(errors, dispatchInputs, "jobs"); + const jobsDescription = stringValue(jobsInput.description); + if (!jobsDescription.includes("default-enabled jobs")) { + errors.push( + "workflow_dispatch jobs input description must say empty dispatch runs default-enabled jobs", + ); + } + if (!jobsDescription.includes("explicit-only jobs")) { + errors.push( + "workflow_dispatch jobs input description must say explicit-only jobs are skipped unless selected", + ); + } if (Object.hasOwn(dispatchInputs, "test_filter")) { errors.push("workflow_dispatch must not expose legacy test_filter input"); } @@ -7749,8 +7774,13 @@ export function validateE2eVitestScenariosWorkflowBoundary( "gateway-health-honest", ); - validateFreeStandingJobSelector(errors, jobs, "jetson-nvmap-gpu-vitest", "jetson-nvmap-gpu"); - + const jetsonJob = asRecord(jobs["jetson-nvmap-gpu-vitest"]); + if (jetsonJob.needs !== "generate-matrix") { + errors.push("jetson-nvmap-gpu-vitest job must depend on generate-matrix"); + } + if (jetsonJob.if !== explicitOnlyFreeStandingJobIf("jetson-nvmap-gpu-vitest", "jetson-nvmap-gpu")) { + errors.push("jetson-nvmap-gpu-vitest job must run only when explicitly selected"); + } validateFreeStandingJobSelector( errors, jobs, @@ -7853,6 +7883,31 @@ export function validateE2eVitestScenariosWorkflowBoundary( "step 'Post Vitest scenario results to PR' run script must include **Requested scenarios:**", ); } + if (!reportScript.includes("All default jobs passed")) { + errors.push( + "step 'Post Vitest scenario results to PR' run script must label empty dispatch as default jobs passed", + ); + } + if (!reportScript.includes("default-enabled free-standing jobs")) { + errors.push( + "step 'Post Vitest scenario results to PR' run script must say empty dispatch uses default-enabled free-standing jobs", + ); + } + if (!reportScript.includes("Explicit-only jobs skipped")) { + errors.push( + "step 'Post Vitest scenario results to PR' run script must list explicit-only skipped jobs on default dispatch", + ); + } + if (!reportScript.includes("jobs=${job}") || !reportScript.includes("jetson-nvmap-gpu-vitest")) { + errors.push( + "step 'Post Vitest scenario results to PR' run script must document the explicit Jetson jobs selector", + ); + } + if (!reportScript.includes("scenarios=${scenario}") || !reportScript.includes("jetson-nvmap-gpu")) { + errors.push( + "step 'Post Vitest scenario results to PR' run script must document the explicit Jetson scenario selector", + ); + } for (const forbidden of [ "toJSON(inputs.pr_number)", "toJSON(inputs.scenarios)",