diff --git a/.github/actions/setup-e2e-cli/action.yml b/.github/actions/setup-e2e-cli/action.yml new file mode 100644 index 0000000000..fb894194c8 --- /dev/null +++ b/.github/actions/setup-e2e-cli/action.yml @@ -0,0 +1,29 @@ +name: Setup E2E CLI +description: Download an architecture-matched prebuilt OpenShell CLI for E2E tests + +inputs: + artifact-prefix: + description: Artifact name prefix; linux- is appended automatically + required: true + +runs: + using: composite + steps: + - name: Download prebuilt CLI + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ format('{0}-linux-{1}', inputs.artifact-prefix, runner.arch == 'X64' && 'amd64' || 'arm64') }} + path: .e2e/prebuilt-cli + + - name: Configure prebuilt CLI + shell: bash + run: | + set -euo pipefail + cli="$GITHUB_WORKSPACE/.e2e/prebuilt-cli/openshell" + if [[ ! -f "$cli" ]]; then + echo "downloaded artifact is missing $cli" >&2 + exit 1 + fi + chmod +x "$cli" + "$cli" --version + echo "OPENSHELL_BIN=$cli" >> "$GITHUB_ENV" diff --git a/.github/actions/setup-e2e-gateway/action.yml b/.github/actions/setup-e2e-gateway/action.yml new file mode 100644 index 0000000000..b0ee52392b --- /dev/null +++ b/.github/actions/setup-e2e-gateway/action.yml @@ -0,0 +1,29 @@ +name: Setup E2E Gateway +description: Download an architecture-matched prebuilt OpenShell gateway for E2E tests + +inputs: + artifact-prefix: + description: Artifact name prefix; linux- is appended automatically + required: true + +runs: + using: composite + steps: + - name: Download prebuilt gateway + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ format('{0}-linux-{1}', inputs.artifact-prefix, runner.arch == 'X64' && 'amd64' || 'arm64') }} + path: .e2e/prebuilt-gateway + + - name: Configure prebuilt gateway + shell: bash + run: | + set -euo pipefail + gateway="$GITHUB_WORKSPACE/.e2e/prebuilt-gateway/openshell-gateway" + if [[ ! -f "$gateway" ]]; then + echo "downloaded artifact is missing $gateway" >&2 + exit 1 + fi + chmod +x "$gateway" + "$gateway" --version + echo "OPENSHELL_GATEWAY_BIN=$gateway" >> "$GITHUB_ENV" diff --git a/.github/actions/setup-e2e-vm-driver/action.yml b/.github/actions/setup-e2e-vm-driver/action.yml new file mode 100644 index 0000000000..20c43cf98f --- /dev/null +++ b/.github/actions/setup-e2e-vm-driver/action.yml @@ -0,0 +1,38 @@ +name: Setup E2E VM Driver +description: Download a prebuilt OpenShell VM driver for E2E tests + +inputs: + artifact-name: + description: Artifact name to download, such as driver-vm-linux-amd64 + required: true + +runs: + using: composite + steps: + - name: Download prebuilt VM driver + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ inputs.artifact-name }} + path: .e2e/prebuilt-vm-driver + + - name: Configure prebuilt VM driver + shell: bash + run: | + set -euo pipefail + extract_dir="$GITHUB_WORKSPACE/.e2e/prebuilt-vm-driver/bin" + mkdir -p "$extract_dir" + archive="$(find "$GITHUB_WORKSPACE/.e2e/prebuilt-vm-driver" -maxdepth 1 -type f -name 'openshell-driver-vm-*.tar.gz' -print -quit)" + if [[ -z "$archive" ]]; then + echo "downloaded artifact is missing openshell-driver-vm-*.tar.gz" >&2 + find "$GITHUB_WORKSPACE/.e2e/prebuilt-vm-driver" -maxdepth 2 -type f -print >&2 || true + exit 1 + fi + tar -xzf "$archive" -C "$extract_dir" + driver="$extract_dir/openshell-driver-vm" + if [[ ! -f "$driver" ]]; then + echo "downloaded artifact is missing $driver" >&2 + exit 1 + fi + chmod +x "$driver" + "$driver" --version + echo "OPENSHELL_VM_DRIVER_BIN=$driver" >> "$GITHUB_ENV" diff --git a/.github/workflows/branch-e2e.yml b/.github/workflows/branch-e2e.yml index 28728e597f..17aa068581 100644 --- a/.github/workflows/branch-e2e.yml +++ b/.github/workflows/branch-e2e.yml @@ -73,7 +73,7 @@ jobs: build-gateway: needs: [pr_metadata] - if: needs.pr_metadata.outputs.should_run == 'true' && (needs.pr_metadata.outputs.run_core_e2e == 'true' || needs.pr_metadata.outputs.run_kubernetes_ha_e2e == 'true') + if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_any_e2e == 'true' permissions: contents: read packages: write @@ -95,38 +95,59 @@ jobs: build-cli: needs: [pr_metadata] - if: needs.pr_metadata.outputs.should_run == 'true' + if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_any_e2e == 'true' permissions: contents: read packages: write uses: ./.github/workflows/docker-build.yml with: component: cli - platform: linux/amd64 + platform: linux/amd64,linux/arm64 + + build-driver-vm-linux: + needs: [pr_metadata] + if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true' + permissions: + contents: read + packages: read + uses: ./.github/workflows/driver-vm-linux.yml + with: + image-tag: ${{ github.sha }} + checkout-ref: ${{ github.sha }} + runtime-platforms: linux-x86_64 + driver-targets: >- + [{"arch":"amd64","runner":"linux-amd64-cpu8","target":"x86_64-unknown-linux-gnu","zig_target":"x86_64-unknown-linux-gnu.2.28","platform":"linux-x86_64","guest_arch":"x86_64"}] e2e: - needs: [pr_metadata, build-gateway, build-supervisor] + needs: [pr_metadata, build-gateway, build-supervisor, build-cli, build-driver-vm-linux] if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true' permissions: + actions: read contents: read packages: read uses: ./.github/workflows/e2e-test.yml with: image-tag: ${{ github.sha }} runner: linux-arm64-cpu8 + cli-artifact-prefix: rust-binary-cli + gateway-artifact-prefix: rust-binary-gateway + vm-driver-artifact-name: driver-vm-linux-amd64 gpu-e2e: - needs: [pr_metadata, build-supervisor] + needs: [pr_metadata, build-gateway, build-supervisor, build-cli] if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_gpu_e2e == 'true' permissions: + actions: read contents: read packages: read uses: ./.github/workflows/e2e-gpu-test.yaml with: image-tag: ${{ github.sha }} + cli-artifact-prefix: rust-binary-cli + gateway-artifact-prefix: rust-binary-gateway kubernetes-e2e: - needs: [pr_metadata, build-gateway, build-supervisor] + needs: [pr_metadata, build-gateway, build-supervisor, build-cli] if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true' strategy: fail-fast: false @@ -145,6 +166,7 @@ jobs: topology: sidecar extra_helm_values: deploy/helm/openshell/ci/values-sidecar.yaml permissions: + actions: read contents: read packages: read uses: ./.github/workflows/e2e-kubernetes-test.yml @@ -153,11 +175,13 @@ jobs: job-name: Kubernetes E2E (Rust smoke, ${{ matrix.topology }}, Agent Sandbox ${{ matrix.agent_sandbox_api }}) agent-sandbox-version: ${{ matrix.agent_sandbox_version }} extra-helm-values: ${{ matrix.extra_helm_values }} + cli-artifact-prefix: rust-binary-cli kubernetes-ha-e2e: - needs: [pr_metadata, build-gateway, build-supervisor] + needs: [pr_metadata, build-gateway, build-supervisor, build-cli] if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_kubernetes_ha_e2e == 'true' permissions: + actions: read contents: read packages: read uses: ./.github/workflows/e2e-kubernetes-test.yml @@ -166,10 +190,11 @@ jobs: job-name: Kubernetes HA E2E (Rust smoke) extra-helm-values: deploy/helm/openshell/ci/values-high-availability.yaml external-postgres-secret: openshell-ha-pg + cli-artifact-prefix: rust-binary-cli core-e2e-result: name: Core E2E result - needs: [pr_metadata, build-gateway, build-supervisor, e2e, kubernetes-e2e] + needs: [pr_metadata, build-gateway, build-supervisor, build-cli, build-driver-vm-linux, e2e, kubernetes-e2e] if: always() && needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true' runs-on: ubuntu-latest steps: @@ -177,6 +202,8 @@ jobs: env: BUILD_GATEWAY_RESULT: ${{ needs.build-gateway.result }} BUILD_SUPERVISOR_RESULT: ${{ needs.build-supervisor.result }} + BUILD_CLI_RESULT: ${{ needs.build-cli.result }} + BUILD_DRIVER_VM_RESULT: ${{ needs.build-driver-vm-linux.result }} E2E_RESULT: ${{ needs.e2e.result }} KUBERNETES_E2E_RESULT: ${{ needs.kubernetes-e2e.result }} run: | @@ -185,6 +212,8 @@ jobs: for item in \ "build-gateway:$BUILD_GATEWAY_RESULT" \ "build-supervisor:$BUILD_SUPERVISOR_RESULT" \ + "build-cli:$BUILD_CLI_RESULT" \ + "build-driver-vm-linux:$BUILD_DRIVER_VM_RESULT" \ "e2e:$E2E_RESULT" \ "kubernetes-e2e:$KUBERNETES_E2E_RESULT"; do name="${item%%:*}" @@ -198,19 +227,23 @@ jobs: gpu-e2e-result: name: GPU E2E result - needs: [pr_metadata, build-supervisor, gpu-e2e] + needs: [pr_metadata, build-gateway, build-supervisor, build-cli, gpu-e2e] if: always() && needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_gpu_e2e == 'true' runs-on: ubuntu-latest steps: - name: Verify GPU E2E jobs env: + BUILD_GATEWAY_RESULT: ${{ needs.build-gateway.result }} BUILD_SUPERVISOR_RESULT: ${{ needs.build-supervisor.result }} + BUILD_CLI_RESULT: ${{ needs.build-cli.result }} GPU_E2E_RESULT: ${{ needs.gpu-e2e.result }} run: | set -euo pipefail failed=0 for item in \ + "build-gateway:$BUILD_GATEWAY_RESULT" \ "build-supervisor:$BUILD_SUPERVISOR_RESULT" \ + "build-cli:$BUILD_CLI_RESULT" \ "gpu-e2e:$GPU_E2E_RESULT"; do name="${item%%:*}" result="${item#*:}" @@ -223,7 +256,7 @@ jobs: kubernetes-ha-e2e-result: name: Kubernetes HA E2E result - needs: [pr_metadata, build-gateway, build-supervisor, kubernetes-ha-e2e] + needs: [pr_metadata, build-gateway, build-supervisor, build-cli, kubernetes-ha-e2e] if: always() && needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_kubernetes_ha_e2e == 'true' runs-on: ubuntu-latest steps: @@ -231,6 +264,7 @@ jobs: env: BUILD_GATEWAY_RESULT: ${{ needs.build-gateway.result }} BUILD_SUPERVISOR_RESULT: ${{ needs.build-supervisor.result }} + BUILD_CLI_RESULT: ${{ needs.build-cli.result }} KUBERNETES_HA_E2E_RESULT: ${{ needs.kubernetes-ha-e2e.result }} run: | set -euo pipefail @@ -238,6 +272,7 @@ jobs: for item in \ "build-gateway:$BUILD_GATEWAY_RESULT" \ "build-supervisor:$BUILD_SUPERVISOR_RESULT" \ + "build-cli:$BUILD_CLI_RESULT" \ "kubernetes-ha-e2e:$KUBERNETES_HA_E2E_RESULT"; do name="${item%%:*}" result="${item#*:}" diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 0ed09d9d4b..da8c07c0a4 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -154,7 +154,7 @@ jobs: echo "arches=${arches[*]}" echo "binary_component=$binary_component" echo "binary_name=$binary_name" - echo "artifact_prefix=rust-binary-${component}-${binary_component}" + echo "artifact_prefix=rust-binary-${component}" echo "image_tag_base=$image_tag_base" echo "features=$features" echo "has_image=$has_image" diff --git a/.github/workflows/driver-vm-linux.yml b/.github/workflows/driver-vm-linux.yml index 7b41dc70ca..7a15271d05 100644 --- a/.github/workflows/driver-vm-linux.yml +++ b/.github/workflows/driver-vm-linux.yml @@ -4,14 +4,26 @@ on: workflow_call: inputs: cargo-version: - required: true + required: false type: string + default: "" image-tag: required: true type: string checkout-ref: required: true type: string + runtime-platforms: + description: "Comma-separated VM runtime platforms to download: linux-aarch64,linux-x86_64" + required: false + type: string + default: "linux-aarch64,linux-x86_64" + driver-targets: + description: "JSON matrix entries for Linux VM driver targets" + required: false + type: string + default: >- + [{"arch":"arm64","runner":"linux-arm64-cpu8","target":"aarch64-unknown-linux-gnu","zig_target":"aarch64-unknown-linux-gnu.2.28","platform":"linux-aarch64","guest_arch":"aarch64"},{"arch":"amd64","runner":"linux-amd64-cpu8","target":"x86_64-unknown-linux-gnu","zig_target":"x86_64-unknown-linux-gnu.2.28","platform":"linux-x86_64","guest_arch":"x86_64"}] permissions: contents: read @@ -39,11 +51,29 @@ jobs: - name: Download Linux runtime tarballs env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RUNTIME_PLATFORMS: ${{ inputs['runtime-platforms'] }} run: | set -euo pipefail mkdir -p runtime-artifacts - for platform in linux-aarch64 linux-x86_64; do + runtime_platforms="${RUNTIME_PLATFORMS//[[:space:]]/}" + IFS=',' read -r -a platforms <<< "$runtime_platforms" + if [ "${#platforms[@]}" -eq 0 ]; then + echo "::error::No VM runtime platforms requested" + exit 1 + fi + for platform in "${platforms[@]}"; do + case "$platform" in + linux-aarch64 | linux-x86_64) ;; + *) + echo "::error::Unsupported VM runtime platform '$platform' in runtime-platforms" + exit 1 + ;; + esac + done + printf '%s\n' "${platforms[@]}" > runtime-artifacts/platforms.txt + + for platform in "${platforms[@]}"; do asset="vm-runtime-${platform}.tar.zst" echo "Downloading ${asset}..." if ! gh release download vm-runtime \ @@ -61,9 +91,9 @@ jobs: - name: Verify downloads run: | set -euo pipefail - for platform in linux-aarch64 linux-x86_64; do + while IFS= read -r platform; do test -f "runtime-artifacts/vm-runtime-${platform}.tar.zst" - done + done < runtime-artifacts/platforms.txt - name: Upload runtime artifacts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 @@ -77,19 +107,7 @@ jobs: needs: [download-kernel-runtime] strategy: matrix: - include: - - arch: arm64 - runner: linux-arm64-cpu8 - target: aarch64-unknown-linux-gnu - zig_target: aarch64-unknown-linux-gnu.2.28 - platform: linux-aarch64 - guest_arch: aarch64 - - arch: amd64 - runner: linux-amd64-cpu8 - target: x86_64-unknown-linux-gnu - zig_target: x86_64-unknown-linux-gnu.2.28 - platform: linux-x86_64 - guest_arch: x86_64 + include: ${{ fromJSON(inputs['driver-targets']) }} runs-on: ${{ matrix.runner }} timeout-minutes: 30 container: diff --git a/.github/workflows/e2e-gpu-test.yaml b/.github/workflows/e2e-gpu-test.yaml index bcbd96bb49..3695764dfb 100644 --- a/.github/workflows/e2e-gpu-test.yaml +++ b/.github/workflows/e2e-gpu-test.yaml @@ -7,8 +7,19 @@ on: description: "Image tag to test (typically the commit SHA)" required: true type: string + cli-artifact-prefix: + description: "Optional prebuilt CLI artifact prefix (artifact suffix is linux-)" + required: false + type: string + default: "" + gateway-artifact-prefix: + description: "Optional prebuilt gateway artifact prefix (artifact suffix is linux-)" + required: false + type: string + default: "" permissions: + actions: read contents: read packages: read @@ -58,6 +69,18 @@ jobs: steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - name: Use prebuilt OpenShell CLI + if: inputs.cli-artifact-prefix != '' + uses: ./.github/actions/setup-e2e-cli + with: + artifact-prefix: ${{ inputs.cli-artifact-prefix }} + + - name: Use prebuilt OpenShell gateway + if: inputs.gateway-artifact-prefix != '' + uses: ./.github/actions/setup-e2e-gateway + with: + artifact-prefix: ${{ inputs.gateway-artifact-prefix }} + - name: Log in to GHCR run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin diff --git a/.github/workflows/e2e-kubernetes-test.yml b/.github/workflows/e2e-kubernetes-test.yml index f8c042b2a6..23399fc2a1 100644 --- a/.github/workflows/e2e-kubernetes-test.yml +++ b/.github/workflows/e2e-kubernetes-test.yml @@ -42,8 +42,14 @@ on: required: false type: string default: "v2026.4.25" + cli-artifact-prefix: + description: "Optional prebuilt CLI artifact prefix (artifact suffix is linux-)" + required: false + type: string + default: "" permissions: + actions: read contents: read packages: read @@ -56,6 +62,7 @@ jobs: runs-on: ${{ inputs.runner }} timeout-minutes: 60 permissions: + actions: read contents: read packages: read env: @@ -68,6 +75,12 @@ jobs: with: ref: ${{ inputs['checkout-ref'] || github.sha }} + - name: Use prebuilt OpenShell CLI + if: inputs.cli-artifact-prefix != '' + uses: ./.github/actions/setup-e2e-cli + with: + artifact-prefix: ${{ inputs.cli-artifact-prefix }} + - name: Install mise run: | curl https://mise.run | MISE_VERSION=v2026.4.25 sh diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index cb7bc67d19..561b6c84d6 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -17,8 +17,24 @@ on: required: false type: string default: "" + cli-artifact-prefix: + description: "Optional prebuilt CLI artifact prefix (artifact suffix is linux-)" + required: false + type: string + default: "" + gateway-artifact-prefix: + description: "Optional prebuilt gateway artifact prefix (artifact suffix is linux-)" + required: false + type: string + default: "" + vm-driver-artifact-name: + description: "Optional prebuilt VM driver artifact name" + required: false + type: string + default: "" permissions: + actions: read contents: read packages: read @@ -63,6 +79,18 @@ jobs: ref: ${{ inputs['checkout-ref'] || github.sha }} persist-credentials: false + - name: Use prebuilt OpenShell CLI + if: inputs.cli-artifact-prefix != '' + uses: ./.github/actions/setup-e2e-cli + with: + artifact-prefix: ${{ inputs.cli-artifact-prefix }} + + - name: Use prebuilt OpenShell gateway + if: inputs.gateway-artifact-prefix != '' + uses: ./.github/actions/setup-e2e-gateway + with: + artifact-prefix: ${{ inputs.gateway-artifact-prefix }} + - name: Check out MCP conformance tests if: matrix.suite == 'mcp' uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 @@ -126,6 +154,18 @@ jobs: ref: ${{ inputs['checkout-ref'] || github.sha }} persist-credentials: false + - name: Use prebuilt OpenShell CLI + if: inputs.cli-artifact-prefix != '' + uses: ./.github/actions/setup-e2e-cli + with: + artifact-prefix: ${{ inputs.cli-artifact-prefix }} + + - name: Use prebuilt OpenShell gateway + if: inputs.gateway-artifact-prefix != '' + uses: ./.github/actions/setup-e2e-gateway + with: + artifact-prefix: ${{ inputs.gateway-artifact-prefix }} + - name: Install mise run: | curl https://mise.run | MISE_VERSION=v2026.4.25 sh @@ -222,6 +262,24 @@ jobs: ref: ${{ inputs['checkout-ref'] || github.sha }} persist-credentials: false + - name: Use prebuilt OpenShell CLI + if: inputs.cli-artifact-prefix != '' + uses: ./.github/actions/setup-e2e-cli + with: + artifact-prefix: ${{ inputs.cli-artifact-prefix }} + + - name: Use prebuilt OpenShell gateway + if: inputs.gateway-artifact-prefix != '' + uses: ./.github/actions/setup-e2e-gateway + with: + artifact-prefix: ${{ inputs.gateway-artifact-prefix }} + + - name: Use prebuilt OpenShell VM driver + if: inputs.vm-driver-artifact-name != '' + uses: ./.github/actions/setup-e2e-vm-driver + with: + artifact-name: ${{ inputs.vm-driver-artifact-name }} + - name: Enable KVM access run: | set -euo pipefail diff --git a/CI.md b/CI.md index 19fad7bd35..2eb3da7571 100644 --- a/CI.md +++ b/CI.md @@ -20,7 +20,7 @@ Three opt-in labels enable the long-running E2E suites: - `test:e2e-kubernetes` runs Kubernetes E2E with the HA Helm overlay (`replicaCount: 2` and bundled PostgreSQL) in `Branch E2E Checks` -When multiple labels are present, `Branch E2E Checks` builds the shared gateway and supervisor images once and fans out all enabled suites in parallel. +When multiple labels are present, `Branch E2E Checks` builds the shared gateway and supervisor images once, builds one CLI artifact per runner architecture, builds the Linux VM driver artifact once, and fans out all enabled suites in parallel. Docker, Podman, GPU, Rust, Python, MCP, and VM E2E jobs reuse the matching prebuilt gateway and CLI binaries instead of compiling additional debug binaries in each job; Kubernetes E2E consumes the gateway image directly and reuses the prebuilt CLI. VM E2E also reuses the prebuilt VM driver artifact and falls back to local VM-driver/runtime preparation for local runs or workflow invocations that omit the artifact. The `OpenShell / E2E` and `OpenShell / GPU E2E` required statuses are evaluated from separate suite result jobs inside that workflow. `test:e2e-kubernetes` is optional while HA behavior is under active iteration: failures are visible in the workflow run but do not publish a required CI gate status. The GitHub ruleset should require the `OpenShell / ...` statuses published by `Required CI Gates`, not the push-triggered workflow jobs directly. diff --git a/e2e/rust/e2e-docker.sh b/e2e/rust/e2e-docker.sh index 99cd6daf75..6c28868083 100755 --- a/e2e/rust/e2e-docker.sh +++ b/e2e/rust/e2e-docker.sh @@ -13,8 +13,6 @@ E2E_TEST="${OPENSHELL_E2E_DOCKER_TEST:-smoke}" E2E_FEATURES="${OPENSHELL_E2E_DOCKER_FEATURES:-e2e,e2e-docker}" DEFAULT_WORKLOAD_MANIFEST="${ROOT}/e2e/gpu/images/.build/workloads.yaml" -cargo build -p openshell-cli - if [ "${E2E_TEST}" = "gpu" ] && [ -z "${OPENSHELL_E2E_WORKLOAD_MANIFEST:-}" ] && [ ! -f "${DEFAULT_WORKLOAD_MANIFEST}" ]; then echo "note: running GPU e2e without a workload manifest; workload validation will log an explicit skip. Build one with 'mise run e2e:workloads:build' or set OPENSHELL_E2E_WORKLOAD_MANIFEST." fi diff --git a/e2e/rust/e2e-kubernetes.sh b/e2e/rust/e2e-kubernetes.sh index ec0faefed5..20343f7231 100755 --- a/e2e/rust/e2e-kubernetes.sh +++ b/e2e/rust/e2e-kubernetes.sh @@ -21,7 +21,12 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" E2E_FEATURES="${OPENSHELL_E2E_KUBERNETES_FEATURES:-e2e,e2e-host-gateway,e2e-kubernetes}" -cargo build -p openshell-cli +# Docker and Podman build their local gateway and CLI together in the shared +# gateway wrapper. Kubernetes consumes published gateway images, so only its +# local CLI needs to be built when CI has not supplied a prebuilt one. +if [ -z "${OPENSHELL_BIN:-}" ]; then + cargo build -p openshell-cli +fi test_filter=() if [ -n "${OPENSHELL_E2E_KUBE_TEST:-}" ]; then diff --git a/e2e/rust/e2e-podman.sh b/e2e/rust/e2e-podman.sh index 39b6b523a8..16796c0562 100755 --- a/e2e/rust/e2e-podman.sh +++ b/e2e/rust/e2e-podman.sh @@ -13,8 +13,6 @@ E2E_TEST="${OPENSHELL_E2E_PODMAN_TEST:-}" E2E_FEATURES="${OPENSHELL_E2E_PODMAN_FEATURES:-e2e-podman}" DEFAULT_WORKLOAD_MANIFEST="${ROOT}/e2e/gpu/images/.build/workloads.yaml" -cargo build -p openshell-cli - if [ "${E2E_TEST}" = "gpu" ] && [ -z "${OPENSHELL_E2E_WORKLOAD_MANIFEST:-}" ] && [ ! -f "${DEFAULT_WORKLOAD_MANIFEST}" ]; then echo "note: running Podman GPU e2e without a workload manifest; workload validation will log an explicit skip. Build one with 'CONTAINER_ENGINE=podman mise run e2e:workloads:build' or set OPENSHELL_E2E_WORKLOAD_MANIFEST." fi diff --git a/e2e/rust/e2e-vm.sh b/e2e/rust/e2e-vm.sh index 8005d09bd0..584c7b91cd 100755 --- a/e2e/rust/e2e-vm.sh +++ b/e2e/rust/e2e-vm.sh @@ -23,9 +23,12 @@ # mise run e2e:vm # # What the script does: -# 1. Ensures the VM runtime (libkrun + gvproxy) and bundled supervisor are staged. +# 1. When no prebuilt VM driver is supplied, ensures the VM runtime +# (libkrun + gvproxy) and bundled supervisor are staged. # 2. Builds `openshell-gateway`, `openshell-driver-vm`, and the -# `openshell` CLI with the embedded runtime. +# `openshell` CLI with the embedded runtime as needed. When CI supplies +# OPENSHELL_GATEWAY_BIN, OPENSHELL_VM_DRIVER_BIN, or OPENSHELL_BIN, the +# matching prebuilt binary is reused instead of rebuilt. # 3. On macOS, codesigns the VM driver (libkrun needs the # `com.apple.security.hypervisor` entitlement). # 4. Writes a per-run gateway config with `[openshell.drivers.vm]` @@ -35,7 +38,7 @@ # 5. Tears the gateway down and (on failure) preserves the gateway # log and every VM serial console log for post-mortem. # -# Prerequisites (handled automatically by this script if missing): +# Prerequisites (handled automatically by this script for local VM-driver builds): # - `mise run vm:setup` — downloads / builds the libkrun runtime. # - `mise run vm:supervisor` — builds the bundled sandbox supervisor. @@ -45,8 +48,9 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" source "${ROOT}/e2e/support/gateway-common.sh" COMPRESSED_DIR="${ROOT}/target/vm-runtime-compressed" -GATEWAY_BIN="${ROOT}/target/debug/openshell-gateway" -DRIVER_BIN="${ROOT}/target/debug/openshell-driver-vm" +GATEWAY_BIN="${OPENSHELL_GATEWAY_BIN:-${ROOT}/target/debug/openshell-gateway}" +DRIVER_BIN="${OPENSHELL_VM_DRIVER_BIN:-${ROOT}/target/debug/openshell-driver-vm}" +CLI_BIN="${OPENSHELL_BIN:-${ROOT}/target/debug/openshell}" E2E_TEST_OVERRIDE="${OPENSHELL_E2E_VM_TEST:-}" E2E_FEATURES="${OPENSHELL_E2E_VM_FEATURES:-e2e-vm}" @@ -73,25 +77,59 @@ if [ -n "${RUSTC_WRAPPER:-}" ] && [ "${OPENSHELL_E2E_VM_ALLOW_RUSTC_WRAPPER:-0}" unset RUSTC_WRAPPER fi -mkdir -p "${COMPRESSED_DIR}" +if [ -z "${OPENSHELL_VM_DRIVER_BIN:-}" ]; then + mkdir -p "${COMPRESSED_DIR}" -if ! find "${COMPRESSED_DIR}" -maxdepth 1 -name 'libkrun*.zst' | grep -q .; then - echo "==> Preparing embedded VM runtime (mise run vm:setup)" - mise run vm:setup + if ! find "${COMPRESSED_DIR}" -maxdepth 1 -name 'libkrun*.zst' | grep -q .; then + echo "==> Preparing embedded VM runtime (mise run vm:setup)" + mise run vm:setup + fi + + if [ ! -f "${COMPRESSED_DIR}/openshell-sandbox.zst" ]; then + echo "==> Building bundled VM supervisor (mise run vm:supervisor)" + mise run vm:supervisor + fi + + export OPENSHELL_VM_RUNTIME_COMPRESSED_DIR="${OPENSHELL_VM_RUNTIME_COMPRESSED_DIR:-${COMPRESSED_DIR}}" +else + echo "==> Skipping embedded VM runtime prep; prebuilt VM driver was supplied" fi -if [ ! -f "${COMPRESSED_DIR}/openshell-sandbox.zst" ]; then - echo "==> Building bundled VM supervisor (mise run vm:supervisor)" - mise run vm:supervisor +build_packages=() +if [ -z "${OPENSHELL_GATEWAY_BIN:-}" ]; then + build_packages+=(-p openshell-server) +else + echo "==> Using prebuilt openshell-gateway at ${GATEWAY_BIN}" +fi +if [ -z "${OPENSHELL_VM_DRIVER_BIN:-}" ]; then + build_packages+=(-p openshell-driver-vm) +else + echo "==> Using prebuilt openshell-driver-vm at ${DRIVER_BIN}" +fi +if [ -z "${OPENSHELL_BIN:-}" ]; then + build_packages+=(-p openshell-cli) +else + echo "==> Using prebuilt openshell CLI at ${CLI_BIN}" fi -export OPENSHELL_VM_RUNTIME_COMPRESSED_DIR="${OPENSHELL_VM_RUNTIME_COMPRESSED_DIR:-${COMPRESSED_DIR}}" +if [ "${#build_packages[@]}" -gt 0 ]; then + echo "==> Building ${build_packages[*]}" + cargo build "${build_packages[@]}" +fi -echo "==> Building openshell-gateway, openshell-driver-vm, openshell (CLI)" -cargo build \ - -p openshell-server \ - -p openshell-driver-vm \ - -p openshell-cli +for pair in \ + "openshell-gateway:${GATEWAY_BIN}" \ + "openshell-driver-vm:${DRIVER_BIN}" \ + "openshell CLI:${CLI_BIN}"; do + label="${pair%%:*}" + path="${pair#*:}" + if [ ! -x "${path}" ]; then + echo "ERROR: expected ${label} binary at ${path}" >&2 + exit 1 + fi +done +export OPENSHELL_BIN="${CLI_BIN}" +DRIVER_DIR="$(dirname "${DRIVER_BIN}")" if [ "$(uname -s)" = "Darwin" ]; then echo "==> Codesigning openshell-driver-vm (Hypervisor entitlement)" @@ -224,7 +262,7 @@ ttl_secs = 0 [openshell.drivers.vm] grpc_endpoint = "https://host.openshell.internal:${HOST_PORT}" -driver_dir = "${ROOT}/target/debug" +driver_dir = "${DRIVER_DIR}" state_dir = "${RUN_STATE_DIR}" guest_tls_ca = "${PKI_DIR}/ca.crt" guest_tls_cert = "${PKI_DIR}/client/tls.crt" diff --git a/e2e/rust/src/harness/binary.rs b/e2e/rust/src/harness/binary.rs index 13b8fa4515..4cf87c4c87 100644 --- a/e2e/rust/src/harness/binary.rs +++ b/e2e/rust/src/harness/binary.rs @@ -3,9 +3,9 @@ //! CLI binary resolution for e2e tests. //! -//! Resolves the `openshell` binary at `/target/debug/openshell`. -//! The binary must already be built — the `e2e:rust` mise task handles -//! this by running `cargo build -p openshell-cli` before the tests. +//! Resolves the `openshell` binary from `OPENSHELL_BIN`, or from +//! `/target/debug/openshell` for local runs. The local binary must +//! already be built — the E2E mise tasks do this before running the tests. use std::path::{Path, PathBuf}; @@ -22,17 +22,20 @@ fn workspace_root() -> PathBuf { /// Return the path to the `openshell` CLI binary. /// -/// Expects the binary at `/target/debug/openshell`. +/// Uses `OPENSHELL_BIN` when set, otherwise expects the binary at +/// `/target/debug/openshell`. /// /// # Panics /// -/// Panics if the binary is not found. Run `cargo build -p openshell-cli` -/// (or `mise run e2e:rust`) first. +/// Panics if the configured or locally-built binary is not found. pub fn openshell_bin() -> PathBuf { - let bin = workspace_root().join("target/debug/openshell"); + let bin = std::env::var_os("OPENSHELL_BIN").map_or_else( + || workspace_root().join("target/debug/openshell"), + PathBuf::from, + ); assert!( bin.is_file(), - "openshell binary not found at {} — run `cargo build -p openshell-cli` first", + "openshell binary not found at {} — set OPENSHELL_BIN or run `cargo build -p openshell-cli` first", bin.display() ); bin diff --git a/e2e/support/gateway-common.sh b/e2e/support/gateway-common.sh index a52b4c0770..6b9e6a0956 100644 --- a/e2e/support/gateway-common.sh +++ b/e2e/support/gateway-common.sh @@ -181,23 +181,31 @@ e2e_build_gateway_binaries() { target_dir="$(e2e_cargo_target_dir "${root}")" printf -v "${target_var}" '%s' "${target_dir}" - printf -v "${gateway_var}" '%s' "${target_dir}/debug/openshell-gateway" - printf -v "${cli_var}" '%s' "${target_dir}/debug/openshell" - - echo "Building openshell-gateway..." - cargo build "${jobs[@]}" \ - -p openshell-server --bin openshell-gateway + printf -v "${gateway_var}" '%s' "${OPENSHELL_GATEWAY_BIN:-${target_dir}/debug/openshell-gateway}" + printf -v "${cli_var}" '%s' "${OPENSHELL_BIN:-${target_dir}/debug/openshell}" + + if [ -z "${OPENSHELL_GATEWAY_BIN:-}" ]; then + echo "Building openshell-gateway..." + cargo build "${jobs[@]}" \ + -p openshell-server --bin openshell-gateway + else + echo "Using prebuilt openshell gateway at ${OPENSHELL_GATEWAY_BIN}" + fi - echo "Building openshell-cli..." - cargo build "${jobs[@]}" \ - -p openshell-cli + if [ -z "${OPENSHELL_BIN:-}" ]; then + echo "Building openshell-cli..." + cargo build "${jobs[@]}" \ + -p openshell-cli + else + echo "Using prebuilt openshell CLI at ${OPENSHELL_BIN}" + fi - if [ ! -x "${target_dir}/debug/openshell-gateway" ]; then - echo "ERROR: expected openshell-gateway binary at ${target_dir}/debug/openshell-gateway" >&2 + if [ ! -x "${!gateway_var}" ]; then + echo "ERROR: expected openshell-gateway binary at ${!gateway_var}" >&2 exit 1 fi - if [ ! -x "${target_dir}/debug/openshell" ]; then - echo "ERROR: expected openshell CLI binary at ${target_dir}/debug/openshell" >&2 + if [ ! -x "${!cli_var}" ]; then + echo "ERROR: expected openshell CLI binary at ${!cli_var}" >&2 exit 1 fi } diff --git a/tasks/test.toml b/tasks/test.toml index a75b05cce2..fa6779f6c4 100644 --- a/tasks/test.toml +++ b/tasks/test.toml @@ -60,14 +60,12 @@ hide = true ["e2e:rust"] description = "Run Rust CLI e2e tests against a Docker-backed gateway" run = [ - "cargo build -p openshell-cli", "e2e/with-docker-gateway.sh cargo test --manifest-path e2e/rust/Cargo.toml --features e2e-docker", ] ["e2e:websocket-conformance"] description = "Run focused WebSocket conformance e2e tests against a Docker-backed gateway" run = [ - "cargo build -p openshell-cli", "e2e/with-docker-gateway.sh cargo test --manifest-path e2e/rust/Cargo.toml --features e2e-docker --test websocket_conformance", ]