diff --git a/.github/workflows/release-agent-e2e-bundle.yml b/.github/workflows/release-agent-e2e-bundle.yml new file mode 100644 index 000000000..c256901c8 --- /dev/null +++ b/.github/workflows/release-agent-e2e-bundle.yml @@ -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 diff --git a/.gitignore b/.gitignore index a8dbb7a7b..8ed8396ed 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,7 @@ bin/ *.db *.db-shm *.db-wal + +# agent e2e bundle staging output (package-e2e-bundle.sh) +e2e/release/dist/ /design/openspec diff --git a/e2e/release/package-e2e-bundle.sh b/e2e/release/package-e2e-bundle.sh new file mode 100755 index 000000000..b50e1801c --- /dev/null +++ b/e2e/release/package-e2e-bundle.sh @@ -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-.tar.gz: a self-contained Gradle +# project carrying the conductor-ai-e2e test sources, pinned to the published +# org.conductoross:conductor-client-ai: 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-/ ; 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" diff --git a/e2e/release/test-package-e2e-bundle.sh b/e2e/release/test-package-e2e-bundle.sh new file mode 100755 index 000000000..e8e1b119f --- /dev/null +++ b/e2e/release/test-package-e2e-bundle.sh @@ -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"