Skip to content
Open
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
74 changes: 74 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 E2E_TEST_OSS_CONDUCTOR_VERSION org var)'
required: false
type: string

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
Expand Down Expand Up @@ -133,3 +138,72 @@ 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 (which matrix-splits
# into 4 parallel buckets to spread load against the shared dev server),
# this runs the whole suite as a single --bucket=all job: the local OSS
# stack is dedicated to this one run, so there's no shared-server
# contention to justify paying for 4x the Docker-stack startup cost.
# --bucket=all also means the "server doesn't reliably fire a task timeout
# on a CI-bounded timeline" carve-out (server_timeout_unreliable, see
# scripts/run_integration_tests.sh) is skipped -- confirmed empirically
# that a local, non-shared OSS instance times those tasks out reliably, so
# the carve-out doesn't apply here. Confirmed empirically end-to-end: 20
# passed, 70 skipped, 0 failed in ~3m53s.
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"
OSS_CONDUCTOR_VERSION: ${{ inputs.oss_conductor_version || vars.E2E_TEST_OSS_CONDUCTOR_VERSION }}
steps:
- name: Verify OSS Conductor version is set
run: |
if [ -z "$OSS_CONDUCTOR_VERSION" ]; then
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
echo "Using conductoross/conductor:$OSS_CONDUCTOR_VERSION"

- 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

- 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 120 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
-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
41 changes: 41 additions & 0 deletions scripts/docker-compose-oss.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# 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).
#
# OSS_CONDUCTOR_VERSION defaults to `latest` for local runs; CI pins it via the
# E2E_TEST_OSS_CONDUCTOR_VERSION org variable (or a workflow_dispatch input).
services:
conductor-server:
image: conductoross/conductor:${OSS_CONDUCTOR_VERSION:-latest}
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:latest
expose:
- "8081"
82 changes: 82 additions & 0 deletions scripts/run-integration-oss.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#!/usr/bin/env bash
#
# Spin up a local Conductor OSS stack and run the SDK integration suite
# against it, mirroring the `integration-tests-oss` job in
# .github/workflows/pull_request.yml. Orkes-Enterprise-only tests/classes/
# modules (Authorization, Secrets, Schema, Service Registry,
# metadata/scheduler tags) check `os.environ.get('CONDUCTOR_SERVER_TYPE')`
# directly and skip themselves when it's "oss" (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.
#
# Usage:
# scripts/run-integration-oss.sh [--keep-up] [--version <tag>] [-- pytest args]
# Examples:
# scripts/run-integration-oss.sh # run scripts/run_integration_tests.sh --bucket=core against `latest`
# scripts/run-integration-oss.sh --version 3.32.0-rc18
# scripts/run-integration-oss.sh --keep-up
# scripts/run-integration-oss.sh -- --bucket=all
set -euo pipefail

KEEP_UP=0
extra=()

while [[ $# -gt 0 ]]; do
case "$1" in
--keep-up) KEEP_UP=1; shift ;;
--version) OSS_CONDUCTOR_VERSION="${2:?--version needs a tag}"; shift 2 ;;
-h|--help)
echo "Usage: $0 [--keep-up] [--version <tag>] [-- pytest args]"
exit 0
;;
--) shift; extra=("$@"); break ;;
*) echo "Unknown argument: $1" >&2; exit 1 ;;
esac
done

export OSS_CONDUCTOR_VERSION="${OSS_CONDUCTOR_VERSION:-latest}"

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() {
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
}
trap cleanup EXIT

echo "Starting Conductor OSS stack (conductoross/conductor:${OSS_CONDUCTOR_VERSION})..."
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
compose logs conductor-server || true
exit 1
fi
sleep 5
done
echo "Conductor is up."

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

bash scripts/run_integration_tests.sh ${extra[@]+"${extra[@]}"}
21 changes: 18 additions & 3 deletions tests/integration/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,25 @@ End-to-end integration tests that run against a **real Conductor server**.

### 1. Conductor Server Running

