Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions .github/workflows/release-agent-e2e-bundle.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
name: Release Agent E2E Bundle

# Packages the agent e2e suite (e2e/) into a version-stamped, self-contained
# tarball and attaches it to the GitHub release, so downstream repos (e.g.
# orkes-io/orkes-conductor) can pin the e2e suite to the exact python-sdk
# release they run against. The bundle resolves conductor-python[agents] at
# the same version from PyPI.
#
# Runs on the same release event as release.yml (PyPI publish). Packaging is
# purely static (no install), so it does not race the PyPI publish — the
# bundle just references the package version.
#
# Mirrors conductor-oss/java-sdk and conductor-oss/javascript-sdk. Note:
# python-sdk release tags carry no `v` prefix (e.g. 2.0.0-rc2), and pip's
# PEP 440 normalization makes ==2.0.0-rc2 resolve the PyPI artifact 2.0.0rc2.

on:
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: "Release tag to package for and attach to (e.g. 2.0.0-rc2) — must already exist"
required: true
type: string

permissions:
contents: write

jobs:
package-e2e-bundle:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Determine version
id: version
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
TAG="${{ inputs.tag }}"
else
TAG="${{ github.event.release.tag_name }}"
fi
VERSION="${TAG#v}"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "Packaging agent e2e bundle for tag ${TAG} (version ${VERSION})"

- name: Package bundle
run: |
./scripts/package-e2e-bundle.sh --version "${{ steps.version.outputs.version }}"

- name: Validate bundle
run: |
./scripts/test-package-e2e-bundle.sh

- name: Generate SHA256 checksums
working-directory: scripts/e2e-bundle-dist
run: |
for f in *.tar.gz; do
sha256sum "$f" | awk '{print $1}' > "${f}.sha256"
echo " $(cat "${f}.sha256") ${f}"
done

