Skip to content

Commit 3f43ac9

Browse files
refactor(cli): swap CliApiClient for an offline shim instead of forking indexDeployment
Previous offline-mode plumbing carved branching into indexDeployment: discriminated union types, `result.mode === "offline"` checks at every API call site, parallel disk-write paths next to the API-call paths. Replace with a CliApiClient stub that, instead of issuing HTTP requests, writes the same payloads to disk: - getEnvironmentVariables → returns `{ variables: {} }` - createDeploymentBackgroundWorker → writes the body's metadata (plus buildPlatform / targetPlatform) to `index-metadata.json` - failDeployment → writes the error to `index-error.json` bootstrap() picks the shim when TRIGGER_INDEX_OFFLINE=1 and the real CliApiClient otherwise. indexDeployment is unchanged — bit-identical to upstream — because both implementations satisfy the same interface for the three methods it actually uses. Diff vs upstream collapses to just two hunks: a 14-line offline branch at the top of bootstrap() and the ~65-line shim factory at the bottom of the file. The flattened `index-metadata.json` shape matches what register-tasks/register.mjs (in govsignals) already reads, so no downstream consumer change. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 18fb29d commit 3f43ac9

1 file changed

Lines changed: 75 additions & 97 deletions

File tree

packages/cli-v3/src/entryPoints/managed-index-controller.ts

Lines changed: 75 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -12,67 +12,30 @@ import { resolveSourceFiles } from "../utilities/sourceFiles.js";
1212
import { execOptionsForRuntime } from "@trigger.dev/core/v3/build";
1313
import { writeJSONFile } from "../utilities/fileSystem.js";
1414

15-
/**
16-
* The managed index controller runs inside the build container at deploy time
17-
* and indexes the user's tasks. It supports two modes:
18-
*
19-
* 1. **Online mode (default, used by Trigger.dev cloud)**:
20-
* - Fetches environment variables from the API via `CliApiClient`.
21-
* - Registers the resulting BackgroundWorker via the API.
22-
* - Reports indexing failures via `failDeployment`.
23-
*
24-
* 2. **Offline mode (opt-in via `TRIGGER_INDEX_OFFLINE=1` build arg)**:
25-
* - Skips the API entirely; no env vars are fetched, no
26-
* BackgroundWorker is registered, no failures are reported.
27-
* - Writes `index-metadata.json` (or `index-error.json` on failure)
28-
* to the working directory inside the build container. The multi-stage
29-
* Containerfile copies them into the final image so downstream tooling
30-
* can read them out of the runtime image (and on failure the indexer
31-
* process exits non-zero, failing the build).
32-
* - Intended for self-hosted setups that drive the build via
33-
* `trigger.dev/internal`'s `buildImage({ offlineIndex: true })`
34-
* without API credentials in the build environment.
35-
*
36-
* Mode is selected by the `TRIGGER_INDEX_OFFLINE=1` env var.
37-
*/
38-
3915
async function loadBuildManifest() {
4016
const manifestContents = await readFile("./build.json", "utf-8");
4117
const raw = JSON.parse(manifestContents);
4218

4319
return BuildManifest.parse(raw);
4420
}
4521

