diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index dc1df361688..14926a1dd63 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -186,14 +186,11 @@ jobs: run: node --experimental-strip-types --no-warnings tools/e2e/dcode-base-image-contract.mts "${RUNNER_TEMP}/dcode-base-contract/contract.json" generate-matrix: - needs: base-image-publication runs-on: ubuntu-latest timeout-minutes: 10 outputs: cli_artifact_provenance: ${{ steps.record_cli_artifact.outputs.provenance }} e2e_credentials_allowed: ${{ steps.e2e_credentials.outputs.allowed }} - dcode_base_contract: ${{ needs.base-image-publication.outputs.dcode_base_contract }} - dcode_base_ref: ${{ needs.base-image-publication.outputs.dcode_base_ref }} matrix: ${{ steps.matrix.outputs.matrix }} test_matrix: ${{ steps.matrix.outputs.test_matrix }} hermes_selected: ${{ steps.matrix.outputs.hermes_selected }} @@ -907,7 +904,7 @@ jobs: ${{ steps.workspace.outputs.work_dir }}/cleanup.json live: - needs: generate-matrix + needs: [base-image-publication, generate-matrix] if: ${{ needs.generate-matrix.outputs.matrix != '[]' }} runs-on: ${{ matrix.runner }} timeout-minutes: 45 @@ -917,7 +914,7 @@ jobs: include: ${{ fromJSON(needs.generate-matrix.outputs.matrix) }} env: E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts/live - NEMOCLAW_LANGCHAIN_DEEPAGENTS_CODE_SANDBOX_BASE_IMAGE_REF: ${{ needs.generate-matrix.outputs.dcode_base_ref }} + NEMOCLAW_LANGCHAIN_DEEPAGENTS_CODE_SANDBOX_BASE_IMAGE_REF: ${{ needs.base-image-publication.outputs.dcode_base_ref }} NEMOCLAW_CLI_BIN: ${{ github.workspace }}/bin/nemoclaw.js NEMOCLAW_RUN_LIVE_E2E: "1" NEMOCLAW_E2E_USE_HOSTED_INFERENCE: "1" @@ -974,7 +971,7 @@ jobs: - name: Record immutable Deep Agents Code base evidence if: ${{ matrix.id == 'ubuntu-repo-cloud-langchain-deepagents-code' }} env: - BASE_CONTRACT: ${{ needs.generate-matrix.outputs.dcode_base_contract }} + BASE_CONTRACT: ${{ needs.base-image-publication.outputs.dcode_base_contract }} CANDIDATE_SHA: ${{ inputs.checkout_sha || github.sha }} TARGET_ID: ${{ matrix.id }} shell: bash diff --git a/test/e2e/support/base-image-publication-workflow-boundary.test.ts b/test/e2e/support/base-image-publication-workflow-boundary.test.ts index f4ac0a5f98e..e62085704c4 100644 --- a/test/e2e/support/base-image-publication-workflow-boundary.test.ts +++ b/test/e2e/support/base-image-publication-workflow-boundary.test.ts @@ -93,6 +93,12 @@ function runClassifier(environment: { } describe("base-image publication workflow boundary (#7372)", () => { + it("keeps Launchable off the base-image publication critical path", () => { + const value = workflow(); + + expect(validate(value)).toEqual([]); + }); + // source-shape-contract: security -- Immutable base contracts must outlive the qualification interval so later E2E cannot fall back to a mutable alias. it("retains immutable base contracts for later qualification (#9049)", () => { const action = YAML.parse( @@ -209,7 +215,19 @@ describe("base-image publication workflow boundary (#7372)", () => { (gateSteps(value)[5].run = "node tools/e2e/dcode-base-image-contract.mts contract.json"), ], ["step count", (value) => gateSteps(value).push({ name: "Unreviewed step", run: "true" })], - ["fanout dependency", (value) => (value.jobs["generate-matrix"].needs = [])], + [ + "matrix publication dependency", + (value) => (value.jobs["generate-matrix"].needs = "base-image-publication"), + ], + ["live publication dependency", (value) => (value.jobs.live.needs = ["generate-matrix"])], + [ + "Launchable publication dependency", + (value) => + (value.jobs["staging-brev-launchable"].needs = [ + "base-image-publication", + "generate-matrix", + ]), + ], [ "matrix base output", (value) => { diff --git a/tools/e2e/cli-artifact-workflow-boundary.mts b/tools/e2e/cli-artifact-workflow-boundary.mts index 39ba4944b57..92390f44969 100644 --- a/tools/e2e/cli-artifact-workflow-boundary.mts +++ b/tools/e2e/cli-artifact-workflow-boundary.mts @@ -340,10 +340,12 @@ function validateConsumer( job: WorkflowRecord, jobSteps: WorkflowStep[], ): void { - const expectedNeeds = - jobName === "mcp-bridge-dev" - ? [CLI_ARTIFACT_PRODUCER_JOB, "openshell-dev-artifact"] - : CLI_ARTIFACT_PRODUCER_JOB; + let expectedNeeds: string | string[] = CLI_ARTIFACT_PRODUCER_JOB; + if (jobName === "mcp-bridge-dev") { + expectedNeeds = [CLI_ARTIFACT_PRODUCER_JOB, "openshell-dev-artifact"]; + } else if (jobName === "live") { + expectedNeeds = ["base-image-publication", CLI_ARTIFACT_PRODUCER_JOB]; + } if (!isDeepStrictEqual(job.needs, expectedNeeds)) { errors.push(`${jobName} must depend directly on the CLI artifact producer`); } diff --git a/tools/e2e/operations-workflow-boundary.mts b/tools/e2e/operations-workflow-boundary.mts index 2b6819f68f1..debe76a50fe 100644 --- a/tools/e2e/operations-workflow-boundary.mts +++ b/tools/e2e/operations-workflow-boundary.mts @@ -578,21 +578,21 @@ export function validateBaseImagePublicationGate(workflow: OperationsWorkflow): "base-image-publication job must preserve its exact trusted-mode classifier, minimal permissions, pinned checkout, and verifier boundary", ); } - if (!needs(workflow.jobs["generate-matrix"] ?? {}).includes("base-image-publication")) { - errors.push("generate-matrix must wait for base-image-publication"); + const matrix = workflow.jobs["generate-matrix"] ?? {}; + if (needs(matrix).includes("base-image-publication")) { + errors.push("generate-matrix must not wait for base-image-publication"); } - const matrixOutputs = workflow.jobs["generate-matrix"]?.outputs ?? {}; - if ( - matrixOutputs.dcode_base_contract !== - "${{ needs.base-image-publication.outputs.dcode_base_contract }}" || - matrixOutputs.dcode_base_ref !== "${{ needs.base-image-publication.outputs.dcode_base_ref }}" - ) { - errors.push("generate-matrix must preserve the immutable Deep Agents Code base outputs"); + const matrixOutputs = matrix.outputs ?? {}; + if ("dcode_base_contract" in matrixOutputs || "dcode_base_ref" in matrixOutputs) { + errors.push("generate-matrix must not relay Deep Agents Code base outputs"); } const live = workflow.jobs.live ?? {}; + if (!sameMembers(needs(live), ["base-image-publication", "generate-matrix"])) { + errors.push("live E2E must wait for matrix generation and base-image publication"); + } if ( live.env?.NEMOCLAW_LANGCHAIN_DEEPAGENTS_CODE_SANDBOX_BASE_IMAGE_REF !== - "${{ needs.generate-matrix.outputs.dcode_base_ref }}" + "${{ needs.base-image-publication.outputs.dcode_base_ref }}" ) { errors.push("live DCode must use the selected immutable base reference"); } @@ -601,13 +601,17 @@ export function validateBaseImagePublicationGate(workflow: OperationsWorkflow): const liveSteps = live.steps ?? []; if ( evidence.if !== "${{ matrix.id == 'ubuntu-repo-cloud-langchain-deepagents-code' }}" || - evidence.env?.BASE_CONTRACT !== "${{ needs.generate-matrix.outputs.dcode_base_contract }}" || + evidence.env?.BASE_CONTRACT !== + "${{ needs.base-image-publication.outputs.dcode_base_contract }}" || !String(evidence.run ?? "").includes("dcode-base-image.json") || liveSteps.indexOf(evidence) >= liveSteps.indexOf(findStep(live, "Run live E2E tests")) || !String(upload.with?.path ?? "").includes("dcode-base-image.json") ) { errors.push("live DCode must record its immutable base contract before E2E execution"); } + if (!sameMembers(needs(workflow.jobs["staging-brev-launchable"] ?? {}), ["generate-matrix"])) { + errors.push("staging-brev-launchable must wait only for generate-matrix"); + } return errors; } diff --git a/tools/e2e/workflow-boundary.mts b/tools/e2e/workflow-boundary.mts index 94c42183e18..bfbf8f8214b 100644 --- a/tools/e2e/workflow-boundary.mts +++ b/tools/e2e/workflow-boundary.mts @@ -2443,8 +2443,10 @@ export function validateE2eWorkflow(workflowValue: unknown): string[] { if (liveTargets["runs-on"] !== "${{ matrix.runner }}") { errors.push("live job must run on the matrix runner"); } - if (liveTargets.needs !== "generate-matrix") { - errors.push("live job must depend on generate-matrix"); + if ( + !isDeepStrictEqual(liveTargets.needs, ["base-image-publication", "generate-matrix"]) + ) { + errors.push("live job must depend on base-image-publication and generate-matrix"); } if (liveTargets.if !== "${{ needs.generate-matrix.outputs.matrix != '[]' }}") { errors.push("live job must run whenever the trusted planner emits typed targets");