Skip to content

Commit 0464ddb

Browse files
d-csclaude
andcommitted
refactor(run-ops): one RUN_OPS_* env var family, drop TASK_RUN_* aliases
The NEW-DB writer had two names (TASK_RUN_DATABASE_URL from the original split + RUN_OPS_DATABASE_URL used by the schema/docker/.env), bridged by a `??` coalesce. Collapse to a single canonical RUN_OPS_* family and delete the aliases (nothing deployed uses them yet): - TASK_RUN_DATABASE_URL -> RUN_OPS_DATABASE_URL (the writer; the ?? and the runOpsNewDatabaseUrl indirection are gone — consumers read env.RUN_OPS_DATABASE_URL directly) - TASK_RUN_LEGACY_DATABASE_URL -> RUN_OPS_LEGACY_DATABASE_URL - TASK_RUN_DATABASE_READ_REPLICA_URL -> RUN_OPS_DATABASE_READ_REPLICA_URL - TASK_RUN_DATABASE_DIRECT_URL -> removed (dead; the schema no longer declares directUrl) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 29cde4a commit 0464ddb

10 files changed

Lines changed: 37 additions & 72 deletions

File tree

.server-changes/run-ops-auto-migrate.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ area: webapp
33
type: feature
44
---
55

6-
Automatically migrate the dedicated run-ops database on deploy (entrypoint + `@internal/run-ops-database` deploy/status scripts) and resolve its connection through one canonical `RUN_OPS_DATABASE_URL` (falling back to `TASK_RUN_DATABASE_URL`) so migrations always target the DB the app connects to.
6+
Automatically migrate the dedicated run-ops database on deploy (entrypoint + `@internal/run-ops-database` deploy/status scripts), and standardize the run-ops DB connection on a single `RUN_OPS_DATABASE_URL` family (dropping the `TASK_RUN_DATABASE_URL` aliases) so migrations always target the DB the app connects to.

