Merge pull request #161 from cloudflare/cina/throw-on-failures

Refactor error handling to stop execution on errors
This commit is contained in:
Cina Saffary 2023-08-30 09:04:22 -05:00 committed by GitHub
commit 47d4afdbd8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 138 additions and 166 deletions

View file

@ -0,0 +1,5 @@
---
"wrangler-action": patch
---
Refactored error handling to stop execution when action fails. Previously, the action would continue executing to completion if one of the steps encountered an error. Fixes #160.

View file

@ -51,6 +51,7 @@ jobs:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
environment: dev environment: dev
preCommands: npx wrangler deploy --env dev # https://github.com/cloudflare/wrangler-action/issues/162
secrets: | secrets: |
SECRET1 SECRET1
SECRET2 SECRET2

View file

@ -1 +0,0 @@
process.env.INPUT_QUIET ??= "false";

View file

@ -25,8 +25,8 @@
"scripts": { "scripts": {
"build": "npx ncc build ./src/index.ts && mv ./dist/index.js ./dist/index.mjs", "build": "npx ncc build ./src/index.ts && mv ./dist/index.js ./dist/index.mjs",
"test": "vitest", "test": "vitest",
"format": "prettier --write . --ignore-path ./dist/**", "format": "prettier --write .",
"check": "prettier --check . --ignore-path ./dist/**" "check": "prettier --check ."
}, },
"dependencies": { "dependencies": {
"@actions/core": "^1.10.0" "@actions/core": "^1.10.0"

View file

@ -9,8 +9,7 @@ import {
getBooleanInput, getBooleanInput,
} from "@actions/core"; } from "@actions/core";
import { execSync, exec } from "node:child_process"; import { execSync, exec } from "node:child_process";
import { existsSync } from "node:fs"; import { checkWorkingDirectory, getNpxCmd, semverCompare } from "./utils";
import * as path from "node:path";
import * as util from "node:util"; import * as util from "node:util";
const execAsync = util.promisify(exec); const execAsync = util.promisify(exec);
@ -31,10 +30,6 @@ const config = {
QUIET_MODE: getBooleanInput("quiet"), QUIET_MODE: getBooleanInput("quiet"),
} as const; } as const;
function getNpxCmd() {
return process.env.RUNNER_OS === "Windows" ? "npx.cmd" : "npx";
}
function info(message: string, bypass?: boolean): void { function info(message: string, bypass?: boolean): void {
if (!config.QUIET_MODE || bypass) { if (!config.QUIET_MODE || bypass) {
originalInfo(message); originalInfo(message);
@ -59,34 +54,19 @@ function endGroup(): void {
} }
} }
/**
* A helper function to compare two semver versions. If the second arg is greater than the first arg, it returns true.
*/
function semverCompare(version1: string, version2: string) {
if (version2 === "latest") return true;
const version1Parts = version1.split(".");
const version2Parts = version2.split(".");
for (const version1Part of version1Parts) {
const version2Part = version2Parts.shift();
if (version1Part !== version2Part && version2Part) {
return version1Part < version2Part ? true : false;
}
}
return false;
}
async function main() { async function main() {
installWrangler(); try {
authenticationSetup(); installWrangler();
await execCommands(getMultilineInput("preCommands"), "pre"); authenticationSetup();
await uploadSecrets(); await execCommands(getMultilineInput("preCommands"), "pre");
await wranglerCommands(); await uploadSecrets();
await execCommands(getMultilineInput("postCommands"), "post"); await wranglerCommands();
info("🏁 Wrangler Action completed", true); await execCommands(getMultilineInput("postCommands"), "post");
info("🏁 Wrangler Action completed", true);
} catch (err: unknown) {
err instanceof Error && error(err.message);
setFailed("🚨 Action failed");
}
} }
async function runProcess( async function runProcess(
@ -103,29 +83,14 @@ async function runProcess(
} catch (err: any) { } catch (err: any) {
err.stdout && info(err.stdout.toString()); err.stdout && info(err.stdout.toString());
err.stderr && error(err.stderr.toString(), true); err.stderr && error(err.stderr.toString(), true);
throw err; throw new Error(`\`${command}\` returned non-zero exit code.`);
}
}
function checkWorkingDirectory(workingDirectory = ".") {
try {
const normalizedPath = path.normalize(workingDirectory);
if (existsSync(normalizedPath)) {
return normalizedPath;
} else {
setFailed(`🚨 Directory ${workingDirectory} does not exist.`);
}
} catch (error) {
setFailed(
`🚨 While checking/creating directory ${workingDirectory} received ${error}`,
);
} }
} }
function installWrangler() { function installWrangler() {
if (config["WRANGLER_VERSION"].startsWith("1")) { if (config["WRANGLER_VERSION"].startsWith("1")) {
setFailed( throw new Error(
`🚨 Wrangler v1 is no longer supported by this action. Please use major version 2 or greater`, `Wrangler v1 is no longer supported by this action. Please use major version 2 or greater`,
); );
} }
startGroup("📥 Installing Wrangler"); startGroup("📥 Installing Wrangler");
@ -145,27 +110,26 @@ async function execCommands(commands: string[], cmdType: string) {
if (!commands.length) { if (!commands.length) {
return; return;
} }
startGroup(`🚀 Running ${cmdType}Commands`); startGroup(`🚀 Running ${cmdType}Commands`);
try {
const arrPromises = commands.map(async (command) => {
const cmd = command.startsWith("wrangler")
? `${getNpxCmd()} ${command}`
: command;
const arrPromises = commands.map(async (command) => { info(`🚀 Executing command: ${cmd}`);
const cmd = command.startsWith("wrangler")
? `${getNpxCmd()} ${command}`
: command;
info(`🚀 Executing command: ${cmd}`); return await runProcess(cmd, {
cwd: config["workingDirectory"],
return await runProcess(cmd, { env: process.env,
cwd: config["workingDirectory"], });
env: process.env,
}); });
});
await Promise.all(arrPromises).catch((result) => { await Promise.all(arrPromises);
result.stdout && info(result.stdout.toString()); } finally {
result.stderr && error(result.stderr.toString(), true); endGroup();
setFailed(`🚨 ${cmdType}Commands failed`); }
});
endGroup();
} }
/** /**
@ -173,12 +137,12 @@ async function execCommands(commands: string[], cmdType: string) {
*/ */
function getSecret(secret: string) { function getSecret(secret: string) {
if (!secret) { if (!secret) {
setFailed("No secret provided"); throw new Error("Secret name cannot be blank.");
} }
const value = process.env[secret]; const value = process.env[secret];
if (!value) { if (!value) {
setFailed(`Secret ${secret} not found`); throw new Error(`Value for secret ${secret} not found.`);
} }
return value; return value;
@ -189,26 +153,22 @@ async function legacyUploadSecrets(
environment?: string, environment?: string,
workingDirectory?: string, workingDirectory?: string,
) { ) {
try { const arrPromises = secrets
const arrPromises = secrets .map((secret) => {
.map((secret) => { const command = `echo ${getSecret(
const command = `echo ${getSecret( secret,
secret, )} | ${getNpxCmd()} wrangler secret put ${secret}`;
)} | ${getNpxCmd()} wrangler secret put ${secret}`; return environment ? command.concat(` --env ${environment}`) : command;
return environment ? command.concat(` --env ${environment}`) : command; })
}) .map(
.map( async (command) =>
async (command) => await execAsync(command, {
await execAsync(command, { cwd: workingDirectory,
cwd: workingDirectory, env: process.env,
env: process.env, }),
}), );
);
await Promise.all(arrPromises); await Promise.all(arrPromises);
} catch {
setFailed(`🚨 Error uploading secrets`);
}
} }
async function uploadSecrets() { async function uploadSecrets() {
@ -219,9 +179,10 @@ async function uploadSecrets() {
if (!secrets.length) { if (!secrets.length) {
return; return;
} }
try {
startGroup("🔑 Uploading Secrets");
startGroup("🔑 Uploading secrets...");
try {
if (semverCompare(config["WRANGLER_VERSION"], "3.4.0")) if (semverCompare(config["WRANGLER_VERSION"], "3.4.0"))
return legacyUploadSecrets(secrets, environment, workingDirectory); return legacyUploadSecrets(secrets, environment, workingDirectory);
@ -244,9 +205,11 @@ async function uploadSecrets() {
env: process.env, env: process.env,
stdio: "ignore", stdio: "ignore",
}); });
info(`✅ Uploaded secrets`); info(`✅ Uploaded secrets`);
} catch { } catch (err) {
setFailed(`🚨 Error uploading secrets`); error(`❌ Upload failed`);
throw new Error(`Failed to upload secrets.`);
} finally { } finally {
endGroup(); endGroup();
} }
@ -258,7 +221,7 @@ function getVarArgs() {
if (process.env[envVar] && process.env[envVar]?.length !== 0) { if (process.env[envVar] && process.env[envVar]?.length !== 0) {
return `${envVar}:${process.env[envVar]!}`; return `${envVar}:${process.env[envVar]!}`;
} else { } else {
setFailed(`🚨 ${envVar} not found in variables.`); throw new Error(`Value for var ${envVar} not found in environment.`);
} }
}); });
@ -267,47 +230,45 @@ function getVarArgs() {
async function wranglerCommands() { async function wranglerCommands() {
startGroup("🚀 Running Wrangler Commands"); startGroup("🚀 Running Wrangler Commands");
const commands = config["COMMANDS"]; try {
const environment = config["ENVIRONMENT"]; const commands = config["COMMANDS"];
const environment = config["ENVIRONMENT"];
if (!commands.length) { if (!commands.length) {
const wranglerVersion = config["WRANGLER_VERSION"]; const wranglerVersion = config["WRANGLER_VERSION"];
const deployCommand = semverCompare("2.20.0", wranglerVersion) const deployCommand = semverCompare("2.20.0", wranglerVersion)
? "deploy" ? "deploy"
: "publish"; : "publish";
commands.push(deployCommand); commands.push(deployCommand);
}
const arrPromises = commands.map(async (command) => {
if (environment.length > 0 && !command.includes(`--env`)) {
command = command.concat(` --env ${environment}`);
} }
const cmd = `${getNpxCmd()} wrangler ${command} ${ const arrPromises = commands.map(async (command) => {
(command.startsWith("deploy") || command.startsWith("publish")) && if (environment.length > 0 && !command.includes(`--env`)) {
!command.includes(`--var`) command = command.concat(` --env ${environment}`);
? getVarArgs() }
: ""
}`.trim();
info(`🚀 Executing command: ${cmd}`); const cmd = `${getNpxCmd()} wrangler ${command} ${
(command.startsWith("deploy") || command.startsWith("publish")) &&
!command.includes(`--var`)
? getVarArgs()
: ""
}`.trim();
return await runProcess(cmd, { info(`🚀 Executing command: ${cmd}`);
cwd: config["workingDirectory"],
env: process.env, return await runProcess(cmd, {
cwd: config["workingDirectory"],
env: process.env,
});
}); });
});
await Promise.all(arrPromises).catch((result) => { await Promise.all(arrPromises);
result.stdout && info(result.stdout.toString()); } finally {
result.stderr && error(result.stderr.toString()); endGroup();
setFailed(`🚨 Command failed`); }
});
endGroup();
} }
main().catch(() => setFailed("🚨 Action failed")); main();
export { export {
wranglerCommands, wranglerCommands,
@ -315,7 +276,4 @@ export {
uploadSecrets, uploadSecrets,
authenticationSetup, authenticationSetup,
installWrangler, installWrangler,
checkWorkingDirectory,
getNpxCmd,
semverCompare,
}; };

View file

@ -1,18 +1,7 @@
import { expect, test, describe } from "vitest"; import { expect, test, describe } from "vitest";
import { checkWorkingDirectory, getNpxCmd, semverCompare } from "./index"; import { checkWorkingDirectory, getNpxCmd, semverCompare } from "./utils";
import path from "node:path"; import path from "node:path";
const config = {
WRANGLER_VERSION: "mockVersion",
secrets: ["mockSercret", "mockSecretAgain"],
workingDirectory: "./mockWorkingDirectory",
CLOUDFLARE_API_TOKEN: "mockAPIToken",
CLOUDFLARE_ACCOUNT_ID: "mockAccountID",
ENVIRONMENT: undefined,
VARS: ["mockVar", "mockVarAgain"],
COMMANDS: ["mockCommand", "mockCommandAgain"],
};
test("getNpxCmd ", async () => { test("getNpxCmd ", async () => {
process.env.RUNNER_OS = "Windows"; process.env.RUNNER_OS = "Windows";
expect(getNpxCmd()).toBe("npx.cmd"); expect(getNpxCmd()).toBe("npx.cmd");
@ -47,18 +36,10 @@ describe("checkWorkingDirectory", () => {
}); });
test("should fail if the directory does not exist", () => { test("should fail if the directory does not exist", () => {
try { expect(() =>
checkWorkingDirectory("/does/not/exist"); checkWorkingDirectory("/does/not/exist"),
} catch (error) { ).toThrowErrorMatchingInlineSnapshot(
expect(error.message).toMatchInlineSnapshot(); '"Directory /does/not/exist does not exist."',
} );
});
test("should fail if an error occurs while checking/creating the directory", () => {
try {
checkWorkingDirectory("/does/not/exist");
} catch (error) {
expect(error.message).toMatchInlineSnapshot();
}
}); });
}); });

