From 5366af9025d84e90efbbe4734767aff3df27991e Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Sat, 8 Aug 2026 14:29:36 -0700 Subject: [PATCH 01/13] ci(workflows): reduce duplicate workflow tests --- .github/workflows/regression-e2e.yaml | 187 +--- ci/source-shape-test-budget.json | 215 ----- test/candidate-compat.test.ts | 98 --- .../ci-cli-coverage-pi-tools-workflow.test.ts | 121 --- .../cloudflared-update-check-workflow.test.ts | 18 - test/code-scanning-workflow.test.ts | 140 --- test/e2e-main-retry-workflow.test.ts | 110 --- test/e2e-release-gate-workflow.test.ts | 14 - test/e2e/support/e2e-workflow.test.ts | 111 --- .../sandbox-name-workflow-boundary.test.ts | 5 +- test/macos-e2e-workflow-boundary.test.ts | 93 -- test/openclaw-dependency-review.test.ts | 48 - test/openclaw-locked-install.test.ts | 54 -- ...enshell-e2e-qualification-workflow.test.ts | 65 -- test/pr-limit-policy.test.ts | 43 - test/pr-workflow-contract.test.ts | 817 ------------------ test/regression-e2e-workflow.test.ts | 165 ---- test/release-lkg-brev-image.test.ts | 26 - test/reviewed-npm-audit-workflow.test.ts | 126 --- test/wechat-runtime-audit-workflow.test.ts | 64 -- 20 files changed, 11 insertions(+), 2509 deletions(-) delete mode 100644 test/ci-cli-coverage-pi-tools-workflow.test.ts delete mode 100644 test/e2e-main-retry-workflow.test.ts delete mode 100644 test/macos-e2e-workflow-boundary.test.ts delete mode 100644 test/openshell-e2e-qualification-workflow.test.ts delete mode 100644 test/pr-limit-policy.test.ts delete mode 100644 test/regression-e2e-workflow.test.ts diff --git a/.github/workflows/regression-e2e.yaml b/.github/workflows/regression-e2e.yaml index b7ddcc68922..e47e56871d7 100644 --- a/.github/workflows/regression-e2e.yaml +++ b/.github/workflows/regression-e2e.yaml @@ -21,7 +21,7 @@ on: jobs: description: >- Comma-separated regression job names to run (empty = all). - Valid: model-router-provider-routed-inference-e2e,openclaw-plugin-runtime-exdev-e2e,whatsapp-qr-compact-e2e + Valid: whatsapp-qr-compact-e2e required: false type: string default: "" @@ -39,9 +39,8 @@ concurrency: jobs: select_regression_jobs: runs-on: ubuntu-latest + timeout-minutes: 5 outputs: - model_router_provider_routed_inference: ${{ steps.select.outputs.model_router_provider_routed_inference }} - openclaw_plugin_runtime_exdev: ${{ steps.select.outputs.openclaw_plugin_runtime_exdev }} whatsapp_qr_compact: ${{ steps.select.outputs.whatsapp_qr_compact }} steps: - id: select @@ -50,180 +49,14 @@ jobs: run: | set -euo pipefail normalized="$(printf '%s' "$JOBS" | tr -d '[:space:]')" - - includes_job() { - case ",${normalized}," in - *",$1,"*) return 0 ;; - *) return 1 ;; - esac - } - - if [ -z "$normalized" ] || includes_job "model-router-provider-routed-inference-e2e"; then - echo "model_router_provider_routed_inference=true" >> "$GITHUB_OUTPUT" - else - echo "model_router_provider_routed_inference=false" >> "$GITHUB_OUTPUT" - fi - - if [ -z "$normalized" ] || includes_job "openclaw-plugin-runtime-exdev-e2e"; then - echo "openclaw_plugin_runtime_exdev=true" >> "$GITHUB_OUTPUT" - else - echo "openclaw_plugin_runtime_exdev=false" >> "$GITHUB_OUTPUT" - fi - - if [ -z "$normalized" ] || includes_job "whatsapp-qr-compact-e2e"; then - echo "whatsapp_qr_compact=true" >> "$GITHUB_OUTPUT" - else - echo "whatsapp_qr_compact=false" >> "$GITHUB_OUTPUT" - fi - - # ── Model Router provider-routed inference E2E ───────────────── - # Coverage guard for #3255. Model Router onboard must generate a routed - # provider that can answer through inference.local instead of returning - # HTTP 503 / "inference service unavailable" after a successful onboard. - model-router-provider-routed-inference-e2e: - needs: select_regression_jobs - if: >- - github.repository == 'NVIDIA/NemoClaw' && - needs.select_regression_jobs.outputs.model_router_provider_routed_inference == 'true' - runs-on: ubuntu-latest - timeout-minutes: 45 - permissions: - contents: read - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Prepare E2E workspace - uses: NVIDIA/NemoClaw/.github/actions/prepare-e2e@f6304bc25fc35bfaa441c8c2fbfee38f72805a75 - - - name: Run Model Router provider-routed inference E2E test - env: - NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} - NEMOCLAW_NON_INTERACTIVE: "1" - NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1" - NEMOCLAW_RUN_LIVE_E2E: "1" - run: npx vitest run --project e2e-live test/e2e/live/model-router-provider-routed-inference.test.ts --silent=false --reporter=default - - - name: Upload Model Router provider-routed inference logs on failure - if: failure() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: model-router-provider-routed-inference-logs - path: | - /tmp/nemoclaw-e2e-model-router-onboard.log - /tmp/nemoclaw-e2e-model-router-health.log - /tmp/nemoclaw-e2e-model-router-response.log - if-no-files-found: ignore - - # ── OpenClaw release-baseline custom-plugin E2E ──────────────── - # The exact v0.0.71 baseline runs in parallel with the current lifecycle - # contract so release provenance does not extend the EXDEV critical path. - openclaw-plugin-runtime-exdev-release-e2e: - needs: select_regression_jobs - if: >- - github.repository == 'NVIDIA/NemoClaw' && - needs.select_regression_jobs.outputs.openclaw_plugin_runtime_exdev == 'true' - runs-on: ubuntu-latest - permissions: - contents: read - timeout-minutes: 55 - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Setup Node - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: "22" - cache: npm - - - name: Install root dependencies - run: npm ci --ignore-scripts - - - name: Build CLI - run: npm run build:cli - - - name: Run OpenClaw custom-plugin release baseline Vitest test - env: - E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts/live/openclaw-plugin-runtime-exdev-release - E2E_TARGET_ID: openclaw-plugin-runtime-exdev-release - NEMOCLAW_RUN_LIVE_E2E: "1" - NEMOCLAW_SANDBOX_NAME: e2e-oc-exdev-rel - run: | - set -euo pipefail - npx vitest run --project e2e-live \ - test/e2e/live/openclaw-plugin-runtime-exdev.test.ts \ - -t release-baseline \ - --silent=false --reporter=default - - - name: Upload OpenClaw plugin release baseline artifacts - if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: openclaw-plugin-runtime-exdev-release-artifacts - path: e2e-artifacts/live/openclaw-plugin-runtime-exdev-release/ - include-hidden-files: false - if-no-files-found: ignore - retention-days: 14 - - # ── OpenClaw current lifecycle and runtime-deps EXDEV E2E ───── - # Coverage guard for #6108 / #3513 / #3127. On Ubuntu/OpenShell sandbox - # layouts where /tmp and /sandbox can live on different filesystems, the - # runtime dependency replacement must complete without EXDEV failures. - openclaw-plugin-runtime-exdev-e2e: - needs: select_regression_jobs - if: >- - github.repository == 'NVIDIA/NemoClaw' && - needs.select_regression_jobs.outputs.openclaw_plugin_runtime_exdev == 'true' - runs-on: ubuntu-latest - permissions: - contents: read - # Two bounded 25-minute onboards plus the 20-minute rebuild and 15-minute - # Vitest buffer need 85 minutes; allow 20 more for setup and teardown. - timeout-minutes: 105 - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Setup Node - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: "22" - cache: npm - - - name: Install root dependencies - run: npm ci --ignore-scripts - - - name: Build CLI - run: npm run build:cli - - - name: Run OpenClaw custom-plugin lifecycle and runtime-deps EXDEV Vitest test - env: - E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts/live/openclaw-plugin-runtime-exdev - E2E_TARGET_ID: openclaw-plugin-runtime-exdev - NEMOCLAW_RUN_LIVE_E2E: "1" - run: | - set -euo pipefail - npx vitest run --project e2e-live \ - test/e2e/live/openclaw-plugin-runtime-exdev.test.ts \ - -t current-lifecycle \ - --silent=false --reporter=default - - - name: Upload OpenClaw plugin runtime-deps EXDEV artifacts - if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: openclaw-plugin-runtime-exdev-artifacts - path: e2e-artifacts/live/openclaw-plugin-runtime-exdev/ - include-hidden-files: false - if-no-files-found: ignore - retention-days: 14 + case ",${normalized}," in + ,|*,whatsapp-qr-compact-e2e,*) + echo "whatsapp_qr_compact=true" >> "$GITHUB_OUTPUT" + ;; + *) + echo "whatsapp_qr_compact=false" >> "$GITHUB_OUTPUT" + ;; + esac # ── WhatsApp compact-QR reporter-workflow E2E ────────────────── # Coverage guard for #4522. Drives the real @openclaw/whatsapp + diff --git a/ci/source-shape-test-budget.json b/ci/source-shape-test-budget.json index c4ba8e1c8ef..8711e598e66 100644 --- a/ci/source-shape-test-budget.json +++ b/ci/source-shape-test-budget.json @@ -41,41 +41,11 @@ "test": "matches the bundled local-inference host-gateway ports (#5744)", "category": "compatibility" }, - { - "file": "test/candidate-compat.test.ts", - "test": "keeps the manual controller read-only and runs digest-bound deterministic and live lanes (#6691)", - "category": "security" - }, - { - "file": "test/ci-cli-coverage-pi-tools-workflow.test.ts", - "test": "installs pinned fd and ripgrep before CLI coverage can invoke Pi", - "category": "security" - }, - { - "file": "test/cloudflared-update-check-workflow.test.ts", - "test": "keeps automatic and on-demand update checks reachable and credential-free", - "category": "security" - }, { "file": "test/code-change-considerations.test.ts", "test": "keeps the stage-neutral questions in one concise owner", "category": "compatibility" }, - { - "file": "test/code-scanning-workflow.test.ts", - "test": "groups CodeQL action updates so Dependabot keeps the shared revision synchronized", - "category": "security" - }, - { - "file": "test/code-scanning-workflow.test.ts", - "test": "keeps every CodeQL action on one immutable revision", - "category": "security" - }, - { - "file": "test/code-scanning-workflow.test.ts", - "test": "runs only the trusted converter and keeps scanner status separate from conversion failures (#6959)", - "category": "security" - }, { "file": "test/corporate-ca-build-tls-anchor.test.ts", "test": "declares exactly one corporate CA build arg so onboard patching stays unambiguous", @@ -116,11 +86,6 @@ "test": "keeps installed fixture dependencies on exact versions", "category": "security" }, - { - "file": "test/e2e-release-gate-workflow.test.ts", - "test": "replaces legacy target_ref dispatches with the validated checkout contract", - "category": "security" - }, { "file": "test/e2e/live/hermes-e2e.test.ts", "test": "hermes-e2e: install.sh onboards Hermes and proves health plus live inference", @@ -226,11 +191,6 @@ "test": "builds the policy boundary before semantic collection and CLI compilation", "category": "compatibility" }, - { - "file": "test/e2e/support/e2e-workflow.test.ts", - "test": "derives test selectors from code and workflow jobs from workflow metadata", - "category": "compatibility" - }, { "file": "test/e2e/support/e2e-workflow.test.ts", "test": "rejects channels stop/start workflow-boundary drift for secret and artifact handling", @@ -326,21 +286,6 @@ "test": "keeps fork-safe labeling inside the trusted metadata boundary", "category": "security" }, - { - "file": "test/macos-e2e-workflow-boundary.test.ts", - "test": "keeps secret-bearing live E2E on trusted main-branch code", - "category": "security" - }, - { - "file": "test/macos-e2e-workflow-boundary.test.ts", - "test": "pins the macOS artifact publisher to an immutable action", - "category": "security" - }, - { - "file": "test/macos-e2e-workflow-boundary.test.ts", - "test": "keeps gateway lifecycle coverage on supported Apple Silicon macOS", - "category": "compatibility" - }, { "file": "test/messaging-image-env-contract.test.ts", "test": "%s keeps the full plan in build processes but not final runtime environments (#5896)", @@ -406,21 +351,11 @@ "test": "copies the legacy OpenClaw remediation helper before the base build invokes it", "category": "security" }, - { - "file": "test/openclaw-dependency-review.test.ts", - "test": "runs and gates the real patched-distribution harness only from trusted main code", - "category": "security" - }, { "file": "test/openclaw-lifecycle-policy.test.ts", "test": "cross-checks the allowlist against every production archive install boundary", "category": "security" }, - { - "file": "test/openclaw-locked-install.test.ts", - "test": "audits the same lock and rebuilds the base when its graph changes", - "category": "security" - }, { "file": "test/openclaw-locked-install.test.ts", "test": "fails closed on missing required packages and symlinked package roots", @@ -451,16 +386,6 @@ "test": "rejects symlinked package manifests", "category": "security" }, - { - "file": "test/reviewed-npm-audit-workflow.test.ts", - "test": "executes the trusted driver and helper against explicit target inputs", - "category": "security" - }, - { - "file": "test/reviewed-npm-audit-workflow.test.ts", - "test": "runs PR audits from trusted code and keeps the main audit on the checked-in action", - "category": "security" - }, { "file": "test/platform-vitest-main-workflow.test.ts", "test": "pins and verifies the Node.js archive in the trusted WSL helper", @@ -561,121 +486,11 @@ "test": "packages the dormant managed-bootstrap native boundary for every agent image", "category": "security" }, - { - "file": "test/pr-limit-policy.test.ts", - "test": "keeps contributor guidance aligned with the enforced maintainer exemption", - "category": "compatibility" - }, - { - "file": "test/pr-workflow-contract.test.ts", - "test": "does not persist checkout credentials in PR or main jobs", - "category": "security" - }, - { - "file": "test/pr-workflow-contract.test.ts", - "test": "does not run npm lifecycle scripts during CI dependency installs", - "category": "security" - }, - { - "file": "test/pr-workflow-contract.test.ts", - "test": "folds hermetic E2E support and Ollama proxy coverage into existing Vitest lanes", - "category": "security" - }, - { - "file": "test/pr-workflow-contract.test.ts", - "test": "keeps the installer verifier inside the trusted composite action", - "category": "security" - }, - { - "file": "test/openshell-e2e-qualification-workflow.test.ts", - "test": "keeps installer verification independent from full E2E qualification", - "category": "security" - }, - { - "file": "test/pr-workflow-contract.test.ts", - "test": "keeps the trusted test-size guard closed around budget policy changes", - "category": "security" - }, - { - "file": "test/pr-workflow-contract.test.ts", - "test": "pins downloaded CI tooling to reviewed integrity", - "category": "security" - }, - { - "file": "test/pr-workflow-contract.test.ts", - "test": "publishes coverage only from same-repository code (#6692)", - "category": "security" - }, - { - "file": "test/pr-workflow-contract.test.ts", - "test": "records the Dependabot DCO bypass as a successful required job", - "category": "security" - }, - { - "file": "test/pr-workflow-contract.test.ts", - "test": "reruns installer hash verification after a pull request base retarget", - "category": "security" - }, - { - "file": "test/pr-workflow-contract.test.ts", - "test": "reuses the same shared CI actions in PR and main workflows", - "category": "security" - }, - { - "file": "test/pr-workflow-contract.test.ts", - "test": "routes only code-changing PRs through the code-check path", - "category": "compatibility" - }, - { - "file": "test/pr-workflow-contract.test.ts", - "test": "runs pull request installer verification from immutable trusted code", - "category": "security" - }, - { - "file": "test/pr-workflow-contract.test.ts", - "test": "preserves repository validation file routing, command scopes, and compatibility aliases", - "category": "compatibility" - }, - { - "file": "test/pr-workflow-contract.test.ts", - "test": "runs the source-shape guard for root and co-located tests", - "category": "compatibility" - }, - { - "file": "test/pr-workflow-contract.test.ts", - "test": "scopes pre-push typechecks to project and transitive inputs", - "category": "compatibility" - }, - { - "file": "test/regression-e2e-workflow.test.ts", - "test": "prepares every discovered non-hermetic Vitest job before execution (#6692)", - "category": "security" - }, - { - "file": "test/regression-e2e-workflow.test.ts", - "test": "runs the OpenClaw custom-plugin lifecycle and EXDEV guard in a secret-free lane", - "category": "security" - }, - { - "file": "test/regression-e2e-workflow.test.ts", - "test": "runs WhatsApp compact QR through Vitest instead of the retired shell script", - "category": "compatibility" - }, - { - "file": "test/regression-e2e-workflow.test.ts", - "test": "stages the public NVIDIA key for the Model Router's NVIDIA credential", - "category": "security" - }, { "file": "test/release-latest-tag-workflow.test.ts", "test": "binds latest promotion to the exact GitHub-verified tag object", "category": "security" }, - { - "file": "test/release-lkg-brev-image.test.ts", - "test": "keeps LKG dispatch inside the trusted secret boundary (#6772)", - "category": "security" - }, { "file": "test/repro-4538-raw-doctor-perms.test.ts", "test": "emitted openclaw() guard restores the contract AND preserves a nonzero exit", @@ -755,36 +570,6 @@ "file": "test/growth-guardrails-workflow-boundary.test.ts", "test": "flags %s", "category": "security" - }, - { - "file": "test/wechat-runtime-audit-workflow.test.ts", - "test": "makes the trusted audit required in PR and main workflows", - "category": "security" - }, - { - "file": "test/e2e-main-retry-workflow.test.ts", - "test": "subscribes only to completed E2E workflow runs", - "category": "security" - }, - { - "file": "test/e2e-main-retry-workflow.test.ts", - "test": "accepts only trusted main push attempts one through three", - "category": "security" - }, - { - "file": "test/e2e-main-retry-workflow.test.ts", - "test": "uses one source-run concurrency identity and least privileges", - "category": "security" - }, - { - "file": "test/e2e-main-retry-workflow.test.ts", - "test": "checks out trusted controller code and invokes the bounded helper", - "category": "security" - }, - { - "file": "test/e2e-main-retry-workflow.test.ts", - "test": "runs the bounded evidence-upload step after evaluation failure", - "category": "security" } ] } diff --git a/test/candidate-compat.test.ts b/test/candidate-compat.test.ts index 6536e4fd6a3..eb0a2620590 100644 --- a/test/candidate-compat.test.ts +++ b/test/candidate-compat.test.ts @@ -81,104 +81,6 @@ function receipt(overrides: Partial = {}): CandidateReceipt { } describe("OpenShell candidate compatibility contract", () => { - // source-shape-contract: security -- The controller is read-only and candidate code is isolated from provenance resolution. - it("keeps the manual controller read-only and runs digest-bound deterministic and live lanes (#6691)", () => { - const source = readFileSync(resolve(".github/workflows/candidate-compatibility.yaml"), "utf8"); - const workflow = parseYaml(source) as { - jobs: Record< - string, - { - permissions?: Record; - steps?: Array<{ - env?: Record; - id?: string; - if?: string; - name?: string; - run?: string; - with?: Record; - "working-directory"?: string; - }>; - } - >; - on: { workflow_dispatch: { inputs: Record } }; - permissions: Record; - }; - const evidence = workflow.jobs.evidence; - const liveSteps = workflow.jobs.live?.steps ?? []; - const finalize = evidence?.steps?.find((step) => step.name === "Finalize auditable evidence"); - const enforce = evidence?.steps?.find((step) => step.name === "Enforce aggregate result"); - const uploadLiveEvidence = workflow.jobs.live?.steps?.find( - (step) => step.name === "Upload live evidence", - ); - const artifactSafety = workflow.jobs.live?.steps?.find( - (step) => step.name === "Validate final OpenShell gateway auth contract artifacts", - ); - const recordLiveResult = workflow.jobs.live?.steps?.find( - (step) => step.name === "Record receipt-bound live result", - ); - expect(Object.keys(workflow.on.workflow_dispatch.inputs).sort()).toEqual([ - "candidate", - "component", - "nemoclaw_ref", - ]); - expect(workflow.permissions).toEqual({ contents: "read" }); - expect(Object.keys(workflow.jobs).sort()).toEqual([ - "deterministic", - "evidence", - "live", - "resolve", - ]); - expect(source).toContain("candidate compatibility must be dispatched from main"); - expect(source).toContain("path: controller"); - expect(source).toContain("path: candidate-source"); - expect(source).toContain("RESOLUTION_ID: ${{ needs.resolve.outputs.resolution_id }}"); - expect(source).toContain("verify-invocations"); - expect(source).toContain("openshell-gateway-auth-source-contract.test.ts"); - expect(artifactSafety).toMatchObject({ - env: { - E2E_ARTIFACT_DIR: - "${{ github.workspace }}/candidate-source/e2e-artifacts/live/openshell-gateway-auth-contract", - }, - id: "artifact_safety", - if: "${{ always() }}", - run: 'node --experimental-strip-types --no-warnings controller/tools/e2e/openshell-gateway-auth-artifact-safety.mts "$E2E_ARTIFACT_DIR"', - }); - expect(liveSteps.findIndex((step) => step.id === "live_test")).toBeLessThan( - liveSteps.indexOf(recordLiveResult!), - ); - expect(liveSteps.indexOf(recordLiveResult!)).toBeLessThan(liveSteps.indexOf(artifactSafety!)); - expect(liveSteps.indexOf(artifactSafety!)).toBeLessThan(liveSteps.indexOf(uploadLiveEvidence!)); - expect(uploadLiveEvidence?.with?.path).toBe( - [ - "candidate-results/live-openshell-gateway-auth-contract.json", - "candidate-live-observed.json", - "${{ steps.artifact_safety.outcome == 'success' && steps.artifact_safety.outputs.approved_path || '' }}", - "", - ].join("\n"), - ); - expect(evidence?.permissions).toEqual({ actions: "read", contents: "read" }); - expect(finalize?.id).toBe("finalize"); - expect(finalize?.env).toMatchObject({ - GH_TOKEN: "${{ github.token }}", - RUN_ATTEMPT: "${{ github.run_attempt }}", - RUN_ID: "${{ github.run_id }}", - RUN_URL: - "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}", - }); - expect(finalize?.run).toContain("actions/runs/$RUN_ID/attempts/$RUN_ATTEMPT/jobs?per_page=100"); - expect(finalize?.run).toContain("Number.isSafeInteger(job.id)"); - expect(finalize?.run).toContain("job.name === expectedJobName"); - expect(finalize?.run).toContain("job.run_id === runId"); - expect(finalize?.run).toContain("job.run_attempt === runAttempt"); - expect(finalize?.run).toContain("matches.length === 1"); - expect(finalize?.run).not.toContain("html_url"); - expect(enforce?.run).toContain("::error title=Candidate installer compatibility failed::See"); - expect(enforce?.run).toContain("::error title=Candidate live compatibility failed::See"); - expect(enforce?.run).not.toContain('test "$DETERMINISTIC_RESULT"'); - expect(enforce?.if).toBe("${{ always() }}"); - expect(source).not.toMatch(/\b(?:git push|gh pr|npm publish|docker push)\b/u); - }); - it("links failed evidence to validated jobs and falls back to the workflow run", () => { const source = readFileSync(resolve(".github/workflows/candidate-compatibility.yaml"), "utf8"); const workflow = parseYaml(source) as { diff --git a/test/ci-cli-coverage-pi-tools-workflow.test.ts b/test/ci-cli-coverage-pi-tools-workflow.test.ts deleted file mode 100644 index 0e44144c0f0..00000000000 --- a/test/ci-cli-coverage-pi-tools-workflow.test.ts +++ /dev/null @@ -1,121 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { spawnSync } from "node:child_process"; -import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { describe, expect, it } from "vitest"; - -import { - type CompositeAction, - readYaml, - type WorkflowJob, - type WorkflowStep, -} from "./helpers/e2e-workflow-contract"; - -type PullRequestWorkflow = { jobs: Record }; - -const action = readYaml(".github/actions/ci-cli-coverage-shard/action.yaml"); -const pullRequestWorkflow = readYaml(".github/workflows/pr.yaml"); -const mainWorkflow = readYaml(".github/workflows/main.yaml"); - -function requiredStep(steps: WorkflowStep[], name: string): WorkflowStep { - const step = steps.find((candidate) => candidate.name === name); - expect(step, `Missing workflow step: ${name}`).toBeDefined(); - return step as WorkflowStep; -} - -function fakeCommand(directory: string, name: string, source: string): void { - writeFileSync(join(directory, name), source, { mode: 0o755 }); -} - -describe("CLI coverage Pi search-tool provisioning", () => { - // source-shape-contract: security -- Base-trusted and bootstrap paths must share one pinned, verified tool contract before untrusted tests invoke Pi - it("installs pinned fd and ripgrep before CLI coverage can invoke Pi", () => { - const actionSteps = action.runs.steps; - const pullRequestJob = pullRequestWorkflow.jobs["cli-test-shards"]; - const mainJob = mainWorkflow.jobs["cli-test-shards"]; - const jobSteps = pullRequestJob.steps ?? []; - const install = requiredStep(actionSteps, "Install pinned Pi search tools"); - const detect = requiredStep(jobSteps, "Detect trusted E2E support sharding"); - const bootstrap = requiredStep(jobSteps, "Install pinned Pi search tools (bootstrap)"); - - expect(install.env).toEqual({ - FD_FIND_VERSION: "9.0.0-1", - RIPGREP_VERSION: "14.1.0-1", - }); - expect(pullRequestJob["runs-on"]).toBe("ubuntu-24.04"); - expect(mainJob["runs-on"]).toBe("ubuntu-24.04"); - expect(actionSteps.indexOf(install)).toBeLessThan( - actionSteps.indexOf(requiredStep(actionSteps, "Install dependencies")), - ); - expect(actionSteps.indexOf(install)).toBeLessThan( - actionSteps.indexOf(requiredStep(actionSteps, "Run CLI coverage and E2E support shard")), - ); - expect(detect.run).toContain("name: Install pinned Pi search tools"); - expect(detect.run).toContain('echo "pi-search-tools=true" >> "$GITHUB_OUTPUT"'); - expect(detect.run).toContain('echo "pi-search-tools=false" >> "$GITHUB_OUTPUT"'); - expect(bootstrap.if).toBe( - "${{ steps.trusted-shard-capabilities.outputs.pi-search-tools != 'true' }}", - ); - expect(bootstrap.env).toEqual(install.env); - expect(bootstrap.run).toBe(install.run); - expect(jobSteps.indexOf(bootstrap)).toBeLessThan( - jobSteps.indexOf(requiredStep(jobSteps, "Run CLI coverage shard")), - ); - - const temp = mkdtempSync(join(tmpdir(), "nemoclaw-cli-pi-tools-install-")); - const fakeBin = join(temp, "bin"); - const callLog = join(temp, "calls.log"); - mkdirSync(fakeBin); - fakeCommand(fakeBin, "sudo", '#!/bin/bash\nprintf \'sudo %s\\n\' "$*" >> "$CALL_LOG"\n'); - fakeCommand( - fakeBin, - "dpkg-query", - `#!/bin/bash -printf 'dpkg-query %s\\n' "$*" >> "$CALL_LOG" -case "$*" in - *fd-find) printf '%s' "$FD_FIND_VERSION" ;; - *ripgrep) printf '%s' "$RIPGREP_VERSION" ;; - *) exit 1 ;; -esac -`, - ); - fakeCommand( - fakeBin, - "fdfind", - "#!/bin/bash\nprintf 'fdfind %s\\n' \"$*\" >> \"$CALL_LOG\"\nprintf 'fdfind 9.0.0\\n'\n", - ); - fakeCommand( - fakeBin, - "rg", - "#!/bin/bash\nprintf 'rg %s\\n' \"$*\" >> \"$CALL_LOG\"\nprintf 'ripgrep 14.1.0\\n-SIMD -AVX\\n'\n", - ); - - try { - const result = spawnSync("bash", ["-c", install.run ?? ""], { - encoding: "utf8", - env: { - ...process.env, - ...install.env, - CALL_LOG: callLog, - PATH: `${fakeBin}:${process.env.PATH ?? ""}`, - }, - timeout: 5_000, - }); - expect(result.status, String(result.stderr)).toBe(0); - const calls = readFileSync(callLog, "utf8"); - expect(calls).toContain("sudo apt-get update -qq"); - expect(calls).toContain( - "sudo apt-get install -y --no-install-recommends fd-find=9.0.0-1 ripgrep=14.1.0-1", - ); - expect(calls).toContain("dpkg-query -W -f=${Version} fd-find"); - expect(calls).toContain("dpkg-query -W -f=${Version} ripgrep"); - expect(calls).toContain("fdfind --version"); - expect(calls).toContain("rg --version"); - } finally { - rmSync(temp, { force: true, recursive: true }); - } - }); -}); diff --git a/test/cloudflared-update-check-workflow.test.ts b/test/cloudflared-update-check-workflow.test.ts index ba996ee8729..2b9d1f4bb1d 100644 --- a/test/cloudflared-update-check-workflow.test.ts +++ b/test/cloudflared-update-check-workflow.test.ts @@ -131,24 +131,6 @@ describe("cloudflared update-check workflow contract", () => { workflow.jobs?.["check-cloudflared"]?.steps?.find((step) => typeof step.run === "string") ?.run ?? ""; - // source-shape-contract: security -- Automatic and on-demand checks must preserve the credential-free dependency monitoring boundary - it("keeps automatic and on-demand update checks reachable and credential-free", () => { - expect({ - automatic: - workflow.on?.schedule?.some( - (entry) => typeof entry.cron === "string" && entry.cron.trim() !== "", - ) ?? false, - onDemand: Object.hasOwn(workflow.on ?? {}, "workflow_dispatch"), - }).toEqual({ automatic: true, onDemand: true }); - expect(workflow.permissions).toEqual({ contents: "read" }); - - const job = workflow.jobs?.["check-cloudflared"]; - const checkout = job?.steps?.find((step) => step.uses?.startsWith("actions/checkout@")); - expect(job?.permissions).toBeUndefined(); - expect(checkout?.uses).toMatch(FULL_SHA_ACTION); - expect(checkout?.with?.["persist-credentials"]).toBe(false); - }); - it("extracts exactly five identical reviewed version and SHA256 pins", () => { const versions = pinValues(e2e, "CLOUDFLARED_VERSION"); const hashes = pinValues(e2e, "CLOUDFLARED_DEB_SHA256"); diff --git a/test/code-scanning-workflow.test.ts b/test/code-scanning-workflow.test.ts index 62603a85d7a..a0e37350207 100644 --- a/test/code-scanning-workflow.test.ts +++ b/test/code-scanning-workflow.test.ts @@ -9,30 +9,15 @@ import path from "node:path"; import { describe, expect, it } from "vitest"; import { readYaml, type Workflow } from "./helpers/e2e-workflow-contract"; -type DependabotUpdate = { - "package-ecosystem"?: string; - directory?: string; - groups?: Record; -}; - const workflow = readYaml(".github/workflows/code-scanning.yaml"); -const dependabot = readYaml<{ updates?: DependabotUpdate[] }>(".github/dependabot.yml"); const shellcheckSteps = workflow.jobs.shellcheck?.steps ?? []; -const codeqlActionPrefix = "github/codeql-action/"; - function requiredStep(name: string) { const step = shellcheckSteps.find((candidate) => candidate.name === name); assert(step, `ShellCheck workflow is missing step: ${name}`); return step; } -function stepIndex(name: string) { - const index = shellcheckSteps.findIndex((candidate) => candidate.name === name); - assert(index >= 0, `ShellCheck workflow is missing step: ${name}`); - return index; -} - function writeExecutable(file: string, content: string) { fs.writeFileSync(file, content, { mode: 0o755 }); } @@ -99,132 +84,7 @@ esac return { calls, result }; } -describe("Code scanning workflow dependency updates", () => { - // source-shape-contract: security -- One immutable CodeQL revision prevents partial scanner action upgrades - it("keeps every CodeQL action on one immutable revision", () => { - const codeqlActions = Object.values(workflow.jobs ?? {}) - .flatMap((job) => job.steps ?? []) - .map((step) => step.uses) - .filter((uses): uses is string => uses?.startsWith(codeqlActionPrefix) ?? false); - - expect( - codeqlActions.map((uses) => uses.slice(codeqlActionPrefix.length).split("@")[0]).sort(), - ).toEqual(["analyze", "init", "upload-sarif"]); - - const revisions = codeqlActions.map((uses) => uses.split("@")[1]); - expect(revisions).toHaveLength(3); - for (const revision of revisions) { - expect(revision).toMatch(/^[0-9a-f]{40}$/); - } - expect(new Set(revisions).size).toBe(1); - }); - - // source-shape-contract: security -- Grouped CodeQL updates preserve the reviewed single-revision scanner boundary - it("groups CodeQL action updates so Dependabot keeps the shared revision synchronized", () => { - const githubActionsUpdate = dependabot.updates?.find( - (update) => update["package-ecosystem"] === "github-actions" && update.directory === "/", - ); - const groups = Object.values(githubActionsUpdate?.groups ?? {}); - - expect(groups.some((group) => group.patterns?.includes("github/codeql-action/*"))).toBe(true); - }); -}); - describe("ShellCheck SARIF workflow boundary", () => { - // source-shape-contract: security -- A sparse trusted checkout, disabled credential persistence, an isolated helper environment, and fail-closed ordering protect SARIF publication - it("runs only the trusted converter and keeps scanner status separate from conversion failures (#6959)", () => { - expect(workflow.jobs.shellcheck).toBeDefined(); - - const checkout = requiredStep("Checkout"); - expect(checkout.with?.path).toBe("source"); - expect(checkout.with?.["persist-credentials"]).toBe(false); - - const trustedCheckout = requiredStep("Check out the trusted ShellCheck converter"); - expect(trustedCheckout.uses).toBe("actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1"); - expect(trustedCheckout.with).toMatchObject({ - ref: "${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.workflow_sha }}", - path: "trusted-shellcheck-converter", - "persist-credentials": false, - "sparse-checkout": "scripts/shellcheck-json1-to-sarif.mts\n", - "sparse-checkout-cone-mode": false, - }); - - const detect = requiredStep("Detect trusted ShellCheck converter"); - expect(detect.id).toBe("converter"); - expect(detect.run).toContain( - "trusted-shellcheck-converter/scripts/shellcheck-json1-to-sarif.mts", - ); - expect(detect.run).toContain('echo "present=false" >> "$GITHUB_OUTPUT"'); - expect(detect.run).toContain("conversion and upload begin after this helper lands"); - - const setupNode = requiredStep("Setup Node.js"); - expect(setupNode.if).toBe("steps.converter.outputs.present == 'true'"); - expect(setupNode.uses).toBe("actions/setup-node@820762786026740c76f36085b0efc47a31fe5020"); - expect(setupNode.with?.["node-version"]).toBe("22.19.0"); - - const install = requiredStep("Install ShellCheck"); - expect(install.if).toBe("steps.converter.outputs.present == 'true'"); - expect(install.run).toContain("shellcheck --format=json1"); - expect(install.run).toContain("Acquire::Retries=3"); - expect(install.run).toContain("Acquire::http::Timeout=15"); - expect(install.run).toContain("Acquire::https::Timeout=15"); - - const collect = requiredStep("Collect shell files"); - expect(collect.if).toBe("steps.converter.outputs.present == 'true'"); - expect(collect).toMatchObject({ "working-directory": "source" }); - expect(collect.run).toContain("$GITHUB_WORKSPACE/shell-files.txt"); - expect(collect.run).toContain("git ls-files -z --"); - expect(collect.run).toContain("sort -zu"); - - const generate = requiredStep("Generate ShellCheck SARIF"); - expect(generate.if).toBe( - "steps.converter.outputs.present == 'true' && steps.shell-files.outputs.has_files == 'true'", - ); - expect(generate).toMatchObject({ "working-directory": "source" }); - expect(generate.run).toContain( - '"$GITHUB_WORKSPACE/trusted-shellcheck-converter/scripts/shellcheck-json1-to-sarif.mts"', - ); - expect(generate.run).not.toContain( - '"$GITHUB_WORKSPACE/source/scripts/shellcheck-json1-to-sarif.mts"', - ); - expect(generate.run).toContain("mapfile -d '' -t shell_files"); - expect(generate.run).toContain('shellcheck --format=json1 -- "${shell_files[@]}"'); - expect(generate.run).not.toContain("xargs"); - expect(generate.run).toContain('case "$sc_exit" in'); - expect(generate.run).toContain('exit "$sc_exit"'); - expect(generate.run).toContain("ShellCheck found issues; continuing"); - expect(generate.run).toContain("refusing to convert or upload incomplete results"); - expect(generate.run).toContain("conversion_exit=$?"); - expect(generate.run).toContain('exit "$conversion_exit"'); - expect(generate.run).not.toContain("def level_map"); - expect(generate.run).not.toContain("cat > shellcheck.sarif"); - - const checkRuns = requiredStep("Check SARIF has runs"); - expect(checkRuns.if).toBe( - "steps.converter.outputs.present == 'true' && steps.shell-files.outputs.has_files == 'true'", - ); - expect(checkRuns.run).toContain("jq '.runs | length' shellcheck.sarif"); - - const upload = requiredStep("Upload ShellCheck SARIF"); - expect(upload.if).toBe( - "steps.converter.outputs.present == 'true' && steps.shell-files.outputs.has_files == 'true' && steps.sarif-runs.outputs.has_runs == 'true'", - ); - expect(upload.with?.checkout_path).toBe("source"); - - const orderedSteps = [ - stepIndex("Checkout"), - stepIndex("Check out the trusted ShellCheck converter"), - stepIndex("Detect trusted ShellCheck converter"), - stepIndex("Setup Node.js"), - stepIndex("Install ShellCheck"), - stepIndex("Collect shell files"), - stepIndex("Generate ShellCheck SARIF"), - stepIndex("Check SARIF has runs"), - stepIndex("Upload ShellCheck SARIF"), - ]; - expect(orderedSteps).toEqual([...orderedSteps].sort((left, right) => left - right)); - }); - it("keeps a preinstalled ShellCheck only when its json1 formatter works (#7684)", () => { const { calls, result } = runShellCheckInstall({ preinstalledSupportsJson1: true }); diff --git a/test/e2e-main-retry-workflow.test.ts b/test/e2e-main-retry-workflow.test.ts deleted file mode 100644 index 059d1f2da48..00000000000 --- a/test/e2e-main-retry-workflow.test.ts +++ /dev/null @@ -1,110 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -import fs from "node:fs"; -import YAML from "yaml"; -import { describe, expect, it } from "vitest"; - -type Step = { - name?: string; - uses?: string; - if?: string; - env?: Record; - run?: string; - with?: Record; -}; - -type Workflow = { - name: string; - on: { workflow_run: { workflows: string[]; types: string[] } }; - permissions: Record; - jobs: { - evaluate: { - if: string; - concurrency: Record; - permissions: Record; - steps: Step[]; - }; - }; -}; - -function workflow(): Workflow { - return YAML.parse(fs.readFileSync(".github/workflows/e2e-main-retry.yaml", "utf8")) as Workflow; -} - -function step(name: string): Step { - return workflow().jobs.evaluate.steps.find((candidate) => candidate.name === name)!; -} - -describe("main E2E retry workflow", () => { - // source-shape-contract: security -- The write-capable retry controller must subscribe only to the reviewed E2E workflow completion event - it("subscribes only to completed E2E workflow runs", () => { - const value = workflow(); - expect(value.name).toBe("E2E / Main Retry"); - expect(value.on).toEqual({ workflow_run: { workflows: ["E2E"], types: ["completed"] } }); - expect(value.permissions).toEqual({}); - }); - - // source-shape-contract: security -- Source identity and attempt guards prevent fork, PR, manual, and out-of-range retry writes - it("accepts only trusted main push attempts one through three", () => { - const guard = workflow().jobs.evaluate.if; - for (const fragment of [ - "github.run_attempt == 1", - "github.repository == 'NVIDIA/NemoClaw'", - "github.event.workflow_run.status == 'completed'", - "github.event.workflow_run.event == 'push'", - "github.event.workflow_run.path == '.github/workflows/e2e.yaml'", - "github.event.workflow_run.display_title == 'E2E main'", - "github.event.workflow_run.head_branch == 'main'", - "github.event.workflow_run.head_repository.full_name == 'NVIDIA/NemoClaw'", - "github.event.workflow_run.run_attempt >= 1", - "github.event.workflow_run.run_attempt <= 3", - ]) { - expect(guard).toContain(fragment); - } - expect(guard).not.toContain("pull_request"); - expect(guard).not.toContain("workflow_dispatch"); - expect(guard).not.toContain("||"); - expect(guard.match(/&&/gu)).toHaveLength(9); - }); - - // source-shape-contract: security -- Source-run serialization and least privilege prevent concurrent or broader GitHub mutations - it("uses one source-run concurrency identity and least privileges", () => { - const job = workflow().jobs.evaluate; - expect(job.concurrency).toEqual({ - group: "e2e-main-retry-${{ github.event.workflow_run.id }}", - "cancel-in-progress": false, - }); - expect(job.permissions).toEqual({ actions: "write", contents: "read" }); - }); - - // source-shape-contract: security -- Trusted default-branch controller code must run before any write-capable GitHub request - it("checks out trusted controller code and invokes the bounded helper", () => { - expect(step("Checkout trusted retry controller")).toMatchObject({ - uses: "actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1", - with: { ref: "${{ github.workflow_sha }}", "persist-credentials": false }, - }); - expect(step("Evaluate main E2E retry")).toMatchObject({ - env: { - GITHUB_TOKEN: "${{ github.token }}", - RETRY_EVIDENCE_PATH: "${{ runner.temp }}/e2e-main-retry-evidence.json", - SOURCE_RUN_ID: "${{ github.event.workflow_run.id }}", - }, - }); - expect(step("Evaluate main E2E retry").run).toContain("tools/e2e/main-run-retry.mts"); - }); - - // source-shape-contract: security -- The bounded upload step must run after evaluation failure without widening artifact paths - it("runs the bounded evidence-upload step after evaluation failure", () => { - expect(step("Upload retry evidence")).toMatchObject({ - if: "${{ always() }}", - uses: "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a", - with: { - name: "e2e-main-retry-${{ github.event.workflow_run.id }}-${{ github.event.workflow_run.run_attempt }}", - path: "${{ runner.temp }}/e2e-main-retry-evidence.json", - "if-no-files-found": "warn", - "retention-days": 14, - }, - }); - }); -}); diff --git a/test/e2e-release-gate-workflow.test.ts b/test/e2e-release-gate-workflow.test.ts index 33ffe5a2230..f2c00dd1718 100644 --- a/test/e2e-release-gate-workflow.test.ts +++ b/test/e2e-release-gate-workflow.test.ts @@ -18,20 +18,6 @@ type E2eWorkflow = { const e2eWorkflow = readYaml(".github/workflows/e2e.yaml"); describe("release gate workflow resource contracts", () => { - // source-shape-contract: security -- Trusted checkout selection binds TUI evidence to the validated controller commit - it("replaces legacy target_ref dispatches with the validated checkout contract", () => { - const inputs = e2eWorkflow.on?.workflow_dispatch?.inputs; - const tuiJob = e2eWorkflow.jobs["openclaw-tui-chat-correlation"]; - const checkout = tuiJob.steps?.find((step) => step.uses?.startsWith("actions/checkout@")); - - expect(inputs).toHaveProperty("checkout_sha"); - expect(inputs).not.toHaveProperty("target_ref"); - expect(tuiJob.permissions).toEqual({ contents: "read" }); - expect(checkout?.with?.ref).toBe("${{ inputs.checkout_sha || github.sha }}"); - expect(tuiJob.env?.NEMOCLAW_TUI_EXPECTED_CHECKOUT_SHA).toBe( - "${{ inputs.checkout_sha || github.sha }}", - ); - }); it("rejects trusted dispatch receipt contract drift", () => { const workflow = structuredClone(e2eWorkflow); const steps = workflow.jobs["generate-matrix"].steps!; diff --git a/test/e2e/support/e2e-workflow.test.ts b/test/e2e/support/e2e-workflow.test.ts index ebc8bb1416e..16b62cdee20 100644 --- a/test/e2e/support/e2e-workflow.test.ts +++ b/test/e2e/support/e2e-workflow.test.ts @@ -707,117 +707,6 @@ describe("e2e workflow boundary", () => { }, ); - // source-shape-contract: compatibility -- Cross-checks generated selectors against the executable workflow job registry - it("derives test selectors from code and workflow jobs from workflow metadata", { - timeout: 60_000, - }, () => { - const inventory = readFreeStandingJobsInventory(); - const workflow = readWorkflow() as { - jobs: Record; if?: string }>; - }; - const workflowJobs = new Set(Object.keys(workflow.jobs)); - const portableWorkflowSource = fs.readFileSync( - path.join(process.cwd(), ".github", "workflows", "portable-profile-e2e.yaml"), - "utf8", - ); - const fullE2eSource = fs.readFileSync( - path.join(process.cwd(), "test", "e2e", "live", "full-e2e.test.ts"), - "utf8", - ); - const portableWorkflow = YAML.parse(portableWorkflowSource) as { - on?: { pull_request?: { paths?: string[] }; push?: { paths?: string[] } }; - }; - const portableProofInputs = [ - "scripts/install-openshell.sh", - "src/lib/sandbox/**", - "test/e2e/live/full-e2e.test.ts", - "test/e2e/live/launch-agent-turn.ts", - "test/e2e/live/portable-profile-gateway-proof.ts", - "test/e2e/live/portable-profile-rootless-linux.test.ts", - "tools/e2e/check-semantic-phases.mts", - ]; - - expect(validateFreeStandingWorkflowInventory()).toEqual([]); - expect(portableWorkflow.on?.push?.paths).toEqual(expect.arrayContaining(portableProofInputs)); - expect(portableWorkflow.on?.push?.paths).toEqual(expect.arrayContaining(portableProofInputs)); - expect(portableWorkflowSource).toContain("github.ref == 'refs/heads/main'"); - expect(portableWorkflowSource).toContain("NEMOCLAW_EXPERIMENTAL_PROFILE: portable"); - expect(portableWorkflowSource).toContain( - "NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }}", - ); - expect(fullE2eSource.match(/id: FULL_E2E_TARGET_ID,/gu)).toHaveLength(2); - expect(inventory.allowedJobs).not.toHaveLength(0); - expect(inventory.targetToJob.size).toBeGreaterThan(0); - expect(inventory.workflowJobs.every((job) => workflowJobs.has(job))).toBe(true); - expect([...inventory.targetToJob.values()].every((job) => workflowJobs.has(job))).toBe(true); - expect(inventory.liveTestToJobs.get("test/e2e/live/token-rotation.test.ts")).toEqual([ - "token-rotation", - ]); - expect(inventory.liveTestToJobs.get("test/e2e/live/full-e2e.test.ts")).toEqual( - expect.arrayContaining(["full-e2e", "security-posture"]), - ); - expect(workflow.jobs["gpu-e2e"]?.env?.NEMOCLAW_MODEL).toBe("qwen3.5:9b"); - expect(workflow.jobs["gpu-double-onboard"]?.env?.NEMOCLAW_MODEL).toBe("qwen3.5:9b"); - const driftedWorkflow = structuredClone(workflow); - const compatibilityJob = driftedWorkflow.jobs["retired-selector-compatibility"] ?? {}; - compatibilityJob.if = compatibilityJob.if?.replace( - ",docs-validation,", - ",future-retired-selector,", - ); - expect(validateE2eWorkflow(driftedWorkflow)).toContain( - "retired-selector-compatibility job selector gate must match retired selector contract", - ); - expect( - focusedE2eJobsForChangedFiles( - [ - "test/e2e/live/token-rotation.test.ts", - "docs/get-started/quickstart.mdx", - "test/e2e/live/token-rotation.test.ts", - ], - inventory, - ), - ).toEqual([ - { - id: "token-rotation", - matchedFiles: ["test/e2e/live/token-rotation.test.ts"], - }, - ]); - expect( - focusedE2eJobsForChangedFiles( - ["test/e2e/live/openclaw-plugin-runtime-exdev-lifecycle.ts"], - inventory, - ), - ).toEqual([ - { - id: "openclaw-plugin-runtime-exdev", - matchedFiles: ["test/e2e/live/openclaw-plugin-runtime-exdev-lifecycle.ts"], - }, - ]); - expect( - focusedE2eJobsForChangedFiles(["test/e2e/live/rebuild-hermes-cron-restore.ts"], inventory), - ).toEqual([ - { - id: "rebuild-hermes", - matchedFiles: ["test/e2e/live/rebuild-hermes-cron-restore.ts"], - }, - { - id: "rebuild-hermes-stale-base", - matchedFiles: ["test/e2e/live/rebuild-hermes-cron-restore.ts"], - }, - ]); - expect( - focusedE2eJobsForChangedFiles( - ["test/e2e/live/openshell-gateway-upgrade-helpers.ts"], - inventory, - ), - ).toEqual([ - { - id: "openshell-gateway-upgrade", - matchedFiles: ["test/e2e/live/openshell-gateway-upgrade-helpers.ts"], - }, - ]); - }); - it("rejects malformed free-standing workflow metadata before matrix generation", { timeout: 60_000, }, () => { diff --git a/test/e2e/support/sandbox-name-workflow-boundary.test.ts b/test/e2e/support/sandbox-name-workflow-boundary.test.ts index d1a85ceb9a9..051ee9cc018 100644 --- a/test/e2e/support/sandbox-name-workflow-boundary.test.ts +++ b/test/e2e/support/sandbox-name-workflow-boundary.test.ts @@ -9,10 +9,7 @@ import { } from "../../../tools/e2e/sandbox-name-workflow-boundary.mts"; import { readYaml, type Workflow } from "../../helpers/e2e-workflow-contract"; -const WORKFLOW_PATHS = [ - ".github/workflows/e2e.yaml", - ".github/workflows/regression-e2e.yaml", -] as const; +const WORKFLOW_PATHS = [".github/workflows/e2e.yaml"] as const; describe("live workflow sandbox name boundary", () => { it.each( diff --git a/test/macos-e2e-workflow-boundary.test.ts b/test/macos-e2e-workflow-boundary.test.ts deleted file mode 100644 index a362d3c67f2..00000000000 --- a/test/macos-e2e-workflow-boundary.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -import fs from "node:fs"; -import path from "node:path"; - -import { describe, expect, it } from "vitest"; -import YAML from "yaml"; - -type WorkflowStep = { - name?: string; - if?: string; - env?: Record; - run?: string; - uses?: string; - with?: Record; -}; - -type WorkflowJob = { - if?: string; - permissions?: Record; - "runs-on"?: string; - "timeout-minutes"?: number; - steps?: WorkflowStep[]; -}; - -type Workflow = { - jobs?: Record; - on?: Record; -}; - -function readMacosWorkflow(): Workflow { - return YAML.parse( - fs.readFileSync(path.join(process.cwd(), ".github", "workflows", "macos-e2e.yaml"), "utf8"), - ) as Workflow; -} - -function jobNamed(name: string): WorkflowJob { - const job = readMacosWorkflow().jobs?.[name]; - expect(job).toBeDefined(); - return job!; -} - -function stepNamed(name: string, jobName = "macos-e2e"): WorkflowStep { - const step = jobNamed(jobName).steps?.find((candidate) => candidate.name === name); - expect(step).toBeDefined(); - return step!; -} - -describe("macOS E2E workflow boundary", () => { - // source-shape-contract: security -- Live credentials must stay gated to trusted main-branch workflow code - it("keeps secret-bearing live E2E on trusted main-branch code", () => { - expect(readMacosWorkflow().on?.pull_request).toBeUndefined(); - - expect(stepNamed("Run macOS full E2E").if).toContain("github.ref == 'refs/heads/main'"); - - expect(String(stepNamed("Run macOS full E2E").env?.NVIDIA_INFERENCE_API_KEY)).toContain( - "github.ref == 'refs/heads/main'", - ); - }); - - // source-shape-contract: compatibility -- OpenShell publishes macOS gateway assets only for Apple Silicon - it("keeps gateway lifecycle coverage on supported Apple Silicon macOS", () => { - const workflow = readMacosWorkflow(); - const lifecycle = stepNamed("Run gateway lifecycle regressions"); - - expect(jobNamed("macos-e2e")["runs-on"]).toBe("macos-26"); - expect(JSON.stringify(workflow.jobs ?? {})).not.toContain("macos-15-intel"); - expect(lifecycle.run).toContain("test/tunnel-gateway-port-release-runtime.test.ts"); - expect(lifecycle.run).toContain("test/onboard-gateway-prelaunch-cutover.test.ts"); - expect(lifecycle.run).toContain("test/onboard-gateway-legacy-identity-upgrade-runtime.test.ts"); - }); - - // source-shape-contract: security -- The failure-only macOS artifact publisher must retain diagnostic paths and an immutable action - it("pins the macOS artifact publisher to an immutable action", () => { - const workflow = readMacosWorkflow(); - const upload = workflow.jobs?.["macos-e2e"]?.steps?.find( - (step) => step.name === "Upload logs on failure", - ); - - expect(upload?.uses).toBe("actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a"); - const paths = String(upload?.with?.path) - .split(/\r?\n/) - .map((entry) => entry.trim()) - .filter(Boolean); - expect(paths).toHaveLength(2); - expect(paths).toEqual([ - "/tmp/nemoclaw-e2e-*.log", - "${{ github.workspace }}/e2e-artifacts/live", - ]); - expect(upload?.if).toBe("failure()"); - }); -}); diff --git a/test/openclaw-dependency-review.test.ts b/test/openclaw-dependency-review.test.ts index bd085cb61e8..f7e0bfd3e39 100644 --- a/test/openclaw-dependency-review.test.ts +++ b/test/openclaw-dependency-review.test.ts @@ -844,52 +844,4 @@ grep -Fq -- '--phase post-agent-install' Dockerfile ); } }); - - // source-shape-contract: security -- Network-fetched distribution audits must execute only from trusted main workflow code - it("runs and gates the real patched-distribution harness only from trusted main code", () => { - const pr = readYaml(".github/workflows/pr.yaml"); - const main = readYaml(".github/workflows/main.yaml"); - const prJob = pr.jobs["real-openclaw-dist-harness"]; - const mainJob = main.jobs["real-openclaw-dist-harness"]; - const prChecks = pr.jobs.checks; - const mainChecks = main.jobs.checks; - - expect(pr.permissions).toEqual({ contents: "read" }); - expect(prJob).toBeUndefined(); - expect(requiredStep(mainJob, "Audit the real patched OpenClaw distribution").env).toMatchObject( - { - NEMOCLAW_REAL_OPENCLAW_DIST_HARNESS: "1", - }, - ); - expect(requiredStep(mainJob, "Audit the real patched OpenClaw distribution").run).toContain( - "test/openclaw-real-patched-dist-harness.test.ts", - ); - expect(requiredStep(mainJob, "Verify reviewed Jaeger header handling").env).toEqual({ - NEMOCLAW_REAL_OPENCLAW_JAEGER_HARNESS: "1", - }); - expect(requiredStep(mainJob, "Verify reviewed Jaeger header handling").run).toContain( - "test/openclaw-diagnostics-jaeger-runtime.test.ts", - ); - expect( - requiredStep(mainJob, "Audit managed OpenClaw security finding suppressions").env, - ).toEqual({ NEMOCLAW_REAL_OPENCLAW_AUDIT_HARNESS: "1" }); - expect( - requiredStep(mainJob, "Audit managed OpenClaw security finding suppressions").run, - ).toContain("test/openclaw-security-audit-suppressions-real.test.ts"); - expect(requiredStep(mainJob, "Install test dependencies").run).toBe("npm ci --ignore-scripts"); - expect(prChecks.needs).not.toContain("real-openclaw-dist-harness"); - expect(mainChecks.needs).toContain("real-openclaw-dist-harness"); - const prGate = requiredStep(prChecks, "Verify required PR checks"); - const mainGate = requiredStep(mainChecks, "Verify required main checks"); - expect(prGate.env).not.toHaveProperty("REAL_OPENCLAW_DIST_HARNESS_RESULT"); - expect(mainGate.env).toMatchObject({ - REAL_OPENCLAW_DIST_HARNESS_RESULT: "${{ needs['real-openclaw-dist-harness'].result }}", - }); - - expect(prGate.run).not.toContain("real-openclaw-dist-harness"); - expect(mainGate.run).toContain( - 'require_success "real-openclaw-dist-harness" "$REAL_OPENCLAW_DIST_HARNESS_RESULT"', - ); - expect(mainGate.run).not.toContain('allow_success_or_skipped "real-openclaw-dist-harness"'); - }); }); diff --git a/test/openclaw-locked-install.test.ts b/test/openclaw-locked-install.test.ts index 8312db94355..1fc9a6a3db2 100644 --- a/test/openclaw-locked-install.test.ts +++ b/test/openclaw-locked-install.test.ts @@ -363,58 +363,4 @@ describe("locked OpenClaw production installation (#5896)", () => { expect(contents).toContain('"lock-sha256=${OPENCLAW_LOCK_SHA256}"'); expect(contents).toContain("locked-ci+reviewed-lifecycle-v2"); }); - - // source-shape-contract: security -- The shipped audit and base-image rebuild inputs must share the exact committed production lock authority - it("audits the same lock and rebuilds the base when its graph changes", () => { - const audit = JSON.parse( - fs.readFileSync(path.join(REPO_ROOT, "ci", "reviewed-npm-audit.json"), "utf-8"), - ); - expect(audit.schemaVersion).toBe(2); - expect(audit.archivePackages).toEqual( - expect.arrayContaining([expect.objectContaining({ packageSpec: PACKAGE_SPEC })]), - ); - expect(audit.lockedGraphs).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - directory: "agents/openclaw/openclaw-runtime", - lockSha256: LOCK_SHA256, - packageSpec: PACKAGE_SPEC, - }), - expect.objectContaining({ - directory: "agents/openclaw/mcporter-runtime", - lockSha256: MCPORTER_LOCK_SHA256, - packageSpec: MCPORTER_PACKAGE_SPEC, - }), - expect.objectContaining({ - directory: "tools/mcp-tool-discovery-runtime", - lockSha256: MCP_TOOL_DISCOVERY_LOCK_SHA256, - packageSpec: MCP_TOOL_DISCOVERY_PACKAGE_SPEC, - }), - ]), - ); - expect(audit.lockedGraphs).toHaveLength(3); - expect( - audit.lockedGraphs.map(({ packageSpec }: { packageSpec?: string }) => packageSpec).sort(), - ).toEqual([MCPORTER_PACKAGE_SPEC, MCP_TOOL_DISCOVERY_PACKAGE_SPEC, PACKAGE_SPEC].sort()); - for (const graph of audit.lockedGraphs) { - expect(graph).not.toHaveProperty("replacementLockSha256"); - expect(graph).not.toHaveProperty("reviewedLockSha256"); - } - - const baseWorkflow = fs.readFileSync( - path.join(REPO_ROOT, ".github", "workflows", "base-image.yaml"), - "utf-8", - ); - expect(baseWorkflow).toContain('"agents/openclaw/openclaw-runtime/package.json"'); - expect(baseWorkflow).toContain('"agents/openclaw/openclaw-runtime/package-lock.json"'); - expect(baseWorkflow).toContain('"scripts/lib/reviewed-npm-archive.mts"'); - - const baseResolver = fs.readFileSync( - path.join(REPO_ROOT, ".github", "actions", "resolve-sandbox-base-image", "action.yaml"), - "utf-8", - ); - expect(baseResolver).toContain("agents/openclaw/openclaw-runtime/package.json"); - expect(baseResolver).toContain("agents/openclaw/openclaw-runtime/package-lock.json"); - expect(baseResolver).toContain("scripts/lib/reviewed-npm-archive.mts"); - }); }); diff --git a/test/openshell-e2e-qualification-workflow.test.ts b/test/openshell-e2e-qualification-workflow.test.ts deleted file mode 100644 index 109ae3a71a2..00000000000 --- a/test/openshell-e2e-qualification-workflow.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -import assert from "node:assert/strict"; - -import { describe, expect, it } from "vitest"; - -import { type CompositeAction, readYaml, type WorkflowJob } from "./helpers/e2e-workflow-contract"; - -type InstallerHashWorkflow = { - permissions?: Record; - jobs: Record; -}; - -type InstallerHashAction = CompositeAction & { inputs?: Record }; - -function requiredWorkflowStep(job: WorkflowJob, name: string) { - const step = job.steps?.find((candidate) => candidate.name === name); - assert(step, `Missing workflow step: ${name}`); - return step; -} - -describe("installer hash workflow", () => { - const workflow = readYaml(".github/workflows/installer-hash-check.yaml"); - const action = readYaml( - ".github/actions/ci-installer-hash-check/action.yaml", - ); - - // source-shape-contract: security -- Installer integrity verification must remain independent from unrelated full E2E completion - it("keeps installer verification independent from full E2E qualification", () => { - const checkHashJob = workflow.jobs["check-hash"]; - const baseCheckout = requiredWorkflowStep( - checkHashJob, - "Checkout base-trusted installer hash action", - ); - const bootstrapCheckout = requiredWorkflowStep( - checkHashJob, - "Checkout immutable installer hash bootstrap", - ); - - expect(workflow.permissions).toEqual({ contents: "read" }); - expect(checkHashJob["timeout-minutes"]).toBe(5); - const sparseCheckout = baseCheckout.with?.["sparse-checkout"]; - assert(typeof sparseCheckout === "string"); - expect(sparseCheckout.trim().split("\n")).toEqual([ - ".github/actions/ci-installer-hash-check", - "scripts/check-installer-hash.sh", - "scripts/checks/extract-installer-pins.mts", - ]); - const bootstrapSparseCheckout = bootstrapCheckout.with?.["sparse-checkout"]; - assert(typeof bootstrapSparseCheckout === "string"); - expect(bootstrapSparseCheckout.trim().split("\n")).toEqual([ - ".github/actions/ci-installer-hash-check", - "scripts/check-installer-hash.sh", - "scripts/checks/extract-installer-pins.mts", - ]); - expect(action.inputs?.["repo-root"]?.required).toBe(true); - expect(action.runs.steps.map((step) => step.name)).toEqual([ - "Verify installer hashes are current", - ]); - expect(JSON.stringify({ action, workflow })).not.toContain( - "verify-openshell-e2e-qualification", - ); - }); -}); diff --git a/test/pr-limit-policy.test.ts b/test/pr-limit-policy.test.ts deleted file mode 100644 index 1a8b89ec17e..00000000000 --- a/test/pr-limit-policy.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -import fs from "node:fs"; -import path from "node:path"; -import { expect, it } from "vitest"; -import YAML from "yaml"; - -const repoRoot = process.cwd(); - -function read(relativePath: string): string { - return fs.readFileSync(path.join(repoRoot, relativePath), "utf8"); -} - -function documentedLimit(source: string): number | undefined { - const match = source.match(/(?:up to|the) (\d+)(?:-open-PR| open PRs)/iu); - return match ? Number(match[1]) : undefined; -} - -// source-shape-contract: compatibility -- Contributor guidance must match the enforced PR count and preserve the workflow-owned maintainer exemption -it("keeps contributor guidance aligned with the enforced maintainer exemption", () => { - const workflow = YAML.parse(read(".github/workflows/pr-limit.yaml")) as { - jobs: Record }>; - }; - const run = workflow.jobs["check-pr-limit"]?.steps.find((step) => step.run)?.run ?? ""; - const enforcedLimit = Number(run.match(/\$OPEN_COUNT" -gt (\d+)/u)?.[1]); - const exemptAccounts = - run - .match(/EXEMPT="([^"]+)"/u)?.[1] - .trim() - .split(/\s+/u) ?? []; - const agents = read("AGENTS.md"); - const contributing = read("CONTRIBUTING.md"); - - expect(enforcedLimit).toBe(10); - expect(exemptAccounts.length).toBeGreaterThan(0); - expect(documentedLimit(agents)).toBe(enforcedLimit); - expect(documentedLimit(contributing)).toBe(enforcedLimit); - expect(agents).toContain("only to accounts that the workflow does not exempt"); - expect(contributing).toContain( - "Core maintainers listed in `.github/workflows/pr-limit.yaml` are exempt from this limit.", - ); -}); diff --git a/test/pr-workflow-contract.test.ts b/test/pr-workflow-contract.test.ts index 8a9e1391b4a..f9957c76638 100644 --- a/test/pr-workflow-contract.test.ts +++ b/test/pr-workflow-contract.test.ts @@ -313,207 +313,6 @@ describe("pull request and main workflow contracts", () => { ), }; - // source-shape-contract: security -- Base retargets must rerun trusted installer verification without minting skipped required evidence - it("reruns installer hash verification after a pull request base retarget", () => { - expect(installerHashWorkflow.on?.pull_request?.types).toEqual([ - "opened", - "synchronize", - "reopened", - "edited", - ]); - expect(installerHashWorkflow.jobs["check-hash"].if).toBe( - "github.repository == 'NVIDIA/NemoClaw'", - ); - }); - - // source-shape-contract: security -- Dependabot's bounded DCO exemption must report an explicit successful required check - it("records the Dependabot DCO bypass as a successful required job", () => { - const job = dcoWorkflow.jobs["dco-check"]; - const bypass = requiredWorkflowStep(job, "Check Dependabot DCO bypass"); - const declaration = requiredWorkflowStep(job, "Check PR body for Signed-off-by"); - - expect(job.if).toBeUndefined(); - expect(job.steps?.some((step) => step.uses?.startsWith("actions/checkout@"))).toBe(false); - expect(bypass.env?.USERNAME).toBe("${{ github.event.pull_request.user.login }}"); - expect(bypass.run).toContain('"$USERNAME" == "dependabot[bot]"'); - expect(bypass.run).toContain('"$USERNAME" == "app/dependabot"'); - expect(bypass.run).not.toContain(".github/dco-bypass.txt"); - expect(declaration.if).toBe("${{ steps.dco-bypass.outputs.bypass != 'true' }}"); - }); - - // source-shape-contract: security -- Installer hashes must be verified by base-trusted or immutable bootstrap code - it("runs pull request installer verification from immutable trusted code", () => { - const job = installerHashWorkflow.jobs["check-hash"]; - const parserRuntimeSetup = requiredWorkflowStep( - job, - "Set up trusted installer hash parser runtime", - ); - const prCheckout = requiredWorkflowStep(job, "Checkout pull request head"); - const baseCheckout = requiredWorkflowStep(job, "Checkout base-trusted installer hash action"); - const trustedActionProbe = requiredWorkflowStep( - job, - "Detect base-trusted installer hash action", - ); - const bootstrapCheckout = requiredWorkflowStep( - job, - "Checkout immutable installer hash bootstrap", - ); - const bootstrapTreeVerification = requiredWorkflowStep( - job, - "Verify immutable installer hash bootstrap tree", - ); - const bootstrapExpiry = requiredWorkflowStep( - job, - "Enforce immutable installer hash bootstrap expiry", - ); - const baseVerification = requiredWorkflowStep( - job, - "Verify pull request installer hashes from base-trusted code", - ); - const bootstrapVerification = requiredWorkflowStep( - job, - "Verify pull request installer hashes from immutable bootstrap", - ); - const trustedEventVerification = requiredWorkflowStep( - job, - "Verify trusted event installer hashes", - ); - - expect(installerHashWorkflow.on?.pull_request?.paths).toBeUndefined(); - expect(installerHashWorkflow.on?.pull_request?.types).toEqual([ - "opened", - "synchronize", - "reopened", - "edited", - ]); - expect(installerHashWorkflow["run-name"]).toContain( - "Installer Hash PR #{0} head {1} base {2} gate true", - ); - expect(installerHashWorkflow["run-name"]).toContain("github.event.pull_request.base.sha"); - expect(installerHashWorkflow["run-name"]).not.toContain("github.event.changes.base"); - expect(job.if).toBe("github.repository == 'NVIDIA/NemoClaw'"); - expect(parserRuntimeSetup.uses).toBe(trustedSetupNodeAction); - expect(parserRuntimeSetup.with?.["node-version"]).toBe("22.19.0"); - expect(prCheckout.with?.repository).toBe( - "${{ github.event.pull_request.head.repo.full_name }}", - ); - expect(prCheckout.with?.ref).toBe("${{ github.event.pull_request.head.sha }}"); - - for (const checkout of (job.steps ?? []).filter( - (step) => step.uses === trustedCheckoutAction, - )) { - expect(checkout.with?.["persist-credentials"], checkout.name).toBe(false); - } - expect( - (job.steps ?? []) - .filter((step) => step.uses?.startsWith("actions/checkout@")) - .every((step) => step.uses === trustedCheckoutAction), - ).toBe(true); - - expect(baseCheckout.with?.ref).toBe("${{ github.event.pull_request.base.sha }}"); - expect(baseCheckout.with?.path).toBe(".trusted-installer-hash"); - expect(baseCheckout.with?.["sparse-checkout"]).toContain( - ".github/actions/ci-installer-hash-check", - ); - expect(baseCheckout.with?.["sparse-checkout"]).toContain("scripts/check-installer-hash.sh"); - expect(baseCheckout.with?.["sparse-checkout"]).toContain( - "scripts/checks/extract-installer-pins.mts", - ); - - expect(trustedActionProbe.id).toBe("trusted-installer-hash"); - expect(trustedActionProbe.run).toContain( - ".trusted-installer-hash/.github/actions/ci-installer-hash-check/action.yaml", - ); - expect(trustedActionProbe.run).not.toContain("scripts/check-installer-hash.sh"); - expect(bootstrapCheckout.with?.ref).toBe(installerHashBootstrapCommit); - expect(String(bootstrapCheckout.with?.ref)).toMatch(/^[a-f0-9]{40}$/u); - expect(bootstrapCheckout.with?.path).toBe(".bootstrap-installer-hash"); - expect(bootstrapCheckout.with?.["sparse-checkout"]).toContain( - ".github/actions/ci-installer-hash-check", - ); - expect(bootstrapCheckout.with?.["sparse-checkout"]).toContain( - "scripts/check-installer-hash.sh", - ); - expect(bootstrapCheckout.with?.["sparse-checkout"]).toContain( - "scripts/checks/extract-installer-pins.mts", - ); - expect(bootstrapCheckout.with?.["sparse-checkout-cone-mode"]).toBe(false); - expect((bootstrapExpiry as WorkflowStep & { shell?: string }).shell).toBe("bash"); - expect(bootstrapExpiry.env).toBeUndefined(); - expect(bootstrapExpiry.run).toContain(installerHashBootstrapCommit); - expect(bootstrapExpiry.run).toContain(installerHashBootstrapExpiresAt); - expect(bootstrapExpiry.if).toBe(bootstrapCheckout.if); - expect(bootstrapExpiry.if).toBe(bootstrapVerification.if); - expect(bootstrapTreeVerification.if).toBe(bootstrapCheckout.if); - expect(bootstrapTreeVerification.run).toContain(installerHashBootstrapCommit); - expect(bootstrapTreeVerification.run).toContain(installerHashBootstrapTree); - expect( - requiredWorkflowStepIndex(job, "Enforce immutable installer hash bootstrap expiry"), - ).toBeLessThan(requiredWorkflowStepIndex(job, "Checkout immutable installer hash bootstrap")); - expect( - requiredWorkflowStepIndex(job, "Checkout immutable installer hash bootstrap"), - ).toBeLessThan( - requiredWorkflowStepIndex(job, "Verify immutable installer hash bootstrap tree"), - ); - expect( - requiredWorkflowStepIndex(job, "Verify immutable installer hash bootstrap tree"), - ).toBeLessThan( - requiredWorkflowStepIndex( - job, - "Verify pull request installer hashes from immutable bootstrap", - ), - ); - expect( - requiredWorkflowStepIndex(job, "Set up trusted installer hash parser runtime"), - ).toBeLessThan( - requiredWorkflowStepIndex(job, "Verify pull request installer hashes from base-trusted code"), - ); - expect( - requiredWorkflowStepIndex(job, "Set up trusted installer hash parser runtime"), - ).toBeLessThan( - requiredWorkflowStepIndex( - job, - "Verify pull request installer hashes from immutable bootstrap", - ), - ); - expect( - requiredWorkflowStepIndex(job, "Set up trusted installer hash parser runtime"), - ).toBeLessThan(requiredWorkflowStepIndex(job, "Verify trusted event installer hashes")); - expect( - (Date.parse(installerHashBootstrapExpiresAt) - Date.parse(installerHashBootstrapCreatedAt)) / - 86_400_000, - ).toBe(180); - expect(bootstrapExpiry.run).toContain("Date.now() >= expiresAtMs"); - expect(bootstrapExpiry.run).toContain("Remove the bootstrap fallback"); - - expect(baseVerification.uses).toBe( - "./.trusted-installer-hash/.github/actions/ci-installer-hash-check", - ); - expect(bootstrapVerification.uses).toBe( - "./.bootstrap-installer-hash/.github/actions/ci-installer-hash-check", - ); - expect(trustedEventVerification.uses).toBe("./.github/actions/ci-installer-hash-check"); - expect(baseVerification.if).toBe( - "github.event_name == 'pull_request' && steps.trusted-installer-hash.outputs.available == 'true'", - ); - expect(bootstrapVerification.if).toBe( - "github.event_name == 'pull_request' && steps.trusted-installer-hash.outputs.available != 'true'", - ); - expect(trustedEventVerification.if).toBe("github.event_name != 'pull_request'"); - for (const verification of [ - baseVerification, - bootstrapVerification, - trustedEventVerification, - ]) { - expect(verification.with?.["repo-root"], verification.name).toBe("${{ github.workspace }}"); - } - - expect(job.steps?.some((step) => step.name === "Detect installer-affecting changes")).toBe( - false, - ); - expect(stepRuns(job).join("\n")).not.toContain("bash scripts/check-installer-hash.sh"); - }); - it("fails closed when the immutable installer hash bootstrap expiry is mutated", () => { const expiryStep = requiredWorkflowStep( installerHashWorkflow.jobs["check-hash"], @@ -594,540 +393,6 @@ describe("pull request and main workflow contracts", () => { } }); - // source-shape-contract: security -- The trusted action must invoke its bundled verifier without PR-controlled resolution - it("keeps the installer verifier inside the trusted composite action", () => { - const verification = requiredStep(installerHashAction, "Verify installer hashes are current"); - - expect(installerHashAction.inputs?.["repo-root"]?.required).toBe(true); - expect(verification.env).toEqual({ - NEMOCLAW_INSTALLER_HASH_REPO_ROOT: "${{ inputs.repo-root }}", - }); - expect(verification.run).toBe( - 'bash "${{ github.action_path }}/../../../scripts/check-installer-hash.sh"', - ); - }); - - // source-shape-contract: compatibility -- Path-filter semantics keep documentation-only and code-changing PR lanes distinct - it("routes only code-changing PRs through the code-check path", () => { - const filterStep = prWorkflow.jobs.changes.steps?.find((step) => step.id === "filter"); - const docsOnlyCheckout = requiredWorkflowStep(prWorkflow.jobs["docs-only-checks"], "Checkout"); - - expect(filterStep?.uses).toContain("dorny/paths-filter"); - expect(docsOnlyCheckout.with?.["fetch-depth"]).toBe(0); - expect(filterStep?.with?.["predicate-quantifier"]).toBe("every"); - expect(filterStep?.with?.filters).toContain("code:"); - expect(filterStep?.with?.filters).toContain("!**/*.md"); - expect(filterStep?.with?.filters).toContain("!docs/**"); - - expect(codeFilterMatchesChangedPaths(prWorkflow, ["docs/get-started/prerequisites.mdx"])).toBe( - false, - ); - expect(codeFilterMatchesChangedPaths(prWorkflow, ["README.md"])).toBe(false); - expect(codeFilterMatchesChangedPaths(prWorkflow, ["src/lib/runner.ts"])).toBe(true); - expect( - codeFilterMatchesChangedPaths(prWorkflow, [ - "docs/get-started/prerequisites.mdx", - "src/lib/runner.ts", - ]), - ).toBe(true); - }); - - // source-shape-contract: compatibility -- Repository validation must preserve file routing, command scopes, and aliases - it("preserves repository validation file routing, command scopes, and compatibility aliases", () => { - const hooks = prekConfig.repos.flatMap((repo) => repo.hooks ?? []); - const repositoryChecks = hooks.find((candidate) => candidate.id === "repository-checks"); - const files = new RegExp(repositoryChecks?.files ?? "(?!)", "u"); - - for (const path of [ - ".pre-commit-config.yaml", - "Dockerfile", - "Dockerfile.base", - "agents/openclaw/manifest.yaml", - "agents/hermes/Dockerfile", - "agents/hermes/Dockerfile.base", - "agents/hermes/manifest.yaml", - "agents/hermes/mcp-config-transaction.py", - "nemoclaw-blueprint/blueprint.yaml", - "nemoclaw/package.json", - "package.json", - "scripts/brev-launchable-ci-cpu.sh", - "scripts/check-installer-hash.sh", - "scripts/install-openshell.sh", - "scripts/update-hermes-agent.sh", - "src/lib/actions/sandbox/mcp-bridge-validation.ts", - "src/lib/actions/sandbox/openshell-child-visible-credentials.v0.0.99.json", - ]) { - expect(files.test(path), path).toBe(true); - } - expect(files.test("dependency-pins.yaml")).toBe(false); - expect(files.test("docs/reference/commands.mdx")).toBe(false); - - const scripts = packageJson.scripts; - const cliCoverageCalls = runLoggedPackageScript(scripts["test:coverage:cli"]); - const pluginCoverageCalls = runLoggedPackageScript(scripts["test:coverage:plugin"]); - const broadCheckCalls = runLoggedPackageScript(scripts.check); - const routinePrCalls = runLoggedPackageScript(scripts["validate:pr"]); - const repositoryCheckCalls = runLoggedPackageScript(scripts["checks:repository"]); - const legacyRepositoryChecks = runLoggedPackageScriptWithOutput(scripts.checks); - - expect(cliCoverageCalls.map(([command]) => command)).toEqual( - "npm npm tsx vitest tsx".split(" "), - ); - expect(cliCoverageCalls[3]).toEqual( - expect.arrayContaining(["--project", "cli", "integration", "--coverage"]), - ); - expect(cliCoverageCalls[4]).toEqual( - "tsx|scripts/check-coverage-ratchet.mts|coverage/cli/coverage-summary.json|ci/coverage-threshold-cli.json|CLI coverage".split( - "|", - ), - ); - expect(pluginCoverageCalls[0]).toEqual( - expect.arrayContaining([ - "--project", - "plugin", - "--coverage.include=nemoclaw/src/**/*.ts", - "--coverage.include=nemoclaw/src/**/*.cts", - ]), - ); - expect(pluginCoverageCalls[1]).toEqual( - "tsx|scripts/check-coverage-ratchet.mts|coverage/plugin/coverage-summary.json|ci/coverage-threshold-plugin.json|Plugin coverage".split( - "|", - ), - ); - expect(broadCheckCalls.map((call) => call.join(" "))).toEqual([ - "npx prek run --all-files --stage pre-commit", - "npx prek run --all-files --stage manual", - ]); - expect(routinePrCalls.map((call) => call.join(" "))).toEqual([ - "npx prek run --from-ref origin/main --to-ref HEAD --stage pre-commit", - "npx commitlint --from origin/main --to HEAD", - "npx prek run --from-ref origin/main --to-ref HEAD --stage pre-push", - ]); - expect(repositoryCheckCalls.map((call) => call.join(" "))).toEqual([ - "tsx scripts/checks/run.mts", - ]); - expect(scripts["check:diff"]).toBe("npm run validate:pr"); - expect(legacyRepositoryChecks.calls.map((call) => call.join(" "))).toEqual([ - "npm run checks:repository", - ]); - expect(legacyRepositoryChecks.stderr).toContain("npm run validate:pr"); - expect(legacyRepositoryChecks.stderr).toContain("npm run checks:repository"); - expect(legacyRepositoryChecks.stderr).toContain("runs only narrow repository checks"); - expect(scripts.lint).toContain("npm run checks:repository"); - expect(scripts["lint:fix"]).toContain("npm run checks:repository"); - expect(repositoryChecks?.entry).toBe("npm run checks:repository"); - }); - - // source-shape-contract: compatibility -- Pre-commit routing must apply the declarative guard to every supported test location - it("runs the source-shape guard for root and co-located tests", () => { - const hooks = prekConfig.repos.flatMap((repo) => repo.hooks ?? []); - const sourceShape = hooks.find((candidate) => candidate.id === "source-shape-test-budget"); - const files = new RegExp(sourceShape?.files ?? "(?!)", "u"); - - expect(sourceShape?.entry).toBe("npm run source-shape:check"); - for (const path of [ - "test/example.test.ts", - "src/lib/example.spec.ts", - "nemoclaw/src/example.test.ts", - "scripts/find-source-shape-tests.mts", - "ci/source-shape-test-budget.json", - ]) { - expect(files.test(path), path).toBe(true); - } - expect(files.test("src/lib/example.ts")).toBe(false); - }); - - // source-shape-contract: compatibility -- Changed-file routing must typecheck each project and its transitive configuration inputs - it("scopes pre-push typechecks to project and transitive inputs", () => { - const hooks = prekConfig.repos.flatMap((repo) => repo.hooks ?? []); - const pluginTypecheck = hooks.find((candidate) => candidate.id === "tsc-plugin"); - const cliTypecheck = hooks.find((candidate) => candidate.id === "tsc-cli"); - const jsTypecheck = hooks.find((candidate) => candidate.id === "tsc-js"); - const pluginFiles = new RegExp(pluginTypecheck?.files ?? "(?!)", "u"); - const files = new RegExp(cliTypecheck?.files ?? "(?!)", "u"); - const jsFiles = new RegExp(jsTypecheck?.files ?? "(?!)", "u"); - - expect(pluginTypecheck?.entry).toBe("npm --prefix nemoclaw run typecheck"); - expect(cliTypecheck?.entry).toBe("npm run typecheck:cli -- --incremental"); - expect(cliTypecheck?.always_run).toBeUndefined(); - for (const include of cliTypeScriptConfig.include) { - const representativeInput = include.replace("**/*", "nested/input"); - expect(files.test(representativeInput), include).toBe(true); - } - for (const path of [ - ".agents/skills/nemoclaw-maintainer-day/scripts/check-gates.ts", - ".agents/skills/nemoclaw-maintainer-day/scripts/shared.ts", - "agents/hermes/generate-config.ts", - "bin/nemoclaw.ts", - "scripts/check.ts", - "scripts/check.mts", - "src/lib/runner.ts", - "test/runner.test.ts", - "tools/e2e/workflow-boundary.mts", - "nemoclaw/src/lib/subprocess-env.ts", - "nemoclaw/src/blueprint/private-networks.ts", - "nemoclaw-blueprint/scripts/render.ts", - "src/lib/actions/sandbox/credentials.json", - "package.json", - "package-lock.json", - "tsconfig.cli.json", - "vitest.config.ts", - ]) { - expect(files.test(path), path).toBe(true); - } - for (const path of [ - ".agents/skills/example/scripts/unchecked.ts", - "agents/hermes/start.sh", - "docs/get-started/quickstart.mdx", - "nemoclaw/src/commands/status.ts", - "scripts/check.js", - ]) { - expect(files.test(path), path).toBe(false); - } - for (const path of [ - "nemoclaw/src/lib/subprocess-env.ts", - "nemoclaw/src/blueprint/private-networks.ts", - "nemoclaw/src/commands/status.ts", - ]) { - expect(pluginFiles.test(path), path).toBe(true); - } - expect(pluginFiles.test(".agents/skills/example/scripts/unchecked.ts")).toBe(false); - for (const path of ["bin/nemoclaw.js", "jsconfig.json", "package.json", "package-lock.json"]) { - expect(jsFiles.test(path), path).toBe(true); - } - expect(jsFiles.test("docs/_components/nemoclaw.js")).toBe(false); - }); - - // source-shape-contract: security -- Pull requests must execute base-trusted actions while main uses reviewed repository actions - it("reuses the same shared CI actions in PR and main workflows", () => { - expect(prWorkflow.on?.pull_request?.types).toEqual([ - "opened", - "synchronize", - "reopened", - "edited", - ]); - expect(prWorkflow["run-name"]).toBe( - "CI PR #${{ github.event.pull_request.number }} head ${{ github.event.pull_request.head.sha }} base ${{ github.event.pull_request.base.sha }} gate ${{ github.event.action != 'edited' || github.event.changes.base != null }}", - ); - expect(prWorkflow.concurrency).toEqual({ - group: - "${{ github.workflow }}-${{ github.ref }}-${{ github.event.action != 'edited' || github.event.changes.base != null }}", - "cancel-in-progress": true, - }); - expect( - requiredWorkflowStep(prWorkflow.jobs["static-checks"], "Checkout").with?.["fetch-depth"], - ).toBe(0); - for (const [jobName, stepName, trustedActionPath, mainActionPath] of [ - [ - "static-checks", - "Run static checks", - trustedPrActionPaths.staticChecks, - sharedActionPaths.staticChecks, - ], - [ - "build-typecheck", - "Run build and type checks", - trustedPrActionPaths.buildTypecheck, - sharedActionPaths.buildTypecheck, - ], - [ - "cli-test-shards", - "Run CLI coverage shard", - trustedPrActionPaths.cliCoverageShard, - sharedActionPaths.cliCoverageShard, - ], - [ - "cli-tests", - "Merge CLI coverage", - trustedPrActionPaths.cliCoverageMerge, - sharedActionPaths.cliCoverageMerge, - ], - [ - "plugin-tests", - "Run plugin coverage", - trustedPrActionPaths.pluginCoverage, - sharedActionPaths.pluginCoverage, - ], - ] as const) { - expect(stepUses(prWorkflow.jobs[jobName]), `PR ${jobName}`).toContain(trustedActionPath); - expect(stepUses(mainWorkflow.jobs[jobName]), `main ${jobName}`).toContain(mainActionPath); - expect(stepUses(prWorkflow.jobs[jobName]), `PR ${jobName}`).not.toContain(mainActionPath); - expect(stepUses(mainWorkflow.jobs[jobName]), `main ${jobName}`).not.toContain( - trustedActionPath, - ); - - const trustedCheckout = requiredWorkflowStep( - prWorkflow.jobs[jobName], - "Checkout trusted CI actions", - ); - expect(trustedCheckout.uses).toBe(trustedCheckoutAction); - expect(trustedCheckout.with?.ref).toBe("${{ github.event.pull_request.base.sha }}"); - expect(trustedCheckout.with?.path).toBe(".trusted-ci-actions"); - expect(trustedCheckout.with?.["persist-credentials"]).toBe(false); - expect(trustedCheckout.with?.["sparse-checkout-cone-mode"]).toBe(false); - for (const trustedActionDir of trustedActionDirs) { - expect(String(trustedCheckout.with?.["sparse-checkout"])).toContain(trustedActionDir); - } - expect( - requiredWorkflowStepIndex(prWorkflow.jobs[jobName], "Checkout trusted CI actions"), - ).toBeLessThan(requiredWorkflowStepIndex(prWorkflow.jobs[jobName], stepName)); - } - - expect(stepUses(prWorkflow.jobs["installer-integration"])).toContain( - trustedPrActionPaths.installerIntegration, - ); - expect(stepUses(prWorkflow.jobs["installer-integration"])).not.toContain( - sharedActionPaths.installerIntegration, - ); - expect(stepUses(mainWorkflow.jobs["installer-integration"])).toContain( - sharedActionPaths.installerIntegration, - ); - expect(stepUses(mainWorkflow.jobs["installer-integration"])).not.toContain( - trustedPrActionPaths.installerIntegration, - ); - const installerTrustedCheckout = requiredWorkflowStep( - prWorkflow.jobs["installer-integration"], - "Checkout trusted CI actions", - ); - expect(installerTrustedCheckout.uses).toBe(trustedCheckoutAction); - expect(installerTrustedCheckout.with?.ref).toBe("${{ github.event.pull_request.base.sha }}"); - expect(installerTrustedCheckout.with?.path).toBe(".trusted-ci-actions"); - expect(installerTrustedCheckout.with?.["persist-credentials"]).toBe(false); - expect(installerTrustedCheckout.with?.["sparse-checkout-cone-mode"]).toBe(false); - expect(String(installerTrustedCheckout.with?.["sparse-checkout"])).toContain( - ".github/actions/ci-installer-integration", - ); - const installerActionProbe = requiredWorkflowStep( - prWorkflow.jobs["installer-integration"], - "Detect trusted installer integration action", - ); - expect(installerActionProbe.id).toBe("trusted-installer-integration"); - expect(installerActionProbe.run).toContain( - ".trusted-ci-actions/.github/actions/ci-installer-integration/action.yaml", - ); - expect(installerActionProbe.run).toContain("available=true"); - expect(installerActionProbe.run).toContain("available=false"); - const installerActionStep = requiredWorkflowStep( - prWorkflow.jobs["installer-integration"], - "Run installer integration tests", - ); - expect(installerActionStep.if).toBe( - "${{ steps.trusted-installer-integration.outputs.available == 'true' }}", - ); - const bootstrapSetup = requiredWorkflowStep( - prWorkflow.jobs["installer-integration"], - "Setup Node.js for installer integration", - ); - expect(bootstrapSetup.if).toBe( - "${{ steps.trusted-installer-integration.outputs.available != 'true' }}", - ); - expect(bootstrapSetup.uses).toBe(trustedSetupNodeAction); - expect(bootstrapSetup.with?.["node-version"]).toBe("22"); - expect(bootstrapSetup.with?.cache).toBe("npm"); - const bootstrapInstall = requiredWorkflowStep( - prWorkflow.jobs["installer-integration"], - "Install installer integration dependencies", - ); - expect(bootstrapInstall.if).toBe( - "${{ steps.trusted-installer-integration.outputs.available != 'true' }}", - ); - expect(bootstrapInstall.run).toContain("npm install --ignore-scripts"); - expect(bootstrapInstall.run).toContain("cd nemoclaw && npm install --ignore-scripts"); - const bootstrapBuild = requiredWorkflowStep( - prWorkflow.jobs["installer-integration"], - "Build installer integration artifacts", - ); - expect(bootstrapBuild.if).toBe( - "${{ steps.trusted-installer-integration.outputs.available != 'true' }}", - ); - expect(bootstrapBuild.run).toContain("npm run build:cli"); - expect(bootstrapBuild.run).toContain("cd nemoclaw && npm run build"); - const bootstrapRun = requiredWorkflowStep( - prWorkflow.jobs["installer-integration"], - "Run installer integration tests (bootstrap)", - ); - expect(bootstrapRun.if).toBe( - "${{ steps.trusted-installer-integration.outputs.available != 'true' }}", - ); - expect(bootstrapRun.run).toBe("CI=true npx vitest run --project installer-integration"); - expect( - requiredWorkflowStepIndex( - prWorkflow.jobs["installer-integration"], - "Checkout trusted CI actions", - ), - ).toBeLessThan( - requiredWorkflowStepIndex( - prWorkflow.jobs["installer-integration"], - "Run installer integration tests", - ), - ); - expect( - requiredWorkflowStepIndex( - prWorkflow.jobs["installer-integration"], - "Detect trusted installer integration action", - ), - ).toBeLessThan( - requiredWorkflowStepIndex( - prWorkflow.jobs["installer-integration"], - "Run installer integration tests (bootstrap)", - ), - ); - - expect(stepUses(mainWorkflow.jobs.checks)).not.toContain("./.github/actions/basic-checks"); - expect(prWorkflow.jobs["cli-test-shards"].strategy?.["fail-fast"]).toBe(false); - expect(mainWorkflow.jobs["cli-test-shards"].strategy?.["fail-fast"]).toBe(false); - expect(prWorkflow.jobs["cli-test-shards"].strategy?.matrix?.shard).toEqual([...cliShardMatrix]); - expect(mainWorkflow.jobs["cli-test-shards"].strategy?.matrix?.shard).toEqual([ - ...cliShardMatrix, - ]); - for (const [workflowName, workflow] of [ - ["pull_request", prWorkflow], - ["main", mainWorkflow], - ] as const) { - expect(workflow.jobs["cli-test-shards"]["timeout-minutes"], workflowName).toBe(15); - const checkoutStep = requiredWorkflowStep(workflow.jobs["cli-test-shards"], "Checkout"); - const shardStep = requiredWorkflowStep( - workflow.jobs["cli-test-shards"], - "Run CLI coverage shard", - ); - const mergeStep = requiredWorkflowStep(workflow.jobs["cli-tests"], "Merge CLI coverage"); - expect(checkoutStep.with?.["fetch-depth"], `${workflowName} checkout depth`).toBe(0); - expect(shardStep.with?.shard, `${workflowName} shard input`).toBe("${{ matrix.shard }}"); - expect(shardStep.with?.["shard-count"], `${workflowName} shard-count input`).toBe( - cliShardCount, - ); - expect(mergeStep.with?.["shard-count"], `${workflowName} merge shard-count`).toBe( - cliShardCount, - ); - expect(workflow.jobs["cli-tests"].permissions?.actions, workflowName).toBe("read"); - expect(workflow.jobs.checks.permissions?.actions, workflowName).toBe("read"); - } - }); - - // source-shape-contract: security -- Base-trusted PR sharding must retain hermetic coverage while retired duplicate lanes stay absent - it("folds hermetic E2E support and Ollama proxy coverage into existing Vitest lanes", () => { - const shardRun = requiredStep( - sharedActions.cliCoverageShard, - "Run CLI coverage and E2E support shard", - ); - expect(shardRun.run).toContain("--project cli --project integration --project e2e-support"); - - const parityStep = requiredStep( - sharedActions.cliCoverageShard, - "Validate changed live E2E mock parity", - ); - expect(parityStep.if).toBe("${{ inputs.shard == '1' }}"); - expect(parityStep.run).toContain("base=HEAD^1"); - expect(parityStep.run).toContain("head=HEAD^2"); - expect(parityStep.run).toContain('base="$PUSH_BASE_SHA"'); - expect(parityStep.run).toContain("npx tsx scripts/checks/e2e-mock-parity.mts"); - expect(parityStep.run).not.toContain("scripts/checks/e2e-mock-parity.ts"); - const trustedCapabilityProbe = requiredWorkflowStep( - prWorkflow.jobs["cli-test-shards"], - "Detect trusted E2E support sharding", - ); - expect(trustedCapabilityProbe.id).toBe("trusted-shard-capabilities"); - expect(trustedCapabilityProbe.run).toContain("--project e2e-support"); - expect(trustedCapabilityProbe.run).toContain("e2e-support=true"); - expect(trustedCapabilityProbe.run).toContain("e2e-support=false"); - - const bootstrapParity = requiredWorkflowStep( - prWorkflow.jobs["cli-test-shards"], - "Validate changed live E2E mock parity (bootstrap)", - ); - expect(bootstrapParity.if).toBe( - "${{ steps.trusted-shard-capabilities.outputs.e2e-support != 'true' && matrix.shard == 1 }}", - ); - expect(bootstrapParity.run).toContain("--base HEAD^1 --head HEAD^2"); - - const bootstrapShard = requiredWorkflowStep( - prWorkflow.jobs["cli-test-shards"], - "Run E2E support shard (bootstrap)", - ); - expect(bootstrapShard.if).toBe( - "${{ steps.trusted-shard-capabilities.outputs.e2e-support != 'true' }}", - ); - expect(bootstrapShard.run).toContain("--project e2e-support"); - expect(bootstrapShard.run).toContain( - '--shard="${E2E_SUPPORT_SHARD}/${E2E_SUPPORT_SHARD_COUNT}"', - ); - - for (const workflow of [prWorkflow, mainWorkflow]) { - expect(workflow.jobs["e2e-support"]).toBeUndefined(); - expect(workflow.jobs["test-e2e-ollama-proxy"]).toBeUndefined(); - expect(workflow.jobs.checks.needs).not.toContain("e2e-support"); - expect(workflow.jobs.checks.needs).not.toContain("test-e2e-ollama-proxy"); - } - - expect(stepRuns(sharedActions.staticChecks).join("\n")).not.toContain( - "skills-frontmatter.test.ts", - ); - const trustedRatchetDependencies = requiredStep( - sharedActions.staticChecks, - "Install base-trusted createRequire verifier dependencies", - ); - const trustedRatchet = requiredStep( - sharedActions.staticChecks, - "Enforce base-trusted createRequire allowlist ratchet", - ); - expect(trustedRatchetDependencies.run).toBe( - 'npm ci --ignore-scripts --no-audit --no-fund --prefix "$GITHUB_ACTION_PATH"', - ); - expect(trustedRatchet.run).toBe( - 'node --experimental-strip-types "$GITHUB_ACTION_PATH/create-require-ratchet.mts"', - ); - expect(stepRuns(sharedActions.staticChecks)).not.toContain( - 'npx tsx "$GITHUB_ACTION_PATH/create-require-ratchet.mts"', - ); - expect( - requiredStepIndex( - sharedActions.staticChecks, - "Install base-trusted createRequire verifier dependencies", - ), - ).toBeLessThan( - requiredStepIndex( - sharedActions.staticChecks, - "Enforce base-trusted createRequire allowlist ratchet", - ), - ); - expect( - requiredStepIndex( - sharedActions.staticChecks, - "Enforce base-trusted createRequire allowlist ratchet", - ), - ).toBeLessThan(requiredStepIndex(sharedActions.staticChecks, "Install dependencies")); - - const ratchetPackage = JSON.parse( - readFileSync(".github/actions/ci-static-checks/package.json", "utf8"), - ) as { dependencies?: Record }; - const ratchetLock = JSON.parse( - readFileSync(".github/actions/ci-static-checks/package-lock.json", "utf8"), - ) as { - packages?: Record; - }; - const ratchetRuntime = readFileSync( - ".github/actions/ci-static-checks/create-require-ratchet.mts", - "utf8", - ); - expect(ratchetPackage.dependencies).toEqual({ typescript: "6.0.3" }); - expect(ratchetLock.packages?.["node_modules/typescript"]?.version).toBe("6.0.3"); - expect(ratchetLock.packages?.["node_modules/typescript"]?.integrity).toMatch(/^sha512-/); - expect(ratchetRuntime).toContain( - 'import ts from "./node_modules/typescript/lib/typescript.js";', - ); - expect(ratchetRuntime).not.toMatch(/from ["']typescript["']/); - }); - - // source-shape-contract: security -- Downloaded CI tooling must use a committed digest rather than upstream metadata - it("pins downloaded CI tooling to reviewed integrity", () => { - const docsRuns = stepRuns(prWorkflow.jobs["docs-only-checks"]).join("\n"); - for (const runs of [stepRuns(sharedActions.staticChecks).join("\n"), docsRuns]) { - expect(runs).toContain("6bf226944684f56c84dd014e8b979d27425c0148f61b3bd99bcc6f39e9dc5a47"); - expect(runs).not.toMatch(/HADOLINT_URL.*sha256|EXPECTED=\$\(curl/); - } - expect(docsRuns.indexOf("HADOLINT_SHA256")).toBeLessThan(docsRuns.indexOf("prek run")); - }); - it("validates CLI shard inputs before using them in shell commands", () => { const shardValidationStep = requiredStep( sharedActions.cliCoverageShard, @@ -1249,44 +514,6 @@ describe("pull request and main workflow contracts", () => { } }); - // source-shape-contract: security -- Growth-budget changes must inspect trusted GitHub data without fetching PR-authored URLs - it("keeps the trusted test-size guard closed around budget policy changes", () => { - const growthGuardrails = readYaml( - ".github/workflows/codebase-growth-guardrails.yaml", - ); - const guardJob = growthGuardrails.jobs["codebase-growth-guardrails"]; - const guardRun = stepRuns(guardJob).join("\n"); - const guardEnv = JSON.stringify((guardJob.steps ?? []).map((step) => step.env ?? {})); - expect(guardEnv).toContain("HEAD_REPO"); - expect(guardRun).not.toContain(".raw_url"); - expect(guardRun).not.toContain("node <<'NODE'"); - expect(guardRun).toContain("tools/growth-guardrails/test-size-budget.mts"); - expect(guardRun).toContain("tools/growth-guardrails/test-conditionals.mts"); - }); - - // source-shape-contract: security -- Coverage publication must exclude fork-authored reports and pin the publishing action - it("publishes coverage only from same-repository code (#6692)", () => { - const sameRepositoryGuard = - "${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}"; - const uploadAction = "actions/upload-code-coverage@abb5995db9e0199b0e2bb9dbd136fce4cb1ec4d3"; - const reports = [ - { - action: sharedActions.cliCoverageMerge, - uploadStep: "Upload CLI coverage report", - }, - { - action: sharedActions.pluginCoverage, - uploadStep: "Upload plugin coverage report", - }, - ] as const; - - for (const report of reports) { - const uploadStep = requiredStep(report.action, report.uploadStep); - expect(uploadStep.if).toBe(sameRepositoryGuard); - expect(uploadStep.uses).toBe(uploadAction); - } - }); - it("links every failed CLI shard and falls back safely when job metadata is unavailable", () => { const runUrl = "https://github.com/NVIDIA/NemoClaw/actions/runs/123"; const failedShards = workflowJobListing([ @@ -1449,48 +676,4 @@ describe("pull request and main workflow contracts", () => { ); expect(oversizedFailure.stdout).not.toContain("actions/runs/123/job/"); }); - - // source-shape-contract: security -- CI dependency installs must never execute package lifecycle scripts from fetched code - it("does not run npm lifecycle scripts during CI dependency installs", () => { - for (const [actionName, action] of Object.entries(sharedActions)) { - const installRuns = stepRuns(action).filter((run) => run.includes("npm install")); - - expect(installRuns.length, `${actionName} install steps`).toBeGreaterThan(0); - for (const run of installRuns) { - for (const line of run.split("\n").map((candidate) => candidate.trim())) { - if (line.includes("npm install")) { - expect(line, `${actionName} install command`).toContain("--ignore-scripts"); - } - } - } - } - - const docsOnlyInstall = stepRuns(prWorkflow.jobs["docs-only-checks"]).find((run) => - run.includes("npm install"), - ); - expect(docsOnlyInstall).toBe("npm install --ignore-scripts"); - const installerBootstrapInstall = stepRuns(prWorkflow.jobs["installer-integration"]).find( - (run) => run.includes("npm install"), - ); - expect(installerBootstrapInstall).toContain("npm install --ignore-scripts"); - expect(installerBootstrapInstall).toContain("cd nemoclaw && npm install --ignore-scripts"); - }); - - // source-shape-contract: security -- Workflow checkouts must not leave write-capable credentials available to later steps - it("does not persist checkout credentials in PR or main jobs", () => { - for (const [workflowName, workflow] of [ - ["pull_request", prWorkflow], - ["main", mainWorkflow], - ] as const) { - for (const [jobName, job] of Object.entries(workflow.jobs)) { - for (const step of job.steps ?? []) { - if (!step.uses?.startsWith("actions/checkout@")) { - continue; - } - - expect(step.with?.["persist-credentials"], `${workflowName} ${jobName}`).toBe(false); - } - } - } - }); }); diff --git a/test/regression-e2e-workflow.test.ts b/test/regression-e2e-workflow.test.ts deleted file mode 100644 index 4fbb71e603d..00000000000 --- a/test/regression-e2e-workflow.test.ts +++ /dev/null @@ -1,165 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { describe, expect, it } from "vitest"; - -import { - PREPARE_E2E_ACTION, - PREPARE_E2E_STEP, -} from "../tools/e2e/prepare-e2e-workflow-boundary.mts"; -import { readYaml, type WorkflowStep } from "./helpers/e2e-workflow-contract"; - -type RegressionWorkflow = { - on?: { - workflow_dispatch?: { - inputs?: { - jobs?: { - description?: string; - }; - }; - }; - }; - permissions?: Record; - jobs?: Record< - string, - { - permissions?: Record; - steps?: WorkflowStep[]; - "timeout-minutes"?: number; - uses?: string; - } - >; -}; - -const FULL_SHA_ACTION = /@[0-9a-f]{40}$/i; - -function preparedVitestJobs(workflow: RegressionWorkflow) { - return Object.entries(workflow.jobs ?? {}).filter(([, job]) => { - const steps = job.steps ?? []; - const invokesVitest = steps.some((step) => /\bvitest\s+run\b/.test(step.run ?? "")); - const usesDirectSetup = steps.some((step) => step.name === "Setup Node"); - return invokesVitest && !usesDirectSetup; - }); -} - -describe("Regression E2E workflow contract", () => { - const workflow = readYaml(".github/workflows/regression-e2e.yaml"); - - // source-shape-contract: compatibility -- Keeps the executable WhatsApp regression on the supported Vitest live runner - it("runs WhatsApp compact QR through Vitest instead of the retired shell script", () => { - const job = workflow.jobs?.["whatsapp-qr-compact-e2e"]; - const runText = (job?.steps ?? []).map((step) => step.run ?? "").join("\n"); - - expect(runText).toContain("test/e2e/live/whatsapp-qr-compact.test.ts"); - expect(runText).toContain("npx vitest run --project e2e-live"); - }); - - // source-shape-contract: security -- Preserves the public NVIDIA credential boundary for Model Router regression execution - it("stages the public NVIDIA key for the Model Router's NVIDIA credential", () => { - const branchValidationCallers = Object.values(workflow.jobs ?? {}).filter( - (job) => job.uses === "./.github/workflows/e2e-branch-validation.yaml", - ); - const job = workflow.jobs?.["model-router-provider-routed-inference-e2e"]; - const runStep = job?.steps?.find( - (step) => step.name === "Run Model Router provider-routed inference E2E test", - ); - expect(runStep?.env?.NVIDIA_API_KEY).toBe("${{ secrets.NVIDIA_API_KEY }}"); - expect(runStep?.env?.NVIDIA_INFERENCE_API_KEY).toBeUndefined(); - expect(branchValidationCallers).toHaveLength(0); - expect(workflow.permissions).toEqual({ - actions: "read", - checks: "write", - contents: "read", - "pull-requests": "write", - }); - }); - - // source-shape-contract: security -- Every discovered non-hermetic Vitest job must use immutable credential-free preparation - it("prepares every discovered non-hermetic Vitest job before execution (#6692)", () => { - const preparedJobs = preparedVitestJobs(workflow); - - expect(preparedJobs.length).toBeGreaterThan(0); - for (const [jobName, job] of preparedJobs) { - const steps = job?.steps ?? []; - const checkoutIndex = steps.findIndex((step) => step.uses?.startsWith("actions/checkout@")); - const prepareIndex = steps.findIndex((step) => step.name === PREPARE_E2E_STEP); - const runIndex = steps.findIndex((step) => /\bvitest\s+run\b/.test(step.run ?? "")); - const checkout = steps[checkoutIndex]; - const prepare = steps[prepareIndex]; - - expect(job?.permissions, jobName).toEqual({ contents: "read" }); - expect(checkout?.uses, jobName).toMatch(FULL_SHA_ACTION); - expect(checkout?.with?.["persist-credentials"], jobName).toBe(false); - expect(prepare?.uses, jobName).toBe(PREPARE_E2E_ACTION); - expect( - prepare?.with === undefined || - JSON.stringify(prepare.with) === JSON.stringify({ "build-cli": "false" }), - `${jobName} prepare inputs`, - ).toBe(true); - expect(prepare?.env, jobName).toBeUndefined(); - expect(prepareIndex, jobName).toBeGreaterThan(checkoutIndex); - expect(runIndex, jobName).toBeGreaterThan(prepareIndex); - expect( - steps.filter((step) => step.uses === PREPARE_E2E_ACTION), - jobName, - ).toHaveLength(1); - expect( - steps.map((step) => step.name), - jobName, - ).not.toContain("Setup Node"); - expect( - steps.map((step) => step.name), - jobName, - ).not.toContain("Install root dependencies"); - expect( - steps.map((step) => step.name), - jobName, - ).not.toContain("Build CLI"); - } - }); - - // source-shape-contract: security -- Keeps the custom-plugin EXDEV regression immutable and free of repository secrets - it("runs the OpenClaw custom-plugin lifecycle and EXDEV guard in a secret-free lane", () => { - const releaseJob = workflow.jobs?.["openclaw-plugin-runtime-exdev-release-e2e"]; - const job = workflow.jobs?.["openclaw-plugin-runtime-exdev-e2e"]; - const steps = job?.steps ?? []; - const runText = steps.map((step) => step.run ?? "").join("\n"); - const checkoutStep = steps.find((step) => - String(step.uses ?? "").startsWith("actions/checkout@"), - ); - const setupNodeStep = steps.find((step) => step.name === "Setup Node"); - const runVitestStep = steps.find( - (step) => - step.name === "Run OpenClaw custom-plugin lifecycle and runtime-deps EXDEV Vitest test", - ); - const serializedJob = JSON.stringify(job); - - expect(job?.permissions).toEqual({ contents: "read" }); - expect(job?.["timeout-minutes"]).toBe(105); - expect(checkoutStep?.uses).toMatch(FULL_SHA_ACTION); - expect(checkoutStep?.with?.["persist-credentials"]).toBe(false); - expect(setupNodeStep?.uses).toMatch(FULL_SHA_ACTION); - expect(runVitestStep?.env?.NEMOCLAW_RUN_LIVE_E2E).toBe("1"); - expect(serializedJob).not.toContain("${{ secrets."); - expect(serializedJob).not.toMatch(/"secrets"\s*:\s*"inherit"/); - for (const step of steps) { - expect( - step.env?.NVIDIA_INFERENCE_API_KEY, - step.name ?? step.uses ?? "", - ).toBeUndefined(); - } - - expect(runText).toContain("test/e2e/live/openclaw-plugin-runtime-exdev.test.ts"); - expect(runText).toContain("-t current-lifecycle"); - expect(runText).toContain("npx vitest run --project e2e-live"); - expect(runText).toContain("npm ci --ignore-scripts"); - expect(runText).toContain("npm run build:cli"); - - const releaseRunText = (releaseJob?.steps ?? []).map((step) => step.run ?? "").join("\n"); - expect(releaseJob?.permissions).toEqual({ contents: "read" }); - expect(releaseJob?.["timeout-minutes"]).toBe(55); - expect(JSON.stringify(releaseJob)).not.toContain("${{ secrets."); - expect(releaseRunText).toContain("test/e2e/live/openclaw-plugin-runtime-exdev.test.ts"); - expect(releaseRunText).toContain("-t release-baseline"); - }); -}); diff --git a/test/release-lkg-brev-image.test.ts b/test/release-lkg-brev-image.test.ts index 9a23835476a..e26a449ff17 100644 --- a/test/release-lkg-brev-image.test.ts +++ b/test/release-lkg-brev-image.test.ts @@ -284,30 +284,4 @@ describe("LKG production image dispatch", () => { expect(summary).toContain("Downstream run: `unavailable`"); expect(summary).not.toContain("actions/runs/"); }); - - // source-shape-contract: security -- The secret-bearing LKG trigger must stay canonical, deletion-safe, read-only, and immutable - it("keeps LKG dispatch inside the trusted secret boundary (#6772)", () => { - const workflow = readYaml(".github/workflows/release-lkg-brev-image.yaml"); - const job = workflow.jobs["dispatch-production-image"]; - const checkout = job.steps?.find((step) => step.name === "Check out LKG target"); - const dispatch = job.steps?.find((step) => step.name === "Dispatch production image build"); - - expect(workflow.on?.push?.tags).toEqual(["lkg"]); - expect(workflow.permissions).toEqual({ contents: "read" }); - expect(job.if).toBe( - "${{ github.repository == 'NVIDIA/NemoClaw' && github.event.deleted == false }}", - ); - expect(job["timeout-minutes"]).toBe(5); - expect(checkout?.uses).toMatch(/^actions\/checkout@[0-9a-f]{40}$/u); - expect(checkout?.with).toEqual({ - ref: "${{ github.sha }}", - "fetch-depth": 0, - "persist-credentials": false, - }); - expect(dispatch?.env).toEqual({ - LKG_SHA: "${{ github.sha }}", - NEMOCLAW_IMAGE_DISPATCH_TOKEN: "${{ secrets.NEMOCLAW_IMAGE_DISPATCH_TOKEN }}", - }); - expect(dispatch?.run).toBe("scripts/release-lkg-brev-image.sh"); - }); }); diff --git a/test/reviewed-npm-audit-workflow.test.ts b/test/reviewed-npm-audit-workflow.test.ts index 7dd961423f7..b2352442b1a 100644 --- a/test/reviewed-npm-audit-workflow.test.ts +++ b/test/reviewed-npm-audit-workflow.test.ts @@ -137,132 +137,6 @@ describe("trusted reviewed npm audit workflow (#5896)", () => { ); }); - // source-shape-contract: security -- PR dependency audit code must come from the base SHA or the one-time signed bootstrap - it("runs PR audits from trusted code and keeps the main audit on the checked-in action", () => { - const pr = readYaml(".github/workflows/pr.yaml"); - const main = readYaml(".github/workflows/main.yaml"); - const prJob = pr.jobs["reviewed-npm-audit"]; - const mainJob = main.jobs["reviewed-npm-audit"]; - - const trustedCheckout = requiredStep(prJob, "Checkout trusted reviewed npm audit"); - expect(trustedCheckout.with).toMatchObject({ - ref: "${{ github.event.pull_request.base.sha }}", - path: ".trusted-reviewed-npm-audit", - "persist-credentials": false, - "sparse-checkout-cone-mode": false, - }); - const sparseCheckout = String(trustedCheckout.with?.["sparse-checkout"]); - expect(sparseCheckout).toContain(".github/actions/ci-reviewed-npm-audit"); - expect(sparseCheckout).toContain("ci/npm-audit-exceptions.json"); - expect(sparseCheckout).toContain("ci/reviewed-npm-audit.json"); - expect(sparseCheckout).toContain("scripts/audit-reviewed-npm-graph.mts"); - expect(sparseCheckout).toContain("scripts/lib/openclaw-npm-remediation.mts"); - expect(sparseCheckout).toContain("scripts/lib/reviewed-npm-archive.mts"); - expect(sparseCheckout).toContain("scripts/lib/reviewed-npm-audit.mts"); - - const detection = requiredStep(prJob, "Detect trusted reviewed npm audit schema"); - expect(detection.id).toBe("trusted-reviewed-npm-audit"); - expect(detection.run).toContain("resolveTrustedAuditConfigPath(TRUSTED_REPO_ROOT)"); - expect(detection.run).toContain(".trusted-reviewed-npm-audit/ci/npm-audit-exceptions.json"); - expect(detection.run).toContain(".trusted-reviewed-npm-audit/ci/reviewed-npm-audit.json"); - expect(detection.run).toContain( - ".trusted-reviewed-npm-audit/scripts/lib/openclaw-npm-remediation.mts", - ); - expect(detection.run).toContain( - ".trusted-reviewed-npm-audit/scripts/lib/reviewed-npm-audit.mts", - ); - - const bootstrap = requiredStep(prJob, "Checkout pinned bootstrap reviewed npm audit"); - expect(bootstrap.if).toBe(BOOTSTRAP_IF); - expect(bootstrap.with).toMatchObject({ - repository: "HOYALIM/NemoClaw", - ref: BOOTSTRAP_SHA, - path: ".trusted-reviewed-npm-audit-bootstrap", - "persist-credentials": false, - }); - const bootstrapSparseCheckout = String(bootstrap.with?.["sparse-checkout"]); - expect(bootstrapSparseCheckout).toContain("ci/npm-audit-exceptions.json"); - expect(bootstrapSparseCheckout).toContain("scripts/lib/openclaw-npm-remediation.mts"); - expect(bootstrapSparseCheckout).toContain("scripts/lib/reviewed-npm-audit.mts"); - const rejectUnavailable = requiredStep(prJob, "Reject unavailable trusted reviewed npm audit"); - expect(rejectUnavailable.if).toBe(REJECT_UNAVAILABLE_IF); - expect(rejectUnavailable.run).toContain("exit 1"); - expect(requiredStep(prJob, "Audit reviewed production npm graphs")).toMatchObject({ - if: "${{ steps.trusted-reviewed-npm-audit.outputs.available == 'true' }}", - uses: "./.trusted-reviewed-npm-audit/.github/actions/ci-reviewed-npm-audit", - with: { - "target-root": "${{ github.workspace }}", - "report-dir": "artifacts/reviewed-npm-audit", - }, - }); - expect( - requiredStep(prJob, "Audit reviewed production npm graphs (pinned bootstrap)"), - ).toMatchObject({ - if: BOOTSTRAP_IF, - uses: "./.trusted-reviewed-npm-audit-bootstrap/.github/actions/ci-reviewed-npm-audit", - }); - expect(requiredStep(mainJob, "Audit reviewed production npm graphs")).toMatchObject({ - uses: "./.github/actions/ci-reviewed-npm-audit", - with: { - "target-root": "${{ github.workspace }}", - "report-dir": "artifacts/reviewed-npm-audit", - }, - }); - }); - - // source-shape-contract: security -- The trusted composite action must execute only its bundled driver while treating the PR checkout as explicit data - it("executes the trusted driver and helper against explicit target inputs", () => { - const action = fs.readFileSync( - path.join(REPO_ROOT, ".github", "actions", "ci-reviewed-npm-audit", "action.yaml"), - "utf8", - ); - const driver = fs.readFileSync( - path.join(REPO_ROOT, "scripts", "audit-reviewed-npm-graph.mts"), - "utf8", - ); - const helper = fs.readFileSync( - path.join(REPO_ROOT, "scripts", "lib", "reviewed-npm-audit.mts"), - "utf8", - ); - const npmBootstrap = fs.readFileSync( - path.join( - REPO_ROOT, - ".github", - "actions", - "ci-reviewed-npm-audit", - "verify-and-install-npm.sh", - ), - "utf8", - ); - - expect(action).toContain('node-version: "22.23.1"'); - expect(action).toContain('NEMOCLAW_REVIEWED_NPM_VERSION: "10.9.4"'); - expect(action).toContain("NEMOCLAW_REVIEWED_NPM_INTEGRITY: >-"); - expect(action).toContain( - "sha512-OnUG836FwboQIbqtefDNlyR0gTHzIfwRfE3DuiNewBvnMnWEpB0VEXwBlFVgqpNzIgYo/MHh3d2Hel/pszapAA==", - ); - expect(action).toContain("run: '\"$GITHUB_ACTION_PATH/verify-and-install-npm.sh\"'"); - expect(action).not.toContain("npm install --global npm@10.9.4"); - expect(npmBootstrap).toContain('npm pack "npm@$version"'); - expect(npmBootstrap).toContain('crypto.createHash("sha512")'); - expect(npmBootstrap).toContain('if [ "$actual_integrity" != "$expected_integrity" ]'); - expect(npmBootstrap).toContain('npm install --global "$archive"'); - expect( - npmBootstrap.indexOf('if [ "$actual_integrity" != "$expected_integrity" ]'), - ).toBeLessThan(npmBootstrap.indexOf('npm install --global "$archive"')); - expect(npmBootstrap).toContain("--ignore-scripts --no-audit --no-fund --offline"); - expect(action).toContain("NEMOCLAW_REVIEWED_NPM_AUDIT_TARGET_ROOT"); - expect(action).toContain("NEMOCLAW_REVIEWED_NPM_AUDIT_REPORT_DIR"); - expect(action).toContain( - 'node --experimental-strip-types "$GITHUB_ACTION_PATH/../../../scripts/audit-reviewed-npm-graph.mts"', - ); - expect(action).not.toContain("run: node --experimental-strip-types scripts/"); - expect(driver).toContain("resolveTrustedAuditConfigPath(TRUSTED_REPO_ROOT)"); - expect(helper).toContain("const NPM_AUDIT_ATTEMPT_TIMEOUT_MS = 45_000"); - expect(helper).toContain("timeout: NPM_AUDIT_ATTEMPT_TIMEOUT_MS"); - expect(driver).not.toContain('resolveTargetPath(\n "ci/reviewed-npm-audit.json"'); - }); - it("rejects a mismatched npm bootstrap archive before installation (#8253)", () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-reviewed-npm-bootstrap-")); const bin = path.join(root, "bin"); diff --git a/test/wechat-runtime-audit-workflow.test.ts b/test/wechat-runtime-audit-workflow.test.ts index 5dd025950db..411cc4f5811 100644 --- a/test/wechat-runtime-audit-workflow.test.ts +++ b/test/wechat-runtime-audit-workflow.test.ts @@ -168,70 +168,6 @@ function requiredStep(job: WorkflowJob, name: string): WorkflowStep { } describe("WeChat runtime audit and install-cache gates (#5896)", () => { - // source-shape-contract: security -- Trusted PR and main workflows must enforce the reviewed WeChat runtime audit boundary - it("makes the trusted audit required in PR and main workflows", () => { - const pr = readYaml(".github/workflows/pr.yaml"); - const main = readYaml(".github/workflows/main.yaml"); - const prJob = pr.jobs["wechat-runtime-audit"]; - const mainJob = main.jobs["wechat-runtime-audit"]; - - const trustedCheckout = requiredStep(prJob, "Checkout trusted WeChat runtime audit"); - expect(trustedCheckout.with).toMatchObject({ - ref: "${{ github.event.pull_request.base.sha }}", - path: ".trusted-wechat-audit", - "persist-credentials": false, - "sparse-checkout-cone-mode": false, - }); - expect(String(trustedCheckout.with?.["sparse-checkout"])).toContain( - ".github/actions/ci-wechat-runtime-audit", - ); - - const bootstrapCheckout = requiredStep(prJob, "Checkout pinned bootstrap WeChat runtime audit"); - expect(bootstrapCheckout.if).toBe( - "${{ steps.trusted-wechat-audit.outputs.available != 'true' && github.event.pull_request.number == 6739 && github.event.pull_request.head.repo.full_name == 'HOYALIM/NemoClaw' }}", - ); - expect(bootstrapCheckout.with).toMatchObject({ - repository: "HOYALIM/NemoClaw", - ref: "0d2256d71d5bbba3bcaaaa4d01714fa56f22d1e2", - path: ".trusted-wechat-audit-bootstrap", - "persist-credentials": false, - }); - expect(requiredStep(prJob, "Audit locked WeChat runtime graph").uses).toBe( - "./.trusted-wechat-audit/.github/actions/ci-wechat-runtime-audit", - ); - expect(requiredStep(prJob, "Audit locked WeChat runtime graph (pinned bootstrap)").uses).toBe( - "./.trusted-wechat-audit-bootstrap/.github/actions/ci-wechat-runtime-audit", - ); - expect(requiredStep(mainJob, "Audit locked WeChat runtime graph").uses).toBe( - "./.github/actions/ci-wechat-runtime-audit", - ); - - for (const [workflowName, workflow, job] of [ - ["pr", pr, prJob], - ["main", main, mainJob], - ] as const) { - const upload = requiredStep(job, "Upload WeChat runtime audit evidence"); - expect(upload.if).toBe("${{ always() }}"); - expect(upload.uses).toBe("actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a"); - expect(upload.with).toMatchObject({ - path: "artifacts/wechat-runtime-audit", - "if-no-files-found": "error", - }); - - expect(workflow.jobs.checks.needs).toContain("wechat-runtime-audit"); - const gate = requiredStep( - workflow.jobs.checks, - workflowName === "pr" ? "Verify required PR checks" : "Verify required main checks", - ); - expect(gate.env).toMatchObject({ - WECHAT_RUNTIME_AUDIT_RESULT: "${{ needs['wechat-runtime-audit'].result }}", - }); - expect(gate.run).toContain( - 'require_success "wechat-runtime-audit" "$WECHAT_RUNTIME_AUDIT_RESULT"', - ); - } - }); - it("audits the installed graph and exercises the exact archive through a copied cache", () => { const script = fs.readFileSync(auditScript, "utf8"); for (const fragment of [ From bc57de5bf113d586e2a7dada07e633360c4cd8d9 Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Sat, 8 Aug 2026 14:33:29 -0700 Subject: [PATCH 02/13] docs(ci): correct workflow coverage claims --- .github/workflows/regression-e2e.yaml | 2 +- agents/openclaw/dependency-review.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/regression-e2e.yaml b/.github/workflows/regression-e2e.yaml index e47e56871d7..22e1dc41743 100644 --- a/.github/workflows/regression-e2e.yaml +++ b/.github/workflows/regression-e2e.yaml @@ -5,7 +5,7 @@ name: E2E / Regression Runner # Regression E2E holding pen. # -# Jobs here are intentionally NOT part of the scheduled E2E workflow. They are +# Jobs here are intentionally not part of the main-push E2E workflow. They are # failing-test-first coverage guards or high-signal regressions that should be # easy to dispatch while the owning fix is in flight. Periodically review this # workflow and promote stable/high-value jobs into .github/workflows/e2e.yaml. diff --git a/agents/openclaw/dependency-review.md b/agents/openclaw/dependency-review.md index 364099cda06..9cf0edef01e 100644 --- a/agents/openclaw/dependency-review.md +++ b/agents/openclaw/dependency-review.md @@ -50,7 +50,7 @@ The reviewed audit wrapper reports lower-severity production findings and blocks Removal condition: delete the PR #6739 bootstrap checkout, its paired conditional audit step, and the bootstrap-specific test assertions in the first follow-up after this PR merges, before the next release tag; all later PRs must use the normal base-SHA action path. - Advisory command: `npm ci --ignore-scripts --omit=dev --legacy-peer-deps --prefix agents/openclaw/wechat-runtime && npm audit --omit=dev --audit-level=low --json --prefix agents/openclaw/wechat-runtime && npm audit signatures --prefix agents/openclaw/wechat-runtime`. - Advisory review: `2026-07-12`; result: `0` known vulnerabilities across the resolved production graph. -- Regression tests: `test/wechat-locked-install.test.ts` keeps the manifest runtime-lock paths and installer verification dispatch synchronized; `test/verify-wechat-runtime-lock.test.ts` proves the installed graph and OpenClaw peer-range compatibility fail closed; `test/wechat-runtime-audit-workflow.test.ts` keeps the Docker cache lifecycle, base-trusted required CI gate, evidence upload, audit threshold, bounded download-only signature retry, invalid-signature denial, and real npm-pack boundary synchronized. +- Regression tests: `test/wechat-locked-install.test.ts` keeps the manifest runtime-lock paths and installer verification dispatch synchronized; `test/verify-wechat-runtime-lock.test.ts` proves the installed graph and OpenClaw peer-range compatibility fail closed; `test/wechat-runtime-audit-workflow.test.ts` keeps the Docker cache lifecycle, audit threshold, bounded download-only signature retry, invalid-signature denial, and real npm-pack boundary synchronized. The dedicated graph intentionally omits the plugin's `openclaw` peer dependency. The image already installs and integrity-verifies the reviewed OpenClaw runtime separately; auto-installing another OpenClaw copy would create a second unreviewed runtime graph. Disabling scripts also prevents transitive packages from executing lifecycle code during the trusted image build. From 42c702b4bab5c1ced87fd6003507497e8b27d852 Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Sat, 8 Aug 2026 15:21:09 -0700 Subject: [PATCH 03/13] ci(workflows): finish workflow cleanup --- .../build-base-image-platform/action.yaml | 131 +++ .../actions/ci-build-typecheck/action.yaml | 7 +- .../actions/ci-cli-coverage-shard/action.yaml | 7 +- .github/actions/ci-install-dependencies.sh | 8 + .../ci-installer-integration/action.yaml | 7 +- .../actions/ci-plugin-coverage/action.yaml | 7 +- .../actions/ci-reviewed-npm-audit/action.yaml | 1 + .../ci-wechat-runtime-audit/action.yaml | 13 +- .../publish-base-image-manifest/action.yaml | 185 +++++ .github/workflows/base-image.yaml | 760 ++---------------- .github/workflows/code-scanning.yaml | 6 +- .github/workflows/commit-lint.yaml | 2 + .github/workflows/docker-pin-check.yaml | 2 + .github/workflows/docs-cli-parity-pr.yaml | 2 + .github/workflows/docs-links-pr.yaml | 1 + .github/workflows/macos-e2e.yaml | 3 + .github/workflows/managed-images.yaml | 6 + .../workflows/pr-merge-conflict-fixer.yaml | 4 + .github/workflows/pr-review-advisor.yaml | 1 + .github/workflows/pr-self-hosted.yaml | 16 + .github/workflows/pr.yaml | 7 + .github/workflows/regression-e2e.yaml | 2 +- .github/workflows/release-latest-tag.yaml | 1 + .github/workflows/sandbox-images-and-e2e.yaml | 3 + .github/workflows/wsl-e2e.yaml | 1 + ci/source-shape-test-budget.json | 5 - test/ci-install-dependencies.test.ts | 40 + test/dcode-base-image-workflow.test.ts | 673 +--------------- ...managed-image-publication-workflow.test.ts | 54 +- ...e-conflict-fixer-workflow-boundary.test.ts | 5 +- test/sandbox-base-image-layout.test.ts | 25 - test/wechat-runtime-audit-workflow.test.ts | 11 - tools/e2e/base-image-publication.mts | 19 +- tools/e2e/cli-artifact-workflow-boundary.mts | 74 +- tools/e2e/prepare-e2e-workflow-boundary.mts | 22 +- .../e2e/sandbox-images-workflow-boundary.mts | 3 +- ...upload-e2e-artifacts-workflow-boundary.mts | 8 +- tools/e2e/workflow-boundary.mts | 23 +- tools/e2e/workflow-contract-registry.mts | 118 +++ 39 files changed, 678 insertions(+), 1585 deletions(-) create mode 100644 .github/actions/build-base-image-platform/action.yaml create mode 100755 .github/actions/ci-install-dependencies.sh create mode 100644 .github/actions/publish-base-image-manifest/action.yaml create mode 100644 test/ci-install-dependencies.test.ts delete mode 100644 test/sandbox-base-image-layout.test.ts create mode 100644 tools/e2e/workflow-contract-registry.mts diff --git a/.github/actions/build-base-image-platform/action.yaml b/.github/actions/build-base-image-platform/action.yaml new file mode 100644 index 00000000000..4976c0c1b15 --- /dev/null +++ b/.github/actions/build-base-image-platform/action.yaml @@ -0,0 +1,131 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +name: build-base-image-platform +description: Build and publish one immutable base-image platform digest. + +inputs: + agent: + description: Agent identifier used for build arguments and artifact names. + required: true + arch: + description: Artifact architecture identifier. + required: true + platform: + description: Docker platform to build. + required: true + dockerfile: + description: Base-image Dockerfile path. + required: true + image: + description: Image repository relative to the registry. + required: true + registry: + description: Container registry host. + required: true + registry-username: + description: Registry login user. + required: true + registry-password: + description: Registry login credential. + required: true + openclaw-version: + description: Optional OpenClaw version build argument. + required: false + default: "" + metadata-tags: + description: Optional docker/metadata-action tag rules. + required: false + default: "" + +runs: + using: composite + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 + + - name: Log in to GHCR + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + with: + registry: ${{ inputs.registry }} + username: ${{ inputs.registry-username }} + password: ${{ inputs.registry-password }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 + env: + DOCKER_METADATA_SHORT_SHA_LENGTH: 8 + with: + images: ${{ inputs.registry }}/${{ inputs.image }} + tags: ${{ inputs.metadata-tags }} + + - name: Validate production Docker build args + id: production-build-args + shell: bash + env: + AGENT: ${{ inputs.agent }} + OPENCLAW_VERSION_INPUT: ${{ inputs.openclaw-version }} + run: | + set -euo pipefail + build_args=() + openclaw_build_arg="" + if [ "$AGENT" = "openclaw" ] && [ -n "${OPENCLAW_VERSION_INPUT}" ]; then + openclaw_build_arg="OPENCLAW_VERSION=${OPENCLAW_VERSION_INPUT}" + build_args+=(--build-arg "$openclaw_build_arg") + fi + if [ "${#build_args[@]}" -gt 0 ]; then + scripts/check-production-build-args.sh "${build_args[@]}" + else + scripts/check-production-build-args.sh + fi + if [ "$AGENT" = "openclaw" ] && [ -n "${OPENCLAW_VERSION_INPUT}" ]; then + if [[ "$OPENCLAW_VERSION_INPUT" == *$'\r'* || "$OPENCLAW_VERSION_INPUT" == *$'\n'* ]]; then + echo "ERROR: OpenClaw version must not contain CR or LF characters." >&2 + exit 1 + fi + if [[ ! "$OPENCLAW_VERSION_INPUT" =~ ^[0-9]+([.][0-9]+)*$ ]]; then + echo "ERROR: OpenClaw version must be a whole decimal dotted version (for example, 2026.6.10)." >&2 + exit 1 + fi + fi + printf 'openclaw_build_arg=%s\n' "$openclaw_build_arg" >> "$GITHUB_OUTPUT" + + - name: Build and push platform digest + id: build + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 + with: + context: . + file: ${{ inputs.dockerfile }} + platforms: ${{ inputs.platform }} + labels: ${{ steps.meta.outputs.labels }} + outputs: type=image,name=${{ inputs.registry }}/${{ inputs.image }},push-by-digest=true,name-canonical=true,push=true + cache-from: type=registry,ref=${{ inputs.registry }}/${{ inputs.image }}:buildcache-${{ inputs.arch }} + cache-to: type=registry,ref=${{ inputs.registry }}/${{ inputs.image }}:buildcache-${{ inputs.arch }},mode=max + build-args: ${{ steps.production-build-args.outputs.openclaw_build_arg }} + + - name: Export platform digest + shell: bash + env: + ARCH: ${{ inputs.arch }} + DIGEST: ${{ steps.build.outputs.digest }} + run: | + set -euo pipefail + if [[ ! "$ARCH" =~ ^(amd64|arm64)$ ]]; then + echo "ERROR: unsupported platform architecture: $ARCH" >&2 + exit 1 + fi + if [[ ! "$DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]]; then + echo "ERROR: build did not return a valid sha256 digest: $DIGEST" >&2 + exit 1 + fi + mkdir -p "$RUNNER_TEMP/digests" + touch "$RUNNER_TEMP/digests/${ARCH}-${DIGEST#sha256:}" + + - name: Upload platform digest + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ inputs.agent }}-base-digest-${{ github.run_id }}-${{ github.run_attempt }}-${{ inputs.arch }} + path: ${{ runner.temp }}/digests/* + if-no-files-found: error + retention-days: 1 diff --git a/.github/actions/ci-build-typecheck/action.yaml b/.github/actions/ci-build-typecheck/action.yaml index 6e1b803a661..c9d73e0a540 100644 --- a/.github/actions/ci-build-typecheck/action.yaml +++ b/.github/actions/ci-build-typecheck/action.yaml @@ -12,12 +12,13 @@ runs: with: node-version: "22" cache: npm + cache-dependency-path: | + package-lock.json + nemoclaw/package-lock.json - name: Install dependencies shell: bash - run: | - npm install --ignore-scripts - cd nemoclaw && npm install --ignore-scripts + run: bash "$GITHUB_ACTION_PATH/../ci-install-dependencies.sh" - name: Build TypeScript plugin shell: bash diff --git a/.github/actions/ci-cli-coverage-shard/action.yaml b/.github/actions/ci-cli-coverage-shard/action.yaml index bbe67d018d7..63aedad52ac 100644 --- a/.github/actions/ci-cli-coverage-shard/action.yaml +++ b/.github/actions/ci-cli-coverage-shard/action.yaml @@ -56,6 +56,9 @@ runs: with: node-version: "22" cache: npm + cache-dependency-path: | + package-lock.json + nemoclaw/package-lock.json - name: Install pinned Pi search tools shell: bash @@ -98,9 +101,7 @@ runs: - name: Install dependencies shell: bash - run: | - npm install --ignore-scripts - cd nemoclaw && npm install --ignore-scripts + run: bash "$GITHUB_ACTION_PATH/../ci-install-dependencies.sh" - name: Validate changed live E2E mock parity if: ${{ inputs.shard == '1' }} diff --git a/.github/actions/ci-install-dependencies.sh b/.github/actions/ci-install-dependencies.sh new file mode 100755 index 00000000000..2dc2d1bff3f --- /dev/null +++ b/.github/actions/ci-install-dependencies.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +npm ci --ignore-scripts +npm --prefix nemoclaw ci --ignore-scripts diff --git a/.github/actions/ci-installer-integration/action.yaml b/.github/actions/ci-installer-integration/action.yaml index f166edd92e6..b8d4487932d 100644 --- a/.github/actions/ci-installer-integration/action.yaml +++ b/.github/actions/ci-installer-integration/action.yaml @@ -12,12 +12,13 @@ runs: with: node-version: "22" cache: npm + cache-dependency-path: | + package-lock.json + nemoclaw/package-lock.json - name: Install dependencies shell: bash - run: | - npm install --ignore-scripts - cd nemoclaw && npm install --ignore-scripts + run: bash "$GITHUB_ACTION_PATH/../ci-install-dependencies.sh" - name: Build installer integration artifacts shell: bash diff --git a/.github/actions/ci-plugin-coverage/action.yaml b/.github/actions/ci-plugin-coverage/action.yaml index 8b238b50ee7..9af48f0b627 100644 --- a/.github/actions/ci-plugin-coverage/action.yaml +++ b/.github/actions/ci-plugin-coverage/action.yaml @@ -12,12 +12,13 @@ runs: with: node-version: "22" cache: npm + cache-dependency-path: | + package-lock.json + nemoclaw/package-lock.json - name: Install dependencies shell: bash - run: | - npm install --ignore-scripts - cd nemoclaw && npm install --ignore-scripts + run: bash "$GITHUB_ACTION_PATH/../ci-install-dependencies.sh" - name: Run plugin coverage shell: bash diff --git a/.github/actions/ci-reviewed-npm-audit/action.yaml b/.github/actions/ci-reviewed-npm-audit/action.yaml index e674c4bedf8..7b8693c3f5f 100644 --- a/.github/actions/ci-reviewed-npm-audit/action.yaml +++ b/.github/actions/ci-reviewed-npm-audit/action.yaml @@ -44,3 +44,4 @@ runs: name: reviewed-npm-audit path: ${{ inputs.target-root }}/${{ inputs.report-dir }}/*.json if-no-files-found: error + retention-days: 14 diff --git a/.github/actions/ci-wechat-runtime-audit/action.yaml b/.github/actions/ci-wechat-runtime-audit/action.yaml index 47befd7eb80..de8324c7474 100644 --- a/.github/actions/ci-wechat-runtime-audit/action.yaml +++ b/.github/actions/ci-wechat-runtime-audit/action.yaml @@ -20,14 +20,13 @@ runs: with: node-version: "22.19.0" - - name: Pin production npm + - name: Download and verify production npm shell: bash - run: >- - cd "$RUNNER_TEMP" && - npm install --global npm@10.9.4 - --userconfig /dev/null - --registry https://registry.npmjs.org/ - --ignore-scripts --no-audit --no-fund + env: + NEMOCLAW_REVIEWED_NPM_VERSION: "10.9.4" + NEMOCLAW_REVIEWED_NPM_INTEGRITY: >- + sha512-OnUG836FwboQIbqtefDNlyR0gTHzIfwRfE3DuiNewBvnMnWEpB0VEXwBlFVgqpNzIgYo/MHh3d2Hel/pszapAA== + run: '"$GITHUB_ACTION_PATH/../ci-reviewed-npm-audit/verify-and-install-npm.sh"' - name: Audit locked WeChat runtime graph shell: bash diff --git a/.github/actions/publish-base-image-manifest/action.yaml b/.github/actions/publish-base-image-manifest/action.yaml new file mode 100644 index 00000000000..a8eaa8eb62c --- /dev/null +++ b/.github/actions/publish-base-image-manifest/action.yaml @@ -0,0 +1,185 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +name: publish-base-image-manifest +description: Validate platform digests and publish one multi-platform base-image manifest. + +inputs: + agent: + description: Agent identifier stored in the managed-base contract. + required: true + display-name: + description: Agent name used in validation errors. + required: true + image: + description: Image repository relative to the registry. + required: true + registry: + description: Container registry host. + required: true + registry-username: + description: Registry login user. + required: true + registry-password: + description: Registry login credential. + required: true + +runs: + using: composite + steps: + - name: Download platform digests + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: ${{ inputs.agent }}-base-digest-${{ github.run_id }}-${{ github.run_attempt }}-* + path: ${{ runner.temp }}/digests + merge-multiple: true + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 + + - name: Log in to GHCR + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + with: + registry: ${{ inputs.registry }} + username: ${{ inputs.registry-username }} + password: ${{ inputs.registry-password }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 + env: + DOCKER_METADATA_SHORT_SHA_LENGTH: 8 + with: + images: ${{ inputs.registry }}/${{ inputs.image }} + tags: | + type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }} + type=ref,event=tag + type=sha,prefix=,format=short + + - name: Create and verify multi-platform manifest + id: manifest + shell: bash + env: + AGENT: ${{ inputs.agent }} + DISPLAY_NAME: ${{ inputs.display-name }} + IMAGE: ${{ inputs.registry }}/${{ inputs.image }} + TAGS: ${{ steps.meta.outputs.tags }} + run: | + set -euo pipefail + shopt -s nullglob + digest_files=("$RUNNER_TEMP"/digests/*) + if [ "${#digest_files[@]}" -ne 2 ]; then + echo "ERROR: expected exactly two platform digests, found ${#digest_files[@]}." >&2 + exit 1 + fi + + declare -A seen_arches=() + declare -A source_digests=() + sources=() + for digest_file in "${digest_files[@]}"; do + digest_artifact="$(basename "$digest_file")" + if [[ ! "$digest_artifact" =~ ^(amd64|arm64)-([0-9a-f]{64})$ ]]; then + echo "ERROR: invalid platform digest artifact: $digest_artifact" >&2 + exit 1 + fi + expected_arch="${BASH_REMATCH[1]}" + digest="${BASH_REMATCH[2]}" + if [ -n "${seen_arches[$expected_arch]:-}" ]; then + echo "ERROR: duplicate platform digest for linux/$expected_arch." >&2 + exit 1 + fi + source="$IMAGE@sha256:$digest" + source_platform="$( + scripts/checks/retry-docker-imagetools-inspect.sh "$source" \ + --format '{{.Image.OS}}/{{.Image.Architecture}}' + )" + if [ "$source_platform" != "linux/$expected_arch" ]; then + echo "ERROR: digest for linux/$expected_arch resolves to $source_platform." >&2 + exit 1 + fi + seen_arches["$expected_arch"]=1 + source_digests["linux/$expected_arch"]="sha256:$digest" + sources+=("$source") + done + if [ "${seen_arches[amd64]:-0}" -ne 1 ] || [ "${seen_arches[arm64]:-0}" -ne 1 ]; then + echo "ERROR: expected one validated digest for linux/amd64 and linux/arm64." >&2 + exit 1 + fi + + mapfile -t tags <<< "$TAGS" + tag_args=() + for tag in "${tags[@]}"; do + if [ -n "$tag" ]; then + tag_args+=(--tag "$tag") + fi + done + if [ "${#tag_args[@]}" -eq 0 ]; then + echo "ERROR: metadata did not produce any publication tags." >&2 + exit 1 + fi + + candidate_tag="$IMAGE:base-candidate-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" + candidate_metadata="$RUNNER_TEMP/$AGENT-base-candidate-metadata.json" + docker buildx imagetools create \ + --tag "$candidate_tag" \ + --metadata-file "$candidate_metadata" \ + "${sources[@]}" + digest="$(jq -er '.["containerimage.descriptor"].digest' "$candidate_metadata")" + if [[ ! "$digest" =~ ^sha256:[0-9a-f]{64}$ ]]; then + echo "ERROR: expected one staged $DISPLAY_NAME base digest." >&2 + exit 1 + fi + reference="$IMAGE@$digest" + actual_platforms="$( + scripts/checks/retry-docker-imagetools-inspect.sh "$reference" --raw \ + | jq -r '.manifests[] | select(.platform.os == "linux") | .platform.architecture' \ + | sort -u \ + | paste -sd, - + )" + if [ "$actual_platforms" != "amd64,arm64" ]; then + echo "ERROR: staged manifest has unexpected platforms: $actual_platforms" >&2 + exit 1 + fi + platform_digests_json="$( + scripts/checks/validate-managed-base-index.sh \ + "$reference" \ + "${source_digests[linux/amd64]}" \ + "${source_digests[linux/arm64]}" + )" + declare -A platform_digests=() + for platform in linux/amd64 linux/arm64; do + platform_digests["$platform"]="$( + jq -er --arg platform "$platform" '.[$platform]' <<< "$platform_digests_json" + )" + done + + scripts/export-managed-base-image-contract.sh \ + "$AGENT" \ + "$IMAGE" \ + "$digest" \ + "${platform_digests[linux/amd64]}" \ + "${platform_digests[linux/arm64]}" \ + "$GITHUB_SHA" \ + "$GITHUB_RUN_ID" \ + "$GITHUB_RUN_ATTEMPT" \ + "$RUNNER_TEMP/managed-base-contract/contract.json" + + publication_metadata="$RUNNER_TEMP/$AGENT-base-publication-metadata.json" + docker buildx imagetools create \ + "${tag_args[@]}" \ + --metadata-file "$publication_metadata" \ + "$reference" + published_digest="$(jq -er '.["containerimage.descriptor"].digest' "$publication_metadata")" + if [ "$published_digest" != "$digest" ]; then + echo "ERROR: published $DISPLAY_NAME base digest differs from the validated candidate." >&2 + exit 1 + fi + printf 'digest=%s\n' "$digest" >> "$GITHUB_OUTPUT" + + - name: Upload managed base image contract + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: managed-base-${{ github.run_id }}-${{ github.run_attempt }}-${{ inputs.agent }} + path: ${{ runner.temp }}/managed-base-contract/contract.json + if-no-files-found: error + retention-days: 1 diff --git a/.github/workflows/base-image.yaml b/.github/workflows/base-image.yaml index bf0e2284c54..5323e063d81 100644 --- a/.github/workflows/base-image.yaml +++ b/.github/workflows/base-image.yaml @@ -23,6 +23,8 @@ on: - ".github/workflows/base-image.yaml" - ".github/workflows/managed-images.yaml" - ".github/actions/ci-reviewed-npm-audit/**" + - ".github/actions/build-base-image-platform/**" + - ".github/actions/publish-base-image-manifest/**" - ".dockerignore" # Complete managed-image inputs. Keep these reviewed families synchronized # with tools/e2e/base-image-publication.mts. @@ -136,96 +138,26 @@ jobs: steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - - - name: Log in to GHCR - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} + persist-credentials: false - - name: Extract metadata - id: meta - uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 - env: - DOCKER_METADATA_SHORT_SHA_LENGTH: 8 + - name: Build and publish platform digest + uses: ./.github/actions/build-base-image-platform with: - images: ${{ env.REGISTRY }}/${{ matrix.image }} - tags: | + agent: ${{ matrix.agent }} + arch: ${{ matrix.arch }} + platform: ${{ matrix.platform }} + dockerfile: ${{ matrix.dockerfile }} + image: ${{ matrix.image }} + registry: ${{ env.REGISTRY }} + registry-username: ${{ github.actor }} + registry-password: ${{ secrets.GITHUB_TOKEN }} + openclaw-version: ${{ inputs.openclaw_version }} + metadata-tags: | type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }} type=ref,event=tag type=sha,prefix=,format=short - - name: Validate production Docker build args - id: production-build-args - env: - AGENT: ${{ matrix.agent }} - OPENCLAW_VERSION_INPUT: ${{ inputs.openclaw_version }} - run: | - set -euo pipefail - build_args=() - openclaw_build_arg="" - if [ "$AGENT" = "openclaw" ] && [ -n "${OPENCLAW_VERSION_INPUT}" ]; then - openclaw_build_arg="OPENCLAW_VERSION=${OPENCLAW_VERSION_INPUT}" - build_args+=(--build-arg "$openclaw_build_arg") - fi - if [ "${#build_args[@]}" -gt 0 ]; then - scripts/check-production-build-args.sh "${build_args[@]}" - else - scripts/check-production-build-args.sh - fi - if [ "$AGENT" = "openclaw" ] && [ -n "${OPENCLAW_VERSION_INPUT}" ]; then - if [[ "$OPENCLAW_VERSION_INPUT" == *$'\r'* || "$OPENCLAW_VERSION_INPUT" == *$'\n'* ]]; then - echo "ERROR: OpenClaw version must not contain CR or LF characters." >&2 - exit 1 - fi - if [[ ! "$OPENCLAW_VERSION_INPUT" =~ ^[0-9]+([.][0-9]+)*$ ]]; then - echo "ERROR: OpenClaw version must be a whole decimal dotted version (for example, 2026.6.10)." >&2 - exit 1 - fi - fi - printf 'openclaw_build_arg=%s\n' "$openclaw_build_arg" >> "$GITHUB_OUTPUT" - - - name: Build and push platform digest - id: build - uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 - with: - context: . - file: ${{ matrix.dockerfile }} - platforms: ${{ matrix.platform }} - labels: ${{ steps.meta.outputs.labels }} - outputs: type=image,name=${{ env.REGISTRY }}/${{ matrix.image }},push-by-digest=true,name-canonical=true,push=true - cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ matrix.image }}:buildcache-${{ matrix.arch }} - cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ matrix.image }}:buildcache-${{ matrix.arch }},mode=max - build-args: ${{ steps.production-build-args.outputs.openclaw_build_arg }} - - - name: Export platform digest - env: - ARCH: ${{ matrix.arch }} - DIGEST: ${{ steps.build.outputs.digest }} - run: | - set -euo pipefail - if [[ ! "$ARCH" =~ ^(amd64|arm64)$ ]]; then - echo "ERROR: unsupported platform architecture: $ARCH" >&2 - exit 1 - fi - if [[ ! "$DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]]; then - echo "ERROR: build did not return a valid sha256 digest: $DIGEST" >&2 - exit 1 - fi - mkdir -p "$RUNNER_TEMP/digests" - touch "$RUNNER_TEMP/digests/${ARCH}-${DIGEST#sha256:}" - - - name: Upload platform digest - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: openclaw-base-digest-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.arch }} - path: ${{ runner.temp }}/digests/* - if-no-files-found: error - retention-days: 1 # The complete Perl suite approaches the image-job timeout under QEMU arm64 emulation. # Build each sibling image on native architecture runners and publish only immutable platform digests. @@ -258,92 +190,22 @@ jobs: steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - - - name: Log in to GHCR - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata - id: meta - uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 - env: - DOCKER_METADATA_SHORT_SHA_LENGTH: 8 with: - images: ${{ env.REGISTRY }}/${{ matrix.image }} - - - name: Validate production Docker build args - id: production-build-args - env: - AGENT: ${{ matrix.agent }} - OPENCLAW_VERSION_INPUT: ${{ inputs.openclaw_version }} - run: | - set -euo pipefail - build_args=() - openclaw_build_arg="" - if [ "$AGENT" = "openclaw" ] && [ -n "${OPENCLAW_VERSION_INPUT}" ]; then - openclaw_build_arg="OPENCLAW_VERSION=${OPENCLAW_VERSION_INPUT}" - build_args+=(--build-arg "$openclaw_build_arg") - fi - if [ "${#build_args[@]}" -gt 0 ]; then - scripts/check-production-build-args.sh "${build_args[@]}" - else - scripts/check-production-build-args.sh - fi - if [ "$AGENT" = "openclaw" ] && [ -n "${OPENCLAW_VERSION_INPUT}" ]; then - if [[ "$OPENCLAW_VERSION_INPUT" == *$'\r'* || "$OPENCLAW_VERSION_INPUT" == *$'\n'* ]]; then - echo "ERROR: OpenClaw version must not contain CR or LF characters." >&2 - exit 1 - fi - if [[ ! "$OPENCLAW_VERSION_INPUT" =~ ^[0-9]+([.][0-9]+)*$ ]]; then - echo "ERROR: OpenClaw version must be a whole decimal dotted version (for example, 2026.6.10)." >&2 - exit 1 - fi - fi - printf 'openclaw_build_arg=%s\n' "$openclaw_build_arg" >> "$GITHUB_OUTPUT" + persist-credentials: false - - name: Build and push platform digest - id: build - uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 + - name: Build and publish platform digest + uses: ./.github/actions/build-base-image-platform with: - context: . - file: ${{ matrix.dockerfile }} - platforms: ${{ matrix.platform }} - labels: ${{ steps.meta.outputs.labels }} - outputs: type=image,name=${{ env.REGISTRY }}/${{ matrix.image }},push-by-digest=true,name-canonical=true,push=true - cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ matrix.image }}:buildcache-${{ matrix.arch }} - cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ matrix.image }}:buildcache-${{ matrix.arch }},mode=max - build-args: ${{ steps.production-build-args.outputs.openclaw_build_arg }} - - - name: Export platform digest - env: - ARCH: ${{ matrix.arch }} - DIGEST: ${{ steps.build.outputs.digest }} - run: | - set -euo pipefail - if [[ ! "$ARCH" =~ ^(amd64|arm64)$ ]]; then - echo "ERROR: unsupported platform architecture: $ARCH" >&2 - exit 1 - fi - if [[ ! "$DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]]; then - echo "ERROR: build did not return a valid sha256 digest: $DIGEST" >&2 - exit 1 - fi - mkdir -p "$RUNNER_TEMP/digests" - touch "$RUNNER_TEMP/digests/${ARCH}-${DIGEST#sha256:}" + agent: ${{ matrix.agent }} + arch: ${{ matrix.arch }} + platform: ${{ matrix.platform }} + dockerfile: ${{ matrix.dockerfile }} + image: ${{ matrix.image }} + registry: ${{ env.REGISTRY }} + registry-username: ${{ github.actor }} + registry-password: ${{ secrets.GITHUB_TOKEN }} + openclaw-version: ${{ inputs.openclaw_version }} - - name: Upload platform digest - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: ${{ matrix.agent }}-base-digest-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.arch }} - path: ${{ runner.temp }}/digests/* - if-no-files-found: error - retention-days: 1 build-dcode-platforms: name: Build ${{ matrix.display_name }} base image (${{ matrix.arch }}) @@ -373,92 +235,22 @@ jobs: steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - - - name: Log in to GHCR - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata - id: meta - uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 - env: - DOCKER_METADATA_SHORT_SHA_LENGTH: 8 with: - images: ${{ env.REGISTRY }}/${{ matrix.image }} - - - name: Validate production Docker build args - id: production-build-args - env: - AGENT: ${{ matrix.agent }} - OPENCLAW_VERSION_INPUT: ${{ inputs.openclaw_version }} - run: | - set -euo pipefail - build_args=() - openclaw_build_arg="" - if [ "$AGENT" = "openclaw" ] && [ -n "${OPENCLAW_VERSION_INPUT}" ]; then - openclaw_build_arg="OPENCLAW_VERSION=${OPENCLAW_VERSION_INPUT}" - build_args+=(--build-arg "$openclaw_build_arg") - fi - if [ "${#build_args[@]}" -gt 0 ]; then - scripts/check-production-build-args.sh "${build_args[@]}" - else - scripts/check-production-build-args.sh - fi - if [ "$AGENT" = "openclaw" ] && [ -n "${OPENCLAW_VERSION_INPUT}" ]; then - if [[ "$OPENCLAW_VERSION_INPUT" == *$'\r'* || "$OPENCLAW_VERSION_INPUT" == *$'\n'* ]]; then - echo "ERROR: OpenClaw version must not contain CR or LF characters." >&2 - exit 1 - fi - if [[ ! "$OPENCLAW_VERSION_INPUT" =~ ^[0-9]+([.][0-9]+)*$ ]]; then - echo "ERROR: OpenClaw version must be a whole decimal dotted version (for example, 2026.6.10)." >&2 - exit 1 - fi - fi - printf 'openclaw_build_arg=%s\n' "$openclaw_build_arg" >> "$GITHUB_OUTPUT" + persist-credentials: false - - name: Build and push platform digest - id: build - uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 + - name: Build and publish platform digest + uses: ./.github/actions/build-base-image-platform with: - context: . - file: ${{ matrix.dockerfile }} - platforms: ${{ matrix.platform }} - labels: ${{ steps.meta.outputs.labels }} - outputs: type=image,name=${{ env.REGISTRY }}/${{ matrix.image }},push-by-digest=true,name-canonical=true,push=true - cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ matrix.image }}:buildcache-${{ matrix.arch }} - cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ matrix.image }}:buildcache-${{ matrix.arch }},mode=max - build-args: ${{ steps.production-build-args.outputs.openclaw_build_arg }} + agent: ${{ matrix.agent }} + arch: ${{ matrix.arch }} + platform: ${{ matrix.platform }} + dockerfile: ${{ matrix.dockerfile }} + image: ${{ matrix.image }} + registry: ${{ env.REGISTRY }} + registry-username: ${{ github.actor }} + registry-password: ${{ secrets.GITHUB_TOKEN }} + openclaw-version: ${{ inputs.openclaw_version }} - - name: Export platform digest - env: - ARCH: ${{ matrix.arch }} - DIGEST: ${{ steps.build.outputs.digest }} - run: | - set -euo pipefail - if [[ ! "$ARCH" =~ ^(amd64|arm64)$ ]]; then - echo "ERROR: unsupported platform architecture: $ARCH" >&2 - exit 1 - fi - if [[ ! "$DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]]; then - echo "ERROR: build did not return a valid sha256 digest: $DIGEST" >&2 - exit 1 - fi - mkdir -p "$RUNNER_TEMP/digests" - touch "$RUNNER_TEMP/digests/${ARCH}-${DIGEST#sha256:}" - - - name: Upload platform digest - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: ${{ matrix.agent }}-base-digest-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.arch }} - path: ${{ runner.temp }}/digests/* - if-no-files-found: error - retention-days: 1 build-and-push-hermes: name: Build and push Hermes base image @@ -474,160 +266,16 @@ jobs: with: persist-credentials: false - - name: Download platform digests - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - pattern: hermes-base-digest-${{ github.run_id }}-${{ github.run_attempt }}-* - path: ${{ runner.temp }}/digests - merge-multiple: true - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - - - name: Log in to GHCR - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + - name: Publish validated multi-platform manifest + uses: ./.github/actions/publish-base-image-manifest with: + agent: hermes + display-name: Hermes + image: nvidia/nemoclaw/hermes-sandbox-base registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata - id: meta - uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 - env: - DOCKER_METADATA_SHORT_SHA_LENGTH: 8 - with: - images: ${{ env.REGISTRY }}/nvidia/nemoclaw/hermes-sandbox-base - tags: | - type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }} - type=ref,event=tag - type=sha,prefix=,format=short + registry-username: ${{ github.actor }} + registry-password: ${{ secrets.GITHUB_TOKEN }} - - name: Create and verify multi-platform manifest - id: manifest - env: - AGENT: hermes - IMAGE: ${{ env.REGISTRY }}/nvidia/nemoclaw/hermes-sandbox-base - TAGS: ${{ steps.meta.outputs.tags }} - run: | - set -euo pipefail - shopt -s nullglob - digest_files=("$RUNNER_TEMP"/digests/*) - if [ "${#digest_files[@]}" -ne 2 ]; then - echo "ERROR: expected exactly two platform digests, found ${#digest_files[@]}." >&2 - exit 1 - fi - - declare -A seen_arches=() - declare -A source_digests=() - sources=() - for digest_file in "${digest_files[@]}"; do - digest_artifact="$(basename "$digest_file")" - if [[ ! "$digest_artifact" =~ ^(amd64|arm64)-([0-9a-f]{64})$ ]]; then - echo "ERROR: invalid platform digest artifact: $digest_artifact" >&2 - exit 1 - fi - expected_arch="${BASH_REMATCH[1]}" - digest="${BASH_REMATCH[2]}" - if [ -n "${seen_arches[$expected_arch]:-}" ]; then - echo "ERROR: duplicate platform digest for linux/$expected_arch." >&2 - exit 1 - fi - source="$IMAGE@sha256:$digest" - source_platform="$( - scripts/checks/retry-docker-imagetools-inspect.sh "$source" \ - --format '{{.Image.OS}}/{{.Image.Architecture}}' - )" - if [ "$source_platform" != "linux/$expected_arch" ]; then - echo "ERROR: digest for linux/$expected_arch resolves to $source_platform." >&2 - exit 1 - fi - seen_arches["$expected_arch"]=1 - source_digests["linux/$expected_arch"]="sha256:$digest" - sources+=("$source") - done - if [ "${seen_arches[amd64]:-0}" -ne 1 ] || [ "${seen_arches[arm64]:-0}" -ne 1 ]; then - echo "ERROR: expected one validated digest for linux/amd64 and linux/arm64." >&2 - exit 1 - fi - - mapfile -t tags <<< "$TAGS" - tag_args=() - for tag in "${tags[@]}"; do - if [ -n "$tag" ]; then - tag_args+=(--tag "$tag") - fi - done - if [ "${#tag_args[@]}" -eq 0 ]; then - echo "ERROR: metadata did not produce any publication tags." >&2 - exit 1 - fi - - candidate_tag="$IMAGE:base-candidate-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" - candidate_metadata="$RUNNER_TEMP/$AGENT-base-candidate-metadata.json" - docker buildx imagetools create \ - --tag "$candidate_tag" \ - --metadata-file "$candidate_metadata" \ - "${sources[@]}" - digest="$(jq -er '.["containerimage.descriptor"].digest' "$candidate_metadata")" - if [[ ! "$digest" =~ ^sha256:[0-9a-f]{64}$ ]]; then - echo "ERROR: expected one staged Hermes base digest." >&2 - exit 1 - fi - reference="$IMAGE@$digest" - actual_platforms="$( - scripts/checks/retry-docker-imagetools-inspect.sh "$reference" --raw \ - | jq -r '.manifests[] | select(.platform.os == "linux") | .platform.architecture' \ - | sort -u \ - | paste -sd, - - )" - if [ "$actual_platforms" != "amd64,arm64" ]; then - echo "ERROR: staged manifest has unexpected platforms: $actual_platforms" >&2 - exit 1 - fi - platform_digests_json="$( - scripts/checks/validate-managed-base-index.sh \ - "$reference" \ - "${source_digests[linux/amd64]}" \ - "${source_digests[linux/arm64]}" - )" - declare -A platform_digests=() - for platform in linux/amd64 linux/arm64; do - platform_digests["$platform"]="$( - jq -er --arg platform "$platform" '.[$platform]' <<< "$platform_digests_json" - )" - done - - scripts/export-managed-base-image-contract.sh \ - "$AGENT" \ - "$IMAGE" \ - "$digest" \ - "${platform_digests[linux/amd64]}" \ - "${platform_digests[linux/arm64]}" \ - "$GITHUB_SHA" \ - "$GITHUB_RUN_ID" \ - "$GITHUB_RUN_ATTEMPT" \ - "$RUNNER_TEMP/managed-base-contract/contract.json" - - publication_metadata="$RUNNER_TEMP/$AGENT-base-publication-metadata.json" - docker buildx imagetools create \ - "${tag_args[@]}" \ - --metadata-file "$publication_metadata" \ - "$reference" - published_digest="$(jq -er '.["containerimage.descriptor"].digest' "$publication_metadata")" - if [ "$published_digest" != "$digest" ]; then - echo "ERROR: published Hermes base digest differs from the validated candidate." >&2 - exit 1 - fi - printf 'digest=%s\n' "$digest" >> "$GITHUB_OUTPUT" - - - name: Upload managed base image contract - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: managed-base-${{ github.run_id }}-${{ github.run_attempt }}-hermes - path: ${{ runner.temp }}/managed-base-contract/contract.json - if-no-files-found: error - retention-days: 1 build-and-push-dcode: name: Build and push Deep Agents Code base image @@ -643,160 +291,16 @@ jobs: with: persist-credentials: false - - name: Download platform digests - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - pattern: langchain-deepagents-code-base-digest-${{ github.run_id }}-${{ github.run_attempt }}-* - path: ${{ runner.temp }}/digests - merge-multiple: true - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - - - name: Log in to GHCR - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + - name: Publish validated multi-platform manifest + uses: ./.github/actions/publish-base-image-manifest with: + agent: langchain-deepagents-code + display-name: Deep Agents Code + image: nvidia/nemoclaw/langchain-deepagents-code-sandbox-base registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata - id: meta - uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 - env: - DOCKER_METADATA_SHORT_SHA_LENGTH: 8 - with: - images: ${{ env.REGISTRY }}/nvidia/nemoclaw/langchain-deepagents-code-sandbox-base - tags: | - type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }} - type=ref,event=tag - type=sha,prefix=,format=short - - - name: Create and verify multi-platform manifest - id: manifest - env: - AGENT: langchain-deepagents-code - IMAGE: ${{ env.REGISTRY }}/nvidia/nemoclaw/langchain-deepagents-code-sandbox-base - TAGS: ${{ steps.meta.outputs.tags }} - run: | - set -euo pipefail - shopt -s nullglob - digest_files=("$RUNNER_TEMP"/digests/*) - if [ "${#digest_files[@]}" -ne 2 ]; then - echo "ERROR: expected exactly two platform digests, found ${#digest_files[@]}." >&2 - exit 1 - fi - - declare -A seen_arches=() - declare -A source_digests=() - sources=() - for digest_file in "${digest_files[@]}"; do - digest_artifact="$(basename "$digest_file")" - if [[ ! "$digest_artifact" =~ ^(amd64|arm64)-([0-9a-f]{64})$ ]]; then - echo "ERROR: invalid platform digest artifact: $digest_artifact" >&2 - exit 1 - fi - expected_arch="${BASH_REMATCH[1]}" - digest="${BASH_REMATCH[2]}" - if [ -n "${seen_arches[$expected_arch]:-}" ]; then - echo "ERROR: duplicate platform digest for linux/$expected_arch." >&2 - exit 1 - fi - source="$IMAGE@sha256:$digest" - source_platform="$( - scripts/checks/retry-docker-imagetools-inspect.sh "$source" \ - --format '{{.Image.OS}}/{{.Image.Architecture}}' - )" - if [ "$source_platform" != "linux/$expected_arch" ]; then - echo "ERROR: digest for linux/$expected_arch resolves to $source_platform." >&2 - exit 1 - fi - seen_arches["$expected_arch"]=1 - source_digests["linux/$expected_arch"]="sha256:$digest" - sources+=("$source") - done - if [ "${seen_arches[amd64]:-0}" -ne 1 ] || [ "${seen_arches[arm64]:-0}" -ne 1 ]; then - echo "ERROR: expected one validated digest for linux/amd64 and linux/arm64." >&2 - exit 1 - fi - - mapfile -t tags <<< "$TAGS" - tag_args=() - for tag in "${tags[@]}"; do - if [ -n "$tag" ]; then - tag_args+=(--tag "$tag") - fi - done - if [ "${#tag_args[@]}" -eq 0 ]; then - echo "ERROR: metadata did not produce any publication tags." >&2 - exit 1 - fi - - candidate_tag="$IMAGE:base-candidate-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" - candidate_metadata="$RUNNER_TEMP/$AGENT-base-candidate-metadata.json" - docker buildx imagetools create \ - --tag "$candidate_tag" \ - --metadata-file "$candidate_metadata" \ - "${sources[@]}" - digest="$(jq -er '.["containerimage.descriptor"].digest' "$candidate_metadata")" - if [[ ! "$digest" =~ ^sha256:[0-9a-f]{64}$ ]]; then - echo "ERROR: expected one staged Deep Agents Code base digest." >&2 - exit 1 - fi - reference="$IMAGE@$digest" - actual_platforms="$( - scripts/checks/retry-docker-imagetools-inspect.sh "$reference" --raw \ - | jq -r '.manifests[] | select(.platform.os == "linux") | .platform.architecture' \ - | sort -u \ - | paste -sd, - - )" - if [ "$actual_platforms" != "amd64,arm64" ]; then - echo "ERROR: staged manifest has unexpected platforms: $actual_platforms" >&2 - exit 1 - fi - platform_digests_json="$( - scripts/checks/validate-managed-base-index.sh \ - "$reference" \ - "${source_digests[linux/amd64]}" \ - "${source_digests[linux/arm64]}" - )" - declare -A platform_digests=() - for platform in linux/amd64 linux/arm64; do - platform_digests["$platform"]="$( - jq -er --arg platform "$platform" '.[$platform]' <<< "$platform_digests_json" - )" - done + registry-username: ${{ github.actor }} + registry-password: ${{ secrets.GITHUB_TOKEN }} - scripts/export-managed-base-image-contract.sh \ - "$AGENT" \ - "$IMAGE" \ - "$digest" \ - "${platform_digests[linux/amd64]}" \ - "${platform_digests[linux/arm64]}" \ - "$GITHUB_SHA" \ - "$GITHUB_RUN_ID" \ - "$GITHUB_RUN_ATTEMPT" \ - "$RUNNER_TEMP/managed-base-contract/contract.json" - - publication_metadata="$RUNNER_TEMP/$AGENT-base-publication-metadata.json" - docker buildx imagetools create \ - "${tag_args[@]}" \ - --metadata-file "$publication_metadata" \ - "$reference" - published_digest="$(jq -er '.["containerimage.descriptor"].digest' "$publication_metadata")" - if [ "$published_digest" != "$digest" ]; then - echo "ERROR: published Deep Agents Code base digest differs from the validated candidate." >&2 - exit 1 - fi - printf 'digest=%s\n' "$digest" >> "$GITHUB_OUTPUT" - - - name: Upload managed base image contract - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: managed-base-${{ github.run_id }}-${{ github.run_attempt }}-langchain-deepagents-code - path: ${{ runner.temp }}/managed-base-contract/contract.json - if-no-files-found: error - retention-days: 1 # Preserve the established required-check name while making tag publication # contingent on both native platform builds succeeding. @@ -814,160 +318,16 @@ jobs: with: persist-credentials: false - - name: Download platform digests - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - pattern: openclaw-base-digest-${{ github.run_id }}-${{ github.run_attempt }}-* - path: ${{ runner.temp }}/digests - merge-multiple: true - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - - - name: Log in to GHCR - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + - name: Publish validated multi-platform manifest + uses: ./.github/actions/publish-base-image-manifest with: + agent: openclaw + display-name: OpenClaw + image: nvidia/nemoclaw/sandbox-base registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata - id: meta - uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 - env: - DOCKER_METADATA_SHORT_SHA_LENGTH: 8 - with: - images: ${{ env.REGISTRY }}/nvidia/nemoclaw/sandbox-base - tags: | - type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }} - type=ref,event=tag - type=sha,prefix=,format=short - - - name: Create and verify multi-platform manifest - id: manifest - env: - AGENT: openclaw - IMAGE: ${{ env.REGISTRY }}/nvidia/nemoclaw/sandbox-base - TAGS: ${{ steps.meta.outputs.tags }} - run: | - set -euo pipefail - shopt -s nullglob - digest_files=("$RUNNER_TEMP"/digests/*) - if [ "${#digest_files[@]}" -ne 2 ]; then - echo "ERROR: expected exactly two platform digests, found ${#digest_files[@]}." >&2 - exit 1 - fi - - declare -A seen_arches=() - declare -A source_digests=() - sources=() - for digest_file in "${digest_files[@]}"; do - digest_artifact="$(basename "$digest_file")" - if [[ ! "$digest_artifact" =~ ^(amd64|arm64)-([0-9a-f]{64})$ ]]; then - echo "ERROR: invalid platform digest artifact: $digest_artifact" >&2 - exit 1 - fi - expected_arch="${BASH_REMATCH[1]}" - digest="${BASH_REMATCH[2]}" - if [ -n "${seen_arches[$expected_arch]:-}" ]; then - echo "ERROR: duplicate platform digest for linux/$expected_arch." >&2 - exit 1 - fi - source="$IMAGE@sha256:$digest" - source_platform="$( - scripts/checks/retry-docker-imagetools-inspect.sh "$source" \ - --format '{{.Image.OS}}/{{.Image.Architecture}}' - )" - if [ "$source_platform" != "linux/$expected_arch" ]; then - echo "ERROR: digest for linux/$expected_arch resolves to $source_platform." >&2 - exit 1 - fi - seen_arches["$expected_arch"]=1 - source_digests["linux/$expected_arch"]="sha256:$digest" - sources+=("$source") - done - if [ "${seen_arches[amd64]:-0}" -ne 1 ] || [ "${seen_arches[arm64]:-0}" -ne 1 ]; then - echo "ERROR: expected one validated digest for linux/amd64 and linux/arm64." >&2 - exit 1 - fi - - mapfile -t tags <<< "$TAGS" - tag_args=() - for tag in "${tags[@]}"; do - if [ -n "$tag" ]; then - tag_args+=(--tag "$tag") - fi - done - if [ "${#tag_args[@]}" -eq 0 ]; then - echo "ERROR: metadata did not produce any publication tags." >&2 - exit 1 - fi + registry-username: ${{ github.actor }} + registry-password: ${{ secrets.GITHUB_TOKEN }} - candidate_tag="$IMAGE:base-candidate-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" - candidate_metadata="$RUNNER_TEMP/$AGENT-base-candidate-metadata.json" - docker buildx imagetools create \ - --tag "$candidate_tag" \ - --metadata-file "$candidate_metadata" \ - "${sources[@]}" - digest="$(jq -er '.["containerimage.descriptor"].digest' "$candidate_metadata")" - if [[ ! "$digest" =~ ^sha256:[0-9a-f]{64}$ ]]; then - echo "ERROR: expected one staged OpenClaw base digest." >&2 - exit 1 - fi - reference="$IMAGE@$digest" - actual_platforms="$( - scripts/checks/retry-docker-imagetools-inspect.sh "$reference" --raw \ - | jq -r '.manifests[] | select(.platform.os == "linux") | .platform.architecture' \ - | sort -u \ - | paste -sd, - - )" - if [ "$actual_platforms" != "amd64,arm64" ]; then - echo "ERROR: staged manifest has unexpected platforms: $actual_platforms" >&2 - exit 1 - fi - platform_digests_json="$( - scripts/checks/validate-managed-base-index.sh \ - "$reference" \ - "${source_digests[linux/amd64]}" \ - "${source_digests[linux/arm64]}" - )" - declare -A platform_digests=() - for platform in linux/amd64 linux/arm64; do - platform_digests["$platform"]="$( - jq -er --arg platform "$platform" '.[$platform]' <<< "$platform_digests_json" - )" - done - - scripts/export-managed-base-image-contract.sh \ - "$AGENT" \ - "$IMAGE" \ - "$digest" \ - "${platform_digests[linux/amd64]}" \ - "${platform_digests[linux/arm64]}" \ - "$GITHUB_SHA" \ - "$GITHUB_RUN_ID" \ - "$GITHUB_RUN_ATTEMPT" \ - "$RUNNER_TEMP/managed-base-contract/contract.json" - - publication_metadata="$RUNNER_TEMP/$AGENT-base-publication-metadata.json" - docker buildx imagetools create \ - "${tag_args[@]}" \ - --metadata-file "$publication_metadata" \ - "$reference" - published_digest="$(jq -er '.["containerimage.descriptor"].digest' "$publication_metadata")" - if [ "$published_digest" != "$digest" ]; then - echo "ERROR: published OpenClaw base digest differs from the validated candidate." >&2 - exit 1 - fi - printf 'digest=%s\n' "$digest" >> "$GITHUB_OUTPUT" - - - name: Upload managed base image contract - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: managed-base-${{ github.run_id }}-${{ github.run_attempt }}-openclaw - path: ${{ runner.temp }}/managed-base-contract/contract.json - if-no-files-found: error - retention-days: 1 # Consume the three exact base-image contracts in this run. The reusable # publisher promotes no mutable cohort alias until every agent and platform diff --git a/.github/workflows/code-scanning.yaml b/.github/workflows/code-scanning.yaml index 9e6770bbe93..d00a7707b3f 100644 --- a/.github/workflows/code-scanning.yaml +++ b/.github/workflows/code-scanning.yaml @@ -33,6 +33,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Initialize CodeQL uses: github/codeql-action/init@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4 @@ -51,15 +53,15 @@ jobs: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - path: source persist-credentials: false + path: source - name: Check out the trusted ShellCheck converter uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + persist-credentials: false ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.workflow_sha }} path: trusted-shellcheck-converter - persist-credentials: false sparse-checkout: | scripts/shellcheck-json1-to-sarif.mts sparse-checkout-cone-mode: false diff --git a/.github/workflows/commit-lint.yaml b/.github/workflows/commit-lint.yaml index b7a5d2c483b..d58a79aca9a 100644 --- a/.github/workflows/commit-lint.yaml +++ b/.github/workflows/commit-lint.yaml @@ -21,6 +21,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Setup Node.js uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 diff --git a/.github/workflows/docker-pin-check.yaml b/.github/workflows/docker-pin-check.yaml index e38cf59cbe9..d9a56e984b9 100644 --- a/.github/workflows/docker-pin-check.yaml +++ b/.github/workflows/docker-pin-check.yaml @@ -23,6 +23,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Check Dockerfile base-image pin run: | diff --git a/.github/workflows/docs-cli-parity-pr.yaml b/.github/workflows/docs-cli-parity-pr.yaml index 7954e6b8953..70b5d861260 100644 --- a/.github/workflows/docs-cli-parity-pr.yaml +++ b/.github/workflows/docs-cli-parity-pr.yaml @@ -37,6 +37,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Setup Node.js uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 diff --git a/.github/workflows/docs-links-pr.yaml b/.github/workflows/docs-links-pr.yaml index 3233847fde3..a9819d1206e 100644 --- a/.github/workflows/docs-links-pr.yaml +++ b/.github/workflows/docs-links-pr.yaml @@ -23,6 +23,7 @@ jobs: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + persist-credentials: false fetch-depth: 0 - name: Determine changed documentation files diff --git a/.github/workflows/macos-e2e.yaml b/.github/workflows/macos-e2e.yaml index eaa418396f5..214d99376c1 100644 --- a/.github/workflows/macos-e2e.yaml +++ b/.github/workflows/macos-e2e.yaml @@ -29,6 +29,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Setup Node.js uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 @@ -109,3 +111,4 @@ jobs: /tmp/nemoclaw-e2e-*.log ${{ github.workspace }}/e2e-artifacts/live if-no-files-found: ignore + retention-days: 14 diff --git a/.github/workflows/managed-images.yaml b/.github/workflows/managed-images.yaml index f50200db5e7..c66ffa61b82 100644 --- a/.github/workflows/managed-images.yaml +++ b/.github/workflows/managed-images.yaml @@ -41,6 +41,10 @@ permissions: contents: read packages: write +concurrency: + group: managed-images-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + env: REGISTRY: ghcr.io @@ -363,6 +367,7 @@ jobs: name: managed-pr-contract-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.agent }} path: ${{ runner.temp }}/managed-pr-contract/contract.json if-no-files-found: error + retention-days: 1 pr-managed-activation: name: PR exact all-agent managed runtime activation @@ -456,6 +461,7 @@ jobs: name: managed-image-activation-${{ github.run_id }}-${{ github.run_attempt }} path: e2e-artifacts/live/managed-image-activation/ if-no-files-found: error + retention-days: 1 build-and-validate: name: Build and validate ${{ matrix.display_name }} managed image (${{ matrix.arch }}) diff --git a/.github/workflows/pr-merge-conflict-fixer.yaml b/.github/workflows/pr-merge-conflict-fixer.yaml index 7125b91255a..0de8f7bca3d 100644 --- a/.github/workflows/pr-merge-conflict-fixer.yaml +++ b/.github/workflows/pr-merge-conflict-fixer.yaml @@ -15,6 +15,7 @@ jobs: scan: name: Scan conflicting PRs runs-on: ubuntu-24.04 + timeout-minutes: 10 permissions: contents: read pull-requests: read @@ -111,6 +112,8 @@ jobs: with: name: pr-conflict-resolution-${{ matrix.item.pr_number }}-${{ matrix.item.head_sha }}-${{ matrix.item.base_sha }} path: ${{ env.ARTIFACT_DIR }}/resolution.patch + if-no-files-found: error + retention-days: 1 publish: name: Publish PR #${{ matrix.item.pr_number }} @@ -119,6 +122,7 @@ jobs: - resolve if: ${{ always() && needs.scan.outputs.count != '0' }} runs-on: ubuntu-24.04 + timeout-minutes: 10 permissions: contents: write pull-requests: read diff --git a/.github/workflows/pr-review-advisor.yaml b/.github/workflows/pr-review-advisor.yaml index 289259eeca1..46958f34782 100644 --- a/.github/workflows/pr-review-advisor.yaml +++ b/.github/workflows/pr-review-advisor.yaml @@ -298,6 +298,7 @@ jobs: name: ${{ matrix.advisor.artifact_name }} path: artifacts/${{ matrix.advisor.artifact_dir }}/ if-no-files-found: warn + retention-days: 14 - name: Verify advisor analysis outcome if: always() diff --git a/.github/workflows/pr-self-hosted.yaml b/.github/workflows/pr-self-hosted.yaml index db0dc7bf9e2..9f832911268 100644 --- a/.github/workflows/pr-self-hosted.yaml +++ b/.github/workflows/pr-self-hosted.yaml @@ -30,6 +30,7 @@ concurrency: jobs: get-pr-info: runs-on: ubuntu-latest + timeout-minutes: 5 outputs: pr-info: ${{ steps.get-pr-info.outputs.pr-info }} steps: @@ -39,6 +40,7 @@ jobs: select-llama-cpp-generic-gpu: needs: get-pr-info runs-on: ubuntu-latest + timeout-minutes: 5 outputs: selected: ${{ steps.changed.outputs.selected }} steps: @@ -145,6 +147,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Resolve sandbox base image uses: ./.github/actions/resolve-sandbox-base-image @@ -172,6 +176,7 @@ jobs: name: sandbox-test-image path: /tmp/sandbox-test-image.tar.gz retention-days: 1 + if-no-files-found: error - name: Upload isolation image uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -179,6 +184,7 @@ jobs: name: isolation-image path: /tmp/isolation-image.tar.gz retention-days: 1 + if-no-files-found: error build-sandbox-images-arm64: runs-on: linux-arm64-cpu4 @@ -186,6 +192,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Resolve sandbox base image uses: ./.github/actions/resolve-sandbox-base-image @@ -209,6 +217,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Download image artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 @@ -229,6 +239,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Set up Node uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 @@ -266,6 +278,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Download image artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 @@ -286,6 +300,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Download image artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 1c7b93097a8..740fddd49d2 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -115,6 +115,7 @@ jobs: .github/actions/ci-cli-coverage-merge .github/actions/ci-plugin-coverage .github/actions/ci-installer-integration + .github/actions/ci-install-dependencies.sh sparse-checkout-cone-mode: false - name: Run static checks @@ -144,6 +145,7 @@ jobs: .github/actions/ci-cli-coverage-merge .github/actions/ci-plugin-coverage .github/actions/ci-installer-integration + .github/actions/ci-install-dependencies.sh sparse-checkout-cone-mode: false - name: Run build and type checks @@ -168,6 +170,7 @@ jobs: persist-credentials: false sparse-checkout: | .github/actions/ci-installer-integration + .github/actions/ci-install-dependencies.sh sparse-checkout-cone-mode: false - name: Detect trusted installer integration action @@ -229,6 +232,7 @@ jobs: persist-credentials: false sparse-checkout: | .github/actions/ci-wechat-runtime-audit + .github/actions/ci-reviewed-npm-audit/verify-and-install-npm.sh sparse-checkout-cone-mode: false - name: Detect trusted WeChat runtime audit @@ -402,6 +406,7 @@ jobs: .github/actions/ci-cli-coverage-merge .github/actions/ci-plugin-coverage .github/actions/ci-installer-integration + .github/actions/ci-install-dependencies.sh sparse-checkout-cone-mode: false - name: Detect trusted E2E support sharding @@ -572,6 +577,7 @@ jobs: .github/actions/ci-cli-coverage-merge .github/actions/ci-plugin-coverage .github/actions/ci-installer-integration + .github/actions/ci-install-dependencies.sh sparse-checkout-cone-mode: false - name: Merge CLI coverage @@ -607,6 +613,7 @@ jobs: .github/actions/ci-cli-coverage-merge .github/actions/ci-plugin-coverage .github/actions/ci-installer-integration + .github/actions/ci-install-dependencies.sh sparse-checkout-cone-mode: false - name: Run plugin coverage diff --git a/.github/workflows/regression-e2e.yaml b/.github/workflows/regression-e2e.yaml index 22e1dc41743..a5b51fee399 100644 --- a/.github/workflows/regression-e2e.yaml +++ b/.github/workflows/regression-e2e.yaml @@ -50,7 +50,7 @@ jobs: set -euo pipefail normalized="$(printf '%s' "$JOBS" | tr -d '[:space:]')" case ",${normalized}," in - ,|*,whatsapp-qr-compact-e2e,*) + ,,|*,whatsapp-qr-compact-e2e,*) echo "whatsapp_qr_compact=true" >> "$GITHUB_OUTPUT" ;; *) diff --git a/.github/workflows/release-latest-tag.yaml b/.github/workflows/release-latest-tag.yaml index b7d81a16e9f..3e955a1d905 100644 --- a/.github/workflows/release-latest-tag.yaml +++ b/.github/workflows/release-latest-tag.yaml @@ -28,6 +28,7 @@ concurrency: jobs: update-latest: runs-on: ubuntu-latest + timeout-minutes: 10 steps: - name: Check out repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 diff --git a/.github/workflows/sandbox-images-and-e2e.yaml b/.github/workflows/sandbox-images-and-e2e.yaml index f9beb01b48a..8df0d7d3648 100644 --- a/.github/workflows/sandbox-images-and-e2e.yaml +++ b/.github/workflows/sandbox-images-and-e2e.yaml @@ -114,6 +114,7 @@ jobs: name: sandbox-test-image path: /tmp/sandbox-test-image.tar.gz retention-days: 1 + if-no-files-found: error - name: Upload isolation image uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -121,6 +122,7 @@ jobs: name: isolation-image path: /tmp/isolation-image.tar.gz retention-days: 1 + if-no-files-found: error - name: Clean up Docker auth if: always() @@ -237,6 +239,7 @@ jobs: name: hermes-isolation-image path: /tmp/hermes-isolation-image.tar.gz retention-days: 1 + if-no-files-found: error - name: Record resources after Hermes image build if: always() diff --git a/.github/workflows/wsl-e2e.yaml b/.github/workflows/wsl-e2e.yaml index 0a697d8c375..0a55683284d 100644 --- a/.github/workflows/wsl-e2e.yaml +++ b/.github/workflows/wsl-e2e.yaml @@ -174,3 +174,4 @@ jobs: path: | C:\Users\runneradmin\AppData\Local\Temp\nemoclaw-e2e-install.log if-no-files-found: ignore + retention-days: 14 diff --git a/ci/source-shape-test-budget.json b/ci/source-shape-test-budget.json index 8711e598e66..29b62dceb32 100644 --- a/ci/source-shape-test-budget.json +++ b/ci/source-shape-test-budget.json @@ -76,11 +76,6 @@ "test": "uses the corporate CA conditionally for all Hermes registry remediations", "category": "security" }, - { - "file": "test/dcode-base-image-workflow.test.ts", - "test": "accepts every discovered publisher and rejects supply-chain mutations", - "category": "security" - }, { "file": "test/e2e-fixture-dependency-review.test.ts", "test": "keeps installed fixture dependencies on exact versions", diff --git a/test/ci-install-dependencies.test.ts b/test/ci-install-dependencies.test.ts new file mode 100644 index 00000000000..3d5a204f781 --- /dev/null +++ b/test/ci-install-dependencies.test.ts @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { spawnSync } from "node:child_process"; +import { chmodSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { afterEach, describe, expect, it } from "vitest"; + +const temporaryRoots: string[] = []; + +afterEach(() => { + for (const root of temporaryRoots.splice(0)) rmSync(root, { force: true, recursive: true }); +}); + +describe("shared CI dependency installer", () => { + it("installs root and plugin lockfiles without lifecycle scripts", () => { + const root = mkdtempSync(join(tmpdir(), "nemoclaw-ci-install-")); + temporaryRoots.push(root); + const bin = join(root, "bin"); + const trace = join(root, "npm.trace"); + mkdirSync(bin); + const npm = join(bin, "npm"); + writeFileSync(npm, `#!/bin/sh\nprintf '%s\n' "$*" >> "$NPM_TRACE"\n`); + chmodSync(npm, 0o755); + + const result = spawnSync("bash", [".github/actions/ci-install-dependencies.sh"], { + cwd: join(import.meta.dirname, ".."), + encoding: "utf8", + env: { ...process.env, NPM_TRACE: trace, PATH: `${bin}:${process.env.PATH}` }, + }); + + expect(result.status, result.stderr).toBe(0); + expect(readFileSync(trace, "utf8").trim().split("\n")).toEqual([ + "ci --ignore-scripts", + "--prefix nemoclaw ci --ignore-scripts", + ]); + }); +}); diff --git a/test/dcode-base-image-workflow.test.ts b/test/dcode-base-image-workflow.test.ts index 9a6450f363f..58fabf45c8f 100644 --- a/test/dcode-base-image-workflow.test.ts +++ b/test/dcode-base-image-workflow.test.ts @@ -5,254 +5,13 @@ import fs from "node:fs"; import path from "node:path"; import { describe, expect, it } from "vitest"; -import YAML from "yaml"; - -import { createDeepAgentsCodeBaseImageResolutionOptions } from "../src/lib/agent/deep-agents-code-base-image.ts"; -import { loadAgent } from "../src/lib/agent/defs.ts"; - -type WorkflowStep = { - name?: string; - id?: string; - uses?: string; - run?: string; - env?: Record; - with?: Record; -}; - -type PublisherMatrixEntry = { - agent?: string; - arch?: string; - display_name?: string; - dockerfile?: string; - image?: string; - platform?: string; - runner?: string; -}; - -type WorkflowJob = { - name?: string; - needs?: string | string[]; - "runs-on"?: string; - "timeout-minutes"?: number; - strategy?: { - "fail-fast"?: boolean; - matrix?: { include?: PublisherMatrixEntry[] }; - }; - steps?: WorkflowStep[]; -}; - -type Workflow = { - on?: { push?: { paths?: string[] } }; - jobs?: Record; -}; - -type Publisher = { - jobName: string; - job: WorkflowJob; - build: WorkflowStep; - buildIndex: number; - dockerfile: string; - matrix: PublisherMatrixEntry; -}; - -type RegistryCacheEntry = { - mode?: string; - ref?: string; -}; const repoRoot = path.resolve(import.meta.dirname, ".."); -const workflow = YAML.parse( - fs.readFileSync(path.join(repoRoot, ".github", "workflows", "base-image.yaml"), "utf8"), -) as Workflow; -const FULL_SHA_ACTION = /^[^@]+@[0-9a-f]{40}$/i; -const OPENCLAW_AGENT_GATE = - 'if [ "$AGENT" = "openclaw" ] && [ -n "${OPENCLAW_VERSION_INPUT}" ]; then'; -const PLATFORM_DIGEST_OUTPUT = - "type=image,name=${{ env.REGISTRY }}/${{ matrix.image }},push-by-digest=true,name-canonical=true,push=true"; - -function renderMatrixValue(value: unknown, matrix: PublisherMatrixEntry): string { - return String(value ?? "").replace( - /\$\{\{\s*matrix\.([a-z_]+)\s*\}\}/gu, - (_match, key: keyof PublisherMatrixEntry) => String(matrix[key] ?? ""), - ); -} - -function publisherBuildSteps(candidate: Workflow): Omit[] { - return Object.entries(candidate.jobs ?? {}).flatMap(([jobName, job]) => { - const steps = job.steps ?? []; - return steps - .map((build, buildIndex) => ({ build, buildIndex })) - .filter(({ build }) => build.uses?.startsWith("docker/build-push-action@")) - .map(({ build, buildIndex }) => ({ jobName, job, build, buildIndex })); - }); -} - -function publisherJobs(candidate: Workflow): Publisher[] { - return publisherBuildSteps(candidate).flatMap(({ jobName, job, build, buildIndex }) => - (job.strategy?.matrix?.include ?? []) - .filter((matrix) => matrix.display_name) - .map((matrix) => ({ - jobName: `${jobName} (${matrix.display_name})`, - job, - build, - buildIndex, - dockerfile: renderMatrixValue(build.with?.file, matrix), - matrix, - })), - ); -} - -function openClawPlatformPublishers(candidate: Workflow): Publisher[] { - const jobName = "build-openclaw-platforms"; - const job = candidate.jobs?.[jobName] as WorkflowJob; - const steps = job?.steps ?? []; - const buildIndex = steps.findIndex((step) => step.uses?.startsWith("docker/build-push-action@")); - const build = steps[buildIndex] as WorkflowStep; - return (job.strategy?.matrix?.include ?? []).map((matrix) => ({ - jobName: `${jobName} (${matrix.arch ?? "unnamed"})`, - job, - build, - buildIndex, - dockerfile: renderMatrixValue(build.with?.file, matrix), - matrix, - })); -} - -function copiedInputs(dockerfile: string): string[] { - return [ - ...fs - .readFileSync(path.join(repoRoot, dockerfile), "utf8") - .matchAll(/^COPY\s+(?!--from=)(?:--\S+\s+)*(\S+)\s+\S+/gm), - ].map(([, input]) => input); -} - -function copiedLocks(dockerfile: string): string[] { - return copiedInputs(dockerfile).filter((input) => input.endsWith(".lock")); -} - -function registryCacheEntries(value: unknown): RegistryCacheEntry[] { - return String(value ?? "") - .split(/\r?\n/u) - .map((entry) => entry.trim()) - .filter((entry) => entry.split(",").includes("type=registry")) - .map((entry) => - Object.fromEntries( - entry - .split(",") - .filter((field) => field !== "type=registry") - .map((field) => field.split("=", 2) as [string, string]), - ), - ); -} - -function hasAgentScopedOpenClawVersion(step: WorkflowStep | undefined): boolean { - const segments = (step?.run ?? "").split(OPENCLAW_AGENT_GATE); - return ( - step?.env?.AGENT === "${{ matrix.agent }}" && - segments.length === 3 && - segments[0].includes('openclaw_build_arg=""') && - segments[1].includes('openclaw_build_arg="OPENCLAW_VERSION=${OPENCLAW_VERSION_INPUT}"') && - segments[2].includes('if [[ "$OPENCLAW_VERSION_INPUT"') - ); -} - -function validatePublisherInputs(candidate: Workflow, publishers: Publisher[]): string[] { - const triggerPaths = candidate.on?.push?.paths ?? []; - return publishers.flatMap(({ jobName, dockerfile }) => { - const dockerfileExists = - dockerfile.length > 0 && fs.existsSync(path.join(repoRoot, dockerfile)); - const copiedInputPaths = dockerfileExists ? copiedInputs(dockerfile) : []; - return [ - ...(!dockerfileExists ? [`${jobName} must publish from an existing Dockerfile`] : []), - ...(!triggerPaths.includes(dockerfile) - ? [`${jobName} Dockerfile must trigger the publisher workflow`] - : []), - ...copiedInputPaths - .filter((input) => !triggerPaths.includes(input)) - .map((input) => `${jobName} copied input must trigger the publisher workflow: ${input}`), - ]; - }); -} - -function validatePublishers(candidate: Workflow): string[] { - const publishers = publisherJobs(candidate); - const exportedCacheRefCounts = new Map(); - for (const { build, matrix } of publishers) { - const cacheRef = - registryCacheEntries(renderMatrixValue(build.with?.["cache-to"], matrix))[0]?.ref ?? ""; - exportedCacheRefCounts.set(cacheRef, (exportedCacheRefCounts.get(cacheRef) ?? 0) + 1); - } - - return [ - ...validatePublisherInputs(candidate, publishers), - ...publishers.flatMap(({ jobName, job, build, buildIndex, matrix }) => { - const steps = job.steps ?? []; - const metadata = steps.find((step) => step.id === "meta"); - const guardIndex = steps.findIndex((step) => - (step.run ?? "").includes("scripts/check-production-build-args.sh"), - ); - const guard = steps[guardIndex]; - const dockerActions = steps.filter((step) => step.uses?.startsWith("docker/")); - const tags = String(metadata?.with?.tags ?? ""); - const metadataImage = renderMatrixValue(metadata?.with?.images, matrix); - const expectedCacheRef = `${metadataImage}:buildcache-${matrix.arch}`; - const cacheFrom = registryCacheEntries(renderMatrixValue(build.with?.["cache-from"], matrix)); - const cacheTo = registryCacheEntries(renderMatrixValue(build.with?.["cache-to"], matrix)); - const importedCacheRef = cacheFrom[0]?.ref; - const exportedCacheRef = cacheTo[0]?.ref; - return [ - ...(guardIndex < 0 || guardIndex >= buildIndex - ? [`${jobName} must validate production build args before publishing`] - : []), - ...(!hasAgentScopedOpenClawVersion(guard) - ? [`${jobName} must scope OpenClaw version handling to the OpenClaw matrix entry`] - : []), - ...(!metadata?.uses?.startsWith("docker/metadata-action@") - ? [`${jobName} must derive publication metadata with docker/metadata-action`] - : []), - ...(metadataImage.length === 0 ? [`${jobName} must declare a publication image`] : []), - ...(tags.length > 0 ? [`${jobName} platform build must not publish mutable tags`] : []), - ...dockerActions - .filter((step) => !FULL_SHA_ACTION.test(step.uses ?? "")) - .map((step) => `${jobName} Docker action must use a full commit SHA: ${step.uses}`), - ...(!FULL_SHA_ACTION.test(build.uses ?? "") - ? [`${jobName} build-push action must use a full commit SHA`] - : []), - ...(build.with?.context !== "." ? [`${jobName} must publish from repository context`] : []), - ...(build.with?.platforms !== "${{ matrix.platform }}" - ? [`${jobName} must build its selected native platform`] - : []), - ...(build.with?.outputs !== PLATFORM_DIGEST_OUTPUT - ? [`${jobName} must push an immutable platform digest`] - : []), - ...(build.with?.tags !== undefined || build.with?.push !== undefined - ? [`${jobName} platform build must not publish tags directly`] - : []), - ...(build.with?.labels !== "${{ steps.meta.outputs.labels }}" - ? [`${jobName} must use the reviewed metadata labels`] - : []), - ...(cacheFrom.length !== 1 || !importedCacheRef - ? [`${jobName} cache-from must declare exactly one registry cache ref`] - : []), - ...(cacheTo.length !== 1 || !exportedCacheRef - ? [`${jobName} cache-to must declare exactly one registry cache ref`] - : []), - ...(importedCacheRef !== exportedCacheRef - ? [`${jobName} must import and export the same registry cache ref`] - : []), - ...(cacheTo[0]?.mode !== "max" - ? [`${jobName} must export its registry cache in max mode`] - : []), - ...(exportedCacheRef && exportedCacheRef !== expectedCacheRef - ? [`${jobName} registry cache must use its publication image buildcache tag`] - : []), - ...(exportedCacheRef && exportedCacheRefCounts.get(exportedCacheRef) !== 1 - ? [`${jobName} must use a publisher-unique registry cache ref`] - : []), - ]; - }), - ]; -} +const baseDockerfiles = [ + "Dockerfile.base", + "agents/hermes/Dockerfile.base", + "agents/langchain-deepagents-code/Dockerfile.base", +] as const; function pinnedAptVersion(dockerfile: string, packageName: string): string { const source = fs.readFileSync(path.join(repoRoot, dockerfile), "utf8"); @@ -261,428 +20,14 @@ function pinnedAptVersion(dockerfile: string, packageName: string): string { return version as string; } -describe("base-image publication behavior", () => { - // source-shape-contract: security -- Publisher mutations must preserve immutable actions, guarded arguments, and trusted registry cache ownership - it("accepts every discovered publisher and rejects supply-chain mutations", () => { - const publishers = publisherJobs(workflow); - expect(publisherBuildSteps(workflow)).toHaveLength(3); - expect( - publishers.map(({ dockerfile, matrix }) => ({ - agent: matrix.agent, - arch: matrix.arch, - dockerfile, - image: matrix.image, - })), - ).toEqual([ - { - agent: "hermes", - arch: "amd64", - dockerfile: "agents/hermes/Dockerfile.base", - image: "nvidia/nemoclaw/hermes-sandbox-base", - }, - { - agent: "hermes", - arch: "arm64", - dockerfile: "agents/hermes/Dockerfile.base", - image: "nvidia/nemoclaw/hermes-sandbox-base", - }, - { - agent: "langchain-deepagents-code", - arch: "amd64", - dockerfile: "agents/langchain-deepagents-code/Dockerfile.base", - image: "nvidia/nemoclaw/langchain-deepagents-code-sandbox-base", - }, - { - agent: "langchain-deepagents-code", - arch: "arm64", - dockerfile: "agents/langchain-deepagents-code/Dockerfile.base", - image: "nvidia/nemoclaw/langchain-deepagents-code-sandbox-base", - }, - ]); - for (const publisher of publishers) { - expect(publisher.job.strategy?.["fail-fast"]).toBe(false); - } - expect(validatePublishers(workflow)).toEqual([]); - expect(validatePublisherInputs(workflow, openClawPlatformPublishers(workflow))).toEqual([]); - - const mutated = structuredClone(workflow); - const mutatedPublisher = publisherJobs(mutated)[0]; - const mutatedSteps = mutatedPublisher.job.steps ?? []; - const otherPublisher = publisherJobs(mutated)[1]; - const otherCacheRef = registryCacheEntries( - renderMatrixValue(otherPublisher.build.with?.["cache-to"], otherPublisher.matrix), - )[0]?.ref; - const mutatedGuard = mutatedSteps.find((step) => - (step.run ?? "").includes("scripts/check-production-build-args.sh"), - ); - mutatedPublisher.build.uses = "docker/build-push-action@v7"; - mutatedPublisher.build.with = { - ...mutatedPublisher.build.with, - push: false, - "cache-from": "type=gha", - "cache-to": `type=registry,ref=${otherCacheRef}`, - }; - mutatedGuard!.run = "true"; - - expect(validatePublishers(mutated)).toEqual( - expect.arrayContaining([ - `${mutatedPublisher.jobName} must validate production build args before publishing`, - `${mutatedPublisher.jobName} Docker action must use a full commit SHA: docker/build-push-action@v7`, - `${mutatedPublisher.jobName} build-push action must use a full commit SHA`, - `${mutatedPublisher.jobName} platform build must not publish tags directly`, - `${mutatedPublisher.jobName} cache-from must declare exactly one registry cache ref`, - `${mutatedPublisher.jobName} must import and export the same registry cache ref`, - `${mutatedPublisher.jobName} must export its registry cache in max mode`, - `${mutatedPublisher.jobName} registry cache must use its publication image buildcache tag`, - `${mutatedPublisher.jobName} must use a publisher-unique registry cache ref`, - ]), - ); - - const invertedGate = structuredClone(workflow); - const invertedPublisher = publisherJobs(invertedGate)[0]; - const invertedGuard = (invertedPublisher.job.steps ?? []).find((step) => - (step.run ?? "").includes("scripts/check-production-build-args.sh"), - ); - invertedGuard!.run = invertedGuard!.run!.replaceAll( - OPENCLAW_AGENT_GATE, - OPENCLAW_AGENT_GATE.replace("openclaw", "hermes"), - ); - - expect(validatePublishers(invertedGate)).toContain( - `${invertedPublisher.jobName} must scope OpenClaw version handling to the OpenClaw matrix entry`, - ); - - const missingTriggers = structuredClone(workflow); - const copiedInput = copiedInputs("Dockerfile.base")[0]; - missingTriggers.on!.push!.paths = missingTriggers.on!.push!.paths!.filter( - (triggerPath) => triggerPath !== "Dockerfile.base" && triggerPath !== copiedInput, - ); - expect( - validatePublisherInputs(missingTriggers, openClawPlatformPublishers(missingTriggers)), - ).toEqual( - expect.arrayContaining([ - "build-openclaw-platforms (amd64) Dockerfile must trigger the publisher workflow", - `build-openclaw-platforms (arm64) copied input must trigger the publisher workflow: ${copiedInput}`, - ]), - ); - }); +describe("base-image dependency contracts", () => { + it("keeps shared apt dependencies pinned and aligned across base images (#6679)", () => { + const curlVersions = baseDockerfiles.map((dockerfile) => pinnedAptVersion(dockerfile, "curl")); - it("publishes OpenClaw atomically from native architecture runners", () => { - const publishers = openClawPlatformPublishers(workflow); - const platformJob = workflow.jobs?.["build-openclaw-platforms"]; - const manifestJob = workflow.jobs?.["build-and-push-openclaw"]; - - expect(platformJob?.needs).toEqual(["reviewed-npm-audit"]); - expect(platformJob?.["timeout-minutes"]).toBe(60); - expect(platformJob?.strategy?.["fail-fast"]).toBe(false); - expect( - publishers.map(({ matrix }) => ({ - agent: matrix.agent, - arch: matrix.arch, - platform: matrix.platform, - runner: matrix.runner, - })), - ).toEqual([ - { - agent: "openclaw", - arch: "amd64", - platform: "linux/amd64", - runner: "ubuntu-24.04", - }, - { - agent: "openclaw", - arch: "arm64", - platform: "linux/arm64", - runner: "ubuntu-24.04-arm", - }, - ]); - - for (const { job, build, buildIndex, dockerfile, matrix } of publishers) { - const steps = job.steps ?? []; - const guardIndex = steps.findIndex((step) => - (step.run ?? "").includes("scripts/check-production-build-args.sh"), - ); - const digestExport = steps.find((step) => step.name === "Export platform digest"); - const digestUpload = steps.find((step) => step.name === "Upload platform digest"); - const cacheSuffix = `buildcache-${matrix.arch}`; - - expect(dockerfile).toBe("Dockerfile.base"); - expect(job["runs-on"]).toBe("${{ matrix.runner }}"); - expect(steps.some((step) => step.uses?.startsWith("docker/setup-qemu-action@"))).toBe(false); - expect(guardIndex).toBeGreaterThanOrEqual(0); - expect(guardIndex).toBeLessThan(buildIndex); - expect(hasAgentScopedOpenClawVersion(steps[guardIndex])).toBe(true); - expect(build.with?.platforms).toBe("${{ matrix.platform }}"); - expect(build.with?.outputs).toBe(PLATFORM_DIGEST_OUTPUT); - expect(build.with?.tags).toBeUndefined(); - expect(renderMatrixValue(build.with?.["cache-from"], matrix)).toContain(cacheSuffix); - expect(renderMatrixValue(build.with?.["cache-to"], matrix)).toContain( - `${cacheSuffix},mode=max`, - ); - expect(digestExport?.env?.ARCH).toBe("${{ matrix.arch }}"); - expect(digestExport?.run).toContain("^sha256:[0-9a-f]{64}$"); - expect(digestExport?.run).toContain('touch "$RUNNER_TEMP/digests/${ARCH}-${DIGEST#sha256:}"'); - expect(digestUpload?.with?.name).toBe( - "openclaw-base-digest-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.arch }}", - ); - for (const step of steps.filter((step) => step.uses)) { - expect(step.uses, `${matrix.arch}: ${step.name}`).toMatch(FULL_SHA_ACTION); - } - } - - expect(manifestJob?.name).toBe("Build and push OpenClaw base image"); - expect(manifestJob?.needs).toEqual(["build-openclaw-platforms", "reviewed-npm-audit"]); - expect(manifestJob?.["timeout-minutes"]).toBe(10); - expect( - manifestJob?.steps?.some((step) => step.uses?.startsWith("docker/build-push-action@")), - ).toBe(false); - const download = manifestJob?.steps?.find((step) => step.name === "Download platform digests"); - const metadata = manifestJob?.steps?.find((step) => step.id === "meta"); - const createManifest = manifestJob?.steps?.find( - (step) => step.name === "Create and verify multi-platform manifest", - ); - expect(download?.with).toMatchObject({ - pattern: "openclaw-base-digest-${{ github.run_id }}-${{ github.run_attempt }}-*", - "merge-multiple": true, - }); - expect(metadata?.with?.images).toBe("${{ env.REGISTRY }}/nvidia/nemoclaw/sandbox-base"); - expect(metadata?.with?.tags).toContain("type=raw,value=latest"); - expect(metadata?.with?.tags).toContain("type=ref,event=tag"); - expect(metadata?.with?.tags).toContain("type=sha,prefix=,format=short"); - expect(createManifest?.env?.AGENT).toBe("openclaw"); - const openClawManifestScript = createManifest?.run ?? ""; - expect(openClawManifestScript).toContain('"${#digest_files[@]}" -ne 2'); - expect(openClawManifestScript).toContain("^(amd64|arm64)-([0-9a-f]{64})$"); - expect(openClawManifestScript).toContain("--format '{{.Image.OS}}/{{.Image.Architecture}}'"); - expect(openClawManifestScript).toContain( - 'if [ "$source_platform" != "linux/$expected_arch" ]; then', - ); - expect(openClawManifestScript).toContain("duplicate platform digest"); - expect(openClawManifestScript).toContain("declare -A source_digests=()"); - expect(openClawManifestScript).toContain( - 'source_digests["linux/$expected_arch"]="sha256:$digest"', - ); - expect(openClawManifestScript.indexOf("source_platform=")).toBeLessThan( - openClawManifestScript.indexOf("docker buildx imagetools create"), - ); - expect(openClawManifestScript).toContain('--tag "$candidate_tag"'); - expect(openClawManifestScript).toContain('--metadata-file "$candidate_metadata"'); - expect(openClawManifestScript).toContain('"${sources[@]}"'); - expect(openClawManifestScript).toContain('"amd64,arm64"'); - expect(openClawManifestScript).toContain('"${source_digests[linux/amd64]}"'); - expect(openClawManifestScript).toContain('"${source_digests[linux/arm64]}"'); - expect(openClawManifestScript).toContain("platform_digests_json="); - expect(openClawManifestScript).toContain("declare -A platform_digests=()"); - expect(openClawManifestScript).toContain("scripts/export-managed-base-image-contract.sh"); - expect(openClawManifestScript).not.toContain("first_tag="); - expect(openClawManifestScript).not.toContain( - 'imagetools create "${tag_args[@]}" "${sources[@]}"', - ); - const openClawValidationIndex = openClawManifestScript.indexOf( - "scripts/checks/validate-managed-base-index.sh", - ); - const openClawPromotionIndex = openClawManifestScript.indexOf("publication_metadata="); - expect(openClawManifestScript.indexOf("candidate_tag=")).toBeLessThan(openClawValidationIndex); - expect(openClawValidationIndex).toBeLessThan(openClawPromotionIndex); - expect(openClawManifestScript.slice(openClawPromotionIndex)).toContain('"${tag_args[@]}"'); - expect(openClawManifestScript.slice(openClawPromotionIndex)).toContain('"$reference"'); - expect(openClawManifestScript).toContain("published_digest="); - expect(openClawManifestScript).toContain('if [ "$published_digest" != "$digest" ]; then'); - for (const step of (manifestJob?.steps ?? []).filter((step) => step.uses)) { - expect(step.uses, step.name).toMatch(FULL_SHA_ACTION); - } - }); - - it("publishes sibling images atomically from native architecture runners", () => { - const publishers = publisherJobs(workflow); - const imagePublishers = [ - { - agent: "hermes", - platformJobName: "build-hermes-platforms", - manifestJobName: "build-and-push-hermes", - manifestName: "Build and push Hermes base image", - artifactPattern: "hermes-base-digest-${{ github.run_id }}-${{ github.run_attempt }}-*", - image: "${{ env.REGISTRY }}/nvidia/nemoclaw/hermes-sandbox-base", - }, - { - agent: "langchain-deepagents-code", - platformJobName: "build-dcode-platforms", - manifestJobName: "build-and-push-dcode", - manifestName: "Build and push Deep Agents Code base image", - artifactPattern: - "langchain-deepagents-code-base-digest-${{ github.run_id }}-${{ github.run_attempt }}-*", - image: "${{ env.REGISTRY }}/nvidia/nemoclaw/langchain-deepagents-code-sandbox-base", - }, - ]; - - for (const { platformJobName } of imagePublishers) { - const platformJob = workflow.jobs?.[platformJobName]; - expect(platformJob?.needs).toEqual(["reviewed-npm-audit"]); - expect(platformJob?.["timeout-minutes"]).toBe(60); - expect(platformJob?.["runs-on"]).toBe("${{ matrix.runner }}"); - expect(platformJob?.strategy?.["fail-fast"]).toBe(false); - } - expect( - publishers.map(({ matrix }) => ({ - agent: matrix.agent, - arch: matrix.arch, - platform: matrix.platform, - runner: matrix.runner, - })), - ).toEqual([ - { - agent: "hermes", - arch: "amd64", - platform: "linux/amd64", - runner: "ubuntu-24.04", - }, - { - agent: "hermes", - arch: "arm64", - platform: "linux/arm64", - runner: "ubuntu-24.04-arm", - }, - { - agent: "langchain-deepagents-code", - arch: "amd64", - platform: "linux/amd64", - runner: "ubuntu-24.04", - }, - { - agent: "langchain-deepagents-code", - arch: "arm64", - platform: "linux/arm64", - runner: "ubuntu-24.04-arm", - }, - ]); - - for (const { job, build, matrix } of publishers) { - const steps = job.steps ?? []; - const digestExport = steps.find((step) => step.name === "Export platform digest"); - const digestUpload = steps.find((step) => step.name === "Upload platform digest"); - - expect(steps.some((step) => step.uses?.startsWith("docker/setup-qemu-action@"))).toBe(false); - expect(build.with?.platforms).toBe("${{ matrix.platform }}"); - expect(build.with?.outputs).toBe(PLATFORM_DIGEST_OUTPUT); - expect(digestExport?.env?.ARCH).toBe("${{ matrix.arch }}"); - expect(digestExport?.run).toContain('touch "$RUNNER_TEMP/digests/${ARCH}-${DIGEST#sha256:}"'); - expect(digestUpload?.with?.name).toBe( - "${{ matrix.agent }}-base-digest-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.arch }}", - ); - expect(renderMatrixValue(digestUpload?.with?.name, matrix)).toBe( - `${matrix.agent}-base-digest-` + - "${{ github.run_id }}-${{ github.run_attempt }}-" + - matrix.arch, - ); - } - - for (const imagePublisher of imagePublishers) { - const manifestJob = workflow.jobs?.[imagePublisher.manifestJobName]; - expect(manifestJob?.name).toBe(imagePublisher.manifestName); - expect(manifestJob?.needs).toEqual([imagePublisher.platformJobName, "reviewed-npm-audit"]); - expect(manifestJob?.["timeout-minutes"]).toBe(10); - expect( - manifestJob?.steps?.some((step) => step.uses?.startsWith("docker/build-push-action@")), - ).toBe(false); - const download = manifestJob?.steps?.find( - (step) => step.name === "Download platform digests", - ); - const metadata = manifestJob?.steps?.find((step) => step.id === "meta"); - const createManifest = manifestJob?.steps?.find( - (step) => step.name === "Create and verify multi-platform manifest", - ); - expect(download?.with).toMatchObject({ - pattern: imagePublisher.artifactPattern, - "merge-multiple": true, - }); - expect(metadata?.with?.images).toBe(imagePublisher.image); - expect(metadata?.with?.tags).toContain("type=raw,value=latest"); - expect(metadata?.with?.tags).toContain("type=ref,event=tag"); - expect(metadata?.with?.tags).toContain("type=sha,prefix=,format=short"); - expect(createManifest?.env?.AGENT).toBe(imagePublisher.agent); - expect(createManifest?.env?.IMAGE).toBe(imagePublisher.image); - const manifestScript = createManifest?.run ?? ""; - expect(manifestScript).toContain('"${#digest_files[@]}" -ne 2'); - expect(manifestScript).toContain("^(amd64|arm64)-([0-9a-f]{64})$"); - expect(manifestScript).toContain("--format '{{.Image.OS}}/{{.Image.Architecture}}'"); - expect(manifestScript).toContain( - 'scripts/checks/retry-docker-imagetools-inspect.sh "$source"', - ); - expect(manifestScript).toContain( - 'scripts/checks/retry-docker-imagetools-inspect.sh "$reference" --raw', - ); - expect(manifestScript).not.toContain("docker buildx imagetools inspect"); - expect(manifestScript).toContain('if [ "$source_platform" != "linux/$expected_arch" ]; then'); - expect(manifestScript).toContain("duplicate platform digest"); - expect(manifestScript).toContain("declare -A source_digests=()"); - expect(manifestScript).toContain('source_digests["linux/$expected_arch"]="sha256:$digest"'); - expect(manifestScript.indexOf("source_platform=")).toBeLessThan( - manifestScript.indexOf("docker buildx imagetools create"), - ); - expect(manifestScript).toContain('--tag "$candidate_tag"'); - expect(manifestScript).toContain('--metadata-file "$candidate_metadata"'); - expect(manifestScript).toContain('"${sources[@]}"'); - expect(manifestScript).toContain('"amd64,arm64"'); - expect(manifestScript).toContain("scripts/checks/validate-managed-base-index.sh"); - expect(manifestScript).toContain('"${source_digests[linux/amd64]}"'); - expect(manifestScript).toContain('"${source_digests[linux/arm64]}"'); - expect(manifestScript).toContain("platform_digests_json="); - expect(manifestScript).toContain("declare -A platform_digests=()"); - expect(manifestScript).toContain("scripts/export-managed-base-image-contract.sh"); - expect(manifestScript).not.toContain("first_tag="); - expect(manifestScript).not.toContain('imagetools create "${tag_args[@]}" "${sources[@]}"'); - const validationIndex = manifestScript.indexOf( - "scripts/checks/validate-managed-base-index.sh", - ); - const promotionIndex = manifestScript.indexOf("publication_metadata="); - expect(manifestScript.indexOf("candidate_tag=")).toBeLessThan(validationIndex); - expect(validationIndex).toBeLessThan(promotionIndex); - expect(manifestScript.slice(promotionIndex)).toContain('"${tag_args[@]}"'); - expect(manifestScript.slice(promotionIndex)).toContain('"$reference"'); - expect(manifestScript).toContain("published_digest="); - expect(manifestScript).toContain('if [ "$published_digest" != "$digest" ]; then'); - for (const step of (manifestJob?.steps ?? []).filter((step) => step.uses)) { - expect(step.uses, step.name).toMatch(FULL_SHA_ACTION); - } - } - }); - - it("keeps shared apt dependencies pinned and aligned across discovered base images (#6679)", () => { - const dockerfiles = [ - ...openClawPlatformPublishers(workflow).map(({ dockerfile }) => dockerfile), - ...publisherJobs(workflow).map(({ dockerfile }) => dockerfile), - ].filter((dockerfile, index, all) => all.indexOf(dockerfile) === index); - const curlVersions = dockerfiles.map((dockerfile) => pinnedAptVersion(dockerfile, "curl")); - - expect(new Set(dockerfiles).size).toBe(dockerfiles.length); expect(new Set(curlVersions).size).toBe(1); - for (const dockerfile of dockerfiles) { + for (const dockerfile of baseDockerfiles) { const source = fs.readFileSync(path.join(repoRoot, dockerfile), "utf8"); expect(source, dockerfile).toMatch(/^FROM\s+\S+@sha256:[0-9a-f]{64}\s*$/m); } }); - - it("binds a copied Deep Agents Code hash lock to the adjacent runtime manifest", () => { - const lockedPublisher = publisherJobs(workflow).find( - ({ dockerfile }) => copiedLocks(dockerfile).length > 0, - ); - expect(lockedPublisher).toBeDefined(); - const [lockPath] = copiedLocks(lockedPublisher!.dockerfile); - const lock = fs.readFileSync(path.join(repoRoot, lockPath), "utf8"); - const dockerfilePath = path.join(repoRoot, lockedPublisher!.dockerfile); - const agent = loadAgent(path.basename(path.dirname(lockedPublisher!.dockerfile))); - const resolution = createDeepAgentsCodeBaseImageResolutionOptions(agent, dockerfilePath); - const lockedVersion = lock.match(/^deepagents-code==([^\s\\]+)/m)?.[1]; - - expect(resolution).toBeDefined(); - expect(resolution?.inputPaths).toEqual( - expect.arrayContaining([agent.manifestPath, path.join(repoRoot, lockPath)]), - ); - expect(lock).toMatch(/^deepagents-code==[^\s\\]+\s+\\\n\s+--hash=sha256:[0-9a-f]{64}/m); - expect(lockedVersion).toBeDefined(); - expect(agent.expectedVersion).toBe(lockedVersion); - expect(resolution?.validationDescription).toBe( - `deepagents-code==${lockedVersion} and the immutable security package inventory`, - ); - }); }); diff --git a/test/managed-image-publication-workflow.test.ts b/test/managed-image-publication-workflow.test.ts index 3b178f33b40..0c3bc43d7c4 100644 --- a/test/managed-image-publication-workflow.test.ts +++ b/test/managed-image-publication-workflow.test.ts @@ -372,20 +372,22 @@ describe("complete managed-image publication workflow", () => { const basePublishers = [ { agent: "hermes", - artifact: "managed-base-${{ github.run_id }}-${{ github.run_attempt }}-hermes", + displayName: "Hermes", + image: "nvidia/nemoclaw/hermes-sandbox-base", job: "build-and-push-hermes", platformsJob: "build-hermes-platforms", }, { agent: "langchain-deepagents-code", - artifact: - "managed-base-${{ github.run_id }}-${{ github.run_attempt }}-langchain-deepagents-code", + displayName: "Deep Agents Code", + image: "nvidia/nemoclaw/langchain-deepagents-code-sandbox-base", job: "build-and-push-dcode", platformsJob: "build-dcode-platforms", }, { agent: "openclaw", - artifact: "managed-base-${{ github.run_id }}-${{ github.run_attempt }}-openclaw", + displayName: "OpenClaw", + image: "nvidia/nemoclaw/sandbox-base", job: "build-and-push-openclaw", platformsJob: "build-openclaw-platforms", }, @@ -397,39 +399,19 @@ describe("complete managed-image publication workflow", () => { `base-image workflow is missing ${expectedPublisher.agent} manifest publisher`, ); expect(basePublisher.needs).toEqual([expectedPublisher.platformsJob, "reviewed-npm-audit"]); - const manifest = step(basePublisher, "Create and verify multi-platform manifest"); - const manifestRun = manifest.run ?? ""; - expect(manifest.id).toBe("manifest"); - expect(manifest.env?.AGENT).toBe(expectedPublisher.agent); - expect(manifest.run).toContain('reference="$IMAGE@$digest"'); - expect(manifest.run).toContain('.["containerimage.descriptor"].digest'); - expect(manifest.run).toContain('--metadata-file "$candidate_metadata"'); - expect(manifestRun).toContain('scripts/checks/retry-docker-imagetools-inspect.sh "$source"'); - expect(manifestRun).toContain( - 'scripts/checks/retry-docker-imagetools-inspect.sh "$reference" --raw', - ); - expect(manifestRun).not.toContain("docker buildx imagetools inspect"); - expect(manifest.run).toContain("scripts/checks/validate-managed-base-index.sh"); - expect(manifest.run).toContain("declare -A source_digests=()"); - expect(manifest.run).toContain('"${source_digests[linux/amd64]}"'); - expect(manifest.run).toContain('"${source_digests[linux/arm64]}"'); - expect(manifest.run).toContain("platform_digests_json="); - expect(manifest.run).toContain("declare -A platform_digests=()"); - expect(manifest.run).toContain("scripts/export-managed-base-image-contract.sh"); - expect(manifest.run).toContain('"${platform_digests[linux/amd64]}"'); - expect(manifest.run).toContain('"${platform_digests[linux/arm64]}"'); - const validationIndex = manifestRun.indexOf("scripts/checks/validate-managed-base-index.sh"); - const promotionIndex = manifestRun.indexOf("publication_metadata="); - expect(manifestRun.indexOf("candidate_tag=")).toBeLessThan(validationIndex); - expect(validationIndex).toBeLessThan(promotionIndex); - expect(manifestRun).toContain("published_digest="); - expect(manifestRun).toContain('if [ "$published_digest" != "$digest" ]; then'); - expect(manifestRun).not.toContain("first_tag="); - expect(manifestRun).not.toContain('imagetools create "${tag_args[@]}" "${sources[@]}"'); + const manifest = step(basePublisher, "Publish validated multi-platform manifest"); + expect(manifest).toMatchObject({ + uses: "./.github/actions/publish-base-image-manifest", + with: { + agent: expectedPublisher.agent, + "display-name": expectedPublisher.displayName, + image: expectedPublisher.image, + registry: "${{ env.REGISTRY }}", + "registry-username": "${{ github.actor }}", + "registry-password": "${{ secrets.GITHUB_TOKEN }}", + }, + }); expect(step(basePublisher, "Checkout").with?.["persist-credentials"]).toBe(false); - expect(step(basePublisher, "Upload managed base image contract").with?.name).toBe( - expectedPublisher.artifact, - ); const nativePlatforms = required( baseWorkflow.jobs?.[expectedPublisher.platformsJob], diff --git a/test/pr-merge-conflict-fixer-workflow-boundary.test.ts b/test/pr-merge-conflict-fixer-workflow-boundary.test.ts index a959552335c..4283fd284a4 100644 --- a/test/pr-merge-conflict-fixer-workflow-boundary.test.ts +++ b/test/pr-merge-conflict-fixer-workflow-boundary.test.ts @@ -62,10 +62,9 @@ describe("PR merge conflict fixer workflow boundary", () => { expect(scan.permissions).toEqual({ contents: "read", "pull-requests": "read" }); expect(resolve.permissions).toEqual({ contents: "read" }); expect(publish.permissions).toEqual({ contents: "write", "pull-requests": "read" }); + expect(scan["timeout-minutes"]).toBe(10); expect(resolve["timeout-minutes"]).toBe(30); - expect( - Object.values(jobs).filter((job) => record(job)["timeout-minutes"] !== undefined), - ).toHaveLength(1); + expect(publish["timeout-minutes"]).toBe(10); }); it("loads each resolve command from the pushed main SHA (#6952)", () => { diff --git a/test/sandbox-base-image-layout.test.ts b/test/sandbox-base-image-layout.test.ts deleted file mode 100644 index 69144b88621..00000000000 --- a/test/sandbox-base-image-layout.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -import fs from "node:fs"; -import path from "node:path"; - -import { describe, expect, it } from "vitest"; - -const repoRoot = path.resolve(import.meta.dirname, ".."); -const dockerfile = fs.readFileSync(path.join(repoRoot, "Dockerfile.base"), "utf8"); - -function completedStage(source: string): string { - const stages = source.split(/(?=^FROM )/gmu).filter((stage) => stage.startsWith("FROM ")); - return stages.at(-1) ?? ""; -} - -describe("sandbox base image layout", () => { - it("keeps the published image within the established layer budget", () => { - const finalStage = completedStage(dockerfile); - const layerInstructions = finalStage.match(/^(?:ADD|COPY|RUN)\b/gmu) ?? []; - - expect(finalStage).toContain("FROM node:22-trixie-slim@"); - expect(layerInstructions).toHaveLength(26); - }); -}); diff --git a/test/wechat-runtime-audit-workflow.test.ts b/test/wechat-runtime-audit-workflow.test.ts index 411cc4f5811..b05b0ca1229 100644 --- a/test/wechat-runtime-audit-workflow.test.ts +++ b/test/wechat-runtime-audit-workflow.test.ts @@ -194,17 +194,6 @@ describe("WeChat runtime audit and install-cache gates (#5896)", () => { ]) { expect(script).toContain(fragment); } - - const action = fs.readFileSync( - path.join(repoRoot, ".github", "actions", "ci-wechat-runtime-audit", "action.yaml"), - "utf8", - ); - expect(action).toContain('node-version: "22.19.0"'); - expect(action).toContain('cd "$RUNNER_TEMP"'); - expect(action).toContain("npm install --global npm@10.9.4"); - expect(action).toContain("--userconfig /dev/null"); - expect(action).toContain("--registry https://registry.npmjs.org/"); - expect(action).toContain('run: bash "$GITHUB_ACTION_PATH/audit.sh"'); }); it("rejects a target-controlled npm registry override", () => { diff --git a/tools/e2e/base-image-publication.mts b/tools/e2e/base-image-publication.mts index 0bda1c48742..7da5f0b8690 100644 --- a/tools/e2e/base-image-publication.mts +++ b/tools/e2e/base-image-publication.mts @@ -23,9 +23,14 @@ const MAX_RETRY_DELAY_MS = 10_000; const SHA_PATTERN = /^[0-9a-f]{40}$/u; const SAFE_PATH_PATTERN = /^[A-Za-z0-9._/-]+$/u; const REVIEWED_PATH_GLOBS = new Map([ + [".github/actions/ci-reviewed-npm-audit/**", /^[.]github\/actions\/ci-reviewed-npm-audit\/.+$/u], [ - ".github/actions/ci-reviewed-npm-audit/**", - /^[.]github\/actions\/ci-reviewed-npm-audit\/.+$/u, + ".github/actions/build-base-image-platform/**", + /^[.]github\/actions\/build-base-image-platform\/.+$/u, + ], + [ + ".github/actions/publish-base-image-manifest/**", + /^[.]github\/actions\/publish-base-image-manifest\/.+$/u, ], ["agents/**", /^agents\/.+$/u], ["nemoclaw/**", /^nemoclaw\/.+$/u], @@ -248,13 +253,11 @@ function defaultGit(args: string[]): string { }).trim(); } -export function expandBaseImagePushPaths( - expectedSha: string, - paths: readonly string[], -): string[] { +export function expandBaseImagePushPaths(expectedSha: string, paths: readonly string[]): string[] { sha(expectedSha, "expected SHA"); - return [...new Set(paths.map((path) => (REVIEWED_PATH_GLOBS.has(path) ? `:(glob)${path}` : path)))] - .sort(); + return [ + ...new Set(paths.map((path) => (REVIEWED_PATH_GLOBS.has(path) ? `:(glob)${path}` : path))), + ].sort(); } export function resolveFirstParentHistory( diff --git a/tools/e2e/cli-artifact-workflow-boundary.mts b/tools/e2e/cli-artifact-workflow-boundary.mts index f2e1aa07a11..f313f689526 100644 --- a/tools/e2e/cli-artifact-workflow-boundary.mts +++ b/tools/e2e/cli-artifact-workflow-boundary.mts @@ -13,13 +13,13 @@ import { PREPARE_E2E_NO_BUILD_JOBS, PREPARE_E2E_TRUSTED_BUILD_JOBS, } from "./prepare-e2e-workflow-boundary.mts"; +import { E2E_ACTION_PROVENANCE, E2E_JOB_CONTRACT } from "./workflow-contract-registry.mts"; export const CLI_ARTIFACT_DOWNLOAD_ACTION = "actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c"; export const CLI_ARTIFACT_UPLOAD_ACTION = "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a"; -export const CLI_ARTIFACT_RESTORE_ACTION = - "NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@c246409193a31133cab10c8a3589001cc0d59eb3"; +export const CLI_ARTIFACT_RESTORE_ACTION = E2E_ACTION_PROVENANCE.restoreCliArtifact.reference; export const CLI_ARTIFACT_PACKAGE_STEP = "Package exact-commit CLI"; export const CLI_ARTIFACT_PUBLISH_STEP = "Publish content-addressed CLI artifact"; export const CLI_ARTIFACT_RESTORE_STEP = "Restore exact-commit CLI artifact"; @@ -32,8 +32,7 @@ const DEFAULT_RESTORE_ACTION_PATH = join( "restore-e2e-cli-artifact", "action.yaml", ); -const RESTORE_ACTION_CONTENT_SHA256 = - "3a81ad631b839aa938eaaf1ad6777bab247204bf86fbca3c43c326a44dfb9c6c"; +const RESTORE_ACTION_CONTENT_SHA256 = E2E_ACTION_PROVENANCE.restoreCliArtifact.contentSha256; const CLI_ARTIFACT_DOWNLOAD_STEP = "Download exact-commit CLI artifact"; const CLI_ARTIFACT_VERIFY_STEP = "Verify and restore exact-commit CLI artifact"; const CLI_ARTIFACT_PROVENANCE_STEP = "Record CLI artifact provenance"; @@ -41,72 +40,7 @@ const CANDIDATE_CHECKOUT_STEP_CONTENT_SHA256 = "3578a053cede863f7aa4814d8399b4ca21ea0b77cee712e6d549c684818f11dd"; const CLI_ARTIFACT_WORKFLOW_CONTRACT_SHA256 = "14fb4de8dffd0cfd3f0dd3177f03e806c8912247bac4f5919a570c0f6b4c0ca2"; -const CLI_ARTIFACT_CONSUMER_JOB_NAMES = [ - "agent-turn-latency", - "bedrock-runtime-compatible-anthropic", - "brave-search", - "channels-add-remove", - "channels-stop-start", - "cloud-inference", - "cloud-onboard", - "common-egress-agent", - "concurrent-gateway-ports", - "cron-preflight-inference-local", - "dashboard-remote-bind", - "device-auth-health", - "double-onboard", - "full-e2e", - "gateway-guard-recovery", - "gpu-double-onboard", - "gpu-e2e", - "hermes-discord", - "hermes-e2e", - "hermes-gpu-startup", - "hermes-inference-switch", - "hermes-shields-config", - "hermes-slack", - "inference-routing", - "issue-2478-crash-loop-recovery", - "issue-4434-tui-unreachable-inference", - "issue-4462-scope-upgrade-approval", - "jetson-nvmap-gpu", - "kimi-inference-compat", - "live", - "llama-cpp-generic-gpu", - "mcp-bridge", - "mcp-bridge-dev", - "messaging-compatible-endpoint", - "messaging-providers", - "model-router-provider-routed-inference", - "network-policy", - "onboard-repair", - "onboard-resume", - "openclaw-discord-pairing", - "openclaw-inference-switch", - "openclaw-plugin-runtime-exdev", - "openclaw-plugin-runtime-exdev-release", - "openclaw-skill-cli", - "openclaw-slack-pairing", - "openclaw-tui-chat-correlation", - "openshell-credential-generation-window", - "openshell-gateway-auth-contract", - "openshell-gateway-upgrade", - "overlayfs-autofix", - "rebuild-hermes", - "rebuild-hermes-stale-base", - "rebuild-openclaw", - "retired-selector-compatibility", - "sandbox-operations", - "sandbox-survival", - "security-posture", - "sessions-agents-cli", - "shared-e2e", - "skill-agent", - "state-backup-restore", - "telegram-injection", - "token-rotation", - "tunnel-lifecycle", -] as const; +const CLI_ARTIFACT_CONSUMER_JOB_NAMES = E2E_JOB_CONTRACT.cliArtifactConsumers; type WorkflowRecord = Record; type WorkflowStep = WorkflowRecord & { diff --git a/tools/e2e/prepare-e2e-workflow-boundary.mts b/tools/e2e/prepare-e2e-workflow-boundary.mts index 77c535e8d1e..b4c80779d75 100644 --- a/tools/e2e/prepare-e2e-workflow-boundary.mts +++ b/tools/e2e/prepare-e2e-workflow-boundary.mts @@ -8,34 +8,24 @@ import { fileURLToPath } from "node:url"; import { isDeepStrictEqual } from "node:util"; import YAML from "yaml"; import { SHARED_E2E_JOB_ID } from "./credential-free-tests.mts"; +import { E2E_ACTION_PROVENANCE, E2E_JOB_CONTRACT } from "./workflow-contract-registry.mts"; const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", ".."); const DEFAULT_ACTION_PATH = join(REPO_ROOT, ".github", "actions", "prepare-e2e", "action.yaml"); -const PREPARE_E2E_ACTION_PROVENANCE = { - reference: "NVIDIA/NemoClaw/.github/actions/prepare-e2e@f6304bc25fc35bfaa441c8c2fbfee38f72805a75", - contentSha256: "1283c2eadfbc38ccb3b795684ba5ced9c89ae2040fffbb6b81854a9d1926802b", -} as const; +const PREPARE_E2E_ACTION_PROVENANCE = E2E_ACTION_PROVENANCE.prepareWorkspace; export const PREPARE_E2E_ACTION = PREPARE_E2E_ACTION_PROVENANCE.reference; export const PREPARE_E2E_STEP = "Prepare E2E workspace"; const CHECKOUT_LOCAL_PREPARE_E2E_ACTION = "./.github/actions/prepare-e2e"; -export const CLI_ARTIFACT_PRODUCER_JOB = "generate-matrix"; +export const CLI_ARTIFACT_PRODUCER_JOB = E2E_JOB_CONTRACT.cliArtifactProducer; const PREINSTALLED_E2E_JOBS = new Set(["staging-brev-launchable"]); const RETIRED_SELECTOR_COMPATIBILITY_JOB = "retired-selector-compatibility"; -export const PREPARE_E2E_NO_BUILD_JOBS = new Set([ - "bootstrap-install-smoke", - "llama-cpp-dgx-spark-qualification", - "managed-image-multiarch-startup", - "ollama-auth-proxy", - "shields-config", - "snapshot-commands", - "spark-install", -]); - -export const PREPARE_E2E_TRUSTED_BUILD_JOBS = new Set(["managed-image-protected-runtime"]); +export const PREPARE_E2E_NO_BUILD_JOBS = new Set(E2E_JOB_CONTRACT.prepareNoBuild); + +export const PREPARE_E2E_TRUSTED_BUILD_JOBS = new Set(E2E_JOB_CONTRACT.prepareTrustedBuild); type WorkflowRecord = Record; type WorkflowStep = WorkflowRecord & { diff --git a/tools/e2e/sandbox-images-workflow-boundary.mts b/tools/e2e/sandbox-images-workflow-boundary.mts index 4efbdc9ab84..e7aca8fd486 100644 --- a/tools/e2e/sandbox-images-workflow-boundary.mts +++ b/tools/e2e/sandbox-images-workflow-boundary.mts @@ -119,7 +119,6 @@ const GUARDED_PRODUCTION_BUILD_CONTRACTS: readonly GuardedProductionBuildContrac }, ]; - type WorkflowRecord = Record; export type SandboxImagesWorkflowStep = WorkflowRecord & { @@ -754,6 +753,7 @@ function validateRuntimeImageReuse(errors: string[], workflow: SandboxImagesWork name: "isolation-image", path: "/tmp/isolation-image.tar.gz", "retention-days": 1, + "if-no-files-found": "error", }) || stepIndex(producer, save.name ?? "") >= stepIndex(producer, isolationUpload.name ?? "") ) { @@ -985,6 +985,7 @@ function validateHermesImageReuse(errors: string[], workflow: SandboxImagesWorkf name: "hermes-isolation-image", path: "/tmp/hermes-isolation-image.tar.gz", "retention-days": 1, + "if-no-files-found": "error", }) || stepIndex(producer, save.name ?? "") >= stepIndex(producer, upload.name ?? "") || stepIndex(producer, upload.name ?? "") >= stepIndex(producer, CLEANUP_STEP_NAME) diff --git a/tools/e2e/upload-e2e-artifacts-workflow-boundary.mts b/tools/e2e/upload-e2e-artifacts-workflow-boundary.mts index 7532cfb953a..2cfed5b662e 100644 --- a/tools/e2e/upload-e2e-artifacts-workflow-boundary.mts +++ b/tools/e2e/upload-e2e-artifacts-workflow-boundary.mts @@ -12,6 +12,8 @@ import { CLI_ARTIFACT_UPLOAD_ACTION, } from "./cli-artifact-workflow-boundary.mts"; import { SHARED_E2E_JOB_ID } from "./credential-free-tests.mts"; +import { E2E_ACTION_PROVENANCE } from "./workflow-contract-registry.mts"; +export { E2E_ACTION_PROVENANCE }; const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", ".."); const DEFAULT_ACTION_PATH = join( @@ -22,11 +24,7 @@ const DEFAULT_ACTION_PATH = join( "action.yaml", ); -export const UPLOAD_E2E_ARTIFACTS_ACTION_PROVENANCE = { - reference: - "NVIDIA/NemoClaw/.github/actions/upload-e2e-artifacts@7768e15eb90d3ee2d33432f481dfe8747e4f6d57", - contentSha256: "8f6f71a0e6d71d85418fa88c2b26a4d601f568bdcaae20aca4085ae423c5044b", -} as const; +export const UPLOAD_E2E_ARTIFACTS_ACTION_PROVENANCE = E2E_ACTION_PROVENANCE.uploadArtifacts; export const UPLOAD_E2E_ARTIFACTS_ACTION = UPLOAD_E2E_ARTIFACTS_ACTION_PROVENANCE.reference; diff --git a/tools/e2e/workflow-boundary.mts b/tools/e2e/workflow-boundary.mts index 8d307700bdf..7fe2d5f3820 100644 --- a/tools/e2e/workflow-boundary.mts +++ b/tools/e2e/workflow-boundary.mts @@ -52,6 +52,7 @@ import { validateTrustedHermesSwapWorkflow, } from "./trusted-hermes-swap-workflow-boundary.mts"; import { + E2E_ACTION_PROVENANCE, UPLOAD_E2E_ARTIFACTS_ACTION, validateUploadE2eArtifactsWorkflowBoundary, } from "./upload-e2e-artifacts-workflow-boundary.mts"; @@ -189,25 +190,10 @@ const NO_IMAGE_E2E_JOBS = new Set(["staging-brev-launchable", 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_PROVENANCE = { - reference: - "NVIDIA/NemoClaw/.github/actions/docker-auth-setup@78091da47e290f49b8fe3f3e70b72362a0853928", - actionSha256: "cf93dcbd19589a56d1d58225fd6b3f8ad2180705662ff79a3407f340b5dba4c0", - scriptSha256: "853a3f742f057c29ed465b63bed1ec8d8f306a1c046877a8556cadf290ef0cb6", -} as const; -const DOCKER_HUB_CLEANUP_PROVENANCE = { - reference: - "NVIDIA/NemoClaw/.github/actions/docker-auth-cleanup@d5f37099766ca82a4516e7d8f0de117cda197fe3", - actionSha256: "8b7bf4bdb793ddd27aa9bab2e38157e91f0401148f6ba684acb516fc75e8d367", - scriptSha256: "4e5ce850c28f309b97695d61e11bcf1f154eae2b1d58c9697a3f49631c76abb4", -} as const; +const DOCKER_HUB_AUTH_PROVENANCE = E2E_ACTION_PROVENANCE.dockerAuth; +const DOCKER_HUB_CLEANUP_PROVENANCE = E2E_ACTION_PROVENANCE.dockerCleanup; const DOCKER_HUB_AUTH_USES = DOCKER_HUB_AUTH_PROVENANCE.reference; -const HOST_DEPENDENCY_ACTION_PROVENANCE = { - reference: - "NVIDIA/NemoClaw/.github/actions/host-dependency-setup@4def1501b34ce586f83b91af50a66b5d22b31d75", - actionSha256: "1ac05a0e0a0159fa0850eb82fccb0704d0e49b15bc6f2d6e3b6bb04c7ab94923", - scriptSha256: "2e910ed80b5dcf9aaf94230371fe586376c46f6df8fcbd76229063cbda1852c8", -} as const; +const HOST_DEPENDENCY_ACTION_PROVENANCE = E2E_ACTION_PROVENANCE.hostDependencies; const HOST_DEPENDENCY_ACTION_USES = HOST_DEPENDENCY_ACTION_PROVENANCE.reference; const DOCKER_HUB_CLEANUP_KEYS = ["if", "name", "run", "shell"]; // The general E2E workflow runs on push/manual dispatch. Its event set is @@ -2797,7 +2783,6 @@ function validateHermesE2EJob(errors: string[], jobs: WorkflowRecord): void { if (asRecord(checkout?.with)["persist-credentials"] !== false) { errors.push("hermes-e2e checkout step must set persist-credentials=false"); } - const runVitest = requireJobStep(errors, jobName, steps, "Run Hermes live Vitest test"); const runVitestEnv = asRecord(runVitest?.env); if (runVitestEnv.NVIDIA_INFERENCE_API_KEY !== GUARDED_HERMES_E2E_INFERENCE_KEY) { diff --git a/tools/e2e/workflow-contract-registry.mts b/tools/e2e/workflow-contract-registry.mts new file mode 100644 index 00000000000..8bb66f5d713 --- /dev/null +++ b/tools/e2e/workflow-contract-registry.mts @@ -0,0 +1,118 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +export const E2E_ACTION_PROVENANCE = { + prepareWorkspace: { + reference: + "NVIDIA/NemoClaw/.github/actions/prepare-e2e@f6304bc25fc35bfaa441c8c2fbfee38f72805a75", + contentSha256: "1283c2eadfbc38ccb3b795684ba5ced9c89ae2040fffbb6b81854a9d1926802b", + }, + restoreCliArtifact: { + reference: + "NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@c246409193a31133cab10c8a3589001cc0d59eb3", + contentSha256: "3a81ad631b839aa938eaaf1ad6777bab247204bf86fbca3c43c326a44dfb9c6c", + }, + uploadArtifacts: { + reference: + "NVIDIA/NemoClaw/.github/actions/upload-e2e-artifacts@7768e15eb90d3ee2d33432f481dfe8747e4f6d57", + contentSha256: "8f6f71a0e6d71d85418fa88c2b26a4d601f568bdcaae20aca4085ae423c5044b", + }, + dockerAuth: { + reference: + "NVIDIA/NemoClaw/.github/actions/docker-auth-setup@78091da47e290f49b8fe3f3e70b72362a0853928", + actionSha256: "cf93dcbd19589a56d1d58225fd6b3f8ad2180705662ff79a3407f340b5dba4c0", + scriptSha256: "853a3f742f057c29ed465b63bed1ec8d8f306a1c046877a8556cadf290ef0cb6", + }, + dockerCleanup: { + reference: + "NVIDIA/NemoClaw/.github/actions/docker-auth-cleanup@d5f37099766ca82a4516e7d8f0de117cda197fe3", + actionSha256: "8b7bf4bdb793ddd27aa9bab2e38157e91f0401148f6ba684acb516fc75e8d367", + scriptSha256: "4e5ce850c28f309b97695d61e11bcf1f154eae2b1d58c9697a3f49631c76abb4", + }, + hostDependencies: { + reference: + "NVIDIA/NemoClaw/.github/actions/host-dependency-setup@4def1501b34ce586f83b91af50a66b5d22b31d75", + actionSha256: "1ac05a0e0a0159fa0850eb82fccb0704d0e49b15bc6f2d6e3b6bb04c7ab94923", + scriptSha256: "2e910ed80b5dcf9aaf94230371fe586376c46f6df8fcbd76229063cbda1852c8", + }, +} as const; + +export const E2E_JOB_CONTRACT = { + cliArtifactProducer: "generate-matrix", + prepareNoBuild: [ + "bootstrap-install-smoke", + "llama-cpp-dgx-spark-qualification", + "managed-image-multiarch-startup", + "ollama-auth-proxy", + "shields-config", + "snapshot-commands", + "spark-install", + ], + prepareTrustedBuild: ["managed-image-protected-runtime"], + cliArtifactConsumers: [ + "agent-turn-latency", + "bedrock-runtime-compatible-anthropic", + "brave-search", + "channels-add-remove", + "channels-stop-start", + "cloud-inference", + "cloud-onboard", + "common-egress-agent", + "concurrent-gateway-ports", + "cron-preflight-inference-local", + "dashboard-remote-bind", + "device-auth-health", + "double-onboard", + "full-e2e", + "gateway-guard-recovery", + "gpu-double-onboard", + "gpu-e2e", + "hermes-discord", + "hermes-e2e", + "hermes-gpu-startup", + "hermes-inference-switch", + "hermes-shields-config", + "hermes-slack", + "inference-routing", + "issue-2478-crash-loop-recovery", + "issue-4434-tui-unreachable-inference", + "issue-4462-scope-upgrade-approval", + "jetson-nvmap-gpu", + "kimi-inference-compat", + "live", + "llama-cpp-generic-gpu", + "mcp-bridge", + "mcp-bridge-dev", + "messaging-compatible-endpoint", + "messaging-providers", + "model-router-provider-routed-inference", + "network-policy", + "onboard-repair", + "onboard-resume", + "openclaw-discord-pairing", + "openclaw-inference-switch", + "openclaw-plugin-runtime-exdev", + "openclaw-plugin-runtime-exdev-release", + "openclaw-skill-cli", + "openclaw-slack-pairing", + "openclaw-tui-chat-correlation", + "openshell-credential-generation-window", + "openshell-gateway-auth-contract", + "openshell-gateway-upgrade", + "overlayfs-autofix", + "rebuild-hermes", + "rebuild-hermes-stale-base", + "rebuild-openclaw", + "retired-selector-compatibility", + "sandbox-operations", + "sandbox-survival", + "security-posture", + "sessions-agents-cli", + "shared-e2e", + "skill-agent", + "state-backup-restore", + "telegram-injection", + "token-rotation", + "tunnel-lifecycle", + ], +} as const; From 83315d412f602b98b94d5484eb5e16795cacab8c Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Sat, 8 Aug 2026 15:29:31 -0700 Subject: [PATCH 04/13] test(workflows): cover regression selector --- .github/scripts/select-regression-e2e-jobs.sh | 16 +++++++ .github/workflows/regression-e2e.yaml | 19 ++++---- test/regression-e2e-selector.test.ts | 45 +++++++++++++++++++ 3 files changed, 69 insertions(+), 11 deletions(-) create mode 100755 .github/scripts/select-regression-e2e-jobs.sh create mode 100644 test/regression-e2e-selector.test.ts diff --git a/.github/scripts/select-regression-e2e-jobs.sh b/.github/scripts/select-regression-e2e-jobs.sh new file mode 100755 index 00000000000..d134a7f2992 --- /dev/null +++ b/.github/scripts/select-regression-e2e-jobs.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +normalized="$(printf '%s' "${JOBS:-}" | tr -d '[:space:]')" +case ",${normalized}," in + ,, | *,whatsapp-qr-compact-e2e,*) + selected=true + ;; + *) + selected=false + ;; +esac +printf 'whatsapp_qr_compact=%s\n' "$selected" >>"${GITHUB_OUTPUT:?}" diff --git a/.github/workflows/regression-e2e.yaml b/.github/workflows/regression-e2e.yaml index a5b51fee399..6834f9fed69 100644 --- a/.github/workflows/regression-e2e.yaml +++ b/.github/workflows/regression-e2e.yaml @@ -43,20 +43,17 @@ jobs: outputs: whatsapp_qr_compact: ${{ steps.select.outputs.whatsapp_qr_compact }} steps: + - name: Checkout trusted selector + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - id: select + name: Select regression jobs env: JOBS: ${{ inputs.jobs }} - run: | - set -euo pipefail - normalized="$(printf '%s' "$JOBS" | tr -d '[:space:]')" - case ",${normalized}," in - ,,|*,whatsapp-qr-compact-e2e,*) - echo "whatsapp_qr_compact=true" >> "$GITHUB_OUTPUT" - ;; - *) - echo "whatsapp_qr_compact=false" >> "$GITHUB_OUTPUT" - ;; - esac + shell: bash + run: bash .github/scripts/select-regression-e2e-jobs.sh # ── WhatsApp compact-QR reporter-workflow E2E ────────────────── # Coverage guard for #4522. Drives the real @openclaw/whatsapp + diff --git a/test/regression-e2e-selector.test.ts b/test/regression-e2e-selector.test.ts new file mode 100644 index 00000000000..1b2e902c58e --- /dev/null +++ b/test/regression-e2e-selector.test.ts @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { spawnSync } from "node:child_process"; +import { mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { afterEach, describe, expect, it } from "vitest"; + +const temporaryRoots: string[] = []; + +afterEach(() => { + for (const root of temporaryRoots.splice(0)) rmSync(root, { force: true, recursive: true }); +}); + +function select(jobs: string): string { + const root = mkdtempSync(join(tmpdir(), "nemoclaw-regression-selector-")); + temporaryRoots.push(root); + const output = join(root, "github-output"); + const result = spawnSync("bash", [".github/scripts/select-regression-e2e-jobs.sh"], { + cwd: join(import.meta.dirname, ".."), + encoding: "utf8", + env: { ...process.env, GITHUB_OUTPUT: output, JOBS: jobs }, + }); + expect(result.status, result.stderr).toBe(0); + return readFileSync(output, "utf8").trim(); +} + +describe("regression E2E selector", () => { + it.each([ + "", + " whatsapp-qr-compact-e2e ", + ])("selects the WhatsApp regression for accepted jobs input %j", (jobs) => { + expect(select(jobs)).toBe("whatsapp_qr_compact=true"); + }); + + it.each([ + "unknown", + "whatsapp-qr-compact-e2", + "whatsapp-qr-compact-e2e-extra", + ])("rejects unsupported jobs input %j", (jobs) => { + expect(select(jobs)).toBe("whatsapp_qr_compact=false"); + }); +}); From 0f767ef8eb916e784bbbb7bd6a5981fc060548ba Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Sat, 8 Aug 2026 15:35:39 -0700 Subject: [PATCH 05/13] docs(ci): clarify selector and npm verification --- .github/workflows/regression-e2e.yaml | 2 +- agents/openclaw/dependency-review.md | 2 +- test/regression-e2e-selector.test.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/regression-e2e.yaml b/.github/workflows/regression-e2e.yaml index 6834f9fed69..a6b9673eddf 100644 --- a/.github/workflows/regression-e2e.yaml +++ b/.github/workflows/regression-e2e.yaml @@ -43,7 +43,7 @@ jobs: outputs: whatsapp_qr_compact: ${{ steps.select.outputs.whatsapp_qr_compact }} steps: - - name: Checkout trusted selector + - name: Checkout regression selector uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false diff --git a/agents/openclaw/dependency-review.md b/agents/openclaw/dependency-review.md index 9cf0edef01e..2078d029d9d 100644 --- a/agents/openclaw/dependency-review.md +++ b/agents/openclaw/dependency-review.md @@ -36,7 +36,7 @@ The reviewed audit wrapper reports lower-severity production findings and blocks Because PR #6739's base SHA predates the action, only that PR can use the pinned bootstrap action from signed immutable commit `HOYALIM/NemoClaw@0d2256d71d5bbba3bcaaaa4d01714fa56f22d1e2`. Other PRs fail closed if their base lacks the action. The `main.yaml` workflow uses the merged action. - The action uses Node `22.19.0` and npm `10.9.4`. + The action uses Node `22.19.0`. It downloads `npm@10.9.4`, verifies the committed SRI value, and installs the archive offline with lifecycle scripts disabled. It materializes the committed graph with scripts disabled. The action rejects any low-or-higher production advisory and verifies registry signatures. The PR and main workflows upload the resulting reports. diff --git a/test/regression-e2e-selector.test.ts b/test/regression-e2e-selector.test.ts index 1b2e902c58e..7aa636eadab 100644 --- a/test/regression-e2e-selector.test.ts +++ b/test/regression-e2e-selector.test.ts @@ -39,7 +39,7 @@ describe("regression E2E selector", () => { "unknown", "whatsapp-qr-compact-e2", "whatsapp-qr-compact-e2e-extra", - ])("rejects unsupported jobs input %j", (jobs) => { + ])("does not select the WhatsApp regression for unsupported jobs input %j", (jobs) => { expect(select(jobs)).toBe("whatsapp_qr_compact=false"); }); }); From 14c96e512e649a457e807327dea7663c9073ced5 Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Sat, 8 Aug 2026 15:40:23 -0700 Subject: [PATCH 06/13] docs(ci): clarify dependency installation --- agents/openclaw/dependency-review.md | 3 ++- test/ci-install-dependencies.test.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/agents/openclaw/dependency-review.md b/agents/openclaw/dependency-review.md index 2078d029d9d..0fc8f164668 100644 --- a/agents/openclaw/dependency-review.md +++ b/agents/openclaw/dependency-review.md @@ -36,7 +36,8 @@ The reviewed audit wrapper reports lower-severity production findings and blocks Because PR #6739's base SHA predates the action, only that PR can use the pinned bootstrap action from signed immutable commit `HOYALIM/NemoClaw@0d2256d71d5bbba3bcaaaa4d01714fa56f22d1e2`. Other PRs fail closed if their base lacks the action. The `main.yaml` workflow uses the merged action. - The action uses Node `22.19.0`. It downloads `npm@10.9.4`, verifies the committed SRI value, and installs the archive offline with lifecycle scripts disabled. + The action uses Node.js `22.19.0`. + It downloads `npm@10.9.4`, verifies the SRI value committed in the action, and installs the archive offline with lifecycle scripts disabled. It materializes the committed graph with scripts disabled. The action rejects any low-or-higher production advisory and verifies registry signatures. The PR and main workflows upload the resulting reports. diff --git a/test/ci-install-dependencies.test.ts b/test/ci-install-dependencies.test.ts index 3d5a204f781..edb82f72c0f 100644 --- a/test/ci-install-dependencies.test.ts +++ b/test/ci-install-dependencies.test.ts @@ -15,7 +15,7 @@ afterEach(() => { }); describe("shared CI dependency installer", () => { - it("installs root and plugin lockfiles without lifecycle scripts", () => { + it("installs root and plugin dependencies from lockfiles without lifecycle scripts", () => { const root = mkdtempSync(join(tmpdir(), "nemoclaw-ci-install-")); temporaryRoots.push(root); const bin = join(root, "bin"); From 2eaff7511e6fc6880b54daa9100e539371c9b58a Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Sat, 8 Aug 2026 16:30:19 -0700 Subject: [PATCH 07/13] ci(e2e): consolidate WhatsApp regression coverage --- .../publish-base-image-manifest/action.yaml | 113 +----------- .../publish-base-image-manifest/publish.sh | 123 +++++++++++++ .github/scripts/select-regression-e2e-jobs.sh | 16 -- .github/workflows/base-image.yaml | 2 - .github/workflows/e2e.yaml | 40 +++++ .github/workflows/regression-e2e.yaml | 92 ---------- test/ci-install-dependencies.test.ts | 2 +- .../cli-artifact-workflow-boundary.test.ts | 14 ++ test/openclaw-dependency-review.test.ts | 10 +- test/publish-base-image-manifest.test.ts | 161 ++++++++++++++++++ test/regression-e2e-selector.test.ts | 45 ----- test/vitest-watch-triggers.test.ts | 1 - tools/e2e/workflow-contract-registry.mts | 1 + 13 files changed, 348 insertions(+), 272 deletions(-) create mode 100755 .github/actions/publish-base-image-manifest/publish.sh delete mode 100755 .github/scripts/select-regression-e2e-jobs.sh delete mode 100644 .github/workflows/regression-e2e.yaml create mode 100644 test/publish-base-image-manifest.test.ts delete mode 100644 test/regression-e2e-selector.test.ts diff --git a/.github/actions/publish-base-image-manifest/action.yaml b/.github/actions/publish-base-image-manifest/action.yaml index a8eaa8eb62c..1c65ad725b9 100644 --- a/.github/actions/publish-base-image-manifest/action.yaml +++ b/.github/actions/publish-base-image-manifest/action.yaml @@ -64,118 +64,7 @@ runs: DISPLAY_NAME: ${{ inputs.display-name }} IMAGE: ${{ inputs.registry }}/${{ inputs.image }} TAGS: ${{ steps.meta.outputs.tags }} - run: | - set -euo pipefail - shopt -s nullglob - digest_files=("$RUNNER_TEMP"/digests/*) - if [ "${#digest_files[@]}" -ne 2 ]; then - echo "ERROR: expected exactly two platform digests, found ${#digest_files[@]}." >&2 - exit 1 - fi - - declare -A seen_arches=() - declare -A source_digests=() - sources=() - for digest_file in "${digest_files[@]}"; do - digest_artifact="$(basename "$digest_file")" - if [[ ! "$digest_artifact" =~ ^(amd64|arm64)-([0-9a-f]{64})$ ]]; then - echo "ERROR: invalid platform digest artifact: $digest_artifact" >&2 - exit 1 - fi - expected_arch="${BASH_REMATCH[1]}" - digest="${BASH_REMATCH[2]}" - if [ -n "${seen_arches[$expected_arch]:-}" ]; then - echo "ERROR: duplicate platform digest for linux/$expected_arch." >&2 - exit 1 - fi - source="$IMAGE@sha256:$digest" - source_platform="$( - scripts/checks/retry-docker-imagetools-inspect.sh "$source" \ - --format '{{.Image.OS}}/{{.Image.Architecture}}' - )" - if [ "$source_platform" != "linux/$expected_arch" ]; then - echo "ERROR: digest for linux/$expected_arch resolves to $source_platform." >&2 - exit 1 - fi - seen_arches["$expected_arch"]=1 - source_digests["linux/$expected_arch"]="sha256:$digest" - sources+=("$source") - done - if [ "${seen_arches[amd64]:-0}" -ne 1 ] || [ "${seen_arches[arm64]:-0}" -ne 1 ]; then - echo "ERROR: expected one validated digest for linux/amd64 and linux/arm64." >&2 - exit 1 - fi - - mapfile -t tags <<< "$TAGS" - tag_args=() - for tag in "${tags[@]}"; do - if [ -n "$tag" ]; then - tag_args+=(--tag "$tag") - fi - done - if [ "${#tag_args[@]}" -eq 0 ]; then - echo "ERROR: metadata did not produce any publication tags." >&2 - exit 1 - fi - - candidate_tag="$IMAGE:base-candidate-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" - candidate_metadata="$RUNNER_TEMP/$AGENT-base-candidate-metadata.json" - docker buildx imagetools create \ - --tag "$candidate_tag" \ - --metadata-file "$candidate_metadata" \ - "${sources[@]}" - digest="$(jq -er '.["containerimage.descriptor"].digest' "$candidate_metadata")" - if [[ ! "$digest" =~ ^sha256:[0-9a-f]{64}$ ]]; then - echo "ERROR: expected one staged $DISPLAY_NAME base digest." >&2 - exit 1 - fi - reference="$IMAGE@$digest" - actual_platforms="$( - scripts/checks/retry-docker-imagetools-inspect.sh "$reference" --raw \ - | jq -r '.manifests[] | select(.platform.os == "linux") | .platform.architecture' \ - | sort -u \ - | paste -sd, - - )" - if [ "$actual_platforms" != "amd64,arm64" ]; then - echo "ERROR: staged manifest has unexpected platforms: $actual_platforms" >&2 - exit 1 - fi - platform_digests_json="$( - scripts/checks/validate-managed-base-index.sh \ - "$reference" \ - "${source_digests[linux/amd64]}" \ - "${source_digests[linux/arm64]}" - )" - declare -A platform_digests=() - for platform in linux/amd64 linux/arm64; do - platform_digests["$platform"]="$( - jq -er --arg platform "$platform" '.[$platform]' <<< "$platform_digests_json" - )" - done - - scripts/export-managed-base-image-contract.sh \ - "$AGENT" \ - "$IMAGE" \ - "$digest" \ - "${platform_digests[linux/amd64]}" \ - "${platform_digests[linux/arm64]}" \ - "$GITHUB_SHA" \ - "$GITHUB_RUN_ID" \ - "$GITHUB_RUN_ATTEMPT" \ - "$RUNNER_TEMP/managed-base-contract/contract.json" - - publication_metadata="$RUNNER_TEMP/$AGENT-base-publication-metadata.json" - docker buildx imagetools create \ - "${tag_args[@]}" \ - --metadata-file "$publication_metadata" \ - "$reference" - published_digest="$(jq -er '.["containerimage.descriptor"].digest' "$publication_metadata")" - if [ "$published_digest" != "$digest" ]; then - echo "ERROR: published $DISPLAY_NAME base digest differs from the validated candidate." >&2 - exit 1 - fi - printf 'digest=%s\n' "$digest" >> "$GITHUB_OUTPUT" - + run: bash "$GITHUB_ACTION_PATH/publish.sh" - name: Upload managed base image contract uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: diff --git a/.github/actions/publish-base-image-manifest/publish.sh b/.github/actions/publish-base-image-manifest/publish.sh new file mode 100755 index 00000000000..07c6ed0ca80 --- /dev/null +++ b/.github/actions/publish-base-image-manifest/publish.sh @@ -0,0 +1,123 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail +: "${AGENT:?}" +: "${DISPLAY_NAME:?}" +: "${IMAGE:?}" +: "${TAGS:?}" +: "${RUNNER_TEMP:?}" +: "${GITHUB_RUN_ID:?}" +: "${GITHUB_RUN_ATTEMPT:?}" +: "${GITHUB_SHA:?}" +: "${GITHUB_OUTPUT:?}" +shopt -s nullglob +digest_files=("$RUNNER_TEMP"/digests/*) +if [ "${#digest_files[@]}" -ne 2 ]; then + echo "ERROR: expected exactly two platform digests, found ${#digest_files[@]}." >&2 + exit 1 +fi + +declare -A seen_arches=() +declare -A source_digests=() +sources=() +for digest_file in "${digest_files[@]}"; do + digest_artifact="$(basename "$digest_file")" + if [[ ! "$digest_artifact" =~ ^(amd64|arm64)-([0-9a-f]{64})$ ]]; then + echo "ERROR: invalid platform digest artifact: $digest_artifact" >&2 + exit 1 + fi + expected_arch="${BASH_REMATCH[1]}" + digest="${BASH_REMATCH[2]}" + if [ -n "${seen_arches[$expected_arch]:-}" ]; then + echo "ERROR: duplicate platform digest for linux/$expected_arch." >&2 + exit 1 + fi + source="$IMAGE@sha256:$digest" + source_platform="$( + scripts/checks/retry-docker-imagetools-inspect.sh "$source" \ + --format '{{.Image.OS}}/{{.Image.Architecture}}' + )" + if [ "$source_platform" != "linux/$expected_arch" ]; then + echo "ERROR: digest for linux/$expected_arch resolves to $source_platform." >&2 + exit 1 + fi + seen_arches["$expected_arch"]=1 + source_digests["linux/$expected_arch"]="sha256:$digest" + sources+=("$source") +done +if [ "${seen_arches[amd64]:-0}" -ne 1 ] || [ "${seen_arches[arm64]:-0}" -ne 1 ]; then + echo "ERROR: expected one validated digest for linux/amd64 and linux/arm64." >&2 + exit 1 +fi + +mapfile -t tags <<<"$TAGS" +tag_args=() +for tag in "${tags[@]}"; do + if [ -n "$tag" ]; then + tag_args+=(--tag "$tag") + fi +done +if [ "${#tag_args[@]}" -eq 0 ]; then + echo "ERROR: metadata did not produce any publication tags." >&2 + exit 1 +fi + +candidate_tag="$IMAGE:base-candidate-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" +candidate_metadata="$RUNNER_TEMP/$AGENT-base-candidate-metadata.json" +docker buildx imagetools create \ + --tag "$candidate_tag" \ + --metadata-file "$candidate_metadata" \ + "${sources[@]}" +digest="$(jq -er '.["containerimage.descriptor"].digest' "$candidate_metadata")" +if [[ ! "$digest" =~ ^sha256:[0-9a-f]{64}$ ]]; then + echo "ERROR: expected one staged $DISPLAY_NAME base digest." >&2 + exit 1 +fi +reference="$IMAGE@$digest" +actual_platforms="$( + scripts/checks/retry-docker-imagetools-inspect.sh "$reference" --raw \ + | jq -r '.manifests[] | select(.platform.os == "linux") | .platform.architecture' \ + | sort -u \ + | paste -sd, - +)" +if [ "$actual_platforms" != "amd64,arm64" ]; then + echo "ERROR: staged manifest has unexpected platforms: $actual_platforms" >&2 + exit 1 +fi +platform_digests_json="$( + scripts/checks/validate-managed-base-index.sh \ + "$reference" \ + "${source_digests['linux/amd64']}" \ + "${source_digests['linux/arm64']}" +)" +declare -A platform_digests=() +for platform in linux/amd64 linux/arm64; do + platform_digests["$platform"]="$( + jq -er --arg platform "$platform" '.[$platform]' <<<"$platform_digests_json" + )" +done + +scripts/export-managed-base-image-contract.sh \ + "$AGENT" \ + "$IMAGE" \ + "$digest" \ + "${platform_digests['linux/amd64']}" \ + "${platform_digests['linux/arm64']}" \ + "$GITHUB_SHA" \ + "$GITHUB_RUN_ID" \ + "$GITHUB_RUN_ATTEMPT" \ + "$RUNNER_TEMP/managed-base-contract/contract.json" + +publication_metadata="$RUNNER_TEMP/$AGENT-base-publication-metadata.json" +docker buildx imagetools create \ + "${tag_args[@]}" \ + --metadata-file "$publication_metadata" \ + "$reference" +published_digest="$(jq -er '.["containerimage.descriptor"].digest' "$publication_metadata")" +if [ "$published_digest" != "$digest" ]; then + echo "ERROR: published $DISPLAY_NAME base digest differs from the validated candidate." >&2 + exit 1 +fi +printf 'digest=%s\n' "$digest" >>"$GITHUB_OUTPUT" diff --git a/.github/scripts/select-regression-e2e-jobs.sh b/.github/scripts/select-regression-e2e-jobs.sh deleted file mode 100755 index d134a7f2992..00000000000 --- a/.github/scripts/select-regression-e2e-jobs.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -set -euo pipefail - -normalized="$(printf '%s' "${JOBS:-}" | tr -d '[:space:]')" -case ",${normalized}," in - ,, | *,whatsapp-qr-compact-e2e,*) - selected=true - ;; - *) - selected=false - ;; -esac -printf 'whatsapp_qr_compact=%s\n' "$selected" >>"${GITHUB_OUTPUT:?}" diff --git a/.github/workflows/base-image.yaml b/.github/workflows/base-image.yaml index 5323e063d81..67932f1c7ba 100644 --- a/.github/workflows/base-image.yaml +++ b/.github/workflows/base-image.yaml @@ -204,7 +204,6 @@ jobs: registry: ${{ env.REGISTRY }} registry-username: ${{ github.actor }} registry-password: ${{ secrets.GITHUB_TOKEN }} - openclaw-version: ${{ inputs.openclaw_version }} build-dcode-platforms: @@ -249,7 +248,6 @@ jobs: registry: ${{ env.REGISTRY }} registry-username: ${{ github.actor }} registry-password: ${{ secrets.GITHUB_TOKEN }} - openclaw-version: ${{ inputs.openclaw_version }} build-and-push-hermes: diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 201a0813b0c..db020f756f9 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -7252,6 +7252,45 @@ jobs: shell: bash run: bash .github/scripts/docker-auth-cleanup.sh + whatsapp-qr-compact: + needs: generate-matrix + if: ${{ (github.event_name != 'workflow_dispatch' || (inputs.jobs == '' && inputs.targets == '')) || contains(format(',{0},', inputs.jobs), ',whatsapp-qr-compact,') || contains(format(',{0},', inputs.targets), ',whatsapp-qr-compact,') }} + runs-on: ubuntu-latest + timeout-minutes: 15 + env: + E2E_JOB: "1" + E2E_TARGET_ID: "whatsapp-qr-compact" + E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts/live/whatsapp-qr-compact + NEMOCLAW_RUN_LIVE_E2E: "1" + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: ${{ inputs.checkout_repository || github.repository }} + ref: ${{ inputs.checkout_sha || github.sha }} + persist-credentials: false + + - *dockerhub-auth + + - name: Prepare E2E workspace + uses: NVIDIA/NemoClaw/.github/actions/prepare-e2e@f6304bc25fc35bfaa441c8c2fbfee38f72805a75 + with: + build-cli: "false" + + - name: Run WhatsApp compact QR live test + run: |- + set -euo pipefail + npx tsx tools/e2e/live-vitest-invocation.mts run --test-path test/e2e/live/whatsapp-qr-compact.test.ts + + - name: Upload WhatsApp compact QR artifacts + if: always() + uses: NVIDIA/NemoClaw/.github/actions/upload-e2e-artifacts@7768e15eb90d3ee2d33432f481dfe8747e4f6d57 + + - name: Clean up Docker auth + if: always() + shell: bash + run: bash .github/scripts/docker-auth-cleanup.sh + + spark-install: needs: generate-matrix if: ${{ (github.event_name != 'workflow_dispatch' || (inputs.jobs == '' && inputs.targets == '')) || contains(format(',{0},', inputs.jobs), ',spark-install,') || contains(format(',{0},', inputs.targets), ',spark-install,') }} @@ -7392,6 +7431,7 @@ jobs: openclaw-discord-pairing, openclaw-slack-pairing, channels-stop-start, + whatsapp-qr-compact, spark-install, ] if: ${{ always() && github.event_name == 'workflow_dispatch' && inputs.checkout_sha == '' }} diff --git a/.github/workflows/regression-e2e.yaml b/.github/workflows/regression-e2e.yaml deleted file mode 100644 index a6b9673eddf..00000000000 --- a/.github/workflows/regression-e2e.yaml +++ /dev/null @@ -1,92 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -name: E2E / Regression Runner - -# Regression E2E holding pen. -# -# Jobs here are intentionally not part of the main-push E2E workflow. They are -# failing-test-first coverage guards or high-signal regressions that should be -# easy to dispatch while the owning fix is in flight. Periodically review this -# workflow and promote stable/high-value jobs into .github/workflows/e2e.yaml. - -on: - workflow_dispatch: - inputs: - pr_number: - description: "PR number (optional; creates a check run on that PR)" - required: false - type: string - default: "" - jobs: - description: >- - Comma-separated regression job names to run (empty = all). - Valid: whatsapp-qr-compact-e2e - required: false - type: string - default: "" - -permissions: - actions: read - contents: read - checks: write - pull-requests: write - -concurrency: - group: regression-e2e-${{ github.event_name }}-${{ github.ref }}-${{ inputs.jobs || 'all' }}-${{ inputs.pr_number || github.run_id }} - cancel-in-progress: true - -jobs: - select_regression_jobs: - runs-on: ubuntu-latest - timeout-minutes: 5 - outputs: - whatsapp_qr_compact: ${{ steps.select.outputs.whatsapp_qr_compact }} - steps: - - name: Checkout regression selector - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - id: select - name: Select regression jobs - env: - JOBS: ${{ inputs.jobs }} - shell: bash - run: bash .github/scripts/select-regression-e2e-jobs.sh - - # ── WhatsApp compact-QR reporter-workflow E2E ────────────────── - # Coverage guard for #4522. Drives the real @openclaw/whatsapp + - # openclaw renderQrTerminal path (the symbol the in-sandbox - # `openclaw channels login --channel whatsapp` onQr callback invokes) - # at the version bundled in Dockerfile.base, and asserts the pairing QR - # renders compact with the NemoClaw preload and oversized without it. - # Hermetic: only needs node + npm (no Docker, GPU, or NVIDIA_INFERENCE_API_KEY). - whatsapp-qr-compact-e2e: - needs: select_regression_jobs - if: >- - github.repository == 'NVIDIA/NemoClaw' && - needs.select_regression_jobs.outputs.whatsapp_qr_compact == 'true' - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Setup Node - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: "22" - - - name: Install root dependencies - run: npm ci --ignore-scripts - - - name: Run WhatsApp compact-QR reporter-workflow Vitest test - env: - NEMOCLAW_RUN_LIVE_E2E: "1" - run: | - npx vitest run --project e2e-live \ - test/e2e/live/whatsapp-qr-compact.test.ts \ - --silent=false --reporter=default diff --git a/test/ci-install-dependencies.test.ts b/test/ci-install-dependencies.test.ts index edb82f72c0f..7c831dbc1d6 100644 --- a/test/ci-install-dependencies.test.ts +++ b/test/ci-install-dependencies.test.ts @@ -28,7 +28,7 @@ describe("shared CI dependency installer", () => { const result = spawnSync("bash", [".github/actions/ci-install-dependencies.sh"], { cwd: join(import.meta.dirname, ".."), encoding: "utf8", - env: { ...process.env, NPM_TRACE: trace, PATH: `${bin}:${process.env.PATH}` }, + env: { ...process.env, NPM_TRACE: trace, PATH: `${bin}:${process.env.PATH || ""}` }, }); expect(result.status, result.stderr).toBe(0); diff --git a/test/e2e/support/cli-artifact-workflow-boundary.test.ts b/test/e2e/support/cli-artifact-workflow-boundary.test.ts index 82d4836e27c..aa7074ecdd7 100644 --- a/test/e2e/support/cli-artifact-workflow-boundary.test.ts +++ b/test/e2e/support/cli-artifact-workflow-boundary.test.ts @@ -14,6 +14,7 @@ import { validateCliArtifactRestoreAction, validateCliArtifactWorkflowBoundary, } from "../../../tools/e2e/cli-artifact-workflow-boundary.mts"; +import { E2E_JOB_CONTRACT } from "../../../tools/e2e/workflow-contract-registry.mts"; import { type CompositeAction, readRepoText, @@ -938,4 +939,17 @@ describe("exact-commit CLI artifact workflow boundary", () => { "security-posture must verify and restore the exact CLI artifact exactly once", ); }); + + it("rejects CLI artifact consumer registry drift", () => { + const consumers = E2E_JOB_CONTRACT.cliArtifactConsumers as unknown as string[]; + const removed = consumers.pop(); + expect(removed).toBeDefined(); + try { + expect(validateCliArtifactWorkflowBoundary(workflowFixture())).toContain( + "CLI artifact consumer job names must match the required list", + ); + } finally { + consumers.push(removed!); + } + }); }); diff --git a/test/openclaw-dependency-review.test.ts b/test/openclaw-dependency-review.test.ts index f7e0bfd3e39..960b18b7825 100644 --- a/test/openclaw-dependency-review.test.ts +++ b/test/openclaw-dependency-review.test.ts @@ -805,9 +805,13 @@ grep -Fq -- '--phase post-agent-install' Dockerfile }); it("accepts reviewed base-image versions and rejects injected build arguments", () => { - const baseImages = readYaml(".github/workflows/base-image.yaml"); - const buildOpenClawPlatforms = baseImages.jobs["build-openclaw-platforms"] as WorkflowJob; - const guard = requiredStep(buildOpenClawPlatforms, "Validate production Docker build args"); + const action = readYaml<{ runs: { steps: WorkflowStep[] } }>( + ".github/actions/build-base-image-platform/action.yaml", + ); + const guard = requiredStep( + { steps: action.runs.steps }, + "Validate production Docker build args", + ); for (const [input, expectedOutput] of [ ["", "openclaw_build_arg=\n"], diff --git a/test/publish-base-image-manifest.test.ts b/test/publish-base-image-manifest.test.ts new file mode 100644 index 00000000000..798b5cae93c --- /dev/null +++ b/test/publish-base-image-manifest.test.ts @@ -0,0 +1,161 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { spawnSync } from "node:child_process"; + +import { afterEach, describe, expect, it } from "vitest"; + +const SHA_AMD64 = "a".repeat(64); +const SHA_ARM64 = "b".repeat(64); +const SHA_CANDIDATE = `sha256:${"c".repeat(64)}`; +const SHA_PUBLISHED = `sha256:${"d".repeat(64)}`; +const helper = resolve( + import.meta.dirname, + "../.github/actions/publish-base-image-manifest/publish.sh", +); +const temporaryRoots: string[] = []; + +afterEach(() => { + for (const root of temporaryRoots.splice(0)) rmSync(root, { force: true, recursive: true }); +}); + +function executable(file: string, content: string): void { + writeFileSync(file, content); + chmodSync(file, 0o755); +} + +function runManifest({ + digestFiles, + wrongPlatform = false, + publicationMismatch = false, +}: { + digestFiles: string[]; + wrongPlatform?: boolean; + publicationMismatch?: boolean; +}) { + const root = mkdtempSync(join(tmpdir(), "nemoclaw-base-manifest-")); + temporaryRoots.push(root); + const runnerTemp = join(root, "runner"); + const digests = join(runnerTemp, "digests"); + const checks = join(root, "scripts", "checks"); + const bin = join(root, "bin"); + mkdirSync(digests, { recursive: true }); + mkdirSync(checks, { recursive: true }); + mkdirSync(bin, { recursive: true }); + for (const file of digestFiles) writeFileSync(join(digests, file), ""); + + executable( + join(checks, "retry-docker-imagetools-inspect.sh"), + `#!/bin/bash +set -euo pipefail +if [[ " $* " == *" --raw "* ]]; then + printf '%s\n' '{"manifests":[{"platform":{"os":"linux","architecture":"amd64"}},{"platform":{"os":"linux","architecture":"arm64"}}]}' + exit 0 +fi +if [ "$WRONG_PLATFORM" = true ]; then + printf '%s\n' linux/arm64 +elif [[ "$1" == *"${SHA_AMD64}"* ]]; then + printf '%s\n' linux/amd64 +else + printf '%s\n' linux/arm64 +fi +`, + ); + executable( + join(checks, "validate-managed-base-index.sh"), + `#!/bin/bash +printf '{"linux/amd64":"sha256:%s","linux/arm64":"sha256:%s"}\n' '${SHA_AMD64}' '${SHA_ARM64}' +`, + ); + executable( + join(root, "scripts", "export-managed-base-image-contract.sh"), + `#!/bin/bash +set -euo pipefail +output="$9" +mkdir -p "$(dirname "$output")" +printf '{}\n' > "$output" +`, + ); + executable( + join(bin, "docker"), + `#!/bin/bash +set -euo pipefail +metadata="" +previous="" +for argument in "$@"; do + if [ "$previous" = "--metadata-file" ]; then metadata="$argument"; fi + previous="$argument" +done +[ -n "$metadata" ] +if [[ "$metadata" == *publication* ]] && [ "$PUBLICATION_MISMATCH" = true ]; then + digest='${SHA_PUBLISHED}' +else + digest='${SHA_CANDIDATE}' +fi +mkdir -p "$(dirname "$metadata")" +printf '{"containerimage.descriptor":{"digest":"%s"}}\n' "$digest" > "$metadata" +`, + ); + + const output = join(root, "github-output"); + return spawnSync("bash", [helper], { + cwd: root, + encoding: "utf8", + env: { + ...process.env, + AGENT: "openclaw", + DISPLAY_NAME: "OpenClaw", + GITHUB_OUTPUT: output, + GITHUB_RUN_ATTEMPT: "1", + GITHUB_RUN_ID: "1", + GITHUB_SHA: "e".repeat(40), + IMAGE: "ghcr.io/nvidia/nemoclaw/sandbox-base", + PATH: `${bin}:${process.env.PATH || ""}`, + PUBLICATION_MISMATCH: String(publicationMismatch), + RUNNER_TEMP: runnerTemp, + TAGS: "ghcr.io/nvidia/nemoclaw/sandbox-base:test", + WRONG_PLATFORM: String(wrongPlatform), + }, + }); +} + +describe("base-image manifest publication", () => { + it("rejects a malformed platform digest artifact", () => { + const result = runManifest({ digestFiles: ["amd64-invalid", `arm64-${SHA_ARM64}`] }); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain("invalid platform digest artifact: amd64-invalid"); + }); + + it("rejects duplicate platform digest artifacts", () => { + const result = runManifest({ digestFiles: [`amd64-${SHA_AMD64}`, `amd64-${SHA_ARM64}`] }); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain("duplicate platform digest for linux/amd64"); + }); + + it("rejects a digest that resolves to the wrong platform", () => { + const result = runManifest({ + digestFiles: [`amd64-${SHA_AMD64}`, `arm64-${SHA_ARM64}`], + wrongPlatform: true, + }); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain("digest for linux/amd64 resolves to linux/arm64"); + }); + + it("rejects a published digest that differs from the validated candidate", () => { + const result = runManifest({ + digestFiles: [`amd64-${SHA_AMD64}`, `arm64-${SHA_ARM64}`], + publicationMismatch: true, + }); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain( + "published OpenClaw base digest differs from the validated candidate", + ); + }); +}); diff --git a/test/regression-e2e-selector.test.ts b/test/regression-e2e-selector.test.ts deleted file mode 100644 index 7aa636eadab..00000000000 --- a/test/regression-e2e-selector.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { spawnSync } from "node:child_process"; -import { mkdtempSync, readFileSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; - -import { afterEach, describe, expect, it } from "vitest"; - -const temporaryRoots: string[] = []; - -afterEach(() => { - for (const root of temporaryRoots.splice(0)) rmSync(root, { force: true, recursive: true }); -}); - -function select(jobs: string): string { - const root = mkdtempSync(join(tmpdir(), "nemoclaw-regression-selector-")); - temporaryRoots.push(root); - const output = join(root, "github-output"); - const result = spawnSync("bash", [".github/scripts/select-regression-e2e-jobs.sh"], { - cwd: join(import.meta.dirname, ".."), - encoding: "utf8", - env: { ...process.env, GITHUB_OUTPUT: output, JOBS: jobs }, - }); - expect(result.status, result.stderr).toBe(0); - return readFileSync(output, "utf8").trim(); -} - -describe("regression E2E selector", () => { - it.each([ - "", - " whatsapp-qr-compact-e2e ", - ])("selects the WhatsApp regression for accepted jobs input %j", (jobs) => { - expect(select(jobs)).toBe("whatsapp_qr_compact=true"); - }); - - it.each([ - "unknown", - "whatsapp-qr-compact-e2", - "whatsapp-qr-compact-e2e-extra", - ])("does not select the WhatsApp regression for unsupported jobs input %j", (jobs) => { - expect(select(jobs)).toBe("whatsapp_qr_compact=false"); - }); -}); diff --git a/test/vitest-watch-triggers.test.ts b/test/vitest-watch-triggers.test.ts index 6243cfc9ded..f451c048c42 100644 --- a/test/vitest-watch-triggers.test.ts +++ b/test/vitest-watch-triggers.test.ts @@ -211,7 +211,6 @@ describe("Vitest opaque-input watch triggers", () => { expect(triggeredBy("scripts/unrelated.py")).toEqual([]); expect(triggeredBy("test/e2e/lib/unrelated.sh")).toEqual([]); expect(triggeredBy("agents/hermes/hermes-wrapper.py")).toEqual([]); - expect(triggeredBy(".github/workflows/regression-e2e.yaml")).toEqual([]); }); it("normalizes Windows-style paths before matching (#6692)", () => { diff --git a/tools/e2e/workflow-contract-registry.mts b/tools/e2e/workflow-contract-registry.mts index 8bb66f5d713..2b5ff797e28 100644 --- a/tools/e2e/workflow-contract-registry.mts +++ b/tools/e2e/workflow-contract-registry.mts @@ -47,6 +47,7 @@ export const E2E_JOB_CONTRACT = { "shields-config", "snapshot-commands", "spark-install", + "whatsapp-qr-compact", ], prepareTrustedBuild: ["managed-image-protected-runtime"], cliArtifactConsumers: [ From ba89ee6211f134f88174e9fb78a42da5fe503f8b Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Sat, 8 Aug 2026 16:37:12 -0700 Subject: [PATCH 08/13] docs(ci): clarify E2E and base image terms --- .github/actions/build-base-image-platform/action.yaml | 6 +++--- .github/actions/publish-base-image-manifest/action.yaml | 4 ++-- .github/workflows/e2e.yaml | 4 ++-- agents/openclaw/dependency-review.md | 3 ++- test/e2e/support/cli-artifact-workflow-boundary.test.ts | 2 +- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.github/actions/build-base-image-platform/action.yaml b/.github/actions/build-base-image-platform/action.yaml index 4976c0c1b15..fd2b3979ba3 100644 --- a/.github/actions/build-base-image-platform/action.yaml +++ b/.github/actions/build-base-image-platform/action.yaml @@ -2,7 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 name: build-base-image-platform -description: Build and publish one immutable base-image platform digest. +description: Build and publish one immutable base image digest for a platform. inputs: agent: @@ -15,7 +15,7 @@ inputs: description: Docker platform to build. required: true dockerfile: - description: Base-image Dockerfile path. + description: Path to the base image Dockerfile. required: true image: description: Image repository relative to the registry. @@ -85,7 +85,7 @@ runs: exit 1 fi if [[ ! "$OPENCLAW_VERSION_INPUT" =~ ^[0-9]+([.][0-9]+)*$ ]]; then - echo "ERROR: OpenClaw version must be a whole decimal dotted version (for example, 2026.6.10)." >&2 + echo "ERROR: OpenClaw version must contain one or more decimal integers separated by periods (for example, 2026.6.10)." >&2 exit 1 fi fi diff --git a/.github/actions/publish-base-image-manifest/action.yaml b/.github/actions/publish-base-image-manifest/action.yaml index 1c65ad725b9..984157b9ec3 100644 --- a/.github/actions/publish-base-image-manifest/action.yaml +++ b/.github/actions/publish-base-image-manifest/action.yaml @@ -2,11 +2,11 @@ # SPDX-License-Identifier: Apache-2.0 name: publish-base-image-manifest -description: Validate platform digests and publish one multi-platform base-image manifest. +description: Validate platform digests and publish one multi-platform base image manifest. inputs: agent: - description: Agent identifier stored in the managed-base contract. + description: Agent identifier stored in the managed base image contract. required: true display-name: description: Agent name used in validation errors. diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index db020f756f9..a9dad8ce7e1 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -7276,12 +7276,12 @@ jobs: with: build-cli: "false" - - name: Run WhatsApp compact QR live test + - name: Run WhatsApp compact QR code E2E test run: |- set -euo pipefail npx tsx tools/e2e/live-vitest-invocation.mts run --test-path test/e2e/live/whatsapp-qr-compact.test.ts - - name: Upload WhatsApp compact QR artifacts + - name: Upload WhatsApp compact QR code artifacts if: always() uses: NVIDIA/NemoClaw/.github/actions/upload-e2e-artifacts@7768e15eb90d3ee2d33432f481dfe8747e4f6d57 diff --git a/agents/openclaw/dependency-review.md b/agents/openclaw/dependency-review.md index 0fc8f164668..822d29da357 100644 --- a/agents/openclaw/dependency-review.md +++ b/agents/openclaw/dependency-review.md @@ -37,7 +37,8 @@ The reviewed audit wrapper reports lower-severity production findings and blocks Other PRs fail closed if their base lacks the action. The `main.yaml` workflow uses the merged action. The action uses Node.js `22.19.0`. - It downloads `npm@10.9.4`, verifies the SRI value committed in the action, and installs the archive offline with lifecycle scripts disabled. + It downloads `npm@10.9.4` and verifies the SRI value committed in the action. + It installs the verified archive in npm offline mode with lifecycle scripts disabled. It materializes the committed graph with scripts disabled. The action rejects any low-or-higher production advisory and verifies registry signatures. The PR and main workflows upload the resulting reports. diff --git a/test/e2e/support/cli-artifact-workflow-boundary.test.ts b/test/e2e/support/cli-artifact-workflow-boundary.test.ts index aa7074ecdd7..50b96a32294 100644 --- a/test/e2e/support/cli-artifact-workflow-boundary.test.ts +++ b/test/e2e/support/cli-artifact-workflow-boundary.test.ts @@ -940,7 +940,7 @@ describe("exact-commit CLI artifact workflow boundary", () => { ); }); - it("rejects CLI artifact consumer registry drift", () => { + it("rejects a missing CLI artifact consumer in the shared job contract", () => { const consumers = E2E_JOB_CONTRACT.cliArtifactConsumers as unknown as string[]; const removed = consumers.pop(); expect(removed).toBeDefined(); From a8bd4d08b5814a75a70b5ade79ddecda10b905a1 Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Sat, 8 Aug 2026 16:41:10 -0700 Subject: [PATCH 09/13] docs(e2e): remove stale regression workflow guidance --- test/e2e/docs/README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test/e2e/docs/README.md b/test/e2e/docs/README.md index 2a35a568f28..9569b5da110 100644 --- a/test/e2e/docs/README.md +++ b/test/e2e/docs/README.md @@ -341,10 +341,9 @@ test/e2e/ Each macOS shard installs the pinned OpenShell formula and has a 30-minute budget. Each WSL shard has a 90-minute budget, and WSL runs its additional root-required contracts on shard 1 only. - `macos-e2e.yaml`, `wsl-e2e.yaml`, and `regression-e2e.yaml` call focused E2E - targets directly for their platform coverage. - Repository-hosted targets, including `ollama-auth-proxy`, are selected - through `.github/workflows/e2e.yaml`. + `macos-e2e.yaml` and `wsl-e2e.yaml` call focused E2E targets directly for + platform coverage. Repository-hosted targets, including `whatsapp-qr-compact` + and `ollama-auth-proxy`, are selected through `.github/workflows/e2e.yaml`. - The `staging-brev-launchable` job validates the exact baked candidate in preinstalled mode. Generic Brev VMs with source overlays are not a qualification boundary. From 921dae69f0ea3e6e9854531730c93cc0e3cf1f2d Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Sat, 8 Aug 2026 17:14:05 -0700 Subject: [PATCH 10/13] refactor(e2e): derive CLI artifact consumers --- .../cli-artifact-workflow-boundary.test.ts | 19 ++---- tools/e2e/cli-artifact-workflow-boundary.mts | 8 +-- tools/e2e/prepare-e2e-workflow-boundary.mts | 8 +-- ...upload-e2e-artifacts-workflow-boundary.mts | 3 +- ...istry.mts => workflow-boundary-policy.mts} | 68 +------------------ 5 files changed, 16 insertions(+), 90 deletions(-) rename tools/e2e/{workflow-contract-registry.mts => workflow-boundary-policy.mts} (54%) diff --git a/test/e2e/support/cli-artifact-workflow-boundary.test.ts b/test/e2e/support/cli-artifact-workflow-boundary.test.ts index 50b96a32294..749941ed759 100644 --- a/test/e2e/support/cli-artifact-workflow-boundary.test.ts +++ b/test/e2e/support/cli-artifact-workflow-boundary.test.ts @@ -14,7 +14,6 @@ import { validateCliArtifactRestoreAction, validateCliArtifactWorkflowBoundary, } from "../../../tools/e2e/cli-artifact-workflow-boundary.mts"; -import { E2E_JOB_CONTRACT } from "../../../tools/e2e/workflow-contract-registry.mts"; import { type CompositeAction, readRepoText, @@ -940,16 +939,12 @@ describe("exact-commit CLI artifact workflow boundary", () => { ); }); - it("rejects a missing CLI artifact consumer in the shared job contract", () => { - const consumers = E2E_JOB_CONTRACT.cliArtifactConsumers as unknown as string[]; - const removed = consumers.pop(); - expect(removed).toBeDefined(); - try { - expect(validateCliArtifactWorkflowBoundary(workflowFixture())).toContain( - "CLI artifact consumer job names must match the required list", - ); - } finally { - consumers.push(removed!); - } + it("rejects an added CLI artifact consumer outside the reviewed workflow contract", () => { + const workflow = workflowFixture(); + workflow.jobs["added-consumer"] = structuredClone(workflow.jobs["cloud-inference"]); + + expect(validateCliArtifactWorkflowBoundary(workflow)).toContain( + "CLI artifact workflow settings, consumer job settings, and steps up to and including CLI artifact restore must match the required contract", + ); }); }); diff --git a/tools/e2e/cli-artifact-workflow-boundary.mts b/tools/e2e/cli-artifact-workflow-boundary.mts index f313f689526..dd9547837ef 100644 --- a/tools/e2e/cli-artifact-workflow-boundary.mts +++ b/tools/e2e/cli-artifact-workflow-boundary.mts @@ -13,7 +13,7 @@ import { PREPARE_E2E_NO_BUILD_JOBS, PREPARE_E2E_TRUSTED_BUILD_JOBS, } from "./prepare-e2e-workflow-boundary.mts"; -import { E2E_ACTION_PROVENANCE, E2E_JOB_CONTRACT } from "./workflow-contract-registry.mts"; +import { E2E_ACTION_PROVENANCE } from "./workflow-boundary-policy.mts"; export const CLI_ARTIFACT_DOWNLOAD_ACTION = "actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c"; @@ -40,7 +40,6 @@ const CANDIDATE_CHECKOUT_STEP_CONTENT_SHA256 = "3578a053cede863f7aa4814d8399b4ca21ea0b77cee712e6d549c684818f11dd"; const CLI_ARTIFACT_WORKFLOW_CONTRACT_SHA256 = "14fb4de8dffd0cfd3f0dd3177f03e806c8912247bac4f5919a570c0f6b4c0ca2"; -const CLI_ARTIFACT_CONSUMER_JOB_NAMES = E2E_JOB_CONTRACT.cliArtifactConsumers; type WorkflowRecord = Record; type WorkflowStep = WorkflowRecord & { @@ -424,11 +423,8 @@ export function validateCliArtifactWorkflowBoundary( } const actualConsumerJobNames = [...consumerJobNames].sort(); - if (!isDeepStrictEqual(actualConsumerJobNames, CLI_ARTIFACT_CONSUMER_JOB_NAMES)) { - errors.push("CLI artifact consumer job names must match the required list"); - } const consumers = Object.fromEntries( - CLI_ARTIFACT_CONSUMER_JOB_NAMES.map((jobName) => [ + actualConsumerJobNames.map((jobName) => [ jobName, jobSettingsAndStepsThroughRestore(record(jobs[jobName])), ]), diff --git a/tools/e2e/prepare-e2e-workflow-boundary.mts b/tools/e2e/prepare-e2e-workflow-boundary.mts index b4c80779d75..a83c07e34ce 100644 --- a/tools/e2e/prepare-e2e-workflow-boundary.mts +++ b/tools/e2e/prepare-e2e-workflow-boundary.mts @@ -8,7 +8,7 @@ import { fileURLToPath } from "node:url"; import { isDeepStrictEqual } from "node:util"; import YAML from "yaml"; import { SHARED_E2E_JOB_ID } from "./credential-free-tests.mts"; -import { E2E_ACTION_PROVENANCE, E2E_JOB_CONTRACT } from "./workflow-contract-registry.mts"; +import { E2E_ACTION_PROVENANCE, E2E_JOB_POLICY } from "./workflow-boundary-policy.mts"; const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", ".."); const DEFAULT_ACTION_PATH = join(REPO_ROOT, ".github", "actions", "prepare-e2e", "action.yaml"); @@ -19,13 +19,13 @@ export const PREPARE_E2E_ACTION = PREPARE_E2E_ACTION_PROVENANCE.reference; export const PREPARE_E2E_STEP = "Prepare E2E workspace"; const CHECKOUT_LOCAL_PREPARE_E2E_ACTION = "./.github/actions/prepare-e2e"; -export const CLI_ARTIFACT_PRODUCER_JOB = E2E_JOB_CONTRACT.cliArtifactProducer; +export const CLI_ARTIFACT_PRODUCER_JOB = E2E_JOB_POLICY.cliArtifactProducer; const PREINSTALLED_E2E_JOBS = new Set(["staging-brev-launchable"]); const RETIRED_SELECTOR_COMPATIBILITY_JOB = "retired-selector-compatibility"; -export const PREPARE_E2E_NO_BUILD_JOBS = new Set(E2E_JOB_CONTRACT.prepareNoBuild); +export const PREPARE_E2E_NO_BUILD_JOBS = new Set(E2E_JOB_POLICY.prepareNoBuild); -export const PREPARE_E2E_TRUSTED_BUILD_JOBS = new Set(E2E_JOB_CONTRACT.prepareTrustedBuild); +export const PREPARE_E2E_TRUSTED_BUILD_JOBS = new Set(E2E_JOB_POLICY.prepareTrustedBuild); type WorkflowRecord = Record; type WorkflowStep = WorkflowRecord & { diff --git a/tools/e2e/upload-e2e-artifacts-workflow-boundary.mts b/tools/e2e/upload-e2e-artifacts-workflow-boundary.mts index 2cfed5b662e..c43e8420008 100644 --- a/tools/e2e/upload-e2e-artifacts-workflow-boundary.mts +++ b/tools/e2e/upload-e2e-artifacts-workflow-boundary.mts @@ -12,7 +12,8 @@ import { CLI_ARTIFACT_UPLOAD_ACTION, } from "./cli-artifact-workflow-boundary.mts"; import { SHARED_E2E_JOB_ID } from "./credential-free-tests.mts"; -import { E2E_ACTION_PROVENANCE } from "./workflow-contract-registry.mts"; +import { E2E_ACTION_PROVENANCE } from "./workflow-boundary-policy.mts"; + export { E2E_ACTION_PROVENANCE }; const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", ".."); diff --git a/tools/e2e/workflow-contract-registry.mts b/tools/e2e/workflow-boundary-policy.mts similarity index 54% rename from tools/e2e/workflow-contract-registry.mts rename to tools/e2e/workflow-boundary-policy.mts index 2b5ff797e28..5b11e6fe8bd 100644 --- a/tools/e2e/workflow-contract-registry.mts +++ b/tools/e2e/workflow-boundary-policy.mts @@ -37,7 +37,7 @@ export const E2E_ACTION_PROVENANCE = { }, } as const; -export const E2E_JOB_CONTRACT = { +export const E2E_JOB_POLICY = { cliArtifactProducer: "generate-matrix", prepareNoBuild: [ "bootstrap-install-smoke", @@ -50,70 +50,4 @@ export const E2E_JOB_CONTRACT = { "whatsapp-qr-compact", ], prepareTrustedBuild: ["managed-image-protected-runtime"], - cliArtifactConsumers: [ - "agent-turn-latency", - "bedrock-runtime-compatible-anthropic", - "brave-search", - "channels-add-remove", - "channels-stop-start", - "cloud-inference", - "cloud-onboard", - "common-egress-agent", - "concurrent-gateway-ports", - "cron-preflight-inference-local", - "dashboard-remote-bind", - "device-auth-health", - "double-onboard", - "full-e2e", - "gateway-guard-recovery", - "gpu-double-onboard", - "gpu-e2e", - "hermes-discord", - "hermes-e2e", - "hermes-gpu-startup", - "hermes-inference-switch", - "hermes-shields-config", - "hermes-slack", - "inference-routing", - "issue-2478-crash-loop-recovery", - "issue-4434-tui-unreachable-inference", - "issue-4462-scope-upgrade-approval", - "jetson-nvmap-gpu", - "kimi-inference-compat", - "live", - "llama-cpp-generic-gpu", - "mcp-bridge", - "mcp-bridge-dev", - "messaging-compatible-endpoint", - "messaging-providers", - "model-router-provider-routed-inference", - "network-policy", - "onboard-repair", - "onboard-resume", - "openclaw-discord-pairing", - "openclaw-inference-switch", - "openclaw-plugin-runtime-exdev", - "openclaw-plugin-runtime-exdev-release", - "openclaw-skill-cli", - "openclaw-slack-pairing", - "openclaw-tui-chat-correlation", - "openshell-credential-generation-window", - "openshell-gateway-auth-contract", - "openshell-gateway-upgrade", - "overlayfs-autofix", - "rebuild-hermes", - "rebuild-hermes-stale-base", - "rebuild-openclaw", - "retired-selector-compatibility", - "sandbox-operations", - "sandbox-survival", - "security-posture", - "sessions-agents-cli", - "shared-e2e", - "skill-agent", - "state-backup-restore", - "telegram-injection", - "token-rotation", - "tunnel-lifecycle", - ], } as const; From 3f8734334dcb741d78f629b55df3c46c35191d21 Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Sat, 8 Aug 2026 17:36:25 -0700 Subject: [PATCH 11/13] fix(e2e): validate WhatsApp selector gate --- test/e2e/support/e2e-workflow.test.ts | 11 +++++++++++ tools/e2e/workflow-boundary.mts | 1 + 2 files changed, 12 insertions(+) diff --git a/test/e2e/support/e2e-workflow.test.ts b/test/e2e/support/e2e-workflow.test.ts index 16b62cdee20..9ca80c466b6 100644 --- a/test/e2e/support/e2e-workflow.test.ts +++ b/test/e2e/support/e2e-workflow.test.ts @@ -138,6 +138,17 @@ describe("e2e workflow boundary", () => { ).toEqual({ runLaunchableE2e: false }); }); + it("rejects WhatsApp compact QR selector drift", () => { + const workflow = readWorkflow() as { + jobs: Record; + }; + workflow.jobs["whatsapp-qr-compact"]!.if = "${{ github.event_name != 'workflow_dispatch' }}"; + + expect(validateE2eWorkflow(workflow)).toContain( + "whatsapp-qr-compact job must use the shared jobs selector condition", + ); + }); + it("rejects a full dispatch with changed input, correlation, or selector contracts (#7487)", () => { const workflow = readWorkflow() as { "run-name": string; diff --git a/tools/e2e/workflow-boundary.mts b/tools/e2e/workflow-boundary.mts index 7fe2d5f3820..e5dc27f027c 100644 --- a/tools/e2e/workflow-boundary.mts +++ b/tools/e2e/workflow-boundary.mts @@ -4958,6 +4958,7 @@ export function validateE2eWorkflow(workflowValue: unknown): string[] { validateStagingBrevLaunchableJob(errors, jobs); validateSkillAgentJob(errors, jobs); validateFreeStandingJobSelector(errors, jobs, "sessions-agents-cli", "sessions-agents-cli"); + validateFreeStandingJobSelector(errors, jobs, "whatsapp-qr-compact", "whatsapp-qr-compact"); validateFreeStandingJobSelector(errors, jobs, "inference-routing", "inference-routing"); validateInferenceRoutingJob(errors, jobs); validateCloudInferenceJob(errors, jobs); From cd73c1268552b616cf6d1f3b3393db52ebc4795b Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Sat, 8 Aug 2026 17:41:47 -0700 Subject: [PATCH 12/13] docs(e2e): correct workflow ownership --- agents/openclaw/dependency-review.md | 2 +- test/e2e/README.md | 7 ++++--- test/e2e/docs/README.md | 7 ++++--- test/e2e/support/e2e-workflow.test.ts | 2 +- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/agents/openclaw/dependency-review.md b/agents/openclaw/dependency-review.md index 822d29da357..1b73ddd4021 100644 --- a/agents/openclaw/dependency-review.md +++ b/agents/openclaw/dependency-review.md @@ -37,7 +37,7 @@ The reviewed audit wrapper reports lower-severity production findings and blocks Other PRs fail closed if their base lacks the action. The `main.yaml` workflow uses the merged action. The action uses Node.js `22.19.0`. - It downloads `npm@10.9.4` and verifies the SRI value committed in the action. + It downloads `npm@10.9.4` and verifies the archive against the committed Subresource Integrity (SRI) value. It installs the verified archive in npm offline mode with lifecycle scripts disabled. It materializes the committed graph with scripts disabled. The action rejects any low-or-higher production advisory and verifies registry signatures. diff --git a/test/e2e/README.md b/test/e2e/README.md index e318bd9bdc8..74c7059fc3a 100644 --- a/test/e2e/README.md +++ b/test/e2e/README.md @@ -19,9 +19,10 @@ before those targets run; local runners must provide it themselves. attempts, requests at most two failed-job reruns, and uploads attempt evidence. - The `staging-brev-launchable` job in `.github/workflows/e2e.yaml` validates the baked candidate without installing or copying NemoClaw source. -- Platform workflows such as macOS, WSL, sandbox image, and regression E2E - call their target E2E tests directly. The Ollama auth proxy target is - selected through `.github/workflows/e2e.yaml`. +- `.github/workflows/macos-e2e.yaml`, `.github/workflows/wsl-e2e.yaml`, and + `.github/workflows/sandbox-images-and-e2e.yaml` call focused E2E targets directly. + `.github/workflows/e2e.yaml` selects free-standing jobs, including + `whatsapp-qr-compact` and `ollama-auth-proxy`. ## CI execution shape diff --git a/test/e2e/docs/README.md b/test/e2e/docs/README.md index 9569b5da110..4f1f399f7e2 100644 --- a/test/e2e/docs/README.md +++ b/test/e2e/docs/README.md @@ -341,9 +341,10 @@ test/e2e/ Each macOS shard installs the pinned OpenShell formula and has a 30-minute budget. Each WSL shard has a 90-minute budget, and WSL runs its additional root-required contracts on shard 1 only. - `macos-e2e.yaml` and `wsl-e2e.yaml` call focused E2E targets directly for - platform coverage. Repository-hosted targets, including `whatsapp-qr-compact` - and `ollama-auth-proxy`, are selected through `.github/workflows/e2e.yaml`. + `.github/workflows/macos-e2e.yaml` and `.github/workflows/wsl-e2e.yaml` call + focused E2E targets directly for platform coverage. `.github/workflows/e2e.yaml` + selects free-standing jobs, including `whatsapp-qr-compact` and + `ollama-auth-proxy`. - The `staging-brev-launchable` job validates the exact baked candidate in preinstalled mode. Generic Brev VMs with source overlays are not a qualification boundary. diff --git a/test/e2e/support/e2e-workflow.test.ts b/test/e2e/support/e2e-workflow.test.ts index 9ca80c466b6..9775e6a96be 100644 --- a/test/e2e/support/e2e-workflow.test.ts +++ b/test/e2e/support/e2e-workflow.test.ts @@ -138,7 +138,7 @@ describe("e2e workflow boundary", () => { ).toEqual({ runLaunchableE2e: false }); }); - it("rejects WhatsApp compact QR selector drift", () => { + it("rejects a WhatsApp compact QR job that omits the jobs and targets dispatch conditions", () => { const workflow = readWorkflow() as { jobs: Record; }; From 6d5bb77347d9b72a3b77588bed88ad7214b23f85 Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Sat, 8 Aug 2026 17:48:07 -0700 Subject: [PATCH 13/13] docs(e2e): describe derived artifact consumers --- test/e2e/README.md | 8 ++++---- test/e2e/docs/README.md | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/test/e2e/README.md b/test/e2e/README.md index 74c7059fc3a..d140eafdd30 100644 --- a/test/e2e/README.md +++ b/test/e2e/README.md @@ -31,10 +31,10 @@ before those targets run; local runners must provide it themselves. The candidate CLI comes from the source commit that an E2E run tests. The `generate-matrix` job builds it once. The job publishes root `dist/` and `nemoclaw/dist/shared/` in one content-addressed artifact. -The workflow has 62 artifact-using job definitions. -Each selected job execution restores the artifact instead of running `npm run build:cli`. -Each selected job still runs the pinned preparation action to install Node.js and project dependencies. -It sets `build-cli: "false"` so the preparation action does not rebuild the CLI. +The boundary validator derives artifact consumers from jobs that use the pinned preparation action. +It excludes `generate-matrix` and the no-build and trusted-build jobs in `E2E_JOB_POLICY`. +Each selected consumer restores the artifact instead of running `npm run build:cli`. +Each consumer runs the pinned preparation action with `build-cli: "false"` to install Node.js and project dependencies. The `managed-image-protected-runtime` qualification does not use this artifact. It builds the CLI from the trusted workflow checkout and never executes or restores the candidate CLI. diff --git a/test/e2e/docs/README.md b/test/e2e/docs/README.md index 4f1f399f7e2..3c573288b7f 100644 --- a/test/e2e/docs/README.md +++ b/test/e2e/docs/README.md @@ -341,10 +341,10 @@ test/e2e/ Each macOS shard installs the pinned OpenShell formula and has a 30-minute budget. Each WSL shard has a 90-minute budget, and WSL runs its additional root-required contracts on shard 1 only. - `.github/workflows/macos-e2e.yaml` and `.github/workflows/wsl-e2e.yaml` call - focused E2E targets directly for platform coverage. `.github/workflows/e2e.yaml` - selects free-standing jobs, including `whatsapp-qr-compact` and - `ollama-auth-proxy`. + `.github/workflows/macos-e2e.yaml`, `.github/workflows/wsl-e2e.yaml`, and + `.github/workflows/sandbox-images-and-e2e.yaml` call focused E2E targets + directly. `.github/workflows/e2e.yaml` selects free-standing jobs, including + `whatsapp-qr-compact` and `ollama-auth-proxy`. - The `staging-brev-launchable` job validates the exact baked candidate in preinstalled mode. Generic Brev VMs with source overlays are not a qualification boundary.