Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
467eef7
refactor(e2e): route Docker Hub auth through a trusted setup script
laitingsheng Jul 17, 2026
567154c
Merge remote-tracking branch 'origin/main' into refactor/e2e-docker-a…
laitingsheng Jul 17, 2026
1154142
fix(e2e): withhold Docker Hub credentials from alternate checkouts
laitingsheng Jul 17, 2026
48b8ba8
Merge remote-tracking branch 'origin/main' into refactor/e2e-docker-a…
laitingsheng Jul 17, 2026
78091da
ci(e2e): add pinned docker-auth-setup composite action
laitingsheng Jul 17, 2026
2517344
fix(e2e): invoke Docker Hub auth from an immutable pinned action
laitingsheng Jul 17, 2026
7691eb4
Merge remote-tracking branch 'origin/main' into refactor/e2e-docker-a…
laitingsheng Jul 17, 2026
3306a58
fix(e2e): bind Docker auth action provenance
apurvvkumaria Jul 17, 2026
01e4cf0
test(e2e): keep Docker auth boundary linear
apurvvkumaria Jul 17, 2026
da7ad13
test(e2e): tag Docker auth boundary coverage
apurvvkumaria Jul 17, 2026
296b34d
ci(e2e): add pinned host-dependency setup action
laitingsheng Jul 18, 2026
f94c78f
refactor(e2e): install host dependencies from a pinned action
laitingsheng Jul 18, 2026
4def150
fix(e2e): reject non-space-separated host dependency package input
laitingsheng Jul 18, 2026
666e08a
fix(e2e): fail closed on host dependency continue-on-error and re-pin…
laitingsheng Jul 18, 2026
7050aff
Merge branch 'main' into refactor/e2e-docker-auth-setup-helper
cv Jul 18, 2026
93d3f10
test(e2e): exercise host dependency helper
cv Jul 18, 2026
56373b0
merge(main): refresh PR 7079
cv Jul 18, 2026
892f2e1
Merge remote-tracking branch 'origin/main' into codex/salvage-7079-ho…
cv Jul 18, 2026
a032ad9
test(e2e): cover host helper argument guard
cv Jul 18, 2026
5409133
merge: refresh PR #7079 from main
cv Jul 18, 2026
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
43 changes: 43 additions & 0 deletions .github/scripts/docker-auth-setup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/usr/bin/env bash
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

set -euo pipefail