46-
// In offline mode (TRIGGER_INDEX_OFFLINE=1) the bootstrap skips API client
47-
// construction entirely. Downstream code treats `cliApiClient === undefined`
48-
// as the signal that we're running offline.
49-
type BootstrapResult = {
50-
buildManifest: BuildManifest;
51-
cliApiClient?: CliApiClient;
52-
projectRef?: string;
53-
deploymentId?: string;
54-
};
55-
56-
/**
57-
* Returns the same shape as `cliApiClient.getEnvironmentVariables` for the
58-
* offline path. We never have project env vars at index time in offline mode
59-
* (the build container has no API access), so it's just an empty `variables`
60-
* map wrapped in the success envelope so downstream code can branch once on
61-
* `$env.success`.
62-
*/
63-
const offlineEnvShim = () =>
64-
({ success: true as const, data: { variables: {} as Record<string, string> } });
65-
66-
async function bootstrap(): Promise<BootstrapResult> {
22+
async function bootstrap() {
6723
const buildManifest = await loadBuildManifest();
6824

69-
// Offline mode: API access is unavailable. The artifacts produced by
70-
// indexDeployment are baked into the final image by the Containerfile.
25+
// Offline mode (TRIGGER_INDEX_OFFLINE=1): swap in a CliApiClient shim that
26+
// writes the same payloads to disk that the real client would have sent
27+
// over the wire. indexDeployment is unchanged — it just gets a different
28+
// implementation of the same interface.
7129
if (env.TRIGGER_INDEX_OFFLINE === "1") {
72-
return { buildManifest };
30+
return {
31+
buildManifest,
32+
cliApiClient: createOfflineCliApiClient(),
33+
// The shim ignores these but the shape needs to match.
34+
projectRef: env.TRIGGER_PROJECT_REF ?? "offline",
35+
deploymentId: env.TRIGGER_DEPLOYMENT_ID ?? "offline",
36+
};
7337
}
7438

75-
// Online mode (default): use the API for env vars and registration.
7639
if (typeof env.TRIGGER_API_URL !== "string") {
7740
console.error("TRIGGER_API_URL is not set");
7841
process.exit(1);
@@ -102,6 +65,8 @@ async function bootstrap(): Promise<BootstrapResult> {
10265
};
10366
}
10467

68+
type BootstrapResult = Awaited<ReturnType<typeof bootstrap>>;
69+
10570
async function indexDeployment({
10671
cliApiClient,
10772
projectRef,
@@ -112,10 +77,7 @@ async function indexDeployment({
11277
const stderr: string[] = [];
11378

11479
try {
115-
const $env =
116-
cliApiClient && projectRef
117-
? await cliApiClient.getEnvironmentVariables(projectRef)
118-
: offlineEnvShim();
80+
const $env = await cliApiClient.getEnvironmentVariables(projectRef);
11981

12082
if (!$env.success) {
12183
throw new Error(`Failed to fetch environment variables: ${$env.error}`);
@@ -149,41 +111,6 @@ async function indexDeployment({
149111
const buildPlatform = process.env.BUILDPLATFORM;
150112
const targetPlatform = process.env.TARGETPLATFORM;
151113

152-
if (!cliApiClient || !deploymentId) {
153-
// Offline mode: write metadata to disk; the multi-stage Containerfile
154-
// copies it into the final image where downstream tooling reads it.
155-
const indexMetadata = {
156-
contentHash: buildManifest.contentHash,
157-
packageVersion: buildManifest.packageVersion,
158-
cliPackageVersion: buildManifest.cliPackageVersion,
159-
tasks: workerManifest.tasks,
160-
queues: workerManifest.queues,
161-
sourceFiles,
162-
runtime: workerManifest.runtime,
163-
runtimeVersion: workerManifest.runtimeVersion,
164-
buildPlatform,
165-
targetPlatform,
166-
};
167-
168-
console.log("Writing index-metadata.json");
169-
170-
await writeFile(
171-
join(process.cwd(), "index-metadata.json"),
172-
JSON.stringify(indexMetadata, null, 2)
173-
);
174-
175-
console.log(
176-
JSON.stringify({
177-
message: "Indexing completed (offline mode)",
178-
buildPlatform,
179-
targetPlatform,
180-
taskCount: workerManifest.tasks.length,
181-
})
182-
);
183-
return;
184-
}
185-
186-
// Online mode: register the BackgroundWorker via the API.
187114
const backgroundWorkerBody: CreateBackgroundWorkerRequestBody = {
188115
localOnly: true,
189116
metadata: {
@@ -233,20 +160,71 @@ async function indexDeployment({
233160

234161
console.error("Failed to index deployment", serialiedIndexError);
235162

236-
if (cliApiClient && deploymentId) {
237-
await cliApiClient.failDeployment(deploymentId, { error: serialiedIndexError });
238-
} else {
239-
// Offline mode: write error to disk so downstream tooling can surface it.
240-
await writeFile(
241-
join(process.cwd(), "index-error.json"),
242-
JSON.stringify({ error: serialiedIndexError }, null, 2)
243-
);
244-
}
163+
await cliApiClient.failDeployment(deploymentId, { error: serialiedIndexError });
245164

246165
process.exit(1);
247166
}
248167
}
249168

169+
/**
170+
* Stub `CliApiClient` for offline indexing (TRIGGER_INDEX_OFFLINE=1).
171+
*
172+
* indexDeployment makes three calls on the API client:
173+
*
174+
* 1. `getEnvironmentVariables(projectRef)` — returns an empty `variables`
175+
* map. The build container has no API access in offline mode, so we
176+
* can't fetch project env vars; the indexer runs with `{}`.
177+
* 2. `createDeploymentBackgroundWorker(deploymentId, body)` — writes the
178+
* flattened body to `index-metadata.json`. Downstream tooling (e.g.
179+
* a register-only Job in the cluster) re-issues this payload to the
180+
* real API.
181+
* 3. `failDeployment(deploymentId, body)` — writes the error to
182+
* `index-error.json`.
183+
*
184+
* The multi-stage Containerfile copies these files into the final image so
185+
* downstream tooling reads them out of the runtime image.
186+
*
187+
* Cast through `unknown` because `CliApiClient` is a concrete class with
188+
* private fields and methods we don't need to stub. indexDeployment only
189+
* touches the three methods above.
190+
*/
191+
function createOfflineCliApiClient(): CliApiClient {
192+
return {
193+
async getEnvironmentVariables() {
194+
return { success: true as const, data: { variables: {} as Record<string, string> } };
195+
},
196+
async createDeploymentBackgroundWorker(
197+
_deploymentId: string,
198+
body: CreateBackgroundWorkerRequestBody
199+
) {
200+
const indexMetadata = {
201+
...body.metadata,
202+
buildPlatform: body.buildPlatform,
203+
targetPlatform: body.targetPlatform,
204+
};
205+
await writeFile(
206+
join(process.cwd(), "index-metadata.json"),
207+
JSON.stringify(indexMetadata, null, 2)
208+
);
209+
return {
210+
success: true as const,
211+
data: {
212+
id: "offline",
213+
version: "offline",
214+
contentHash: body.metadata.contentHash,
215+
},
216+
};
217+
},
218+
async failDeployment(_deploymentId: string, body: { error: unknown }) {
219+
await writeFile(
220+
join(process.cwd(), "index-error.json"),
221+
JSON.stringify(body, null, 2)
222+
);
223+
return { success: true as const, data: { id: "offline" } };
224+
},
225+
} as unknown as CliApiClient;
226+
}
227+
250228
const results = await bootstrap();
251229

252230
await indexDeployment(results);

0 commit comments

Comments
 (0)