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);