Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
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
107 changes: 107 additions & 0 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ on:
branches:
- main
workflow_dispatch:
inputs:
oss_conductor_version:
description: 'OSS Conductor image tag (falls back to the E2E_TEST_OSS_CONDUCTOR_VERSION org var, then to the default in scripts/docker-compose-oss.yaml on fork PRs)'
required: false
type: string

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
Expand Down Expand Up @@ -133,3 +138,105 @@ jobs:
bash scripts/run_integration_tests.sh --bucket=${{ matrix.bucket }}
-s --log-cli-level=INFO
--log-cli-format='%(asctime)s %(levelname)s %(name)s: %(message)s'

# Integration tests (OSS): spins up Conductor OSS + Postgres via
# scripts/docker-compose-oss.yaml and runs the integration suite
# unauthenticated, with Orkes-only tests gated out via
# CONDUCTOR_SERVER_TYPE=oss (see the individual test files for the
# empirically-confirmed gaps). The same stack can be run locally with
# scripts/run-integration-oss.sh.
#
# Unlike the authenticated integration-test job above, this runs the whole
# suite in one job (--bucket=all) instead of matrix-splitting it: the full
# OSS run finishes in under 5 minutes, so splitting it would just cause
# extra runner usage for no performance benefit.
#
# --bucket=all also means the server_timeout_unreliable carve-out (see
# scripts/run_integration_tests.sh) is not applied here, deliberately: that
# carve-out exists because the shared sdkdev server doesn't fire server-side
# task timeouts on a CI-bounded timeline, which doesn't hold for a dedicated
# local OSS stack. This job is the only CI coverage those cases get.
integration-tests-oss:
runs-on: ubuntu-latest
timeout-minutes: 30
env:
CONDUCTOR_SERVER_URL: http://localhost:8080/api
CONDUCTOR_SERVER_TYPE: oss
# See the comment on CONDUCTOR_HTTP2_ENABLED in the integration-test job
# above; kept consistent here even though the local OSS stack doesn't
# have the same proxy/LB in front of it.
CONDUCTOR_HTTP2_ENABLED: "false"
steps:
# OSS_CONDUCTOR_VERSION is resolved here rather than in the job `env` so
# that the two ways it can come back empty get different treatment:
#
# - Fork PR: GitHub withholds org/repo variables from pull_request runs
# on forks exactly as it withholds secrets, so vars.* is always "" for
# an outside contributor (observed in csharp-sdk#178). This job needs
# no secrets, only a tag, so leave the var unset and let the default
# baked into the `image:` line of scripts/docker-compose-oss.yaml
# apply. That is the same tag a plain local run of
# scripts/run-integration-oss.sh gets, and the one place it is
# written -- no second copy to drift out of sync here.
# - Anything else: the org variable is genuinely missing or its
# repository access policy no longer covers this repo. Fail loudly
# rather than silently drifting onto the default.
- name: Resolve OSS Conductor version
env:
REQUESTED_VERSION: ${{ inputs.oss_conductor_version || vars.E2E_TEST_OSS_CONDUCTOR_VERSION }}
IS_FORK_PR: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository }}
run: |
if [ -n "$REQUESTED_VERSION" ]; then
echo "OSS_CONDUCTOR_VERSION=${REQUESTED_VERSION}" >> "$GITHUB_ENV"
elif [ "$IS_FORK_PR" = "true" ]; then
echo "::notice::Fork PR: org variables are withheld, falling back to the default tag in scripts/docker-compose-oss.yaml"
else
echo "::error::No Conductor OSS image tag resolved. Set the E2E_TEST_OSS_CONDUCTOR_VERSION organization variable (and ensure its repository access policy includes this repo), or pass the oss_conductor_version input via workflow_dispatch."
exit 1
fi

- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e .
pip install pytest

# `docker compose up` only pulls an image when it is missing locally. On a
# GitHub-hosted runner the VM is ephemeral and starts with no cached copy
# of this image, so `up` would pull anyway and this step is redundant
# today. It is here deliberately: it costs no extra network pull (`up`
# then finds the image locally), it separates "couldn't pull the image"
# from "the stack didn't come up" into two distinct red steps, and it is
# what keeps a mutable tag from going stale if this job ever moves to a
# self-hosted runner with a warm Docker daemon -- the same reason
# scripts/run-integration-oss.sh pulls. It also prints the tag actually in
# use, which for a fork PR comes from the compose file's default.
- name: Pull Conductor OSS image
run: |
echo "Using $(docker compose -f scripts/docker-compose-oss.yaml config --images | grep -m1 '^conductoross/conductor:')"
docker compose -f scripts/docker-compose-oss.yaml pull conductor-server

