Skip to content
65 changes: 60 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,15 @@ name: CI

on:
push:
branches: [main, develop]
branches: [main, develop, e2e-against-conductor-with-local-script] # TODO: Remove e2e-against-conductor-with-local-script branch after merging
pull_request:
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

jobs:
test:
Expand Down Expand Up @@ -86,7 +92,7 @@ jobs:
integration-test:
name: Integration Tests
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
if: github.event_name == 'push'
needs: [test, build]
steps:
- name: Checkout code
Expand All @@ -105,9 +111,58 @@ jobs:
if: env.CONDUCTOR_SERVER_URL != ''
env:
CONDUCTOR_INTEGRATION: 'true'
CONDUCTOR_SERVER_URL: ${{ secrets.CONDUCTOR_SERVER_URL }}
CONDUCTOR_AUTH_KEY: ${{ secrets.CONDUCTOR_AUTH_KEY }}
CONDUCTOR_SERVER_URL: ${{ vars.CONDUCTOR_SERVER_URL }}
CONDUCTOR_AUTH_KEY: ${{ vars.CONDUCTOR_AUTH_KEY }}
CONDUCTOR_AUTH_SECRET: ${{ secrets.CONDUCTOR_AUTH_SECRET }}
run: |
bundle exec rspec spec/integration/ --format documentation
continue-on-error: true

# Integration tests (OSS): spins up Conductor OSS + Postgres via
# scripts/docker-compose-oss.yaml and runs the integration spec suite against
# it unauthenticated. The same stack can be run locally with
# scripts/run-integration-oss.sh. Unlike the cloud `integration-test`
# job above, this needs no secrets, so it runs on every push/PR and is not
# continue-on-error.
integration-tests-oss:
name: Integration Tests (OSS)
runs-on: ubuntu-latest
timeout-minutes: 30
needs: [test, build]
env:
CONDUCTOR_SERVER_URL: http://localhost:8080/api
CONDUCTOR_SERVER_TYPE: oss
CONDUCTOR_INTEGRATION: 'true'
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 Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2'
bundler-cache: true

- name: Install dependencies
run: bundle install

- 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: bundle exec rspec spec/integration/ --format documentation

- name: Dump Conductor logs
if: failure()
run: docker compose -f scripts/docker-compose-oss.yaml logs conductor-server
43 changes: 31 additions & 12 deletions lib/conductor/http/api/scheduler_resource_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,27 +67,24 @@ def delete_schedule(name)
end

# Pause a schedule
#
# Per-schedule pause/resume is PUT-mapped on OSS Conductor but GET-only
# on some Orkes Conductor deployments. PUT is tried first and a 405
# response falls back to GET, mirroring the python-sdk/csharp-sdk/rust-sdk
# clients.
# @param [String] name Schedule name
# @return [void]
def pause_schedule(name)
@api_client.call_api(
'/scheduler/schedules/{name}/pause',
'GET',
path_params: { name: name },
return_http_data_only: true
)
call_with_verb_fallback('/scheduler/schedules/{name}/pause', name)
end

# Resume a schedule
#
# See {#pause_schedule} for the PUT-with-GET-fallback rationale.
# @param [String] name Schedule name
# @return [void]
def resume_schedule(name)
@api_client.call_api(
'/scheduler/schedules/{name}/resume',
'GET',
path_params: { name: name },
return_http_data_only: true
)
call_with_verb_fallback('/scheduler/schedules/{name}/resume', name)
end

# Pause all schedules
Expand Down Expand Up @@ -205,6 +202,28 @@ def delete_tag_for_schedule(name, tags)
return_http_data_only: true
)
end

private

# Try PUT first (OSS dialect); fall back to GET on 405 (some Orkes
# deployments only accept GET for these two routes).
def call_with_verb_fallback(templated_path, name)
@api_client.call_api(
templated_path,
'PUT',
path_params: { name: name },
return_http_data_only: true
)
rescue Conductor::ApiError => e
raise unless e.status == 405

@api_client.call_api(
templated_path,
'GET',
path_params: { name: name },
return_http_data_only: true
)
end
end
end
end
Expand Down
35 changes: 35 additions & 0 deletions scripts/docker-compose-oss.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
services:
conductor-server:
image: conductoross/conductor:${OSS_CONDUCTOR_VERSION:-latest}
environment:
- CONFIG_PROP=config-postgres.properties
# Dummy, non-sensitive value so OSS's bundled env-backed SecretsDAO has
# something real to read back in spec/integration/orkes_spec.rb (get/list/
# exists). OSS Conductor has no authentication at all, so an
# unauthenticated /api/secrets/{key} read doesn't change the threat
# model versus any other unauthenticated OSS endpoint -- don't put a
# real credential here.
- CONDUCTOR_SECRET_RUBY_SDK_INTEGRATION_TEST=ruby-sdk-oss-secret-value
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
78 changes: 78 additions & 0 deletions scripts/run-integration-oss.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/usr/bin/env bash
#
# Spin up a local Conductor OSS stack and run the integration spec suite
# against it, mirroring the `integration-tests-oss` job in
# .github/workflows/ci.yml. Orkes-Enterprise-only specs/examples are skipped
# via the existing `skip: !ENV['CONDUCTOR_INTEGRATION']` pattern extended with
# an OSS-aware condition (see the individual spec files for the
# empirically-confirmed gaps).
#
# The stack (Conductor OSS + Postgres) 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>] [-- rspec args]
# Examples:
# scripts/run-integration-oss.sh
# scripts/run-integration-oss.sh --version 3.32.0-rc18
# scripts/run-integration-oss.sh --keep-up
# scripts/run-integration-oss.sh -- spec/integration/workflow_spec.rb
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>] [-- rspec 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"
export CONDUCTOR_INTEGRATION="true"

bundle exec rspec spec/integration/ --format documentation ${extra[@]+"${extra[@]}"}
6 changes: 4 additions & 2 deletions spec/integration/event_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -394,8 +394,10 @@ def skip_if_limit_reached(error)
event_api.get_queue_config(queue_type, queue_name)
end.to raise_error(Conductor::ApiError) { |e| expect(e.status).to eq(404) }
rescue Conductor::ApiError => e
# Queue operations may not be available
if e.status == 501 || e.message.include?('not supported')
# Queue operations may not be available. OSS Conductor doesn't register this route at
# all (plain 404), whereas Orkes Cloud registers it but deprecates it in favor of the
# integrations API (400/501/403 below).
if e.status == 404 || e.status == 501 || e.message.include?('not supported')
skip 'Queue configuration API not available in this environment'
elsif e.status == 400 && e.message.include?('integrations API')
skip 'Queue configuration is managed via integrations API in Orkes Cloud'
Expand Down
2 changes: 1 addition & 1 deletion spec/integration/integration_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def self.configuration
key_id = ENV.fetch('CONDUCTOR_AUTH_KEY', nil)
key_secret = ENV.fetch('CONDUCTOR_AUTH_SECRET', nil)
if key_id && key_secret
config.authentication_settings = Conductor::Configuration::AuthenticationSettings.new(
config.authentication_settings = Conductor::AuthenticationSettings.new(
key_id: key_id,
key_secret: key_secret
)
Expand Down
Loading
Loading