- name: Upload bundle to GitHub release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release upload "${{ steps.version.outputs.tag }}" \
scripts/e2e-bundle-dist/*.tar.gz \
scripts/e2e-bundle-dist/*.sha256 \
--repo "${{ github.repository }}" \
--clobber
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,6 @@ codegen/
.vscode/
tests/unit/automator/_trial_temp/_trial_marker
tests/unit/automator/_trial_temp/_trial_marker

# agent e2e bundle staging output (scripts/package-e2e-bundle.sh)
scripts/e2e-bundle-dist/
152 changes: 152 additions & 0 deletions scripts/package-e2e-bundle.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
#!/usr/bin/env bash
set -euo pipefail

# ── Package the agent e2e suite as a standalone bundle ───────────────────────
# Builds conductor-ai-e2e-python-<version>.tar.gz: a self-contained pytest
# project carrying the agent e2e test sources (repo-root e2e/), pinned to the
# published conductor-python[agents]==<version> package from PyPI (no SDK
# source vendored).
#
# Downstream repos (e.g. orkes-io/orkes-conductor) download the bundle from
# the python-sdk GitHub release and run it against their own server build.
# This replaces the agentspan-sdk-e2e-python-* bundles formerly cut from
# agentspan-ai/agentspan — python-sdk is now the canonical home of these
# suites. Mirrors conductor-oss/java-sdk (conductor-ai-e2e/release/) and
# conductor-oss/javascript-sdk (scripts/).
#
# Usage:
# ./scripts/package-e2e-bundle.sh --version 2.0.0-rc2 [--out DIR]
#
# Packaging is static (no install, no network) — the pinned version does not
# have to be on PyPI yet, so this can run before the publish job finishes.
# Note: pip normalizes PEP 440 spellings, so pinning ==2.0.0-rc2 resolves the
# PyPI artifact 2.0.0rc2.

HERE="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$HERE/.." && pwd)"

VERSION=""
OUT_DIR="$HERE/e2e-bundle-dist"

while [[ $# -gt 0 ]]; do
case "$1" in
--version) VERSION="$2"; shift 2 ;;
--out) OUT_DIR="$2"; shift 2 ;;
*) echo "ERROR: unknown arg '$1' (want --version X.Y.Z [--out DIR])" >&2; exit 1 ;;
esac
done

[[ -n "$VERSION" ]] || { echo "ERROR: --version is required" >&2; exit 1; }

NAME="conductor-ai-e2e-python-$VERSION"
STAGE="$OUT_DIR/$NAME"

echo "Packaging agent e2e bundle ($NAME)..."
rm -rf "$STAGE"
mkdir -p "$STAGE"

# Suites import the SDK by package (conductor.ai.agents), so the sources copy
# over verbatim — imports resolve from the installed PyPI package.
cp -R "$REPO_ROOT/e2e" "$STAGE/e2e"

# Deps mirror the repo's agent-e2e.yml install step, with the editable SDK
# install swapped for the published pin.
cat > "$STAGE/requirements.txt" <<'EOF'
# Pins the published SDK (with the agents extra) to the python-sdk release
# this bundle was cut from.
conductor-python[agents]==@VERSION@

# Test runner + e2e support deps (mirrors .github/workflows/agent-e2e.yml).
pytest
pytest-asyncio
pytest-xdist
pytest-rerunfailures

# Live MCP server used by the MCP tool suites.
mcp-testkit
EOF

cat > "$STAGE/run.sh" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
# Runs the agent e2e suite against a live Conductor server with the agent
# runtime enabled (conductor-oss >= 3.32.0-rc.8, or orkes-conductor with
# agentspan.embedded=true).
#
# Required services (NOT started by this script):
# - Conductor server → AGENTSPAN_SERVER_URL (default http://localhost:8080/api)
# - mcp-testkit → MCP_TESTKIT_URL (default http://localhost:3001)
# Optional:
# - AGENTSPAN_LLM_MODEL (default openai/gpt-4o-mini); the provider API key
# must be configured on the SERVER — the suites never read it.
# - AGENTSPAN_CLI_PATH (default `agentspan` on PATH) — CLI suites skip if absent.
#
# Requires python 3.10–3.13 on PATH as `python` (use a venv; the harness deps
# may not build on newer interpreters). Usage: ./run.sh [extra pytest args]
HERE="$(cd "$(dirname "$0")" && pwd)"
cd "$HERE"
python -m pip install -r requirements.txt
mkdir -p results
python -m pytest e2e/ -v --tb=short -n 3 --dist=loadgroup \
--junitxml=results/junit-e2e.xml "$@"
python e2e/report_generator.py results/junit-e2e.xml results/report.html || true
echo "Results: $HERE/results/junit-e2e.xml (report.html alongside)"
EOF
chmod +x "$STAGE/run.sh"

cat > "$STAGE/README.md" <<'EOF'
# Conductor Agent SDK (python) — E2E suite @VERSION@

Self-contained end-to-end tests for the Conductor Python agent SDK, pinned to
release **@VERSION@**. Resolves `conductor-python[agents]==@VERSION@` from
PyPI — no SDK source is vendored. Cut from
[conductor-oss/python-sdk](https://github.com/conductor-oss/python-sdk)
(`e2e/`); supersedes the `agentspan-sdk-e2e-python-*` bundles formerly
released from agentspan-ai/agentspan.

## Prerequisites (you provide these)

| Requirement | Env var | Default |
|-----------------------------------|------------------------|-----------------------------|
| python 3.10–3.13 (use a venv) | — | — |
| Conductor server w/ agent runtime | `AGENTSPAN_SERVER_URL` | `http://localhost:8080/api` |
| LLM model | `AGENTSPAN_LLM_MODEL` | `openai/gpt-4o-mini` |
| mcp-testkit (MCP suites) | `MCP_TESTKIT_URL` | `http://localhost:3001` |
| agentspan CLI (CLI suites) | `AGENTSPAN_CLI_PATH` | `agentspan` (on `PATH`) |

The server needs the agent runtime: conductor-oss `>= 3.32.0-rc.8`, or
orkes-conductor booted with `agentspan.embedded=true`. LLM provider API keys
(e.g. `OPENAI_API_KEY`) go to the **server** process, not this suite.
Suites that need an absent optional service (CLI, LangGraph) skip rather
than fail.

## Run

```bash
python3.12 -m venv .venv && source .venv/bin/activate
./run.sh # full suite
./run.sh -k suite1 # filter, plus any pytest args
```

JUnit XML lands in `results/junit-e2e.xml`, HTML report in
`results/report.html`.

## Testing an unreleased SDK

```bash
pip install /path/to/conductor_python-X.Y.Z-py3-none-any.whl'[agents]'
python -m pytest e2e/ -v --tb=short -n 3 --dist=loadgroup
```
EOF

# Stamp the version everywhere (skip binary assets).
find "$STAGE" -type f ! -name '*.png' ! -name '*.jpg' ! -name '*.jpeg' \
! -name '*.gif' ! -name '*.webp' ! -name '*.pdf' -print0 \
| xargs -0 sed -i.bak "s/@VERSION@/$VERSION/g"
find "$STAGE" -name '*.bak' -delete

mkdir -p "$OUT_DIR"
tar -czf "$OUT_DIR/$NAME.tar.gz" -C "$OUT_DIR" "$NAME"
rm -rf "$STAGE"

echo "OK: $OUT_DIR/$NAME.tar.gz"
75 changes: 75 additions & 0 deletions scripts/test-package-e2e-bundle.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#!/usr/bin/env bash
set -euo pipefail

# ── Validator for package-e2e-bundle.sh ──────────────────────────────────────
# Builds the bundle at a throwaway version and asserts:
# - tarball exists and extracts to the expected dir
# - carries an executable, syntactically-valid run.sh + README
# - every e2e source/asset from the repo made it in (file-count parity)
# - test sources are syntactically valid python (compile only, no imports)
# - the SDK is pinned at the version (with the agents extra), and no
# @VERSION@ placeholder is left anywhere
# - binary assets survived the version stamping uncorrupted
# All checks are static + deterministic (no network, no install, no server).
# Run: ./scripts/test-package-e2e-bundle.sh

HERE="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$HERE/.." && pwd)"
VERSION="9.9.9-test"
WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT

fail() { echo "FAIL: $*" >&2; exit 1; }
pass() { echo " ok: $*"; }

"$HERE/package-e2e-bundle.sh" --version "$VERSION" --out "$WORK/dist" >/dev/null

NAME="conductor-ai-e2e-python-$VERSION"
TAR="$WORK/dist/$NAME.tar.gz"

[[ -f "$TAR" ]] || fail "tarball not produced ($TAR)"
pass "tarball produced"

mkdir -p "$WORK/x"
tar -xzf "$TAR" -C "$WORK/x"
ROOT="$WORK/x/$NAME"
[[ -d "$ROOT" ]] || fail "tarball does not extract to $NAME/"
pass "extracts to $NAME/"

[[ -f "$ROOT/run.sh" ]] || fail "missing run.sh"
[[ -x "$ROOT/run.sh" ]] || fail "run.sh not executable"
bash -n "$ROOT/run.sh" || fail "run.sh has a bash syntax error"
[[ -f "$ROOT/README.md" ]] || fail "missing README.md"
pass "run.sh + README present and valid"

# Every e2e file (sources, conftest, assets) made it into the bundle.
SRC_COUNT="$(find "$REPO_ROOT/e2e" -type f | wc -l | tr -d ' ')"
BUNDLE_COUNT="$(find "$ROOT/e2e" -type f | wc -l | tr -d ' ')"
[[ "$SRC_COUNT" == "$BUNDLE_COUNT" ]] \
|| fail "source parity: repo e2e/ has $SRC_COUNT files, bundle has $BUNDLE_COUNT"
pass "all $SRC_COUNT e2e files present"

# Test sources must be syntactically valid python (compile only, no imports).
python3 -m py_compile "$ROOT"/e2e/*.py || fail "a test file has a syntax error"
pass "sources compile"

# SDK pinned at the packaged version with the agents extra, no unexpanded
# placeholders anywhere.
grep -q "conductor-python\[agents\]==$VERSION" "$ROOT/requirements.txt" \
|| fail "requirements.txt does not pin conductor-python[agents]==$VERSION"
if grep -rn '@VERSION@' "$ROOT" >/dev/null 2>&1; then
fail "unexpanded @VERSION@ placeholder left in bundle"
fi
pass "SDK pinned at $VERSION, no placeholders"

# Binary assets are excluded from version stamping; prove none were corrupted.
while IFS= read -r -d '' img; do
python3 -c "
import struct, sys
with open(sys.argv[1], 'rb') as f:
assert f.read(8) == b'\x89PNG\r\n\x1a\n', 'bad PNG header'
" "$img" || fail "binary asset corrupted: $img"
done < <(find "$ROOT/e2e" -name '*.png' -print0)
pass "binary assets intact"

echo "ALL CHECKS PASSED"
Loading