- name: Start Conductor OSS stack
run: docker compose -f scripts/docker-compose-oss.yaml up -d

- name: Wait for Conductor to be healthy
run: timeout 180 bash -c 'until curl -sf http://localhost:8080/health; do sleep 5; done'

- name: Run integration tests (OSS)
run: >-
bash scripts/run_integration_tests.sh --bucket=all
Comment thread
dfont-orkes marked this conversation as resolved.
-s --log-cli-level=INFO
--log-cli-format='%(asctime)s %(levelname)s %(name)s: %(message)s'

- name: Dump Conductor logs
if: failure()
run: docker compose -f scripts/docker-compose-oss.yaml logs conductor-server
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ markers = [
"slow_sync: long-running sync lease-extension tests (~90s)",
"slow_async: long-running async lease-extension tests (~90s)",
"slow_test_all: long-running aggregate workflow-client test_all (~83s)",
"server_timeout_unreliable: depends on server-side task timeout firing in a bounded window; excluded from CI",
"server_timeout_unreliable: depends on server-side task timeout firing in a bounded window; deselected in the shared-server long-* buckets, still run against the dedicated OSS stack (--bucket=all)",
]

[tool.coverage.run]
Expand Down
49 changes: 49 additions & 0 deletions scripts/docker-compose-oss.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Conductor OSS stack used to run the SDK integration tests against open-source
# Conductor. Shared by scripts/run-integration-oss.sh and the
# integration-tests-oss job in .github/workflows/pull_request.yml.
#
# The Conductor server reaches httpbin over the compose network at
# http://httpbin:8081 (see e.g. tests/integration/client/orkes/test_orkes_service_registry_client.py
# and the complex_wf_signal_test*.json fixtures).
#
# The `image:` default below is the SINGLE place the Conductor OSS image tag is
# written. Everything that does not override OSS_CONDUCTOR_VERSION lands on it:
# a plain `scripts/run-integration-oss.sh` run, and the integration-tests-oss
# job on a fork PR (where GitHub withholds org variables). Overrides are the
# script's --version flag and, in CI, the E2E_TEST_OSS_CONDUCTOR_VERSION org
# variable or a workflow_dispatch input. Bump the tag here and both follow.
#
# Note that the org variable is currently set to `latest`, so a normal CI run
# still tracks whatever `latest` resolves to at run time; this default is what
# actually pins fork PRs and local runs until someone sets it to a real tag.
services:
conductor-server:
image: conductoross/conductor:${OSS_CONDUCTOR_VERSION:-3.32.3}
environment:
- CONFIG_PROP=config-postgres.properties
ports:
- "8080:8080"
healthcheck:
test: ["CMD", "curl", "-I", "-XGET", "http://localhost:8080/health"]
interval: 10s
timeout: 10s
retries: 20
links:
- conductor-postgres:postgresdb
depends_on:
conductor-postgres:
condition: service_healthy
conductor-postgres:
image: postgres:16
environment:
- POSTGRES_USER=conductor
- POSTGRES_PASSWORD=conductor
healthcheck:
test: timeout 5 bash -c 'cat < /dev/null > /dev/tcp/localhost/5432'
interval: 5s
timeout: 5s
retries: 12
httpbin:
image: ghcr.io/conductor-oss/httpbin:v1.0.1
expose:
- "8081"
130 changes: 130 additions & 0 deletions scripts/run-integration-oss.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
#!/usr/bin/env bash
#
# Spin up a local Conductor OSS stack and run the SDK integration suite
# against it. To reproduce the `integration-tests-oss` job in
# .github/workflows/pull_request.yml you need `-- --bucket=all`: that job runs
# the full suite, whereas this script defaults to the faster `core` bucket,
# which excludes tests/integration/test_workflow_client_intg.py -- the only
# entry point to the workflow-execution and Signal API scenarios.
#
# Orkes-Enterprise-only tests, classes, and modules (Authorization, Secrets,
# Schema, Service Registry, metadata/scheduler tags) gate themselves on
# is_oss() in tests/integration/conftest.py, which reads the
# CONDUCTOR_SERVER_TYPE this script exports below; its docstring carries the
# authoritative list of gated surface (confirmed empirically not implemented by
# plain OSS Conductor -- see the individual test files for details on each
# gap). The Signal API tests run on OSS too, using a WAIT-task-based fixture
# variant instead of the YIELD-based one used against Orkes Enterprise -- see
# _signal_test_workflow_names() in
# tests/integration/workflow/test_workflow_execution.py.
#
# The stack (Conductor OSS + Postgres + httpbin) is defined in
# scripts/docker-compose-oss.yaml and is torn down automatically on exit. That
# file's `image:` line is also where the default tag lives -- this script
# applies no default of its own, so a plain run and a fork-PR CI run land on the
# identical image.
#
# Usage:
# scripts/run-integration-oss.sh [--up-only] [--keep-up] [--version <tag>] [-- pytest args]
# Examples:
# scripts/run-integration-oss.sh -- --bucket=all # what CI runs: the full suite
# scripts/run-integration-oss.sh # faster: --bucket=core, default tag
# scripts/run-integration-oss.sh --version 3.33.0-rc1
# scripts/run-integration-oss.sh --keep-up # leave the stack up afterwards
# scripts/run-integration-oss.sh --up-only # start the stack, skip the suite
set -euo pipefail