**Option A: Local Conductor (Docker)**
**Option A: Local Conductor OSS (Docker Compose, recommended)**
```bash
docker run --init -p 8080:8080 -p 5000:5000 conductoross/conductor-standalone:3.15.0
scripts/run-integration-oss.sh
```
This starts a Postgres-backed Conductor OSS stack (`scripts/docker-compose-oss.yaml`),
waits for it to become healthy, and runs the suite against it with
`CONDUCTOR_SERVER_TYPE=oss` set. Orkes-Enterprise-only tests/classes/modules
(Authorization, Secrets, Schema, Service Registry, metadata/scheduler tags)
check that env var directly and skip themselves -- see the individual test
files for the specific gaps confirmed empirically against plain OSS
Conductor. The Signal API tests run on OSS too, using a WAIT-task-based
fixture (`complex_wf_signal_test_oss` and friends) instead of the
Orkes-Enterprise-only YIELD-based one -- see `_signal_test_workflow_names()`
in `tests/integration/workflow/test_workflow_execution.py`.

Pass `--version <tag>` to pin a specific
`conductoross/conductor` image, or `--keep-up` to leave the stack running
after the suite finishes; anything after `--` is forwarded to
`scripts/run_integration_tests.sh` (e.g. `-- --bucket=all`).

**Option B: Orkes Cloud**
```bash
Expand Down Expand Up @@ -408,7 +423,7 @@ curl http://localhost:8080/api/health
echo $CONDUCTOR_SERVER_URL

# Start local server
docker run --init -p 8080:8080 -p 5000:5000 conductoross/conductor-standalone:3.15.0
scripts/run-integration-oss.sh --keep-up
```

### Tests Timeout
Expand Down
66 changes: 49 additions & 17 deletions tests/integration/client/orkes/test_orkes_clients.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
import os
import time

from shortuuid import uuid
Expand Down Expand Up @@ -130,14 +131,23 @@ def run(self, deadline=None) -> None:
workflowDef, workflow, deadline=deadline)
retry_scenario('test_task_lifecycle', self.test_task_lifecycle,
deadline=deadline)
retry_scenario('test_secret_lifecycle', self.test_secret_lifecycle,
deadline=deadline)
retry_scenario('test_scheduler_lifecycle', self.test_scheduler_lifecycle,
workflowDef, deadline=deadline)
retry_scenario('test_application_lifecycle', self.test_application_lifecycle,
deadline=deadline)
retry_scenario('__test_unit_test_workflow', self.__test_unit_test_workflow,
deadline=deadline)

# Secret and Authorization (application/user/group/permission) APIs are
# not implemented by plain OSS Conductor -- confirmed empirically: every
# call 404s "No static resource api/secrets|applications|users|groups...".
# Gate these Orkes-Enterprise-only lifecycles rather than letting them
# fail against a local OSS stack.
if os.environ.get('CONDUCTOR_SERVER_TYPE') == 'oss':
return

retry_scenario('test_secret_lifecycle', self.test_secret_lifecycle,
deadline=deadline)
retry_scenario('test_application_lifecycle', self.test_application_lifecycle,
deadline=deadline)
retry_scenario('test_user_group_permissions_lifecycle',
self.test_user_group_permissions_lifecycle, workflowDef,
deadline=deadline)
Expand All @@ -147,7 +157,13 @@ def test_workflow_lifecycle(self, workflowDef, workflow):
self.__test_get_workflow_definition()
self.__test_update_workflow_definition(workflow)
self.__test_workflow_execution_lifecycle()
self.__test_workflow_tags()
# Metadata tagging (/metadata/workflow/{name}/tags) is not implemented
# by plain OSS Conductor -- confirmed empirically: every HTTP verb on
# that path 404s/500s as an unmapped route (DELETE even falls through
# to the unrelated /metadata/workflow/{name}/{version} route, taking
# "tags" as the version path segment).
if os.environ.get('CONDUCTOR_SERVER_TYPE') != 'oss':
self.__test_workflow_tags()
self.__test_unregister_workflow_definition()

def test_task_lifecycle(self):
Expand All @@ -170,7 +186,11 @@ def test_task_lifecycle(self):
assert fetchedTaskDef.description == taskDef.description
assert len(fetchedTaskDef.input_keys) == 3

