Repository: pulumi/actions
Pull request #813 (2022-12-28 22:30:17)

refactor: improve config


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3092ae0..05b9280 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,7 @@
 ## HEAD (Unreleased)
 
 - chore: add dist files to gitignore and forcebly commit them instead
+- refactor: Improve config
 
 --
 
diff --git a/action.yml b/action.yml
index e0bf63c..ebb98b9 100644
--- a/action.yml
+++ b/action.yml
@@ -11,6 +11,10 @@ inputs:
   stack-name:
     description: 'Which stack you want to apply to, eg. dev'
     required: false
+  pulumi-version:
+    description: 'Install a specific version of the Pulumi CLI'
+    required: false
+    default: '^3'
   work-dir:
     description: 'Location of your Pulumi files. Defaults to ./'
     required: false
@@ -22,7 +26,6 @@ inputs:
   comment-on-pr-number:
     description: 'Overrides the PR used to comment on'
     required: false
-    default: '0'
   github-token:
     description: 'Github Token'
     required: false
@@ -30,29 +33,26 @@ inputs:
   cloud-url:
     description: 'A cloud URL to log in to'
     required: false
-    default: ''
   secrets-provider:
     description: 'The type of the provider that should be used to encrypt and decrypt secrets. Possible choices: default, passphrase, awskms, azurekeyvault, gcpkms, hashivault'
     required: false
-    default: ''
   parallel:
     description: 'Allow P resource operations to run in parallel at once (1 for no parallelism). Defaults to unbounded.'
     required: false
-    default: '2147483647'
   message:
     description: 'Optional message to associate with the update operation'
     required: false
-    default: ''
   config-map:
     description: 'Config to use during the operations'
     required: false
-    default: ''
   expect-no-changes:
     description: 'Return an error if any changes occur during this update'
     required: false
+    default: 'false'
   diff:
     description: 'Display operation as a rich diff showing the overall change'
     required: false
+    default: 'false'
   replace:
     description: 'Specify resources to replace. Multiple resources can be specified one per line'
     required: false
@@ -85,10 +85,6 @@ inputs:
     description: 'Edit previous PR comment instead of posting new one'
     required: false
     default: 'true'
-  pulumi-version:
-    description: 'Install a specific version of the Pulumi CLI'
-    required: false
-    default: '^3'
   color:
     description: 'Colorize output. Choices are: always, never, raw, auto'
     required: false
diff --git a/package.json b/package.json
index 25c35d4..bcd3a84 100644
--- a/package.json
+++ b/package.json
@@ -48,13 +48,13 @@
     "@actions/io": "^1.1.3",
     "@actions/tool-cache": "^2.0.1",
     "@pulumi/pulumi": "3.62.0",
+    "actions-parsers": "^1.0.2",
     "dedent": "^0.7.0",
     "envalid": "^7.3.1",
     "got": "^11.8.6",
     "runtypes": "^6.6.0",
     "semver": "^7.3.8",
     "ts-invariant": "^0.10.3",
-    "typescript": "~4.1.3",
-    "yaml": "~2.0.0"
+    "typescript": "4.5"
   }
 }
diff --git a/src/config.ts b/src/config.ts
index 5471d4a..23e12fb 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -1,17 +1,13 @@
-import { getInput } from '@actions/core';
-import { context } from '@actions/github';
+import {
+  getBooleanInput,
+  getInput,
+  getMultilineInput,
+  getNumberInput,
+  getUnionInput,
+  getYAMLInput,
+} from 'actions-parsers';
 import * as rt from 'runtypes';
-import { parseArray, parseBoolean, parseNumber } from './libs/utils';
-
-export const command = rt.Union(
-  rt.Literal('destroy'),
-  rt.Literal('preview'),
-  rt.Literal('refresh'),
-  rt.Literal('up'),
-  rt.Literal('update'),
-);
-
-export type Commands = rt.Static<typeof command>;
+import { parseSemicolorToArray } from './libs/utils';
 
 // installationConfig is the expected Action inputs when
 // the user intends to download the Pulumi CLI without
@@ -27,81 +23,60 @@ export type InstallationConfig = rt.Static<typeof installationConfig>;
 export function makeInstallationConfig(): rt.Result<InstallationConfig> {
   return installationConfig.validate({
     command: getInput('command') || undefined,
-    pulumiVersion: getInput('pulumi-version') || "^3",
+    pulumiVersion: getInput('pulumi-version') || '^3',
   });
 }
 
