Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
f78c3be
initial commit for verify
jasonmorais May 19, 2026
09e9ab0
staging changes for portless modification - using worktree as an env …
jasonmorais May 19, 2026
b4db123
fixes for e2e tests along with overall pattern cleanup
jasonmorais May 21, 2026
2d54c57
overall improvements to workspace files, along with implementation of…
jasonmorais May 26, 2026
6c415ea
small fixes for build file issue
jasonmorais May 26, 2026
395bfcd
small improvements based on my comments, simplified overcomplicated l…
jasonmorais May 27, 2026
628694e
Merge branch 'main' of github.com:CellixJs/cellixjs into jason/portle…
jasonmorais May 27, 2026
372b674
Merge remote-tracking branch 'origin/main' into jason/portless-worktrees
jasonmorais May 28, 2026
1522602
retrigger build
jasonmorais May 28, 2026
843f3d0
sourcery suggestions pt1
jasonmorais May 28, 2026
c184d92
sourcery suggestions reound 2 - reusability
jasonmorais May 28, 2026
fabfb36
Update readme.md
jasonmorais May 28, 2026
0b76477
undo tsc to tsgo
jasonmorais May 28, 2026
110f771
fix serenity pattern - use task pattern instead of interaction
jasonmorais May 29, 2026
ba94849
initial commit for verify
jasonmorais Jun 1, 2026
9be2b2d
modularize serenity framework in a way that can be more easily used b…
jasonmorais Jun 2, 2026
865a5c3
reviewed changes on local with opus 4.8, made some adjustments from j…
jasonmorais Jun 2, 2026
c0ab794
made changes regarding infrastructure patterns, simplified what was n…
jasonmorais Jun 3, 2026
de71fea
Merge remote-tracking branch 'origin/main' into jason/serenity-framew…
jasonmorais Jun 3, 2026
a44fbe9
small refactor for test server pattern in api
jasonmorais Jun 3, 2026
0c1ae41
small adjustments based on codex review
jasonmorais Jun 4, 2026
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
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,3 @@ apps/api/deploy/

# Build-time evidence artifacts (generated)
build-artifacts/

