Skip to content
Merged
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
57 changes: 57 additions & 0 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,60 @@ jobs:
flags: integration-v4-sm
name: codecov-integration-v4-sm-node-${{ matrix.node-version }}-shard-${{ matrix.shard }}
fail_ci_if_error: false

# 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
# test:integration:oss). The same stack can be run locally with
# scripts/run-integration-oss.sh.
integration-tests-oss:
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
node-version: [20, 22, 24]
name: Node.js v${{ matrix.node-version }} - integration oss
env:
CONDUCTOR_SERVER_URL: http://localhost:8080/api
CONDUCTOR_SERVER_TYPE: oss
CONDUCTOR_REQUEST_TIMEOUT_MS: "120000"
CONDUCTOR_RETRY_SERVER_ERRORS: "true"
HTTPBIN_SERVICE_HOSTNAME: httpbin
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: "npm"
- name: Cache node_modules
id: cache
uses: actions/cache@v4
with:
path: node_modules
key: npm-${{ matrix.node-version }}-${{ hashFiles('package-lock.json') }}
restore-keys: |
npm-${{ matrix.node-version }}-
- name: Install Dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: npm ci
- 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: npm run test:integration:oss -- --ci --runInBand --testTimeout=120000 --reporters=default --reporters=github-actions --reporters=jest-junit
env:
JEST_JUNIT_OUTPUT_NAME: integration-oss-node-${{ matrix.node-version }}-test-results.xml
- name: Dump Conductor logs
if: failure()
run: docker compose -f scripts/docker-compose-oss.yaml logs conductor-server
- name: Publish Test Results
uses: dorny/test-reporter@v2
if: ${{ !cancelled() }}
with:
name: integration oss (Node ${{ matrix.node-version }})
path: reports/integration-oss-node-${{ matrix.node-version }}-test-results.xml
reporter: jest-junit
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"test:integration:base": "jest --force-exit --detectOpenHandles --testMatch='**/src/integration-tests/*.test.[jt]s?(x)'",
"test:integration:v5": "cross-env ORKES_BACKEND_VERSION=5 npm run test:integration:base --",
"test:integration:v4": "cross-env ORKES_BACKEND_VERSION=4 npm run test:integration:base --",
"test:integration:oss": "cross-env ORKES_BACKEND_VERSION=5 CONDUCTOR_SERVER_TYPE=oss npm run test:integration:base --",
"test:integration:v5:batch": "cross-env ORKES_BACKEND_VERSION=5 node scripts/run-integration-batch.mjs",
"test:integration:v4:batch": "cross-env ORKES_BACKEND_VERSION=4 node scripts/run-integration-batch.mjs",
"ci": "npm run lint && npm run test",
Expand Down
37 changes: 37 additions & 0 deletions scripts/docker-compose-oss.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# 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, which matches HTTPBIN_SERVICE_HOSTNAME=httpbin.
services:
conductor-server:
image: conductoross/conductor: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/orkes-io/test-utils/httpbin:latest
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, mirroring the `integration-tests-oss` job in
# .github/workflows/pull_request.yml. Orkes-only tests are gated out via
# CONDUCTOR_SERVER_TYPE=oss (see the test:integration:oss npm script).
#
# The stack (Conductor OSS + Postgres + httpbin) is defined in
# scripts/docker-compose-oss.yaml and is torn down automatically on exit.
#
# Test output is written to both stdout and a log file (default:
# scripts/oss-test-run.log, override with -l|--log) so it can be shared later.
#
# Usage:
# scripts/run-integration-oss.sh [-t|--test <path|pattern>] [-l|--log <file>] [--keep-up] [-- jest args]
# Examples:
# scripts/run-integration-oss.sh # full OSS-gated suite
# scripts/run-integration-oss.sh --test WorkflowExecutor
# scripts/run-integration-oss.sh --log /tmp/oss.log # custom log path
# scripts/run-integration-oss.sh --keep-up # leave the stack running afterwards
# scripts/run-integration-oss.sh -- --testPathPatterns="EventClient"
set -euo pipefail

TEST_PATTERN=""
KEEP_UP=0
LOG_FILE=""

# Print the leading comment block (everything after the shebang up to the first
# non-comment line) as help text, so it stays in sync with the header above.
usage() { awk 'NR>1 && /^#/ {sub(/^# ?/, ""); print; next} NR>1 {exit}' "$0"; }

extra=()
while [[ $# -gt 0 ]]; do
case "$1" in
-t|--test) TEST_PATTERN="${2:?--test needs a path or pattern}"; shift 2 ;;
-l|--log) LOG_FILE="${2:?--log needs a file path}"; shift 2 ;;
--keep-up) KEEP_UP=1; shift ;;
-h|--help) usage; exit 0 ;;
--) shift; extra=("$@"); break ;;
*) echo "Unknown argument: $1" >&2; usage; exit 1 ;;
esac
done

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