KEEP_UP=0
UP_ONLY=0
extra=()

while [[ $# -gt 0 ]]; do
case "$1" in
--keep-up) KEEP_UP=1; shift ;;
# Bring the stack up and stop there, for pointing repeated test runs at it
# by hand. Implies --keep-up: tearing down the stack we just started would
# defeat the purpose.
--up-only) UP_ONLY=1; KEEP_UP=1; shift ;;
--version) OSS_CONDUCTOR_VERSION="${2:?--version needs a tag}"; shift 2 ;;
-h|--help)
echo "Usage: $0 [--up-only] [--keep-up] [--version <tag>] [-- pytest args]"
exit 0
;;
--) shift; extra=("$@"); break ;;
*) echo "Unknown argument: $1" >&2; exit 1 ;;
esac
done

# No default is applied here on purpose. The default tag is written once, in the
# `image:` line of scripts/docker-compose-oss.yaml, so leaving OSS_CONDUCTOR_VERSION
# unset lets compose supply it -- the same path a fork PR takes in CI. Only export
# it when the caller actually asked for a specific tag, otherwise a value set but
# not exported in the caller's shell would never reach compose anyway.
if [[ -n "${OSS_CONDUCTOR_VERSION:-}" ]]; then
export OSS_CONDUCTOR_VERSION
fi

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
COMPOSE_FILE="${SCRIPT_DIR}/docker-compose-oss.yaml"
cd "${REPO_ROOT}"

compose() { docker compose -f "${COMPOSE_FILE}" "$@"; }

cleanup() {
local status=$?
if [[ "${status}" -ne 0 ]]; then
echo "Dumping conductor-server logs (exit ${status})..." >&2
compose logs conductor-server || true
fi
if [[ "${KEEP_UP}" == "1" ]]; then
echo "--keep-up set: leaving the OSS stack running. Tear down with:"
echo " docker compose -f ${COMPOSE_FILE} down -v"
return
fi
echo "Tearing down Conductor OSS stack..."
compose down -v || true
Comment thread
dfont-orkes marked this conversation as resolved.
}
trap cleanup EXIT

# Ask compose what it resolved rather than reconstructing the tag here, so this
# stays correct whether the tag came from --version or from the compose default.
# `--images` lists every service's image and does not reliably honour a service
# filter, so select the server's by name rather than by position.
SERVER_IMAGE="$(compose config --images | grep -m1 '^conductoross/conductor:')"
echo "Using ${SERVER_IMAGE}"

# `docker compose up` only pulls an image when it is missing locally, so a
# previously-cached mutable tag (a re-pushed rc, or `latest` if that is what was
# asked for) would silently be reused instead of getting the current version.
# Pull unconditionally so the stack always reflects the tag we just printed.
echo "Pulling ${SERVER_IMAGE} to ensure it's current..."
compose pull conductor-server

echo "Starting Conductor OSS stack..."
compose up -d

echo "Waiting for Conductor to be healthy..."
HEALTH_TIMEOUT="${HEALTH_TIMEOUT:-180}"
deadline=$(( SECONDS + HEALTH_TIMEOUT ))
until curl -sf http://localhost:8080/health >/dev/null 2>&1; do
if (( SECONDS >= deadline )); then
echo "Error: Conductor did not become healthy within ${HEALTH_TIMEOUT}s." >&2
exit 1
fi
sleep 5
done
echo "Conductor is up."

export CONDUCTOR_SERVER_URL="http://localhost:8080/api"
export CONDUCTOR_SERVER_TYPE="oss"

if [[ "${UP_ONLY}" == "1" ]]; then
echo "--up-only set: stack is up, not running the suite. Point runs at it with:"
echo " export CONDUCTOR_SERVER_URL=\"${CONDUCTOR_SERVER_URL}\""
echo " export CONDUCTOR_SERVER_TYPE=\"${CONDUCTOR_SERVER_TYPE}\""
echo " bash scripts/run_integration_tests.sh --bucket=all"
exit 0
fi

bash scripts/run_integration_tests.sh ${extra[@]+"${extra[@]}"}
Loading
Loading