self.__test_task_tags()
# Metadata tagging (/metadata/task/{name}/tags) is not implemented by
# plain OSS Conductor -- confirmed empirically (404 "No static
# resource api/metadata/task/.../tags").
if os.environ.get('CONDUCTOR_SERVER_TYPE') != 'oss':
self.__test_task_tags()
self.__test_task_execution_lifecycle()

self.metadata_client.unregister_task_def(TASK_TYPE)
Expand Down Expand Up @@ -237,16 +257,20 @@ def test_scheduler_lifecycle(self, workflowDef):
times = self.scheduler_client.get_next_few_schedule_execution_times("0 */5 * ? * *", limit=1)
assert (len(times) == 1)

tags = [
MetadataTag("sch_tag", "val"), MetadataTag("sch_tag_2", "val2")
]
self.scheduler_client.set_scheduler_tags(tags, SCHEDULE_NAME)
fetched_tags = self.scheduler_client.get_scheduler_tags(SCHEDULE_NAME)
assert len(fetched_tags) == 2

self.scheduler_client.delete_scheduler_tags(tags, SCHEDULE_NAME)
fetched_tags = self.scheduler_client.get_scheduler_tags(SCHEDULE_NAME)
assert len(fetched_tags) == 0
# Metadata tagging (/scheduler/schedules/{name}/tags) is not
# implemented by plain OSS Conductor -- confirmed empirically (404
# "No static resource api/scheduler/schedules/.../tags").
if os.environ.get('CONDUCTOR_SERVER_TYPE') != 'oss':
tags = [
MetadataTag("sch_tag", "val"), MetadataTag("sch_tag_2", "val2")
]
self.scheduler_client.set_scheduler_tags(tags, SCHEDULE_NAME)
fetched_tags = self.scheduler_client.get_scheduler_tags(SCHEDULE_NAME)
assert len(fetched_tags) == 2

self.scheduler_client.delete_scheduler_tags(tags, SCHEDULE_NAME)
fetched_tags = self.scheduler_client.get_scheduler_tags(SCHEDULE_NAME)
assert len(fetched_tags) == 0

self.scheduler_client.delete_schedule(SCHEDULE_NAME)
_assert_not_found(
Expand Down Expand Up @@ -608,7 +632,15 @@ def __test_workflow_execution_lifecycle(self):
workflow = self.workflow_client.get_workflow(workflow_uuid, False)
assert workflow.status == "RUNNING"

self.workflow_client.delete_workflow(workflow_uuid)
# archive_workflow=True (the default) requires a terminal-state workflow;
# this workflow is intentionally still RUNNING at this point (confirmed
# empirically: "Cannot archive workflow ... with status: RUNNING" on
# plain OSS Conductor), so skip archiving for this delete when running
# against OSS.
self.workflow_client.delete_workflow(
workflow_uuid,
archive_workflow=os.environ.get('CONDUCTOR_SERVER_TYPE') != 'oss'
)
_assert_not_found(
lambda: self.workflow_client.get_workflow(workflow_uuid, False), workflow_uuid
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,12 @@ def __get_proto_data(self) -> bytes:
return b'\x08\x96\x01\x12\x04\x08\x02\x10\x03'


@unittest.skipIf(
os.environ.get('CONDUCTOR_SERVER_TYPE') == 'oss',
"The Service Registry API (/service-registry) is not implemented by "
"plain OSS Conductor -- confirmed empirically (404 'No static resource "
"api/service-registry')."
)
class TestOrkesServiceRegistryClientIntg(unittest.TestCase):
"""Integration test wrapper following your existing pattern"""

Expand Down
8 changes: 8 additions & 0 deletions tests/integration/metadata/test_schema_service.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json
import logging
import os
import unittest
from conductor.client.configuration.configuration import Configuration
from conductor.client.http.api.schema_resource_api import SchemaResourceApi
Expand All @@ -20,6 +21,13 @@
}
}
}


@unittest.skipIf(
os.environ.get('CONDUCTOR_SERVER_TYPE') == 'oss',
"The Schema API (/schema) is not implemented by plain OSS Conductor "
"-- confirmed empirically (404 'No static resource api/schema')."
)
class TestOrkesSchemaClient(unittest.TestCase):

@classmethod
Expand Down
Loading
Loading