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"; import { createGitHubDeployment, createJobSummary } from "./github"; import { getOctokit } from "@actions/github"; 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(), GITHUB_TOKEN: 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); // create github deployment, if GITHUB_TOKEN is provided if ( config.GITHUB_TOKEN && pagesArtifactFields.production_branch && pagesArtifactFields.project_name && pagesArtifactFields.deployment_trigger && pagesArtifactFields.stages ) { const octokit = getOctokit(config.GITHUB_TOKEN); await Promise.all([ createGitHubDeployment({ config, octokit, deploymentUrl: pagesArtifactFields.url, productionBranch: pagesArtifactFields.production_branch, environment: pagesArtifactFields.environment, deploymentId: pagesArtifactFields.deployment_id, projectName: pagesArtifactFields.project_name, }), createJobSummary({ commitHash: pagesArtifactFields.deployment_trigger.metadata.commit_hash.substring( 0, 8, ), deploymentUrl: pagesArtifactFields.url, aliasUrl: pagesArtifactFields.alias, }), ]); } } 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, };