Skip to content

Commit 94b7aad

Browse files
committed
fix: centralize local script instance targeting
Signed-off-by: zebbern <185730623+zebbern@users.noreply.github.com>
1 parent 92612fb commit 94b7aad

16 files changed

Lines changed: 315 additions & 58 deletions

AGENTS.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,9 @@ Local development runs as **multiple app instances** (PM2) on top of **one share
9999
- If the user says “use instance N”, prefer either:
100100
- `just instance use N` then run `just dev` / `bun run test:e2e`, or
101101
- explicit env override (`SENTRIS_INSTANCE=N just dev ...`) for one-off commands.
102-
- Backend maintenance scripts that mutate or inspect local Postgres data must use the shared script database resolver (`backend/scripts/lib/script-database-target.ts`) instead of reading `DATABASE_URL` directly. `DATABASE_URL` is for the running backend process, Drizzle CLI, and explicit app env files; local scripts should target `SENTRIS_INSTANCE` / `.sentris-instance` by default and only use script-specific overrides such as `TEMPLATE_SEED_DATABASE_URL` or the generic `SENTRIS_SCRIPT_DATABASE_URL`.
103-
- Maintenance scripts must print the target database before mutating data.
102+
- Local maintenance scripts that mutate or inspect local Postgres data must use the shared script runtime (`scripts/lib/local-script-runtime.ts`) instead of reading `DATABASE_URL` directly. `DATABASE_URL` is for the running app process, Drizzle CLI, and explicit app env files; local scripts should target `SENTRIS_INSTANCE` / `.sentris-instance` by default and only use script-specific overrides such as `TEMPLATE_SEED_DATABASE_URL` or the generic `SENTRIS_SCRIPT_DATABASE_URL`.
103+
- Local scripts that start or inspect Temporal workflows must also use `getScriptTemporalTarget()` from the shared script runtime instead of reading `TEMPORAL_NAMESPACE` / `TEMPORAL_TASK_QUEUE` directly. Use script-specific `*_TEMPORAL_NAMESPACE` + `*_TEMPORAL_TASK_QUEUE` variables or `SENTRIS_SCRIPT_TEMPORAL_NAMESPACE` + `SENTRIS_SCRIPT_TEMPORAL_TASK_QUEUE` when intentionally targeting another namespace.
104+
- Maintenance scripts must print the target database and/or Temporal target before mutating data or starting workflows.
104105

105106
#### Ports / URLs
106107

backend/.env.example

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
# Sentris backend environment defaults
22

33
# Primary Postgres database connection
4-
DATABASE_URL="postgresql://sentris:sentris@localhost:5433/sentris"
4+
DATABASE_URL="postgresql://sentris:sentris@localhost:5433/sentris_instance_0"
55

66
# HTTP server configuration
77
PORT="3211"
88

99
# Temporal connection details
1010
TEMPORAL_ADDRESS="localhost:7233"
11-
TEMPORAL_NAMESPACE="sentris-dev"
12-
TEMPORAL_TASK_QUEUE="sentris-dev"
11+
TEMPORAL_NAMESPACE="sentris-dev-0"
12+
TEMPORAL_TASK_QUEUE="sentris-dev-0"
1313

1414
# MinIO credentials for artifact storage
1515
MINIO_ROOT_USER="minioadmin"

backend/scripts/__tests__/script-database-target.test.ts

Lines changed: 92 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
import { mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'fs';
22
import { tmpdir } from 'os';
3-
import { dirname, join } from 'path';
3+
import { dirname, join, resolve } from 'path';
44
import { fileURLToPath } from 'url';
55
import { afterEach, describe, expect, it } from 'bun:test';
66
import {
7+
formatTemporalTarget,
78
getScriptDatabaseTarget,
9+
getScriptTemporalTarget,
810
readActiveInstance,
911
redactConnectionString,
10-
} from '../lib/script-database-target';
12+
} from '../../../scripts/lib/local-script-runtime';
1113

1214
let cleanupDirs: string[] = [];
1315
const scriptsDir = dirname(dirname(fileURLToPath(import.meta.url)));
16+
const repoRoot = resolve(scriptsDir, '..', '..');
1417

1518
function createRootWithInstance(instance: string): string {
1619
const root = mkdtempSync(join(tmpdir(), 'sentris-script-db-target-'));
@@ -97,19 +100,105 @@ describe('script database target resolver', () => {
97100
).toBe('postgresql://sentris:***@localhost:5433/sentris_instance_0');
98101
});
99102

