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
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();
}
},
);
});
67 changes: 67 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,67 @@
// 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.each([
["workflow", (workflow: ReturnType<typeof readE2eOperationsWorkflow>) => workflow],
[
"generate-matrix job",
(workflow: ReturnType<typeof readE2eOperationsWorkflow>) => workflow.jobs["generate-matrix"],
],
])("rejects obsolete catalog inputs inherited from the %s environment", (_scope, target) => {
const workflow = readE2eOperationsWorkflow();
target(workflow).env = {
MANAGED_IMAGE_CATALOG: "candidate catalog",
MANAGED_IMAGE_CATALOG_SHA256: "candidate digest",
};

expect(validateE2eOperationsWorkflow(workflow)).toContain(
"Manual PR CLI packaging must not accept obsolete managed-image catalog authority",
);
});

it("rejects an obsolete staged catalog reference in CLI packaging", () => {
const workflow = readE2eOperationsWorkflow();
const packageStep = workflow.jobs["generate-matrix"].steps!.find(
(step) => step.name === "Package exact-commit CLI",
)!;
packageStep.run = `${packageStep.run ?? ""}\ncat pr-managed-image-catalog.json\n`;

expect(validateE2eOperationsWorkflow(workflow)).toContain(
"Manual PR CLI packaging must not accept obsolete managed-image catalog authority",
);
});

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
Loading
Loading