Skip to content

Commit 48dfab3

Browse files
feat(e2e): export the agent e2e suite as a release bundle for downstream repos
Package the agent e2e suite (e2e/) into a self-contained, version-stamped tarball (conductor-ai-e2e-python-<version>.tar.gz) attached to GitHub releases, so downstream repos (e.g. orkes-io/orkes-conductor) can pin the suite to the exact python-sdk release they run against. Replaces the agentspan-sdk-e2e-python bundles formerly cut from agentspan-ai/agentspan. Mirrors conductor-oss/java-sdk (conductor-ai-e2e/release/) and conductor-oss/javascript-sdk (scripts/). - scripts/package-e2e-bundle.sh: stages e2e/ verbatim (suites import conductor.ai.agents by package) + requirements.txt pinning conductor-python[agents]==<version> from PyPI + run.sh + README; deps mirror the agent-e2e.yml install step - scripts/test-package-e2e-bundle.sh: static validator (file parity, py_compile, version pin, binary assets uncorrupted; no network) - .github/workflows/release-agent-e2e-bundle.yml: package + validate + sha256 + upload on release: published. NOTE: python-sdk tags have no `v` prefix; pip's PEP 440 normalization makes ==2.0.0-rc2 resolve 2.0.0rc2 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent bf55db9 commit 48dfab3

4 files changed

Lines changed: 303 additions & 0 deletions