103+
it('uses the active instance instead of stale Temporal env for local scripts', () => {
104+
const target = getScriptTemporalTarget({
105+
env: {
106+
SENTRIS_INSTANCE: '4',
107+
TEMPORAL_NAMESPACE: 'sentris-dev',
108+
TEMPORAL_TASK_QUEUE: 'sentris-default',
109+
},
110+
repoRoot: createRootWithInstance('2'),
111+
});
112+
113+
expect(target.namespace).toBe('sentris-dev-4');
114+
expect(target.taskQueue).toBe('sentris-dev-4');
115+
expect(target.source).toBe('env:SENTRIS_INSTANCE');
116+
expect(target.ignoredTemporalEnv).toBe(true);
117+
expect(formatTemporalTarget(target)).toContain(
118+
'TEMPORAL_NAMESPACE/TEMPORAL_TASK_QUEUE ignored',
119+
);
120+
});
121+
122+
it('allows script-specific Temporal overrides when intentionally targeting another namespace', () => {
123+
const target = getScriptTemporalTarget({
124+
namespaceOverrideEnvVar: 'WORKFLOW_RUNNER_TEMPORAL_NAMESPACE',
125+
taskQueueOverrideEnvVar: 'WORKFLOW_RUNNER_TEMPORAL_TASK_QUEUE',
126+
env: {
127+
SENTRIS_INSTANCE: '4',
128+
SENTRIS_SCRIPT_TEMPORAL_NAMESPACE: 'generic-namespace',
129+
SENTRIS_SCRIPT_TEMPORAL_TASK_QUEUE: 'generic-task-queue',
130+
WORKFLOW_RUNNER_TEMPORAL_NAMESPACE: 'manual-namespace',
131+
WORKFLOW_RUNNER_TEMPORAL_TASK_QUEUE: 'manual-task-queue',
132+
},
133+
repoRoot: createRootWithInstance('2'),
134+
});
135+
136+
expect(target.namespace).toBe('manual-namespace');
137+
expect(target.taskQueue).toBe('manual-task-queue');
138+
expect(target.source).toBe(
139+
'env:WORKFLOW_RUNNER_TEMPORAL_NAMESPACE/env:WORKFLOW_RUNNER_TEMPORAL_TASK_QUEUE',
140+
);
141+
});
142+
100143
it('prevents backend maintenance scripts from reading DATABASE_URL directly', () => {
101144
const scriptFiles = readdirSync(scriptsDir)
102145
.filter((file) => file.endsWith('.ts'))
103146
.filter((file) => file !== 'generate-openapi.ts' && file !== 'version-check-summary.ts');
104147

105148
for (const file of scriptFiles) {
106149
const source = readFileSync(join(scriptsDir, file), 'utf-8');
107-
expect(source, `${file} should use script-database-target.ts`).not.toContain(
150+
expect(source, `${file} should use local-script-runtime.ts`).not.toContain(
108151
'process.env.DATABASE_URL',
109152
);
110153
expect(source, `${file} should not fall back to the legacy sentris database`).not.toContain(
111154
'postgresql://sentris:sentris@localhost:5433/sentris',
112155
);
113156
}
114157
});
158+
159+
it('prevents all local database maintenance scripts from reading DATABASE_URL directly', () => {
160+
const databaseScriptFiles = [
161+
join(scriptsDir, 'delete-all-workflow-runs.ts'),
162+
join(scriptsDir, 'migration-smoke.ts'),
163+
join(scriptsDir, 'seed-stress-test.ts'),
164+
join(scriptsDir, 'seed-templates.ts'),
165+
join(repoRoot, 'worker', 'scripts', 'run-long-lived-workflow.ts'),
166+
join(repoRoot, 'worker', 'scripts', 'workflow-execute-inline.ts'),
167+
join(repoRoot, 'worker', 'scripts', 'workflow-runner.ts'),
168+
];
169+
170+
for (const file of databaseScriptFiles) {
171+
const source = readFileSync(file, 'utf-8');
172+
expect(source, `${file} should use the shared script database resolver`).toContain(
173+
'getScriptDatabaseTarget',
174+
);
175+
expect(source, `${file} should not read DATABASE_URL directly`).not.toContain(
176+
'process.env.DATABASE_URL',
177+
);
178+
expect(source, `${file} should not fall back to the legacy sentris database`).not.toContain(
179+
'postgresql://sentris:sentris@localhost:5433/sentris',
180+
);
181+
}
182+
});
183+
184+
it('prevents local Temporal scripts from reading stale Temporal env directly', () => {
185+
const temporalScriptFiles = [
186+
join(repoRoot, 'worker', 'scripts', 'benchmark-scheduler.ts'),
187+
join(repoRoot, 'worker', 'scripts', 'run-long-lived-workflow.ts'),
188+
join(repoRoot, 'worker', 'scripts', 'workflow-runner.ts'),
189+
];
190+
191+
for (const file of temporalScriptFiles) {
192+
const source = readFileSync(file, 'utf-8');
193+
expect(source, `${file} should use the shared script Temporal resolver`).toContain(
194+
'getScriptTemporalTarget',
195+
);
196+
expect(source, `${file} should not read TEMPORAL_NAMESPACE directly`).not.toContain(
197+
'process.env.TEMPORAL_NAMESPACE',
198+
);
199+
expect(source, `${file} should not read TEMPORAL_TASK_QUEUE directly`).not.toContain(
200+
'process.env.TEMPORAL_TASK_QUEUE',
201+
);
202+
}
203+
});
115204
});

