Skip to content
Draft
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
56 changes: 44 additions & 12 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
name: Continuous Integration

on: [pull_request, workflow_dispatch]
on:
push:
branches:
- main
# Temporary: run CI on this branch's pushes until it's exercised via a
# real PR targeting main. Remove once that PR validates the workflow.
- integ-test-on-pr
pull_request:
branches:
- main
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: true

jobs:
Expand Down Expand Up @@ -35,9 +45,6 @@ jobs:
id: unit_tests
continue-on-error: true
env:
CONDUCTOR_AUTH_KEY: ${{ secrets.CONDUCTOR_AUTH_KEY }}
CONDUCTOR_AUTH_SECRET: ${{ secrets.CONDUCTOR_AUTH_SECRET }}
CONDUCTOR_SERVER_URL: ${{ secrets.CONDUCTOR_SERVER_URL }}
COVERAGE_FILE: ${{ env.COVERAGE_DIR }}/.coverage.unit
run: |
coverage run -m pytest tests/unit -v
Expand All @@ -46,9 +53,6 @@ jobs:
id: bc_tests
continue-on-error: true
env:
CONDUCTOR_AUTH_KEY: ${{ secrets.CONDUCTOR_AUTH_KEY }}
CONDUCTOR_AUTH_SECRET: ${{ secrets.CONDUCTOR_AUTH_SECRET }}
CONDUCTOR_SERVER_URL: ${{ secrets.CONDUCTOR_SERVER_URL }}
COVERAGE_FILE: ${{ env.COVERAGE_DIR }}/.coverage.bc
run: |
coverage run -m pytest tests/backwardcompatibility -v
Expand All @@ -57,9 +61,6 @@ jobs:
id: serdeser_tests
continue-on-error: true
env:
CONDUCTOR_AUTH_KEY: ${{ secrets.CONDUCTOR_AUTH_KEY }}
CONDUCTOR_AUTH_SECRET: ${{ secrets.CONDUCTOR_AUTH_SECRET }}
CONDUCTOR_SERVER_URL: ${{ secrets.CONDUCTOR_SERVER_URL }}
COVERAGE_FILE: ${{ env.COVERAGE_DIR }}/.coverage.serdeser
run: |
coverage run -m pytest tests/serdesertest -v
Expand Down Expand Up @@ -92,4 +93,35 @@ jobs:

- name: Check test results
if: steps.unit_tests.outcome == 'failure' || steps.bc_tests.outcome == 'failure' || steps.serdeser_tests.outcome == 'failure'
run: exit 1
run: exit 1

integration-test:
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
bucket: [test-all, long-sync, long-async, core]
name: integration-test (${{ matrix.bucket }})
env:
CONDUCTOR_SERVER_URL: ${{ vars.SDKDEV_V5_SERVER_URL }}
CONDUCTOR_AUTH_KEY: ${{ vars.SDKDEV_V5_AUTH_KEY }}
CONDUCTOR_AUTH_SECRET: ${{ secrets.SDKDEV_V5_AUTH_SECRET }}
steps:
- 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: Run integration tests
run: bash scripts/run_integration_tests.sh --bucket=${{ matrix.bucket }}
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,11 @@ line-ending = "auto"

[tool.pytest.ini_options]
pythonpath = ["src"]
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)",
]

