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
69 changes: 69 additions & 0 deletions .github/workflows/release-agent-e2e-bundle.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
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
# java-sdk release they run against. The bundle resolves
# org.conductoross:conductor-client-ai at the same version from Maven Central.
#
# Runs on the same release events as publish-release.yml. Packaging is purely
# static (no compilation), so it does not race the Maven Central publish —
# the bundle just references the coordinate.

on:
release:
types: [released, prereleased]
workflow_dispatch:
inputs:
version:
description: "Version (e.g. 5.1.0) — 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@v6

- 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: |
./e2e/release/package-e2e-bundle.sh --version "${{ steps.version.outputs.version }}"

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

- name: Generate SHA256 checksums
working-directory: e2e/release/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}" \
e2e/release/dist/*.tar.gz \
e2e/release/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 @@ -12,4 +12,7 @@ bin/
*.db
*.db-shm
*.db-wal

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

# ── Package the agent e2e suite as a standalone bundle ───────────────────────
# Builds conductor-ai-e2e-java-<version>.tar.gz: a self-contained Gradle
# project carrying the conductor-ai-e2e test sources, pinned to the published
# org.conductoross:conductor-client-ai:<version> artifact (no SDK source vendored).
#
# Downstream repos (e.g. orkes-io/orkes-conductor) download the bundle from
# the java-sdk GitHub release and run it against their own server build. This
# replaces the agentspan-sdk-e2e-java-* bundles formerly cut from
# agentspan-ai/agentspan — java-sdk is now the canonical home of these suites.
#
# Usage:
# ./e2e/release/package-e2e-bundle.sh --version 6.0.0 [--out DIR]
#
# The bundle only references the SDK by Maven coordinate, so packaging needs
# no compilation and no network — the pinned version does not have to be on
# Maven Central yet (release ordering: this can run before the publish job
# finishes staging).

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

VERSION=""
OUT_DIR="$HERE/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-java-$VERSION"
STAGE="$OUT_DIR/$NAME"

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

