Skip to content

Commit cc8a463

Browse files
committed
ci(e2e): run the E2E suite against the pinned core gateway
e2e.yml and scripts/e2e/{start,stop}-gateway.sh come from sdk-go, so both SDKs test against the same published gateway setup. The core version defaults to OPENSHELL_CORE_REF, so a core bump PR tests its own pin. start-gateway.sh adds --default-image, which points the gateway's default sandbox image at the E2E image. ci.yml runs E2E on every PR and push to main. The README and CONTRIBUTING.md explain how to run it locally. Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
1 parent 9abbbae commit cc8a463

6 files changed

Lines changed: 257 additions & 1 deletion

File tree

‎.github/workflows/ci.yml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ jobs:
7070
- name: Build, verify, and install the wheel and sdist
7171
run: mise run build
7272

73+
e2e:
74+
name: e2e
75+
uses: ./.github/workflows/e2e.yml
76+
7377
# The one required status check. Its name matches ai-openshell/sdk-go, so
7478
# rulesets can require `ci` in every SDK repository regardless of the jobs
7579
# each one runs.

‎.github/workflows/e2e.yml‎

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
3+
# Runs the E2E suite against the published gateway of a core version, started
4+
# from core's deploy/docker files (scripts/e2e/start-gateway.sh). Sandboxes
5+
# run the image from tests/e2e/Dockerfile.workload, which has the same Python
6+
# as the client, so exec_python can unpickle what the client pickled.
7+
name: e2e
8+
9+
on:
10+
workflow_call:
11+
inputs:
12+
core-version:
13+
description: Core version (default the pin, OPENSHELL_CORE_REF in mise.toml)
14+
type: string
15+
default: ""
16+
workflow_dispatch:
17+
inputs:
18+
core-version:
19+
description: Core version, for example v0.1.7 (default the pin, OPENSHELL_CORE_REF in mise.toml)
20+
type: string
21+
default: ""
22+
23+
permissions:
24+
contents: read
25+
26+
jobs:
27+
e2e:
28+
name: e2e
29+
runs-on: ubuntu-latest
30+
timeout-minutes: 20
31+
steps:
32+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
33+
with:
34+
persist-credentials: false
35+
36+
- uses: jdx/mise-action@e6a8b3978addb5a52f2b4cd9d91eafa7f0ab959d # v4.2.0
37+
38+
- name: Build the sandbox image
39+
run: mise run e2e:image
40+
41+
- name: Start the gateway
42+
env:
43+
CORE_VERSION: ${{ inputs.core-version }}
44+
run: |
45+
set -euo pipefail
46+
version=${CORE_VERSION:-$(mise run -q core:version)}
47+
echo "Core version: $version" >>"$GITHUB_STEP_SUMMARY"
48+
scripts/e2e/start-gateway.sh "$version" --default-image openshell/e2e-python:dev
49+
50+
- name: Run the E2E suite
51+
env:
52+
OPENSHELL_GATEWAY: e2e
53+
run: mise run e2e
54+
55+
- name: Stop the gateway
56+
if: always()
57+
run: scripts/e2e/stop-gateway.sh
58+
59+
- name: Upload the gateway log
60+
if: failure()
61+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
62+
with:
63+
name: gateway-log
64+
path: ${{ runner.temp }}/openshell-e2e/gateway.log
65+
if-no-files-found: ignore

‎CONTRIBUTING.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ virtual environment. `mise tasks` lists every task. The ones you need most:
2020
| `mise run format` | Format the code |
2121
| `mise run test` | Unit tests; `UV_PYTHON=3.11` picks the interpreter |
2222
| `mise run build` | Build the wheel and sdist, verify them, and install them on Python 3.11 |
23+
| `mise run e2e` | E2E tests against a running gateway; see the README for setup |
2324
| `mise run proto:gen` | Regenerate the bindings from core at the pinned ref |
2425
| `mise run core:version` | Print the pinned core version |
2526

‎README.md‎

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,29 @@ reads them, and `grpcio-tools` generates the bindings:
4040
- A change of the pin shows the effect on the generated API in its diff.
4141
- `mise run proto:check` fails when the committed bindings differ from the
4242
pinned protos, and `mise run conformance:check` does the same for the
43-
vendored test fixtures.
43+
vendored test fixtures and `tests/e2e/Dockerfile.workload`.
44+
45+
## E2E tests
46+
47+
`tests/e2e` runs against a real gateway; `mise run test` skips it. CI runs it
48+
against the published gateway of the pinned core version, with the Docker
49+
compute driver. To run it locally, you need Docker with the compose plugin:
50+
51+
```shell
52+
mise run e2e:image
53+
scripts/e2e/start-gateway.sh "$(mise run -q core:version)" --default-image openshell/e2e-python:dev
54+
OPENSHELL_GATEWAY=e2e mise run e2e
55+
scripts/e2e/stop-gateway.sh
56+
```
57+
58+
`e2e:image` builds the sandbox image from `tests/e2e/Dockerfile.workload`, a
59+
copy of core's, with the Python version from `.python-version` and
60+
`cloudpickle`, so `Sandbox.exec_python` can run the client's callables.
61+
62+
`start-gateway.sh` is meant for throwaway hosts. It registers `e2e` as the
63+
active gateway, and `stop-gateway.sh` removes every sandbox container labelled
64+
with core's default namespace. Set `XDG_CONFIG_HOME` to keep your own gateway
65+
registrations apart.
4466

