Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 23 additions & 4 deletions .github/workflows/e2e-vitest-scenarios.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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'
Expand All @@ -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.`);
Expand Down
39 changes: 39 additions & 0 deletions test/e2e-scenario/support-tests/jetson-workflow-boundary.test.ts
Original file line number Diff line number Diff line change
@@ -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([]);
});
});
67 changes: 61 additions & 6 deletions tools/e2e-scenarios/workflow-boundary.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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,
};
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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");
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)",
Expand Down
Loading