Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
3 changes: 3 additions & 0 deletions mise-tasks/dev
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
#MISE description="Start full dev stack (realm server, workers, test realms)"
#MISE dir="packages/realm-server"

# Add node_modules/.bin to PATH (mise run bypasses pnpm which normally does this)
PATH="$(pwd)/../../node_modules/.bin:$(pwd)/node_modules/.bin:$PATH"

. "$(cd "$(dirname "$0")" && pwd)/lib/dev-common.sh"

WAIT_ON_TIMEOUT=2400000 NODE_NO_WARNINGS=1 start-server-and-test \
Expand Down
24 changes: 24 additions & 0 deletions mise-tasks/lib/env-vars.sh
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ if [ -n "${BOXEL_ENVIRONMENT:-}" ]; then
# Paths
export REALMS_ROOT="./realms/${ENV_SLUG}"
export REALMS_TEST_ROOT="./realms/${ENV_SLUG}_test"

# Matrix test services (isolated realm server + worker for Playwright tests)
export MATRIX_TEST_REALM_URL="http://realm-matrix-test.${ENV_SLUG}.localhost"
export MATRIX_TEST_REALM_PORT=0
export MATRIX_TEST_WORKER_PORT=0
export MATRIX_TEST_PUBLISHED_DOMAIN="realm-matrix-test.${ENV_SLUG}.localhost"
export SMTP_URL="http://smtp.${ENV_SLUG}.localhost"
export SMTP_PORT=0
else
# Capture previous ENV_MODE before resetting it, so we can detect transitions
_PREV_ENV_MODE="${ENV_MODE:-}"
Expand Down Expand Up @@ -91,6 +99,14 @@ else
# Paths
export REALMS_ROOT="./realms/localhost_4201"
export REALMS_TEST_ROOT="./realms/localhost_4202"

# Matrix test services
export MATRIX_TEST_REALM_URL="http://localhost:4205"
export MATRIX_TEST_REALM_PORT=4205
export MATRIX_TEST_WORKER_PORT=4232
export MATRIX_TEST_PUBLISHED_DOMAIN="localhost:4205"
export SMTP_URL="http://localhost:5001"
export SMTP_PORT=5001
else
# Fresh standard mode or non-env-mode shell:
# use :- so production/staging env vars are not clobbered.
Expand Down Expand Up @@ -122,6 +138,14 @@ else
# Paths
export REALMS_ROOT="${REALMS_ROOT:-./realms/localhost_4201}"
export REALMS_TEST_ROOT="${REALMS_TEST_ROOT:-./realms/localhost_4202}"

# Matrix test services
export MATRIX_TEST_REALM_URL="${MATRIX_TEST_REALM_URL:-http://localhost:4205}"
export MATRIX_TEST_REALM_PORT="${MATRIX_TEST_REALM_PORT:-4205}"
export MATRIX_TEST_WORKER_PORT="${MATRIX_TEST_WORKER_PORT:-4232}"
export MATRIX_TEST_PUBLISHED_DOMAIN="${MATRIX_TEST_PUBLISHED_DOMAIN:-localhost:4205}"
export SMTP_URL="${SMTP_URL:-http://localhost:5001}"
export SMTP_PORT="${SMTP_PORT:-5001}"
fi

unset _PREV_ENV_MODE
Expand Down
29 changes: 23 additions & 6 deletions mise-tasks/services/realm-server-base
Original file line number Diff line number Diff line change
@@ -1,32 +1,49 @@
#!/bin/sh
#MISE description="Start base realm server only"
#MISE depends=["infra:ensure-pg"]
#MISE depends=["infra:ensure-traefik", "infra:ensure-pg"]
#MISE dir="packages/realm-server"

if [ -z "$MATRIX_REGISTRATION_SHARED_SECRET" ]; then
MATRIX_REGISTRATION_SHARED_SECRET=$(ts-node --transpileOnly ./scripts/matrix-registration-secret.ts)
export MATRIX_REGISTRATION_SHARED_SECRET
fi