if (($# != 0)); then
echo "::error::Docker auth setup does not accept arguments." >&2
exit 1
fi

docker_config="$(mktemp -d "${RUNNER_TEMP}/docker-config-${GITHUB_JOB}-XXXXXX")"
chmod 700 "${docker_config}"
export DOCKER_CONFIG="${docker_config}"
printf 'DOCKER_CONFIG=%s\n' "${DOCKER_CONFIG}" >>"${GITHUB_ENV}"

if [[ "${DOCKERHUB_AUTH_REQUIRED}" != "1" ]]; then
echo "::notice::Docker Hub credentials are withheld for this ref; continuing with anonymous pulls."
exit 0
fi
if [[ -z "${DOCKERHUB_USERNAME}" || -z "${DOCKERHUB_TOKEN}" ]]; then
echo "::error::Docker Hub credentials are required for trusted E2E runs."
exit 1
fi

auth_marker="${DOCKER_CONFIG}/.nemoclaw-docker-login-attempted"
: >"${auth_marker}"
chmod 600 "${auth_marker}"
login_succeeded=0
for attempt in 1 2 3; do
if printf '%s' "${DOCKERHUB_TOKEN}" | timeout 30s docker login docker.io --username "${DOCKERHUB_USERNAME}" --password-stdin; then
login_succeeded=1
break
fi
if [[ "${attempt}" -lt 3 ]]; then
echo "::warning::Docker Hub login attempt ${attempt} failed; retrying."
sleep 5
fi
done
if [[ "${login_succeeded}" -ne 1 ]]; then
echo "::error::Docker Hub login failed after 3 attempts."
exit 1
fi
35 changes: 1 addition & 34 deletions .github/workflows/e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -306,40 +306,7 @@ jobs:
DOCKERHUB_USERNAME: ${{ github.repository == 'NVIDIA/NemoClaw' && github.ref == 'refs/heads/main' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && secrets.DOCKERHUB_USERNAME || '' }}
DOCKERHUB_TOKEN: ${{ github.repository == 'NVIDIA/NemoClaw' && github.ref == 'refs/heads/main' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && secrets.DOCKERHUB_TOKEN || '' }}
shell: bash
run: |
set -euo pipefail
docker_config="$(mktemp -d "${RUNNER_TEMP}/docker-config-${GITHUB_JOB}-XXXXXX")"
chmod 700 "${docker_config}"
export DOCKER_CONFIG="${docker_config}"
printf 'DOCKER_CONFIG=%s\n' "${DOCKER_CONFIG}" >> "${GITHUB_ENV}"

if [[ "${DOCKERHUB_AUTH_REQUIRED}" != "1" ]]; then
echo "::notice::Docker Hub credentials are withheld for this ref; continuing with anonymous pulls."
exit 0
fi
if [[ -z "${DOCKERHUB_USERNAME}" || -z "${DOCKERHUB_TOKEN}" ]]; then
echo "::error::Docker Hub credentials are required for trusted E2E runs."
exit 1
fi

auth_marker="${DOCKER_CONFIG}/.nemoclaw-docker-login-attempted"
: > "${auth_marker}"
chmod 600 "${auth_marker}"
login_succeeded=0
for attempt in 1 2 3; do
if printf '%s' "${DOCKERHUB_TOKEN}" | timeout 30s docker login docker.io --username "${DOCKERHUB_USERNAME}" --password-stdin; then
login_succeeded=1
break
fi
if [[ "${attempt}" -lt 3 ]]; then
echo "::warning::Docker Hub login attempt ${attempt} failed; retrying."
sleep 5
fi
done
if [[ "${login_succeeded}" -ne 1 ]]; then
echo "::error::Docker Hub login failed after 3 attempts."
exit 1
fi
run: bash .github/scripts/docker-auth-setup.sh
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

- name: Configure live E2E trace directory
env:
Expand Down
50 changes: 22 additions & 28 deletions test/e2e/support/dockerhub-auth-workflow-boundary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ const NO_IMAGE_E2E_JOBS = ["gateway-health-honest", "shared-e2e"] as const;
const AUTH_STEP_NAME = "Authenticate to Docker Hub";
const CLEANUP_STEP_NAME = "Clean up Docker auth";
const CLEANUP_HELPER_RUN = "bash .github/scripts/docker-auth-cleanup.sh";
const AUTH_HELPER_RUN = "bash .github/scripts/docker-auth-setup.sh";
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
const CLEANUP_HELPER_PATH = path.join(REPO_ROOT, ".github", "scripts", "docker-auth-cleanup.sh");
const AUTH_HELPER_PATH = path.join(REPO_ROOT, ".github", "scripts", "docker-auth-setup.sh");

type WorkflowStep = Record<string, unknown> & {
env?: Record<string, unknown>;
Expand Down Expand Up @@ -146,7 +148,7 @@ describe("shared Docker Hub authentication workflow boundary", () => {
);
});

it("rejects trust, isolation, retry, password, and cleanup mapping drift", () => {
it("rejects trust, helper, and cleanup mapping drift", () => {
const errors = validateMutation((workflow) => {
const auth = namedStep(workflow.jobs.live, AUTH_STEP_NAME);
const cleanup = namedStep(workflow.jobs.live, CLEANUP_STEP_NAME);
Expand All @@ -158,20 +160,7 @@ describe("shared Docker Hub authentication workflow boundary", () => {
...auth!.env,
DOCKERHUB_USERNAME: "${{ secrets.DOCKERHUB_USERNAME }}",
};
auth!.run = String(auth!.run)
.replace(
"${RUNNER_TEMP}/docker-config-${GITHUB_JOB}-XXXXXX",
"${GITHUB_WORKSPACE}/docker-config",
)
.replace("for attempt in 1 2 3; do", "for attempt in 1 2; do")
.replace(
'auth_marker="${DOCKER_CONFIG}/.nemoclaw-docker-login-attempted"',
'auth_marker="${GITHUB_WORKSPACE}/login-attempted"',
)
.replace(': > "${auth_marker}"', 'touch "${auth_marker}"')
.replace('chmod 600 "${auth_marker}"', 'chmod 644 "${auth_marker}"')
.replace("--password-stdin", '--password "${DOCKERHUB_TOKEN}"')
.replaceAll("exit 1", "exit 0");
auth!.run = "bash ./unaudited-docker-auth.sh";

cleanup!.if = "success()";
cleanup!.run = `${String(cleanup!.run)} || true`;
Expand All @@ -189,17 +178,7 @@ describe("shared Docker Hub authentication workflow boundary", () => {
expect.arrayContaining([
"canonical Docker Hub auth step must always run so untrusted refs receive an isolated empty Docker config",
"canonical Docker Hub auth must gate DOCKERHUB_USERNAME on the trusted repository, main ref, and scheduled/manual events",
'canonical Docker Hub auth run script must include mktemp -d "${RUNNER_TEMP}/docker-config-${GITHUB_JOB}-XXXXXX"',
"canonical Docker Hub auth directory must not use the checkout workspace",
"canonical Docker Hub auth run script must include for attempt in 1 2 3; do",
'canonical Docker Hub auth run script must include auth_marker="${DOCKER_CONFIG}/.nemoclaw-docker-login-attempted"',
'canonical Docker Hub auth run script must include : > "${auth_marker}"',
'canonical Docker Hub auth run script must include chmod 600 "${auth_marker}"',
"canonical Docker Hub auth must create and protect its login-attempt marker after trusted credential validation and before login",
"canonical Docker Hub auth run script must include --password-stdin",
"canonical Docker Hub auth must pass the token only through --password-stdin",
"canonical Docker Hub auth must fail when trusted credentials are missing",
"canonical Docker Hub auth must fail after exhausting login retries",
`canonical Docker Hub auth step must run only ${AUTH_HELPER_RUN}`,
"live Docker Hub cleanup step must contain exactly name, if, shell, and run",
"live Docker Hub cleanup step must always run",
`live Docker Hub cleanup step must run only ${CLEANUP_HELPER_RUN}`,
Expand Down Expand Up @@ -247,7 +226,8 @@ describe("shared Docker Hub authentication workflow boundary", () => {

it("executes the shared auth script with isolated config and bounded fail-closed retries", () => {
const workflow = loadWorkflow();
const authScript = String(namedStep(workflow.jobs.live, AUTH_STEP_NAME)?.run ?? "");
expect(namedStep(workflow.jobs.live, AUTH_STEP_NAME)?.run).toBe(AUTH_HELPER_RUN);
expect(fs.statSync(AUTH_HELPER_PATH).mode & 0o111).not.toBe(0);
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-docker-auth-script-"));
const fakeBin = path.join(directory, "bin");
const runnerTemp = path.join(directory, "runner-temp");
Expand Down Expand Up @@ -284,7 +264,7 @@ fi
fs.rmSync(callsPath, { force: true });
fs.rmSync(tokensPath, { force: true });
fs.rmSync(githubEnv, { force: true });
return spawnSync("bash", ["-c", authScript], {
return spawnSync(AUTH_HELPER_PATH, [], {
encoding: "utf8",
env: {
...process.env,
Expand Down Expand Up @@ -351,6 +331,20 @@ fi
expect(`${missing.stdout}${missing.stderr}`).toContain(
"Docker Hub credentials are required for trusted E2E runs",
);

const rejectedArgs = spawnSync(AUTH_HELPER_PATH, ["unexpected"], {
encoding: "utf8",
env: {
...process.env,
DOCKERHUB_AUTH_REQUIRED: "0",
GITHUB_ENV: githubEnv,
GITHUB_JOB: "live",
PATH: `${fakeBin}:${process.env.PATH}`,
RUNNER_TEMP: runnerTemp,
},
});
expect(rejectedArgs.status).toBe(1);
expect(`${rejectedArgs.stdout}${rejectedArgs.stderr}`).toContain("does not accept arguments");
} finally {
fs.rmSync(directory, { force: true, recursive: true });
}
Expand Down
86 changes: 6 additions & 80 deletions tools/e2e/workflow-boundary.mts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ const NO_IMAGE_E2E_JOBS = new Set(["gateway-health-honest", SHARED_E2E_JOB_ID]);
const DOCKER_HUB_AUTH_STEP = "Authenticate to Docker Hub";
const DOCKER_HUB_CLEANUP_STEP = "Clean up Docker auth";
const DOCKER_HUB_CLEANUP_RUN = "bash .github/scripts/docker-auth-cleanup.sh";
const DOCKER_HUB_AUTH_RUN = "bash .github/scripts/docker-auth-setup.sh";
const DOCKER_HUB_CLEANUP_KEYS = ["if", "name", "run", "shell"];
// The general E2E workflow runs on schedule/manual dispatch. Its event set is
// intentionally distinct from the reusable image workflow's push/manual boundary.
Expand Down Expand Up @@ -1998,7 +1999,9 @@ function requireCanonicalDockerHubAuthRun(
errors.push("canonical Docker Hub auth step must use bash");
}
if (authStep.uses !== undefined) {
errors.push("canonical Docker Hub auth step must use the audited inline retry script");
errors.push(
"canonical Docker Hub auth step must invoke the audited setup script, not a composite action",
);
}
if (authStep["continue-on-error"] !== undefined) {
errors.push(
Expand Down Expand Up @@ -2029,85 +2032,8 @@ function requireCanonicalDockerHubAuthRun(
errors.push("canonical Docker Hub auth step must expose only its three guarded inputs");
}

const runScript = stringValue(authStep.run);
for (const fragment of [
'mktemp -d "${RUNNER_TEMP}/docker-config-${GITHUB_JOB}-XXXXXX"',
'chmod 700 "${docker_config}"',
'export DOCKER_CONFIG="${docker_config}"',
'if [[ "${DOCKERHUB_AUTH_REQUIRED}" != "1" ]]; then',
"continuing with anonymous pulls",
'if [[ -z "${DOCKERHUB_USERNAME}" || -z "${DOCKERHUB_TOKEN}" ]]; then',
'auth_marker="${DOCKER_CONFIG}/.nemoclaw-docker-login-attempted"',
': > "${auth_marker}"',
'chmod 600 "${auth_marker}"',
"for attempt in 1 2 3; do",
"timeout 30s docker login docker.io",
'--username "${DOCKERHUB_USERNAME}"',
"--password-stdin",
"Docker Hub login failed after 3 attempts",
]) {
if (!runScript.includes(fragment)) {
errors.push(`canonical Docker Hub auth run script must include ${fragment}`);
}
}
if (
!runScript.includes("printf 'DOCKER_CONFIG=%s\\n'") ||
!runScript.includes('"${DOCKER_CONFIG}"') ||
!runScript.includes('>> "${GITHUB_ENV}"')
) {
errors.push(
"canonical Docker Hub auth run script must persist the isolated DOCKER_CONFIG through GITHUB_ENV",
);
}
if (runScript.includes("${{ github.workspace }}") || runScript.includes("GITHUB_WORKSPACE")) {
errors.push("canonical Docker Hub auth directory must not use the checkout workspace");
}
if (/--password(?:=|\s)(?!-stdin\b)/u.test(runScript)) {
errors.push("canonical Docker Hub auth must pass the token only through --password-stdin");
}

const configIndex = runScript.indexOf(
'mktemp -d "${RUNNER_TEMP}/docker-config-${GITHUB_JOB}-XXXXXX"',
);
const trustIndex = runScript.indexOf('if [[ "${DOCKERHUB_AUTH_REQUIRED}" != "1" ]]; then');
const loginIndex = runScript.indexOf("docker login docker.io");
if (configIndex < 0 || trustIndex <= configIndex || loginIndex <= trustIndex) {
errors.push(
"canonical Docker Hub auth must isolate Docker config before evaluating trust and authenticating",
);
}
const missingCredentialsIndex = runScript.indexOf(
'if [[ -z "${DOCKERHUB_USERNAME}" || -z "${DOCKERHUB_TOKEN}" ]]; then',
);
const missingCredentialsEndIndex = runScript.indexOf("\nfi", missingCredentialsIndex);
const markerPathIndex = runScript.indexOf(
'auth_marker="${DOCKER_CONFIG}/.nemoclaw-docker-login-attempted"',
);
const markerCreateIndex = runScript.indexOf(': > "${auth_marker}"');
const markerChmodIndex = runScript.indexOf('chmod 600 "${auth_marker}"');
const retryIndex = runScript.indexOf("for attempt in 1 2 3; do");
const missingCredentialsBlock =
missingCredentialsIndex >= 0 && retryIndex > missingCredentialsIndex
? runScript.slice(missingCredentialsIndex, retryIndex)
: "";
if (!missingCredentialsBlock.includes("exit 1")) {
errors.push("canonical Docker Hub auth must fail when trusted credentials are missing");
}
if (
missingCredentialsEndIndex < 0 ||
markerPathIndex <= missingCredentialsEndIndex ||
markerCreateIndex <= markerPathIndex ||
markerChmodIndex <= markerCreateIndex ||
retryIndex <= markerChmodIndex ||
loginIndex <= retryIndex
) {
errors.push(
"canonical Docker Hub auth must create and protect its login-attempt marker after trusted credential validation and before login",
);
}
const exhaustedLoginIndex = runScript.indexOf("Docker Hub login failed after 3 attempts");
if (exhaustedLoginIndex < 0 || !runScript.slice(exhaustedLoginIndex).includes("exit 1")) {
errors.push("canonical Docker Hub auth must fail after exhausting login retries");
if (stringValue(authStep.run) !== DOCKER_HUB_AUTH_RUN) {
errors.push(`canonical Docker Hub auth step must run only ${DOCKER_HUB_AUTH_RUN}`);
}
}

Expand Down
Loading