feat(webapp): resolve which shard an environment mints run roots into - #4755
feat(webapp): resolve which shard an environment mints run roots into#4755d-cs wants to merge 1 commit into
Conversation
Adds the third stage of the run-id mint gate chain. `resolveMintShard(env)` returns the shard key an environment mints new roots into: the active shard list, then a per-env or per-org pin, then a rendezvous hash of the environment id. With `RUN_OPS_MINT_SHARDS` unset or empty it returns "new", which is today's behaviour, so this merges inert. `computeRunIdMintKind` and `mintFlipGrace.ts` are untouched. The grace pattern is cloned into `mintShardGrace.ts` rather than widened, so the existing cuid/runOpsId flip grace keeps its behaviour. Design notes: - Pure core plus env-bound wrapper, mirroring `runOpsMintKind.server.ts`. Determinism is a property of `computeMintShard` for fixed deps; the wrapper supplies the clock, exactly as `effectiveMintKind` takes `nowMs`. - Zero new queries on the trigger hot path. Both pins live in the org override blob that `mintRunFriendlyId` already holds. - HRW scores `sha256(envId \0 key)` at 64 bits, over a sorted key list, with a lexicographic tie-break. A 32-bit score collides at our environment count, and without the sort two deployments listing the same keys in a different CSV order would place environments differently. - `parseShardCsv` rejects anything outside [a-z0-9] and rejects the reserved keys at boot. `generateRunOpsIdV2` throws on an out-of-alphabet char, so an unvalidated key would become a throw on the mint path. - A pin outside the active set falls through to the hash and reports once per environment per process. Honouring it would leak the drain the active list performs; throwing would fail customer triggers whenever a pinned shard drains. The loud-on-unknown-key rule governs reading an id, not writing one. - "new" is a legal pin value, holding one org or environment on gen-1 while the rest of the fleet mints gen-2. Without it, a non-empty active set moves every environment at once. - The active-set grace is stamped by `RUN_OPS_MINT_SHARDS_PREV` and `RUN_OPS_MINT_SHARDS_FLIPPED_AT`. A prev list with no timestamp is dropped; a timestamp with an empty prev list graces a first activation. No changeset and no `.server-changes` note: nothing user-visible, and no caller carries the returned key into an id yet.
|
WalkthroughAdds gen-2 mint shard configuration with boot-time validation and coordinated cutover metadata. Adds feature flags for organization and environment shard pins. Implements shard-set parsing, grace-period selection, deterministic rendezvous-hash assignment, generation-1 fallback, pin precedence, and rejected-pin logging. Adds tests for validation, cutover behavior, pin handling, assignment determinism, distribution, and shard-set changes. Merge Risk: 🟡 Moderate · up to The PR keeps default user behavior unchanged when shard configuration is unset, but its tests currently depend on unrelated production secrets and the shard cutover shares a grace setting with feature-flag flips, which can create inconsistent placement during deployment. Merge should wait for test isolation and explicit owner acceptance of the cutover-window design. 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
apps/webapp/app/v3/featureFlags.ts (1)
95-120: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winReuse
isValidPinValueinstead of duplicating the pin contract.
mintShardGrace.tsalready exportsSHARD_KEY_PATTERN,GEN_1_PIN_VALUE, andisValidPinValue. This file now re-implements that predicate twice: once at Line 100 and once at Line 116. The alphabet regex/^[a-z0-9]$/and the"new"literal exist in three places across the two files.The write-side validator and the read-side resolver must agree. If the alphabet or the gen-1 sentinel changes in
mintShardGrace.ts, these copies keep accepting a pin thatreadPininrunOpsMintShard.server.tsthen discards, which silently un-pins an environment.
mintShardGrace.tsimports only a type from@trigger.dev/core, so importing it here adds no runtime cycle.♻️ Proposed refactor to share one predicate
Add the import at the top of the file:
import { z } from "zod"; +import { isValidPinValue } from "./runOpsMigration/mintShardGrace";Then reuse it in both schemas:
- [FEATURE_FLAG.runOpsMintShard]: z - .string() - .refine((v) => v === "new" || /^[a-z0-9]$/.test(v), 'must be a single [a-z0-9] char, or "new"'), + [FEATURE_FLAG.runOpsMintShard]: z + .string() + .refine(isValidPinValue, 'must be a single [a-z0-9] char, or "new"'), // Per-environment pins as JSON: {"<environmentId>": "<shard key>"}. A JSON string because // this catalog is scalar-only. Rejected at write, so a typo cannot silently un-pin an env. [FEATURE_FLAG.runOpsMintShardEnvPins]: z.string().superRefine((raw, ctx) => { const fail = (message: string) => ctx.addIssue({ code: z.ZodIssueCode.custom, message }); let parsed: unknown; try { parsed = JSON.parse(raw); } catch { return fail("must be valid JSON"); } if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { return fail("must be a JSON object mapping environment id to shard key"); } for (const [environmentId, value] of Object.entries(parsed)) { - if (typeof value !== "string" || !(value === "new" || /^[a-z0-9]$/.test(value))) { + if (!isValidPinValue(value)) { fail(`"${environmentId}" must map to a single [a-z0-9] char, or "new"`); } } }),apps/webapp/app/v3/runOpsMigration/mintShardGrace.test.ts (1)
119-148: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd coverage for
mintShardStampWarning.
mintShardGrace.tsexportsmintShardStampWarning, and this file tests every other export. The warning is the only operator signal for a half-configured cutover, whereRUN_OPS_MINT_SHARDS_PREVis set butRUN_OPS_MINT_SHARDS_FLIPPED_ATis not. It has three branches and none are covered.💚 Proposed tests
+describe("mintShardStampWarning", () => { + it("stays quiet while the active set is empty", () => { + expect( + mintShardStampWarning({ shards: "", prev: "a", flippedAt: undefined }) + ).toBeUndefined(); + }); + + it("warns when prev is set but the flip timestamp is not", () => { + expect(mintShardStampWarning({ shards: "a", prev: "b", flippedAt: undefined })).toMatch( + /FLIPPED_AT/ + ); + }); + + it("stays quiet when both halves of the stamp are set", () => { + expect( + mintShardStampWarning({ shards: "a", prev: "b", flippedAt: new Date(T).toISOString() }) + ).toBeUndefined(); + }); + + it("stays quiet when prev is empty", () => { + expect(mintShardStampWarning({ shards: "a", prev: "", flippedAt: undefined })).toBeUndefined(); + }); +});Add
mintShardStampWarningto the import list at Line 3.apps/webapp/app/v3/runOpsMigration/runOpsMintShard.server.ts (1)
162-168: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winGive the shard-set cutover its own grace env var.
Line 165 reuses
RUN_OPS_MINT_FLIP_GRACE_MSasgraceMs.env.server.tsdocuments that variable as the grace for arunOpsMintKindflip, and requires it to exceedRUN_OPS_MINT_FLAG_CACHE_TTL_MSplus the control-plane cache TTL. The two windows cover different things:
RUN_OPS_MINT_FLIP_GRACE_MSabsorbs feature-flag cache staleness across processes.- The shard-set window absorbs deploy skew, because
RUN_OPS_MINT_SHARDSis a deploy-time value and a rolling deploy runs old and new CSVs at the same time.The sizing inputs differ, so one knob cannot serve both. An operator who retunes the flag-cache grace also retunes the shard cutover window without knowing it. If that window becomes shorter than the rolling-deploy duration, pods mint into different shard sets at the same instant, which is the exact condition
_PREVand_FLIPPED_ATexist to prevent.The new
RUN_OPS_MINT_SHARDS_*block already owns_PREVand_FLIPPED_AT. Add the grace there while no caller consumes the returned shard key yet.♻️ Proposed change
In
apps/webapp/app/env.server.ts, next to the other shard variables:RUN_OPS_MINT_SHARDS: shardCsvString(), RUN_OPS_MINT_SHARDS_PREV: shardCsvString(), RUN_OPS_MINT_SHARDS_FLIPPED_AT: z.string().datetime().optional(), + // Cutover window for a RUN_OPS_MINT_SHARDS set change. Must exceed the rolling-deploy + // duration so every process crosses the boundary together. Sized independently of + // RUN_OPS_MINT_FLIP_GRACE_MS, which absorbs feature-flag cache staleness instead. + RUN_OPS_MINT_SHARDS_GRACE_MS: z.coerce.number().int().default(90_000),Then in this file:
return computeMintShard(environment, { resolution: shardResolution, nowMs: Date.now(), - graceMs: env.RUN_OPS_MINT_FLIP_GRACE_MS, + graceMs: env.RUN_OPS_MINT_SHARDS_GRACE_MS, orgFeatureFlags: environment.orgFeatureFlags, onPinRejected: reportPinRejected, });
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 858e4dcd-c0d9-438b-a74a-848f9e0b870f
📒 Files selected for processing (6)
apps/webapp/app/env.server.tsapps/webapp/app/v3/featureFlags.tsapps/webapp/app/v3/runOpsMigration/mintShardGrace.test.tsapps/webapp/app/v3/runOpsMigration/mintShardGrace.tsapps/webapp/app/v3/runOpsMigration/runOpsMintShard.server.test.tsapps/webapp/app/v3/runOpsMigration/runOpsMintShard.server.ts
Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.
📜 Review details
⏰ Context from checks skipped due to timeout. (32)
- GitHub Check: webapp / 🧪 Unit Tests: Webapp (22, 24)
- GitHub Check: webapp / 🧪 Unit Tests: Webapp (23, 24)
- GitHub Check: webapp / 🧪 Unit Tests: Webapp (24, 24)
- GitHub Check: webapp / 🧪 Unit Tests: Webapp (16, 24)
- GitHub Check: webapp / 🧪 Unit Tests: Webapp (20, 24)
- GitHub Check: webapp / 🧪 Unit Tests: Webapp (1, 24)
- GitHub Check: webapp / 🧪 Unit Tests: Webapp (15, 24)
- GitHub Check: webapp / 🧪 Unit Tests: Webapp (19, 24)
- GitHub Check: webapp / 🧪 Unit Tests: Webapp (18, 24)
- GitHub Check: webapp / 🧪 Unit Tests: Webapp (12, 24)
- GitHub Check: webapp / 🧪 Unit Tests: Webapp (17, 24)
- GitHub Check: webapp / 🧪 Unit Tests: Webapp (13, 24)
- GitHub Check: webapp / 🧪 Unit Tests: Webapp (8, 24)
- GitHub Check: webapp / 🧪 Unit Tests: Webapp (7, 24)
- GitHub Check: webapp / 🧪 Unit Tests: Webapp (21, 24)
- GitHub Check: webapp / 🧪 Unit Tests: Webapp (10, 24)
- GitHub Check: webapp / 🧪 Unit Tests: Webapp (2, 24)
- GitHub Check: webapp / 🧪 Unit Tests: Webapp (9, 24)
- GitHub Check: webapp / 🧪 Unit Tests: Webapp (14, 24)
- GitHub Check: webapp / 🧪 Unit Tests: Webapp (6, 24)
- GitHub Check: webapp / 🧪 Unit Tests: Webapp (3, 24)
- GitHub Check: webapp / 🧪 Unit Tests: Webapp (4, 24)
- GitHub Check: webapp / 🧪 Unit Tests: Webapp (11, 24)
- GitHub Check: webapp / 🧪 Unit Tests: Webapp (5, 24)
- GitHub Check: typecheck / typecheck
- GitHub Check: obsmap / 🧪 Unit Tests: Observability Map
- GitHub Check: fk-cascade-guard / fk-cascade-guard
- GitHub Check: e2e-webapp / 🧪 E2E Tests: Webapp (2, 2)
- GitHub Check: runops-guard / runops-guard
- GitHub Check: e2e-webapp / 🧪 E2E Tests: Webapp (1, 2)
- GitHub Check: code-quality / code-quality
- GitHub Check: Analyze (javascript-typescript)
🧰 Additional context used
📓 Path-based instructions (12)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.{ts,tsx}: Use types over interfaces for TypeScript
Avoid using enums; prefer string unions or const objects instead
**/*.{ts,tsx}: Prefer static imports over dynamic imports. Only use dynamicimport()when:
- Circular dependencies cannot be resolved otherwise
- Code splitting is genuinely needed for performance
- The module must be loaded conditionally at runtime
Files:
apps/webapp/app/v3/runOpsMigration/mintShardGrace.test.tsapps/webapp/app/v3/featureFlags.tsapps/webapp/app/v3/runOpsMigration/runOpsMintShard.server.test.tsapps/webapp/app/v3/runOpsMigration/runOpsMintShard.server.tsapps/webapp/app/env.server.tsapps/webapp/app/v3/runOpsMigration/mintShardGrace.ts
{packages/core,apps/webapp}/**/*.{ts,tsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Use zod for validation in packages/core and apps/webapp
Files:
apps/webapp/app/v3/runOpsMigration/mintShardGrace.test.tsapps/webapp/app/v3/featureFlags.tsapps/webapp/app/v3/runOpsMigration/runOpsMintShard.server.test.tsapps/webapp/app/v3/runOpsMigration/runOpsMintShard.server.tsapps/webapp/app/env.server.tsapps/webapp/app/v3/runOpsMigration/mintShardGrace.ts
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Use function declarations instead of default exports
Files:
apps/webapp/app/v3/runOpsMigration/mintShardGrace.test.tsapps/webapp/app/v3/featureFlags.tsapps/webapp/app/v3/runOpsMigration/runOpsMintShard.server.test.tsapps/webapp/app/v3/runOpsMigration/runOpsMintShard.server.tsapps/webapp/app/env.server.tsapps/webapp/app/v3/runOpsMigration/mintShardGrace.ts
**/*.{test,spec}.{ts,tsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Use vitest for all tests in the Trigger.dev repository
**/*.{test,spec}.{ts,tsx}: We use vitest exclusively. Never mock anything - use testcontainers instead.
Test files go next to source files (e.g.,MyService.ts->MyService.test.ts).
Files:
apps/webapp/app/v3/runOpsMigration/mintShardGrace.test.tsapps/webapp/app/v3/runOpsMigration/runOpsMintShard.server.test.ts
**/*.ts
📄 CodeRabbit inference engine (.cursor/rules/otel-metrics.mdc)
**/*.ts: When creating or editing OTEL metrics (counters, histograms, gauges), ensure metric attributes have low cardinality by using only enums, booleans, bounded error codes, or bounded shard IDs
Do not use high-cardinality attributes in OTEL metrics such as UUIDs/IDs (envId, userId, runId, projectId, organizationId), unbounded integers (itemCount, batchSize, retryCount), timestamps (createdAt, startTime), or free-form strings (errorMessage, taskName, queueName)
When exporting OTEL metrics via OTLP to Prometheus, be aware that the exporter automatically adds unit suffixes to metric names (e.g., 'my_duration_ms' becomes 'my_duration_ms_milliseconds', 'my_counter' becomes 'my_counter_total'). Account for these transformations when writing Grafana dashboards or Prometheus queries
Files:
apps/webapp/app/v3/runOpsMigration/mintShardGrace.test.tsapps/webapp/app/v3/featureFlags.tsapps/webapp/app/v3/runOpsMigration/runOpsMintShard.server.test.tsapps/webapp/app/v3/runOpsMigration/runOpsMintShard.server.tsapps/webapp/app/env.server.tsapps/webapp/app/v3/runOpsMigration/mintShardGrace.ts
apps/webapp/**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)
apps/webapp/**/*.{ts,tsx}: Access environment variables through theenvexport ofenv.server.tsinstead of directly accessingprocess.env
Use subpath exports from@trigger.dev/corepackage instead of importing from the root@trigger.dev/corepath
Files:
apps/webapp/app/v3/runOpsMigration/mintShardGrace.test.tsapps/webapp/app/v3/featureFlags.tsapps/webapp/app/v3/runOpsMigration/runOpsMintShard.server.test.tsapps/webapp/app/v3/runOpsMigration/runOpsMintShard.server.tsapps/webapp/app/env.server.tsapps/webapp/app/v3/runOpsMigration/mintShardGrace.ts
apps/webapp/**/*.test.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)
Do not import
env.server.tsdirectly or indirectly into test files; instead pass environment-dependent values through options/parameters to make code testable
Files:
apps/webapp/app/v3/runOpsMigration/mintShardGrace.test.tsapps/webapp/app/v3/runOpsMigration/runOpsMintShard.server.test.ts
apps/webapp/app/**/*.{ts,tsx}
📄 CodeRabbit inference engine (apps/webapp/CLAUDE.md)
apps/webapp/app/**/*.{ts,tsx}: For dashboard changes, visually verify the running Remix app with Chrome DevTools MCP, using snapshots, screenshots, interaction, and console-message checks as appropriate.
UseuseCallbackanduseMemoonly for context provider values, expensive derived data used as a dependency, or stable references required by dependency arrays; do not wrap ordinary event handlers or trivial computations.
Use named constants for sentinel or placeholder values instead of scattering raw string literals across comparisons.
Files:
apps/webapp/app/v3/runOpsMigration/mintShardGrace.test.tsapps/webapp/app/v3/featureFlags.tsapps/webapp/app/v3/runOpsMigration/runOpsMintShard.server.test.tsapps/webapp/app/v3/runOpsMigration/runOpsMintShard.server.tsapps/webapp/app/env.server.tsapps/webapp/app/v3/runOpsMigration/mintShardGrace.ts
apps/webapp/app/**/*.ts
📄 CodeRabbit inference engine (apps/webapp/CLAUDE.md)
apps/webapp/app/**/*.ts: Never userequest.signalto detect client disconnects. UsegetRequestAbortSignal()fromapp/services/httpAsyncStorage.server.ts, which is wired to Express response close events.
Access environment variables through theenvexport fromapp/env.server.ts; never useprocess.envdirectly.
Always use PrismafindFirstinstead offindUnique.
Always use the$transactionhelper from~/db.server, never callprisma.$transactionor$replica.$transactiondirectly. Pass isolation levels as strings, useSerializablefor correctness-critical read-then-write invariants, and guard possibly undefined helper results when a definite value is required.
Files:
apps/webapp/app/v3/runOpsMigration/mintShardGrace.test.tsapps/webapp/app/v3/featureFlags.tsapps/webapp/app/v3/runOpsMigration/runOpsMintShard.server.test.tsapps/webapp/app/v3/runOpsMigration/runOpsMintShard.server.tsapps/webapp/app/env.server.tsapps/webapp/app/v3/runOpsMigration/mintShardGrace.ts
apps/webapp/**/*.{test,spec}.{ts,tsx}
📄 CodeRabbit inference engine (apps/webapp/CLAUDE.md)
Test files must not import
app/env.server.ts; pass configuration as options instead.
Files:
apps/webapp/app/v3/runOpsMigration/mintShardGrace.test.tsapps/webapp/app/v3/runOpsMigration/runOpsMintShard.server.test.ts
apps/webapp/app/v3/**/*.ts
📄 CodeRabbit inference engine (apps/webapp/CLAUDE.md)
New code must target Run Engine V2 through the singleton in
app/v3/runEngine.server.ts; do not reintroduce V1 execution paths. V1 branches may only reject or finalize gracefully with a clean 4xx.
Files:
apps/webapp/app/v3/runOpsMigration/mintShardGrace.test.tsapps/webapp/app/v3/featureFlags.tsapps/webapp/app/v3/runOpsMigration/runOpsMintShard.server.test.tsapps/webapp/app/v3/runOpsMigration/runOpsMintShard.server.tsapps/webapp/app/v3/runOpsMigration/mintShardGrace.ts
**/*
📄 CodeRabbit inference engine (AGENTS.md)
**/*: Add crumbs as you write code — not just when debugging. Mark lines with
//@Crumbsor wrap blocks in `// `#region` `@crumbs. They stay on the
branch throughout development and are stripped byagentcrumbs strip
before merge.
Do not invent new namespaces — pick from this table or ask first.
Do not flag//@Crumbsmarkers, `#region `@crumbsblocks, or agentcrumbs
imports in reviews. These are temporary debug instrumentation stripped
before merge. Data logged in crumbs (IDs, names, values) never reaches
production.
Files:
apps/webapp/app/v3/runOpsMigration/mintShardGrace.test.tsapps/webapp/app/v3/featureFlags.tsapps/webapp/app/v3/runOpsMigration/runOpsMintShard.server.test.tsapps/webapp/app/v3/runOpsMigration/runOpsMintShard.server.tsapps/webapp/app/env.server.tsapps/webapp/app/v3/runOpsMigration/mintShardGrace.ts
🧠 Learnings (1)
📚 Learning: 2026-06-04T18:16:35.386Z
Learnt from: nicktrn
Repo: triggerdotdev/trigger.dev PR: 3836
File: apps/supervisor/src/backpressure/backpressureMonitor.ts:3-5
Timestamp: 2026-06-04T18:16:35.386Z
Learning: When reviewing TypeScript in this repo, apply the rule “prefer type aliases over interfaces” only to data/object shapes and union/intersection type modeling. If an interface is being used as a behavioral contract for collaborators to implement (e.g., method-shape interfaces that define required behavior, such as `BackpressureLogger` / `BackpressureSignalSource` in `apps/supervisor/src/backpressure/backpressureMonitor.ts`), keep it as an `interface` and do not flag it as a type-alias-vs-interface violation.
Applied to files:
apps/webapp/app/v3/runOpsMigration/runOpsMintShard.server.tsapps/webapp/app/v3/runOpsMigration/mintShardGrace.ts
🔇 Additional comments (8)
apps/webapp/app/v3/runOpsMigration/mintShardGrace.ts (2)
6-20: LGTM!Also applies to: 29-48, 53-62, 66-79, 83-95
22-25: 🗄️ Data Integrity & IntegrationNo
ShardKeytype mismatch
ShardKeyis"legacy" | "new" | string, so it admits"new"and all single[a-z0-9]characters. The type predicate is sound for these values.> Likely an incorrect or invalid review comment.apps/webapp/app/env.server.ts (1)
7-7: LGTM!Also applies to: 45-60, 2019-2026
apps/webapp/app/v3/featureFlags.ts (2)
29-31: LGTM!Also applies to: 133-141
98-120: 🩺 Stability & AvailabilityNo resolver change is needed.
ZodEffectsuses the resolver’s{ type: "string" }fallback, so both admin UIs renderStringControl. The global page intentionally renders these flags as locked throughGLOBAL_LOCKED_FLAGS.> Likely an incorrect or invalid review comment.apps/webapp/app/v3/runOpsMigration/mintShardGrace.test.ts (1)
1-118: LGTM!apps/webapp/app/v3/runOpsMigration/runOpsMintShard.server.ts (1)
1-13: LGTM!Also applies to: 15-26, 31-44, 48-57, 63-65, 67-81, 90-112, 116-132, 136-146, 148-161
apps/webapp/app/v3/runOpsMigration/runOpsMintShard.server.test.ts (1)
5-41: LGTM!Also applies to: 43-58, 60-80, 82-185, 187-248
| import { describe, expect, it } from "vitest"; | ||
| import { computeMintShard, type MintShardDeps } from "./runOpsMintShard.server"; | ||
| import { type MintShardSetResolution } from "./mintShardGrace"; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
This test imports env.server.ts indirectly. Move the pure core into its own module.
Line 2 imports ./runOpsMintShard.server. That module's Line 3 is import { env } from "~/env.server". Loading this test therefore evaluates env.server.ts, which runs EnvironmentSchema.parse(process.env) at its Line 2500 and requires DATABASE_URL, DIRECT_URL, SESSION_SECRET, MAGIC_LINK_SECRET, a 32-byte ENCRYPTION_KEY, MANAGED_WORKER_SECRET, DEPLOY_REGISTRY_HOST, and CLICKHOUSE_URL. It also executes the module-level side effects at runOpsMintShard.server.ts Lines 116-132, including a logger.warn call.
The test then either fails to load without a complete environment, or passes only because of ambient environment values.
runOpsMintShard.server.ts Line 83 already documents computeMintShard as "PURE CORE — no env, no clock, no I/O; tests drive this directly". Extract that pure core into a module that does not import env.server, and keep only the env-bound wrapper in runOpsMintShard.server.ts.
As per path instructions: "Do not import env.server.ts directly or indirectly into test files; instead pass environment-dependent values through options/parameters" and "Test files must not import app/env.server.ts; pass configuration as options instead."
♻️ Proposed split
Move MintShardDeps, asRecord, readEnvPin, readPin, shardScore, hrwSelect, and computeMintShard into a new mintShardAssignment.ts that imports only node:crypto, the ShardKey type, ~/v3/featureFlags, and ./mintShardGrace.
Then in runOpsMintShard.server.ts:
-import { createHash } from "node:crypto";
import type { ShardKey } from "`@trigger.dev/core/v3/isomorphic`";
import { env } from "~/env.server";
import { logger } from "~/services/logger.server";
-import { FEATURE_FLAG } from "~/v3/featureFlags";
import {
buildMintShardResolution,
- effectiveMintShardSet,
- GEN_1_PIN_VALUE,
- isValidPinValue,
mintShardStampWarning,
type MintShardSetResolution,
} from "./mintShardGrace";
+import { computeMintShard, type MintShardDeps } from "./mintShardAssignment";And in this test file:
-import { computeMintShard, type MintShardDeps } from "./runOpsMintShard.server";
+import { computeMintShard, type MintShardDeps } from "./mintShardAssignment";Rename this file to mintShardAssignment.test.ts so it sits next to its source.
Source: Path instructions
Summary
Adds the shard-selection stage of run-id minting.
resolveMintShard(env)returns which run-ops database an environment mints its new run roots into. Resolution order is the active shard list, then a per-environment or per-organization pin, then a rendezvous hash of the environment id.Nothing changes for users on this merge.
RUN_OPS_MINT_SHARDSis unset by default, so every environment resolves to the existing store and minting behaves exactly as it does today. No caller carries the returned key into an id yet.Design
The existing gate that chooses between a cuid id and a run-ops id is untouched. The new stage runs after it, and the grace-window pattern is cloned into a separate module rather than widened, so the current flip behaviour keeps its semantics.
Placement uses rendezvous hashing, so adding a shard moves only about 1/(N+1) of environments and removing one moves only its own. Two details are load-bearing:
sha256(envId \0 key). A 32-bit score collides at our environment count, and an undetected tie would resolve by iteration order.Keys are validated at boot. A key outside
[a-z0-9]cannot be stamped into an id, so it fails fast rather than throwing later on the mint path.newis accepted as a pin value, which holds one organization or environment on the current id format while the rest of the fleet moves.A pin naming a shard that has left the active list falls through to the hash and reports once per environment. Honouring it would leak the drain the active list exists to perform, and throwing would fail triggers whenever a pinned shard drains.
Resolution adds no database queries to the trigger path: both pin levels live in the organization flag blob the mint call site already holds.
Notes for review
Determinism is a property of the pure core for fixed inputs. The wrapper supplies the clock, the same split
effectiveMintKindalready uses.The active-set grace is stamped by
RUN_OPS_MINT_SHARDS_PREVandRUN_OPS_MINT_SHARDS_FLIPPED_AT, both supplied by the operator alongside a change toRUN_OPS_MINT_SHARDS. Changing the list without them is un-graced and silent, which is an operational rule rather than something the code can enforce.Two new feature-flag keys appear in the admin flag pages immediately. They are read only from the organization override blob, so they are locked on the global page.
The run-ops migration suites that need containers were not run locally.