apps/webapp/app/db.server.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
import { RunOpsPrismaClient } from "@internal/run-ops-database";
1111
import invariant from "tiny-invariant";
1212
import { z } from "zod";
13-
import { env, runOpsNewDatabaseUrl } from "./env.server";
13+
import { env } from "./env.server";
1414
import { logger } from "./services/logger.server";
1515
import { isValidDatabaseUrl } from "./utils/db";
1616
import {
@@ -237,16 +237,16 @@ export function selectRunOpsTopology(
237237
// nothing new. The builders apply the SAME wrapper pair the control-plane
238238
// singletons use (captureInfrastructureErrors(tagDatasource(role, raw))).
239239
const runOpsTopology: RunOpsTopology = singleton("runOpsTopology", () => {
240-
const newUrl = runOpsNewDatabaseUrl;
240+
const newUrl = env.RUN_OPS_DATABASE_URL;
241241
// Gate on the opt-in flag too: the distinct-DB sentinel only runs when the flag is on.
242-
const splitEnabled = env.RUN_OPS_SPLIT_ENABLED && !!newUrl && !!env.TASK_RUN_LEGACY_DATABASE_URL;
242+
const splitEnabled = env.RUN_OPS_SPLIT_ENABLED && !!newUrl && !!env.RUN_OPS_LEGACY_DATABASE_URL;
243243

244244
return selectRunOpsTopology(
245245
{
246246
splitEnabled,
247-
legacyUrl: env.TASK_RUN_LEGACY_DATABASE_URL,
247+
legacyUrl: env.RUN_OPS_LEGACY_DATABASE_URL,
248248
newUrl,
249-
newReplicaUrl: env.TASK_RUN_DATABASE_READ_REPLICA_URL,
249+
newReplicaUrl: env.RUN_OPS_DATABASE_READ_REPLICA_URL,
250250
},
251251
{
252252
controlPlane: { writer: prisma, replica: $replica },
@@ -278,8 +278,8 @@ export const runOpsSplitReadEnabled: boolean = computeRunOpsSplitReadEnabled({
278278
newReplica: runOpsNewReplicaClient,
279279
controlPlaneWriter: prisma,
280280
controlPlaneReplica: $replica,
281-
hasNewUrl: !!runOpsNewDatabaseUrl,
282-
hasLegacyUrl: !!env.TASK_RUN_LEGACY_DATABASE_URL,
281+
hasNewUrl: !!env.RUN_OPS_DATABASE_URL,
282+
hasLegacyUrl: !!env.RUN_OPS_LEGACY_DATABASE_URL,
283283
});
284284

285285
// Boot-time interlock: if the flag is on but the distinct-DB sentinel does not

apps/webapp/app/env.server.ts

Lines changed: 9 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -132,36 +132,24 @@ const EnvironmentSchema = z
132132
// Explicit positive opt-in. Split behavior is unreachable unless this is true
133133
// AND the distinct-DB sentinel confirms the two URLs are physically distinct DBs.
134134
RUN_OPS_SPLIT_ENABLED: BoolEnv.default(false),
135-
// Canonical connection URL for the dedicated run-ops DB — drives the runtime pool, the split
136-
// decision, replication, and migrations (resolved via runOpsNewDatabaseUrl). Takes precedence
137-
// over TASK_RUN_DATABASE_URL, which remains as the compatibility fallback.
135+
// Canonical connection URL for the dedicated NEW run-ops DB — drives the runtime pool, the split
136+
// decision, replication, and migrations. Optional so single-DB installs never set it.
138137
RUN_OPS_DATABASE_URL: z
139138
.string()
140139
.refine(isValidDatabaseUrl, "RUN_OPS_DATABASE_URL is invalid")
141140
.optional(),
142-
// The NEW dedicated run-ops DB writer. Optional so single-DB installs never set it.
143-
TASK_RUN_DATABASE_URL: z
144-
.string()
145-
.refine(isValidDatabaseUrl, "TASK_RUN_DATABASE_URL is invalid")
146-
.optional(),
147-
// The NEW run-ops DB unpooled/direct endpoint (Prisma migrate/introspection;
148-
// connection poolers break advisory locks). Consumed by the migrations.
149-
TASK_RUN_DATABASE_DIRECT_URL: z
150-
.string()
151-
.refine(isValidDatabaseUrl, "TASK_RUN_DATABASE_DIRECT_URL is invalid")
152-
.optional(),
153141
// The LEGACY run-ops DB (the control-plane DB during the transition). When unset, legacy
154142
// run-ops reuses the existing DATABASE_URL (legacy run-ops == control-plane DB initially).
155-
TASK_RUN_LEGACY_DATABASE_URL: z
143+
RUN_OPS_LEGACY_DATABASE_URL: z
156144
.string()
157-
.refine(isValidDatabaseUrl, "TASK_RUN_LEGACY_DATABASE_URL is invalid")
145+
.refine(isValidDatabaseUrl, "RUN_OPS_LEGACY_DATABASE_URL is invalid")
158146
.optional(),
159147
// The NEW dedicated run-ops DB read replica. Optional; self-host never sets it.
160148
// Refined (unlike the unrefined control-plane DATABASE_READ_REPLICA_URL) so a malformed run-ops
161149
// replica URL fails boot loudly rather than silently degrading — do not align it down to the CP shape.
162-
TASK_RUN_DATABASE_READ_REPLICA_URL: z
150+
RUN_OPS_DATABASE_READ_REPLICA_URL: z
163151
.string()
164-
.refine(isValidDatabaseUrl, "TASK_RUN_DATABASE_READ_REPLICA_URL is invalid")
152+
.refine(isValidDatabaseUrl, "RUN_OPS_DATABASE_READ_REPLICA_URL is invalid")
165153
.optional(),
166154
// --- Control-plane datasource repoint. Additive-only. ---
167155
// Optional control-plane DB. Unset (self-host/single-DB) -> getClient()/getReplicaClient() fall back to
@@ -1725,9 +1713,9 @@ const EnvironmentSchema = z
17251713

17261714
// --- Run-ops DB split — second replication source (the NEW dedicated run-ops DB). ---
17271715
// Cloud-only; only consulted when isSplitEnabled() is true. Self-host never sets these.
1728-
// The NEW source's connection URL is runOpsNewDatabaseUrl (RUN_OPS_DATABASE_URL ?? TASK_RUN_DATABASE_URL);
1729-
// these add the NEW source's replication slot/publication and an explicit per-source enable so it can be
1730-
// brought up independently of the legacy source during the transition.
1716+
// The NEW source's connection URL is RUN_OPS_DATABASE_URL; these add the NEW source's replication
1717+
// slot/publication and an explicit per-source enable so it can be brought up independently of the
1718+
// legacy source during the transition.
17311719
RUN_REPLICATION_NEW_SLOT_NAME: z.string().default("task_runs_to_clickhouse_v2"),
17321720
RUN_REPLICATION_NEW_PUBLICATION_NAME: z
17331721
.string()
@@ -2103,10 +2091,3 @@ const EnvironmentSchema = z
21032091

21042092
export type Environment = z.infer<typeof EnvironmentSchema>;
21052093
export const env = EnvironmentSchema.parse(process.env);
2106-
2107-
// Canonical connection URL for the NEW dedicated run-ops DB. RUN_OPS_DATABASE_URL is the canonical
2108-
// name; TASK_RUN_DATABASE_URL is the compatibility fallback. Every NEW-DB consumer (connect path,
2109-
// split-decision predicates, replication source, migration runner) resolves through this single
2110-
// value so they can never disagree about which physical database the split targets.
2111-
export const runOpsNewDatabaseUrl: string | undefined =
2112-
env.RUN_OPS_DATABASE_URL ?? env.TASK_RUN_DATABASE_URL;

apps/webapp/app/services/runsReplicationInstance.server.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import invariant from "tiny-invariant";
2-
import { env, runOpsNewDatabaseUrl } from "~/env.server";
2+
import { env } from "~/env.server";
33
import { clickhouseFactory } from "~/services/clickhouse/clickhouseFactoryInstance.server";
44
import { singleton } from "~/utils/singleton";
55
import { isSplitEnabled } from "~/v3/runOpsMigration/splitMode.server";
@@ -172,7 +172,7 @@ function initializeRunsReplicationInstance() {
172172
const sources = buildReplicationSources({
173173
splitEnabled,
174174
legacyUrl: DATABASE_URL,
175-
newUrl: runOpsNewDatabaseUrl,
175+
newUrl: env.RUN_OPS_DATABASE_URL,
176176
newSourceOverride: env.RUN_REPLICATION_NEW_ENABLED === "disabled" ? false : undefined,
177177
legacySlotName: env.RUN_REPLICATION_SLOT_NAME,
178178
legacyPublicationName: env.RUN_REPLICATION_PUBLICATION_NAME,

apps/webapp/app/v3/runOpsMigration/controlPlaneResolver.server.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type {
55
RuntimeEnvironmentType,
66
} from "@trigger.dev/database";
77
import { prisma, $replica } from "~/db.server";
8-
import { env, runOpsNewDatabaseUrl } from "~/env.server";
8+
import { env } from "~/env.server";
99
import {
1010
ControlPlaneCache,
1111
DEFAULT_CP_CACHE_MAX_ENTRIES,
@@ -449,7 +449,7 @@ export class ControlPlaneResolver {
449449
// run-ops topology factory uses); the async isSplitEnabled() distinct-DB sentinel is enforced
450450
// at boot elsewhere and is never awaited on a resolver hot path.
451451
const SPLIT_ENABLED =
452-
env.RUN_OPS_SPLIT_ENABLED && !!runOpsNewDatabaseUrl && !!env.TASK_RUN_LEGACY_DATABASE_URL;
452+
env.RUN_OPS_SPLIT_ENABLED && !!env.RUN_OPS_DATABASE_URL && !!env.RUN_OPS_LEGACY_DATABASE_URL;
453453

454454
export const controlPlaneResolver = new ControlPlaneResolver({
455455
controlPlanePrimary: prisma,

apps/webapp/app/v3/runOpsMigration/splitMode.server.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* infer split-vs-single from URL string-equality — distinctness is proven by the
55
* runtime sentinel.
66
*/
7-
import { env, runOpsNewDatabaseUrl } from "~/env.server";
7+
import { env } from "~/env.server";
88
import { logger } from "~/services/logger.server";
99
import { probeDistinctDatabases as defaultProbe } from "./distinctDbSentinel.server";
1010

@@ -30,7 +30,7 @@ export async function computeSplitEnabled(
3030
// Both URLs are required to even consider a split.
3131
if (!config.legacyUrl || !config.newUrl) {
3232
deps.logger?.warn(
33-
"RUN_OPS_SPLIT_ENABLED is on but TASK_RUN_LEGACY_DATABASE_URL / TASK_RUN_DATABASE_URL are not both set; staying single-DB."
33+
"RUN_OPS_SPLIT_ENABLED is on but RUN_OPS_LEGACY_DATABASE_URL / RUN_OPS_DATABASE_URL are not both set; staying single-DB."
3434
);
3535
return false;
3636
}
@@ -70,8 +70,8 @@ export function isSplitEnabled(): Promise<boolean> {
7070
cached = computeSplitEnabled(
7171
{
7272
flagEnabled: env.RUN_OPS_SPLIT_ENABLED,
73-
legacyUrl: env.TASK_RUN_LEGACY_DATABASE_URL,
74-
newUrl: runOpsNewDatabaseUrl,
73+
legacyUrl: env.RUN_OPS_LEGACY_DATABASE_URL,
74+
newUrl: env.RUN_OPS_DATABASE_URL,
7575
},
7676
{ logger }
7777
);

apps/webapp/app/v3/runStore.server.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
runOpsNewPrismaClient,
1111
runOpsNewReplicaClient,
1212
} from "~/db.server";
13-
import { env, runOpsNewDatabaseUrl } from "~/env.server";
13+
import { env } from "~/env.server";
1414
import { singleton } from "~/utils/singleton";
1515

1616
type BuildRunStoreDeps = {
@@ -76,7 +76,7 @@ export function buildRunStore(deps: BuildRunStoreDeps): RunStore {
7676
// RUN_OPS_SPLIT_ENABLED. Reads must fan out across both DBs so a run that lives on the new
7777
// DB stays visible even with the flag off (matches the db.server topology factory). The flag
7878
// governs write/mint residency + migration via isSplitEnabled(), not read visibility.
79-
const ROUTING_ENABLED = !!runOpsNewDatabaseUrl && !!env.TASK_RUN_LEGACY_DATABASE_URL;
79+
const ROUTING_ENABLED = !!env.RUN_OPS_DATABASE_URL && !!env.RUN_OPS_LEGACY_DATABASE_URL;
8080

8181
// Resolve the run-ops handles, tolerating contexts where they are absent — tests that mock
8282
// ~/db.server minimally omit them, and accessing a missing export under vi.mock throws. A

apps/webapp/test/runsReplicationInstance.test.ts

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -126,35 +126,19 @@ describe("buildReplicationSources (pure)", () => {
126126
expect(sources[0].id).toBe("legacy");
127127
});
128128

129-
it("RUN_OPS_DATABASE_URL takes precedence: new source pgConnectionUrl === RUN_OPS_DATABASE_URL when both are supplied", () => {
129+
it("new source pgConnectionUrl === the provided RUN_OPS_DATABASE_URL", () => {
130130
const runOpsUrl = "postgres://run-ops-dedicated";
131-
const taskRunUrl = "postgres://task-run-legacy-alias";
132131

133132
const sources = buildReplicationSources({
134133
...baseArgs,
135134
splitEnabled: true,
136-
// Simulates env.RUN_OPS_DATABASE_URL ?? env.TASK_RUN_DATABASE_URL with RUN_OPS set
137-
newUrl: runOpsUrl ?? taskRunUrl,
135+
newUrl: runOpsUrl,
138136
});
139137

140138
expect(sources).toHaveLength(2);
141139
expect(sources[1]!.id).toBe("new");
142140
expect(sources[1]!.pgConnectionUrl).toBe(runOpsUrl);
143141
});
144-
145-
it("falls back to TASK_RUN_DATABASE_URL when RUN_OPS_DATABASE_URL is absent", () => {
146-
const taskRunUrl = "postgres://task-run-legacy-alias";
147-
148-
const sources = buildReplicationSources({
149-
...baseArgs,
150-
splitEnabled: true,
151-
// Simulates env.RUN_OPS_DATABASE_URL ?? env.TASK_RUN_DATABASE_URL with RUN_OPS unset
152-
newUrl: undefined ?? taskRunUrl,
153-
});
154-
155-
expect(sources).toHaveLength(2);
156-
expect(sources[1]!.pgConnectionUrl).toBe(taskRunUrl);
157-
});
158142
});
159143

160144
describe("assertReplicationCoversSplit (boot gate-coupling)", () => {

docker/scripts/entrypoint.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ fi
1515

1616
# Run-ops split: migrate the dedicated NEW run-ops database only when it is configured. Single-DB
1717
# installs never set the URL, so this is a no-op there.
18-
if [ -n "$RUN_OPS_DATABASE_URL" ] || [ -n "$TASK_RUN_DATABASE_URL" ]; then
18+
if [ -n "$RUN_OPS_DATABASE_URL" ]; then
1919
if [ "$SKIP_RUN_OPS_MIGRATIONS" != "1" ]; then
2020
echo "Running run-ops migrations"
2121
pnpm --filter @internal/run-ops-database db:migrate:deploy
@@ -24,7 +24,7 @@ if [ -n "$RUN_OPS_DATABASE_URL" ] || [ -n "$TASK_RUN_DATABASE_URL" ]; then
2424
echo "SKIP_RUN_OPS_MIGRATIONS=1, skipping run-ops migrations."
2525
fi
2626
else
27-
echo "RUN_OPS_DATABASE_URL/TASK_RUN_DATABASE_URL not set, skipping run-ops migrations."
27+
echo "RUN_OPS_DATABASE_URL not set, skipping run-ops migrations."
2828
fi
2929

3030
if [ "$SKIP_DASHBOARD_AGENT_MIGRATIONS" != "1" ]; then

internal-packages/run-ops-database/scripts/migrate.mjs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Run Prisma migrations against the dedicated NEW run-ops database (the second physical DB in the
22
// split). It owns its own migration history, so it is migrated independently of the control-plane
3-
// DB. The connection is resolved the same way the webapp resolves it (RUN_OPS_DATABASE_URL, falling
4-
// back to TASK_RUN_DATABASE_URL) so migrations always target the DB the app connects to.
3+
// DB. Connects via RUN_OPS_DATABASE_URL — the same var the webapp uses — so migrations always
4+
// target the DB the app connects to.
55
//
66
// Usage: node scripts/migrate.mjs [deploy|status] (defaults to deploy)
77
import { spawnSync } from "node:child_process";
@@ -45,13 +45,13 @@ const redact = (url) => url.replace(/:\/\/[^@]*@/, "://***@");
4545

4646
const subcommand = process.argv[2] === "status" ? "status" : "deploy";
4747

48-
const databaseUrl = resolveVar("RUN_OPS_DATABASE_URL") || resolveVar("TASK_RUN_DATABASE_URL");
48+
const databaseUrl = resolveVar("RUN_OPS_DATABASE_URL");
4949

5050
if (!databaseUrl) {
51-
// Single-DB installs never set these — safe no-op. A genuinely-expected DB is gated on by the caller.
51+
// Single-DB installs never set it — safe no-op. A genuinely-expected DB is gated on by the caller.
5252
console.log(
53-
`run-ops migrate ${subcommand}: neither RUN_OPS_DATABASE_URL nor TASK_RUN_DATABASE_URL is set ` +
54-
"(checked env and .env). No dedicated run-ops database configured — skipping."
53+
`run-ops migrate ${subcommand}: RUN_OPS_DATABASE_URL is not set (checked env and .env). ` +
54+
"No dedicated run-ops database configured — skipping."
5555
);
5656
process.exit(0);
5757
}

0 commit comments

Comments
 (0)