diff --git a/apps/webapp/app/components/RuntimeIcon.tsx b/apps/webapp/app/components/RuntimeIcon.tsx
index f0626e97a38..65d2813c404 100644
--- a/apps/webapp/app/components/RuntimeIcon.tsx
+++ b/apps/webapp/app/components/RuntimeIcon.tsx
@@ -29,15 +29,10 @@ export function RuntimeIcon({
}: RuntimeIconProps) {
const parsedRuntime = parseRuntime(runtime);
- // Default to Node.js if no runtime is specified
- const effectiveRuntime = parsedRuntime || {
- runtime: "node" as const,
- originalRuntime: "node",
- displayName: "Node.js",
- };
-
- const icon = getIcon(effectiveRuntime.runtime, className);
- const formattedText = formatRuntimeWithVersion(effectiveRuntime.originalRuntime, runtimeVersion);
+ const icon = parsedRuntime ? getIcon(parsedRuntime.runtime, className) : –;
+ const formattedText = parsedRuntime
+ ? formatRuntimeWithVersion(parsedRuntime.originalRuntime, runtimeVersion)
+ : "Unknown";
if (withLabel) {
return (
diff --git a/apps/webapp/app/routes/admin.api.v1.runs-replication.create.ts b/apps/webapp/app/routes/admin.api.v1.runs-replication.create.ts
index 483c2d219a1..29b98a6d5ae 100644
--- a/apps/webapp/app/routes/admin.api.v1.runs-replication.create.ts
+++ b/apps/webapp/app/routes/admin.api.v1.runs-replication.create.ts
@@ -77,8 +77,12 @@ export async function action({ request }: ActionFunctionArgs) {
}
function createRunReplicationService(params: CreateRunReplicationServiceParams) {
+ const url = new URL(env.RUN_REPLICATION_CLICKHOUSE_URL);
+ // Remove secure param to prevent Unknown URL parameters error
+ url.searchParams.delete("secure");
+
const clickhouse = new ClickHouse({
- url: env.RUN_REPLICATION_CLICKHOUSE_URL,
+ url: url.toString(),
name: params.name,
keepAlive: {
enabled: params.keepAliveEnabled,
diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.github.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.github.tsx
index 38ef50126cd..2cd41284475 100644
--- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.github.tsx
+++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.github.tsx
@@ -514,7 +514,7 @@ export function ConnectGitHubRepoModal({
GitHub
@@ -632,9 +632,9 @@ export function ConnectedGitHubRepoForm({
useEffect(() => {
const hasChanges =
gitSettingsValues.productionBranch !==
- (connectedGitHubRepo.branchTracking?.prod?.branch || "") ||
+ (connectedGitHubRepo.branchTracking?.prod?.branch || "") ||
gitSettingsValues.stagingBranch !==
- (connectedGitHubRepo.branchTracking?.staging?.branch || "") ||
+ (connectedGitHubRepo.branchTracking?.staging?.branch || "") ||
gitSettingsValues.previewDeploymentsEnabled !== connectedGitHubRepo.previewDeploymentsEnabled;
setHasGitSettingsChanges(hasChanges);
}, [gitSettingsValues, connectedGitHubRepo]);
@@ -898,6 +898,6 @@ export function GitHubSettingsPanel({
)}
-
+
);
}
diff --git a/apps/webapp/app/services/clickhouseInstance.server.ts b/apps/webapp/app/services/clickhouseInstance.server.ts
index f88b3baaaed..61494811a0e 100644
--- a/apps/webapp/app/services/clickhouseInstance.server.ts
+++ b/apps/webapp/app/services/clickhouseInstance.server.ts
@@ -83,6 +83,9 @@ function initializeQueryClickhouseClient() {
const url = new URL(env.QUERY_CLICKHOUSE_URL);
+ // Remove secure param
+ url.searchParams.delete("secure");
+
return new ClickHouse({
url: url.toString(),
name: "query-clickhouse",
diff --git a/apps/webapp/app/services/emailAuth.server.tsx b/apps/webapp/app/services/emailAuth.server.tsx
index 81d4ffcc18c..d115526087f 100644
--- a/apps/webapp/app/services/emailAuth.server.tsx
+++ b/apps/webapp/app/services/emailAuth.server.tsx
@@ -17,6 +17,7 @@ const emailStrategy = new EmailLinkStrategy(
secret,
callbackURL: "/magic",
sessionMagicLinkKey: "triggerdotdev:magiclink",
+ validateSession: false,
},
async ({
email,
diff --git a/apps/webapp/app/services/runsReplicationInstance.server.ts b/apps/webapp/app/services/runsReplicationInstance.server.ts
index 8dc078d338f..b1a9015bc81 100644
--- a/apps/webapp/app/services/runsReplicationInstance.server.ts
+++ b/apps/webapp/app/services/runsReplicationInstance.server.ts
@@ -22,8 +22,12 @@ function initializeRunsReplicationInstance() {
console.log("🗃️ Runs replication service enabled");
+ const url = new URL(env.RUN_REPLICATION_CLICKHOUSE_URL);
+ // Remove secure param to prevent Unknown URL parameters error
+ url.searchParams.delete("secure");
+
const clickhouse = new ClickHouse({
- url: env.RUN_REPLICATION_CLICKHOUSE_URL,
+ url: url.toString(),
name: "runs-replication",
keepAlive: {
enabled: env.RUN_REPLICATION_KEEP_ALIVE_ENABLED === "1",
diff --git a/internal-packages/clickhouse/Dockerfile b/internal-packages/clickhouse/Dockerfile
index ceb5092021b..89b01649b73 100644
--- a/internal-packages/clickhouse/Dockerfile
+++ b/internal-packages/clickhouse/Dockerfile
@@ -1,12 +1,18 @@
FROM golang
-
RUN go install github.com/pressly/goose/v3/cmd/goose@latest
-
+WORKDIR /app
COPY ./schema ./schema
+COPY ./cmd ./cmd
+COPY ./migrate.sh ./migrate.sh
+
+RUN go build -o /usr/local/bin/transform ./cmd/transform/main.go
+RUN chmod +x ./migrate.sh
ENV GOOSE_DRIVER=clickhouse
ENV GOOSE_DBSTRING="tcp://default:password@clickhouse:9000"
ENV GOOSE_MIGRATION_DIR=./schema
-CMD ["goose", "up"]
+
+ENTRYPOINT ["./migrate.sh"]
+CMD ["up"]
diff --git a/packages/build/src/extensions/playwright.ts b/packages/build/src/extensions/playwright.ts
index 0931a4855c7..a9dbe8a3f0c 100644
--- a/packages/build/src/extensions/playwright.ts
+++ b/packages/build/src/extensions/playwright.ts
@@ -317,22 +317,26 @@ class PlaywrightExtension implements BuildExtension {
Array.from(browsersToInstall).forEach((browser) => {
instructions.push(
- `RUN grep -A5 -m1 "browser: ${browser}" /tmp/browser-info.txt > /tmp/${browser}-info.txt`,
+ // Extract the block for the specific browser.
+ // We look for a line starting with "browser: {browser}" OR "{browser} v" (legacy)
+ // Then we collect lines until the next block starts (line starting with browser: or certain chars) or an empty line.
+ `RUN awk '/^browser: ${browser}|^${browser} v/{flag=1; print; next} /^(browser:|[a-z-]+ v)/{flag=0} flag' /tmp/browser-info.txt > /tmp/${browser}-info.txt`,
- `RUN INSTALL_DIR=$(grep "Install location:" /tmp/${browser}-info.txt | cut -d':' -f2- | xargs) && \
+ `RUN INSTALL_DIR=$(grep -i "Install location:" /tmp/${browser}-info.txt | cut -d':' -f2- | xargs) && \
DIR_NAME=$(basename "$INSTALL_DIR") && \
- if [ -z "$DIR_NAME" ]; then echo "Failed to extract installation directory for ${browser}"; exit 1; fi && \
+ if [ -z "$DIR_NAME" ]; then echo "Failed to extract installation directory for ${browser}. Content of /tmp/${browser}-info.txt:"; cat /tmp/${browser}-info.txt; exit 1; fi && \
MS_DIR="/ms-playwright/$DIR_NAME" && \
mkdir -p "$MS_DIR"`,
- `RUN DOWNLOAD_URL=$(grep "Download url:" /tmp/${browser}-info.txt | cut -d':' -f2- | xargs | sed "s/mac-arm64/linux/g" | sed "s/mac-15-arm64/ubuntu-20.04/g") && \
+ `RUN DOWNLOAD_URL=$(grep -i "Download url:" /tmp/${browser}-info.txt | cut -d':' -f2- | xargs | sed "s/mac-arm64/linux/g" | sed "s/mac-15-arm64/ubuntu-20.04/g") && \
if [ -z "$DOWNLOAD_URL" ]; then echo "Failed to extract download URL for ${browser}"; exit 1; fi && \
echo "Downloading ${browser} from $DOWNLOAD_URL" && \
curl -L -o /tmp/${browser}.zip "$DOWNLOAD_URL" && \
if [ $? -ne 0 ]; then echo "Failed to download ${browser}"; exit 1; fi && \
- unzip -q /tmp/${browser}.zip -d "/ms-playwright/$(basename $(grep "Install location:" /tmp/${browser}-info.txt | cut -d':' -f2- | xargs))" && \
+ INSTALL_LOCATION=$(grep -i "Install location:" /tmp/${browser}-info.txt | cut -d':' -f2- | xargs) && \
+ unzip -q /tmp/${browser}.zip -d "/ms-playwright/$(basename "$INSTALL_LOCATION")" && \
if [ $? -ne 0 ]; then echo "Failed to extract ${browser}"; exit 1; fi && \
- chmod -R +x "/ms-playwright/$(basename $(grep "Install location:" /tmp/${browser}-info.txt | cut -d':' -f2- | xargs))" && \
+ chmod -R +x "/ms-playwright/$(basename "$INSTALL_LOCATION")" && \
rm /tmp/${browser}.zip`
);
});
diff --git a/packages/cli-v3/src/deploy/buildImage.ts b/packages/cli-v3/src/deploy/buildImage.ts
index 2225d7db056..36a143dd929 100644
--- a/packages/cli-v3/src/deploy/buildImage.ts
+++ b/packages/cli-v3/src/deploy/buildImage.ts
@@ -486,47 +486,57 @@ async function localBuildImage(options: SelfHostedBuildImageOptions): Promise(
+ async appendToStream(
runId: string,
target: string,
streamId: string,
part: TBody,
requestOptions?: ZodFetchOptions
) {
+ // Serialize object payloads to JSON to prevent [object Object] coercion by fetch
+ const body =
+ typeof part === "string" || part instanceof ArrayBuffer || part instanceof Blob ||
+ part instanceof FormData || part instanceof URLSearchParams ||
+ (typeof ReadableStream !== "undefined" && part instanceof ReadableStream)
+ ? (part as BodyInit)
+ : JSON.stringify(part);
+
return zodfetch(
AppendToStreamResponseBody,
`${this.baseUrl}/realtime/v1/streams/${runId}/${target}/${streamId}/append`,
{
method: "POST",
headers: this.#getHeaders(false),
- body: part,
+ body,
},
mergeRequestOptions(this.defaultRequestOptions, requestOptions)
);
diff --git a/packages/core/src/v3/apiClient/runStream.ts b/packages/core/src/v3/apiClient/runStream.ts
index 520ecd8dc2b..16f866563bd 100644
--- a/packages/core/src/v3/apiClient/runStream.ts
+++ b/packages/core/src/v3/apiClient/runStream.ts
@@ -20,36 +20,36 @@ import { zodShapeStream } from "./stream.js";
export type RunShape = TRunTypes extends AnyRunTypes
? {
- id: string;
- taskIdentifier: TRunTypes["taskIdentifier"];
- payload: TRunTypes["payload"];
- output?: TRunTypes["output"];
- createdAt: Date;
- updatedAt: Date;
- status: RunStatus;
- durationMs: number;
- costInCents: number;
- baseCostInCents: number;
- tags: string[];
- idempotencyKey?: string;
- expiredAt?: Date;
- ttl?: string;
- finishedAt?: Date;
- startedAt?: Date;
- delayedUntil?: Date;
- queuedAt?: Date;
- metadata?: Record;
- error?: SerializedError;
- isTest: boolean;
- isQueued: boolean;
- isExecuting: boolean;
- isWaiting: boolean;
- isCompleted: boolean;
- isFailed: boolean;
- isSuccess: boolean;
- isCancelled: boolean;
- realtimeStreams: string[];
- }
+ id: string;
+ taskIdentifier: TRunTypes["taskIdentifier"];
+ payload: TRunTypes["payload"];
+ output?: TRunTypes["output"];
+ createdAt: Date;
+ updatedAt: Date;
+ status: RunStatus;
+ durationMs: number;
+ costInCents: number;
+ baseCostInCents: number;
+ tags: string[];
+ idempotencyKey?: string;
+ expiredAt?: Date;
+ ttl?: string;
+ finishedAt?: Date;
+ startedAt?: Date;
+ delayedUntil?: Date;
+ queuedAt?: Date;
+ metadata?: Record;
+ error?: SerializedError;
+ isTest: boolean;
+ isQueued: boolean;
+ isExecuting: boolean;
+ isWaiting: boolean;
+ isCompleted: boolean;
+ isFailed: boolean;
+ isSuccess: boolean;
+ isCancelled: boolean;
+ realtimeStreams: string[];
+ }
: never;
export type AnyRunShape = RunShape;
@@ -102,9 +102,9 @@ export type StreamPartResult> = {
export type RunWithStreamsResult> =
| {
- type: "run";
- run: TRun;
- }
+ type: "run";
+ run: TRun;
+ }
| StreamPartResult;
export function runShapeStream(
@@ -297,7 +297,10 @@ export class SSEStreamSubscription implements StreamSubscription {
chunkController.enqueue({
id: record.seq_num.toString(),
- chunk: parsedBody.data,
+ chunk:
+ typeof parsedBody.data === "string"
+ ? safeParseJSON(parsedBody.data)
+ : parsedBody.data,
timestamp: record.timestamp,
});
}
@@ -390,7 +393,7 @@ export class SSEStreamSubscriptionFactory implements StreamSubscriptionFactory {
headers?: Record;
signal?: AbortSignal;
}
- ) {}
+ ) { }
createSubscription(
runId: string,
@@ -750,10 +753,10 @@ if (isSafari()) {
function getStreamsFromRunShape(run: AnyRunShape): string[] {
const metadataStreams =
run.metadata &&
- "$$streams" in run.metadata &&
- Array.isArray(run.metadata.$$streams) &&
- run.metadata.$$streams.length > 0 &&
- run.metadata.$$streams.every((stream) => typeof stream === "string")
+ "$$streams" in run.metadata &&
+ Array.isArray(run.metadata.$$streams) &&
+ run.metadata.$$streams.length > 0 &&
+ run.metadata.$$streams.every((stream) => typeof stream === "string")
? run.metadata.$$streams
: undefined;
diff --git a/packages/core/src/v3/build/runtime.ts b/packages/core/src/v3/build/runtime.ts
index 1618a50ffd4..755312e8afb 100644
--- a/packages/core/src/v3/build/runtime.ts
+++ b/packages/core/src/v3/build/runtime.ts
@@ -4,6 +4,8 @@ import { BuildRuntime } from "../schemas/build.js";
import { dedupFlags } from "./flags.js";
import { homedir } from "node:os";
+import { existsSync } from "node:fs";
+
export const DEFAULT_RUNTIME = "node" satisfies BuildRuntime;
export function binaryForRuntime(runtime: BuildRuntime): string {
@@ -25,14 +27,28 @@ export function execPathForRuntime(runtime: BuildRuntime): string {
return process.execPath;
case "bun":
if (typeof process.env.BUN_INSTALL === "string") {
- return join(process.env.BUN_INSTALL, "bin", "bun");
+ const binPath = join(process.env.BUN_INSTALL, "bin", "bun");
+
+ if (existsSync(binPath)) {
+ return binPath;
+ }
}
if (typeof process.env.BUN_INSTALL_BIN === "string") {
- return join(process.env.BUN_INSTALL_BIN, "bun");
+ const binPath = join(process.env.BUN_INSTALL_BIN, "bun");
+
+ if (existsSync(binPath)) {
+ return binPath;
+ }
}
- return join(homedir(), ".bun", "bin", "bun");
+ const defaultPath = join(homedir(), ".bun", "bin", "bun");
+
+ if (existsSync(defaultPath)) {
+ return defaultPath;
+ }
+
+ return "bun";
default:
throw new Error(`Unsupported runtime ${runtime}`);
}
diff --git a/packages/core/src/v3/schemas/api.ts b/packages/core/src/v3/schemas/api.ts
index 2a7bcb96502..2eda24edd24 100644
--- a/packages/core/src/v3/schemas/api.ts
+++ b/packages/core/src/v3/schemas/api.ts
@@ -1514,8 +1514,8 @@ export const ApiDeploymentListResponseItem = z.object({
createdAt: z.coerce.date(),
shortCode: z.string(),
version: z.string(),
- runtime: z.string(),
- runtimeVersion: z.string(),
+ runtime: z.string().nullable(),
+ runtimeVersion: z.string().nullable(),
status: z.enum([
"PENDING",
"BUILDING",
diff --git a/packages/core/src/v3/utils/flattenAttributes.ts b/packages/core/src/v3/utils/flattenAttributes.ts
index 83d1a14f2cd..d610895e0bd 100644
--- a/packages/core/src/v3/utils/flattenAttributes.ts
+++ b/packages/core/src/v3/utils/flattenAttributes.ts
@@ -5,6 +5,36 @@ export const CIRCULAR_REFERENCE_SENTINEL = "$@circular((";
const DEFAULT_MAX_DEPTH = 128;
+function escapeKey(key: string): string {
+ return key.replace(/\\/g, "\\\\").replace(/\./g, "\\.");
+}
+
+function unescapeKey(key: string): string {
+ return key.replace(/\\\./g, ".").replace(/\\\\/g, "\\");
+}
+
+function splitKey(key: string): string[] {
+ const parts: string[] = [];
+ let currentPart = "";
+ for (let i = 0; i < key.length; i++) {
+ if (key[i] === "\\" && i + 1 < key.length) {
+ if (key[i + 1] === "." || key[i + 1] === "\\") {
+ currentPart += key[i + 1];
+ i++;
+ } else {
+ currentPart += key[i];
+ }
+ } else if (key[i] === ".") {
+ parts.push(currentPart);
+ currentPart = "";
+ } else {
+ currentPart += key[i];
+ }
+ }
+ parts.push(currentPart);
+ return parts;
+}
+
export function flattenAttributes(
obj: unknown,
prefix?: string,
@@ -24,7 +54,7 @@ class AttributeFlattener {
constructor(
private maxAttributeCount?: number,
private maxDepth: number = DEFAULT_MAX_DEPTH
- ) {}
+ ) { }
get attributes(): Attributes {
return this.result;
@@ -200,7 +230,8 @@ class AttributeFlattener {
break;
}
- const newPrefix = `${prefix ? `${prefix}.` : ""}${Array.isArray(obj) ? `[${key}]` : key}`;
+ const escapedKey = Array.isArray(obj) ? `[${key}]` : escapeKey(key);
+ const newPrefix = `${prefix ? `${prefix}.` : ""}${escapedKey}`;
if (Array.isArray(value)) {
for (let i = 0; i < value.length; i++) {
@@ -278,9 +309,9 @@ export function unflattenAttributes(
continue;
}
- const parts = key.split(".").reduce(
+ const parts = splitKey(key).reduce(
(acc, part) => {
- if (part.startsWith("[") && part.endsWith("]")) {
+ if (typeof part === "string" && part.startsWith("[") && part.endsWith("]")) {
// Handle array indices more precisely
const match = part.match(/^\[(\d+)\]$/);
if (match && match[1]) {
diff --git a/repro_1510.ts b/repro_1510.ts
new file mode 100644
index 00000000000..3e90f587b02
--- /dev/null
+++ b/repro_1510.ts
@@ -0,0 +1,51 @@
+import { flattenAttributes, unflattenAttributes } from "./packages/core/src/v3/utils/flattenAttributes";
+
+const cases = [
+ {
+ name: "Key with period",
+ obj: { "Key 0.002mm": 31.4 },
+ },
+ {
+ name: "Nested key with period",
+ obj: { parent: { "child.key": "value" } },
+ },
+ {
+ name: "Regular nested key",
+ obj: { parent: { child: "value" } },
+ },
+ {
+ name: "Array with period in key",
+ obj: { "list.0": ["item1"] },
+ },
+ {
+ name: "Complex mixed",
+ obj: {
+ "a.b": {
+ "c.d": "value",
+ e: [1, 2]
+ }
+ }
+ }
+];
+
+let allPassed = true;
+
+for (const { name, obj } of cases) {
+ const flattened = flattenAttributes(obj);
+ const unflattened = unflattenAttributes(flattened);
+ const success = JSON.stringify(unflattened) === JSON.stringify(obj);
+
+ console.log(`Case: ${name}`);
+ console.log(" Flattened:", JSON.stringify(flattened));
+ console.log(" Unflattened:", JSON.stringify(unflattened));
+ console.log(" Result:", success ? "SUCCESS" : "FAILURE");
+
+ if (!success) allPassed = false;
+}
+
+if (allPassed) {
+ console.log("\nALL TESTS PASSED!");
+} else {
+ console.log("\nSOME TESTS FAILED!");
+ process.exit(1);
+}
diff --git a/test-flatten.ts b/test-flatten.ts
new file mode 100644
index 00000000000..b982dcaf6d2
--- /dev/null
+++ b/test-flatten.ts
@@ -0,0 +1,14 @@
+import { flattenAttributes, unflattenAttributes } from "./packages/core/src/v3/utils/flattenAttributes";
+
+const obj1 = {
+ "my.key.with.periods": "value1",
+ nested: {
+ "another.key": "value2"
+ }
+};
+
+const flat = flattenAttributes(obj1);
+console.log("Flattened:", flat);
+
+const unflat = unflattenAttributes(flat);
+console.log("Unflattened:", unflat);