PSA/scripts/temporal-worker-docker-parity.mjs
Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

274 lines
7.5 KiB
JavaScript

#!/usr/bin/env node
import { randomUUID } from 'node:crypto';
import { spawnSync } from 'node:child_process';
import { setTimeout as delay } from 'node:timers/promises';
import { Client, Connection } from '@temporalio/client';
const id = randomUUID().slice(0, 8);
const imageTag = `temporal-worker-readiness:${id}`;
const networkName = `temporal-readiness-${id}`;
const postgresContainer = `temporal-readiness-pg-${id}`;
const temporalContainer = `temporal-readiness-temporal-${id}`;
const workerContainer = `temporal-readiness-worker-${id}`;
const temporalHostPort = 17233;
const temporalAddress = `127.0.0.1:${temporalHostPort}`;
const taskQueue = 'alga-jobs';
function run(cmd, args, { capture = false, allowFailure = false } = {}) {
const result = spawnSync(cmd, args, {
stdio: capture ? 'pipe' : 'inherit',
encoding: 'utf8',
});
if (!allowFailure && result.status !== 0) {
const stderr = (result.stderr || '').trim();
const stdout = (result.stdout || '').trim();
throw new Error(
`Command failed (${cmd} ${args.join(' ')}):\n${stderr || stdout || `exit code ${result.status}`}`
);
}
return result;
}
async function waitFor(check, timeoutMs, label) {
const started = Date.now();
while (Date.now() - started < timeoutMs) {
try {
const ok = await check();
if (ok) return;
} catch {
// retry
}
await delay(1000);
}
throw new Error(`Timed out waiting for ${label} (${timeoutMs}ms)`);
}
async function waitForTemporal(address) {
await waitFor(
async () => {
const connection = await Connection.connect({ address });
await connection.close();
return true;
},
150_000,
`Temporal at ${address}`
);
}
async function waitForPoller(address, namespace, queue) {
await waitFor(
async () => {
const connection = await Connection.connect({ address });
try {
const response = await connection.workflowService.describeTaskQueue({
namespace,
taskQueue: { name: queue },
taskQueueType: 1,
});
const pollers = response.pollers ?? [];
return pollers.length > 0;
} finally {
await connection.close();
}
},
120_000,
`worker poller on queue ${queue}`
);
}
async function runRoundtripWorkflow(address, namespace, queue) {
const connection = await Connection.connect({ address });
try {
const client = new Client({ connection, namespace });
const workflowId = `readiness-${Date.now()}`;
const handle = await client.workflow.start('readinessWorkflow', {
workflowId,
taskQueue: queue,
args: [{ echo: 'docker-parity' }],
workflowExecutionTimeout: '30s',
retry: { maximumAttempts: 1 },
});
const result = await Promise.race([
handle.result(),
(async () => {
await delay(20_000);
throw new Error('Timed out waiting for readiness workflow result');
})(),
]);
if (!result || result.ok !== true || result.echo !== 'docker-parity') {
throw new Error(`Unexpected readiness workflow result: ${JSON.stringify(result)}`);
}
} finally {
await connection.close();
}
}
function cleanup() {
run('docker', ['rm', '-f', workerContainer], { capture: true, allowFailure: true });
run('docker', ['rm', '-f', temporalContainer], { capture: true, allowFailure: true });
run('docker', ['rm', '-f', postgresContainer], { capture: true, allowFailure: true });
run('docker', ['network', 'rm', networkName], { capture: true, allowFailure: true });
}
async function main() {
console.log('=== Temporal worker Docker parity check ===');
console.log(`ID: ${id}`);
cleanup();
try {
console.log('\n[1/7] Build temporal worker dist locally...');
run('npm', ['--prefix', 'ee/temporal-workflows', 'run', 'build']);
console.log('\n[2/7] Build worker image...');
run('docker', [
'build',
'--target',
'production-prebuilt',
'-f',
'ee/temporal-workflows/Dockerfile',
'-t',
imageTag,
'.',
]);
console.log('\n[3/7] Create isolated Docker network...');
run('docker', ['network', 'create', networkName]);
console.log('\n[4/7] Start Postgres for worker startup validation...');
run('docker', [
'run',
'-d',
'--name',
postgresContainer,
'--network',
networkName,
'-e',
'POSTGRES_USER=app_user',
'-e',
'POSTGRES_PASSWORD=postpass123',
'-e',
'POSTGRES_DB=server',
'postgres:16-alpine',
]);
await waitFor(
async () => {
const result = run(
'docker',
['exec', postgresContainer, 'pg_isready', '-U', 'app_user', '-d', 'server'],
{ capture: true, allowFailure: true }
);
return result.status === 0;
},
60_000,
'Postgres readiness'
);
console.log('\n[5/7] Start Temporal server...');
run('docker', [
'run',
'-d',
'--name',
temporalContainer,
'--network',
networkName,
'-p',
`${temporalHostPort}:7233`,
'-e',
'DB=postgres12',
'-e',
'DB_PORT=5432',
'-e',
'POSTGRES_USER=app_user',
'-e',
'POSTGRES_PWD=postpass123',
'-e',
`POSTGRES_SEEDS=${postgresContainer}`,
'temporalio/auto-setup:1.24.2',
]);
await waitForTemporal(temporalAddress);
console.log('\n[6/7] Start worker container...');
run('docker', [
'run',
'-d',
'--name',
workerContainer,
'--network',
networkName,
'-e',
'LOG_LEVEL=info',
'-e',
'SECRET_READ_CHAIN=env',
'-e',
'SECRET_WRITE_PROVIDER=env',
'-e',
'ALGA_AUTH_KEY=test-auth-key',
'-e',
'NEXTAUTH_SECRET=test-nextauth-secret',
'-e',
'APPLICATION_URL=http://localhost:3000',
'-e',
'DB_HOST=' + postgresContainer,
'-e',
'DB_PORT=5432',
'-e',
'DB_NAME_SERVER=server',
'-e',
'DB_USER_SERVER=app_user',
'-e',
'DB_PASSWORD_SERVER=postpass123',
'-e',
'DB_USER_ADMIN=app_user',
'-e',
'DB_PASSWORD_ADMIN=postpass123',
'-e',
'TEMPORAL_ADDRESS=' + temporalContainer + ':7233',
'-e',
'TEMPORAL_NAMESPACE=default',
'-e',
'TEMPORAL_TASK_QUEUE=' + taskQueue,
'-e',
'TEMPORAL_TASK_QUEUES=' + taskQueue,
'-e',
'PORTAL_DOMAIN_BASE_VIRTUAL_SERVICE=msp/alga-psa-vs',
'-e',
'ENABLE_HEALTH_CHECK=false',
imageTag,
]);
await waitForPoller(temporalAddress, 'default', taskQueue);
console.log('\n[7/7] Run readiness workflow roundtrip...');
await runRoundtripWorkflow(temporalAddress, 'default', taskQueue);
console.log('\nTemporal worker Docker parity check passed.');
} catch (error) {
console.error('\nTemporal worker Docker parity check failed.\n');
console.error(error instanceof Error ? error.message : error);
console.error('\nTemporal logs:');
run('docker', ['logs', '--tail', '200', temporalContainer], { allowFailure: true });
console.error('\nPostgres logs:');
run('docker', ['logs', '--tail', '200', postgresContainer], { allowFailure: true });
const inspectResult = run('docker', ['inspect', workerContainer], {
capture: true,
allowFailure: true,
});
if (inspectResult.status === 0) {
console.error('\nWorker logs:');
run('docker', ['logs', '--tail', '200', workerContainer], { allowFailure: true });
}
throw error;
} finally {
cleanup();
}
}
main().catch(() => {
process.exit(1);
});