Skip to content

Commit 0f09b92

Browse files
committed
feat(webapp): let the deployment S2 endpoint be overridden
The S2 SDK honours only endpoints passed to its constructor. S2Environment.parse(), which reads the endpoint variables, is an opt-in helper the webapp never called, so deployment event logs always went to hosted S2 and could not be pointed at the local s2 container in docker compose. Realtime streams already had this knob. S2_DEPLOYMENT_ENDPOINT is a single value covering both the account and basin hosts. Two separate variables would let a half-set config send the access token to the hosted service while the operator believed the client was entirely local. With the variable unset the options object carries no endpoints key at all, so the call into the SDK is the one production already makes. An endpoints key with undefined members resolves to the same hosted URLs, so no assertion on the built client would catch a regression there; the options builder is exported and asserted directly instead.
1 parent fe94700 commit 0f09b92

5 files changed

Lines changed: 109 additions & 4 deletions

File tree

apps/webapp/app/env.server.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,15 @@ const S2EnvSchema = z.preprocess(
7979
S2_ACCESS_TOKEN: z.string(),
8080
S2_DEPLOYMENT_LOGS_BASIN_NAME: z.string(),
8181
S2_DEPLOYMENT_STREAMS_LOCAL: z.string().default("0"),
82+
// Points deployment event logs at an S2 service other than the hosted one, e.g. the
83+
// local s2-lite in docker compose. One value covers both the account and basin
84+
// endpoints: splitting them lets a half-set config send the access token to the
85+
// hosted service while the operator believes they are entirely local.
86+
S2_DEPLOYMENT_ENDPOINT: z
87+
.string()
88+
.url()
89+
.optional()
90+
.or(z.literal("").transform(() => undefined)),
8291
}),
8392
z.object({
8493
S2_ENABLED: z.literal("0"),

apps/webapp/app/presenters/v3/DeploymentPresenter.server.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { type User } from "~/models/user.server";
1313
import { getUsername } from "~/utils/username";
1414
import { processGitMetadata } from "./BranchesPresenter.server";
1515
import { VercelProjectIntegrationDataSchema } from "~/v3/vercel/vercelProjectIntegrationSchema";
16-
import { S2 } from "@s2-dev/streamstore";
16+
import { createDeploymentS2Client } from "~/v3/s2Client.server";
1717
import { env } from "~/env.server";
1818
import { createRedisClient } from "~/redis.server";
1919
import { tryCatch } from "@trigger.dev/core";
@@ -30,7 +30,7 @@ const s2TokenRedis = createRedisClient("s2-token-cache", {
3030
clusterMode: env.CACHE_REDIS_CLUSTER_MODE_ENABLED === "1",
3131
});
3232

33-
const s2 = env.S2_ENABLED === "1" ? new S2({ accessToken: env.S2_ACCESS_TOKEN }) : undefined;
33+
const s2 = createDeploymentS2Client();
3434

3535
export type ErrorData = {
3636
name: string;
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { S2 } from "@s2-dev/streamstore";
2+
import { env } from "~/env.server";
3+
4+
export type DeploymentS2Config = {
5+
accessToken: string;
6+
endpoint?: string;
7+
};
8+
9+
type DeploymentS2ClientOptions = {
10+
accessToken: string;
11+
endpoints?: { account: string; basin: string };
12+
};
13+
14+
// Exported so a test can pin the shape handed to the SDK: with no endpoint the options must carry
15+
// no `endpoints` key, matching the call production already makes.
16+
export function deploymentS2ClientOptions({
17+
accessToken,
18+
endpoint,
19+
}: DeploymentS2Config): DeploymentS2ClientOptions {
20+
if (endpoint === undefined) {
21+
return { accessToken };
22+
}
23+
24+
// One value drives both hosts. Overriding just one would send the access token to the hosted
25+
// service while the other half went elsewhere.
26+
return { accessToken, endpoints: { account: endpoint, basin: endpoint } };
27+
}
28+
29+
export function buildDeploymentS2Client(config: DeploymentS2Config): S2 {
30+
return new S2(deploymentS2ClientOptions(config));
31+
}
32+
33+
export function createDeploymentS2Client(): S2 | undefined {
34+
if (env.S2_ENABLED !== "1") {
35+
return undefined;
36+
}
37+
38+
return buildDeploymentS2Client({
39+
accessToken: env.S2_ACCESS_TOKEN,
40+
endpoint: env.S2_DEPLOYMENT_ENDPOINT,
41+
});
42+
}

apps/webapp/app/v3/services/deployment.server.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ import {
2323
import { FEATURE_FLAG, type FeatureFlagKey } from "../featureFlags";
2424
import { flags } from "../featureFlags.server";
2525
import { globalFlagsRegistry } from "../globalFlagsRegistry.server";
26-
import { AppendInput, AppendRecord, S2 } from "@s2-dev/streamstore";
26+
import { AppendInput, AppendRecord } from "@s2-dev/streamstore";
27+
import { createDeploymentS2Client } from "~/v3/s2Client.server";
2728
import { createRedisClient } from "~/redis.server";
2829

2930
const S2_TOKEN_KEY_PREFIX = "s2-token:read:deployment-event-stream:project:";
@@ -35,7 +36,7 @@ const s2TokenRedis = createRedisClient("s2-token-cache", {
3536
tlsDisabled: env.CACHE_REDIS_TLS_DISABLED === "true",
3637
clusterMode: env.CACHE_REDIS_CLUSTER_MODE_ENABLED === "1",
3738
});
38-
const s2 = env.S2_ENABLED === "1" ? new S2({ accessToken: env.S2_ACCESS_TOKEN }) : undefined;
39+
const s2 = createDeploymentS2Client();
3940

4041
const DEPLOY_BUILD_PATH_ENV_FLAG: Partial<Record<RuntimeEnvironmentType, FeatureFlagKey>> = {
4142
PREVIEW: FEATURE_FLAG.deployBuildPathPreview,
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { describe, expect, it } from "vitest";
2+
import { buildDeploymentS2Client, deploymentS2ClientOptions } from "~/v3/s2Client.server";
3+
4+
const BASIN = "trigger-local";
5+
6+
describe("buildDeploymentS2Client", () => {
7+
// The SDK resolves an endpoints key with undefined members to the same hosted URLs, so nothing
8+
// on the built client distinguishes the two calls. Assert on the options instead.
9+
it("hands the SDK no endpoints key at all when no endpoint is configured", () => {
10+
expect(deploymentS2ClientOptions({ accessToken: "token" })).toEqual({ accessToken: "token" });
11+
expect(deploymentS2ClientOptions({ accessToken: "token" })).not.toHaveProperty("endpoints");
12+
});
13+
14+
it("hands the SDK one endpoint for both hosts when configured", () => {
15+
expect(
16+
deploymentS2ClientOptions({ accessToken: "token", endpoint: "http://localhost:4566" })
17+
).toEqual({
18+
accessToken: "token",
19+
endpoints: { account: "http://localhost:4566", basin: "http://localhost:4566" },
20+
});
21+
});
22+
23+
it("still resolves the SDK's hosted defaults when no endpoint is configured", () => {
24+
const client = buildDeploymentS2Client({ accessToken: "token" });
25+
26+
expect(client.endpoints.accountBaseUrl()).toBe("https://a.s2.dev/v1");
27+
expect(client.endpoints.basinBaseUrl(BASIN)).toBe(`https://${BASIN}.b.s2.dev/v1`);
28+
expect(client.endpoints.includeBasinHeader).toBe(false);
29+
});
30+
31+
it("points both the account and basin hosts at a configured endpoint", () => {
32+
const client = buildDeploymentS2Client({
33+
accessToken: "token",
34+
endpoint: "http://localhost:4566",
35+
});
36+
37+
expect(client.endpoints.accountBaseUrl()).toBe("http://localhost:4566/v1");
38+
expect(client.endpoints.basinBaseUrl(BASIN)).toBe("http://localhost:4566/v1");
39+
expect(client.endpoints.includeBasinHeader).toBe(true);
40+
});
41+
42+
// A split configuration would send the access token to the hosted service while the operator
43+
// believed the client was entirely local, so one value has to drive both hosts.
44+
it("never leaves one host hosted while the other is overridden", () => {
45+
const client = buildDeploymentS2Client({
46+
accessToken: "token",
47+
endpoint: "http://localhost:4566",
48+
});
49+
50+
expect(client.endpoints.accountBaseUrl()).not.toContain("s2.dev");
51+
expect(client.endpoints.basinBaseUrl(BASIN)).not.toContain("s2.dev");
52+
});
53+
});

0 commit comments

Comments
 (0)