backend/scripts/delete-all-workflow-runs.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { Pool } from 'pg';
2-
import { formatDatabaseTarget, getScriptDatabaseTarget } from './lib/script-database-target';
2+
import {
3+
formatDatabaseTarget,
4+
getScriptDatabaseTarget,
5+
} from '../../scripts/lib/local-script-runtime';
36

47
async function main() {
58
const databaseTarget = getScriptDatabaseTarget({

backend/scripts/migration-smoke.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { readFileSync, readdirSync } from 'fs';
22
import { join, resolve } from 'path';
33
import { Pool } from 'pg';
4-
import { formatDatabaseTarget, getScriptDatabaseTarget } from './lib/script-database-target';
4+
import {
5+
formatDatabaseTarget,
6+
getScriptDatabaseTarget,
7+
} from '../../scripts/lib/local-script-runtime';
58

69
async function main() {
710
const databaseTarget = getScriptDatabaseTarget({

backend/scripts/seed-stress-test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import { Pool, PoolClient } from 'pg';
22
import { randomUUID } from 'crypto';
33
import { SecretEncryption, parseMasterKey } from '@sentris/shared';
44
import * as bcrypt from 'bcryptjs';
5-
import { formatDatabaseTarget, getScriptDatabaseTarget } from './lib/script-database-target';
5+
import {
6+
formatDatabaseTarget,
7+
getScriptDatabaseTarget,
8+
} from '../../scripts/lib/local-script-runtime';
69

710
// ─── Configuration ───────────────────────────────────────────────────────────
811

backend/scripts/seed-templates.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ import { randomUUID } from 'crypto';
1414
import { readFileSync, readdirSync } from 'fs';
1515
import { join, dirname } from 'path';
1616
import { fileURLToPath } from 'url';
17-
import { formatDatabaseTarget, getScriptDatabaseTarget } from './lib/script-database-target';
17+
import {
18+
formatDatabaseTarget,
19+
getScriptDatabaseTarget,
20+
} from '../../scripts/lib/local-script-runtime';
1821

1922
const __filename = fileURLToPath(import.meta.url);
2023
const __dirname = dirname(__filename);

docs/MULTI-INSTANCE-DEV.mdx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,17 +138,29 @@ When stopping/cleaning instance 0, Docker infra is also torn down. For non-zero
138138

139139
## Local Maintenance Scripts
140140

141-
Backend maintenance scripts that mutate or inspect local Postgres data are instance-aware. They resolve their target database in this order:
141+
Local maintenance scripts use the shared script runtime at `scripts/lib/local-script-runtime.ts`.
142+
143+
Scripts that mutate or inspect local Postgres data are instance-aware. They resolve their target database in this order:
142144

143145
1. Script-specific override, such as `TEMPLATE_SEED_DATABASE_URL`, `DELETE_WORKFLOW_RUNS_DATABASE_URL`, `MIGRATION_SMOKE_DATABASE_URL`, or `SEED_STRESS_DATABASE_URL`
144146
2. Generic script override: `SENTRIS_SCRIPT_DATABASE_URL`
145147
3. `SENTRIS_INSTANCE`
146148
4. `.sentris-instance`
147149
5. Instance `0`
148150

149-
These scripts intentionally do **not** use plain `DATABASE_URL` by default. `DATABASE_URL` belongs to the running backend process, Drizzle CLI, and app env files; using it for local maintenance scripts can accidentally target the legacy `sentris` database instead of `sentris_instance_N`.
151+
These scripts intentionally do **not** use plain `DATABASE_URL` by default. `DATABASE_URL` belongs to the running app process, Drizzle CLI, and app env files; using it for local maintenance scripts can accidentally target the legacy `sentris` database instead of `sentris_instance_N`.
152+
153+
Scripts that start or inspect Temporal workflows resolve namespace and task queue in this order:
154+
155+
1. Script-specific override pair, such as `WORKFLOW_RUNNER_TEMPORAL_NAMESPACE` + `WORKFLOW_RUNNER_TEMPORAL_TASK_QUEUE`
156+
2. Generic override pair: `SENTRIS_SCRIPT_TEMPORAL_NAMESPACE` + `SENTRIS_SCRIPT_TEMPORAL_TASK_QUEUE`
157+
3. `SENTRIS_INSTANCE`
158+
4. `.sentris-instance`
159+
5. Instance `0`
160+
161+
These scripts intentionally do **not** use plain `TEMPORAL_NAMESPACE` / `TEMPORAL_TASK_QUEUE` by default for the same reason: stale app env files can point a script at the wrong local instance.
150162

151-
Every maintenance script should print its target database before mutating data:
163+
Every maintenance script should print its target database and/or Temporal target before mutating data or starting workflows:
152164

153165
```bash
154166
bun --cwd backend scripts/seed-templates.ts
@@ -159,6 +171,9 @@ SENTRIS_INSTANCE=2 bun --cwd backend scripts/seed-templates.ts
159171

160172
TEMPLATE_SEED_DATABASE_URL=postgresql://sentris:sentris@localhost:5433/custom_db bun --cwd backend scripts/seed-templates.ts
161173
# Target database: custom_db via env:TEMPLATE_SEED_DATABASE_URL
174+
175+
bun --cwd worker scripts/workflow-runner.ts --workflow <workflow-id>
176+
# Target Temporal: namespace=sentris-dev-0, taskQueue=sentris-dev-0 via default:instance-0
162177
```
163178

164179
## E2E Tests (Instance-Aware)

docs/development/dev-environment.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ Key variables:
231231

232232
| Variable | Default | Purpose |
233233
| ------------------------- | ----------------------------------------------------- | ---------------------------------- |
234-
| `DATABASE_URL` | `postgresql://sentris:sentris@localhost:5433/sentris` | Postgres connection |
234+
| `DATABASE_URL` | `postgresql://sentris:sentris@localhost:5433/sentris_instance_0` | Postgres connection |
235235
| `PORT` | `3211` | Backend HTTP port |
236236
| `TEMPORAL_ADDRESS` | `localhost:7233` | Temporal gRPC endpoint |
237237
| `AUTH_PROVIDER` | `local` | Auth mode (`local` or `clerk`) |
@@ -245,7 +245,7 @@ Key variables:
245245

246246
| Variable | Default | Purpose |
247247
| ------------------------- | ----------------------------------------------------- | ----------------------------- |
248-
| `DATABASE_URL` | `postgresql://sentris:sentris@localhost:5433/sentris` | Postgres connection |
248+
| `DATABASE_URL` | `postgresql://sentris:sentris@localhost:5433/sentris_instance_0` | Postgres connection |
249249
| `TEMPORAL_ADDRESS` | `localhost:7233` | Temporal gRPC endpoint |
250250
| `MINIO_ENDPOINT` | `localhost` | MinIO host |
251251
| `SECRET_STORE_MASTER_KEY` | `CHANGE_ME_32_CHAR_SECRET_KEY!!!!` | Must match backend |

scripts/instance-env.sh

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ set -euo pipefail
1212
APPS=(backend worker frontend)
1313
BASE_BACKEND_PORT=3211
1414
BASE_FRONTEND_PORT=5173
15-
BASE_DB_NAME="sentris"
15+
BASE_DB_NAME="sentris_instance"
1616
BASE_TEMPORAL_NS="sentris-dev"
1717

1818
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -48,19 +48,11 @@ get_frontend_port() {
4848
}
4949

5050
get_db_name() {
51-
if [ "$1" -eq 0 ]; then
52-
echo "$BASE_DB_NAME"
53-
else
54-
echo "${BASE_DB_NAME}_instance_$1"
55-
fi
51+
echo "${BASE_DB_NAME}_$1"
5652
}
5753

5854
get_temporal_ns() {
59-
if [ "$1" -eq 0 ]; then
60-
echo "$BASE_TEMPORAL_NS"
61-
else
62-
echo "${BASE_TEMPORAL_NS}-$1"
63-
fi
55+
echo "${BASE_TEMPORAL_NS}-$1"
6456
}
6557

6658
get_db_url() {

0 commit comments

Comments
 (0)