Feat: add support for checking if source folder exists (#463)

* Refactor: check parameters in `init`

* Test: add test for checking if folder exist

- rm unnecessary tests

* Test: fix wrong error message

* Refactor: mv initialization functions to `run`

Refactor: rm `action.root`, `action.rootPath`

Refactor: `generateFolderPath`, `hasRequiredParameters`

Test: rm some tests for `init`, add tests for `checkParameters`
diff --git a/__tests__/git.test.ts b/__tests__/git.test.ts
index 404fb9a..1ef54c6 100644
--- a/__tests__/git.test.ts
+++ b/__tests__/git.test.ts
@@ -32,171 +32,6 @@
   })
 
   describe('init', () => {
-    it('should execute commands if a GitHub token is provided', async () => {
-      Object.assign(action, {
-        silent: false,
-        repositoryPath: 'JamesIves/github-pages-deploy-action',
-        folder: 'assets',
-        branch: 'branch',
-        gitHubToken: '123',
-        pusher: {
-          name: 'asd',
-          email: 'as@cat'
-        }
-      })
-
-      await init(action)
-      expect(execute).toBeCalledTimes(6)
-    })
-
-    it('should execute commands if an Access Token is provided', async () => {
-      Object.assign(action, {
-        silent: false,
-        repositoryPath: 'JamesIves/github-pages-deploy-action',
-        folder: 'assets',
-        branch: 'branch',
-        accessToken: '123',
-        pusher: {
-          name: 'asd',
-          email: 'as@cat'
-        }
-      })
-
-      await init(action)
-      expect(execute).toBeCalledTimes(6)
-    })
-
-    it('should execute commands if SSH is true', async () => {
-      Object.assign(action, {
-        silent: false,
-        repositoryPath: 'JamesIves/github-pages-deploy-action',
-        folder: 'assets',
-        branch: 'branch',
-        ssh: true,
-        pusher: {
-          name: 'asd',
-          email: 'as@cat'
-        }
-      })
-
-      await init(action)
-
-      expect(execute).toBeCalledTimes(6)
-    })
-
-    it('should fail if there is no provided GitHub Token, Access Token or SSH bool', async () => {
-      Object.assign(action, {
-        silent: false,
-        repositoryPath: null,
-        folder: 'assets',
-        branch: 'branch',
-        pusher: {
-          name: 'asd',
-          email: 'as@cat'
-        }
-      })
-
-      try {
-        await init(action)
-      } catch (e) {
-        expect(execute).toBeCalledTimes(0)
-        expect(e.message).toMatch(
-          'There was an error initializing the repository: No deployment token/method was provided. You must provide the action with either a Personal Access Token or the GitHub Token secret in order to deploy. If you wish to use an ssh deploy token then you must set SSH to true. ❌'
-        )
-      }
-    })
-
-    it('should fail if access token is defined but it is an empty string', async () => {
-      Object.assign(action, {
-        silent: false,
-        repositoryPath: null,
-        folder: 'assets',
-        branch: 'branch',
-        pusher: {
-          name: 'asd',
-          email: 'as@cat'
-        },
-        accessToken: ''
-      })
-
-      try {
-        await init(action)
-      } catch (e) {
-        expect(execute).toBeCalledTimes(0)
-        expect(e.message).toMatch(
-          'There was an error initializing the repository: No deployment token/method was provided. You must provide the action with either a Personal Access Token or the GitHub Token secret in order to deploy. If you wish to use an ssh deploy token then you must set SSH to true. ❌'
-        )
-      }
-    })
-
-    it('should fail if there is no folder', async () => {
-      Object.assign(action, {
-        silent: false,
-        repositoryPath: 'JamesIves/github-pages-deploy-action',
-        gitHubToken: '123',
-        branch: 'branch',
-        pusher: {
-          name: 'asd',
-          email: 'as@cat'
-        },
-        folder: null,
-        ssh: true
-      })
-
-      try {
-        await init(action)
-      } catch (e) {
-        expect(execute).toBeCalledTimes(0)
-        expect(e.message).toMatch(
-          'There was an error initializing the repository: You must provide the action with a folder to deploy. ❌'
-        )
-      }
-    })
-
-    it('should fail if there is no provided repository path', async () => {
-      Object.assign(action, {
-        silent: true,
-        repositoryPath: null,
-        folder: 'assets',
-        branch: 'branch',
-        pusher: {
-          name: 'asd',
-          email: 'as@cat'
-        },
-        gitHubToken: '123',
-        accessToken: null,
-        ssh: null
-      })
-
-      try {
-        await init(action)
-      } catch (e) {
-        expect(execute).toBeCalledTimes(0)
-        expect(e.message).toMatch(
-          'There was an error initializing the repository: No deployment token/method was provided. You must provide the action with either a Personal Access Token or the GitHub Token secret in order to deploy. If you wish to use an ssh deploy token then you must set SSH to true. '
-        )
-      }
-    })
-
-    it('should not fail if root is used', async () => {
-      Object.assign(action, {
-        silent: false,
-        repositoryPath: 'JamesIves/github-pages-deploy-action',
-        accessToken: '123',
-        branch: 'branch',
-        folder: '.',
-        root: '.',
-        pusher: {
-          name: 'asd',
-          email: 'as@cat'
-        }
-      })
-
-      await init(action)
-
-      expect(execute).toBeCalledTimes(6)
-    })
-
     it('should stash changes if preserve is true', async () => {
       Object.assign(action, {
         silent: false,
@@ -213,7 +48,6 @@
       })
 
       await init(action)
-
       expect(execute).toBeCalledTimes(7)
     })
   })