[tool.coverage.run]
source = ["src/conductor"]
Expand Down
126 changes: 126 additions & 0 deletions scripts/run_integration_tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
#!/usr/bin/env bash
#
# Run the SDK integration test suite locally, mirroring the `integration-test`
# job in .github/workflows/pull_request.yml.
#
# The AI/agentic integration tests are excluded on purpose: they target a
# dedicated AI-enabled server (test_ai_task_types.py and test_ai_examples.py
# hardcode http://localhost:7001/api, and test_agentic_workflows.py needs an
# `openai` LLM provider configured on the server), so they don't run against
# the standard test server this suite targets.
#
# The performance test (test_update_task_v2_perf.py) is also excluded by
# default: it submits ~1000 workflows and takes several minutes. Pass
# --with-perf to include it.
#
# Server connection is read from the environment (see
# src/conductor/client/configuration/configuration.py):
# CONDUCTOR_SERVER_URL (required; defaults to http://localhost:8080/api)
# CONDUCTOR_AUTH_KEY (optional; needed for Orkes/authenticated servers)
# CONDUCTOR_AUTH_SECRET (optional; needed for Orkes/authenticated servers)
#
# Usage:
# export CONDUCTOR_SERVER_URL="http://localhost:8080/api"
# ./scripts/run_integration_tests.sh
# ./scripts/run_integration_tests.sh --with-perf # also run the perf test
#
# Buckets (--bucket=<name>): the slowest tests are split into their own buckets
# so they can run as separate parallel CI jobs and are skipped by default
# locally. Each slow bucket is path-scoped so it only imports its own module.
# core (default) everything except the slow buckets below
# long-sync sync lease-extension tests (test_lease_extension.py, ~90s)
# long-async async lease-extension tests (test_async_lease_extension.py, ~90s)
# test-all aggregate workflow-client test_all (test_workflow_client_intg.py, ~83s)
# all the full suite (no bucket filtering) — for a complete local run
#
# ./scripts/run_integration_tests.sh # fast: skips slow buckets
# ./scripts/run_integration_tests.sh --bucket=long-sync
# ./scripts/run_integration_tests.sh --bucket=all # run everything
#
# Any other arguments are passed straight through to pytest, which is handy for
# targeting a subset of tests or getting more detail on failures:
#
# # run a subset (by keyword or path) with live output
# ./scripts/run_integration_tests.sh -s -k lease
# ./scripts/run_integration_tests.sh tests/integration/test_lease_extension.py
#
# # short tracebacks + a one-line reason for every failure/skip, with live logs
# ./scripts/run_integration_tests.sh -ra --tb=short --log-cli-level=INFO
#
# # stop at the first failure instead of waiting for the whole suite
# ./scripts/run_integration_tests.sh -x --tb=long
#
# # re-run just specific tests, e.g. the ones that failed
# ./scripts/run_integration_tests.sh -s -k "upsert_group or create_role"
#
# # show the 15 slowest tests (setup/call/teardown timings) after the run
# ./scripts/run_integration_tests.sh --durations=15

set -euo pipefail

REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$REPO_ROOT"

# Slow-test files that get their own buckets / CI jobs (see --bucket above).
LEASE_SYNC=tests/integration/test_lease_extension.py
LEASE_ASYNC=tests/integration/test_async_lease_extension.py
TEST_ALL_FILE=tests/integration/test_workflow_client_intg.py

# The perf test is skipped by default; --with-perf opts back in.
perf_ignore=(--ignore=tests/integration/test_update_task_v2_perf.py)
bucket="core"
pytest_args=()
for arg in "$@"; do
case "$arg" in
--with-perf) perf_ignore=() ;;
--bucket=*) bucket="${arg#*=}" ;;
*) pytest_args+=("$arg") ;;
esac
done

# The AI/agentic tests always target a dedicated server (see header) and are
# never part of these buckets.
ai_ignore=(
--ignore=tests/integration/test_ai_task_types.py
--ignore=tests/integration/test_ai_examples.py
--ignore=tests/integration/test_agentic_workflows.py
)

