Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
63 changes: 1 addition & 62 deletions .github/workflows/e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,6 @@ jobs:
cli_artifact_provenance: ${{ steps.record_cli_artifact.outputs.provenance }}
workload_source: ${{ needs.base-image-publication.outputs.workload_source }}
e2e_credentials_allowed: ${{ steps.e2e_credentials.outputs.allowed }}
managed_image_catalog: ${{ steps.resolve_pr_managed_image_catalog.outputs.catalog }}
matrix: ${{ steps.matrix.outputs.matrix }}
test_matrix: ${{ steps.matrix.outputs.test_matrix }}
hermes_selected: ${{ steps.matrix.outputs.hermes_selected }}
Expand Down Expand Up @@ -668,33 +667,6 @@ jobs:
fi
fi

- id: resolve_pr_managed_image_catalog
name: Resolve exact PR managed-image catalog
if: ${{ inputs.checkout_sha != '' && (inputs.jobs != 'native-runtime-qualification-producer' || inputs.targets != '') }}
env:
BASE_SHA: ${{ inputs.base_sha }}
CANDIDATE_REPOSITORY: ${{ inputs.checkout_repository }}
CANDIDATE_SHA: ${{ inputs.checkout_sha }}
GITHUB_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ inputs.pr_number }}
shell: bash
run: |
set -euo pipefail
catalog_path="${RUNNER_TEMP}/pr-managed-image-catalog.json"
rm -f -- "$catalog_path"
node --experimental-strip-types --no-warnings \
tools/e2e/pr-managed-image-publication.mts "$catalog_path"
if [[ -e "$catalog_path" ]]; then
[[ -f "$catalog_path" && ! -L "$catalog_path" && -s "$catalog_path" ]] || {
echo "::error::trusted PR managed-image catalog is not a nonempty regular file" >&2
exit 1
}
catalog="$(jq -c . "$catalog_path")"
catalog_sha256="$(printf '%s\n' "$catalog" | sha256sum | awk '{print $1}')"
printf 'catalog=%s\n' "$catalog" >>"$GITHUB_OUTPUT"
printf 'catalog_sha256=%s\n' "$catalog_sha256" >>"$GITHUB_OUTPUT"
fi

- name: Check out E2E candidate
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
if: ${{ inputs.checkout_sha == '' || inputs.jobs != 'native-runtime-qualification-producer' || inputs.targets != '' }}
Expand Down Expand Up @@ -772,8 +744,6 @@ jobs:
env:
CANDIDATE_REPOSITORY: ${{ inputs.checkout_repository || github.repository }}
CANDIDATE_SHA: ${{ inputs.checkout_sha || github.sha }}
MANAGED_IMAGE_CATALOG: ${{ steps.resolve_pr_managed_image_catalog.outputs.catalog }}
MANAGED_IMAGE_CATALOG_SHA256: ${{ steps.resolve_pr_managed_image_catalog.outputs.catalog_sha256 }}
RUN_ATTEMPT: ${{ github.run_attempt }}
RUN_ID: ${{ github.run_id }}
WORKFLOW_SHA: ${{ github.workflow_sha }}
Expand Down Expand Up @@ -811,42 +781,11 @@ jobs:
' dist/build-identity.json >/dev/null ||
{ echo "::error::candidate CLI build identity does not match the candidate commit SHA"; exit 1; }

# BEGIN exact managed-image catalog staging
managed_catalog="${RUNNER_TEMP}/pr-managed-image-catalog.json"
artifact_catalog="dist/e2e-managed-image-catalog.json"
[[ ! -e "$artifact_catalog" && ! -L "$artifact_catalog" ]] || {
echo "::error::candidate build created the trusted managed-image catalog path" >&2
echo "::error::candidate build created the managed-image catalog path" >&2
exit 1
}
if [[ -n "$MANAGED_IMAGE_CATALOG" ]]; then
[[ "$MANAGED_IMAGE_CATALOG_SHA256" =~ ^[a-f0-9]{64}$ ]] || {
echo "::error::trusted PR managed-image catalog digest is invalid" >&2
exit 1
}
[[ -f "$managed_catalog" && ! -L "$managed_catalog" && -s "$managed_catalog" ]] || {
echo "::error::trusted PR managed-image catalog is not a nonempty regular file" >&2
exit 1
}
[[ "$(sha256sum "$managed_catalog" | awk '{print $1}')" == "$MANAGED_IMAGE_CATALOG_SHA256" ]] || {
echo "::error::trusted PR managed-image catalog changed after authentication" >&2
exit 1
}
(umask 077 && set -o noclobber && printf '%s\n' "$MANAGED_IMAGE_CATALOG" >"$artifact_catalog")
[[ -f "$artifact_catalog" && ! -L "$artifact_catalog" && -s "$artifact_catalog" ]] || {
echo "::error::packaged PR managed-image catalog is invalid" >&2
exit 1
}
[[ "$(sha256sum "$artifact_catalog" | awk '{print $1}')" == "$MANAGED_IMAGE_CATALOG_SHA256" ]] || {
echo "::error::packaged PR managed-image catalog does not match trusted output" >&2
exit 1
}
else
[[ -z "$MANAGED_IMAGE_CATALOG_SHA256" && ! -e "$managed_catalog" && ! -L "$managed_catalog" ]] || {
echo "::error::managed-image catalog authority is inconsistent" >&2
exit 1
}
fi
# END exact managed-image catalog staging

