diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index a68b5a9..06dc178 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -14,7 +14,8 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v3 with: - node-version: "latest" + # Pinned due to compatibility issues on 23.2.0 + node-version: "22" cache: "npm" - name: Install modules and build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 30009a2..82054bf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,7 +27,8 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v3 with: - node-version: "latest" + # Pinned due to compatibility issues on 23.2.0 + node-version: "22" cache: "npm" - name: Install modules diff --git a/src/index.ts b/src/index.ts index fbe86f4..24f7cfd 100755 --- a/src/index.ts +++ b/src/index.ts @@ -1,30 +1,16 @@ -import { - debug, - getBooleanInput, - getInput, - getMultilineInput, - endGroup as originalEndGroup, - error as originalError, - info as originalInfo, - startGroup as originalStartGroup, - setFailed, - setOutput, -} from "@actions/core"; -import { getExecOutput } from "@actions/exec"; -import semverEq from "semver/functions/eq"; -import { exec, execShell } from "./exec"; -import { getPackageManager } from "./packageManagers"; -import { checkWorkingDirectory, semverCompare } from "./utils"; -import { getDetailedPagesDeployOutput } from "./wranglerArtifactManager"; -import { join } from "path"; +import { getBooleanInput, getInput, getMultilineInput } from "@actions/core"; import { tmpdir } from "os"; +import { join } from "path"; +import { getPackageManager } from "./packageManagers"; +import { checkWorkingDirectory } from "./utils"; +import { main, WranglerActionConfig } from "./wranglerAction"; const DEFAULT_WRANGLER_VERSION = "3.81.0"; /** * A configuration object that contains all the inputs & immutable state for the action. */ -const config = { +const config: WranglerActionConfig = { WRANGLER_VERSION: getInput("wranglerVersion") || DEFAULT_WRANGLER_VERSION, didUserProvideWranglerVersion: Boolean(getInput("wranglerVersion")), secrets: getMultilineInput("secrets"), @@ -46,376 +32,4 @@ const packageManager = getPackageManager(config.PACKAGE_MANAGER, { workingDirectory: config.workingDirectory, }); -function info(message: string, bypass?: boolean): void { - if (!config.QUIET_MODE || bypass) { - originalInfo(message); - } -} - -function error(message: string, bypass?: boolean): void { - if (!config.QUIET_MODE || bypass) { - originalError(message); - } -} - -function startGroup(name: string): void { - if (!config.QUIET_MODE) { - originalStartGroup(name); - } -} - -function endGroup(): void { - if (!config.QUIET_MODE) { - originalEndGroup(); - } -} - -async function main() { - try { - authenticationSetup(); - await installWrangler(); - await execCommands(getMultilineInput("preCommands"), "pre"); - await uploadSecrets(); - await wranglerCommands(); - await execCommands(getMultilineInput("postCommands"), "post"); - info("🏁 Wrangler Action completed", true); - } catch (err: unknown) { - err instanceof Error && error(err.message); - setFailed("🚨 Action failed"); - } -} - -async function installWrangler() { - if (config["WRANGLER_VERSION"].startsWith("1")) { - throw new Error( - `Wrangler v1 is no longer supported by this action. Please use major version 2 or greater`, - ); - } - - startGroup("🔍 Checking for existing Wrangler installation"); - let installedVersion = ""; - let installedVersionSatisfiesRequirement = false; - try { - const { stdout } = await getExecOutput( - // We want to simply invoke wrangler to check if it's installed, but don't want to auto-install it at this stage - packageManager.execNoInstall, - ["wrangler", "--version"], - { - cwd: config["workingDirectory"], - silent: config.QUIET_MODE, - }, - ); - // There are two possible outputs from `wrangler --version`: - // ` ⛅️ wrangler 3.48.0 (update available 3.53.1)` - // and - // `3.48.0` - const versionMatch = - stdout.match(/wrangler (\d+\.\d+\.\d+)/) ?? - stdout.match(/^(\d+\.\d+\.\d+)/m); - if (versionMatch) { - installedVersion = versionMatch[1]; - } - if (config.didUserProvideWranglerVersion) { - installedVersionSatisfiesRequirement = semverEq( - installedVersion, - config["WRANGLER_VERSION"], - ); - } - if (!config.didUserProvideWranglerVersion && installedVersion) { - info( - `✅ No wrangler version specified, using pre-installed wrangler version ${installedVersion}`, - true, - ); - endGroup(); - return; - } - if ( - config.didUserProvideWranglerVersion && - installedVersionSatisfiesRequirement - ) { - info(`✅ Using Wrangler ${installedVersion}`, true); - endGroup(); - return; - } - info( - "⚠️ Wrangler not found or version is incompatible. Installing...", - true, - ); - } catch (error) { - debug(`Error checking Wrangler version: ${error}`); - info( - "⚠️ Wrangler not found or version is incompatible. Installing...", - true, - ); - } finally { - endGroup(); - } - - startGroup("📥 Installing Wrangler"); - try { - await exec( - packageManager.install, - [`wrangler@${config["WRANGLER_VERSION"]}`], - { - cwd: config["workingDirectory"], - silent: config["QUIET_MODE"], - }, - ); - - info(`✅ Wrangler installed`, true); - } finally { - endGroup(); - } -} - -function authenticationSetup() { - process.env.CLOUDFLARE_API_TOKEN = config["CLOUDFLARE_API_TOKEN"]; - process.env.CLOUDFLARE_ACCOUNT_ID = config["CLOUDFLARE_ACCOUNT_ID"]; -} - -async function execCommands(commands: string[], cmdType: string) { - if (!commands.length) { - return; - } - - startGroup(`🚀 Running ${cmdType}Commands`); - try { - for (const command of commands) { - const cmd = command.startsWith("wrangler") - ? `${packageManager.exec} ${command}` - : command; - - await execShell(cmd, { - cwd: config["workingDirectory"], - silent: config["QUIET_MODE"], - }); - } - } finally { - endGroup(); - } -} - -function getSecret(secret: string) { - if (!secret) { - throw new Error("Secret name cannot be blank."); - } - - const value = process.env[secret]; - if (!value) { - throw new Error(`Value for secret ${secret} not found in environment.`); - } - - return value; -} - -function getEnvVar(envVar: string) { - if (!envVar) { - throw new Error("Var name cannot be blank."); - } - - const value = process.env[envVar]; - if (!value) { - throw new Error(`Value for var ${envVar} not found in environment.`); - } - - return value; -} - -async function legacyUploadSecrets( - secrets: string[], - environment?: string, - workingDirectory?: string, -) { - for (const secret of secrets) { - const args = ["wrangler", "secret", "put", secret]; - if (environment) { - args.push("--env", environment); - } - await exec(packageManager.exec, args, { - cwd: workingDirectory, - silent: config["QUIET_MODE"], - input: Buffer.from(getSecret(secret)), - }); - } -} - -async function uploadSecrets() { - const secrets: string[] = config["secrets"]; - const environment = config["ENVIRONMENT"]; - const workingDirectory = config["workingDirectory"]; - - if (!secrets.length) { - return; - } - - startGroup("🔑 Uploading secrets..."); - - try { - if (semverCompare(config["WRANGLER_VERSION"], "3.4.0")) { - return legacyUploadSecrets(secrets, environment, workingDirectory); - } - - const args = ["wrangler", "secret:bulk"]; - - if (environment) { - args.push("--env", environment); - } - - await exec(packageManager.exec, args, { - cwd: workingDirectory, - silent: config["QUIET_MODE"], - input: Buffer.from( - JSON.stringify( - Object.fromEntries( - secrets.map((secret) => [secret, getSecret(secret)]), - ), - ), - ), - }); - } catch (err: unknown) { - if (err instanceof Error) { - error(err.message); - err.stack && debug(err.stack); - } - throw new Error(`Failed to upload secrets.`); - } finally { - endGroup(); - } -} - -// 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() { - startGroup("🚀 Running Wrangler Commands"); - try { - const commands = config["COMMANDS"]; - const environment = config["ENVIRONMENT"]; - - if (!commands.length) { - const wranglerVersion = config["WRANGLER_VERSION"]; - const deployCommand = semverCompare("2.20.0", wranglerVersion) - ? "deploy" - : "publish"; - commands.push(deployCommand); - } - - for (let command of commands) { - const args = []; - - if (environment && !command.includes("--env")) { - args.push("--env", environment); - } - - if ( - config["VARS"].length && - (command.startsWith("deploy") || command.startsWith("publish")) && - !command.includes("--var") - ) { - args.push("--var"); - for (const v of config["VARS"]) { - args.push(`${v}:${getEnvVar(v)}`); - } - } - - // Used for saving the wrangler output - let stdOut = ""; - let stdErr = ""; - - // set WRANGLER_OUTPUT_FILE_DIRECTORY env for exec - process.env.WRANGLER_OUTPUT_FILE_DIRECTORY = config.WRANGLER_OUTPUT_DIR; - - const options = { - cwd: config["workingDirectory"], - silent: config["QUIET_MODE"], - listeners: { - stdout: (data: Buffer) => { - stdOut += data.toString(); - }, - stderr: (data: Buffer) => { - stdErr += data.toString(); - }, - }, - }; - - // Execute the wrangler command - await exec(`${packageManager.exec} wrangler ${command}`, args, options); - - // Set the outputs for the command - 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, 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); - } - // 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); - } else { - info( - "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); - } - } - } - } finally { - endGroup(); - } -} - -main(); - -export { - authenticationSetup, - execCommands, - installWrangler, - uploadSecrets, - wranglerCommands, -}; +main(config, packageManager); diff --git a/src/packageManagers.ts b/src/packageManagers.ts index b91386b..c3d219b 100644 --- a/src/packageManagers.ts +++ b/src/packageManagers.ts @@ -1,7 +1,7 @@ import { existsSync } from "node:fs"; import * as path from "node:path"; -interface PackageManager { +export interface PackageManager { install: string; exec: string; execNoInstall: string; diff --git a/src/wranglerAction.test.ts b/src/wranglerAction.test.ts new file mode 100644 index 0000000..b3d8c2a --- /dev/null +++ b/src/wranglerAction.test.ts @@ -0,0 +1,125 @@ +import * as core from "@actions/core"; +import * as exec from "@actions/exec"; +import { describe, expect, it, vi } from "vitest"; +import { installWrangler } from "./wranglerAction"; + +describe("installWrangler", () => { + const testPackageManager = { + install: "npm i", + exec: "npx", + execNoInstall: "npx --no-install", + }; + + it("Errors on unsupported wrangler version", async () => { + const testConfig = { + WRANGLER_VERSION: "1", + didUserProvideWranglerVersion: false, + secrets: [], + workingDirectory: "/test", + CLOUDFLARE_API_TOKEN: "foo", + CLOUDFLARE_ACCOUNT_ID: "bar", + ENVIRONMENT: "dev", + VARS: [], + COMMANDS: [], + QUIET_MODE: false, + PACKAGE_MANAGER: "npm", + WRANGLER_OUTPUT_DIR: "/tmp/wranglerArtifacts", + }; + await expect( + installWrangler(testConfig, testPackageManager), + ).rejects.toThrowError( + `Wrangler v1 is no longer supported by this action. Please use major version 2 or greater`, + ); + }); + + it("Does nothing if no wrangler version is specified and wrangler is already installed", async () => { + const testConfig = { + WRANGLER_VERSION: "3.81.0", + didUserProvideWranglerVersion: false, + secrets: [], + workingDirectory: "/test", + CLOUDFLARE_API_TOKEN: "foo", + CLOUDFLARE_ACCOUNT_ID: "bar", + ENVIRONMENT: "dev", + VARS: [], + COMMANDS: [], + QUIET_MODE: false, + PACKAGE_MANAGER: "npm", + WRANGLER_OUTPUT_DIR: "/tmp/wranglerArtifacts", + }; + vi.spyOn(exec, "getExecOutput").mockImplementation( + async (commandLine: string, args?: string[]) => { + return { + exitCode: 0, + stderr: "", + stdout: ` ⛅️ wrangler 3.48.0 (update available 3.53.1)`, + }; + }, + ); + const infoSpy = vi.spyOn(core, "info"); + await installWrangler(testConfig, testPackageManager); + expect(infoSpy).toBeCalledWith( + "✅ No wrangler version specified, using pre-installed wrangler version 3.48.0", + ); + }); + + it("Does nothing if the wrangler version specified is the same as the one installed", async () => { + const testConfig = { + WRANGLER_VERSION: "3.48.0", + didUserProvideWranglerVersion: true, + secrets: [], + workingDirectory: "/test", + CLOUDFLARE_API_TOKEN: "foo", + CLOUDFLARE_ACCOUNT_ID: "bar", + ENVIRONMENT: "dev", + VARS: [], + COMMANDS: [], + QUIET_MODE: false, + PACKAGE_MANAGER: "npm", + WRANGLER_OUTPUT_DIR: "/tmp/wranglerArtifacts", + }; + vi.spyOn(exec, "getExecOutput").mockImplementation( + async (commandLine: string, args?: string[]) => { + return { + exitCode: 0, + stderr: "", + stdout: ` ⛅️ wrangler 3.48.0 (update available 3.53.1)`, + }; + }, + ); + const infoSpy = vi.spyOn(core, "info"); + await installWrangler(testConfig, testPackageManager); + expect(infoSpy).toBeCalledWith("✅ Using Wrangler 3.48.0"); + }); + it("Should install wrangler if the version specified is not already available", async () => { + const testConfig = { + WRANGLER_VERSION: "3.48.0", + didUserProvideWranglerVersion: true, + secrets: [], + workingDirectory: "/test", + CLOUDFLARE_API_TOKEN: "foo", + CLOUDFLARE_ACCOUNT_ID: "bar", + ENVIRONMENT: "dev", + VARS: [], + COMMANDS: [], + QUIET_MODE: false, + PACKAGE_MANAGER: "npm", + WRANGLER_OUTPUT_DIR: "/tmp/wranglerArtifacts", + }; + vi.spyOn(exec, "getExecOutput").mockImplementation( + async (commandLine: string, args?: string[]) => { + return { + exitCode: 0, + stderr: "", + stdout: ` ⛅️ wrangler 3.20.0 (update available 3.53.1)`, + }; + }, + ); + vi.spyOn(exec, "exec").mockImplementation(async (commandLine: string) => { + return 0; + }); + const infoSpy = vi.spyOn(core, "info"); + await installWrangler(testConfig, testPackageManager); + expect(infoSpy).toBeCalledWith("✅ Wrangler installed"); + }); +}); diff --git a/src/wranglerAction.ts b/src/wranglerAction.ts new file mode 100644 index 0000000..a41c6c8 --- /dev/null +++ b/src/wranglerAction.ts @@ -0,0 +1,456 @@ +import { + debug, + getMultilineInput, + endGroup as originalEndGroup, + error as originalError, + info as originalInfo, + startGroup as originalStartGroup, + setFailed, + setOutput, +} from "@actions/core"; +import { getExecOutput } from "@actions/exec"; +import semverEq from "semver/functions/eq"; +import { z } from "zod"; +import { exec, execShell } from "./exec"; +import { PackageManager } from "./packageManagers"; +import { semverCompare } from "./utils"; +import { getDetailedPagesDeployOutput } from "./wranglerArtifactManager"; + +export type WranglerActionConfig = z.infer; +export const wranglerActionConfig = z.object({ + WRANGLER_VERSION: z.string(), + didUserProvideWranglerVersion: z.boolean(), + secrets: z.array(z.string()), + workingDirectory: z.string(), + CLOUDFLARE_API_TOKEN: z.string(), + CLOUDFLARE_ACCOUNT_ID: z.string(), + ENVIRONMENT: z.string(), + VARS: z.array(z.string()), + COMMANDS: z.array(z.string()), + QUIET_MODE: z.boolean(), + PACKAGE_MANAGER: z.string(), + WRANGLER_OUTPUT_DIR: z.string(), +}); + +function info( + config: WranglerActionConfig, + message: string, + bypass?: boolean, +): void { + if (!config.QUIET_MODE || bypass) { + originalInfo(message); + } +} + +function error( + config: WranglerActionConfig, + message: string, + bypass?: boolean, +): void { + if (!config.QUIET_MODE || bypass) { + originalError(message); + } +} + +function startGroup(config: WranglerActionConfig, name: string): void { + if (!config.QUIET_MODE) { + originalStartGroup(name); + } +} + +function endGroup(config: WranglerActionConfig): void { + if (!config.QUIET_MODE) { + originalEndGroup(); + } +} + +async function main( + config: WranglerActionConfig, + packageManager: PackageManager, +) { + try { + wranglerActionConfig.parse(config); + authenticationSetup(config); + await installWrangler(config, packageManager); + await execCommands( + config, + packageManager, + getMultilineInput("preCommands"), + "pre", + ); + await uploadSecrets(config, packageManager); + await wranglerCommands(config, packageManager); + await execCommands( + config, + packageManager, + getMultilineInput("postCommands"), + "post", + ); + info(config, "🏁 Wrangler Action completed", true); + } catch (err: unknown) { + err instanceof Error && error(config, err.message); + setFailed("🚨 Action failed"); + } +} + +async function installWrangler( + config: WranglerActionConfig, + packageManager: PackageManager, +) { + if (config["WRANGLER_VERSION"].startsWith("1")) { + throw new Error( + `Wrangler v1 is no longer supported by this action. Please use major version 2 or greater`, + ); + } + + startGroup(config, "🔍 Checking for existing Wrangler installation"); + let installedVersion = ""; + let installedVersionSatisfiesRequirement = false; + try { + const { stdout } = await getExecOutput( + // We want to simply invoke wrangler to check if it's installed, but don't want to auto-install it at this stage + packageManager.execNoInstall, + ["wrangler", "--version"], + { + cwd: config["workingDirectory"], + silent: config.QUIET_MODE, + }, + ); + + // There are two possible outputs from `wrangler --version`: + // ` ⛅️ wrangler 3.48.0 (update available 3.53.1)` + // and + // `3.48.0` + const versionMatch = + stdout.match(/wrangler (\d+\.\d+\.\d+)/) ?? + stdout.match(/^(\d+\.\d+\.\d+)/m); + if (versionMatch) { + installedVersion = versionMatch[1]; + } + if (config.didUserProvideWranglerVersion) { + installedVersionSatisfiesRequirement = semverEq( + installedVersion, + config["WRANGLER_VERSION"], + ); + } + if (!config.didUserProvideWranglerVersion && installedVersion) { + info( + config, + `✅ No wrangler version specified, using pre-installed wrangler version ${installedVersion}`, + true, + ); + endGroup(config); + return; + } + if ( + config.didUserProvideWranglerVersion && + installedVersionSatisfiesRequirement + ) { + info(config, `✅ Using Wrangler ${installedVersion}`, true); + endGroup(config); + return; + } + info( + config, + "⚠️ Wrangler not found or version is incompatible. Installing...", + true, + ); + } catch (error) { + debug(`Error checking Wrangler version: ${error}`); + info( + config, + "⚠️ Wrangler not found or version is incompatible. Installing...", + true, + ); + } finally { + endGroup(config); + } + + startGroup(config, "📥 Installing Wrangler"); + try { + await exec( + packageManager.install, + [`wrangler@${config["WRANGLER_VERSION"]}`], + { + cwd: config["workingDirectory"], + silent: config["QUIET_MODE"], + }, + ); + + info(config, `✅ Wrangler installed`, true); + } finally { + endGroup(config); + } +} + +function authenticationSetup(config: WranglerActionConfig) { + process.env.CLOUDFLARE_API_TOKEN = config["CLOUDFLARE_API_TOKEN"]; + process.env.CLOUDFLARE_ACCOUNT_ID = config["CLOUDFLARE_ACCOUNT_ID"]; +} + +async function execCommands( + config: WranglerActionConfig, + packageManager: PackageManager, + commands: string[], + cmdType: string, +) { + if (!commands.length) { + return; + } + + startGroup(config, `🚀 Running ${cmdType}Commands`); + try { + for (const command of commands) { + const cmd = command.startsWith("wrangler") + ? `${packageManager.exec} ${command}` + : command; + + await execShell(cmd, { + cwd: config["workingDirectory"], + silent: config["QUIET_MODE"], + }); + } + } finally { + endGroup(config); + } +} + +function getSecret(secret: string) { + if (!secret) { + throw new Error("Secret name cannot be blank."); + } + + const value = process.env[secret]; + if (!value) { + throw new Error(`Value for secret ${secret} not found in environment.`); + } + + return value; +} + +function getEnvVar(envVar: string) { + if (!envVar) { + throw new Error("Var name cannot be blank."); + } + + const value = process.env[envVar]; + if (!value) { + throw new Error(`Value for var ${envVar} not found in environment.`); + } + + return value; +} + +async function legacyUploadSecrets( + config: WranglerActionConfig, + packageManager: PackageManager, + secrets: string[], + environment?: string, + workingDirectory?: string, +) { + for (const secret of secrets) { + const args = ["wrangler", "secret", "put", secret]; + if (environment) { + args.push("--env", environment); + } + await exec(packageManager.exec, args, { + cwd: workingDirectory, + silent: config["QUIET_MODE"], + input: Buffer.from(getSecret(secret)), + }); + } +} + +async function uploadSecrets( + config: WranglerActionConfig, + packageManager: PackageManager, +) { + const secrets: string[] = config["secrets"]; + const environment = config["ENVIRONMENT"]; + const workingDirectory = config["workingDirectory"]; + + if (!secrets.length) { + return; + } + + startGroup(config, "🔑 Uploading secrets..."); + + try { + if (semverCompare(config["WRANGLER_VERSION"], "3.4.0")) { + return legacyUploadSecrets( + config, + packageManager, + secrets, + environment, + workingDirectory, + ); + } + + const args = ["wrangler", "secret:bulk"]; + + if (environment) { + args.push("--env", environment); + } + + await exec(packageManager.exec, args, { + cwd: workingDirectory, + silent: config["QUIET_MODE"], + input: Buffer.from( + JSON.stringify( + Object.fromEntries( + secrets.map((secret) => [secret, getSecret(secret)]), + ), + ), + ), + }); + } catch (err: unknown) { + if (err instanceof Error) { + error(config, err.message); + err.stack && debug(err.stack); + } + throw new Error(`Failed to upload secrets.`); + } finally { + endGroup(config); + } +} + +// 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, +) { + startGroup(config, "🚀 Running Wrangler Commands"); + try { + const commands = config["COMMANDS"]; + const environment = config["ENVIRONMENT"]; + + if (!commands.length) { + const wranglerVersion = config["WRANGLER_VERSION"]; + const deployCommand = semverCompare("2.20.0", wranglerVersion) + ? "deploy" + : "publish"; + commands.push(deployCommand); + } + + for (let command of commands) { + const args = []; + + if (environment && !command.includes("--env")) { + args.push("--env", environment); + } + + if ( + config["VARS"].length && + (command.startsWith("deploy") || command.startsWith("publish")) && + !command.includes("--var") + ) { + args.push("--var"); + for (const v of config["VARS"]) { + args.push(`${v}:${getEnvVar(v)}`); + } + } + + // Used for saving the wrangler output + let stdOut = ""; + let stdErr = ""; + + // set WRANGLER_OUTPUT_FILE_DIRECTORY env for exec + process.env.WRANGLER_OUTPUT_FILE_DIRECTORY = config.WRANGLER_OUTPUT_DIR; + + const options = { + cwd: config["workingDirectory"], + silent: config["QUIET_MODE"], + listeners: { + stdout: (data: Buffer) => { + stdOut += data.toString(); + }, + stderr: (data: Buffer) => { + stdErr += data.toString(); + }, + }, + }; + + // Execute the wrangler command + await exec(`${packageManager.exec} wrangler ${command}`, args, options); + + // Set the outputs for the command + 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, 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); + } + // 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); + } 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); + } + } + } + } finally { + endGroup(config); + } +} + +export { + authenticationSetup, + execCommands, + info, + installWrangler, + main, + uploadSecrets, + wranglerCommands, +};