# The base-only realm server uses dedicated port/db/paths that differ from
# the main development realm server. In env mode, use the env vars; in
# standard mode, use the base-specific defaults.
if [ -n "$ENV_MODE" ]; then
WORKER_MANAGER_ARG="--workerManagerUrl=${WORKER_MGR_URL}"
REALM_BASE_PORT="${REALM_PORT}"
REALM_BASE_DB="${PGDATABASE}"
REALM_BASE_ROOT="${REALMS_ROOT}"
REALM_BASE_TO_URL="${REALM_BASE_URL}/base/"
else
WORKER_MANAGER_ARG="$1"
REALM_BASE_PORT=4201
REALM_BASE_DB=boxel_base
REALM_BASE_ROOT="./realms/localhost_4201_base"
REALM_BASE_TO_URL="http://localhost:4201/base/"
fi

NODE_ENV=development \
NODE_NO_WARNINGS=1 \
PGPORT="${PGPORT}" \
PGDATABASE=boxel_base \
PGDATABASE="${REALM_BASE_DB}" \
REALM_SERVER_SECRET_SEED="mum's the word" \
REALM_SECRET_SEED="shhh! it's a secret" \
GRAFANA_SECRET="shhh! it's a secret" \
MATRIX_URL="${MATRIX_URL_VAL}" \
REALM_SERVER_MATRIX_USERNAME=realm_server \
ts-node \
--transpileOnly main \
--port=4201 \
--port="${REALM_BASE_PORT}" \
--matrixURL="${MATRIX_URL_VAL}" \
--realmsRootPath='./realms/localhost_4201_base' \
--realmsRootPath="${REALM_BASE_ROOT}" \
--prerendererUrl="${PRERENDER_URL}" \
--migrateDB \
$1 \
$WORKER_MANAGER_ARG \
\
--path='../base' \
--username='base_realm' \
--fromUrl='https://cardstack.com/base/' \
--toUrl='http://localhost:4201/base/'
--toUrl="${REALM_BASE_TO_URL}"
19 changes: 15 additions & 4 deletions mise-tasks/services/worker-base
Original file line number Diff line number Diff line change
@@ -1,21 +1,32 @@
#!/bin/sh
#MISE description="Start worker manager for base realm only"
#MISE depends=["infra:ensure-pg", "infra:wait-for-prerender"]
#MISE depends=["infra:ensure-traefik", "infra:ensure-pg", "infra:wait-for-prerender"]
#MISE dir="packages/realm-server"

# The base-only worker uses dedicated port/db that differ from the main
# development worker (WORKER_PORT/PGDATABASE). In env mode, use the env
# vars; in standard mode, use the base-specific defaults.
if [ -n "$ENV_MODE" ]; then
WORKER_BASE_PORT="${WORKER_PORT}"
WORKER_BASE_DB="${PGDATABASE}"
else
WORKER_BASE_PORT=4213
WORKER_BASE_DB=boxel_base
fi

NODE_ENV=development \
NODE_NO_WARNINGS=1 \
NODE_OPTIONS="${NODE_OPTIONS:---max-old-space-size=4096}" \
PGPORT="${PGPORT}" \
PGDATABASE=boxel_base \
PGDATABASE="${WORKER_BASE_DB}" \
REALM_SECRET_SEED="shhh! it's a secret" \
REALM_SERVER_MATRIX_USERNAME=realm_server \
LOW_CREDIT_THRESHOLD=2000 \
ts-node \
--transpileOnly worker-manager \
--port=4213 \
--port="${WORKER_BASE_PORT}" \
--matrixURL="${MATRIX_URL_VAL}" \
--prerendererUrl="${PRERENDER_MGR_URL}" \
\
--fromUrl='https://cardstack.com/base/' \
--toUrl='http://localhost:4201/base/'
--toUrl="${REALM_BASE_URL:-http://localhost:4201}/base/"
20 changes: 20 additions & 0 deletions mise-tasks/test-matrix
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/bin/sh
#MISE description="Run Playwright matrix tests (environment-aware)"
#MISE dir="packages/matrix"