# Build the target paths + selection for the chosen bucket. The slow buckets are
# path-scoped so each job only imports its own module: this keeps
# scan_for_annotated_workers from starting unrelated workers and lets the four
# buckets run in parallel against one server without stealing each other's tasks.
case "$bucket" in
core)
targets=(tests/integration)
select=("${ai_ignore[@]}" ${perf_ignore[@]+"${perf_ignore[@]}"} \
--ignore="$LEASE_SYNC" --ignore="$LEASE_ASYNC" --ignore="$TEST_ALL_FILE")
;;
all)
targets=(tests/integration)
select=("${ai_ignore[@]}" ${perf_ignore[@]+"${perf_ignore[@]}"})
;;
long-sync)
targets=("$LEASE_SYNC")
select=(-m slow_sync)
;;
long-async)
targets=("$LEASE_ASYNC")
select=(-m slow_async)
;;
test-all)
targets=("$TEST_ALL_FILE")
select=(-m slow_test_all)
;;
*)
echo "Unknown --bucket='$bucket' (expected: core, long-sync, long-async, test-all, all)" >&2
exit 2
;;
esac

# Note: the "${arr[@]+"${arr[@]}"}" form is required so empty arrays don't trip
# "unbound variable" under `set -u` on bash 3.2 (the default macOS bash).
exec python3 -m pytest -v \
"${targets[@]}" \
${select[@]+"${select[@]}"} \
${pytest_args[@]+"${pytest_args[@]}"}
2 changes: 1 addition & 1 deletion src/conductor/client/http/models/service_method.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class ServiceMethod:
'input_type': 'str',
'output_type': 'str',
'request_params': 'list[RequestParam]',
'example_input': 'dict'
'example_input': 'object'
}

attribute_map = {
Expand Down
42 changes: 37 additions & 5 deletions src/conductor/client/http/models/upsert_user_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import re # noqa: F401
import six
from dataclasses import dataclass, field, InitVar
from typing import List, Optional
from typing import Dict, List, Optional
from enum import Enum


Expand Down Expand Up @@ -30,41 +30,50 @@ class UpsertUserRequest:
name: InitVar[Optional[str]] = None
roles: InitVar[Optional[List[str]]] = None
groups: InitVar[Optional[List[str]]] = None
contact_information: InitVar[Optional[Dict[str, str]]] = None

_name: str = field(default=None, init=False)
_roles: List[str] = field(default=None, init=False)
_groups: List[str] = field(default=None, init=False)
_contact_information: Dict[str, str] = field(default=None, init=False)

swagger_types = {
'name': 'str',
'roles': 'list[str]',
'groups': 'list[str]'
'groups': 'list[str]',
'contact_information': 'dict(str, str)'
}

attribute_map = {
'name': 'name',
'roles': 'roles',
'groups': 'groups'
'groups': 'groups',
'contact_information': 'contactInformation'
}

def __init__(self, name=None, roles=None, groups=None): # noqa: E501
def __init__(self, name=None, roles=None, groups=None, contact_information=None): # noqa: E501
"""UpsertUserRequest - a model defined in Swagger""" # noqa: E501
self._name = None
self._roles = None
self._groups = None
self._contact_information = None
self.discriminator = None
self.name = name
if roles is not None:
self.roles = roles
if groups is not None:
self.groups = groups
if contact_information is not None:
self.contact_information = contact_information

def __post_init__(self, name, roles, groups):
def __post_init__(self, name, roles, groups, contact_information):
self.name = name
if roles is not None:
self.roles = roles
if groups is not None:
self.groups = groups
if contact_information is not None:
self.contact_information = contact_information

@property
def name(self):
Expand Down Expand Up @@ -139,6 +148,29 @@ def groups(self, groups):

self._groups = groups

@property
def contact_information(self):
"""Gets the contact_information of this UpsertUserRequest. # noqa: E501

User's contact information, e.g. {"email": "user@example.com"} # noqa: E501

:return: The contact_information of this UpsertUserRequest. # noqa: E501
:rtype: dict(str, str)
"""
return self._contact_information

@contact_information.setter
def contact_information(self, contact_information):
"""Sets the contact_information of this UpsertUserRequest.

User's contact information, e.g. {"email": "user@example.com"} # noqa: E501

:param contact_information: The contact_information of this UpsertUserRequest. # noqa: E501
:type: dict(str, str)
"""

self._contact_information = contact_information

def to_dict(self):
"""Returns the model properties as a dict"""
result = {}
Expand Down
Loading
Loading