artifact_dir="${RUNNER_TEMP}/nemoclaw-cli-artifact"
install -d -m 0700 "$artifact_dir"
Expand Down
144 changes: 144 additions & 0 deletions test/e2e/support/cli-artifact-packaging.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// 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 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 } from "../../../tools/e2e/cli-artifact-workflow-boundary.mts";
import { readWorkflow, type Workflow } from "../../helpers/e2e-workflow-contract";

type CatalogInput = "absent" | "file" | "symlink";

const CATALOG_INPUT_WRITERS = {
absent: () => undefined,
file: (catalog: string) => fs.writeFileSync(catalog, "{}\n"),
symlink: (catalog: string) => fs.symlinkSync("missing-catalog.json", catalog),
} satisfies Record<CatalogInput, (catalog: string) => void>;

function runCliArtifactPackaging(catalogInput: CatalogInput) {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "cli-artifact-package-"));
const workspace = path.join(root, "workspace");
const runnerTemp = path.join(root, "runner-temp");
const toolDirectory = path.join(root, "tools");
fs.mkdirSync(workspace);
fs.mkdirSync(runnerTemp);
fs.mkdirSync(toolDirectory);
const systemTar = execFileSync("which", ["tar"], { encoding: "utf8" }).trim();
fs.writeFileSync(
path.join(toolDirectory, "tar"),
`#!/usr/bin/env bash
set -euo pipefail
args=()
for argument in "$@"; do
case "$argument" in
--sort=name|--mtime=@0|--owner=0|--group=0|--numeric-owner) ;;
*) args+=("$argument") ;;
esac
done
exec ${JSON.stringify(systemTar)} "\${args[@]}"
`,
{ mode: 0o755 },
);
fs.writeFileSync(path.join(workspace, "package-lock.json"), '{"lockfileVersion":3}\n');
execFileSync("git", ["init", "--quiet"], { cwd: workspace });
execFileSync("git", ["add", "package-lock.json"], { 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 dist = path.join(workspace, "dist");
const shared = path.join(workspace, "nemoclaw", "dist", "shared");
fs.mkdirSync(dist);
fs.mkdirSync(shared, { recursive: true });
fs.writeFileSync(path.join(dist, "nemoclaw.js"), 'console.log("fixture");\n');
fs.writeFileSync(
path.join(dist, "build-identity.json"),
`${JSON.stringify({ nemoclawVersion: "0.0.0", sourceRevision: candidateSha })}\n`,
);
for (const boundary of [
"openshell-policy-boundary.cjs",
"sandbox-name.cjs",
"snapshot-sanitizer-boundary.cjs",
]) {
fs.writeFileSync(path.join(shared, boundary), "module.exports = {};\n");
}

const catalog = path.join(dist, "e2e-managed-image-catalog.json");
CATALOG_INPUT_WRITERS[catalogInput](catalog);

const workflow = readWorkflow() as Workflow;
const packageStep = workflow.jobs["generate-matrix"]?.steps?.find(
(step) => step.name === CLI_ARTIFACT_PACKAGE_STEP,
);
expect(packageStep?.run).toEqual(expect.any(String));
const result = spawnSync("bash", ["-c", packageStep!.run!], {
cwd: workspace,
encoding: "utf8",
env: {
...process.env,
CANDIDATE_REPOSITORY: "NVIDIA/NemoClaw",
CANDIDATE_SHA: candidateSha,
GITHUB_OUTPUT: path.join(root, "github-output"),
PATH: `${toolDirectory}:${process.env.PATH ?? ""}`,
RUN_ATTEMPT: "1",
RUN_ID: "12345",
RUNNER_ARCH: "X64",
RUNNER_OS: "Linux",
RUNNER_TEMP: runnerTemp,
WORKFLOW_SHA: "d".repeat(40),
},
});
return {
artifactExists: fs.existsSync(path.join(runnerTemp, "nemoclaw-cli-artifact")),
cleanup: () => fs.rmSync(root, { force: true, recursive: true }),
output: `${result.stdout}${result.stderr}`,
result,
};
}

describe("CLI artifact packaging", () => {
it("packages the candidate CLI when no managed-image catalog exists", () => {
const fixture = runCliArtifactPackaging("absent");
try {
expect(fixture.result.status, fixture.output).toBe(0);
expect(fixture.artifactExists).toBe(true);
} finally {
fixture.cleanup();
}
});

it.each(["file", "symlink"] as const)(
"rejects a candidate-created managed-image catalog %s before artifact creation",
(catalogInput) => {
const fixture = runCliArtifactPackaging(catalogInput);
try {
expect(fixture.result.status, fixture.output).not.toBe(0);
expect(fixture.output).toContain("candidate build created the managed-image catalog path");
expect(fixture.artifactExists).toBe(false);
} finally {
fixture.cleanup();
}
},
);
});
37 changes: 37 additions & 0 deletions test/e2e/support/pr-managed-image-workflow-boundary.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { describe, expect, it } from "vitest";

import {
readE2eOperationsWorkflow,
validateE2eOperationsWorkflow,
} from "../../../tools/e2e/operations-workflow-boundary.mts";

describe("manual PR managed-image workflow boundary", () => {
it("rejects restoration of the obsolete manual PR catalog resolver", () => {
const workflow = readE2eOperationsWorkflow();
const matrixJob = workflow.jobs["generate-matrix"];
matrixJob.outputs!.managed_image_catalog =
"${{ steps.resolve_pr_managed_image_catalog.outputs.catalog }}";
const checkoutIndex = matrixJob.steps!.findIndex(
(step) => step.name === "Check out E2E candidate",
);
matrixJob.steps!.splice(checkoutIndex, 0, {
id: "resolve_pr_managed_image_catalog",
name: "Resolve exact PR managed-image catalog",
shell: "bash",
run: "node tools/e2e/pr-managed-image-publication.mts catalog.json",
});
const packageStep = matrixJob.steps!.find((step) => step.name === "Package exact-commit CLI")!;
packageStep.env!.MANAGED_IMAGE_CATALOG =
"${{ steps.resolve_pr_managed_image_catalog.outputs.catalog }}";

expect(validateE2eOperationsWorkflow(workflow)).toEqual(
expect.arrayContaining([
"Manual PR E2E must not resolve an exact candidate managed-image catalog",
"Manual PR CLI packaging must not accept obsolete managed-image catalog authority",
]),
);
});
});
6 changes: 3 additions & 3 deletions tools/e2e/cli-artifact-workflow-boundary.mts
Original file line number Diff line number Diff line change
Expand Up @@ -293,9 +293,6 @@ function validateProducer(errors: string[], producer: WorkflowRecord): void {
!isDeepStrictEqual(record(packageStep.env), {
CANDIDATE_REPOSITORY: "${{ inputs.checkout_repository || github.repository }}",
CANDIDATE_SHA: "${{ inputs.checkout_sha || github.sha }}",
MANAGED_IMAGE_CATALOG: "${{ steps.resolve_pr_managed_image_catalog.outputs.catalog }}",
MANAGED_IMAGE_CATALOG_SHA256:
"${{ steps.resolve_pr_managed_image_catalog.outputs.catalog_sha256 }}",
RUN_ATTEMPT: "${{ github.run_attempt }}",
RUN_ID: "${{ github.run_id }}",
WORKFLOW_SHA: "${{ github.workflow_sha }}",
Expand All @@ -311,6 +308,9 @@ function validateProducer(errors: string[], producer: WorkflowRecord): void {
'[[ -f "$required_file" && ! -L "$required_file" && -s "$required_file" ]]',
"sandbox-name.cjs",
'[[ -f "$boundary_path" && ! -L "$boundary_path" && -s "$boundary_path" ]]',
'artifact_catalog="dist/e2e-managed-image-catalog.json"',
'[[ ! -e "$artifact_catalog" && ! -L "$artifact_catalog" ]]',
"candidate build created the managed-image catalog path",

".sourceRevision == $candidateSha",
"candidate CLI build identity does not match the candidate commit SHA",
Expand Down
48 changes: 9 additions & 39 deletions tools/e2e/operations-workflow-boundary.mts
Original file line number Diff line number Diff line change
Expand Up @@ -289,9 +289,6 @@ function validateManualPrDispatch(errors: string[], workflow: OperationsWorkflow
(step) => step.name === "Authenticate manual PR dispatch",
);
const checkoutIndex = steps.findIndex((step) => step.name === "Check out E2E candidate");
const managedCatalogResolverIndex = steps.findIndex(
(step) => step.id === "resolve_pr_managed_image_catalog",
);
const validationIndex = steps.findIndex((step) => step.name === "Validate manual PR checkout");
const credentialAuthorizationIndex = steps.findIndex(
(step) => step.name === "Authorize E2E credentials",
Expand All @@ -312,51 +309,24 @@ function validateManualPrDispatch(errors: string[], workflow: OperationsWorkflow
errors.push("Manual PR authorization and validation must surround checkout before preparation");
}

const managedCatalogResolver =
managedCatalogResolverIndex >= 0 ? steps[managedCatalogResolverIndex] : {};
if (
matrixJob.outputs?.managed_image_catalog !==
"${{ steps.resolve_pr_managed_image_catalog.outputs.catalog }}" ||
managedCatalogResolverIndex <= authenticationIndex ||
managedCatalogResolverIndex >= checkoutIndex ||
managedCatalogResolver.if !==
"${{ inputs.checkout_sha != '' && (inputs.jobs != 'native-runtime-qualification-producer' || inputs.targets != '') }}" ||
managedCatalogResolver.shell !== "bash" ||
!isDeepStrictEqual(managedCatalogResolver.env, {
BASE_SHA: "${{ inputs.base_sha }}",
CANDIDATE_REPOSITORY: "${{ inputs.checkout_repository }}",
CANDIDATE_SHA: "${{ inputs.checkout_sha }}",
GITHUB_TOKEN: "${{ github.token }}",
PR_NUMBER: "${{ inputs.pr_number }}",
})
matrixJob.outputs?.managed_image_catalog !== undefined ||
steps.some((step) => step.id === "resolve_pr_managed_image_catalog")
) {
errors.push("Manual PR managed-image catalog must be authenticated before candidate checkout");
}
const managedCatalogResolverSource = String(managedCatalogResolver.run ?? "");
for (const fragment of [
"tools/e2e/pr-managed-image-publication.mts",
"catalog=",
"catalog_sha256=",
]) {
if (!managedCatalogResolverSource.includes(fragment)) {
errors.push(`Manual PR managed-image catalog resolver must retain ${fragment}`);
}
errors.push("Manual PR E2E must not resolve an exact candidate managed-image catalog");
}

const packageCli = packageIndex >= 0 ? steps[packageIndex] : {};
const packageSource = String(packageCli.run ?? "");
if (
packageIndex <= prepareIndex ||
packageCli.env?.MANAGED_IMAGE_CATALOG !==
"${{ steps.resolve_pr_managed_image_catalog.outputs.catalog }}" ||
packageCli.env?.MANAGED_IMAGE_CATALOG_SHA256 !==
"${{ steps.resolve_pr_managed_image_catalog.outputs.catalog_sha256 }}" ||
!packageSource.includes("# BEGIN exact managed-image catalog staging") ||
!packageSource.includes("# END exact managed-image catalog staging") ||
!packageSource.includes("trusted PR managed-image catalog changed after authentication") ||
!packageSource.includes("packaged PR managed-image catalog does not match trusted output")
packageCli.env?.MANAGED_IMAGE_CATALOG !== undefined ||
packageCli.env?.MANAGED_IMAGE_CATALOG_SHA256 !== undefined ||
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
packageSource.includes("pr-managed-image-catalog.json")
) {
errors.push("Manual PR managed-image catalog must be sealed into the CLI artifact");
errors.push(
"Manual PR CLI packaging must not accept obsolete managed-image catalog authority",
);
}

const authentication = authenticationIndex >= 0 ? steps[authenticationIndex] : {};
Expand Down
Loading