# Usage: mise run test-matrix [shard]
# In environment mode, uses Traefik-routed URLs; otherwise uses fixed ports.

shard_flag=${1:+--shard}

BASE_REALM_HOST="${REALM_BASE_URL:-http://localhost:4201}"
READY_PATH="_readiness-check?acceptHeader=application%2Fvnd.api%2Bjson"
BASE_REALM_READY="http-get://${BASE_REALM_HOST#http://}/base/${READY_PATH}"

echo "Waiting for base realm at ${BASE_REALM_HOST}..."
echo "Running matrix tests${1:+ (shard: $1)}"

WAIT_ON_TIMEOUT=600000 pnpm exec start-server-and-test \
'pnpm run wait' \
"$BASE_REALM_READY" \
"pnpm exec playwright test ${shard_flag} ${1}"
55 changes: 47 additions & 8 deletions packages/matrix/docker/smtp4dev.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,72 @@
import { dockerCreateNetwork, dockerRun, dockerStop, dockerRm } from './index';
import {
isEnvironmentMode,
getEnvironmentSlug,
registerServiceWithTraefik,
deregisterServiceFromTraefik,
} from '../helpers/environment-config';
import { execSync } from 'child_process';

interface Options {
mailClientPort?: number;
traefikServiceName?: string;
}

let _smtpServiceName = 'smtp';

function smtpContainerName(): string {
if (isEnvironmentMode()) {
return `boxel-${_smtpServiceName}-${getEnvironmentSlug()}`;
}
return 'boxel-smtp';
}

export async function smtpStart(opts?: Options) {
if (opts?.traefikServiceName) {
_smtpServiceName = opts.traefikServiceName;
}
let containerName = smtpContainerName();
try {
await smtpStop();
} catch (e: any) {
if (!e.message.includes('No such container')) {
throw e;
}
}
let mailClientPort = opts?.mailClientPort ?? 5001;
let portMapping = `${mailClientPort}:80`;
let envMode = isEnvironmentMode();
let mailClientPort = envMode
? 0
: (opts?.mailClientPort ?? parseInt(process.env.SMTP_PORT || '5001', 10));
let portMapping = envMode ? '0:80' : `${mailClientPort}:80`;
await dockerCreateNetwork({ networkName: 'boxel' });
const containerId = await dockerRun({
image: 'rnwood/smtp4dev:v3.1',
containerName: 'boxel-smtp',
containerName,
dockerParams: ['-p', portMapping, '--network=boxel'],
});

console.log(
`Started smtp4dev with id ${containerId} mapped to host port ${mailClientPort}.`,
);
if (envMode) {
let portOutput = execSync(`docker port ${containerId} 80/tcp`, {
encoding: 'utf-8',
}).trim();
let hostPort = parseInt(portOutput.split('\n')[0].split(':').pop()!, 10);
registerServiceWithTraefik(_smtpServiceName, hostPort);
console.log(
`Started smtp4dev with id ${containerId} on dynamic port ${hostPort} (Traefik).`,
);
} else {
console.log(
`Started smtp4dev with id ${containerId} mapped to host port ${mailClientPort}.`,
);
}
return containerId;
}

export async function smtpStop() {
await dockerStop({ containerId: 'boxel-smtp' });
await dockerRm({ containerId: 'boxel-smtp' });
let containerName = smtpContainerName();
if (isEnvironmentMode()) {
deregisterServiceFromTraefik(_smtpServiceName);
}
await dockerStop({ containerId: containerName });
await dockerRm({ containerId: containerName });
}
9 changes: 7 additions & 2 deletions packages/matrix/docker/synapse/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
isEnvironmentMode,
getSynapseContainerName,
getSynapseURL,
registerSynapseWithTraefik,
registerServiceWithTraefik,
} from '../../helpers/environment-config';