10 changes: 5 additions & 5 deletions .snyk
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,13 @@ ignore:
reason: 'Transitive dependency in @docusaurus/preset-classic; not exploitable in current usage.'
expires: '2026-06-18T00:00:00.000Z'
created: '2026-05-18T11:04:00.000Z'
'SNYK-JS-POSTCSSSELECTORPARSER-16873882':
- '* > postcss-selector-parser':
reason: 'Transitive dependency in @docusaurus/core build pipeline; no upgrade or patch available from upstream. Not exploitable in current usage (build-time CSS processing only).'
expires: '2026-08-26T00:00:00.000Z'
created: '2026-05-26T00:00:00.000Z'
'SNYK-JS-OPENTELEMETRYEXPORTERPROMETHEUS-16758050':
- '* > @opentelemetry/exporter-prometheus@0.57.2':
reason: 'Requires upgrade of @opentelemetry/sdk-node to 0.217.0, which has type errors that break compilation. Created task to upgrade OTEL service to 2.x and resolve vulnerability that way.'
expires: '2026-07-28T00:00:00.000Z'
created: '2026-06-01T10:00:00.000Z'
'SNYK-JS-POSTCSSSELECTORPARSER-16873882':
- '* > postcss-selector-parser':
reason: 'Transitive dependency in Docusaurus CSS optimization/build tooling; Snyk reports no fixed version for postcss-selector-parser yet. Not exploitable at runtime because docs CSS is repository-controlled and processed at build time.'
expires: '2026-07-28T00:00:00.000Z'
created: '2026-05-27T00:00:00.000Z'
29 changes: 29 additions & 0 deletions apps/api/local-settings.e2e.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"IsEncrypted": false,
"Values": {
"FUNCTIONS_WORKER_RUNTIME": "node",
"NODE_ENV": "development",
"languageWorkers__node__arguments": "",
"AZURE_STORAGE_CONNECTION_STRING": "UseDevelopmentStorage=true",
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"ACCOUNT_PORTAL_OIDC_AUDIENCE": "mock-client",
"ACCOUNT_PORTAL_OIDC_ENDPOINT": "https://mock-auth.ownercommunity.localhost:1355/community/.well-known/jwks.json",
"ACCOUNT_PORTAL_OIDC_ISSUER": "https://mock-auth.ownercommunity.localhost:1355/community",
"ACCOUNT_PORTAL_OIDC_IGNORE_ISSUER": true,
"APPLICATIONINSIGHTS_CONNECTION_STRING": "",
"CONFIG_VERSION": "3.0",
"COSMOSDB_CONNECTION_STRING": "mongodb://127.0.0.1:50000/owner-community?replicaSet=globaldb",
"COSMOSDB_DBNAME": "owner-community",
"STAFF_PORTAL_OIDC_AUDIENCE": "mock-client",
"STAFF_PORTAL_OIDC_ENDPOINT": "https://mock-auth.ownercommunity.localhost:1355/staff/.well-known/jwks.json",
"STAFF_PORTAL_OIDC_ISSUER": "https://mock-auth.ownercommunity.localhost:1355/staff",
"STAFF_PORTAL_OIDC_IGNORE_ISSUER": true,
"STORAGE_ACCOUNT_NAME": "devstoreaccount1",
"STORAGE_ACCOUNT_KEY": "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="
},
"ConnectionStrings": {},
"Host": {
"LocalHttpPort": 7071,
"CORS": "*"
}
}
9 changes: 6 additions & 3 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
"build": "tsgo --build && rolldown -c rolldown.config.ts",
"predev": "pnpm run prepare:deploy && pnpm run sync-local-settings",
"dev": "pnpm exec portless data-access.ownercommunity.localhost --force node start-dev.mjs",
"predev:worktree": "pnpm run prepare:deploy && pnpm run sync-local-settings",
"dev:worktree": "pnpm exec portless data-access.ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.mjs",
"prepare:deploy": "cellix-prepare-func-deploy",
"watch": "tsgo --watch",
"test": "vitest run --silent --reporter=dot",
Expand All @@ -21,8 +23,8 @@
"clean": "rimraf dist deploy",
"prestart": "pnpm run prepare:deploy && pnpm run sync-local-settings",
"start": "func start --typescript --script-root deploy/",
"sync-local-settings": "node -e \"const fs=require('node:fs'); fs.mkdirSync('deploy',{recursive:true}); if (fs.existsSync('local.settings.json')) fs.copyFileSync('local.settings.json','deploy/local.settings.json');\"",
"azurite": "azurite-blob --silent --location ../../__blobstorage__ & azurite-queue --silent --location ../../__queuestorage__ & azurite-table --silent --location ../../__tablestorage__"
"sync-local-settings": "node scripts/sync-local-settings.mjs",
"azurite": "node start-azurite.mjs"
},
"dependencies": {
"@azure/functions": "catalog:",
Expand All @@ -31,8 +33,8 @@
"@ocom/application-services": "workspace:*",
"@ocom/context-spec": "workspace:*",
"@ocom/event-handler": "workspace:*",
"@ocom/graphql-handler": "workspace:*",
"@ocom/graphql": "workspace:*",
"@ocom/graphql-handler": "workspace:*",
"@ocom/persistence": "workspace:*",
"@ocom/rest": "workspace:*",
"@ocom/service-apollo-server": "workspace:*",
Expand All @@ -47,6 +49,7 @@
"@cellix/config-typescript": "workspace:*",
"@cellix/config-vitest": "workspace:*",
"@vitest/coverage-istanbul": "catalog:",
"azurite": "^3.35.0",
"rimraf": "catalog:",
"rolldown": "1.0.0-beta.55",
"typescript": "catalog:",
Expand Down
63 changes: 63 additions & 0 deletions apps/api/scripts/sync-local-settings.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { buildPortlessUrl, getHostnames } from '../../../scripts/local-dev/portless-hostnames.mjs';
import { getAzuriteConnectionString } from '../../../scripts/local-dev/worktree-ports.mjs';

const apiDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const mode = process.argv[2] ?? (isE2E() ? 'e2e' : undefined);
const localSettingsPath = path.join(apiDir, 'local.settings.json');
const e2eLocalSettingsPath = path.join(apiDir, 'local-settings.e2e.json');
const targetPath = path.join(apiDir, 'deploy', 'local.settings.json');

mkdirSync(path.dirname(targetPath), { recursive: true });

if (!mode) {
if (existsSync(localSettingsPath)) {
copyFileSync(localSettingsPath, targetPath);
}
process.exit(0);
}

if (mode !== 'e2e') {
throw new Error('[sync-local-settings] Invalid mode: expected one of e2e');
}

if (!existsSync(e2eLocalSettingsPath)) {
throw new Error(`[sync-local-settings] Missing local settings for mode "e2e": ${e2eLocalSettingsPath}`);
}

const settings = JSON.parse(readFileSync(e2eLocalSettingsPath, 'utf-8'));
applyE2EOverrides(settings);
writeFileSync(targetPath, `${JSON.stringify(settings, null, '\t')}\n`);

function applyE2EOverrides(settings) {
const values = { ...(settings.Values ?? {}) };

// Worktree-scoped overrides: when WORKTREE_NAME is set the proxy hostnames
// and Azurite ports are scoped to that worktree, so we rewrite the URLs and
// connection strings here. Without WORKTREE_NAME the committed JSON values
// already match the default hostnames, so we leave them alone.
if (process.env.WORKTREE_NAME) {
const hostnames = getHostnames();
values.ACCOUNT_PORTAL_OIDC_ISSUER = buildPortlessUrl(hostnames.mockAuth, '/community');
values.ACCOUNT_PORTAL_OIDC_ENDPOINT = buildPortlessUrl(hostnames.mockAuth, '/community/.well-known/jwks.json');
values.STAFF_PORTAL_OIDC_ISSUER = buildPortlessUrl(hostnames.mockAuth, '/staff');
values.STAFF_PORTAL_OIDC_ENDPOINT = buildPortlessUrl(hostnames.mockAuth, '/staff/.well-known/jwks.json');
const azurite = getAzuriteConnectionString(values);
values.AZURE_STORAGE_CONNECTION_STRING = azurite;
values.AzureWebJobsStorage = azurite;
}

// Runtime-only injection: the e2e harness spawns MongoMemoryServer on a
// random port and passes the connection string through process.env.
if (process.env.COSMOSDB_CONNECTION_STRING) {
values.COSMOSDB_CONNECTION_STRING = process.env.COSMOSDB_CONNECTION_STRING;
}

settings.Values = values;
}

function isE2E() {
return ['1', 'true', 'yes'].includes((process.env.E2E ?? '').toLowerCase());
}
47 changes: 47 additions & 0 deletions apps/api/start-azurite.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { spawn } from 'node:child_process';
import { isGracefulInterruptExit } from '../../scripts/local-dev/dev-process-exit.mjs';
import { getAzuritePorts } from '../../scripts/local-dev/worktree-ports.mjs';

const ports = getAzuritePorts();
const worktreeName = process.env.WORKTREE_NAME ?? '';
const storageSuffix = worktreeName ? `-${worktreeName}` : '';

const blobDir = `../../__blobstorage__${storageSuffix}`;
const queueDir = `../../__queuestorage__${storageSuffix}`;
const tableDir = `../../__tablestorage__${storageSuffix}`;

const procSpecs = [
['azurite-blob', ['--silent', '--blobPort', String(ports.blob), '--location', blobDir]],
['azurite-queue', ['--silent', '--queuePort', String(ports.queue), '--location', queueDir]],
['azurite-table', ['--silent', '--tablePort', String(ports.table), '--location', tableDir]],
];
const procs = procSpecs.map(([command, args]) => {
const proc = spawn(command, args, { stdio: 'inherit' });
proc.on('error', (error) => {
console.error(`[azurite] failed to start ${command}: ${error.message}`);
for (const p of procs) p.kill();
process.exit(1);
});
return proc;
});

console.log(`[azurite] started (blob=${ports.blob}, queue=${ports.queue}, table=${ports.table})`);

let exited = 0;
for (const proc of procs) {
proc.on('exit', (code, signal) => {
if (isGracefulInterruptExit(signal, code)) {
if (++exited === procs.length) process.exit(0);
return;
}
console.error(`[azurite] process exited unexpectedly: code=${code} signal=${signal}`);
for (const p of procs) p.kill();
process.exit(code ?? 1);
});
}
process.on('SIGINT', () => {
for (const p of procs) p.kill('SIGINT');
});
process.on('SIGTERM', () => {
for (const p of procs) p.kill('SIGTERM');
});
29 changes: 20 additions & 9 deletions apps/api/start-dev.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { spawn } from 'node:child_process';
import os from 'node:os';
import path from 'node:path';
import { isGracefulInterruptExit } from '../../build-pipeline/scripts/dev-process-exit.mjs';
import { forwardChildExit } from '../../scripts/local-dev/dev-process-exit.mjs';
import { buildPortlessUrl, getHostnames } from '../../scripts/local-dev/portless-hostnames.mjs';
import { getAzuriteConnectionString, getMongoConnectionString } from '../../scripts/local-dev/worktree-ports.mjs';

const envPort = process.env.PORT;

Expand All @@ -18,6 +20,22 @@ const childEnv = {
NODE_OPTIONS: `${process.env.NODE_OPTIONS ?? ''} --use-system-ca`.trim(),
};

// Only inject worktree-scoped overrides when running in worktree mode.
// When WORKTREE_NAME is absent, local.settings.json remains the source of truth.
// Use `??=` so callers can override any individual value via process.env.
if (process.env.WORKTREE_NAME) {
const hostnames = getHostnames();
childEnv.ACCOUNT_PORTAL_OIDC_ISSUER ??= buildPortlessUrl(hostnames.mockAuth, '/community');
childEnv.ACCOUNT_PORTAL_OIDC_ENDPOINT ??= buildPortlessUrl(hostnames.mockAuth, '/community/.well-known/jwks.json');
childEnv.STAFF_PORTAL_OIDC_ISSUER ??= buildPortlessUrl(hostnames.mockAuth, '/staff');
childEnv.STAFF_PORTAL_OIDC_ENDPOINT ??= buildPortlessUrl(hostnames.mockAuth, '/staff/.well-known/jwks.json');
childEnv.COSMOSDB_CONNECTION_STRING ??= getMongoConnectionString();
childEnv.AZURE_STORAGE_CONNECTION_STRING ??= getAzuriteConnectionString();
childEnv.AzureWebJobsStorage ??= getAzuriteConnectionString();
// Disable the Node.js inspector — port 5858 is already used by the primary worktree.
childEnv.languageWorkers__node__arguments ??= '';
}

// `--cors '*'` matches Host.CORS in local.settings.json but does not depend on
// that file existing — local.settings.json is gitignored, so CI has no CORS
// allowance otherwise and the UI's cross-origin GraphQL requests are blocked.
Expand All @@ -26,11 +44,4 @@ const child = spawn('func', ['start', '--typescript', '--script-root', 'deploy/'
env: childEnv,
});

child.on('exit', (code, signal) => {
// Turbo sends signals to interrupt persistent tasks; treat those as graceful exits.
if (isGracefulInterruptExit(signal, code)) {
process.exitCode = 0;
return;
}
process.exitCode = code ?? 1;
});
forwardChildExit(child);
7 changes: 6 additions & 1 deletion apps/api/turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,18 @@
"build": {
"cache": true,
"dependsOn": ["^build", "//#gen"],
"inputs": ["$TURBO_EXTENDS$", "rolldown.config.ts", "host.json", "$TURBO_ROOT$/build-pipeline/scripts/**"],
"inputs": ["$TURBO_EXTENDS$", "rolldown.config.ts", "host.json", "$TURBO_ROOT$/scripts/local-dev/**"],
"outputs": ["$TURBO_EXTENDS$", "deploy/**"]
},
"dev": {
"dependsOn": ["build"],
"interruptible": true,
"inputs": []
},
"dev:worktree": {
"dependsOn": ["build"],
"interruptible": true,
"inputs": []
}
}
}
1 change: 1 addition & 0 deletions apps/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"scripts": {
"docusaurus": "docusaurus",
"dev": "pnpm exec portless docs.ownercommunity.localhost --force node start-dev.mjs",
"dev:worktree": "pnpm exec portless docs.ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.mjs",
"start": "docusaurus start --port 3001",
"build": "docusaurus build",
"swizzle": "docusaurus swizzle",
Expand Down
11 changes: 2 additions & 9 deletions apps/docs/start-dev.mjs
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
import { spawn } from 'node:child_process';
import { isGracefulInterruptExit } from '../../build-pipeline/scripts/dev-process-exit.mjs';
import { forwardChildExit } from '../../scripts/local-dev/dev-process-exit.mjs';

const port = process.env.PORT ?? '3001';

const child = spawn('docusaurus', ['start', '--port', port, '--host', '127.0.0.1', '--no-open'], {
stdio: 'inherit',
});

child.on('exit', (code, signal) => {
// Turbo sends signals to interrupt persistent tasks; treat those as graceful exits.
if (isGracefulInterruptExit(signal, code)) {
process.exitCode = 0;
return;
}
process.exitCode = code ?? 1;
});
forwardChildExit(child);
6 changes: 6 additions & 0 deletions apps/docs/turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
"interruptible": false,
"inputs": [".env", "package.json", "start-dev.mjs", "docusaurus.config.ts", "sidebars.ts", "tsconfig.json"]
},
"dev:worktree": {
"dependsOn": [],
"persistent": true,
"interruptible": false,
"inputs": [".env", "package.json", "start-dev.mjs", "docusaurus.config.ts", "sidebars.ts", "tsconfig.json"]
},
"test": {
"inputs": ["$TURBO_EXTENDS$", "!docs/**", "!blog/**", "!static/**"]
},
Expand Down
3 changes: 2 additions & 1 deletion apps/server-mongodb-memory-mock/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"format": "biome format --write",
"format:check": "biome format .",
"start": "node dist/index.js",
"dev": "tsx src/index.ts"
"dev": "tsx src/index.ts",
"dev:worktree": "node start-mongo.mjs"
},
"dependencies": {
"@cellix/server-mongodb-memory-mock-seedwork": "workspace:*",
Expand Down
12 changes: 12 additions & 0 deletions apps/server-mongodb-memory-mock/start-mongo.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { spawn } from 'node:child_process';
import { forwardChildExit } from '../../scripts/local-dev/dev-process-exit.mjs';
import { getMongoPort } from '../../scripts/local-dev/worktree-ports.mjs';

const MONGO_PORT = getMongoPort();

const child = spawn('tsx', ['src/index.ts'], {
stdio: 'inherit',
env: { ...process.env, PORT: String(MONGO_PORT) },
});

forwardChildExit(child);
6 changes: 6 additions & 0 deletions apps/server-mongodb-memory-mock/turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
"persistent": true,
"interruptible": true,
"inputs": []
},
"dev:worktree": {
"dependsOn": ["build"],
"persistent": true,
"interruptible": true,
"inputs": []
}
}
}
1 change: 1 addition & 0 deletions apps/server-oauth2-mock/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"format:check": "biome format .",
"start": "node dist/index.js",
"dev": "pnpm exec portless mock-auth.ownercommunity.localhost --force tsx src/index.ts",
"dev:worktree": "pnpm exec portless mock-auth.ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.mjs",
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"test:watch": "vitest"
Expand Down
9 changes: 5 additions & 4 deletions apps/server-oauth2-mock/src/portal-discovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,16 +119,17 @@ function buildPortalFromConfig(config: MockOidcConfig, parsedEnv: Record<string,
const clientIdVar = config.envVars.clientId;
const redirectUriVar = config.envVars.redirectUri;

const clientId = parsedEnv[clientIdVar];
const redirectUri = parsedEnv[redirectUriVar];
// process.env takes precedence — allows worktree-scoped overrides injected at startup
const clientId = process.env[clientIdVar] ?? parsedEnv[clientIdVar];
const redirectUri = process.env[redirectUriVar] ?? parsedEnv[redirectUriVar];

if (!clientId) {
console.warn(`[server-oauth2-mock] Skipping ${entryName}: env var ${clientIdVar} not found in .env`);
console.warn(`[server-oauth2-mock] Skipping ${entryName}: env var ${clientIdVar} not found in .env or process.env`);
return null;
}

if (!redirectUri) {
console.warn(`[server-oauth2-mock] Skipping ${entryName}: env var ${redirectUriVar} not found in .env`);
console.warn(`[server-oauth2-mock] Skipping ${entryName}: env var ${redirectUriVar} not found in .env or process.env`);
return null;
}

Expand Down
20 changes: 20 additions & 0 deletions apps/server-oauth2-mock/start-dev.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { spawn } from 'node:child_process';
import { forwardChildExit } from '../../scripts/local-dev/dev-process-exit.mjs';
import { buildPortlessUrl, getHostnames } from '../../scripts/local-dev/portless-hostnames.mjs';

const childEnv = { ...process.env };

if (process.env.WORKTREE_NAME) {
const hostnames = getHostnames();
childEnv.BASE_URL = buildPortlessUrl(hostnames.mockAuth);
// Override redirect URIs so portal-discovery picks up worktree-scoped URLs.
childEnv.VITE_APP_UI_COMMUNITY_B2C_REDIRECT_URI = buildPortlessUrl(hostnames.uiCommunity, '/auth-redirect');
childEnv.VITE_APP_UI_STAFF_AAD_REDIRECT_URI = buildPortlessUrl(hostnames.uiStaff, '/auth-redirect');
}

const child = spawn('tsx', ['src/index.ts'], {
stdio: 'inherit',
env: childEnv,
});

forwardChildExit(child);
Loading
Loading