# The e2e sources are in the default package (no package decl), so they live
# directly under src/test/java in the standalone Gradle layout.
cp "$REPO_ROOT"/e2e/src/test/java/*.java "$STAGE/src/test/java/"

# Standalone build pins the published SDK; framework/test deps mirror
# conductor-ai-e2e/build.gradle (versions from versions.gradle) so the
# framework-bridge suites compile + link.
cat > "$STAGE/build.gradle" <<'EOF'
plugins {
id 'java'
}

group = 'org.conductoross'

java {
toolchain { languageVersion = JavaLanguageVersion.of(21) }
}

// Pinned to the java-sdk release this bundle was cut from. Override to test
// an unreleased SDK: ./gradlew test -PconductorClientAiVersion=X.Y.Z-SNAPSHOT -PuseMavenLocal
def sdkVersion = project.findProperty('conductorClientAiVersion') ?: '@VERSION@'

repositories {
if (project.hasProperty('useMavenLocal')) {
mavenLocal()
}
mavenCentral()
}

ext {
junitVersion = '5.10.3'
junitPlatformVersion = '1.10.3' // launcher must match the Jupiter 5.x platform
langchain4jVersion = '1.0.0'
googleAdkVersion = '1.3.0'
langgraph4jVersion = '1.6.0-beta5'
}

dependencies {
testImplementation "org.conductoross:conductor-client-ai:${sdkVersion}"

testImplementation "org.junit.jupiter:junit-jupiter-api:${junitVersion}"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${junitVersion}"
// Gradle 9 no longer puts the JUnit Platform launcher on the test runtime
// classpath automatically (Gradle 8 did). Declare it so the bundle runs on
// both Gradle 8 and 9.
testRuntimeOnly "org.junit.platform:junit-platform-launcher:${junitPlatformVersion}"
testImplementation 'ch.qos.logback:logback-classic:1.5.32'

// LLM frameworks: not imported by the suites directly, but the SDK's bridge
// classes (compileOnly there) need them on the runtime classpath when the
// framework-facing suites execute.
testImplementation "dev.langchain4j:langchain4j:${langchain4jVersion}"
testImplementation "dev.langchain4j:langchain4j-open-ai:${langchain4jVersion}"
testImplementation "com.google.adk:google-adk:${googleAdkVersion}"
testImplementation "org.bsc.langgraph4j:langgraph4j-core:${langgraph4jVersion}"
testImplementation "org.bsc.langgraph4j:langgraph4j-agent-executor:${langgraph4jVersion}"
}

// tool/agent parameter names are read reflectively at runtime
compileTestJava.options.compilerArgs << '-parameters'

test {
useJUnitPlatform()
testLogging {
events 'passed', 'skipped', 'failed'
exceptionFormat 'full'
}
// e2e suites are I/O-bound (LLM calls) and use unique agent/task names,
// so they can safely run concurrently.
maxParallelForks = 3
// BaseTest reads these from the environment; a -D on the gradle command
// line wins over the caller's env, and the defaults apply when neither
// is set.
environment 'CONDUCTOR_SERVER_URL',
System.getProperty('CONDUCTOR_SERVER_URL', System.getenv('CONDUCTOR_SERVER_URL') ?: 'http://localhost:8080/api')
environment 'CONDUCTOR_AGENT_LLM_MODEL',
System.getProperty('CONDUCTOR_AGENT_LLM_MODEL', System.getenv('CONDUCTOR_AGENT_LLM_MODEL') ?: 'openai/gpt-4o-mini')
}
EOF
sed -i.bak "s/@VERSION@/$VERSION/g" "$STAGE/build.gradle" && rm "$STAGE/build.gradle.bak"

cat > "$STAGE/settings.gradle" <<'EOF'
rootProject.name = 'conductor-ai-e2e-java'
EOF

# Bundle the repo's pinned Gradle wrapper so the suite is self-contained and
# runs identically regardless of the host's Gradle — no system gradle needed.
cp "$REPO_ROOT/gradlew" "$STAGE/gradlew"
cp "$REPO_ROOT/gradlew.bat" "$STAGE/gradlew.bat"
mkdir -p "$STAGE/gradle/wrapper"
cp "$REPO_ROOT/gradle/wrapper/gradle-wrapper.jar" "$STAGE/gradle/wrapper/"
cp "$REPO_ROOT/gradle/wrapper/gradle-wrapper.properties" "$STAGE/gradle/wrapper/"
chmod +x "$STAGE/gradlew"

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 → CONDUCTOR_SERVER_URL (default http://localhost:8080/api)
# - MCP testkit on http://localhost:9999/mcp (Suite4McpTools; URL is fixed
# in the suite)
# Optional:
# - CONDUCTOR_AGENT_LLM_MODEL (default openai/gpt-4o-mini); the matching provider
# API key must be configured on the SERVER — the suites never read it
# (asserted by Suite2ToolCallingCredentials).
#
# Requires only JDK 21 — the bundled Gradle wrapper (./gradlew) pins the Gradle
# version, so no system gradle is needed. Usage: ./run.sh [extra gradle args]
HERE="$(cd "$(dirname "$0")" && pwd)"
cd "$HERE"
./gradlew test \
-DCONDUCTOR_SERVER_URL="${CONDUCTOR_SERVER_URL:-http://localhost:8080/api}" \
-DCONDUCTOR_AGENT_LLM_MODEL="${CONDUCTOR_AGENT_LLM_MODEL:-openai/gpt-4o-mini}" "$@"
echo "Report: $HERE/build/reports/tests/test/index.html"
EOF
chmod +x "$STAGE/run.sh"

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

Self-contained end-to-end tests for the Conductor Java agent SDK, pinned to
release **@VERSION@**. Resolves `org.conductoross:conductor-client-ai:@VERSION@` from
Maven Central — no SDK source is vendored. Cut from
[conductor-oss/java-sdk](https://github.com/conductor-oss/java-sdk)
(`e2e/`); supersedes the `agentspan-sdk-e2e-java-*` bundles
formerly released from agentspan-ai/agentspan.

## Prerequisites (you provide these)

| Requirement | Env var | Default |
|---------------------------------|------------------------|-----------------------------|
| JDK 21 (Gradle wrapper bundled) | — | — |
| Conductor server w/ agent runtime | `CONDUCTOR_SERVER_URL` | `http://localhost:8080/api` |
| LLM model | `CONDUCTOR_AGENT_LLM_MODEL` | `openai/gpt-4o-mini` |
| MCP testkit (Suite4 only) | — (fixed in suite) | `http://localhost:9999/mcp` |

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 — the
suites intentionally never read them (`Suite2ToolCallingCredentials` asserts
this; credentials reach workers via the `runtimeMetadata` wire contract).

## Run

```bash
./run.sh # full suite
./run.sh --tests 'Suite1*' # filter, plus any gradle args
```

JUnit XML lands in `build/test-results/test/`, HTML report in
`build/reports/tests/test/`.

## Testing an unreleased SDK

```bash
./gradlew test -PconductorClientAiVersion=X.Y.Z-SNAPSHOT -PuseMavenLocal
```
EOF
sed -i.bak "s/@VERSION@/$VERSION/g" "$STAGE/README.md" && rm "$STAGE/README.md.bak"

# Tarball extracts to conductor-ai-e2e-java-<version>/ ; stage dir is removed
# so dist/ holds only the artifacts to upload.
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"
68 changes: 68 additions & 0 deletions e2e/release/test-package-e2e-bundle.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#!/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 test source from conductor-ai-e2e made it in (file-count parity)
# - the SDK is pinned at the version, with no @VERSION@ placeholder left
# - the Gradle wrapper is complete and gradlew is executable
# All checks are static + deterministic (no network, no live server, no
# compilation — the pinned SDK version need not exist on Maven Central).
# Run: ./e2e/release/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-java-$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 suite source made it into the bundle.
SRC_COUNT="$(ls "$REPO_ROOT"/e2e/src/test/java/*.java | wc -l | tr -d ' ')"
BUNDLE_COUNT="$(ls "$ROOT"/src/test/java/*.java | wc -l | tr -d ' ')"
[[ "$SRC_COUNT" == "$BUNDLE_COUNT" ]] \
|| fail "source parity: repo has $SRC_COUNT test sources, bundle has $BUNDLE_COUNT"
pass "all $SRC_COUNT test sources present"

# SDK pinned at the packaged version, no unexpanded placeholders anywhere.
grep -q "org.conductoross:conductor-client-ai:" "$ROOT/build.gradle" \
|| fail "build.gradle does not pin org.conductoross:conductor-client-ai"
grep -q "'$VERSION'" "$ROOT/build.gradle" \
|| fail "build.gradle does not pin version $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"

# Self-contained Gradle wrapper.
for f in gradlew gradlew.bat gradle/wrapper/gradle-wrapper.jar gradle/wrapper/gradle-wrapper.properties settings.gradle; do
[[ -f "$ROOT/$f" ]] || fail "missing $f"
done
[[ -x "$ROOT/gradlew" ]] || fail "gradlew not executable"
pass "gradle wrapper complete"

echo "ALL CHECKS PASSED"
Loading