35
src/utils.ts Normal file
View file

@ -0,0 +1,35 @@
import { existsSync } from "node:fs";
import * as path from "node:path";
export function getNpxCmd() {
return process.env.RUNNER_OS === "Windows" ? "npx.cmd" : "npx";
}
/**
* A helper function to compare two semver versions. If the second arg is greater than the first arg, it returns true.
*/
export function semverCompare(version1: string, version2: string) {
if (version2 === "latest") return true;
const version1Parts = version1.split(".");
const version2Parts = version2.split(".");
for (const version1Part of version1Parts) {
const version2Part = version2Parts.shift();
if (version1Part !== version2Part && version2Part) {
return version1Part < version2Part ? true : false;
}
}
return false;
}
export function checkWorkingDirectory(workingDirectory = ".") {
const normalizedPath = path.normalize(workingDirectory);
if (existsSync(normalizedPath)) {
return normalizedPath;
} else {
throw new Error(`Directory ${workingDirectory} does not exist.`);
}
}

View file

@ -9,12 +9,12 @@
"isolatedModules": true, "isolatedModules": true,
"target": "ESNext", "target": "ESNext",
"module": "ESNext", "module": "ESNext",
"moduleResolution": "NodeNext", "moduleResolution": "Bundler",
"rootDir": "./src", "rootDir": "./src",
"outDir": "./dist", "outDir": "./dist",
"lib": ["ESNext"], "lib": ["ESNext"],
"types": ["node", "@cloudflare/workers-types"] "types": ["node", "@cloudflare/workers-types"]
}, },
"exclude": ["node_modules", "**/*.test.ts"], "exclude": ["node_modules", "**/*.test.ts"],
"include": ["src/index.ts"] "include": ["src"]
} }

View file

@ -1,7 +0,0 @@
import { defineConfig } from "vitest/dist/config";
export default defineConfig({
test: {
setupFiles: ["./action-env-setup.ts"],
},
});