4567
## History
4668

‎scripts/e2e/start-gateway.sh‎

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
#!/usr/bin/env bash
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
# start-gateway.sh: run the published OpenShell gateway for a core version
5+
# with the Docker compute driver, for the E2E lane.
6+
#
7+
# Usage: start-gateway.sh <core-version> [--state-dir <dir>] [--default-image <image>]
8+
#
9+
# Uses core's own deployment files, deploy/docker/docker-compose.yml and
10+
# gateway.toml, at the core tag: an init container generates the supervisor
11+
# JWT keys, and the gateway serves plaintext gRPC on 127.0.0.1:8080 and
12+
# /readyz on 127.0.0.1:8081. The supervisor and sandbox images are pinned to
13+
# the same version instead of :latest. --default-image replaces the image that
14+
# sandboxes run when they don't name one (core's default is plain Ubuntu); the
15+
# gateway pulls it only when it isn't present locally. Then registers the
16+
# gateway as "e2e" for the SDK in $XDG_CONFIG_HOME/openshell. Waits up to 15
17+
# minutes for the gateway image of a fresh tag to be published.
18+
#
19+
# Needs docker with the compose plugin and a host path /var/lib/openshell
20+
# (core's compose file bind-mounts it at the same path). Meant for throwaway
21+
# CI hosts: it registers the gateway as the active one, and stop-gateway.sh
22+
# removes every container labelled with core's default sandbox namespace,
23+
# including sandboxes of another local gateway that uses the same namespace.
24+
set -euo pipefail
25+
26+
core_repo=${CORE_REPO:-NVIDIA/OpenShell}
27+
registry=ghcr.io/nvidia/openshell
28+
name=e2e
29+
port=8080
30+
health_port=8081
31+
32+
tag=
33+
default_image=
34+
state=${OPENSHELL_E2E_STATE:-${RUNNER_TEMP:-/tmp}/openshell-e2e}
35+
while [ $# -gt 0 ]; do
36+
case $1 in
37+
--state-dir) state=${2:?--state-dir needs a path}; shift 2 ;;
38+
--default-image) default_image=${2:?--default-image needs an image}; shift 2 ;;
39+
-*) echo "usage: $0 <core-version> [--state-dir <dir>] [--default-image <image>]" >&2; exit 2 ;;
40+
*) tag=$1; shift ;;
41+
esac
42+
done
43+
[[ "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-pre\.[0-9]+)?$ ]] || {
44+
echo "usage: $0 <core-version> (vX.Y.Z or vX.Y.Z-pre.N)" >&2
45+
exit 2
46+
}
47+
semver=${tag#v}
48+
mkdir -p "$state"
49+
echo "$tag" >"$state/core-version"
50+
51+
# Core pushes images after tagging; a proposal can be faster than that.
52+
manifest_exists() {
53+
local token
54+
token=$(curl -fsS "https://ghcr.io/token?scope=repository:nvidia/openshell/gateway:pull" | jq -r .token) || return 1
55+
curl -fsS -o /dev/null -H "Authorization: Bearer $token" \
56+
-H 'Accept: application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.oci.image.manifest.v1+json' \
57+
"https://ghcr.io/v2/nvidia/openshell/gateway/manifests/$semver"
58+
}
59+
for i in $(seq 1 15); do
60+
manifest_exists && break
61+
if [ "$i" -eq 15 ]; then
62+
echo "ERROR: gateway image for $semver not published yet, re-run later" >&2
63+
exit 1
64+
fi
65+
echo "Waiting for $registry/gateway:$semver ($i/15) ..."
66+
sleep 60
67+
done
68+
69+
# Core's deployment files at the tag.
70+
base="https://raw.githubusercontent.com/$core_repo/$tag/deploy/docker"
71+
curl -fsSL "$base/docker-compose.yml" -o "$state/docker-compose.yml"
72+
curl -fsSL "$base/gateway.toml" -o "$state/gateway.toml"
73+
sed -i.bak \
74+
-e "s#\"$registry/supervisor:latest\"#\"$registry/supervisor:$semver\"#" \
75+
-e "s#\"$registry/sandbox:latest\"#\"$registry/sandbox:$semver\"#" \
76+
"$state/gateway.toml" && rm -f "$state/gateway.toml.bak"
77+
for img in supervisor sandbox; do
78+
grep -q "\"$registry/$img:$semver\"" "$state/gateway.toml" || {
79+
echo "ERROR: could not pin the $img image in core's gateway.toml at $tag" >&2
80+
exit 1
81+
}
82+
done
83+
# Core's file binds the health server to loopback inside the container, where
84+
# the published port can't reach it. Bind it to all container interfaces; the
85+
# compose file publishes the port on the host's 127.0.0.1 only.
86+
sed -i.bak 's#^health_bind_address *= *"127\.0\.0\.1:8081"#health_bind_address = "0.0.0.0:8081"#' \
87+
"$state/gateway.toml" && rm -f "$state/gateway.toml.bak"
88+
grep -q '^health_bind_address = "0.0.0.0:8081"' "$state/gateway.toml" || {
89+
echo "ERROR: could not rebind the health server in core's gateway.toml at $tag" >&2
90+
exit 1
91+
}
92+
93+
if [ -n "$default_image" ]; then
94+
sed -i.bak "s#^default_image *= *\".*\"#default_image = \"$default_image\"#" \
95+
"$state/gateway.toml" && rm -f "$state/gateway.toml.bak"
96+
grep -qF "default_image = \"$default_image\"" "$state/gateway.toml" || {
97+
echo "ERROR: could not set default_image in core's gateway.toml at $tag" >&2
98+
exit 1
99+
}
100+
fi
101+
102+
compose() { IMAGE_TAG=$semver docker compose -p openshell-e2e -f "$state/docker-compose.yml" "$@"; }
103+
echo "Starting gateway $semver ..."
104+
compose up -d --quiet-pull
105+
106+
for i in $(seq 1 90); do
107+
if curl -fsS -o /dev/null "http://127.0.0.1:$health_port/readyz"; then
108+
break
109+
fi
110+
if [ "$i" -eq 90 ]; then
111+
echo "ERROR: gateway not ready after 180 s" >&2
112+
compose logs --no-color gateway | tail -n 80 >&2 || true
113+
exit 1
114+
fi
115+
sleep 2
116+
done
117+
echo "Gateway $semver ready on 127.0.0.1:$port."
118+
119+
config_home=${XDG_CONFIG_HOME:-$HOME/.config}
120+
dir="$config_home/openshell/gateways/$name"
121+
mkdir -p "$dir"
122+
cat >"$dir/metadata.json" <<JSON
123+
{
124+
"name": "$name",
125+
"gateway_endpoint": "http://127.0.0.1:$port",
126+
"is_remote": false,
127+
"gateway_port": $port,
128+
"auth_mode": "plaintext"
129+
}
130+
JSON
131+
printf '%s' "$name" >"$config_home/openshell/active_gateway"
132+
echo "Registered gateway '$name' in $config_home/openshell; run the suite with OPENSHELL_GATEWAY=$name."

‎scripts/e2e/stop-gateway.sh‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#!/usr/bin/env bash
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
# stop-gateway.sh: stop the E2E gateway started by start-gateway.sh, save its
5+
# log, remove the sandbox containers, and remove the "e2e" gateway
6+
# registration (and the active-gateway pointer when it still names "e2e").
7+
#
8+
# Usage: stop-gateway.sh [--state-dir <dir>]
9+
#
10+
# Writes <state-dir>/gateway.log. Safe to run when nothing is running.
11+
set -uo pipefail
12+
13+
state=${OPENSHELL_E2E_STATE:-${RUNNER_TEMP:-/tmp}/openshell-e2e}
14+
[ "${1:-}" != --state-dir ] || state=${2:?--state-dir needs a path}
15+
[ -f "$state/docker-compose.yml" ] || { echo "No E2E gateway state in $state."; exit 0; }
16+
17+
semver=$(sed 's/^v//' "$state/core-version" 2>/dev/null || echo latest)
18+
compose() { IMAGE_TAG=$semver docker compose -p openshell-e2e -f "$state/docker-compose.yml" "$@"; }
19+
20+
compose logs --no-color --timestamps >"$state/gateway.log" 2>&1
21+
echo "Gateway log: $state/gateway.log"
22+
# Sandbox containers carry the namespace label from core's gateway.toml.
23+
ids=$(docker ps -aq --filter label=openshell.sandbox_namespace=openshell)
24+
[ -z "$ids" ] || docker rm -f $ids >/dev/null
25+
compose down --volumes --remove-orphans >/dev/null 2>&1
26+
27+
config_home=${XDG_CONFIG_HOME:-$HOME/.config}
28+
rm -rf "$config_home/openshell/gateways/e2e"
29+
if [ "$(cat "$config_home/openshell/active_gateway" 2>/dev/null)" = e2e ]; then
30+
rm -f "$config_home/openshell/active_gateway"
31+
fi
32+
echo "Gateway stopped."

0 commit comments

Comments
 (0)