Skip to content

Commit 435e5a2

Browse files
certification worker (#121)
* certification worker * Add gh action wf to build test worker image * bump to node 24 in an effort to get past some dependency related error * testing the dispatch * Add missing .dockerignore file * fix an issue with re-reading readable streams * remove temp branch
1 parent 1d4cbdf commit 435e5a2

17 files changed

Lines changed: 991 additions & 12 deletions

.dockerignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
node_modules
2+
dist
3+
reports
4+
.git
5+
.github
6+
.vscode
7+
*.md
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
name: Harness Worker Image
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths:
7+
- "Dockerfile"
8+
- "harness/**"
9+
- "src/**"
10+
- "package.json"
11+
- "package-lock.json"
12+
- ".github/workflows/harness-image.yml"
13+
release:
14+
types: [published]
15+
workflow_dispatch:
16+
17+
concurrency:
18+
group: ${{ github.workflow }}-${{ github.ref }}
19+
cancel-in-progress: true
20+
21+
jobs:
22+
build-and-push:
23+
runs-on: ubuntu-latest
24+
permissions:
25+
contents: read
26+
packages: write
27+
steps:
28+
- name: Checkout
29+
uses: actions/checkout@v4
30+
31+
- name: Set up QEMU
32+
uses: docker/setup-qemu-action@v3
33+
34+
- name: Set up Docker Buildx
35+
uses: docker/setup-buildx-action@v3
36+
37+
- name: Log in to GHCR
38+
uses: docker/login-action@v3
39+
with:
40+
registry: ghcr.io
41+
username: ${{ github.actor }}
42+
password: ${{ secrets.GITHUB_TOKEN }}
43+
44+
- name: Docker metadata
45+
id: meta
46+
uses: docker/metadata-action@v5
47+
with:
48+
images: ghcr.io/conductor-oss/javascript-sdk/harness-worker
49+
tags: |
50+
type=raw,value=latest
51+
type=raw,value=${{ github.event.release.tag_name }},enable=${{ github.event_name == 'release' }}
52+
53+
- name: Build and push
54+
uses: docker/build-push-action@v6
55+
with:
56+
context: .
57+
file: ./Dockerfile
58+
target: harness
59+
platforms: linux/amd64,linux/arm64
60+
push: true
61+
tags: ${{ steps.meta.outputs.tags }}
62+
63+
dispatch-deploy:
64+
if: github.event_name == 'release'
65+
needs: build-and-push
66+
runs-on: ubuntu-latest
67+
permissions:
68+
contents: read
69+
steps:
70+
- uses: peter-evans/repository-dispatch@v3
71+
with:
72+
token: ${{ secrets.CI_UTIL_DISPATCH_TOKEN }}
73+
repository: conductor-oss/oss-ci-util
74+
event-type: sdk_release
75+
client-payload: |-
76+
{"tag": "${{ github.event.release.tag_name || 'latest' }}", "repo": "${{ github.repository }}"}

Dockerfile

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
FROM node:24-alpine AS build
2+
WORKDIR /package
3+
COPY package*.json ./
4+
RUN npm ci
5+
COPY . .
6+
RUN npm run build
7+
8+
FROM build AS harness-build
9+
RUN npx tsup harness/main.ts \
10+
--outDir /app \
11+
--format cjs \
12+
--target node24 \
13+
--no-splitting
14+
15+
FROM node:24-alpine AS harness-deps
16+
WORKDIR /package
17+
COPY package*.json ./
18+
RUN npm ci --omit=dev
19+
20+
FROM node:24-alpine AS harness
21+
RUN adduser -D -u 65532 nonroot
22+
USER nonroot
23+
WORKDIR /app
24+
COPY --from=harness-deps /package/node_modules /app/node_modules
25+
COPY --from=harness-deps /package/package.json /app/package.json
26+
COPY --from=harness-build /app/main.js /app/main.js
27+
ENTRYPOINT ["node", "main.js"]

harness/README.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# JS SDK Docker Harness
2+
3+
Two Docker targets built from the root `Dockerfile`: an **SDK build** and a **long-running worker harness**.
4+
5+
## Worker Harness
6+
7+
A self-feeding worker that runs indefinitely. On startup it registers five simulated tasks (`js_worker_0` through `js_worker_4`) and the `js_simulated_tasks_workflow`, then runs two background services:
8+
9+
- **WorkflowGovernor** -- starts a configurable number of `js_simulated_tasks_workflow` instances per second (default 2), indefinitely.
10+
- **SimulatedTaskWorkers** -- five task handlers, each with a codename and a default sleep duration. Each worker supports configurable delay types, failure simulation, and output generation via task input parameters. The workflow chains them in sequence: quickpulse (1s) → whisperlink (2s) → shadowfetch (3s) → ironforge (4s) → deepcrawl (5s).
11+
12+
### Building Locally
13+
14+
```bash
15+
docker build --target harness -t js-sdk-harness .
16+
```
17+
18+
### Multiplatform Build and Push
19+
20+
To build for both `linux/amd64` and `linux/arm64` and push to GHCR:
21+
22+
```bash
23+
# One-time: create a buildx builder if you don't have one
24+
docker buildx create --name multiarch --use --bootstrap
25+
26+
# Build and push
27+
docker buildx build \
28+
--platform linux/amd64,linux/arm64 \
29+
--target harness \
30+
-t ghcr.io/conductor-oss/javascript-sdk/harness-worker:latest \
31+
--push .
32+
```
33+
34+
> **Note:** Multi-platform builds require `docker buildx` and a builder that supports cross-compilation. On macOS this works out of the box with Docker Desktop. On Linux you may need to install QEMU user-space emulators:
35+
>
36+
> ```bash
37+
> docker run --privileged --rm tonistiigi/binfmt --install all
38+
> ```
39+
40+
### Running
41+
42+
```bash
43+
docker run -d \
44+
-e CONDUCTOR_SERVER_URL=https://your-cluster.example.com/api \
45+
-e CONDUCTOR_AUTH_KEY=$CONDUCTOR_AUTH_KEY \
46+
-e CONDUCTOR_AUTH_SECRET=$CONDUCTOR_AUTH_SECRET \
47+
-e HARNESS_WORKFLOWS_PER_SEC=4 \
48+
js-sdk-harness
49+
```
50+
51+
You can also run the harness locally without Docker:
52+
53+
```bash
54+
export CONDUCTOR_SERVER_URL=https://your-cluster.example.com/api
55+
export CONDUCTOR_AUTH_KEY=$CONDUCTOR_AUTH_KEY
56+
export CONDUCTOR_AUTH_SECRET=$CONDUCTOR_AUTH_SECRET
57+
58+
npx tsx harness/main.ts
59+
```
60+
61+
Override defaults with environment variables as needed:
62+
63+
```bash
64+
HARNESS_WORKFLOWS_PER_SEC=4 HARNESS_BATCH_SIZE=10 npx tsx harness/main.ts
65+
```
66+
67+
All resource names use a `js_` prefix so multiple SDK harnesses (C#, Python, Go, etc.) can coexist on the same cluster.
68+
69+
### Environment Variables
70+
71+
| Variable | Required | Default | Description |
72+
|---|---|---|---|
73+
| `CONDUCTOR_SERVER_URL` | yes | -- | Conductor API base URL |
74+
| `CONDUCTOR_AUTH_KEY` | no | -- | Orkes auth key |
75+
| `CONDUCTOR_AUTH_SECRET` | no | -- | Orkes auth secret |
76+
| `HARNESS_WORKFLOWS_PER_SEC` | no | 2 | Workflows to start per second |
77+
| `HARNESS_BATCH_SIZE` | no | 20 | Number of tasks each worker polls per batch |
78+
| `HARNESS_POLL_INTERVAL_MS` | no | 100 | Milliseconds between poll cycles |

harness/main.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import {
2+
OrkesClients,
3+
ConductorWorkflow,
4+
TaskHandler,
5+
simpleTask,
6+
} from "../src/sdk";
7+
import { MetadataResource } from "../src/open-api/generated";
8+
import type { ConductorWorker } from "../src/sdk/clients/worker/types";
9+
import { SimulatedTaskWorker } from "./simulatedTaskWorker";
10+
import { WorkflowGovernor } from "./workflowGovernor";
11+
12+
const WORKFLOW_NAME = "js_simulated_tasks_workflow";
13+
14+
const SIMULATED_WORKERS: {
15+
taskName: string;
16+
codename: string;
17+
sleepSeconds: number;
18+
}[] = [
19+
{ taskName: "js_worker_0", codename: "quickpulse", sleepSeconds: 1 },
20+
{ taskName: "js_worker_1", codename: "whisperlink", sleepSeconds: 2 },
21+
{ taskName: "js_worker_2", codename: "shadowfetch", sleepSeconds: 3 },
22+
{ taskName: "js_worker_3", codename: "ironforge", sleepSeconds: 4 },
23+
{ taskName: "js_worker_4", codename: "deepcrawl", sleepSeconds: 5 },
24+
];
25+
26+
function envIntOrDefault(key: string, defaultVal: number): number {
27+
const s = process.env[key];
28+
if (!s) return defaultVal;
29+
const v = parseInt(s, 10);
30+
return isNaN(v) ? defaultVal : v;
31+
}
32+
33+
async function registerMetadata(
34+
client: Awaited<ReturnType<typeof OrkesClients.prototype.getClient>>,
35+
workflowClient: ReturnType<typeof OrkesClients.prototype.getWorkflowClient>,
36+
): Promise<void> {
37+
const taskDefs = SIMULATED_WORKERS.map((def) => ({
38+
name: def.taskName,
39+
description: `JS SDK harness simulated task (${def.codename}, default delay ${def.sleepSeconds}s)`,
40+
retryCount: 1,
41+
timeoutSeconds: 300,
42+
responseTimeoutSeconds: 300,
43+
totalTimeoutSeconds: 0,
44+
}));
45+
46+
await MetadataResource.registerTaskDef({
47+
client,
48+
body: taskDefs,
49+
});
50+
51+
const wf = new ConductorWorkflow(workflowClient, WORKFLOW_NAME)
52+
.version(1)
53+
.description("JS SDK harness simulated task workflow")
54+
.ownerEmail("js-sdk-harness@conductor.io");
55+
56+
for (const def of SIMULATED_WORKERS) {
57+
wf.add(simpleTask(def.codename, def.taskName, {}));
58+
}
59+
60+
await wf.register(true);
61+
62+
console.log(
63+
`Registered workflow ${WORKFLOW_NAME} with ${SIMULATED_WORKERS.length} tasks`,
64+
);
65+
}
66+
67+
async function main(): Promise<void> {
68+
const clients = await OrkesClients.from();
69+
const workflowClient = clients.getWorkflowClient();
70+
const client = clients.getClient();
71+
72+
await registerMetadata(client, workflowClient);
73+
74+
const workflowsPerSec = envIntOrDefault("HARNESS_WORKFLOWS_PER_SEC", 2);
75+
const batchSize = envIntOrDefault("HARNESS_BATCH_SIZE", 20);
76+
const pollIntervalMs = envIntOrDefault("HARNESS_POLL_INTERVAL_MS", 100);
77+
78+
const workers: ConductorWorker[] = SIMULATED_WORKERS.map((def) => {
79+
const sim = new SimulatedTaskWorker(
80+
def.taskName,
81+
def.codename,
82+
def.sleepSeconds,
83+
batchSize,
84+
pollIntervalMs,
85+
);
86+
return {
87+
taskDefName: sim.taskName,
88+
execute: sim.execute.bind(sim),
89+
concurrency: sim.batchSize,
90+
pollInterval: sim.pollInterval,
91+
};
92+
});
93+
94+
const handler = new TaskHandler({
95+
client,
96+
workers,
97+
scanForDecorated: false,
98+
});
99+
await handler.startWorkers();
100+
101+
const governor = new WorkflowGovernor(
102+
workflowClient,
103+
WORKFLOW_NAME,
104+
workflowsPerSec,
105+
);
106+
governor.start();
107+
108+
const shutdown = async () => {
109+
console.log("Shutting down...");
110+
governor.stop();
111+
await handler.stopWorkers();
112+
process.exit(0);
113+
};
114+
115+
process.on("SIGINT", shutdown);
116+
process.on("SIGTERM", shutdown);
117+
}
118+
119+
main().catch((err) => {
120+
console.error("Fatal error:", err);
121+
process.exit(1);
122+
});

0 commit comments

Comments
 (0)