From c1b1790ba20476b525c45657e9815719bed837ce Mon Sep 17 00:00:00 2001 From: Ling-Sen Peng Date: Wed, 15 Jul 2026 16:48:25 -0700 Subject: [PATCH] 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-typescript-.tar.gz) attached to GitHub releases, so downstream repos (e.g. orkes-io/orkes-conductor) can pin the suite to the exact javascript-sdk release they run against. Replaces the agentspan-sdk-e2e-typescript bundles formerly cut from agentspan-ai/agentspan. Mirrors conductor-oss/java-sdk's conductor-ai-e2e/release/ export. - scripts/package-e2e-bundle.sh: stages e2e/ verbatim (suites already import by package name) + standalone package.json pinning @io-orkes/conductor-javascript@ from npm + jest config with NO src aliases + run.sh + README - scripts/test-package-e2e-bundle.sh: static validator (file parity, version pin, no src aliases, junit reporter; no network) - .github/workflows/release-agent-e2e-bundle.yml: package + validate + sha256 + upload on release, same triggers as release.yml Co-Authored-By: Claude Fable 5 --- .../workflows/release-agent-e2e-bundle.yml | 71 +++++++ .gitignore | 3 + scripts/package-e2e-bundle.sh | 185 ++++++++++++++++++ scripts/test-package-e2e-bundle.sh | 73 +++++++ 4 files changed, 332 insertions(+) create mode 100644 .github/workflows/release-agent-e2e-bundle.yml create mode 100755 scripts/package-e2e-bundle.sh create mode 100755 scripts/test-package-e2e-bundle.sh diff --git a/.github/workflows/release-agent-e2e-bundle.yml b/.github/workflows/release-agent-e2e-bundle.yml new file mode 100644 index 00000000..0c103a19 --- /dev/null +++ b/.github/workflows/release-agent-e2e-bundle.yml @@ -0,0 +1,71 @@ +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 javascript-sdk +# release they run against. The bundle resolves +# @io-orkes/conductor-javascript at the same version from npm. +# +# Runs on the same release events as release.yml (npm publish). Packaging is +# purely static (no install), so it does not race the npm publish — the +# bundle just references the package version. +# +# Mirrors conductor-oss/java-sdk's release-agent-e2e-bundle.yml. + +on: + release: + types: [released, prereleased] + workflow_dispatch: + inputs: + version: + description: "Version (e.g. 4.0.0-rc1) — a release vX.Y.Z must already exist to attach to" + 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 + VERSION="${{ inputs.version }}" + else + TAG="${{ github.event.release.tag_name }}" + VERSION="${TAG#v}" + fi + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "Packaging agent e2e bundle for 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: | + VERSION="${{ steps.version.outputs.version }}" + gh release upload "v${VERSION}" \ + scripts/e2e-bundle-dist/*.tar.gz \ + scripts/e2e-bundle-dist/*.sha256 \ + --repo "${{ github.repository }}" \ + --clobber diff --git a/.gitignore b/.gitignore index cc46609b..849dc0ba 100644 --- a/.gitignore +++ b/.gitignore @@ -89,3 +89,6 @@ fabric.properties # jest-junit output (unit CI uses reports/, agent e2e uses results/) reports/ results/ + +# 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 00000000..80b4c6b8 --- /dev/null +++ b/scripts/package-e2e-bundle.sh @@ -0,0 +1,185 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ── Package the agent e2e suite as a standalone bundle ─────────────────────── +# Builds conductor-ai-e2e-typescript-.tar.gz: a self-contained npm +# project carrying the e2e test sources (repo-root e2e/), pinned to the +# published @io-orkes/conductor-javascript@ package (no SDK source +# vendored). +# +# Downstream repos (e.g. orkes-io/orkes-conductor) download the bundle from +# the javascript-sdk GitHub release and run it against their own server build. +# This replaces the agentspan-sdk-e2e-typescript-* bundles formerly cut from +# agentspan-ai/agentspan — javascript-sdk is now the canonical home of these +# suites. Mirrors conductor-oss/java-sdk's conductor-ai-e2e/release/ export. +# +# Usage: +# ./scripts/package-e2e-bundle.sh --version 4.0.0-rc1 [--out DIR] +# +# Packaging is static (no compilation, no network) — the pinned version does +# not have to be on npm yet, so this can run before the publish job finishes. + +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-typescript-$VERSION" +STAGE="$OUT_DIR/$NAME" + +echo "Packaging agent e2e bundle ($NAME)..." +rm -rf "$STAGE" +mkdir -p "$STAGE" + +# Suites import the SDK by package name (@io-orkes/conductor-javascript/agents), +# so the sources copy over verbatim — the in-repo jest moduleNameMapper aliases +# to src/ simply don't exist here and imports resolve from node_modules. +cp -R "$REPO_ROOT/e2e" "$STAGE/e2e" + +cat > "$STAGE/package.json" <<'EOF' +{ + "name": "conductor-ai-e2e-typescript", + "version": "@VERSION@", + "private": true, + "scripts": { + "test": "jest --config jest.config.mjs --forceExit" + }, + "dependencies": { + "@io-orkes/conductor-javascript": "@VERSION@" + }, + "devDependencies": { + "@jest/globals": "^30.1.3", + "@types/node": "^22.0.0", + "jest": "^30.1.3", + "jest-junit": "^16.0.0", + "ts-jest": "^29.4.2", + "tsx": "^4.21.0", + "typescript": "^5.9.2", + "zod": "^3.25.76", + "zod-to-json-schema": "^3.23.5" + } +} +EOF + +# Standalone jest config: same shape as the repo's jest.e2e.config.mjs but with +# NO moduleNameMapper SDK aliases — the package import must resolve from the +# installed npm package, proving the published artifact. +cat > "$STAGE/jest.config.mjs" <<'EOF' +export default { + preset: "ts-jest", + testMatch: ["**/e2e/**/*.test.ts"], + testTimeout: 60_000, + // Credential names are unique per suite; 3 workers keeps server load + // manageable (mirrors the in-repo jest.e2e.config.mjs). + maxWorkers: 3, + reporters: [ + "default", + ["jest-junit", { outputDirectory: "results", outputName: "junit-e2e.xml" }], + ], + transform: { + "^.+\\.tsx?$": [ + "ts-jest", + // isolatedModules puts ts-jest in transpile-only mode (as in the repo's + // root tsconfig): suites dynamically import optional provider SDKs + // (@anthropic-ai/sdk, openai, @anthropic-ai/mcp) inside try/catch and + // skip when absent — full type-check would hard-fail on those + // specifiers. Package-subpath resolution (…/agents) happens at runtime + // via jest's package-exports support. + { tsconfig: { module: "commonjs", esModuleInterop: true, isolatedModules: true } }, + ], + }, +}; +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 node >= 20. Usage: ./run.sh [extra jest args] +HERE="$(cd "$(dirname "$0")" && pwd)" +cd "$HERE" +npm install --no-audit --no-fund --loglevel=error +npx jest --config jest.config.mjs --forceExit "$@" +npx tsx e2e/generate-report.ts 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 (typescript) — E2E suite @VERSION@ + +Self-contained end-to-end tests for the Conductor JavaScript/TypeScript agent +SDK, pinned to release **@VERSION@**. Resolves +`@io-orkes/conductor-javascript@@VERSION@` from npm — no SDK source is +vendored. Cut from +[conductor-oss/javascript-sdk](https://github.com/conductor-oss/javascript-sdk) +(`e2e/`); supersedes the `agentspan-sdk-e2e-typescript-*` bundles formerly +released from agentspan-ai/agentspan. + +## Prerequisites (you provide these) + +| Requirement | Env var | Default | +|-----------------------------------|------------------------|-----------------------------| +| node >= 20 | — | — | +| 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 wrappers) skip +rather than fail. + +## Run + +```bash +./run.sh # full suite +./run.sh -t 'suite1' # filter, plus any jest args +``` + +JUnit XML lands in `results/junit-e2e.xml`, HTML report in +`results/report.html`. + +## Testing an unreleased SDK + +```bash +npm install @io-orkes/conductor-javascript@ +npx jest --config jest.config.mjs --forceExit +``` +EOF + +# Stamp the version everywhere (skip binary fixtures). +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 00000000..35db0176 --- /dev/null +++ b/scripts/test-package-e2e-bundle.sh @@ -0,0 +1,73 @@ +#!/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/fixture from the repo made it in (file-count parity) +# - the SDK is pinned at the version, with no @VERSION@ placeholder left +# - package.json is valid JSON and the jest config has NO src aliases +# (imports must resolve from the installed npm package) +# 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-typescript-$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, configs, fixtures) 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" + +# SDK pinned at the packaged version, no unexpanded placeholders anywhere. +python3 -c " +import json, sys +p = json.load(open(sys.argv[1])) +assert p['dependencies']['@io-orkes/conductor-javascript'] == sys.argv[2], \ + f'pin mismatch: {p[\"dependencies\"]}' +" "$ROOT/package.json" "$VERSION" \ + || fail "package.json does not pin @io-orkes/conductor-javascript@$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" + +# The standalone jest config must resolve the SDK from node_modules — no +# moduleNameMapper aliases pointing package imports back at repo sources. +[[ -f "$ROOT/jest.config.mjs" ]] || fail "missing jest.config.mjs" +! grep -q "src/agents" "$ROOT/jest.config.mjs" \ + || fail "jest.config.mjs still aliases the in-repo SDK source" +grep -q "jest-junit" "$ROOT/jest.config.mjs" \ + || fail "jest.config.mjs missing junit reporter" +pass "jest config standalone (no src aliases), junit reporter wired" + +echo "ALL CHECKS PASSED"