diff --git a/.github/workflows/release-agent-e2e-bundle.yml b/.github/workflows/release-agent-e2e-bundle.yml new file mode 100644 index 000000000..6d044af4e --- /dev/null +++ b/.github/workflows/release-agent-e2e-bundle.yml @@ -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 diff --git a/.gitignore b/.gitignore index 09aa5181e..0dd6548f0 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/scripts/package-e2e-bundle.sh b/scripts/package-e2e-bundle.sh new file mode 100755 index 000000000..6732199a1 --- /dev/null +++ b/scripts/package-e2e-bundle.sh @@ -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-.tar.gz: a self-contained pytest +# project carrying the agent e2e test sources (repo-root e2e/), pinned to the +# published conductor-python[agents]== 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" diff --git a/scripts/test-package-e2e-bundle.sh b/scripts/test-package-e2e-bundle.sh new file mode 100755 index 000000000..910f56ddc --- /dev/null +++ b/scripts/test-package-e2e-bundle.sh @@ -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"