From 874e0e525157e94e380242020efcd5e1dd86d5a2 Mon Sep 17 00:00:00 2001 From: Serena Shah-Simpson Date: Thu, 9 Apr 2026 16:14:01 -0400 Subject: [PATCH] Add preview entry to wrangler output file --- .changeset/add-preview-output-entry.md | 9 +++++ .../wrangler/src/__tests__/output.test.ts | 32 +++++++++++++++++ .../wrangler/src/__tests__/preview.test.ts | 34 +++++++++++++++++-- packages/wrangler/src/output.ts | 19 +++++++++++ packages/wrangler/src/preview/preview.ts | 13 +++++++ 5 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 .changeset/add-preview-output-entry.md diff --git a/.changeset/add-preview-output-entry.md b/.changeset/add-preview-output-entry.md new file mode 100644 index 0000000000..adf88b7d51 --- /dev/null +++ b/.changeset/add-preview-output-entry.md @@ -0,0 +1,9 @@ +--- +"wrangler": minor +--- + +Add `preview` output-file entries for `wrangler preview` deployments + +`wrangler preview` now writes a `preview` entry to the Wrangler output file when `WRANGLER_OUTPUT_FILE_PATH` or `WRANGLER_OUTPUT_FILE_DIRECTORY` is configured. The entry includes the Worker name, preview metadata (`preview_id`, `preview_name`, `preview_slug`, `preview_urls`) and deployment metadata (`deployment_id`, `deployment_urls`). + +This makes preview command runs machine-readable in the same output stream as other Wrangler commands, which helps CI integrations consume preview URLs and IDs directly. diff --git a/packages/wrangler/src/__tests__/output.test.ts b/packages/wrangler/src/__tests__/output.test.ts index ff783a7234..e42562d05a 100644 --- a/packages/wrangler/src/__tests__/output.test.ts +++ b/packages/wrangler/src/__tests__/output.test.ts @@ -247,6 +247,38 @@ describe("writeOutput()", () => { ]); }); + it("should write preview outputs with separate preview and deployment URLs", () => { + const WRANGLER_OUTPUT_FILE_PATH = "output.json"; + vi.stubEnv("WRANGLER_OUTPUT_FILE_DIRECTORY", ""); + vi.stubEnv("WRANGLER_OUTPUT_FILE_PATH", WRANGLER_OUTPUT_FILE_PATH); + writeOutput({ + type: "preview", + version: 1, + worker_name: "worker", + preview_id: "preview-id", + preview_name: "branch-name", + preview_slug: "branch-name", + preview_urls: ["https://branch-name.worker.cloudflare.app"], + deployment_id: "deployment-id", + deployment_urls: ["https://abc12345.worker.cloudflare.app"], + }); + + const outputFile = readFileSync(WRANGLER_OUTPUT_FILE_PATH, "utf8"); + expect(outputFile).toContainEntries([ + { + type: "preview", + version: 1, + worker_name: "worker", + preview_id: "preview-id", + preview_name: "branch-name", + preview_slug: "branch-name", + preview_urls: ["https://branch-name.worker.cloudflare.app"], + deployment_id: "deployment-id", + deployment_urls: ["https://abc12345.worker.cloudflare.app"], + }, + ]); + }); + it("should write an error log when a handler throws an error", async () => { vi.mock("../user/whoami", () => { return { diff --git a/packages/wrangler/src/__tests__/preview.test.ts b/packages/wrangler/src/__tests__/preview.test.ts index b661587687..51de704b58 100644 --- a/packages/wrangler/src/__tests__/preview.test.ts +++ b/packages/wrangler/src/__tests__/preview.test.ts @@ -1,9 +1,10 @@ import * as childProcess from "node:child_process"; -import { mkdirSync, writeFileSync } from "node:fs"; +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { stripVTControlCharacters } from "node:util"; import { defaultWranglerConfig } from "@cloudflare/workers-utils"; import { http, HttpResponse } from "msw"; -import { afterAll, beforeEach, describe, test, vi } from "vitest"; +import { afterAll, afterEach, beforeEach, describe, test, vi } from "vitest"; +import { clearOutputFilePath } from "../output"; import { extractConfigBindings, getBranchName } from "../preview/shared"; import { mockAccountId, mockApiToken } from "./helpers/mock-account-id"; import { mockConsoleMethods } from "./helpers/mock-console"; @@ -14,6 +15,7 @@ import { writeRedirectedWranglerConfig, writeWranglerConfig, } from "./helpers/write-wrangler-config"; +import type { OutputEntry } from "../output"; import type { Config, PreviewsConfig } from "@cloudflare/workers-utils"; vi.mock("node:child_process", async () => { @@ -37,6 +39,10 @@ describe("wrangler preview", () => { runInTempDir(); mockApiToken(); mockAccountId(); + afterEach(() => { + clearOutputFilePath(); + }); + describe("getBranchName", () => { beforeEach(() => { vi.unstubAllEnvs(); @@ -502,6 +508,7 @@ describe("wrangler preview", () => { test("should output preview and deployment JSON with --json", async ({ expect, }) => { + const outputFile = "./output.json"; msw.use( http.get( `*/accounts/:accountId/workers/workers/:workerId/previews/:previewId`, @@ -554,7 +561,10 @@ describe("wrangler preview", () => { ) ); - await runWrangler("preview --name test-preview --json"); + await runWrangler("preview --name test-preview --json", { + ...process.env, + WRANGLER_OUTPUT_FILE_PATH: outputFile, + }); expect(std.out).toContain('"preview"'); expect(std.out).toContain('"deployment"'); @@ -562,6 +572,24 @@ describe("wrangler preview", () => { expect(std.out).toContain('"id": "deployment-id-json"'); expect(std.out).not.toContain("Preview: test-preview"); expect(std.out).not.toContain("Deployment:"); + + const outputEntries = readFileSync(outputFile, "utf8") + .split("\n") + .filter(Boolean) + .map((line) => JSON.parse(line)) as OutputEntry[]; + + expect(outputEntries).toContainEqual( + expect.objectContaining({ + type: "preview", + worker_name: "test-worker", + preview_id: "preview-id-json", + preview_name: "test-preview", + preview_slug: "test-preview", + preview_urls: ["https://test-preview.test-worker.cloudflare.app"], + deployment_id: "deployment-id-json", + deployment_urls: ["https://json123.test-worker.cloudflare.app"], + }) + ); }); test("should build correctly when using a redirected config", async ({ diff --git a/packages/wrangler/src/output.ts b/packages/wrangler/src/output.ts index 23de6e3900..51d93e03cf 100644 --- a/packages/wrangler/src/output.ts +++ b/packages/wrangler/src/output.ts @@ -69,6 +69,7 @@ interface OutputEntryBase { export type OutputEntry = | OutputEntrySession | OutputEntryDeployment + | OutputEntryPreview | OutputEntryPagesDeployment | OutputEntryVersionUpload | OutputEntryVersionDeployment @@ -102,6 +103,24 @@ interface OutputEntryDeployment extends OutputEntryBase<"deploy"> { wrangler_environment: string | undefined; } +interface OutputEntryPreview extends OutputEntryBase<"preview"> { + version: 1; + /** The name of the Worker. */ + worker_name: string | null; + /** The ID of the Preview resource. */ + preview_id: string; + /** The human-readable name of the Preview resource. */ + preview_name: string; + /** The slug of the Preview resource. */ + preview_slug: string; + /** A list of URLs associated with the Preview resource. */ + preview_urls: string[] | undefined; + /** The ID of the Preview deployment resource. */ + deployment_id: string; + /** A list of URLs associated with the Preview deployment. */ + deployment_urls: string[] | undefined; +} + interface OutputEntryAutoConfig extends OutputEntryBase<"autoconfig"> { version: 1; /** The command that triggered autoconfig */ diff --git a/packages/wrangler/src/preview/preview.ts b/packages/wrangler/src/preview/preview.ts index 4a62436b36..e083c85add 100644 --- a/packages/wrangler/src/preview/preview.ts +++ b/packages/wrangler/src/preview/preview.ts @@ -23,6 +23,7 @@ import { loadSourceMaps } from "../deployment-bundle/source-maps"; import { confirm } from "../dialogs"; import { logger } from "../logger"; import { isNavigatorDefined } from "../navigator-user-agent"; +import { writeOutput } from "../output"; import { requireAuth } from "../user"; import { drawBox, @@ -845,6 +846,18 @@ export async function handlePreviewCommand( { ignoreDefaults } ); + writeOutput({ + type: "preview", + version: 1, + worker_name: workerName, + preview_id: preview.id, + preview_name: preview.name, + preview_slug: preview.slug, + preview_urls: preview.urls, + deployment_id: deployment.id, + deployment_urls: deployment.urls, + }); + if (args.json) { logger.log(JSON.stringify({ preview, deployment }, null, 2)); return;