export const SYNAPSE_IP_ADDRESS = '172.20.0.5';
Expand Down Expand Up @@ -157,6 +157,7 @@ interface StartOptions {
dataDir?: string;
containerName?: string;
suppressRegistrationSecretFile?: true;
traefikServiceName?: string;
dynamicHostPort?: true;
}

Expand Down Expand Up @@ -230,6 +231,9 @@ export async function synapseStart(
);
}

// Clean up stale container from a previous interrupted run
await dockerStop({ containerId: containerName }).catch(() => {});

try {
synapseId = await dockerRun({
image: 'matrixdotorg/synapse:v1.126.0',
Expand Down Expand Up @@ -286,7 +290,8 @@ export async function synapseStart(
}

if (isEnvironmentMode()) {
registerSynapseWithTraefik(hostPort);
let synapseServiceName = opts?.traefikServiceName || 'matrix';
registerServiceWithTraefik(synapseServiceName, hostPort);
}

const synapse: SynapseInstance = {
Expand Down
37 changes: 30 additions & 7 deletions packages/matrix/helpers/environment-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,19 @@ export function getSynapseContainerName(): string {
return 'boxel-synapse';
}

let _synapseURLOverride: string | undefined;

export function setSynapseURL(url: string): void {
_synapseURLOverride = url;
}

export function getSynapseURL(synapse?: {
baseUrl?: string;
port?: number;
}): string {
if (_synapseURLOverride) {
return _synapseURLOverride;
}
if (synapse?.baseUrl) {
return synapse.baseUrl;
}
Expand All @@ -88,9 +97,11 @@ export function getSynapseURL(synapse?: {
}
}

export function registerSynapseWithTraefik(hostPort: number): void {
export function registerServiceWithTraefik(
serviceName: string,
hostPort: number,
): void {
let slug = getEnvironmentSlug();
let serviceName = 'matrix';
let configPath = join(traefikDynamicDir(), `${slug}-${serviceName}.yml`);
let routerKey = `${serviceName}-${slug}`;
let hostname = `${serviceName}.${slug}.${DOMAIN}`;
Expand All @@ -115,27 +126,39 @@ export function registerSynapseWithTraefik(hostPort: number): void {
};

atomicWrite(configPath, yaml.stringify(config));
console.log(`Registered Synapse at ${hostname} -> localhost:${hostPort}`);
console.log(
`Registered ${serviceName} at ${hostname} -> localhost:${hostPort}`,
);
}

export function deregisterSynapseFromTraefik(): void {
export function registerSynapseWithTraefik(hostPort: number): void {
registerServiceWithTraefik('matrix', hostPort);
}

export function deregisterServiceFromTraefik(serviceName: string): void {
if (!isEnvironmentMode()) {
return;
}
let slug = getEnvironmentSlug();
let configPath = join(traefikDynamicDir(), `${slug}-matrix.yml`);
let configPath = join(traefikDynamicDir(), `${slug}-${serviceName}.yml`);
try {
unlinkSync(configPath);
console.log(`Deregistered Synapse for environment ${slug} from Traefik`);
console.log(
`Deregistered ${serviceName} for environment ${slug} from Traefik`,
);
} catch (e: any) {
if (e.code !== 'ENOENT') {
console.error(
`Failed to deregister Synapse for environment ${slug}: ${e.message}`,
`Failed to deregister ${serviceName} for environment ${slug}: ${e.message}`,
);
}
}
}

export function deregisterSynapseFromTraefik(): void {
deregisterServiceFromTraefik('matrix');
}

function atomicWrite(filePath: string, content: string): void {
let tmpPath = `${filePath}.tmp`;
writeFileSync(tmpPath, content, 'utf-8');
Expand Down
Loading
Loading