# Guard: this script spins up and tests against a *local* OSS stack. If enterprise
# / remote server vars are already in the environment they would silently redirect
# the suite at a deployed server (e.g. sdkdev) and make Orkes-only tests pass,
# defeating the purpose of the OSS run. Refuse to run unless explicitly overridden.
if [[ "${ALLOW_REMOTE_SERVER:-0}" != "1" ]]; then
offending=()
[[ -n "${CONDUCTOR_AUTH_KEY:-}" ]] && offending+=("CONDUCTOR_AUTH_KEY")
[[ -n "${CONDUCTOR_AUTH_SECRET:-}" ]] && offending+=("CONDUCTOR_AUTH_SECRET")
if [[ -n "${CONDUCTOR_SERVER_URL:-}" \
&& ! "${CONDUCTOR_SERVER_URL}" =~ ^https?://(localhost|127\.0\.0\.1)(:|/|$) ]]; then
offending+=("CONDUCTOR_SERVER_URL=${CONDUCTOR_SERVER_URL}")
fi
if (( ${#offending[@]} > 0 )); then
echo "Error: refusing to run the OSS suite — remote/enterprise server vars are set:" >&2
printf ' - %s\n' "${offending[@]}" >&2
echo >&2
echo "This script runs against a LOCAL OSS stack. Unset these first:" >&2
echo " unset CONDUCTOR_SERVER_URL CONDUCTOR_AUTH_KEY CONDUCTOR_AUTH_SECRET" >&2
echo "Or set ALLOW_REMOTE_SERVER=1 to bypass this check intentionally." >&2
exit 1
fi
fi

CONDUCTOR_SERVER_URL="${CONDUCTOR_SERVER_URL:-http://localhost:8080/api}"
HEALTH_URL="${CONDUCTOR_SERVER_URL%/api}/health"

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 (${COMPOSE_FILE})..."
compose up -d

echo "Waiting for Conductor to be healthy at ${HEALTH_URL} ..."
# Portable wait loop using bash's built-in SECONDS (macOS has no `timeout`).
HEALTH_TIMEOUT="${HEALTH_TIMEOUT:-180}"
deadline=$(( SECONDS + HEALTH_TIMEOUT ))
until curl -sf "${HEALTH_URL}" >/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
export CONDUCTOR_SERVER_TYPE=oss
export HTTPBIN_SERVICE_HOSTNAME="${HTTPBIN_SERVICE_HOSTNAME:-httpbin}"
export CONDUCTOR_REQUEST_TIMEOUT_MS="${CONDUCTOR_REQUEST_TIMEOUT_MS:-120000}"
export CONDUCTOR_RETRY_SERVER_ERRORS="${CONDUCTOR_RETRY_SERVER_ERRORS:-true}"

test_cmd=(npm run test:integration:oss -- --ci --runInBand --testTimeout=120000)
if [[ -n "${TEST_PATTERN}" ]]; then
# Targeting a specific test: pass it straight through as a path pattern.
test_cmd+=("--testPathPatterns=${TEST_PATTERN}")
fi
if (( ${#extra[@]} > 0 )); then
test_cmd+=("${extra[@]}")
fi

if [[ -n "${TEST_PATTERN}" ]]; then
echo "Running OSS integration tests | test '${TEST_PATTERN}' | server ${CONDUCTOR_SERVER_URL}"
else
echo "Running OSS integration tests | full OSS-gated suite | server ${CONDUCTOR_SERVER_URL}"
fi
echo "Writing output to ${LOG_FILE}"

# Tee to a log file for later sharing. pipefail ensures the test command's exit
# status (not tee's) is what propagates.
"${test_cmd[@]}" 2>&1 | tee "${LOG_FILE}"
6 changes: 3 additions & 3 deletions src/integration-tests/ApplicationClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import {
import { ApplicationClient } from "../sdk";
import { createClientWithRetry } from "./utils/createClientWithRetry";
import type { Tag } from "../open-api";
import { describeForOrkesV4 } from "./utils/customJestDescribe";
import { describeForOrkesOnlyV4 } from "./utils/customJestDescribe";

describe("ApplicationClient", () => {
describeForOrkesV4("ApplicationClient V4+", () => {
describeForOrkesOnlyV4("ApplicationClient V4+", () => {
jest.setTimeout(60000);

let applicationClient: ApplicationClient;
Expand Down Expand Up @@ -578,5 +578,5 @@ describe("ApplicationClient", () => {
// ).rejects.toThrow();
// });
});
}); // end describeForOrkesV4
}); // end describeForOrkesOnlyV4
});
6 changes: 3 additions & 3 deletions src/integration-tests/AuthorizationClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
MetadataClient,
} from "../sdk";
import { createClientWithRetry } from "./utils/createClientWithRetry";
import { describeForOrkesV4 } from "./utils/customJestDescribe";
import { describeForOrkesOnlyV4 } from "./utils/customJestDescribe";
import { registerWorkflowDefWithRetry } from "./utils/registerWorkflowWithRetry";

/**
Expand All @@ -15,7 +15,7 @@ import { registerWorkflowDefWithRetry } from "./utils/registerWorkflowWithRetry"
* in a lifecycle order: create → read → update → delete.
*/
describe("AuthorizationClient", () => {
describeForOrkesV4("AuthorizationClient V4+", () => {
describeForOrkesOnlyV4("AuthorizationClient V4+", () => {
jest.setTimeout(60000);

const suffix = Date.now();
Expand Down Expand Up @@ -355,5 +355,5 @@ describe("AuthorizationClient", () => {
).rejects.toThrow();
});
});
}); // end describeForOrkesV4
}); // end describeForOrkesOnlyV4
});
15 changes: 9 additions & 6 deletions src/integration-tests/EventClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ import type {
} from "../open-api";
import { EventClient } from "../sdk";
import { createClientWithRetry } from "./utils/createClientWithRetry";
import { describeForOrkesV4, describeForOrkesV5 } from "./utils/customJestDescribe";
import {
describeForOrkesOnlyV4,
describeForOrkesOnlyV5,
} from "./utils/customJestDescribe";
import { pollUntil } from "./utils/pollUntil";

const TEST_HANDLER_NAME_PREFIX = "jsSdkTest:";
Expand Down Expand Up @@ -79,7 +82,7 @@ describe("EventClient", () => {
description: `Test event handler: ${name}`,
});

describeForOrkesV4("Event Handler Management", () => {
describeForOrkesOnlyV4("Event Handler Management", () => {
test("Should add a single event handler", async () => {
const handlerName = createUniqueName("event-handler");
const eventName = createUniqueName("event");
Expand Down Expand Up @@ -312,7 +315,7 @@ describe("EventClient", () => {
});
});

describeForOrkesV4("Tag Management", () => {
describeForOrkesOnlyV4("Tag Management", () => {
test("Should get tags for an event handler", async () => {
const handlerName = createUniqueName("event-handler");
const eventName = createUniqueName("event");
Expand Down Expand Up @@ -483,7 +486,7 @@ describe("EventClient", () => {
});
});

describeForOrkesV4("Test Endpoint", () => {
describeForOrkesOnlyV4("Test Endpoint", () => {
test("Should call test endpoint", async () => {

const result = await eventClient.test();
Expand Down Expand Up @@ -575,7 +578,7 @@ describe("EventClient", () => {
});
});

describeForOrkesV5("Event Processing", () => {
describeForOrkesOnlyV5("Event Processing", () => {
test("Should handle incoming event", async () => {
const handlerName = createUniqueName("event-handler");
const eventName = createUniqueName("event");
Expand Down Expand Up @@ -666,7 +669,7 @@ describe("EventClient", () => {
});
});

describeForOrkesV5("Event Executions and Statistics", () => {
describeForOrkesOnlyV5("Event Executions and Statistics", () => {
test("Should get all active event handlers (execution view)", async () => {

const handlerName = createUniqueName("event-handler");
Expand Down
14 changes: 9 additions & 5 deletions src/integration-tests/MetadataClient.complete.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ import {
OrkesClients,
} from "../sdk";
import { createClientWithRetry } from "./utils/createClientWithRetry";
import { describeForOrkesV4 } from "./utils/customJestDescribe";
import {
describeForOrkesOnly,
describeForOrkesOnlyV4,
describeForOssSchedulerWip,
} from "./utils/customJestDescribe";
import { registerWorkflowDefWithRetry } from "./utils/registerWorkflowWithRetry";

/**
Expand Down Expand Up @@ -168,7 +172,7 @@ describe("MetadataClient Complete Coverage", () => {

// ==================== Workflow Tags ====================

describeForOrkesV4("Workflow Tags", () => {
describeForOrkesOnlyV4("Workflow Tags", () => {
test("addWorkflowTag should add a tag to a workflow definition", async () => {
await expect(
metadataClient.addWorkflowTag(
Expand Down Expand Up @@ -222,7 +226,7 @@ describe("MetadataClient Complete Coverage", () => {

// ==================== Task Tags ====================

describeForOrkesV4("Task Tags", () => {
describeForOrkesOnlyV4("Task Tags", () => {
test("addTaskTag should add a tag to a task definition", async () => {
await expect(
metadataClient.addTaskTag(
Expand Down Expand Up @@ -276,7 +280,7 @@ describe("MetadataClient Complete Coverage", () => {
// ==================== Rate Limits ====================
// Rate limit API may not be available on all server versions

describe("Rate Limits", () => {
describeForOrkesOnly("Rate Limits", () => {
let rateLimitSupported = true;

test("setWorkflowRateLimit should configure rate limiting", async () => {
Expand Down Expand Up @@ -320,7 +324,7 @@ describe("MetadataClient Complete Coverage", () => {

// ==================== Scheduler Extended ====================

describeForOrkesV4("Scheduler Extended", () => {
describeForOssSchedulerWip("Scheduler Extended", () => {
beforeAll(async () => {
if (!schedulerClient) {
console.warn("schedulerClient is undefined (client creation likely failed), skipping Scheduler Extended setup");
Expand Down
Loading
Loading