@@ -234,27 +68,6 @@
       await generateBranch(action)
       expect(execute).toBeCalledTimes(6)
     })
-
-    it('should fail if there is no branch', async () => {
-      Object.assign(action, {
-        silent: false,
-        accessToken: '123',
-        branch: null,
-        folder: '.',
-        pusher: {
-          name: 'asd',
-          email: 'as@cat'
-        }
-      })
-
-      try {
-        await generateBranch(action)
-      } catch (e) {
-        expect(e.message).toMatch(
-          'There was an error creating the deployment branch: Branch is required. ❌'
-        )
-      }
-    })
   })
 
   describe('switchToBaseBranch', () => {
@@ -290,31 +103,6 @@
       await switchToBaseBranch(action)
       expect(execute).toBeCalledTimes(1)
     })
-
-    it('should fail if one of the required parameters is not available', async () => {
-      Object.assign(action, {
-        silent: false,
-        baseBranch: '123',
-        accessToken: null,
-        gitHubToken: null,
-        ssh: null,
-        branch: 'branch',
-        folder: null,
-        pusher: {
-          name: 'asd',
-          email: 'as@cat'
-        }
-      })
-
-      try {
-        await switchToBaseBranch(action)
-      } catch (e) {
-        expect(execute).toBeCalledTimes(0)
-        expect(e.message).toMatch(
-          'There was an error switching to the base branch: No deployment token/method was provided. You must provide the action with either a Personal Access Token or the GitHub Token secret in order to deploy. If you wish to use an ssh deploy token then you must set SSH to true. ❌'
-        )
-      }
-    })
   })
 
   describe('deploy', () => {
@@ -488,31 +276,5 @@
       expect(rmRF).toBeCalledTimes(1)
       expect(response).toBe(Status.SKIPPED)
     })
-
-    it('should throw an error if one of the required parameters is not available', async () => {
-      Object.assign(action, {
-        silent: false,
-        folder: 'assets',
-        branch: 'branch',
-        ssh: null,
-        accessToken: null,
-        gitHubToken: null,
-        pusher: {
-          name: 'asd',
-          email: 'as@cat'
-        },
-        isTest: false // Setting this env variable to false means there will never be anything to commit and the action will exit early.
-      })
-
-      try {
-        await deploy(action)
-      } catch (e) {
-        expect(execute).toBeCalledTimes(1)
-        expect(rmRF).toBeCalledTimes(1)
-        expect(e.message).toMatch(
-          'The deploy step encountered an error: No deployment token/method was provided. You must provide the action with either a Personal Access Token or the GitHub Token secret in order to deploy. If you wish to use an ssh deploy token then you must set SSH to true. ❌'
-        )
-      }
-    })
   })
 })