File tree

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
name: Release Agent E2E Bundle
2+
3+
# Packages the agent e2e suite (e2e/) into a version-stamped, self-contained
4+
# tarball and attaches it to the GitHub release, so downstream repos (e.g.
5+
# orkes-io/orkes-conductor) can pin the e2e suite to the exact python-sdk
6+
# release they run against. The bundle resolves conductor-python[agents] at
7+
# the same version from PyPI.
8+
#
9+
# Runs on the same release event as release.yml (PyPI publish). Packaging is
10+
# purely static (no install), so it does not race the PyPI publish — the
11+
# bundle just references the package version.
12+
#
13+
# Mirrors conductor-oss/java-sdk and conductor-oss/javascript-sdk. Note:
14+
# python-sdk release tags carry no `v` prefix (e.g. 2.0.0-rc2), and pip's
15+
# PEP 440 normalization makes ==2.0.0-rc2 resolve the PyPI artifact 2.0.0rc2.
16+
17+
on:
18+
release:
19+
types: [published]
20+
workflow_dispatch:
21+
inputs:
22+
tag:
23+
description: "Release tag to package for and attach to (e.g. 2.0.0-rc2) — must already exist"
24+
required: true
25+
type: string
26+
27+
permissions:
28+
contents: write
29+
30+
jobs:
31+
package-e2e-bundle:
32+
runs-on: ubuntu-latest
33+
steps:
34+
- uses: actions/checkout@v4
35+
36+
- name: Determine version
37+
id: version
38+
run: |
39+
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
40+
TAG="${{ inputs.tag }}"
41+
else
42+
TAG="${{ github.event.release.tag_name }}"
43+
fi
44+
VERSION="${TAG#v}"
45+
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
46+
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
47+
echo "Packaging agent e2e bundle for tag ${TAG} (version ${VERSION})"
48+
49+
- name: Package bundle
50+
run: |
51+
./scripts/package-e2e-bundle.sh --version "${{ steps.version.outputs.version }}"
52+
53+
- name: Validate bundle
54+
run: |
55+
./scripts/test-package-e2e-bundle.sh
56+
57+
- name: Generate SHA256 checksums
58+
working-directory: scripts/e2e-bundle-dist
59+
run: |
60+
for f in *.tar.gz; do
61+
sha256sum "$f" | awk '{print $1}' > "${f}.sha256"
62+
echo " $(cat "${f}.sha256") ${f}"
63+
done
64+
65+
- name: Upload bundle to GitHub release
66+
env:
67+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
68+
run: |
69+
gh release upload "${{ steps.version.outputs.tag }}" \
70+
scripts/e2e-bundle-dist/*.tar.gz \
71+
scripts/e2e-bundle-dist/*.sha256 \
72+
--repo "${{ github.repository }}" \
73+
--clobber

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,3 +170,6 @@ codegen/
170170
.vscode/
171171
tests/unit/automator/_trial_temp/_trial_marker
172172
tests/unit/automator/_trial_temp/_trial_marker
173+
174+
# agent e2e bundle staging output (scripts/package-e2e-bundle.sh)
175+
scripts/e2e-bundle-dist/

scripts/package-e2e-bundle.sh

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# ── Package the agent e2e suite as a standalone bundle ───────────────────────
5+
# Builds conductor-ai-e2e-python-<version>.tar.gz: a self-contained pytest
6+
# project carrying the agent e2e test sources (repo-root e2e/), pinned to the
7+
# published conductor-python[agents]==<version> package from PyPI (no SDK
8+
# source vendored).
9+
#
10+
# Downstream repos (e.g. orkes-io/orkes-conductor) download the bundle from
11+
# the python-sdk GitHub release and run it against their own server build.
12+
# This replaces the agentspan-sdk-e2e-python-* bundles formerly cut from
13+
# agentspan-ai/agentspan — python-sdk is now the canonical home of these
14+
# suites. Mirrors conductor-oss/java-sdk (conductor-ai-e2e/release/) and
15+
# conductor-oss/javascript-sdk (scripts/).
16+
#
17+
# Usage:
18+
# ./scripts/package-e2e-bundle.sh --version 2.0.0-rc2 [--out DIR]
19+
#
20+
# Packaging is static (no install, no network) — the pinned version does not
21+
# have to be on PyPI yet, so this can run before the publish job finishes.
22+
# Note: pip normalizes PEP 440 spellings, so pinning ==2.0.0-rc2 resolves the
23+
# PyPI artifact 2.0.0rc2.
24+
25+
HERE="$(cd "$(dirname "$0")" && pwd)"
26+
REPO_ROOT="$(cd "$HERE/.." && pwd)"
27+
28+
VERSION=""
29+
OUT_DIR="$HERE/e2e-bundle-dist"
30+
31+
while [[ $# -gt 0 ]]; do
32+
case "$1" in
33+
--version) VERSION="$2"; shift 2 ;;
34+
--out) OUT_DIR="$2"; shift 2 ;;
35+
*) echo "ERROR: unknown arg '$1' (want --version X.Y.Z [--out DIR])" >&2; exit 1 ;;
36+
esac
37+
done
38+
39+
[[ -n "$VERSION" ]] || { echo "ERROR: --version is required" >&2; exit 1; }
40+
41+
NAME="conductor-ai-e2e-python-$VERSION"
42+
STAGE="$OUT_DIR/$NAME"
43+
44+
echo "Packaging agent e2e bundle ($NAME)..."
45+
rm -rf "$STAGE"
46+
mkdir -p "$STAGE"
47+
48+
# Suites import the SDK by package (conductor.ai.agents), so the sources copy
49+
# over verbatim — imports resolve from the installed PyPI package.
50+
cp -R "$REPO_ROOT/e2e" "$STAGE/e2e"
51+
52+
# Deps mirror the repo's agent-e2e.yml install step, with the editable SDK
53+
# install swapped for the published pin.
54+
cat > "$STAGE/requirements.txt" <<'EOF'
55+
# Pins the published SDK (with the agents extra) to the python-sdk release
56+
# this bundle was cut from.
57+
conductor-python[agents]==@VERSION@
58+
59+
# Test runner + e2e support deps (mirrors .github/workflows/agent-e2e.yml).
60+
pytest
61+
pytest-asyncio
62+
pytest-xdist
63+
pytest-rerunfailures
64+
65+
# Live MCP server used by the MCP tool suites.
66+
mcp-testkit
67+
EOF
68+
69+
cat > "$STAGE/run.sh" <<'EOF'
70+
#!/usr/bin/env bash
71+
set -euo pipefail
72+
# Runs the agent e2e suite against a live Conductor server with the agent
73+
# runtime enabled (conductor-oss >= 3.32.0-rc.8, or orkes-conductor with
74+
# agentspan.embedded=true).
75+
#
76+
# Required services (NOT started by this script):
77+
# - Conductor server → AGENTSPAN_SERVER_URL (default http://localhost:8080/api)
78+
# - mcp-testkit → MCP_TESTKIT_URL (default http://localhost:3001)
79+
# Optional:
80+
# - AGENTSPAN_LLM_MODEL (default openai/gpt-4o-mini); the provider API key
81+
# must be configured on the SERVER — the suites never read it.
82+
# - AGENTSPAN_CLI_PATH (default `agentspan` on PATH) — CLI suites skip if absent.
83+
#
84+
# Requires python 3.10–3.13 on PATH as `python` (use a venv; the harness deps
85+
# may not build on newer interpreters). Usage: ./run.sh [extra pytest args]
86+
HERE="$(cd "$(dirname "$0")" && pwd)"
87+
cd "$HERE"
88+
python -m pip install -r requirements.txt
89+
mkdir -p results
90+
python -m pytest e2e/ -v --tb=short -n 3 --dist=loadgroup \
91+
--junitxml=results/junit-e2e.xml "$@"
92+
python e2e/report_generator.py results/junit-e2e.xml results/report.html || true
93+
echo "Results: $HERE/results/junit-e2e.xml (report.html alongside)"
94+
EOF
95+
chmod +x "$STAGE/run.sh"
96+
97+
cat > "$STAGE/README.md" <<'EOF'
98+
# Conductor Agent SDK (python) — E2E suite @VERSION@
99+
100+
Self-contained end-to-end tests for the Conductor Python agent SDK, pinned to
101+
release **@VERSION@**. Resolves `conductor-python[agents]==@VERSION@` from
102+
PyPI — no SDK source is vendored. Cut from
103+
[conductor-oss/python-sdk](https://github.com/conductor-oss/python-sdk)
104+
(`e2e/`); supersedes the `agentspan-sdk-e2e-python-*` bundles formerly
105+
released from agentspan-ai/agentspan.
106+
107+
## Prerequisites (you provide these)
108+
109+
| Requirement | Env var | Default |
110+
|-----------------------------------|------------------------|-----------------------------|
111+
| python 3.10–3.13 (use a venv) | — | — |
112+
| Conductor server w/ agent runtime | `AGENTSPAN_SERVER_URL` | `http://localhost:8080/api` |
113+
| LLM model | `AGENTSPAN_LLM_MODEL` | `openai/gpt-4o-mini` |
114+
| mcp-testkit (MCP suites) | `MCP_TESTKIT_URL` | `http://localhost:3001` |
115+
| agentspan CLI (CLI suites) | `AGENTSPAN_CLI_PATH` | `agentspan` (on `PATH`) |
116+
117+
The server needs the agent runtime: conductor-oss `>= 3.32.0-rc.8`, or
118+
orkes-conductor booted with `agentspan.embedded=true`. LLM provider API keys
119+
(e.g. `OPENAI_API_KEY`) go to the **server** process, not this suite.
120+
Suites that need an absent optional service (CLI, LangGraph) skip rather
121+
than fail.
122+
123+
## Run
124+
125+
```bash
126+
python3.12 -m venv .venv && source .venv/bin/activate
127+
./run.sh # full suite
128+
./run.sh -k suite1 # filter, plus any pytest args
129+
```
130+
131+
JUnit XML lands in `results/junit-e2e.xml`, HTML report in
132+
`results/report.html`.
133+
134+
## Testing an unreleased SDK
135+
136+
```bash
137+
pip install /path/to/conductor_python-X.Y.Z-py3-none-any.whl'[agents]'
138+
python -m pytest e2e/ -v --tb=short -n 3 --dist=loadgroup
139+
```
140+
EOF
141+
142+
# Stamp the version everywhere (skip binary assets).
143+
find "$STAGE" -type f ! -name '*.png' ! -name '*.jpg' ! -name '*.jpeg' \
144+
! -name '*.gif' ! -name '*.webp' ! -name '*.pdf' -print0 \
145+
| xargs -0 sed -i.bak "s/@VERSION@/$VERSION/g"
146+
find "$STAGE" -name '*.bak' -delete
147+
148+
mkdir -p "$OUT_DIR"
149+
tar -czf "$OUT_DIR/$NAME.tar.gz" -C "$OUT_DIR" "$NAME"
150+
rm -rf "$STAGE"
151+
152+
echo "OK: $OUT_DIR/$NAME.tar.gz"

scripts/test-package-e2e-bundle.sh

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# ── Validator for package-e2e-bundle.sh ──────────────────────────────────────
5+
# Builds the bundle at a throwaway version and asserts:
6+
# - tarball exists and extracts to the expected dir
7+
# - carries an executable, syntactically-valid run.sh + README
8+
# - every e2e source/asset from the repo made it in (file-count parity)
9+
# - test sources are syntactically valid python (compile only, no imports)
10+
# - the SDK is pinned at the version (with the agents extra), and no
11+
# @VERSION@ placeholder is left anywhere
12+
# - binary assets survived the version stamping uncorrupted
13+
# All checks are static + deterministic (no network, no install, no server).
14+
# Run: ./scripts/test-package-e2e-bundle.sh
15+
16+
HERE="$(cd "$(dirname "$0")" && pwd)"
17+
REPO_ROOT="$(cd "$HERE/.." && pwd)"
18+
VERSION="9.9.9-test"
19+
WORK="$(mktemp -d)"
20+
trap 'rm -rf "$WORK"' EXIT
21+
22+
fail() { echo "FAIL: $*" >&2; exit 1; }
23+
pass() { echo " ok: $*"; }
24+
25+
"$HERE/package-e2e-bundle.sh" --version "$VERSION" --out "$WORK/dist" >/dev/null
26+
27+
NAME="conductor-ai-e2e-python-$VERSION"
28+
TAR="$WORK/dist/$NAME.tar.gz"
29+
30+
[[ -f "$TAR" ]] || fail "tarball not produced ($TAR)"
31+
pass "tarball produced"
32+
33+
mkdir -p "$WORK/x"
34+
tar -xzf "$TAR" -C "$WORK/x"
35+
ROOT="$WORK/x/$NAME"
36+
[[ -d "$ROOT" ]] || fail "tarball does not extract to $NAME/"
37+
pass "extracts to $NAME/"
38+
39+
[[ -f "$ROOT/run.sh" ]] || fail "missing run.sh"
40+
[[ -x "$ROOT/run.sh" ]] || fail "run.sh not executable"
41+
bash -n "$ROOT/run.sh" || fail "run.sh has a bash syntax error"
42+
[[ -f "$ROOT/README.md" ]] || fail "missing README.md"
43+
pass "run.sh + README present and valid"
44+
45+
# Every e2e file (sources, conftest, assets) made it into the bundle.
46+
SRC_COUNT="$(find "$REPO_ROOT/e2e" -type f | wc -l | tr -d ' ')"
47+
BUNDLE_COUNT="$(find "$ROOT/e2e" -type f | wc -l | tr -d ' ')"
48+
[[ "$SRC_COUNT" == "$BUNDLE_COUNT" ]] \
49+
|| fail "source parity: repo e2e/ has $SRC_COUNT files, bundle has $BUNDLE_COUNT"
50+
pass "all $SRC_COUNT e2e files present"
51+
52+
# Test sources must be syntactically valid python (compile only, no imports).
53+
python3 -m py_compile "$ROOT"/e2e/*.py || fail "a test file has a syntax error"
54+
pass "sources compile"
55+
56+
# SDK pinned at the packaged version with the agents extra, no unexpanded
57+
# placeholders anywhere.
58+
grep -q "conductor-python\[agents\]==$VERSION" "$ROOT/requirements.txt" \
59+
|| fail "requirements.txt does not pin conductor-python[agents]==$VERSION"
60+
if grep -rn '@VERSION@' "$ROOT" >/dev/null 2>&1; then
61+
fail "unexpanded @VERSION@ placeholder left in bundle"
62+
fi
63+
pass "SDK pinned at $VERSION, no placeholders"
64+
65+
# Binary assets are excluded from version stamping; prove none were corrupted.
66+
while IFS= read -r -d '' img; do
67+
python3 -c "
68+
import struct, sys
69+
with open(sys.argv[1], 'rb') as f:
70+
assert f.read(8) == b'\x89PNG\r\n\x1a\n', 'bad PNG header'
71+
" "$img" || fail "binary asset corrupted: $img"
72+
done < <(find "$ROOT/e2e" -name '*.png' -print0)
73+
pass "binary assets intact"
74+
75+
echo "ALL CHECKS PASSED"

0 commit comments

Comments
 (0)