From 414f73cea1fe3d6773ffec58e828f1362f57b8f5 Mon Sep 17 00:00:00 2001 From: Maximo Guk <62088388+Maximo-Guk@users.noreply.github.com> Date: Sun, 9 Feb 2025 16:43:25 -0600 Subject: [PATCH] Use wrangler outputs for version upload and wrangler deploy --- .changeset/real-bananas-poke.md | 5 + src/commandOutputParsing.ts | 180 ++++++++++++++++++++++++++++ src/wranglerAction.ts | 69 +---------- src/wranglerArtifactManager.test.ts | 160 ++++++++++++++++++++----- src/wranglerArtifactManager.ts | 65 ++++++---- 5 files changed, 360 insertions(+), 119 deletions(-) create mode 100644 .changeset/real-bananas-poke.md create mode 100644 src/commandOutputParsing.ts diff --git a/.changeset/real-bananas-poke.md b/.changeset/real-bananas-poke.md new file mode 100644 index 00000000..82ebd14b --- /dev/null +++ b/.changeset/real-bananas-poke.md @@ -0,0 +1,5 @@ +--- +"wrangler-action": minor +--- + +Use wrangler outputs for version upload and wrangler deploy diff --git a/src/commandOutputParsing.ts b/src/commandOutputParsing.ts new file mode 100644 index 00000000..47381de8 --- /dev/null +++ b/src/commandOutputParsing.ts @@ -0,0 +1,180 @@ +import { setOutput } from "@actions/core"; +import { info, WranglerActionConfig } from "./wranglerAction"; +import { + getOutputEntry, + OutputEntryDeployment, + OutputEntryPagesDeployment, + OutputEntryVersionUpload, +} from "./wranglerArtifactManager"; +import { createGitHubDeploymentAndJobSummary } from "./service/github"; + +// fallback to trying to extract the deployment-url and pages-deployment-alias-url from stdout for wranglerVersion < 3.81.0 +function extractDeploymentUrlsFromStdout(stdOut: string): { + deploymentUrl?: string; + aliasUrl?: string; +} { + let deploymentUrl = ""; + let aliasUrl = ""; + + // Try to extract the deployment URL + const deploymentUrlMatch = stdOut.match(/https?:\/\/[a-zA-Z0-9-./]+/); + if (deploymentUrlMatch && deploymentUrlMatch[0]) { + deploymentUrl = deploymentUrlMatch[0].trim(); + } + + // And also try to extract the alias URL (since wrangler@3.78.0) + const aliasUrlMatch = stdOut.match(/alias URL: (https?:\/\/[a-zA-Z0-9-./]+)/); + if (aliasUrlMatch && aliasUrlMatch[1]) { + aliasUrl = aliasUrlMatch[1].trim(); + } + + return { deploymentUrl, aliasUrl }; +} + +async function handlePagesDeployOutputEntry( + config: WranglerActionConfig, + pagesDeployOutputEntry: OutputEntryPagesDeployment, +) { + setOutput("deployment-url", pagesDeployOutputEntry.url); + // DEPRECATED: deployment-alias-url in favour of pages-deployment-alias, drop in next wrangler-action major version change + setOutput("deployment-alias-url", pagesDeployOutputEntry.alias); + setOutput("pages-deployment-alias-url", pagesDeployOutputEntry.alias); + setOutput("pages-deployment-id", pagesDeployOutputEntry.deployment_id); + setOutput("pages-environment", pagesDeployOutputEntry.environment); + + // Create github deployment, if GITHUB_TOKEN is present in config + await createGitHubDeploymentAndJobSummary(config, pagesDeployOutputEntry); +} + +/** + * If no wrangler output file found, fallback to extracting deployment-url from stdout. + * @deprecated Use {@link handlePagesDeployOutputEntry} instead. + */ +function handlePagesDeployCommand( + config: WranglerActionConfig, + stdOut: string, +) { + info( + config, + "Unable to find a WRANGLER_OUTPUT_DIR, environment and id fields will be unavailable for output. Have you updated wrangler to version >=3.81.0?", + ); + // DEPRECATED: deployment-alias-url in favour of pages-deployment-alias, drop in next wrangler-action major version change + const { deploymentUrl, aliasUrl } = extractDeploymentUrlsFromStdout(stdOut); + + setOutput("deployment-url", deploymentUrl); + // DEPRECATED: deployment-alias-url in favour of pages-deployment-alias, drop in next wrangler-action major version change + setOutput("deployment-alias-url", aliasUrl); + setOutput("pages-deployment-alias-url", aliasUrl); +} + +function handleWranglerDeployOutputEntry( + config: WranglerActionConfig, + wranglerDeployOutputEntry: OutputEntryDeployment, +) { + // If no deployment urls found in wrangler output file, log that we couldn't find any urls and return. + if ( + !wranglerDeployOutputEntry.targets || + wranglerDeployOutputEntry.targets.length === 0 + ) { + info(config, "No deployment-url found in wrangler deploy output file"); + return; + } + + // If more than 1 deployment url found, log that we're going to set deployment-url to the first match. + // In a future wrangler-action version we should consider how we're going to output multiple deployment-urls + if (wranglerDeployOutputEntry.targets.length > 1) { + info( + config, + "Multiple deployment urls found in wrangler deploy output file, deployment-url will be set to the first url", + ); + } + + setOutput("deployment-url", wranglerDeployOutputEntry.targets[0]); +} + +/** + * If no wrangler output file found, fallback to extracting deployment-url from stdout. + * @deprecated Use {@link handleWranglerDeployOutputEntry} instead. + */ +function handleWranglerDeployCommand( + config: WranglerActionConfig, + stdOut: string, +) { + info( + config, + "Unable to find a WRANGLER_OUTPUT_DIR, deployment-url may have an unreliable output. Have you updated wrangler to version >=3.88.0?", + ); + const { deploymentUrl } = extractDeploymentUrlsFromStdout(stdOut); + setOutput("deployment-url", deploymentUrl); +} + +function handleVersionsOutputEntry( + versionsOutputEntry: OutputEntryVersionUpload, +) { + setOutput("deployment-url", versionsOutputEntry.preview_url); +} + +/** + * If no wrangler output file found, log a message stating deployment-url will be unavailable for output. + * @deprecated Use {@link handleVersionsOutputEntry} instead. + */ +function handleVersionsOutputCommand(config: WranglerActionConfig) { + info( + config, + "Unable to find a WRANGLER_OUTPUT_DIR, deployment-url will be unavailable for output. Have you updated wrangler to version >=3.88.0?", + ); +} + +function handleDeprectatedStdoutParsing( + config: WranglerActionConfig, + command: string, + stdOut: string, +) { + // Check if this command is a pages deployment + if ( + command.startsWith("pages deploy") || + command.startsWith("pages publish") + ) { + handlePagesDeployCommand(config, stdOut); + return; + } + + // Check if this command is a workers deployment + if (command.startsWith("deploy") || command.startsWith("publish")) { + handleWranglerDeployCommand(config, stdOut); + return; + } + + // Check if this command is a versions deployment + if (command.startsWith("versions")) { + handleVersionsOutputCommand(config); + return; + } +} + +export async function handleCommandOutputParsing( + config: WranglerActionConfig, + command: string, + stdOut: string, +) { + // get first OutputEntry found within wrangler artifact output directory + const outputEntry = await getOutputEntry(config.WRANGLER_OUTPUT_DIR); + + if (outputEntry === null) { + // if no outputEntry found, fallback to deprecated stdOut parsing + handleDeprectatedStdoutParsing(config, command, stdOut); + return; + } + + switch (outputEntry.type) { + case "pages-deploy-detailed": + await handlePagesDeployOutputEntry(config, outputEntry); + break; + case "deploy": + handleWranglerDeployOutputEntry(config, outputEntry); + break; + case "version-upload": + handleVersionsOutputEntry(outputEntry); + break; + } +} diff --git a/src/wranglerAction.ts b/src/wranglerAction.ts index a12e4ad8..82a42eef 100644 --- a/src/wranglerAction.ts +++ b/src/wranglerAction.ts @@ -12,8 +12,7 @@ import { z } from "zod"; import { exec, execShell } from "./exec"; import { PackageManager } from "./packageManagers"; import { error, info, semverCompare } from "./utils"; -import { getDetailedPagesDeployOutput } from "./wranglerArtifactManager"; -import { createGitHubDeploymentAndJobSummary } from "./service/github"; +import { handleCommandOutputParsing } from "./commandOutputParsing"; export type WranglerActionConfig = z.infer; export const wranglerActionConfig = z.object({ @@ -294,29 +293,6 @@ async function uploadSecrets( } } -// fallback to trying to extract the deployment-url and pages-deployment-alias-url from stdout for wranglerVersion < 3.81.0 -function extractDeploymentUrlsFromStdout(stdOut: string): { - deploymentUrl?: string; - aliasUrl?: string; -} { - let deploymentUrl = ""; - let aliasUrl = ""; - - // Try to extract the deployment URL - const deploymentUrlMatch = stdOut.match(/https?:\/\/[a-zA-Z0-9-./]+/); - if (deploymentUrlMatch && deploymentUrlMatch[0]) { - deploymentUrl = deploymentUrlMatch[0].trim(); - } - - // And also try to extract the alias URL (since wrangler@3.78.0) - const aliasUrlMatch = stdOut.match(/alias URL: (https?:\/\/[a-zA-Z0-9-./]+)/); - if (aliasUrlMatch && aliasUrlMatch[1]) { - aliasUrl = aliasUrlMatch[1].trim(); - } - - return { deploymentUrl, aliasUrl }; -} - async function wranglerCommands( config: WranglerActionConfig, packageManager: PackageManager, @@ -379,47 +355,8 @@ async function wranglerCommands( setOutput("command-output", stdOut); setOutput("command-stderr", stdErr); - // Check if this command is a workers deployment - if (command.startsWith("deploy") || command.startsWith("publish")) { - const { deploymentUrl } = extractDeploymentUrlsFromStdout(stdOut); - setOutput("deployment-url", deploymentUrl); - } - // Check if this command is a pages deployment - if ( - command.startsWith("pages publish") || - command.startsWith("pages deploy") - ) { - const pagesArtifactFields = await getDetailedPagesDeployOutput( - config.WRANGLER_OUTPUT_DIR, - ); - - if (pagesArtifactFields) { - setOutput("deployment-url", pagesArtifactFields.url); - // DEPRECATED: deployment-alias-url in favour of pages-deployment-alias, drop in next wrangler-action major version change - setOutput("deployment-alias-url", pagesArtifactFields.alias); - setOutput("pages-deployment-alias-url", pagesArtifactFields.alias); - setOutput("pages-deployment-id", pagesArtifactFields.deployment_id); - setOutput("pages-environment", pagesArtifactFields.environment); - // Create github deployment, if GITHUB_TOKEN is present in config - await createGitHubDeploymentAndJobSummary( - config, - pagesArtifactFields, - ); - } else { - info( - config, - "Unable to find a WRANGLER_OUTPUT_DIR, environment and id fields will be unavailable for output. Have you updated wrangler to version >=3.81.0?", - ); - // DEPRECATED: deployment-alias-url in favour of pages-deployment-alias, drop in next wrangler-action major version change - const { deploymentUrl, aliasUrl } = - extractDeploymentUrlsFromStdout(stdOut); - - setOutput("deployment-url", deploymentUrl); - // DEPRECATED: deployment-alias-url in favour of pages-deployment-alias, drop in next wrangler-action major version change - setOutput("deployment-alias-url", aliasUrl); - setOutput("pages-deployment-alias-url", aliasUrl); - } - } + // Handles setting github action outputs and creating github deployment and job summary + await handleCommandOutputParsing(config, command, stdOut); } } finally { endGroup(config); diff --git a/src/wranglerArtifactManager.test.ts b/src/wranglerArtifactManager.test.ts index cff15d90..962bb337 100644 --- a/src/wranglerArtifactManager.test.ts +++ b/src/wranglerArtifactManager.test.ts @@ -1,7 +1,7 @@ import mockfs from "mock-fs"; import { afterEach, describe, expect, it } from "vitest"; import { - getDetailedPagesDeployOutput, + getOutputEntry, getWranglerArtifacts, } from "./wranglerArtifactManager"; @@ -36,50 +36,148 @@ describe("wranglerArtifactsManager", () => { }); }); - describe("getDetailedPagesDeployOutput()", async () => { - it("Returns only detailed pages deploy output from wrangler artifacts", async () => { - mockfs({ - testOutputDir: { - "wrangler-output-2024-10-17_18-48-40_463-2e6e83.json": ` - {"version": 1, "type":"wrangler-session", "wrangler_version":"3.81.0", "command_line_args":["what's up"], "log_file_path": "/here"} - {"version": 1, "type":"pages-deploy-detailed", "pages_project": "project", "environment":"production", "alias":"test.com", "deployment_id": "123", "url":"url.com"}`, - "not-wrangler-output.json": "test", - }, - }); - - const artifacts = await getDetailedPagesDeployOutput("./testOutputDir"); - - expect(artifacts).toEqual({ - version: 1, - pages_project: "project", - type: "pages-deploy-detailed", - url: "url.com", - environment: "production", - deployment_id: "123", - alias: "test.com", - }); - }), - it("Skips artifact entries that are not parseable", async () => { + describe("getOutputEntry()", async () => { + describe("OutputEntryPagesDeployment", async () => { + it("Returns only detailed pages deploy output from wrangler artifacts", async () => { mockfs({ testOutputDir: { "wrangler-output-2024-10-17_18-48-40_463-2e6e83.json": ` - this line is invalid json. - {"version": 1, "type":"pages-deploy-detailed", "pages_project": "project", "environment":"production", "alias":"test.com", "deployment_id": "123", "url":"url.com"}`, + {"version": 1, "type":"wrangler-session", "wrangler_version":"3.81.0", "command_line_args":["what's up"], "log_file_path": "/here"} + {"version": 1, "type":"pages-deploy-detailed", "pages_project": "project", "environment":"production", "alias":"test.com", "deployment_id": "123", "url":"url.com"}`, "not-wrangler-output.json": "test", }, }); - const artifacts = await getDetailedPagesDeployOutput("./testOutputDir"); + const artifact = await getOutputEntry("./testOutputDir"); + if (artifact?.type !== "pages-deploy-detailed") { + throw new Error(`Unexpected type ${artifact?.type}`); + } - expect(artifacts).toEqual({ + expect(artifact).toEqual({ version: 1, - type: "pages-deploy-detailed", pages_project: "project", + type: "pages-deploy-detailed", url: "url.com", environment: "production", deployment_id: "123", alias: "test.com", }); - }); + }), + it("Skips artifact entries that are not parseable", async () => { + mockfs({ + testOutputDir: { + "wrangler-output-2024-10-17_18-48-40_463-2e6e83.json": ` + this line is invalid json. + {"version": 1, "type":"pages-deploy-detailed", "pages_project": "project", "environment":"production", "alias":"test.com", "deployment_id": "123", "url":"url.com"}`, + "not-wrangler-output.json": "test", + }, + }); + + const artifact = await getOutputEntry("./testOutputDir"); + if (artifact?.type !== "pages-deploy-detailed") { + throw new Error(`Unexpected type ${artifact?.type}`); + } + + expect(artifact).toEqual({ + version: 1, + type: "pages-deploy-detailed", + pages_project: "project", + url: "url.com", + environment: "production", + deployment_id: "123", + alias: "test.com", + }); + }); + }); + + describe("OutputEntryDeployment", async () => { + it("Returns only wrangler deploy output from wrangler artifacts", async () => { + mockfs({ + testOutputDir: { + "wrangler-output-2024-10-17_18-48-40_463-2e6e83.json": ` + {"version": 1, "type":"wrangler-session", "wrangler_version":"3.81.0", "command_line_args":["what's up"], "log_file_path": "/here"} + {"version": 1, "type":"deploy", "targets": ["https://example.com"]}`, + "not-wrangler-output.json": "test", + }, + }); + + const artifact = await getOutputEntry("./testOutputDir"); + if (artifact?.type !== "deploy") { + throw new Error(`Unexpected type ${artifact?.type}`); + } + + expect(artifact).toEqual({ + version: 1, + type: "deploy", + targets: ["https://example.com"], + }); + }), + it("Skips artifact entries that are not parseable", async () => { + mockfs({ + testOutputDir: { + "wrangler-output-2024-10-17_18-48-40_463-2e6e83.json": ` + this line is invalid json. + {"version": 1, "type":"deploy", "targets": ["https://example.com"]}`, + "not-wrangler-output.json": "test", + }, + }); + + const artifact = await getOutputEntry("./testOutputDir"); + if (artifact?.type !== "deploy") { + throw new Error(`Unexpected type ${artifact?.type}`); + } + + expect(artifact).toEqual({ + version: 1, + type: "deploy", + targets: ["https://example.com"], + }); + }); + }); + + describe("OutputEntryVersionUpload", async () => { + it("Returns only version upload output from wrangler artifacts", async () => { + mockfs({ + testOutputDir: { + "wrangler-output-2024-10-17_18-48-40_463-2e6e83.json": ` + {"version": 1, "type":"wrangler-session", "wrangler_version":"3.81.0", "command_line_args":["what's up"], "log_file_path": "/here"} + {"version": 1, "type":"version-upload", "preview_url": "https://example.com"}`, + "not-wrangler-output.json": "test", + }, + }); + + const artifact = await getOutputEntry("./testOutputDir"); + if (artifact?.type !== "version-upload") { + throw new Error(`Unexpected type ${artifact?.type}`); + } + + expect(artifact).toEqual({ + version: 1, + type: "version-upload", + preview_url: "https://example.com", + }); + }), + it("Skips artifact entries that are not parseable", async () => { + mockfs({ + testOutputDir: { + "wrangler-output-2024-10-17_18-48-40_463-2e6e83.json": ` + this line is invalid json. + {"version": 1, "type":"version-upload", "preview_url": "https://example.com"}`, + "not-wrangler-output.json": "test", + }, + }); + + const artifact = await getOutputEntry("./testOutputDir"); + if (artifact?.type !== "version-upload") { + throw new Error(`Unexpected type ${artifact?.type}`); + } + + expect(artifact).toEqual({ + version: 1, + type: "version-upload", + preview_url: "https://example.com", + }); + }); + }); }); }); diff --git a/src/wranglerArtifactManager.ts b/src/wranglerArtifactManager.ts index a6b144d7..4ef7d8ea 100644 --- a/src/wranglerArtifactManager.ts +++ b/src/wranglerArtifactManager.ts @@ -6,6 +6,7 @@ const OutputEntryBase = z.object({ type: z.string(), }); +export type OutputEntryPagesDeployment = z.infer; const OutputEntryPagesDeployment = OutputEntryBase.merge( z.object({ type: z.literal("pages-deploy-detailed"), @@ -28,9 +29,31 @@ const OutputEntryPagesDeployment = OutputEntryBase.merge( }), ); -export type OutputEntryPagesDeployment = z.infer< - typeof OutputEntryPagesDeployment ->; +export type OutputEntryDeployment = z.infer; +const OutputEntryDeployment = OutputEntryBase.merge( + z.object({ + type: z.literal("deploy"), + /** A list of URLs that represent the HTTP triggers associated with this deployment */ + /** basically, for wrangler-action purposes this is the deployment urls */ + targets: z.array(z.string()).optional(), + }), +); + +export type OutputEntryVersionUpload = z.infer; +const OutputEntryVersionUpload = OutputEntryBase.merge( + z.object({ + type: z.literal("version-upload"), + /** The preview URL associated with this version upload */ + preview_url: z.string().optional(), + }), +); + +export type SupportedOutputEntry = z.infer; +const SupportedOutputEntry = z.discriminatedUnion("type", [ + OutputEntryPagesDeployment, + OutputEntryDeployment, + OutputEntryVersionUpload, +]); /** * Parses file names in a directory to find wrangler artifact files @@ -65,34 +88,32 @@ export async function getWranglerArtifacts( } /** - * Searches for detailed wrangler output from a pages deploy + * Searches for a supported wrangler OutputEntry * * @param artifactDirectory - * @returns The first pages-output-detailed found within a wrangler artifact directory + * @returns The first SupportedOutputEntry found within a wrangler artifact directory */ -export async function getDetailedPagesDeployOutput( +export async function getOutputEntry( artifactDirectory: string, -): Promise { +): Promise { const artifactFilePaths = await getWranglerArtifacts(artifactDirectory); - for (let i = 0; i < artifactFilePaths.length; i++) { - const file = await open(artifactFilePaths[i], "r"); - - for await (const line of file.readLines()) { - try { - const output = JSON.parse(line); - const parsedOutput = OutputEntryPagesDeployment.parse(output); - if (parsedOutput.type === "pages-deploy-detailed") { - // Assume, in the context of the action, the first detailed deploy instance seen will suffice - return parsedOutput; + for (const filePath of artifactFilePaths) { + const file = await open(filePath, "r"); + try { + for await (const line of file.readLines()) { + try { + // Attempt to parse and validate the JSON line against the union schema. + // Assume, in the context of the action, the first OutputEntry seen will suffice + return SupportedOutputEntry.parse(JSON.parse(line)); + } catch { + // Skip lines that are invalid JSON or don't match any schema. + continue; } - } catch (err) { - // If the line can't be parsed, skip it - continue; } + } finally { + await file.close(); } - - await file.close(); } return null;