diff --git a/__tests__/util.test.ts b/__tests__/util.test.ts
index 9d9724a..0437586 100644
--- a/__tests__/util.test.ts
+++ b/__tests__/util.test.ts
@@ -1,9 +1,11 @@
+import {ActionInterface} from '../src/constants'
 import {
   isNullOrUndefined,
   generateTokenType,
   generateRepositoryPath,
   generateFolderPath,
-  suppressSensitiveInformation
+  suppressSensitiveInformation,
+  checkParameters
 } from '../src/util'
 
 describe('util', () => {
@@ -22,13 +24,17 @@
       const value = 'montezuma'
       expect(isNullOrUndefined(value)).toBeFalsy()
     })
+
+    it('should return false if the value is empty string', async () => {
+      const value = ''
+      expect(isNullOrUndefined(value)).toBeTruthy()
+    })
   })
 
   describe('generateTokenType', () => {
     it('should return ssh if ssh is provided', async () => {
       const action = {
         branch: '123',
-        root: '.',
         workspace: 'src/',
         folder: 'build',
         gitHubToken: null,
@@ -42,7 +48,6 @@
     it('should return access token if access token is provided', async () => {
       const action = {
         branch: '123',
-        root: '.',
         workspace: 'src/',
         folder: 'build',
         gitHubToken: null,
@@ -56,7 +61,6 @@
     it('should return github token if github token is provided', async () => {
       const action = {
         branch: '123',
-        root: '.',
         workspace: 'src/',
         folder: 'build',
         gitHubToken: '123',
@@ -70,7 +74,6 @@
     it('should return ... if no token is provided', async () => {
       const action = {
         branch: '123',
-        root: '.',
         workspace: 'src/',
         folder: 'build',
         gitHubToken: null,
@@ -87,7 +90,6 @@
       const action = {
         repositoryName: 'JamesIves/github-pages-deploy-action',
         branch: '123',
-        root: '.',
         workspace: 'src/',
         folder: 'build',
         gitHubToken: null,
@@ -104,7 +106,6 @@
       const action = {
         repositoryName: 'JamesIves/github-pages-deploy-action',
         branch: '123',
-        root: '.',
         workspace: 'src/',
         folder: 'build',
         gitHubToken: null,
@@ -121,7 +122,6 @@
       const action = {
         repositoryName: 'JamesIves/github-pages-deploy-action',
         branch: '123',
-        root: '.',
         workspace: 'src/',
         folder: 'build',
         gitHubToken: '123',
@@ -141,7 +141,6 @@
           repositoryPath:
             'https://x-access-token:supersecret999%%%@github.com/anothersecret123333',
           branch: '123',
-          root: '.',
           workspace: 'src/',
           folder: 'build',
           accessToken: 'supersecret999%%%',
@@ -161,7 +160,6 @@
           repositoryPath:
             'https://x-access-token:supersecret999%%%@github.com/anothersecret123333',
           branch: '123',
-          root: '.',
           workspace: 'src/',
           folder: 'build',
           accessToken: 'supersecret999%%%',
@@ -183,7 +181,6 @@
     it('should return absolute path if folder name is provided', () => {
       const action = {
         branch: '123',
-        root: '.',
         workspace: 'src/',
         folder: 'build',
         gitHubToken: null,
@@ -191,13 +188,12 @@
         ssh: null,
         silent: false
       }
-      expect(generateFolderPath(action, 'folder')).toEqual('src/build')
+      expect(generateFolderPath(action)).toEqual('src/build')
     })
 
     it('should return original path if folder name begins with /', () => {
       const action = {
         branch: '123',
-        root: '.',
         workspace: 'src/',
         folder: '/home/user/repo/build',
         gitHubToken: null,
@@ -205,15 +201,12 @@
         ssh: null,
         silent: false
       }
-      expect(generateFolderPath(action, 'folder')).toEqual(
-        '/home/user/repo/build'
-      )
+      expect(generateFolderPath(action)).toEqual('/home/user/repo/build')
     })
 
     it('should process as relative path if folder name begins with ./', () => {
       const action = {
         branch: '123',
-        root: '.',
         workspace: 'src/',
         folder: './build',
         gitHubToken: null,
@@ -221,13 +214,12 @@
         ssh: null,
         silent: false
       }
-      expect(generateFolderPath(action, 'folder')).toEqual('src/build')
+      expect(generateFolderPath(action)).toEqual('src/build')
     })
 
     it('should return absolute path if folder name begins with ~', () => {
       const action = {
         branch: '123',
-        root: '.',
         workspace: 'src/',
         folder: '~/repo/build',
         gitHubToken: null,
@@ -236,9 +228,102 @@
         silent: false
       }
       process.env.HOME = '/home/user'
-      expect(generateFolderPath(action, 'folder')).toEqual(
-        '/home/user/repo/build'
-      )
+      expect(generateFolderPath(action)).toEqual('/home/user/repo/build')
+    })
+  })
+
+  describe('hasRequiredParameters', () => {
+    it('should fail if there is no provided GitHub Token, Access Token or SSH bool', () => {
+      const action = {
+        silent: false,
+        repositoryPath: undefined,
+        branch: 'branch',
+        folder: 'build',
+        workspace: 'src/'
+      }
+
+      try {
+        checkParameters(action)
+      } catch (e) {
+        expect(e.message).toMatch(
+          'No deployment token/method was provided. You must provide the action with either a Personal Access Token or the GitHub Token secret in order to deploy. If you wish to use an ssh deploy token then you must set SSH to true.'
+        )
+      }
+    })
+
+    it('should fail if access token is defined but it is an empty string', () => {
+      const action = {
+        silent: false,
+        repositoryPath: undefined,
+        accessToken: '',
+        branch: 'branch',
+        folder: 'build',
+        workspace: 'src/'
+      }
+
+      try {
+        checkParameters(action)
+      } catch (e) {
+        expect(e.message).toMatch(
+          'No deployment token/method was provided. You must provide the action with either a Personal Access Token or the GitHub Token secret in order to deploy. If you wish to use an ssh deploy token then you must set SSH to true.'
+        )
+      }
+    })
+
+    it('should fail if there is no branch', () => {
+      const action = {
+        silent: false,
+        repositoryPath: undefined,
+        accessToken: '123',
+        branch: '',
+        folder: 'build',
+        workspace: 'src/'
+      }
+
+      try {
+        checkParameters(action)
+      } catch (e) {
+        expect(e.message).toMatch('Branch is required.')
+      }
+    })
+
+    it('should fail if there is no folder', () => {
+      const action = {
+        silent: false,
+        repositoryPath: undefined,
+        gitHubToken: '123',
+        branch: 'branch',
+        folder: '',
+        workspace: 'src/'
+      }
+
+      try {
+        checkParameters(action)
+      } catch (e) {
+        expect(e.message).toMatch(
+          'You must provide the action with a folder to deploy.'
+        )
+      }
+    })
+
+    it('should fail if the folder does not exist in the tree', () => {
+      const action: ActionInterface = {
+        silent: false,
+        repositoryPath: undefined,
+        gitHubToken: '123',
+        branch: 'branch',
+        folder: 'notARealFolder',
+        workspace: '.'
+      }
+
+      try {
+        action.folderPath = generateFolderPath(action)
+        checkParameters(action)
+      } catch (e) {
+        expect(e.message).toMatch(
+          `The ${action.folderPath} directory you're trying to deploy doesn't exist.`
+        )
+      }
     })
   })
 })
diff --git a/src/constants.ts b/src/constants.ts
index 5e43eef..8c56f2a 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -24,6 +24,7 @@
   email?: string
   /** The folder to deploy. */
   folder: string
+  folderPath?: string
   /** GitHub deployment token. */
   gitHubToken?: string | null
   /** Determines if the action is running in test mode or not. */
@@ -38,8 +39,6 @@
   repositoryName?: string
   /** The fully qualified repositpory path, this gets auto generated if repositoryName is provided. */
   repositoryPath?: string
-  /** The root directory where your project lives. */
-  root: string
   /** Wipes the commit history from the deployment branch in favor of a single commit. */
   singleCommit?: boolean | null
   /** Determines if the action should run in silent mode or not. */
@@ -95,7 +94,6 @@
     : repository && repository.full_name
     ? repository.full_name
     : process.env.GITHUB_REPOSITORY,
-  root: '.',
   singleCommit: !isNullOrUndefined(getInput('SINGLE_COMMIT'))
     ? getInput('SINGLE_COMMIT').toLowerCase() === 'true'
     : false,
@@ -109,8 +107,9 @@
   workspace: process.env.GITHUB_WORKSPACE || ''
 }
 
-export type ActionFolders = NonNullable<
-  Pick<ActionInterface, 'folder' | 'root'>
+export type RequiredActionParameters = Pick<
+  ActionInterface,
+  'accessToken' | 'gitHubToken' | 'ssh' | 'branch' | 'folder'
 >
 
 /** Status codes for the action. */
diff --git a/src/git.ts b/src/git.ts
index bb7ffc2..13a7344 100644
--- a/src/git.ts
+++ b/src/git.ts
@@ -3,18 +3,11 @@
 import fs from 'fs'
 import {ActionInterface, Status} from './constants'
 import {execute} from './execute'
-import {
-  generateFolderPath,
-  hasRequiredParameters,
-  isNullOrUndefined,
-  suppressSensitiveInformation
-} from './util'
+import {isNullOrUndefined, suppressSensitiveInformation} from './util'
 
 /* Initializes git in the workspace. */
 export async function init(action: ActionInterface): Promise<void | Error> {
   try {
-    hasRequiredParameters(action)
-
     info(`Deploying using ${action.tokenType}… 🔑`)
     info('Configuring git…')
 
@@ -73,8 +66,6 @@
   action: ActionInterface
 ): Promise<void> {
   try {
-    hasRequiredParameters(action)
-
     await execute(
       `git checkout --progress --force ${
         action.baseBranch ? action.baseBranch : action.defaultBranch
@@ -95,8 +86,6 @@
 /* Generates the branch if it doesn't exist on the remote. */
 export async function generateBranch(action: ActionInterface): Promise<void> {
   try {
-    hasRequiredParameters(action)
-
     info(`Creating the ${action.branch} branch…`)
 
     await switchToBaseBranch(action)
@@ -131,8 +120,6 @@
 
 /* Runs the necessary steps to make the deployment. */
 export async function deploy(action: ActionInterface): Promise<Status> {
-  const folderPath = generateFolderPath(action, 'folder')
-  const rootPath = generateFolderPath(action, 'root')
   const temporaryDeploymentDirectory =
     'github-pages-deploy-action-temp-deployment-folder'
   const temporaryDeploymentBranch = `github-pages-deploy-action/${Math.random()
@@ -142,8 +129,6 @@
   info('Starting to commit changes…')
 
   try {
-    hasRequiredParameters(action)
-
     const commitMessage = !isNullOrUndefined(action.commitMessage)
       ? (action.commitMessage as string)
       : `Deploying to ${action.branch} from ${action.baseBranch} ${
@@ -232,22 +217,24 @@
       Allows the user to specify the root if '.' is provided.
       rsync is used to prevent file duplication. */
     await execute(
-      `rsync -q -av --checksum --progress ${folderPath}/. ${
+      `rsync -q -av --checksum --progress ${action.folderPath}/. ${
         action.targetFolder
           ? `${temporaryDeploymentDirectory}/${action.targetFolder}`
           : temporaryDeploymentDirectory
       } ${
         action.clean
           ? `--delete ${excludes} ${
-              !fs.existsSync(`${folderPath}/CNAME`) ? '--exclude CNAME' : ''
+              !fs.existsSync(`${action.folderPath}/CNAME`)
+                ? '--exclude CNAME'
+                : ''
             } ${
-              !fs.existsSync(`${folderPath}/.nojekyll`)
+              !fs.existsSync(`${action.folderPath}/.nojekyll`)
                 ? '--exclude .nojekyll'
                 : ''
             }`
           : ''
       }  --exclude .ssh --exclude .git --exclude .github ${
-        folderPath === rootPath
+        action.folderPath === action.workspace
           ? `--exclude ${temporaryDeploymentDirectory}`
           : ''
       }`,
diff --git a/src/lib.ts b/src/lib.ts
index 4286d8b..6569351 100644
--- a/src/lib.ts
+++ b/src/lib.ts
@@ -1,7 +1,12 @@
 import {exportVariable, info, setFailed} from '@actions/core'
 import {action, ActionInterface, Status} from './constants'
 import {deploy, init} from './git'
-import {generateRepositoryPath, generateTokenType} from './util'
+import {
+  generateFolderPath,
+  checkParameters,
+  generateRepositoryPath,
+  generateTokenType
+} from './util'
 
 /** Initializes and runs the action.
  *
@@ -30,6 +35,11 @@
       ...configuration
     }
 
+    // Defines the folder paths
+    settings.folderPath = generateFolderPath(settings)
+
+    checkParameters(settings)
+
     // Defines the repository paths and token types.
     settings.repositoryPath = generateRepositoryPath(settings)
     settings.tokenType = generateTokenType(settings)
diff --git a/src/util.ts b/src/util.ts
index d423b81..fc52f54 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -1,6 +1,7 @@
+import {existsSync} from 'fs'
 import path from 'path'
 import {isDebug} from '@actions/core'
-import {ActionInterface, ActionFolders} from './constants'
+import {ActionInterface, RequiredActionParameters} from './constants'
 
 const replaceAll = (input: string, find: string, replace: string): string =>
   input.split(find).join(replace)
@@ -28,40 +29,46 @@
       }@github.com/${action.repositoryName}.git`
 
 /* Genetate absolute folder path by the provided folder name */
-export const generateFolderPath = <K extends keyof ActionFolders>(
-  action: ActionInterface,
-  key: K
-): string => {
-  const folderName = action[key]
-  const folderPath = path.isAbsolute(folderName)
+export const generateFolderPath = (action: ActionInterface): string => {
+  const folderName = action['folder']
+  return path.isAbsolute(folderName)
     ? folderName
     : folderName.startsWith('~')
     ? folderName.replace('~', process.env.HOME as string)
     : path.join(action.workspace, folderName)
-  return folderPath
 }
 
 /* Checks for the required tokens and formatting. Throws an error if any case is matched. */
-export const hasRequiredParameters = (action: ActionInterface): void => {
-  if (
-    (isNullOrUndefined(action.accessToken) &&
-      isNullOrUndefined(action.gitHubToken) &&
-      isNullOrUndefined(action.ssh)) ||
-    isNullOrUndefined(action.repositoryPath) ||
-    (action.accessToken && action.accessToken === '')
-  ) {
+const hasRequiredParameters = <K extends keyof RequiredActionParameters>(
+  action: ActionInterface,
+  params: K[]
+): boolean => {
+  const nonNullParams = params.filter(
+    param => !isNullOrUndefined(action[param])
+  )
+  return Boolean(nonNullParams.length)
+}
+
+export const checkParameters = (action: ActionInterface): void => {
+  if (!hasRequiredParameters(action, ['accessToken', 'gitHubToken', 'ssh'])) {
     throw new Error(
       'No deployment token/method was provided. You must provide the action with either a Personal Access Token or the GitHub Token secret in order to deploy. If you wish to use an ssh deploy token then you must set SSH to true.'
     )
   }
 
-  if (isNullOrUndefined(action.branch)) {
+  if (!hasRequiredParameters(action, ['branch'])) {
     throw new Error('Branch is required.')
   }
 
-  if (!action.folder || isNullOrUndefined(action.folder)) {
+  if (!hasRequiredParameters(action, ['folder'])) {
     throw new Error('You must provide the action with a folder to deploy.')
   }
+
+  if (!existsSync(action.folderPath as string)) {
+    throw new Error(
+      `The ${action.folderPath} directory you're trying to deploy doesn't exist. ❗`
+    )
+  }
 }
 
 /* Suppresses sensitive information from being exposed in error messages. */