-export const options = rt.Partial({
-  parallel: rt.Number,
-  message: rt.String,
-  expectNoChanges: rt.Boolean,
-  diff: rt.Boolean,
-  replace: rt.Array(rt.String),
-  target: rt.Array(rt.String),
-  policyPacks: rt.Array(rt.String),
-  policyPackConfigs: rt.Array(rt.String),
-  targetDependents: rt.Boolean,
-  editCommentOnPr: rt.Boolean,
-  userAgent: rt.Literal('pulumi/actions@v4'),
-  pulumiVersion: rt.String,
-});
+const configValueRt = rt.Dictionary(
+  rt.Record({
+    value: rt.String,
+    secret: rt.Boolean.optional(),
+  }),
+  rt.String,
+);
 
-export const config = rt
-  .Record({
-    // Required options
-    command: command,
-    stackName: rt.String,
-    workDir: rt.String,
-    commentOnPr: rt.Boolean,
-    options: options,
-    // Information inferred from the environment that must be present
-    isPullRequest: rt.Boolean,
-  })
-  .And(
-    rt.Partial({
-      // Optional options
-      cloudUrl: rt.String,
-      configMap: rt.String,
-      githubToken: rt.String,
-      upsert: rt.Boolean,
-      remove: rt.Boolean,
-      refresh: rt.Boolean,
-      secretsProvider: rt.String,
-      commentOnPrNumber: rt.Number,
+// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+export function makeConfig() {
+  return {
+    command: getUnionInput('command', {
+      required: true,
+      alternatives: ['up', 'update', 'refresh', 'destroy', 'preview'] as const,
     }),
-  );
-
-export type Config = rt.Static<typeof config>;
-
-export async function makeConfig(): Promise<Config> {
-  return config.check({
-    command: getInput('command', { required: true }),
     stackName: getInput('stack-name', { required: true }),
-    workDir: getInput('work-dir') || './',
+    pulumiVersion: getInput('pulumi-version', { required: true }),
+    workDir: getInput('work-dir', { required: true }),
     secretsProvider: getInput('secrets-provider'),
     cloudUrl: getInput('cloud-url'),
     githubToken: getInput('github-token'),
-    commentOnPr: parseBoolean(getInput('comment-on-pr')),
-    commentOnPrNumber: parseNumber(getInput('comment-on-pr-number')),
-    upsert: parseBoolean(getInput('upsert')),
-    remove: parseBoolean(getInput('remove')),
-    refresh: parseBoolean(getInput('refresh')),
-    configMap: getInput('config-map'),
-    isPullRequest: context?.payload?.pull_request !== undefined,
+    commentOnPr: getBooleanInput('comment-on-pr'),
+    commentOnPrNumber: getNumberInput('comment-on-pr-number', {}),
+    upsert: getBooleanInput('upsert'),
+    remove: getBooleanInput('remove'),
+    refresh: getBooleanInput('refresh'),
+    configMap: getYAMLInput('config-map', {
+      parser: (configMap) => configValueRt.check(configMap),
+    }),
+    editCommentOnPr: getBooleanInput('edit-pr-comment'),
+
     options: {
-      parallel: parseNumber(getInput('parallel')),
+      parallel: getNumberInput('parallel', {}),
       message: getInput('message'),
-      expectNoChanges: parseBoolean(getInput('expect-no-changes')),
-      diff: parseBoolean(getInput('diff')),
-      replace: parseArray(getInput('replace')),
-      target: parseArray(getInput('target')),
-      targetDependents: parseBoolean(getInput('target-dependents')),
-      policyPacks: parseArray(getInput('policyPacks')),
-      policyPackConfigs: parseArray(getInput('policyPackConfigs')),
-      editCommentOnPr: parseBoolean(getInput('edit-pr-comment')),
-      userAgent: 'pulumi/actions@v4',
-      pulumiVersion: getInput('pulumi-version') || "^3",
-      color: getInput('color'),
+      expectNoChanges: getBooleanInput('expect-no-changes'),
+      diff: getBooleanInput('diff'),
+      replace: parseSemicolorToArray(getMultilineInput('replace')),
+      target: parseSemicolorToArray(getMultilineInput('target')),
+      targetDependents: getBooleanInput('target-dependents'),
+      policyPacks: parseSemicolorToArray(getMultilineInput('policyPacks')),
+      policyPackConfigs: parseSemicolorToArray(
+        getMultilineInput('policyPackConfigs'),
+      ),
+      userAgent: 'pulumi/actions@v3',
+      color: getUnionInput('color', {
+        alternatives: ['always', 'never', 'raw', 'auto'] as const,
+      }),
     },
-  });
+  };
 }
+
+export type Config = ReturnType<typeof makeConfig>;
+export type Commands = Config['command'];
diff --git a/src/libs/pr.ts b/src/libs/pr.ts
index dbbafce..e0098a8 100644
--- a/src/libs/pr.ts
+++ b/src/libs/pr.ts
@@ -13,7 +13,7 @@ export async function handlePullRequestMessage(
     githubToken,
     command,
     stackName,
-    options: { editCommentOnPr },
+    editCommentOnPr,
   } = config;
 
   const heading = `#### :tropical_drink: \`${command}\` on ${projectName}/${stackName}`;
diff --git a/src/libs/utils.ts b/src/libs/utils.ts
index 6053118..a150cdb 100644
--- a/src/libs/utils.ts
+++ b/src/libs/utils.ts
@@ -1,24 +1,15 @@
-export function parseArray(input: string): string[] {
-  return parseUndefined(input)
-    ? input.split(/\r?\n/).reduce<string[]>(
-        (acc, line) =>
-          acc
-            .concat(line.split(','))
-            .filter((pat) => pat)
-            .map((pat) => pat.trim()),
-        [],
-      )
-    : undefined;
-}
-
-export function parseUndefined(input: string): string | undefined {
-  return input === undefined || input === '' ? undefined : input;
-}
-
-export function parseBoolean(input: string): boolean | undefined {
-  return parseUndefined(input) ? input === 'true' : undefined;
-}
+export function parseSemicolorToArray(input: string[]): string[];
+export function parseSemicolorToArray(input?: string[]): undefined | string[] {
+  if (!input) {
+    return undefined;
+  }
 
-export function parseNumber(input: string): number | undefined {
-  return parseUndefined(input) ? Number(input) : undefined;
+  return input.reduce<string[]>(
+    (acc, line) =>
+      acc
+        .concat(line.split(','))
+        .filter((x) => x !== '')
+        .map((x) => x.trim()),
+    [],
+  );
 }
diff --git a/src/main.ts b/src/main.ts
index ac745fc..24f48e5 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,14 +1,19 @@
 import { resolve } from 'path';
 import * as core from '@actions/core';
+import { context } from '@actions/github';
 import {
-  ConfigMap,
   LocalProgramArgs,
   LocalWorkspace,
   LocalWorkspaceOptions,
 } from '@pulumi/pulumi/automation';
 import invariant from 'ts-invariant';
-import YAML from 'yaml';
-import { Commands, Config, InstallationConfig, makeConfig, makeInstallationConfig } from './config';
+import {
+  Commands,
+  Config,
+  InstallationConfig,
+  makeConfig,
+  makeInstallationConfig,
+} from './config';
 import { environmentVariables } from './libs/envs';
 import { handlePullRequestMessage } from './libs/pr';
 import * as pulumiCli from './libs/pulumi-cli';
@@ -18,7 +23,7 @@ const main = async () => {
   const downloadConfig = makeInstallationConfig();
   if (downloadConfig.success) {
     await installOnly(downloadConfig.value);
-    core.info("Pulumi has been successfully installed. Exiting.");
+    core.info('Pulumi has been successfully installed. Exiting.');
     return;
   }
 
@@ -33,10 +38,10 @@ const main = async () => {
 // intends to install the Pulumi CLI without running additional commands.
 const installOnly = async (config: InstallationConfig): Promise<void> => {
   await pulumiCli.downloadCli(config.pulumiVersion);
-}
+};
 
 const runAction = async (config: Config): Promise<void> => {
-  await pulumiCli.downloadCli(config.options.pulumiVersion);
+  await pulumiCli.downloadCli(config.pulumiVersion);
   await login(config.cloudUrl, environmentVariables.PULUMI_ACCESS_TOKEN);
 
   const workDir = resolve(
@@ -67,9 +72,8 @@ const runAction = async (config: Config): Promise<void> => {
     core.info(msg);
   };
 
-  if (config.configMap != '') {
-    const configMap: ConfigMap = YAML.parse(config.configMap);
-    await stack.setAllConfig(configMap);
+  if (config.configMap) {
+    await stack.setAllConfig(config.configMap);
   }
 
   if (config.refresh) {
@@ -111,13 +115,13 @@ const runAction = async (config: Config): Promise<void> => {
     }
   }
 
-  if (
-    config.commentOnPrNumber ||
-    (config.commentOnPr && config.isPullRequest)
-  ) {
-    core.debug(`Commenting on pull request`);
-    invariant(config.githubToken, 'github-token is missing.');
-    handlePullRequestMessage(config, projectName, output);
+  if (config.commentOnPrNumber || config.commentOnPr) {
+    const isPullRequest = context.payload.pull_request !== undefined;
+    if (isPullRequest) {
+      core.debug(`Commenting on pull request`);
+      invariant(config.githubToken, 'github-token is missing.');
+      handlePullRequestMessage(config, projectName, output);
+    }
   }
 
   if (config.remove && config.command === 'destroy') {
diff --git a/yarn.lock b/yarn.lock
index a9d4465..17f6d7e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1276,6 +1276,13 @@ acorn@^8.2.4:
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30"
   integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==
 
+actions-parsers@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/actions-parsers/-/actions-parsers-1.0.2.tgz#104c014ecc89c6403428dc88995aa8abd0d2d6c3"
+  integrity sha512-v5e1cDI2Ut11VRO6s+/vSKyVD1K0XZA167J20A/4JaKOw5jTy/fME0Kr2QOwi4ECrMJ29gi5lrkyc0CDMbiqIQ==
+  dependencies:
+    js-yaml "^4.1.0"
+
 agent-base@6:
   version "6.0.2"
   resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
@@ -1380,6 +1387,11 @@ argparse@^1.0.7:
   dependencies:
     sprintf-js "~1.0.2"
 
+argparse@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
+  integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
+
 arr-diff@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
@@ -3895,6 +3907,13 @@ js-yaml@^3.13.1, js-yaml@^3.14.0:
     argparse "^1.0.7"
     esprima "^4.0.0"
 
+js-yaml@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
+  integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
+  dependencies:
+    argparse "^2.0.1"
+
 jsdom@^16.4.0:
   version "16.7.0"
   resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.7.0.tgz#918ae71965424b197c819f8183a754e18977b710"
@@ -5690,16 +5709,16 @@ typedarray-to-buffer@^3.1.5:
   dependencies:
     is-typedarray "^1.0.0"
 
+typescript@4.5:
+  version "4.5.5"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.5.tgz#d8c953832d28924a9e3d37c73d729c846c5896f3"
+  integrity sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==
+
 typescript@~3.8.3:
   version "3.8.3"
   resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061"
   integrity sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==
 
-typescript@~4.1.3:
-  version "4.1.6"
-  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.6.tgz#1becd85d77567c3c741172339e93ce2e69932138"
-  integrity sha512-pxnwLxeb/Z5SP80JDRzVjh58KsM6jZHRAOtTpS7sXLS4ogXNKC9ANxHHZqLLeVHZN35jCtI4JdmLLbLiC1kBow==
-
 unbox-primitive@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e"
@@ -5987,11 +6006,6 @@ yaml@^2.2.1:
   resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.2.1.tgz#3014bf0482dcd15147aa8e56109ce8632cd60ce4"
   integrity sha512-e0WHiYql7+9wr4cWMx3TVQrNwejKaEe7/rHNmQmqRjazfOP5W8PB6Jpebb5o6fIapbz9o9+2ipcaTM2ZwDI6lw==
 
-yaml@~2.0.0:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.0.1.tgz#71886d6021f3da28169dbefde78d4dd0f8d83650"
-  integrity sha512-1NpAYQ3wjzIlMs0mgdBmYzLkFgWBIWrzYVDYfrixhoFNNgJ444/jT2kUT2sicRbJES3oQYRZugjB6Ro8SjKeFg==
-
 yargs-parser@20.x, yargs-parser@^20.2.2:
   version "20.2.9"
   resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"


Test patch:
diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts
index 02d5689..e3333b4 100644
--- a/src/__tests__/config.test.ts
+++ b/src/__tests__/config.test.ts
@@ -1,3 +1,5 @@
+import { makeConfig } from '../config';
+
 const defaultConfig: Record<string, string> = {
   command: 'up',
   'stack-name': 'dev',
@@ -5,29 +7,30 @@ const defaultConfig: Record<string, string> = {
   'cloud-url': 'file://~',
   'github-token': 'n/a',
   'pulumi-version': '^3',
+  'comment-on-pr': 'false',
+  upsert: 'false',
+  remove: 'false',
+  refresh: 'false',
+  'edit-pr-comment': 'false',
+  'expect-no-changes': 'false',
+  diff: 'false',
+  'target-dependents': 'false',
 };
 
+function setupMockedConfig(config: Record<string, string>) {
+  Object.entries(config).forEach(([key, value]) => {
+    process.env[`INPUT_${key.replace(/ /g, '_').toUpperCase()}`] = value;
+  });
+}
+
 describe('config.ts', () => {
   beforeEach(() => {
     jest.resetModules();
   });
   it('should validate a configuration', async () => {
-    const config = {
-      ...defaultConfig,
-      'comment-on-pr': 'false',
-    };
-    jest.mock('@actions/core', () => ({
-      getInput: jest.fn((name: string) => {
-        return config[name];
-      }),
-    }));
-    jest.mock('@actions/github', () => ({
-      context: {},
-    }));
-
-    const { makeConfig } = require('../config');
-
+    setupMockedConfig(defaultConfig);
     const c = await makeConfig();
+
     expect(c).toBeTruthy();
     expect(c).toMatchInlineSnapshot(`
       Object {
@@ -36,152 +39,40 @@ describe('config.ts', () => {
         "commentOnPr": false,
         "commentOnPrNumber": undefined,
         "configMap": undefined,
+        "editCommentOnPr": false,
         "githubToken": "n/a",
-        "isPullRequest": false,
         "options": Object {
           "color": undefined,
-          "diff": undefined,
-          "editCommentOnPr": undefined,
-          "expectNoChanges": undefined,
-          "message": undefined,
+          "diff": false,
+          "expectNoChanges": false,
+          "message": "",
           "parallel": undefined,
-          "policyPackConfigs": undefined,
-          "policyPacks": undefined,
-          "pulumiVersion": "^3",
-          "replace": undefined,
-          "target": undefined,
-          "targetDependents": undefined,
-          "userAgent": "pulumi/actions@v4",
+          "policyPackConfigs": Array [],
+          "policyPacks": Array [],
+          "replace": Array [],
+          "target": Array [],
+          "targetDependents": false,
+          "userAgent": "pulumi/actions@v3",
         },
-        "refresh": undefined,
-        "remove": undefined,
-        "secretsProvider": undefined,
+        "pulumiVersion": "^3",
+        "refresh": false,
+        "remove": false,
+        "secretsProvider": "",
         "stackName": "dev",
-        "upsert": undefined,
+        "upsert": false,
         "workDir": "./",
       }
     `);
   });
   it('should fail if configuration are invalid', async () => {
-    const config: Record<string, string> = {
-      command: 'sideways',
-    };
-    jest.mock('@actions/core', () => ({
-      getInput: jest.fn((name: string) => {
-        return config[name];
-      }),
-    }));
-    jest.mock('@actions/github', () => ({
-      context: {},
-    }));
-
-    const { makeConfig } = require('../config');
-
-    await expect(makeConfig()).rejects.toThrow();
-  });
-  it('should validate a configuration with commentOnPr eq true', async () => {
-    const config = {
+    setupMockedConfig({
       ...defaultConfig,
-      'comment-on-pr': 'true',
-    };
-    jest.mock('@actions/core', () => ({
-      getInput: jest.fn((name: string) => {
-        return config[name];
-      }),
-    }));
+      'stack-name': '',
+      command: 'invalid',
+    });
 
-    const { makeConfig } = require('../config');
-
-    const c = await makeConfig();
-    expect(c).toBeTruthy();
-    expect(c).toMatchInlineSnapshot(`
-      Object {
-        "cloudUrl": "file://~",
-        "command": "up",
-        "commentOnPr": true,
-        "commentOnPrNumber": undefined,
-        "configMap": undefined,
-        "githubToken": "n/a",
-        "isPullRequest": false,
-        "options": Object {
-          "color": undefined,
-          "diff": undefined,
-          "editCommentOnPr": undefined,
-          "expectNoChanges": undefined,
-          "message": undefined,
-          "parallel": undefined,
-          "policyPackConfigs": undefined,
-          "policyPacks": undefined,
-          "pulumiVersion": "^3",
-          "replace": undefined,
-          "target": undefined,
-          "targetDependents": undefined,
-          "userAgent": "pulumi/actions@v4",
-        },
-        "refresh": undefined,
-        "remove": undefined,
-        "secretsProvider": undefined,
-        "stackName": "dev",
-        "upsert": undefined,
-        "workDir": "./",
-      }
-    `);
-  });
-  it('should determine when in a PR', async () => {
-    const config = {
-      ...defaultConfig,
-      'comment-on-pr': 'false',
-    };
-    jest.mock('@actions/core', () => ({
-      getInput: jest.fn((name: string) => {
-        return config[name];
-      }),
-    }));
-    jest.mock('@actions/github', () => ({
-      context: {
-        payload: {
-          pull_request: {
-            number: 5678,
-          },
-        },
-      },
-    }));
-
-    const { makeConfig } = require('../config');
-
-    const c = await makeConfig();
-    expect(c).toBeTruthy();
-    expect(c).toMatchInlineSnapshot(`
-      Object {
-        "cloudUrl": "file://~",
-        "command": "up",
-        "commentOnPr": false,
-        "commentOnPrNumber": undefined,
-        "configMap": undefined,
-        "githubToken": "n/a",
-        "isPullRequest": true,
-        "options": Object {
-          "color": undefined,
-          "diff": undefined,
-          "editCommentOnPr": undefined,
-          "expectNoChanges": undefined,
-          "message": undefined,
-          "parallel": undefined,
-          "policyPackConfigs": undefined,
-          "policyPacks": undefined,
-          "pulumiVersion": "^3",
-          "replace": undefined,
-          "target": undefined,
-          "targetDependents": undefined,
-          "userAgent": "pulumi/actions@v4",
-        },
-        "refresh": undefined,
-        "remove": undefined,
-        "secretsProvider": undefined,
-        "stackName": "dev",
-        "upsert": undefined,
-        "workDir": "./",
-      }
-    `);
+    await expect(() => makeConfig()).toThrowErrorMatchingInlineSnapshot(
+      `"Input was not correct for command. Valid alternatives are: up, update, refresh, destroy, preview"`,
+    );
   });
 });
diff --git a/src/__tests__/run.test.ts b/src/__tests__/run.test.ts
index 6d3e9fa..104a8a9 100644
--- a/src/__tests__/run.test.ts
+++ b/src/__tests__/run.test.ts
@@ -1,11 +1,11 @@
-import * as pulumiCli from "../libs/pulumi-cli";
+import * as pulumiCli from '../libs/pulumi-cli';
 import { login } from '../login';
 
 const spy = jest.spyOn(pulumiCli, 'run');
 
 const installConfig: Record<string, string> = {
   command: undefined,
-  "pulumi-version": "^2", // test with a non-default value
+  'pulumi-version': '^2', // test with a non-default value
 };
 
 describe('Config without a provided command', () => {
@@ -13,7 +13,7 @@ describe('Config without a provided command', () => {
   beforeEach(() => {
     spy.mockClear();
     jest.resetModules();
-    // Save, then restore the current env var for GITHUB_WORKSPACE 
+    // Save, then restore the current env var for GITHUB_WORKSPACE
     oldWorkspace = process.env.GITHUB_WORKSPACE;
     process.env.GITHUB_WORKSPACE = 'n/a';
   });
@@ -22,34 +22,29 @@ describe('Config without a provided command', () => {
   });
 
   it('should not be validated by makeConfig', async () => {
-    jest.mock('@actions/core', () => ({
-      getInput: jest.fn((name: string) => {
-        return installConfig[name];
-      }),
-      info: jest.fn(),
-    }));
     jest.mock('@actions/github', () => ({
       context: {},
     }));
     const { makeConfig } = require('../config');
-    await expect(makeConfig())
-      .rejects
-      .toThrow();
+    await expect(() => makeConfig()).toThrow();
   });
 
-  it('should be validated by makeInstallationConfig', async() => {
+  it('should be validated by makeInstallationConfig', async () => {
     jest.mock('@actions/core', () => ({
       getInput: jest.fn((name: string) => {
         return installConfig[name];
       }),
+      getBooleanInput: jest.fn((name: string) => {
+        return Boolean(installConfig[name]);
+      }),
       info: jest.fn(),
     }));
     const { makeInstallationConfig } = require('../config');
-    const conf = makeInstallationConfig()
+    const conf = makeInstallationConfig();
     expect(conf.success).toBeTruthy();
     expect(conf.value).toEqual({
       command: undefined,
-      pulumiVersion: "^2",
+      pulumiVersion: '^2',
     });
   });
 });
@@ -59,26 +54,26 @@ describe('main.login', () => {
     spy.mockClear();
   });
   it('should read self-hosted backend config', async () => {
-    const cloudUrl = "s3://my_region.aws.com";    
-    await login(cloudUrl, "");
+    const cloudUrl = 's3://my_region.aws.com';
+    await login(cloudUrl, '');
     expect(spy).toHaveBeenCalledTimes(1);
     expect(spy).toHaveBeenCalledWith('--non-interactive', 'login', cloudUrl);
-  })
+  });
   it('should login when an access token is provided', async () => {
-    const accessToken = "my_access_token";    
-    await login("", accessToken);
+    const accessToken = 'my_access_token';
+    await login('', accessToken);
     expect(spy).toHaveBeenCalledTimes(1);
     expect(spy).toHaveBeenCalledWith('--non-interactive', 'login');
-  })
+  });
   it('should not login when nothing provided', async () => {
-    await login("", "");
+    await login('', '');
     expect(spy).not.toHaveBeenCalled();
-  })
+  });
   it('should login with cloud url when both are specified', async () => {
-    const cloudLogin = "my login url";    
-    const accessToken = "my access token";    
+    const cloudLogin = 'my login url';
+    const accessToken = 'my access token';
     await login(cloudLogin, accessToken);
     expect(spy).toHaveBeenCalledTimes(1);
     expect(spy).toHaveBeenCalledWith('--non-interactive', 'login', cloudLogin);
-  })
-})
\ No newline at end of file
+  });
+});
diff --git a/src/libs/__tests__/pr.test.ts b/src/libs/__tests__/pr.test.ts
index 867c31b..8c18978 100644
--- a/src/libs/__tests__/pr.test.ts
+++ b/src/libs/__tests__/pr.test.ts
@@ -136,7 +136,7 @@ describe('pr.ts', () => {
       command: 'preview',
       stackName: 'staging',
       commentOnPrNumber: 123,
-      options: { editCommentOnPr: true },
+      editCommentOnPr: true,
     } as Config;
 
     await handlePullRequestMessage(options, projectName, 'test');
diff --git a/src/libs/__tests__/utils.test.ts b/src/libs/__tests__/utils.test.ts
index 1979412..e335347 100644
--- a/src/libs/__tests__/utils.test.ts
+++ b/src/libs/__tests__/utils.test.ts
@@ -1,40 +1,11 @@
-import { parseArray, parseBoolean, parseNumber } from '../utils';
+import { parseSemicolorToArray } from '../utils';
 
 describe('utils.ts', () => {
-  it('should parse input to array', () => {
-    const input = `
-      hello
-      world
-    `;
+  it('should parse semicolon to array', () => {
 
-    const undef = '';
-
-    const inputArray = parseArray(input);
+    const inputArray = parseSemicolorToArray(['hello,world']);
 
     expect(inputArray[0]).toBe('hello');
     expect(inputArray[1]).toBe('world');
-    expect(parseArray(undef)).toBeUndefined();
-  });
-
-  it('should parse boolean', () => {
-    const truthy = 'true';
-    const falsy = 'false';
-    const falsyTypo = 'fl';
-    const undef = '';
-
-    expect(parseBoolean(truthy)).toBeTruthy();
-    expect(parseBoolean(falsy)).toBeFalsy();
-    expect(parseBoolean(falsyTypo)).toBeFalsy();
-    expect(parseBoolean(undef)).toBeUndefined();
-  });
-
-  it('should parse number', () => {
-    const truthy = '1';
-    const falsy = 'false';
-    const undef = '';
-
-    expect(parseNumber(truthy)).toBe(1);
-    expect(parseNumber(falsy)).toBeNaN();
-    expect(parseNumber(undef)).toBeUndefined();
   });
 });