diff --git a/.changeset/pretty-preview-output.md b/.changeset/pretty-preview-output.md new file mode 100644 index 00000000000..d515ad5d5d7 --- /dev/null +++ b/.changeset/pretty-preview-output.md @@ -0,0 +1,10 @@ +--- +"@cloudflare/deploy-helpers": patch +"wrangler": patch +--- + +Clean up Worker Preview and custom domain deploy success output. + +`wrangler preview` now prints a compact success summary with Preview URL(s), Deployment ID, Deployment URL(s), and a Pull Request link when CI metadata is detected. It no longer prints box art, configuration source markers, observability/logpush settings, or preview bindings in the success output. + +`wrangler deploy` custom domain targets are now grouped under a `Custom Domains:` section and no longer include internal `(custom domain)` or preview-state annotations. diff --git a/packages/deploy-helpers/src/preview/api.ts b/packages/deploy-helpers/src/preview/api.ts index 6359b250ee1..1168cd9325a 100644 --- a/packages/deploy-helpers/src/preview/api.ts +++ b/packages/deploy-helpers/src/preview/api.ts @@ -102,6 +102,9 @@ export type CreatePreviewDeploymentRequestParams = { compatibility_flags?: string[]; annotations?: { "workers/message"?: string; + "workers/pull_request_number"?: string; + "workers/pull_request_url"?: string; + "workers/repository_url"?: string; "workers/tag"?: string; }; migrations?: CfWorkerInit["migrations"]; diff --git a/packages/deploy-helpers/src/preview/preview.ts b/packages/deploy-helpers/src/preview/preview.ts index 7ab05b13432..d94bf51d4b1 100644 --- a/packages/deploy-helpers/src/preview/preview.ts +++ b/packages/deploy-helpers/src/preview/preview.ts @@ -1,5 +1,6 @@ import path from "node:path"; import { + APIError, configFileName, getBindingTypeFriendlyName, UserError, @@ -19,14 +20,14 @@ import { getPreviewDeployment, getWorkerPreviewDefaults, } from "./api"; -import { drawBox, drawConnectedChildBox } from "./box"; -import { formatAlignedRows, formatBindings } from "./format"; import { assemblePreviewScriptSettings, extractConfigBindings, getBranchName, getHeadCommitMessage, getHeadCommitRef, + getPullRequestMetadata, + getRepositoryUrl, resolveWorkerName, shouldUseCIMetadataFallback, } from "./shared"; @@ -37,6 +38,7 @@ import type { DeploymentResource, PreviewResource, } from "./api"; +import type { PullRequestMetadata } from "./shared"; import type { Config, PreviewsConfig } from "@cloudflare/workers-utils"; type PreviewDeploymentModule = { @@ -45,55 +47,6 @@ type PreviewDeploymentModule = { content_base64: string; }; -type MergedBinding = Binding & { fromConfig: boolean }; - -type MergedScriptLevel = { - observability?: { - enabled?: boolean; - head_sampling_rate?: number; - fromConfig: boolean; - }; - logpush?: { - value: boolean; - fromConfig: boolean; - }; - tail_consumers?: Array<{ name: string }>; -}; - -type MergedVersionLevel = { - compatibility_date?: { - value: string; - fromConfig: boolean; - }; - compatibility_flags?: { - value: string[]; - fromConfig: boolean; - }; - limits?: { - value: Config["limits"]; - fromConfig: boolean; - }; - placement?: { - value: { mode: string }; - fromConfig: boolean; - }; - cache?: { - value: Config["cache"]; - fromConfig: boolean; - }; - assets?: { - value: { - directory?: string; - binding?: string; - html_handling?: string; - not_found_handling?: string; - run_worker_first?: string[] | boolean; - }; - fromConfig: boolean; - }; - env: Record; -}; - export type PreviewArgs = { script?: string; name?: string; @@ -240,6 +193,8 @@ async function assemblePreviewDeploymentSettings( options: { message?: string; tag?: string; + repositoryUrl?: string; + pullRequest?: PullRequestMetadata; assetsOptions?: PreviewAssetsOptions; } ): Promise { @@ -276,10 +231,25 @@ async function assemblePreviewDeploymentSettings( if (config.compatibility_flags && config.compatibility_flags.length > 0) { request.compatibility_flags = config.compatibility_flags; } - if (options.message || options.tag) { + if ( + options.message || + options.tag || + options.repositoryUrl || + options.pullRequest?.number || + options.pullRequest?.url + ) { request.annotations = { ...(options.message && { "workers/message": options.message }), ...(options.tag && { "workers/tag": options.tag }), + ...(options.repositoryUrl && { + "workers/repository_url": options.repositoryUrl, + }), + ...(options.pullRequest?.number && { + "workers/pull_request_number": options.pullRequest.number, + }), + ...(options.pullRequest?.url && { + "workers/pull_request_url": options.pullRequest.url, + }), }; } if (config.migrations.length > 0) { @@ -336,285 +306,94 @@ async function assemblePreviewDeploymentSettings( return request; } -function buildMergedScriptLevel( - config: Config, - previewResource: PreviewResource -): MergedScriptLevel { - const previews = config.previews as PreviewsConfig | undefined; - const result: MergedScriptLevel = {}; - const configHasObservability = - previews?.observability !== undefined || config.observability !== undefined; - const configHasLogpush = - previews?.logpush !== undefined || config.logpush !== undefined; - - if (previewResource.observability !== undefined) { - result.observability = { - enabled: previewResource.observability.enabled, - head_sampling_rate: previewResource.observability.head_sampling_rate, - fromConfig: configHasObservability, - }; - } - - if (previewResource.logpush !== undefined) { - result.logpush = { - value: previewResource.logpush, - fromConfig: configHasLogpush, - }; - } - - if ( - previewResource.tail_consumers && - previewResource.tail_consumers.length > 0 - ) { - result.tail_consumers = previewResource.tail_consumers; +function formatUrls( + singularLabel: string, + pluralLabel: string, + urls?: string[] +) { + if (!urls || urls.length === 0) { + return []; } - return result; -} - -function buildMergedVersionLevel( - config: Config, - deployment: DeploymentResource -): MergedVersionLevel { - const previews = config.previews as PreviewsConfig | undefined; - const configBindingNames = new Set( - Object.keys(extractConfigBindings(config)) - ); - const result: MergedVersionLevel = { env: {} }; - - if (deployment.compatibility_date) { - result.compatibility_date = { - value: deployment.compatibility_date, - fromConfig: !!config.compatibility_date, - }; - } - if ( - deployment.compatibility_flags && - deployment.compatibility_flags.length > 0 - ) { - result.compatibility_flags = { - value: deployment.compatibility_flags, - fromConfig: !!( - config.compatibility_flags && config.compatibility_flags.length > 0 - ), - }; - } - if ( - deployment.limits?.cpu_ms !== undefined || - deployment.limits?.subrequests !== undefined - ) { - result.limits = { - value: { - ...(deployment.limits?.cpu_ms !== undefined && { - cpu_ms: deployment.limits.cpu_ms, - }), - ...(deployment.limits?.subrequests !== undefined && { - subrequests: deployment.limits.subrequests, - }), - }, - fromConfig: !!( - previews?.limits !== undefined || config.limits !== undefined - ), - }; - } - if (deployment.placement?.mode) { - result.placement = { - value: { mode: deployment.placement.mode }, - fromConfig: !!config.placement?.mode, - }; - } - if (deployment.cache !== undefined) { - result.cache = { - value: deployment.cache, - fromConfig: previews?.cache !== undefined || config.cache !== undefined, - }; - } - if (config.assets) { - result.assets = { - value: { - directory: config.assets.directory, - binding: config.assets.binding, - html_handling: config.assets.html_handling, - not_found_handling: config.assets.not_found_handling, - run_worker_first: config.assets.run_worker_first, - }, - fromConfig: true, - }; - } - for (const [name, binding] of Object.entries(deployment.env ?? {})) { - result.env[name] = { ...binding, fromConfig: configBindingNames.has(name) }; + if (urls.length === 1) { + return [`${singularLabel}: ${chalk.underline(urls[0])}`]; } - return result; + return [`${pluralLabel}:`, ...urls.map((url) => ` ${chalk.underline(url)}`)]; } -function formatPreviewResource( +function formatPreviewResult( previewResource: PreviewResource, - scriptLevel: MergedScriptLevel, + deployment: DeploymentResource, isNew: boolean, - configName: string + pullRequest: PullRequestMetadata | undefined ): string { const statusLabel = isNew ? chalk.green("(new)") : chalk.dim("(updated)"); - const obsEnabled = scriptLevel.observability?.enabled ?? false; - const obsRate = scriptLevel.observability?.head_sampling_rate; - const formattedRate = obsRate !== undefined ? obsRate.toFixed(1) : undefined; - const obsValue = obsEnabled - ? `enabled${ - formattedRate !== undefined ? `, ${formattedRate} sampling` : "" - }` - : "disabled"; - const lines: string[] = [ `${chalk.bold("Preview:")} ${previewResource.name} ${statusLabel}`, + ...formatUrls("Preview URL", "Preview URLs", previewResource.urls), "", - ...(previewResource.urls ?? []).map( - (url) => ` ${chalk.bold.underline(url)}` - ), + `${chalk.bold("Deployment ID:")} ${deployment.id}`, + ...formatUrls("Deployment URL", "Deployment URLs", deployment.urls), ]; - const settingsRows: Array<[string, string, boolean]> = []; - if (scriptLevel.observability !== undefined) { - settingsRows.push([ - "observability", - obsValue, - scriptLevel.observability.fromConfig, - ]); - } - if (scriptLevel.logpush !== undefined) { - settingsRows.push([ - "logpush", - scriptLevel.logpush.value ? "enabled" : "disabled", - scriptLevel.logpush.fromConfig, - ]); - } - if (scriptLevel.tail_consumers && scriptLevel.tail_consumers.length > 0) { - settingsRows.push([ - "tail_consumers", - scriptLevel.tail_consumers.map((tc) => tc.name).join(", "), - false, - ]); - } - if (settingsRows.length > 0) { + if (pullRequest?.url) { lines.push(""); - lines.push(...formatAlignedRows(settingsRows)); + lines.push( + `${chalk.bold("Pull Request:")} ${chalk.underline(pullRequest.url)}` + ); } - const hasConfigValues = settingsRows.some(([, , fromConfig]) => fromConfig); - const footerLines = hasConfigValues - ? ["", chalk.hex("#FFA500")(`◆ from ${configName}`)] - : undefined; - - return drawBox(lines, { footerLines, connectToChild: true }); + return lines.join("\n"); } -function formatDeploymentResource( - deployment: DeploymentResource, - versionLevel: MergedVersionLevel, - configName: string -): string { - const lines: string[] = [ - `${chalk.bold("Deployment:")} ${deployment.id}`, - "", - ...(deployment.urls ?? []).map((url) => ` ${chalk.bold.underline(url)}`), - ]; - - const settingsRows: Array<[string, string, boolean]> = []; - if (versionLevel.compatibility_date) { - settingsRows.push([ - "compatibility_date", - versionLevel.compatibility_date.value, - versionLevel.compatibility_date.fromConfig, - ]); - } - if (versionLevel.compatibility_flags) { - settingsRows.push([ - "compatibility_flags", - versionLevel.compatibility_flags.value.join(", "), - versionLevel.compatibility_flags.fromConfig, - ]); - } - if ( - versionLevel.limits?.value?.cpu_ms !== undefined || - versionLevel.limits?.value?.subrequests !== undefined - ) { - const limitParts = [ - versionLevel.limits?.value?.cpu_ms !== undefined - ? `cpu_ms: ${versionLevel.limits.value.cpu_ms}` - : undefined, - versionLevel.limits?.value?.subrequests !== undefined - ? `subrequests: ${versionLevel.limits.value.subrequests}` - : undefined, - ].filter((value): value is string => value !== undefined); - settingsRows.push([ - "limits", - limitParts.join(", "), - versionLevel.limits.fromConfig, - ]); - } - if (versionLevel.placement) { - settingsRows.push([ - "placement", - versionLevel.placement.value.mode, - versionLevel.placement.fromConfig, - ]); - } - if (versionLevel.cache !== undefined) { - settingsRows.push([ - "cache", - versionLevel.cache.value?.enabled ? "enabled" : "disabled", - versionLevel.cache.fromConfig, - ]); - } - if (settingsRows.length > 0) { - lines.push(""); - lines.push(...formatAlignedRows(settingsRows)); - } - - if (versionLevel.assets) { - lines.push(""); - lines.push(chalk.bold(" Assets")); - const assetsRows: Array<[string, string, boolean]> = []; - const assets = versionLevel.assets.value; - const fromConfig = versionLevel.assets.fromConfig; - if (assets.directory) { - assetsRows.push(["directory", assets.directory, fromConfig]); - } - if (assets.binding) { - assetsRows.push(["binding", assets.binding, fromConfig]); - } - if (assets.html_handling) { - assetsRows.push(["html_handling", assets.html_handling, fromConfig]); - } - if (assets.not_found_handling) { - assetsRows.push([ - "not_found_handling", - assets.not_found_handling, - fromConfig, - ]); - } - if (assets.run_worker_first !== undefined) { - const value = - typeof assets.run_worker_first === "boolean" - ? String(assets.run_worker_first) - : assets.run_worker_first.join(", "); - assetsRows.push(["run_worker_first", value, fromConfig]); - } - lines.push(...formatAlignedRows(assetsRows, " ")); - } - - lines.push(""); - lines.push(chalk.bold(" Bindings")); - lines.push(...formatBindings(versionLevel.env)); +function hasPreviewMetadataAnnotations( + deploymentRequest: CreatePreviewDeploymentRequestParams +): boolean { + return Boolean( + deploymentRequest.annotations?.["workers/pull_request_number"] || + deploymentRequest.annotations?.["workers/pull_request_url"] || + deploymentRequest.annotations?.["workers/repository_url"] + ); +} - const hasConfigValues = - settingsRows.some(([, , fromConfig]) => fromConfig) || - versionLevel.assets?.fromConfig || - Object.values(versionLevel.env).some((binding) => binding.fromConfig); - const footerLines = hasConfigValues - ? ["", chalk.hex("#FFA500")(`◆ from ${configName}`)] +function omitPreviewMetadataAnnotations( + deploymentRequest: CreatePreviewDeploymentRequestParams +): CreatePreviewDeploymentRequestParams { + const { annotations, ...rest } = deploymentRequest; + const remainingAnnotations = annotations + ? Object.fromEntries( + Object.entries(annotations).filter( + ([key]) => + key !== "workers/pull_request_number" && + key !== "workers/pull_request_url" && + key !== "workers/repository_url" + ) + ) : undefined; - return drawConnectedChildBox(lines, { footerLines, indent: " " }); + return { + ...rest, + ...(remainingAnnotations && + Object.keys(remainingAnnotations).length > 0 && { + annotations: remainingAnnotations, + }), + }; +} + +function isPreviewMetadataAnnotationsUnsupportedError(error: unknown): boolean { + const messages = [ + error instanceof Error ? error.message : undefined, + error instanceof APIError ? error.text : undefined, + ] + .filter(Boolean) + .join("\n"); + + return ( + messages.includes("annotations not allowed") && + (messages.includes("workers/pull_request") || + messages.includes("workers/repository_url")) + ); } function logMissingPreviewsBindingsWarning( @@ -682,6 +461,8 @@ export async function preview( !args.message && shouldUseCIMetadataFallback() ? getHeadCommitMessage() : undefined; + const repositoryUrl = getRepositoryUrl(); + const pullRequest = getPullRequestMetadata(); let existingPreview: PreviewResource | null = null; try { @@ -732,35 +513,52 @@ export async function preview( { message: args.message ?? fallbackMessage, tag: args.tag ?? fallbackTag, + repositoryUrl, + pullRequest, assetsOptions, } ); - const deployment = await createPreviewDeployment( - config, - accountId, - workerName, - previewResource.id, - deploymentRequest, - { ignoreDefaults } - ); + let deployment: DeploymentResource; + try { + deployment = await createPreviewDeployment( + config, + accountId, + workerName, + previewResource.id, + deploymentRequest, + { ignoreDefaults } + ); + } catch (error) { + if ( + hasPreviewMetadataAnnotations(deploymentRequest) && + isPreviewMetadataAnnotationsUnsupportedError(error) + ) { + deployment = await createPreviewDeployment( + config, + accountId, + workerName, + previewResource.id, + omitPreviewMetadataAnnotations(deploymentRequest), + { ignoreDefaults } + ); + } else { + throw error; + } + } if (args.json) { logger.log( JSON.stringify({ preview: previewResource, deployment }, null, 2) ); } else { - const scriptLevel = buildMergedScriptLevel(config, previewResource); - const versionLevel = buildMergedVersionLevel(config, deployment); - const configName = configFileName(config.configPath); logger.log( - formatPreviewResource( + formatPreviewResult( previewResource, - scriptLevel, + deployment, isNewPreview, - configName + pullRequest ) ); - logger.log(formatDeploymentResource(deployment, versionLevel, configName)); const topLevelBindings = getBindings(config); if (Object.keys(topLevelBindings).length > 0) { diff --git a/packages/deploy-helpers/src/preview/shared.ts b/packages/deploy-helpers/src/preview/shared.ts index 19fc69786a8..2601379dd07 100644 --- a/packages/deploy-helpers/src/preview/shared.ts +++ b/packages/deploy-helpers/src/preview/shared.ts @@ -1,4 +1,5 @@ import { execSync } from "node:child_process"; +import { readFileSync } from "node:fs"; import { configFileName, getWorkersCIBranchName, @@ -55,6 +56,160 @@ export function getHeadCommitMessage(): string | undefined { } } +function normalizeRepositoryUrl(repositoryUrl: string): string | undefined { + const trimmed = repositoryUrl.trim(); + if (!trimmed) { + return undefined; + } + + const scpLikeSshMatch = trimmed.match(/^git@([^:]+):(.+)$/); + if (scpLikeSshMatch) { + const [, host, pathname] = scpLikeSshMatch; + return `https://${host}/${pathname.replace(/\.git$/, "")}`; + } + + try { + const url = new URL(trimmed); + if (url.protocol === "ssh:" && url.username === "git") { + return `https://${url.host}${url.pathname.replace(/\.git$/, "")}`; + } + + if (url.protocol !== "https:" && url.protocol !== "http:") { + return undefined; + } + + url.username = ""; + url.password = ""; + url.search = ""; + url.hash = ""; + url.pathname = url.pathname.replace(/\.git$/, ""); + return url.toString().replace(/\/$/, ""); + } catch { + return undefined; + } +} + +export function getRepositoryUrl(): string | undefined { + const repositoryUrl = + process.env.CI_PROJECT_URL || + process.env.CI_REPOSITORY_URL || + process.env.CIRCLE_REPOSITORY_URL || + process.env.BUILDKITE_REPO || + process.env.BITBUCKET_GIT_HTTP_ORIGIN || + process.env.BITBUCKET_GIT_SSH_ORIGIN || + process.env.REPOSITORY_URL; + if (repositoryUrl) { + return normalizeRepositoryUrl(repositoryUrl); + } + + if (process.env.GITHUB_REPOSITORY) { + const githubServerUrl = + process.env.GITHUB_SERVER_URL || "https://github.com"; + return normalizeRepositoryUrl( + `${githubServerUrl.replace(/\/$/, "")}/${process.env.GITHUB_REPOSITORY}` + ); + } + + return undefined; +} + +export type PullRequestMetadata = { + number?: string; + url?: string; +}; + +function normalizePullRequestNumber(number: string | number | undefined) { + if (number === undefined) { + return undefined; + } + + const normalizedNumber = String(number).trim(); + return normalizedNumber ? normalizedNumber : undefined; +} + +function getGitHubPullRequestMetadata(): PullRequestMetadata | undefined { + if (process.env.GITHUB_EVENT_PATH) { + try { + const event = JSON.parse( + readFileSync(process.env.GITHUB_EVENT_PATH, "utf8") + ) as { + pull_request?: { html_url?: string; number?: number }; + }; + const number = normalizePullRequestNumber(event.pull_request?.number); + const url = event.pull_request?.html_url + ? normalizeRepositoryUrl(event.pull_request.html_url) + : undefined; + if (number || url) { + return { number, url }; + } + } catch { + // Fall back to environment-derived metadata below. + } + } + + const refPullRequestNumber = + process.env.GITHUB_REF?.match(/^refs\/pull\/(\d+)\//)?.[1]; + const number = normalizePullRequestNumber(refPullRequestNumber); + if (!number || !process.env.GITHUB_REPOSITORY) { + return undefined; + } + + const githubServerUrl = process.env.GITHUB_SERVER_URL || "https://github.com"; + return { + number, + url: normalizeRepositoryUrl( + `${githubServerUrl.replace(/\/$/, "")}/${process.env.GITHUB_REPOSITORY}/pull/${number}` + ), + }; +} + +function getGitLabPullRequestMetadata(): PullRequestMetadata | undefined { + const number = normalizePullRequestNumber(process.env.CI_MERGE_REQUEST_IID); + const projectUrl = + process.env.CI_MERGE_REQUEST_PROJECT_URL || process.env.CI_PROJECT_URL; + if (!number || !projectUrl) { + return undefined; + } + + const normalizedProjectUrl = projectUrl + .replace(/\.git$/, "") + .replace(/\/$/, ""); + return { + number, + url: normalizeRepositoryUrl( + `${normalizedProjectUrl}/-/merge_requests/${number}` + ), + }; +} + +function getDirectPullRequestMetadata(): PullRequestMetadata | undefined { + const directUrl = + process.env.PULL_REQUEST_URL || + process.env.PR_URL || + process.env.CHANGE_URL || + process.env.CIRCLE_PULL_REQUEST; + const number = normalizePullRequestNumber( + process.env.PULL_REQUEST_NUMBER || + process.env.PR_NUMBER || + process.env.CHANGE_ID + ); + const url = directUrl ? normalizeRepositoryUrl(directUrl) : undefined; + + if (number || url) { + return { number, url }; + } + + return undefined; +} + +export function getPullRequestMetadata(): PullRequestMetadata | undefined { + return ( + getDirectPullRequestMetadata() ?? + getGitHubPullRequestMetadata() ?? + getGitLabPullRequestMetadata() + ); +} + export function resolveWorkerName( args: { workerName?: string; "worker-name"?: string }, config: Config diff --git a/packages/deploy-helpers/src/shared/types.ts b/packages/deploy-helpers/src/shared/types.ts index 48a80d91899..b43f62fb6d3 100644 --- a/packages/deploy-helpers/src/shared/types.ts +++ b/packages/deploy-helpers/src/shared/types.ts @@ -156,6 +156,11 @@ export type WorkerBuildResult = { export interface TriggerDeployment { targets: string[]; + customDomainTargets?: Array<{ + target: string; + enabled?: boolean; + previewsEnabled?: boolean; + }>; category?: string; resource?: string; error?: Error; diff --git a/packages/deploy-helpers/src/triggers/deploy.ts b/packages/deploy-helpers/src/triggers/deploy.ts index a9df364d071..d0dc17429d8 100644 --- a/packages/deploy-helpers/src/triggers/deploy.ts +++ b/packages/deploy-helpers/src/triggers/deploy.ts @@ -429,15 +429,10 @@ export async function triggersDeploy( const targets = completedDeployments .flatMap((deployment) => deployment.targets) - .map( - // Append protocol only on workers.dev domains - (target) => (target.endsWith("workers.dev") ? "https://" : "") + target - ); + .map(formatDeployTarget); if (targets.length > 0) { logger.log(`Deployed ${workerName} triggers`, formatTime(deployMs)); - for (const target of targets) { - logger.log(" ", target); - } + logDeployTargets(completedDeployments); } else { logger.log("No targets deployed for", workerName, formatTime(deployMs)); } @@ -530,6 +525,90 @@ function aggregateTelemetryMessages(errors: Error[]): string { return Array.from(new Set(labels)).sort().join(", "); } +function formatDeployTarget(target: string): string { + // Append protocol only on workers.dev domains + return (target.endsWith("workers.dev") ? "https://" : "") + target; +} + +function getCustomDomainTargetGroup(target: { + enabled?: boolean; + previewsEnabled?: boolean; +}): "Production and Preview" | "Production" | "Preview" | "Disabled" { + if (target.enabled === false && target.previewsEnabled) { + return "Preview"; + } + if (target.enabled === false) { + return "Disabled"; + } + if (target.previewsEnabled) { + return "Production and Preview"; + } + return "Production"; +} + +function logCustomDomainTargets(deployment: TriggerDeployment): void { + logger.log(""); + logger.log("Custom Domains:"); + + if (!deployment.customDomainTargets?.length) { + for (const target of deployment.targets.map(formatDeployTarget)) { + logger.log(" ", target); + } + return; + } + + const groups = new Map(); + for (const target of deployment.customDomainTargets) { + const group = getCustomDomainTargetGroup(target); + const targets = groups.get(group) ?? []; + targets.push(formatDeployTarget(target.target)); + groups.set(group, targets); + } + + let hasLoggedGroup = false; + for (const group of [ + "Production and Preview", + "Production", + "Preview", + "Disabled", + ]) { + const targets = groups.get(group); + if (!targets?.length) { + continue; + } + if (hasLoggedGroup) { + logger.log(""); + } + logger.log(` ${group}:`); + for (const target of targets) { + logger.log(" ", target); + } + hasLoggedGroup = true; + } +} + +function logDeployTargets(deployments: TriggerDeployment[]): void { + const hasCustomDomains = deployments.some( + (deployment) => + deployment.category === "Custom domains" && deployment.targets.length > 0 + ); + + for (const deployment of deployments) { + if (deployment.targets.length === 0) { + continue; + } + + if (hasCustomDomains && deployment.category === "Custom domains") { + logCustomDomainTargets(deployment); + continue; + } + + for (const target of deployment.targets.map(formatDeployTarget)) { + logger.log(" ", target); + } + } +} + // getSubdomainValues returns the values for workers_dev and preview_urls. // Defaults are computed at the API level. export function getSubdomainValues( diff --git a/packages/deploy-helpers/src/triggers/publish-routes.ts b/packages/deploy-helpers/src/triggers/publish-routes.ts index 8910a6f085d..3cfb3730075 100644 --- a/packages/deploy-helpers/src/triggers/publish-routes.ts +++ b/packages/deploy-helpers/src/triggers/publish-routes.ts @@ -51,32 +51,11 @@ export function renderRoute(route: Route): string { const isCustomDomain = Boolean( "custom_domain" in route && route.custom_domain ); - if (isCustomDomain && "zone_id" in route) { - result += ` (custom domain - zone id: ${route.zone_id})`; - } else if (isCustomDomain && "zone_name" in route) { - result += ` (custom domain - zone name: ${route.zone_name})`; - } else if (isCustomDomain) { - result += ` (custom domain)`; - } else if ("zone_id" in route) { + if (!isCustomDomain && "zone_id" in route) { result += ` (zone id: ${route.zone_id})`; - } else if ("zone_name" in route) { + } else if (!isCustomDomain && "zone_name" in route) { result += ` (zone name: ${route.zone_name})`; } - - if (isCustomDomain) { - const flags: string[] = []; - if ("enabled" in route && route.enabled !== undefined) { - flags.push(route.enabled ? "enabled" : "disabled"); - } - if ("previews_enabled" in route && route.previews_enabled !== undefined) { - flags.push( - route.previews_enabled ? "previews: enabled" : "previews: disabled" - ); - } - if (flags.length > 0) { - result += ` [${flags.join(", ")}]`; - } - } } return result; } @@ -351,5 +330,13 @@ Update them to point to this script instead?`; }, }); - return { targets: domains.map((domain) => renderRoute(domain)) }; + return { + targets: domains.map((domain) => renderRoute(domain)), + customDomainTargets: domains.map((domain) => ({ + target: renderRoute(domain), + enabled: "enabled" in domain ? domain.enabled : undefined, + previewsEnabled: + "previews_enabled" in domain ? domain.previews_enabled : undefined, + })), + }; } diff --git a/packages/wrangler/src/__tests__/deploy/routes.test.ts b/packages/wrangler/src/__tests__/deploy/routes.test.ts index 91f99cbe735..8dfad83b736 100644 --- a/packages/wrangler/src/__tests__/deploy/routes.test.ts +++ b/packages/wrangler/src/__tests__/deploy/routes.test.ts @@ -464,7 +464,10 @@ describe("deploy", () => { domains: [{ hostname: "api.example.com" }], }); await runWrangler("deploy ./index"); - expect(std.out).toContain("api.example.com (custom domain)"); + expect(std.out).toContain("Custom Domains:"); + expect(std.out).toContain("Production:"); + expect(std.out).toContain("api.example.com"); + expect(std.out).not.toContain("api.example.com (custom domain)"); }); it("should pass enabled and previews_enabled to the custom domains API", async ({ @@ -501,8 +504,11 @@ describe("deploy", () => { ], }); await runWrangler("deploy ./index"); - expect(std.out).toContain("api.example.com (custom domain)"); - expect(std.out).toContain("[enabled, previews: enabled]"); + expect(std.out).toContain("Custom Domains:"); + expect(std.out).toContain("Production and Preview:"); + expect(std.out).toContain("api.example.com"); + expect(std.out).not.toContain("api.example.com (custom domain)"); + expect(std.out).not.toContain("[enabled, previews: enabled]"); }); it("should confirm override if custom domain deploy would override an existing domain", async ({ @@ -558,7 +564,8 @@ Update them to point to this script instead?`, result: true, }); await runWrangler("deploy ./index"); - expect(std.out).toContain("api.example.com (custom domain)"); + expect(std.out).toContain("api.example.com"); + expect(std.out).not.toContain("api.example.com (custom domain)"); }); it("should confirm override if custom domain deploy contains a conflicting DNS record", async ({ @@ -604,7 +611,8 @@ Update them to point to this script instead?`, result: true, }); await runWrangler("deploy ./index"); - expect(std.out).toContain("api.example.com (custom domain)"); + expect(std.out).toContain("api.example.com"); + expect(std.out).not.toContain("api.example.com (custom domain)"); }); it("should confirm for conflicting custom domains and then again for conflicting dns", async ({ @@ -681,7 +689,8 @@ Update them to point to this script instead?`, } ); await runWrangler("deploy ./index"); - expect(std.out).toContain("api.example.com (custom domain)"); + expect(std.out).toContain("api.example.com"); + expect(std.out).not.toContain("api.example.com (custom domain)"); }); it("should throw if an invalid custom domain is requested", async ({ @@ -787,7 +796,9 @@ Update them to point to this script instead?`, }); await runWrangler("deploy ./index --domain api.example.com"); - expect(std.out).toContain("api.example.com (custom domain)"); + expect(std.out).toContain("Custom Domains:"); + expect(std.out).toContain("api.example.com"); + expect(std.out).not.toContain("api.example.com (custom domain)"); }); it("should deploy multiple domains passed via --domain flags", async ({ @@ -814,8 +825,11 @@ Update them to point to this script instead?`, await runWrangler( "deploy ./index --domain api.example.com --domain app.example.com" ); - expect(std.out).toContain("api.example.com (custom domain)"); - expect(std.out).toContain("app.example.com (custom domain)"); + expect(std.out).toContain("Custom Domains:"); + expect(std.out).toContain("api.example.com"); + expect(std.out).toContain("app.example.com"); + expect(std.out).not.toContain("api.example.com (custom domain)"); + expect(std.out).not.toContain("app.example.com (custom domain)"); }); it("should deploy --domain flags alongside routes (from config when no CLI routes)", async ({ @@ -875,7 +889,9 @@ Update them to point to this script instead?`, await runWrangler("deploy ./index --domain api.example.com"); expect(std.out).toContain("example.com/api/*"); - expect(std.out).toContain("api.example.com (custom domain)"); + expect(std.out).toContain("Custom Domains:"); + expect(std.out).toContain("api.example.com"); + expect(std.out).not.toContain("api.example.com (custom domain)"); }); it("should validate domain flags and reject invalid domains with wildcards", async ({ @@ -971,7 +987,9 @@ Update them to point to this script instead?`, "deploy ./index --route cli.com/override/* --domain api.example.com" ); expect(std.out).toContain("cli.com/override/*"); - expect(std.out).toContain("api.example.com (custom domain)"); + expect(std.out).toContain("Custom Domains:"); + expect(std.out).toContain("api.example.com"); + expect(std.out).not.toContain("api.example.com (custom domain)"); expect(std.out).not.toContain("config.com/api/*"); }); }); diff --git a/packages/wrangler/src/__tests__/preview.test.ts b/packages/wrangler/src/__tests__/preview.test.ts index 6165c3785b7..ce8a2b29cae 100644 --- a/packages/wrangler/src/__tests__/preview.test.ts +++ b/packages/wrangler/src/__tests__/preview.test.ts @@ -4,6 +4,8 @@ import { stripVTControlCharacters } from "node:util"; import { extractConfigBindings, getBranchName, + getPullRequestMetadata, + getRepositoryUrl, } from "@cloudflare/deploy-helpers"; import { defaultWranglerConfig } from "@cloudflare/workers-utils"; import { runInTempDir } from "@cloudflare/workers-utils/test-helpers"; @@ -37,6 +39,29 @@ function configWithPreviews(previews: PreviewsConfig): Config { }; } +function clearPreviewMetadataEnvs() { + vi.stubEnv("GITHUB_REPOSITORY", ""); + vi.stubEnv("GITHUB_SERVER_URL", ""); + vi.stubEnv("GITHUB_EVENT_PATH", ""); + vi.stubEnv("GITHUB_REF", ""); + vi.stubEnv("CI_PROJECT_URL", ""); + vi.stubEnv("CI_REPOSITORY_URL", ""); + vi.stubEnv("CI_MERGE_REQUEST_IID", ""); + vi.stubEnv("CI_MERGE_REQUEST_PROJECT_URL", ""); + vi.stubEnv("CIRCLE_REPOSITORY_URL", ""); + vi.stubEnv("CIRCLE_PULL_REQUEST", ""); + vi.stubEnv("BUILDKITE_REPO", ""); + vi.stubEnv("BITBUCKET_GIT_HTTP_ORIGIN", ""); + vi.stubEnv("BITBUCKET_GIT_SSH_ORIGIN", ""); + vi.stubEnv("REPOSITORY_URL", ""); + vi.stubEnv("PULL_REQUEST_URL", ""); + vi.stubEnv("PULL_REQUEST_NUMBER", ""); + vi.stubEnv("PR_URL", ""); + vi.stubEnv("PR_NUMBER", ""); + vi.stubEnv("CHANGE_URL", ""); + vi.stubEnv("CHANGE_ID", ""); +} + describe("wrangler preview", () => { const std = mockConsoleMethods(); runInTempDir(); @@ -84,6 +109,92 @@ describe("wrangler preview", () => { }); }); + describe("getRepositoryUrl", () => { + beforeEach(() => { + vi.unstubAllEnvs(); + clearPreviewMetadataEnvs(); + }); + + afterAll(() => { + vi.unstubAllEnvs(); + }); + + test("should use GitHub Actions repository env vars", ({ expect }) => { + vi.stubEnv("GITHUB_REPOSITORY", "cloudflare/workers-sdk"); + + expect(getRepositoryUrl()).toBe( + "https://github.com/cloudflare/workers-sdk" + ); + }); + + test("should use the first non-empty repository env var", ({ expect }) => { + vi.stubEnv("CI_PROJECT_URL", ""); + vi.stubEnv( + "CI_REPOSITORY_URL", + "git@git.example.com:acme/worker-project.git" + ); + + expect(getRepositoryUrl()).toBe( + "https://git.example.com/acme/worker-project" + ); + }); + }); + + describe("getPullRequestMetadata", () => { + beforeEach(() => { + vi.unstubAllEnvs(); + clearPreviewMetadataEnvs(); + }); + + afterAll(() => { + vi.unstubAllEnvs(); + }); + + test("should use direct pull request URL env vars", ({ expect }) => { + vi.stubEnv( + "PULL_REQUEST_URL", + "https://git.example.com/acme/worker-project/pulls/13" + ); + vi.stubEnv("PULL_REQUEST_NUMBER", "13"); + + expect(getPullRequestMetadata()).toEqual({ + number: "13", + url: "https://git.example.com/acme/worker-project/pulls/13", + }); + }); + + test("should use GitHub event pull request metadata", ({ expect }) => { + writeFileSync( + "github-event.json", + JSON.stringify({ + pull_request: { + number: 13, + html_url: "https://github.com/acme/worker-project/pull/13", + }, + }) + ); + vi.stubEnv("GITHUB_EVENT_PATH", "github-event.json"); + + expect(getPullRequestMetadata()).toEqual({ + number: "13", + url: "https://github.com/acme/worker-project/pull/13", + }); + }); + + test("should use GitLab merge request metadata", ({ expect }) => { + vi.stubEnv( + "CI_PROJECT_URL", + "https://gitlab.example.com/acme/worker-project" + ); + vi.stubEnv("CI_MERGE_REQUEST_IID", "13"); + + expect(getPullRequestMetadata()).toEqual({ + number: "13", + url: "https://gitlab.example.com/acme/worker-project/-/merge_requests/13", + }); + }); + }); + describe("extractConfigBindings", () => { test("should extract vars as plain_text bindings", ({ expect }) => { const config = configWithPreviews({ @@ -290,6 +401,7 @@ describe("wrangler preview", () => { describe("preview command", () => { beforeEach(() => { vi.stubEnv("CI", undefined); + clearPreviewMetadataEnvs(); mkdirSync("src", { recursive: true }); writeFileSync( "src/index.ts", @@ -411,10 +523,18 @@ describe("wrangler preview", () => { "/workers/workers/override-worker/previews/" ); expect(std.out).toContain("Preview: test-preview (new)"); - expect(std.out).toContain("Deployment:"); - expect(std.out).toContain("DEFAULT_VAR"); - expect(std.out).toContain('"from-defaults"'); - expect(std.out).toContain("◆ from wrangler.json"); + expect(std.out).toContain( + "Preview URL: https://test-preview.test-worker.cloudflare.app" + ); + expect(std.out).toContain("Deployment ID: deployment-id-123"); + expect(std.out).toContain( + "Deployment URL: https://abc12345.test-worker.cloudflare.app" + ); + expect(std.out).not.toContain("DEFAULT_VAR"); + expect(std.out).not.toContain('"from-defaults"'); + expect(std.out).not.toContain("◆ from wrangler.json"); + expect(std.out).not.toContain("Bindings"); + expect(std.out).not.toContain("observability"); }); test("should warn about top-level bindings missing from preview settings", async ({ @@ -662,7 +782,7 @@ describe("wrangler preview", () => { expect(deploymentRequestBody?.env?.FLAGS).not.toMatchObject({ app_id: "production-app-id", }); - expect(std.out).toContain("preview-app-id"); + expect(std.out).not.toContain("preview-app-id"); expect(std.warn).not.toContain("FLAGS"); }); @@ -818,7 +938,7 @@ describe("wrangler preview", () => { expect(std.out).toContain('"id": "preview-id-json"'); expect(std.out).toContain('"id": "deployment-id-json"'); expect(std.out).not.toContain("Preview: test-preview"); - expect(std.out).not.toContain("Deployment:"); + expect(std.out).not.toContain("Deployment ID:"); const outputEntries = readFileSync(outputFile, "utf8") .split("\n") @@ -839,6 +959,90 @@ describe("wrangler preview", () => { ); }); + test("should include and print CI pull request metadata", async ({ + expect, + }) => { + vi.stubEnv( + "CI_PROJECT_URL", + "https://gitlab.example.com/acme/worker-project.git" + ); + vi.stubEnv("CI_MERGE_REQUEST_IID", "13"); + + let deploymentRequestBody: + | (Record & { + annotations?: Record; + }) + | undefined; + + msw.use( + http.get( + `*/accounts/:accountId/workers/workers/:workerId/previews/:previewId`, + () => + HttpResponse.json( + { + success: false, + result: null, + errors: [{ code: 10025, message: "Preview not found" }], + }, + { status: 404 } + ) + ), + http.post( + `*/accounts/:accountId/workers/workers/:workerId/previews`, + () => + HttpResponse.json( + { + success: true, + result: { + id: "preview-id-annotations", + name: "test-preview", + slug: "test-preview", + urls: ["https://test-preview.test-worker.cloudflare.app"], + worker_name: "test-worker", + created_on: new Date().toISOString(), + }, + }, + { status: 201 } + ) + ), + http.post( + `*/accounts/:accountId/workers/workers/:workerId/previews/:previewId/deployments`, + async ({ request }) => { + deploymentRequestBody = + (await request.json()) as typeof deploymentRequestBody; + return HttpResponse.json( + { + success: true, + result: { + id: "deployment-id-annotations", + preview_id: "preview-id-annotations", + preview_name: "test-preview", + urls: ["https://annotations123.test-worker.cloudflare.app"], + compatibility_date: "2025-01-01", + env: {}, + created_on: new Date().toISOString(), + }, + }, + { status: 201 } + ); + } + ) + ); + + await runWrangler("preview --name test-preview"); + + expect(deploymentRequestBody?.annotations).toMatchObject({ + "workers/pull_request_number": "13", + "workers/pull_request_url": + "https://gitlab.example.com/acme/worker-project/-/merge_requests/13", + "workers/repository_url": + "https://gitlab.example.com/acme/worker-project", + }); + expect(std.out).toContain( + "Pull Request: https://gitlab.example.com/acme/worker-project/-/merge_requests/13" + ); + }); + test("should build correctly when using a redirected config", async ({ expect, }) => { @@ -1162,7 +1366,7 @@ describe("wrangler preview", () => { await runWrangler("preview --name test-preview"); expect(std.out).toContain("Preview: test-preview"); expect(std.out).toContain("(updated)"); - expect(std.out).toContain("Deployment:"); + expect(std.out).toContain("Deployment ID: deployment-id-456"); }); test("should use the URL-encoded preview name as the Preview identifier in path params", async ({ @@ -1299,10 +1503,12 @@ describe("wrangler preview", () => { ); await runWrangler("preview --name no-defaults-preview"); expect(std.out).toContain("Preview: no-defaults-preview (new)"); - expect(std.out).toContain("◆ from wrangler.json"); + expect(std.out).not.toContain("◆ from wrangler.json"); + expect(std.out).not.toContain("ENVIRONMENT"); + expect(std.out).not.toContain("MY_KV"); }); - test("should show observability settings when configured", async ({ + test("should not show observability settings in success output", async ({ expect, }) => { writeFileSync( @@ -1369,8 +1575,9 @@ describe("wrangler preview", () => { ) ); await runWrangler("preview --name test-preview"); - expect(std.out).toContain("observability"); - expect(std.out).toContain("enabled"); + expect(std.out).toContain("Preview: test-preview (new)"); + expect(std.out).not.toContain("observability"); + expect(std.out).not.toContain("logpush"); }); test("should include previews tail_consumers in the preview resource request", async ({ @@ -1458,9 +1665,15 @@ describe("wrangler preview", () => { ]); }); - test("should show compatibility_date when configured", async ({ + test("should use compatibility_date when configured", async ({ expect, }) => { + let deploymentRequestBody: + | { + compatibility_date?: string; + } + | undefined; + msw.use( http.get( `*/accounts/:accountId/workers/workers/:workerId/previews/:previewId`, @@ -1494,8 +1707,11 @@ describe("wrangler preview", () => { ), http.post( `*/accounts/:accountId/workers/workers/:workerId/previews/:previewId/deployments`, - () => - HttpResponse.json( + async ({ request }) => { + deploymentRequestBody = + (await request.json()) as typeof deploymentRequestBody; + + return HttpResponse.json( { success: true, result: { @@ -1509,12 +1725,13 @@ describe("wrangler preview", () => { }, }, { status: 201 } - ) + ); + } ) ); await runWrangler("preview --name test-preview"); - expect(std.out).toContain("compatibility_date"); - expect(std.out).toContain("2025-01-01"); + expect(deploymentRequestBody?.compatibility_date).toBe("2025-01-01"); + expect(std.out).not.toContain("compatibility_date"); }); test("should pass ignore_defaults query param when --ignore-defaults flag is used", async ({