From 560e416032bc3fc5510bc5f539316b0ba4fda892 Mon Sep 17 00:00:00 2001
From: Axel Hecht
Date: Mon, 7 Dec 2020 16:11:58 +0100
Subject: [PATCH 01/32] Stop checking out workspace (#515)
* Stop checking out base branch before deployment, drop option.
* Don't check out default branch, as we don't check out base branch, drop option.
* Don't stash/unstash as we don't update the workdir, drop preserve option.
* Don't init the workspace
* Only fetch the remote branch if it exists, only with depth 1.
* Rely on previous checkouts to have handled lfs files correctly, drop option.
* Update README, action.yml, integration tests
---
.github/workflows/integration-beta.yml | 7 --
.github/workflows/integration.yml | 7 --
README.md | 4 -
__tests__/git.test.ts | 160 ++-----------------------
__tests__/main.test.ts | 5 +-
action.yml | 12 --
src/constants.ts | 16 ---
src/git.ts | 88 +-------------
8 files changed, 17 insertions(+), 282 deletions(-)
diff --git a/.github/workflows/integration-beta.yml b/.github/workflows/integration-beta.yml
index 0c9f80e55..373331e0f 100644
--- a/.github/workflows/integration-beta.yml
+++ b/.github/workflows/integration-beta.yml
@@ -21,7 +21,6 @@ jobs:
ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
BRANCH: gh-pages-test
FOLDER: integration
- BASE_BRANCH: dev
TARGET_FOLDER: cat/montezuma
GIT_CONFIG_NAME: Montezuma
GIT_CONFIG_EMAIL: montezuma@jamesiv.es
@@ -49,7 +48,6 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BRANCH: gh-pages-test
FOLDER: integration
- BASE_BRANCH: dev
TARGET_FOLDER: cat/montezuma2
- name: Cleanup Generated Branch
@@ -83,7 +81,6 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BRANCH: gh-pages-test
FOLDER: integration
- BASE_BRANCH: dev
TARGET_FOLDER: cat/montezuma2
- name: Cleanup Generated Branch
@@ -114,7 +111,6 @@ jobs:
SSH: true
BRANCH: gh-pages-test
FOLDER: integration
- BASE_BRANCH: dev
TARGET_FOLDER: cat/montezuma3
- name: Cleanup Generated Branch
@@ -149,7 +145,6 @@ jobs:
SSH: true
BRANCH: gh-pages-test
FOLDER: integration
- BASE_BRANCH: dev
TARGET_FOLDER: cat/montezuma4
- name: Cleanup Generated Branch
@@ -175,7 +170,6 @@ jobs:
ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
BRANCH: gh-pages-test
FOLDER: integration
- BASE_BRANCH: dev
CLEAN: true
# Deploys to a branch that doesn't exist with SINGLE_COMMIT.
@@ -193,7 +187,6 @@ jobs:
ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
BRANCH: integration-test-delete-beta
FOLDER: integration
- BASE_BRANCH: dev
SINGLE_COMMIT: true
- name: Cleanup Generated Branch
diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index e4f42edd8..6db33996b 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -23,7 +23,6 @@ jobs:
ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
BRANCH: gh-pages
FOLDER: integration
- BASE_BRANCH: dev
TARGET_FOLDER: cat/montezuma
GIT_CONFIG_NAME: Montezuma
GIT_CONFIG_EMAIL: montezuma@jamesiv.es
@@ -50,7 +49,6 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BRANCH: gh-pages
FOLDER: integration
- BASE_BRANCH: dev
TARGET_FOLDER: cat/montezuma2
- name: Cleanup Generated Branch
@@ -84,7 +82,6 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BRANCH: gh-pages
FOLDER: integration
- BASE_BRANCH: dev
TARGET_FOLDER: cat/montezuma2
- name: Cleanup Generated Branch
@@ -114,7 +111,6 @@ jobs:
SSH: true
BRANCH: gh-pages
FOLDER: integration
- BASE_BRANCH: dev
TARGET_FOLDER: cat/montezuma3
- name: Cleanup Generated Branch
@@ -148,7 +144,6 @@ jobs:
SSH: true
BRANCH: gh-pages
FOLDER: integration
- BASE_BRANCH: dev
TARGET_FOLDER: cat/montezuma4
- name: Cleanup Generated Branch
@@ -173,7 +168,6 @@ jobs:
ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
BRANCH: gh-pages
FOLDER: integration
- BASE_BRANCH: dev
CLEAN: true
# Deploys to a branch that doesn't exist with SINGLE_COMMIT.
@@ -192,7 +186,6 @@ jobs:
ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
BRANCH: integration-test-delete-prod
FOLDER: integration
- BASE_BRANCH: dev
SINGLE_COMMIT: true
- name: Cleanup Generated Branch
diff --git a/README.md b/README.md
index bf0f44830..aa00c41f1 100644
--- a/README.md
+++ b/README.md
@@ -145,13 +145,10 @@ In addition to the deployment options you must also configure the following.
| `GIT_CONFIG_EMAIL` | Allows you to customize the email that is attached to the git config which is used when pushing the deployment commits. If this is not included it will use the email in the GitHub context, followed by a generic noreply GitHub email. | `with` | **No** |
| `REPOSITORY_NAME` | Allows you to specify a different repository path so long as you have permissions to push to it. This should be formatted like so: `JamesIves/github-pages-deploy-action`. You'll need to use an `ACCESS_TOKEN` for this configuration option to work properly. | `with` | **No** |
| `TARGET_FOLDER` | If you'd like to push the contents of the deployment folder into a specific directory on the deployment branch you can specify it here. | `with` | **No** |
-| `BASE_BRANCH` | The base branch of your repository which you'd like to checkout prior to deploying. This defaults to the current commit [SHA](http://en.wikipedia.org/wiki/SHA-1) that triggered the build followed by `master` if it doesn't exist. This is useful for making deployments from another branch, and also may be necessary when using a scheduled job. | `with` | **No** |
| `COMMIT_MESSAGE` | If you need to customize the commit message for an integration you can do so. | `with` | **No** |
| `CLEAN` | If your project generates hashed files on build you can use this option to automatically delete them from the deployment branch with each deploy. This option is turned on by default, and can be toggled off by setting it to `false`. | `with` | **No** |
| `CLEAN_EXCLUDE` | If you need to use `CLEAN` but you'd like to preserve certain files or folders you can use this option. This should be formatted as an array but stored as a string. For example: `'["filename.js", "folder"]'` | `with` | **No** |
| `SINGLE_COMMIT` | This option can be toggled to `true` if you'd prefer to have a single commit on the deployment branch instead of maintaining the full history. **Using this option will also cause any existing history to be wiped from the deployment branch**. | `with` | **No** |
-| `LFS` | If toggled all files will be migrated from [Git LFS](https://git-lfs.github.com/) so they can be comitted to the deployment branch. | `with` | **No** |
-| `PRESERVE` | Preserves and restores the workspace prior to deployment. This option is useful if you're modifying files in the worktree that aren't comitted to Git. | `with` | **No** |
| `SILENT` | Silences the action output preventing it from displaying git messages. | `with` | **No** |
| `WORKSPACE` | This should point to where your project lives on the virtual machine. The GitHub Actions environment will set this for you. It is only necessary to set this variable if you're using the node module. | `with` | **No** |
@@ -226,7 +223,6 @@ jobs:
- name: Deploy 🚀
uses: JamesIves/github-pages-deploy-action@3.7.1
with:
- BASE_BRANCH: master
BRANCH: gh-pages
FOLDER: build
CLEAN: true
diff --git a/__tests__/git.test.ts b/__tests__/git.test.ts
index baf84d9aa..cdd282aeb 100644
--- a/__tests__/git.test.ts
+++ b/__tests__/git.test.ts
@@ -5,7 +5,7 @@ process.env['GITHUB_SHA'] = '123'
import {mkdirP, rmRF} from '@actions/io'
import {action, Status} from '../src/constants'
import {execute} from '../src/execute'
-import {deploy, generateBranch, init, switchToBaseBranch} from '../src/git'
+import {deploy, generateBranch, init} from '../src/git'
import fs from 'fs'
const originalAction = JSON.stringify(action)
@@ -33,25 +33,6 @@ describe('git', () => {
})
describe('init', () => {
- it('should stash changes if preserve is true', async () => {
- Object.assign(action, {
- silent: false,
- repositoryPath: 'JamesIves/github-pages-deploy-action',
- accessToken: '123',
- branch: 'branch',
- folder: '.',
- preserve: true,
- isTest: true,
- pusher: {
- name: 'asd',
- email: 'as@cat'
- }
- })
-
- await init(action)
- expect(execute).toBeCalledTimes(7)
- })
-
it('should catch when a function throws an error', async () => {
;(execute as jest.Mock).mockImplementationOnce(() => {
throw new Error('Mocked throw')
@@ -63,7 +44,6 @@ describe('git', () => {
accessToken: '123',
branch: 'branch',
folder: '.',
- preserve: true,
isTest: true,
pusher: {
name: 'asd',
@@ -82,7 +62,7 @@ describe('git', () => {
})
describe('generateBranch', () => {
- it('should execute six commands', async () => {
+ it('should execute five commands', async () => {
Object.assign(action, {
silent: false,
accessToken: '123',
@@ -95,7 +75,7 @@ describe('git', () => {
})
await generateBranch(action)
- expect(execute).toBeCalledTimes(6)
+ expect(execute).toBeCalledTimes(5)
})
it('should catch when a function throws an error', async () => {
@@ -118,68 +98,7 @@ describe('git', () => {
await generateBranch(action)
} catch (error) {
expect(error.message).toBe(
- 'There was an error creating the deployment branch: There was an error switching to the base branch: Mocked throw ❌ ❌'
- )
- }
- })
- })
-
- describe('switchToBaseBranch', () => {
- it('should execute one command', async () => {
- Object.assign(action, {
- silent: false,
- accessToken: '123',
- branch: 'branch',
- folder: '.',
- pusher: {
- name: 'asd',
- email: 'as@cat'
- }
- })
-
- await switchToBaseBranch(action)
- expect(execute).toBeCalledTimes(1)
- })
-
- it('should execute one command if using custom baseBranch', async () => {
- Object.assign(action, {
- silent: false,
- baseBranch: '123',
- accessToken: '123',
- branch: 'branch',
- folder: '.',
- pusher: {
- name: 'asd',
- email: 'as@cat'
- }
- })
-
- await switchToBaseBranch(action)
- expect(execute).toBeCalledTimes(1)
- })
-
- it('should catch when a function throws an error', async () => {
- ;(execute as jest.Mock).mockImplementationOnce(() => {
- throw new Error('Mocked throw')
- })
-
- Object.assign(action, {
- silent: false,
- baseBranch: '123',
- accessToken: '123',
- branch: 'branch',
- folder: '.',
- pusher: {
- name: 'asd',
- email: 'as@cat'
- }
- })
-
- try {
- await switchToBaseBranch(action)
- } catch (error) {
- expect(error.message).toBe(
- 'There was an error switching to the base branch: Mocked throw ❌'
+ 'There was an error creating the deployment branch: Mocked throw ❌'
)
}
})
@@ -192,62 +111,6 @@ describe('git', () => {
folder: 'assets',
branch: 'branch',
gitHubToken: '123',
- lfs: true,
- pusher: {
- name: 'asd',
- email: 'as@cat'
- }
- })
-
- const response = await deploy(action)
-
- // Includes the call to generateBranch
- expect(execute).toBeCalledTimes(13)
- expect(rmRF).toBeCalledTimes(1)
- expect(response).toBe(Status.SUCCESS)
- })
-
- it('should execute stash apply commands if preserve is true', async () => {
- Object.assign(action, {
- silent: false,
- folder: 'assets',
- folderPath: 'assets',
- branch: 'branch',
- gitHubToken: '123',
- lfs: true,
- preserve: true,
- isTest: true,
- pusher: {
- name: 'asd',
- email: 'as@cat'
- }
- })
-
- const response = await deploy(action)
-
- // Includes the call to generateBranch
- expect(execute).toBeCalledTimes(14)
- expect(rmRF).toBeCalledTimes(1)
- expect(response).toBe(Status.SUCCESS)
- })
-
- it('should appropriately move along if git stash errors', async () => {
- ;(execute as jest.Mock).mockImplementation(cmd => {
- if (cmd === 'git stash apply') {
- // Mocks the case where git stash apply errors.
- throw new Error()
- }
- })
-
- Object.assign(action, {
- silent: false,
- folder: 'assets',
- folderPath: 'assets',
- branch: 'branch',
- gitHubToken: '123',
- lfs: true,
- preserve: true,
- isTest: true,
pusher: {
name: 'asd',
email: 'as@cat'
@@ -257,7 +120,7 @@ describe('git', () => {
const response = await deploy(action)
// Includes the call to generateBranch
- expect(execute).toBeCalledTimes(14)
+ expect(execute).toBeCalledTimes(10)
expect(rmRF).toBeCalledTimes(1)
expect(response).toBe(Status.SUCCESS)
})
@@ -280,7 +143,7 @@ describe('git', () => {
await deploy(action)
// Includes the call to generateBranch
- expect(execute).toBeCalledTimes(18)
+ expect(execute).toBeCalledTimes(16)
expect(rmRF).toBeCalledTimes(1)
})
@@ -304,7 +167,7 @@ describe('git', () => {
const response = await deploy(action)
// Includes the call to generateBranch
- expect(execute).toBeCalledTimes(12)
+ expect(execute).toBeCalledTimes(10)
expect(rmRF).toBeCalledTimes(1)
expect(response).toBe(Status.SUCCESS)
})
@@ -329,7 +192,7 @@ describe('git', () => {
await deploy(action)
// Includes the call to generateBranch
- expect(execute).toBeCalledTimes(12)
+ expect(execute).toBeCalledTimes(10)
expect(rmRF).toBeCalledTimes(1)
})
@@ -351,7 +214,7 @@ describe('git', () => {
await deploy(action)
// Includes the call to generateBranch
- expect(execute).toBeCalledTimes(12)
+ expect(execute).toBeCalledTimes(10)
expect(rmRF).toBeCalledTimes(1)
})
@@ -371,7 +234,7 @@ describe('git', () => {
await deploy(action)
- expect(execute).toBeCalledTimes(12)
+ expect(execute).toBeCalledTimes(10)
expect(rmRF).toBeCalledTimes(1)
expect(mkdirP).toBeCalledTimes(1)
})
@@ -390,7 +253,7 @@ describe('git', () => {
})
const response = await deploy(action)
- expect(execute).toBeCalledTimes(13)
+ expect(execute).toBeCalledTimes(10)
expect(rmRF).toBeCalledTimes(1)
expect(response).toBe(Status.SKIPPED)
})
@@ -405,7 +268,6 @@ describe('git', () => {
folder: 'assets',
branch: 'branch',
gitHubToken: '123',
- lfs: true,
pusher: {
name: 'asd',
email: 'as@cat'
diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts
index 303e47980..8df7c3a67 100644
--- a/__tests__/main.test.ts
+++ b/__tests__/main.test.ts
@@ -47,7 +47,7 @@ describe('main', () => {
debug: true
})
await run(action)
- expect(execute).toBeCalledTimes(19)
+ expect(execute).toBeCalledTimes(12)
expect(rmRF).toBeCalledTimes(1)
expect(exportVariable).toBeCalledTimes(1)
})
@@ -64,7 +64,7 @@ describe('main', () => {
}
})
await run(action)
- expect(execute).toBeCalledTimes(18)
+ expect(execute).toBeCalledTimes(12)
expect(rmRF).toBeCalledTimes(1)
expect(exportVariable).toBeCalledTimes(1)
})
@@ -73,7 +73,6 @@ describe('main', () => {
Object.assign(action, {
folder: 'assets',
branch: 'branch',
- baseBranch: 'master',
gitHubToken: null,
ssh: null,
accessToken: null,
diff --git a/action.yml b/action.yml
index e2d30ae90..abd495e62 100644
--- a/action.yml
+++ b/action.yml
@@ -32,10 +32,6 @@ inputs:
description: 'If you would like to push the contents of the deployment folder into a specific directory on the deployment branch you can specify it here.'
required: false
- BASE_BRANCH:
- description: 'The base branch of your repository which you would like to checkout prior to deploying. This defaults to the current commit SHA that triggered the build followed by master if it does not exist. This is useful for making deployments from another branch, and also may be necessary when using a scheduled job.'
- required: false
-
COMMIT_MESSAGE:
description: 'If you need to customize the commit message for an integration you can do so.'
required: false
@@ -68,19 +64,11 @@ inputs:
SINGLE_COMMIT:
description: "This option can be used if you'd prefer to have a single commit on the deployment branch instead of maintaining the full history."
required: false
-
- LFS:
- description: "Migrates files from Git LFS so they can be comitted to the deployment branch."
- required: false
SILENT:
description: "Silences the action output preventing it from displaying git messages."
required: false
- PRESERVE:
- description: "Preserves and restores any workspace changes prior to deployment."
- required: false
-
outputs:
DEPLOYMENT_STATUS:
description: 'The status of the deployment that indicates if the run failed or passed. Possible outputs include: success|failed|skipped'
diff --git a/src/constants.ts b/src/constants.ts
index af15edcf8..f3ce8c105 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -8,8 +8,6 @@ const {pusher, repository} = github.context.payload
export interface ActionInterface {
/** Deployment access token. */
accessToken?: string | null
- /** The base branch that the deploy should be made from. */
- baseBranch?: string
/** The branch that the action should deploy to. */
branch: string
/** If your project generates hashed files on build you can use this option to automatically delete them from the deployment branch with each deploy. This option can be toggled on by setting it to true. */
@@ -18,8 +16,6 @@ export interface ActionInterface {
cleanExclude?: string | string[]
/** If you need to customize the commit message for an integration you can do so. */
commitMessage?: string
- /** The default branch of the deployment. Similar to baseBranch if you're using this action as a module. */
- defaultBranch?: string
/** The git config email. */
email?: string
/** The folder to deploy. */
@@ -30,12 +26,8 @@ export interface ActionInterface {
gitHubToken?: string | null
/** Determines if the action is running in test mode or not. */
isTest?: boolean | null
- /** Removes files from LFS if toggled to allow normal deployment. */
- lfs?: boolean | null
/** The git config name. */
name?: string
- /** Determines if the workspace should be stashed/restored prior to comitting. */
- preserve?: boolean | null
/** The repository path, for example JamesIves/github-pages-deploy-action. */
repositoryName?: string
/** The fully qualified repositpory path, this gets auto generated if repositoryName is provided. */
@@ -77,7 +69,6 @@ export interface NodeActionInterface {
/* Required action data that gets initialized when running within the GitHub Actions environment. */
export const action: ActionInterface = {
accessToken: getInput('ACCESS_TOKEN'),
- baseBranch: getInput('BASE_BRANCH'),
folder: getInput('FOLDER'),
branch: getInput('BRANCH'),
commitMessage: getInput('COMMIT_MESSAGE'),
@@ -85,13 +76,9 @@ export const action: ActionInterface = {
? getInput('CLEAN').toLowerCase() === 'true'
: false,
cleanExclude: getInput('CLEAN_EXCLUDE'),
- defaultBranch: process.env.GITHUB_SHA ? process.env.GITHUB_SHA : 'master',
isTest: process.env.UNIT_TEST
? process.env.UNIT_TEST.toLowerCase() === 'true'
: false,
- lfs: !isNullOrUndefined(getInput('LFS'))
- ? getInput('LFS').toLowerCase() === 'true'
- : false,
email: !isNullOrUndefined(getInput('GIT_CONFIG_EMAIL'))
? getInput('GIT_CONFIG_EMAIL')
: pusher && pusher.email
@@ -107,9 +94,6 @@ export const action: ActionInterface = {
: process.env.GITHUB_ACTOR
? process.env.GITHUB_ACTOR
: 'GitHub Pages Deploy Action',
- preserve: !isNullOrUndefined(getInput('PRESERVE'))
- ? getInput('PRESERVE').toLowerCase() === 'true'
- : false,
repositoryName: !isNullOrUndefined(getInput('REPOSITORY_NAME'))
? getInput('REPOSITORY_NAME')
: repository && repository.full_name
diff --git a/src/git.ts b/src/git.ts
index 2c421274d..6ab6131f6 100644
--- a/src/git.ts
+++ b/src/git.ts
@@ -11,7 +11,6 @@ export async function init(action: ActionInterface): Promise {
info(`Deploying using ${action.tokenType}… 🔑`)
info('Configuring git…')
- await execute(`git init`, action.workspace, action.silent)
await execute(
`git config user.name "${action.name}"`,
action.workspace,
@@ -23,33 +22,6 @@ export async function init(action: ActionInterface): Promise {
action.silent
)
- try {
- await execute(`git remote rm origin`, action.workspace, action.silent)
-
- if (action.isTest) {
- throw new Error()
- }
- } catch {
- info('Attempted to remove origin but failed, continuing…')
- }
-
- await execute(
- `git remote add origin ${action.repositoryPath}`,
- action.workspace,
- action.silent
- )
-
- if (action.preserve) {
- info(`Stashing workspace changes… ⬆️`)
- await execute(`git stash`, action.workspace, action.silent)
- }
-
- await execute(
- `git fetch --no-recurse-submodules`,
- action.workspace,
- action.silent
- )
-
info('Git configured… 🔧')
} catch (error) {
throw new Error(
@@ -61,34 +33,11 @@ export async function init(action: ActionInterface): Promise {
}
}
-/* Switches to the base branch. */
-export async function switchToBaseBranch(
- action: ActionInterface
-): Promise {
- try {
- await execute(
- `git checkout --progress --force ${
- action.baseBranch ? action.baseBranch : action.defaultBranch
- }`,
- action.workspace,
- action.silent
- )
- } catch (error) {
- throw new Error(
- `There was an error switching to the base branch: ${suppressSensitiveInformation(
- error.message,
- action
- )} ❌`
- )
- }
-}
-
/* Generates the branch if it doesn't exist on the remote. */
export async function generateBranch(action: ActionInterface): Promise {
try {
info(`Creating the ${action.branch} branch…`)
- await switchToBaseBranch(action)
await execute(
`git checkout --orphan ${action.branch}`,
action.workspace,
@@ -131,8 +80,8 @@ export async function deploy(action: ActionInterface): Promise {
try {
const commitMessage = !isNullOrUndefined(action.commitMessage)
? (action.commitMessage as string)
- : `Deploying to ${action.branch} from ${action.baseBranch} ${
- process.env.GITHUB_SHA ? `@ ${process.env.GITHUB_SHA}` : ''
+ : `Deploying to ${action.branch}${
+ process.env.GITHUB_SHA ? ` from @ ${process.env.GITHUB_SHA}` : ''
} 🚀`
/*
@@ -147,37 +96,14 @@ export async function deploy(action: ActionInterface): Promise {
if (!branchExists && !action.isTest) {
await generateBranch(action)
- }
-
- // Checks out the base branch to begin the deployment process.
- await switchToBaseBranch(action)
-
- await execute(
- `git fetch ${action.repositoryPath}`,
- action.workspace,
- action.silent
- )
-
- if (action.lfs) {
- // Migrates data from LFS so it can be comitted the "normal" way.
- info(`Migrating from Git LFS… ⚓`)
+ } else {
await execute(
- `git lfs migrate export --include="*" --yes`,
+ `git fetch --no-recurse-submodules --depth=1 origin ${action.branch}`,
action.workspace,
action.silent
)
}
- if (action.preserve) {
- info(`Applying stashed workspace changes… ⬆️`)
-
- try {
- await execute(`git stash apply`, action.workspace, action.silent)
- } catch {
- info('Unable to apply from stash, continuing…')
- }
- }
-
await execute(
`git worktree add --checkout ${temporaryDeploymentDirectory} origin/${action.branch}`,
action.workspace,
@@ -307,12 +233,6 @@ export async function deploy(action: ActionInterface): Promise {
info('Cleared git history… 🚿')
}
- await execute(
- `git checkout --progress --force ${action.defaultBranch}`,
- action.workspace,
- action.silent
- )
-
return Status.SUCCESS
} catch (error) {
throw new Error(
From c4d24de7a0c17f3426dc3bf6a3d558f28319f35d Mon Sep 17 00:00:00 2001
From: Axel Hecht
Date: Mon, 7 Dec 2020 16:12:15 +0100
Subject: [PATCH 02/32] Set up eslint for test files. (#517)
---
.eslintrc.json | 2 +-
__tests__/git.test.ts | 2 ++
__tests__/main.test.ts | 1 +
package.json | 2 +-
tsconfig.lint.json | 7 +++++++
5 files changed, 12 insertions(+), 2 deletions(-)
create mode 100644 tsconfig.lint.json
diff --git a/.eslintrc.json b/.eslintrc.json
index fa6b50dc5..7c438075d 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -5,7 +5,7 @@
"parserOptions": {
"ecmaVersion": 9,
"sourceType": "module",
- "project": "./tsconfig.json"
+ "project": "./tsconfig.lint.json"
},
"globals": {
"fetch": true
diff --git a/__tests__/git.test.ts b/__tests__/git.test.ts
index cdd282aeb..990ae0adb 100644
--- a/__tests__/git.test.ts
+++ b/__tests__/git.test.ts
@@ -1,3 +1,4 @@
+/* eslint-disable import/first */
// Initial env variable setup for tests.
process.env['INPUT_FOLDER'] = 'build'
process.env['GITHUB_SHA'] = '123'
@@ -23,6 +24,7 @@ jest.mock('@actions/io', () => ({
}))
jest.mock('../src/execute', () => ({
+ // eslint-disable-next-line @typescript-eslint/naming-convention
__esModule: true,
execute: jest.fn()
}))
diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts
index 8df7c3a67..7a475eb5b 100644
--- a/__tests__/main.test.ts
+++ b/__tests__/main.test.ts
@@ -1,3 +1,4 @@
+/* eslint-disable import/first */
// Initial env variable setup for tests.
process.env['INPUT_FOLDER'] = 'build'
process.env['GITHUB_SHA'] = '123'
diff --git a/package.json b/package.json
index 80fe0b03a..5b8de4825 100644
--- a/package.json
+++ b/package.json
@@ -9,7 +9,7 @@
"scripts": {
"build": "rimraf lib && tsc --declaration",
"test": "jest",
- "lint": "eslint src/**/*.ts",
+ "lint": "eslint src/**/*.ts __tests__/**/*.ts",
"format": "prettier --write './**/*.ts'"
},
"repository": {
diff --git a/tsconfig.lint.json b/tsconfig.lint.json
new file mode 100644
index 000000000..be0508f77
--- /dev/null
+++ b/tsconfig.lint.json
@@ -0,0 +1,7 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "rootDir": "."
+ },
+ "exclude": ["node_modules"]
+}
From 951989533aa8fa697ba99561c237edef9cb008f1 Mon Sep 17 00:00:00 2001
From: Axel Hecht
Date: Tue, 8 Dec 2020 16:14:59 +0100
Subject: [PATCH 03/32] Add DRY_RUN option, passing --dry-run to git push.
(#526)
See #499 for the proposal.
---
README.md | 1 +
__tests__/git.test.ts | 33 +++++++++++++++++++++++++++++++++
src/constants.ts | 5 +++++
src/git.ts | 8 +++++---
4 files changed, 44 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index aa00c41f1..36e310518 100644
--- a/README.md
+++ b/README.md
@@ -148,6 +148,7 @@ In addition to the deployment options you must also configure the following.
| `COMMIT_MESSAGE` | If you need to customize the commit message for an integration you can do so. | `with` | **No** |
| `CLEAN` | If your project generates hashed files on build you can use this option to automatically delete them from the deployment branch with each deploy. This option is turned on by default, and can be toggled off by setting it to `false`. | `with` | **No** |
| `CLEAN_EXCLUDE` | If you need to use `CLEAN` but you'd like to preserve certain files or folders you can use this option. This should be formatted as an array but stored as a string. For example: `'["filename.js", "folder"]'` | `with` | **No** |
+| `DRY_RUN` | Do not actually push back, but use `--dry-run` on `git push` invocations insead. | `with` | **No** |
| `SINGLE_COMMIT` | This option can be toggled to `true` if you'd prefer to have a single commit on the deployment branch instead of maintaining the full history. **Using this option will also cause any existing history to be wiped from the deployment branch**. | `with` | **No** |
| `SILENT` | Silences the action output preventing it from displaying git messages. | `with` | **No** |
| `WORKSPACE` | This should point to where your project lives on the virtual machine. The GitHub Actions environment will set this for you. It is only necessary to set this variable if you're using the node module. | `with` | **No** |
diff --git a/__tests__/git.test.ts b/__tests__/git.test.ts
index 990ae0adb..ce3fda76f 100644
--- a/__tests__/git.test.ts
+++ b/__tests__/git.test.ts
@@ -123,6 +123,39 @@ describe('git', () => {
// Includes the call to generateBranch
expect(execute).toBeCalledTimes(10)
+ expect(execute).toHaveBeenNthCalledWith(
+ 9,
+ expect.not.stringContaining('--dry-run'),
+ expect.anything(),
+ false
+ )
+ expect(rmRF).toBeCalledTimes(1)
+ expect(response).toBe(Status.SUCCESS)
+ })
+
+ it('should push with --dry-run', async () => {
+ Object.assign(action, {
+ silent: false,
+ dryRun: true,
+ folder: 'assets',
+ branch: 'branch',
+ gitHubToken: '123',
+ pusher: {
+ name: 'asd',
+ email: 'as@cat'
+ }
+ })
+
+ const response = await deploy(action)
+
+ // Includes the call to generateBranch
+ expect(execute).toBeCalledTimes(10)
+ expect(execute).toHaveBeenNthCalledWith(
+ 9,
+ expect.stringContaining('--dry-run'),
+ expect.anything(),
+ false
+ )
expect(rmRF).toBeCalledTimes(1)
expect(response).toBe(Status.SUCCESS)
})
diff --git a/src/constants.ts b/src/constants.ts
index f3ce8c105..63558f8b5 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -10,6 +10,8 @@ export interface ActionInterface {
accessToken?: string | null
/** The branch that the action should deploy to. */
branch: string
+ /** git push with --dry-run */
+ dryRun?: boolean | null
/** If your project generates hashed files on build you can use this option to automatically delete them from the deployment branch with each deploy. This option can be toggled on by setting it to true. */
clean?: boolean | null
/** If you need to use CLEAN but you'd like to preserve certain files or folders you can use this option. */
@@ -72,6 +74,9 @@ export const action: ActionInterface = {
folder: getInput('FOLDER'),
branch: getInput('BRANCH'),
commitMessage: getInput('COMMIT_MESSAGE'),
+ dryRun: !isNullOrUndefined(getInput('DRY_RUN'))
+ ? getInput('DRY_RUN').toLowerCase() === 'true'
+ : false,
clean: !isNullOrUndefined(getInput('CLEAN'))
? getInput('CLEAN').toLowerCase() === 'true'
: false,
diff --git a/src/git.ts b/src/git.ts
index 6ab6131f6..de35d3e7c 100644
--- a/src/git.ts
+++ b/src/git.ts
@@ -49,8 +49,9 @@ export async function generateBranch(action: ActionInterface): Promise {
action.workspace,
action.silent
)
+ const dry = action.dryRun ? '--dry-run ' : ''
await execute(
- `git push --force ${action.repositoryPath} ${action.branch}`,
+ `git push --force ${dry}${action.repositoryPath} ${action.branch}`,
action.workspace,
action.silent
)
@@ -74,6 +75,7 @@ export async function deploy(action: ActionInterface): Promise {
const temporaryDeploymentBranch = `github-pages-deploy-action/${Math.random()
.toString(36)
.substr(2, 9)}`
+ const dry = action.dryRun ? '--dry-run ' : ''
info('Starting to commit changes…')
@@ -191,7 +193,7 @@ export async function deploy(action: ActionInterface): Promise {
action.silent
)
await execute(
- `git push --force ${action.repositoryPath} ${temporaryDeploymentBranch}:${action.branch}`,
+ `git push --force ${dry}${action.repositoryPath} ${temporaryDeploymentBranch}:${action.branch}`,
`${action.workspace}/${temporaryDeploymentDirectory}`,
action.silent
)
@@ -225,7 +227,7 @@ export async function deploy(action: ActionInterface): Promise {
action.silent
)
await execute(
- `git push origin ${action.branch} --force`,
+ `git push origin ${action.branch} ${dry}--force`,
`${action.workspace}/${temporaryDeploymentDirectory}`,
action.silent
)
From 7a948416001d0886e19bd2575d9010b58e721198 Mon Sep 17 00:00:00 2001
From: James Ives
Date: Thu, 10 Dec 2020 11:49:37 -0500
Subject: [PATCH 04/32] Simplifies Token Setup (#530)
* Token simplification
* Access Token / Github Token -> Token
* Oops
* Typos
* Update README.md
* Update README.md
* Update action.yml
Co-authored-by: Axel Hecht
* Update README.md
Co-authored-by: Axel Hecht
* Update README.md
Co-authored-by: Axel Hecht
---
README.md | 23 +++++------
__tests__/git.test.ts | 24 ++++++------
__tests__/main.test.ts | 7 ++--
__tests__/util.test.ts | 86 +++++++++++-------------------------------
action.yml | 15 +++++---
src/constants.ts | 19 ++++------
src/util.ts | 24 ++++--------
7 files changed, 73 insertions(+), 125 deletions(-)
diff --git a/README.md b/README.md
index 36e310518..e5a017a59 100644
--- a/README.md
+++ b/README.md
@@ -64,10 +64,8 @@ jobs:
- name: Deploy 🚀
uses: JamesIves/github-pages-deploy-action@3.7.1
with:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BRANCH: gh-pages # The branch the action should deploy to.
FOLDER: build # The folder the action should deploy.
- CLEAN: true # Automatically remove deleted files from the deploy branch
```
If you'd like to make it so the workflow only triggers on push events to specific branches then you can modify the `on` section.
@@ -105,7 +103,7 @@ Calling the functions directly will require you to pass in an object containing
import run from "github-pages-deploy-action";
run({
- accessToken: process.env["ACCESS_TOKEN"],
+ token: process.env["ACCESS_TOKEN"],
branch: "gh-pages",
folder: "build",
repositoryName: "JamesIves/github-pages-deploy-action",
@@ -122,28 +120,27 @@ The `with` portion of the workflow **must** be configured before the action will
#### Required Setup
-One of the following deployment options must be configured.
-
-| Key | Value Information | Type | Required |
-| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | -------- |
-| `GITHUB_TOKEN` | In order for GitHub to trigger the rebuild of your page you must provide the action with the repository's provided GitHub token. This can be referenced in the workflow `yml` file by using `${{ secrets.GITHUB_TOKEN }}`. If you experience any issues with your changes not being reflected after the deployment it may be neccersary to use either the `SSH` or `ACCESS_TOKEN` options. | `secrets / with` | **Yes** |
-| `SSH` | You can configure the action to deploy using SSH by setting this option to `true`. For more information on how to add your ssh key pair please refer to the [Using a Deploy Key section of this README](https://github.com/JamesIves/github-pages-deploy-action/tree/dev#using-an-ssh-deploy-key-). | `with` | **Yes** |
-| `ACCESS_TOKEN` | Depending on the repository's permissions you may need to provide the action with a GitHub personal access token instead of the provided GitHub token in order to deploy. You can [learn more about how to generate one here](https://help.github.com/en/articles/creating-a-personal-access-token-for-the-command-line). **This should be stored as a secret**. | `secrets / with` | **Yes** |
-
-In addition to the deployment options you must also configure the following.
+The following options must be configured in order to make a deployment.
| Key | Value Information | Type | Required |
| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | -------- |
| `BRANCH` | This is the branch you wish to deploy to, for example `gh-pages` or `docs`. | `with` | **Yes** |
| `FOLDER` | The folder in your repository that you want to deploy. If your build script compiles into a directory named `build` you'd put it here. If you wish to deploy the root directory you can place a `.` here. You can also utilize absolute file paths by appending `~` to your folder path. | `with` | **Yes** |
+By default the action does not need any token configuration and uses the provided repository scoped GitHub token to make the deployment. If you require most customization you can modify the deployment type using the following options.
+
+| Key | Value Information | Type | Required |
+| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | -------- |
+| `TOKEN` | This option defaults to the repository scoped GitHub Token. However if you need more permissions for things such as deploying to another repository, you can add a Personal Access Token (PAT) here. This should be stored in the `secrets / with` menu **as a secret**. We reccomend using a service account with the least permissions neccersary and recommend when generating a new PAT that you select the least permission scopes neccersary. [Learn more about creating and using encrypted secrets here.](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets) | **No** |
+| `SSH` | You can configure the action to deploy using SSH by setting this option to `true`. For more information on how to add your ssh key pair please refer to the [Using a Deploy Key section of this README](https://github.com/JamesIves/github-pages-deploy-action/tree/dev#using-an-ssh-deploy-key-). | `with` | **No** |
+
#### Optional Choices
| Key | Value Information | Type | Required |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | -------- |
| `GIT_CONFIG_NAME` | Allows you to customize the name that is attached to the git config which is used when pushing the deployment commits. If this is not included it will use the name in the GitHub context, followed by the name of the action. | `with` | **No** |
| `GIT_CONFIG_EMAIL` | Allows you to customize the email that is attached to the git config which is used when pushing the deployment commits. If this is not included it will use the email in the GitHub context, followed by a generic noreply GitHub email. | `with` | **No** |
-| `REPOSITORY_NAME` | Allows you to specify a different repository path so long as you have permissions to push to it. This should be formatted like so: `JamesIves/github-pages-deploy-action`. You'll need to use an `ACCESS_TOKEN` for this configuration option to work properly. | `with` | **No** |
+| `REPOSITORY_NAME` | Allows you to specify a different repository path so long as you have permissions to push to it. This should be formatted like so: `JamesIves/github-pages-deploy-action`. You'll need to use a PAT in the `TOKEN` input for this configuration option to work properly. | `with` | **No** |
| `TARGET_FOLDER` | If you'd like to push the contents of the deployment folder into a specific directory on the deployment branch you can specify it here. | `with` | **No** |
| `COMMIT_MESSAGE` | If you need to customize the commit message for an integration you can do so. | `with` | **No** |
| `CLEAN` | If your project generates hashed files on build you can use this option to automatically delete them from the deployment branch with each deploy. This option is turned on by default, and can be toggled off by setting it to `false`. | `with` | **No** |
diff --git a/__tests__/git.test.ts b/__tests__/git.test.ts
index ce3fda76f..6a47a1fd0 100644
--- a/__tests__/git.test.ts
+++ b/__tests__/git.test.ts
@@ -43,7 +43,7 @@ describe('git', () => {
Object.assign(action, {
silent: false,
repositoryPath: 'JamesIves/github-pages-deploy-action',
- accessToken: '123',
+ token: '123',
branch: 'branch',
folder: '.',
isTest: true,
@@ -67,7 +67,7 @@ describe('git', () => {
it('should execute five commands', async () => {
Object.assign(action, {
silent: false,
- accessToken: '123',
+ token: '123',
branch: 'branch',
folder: '.',
pusher: {
@@ -87,7 +87,7 @@ describe('git', () => {
Object.assign(action, {
silent: false,
- accessToken: '123',
+ token: '123',
branch: 'branch',
folder: '.',
pusher: {
@@ -112,7 +112,7 @@ describe('git', () => {
silent: false,
folder: 'assets',
branch: 'branch',
- gitHubToken: '123',
+ token: '123',
pusher: {
name: 'asd',
email: 'as@cat'
@@ -139,7 +139,7 @@ describe('git', () => {
dryRun: true,
folder: 'assets',
branch: 'branch',
- gitHubToken: '123',
+ token: '123',
pusher: {
name: 'asd',
email: 'as@cat'
@@ -166,7 +166,7 @@ describe('git', () => {
folder: 'other',
folderPath: 'other',
branch: 'branch',
- gitHubToken: '123',
+ token: '123',
singleCommit: true,
pusher: {
name: 'asd',
@@ -188,7 +188,7 @@ describe('git', () => {
folder: 'assets',
folderPath: 'assets',
branch: 'branch',
- gitHubToken: '123',
+ token: '123',
pusher: {
name: 'asd',
email: 'as@cat'
@@ -214,7 +214,7 @@ describe('git', () => {
folder: 'other',
folderPath: 'other',
branch: 'branch',
- gitHubToken: '123',
+ token: '123',
pusher: {
name: 'asd',
email: 'as@cat'
@@ -237,7 +237,7 @@ describe('git', () => {
folder: 'assets',
folderPath: 'assets',
branch: 'branch',
- gitHubToken: '123',
+ token: '123',
pusher: {
name: 'asd',
email: 'as@cat'
@@ -258,7 +258,7 @@ describe('git', () => {
silent: false,
folder: '.',
branch: 'branch',
- gitHubToken: '123',
+ token: '123',
pusher: {},
clean: true,
targetFolder: 'new_folder',
@@ -279,7 +279,7 @@ describe('git', () => {
silent: false,
folder: 'assets',
branch: 'branch',
- gitHubToken: '123',
+ token: '123',
pusher: {
name: 'asd',
email: 'as@cat'
@@ -302,7 +302,7 @@ describe('git', () => {
silent: false,
folder: 'assets',
branch: 'branch',
- gitHubToken: '123',
+ token: '123',
pusher: {
name: 'asd',
email: 'as@cat'
diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts
index 7a475eb5b..6044032fc 100644
--- a/__tests__/main.test.ts
+++ b/__tests__/main.test.ts
@@ -39,7 +39,7 @@ describe('main', () => {
repositoryPath: 'JamesIves/github-pages-deploy-action',
folder: 'assets',
branch: 'branch',
- gitHubToken: '123',
+ token: '123',
pusher: {
name: 'asd',
email: 'as@cat'
@@ -58,7 +58,7 @@ describe('main', () => {
repositoryPath: 'JamesIves/github-pages-deploy-action',
folder: 'assets',
branch: 'branch',
- gitHubToken: '123',
+ token: '123',
pusher: {
name: 'asd',
email: 'as@cat'
@@ -74,9 +74,8 @@ describe('main', () => {
Object.assign(action, {
folder: 'assets',
branch: 'branch',
- gitHubToken: null,
+ token: null,
ssh: null,
- accessToken: null,
pusher: {
name: 'asd',
email: 'as@cat'
diff --git a/__tests__/util.test.ts b/__tests__/util.test.ts
index 1f21cf396..dd37eff02 100644
--- a/__tests__/util.test.ts
+++ b/__tests__/util.test.ts
@@ -37,38 +37,23 @@ describe('util', () => {
branch: '123',
workspace: 'src/',
folder: 'build',
- gitHubToken: null,
- accessToken: null,
+ token: null,
ssh: true,
silent: false
}
expect(generateTokenType(action)).toEqual('SSH Deploy Key')
})
- it('should return access token if access token is provided', async () => {
+ it('should return deploy token if token is provided', async () => {
const action = {
branch: '123',
workspace: 'src/',
folder: 'build',
- gitHubToken: null,
- accessToken: '123',
+ token: '123',
ssh: null,
silent: false
}
- expect(generateTokenType(action)).toEqual('Access Token')
- })
-
- it('should return github token if github token is provided', async () => {
- const action = {
- branch: '123',
- workspace: 'src/',
- folder: 'build',
- gitHubToken: '123',
- accessToken: null,
- ssh: null,
- silent: false
- }
- expect(generateTokenType(action)).toEqual('GitHub Token')
+ expect(generateTokenType(action)).toEqual('Deploy Token')
})
it('should return ... if no token is provided', async () => {
@@ -76,8 +61,7 @@ describe('util', () => {
branch: '123',
workspace: 'src/',
folder: 'build',
- gitHubToken: null,
- accessToken: null,
+ token: null,
ssh: null,
silent: false
}
@@ -92,8 +76,7 @@ describe('util', () => {
branch: '123',
workspace: 'src/',
folder: 'build',
- gitHubToken: null,
- accessToken: null,
+ token: null,
ssh: true,
silent: false
}
@@ -102,30 +85,13 @@ describe('util', () => {
)
})
- it('should return https if access token is provided', async () => {
- const action = {
- repositoryName: 'JamesIves/github-pages-deploy-action',
- branch: '123',
- workspace: 'src/',
- folder: 'build',
- gitHubToken: null,
- accessToken: '123',
- ssh: null,
- silent: false
- }
- expect(generateRepositoryPath(action)).toEqual(
- 'https://123@github.com/JamesIves/github-pages-deploy-action.git'
- )
- })
-
- it('should return https with x-access-token if github token is provided', async () => {
+ it('should return https with x-access-token if deploy token is provided', async () => {
const action = {
repositoryName: 'JamesIves/github-pages-deploy-action',
branch: '123',
workspace: 'src/',
folder: 'build',
- gitHubToken: '123',
- accessToken: null,
+ token: '123',
ssh: null,
silent: false
}
@@ -143,14 +109,13 @@ describe('util', () => {
branch: '123',
workspace: 'src/',
folder: 'build',
- accessToken: 'supersecret999%%%',
- gitHubToken: 'anothersecret123333',
+ token: 'anothersecret123333',
silent: false
}
- const string = `This is an error message! It contains ${action.accessToken} and ${action.gitHubToken} and ${action.repositoryPath} and ${action.accessToken} again!`
+ const string = `This is an error message! It contains ${action.token} and ${action.repositoryPath} and ${action.token} again!`
expect(suppressSensitiveInformation(string, action)).toBe(
- 'This is an error message! It contains *** and *** and *** and *** again!'
+ 'This is an error message! It contains *** and *** and *** again!'
)
})
@@ -162,16 +127,15 @@ describe('util', () => {
branch: '123',
workspace: 'src/',
folder: 'build',
- accessToken: 'supersecret999%%%',
- gitHubToken: 'anothersecret123333',
+ token: 'anothersecret123333',
silent: false
}
process.env['RUNNER_DEBUG'] = '1'
- const string = `This is an error message! It contains ${action.accessToken} and ${action.gitHubToken} and ${action.repositoryPath}`
+ const string = `This is an error message! It contains ${action.token} and ${action.repositoryPath}`
expect(suppressSensitiveInformation(string, action)).toBe(
- 'This is an error message! It contains supersecret999%%% and anothersecret123333 and https://x-access-token:supersecret999%%%@github.com/anothersecret123333'
+ 'This is an error message! It contains anothersecret123333 and https://x-access-token:supersecret999%%%@github.com/anothersecret123333'
)
})
})
@@ -183,8 +147,7 @@ describe('util', () => {
branch: '123',
workspace: 'src/',
folder: 'build',
- gitHubToken: null,
- accessToken: null,
+ token: null,
ssh: null,
silent: false
}
@@ -196,8 +159,7 @@ describe('util', () => {
branch: '123',
workspace: 'src/',
folder: '/home/user/repo/build',
- gitHubToken: null,
- accessToken: null,
+ token: null,
ssh: null,
silent: false
}
@@ -209,8 +171,7 @@ describe('util', () => {
branch: '123',
workspace: 'src/',
folder: './build',
- gitHubToken: null,
- accessToken: null,
+ token: null,
ssh: null,
silent: false
}
@@ -222,8 +183,7 @@ describe('util', () => {
branch: '123',
workspace: 'src/',
folder: '~/repo/build',
- gitHubToken: null,
- accessToken: null,
+ token: null,
ssh: null,
silent: false
}
@@ -251,11 +211,11 @@ describe('util', () => {
}
})
- it('should fail if access token is defined but it is an empty string', () => {
+ it('should fail if token is defined but it is an empty string', () => {
const action = {
silent: false,
repositoryPath: undefined,
- accessToken: '',
+ token: '',
branch: 'branch',
folder: 'build',
workspace: 'src/'
@@ -274,7 +234,7 @@ describe('util', () => {
const action = {
silent: false,
repositoryPath: undefined,
- accessToken: '123',
+ token: '123',
branch: '',
folder: 'build',
workspace: 'src/'
@@ -291,7 +251,7 @@ describe('util', () => {
const action = {
silent: false,
repositoryPath: undefined,
- gitHubToken: '123',
+ token: '123',
branch: 'branch',
folder: '',
workspace: 'src/'
@@ -310,7 +270,7 @@ describe('util', () => {
const action: ActionInterface = {
silent: false,
repositoryPath: undefined,
- gitHubToken: '123',
+ token: '123',
branch: 'branch',
folder: 'notARealFolder',
workspace: '.'
diff --git a/action.yml b/action.yml
index abd495e62..2383a022a 100644
--- a/action.yml
+++ b/action.yml
@@ -12,13 +12,18 @@ inputs:
description: 'You can configure the action to deploy using SSH by setting this option to true. More more information on how to add your ssh key pair please refer to the Using a Deploy Key section of this README.'
required: false
- ACCESS_TOKEN:
- description: 'Depending on the repository permissions you may need to provide the action with a GitHub personal access token instead of the provided GitHub token in order to deploy. This should be stored as a secret.'
- required: false
+ TOKEN:
+ description: >
+ This option defaults to the repository scoped GitHub Token.
+ However if you need more permissions for things such as deploying to another repository, you can add a Personal Access Token (PAT) here.
+ This should be stored in the `secrets / with` menu **as a secret**.
- GITHUB_TOKEN:
- description: 'In order for GitHub to trigger the rebuild of your page you must provide the action with the repositories provided GitHub token.'
+ We recommend using a service account with the least permissions neccersary
+ and when generating a new PAT that you select the least permission scopes required.
+
+ [Learn more about creating and using encrypted secrets here.](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets)
required: false
+ default: ${{ github.token }}
BRANCH:
description: 'This is the branch you wish to deploy to, for example gh-pages or docs.'
diff --git a/src/constants.ts b/src/constants.ts
index 63558f8b5..0b85dcde8 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -6,8 +6,6 @@ const {pusher, repository} = github.context.payload
/* For more information please refer to the README: https://github.com/JamesIves/github-pages-deploy-action */
export interface ActionInterface {
- /** Deployment access token. */
- accessToken?: string | null
/** The branch that the action should deploy to. */
branch: string
/** git push with --dry-run */
@@ -24,8 +22,6 @@ export interface ActionInterface {
folder: string
/** The auto generated folder path. */
folderPath?: string
- /** GitHub deployment token. */
- gitHubToken?: string | null
/** Determines if the action is running in test mode or not. */
isTest?: boolean | null
/** The git config name. */
@@ -42,7 +38,9 @@ export interface ActionInterface {
ssh?: boolean | null
/** If you'd like to push the contents of the deployment folder into a specific directory on the deployment branch you can specify it here. */
targetFolder?: string
- /** The token type, ie ssh/github token/access token, this gets automatically generated. */
+ /** Deployment token. */
+ token?: string | null
+ /** The token type, ie ssh/token, this gets automatically generated. */
tokenType?: string
/** The folder where your deployment project lives. */
workspace: string
@@ -50,16 +48,14 @@ export interface ActionInterface {
/** The minimum required values to run the action as a node module. */
export interface NodeActionInterface {
- /** Deployment access token. */
- accessToken?: string | null
/** The branch that the action should deploy to. */
branch: string
/** The folder to deploy. */
folder: string
- /** GitHub deployment token. */
- gitHubToken?: string | null
/** The repository path, for example JamesIves/github-pages-deploy-action. */
repositoryName: string
+ /** GitHub deployment token. */
+ token?: string | null
/** Determines if the action should run in silent mode or not. */
silent: boolean
/** Set to true if you're using an ssh client in your build step. */
@@ -70,7 +66,6 @@ export interface NodeActionInterface {
/* Required action data that gets initialized when running within the GitHub Actions environment. */
export const action: ActionInterface = {
- accessToken: getInput('ACCESS_TOKEN'),
folder: getInput('FOLDER'),
branch: getInput('BRANCH'),
commitMessage: getInput('COMMIT_MESSAGE'),
@@ -91,7 +86,6 @@ export const action: ActionInterface = {
: `${
process.env.GITHUB_ACTOR || 'github-pages-deploy-action'
}@users.noreply.github.com`,
- gitHubToken: getInput('GITHUB_TOKEN'),
name: !isNullOrUndefined(getInput('GIT_CONFIG_NAME'))
? getInput('GIT_CONFIG_NAME')
: pusher && pusher.name
@@ -104,6 +98,7 @@ export const action: ActionInterface = {
: repository && repository.full_name
? repository.full_name
: process.env.GITHUB_REPOSITORY,
+ token: getInput('TOKEN'),
singleCommit: !isNullOrUndefined(getInput('SINGLE_COMMIT'))
? getInput('SINGLE_COMMIT').toLowerCase() === 'true'
: false,
@@ -120,7 +115,7 @@ export const action: ActionInterface = {
/** Types for the required action parameters. */
export type RequiredActionParameters = Pick<
ActionInterface,
- 'accessToken' | 'gitHubToken' | 'ssh' | 'branch' | 'folder'
+ 'token' | 'ssh' | 'branch' | 'folder'
>
/** Status codes for the action. */
diff --git a/src/util.ts b/src/util.ts
index cde4c3de2..d43a1e09b 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -13,21 +13,15 @@ export const isNullOrUndefined = (value: any): boolean =>
/* Generates a token type used for the action. */
export const generateTokenType = (action: ActionInterface): string =>
- action.ssh
- ? 'SSH Deploy Key'
- : action.accessToken
- ? 'Access Token'
- : action.gitHubToken
- ? 'GitHub Token'
- : '…'
+ action.ssh ? 'SSH Deploy Key' : action.token ? 'Deploy Token' : '…'
/* Generates a the repository path used to make the commits. */
export const generateRepositoryPath = (action: ActionInterface): string =>
action.ssh
? `git@github.com:${action.repositoryName}`
- : `https://${
- action.accessToken || `x-access-token:${action.gitHubToken}`
- }@github.com/${action.repositoryName}.git`
+ : `https://${`x-access-token:${action.token}`}@github.com/${
+ action.repositoryName
+ }.git`
/* Genetate absolute folder path by the provided folder name */
export const generateFolderPath = (action: ActionInterface): string => {
@@ -52,7 +46,7 @@ const hasRequiredParameters = (
/* Verifies the action has the required parameters to run, otherwise throw an error. */
export const checkParameters = (action: ActionInterface): void => {
- if (!hasRequiredParameters(action, ['accessToken', 'gitHubToken', 'ssh'])) {
+ if (!hasRequiredParameters(action, ['token', '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.'
)
@@ -85,11 +79,9 @@ export const suppressSensitiveInformation = (
return value
}
- const orderedByLength = ([
- action.accessToken,
- action.gitHubToken,
- action.repositoryPath
- ].filter(Boolean) as string[]).sort((a, b) => b.length - a.length)
+ const orderedByLength = ([action.token, action.repositoryPath].filter(
+ Boolean
+ ) as string[]).sort((a, b) => b.length - a.length)
for (const find of orderedByLength) {
value = replaceAll(value, find, '***')
From a92aacd4ba996245acc0b075ffaf48daf2bf0b7d Mon Sep 17 00:00:00 2001
From: Axel Hecht
Date: Fri, 11 Dec 2020 15:36:21 +0100
Subject: [PATCH 05/32] Adjust codeql action to latest recommendations (#540)
Also, add the dev and release branches, and drop master.
---
.github/workflows/codeql-analysis.yml | 18 +++++++-----------
1 file changed, 7 insertions(+), 11 deletions(-)
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index acb70a291..1d7daa307 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -2,10 +2,15 @@ name: "CodeQL"
on:
push:
- branches: [dev, master, releases/v2, releases/v3]
+ branches:
+ - dev
+ - 'dev-v*'
+ - 'releases/v*'
pull_request:
# The branches below must be a subset of the branches above
- branches: [dev]
+ branches:
+ - dev
+ - 'dev-v*'
schedule:
- cron: '0 9 * * 4'
@@ -17,15 +22,6 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
- with:
- # We must fetch at least the immediate parents so that if this is
- # a pull request then we can checkout the head.
- fetch-depth: 2
-
- # If this run was triggered by a pull request event, then checkout
- # the head of the pull request instead of the merge commit.
- - run: git checkout HEAD^2
- if: ${{ github.event_name == 'pull_request' }}
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
From 0da20f04b8c317fc00861e2fe71106a59f4ac38f Mon Sep 17 00:00:00 2001
From: Axel Hecht
Date: Mon, 14 Dec 2020 16:26:48 +0100
Subject: [PATCH 06/32] Add workflow to update build and node_modules on
release branches (#541)
---
.github/workflows/production.yml | 50 ++++++++++++++++++++++++++++++++
1 file changed, 50 insertions(+)
create mode 100644 .github/workflows/production.yml
diff --git a/.github/workflows/production.yml b/.github/workflows/production.yml
new file mode 100644
index 000000000..55247f28a
--- /dev/null
+++ b/.github/workflows/production.yml
@@ -0,0 +1,50 @@
+name: Land production js and modules
+on:
+ workflow_dispatch:
+ push:
+ branches:
+ - 'releases/v*'
+ tags-ignore:
+ - '*.*'
+
+jobs:
+ build:
+ name: Build production
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - uses: actions/setup-node@v1.4.4
+ with:
+ node-version: 'v12.18.4'
+ registry-url: 'https://registry.npmjs.org'
+
+ - name: Install Yarn
+ run: npm install -g yarn
+
+ - name: Clobber lib
+ run: rm -rf lib
+
+ - name: Set up .gitignore
+ run: |
+ sed -i -e's/^lib/# lib/' -e's/^node_module/# node_modules/' .gitignore
+
+ - name: Build
+ run: |
+ yarn install
+ yarn build
+
+ - name: Production node_modules
+ run: |
+ yarn install --production
+
+ - name: Commit and push
+ # keep the run green if the commit fails for the lack of changes
+ continue-on-error: True
+ run: |
+ git config user.email "iam@jamesiv.es"
+ git config user.name "James Ives"
+ git add .
+ git commit -m"Deploy production js code for ${{ github.sha }}"
+ git push
From 27009d7a9627b0063ec5957bd75621a31a77984f Mon Sep 17 00:00:00 2001
From: James Ives
Date: Mon, 14 Dec 2020 10:31:54 -0500
Subject: [PATCH 07/32] Stores username/email in secrets
---
.github/workflows/codeql-analysis.yml | 1 -
.github/workflows/production.yml | 14 +++++++-------
2 files changed, 7 insertions(+), 8 deletions(-)
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 1d7daa307..4abe55af3 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -1,5 +1,4 @@
name: "CodeQL"
-
on:
push:
branches:
diff --git a/.github/workflows/production.yml b/.github/workflows/production.yml
index 55247f28a..28ea37128 100644
--- a/.github/workflows/production.yml
+++ b/.github/workflows/production.yml
@@ -1,4 +1,4 @@
-name: Land production js and modules
+name: Deploy Production Dependencies and Code
on:
workflow_dispatch:
push:
@@ -35,16 +35,16 @@ jobs:
yarn install
yarn build
- - name: Production node_modules
+ - name: Install Production node_modules
run: |
yarn install --production
- - name: Commit and push
- # keep the run green if the commit fails for the lack of changes
+ - name: Commit and Push
+ # Keep the run green if the commit fails for the lack of changes
continue-on-error: True
run: |
- git config user.email "iam@jamesiv.es"
- git config user.name "James Ives"
+ git config user.email "${{ secrets.GIT_CONFIG_EMAIL }}"
+ git config user.name "${{ secrets.GIT_CONFIG_NAME }}"
git add .
- git commit -m"Deploy production js code for ${{ github.sha }}"
+ git commit -m "Deploy Production Code for Commit ${{ github.sha }} 🚀"
git push
From ac885860a854923a477105dc2d46bd33c63f34b7 Mon Sep 17 00:00:00 2001
From: James Ives
Date: Mon, 14 Dec 2020 10:33:37 -0500
Subject: [PATCH 08/32] Removing stale bot integration
---
.github/stale.yml | 17 -----------------
1 file changed, 17 deletions(-)
delete mode 100644 .github/stale.yml
diff --git a/.github/stale.yml b/.github/stale.yml
deleted file mode 100644
index dc90e5a1c..000000000
--- a/.github/stale.yml
+++ /dev/null
@@ -1,17 +0,0 @@
-# Number of days of inactivity before an issue becomes stale
-daysUntilStale: 60
-# Number of days of inactivity before a stale issue is closed
-daysUntilClose: 7
-# Issues with these labels will never be considered stale
-exemptLabels:
- - pinned
- - security
-# Label to use when marking an issue as stale
-staleLabel: wontfix
-# Comment to post when marking an issue as stale. Set to `false` to disable
-markComment: >
- This issue has been automatically marked as stale because it has not had
- recent activity. It will be closed if no further activity occurs. Thank you
- for your contributions.
-# Comment to post when closing a stale issue. Set to `false` to disable
-closeComment: false
From 4e40ddd3f56be95bb196db27a0119da877200e72 Mon Sep 17 00:00:00 2001
From: Axel Hecht
Date: Mon, 14 Dec 2020 18:30:22 +0100
Subject: [PATCH 09/32] Test current code base as an integration test for PRs
and pushes (#505)
* Add a build step to create lib and node_modules artifact
* Run integration test with built dist and current SHA as base
For pull requests, the github.sha is the sha of the merge to the
target branch, not the head of the PR. Special case that.
* Use v2 checkout, and DRY_RUN for the integration test.
I also made the branches more generic, as there are now more of them.
* Fix #536, don't push at all on dryRun
Also add tests for dryRun and singleCommit and generateBranch
code flows.
* Try to fix dryRun on new remote branches, refactor fetch
* Try to fix dryRun, only fetch if origin branch exists
* Refactor worktree setup to include branch generation and setup for singleCommit
This is a continuation of the no-checkout work, and sadly suggested pretty
intensive changes.
* Set up git config to fix tests, also make debugging easier
* Add matrix for existing and non-existing branch
* Add matrix for singleCommit and not
* Drop GITHUB_TOKEN, add DRY_RUN to action.yml
* When deploying existing branch, add a modifcation and deploy again
* Force branch checkout to work in redeployment scenarios
* Make singleCommit easier to see in job descriptions
* Review comments
* Add a test-only property to action to test code paths with remote branch.
* Introduce TestFlag enum to signal different test scenarios to unit tests
* Fix util.test.ts
---
.eslintrc.json | 5 +-
.github/workflows/build.yml | 85 +++++++++++++-
__tests__/env.js | 3 +-
__tests__/git.test.ts | 147 ++++++++++++-----------
__tests__/main.test.ts | 13 ++-
__tests__/util.test.ts | 50 +++++---
__tests__/worktree.error.test.ts | 35 ++++++
__tests__/worktree.test.ts | 195 +++++++++++++++++++++++++++++++
action.yml | 4 +
src/constants.ts | 18 ++-
src/git.ts | 131 +++++----------------
src/worktree.ts | 85 ++++++++++++++
12 files changed, 558 insertions(+), 213 deletions(-)
create mode 100644 __tests__/worktree.error.test.ts
create mode 100644 __tests__/worktree.test.ts
create mode 100644 src/worktree.ts
diff --git a/.eslintrc.json b/.eslintrc.json
index 7c438075d..adf484fbc 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -55,8 +55,9 @@
"@typescript-eslint/type-annotation-spacing": "error",
"@typescript-eslint/unbound-method": "error",
"no-console": "off",
- "no-shadow": ["error", { "builtinGlobals": false, "hoist": "all", "allow": ["Status"] }]
- },
+ "no-shadow": "off", // replaced by ts-eslint rule below
+ "@typescript-eslint/no-shadow": "error"
+ },
"env": {
"node": true,
"es6": true,
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index ef6465a83..2ee7ccb4f 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -2,8 +2,8 @@ name: unit-tests
on:
pull_request:
branches:
- - dev
- - releases/v3
+ - 'dev*'
+ - 'releases/v*'
push:
branches:
- dev
@@ -14,11 +14,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v1
+ uses: actions/checkout@v2
- uses: actions/setup-node@v1.4.4
with:
- node-version: '10.15.1'
+ node-version: 'v12.18.4'
registry-url: 'https://registry.npmjs.org'
- name: Install Yarn
@@ -34,3 +34,80 @@ jobs:
uses: codecov/codecov-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
+
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - uses: actions/setup-node@v1.4.4
+ with:
+ node-version: 'v12.18.4'
+ registry-url: 'https://registry.npmjs.org'
+
+ - name: Install Yarn
+ run: npm install -g yarn
+
+ - name: Build lib
+ run: |
+ yarn install
+ yarn build
+
+ - name: Rebuild production node_modules
+ run: |
+ yarn install --production
+ ls node_modules
+
+ - name: artifact
+ uses: actions/upload-artifact@v2
+ with:
+ name: dist
+ path: |
+ lib
+ node_modules
+
+ integration:
+ runs-on: ubuntu-latest
+ needs: build
+ strategy:
+ matrix:
+ branch: ["gh-pages", "no-pages"]
+ commit: ["singleCommit", "add commits"]
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+ with:
+ persist-credentials: false
+
+ - uses: actions/setup-node@v1.4.4
+ with:
+ node-version: 'v12.18.4'
+ registry-url: 'https://registry.npmjs.org'
+
+ - name: Download artifact
+ uses: actions/download-artifact@v2
+ with:
+ name: dist
+
+ - name: Deploy
+ uses: ./
+ with:
+ FOLDER: integration
+ BRANCH: ${{ matrix.branch }}
+ SINGLE_COMMIT: ${{ matrix.commit == 'singleCommit' }}
+ DRY_RUN: true
+
+ - name: Tweak content to publish to existing branch
+ if: ${{ matrix.branch == 'gh-pages' }}
+ run: |
+ echo "" >> integration/index.html
+
+ - name: Deploy with modifications to existing branch
+ uses: ./
+ if: ${{ matrix.branch == 'gh-pages' }}
+ with:
+ FOLDER: integration
+ BRANCH: ${{ matrix.branch }}
+ SINGLE_COMMIT: ${{ matrix.commit == 'singleCommit' }}
+ DRY_RUN: true
diff --git a/__tests__/env.js b/__tests__/env.js
index 67c9c4dca..5c29df783 100644
--- a/__tests__/env.js
+++ b/__tests__/env.js
@@ -1,2 +1 @@
-process.env.UNIT_TEST = 'true'
-process.env.ACTIONS_STEP_DEBUG = 'false'
\ No newline at end of file
+process.env.ACTIONS_STEP_DEBUG = 'false'
diff --git a/__tests__/git.test.ts b/__tests__/git.test.ts
index 6a47a1fd0..e86df2afc 100644
--- a/__tests__/git.test.ts
+++ b/__tests__/git.test.ts
@@ -4,9 +4,9 @@ process.env['INPUT_FOLDER'] = 'build'
process.env['GITHUB_SHA'] = '123'
import {mkdirP, rmRF} from '@actions/io'
-import {action, Status} from '../src/constants'
+import {action, Status, TestFlag} from '../src/constants'
import {execute} from '../src/execute'
-import {deploy, generateBranch, init} from '../src/git'
+import {deploy, init} from '../src/git'
import fs from 'fs'
const originalAction = JSON.stringify(action)
@@ -46,11 +46,11 @@ describe('git', () => {
token: '123',
branch: 'branch',
folder: '.',
- isTest: true,
pusher: {
name: 'asd',
email: 'as@cat'
- }
+ },
+ isTest: TestFlag.HAS_CHANGED_FILES
})
try {
@@ -63,122 +63,117 @@ describe('git', () => {
})
})
- describe('generateBranch', () => {
- it('should execute five commands', async () => {
+ describe('deploy', () => {
+ it('should execute commands', async () => {
Object.assign(action, {
silent: false,
- token: '123',
+ folder: 'assets',
branch: 'branch',
- folder: '.',
+ token: '123',
pusher: {
name: 'asd',
email: 'as@cat'
- }
+ },
+ isTest: TestFlag.HAS_CHANGED_FILES
})
- await generateBranch(action)
- expect(execute).toBeCalledTimes(5)
- })
+ const response = await deploy(action)
- it('should catch when a function throws an error', async () => {
- ;(execute as jest.Mock).mockImplementationOnce(() => {
- throw new Error('Mocked throw')
- })
+ // Includes the call to generateWorktree
+ expect(execute).toBeCalledTimes(11)
+ expect(rmRF).toBeCalledTimes(1)
+ expect(response).toBe(Status.SUCCESS)
+ })
+ it('should not push when asked to dryRun', async () => {
Object.assign(action, {
silent: false,
- token: '123',
+ dryRun: true,
+ folder: 'assets',
branch: 'branch',
- folder: '.',
+ token: '123',
pusher: {
name: 'asd',
email: 'as@cat'
- }
+ },
+ isTest: TestFlag.HAS_CHANGED_FILES
})
- try {
- await generateBranch(action)
- } catch (error) {
- expect(error.message).toBe(
- 'There was an error creating the deployment branch: Mocked throw ❌'
- )
- }
+ const response = await deploy(action)
+
+ // Includes the call to generateWorktree
+ expect(execute).toBeCalledTimes(10)
+ expect(rmRF).toBeCalledTimes(1)
+ expect(response).toBe(Status.SUCCESS)
})
- })
- describe('deploy', () => {
- it('should execute commands', async () => {
+ it('should execute commands with single commit toggled', async () => {
Object.assign(action, {
silent: false,
- folder: 'assets',
+ folder: 'other',
+ folderPath: 'other',
branch: 'branch',
token: '123',
+ singleCommit: true,
pusher: {
name: 'asd',
email: 'as@cat'
- }
+ },
+ clean: true,
+ isTest: TestFlag.HAS_CHANGED_FILES
})
- const response = await deploy(action)
+ await deploy(action)
- // Includes the call to generateBranch
+ // Includes the call to generateWorktree
expect(execute).toBeCalledTimes(10)
- expect(execute).toHaveBeenNthCalledWith(
- 9,
- expect.not.stringContaining('--dry-run'),
- expect.anything(),
- false
- )
expect(rmRF).toBeCalledTimes(1)
- expect(response).toBe(Status.SUCCESS)
})
- it('should push with --dry-run', async () => {
+ it('should execute commands with single commit toggled and existing branch', async () => {
Object.assign(action, {
silent: false,
- dryRun: true,
- folder: 'assets',
+ folder: 'other',
+ folderPath: 'other',
branch: 'branch',
token: '123',
+ singleCommit: true,
pusher: {
name: 'asd',
email: 'as@cat'
- }
+ },
+ clean: true,
+ isTest: TestFlag.HAS_CHANGED_FILES | TestFlag.HAS_REMOTE_BRANCH
})
- const response = await deploy(action)
+ await deploy(action)
- // Includes the call to generateBranch
- expect(execute).toBeCalledTimes(10)
- expect(execute).toHaveBeenNthCalledWith(
- 9,
- expect.stringContaining('--dry-run'),
- expect.anything(),
- false
- )
+ // Includes the call to generateWorktree
+ expect(execute).toBeCalledTimes(9)
expect(rmRF).toBeCalledTimes(1)
- expect(response).toBe(Status.SUCCESS)
})
- it('should execute commands with single commit toggled', async () => {
+ it('should execute commands with single commit and dryRun toggled', async () => {
Object.assign(action, {
silent: false,
folder: 'other',
folderPath: 'other',
branch: 'branch',
- token: '123',
+ gitHubToken: '123',
singleCommit: true,
+ dryRun: true,
pusher: {
name: 'asd',
email: 'as@cat'
},
- clean: true
+ clean: true,
+ isTest: TestFlag.HAS_CHANGED_FILES
})
await deploy(action)
- // Includes the call to generateBranch
- expect(execute).toBeCalledTimes(16)
+ // Includes the call to generateWorktree
+ expect(execute).toBeCalledTimes(9)
expect(rmRF).toBeCalledTimes(1)
})
@@ -193,7 +188,8 @@ describe('git', () => {
name: 'asd',
email: 'as@cat'
},
- clean: true
+ clean: true,
+ isTest: TestFlag.HAS_CHANGED_FILES
})
fs.createWriteStream('assets/.nojekyll')
@@ -201,13 +197,13 @@ describe('git', () => {
const response = await deploy(action)
- // Includes the call to generateBranch
- expect(execute).toBeCalledTimes(10)
+ // Includes the call to generateWorktree
+ expect(execute).toBeCalledTimes(11)
expect(rmRF).toBeCalledTimes(1)
expect(response).toBe(Status.SUCCESS)
})
- it('should execute commands with clean options, ommits sha commit message', async () => {
+ it('should execute commands with clean options, commits sha commit message', async () => {
process.env.GITHUB_SHA = ''
Object.assign(action, {
silent: false,
@@ -221,13 +217,14 @@ describe('git', () => {
},
clean: true,
cleanExclude: '["cat", "montezuma"]',
- workspace: 'other'
+ workspace: 'other',
+ isTest: TestFlag.NONE
})
await deploy(action)
- // Includes the call to generateBranch
- expect(execute).toBeCalledTimes(10)
+ // Includes the call to generateWorktree
+ expect(execute).toBeCalledTimes(8)
expect(rmRF).toBeCalledTimes(1)
})
@@ -243,13 +240,14 @@ describe('git', () => {
email: 'as@cat'
},
clean: true,
- cleanExclude: ['cat', 'montezuma']
+ cleanExclude: ['cat', 'montezuma'],
+ isTest: TestFlag.NONE
})
await deploy(action)
- // Includes the call to generateBranch
- expect(execute).toBeCalledTimes(10)
+ // Includes the call to generateWorktree
+ expect(execute).toBeCalledTimes(8)
expect(rmRF).toBeCalledTimes(1)
})
@@ -263,13 +261,13 @@ describe('git', () => {
clean: true,
targetFolder: 'new_folder',
commitMessage: 'Hello!',
- isTest: true,
+ isTest: TestFlag.NONE,
cleanExclude: '["cat, "montezuma"]' // There is a syntax errror in the string.
})
await deploy(action)
- expect(execute).toBeCalledTimes(10)
+ expect(execute).toBeCalledTimes(8)
expect(rmRF).toBeCalledTimes(1)
expect(mkdirP).toBeCalledTimes(1)
})
@@ -284,11 +282,11 @@ describe('git', () => {
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.
+ isTest: TestFlag.NONE // Setting this flag to None means there will never be anything to commit and the action will exit early.
})
const response = await deploy(action)
- expect(execute).toBeCalledTimes(10)
+ expect(execute).toBeCalledTimes(8)
expect(rmRF).toBeCalledTimes(1)
expect(response).toBe(Status.SKIPPED)
})
@@ -306,7 +304,8 @@ describe('git', () => {
pusher: {
name: 'asd',
email: 'as@cat'
- }
+ },
+ isTest: TestFlag.HAS_CHANGED_FILES
})
try {
diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts
index 6044032fc..4d02492fd 100644
--- a/__tests__/main.test.ts
+++ b/__tests__/main.test.ts
@@ -5,7 +5,7 @@ process.env['GITHUB_SHA'] = '123'
process.env['INPUT_DEBUG'] = 'debug'
import '../src/main'
-import {action} from '../src/constants'
+import {action, TestFlag} from '../src/constants'
import run from '../src/lib'
import {execute} from '../src/execute'
import {rmRF} from '@actions/io'
@@ -44,11 +44,11 @@ describe('main', () => {
name: 'asd',
email: 'as@cat'
},
- isTest: false,
+ isTest: TestFlag.NONE,
debug: true
})
await run(action)
- expect(execute).toBeCalledTimes(12)
+ expect(execute).toBeCalledTimes(10)
expect(rmRF).toBeCalledTimes(1)
expect(exportVariable).toBeCalledTimes(1)
})
@@ -62,10 +62,11 @@ describe('main', () => {
pusher: {
name: 'asd',
email: 'as@cat'
- }
+ },
+ isTest: TestFlag.HAS_CHANGED_FILES
})
await run(action)
- expect(execute).toBeCalledTimes(12)
+ expect(execute).toBeCalledTimes(13)
expect(rmRF).toBeCalledTimes(1)
expect(exportVariable).toBeCalledTimes(1)
})
@@ -80,7 +81,7 @@ describe('main', () => {
name: 'asd',
email: 'as@cat'
},
- isTest: true
+ isTest: TestFlag.HAS_CHANGED_FILES
})
await run(action)
expect(execute).toBeCalledTimes(0)
diff --git a/__tests__/util.test.ts b/__tests__/util.test.ts
index dd37eff02..7fa92024d 100644
--- a/__tests__/util.test.ts
+++ b/__tests__/util.test.ts
@@ -1,4 +1,4 @@
-import {ActionInterface} from '../src/constants'
+import {ActionInterface, TestFlag} from '../src/constants'
import {
isNullOrUndefined,
generateTokenType,
@@ -39,7 +39,8 @@ describe('util', () => {
folder: 'build',
token: null,
ssh: true,
- silent: false
+ silent: false,
+ isTest: TestFlag.NONE
}
expect(generateTokenType(action)).toEqual('SSH Deploy Key')
})
@@ -51,7 +52,8 @@ describe('util', () => {
folder: 'build',
token: '123',
ssh: null,
- silent: false
+ silent: false,
+ isTest: TestFlag.NONE
}
expect(generateTokenType(action)).toEqual('Deploy Token')
})
@@ -63,7 +65,8 @@ describe('util', () => {
folder: 'build',
token: null,
ssh: null,
- silent: false
+ silent: false,
+ isTest: TestFlag.NONE
}
expect(generateTokenType(action)).toEqual('…')
})
@@ -78,7 +81,8 @@ describe('util', () => {
folder: 'build',
token: null,
ssh: true,
- silent: false
+ silent: false,
+ isTest: TestFlag.NONE
}
expect(generateRepositoryPath(action)).toEqual(
'git@github.com:JamesIves/github-pages-deploy-action'
@@ -93,7 +97,8 @@ describe('util', () => {
folder: 'build',
token: '123',
ssh: null,
- silent: false
+ silent: false,
+ isTest: TestFlag.NONE
}
expect(generateRepositoryPath(action)).toEqual(
'https://x-access-token:123@github.com/JamesIves/github-pages-deploy-action.git'
@@ -110,7 +115,8 @@ describe('util', () => {
workspace: 'src/',
folder: 'build',
token: 'anothersecret123333',
- silent: false
+ silent: false,
+ isTest: TestFlag.NONE
}
const string = `This is an error message! It contains ${action.token} and ${action.repositoryPath} and ${action.token} again!`
@@ -128,7 +134,8 @@ describe('util', () => {
workspace: 'src/',
folder: 'build',
token: 'anothersecret123333',
- silent: false
+ silent: false,
+ isTest: TestFlag.NONE
}
process.env['RUNNER_DEBUG'] = '1'
@@ -149,7 +156,8 @@ describe('util', () => {
folder: 'build',
token: null,
ssh: null,
- silent: false
+ silent: false,
+ isTest: TestFlag.NONE
}
expect(generateFolderPath(action)).toEqual('src/build')
})
@@ -161,7 +169,8 @@ describe('util', () => {
folder: '/home/user/repo/build',
token: null,
ssh: null,
- silent: false
+ silent: false,
+ isTest: TestFlag.NONE
}
expect(generateFolderPath(action)).toEqual('/home/user/repo/build')
})
@@ -173,7 +182,8 @@ describe('util', () => {
folder: './build',
token: null,
ssh: null,
- silent: false
+ silent: false,
+ isTest: TestFlag.NONE
}
expect(generateFolderPath(action)).toEqual('src/build')
})
@@ -185,7 +195,8 @@ describe('util', () => {
folder: '~/repo/build',
token: null,
ssh: null,
- silent: false
+ silent: false,
+ isTest: TestFlag.NONE
}
process.env.HOME = '/home/user'
expect(generateFolderPath(action)).toEqual('/home/user/repo/build')
@@ -199,7 +210,8 @@ describe('util', () => {
repositoryPath: undefined,
branch: 'branch',
folder: 'build',
- workspace: 'src/'
+ workspace: 'src/',
+ isTest: TestFlag.NONE
}
try {
@@ -218,7 +230,8 @@ describe('util', () => {
token: '',
branch: 'branch',
folder: 'build',
- workspace: 'src/'
+ workspace: 'src/',
+ isTest: TestFlag.NONE
}
try {
@@ -237,7 +250,8 @@ describe('util', () => {
token: '123',
branch: '',
folder: 'build',
- workspace: 'src/'
+ workspace: 'src/',
+ isTest: TestFlag.NONE
}
try {
@@ -254,7 +268,8 @@ describe('util', () => {
token: '123',
branch: 'branch',
folder: '',
- workspace: 'src/'
+ workspace: 'src/',
+ isTest: TestFlag.NONE
}
try {
@@ -273,7 +288,8 @@ describe('util', () => {
token: '123',
branch: 'branch',
folder: 'notARealFolder',
- workspace: '.'
+ workspace: '.',
+ isTest: TestFlag.NONE
}
try {
diff --git a/__tests__/worktree.error.test.ts b/__tests__/worktree.error.test.ts
new file mode 100644
index 000000000..57676ebd5
--- /dev/null
+++ b/__tests__/worktree.error.test.ts
@@ -0,0 +1,35 @@
+import {TestFlag} from '../src/constants'
+import {execute} from '../src/execute'
+import {generateWorktree} from '../src/worktree'
+
+jest.mock('../src/execute', () => ({
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ __esModule: true,
+ execute: jest.fn()
+}))
+
+describe('generateWorktree', () => {
+ it('should catch when a function throws an error', async () => {
+ ;(execute as jest.Mock).mockImplementationOnce(() => {
+ throw new Error('Mocked throw')
+ })
+ try {
+ await generateWorktree(
+ {
+ workspace: 'somewhere',
+ singleCommit: false,
+ branch: 'gh-pages',
+ folder: '',
+ silent: true,
+ isTest: TestFlag.HAS_CHANGED_FILES
+ },
+ 'worktree',
+ true
+ )
+ } catch (error) {
+ expect(error.message).toBe(
+ 'There was an error creating the worktree: Mocked throw ❌'
+ )
+ }
+ })
+})
diff --git a/__tests__/worktree.test.ts b/__tests__/worktree.test.ts
new file mode 100644
index 000000000..91626f7ca
--- /dev/null
+++ b/__tests__/worktree.test.ts
@@ -0,0 +1,195 @@
+import {rmRF} from '@actions/io'
+import {TestFlag} from '../src/constants'
+import {generateWorktree} from '../src/worktree'
+import {execute} from '../src/execute'
+import fs from 'fs'
+import os from 'os'
+import path from 'path'
+
+jest.mock('@actions/core', () => ({
+ setFailed: jest.fn(),
+ getInput: jest.fn(),
+ isDebug: jest.fn(),
+ info: jest.fn()
+}))
+
+/*
+ Test generateWorktree against a known git repository.
+ The upstream repository `origin` is set up once for the test suite,
+ and for each test run, a new clone is created.
+
+ See workstree.error.test.ts for testing mocked errors from git.*/
+
+describe('generateWorktree', () => {
+ let tempdir: string | null = null
+ let clonedir: string | null = null
+ beforeAll(async () => {
+ // Set up origin repository
+ const silent = true
+ tempdir = fs.mkdtempSync(path.join(os.tmpdir(), 'gh-deploy-'))
+ const origin = path.join(tempdir, 'origin')
+ await execute('git init origin', tempdir, silent)
+ await execute('git config user.email "you@example.com"', origin, silent)
+ await execute('git config user.name "Jane Doe"', origin, silent)
+ await execute('git checkout -b main', origin, silent)
+ fs.writeFileSync(path.join(origin, 'f1'), 'hello world\n')
+ await execute('git add .', origin, silent)
+ await execute('git commit -mc0', origin, silent)
+ fs.writeFileSync(path.join(origin, 'f1'), 'hello world\nand planets\n')
+ await execute('git add .', origin, silent)
+ await execute('git commit -mc1', origin, silent)
+ await execute('git checkout --orphan gh-pages', origin, silent)
+ await execute('git reset --hard', origin, silent)
+ await fs.promises.writeFile(path.join(origin, 'gh1'), 'pages content\n')
+ await execute('git add .', origin, silent)
+ await execute('git commit -mgh0', origin, silent)
+ await fs.promises.writeFile(
+ path.join(origin, 'gh1'),
+ 'pages content\ngoes on\n'
+ )
+ await execute('git add .', origin, silent)
+ await execute('git commit -mgh1', origin, silent)
+ })
+ beforeEach(async () => {
+ // Clone origin to our workspace for each test
+ const silent = true
+ clonedir = path.join(tempdir as string, 'clone')
+ await execute('git init clone', tempdir as string, silent)
+ await execute('git config user.email "you@example.com"', clonedir, silent)
+ await execute('git config user.name "Jane Doe"', clonedir, silent)
+ await execute(
+ `git remote add origin ${path.join(tempdir as string, 'origin')}`,
+ clonedir,
+ silent
+ )
+ await execute('git fetch --depth=1 origin main', clonedir, silent)
+ await execute('git checkout main', clonedir, silent)
+ })
+ afterEach(async () => {
+ // Tear down workspace
+ await rmRF(clonedir as string)
+ })
+ afterAll(async () => {
+ // Tear down origin repository
+ if (tempdir) {
+ await rmRF(tempdir)
+ // console.log(tempdir)
+ }
+ })
+ describe('with existing branch and new commits', () => {
+ it('should check out the latest commit', async () => {
+ const workspace = clonedir as string
+ await generateWorktree(
+ {
+ workspace,
+ singleCommit: false,
+ branch: 'gh-pages',
+ folder: '',
+ silent: true,
+ isTest: TestFlag.NONE
+ },
+ 'worktree',
+ true
+ )
+ const dirEntries = await fs.promises.readdir(
+ path.join(workspace, 'worktree')
+ )
+ expect(dirEntries.sort((a, b) => a.localeCompare(b))).toEqual([
+ '.git',
+ 'gh1'
+ ])
+ const commitMessages = await execute(
+ 'git log --format=%s',
+ path.join(workspace, 'worktree'),
+ true
+ )
+ expect(commitMessages).toBe('gh1')
+ })
+ })
+ describe('with missing branch and new commits', () => {
+ it('should create initial commit', async () => {
+ const workspace = clonedir as string
+ await generateWorktree(
+ {
+ workspace,
+ singleCommit: false,
+ branch: 'no-pages',
+ folder: '',
+ silent: true,
+ isTest: TestFlag.NONE
+ },
+ 'worktree',
+ false
+ )
+ const dirEntries = await fs.promises.readdir(
+ path.join(workspace, 'worktree')
+ )
+ expect(dirEntries).toEqual(['.git'])
+ const commitMessages = await execute(
+ 'git log --format=%s',
+ path.join(workspace, 'worktree'),
+ true
+ )
+ expect(commitMessages).toBe('Initial no-pages commit')
+ })
+ })
+ describe('with existing branch and singleCommit', () => {
+ it('should check out the latest commit', async () => {
+ const workspace = clonedir as string
+ await generateWorktree(
+ {
+ workspace,
+ singleCommit: true,
+ branch: 'gh-pages',
+ folder: '',
+ silent: true,
+ isTest: TestFlag.NONE
+ },
+ 'worktree',
+ true
+ )
+ const dirEntries = await fs.promises.readdir(
+ path.join(workspace, 'worktree')
+ )
+ expect(dirEntries.sort((a, b) => a.localeCompare(b))).toEqual([
+ '.git',
+ 'gh1'
+ ])
+ expect(async () => {
+ await execute(
+ 'git log --format=%s',
+ path.join(workspace, 'worktree'),
+ true
+ )
+ }).rejects.toThrow()
+ })
+ })
+ describe('with missing branch and singleCommit', () => {
+ it('should create initial commit', async () => {
+ const workspace = clonedir as string
+ await generateWorktree(
+ {
+ workspace,
+ singleCommit: true,
+ branch: 'no-pages',
+ folder: '',
+ silent: true,
+ isTest: TestFlag.NONE
+ },
+ 'worktree',
+ false
+ )
+ const dirEntries = await fs.promises.readdir(
+ path.join(workspace, 'worktree')
+ )
+ expect(dirEntries).toEqual(['.git'])
+ expect(async () => {
+ await execute(
+ 'git log --format=%s',
+ path.join(workspace, 'worktree'),
+ true
+ )
+ }).rejects.toThrow()
+ })
+ })
+})
diff --git a/action.yml b/action.yml
index 2383a022a..746943031 100644
--- a/action.yml
+++ b/action.yml
@@ -50,6 +50,10 @@ inputs:
description: "If you need to use CLEAN but you would like to preserve certain files or folders you can use this option. This should be formatted as an array but stored as a string."
required: false
+ DRY_RUN:
+ description: "Do not actually push back, but use `--dry-run` on `git push` invocations insead."
+ required: false
+
GIT_CONFIG_NAME:
description: "Allows you to customize the name that is attached to the GitHub config which is used when pushing the deployment commits. If this is not included it will use the name in the GitHub context, followed by the name of the action."
required: false
diff --git a/src/constants.ts b/src/constants.ts
index 0b85dcde8..91a323050 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -4,6 +4,13 @@ import {isNullOrUndefined} from './util'
const {pusher, repository} = github.context.payload
+/* Flags to signal different scenarios to test cases */
+export enum TestFlag {
+ NONE = 0,
+ HAS_CHANGED_FILES = 1 << 1, // Assume changes to commit
+ HAS_REMOTE_BRANCH = 1 << 2 // Assume remote repository has existing commits
+}
+
/* For more information please refer to the README: https://github.com/JamesIves/github-pages-deploy-action */
export interface ActionInterface {
/** The branch that the action should deploy to. */
@@ -22,8 +29,8 @@ export interface ActionInterface {
folder: string
/** The auto generated folder path. */
folderPath?: string
- /** Determines if the action is running in test mode or not. */
- isTest?: boolean | null
+ /** Determines test scenarios the action is running in. */
+ isTest: TestFlag
/** The git config name. */
name?: string
/** The repository path, for example JamesIves/github-pages-deploy-action. */
@@ -62,6 +69,7 @@ export interface NodeActionInterface {
ssh?: boolean | null
/** The folder where your deployment project lives. */
workspace: string
+ isTest: TestFlag
}
/* Required action data that gets initialized when running within the GitHub Actions environment. */
@@ -76,9 +84,7 @@ export const action: ActionInterface = {
? getInput('CLEAN').toLowerCase() === 'true'
: false,
cleanExclude: getInput('CLEAN_EXCLUDE'),
- isTest: process.env.UNIT_TEST
- ? process.env.UNIT_TEST.toLowerCase() === 'true'
- : false,
+ isTest: TestFlag.NONE,
email: !isNullOrUndefined(getInput('GIT_CONFIG_EMAIL'))
? getInput('GIT_CONFIG_EMAIL')
: pusher && pusher.email
@@ -115,7 +121,7 @@ export const action: ActionInterface = {
/** Types for the required action parameters. */
export type RequiredActionParameters = Pick<
ActionInterface,
- 'token' | 'ssh' | 'branch' | 'folder'
+ 'token' | 'ssh' | 'branch' | 'folder' | 'isTest'
>
/** Status codes for the action. */
diff --git a/src/git.ts b/src/git.ts
index de35d3e7c..354e4b477 100644
--- a/src/git.ts
+++ b/src/git.ts
@@ -1,8 +1,9 @@
import {info} from '@actions/core'
import {mkdirP, rmRF} from '@actions/io'
import fs from 'fs'
-import {ActionInterface, Status} from './constants'
+import {ActionInterface, Status, TestFlag} from './constants'
import {execute} from './execute'
+import {generateWorktree} from './worktree'
import {isNullOrUndefined, suppressSensitiveInformation} from './util'
/* Initializes git in the workspace. */
@@ -33,41 +34,6 @@ export async function init(action: ActionInterface): Promise {
}
}
-/* Generates the branch if it doesn't exist on the remote. */
-export async function generateBranch(action: ActionInterface): Promise {
- try {
- info(`Creating the ${action.branch} branch…`)
-
- await execute(
- `git checkout --orphan ${action.branch}`,
- action.workspace,
- action.silent
- )
- await execute(`git reset --hard`, action.workspace, action.silent)
- await execute(
- `git commit --no-verify --allow-empty -m "Initial ${action.branch} commit"`,
- action.workspace,
- action.silent
- )
- const dry = action.dryRun ? '--dry-run ' : ''
- await execute(
- `git push --force ${dry}${action.repositoryPath} ${action.branch}`,
- action.workspace,
- action.silent
- )
- await execute(`git fetch`, action.workspace, action.silent)
-
- info(`Created the ${action.branch} branch… 🔧`)
- } catch (error) {
- throw new Error(
- `There was an error creating the deployment branch: ${suppressSensitiveInformation(
- error.message,
- action
- )} ❌`
- )
- }
-}
-
/* Runs the necessary steps to make the deployment. */
export async function deploy(action: ActionInterface): Promise {
const temporaryDeploymentDirectory =
@@ -75,7 +41,6 @@ export async function deploy(action: ActionInterface): Promise {
const temporaryDeploymentBranch = `github-pages-deploy-action/${Math.random()
.toString(36)
.substr(2, 9)}`
- const dry = action.dryRun ? '--dry-run ' : ''
info('Starting to commit changes…')
@@ -86,31 +51,16 @@ export async function deploy(action: ActionInterface): Promise {
process.env.GITHUB_SHA ? ` from @ ${process.env.GITHUB_SHA}` : ''
} 🚀`
- /*
- Checks to see if the remote exists prior to deploying.
- If the branch doesn't exist it gets created here as an orphan.
- */
- const branchExists = await execute(
- `git ls-remote --heads ${action.repositoryPath} ${action.branch} | wc -l`,
- action.workspace,
- action.silent
- )
-
- if (!branchExists && !action.isTest) {
- await generateBranch(action)
- } else {
- await execute(
- `git fetch --no-recurse-submodules --depth=1 origin ${action.branch}`,
+ // Checks to see if the remote exists prior to deploying.
+ const branchExists =
+ action.isTest & TestFlag.HAS_REMOTE_BRANCH ||
+ (await execute(
+ `git ls-remote --heads ${action.repositoryPath} ${action.branch} | wc -l`,
action.workspace,
action.silent
- )
- }
+ ))
- await execute(
- `git worktree add --checkout ${temporaryDeploymentDirectory} origin/${action.branch}`,
- action.workspace,
- action.silent
- )
+ await generateWorktree(action, temporaryDeploymentDirectory, branchExists)
// Ensures that items that need to be excluded from the clean job get parsed.
let excludes = ''
@@ -166,13 +116,23 @@ export async function deploy(action: ActionInterface): Promise {
action.silent
)
- const hasFilesToCommit = await execute(
- `git status --porcelain`,
- `${action.workspace}/${temporaryDeploymentDirectory}`,
- action.silent
- )
+ // Use git status to check if we have something to commit.
+ // Special case is singleCommit with existing history, when
+ // we're really interested if the diff against the upstream branch
+ // changed.
+ const checkGitStatus =
+ branchExists && action.singleCommit
+ ? `git diff origin/${action.branch}`
+ : `git status --porcelain`
+ const hasFilesToCommit =
+ action.isTest & TestFlag.HAS_CHANGED_FILES ||
+ (await execute(
+ checkGitStatus,
+ `${action.workspace}/${temporaryDeploymentDirectory}`,
+ action.silent
+ ))
- if (!hasFilesToCommit && !action.isTest) {
+ if (!hasFilesToCommit) {
return Status.SKIPPED
}
@@ -192,49 +152,16 @@ export async function deploy(action: ActionInterface): Promise {
`${action.workspace}/${temporaryDeploymentDirectory}`,
action.silent
)
- await execute(
- `git push --force ${dry}${action.repositoryPath} ${temporaryDeploymentBranch}:${action.branch}`,
- `${action.workspace}/${temporaryDeploymentDirectory}`,
- action.silent
- )
-
- info(`Changes committed to the ${action.branch} branch… 📦`)
-
- if (action.singleCommit) {
+ if (!action.dryRun) {
await execute(
- `git fetch ${action.repositoryPath}`,
- action.workspace,
- action.silent
- )
- await execute(
- `git checkout --orphan ${action.branch}-temp`,
+ `git push --force ${action.repositoryPath} ${temporaryDeploymentBranch}:${action.branch}`,
`${action.workspace}/${temporaryDeploymentDirectory}`,
action.silent
)
- await execute(
- `git add --all .`,
- `${action.workspace}/${temporaryDeploymentDirectory}`,
- action.silent
- )
- await execute(
- `git commit -m "${commitMessage}" --quiet --no-verify`,
- `${action.workspace}/${temporaryDeploymentDirectory}`,
- action.silent
- )
- await execute(
- `git branch -M ${action.branch}-temp ${action.branch}`,
- `${action.workspace}/${temporaryDeploymentDirectory}`,
- action.silent
- )
- await execute(
- `git push origin ${action.branch} ${dry}--force`,
- `${action.workspace}/${temporaryDeploymentDirectory}`,
- action.silent
- )
-
- info('Cleared git history… 🚿')
}
+ info(`Changes committed to the ${action.branch} branch… 📦`)
+
return Status.SUCCESS
} catch (error) {
throw new Error(
diff --git a/src/worktree.ts b/src/worktree.ts
new file mode 100644
index 000000000..0fad213e3
--- /dev/null
+++ b/src/worktree.ts
@@ -0,0 +1,85 @@
+import {info} from '@actions/core'
+import {ActionInterface} from './constants'
+import {execute} from './execute'
+import {suppressSensitiveInformation} from './util'
+
+export class GitCheckout {
+ orphan = false
+ commitish?: string | null = null
+ branch: string
+ constructor(branch: string) {
+ this.branch = branch
+ }
+ toString(): string {
+ return [
+ 'git',
+ 'checkout',
+ this.orphan ? '--orphan' : '-B',
+ this.branch,
+ this.commitish || ''
+ ].join(' ')
+ }
+}
+/* Generate the worktree and set initial content if it exists */
+
+export async function generateWorktree(
+ action: ActionInterface,
+ worktreedir: string,
+ branchExists: boolean
+): Promise {
+ try {
+ info('Creating worktree…')
+
+ if (branchExists) {
+ await execute(
+ `git fetch --no-recurse-submodules --depth=1 origin ${action.branch}`,
+ action.workspace,
+ action.silent
+ )
+ }
+
+ await execute(
+ `git worktree add --no-checkout --detach ${worktreedir}`,
+ action.workspace,
+ action.silent
+ )
+ const checkout = new GitCheckout(action.branch)
+ if (branchExists) {
+ // There's existing data on the branch to check out
+ checkout.commitish = `origin/${action.branch}`
+ }
+ if (!branchExists || action.singleCommit) {
+ // Create a new history if we don't have the branch, or if we want to reset it
+ checkout.orphan = true
+ }
+ await execute(
+ checkout.toString(),
+ `${action.workspace}/${worktreedir}`,
+ action.silent
+ )
+ if (!branchExists) {
+ info(`Created the ${action.branch} branch… 🔧`)
+ // Our index is in HEAD state, reset
+ await execute(
+ 'git reset --hard',
+ `${action.workspace}/${worktreedir}`,
+ action.silent
+ )
+ if (!action.singleCommit) {
+ // New history isn't singleCommit, create empty initial commit
+ await execute(
+ `git commit --no-verify --allow-empty -m "Initial ${action.branch} commit"`,
+ `${action.workspace}/${worktreedir}`,
+ action.silent
+ )
+ }
+ }
+ } catch (error) {
+ throw new Error(
+ `There was an error creating the worktree: ${suppressSensitiveInformation(
+ error.message,
+ action
+ )} ❌`
+ )
+ }
+}
From 570f002e7e609191ed3986f71e135d808aa790a5 Mon Sep 17 00:00:00 2001
From: James Ives
Date: Mon, 14 Dec 2020 12:41:27 -0500
Subject: [PATCH 10/32] Update worktree.ts
---
src/worktree.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/worktree.ts b/src/worktree.ts
index 0fad213e3..9a820e747 100644
--- a/src/worktree.ts
+++ b/src/worktree.ts
@@ -20,8 +20,8 @@ export class GitCheckout {
].join(' ')
}
}
-/* Generate the worktree and set initial content if it exists */
+/* Generate the worktree and set initial content if it exists */
export async function generateWorktree(
action: ActionInterface,
worktreedir: string,
From 2a503ef9b5eda9c70da4b5396390143ae096292c Mon Sep 17 00:00:00 2001
From: Axel Hecht
Date: Tue, 15 Dec 2020 16:58:52 +0100
Subject: [PATCH 11/32] =?UTF-8?q?Fix=20a=20few=20nits=20in=20tests=20and?=
=?UTF-8?q?=20automation.=20Don't=20try=20to=20wordcount=20ls-rem=E2=80=A6?=
=?UTF-8?q?=20(#546)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Fix a few nits in tests and automation. Don't try to wordcount ls-remote.
Nits in tests are around undoing changes made to the environment,
and to not modify the checkout.
* Describe suite with empty SHA
---
.github/workflows/build.yml | 2 +-
__tests__/git.test.ts | 62 +++++++++++++++++++++++--------------
src/git.ts | 2 +-
3 files changed, 41 insertions(+), 25 deletions(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 2ee7ccb4f..a088f6f65 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -6,7 +6,7 @@ on:
- 'releases/v*'
push:
branches:
- - dev
+ - 'dev*'
tags-ignore:
- '*.*'
jobs:
diff --git a/__tests__/git.test.ts b/__tests__/git.test.ts
index e86df2afc..4b8bc7e2e 100644
--- a/__tests__/git.test.ts
+++ b/__tests__/git.test.ts
@@ -11,6 +11,10 @@ import fs from 'fs'
const originalAction = JSON.stringify(action)
+jest.mock('fs', () => ({
+ existsSync: jest.fn()
+}))
+
jest.mock('@actions/core', () => ({
setFailed: jest.fn(),
getInput: jest.fn(),
@@ -178,6 +182,14 @@ describe('git', () => {
})
it('should not ignore CNAME or nojekyll if they exist in the deployment folder', async () => {
+ ;(fs.existsSync as jest.Mock)
+ .mockImplementationOnce(() => {
+ return true
+ })
+ .mockImplementationOnce(() => {
+ return true
+ })
+
Object.assign(action, {
silent: false,
folder: 'assets',
@@ -192,40 +204,44 @@ describe('git', () => {
isTest: TestFlag.HAS_CHANGED_FILES
})
- fs.createWriteStream('assets/.nojekyll')
- fs.createWriteStream('assets/CNAME')
-
const response = await deploy(action)
// Includes the call to generateWorktree
expect(execute).toBeCalledTimes(11)
expect(rmRF).toBeCalledTimes(1)
+ expect(fs.existsSync).toBeCalledTimes(2)
expect(response).toBe(Status.SUCCESS)
})
- it('should execute commands with clean options, commits sha commit message', async () => {
- process.env.GITHUB_SHA = ''
- Object.assign(action, {
- silent: false,
- folder: 'other',
- folderPath: 'other',
- branch: 'branch',
- token: '123',
- pusher: {
- name: 'asd',
- email: 'as@cat'
- },
- clean: true,
- cleanExclude: '["cat", "montezuma"]',
- workspace: 'other',
- isTest: TestFlag.NONE
+ describe('with empty GITHUB_SHA', () => {
+ const oldSha = process.env.GITHUB_SHA
+ afterAll(() => {
+ process.env.GITHUB_SHA = oldSha
})
+ it('should execute commands with clean options', async () => {
+ process.env.GITHUB_SHA = ''
+ Object.assign(action, {
+ silent: false,
+ folder: 'other',
+ folderPath: 'other',
+ branch: 'branch',
+ token: '123',
+ pusher: {
+ name: 'asd',
+ email: 'as@cat'
+ },
+ clean: true,
+ cleanExclude: '["cat", "montezuma"]',
+ workspace: 'other',
+ isTest: TestFlag.NONE
+ })
- await deploy(action)
+ await deploy(action)
- // Includes the call to generateWorktree
- expect(execute).toBeCalledTimes(8)
- expect(rmRF).toBeCalledTimes(1)
+ // Includes the call to generateWorktree
+ expect(execute).toBeCalledTimes(8)
+ expect(rmRF).toBeCalledTimes(1)
+ })
})
it('should execute commands with clean options stored as an array instead', async () => {
diff --git a/src/git.ts b/src/git.ts
index 354e4b477..1c931c188 100644
--- a/src/git.ts
+++ b/src/git.ts
@@ -55,7 +55,7 @@ export async function deploy(action: ActionInterface): Promise {
const branchExists =
action.isTest & TestFlag.HAS_REMOTE_BRANCH ||
(await execute(
- `git ls-remote --heads ${action.repositoryPath} ${action.branch} | wc -l`,
+ `git ls-remote --heads ${action.repositoryPath} ${action.branch}`,
action.workspace,
action.silent
))
From d8c795395d7f9a3dd60de149b1dcf5c4812b6f26 Mon Sep 17 00:00:00 2001
From: James Ives
Date: Tue, 15 Dec 2020 18:32:06 -0500
Subject: [PATCH 12/32] Lowercase Inputs (#547)
* Lowercases inputs
* Adjusts workflow tests and deployment_status
---
.github/workflows/build.yml | 14 +-
.github/workflows/integration-beta.yml | 196 -------------------------
.github/workflows/integration.yml | 74 +++++-----
README.md | 58 ++++----
src/constants.ts | 44 +++---
src/lib.ts | 2 +-
6 files changed, 95 insertions(+), 293 deletions(-)
delete mode 100644 .github/workflows/integration-beta.yml
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index a088f6f65..ad8d45367 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -93,10 +93,10 @@ jobs:
- name: Deploy
uses: ./
with:
- FOLDER: integration
- BRANCH: ${{ matrix.branch }}
+ folder: integration
+ branch: ${{ matrix.branch }}
SINGLE_COMMIT: ${{ matrix.commit == 'singleCommit' }}
- DRY_RUN: true
+ dry_run: true
- name: Tweak content to publish to existing branch
if: ${{ matrix.branch == 'gh-pages' }}
@@ -107,7 +107,7 @@ jobs:
uses: ./
if: ${{ matrix.branch == 'gh-pages' }}
with:
- FOLDER: integration
- BRANCH: ${{ matrix.branch }}
- SINGLE_COMMIT: ${{ matrix.commit == 'singleCommit' }}
- DRY_RUN: true
+ folder: integration
+ branch: ${{ matrix.branch }}
+ single_commit: ${{ matrix.commit == 'singleCommit' }}
+ dry_run: true
diff --git a/.github/workflows/integration-beta.yml b/.github/workflows/integration-beta.yml
deleted file mode 100644
index 373331e0f..000000000
--- a/.github/workflows/integration-beta.yml
+++ /dev/null
@@ -1,196 +0,0 @@
-name: integration-tests-beta
-on:
- schedule:
- - cron: 30 15 * * 0-6
- push:
- branches:
- - dev
- - releases/v3-test
-
-jobs:
- # Deploys using checkout@v1 with an ACCESS_TOKEN.
- integration-checkout-v1:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v1
-
- - name: Build and Deploy
- uses: JamesIves/github-pages-deploy-action@releases/v3-test
- with:
- ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
- BRANCH: gh-pages-test
- FOLDER: integration
- TARGET_FOLDER: cat/montezuma
- GIT_CONFIG_NAME: Montezuma
- GIT_CONFIG_EMAIL: montezuma@jamesiv.es
-
- - name: Cleanup Generated Branch
- uses: dawidd6/action-delete-branch@v2.0.1
- with:
- github_token: ${{ secrets.GITHUB_TOKEN }}
- branches: gh-pages-test
-
-
- # Deploys using checkout@v2 with a GITHUB_TOKEN.
- integration-checkout-v2:
- needs: integration-checkout-v1
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v2
- with:
- persist-credentials: false
-
- - name: Build and Deploy
- uses: JamesIves/github-pages-deploy-action@releases/v3-test
- with:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- BRANCH: gh-pages-test
- FOLDER: integration
- TARGET_FOLDER: cat/montezuma2
-
- - name: Cleanup Generated Branch
- uses: dawidd6/action-delete-branch@v2.0.1
- with:
- github_token: ${{ secrets.GITHUB_TOKEN }}
- branches: gh-pages-test
-
-
- # Deploys using a container that requires you to install rsync.
- integration-container:
- needs: integration-checkout-v2
- runs-on: ubuntu-latest
- container:
- image: ruby:2.6
- env:
- LANG: C.UTF-8
- steps:
- - name: Checkout
- uses: actions/checkout@v2
- with:
- persist-credentials: false
-
- - name: Install rsync
- run: |
- apt-get update && apt-get install -y rsync
-
- - name: Build and Deploy
- uses: JamesIves/github-pages-deploy-action@releases/v3-test
- with:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- BRANCH: gh-pages-test
- FOLDER: integration
- TARGET_FOLDER: cat/montezuma2
-
- - name: Cleanup Generated Branch
- uses: dawidd6/action-delete-branch@v2.0.1
- with:
- github_token: ${{ secrets.GITHUB_TOKEN }}
- branches: gh-pages-test
-
-
- # Deploys using an SSH key.
- integration-ssh:
- needs: integration-container
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v2
- with:
- persist-credentials: false
-
- - name: Install SSH Client
- uses: webfactory/ssh-agent@v0.4.1
- with:
- ssh-private-key: ${{ secrets.DEPLOY_KEY }}
-
- - name: Build and Deploy
- uses: JamesIves/github-pages-deploy-action@releases/v3-test
- with:
- SSH: true
- BRANCH: gh-pages-test
- FOLDER: integration
- TARGET_FOLDER: cat/montezuma3
-
- - name: Cleanup Generated Branch
- uses: dawidd6/action-delete-branch@v2.0.1
- with:
- github_token: ${{ secrets.GITHUB_TOKEN }}
- branches: gh-pages-test
-
-
- # Deploys using a custom env.
- integration-env:
- needs: integration-ssh
- runs-on: ubuntu-latest
- steps:
- - uses: actions/setup-node@v1.4.4
- with:
- node-version: '10.x'
-
- - name: Checkout
- uses: actions/checkout@v2
- with:
- persist-credentials: false
-
- - name: Install SSH Client
- uses: webfactory/ssh-agent@v0.4.1
- with:
- ssh-private-key: ${{ secrets.DEPLOY_KEY }}
-
- - name: Build and Deploy
- uses: JamesIves/github-pages-deploy-action@releases/v3-test
- with:
- SSH: true
- BRANCH: gh-pages-test
- FOLDER: integration
- TARGET_FOLDER: cat/montezuma4
-
- - name: Cleanup Generated Branch
- uses: dawidd6/action-delete-branch@v2.0.1
- with:
- github_token: ${{ secrets.GITHUB_TOKEN }}
- branches: gh-pages-test
-
-
- # Deploys using the CLEAN option toggled.
- integration-clean:
- needs: [integration-checkout-v1, integration-checkout-v2, integration-container, integration-ssh, integration-env]
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v2
- with:
- persist-credentials: false
-
- - name: Build and Deploy
- uses: JamesIves/github-pages-deploy-action@releases/v3-test
- with:
- ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
- BRANCH: gh-pages-test
- FOLDER: integration
- CLEAN: true
-
- # Deploys to a branch that doesn't exist with SINGLE_COMMIT.
- integration-branch-creation:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v2
- with:
- persist-credentials: false
-
- - name: Build and Deploy
- uses: JamesIves/github-pages-deploy-action@releases/v3-test
- with:
- ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
- BRANCH: integration-test-delete-beta
- FOLDER: integration
- SINGLE_COMMIT: true
-
- - name: Cleanup Generated Branch
- uses: dawidd6/action-delete-branch@v2.0.1
- with:
- github_token: ${{ secrets.GITHUB_TOKEN }}
- branches: integration-test-delete-beta
diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index 6db33996b..2d1de3f65 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -7,7 +7,7 @@ on:
- '*.*'
branches:
- dev
- - releases/v3
+ - releases/v4
jobs:
# Deploys using checkout@v1 with an ACCESS_TOKEN.
@@ -18,14 +18,14 @@ jobs:
uses: actions/checkout@v1
- name: Build and Deploy
- uses: JamesIves/github-pages-deploy-action@releases/v3
+ uses: JamesIves/github-pages-deploy-action@releases/v4
with:
- ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
- BRANCH: gh-pages
- FOLDER: integration
- TARGET_FOLDER: cat/montezuma
- GIT_CONFIG_NAME: Montezuma
- GIT_CONFIG_EMAIL: montezuma@jamesiv.es
+ token: ${{ secrets.ACCESS_TOKEN }}
+ branch: gh-pages
+ folder: integration
+ target_folder: cat/montezuma
+ git_config_name: Montezuma
+ git_config_email: montezuma@jamesiv.es
- name: Cleanup Generated Branch
uses: dawidd6/action-delete-branch@v2.0.1
@@ -44,12 +44,11 @@ jobs:
persist-credentials: false
- name: Build and Deploy
- uses: JamesIves/github-pages-deploy-action@releases/v3
+ uses: JamesIves/github-pages-deploy-action@releases/v4
with:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- BRANCH: gh-pages
- FOLDER: integration
- TARGET_FOLDER: cat/montezuma2
+ branch: gh-pages
+ folder: integration
+ target_folder: cat/montezuma2
- name: Cleanup Generated Branch
uses: dawidd6/action-delete-branch@v2.0.1
@@ -77,12 +76,11 @@ jobs:
apt-get update && apt-get install -y rsync
- name: Build and Deploy
- uses: JamesIves/github-pages-deploy-action@releases/v3
+ uses: JamesIves/github-pages-deploy-action@releases/v4
with:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- BRANCH: gh-pages
- FOLDER: integration
- TARGET_FOLDER: cat/montezuma2
+ branch: gh-pages
+ folder: integration
+ target_folder: cat/montezuma2
- name: Cleanup Generated Branch
uses: dawidd6/action-delete-branch@v2.0.1
@@ -106,12 +104,12 @@ jobs:
ssh-private-key: ${{ secrets.DEPLOY_KEY }}
- name: Build and Deploy
- uses: JamesIves/github-pages-deploy-action@releases/v3
+ uses: JamesIves/github-pages-deploy-action@releases/v4
with:
- SSH: true
- BRANCH: gh-pages
- FOLDER: integration
- TARGET_FOLDER: cat/montezuma3
+ ssh: true
+ branch: gh-pages
+ folder: integration
+ target_folder: cat/montezuma3
- name: Cleanup Generated Branch
uses: dawidd6/action-delete-branch@v2.0.1
@@ -139,12 +137,12 @@ jobs:
ssh-private-key: ${{ secrets.DEPLOY_KEY }}
- name: Build and Deploy
- uses: JamesIves/github-pages-deploy-action@releases/v3
+ uses: JamesIves/github-pages-deploy-action@releases/v4
with:
- SSH: true
- BRANCH: gh-pages
- FOLDER: integration
- TARGET_FOLDER: cat/montezuma4
+ ssh: true
+ branch: gh-pages
+ folder: integration
+ target_folder: cat/montezuma4
- name: Cleanup Generated Branch
uses: dawidd6/action-delete-branch@v2.0.1
@@ -163,12 +161,12 @@ jobs:
persist-credentials: false
- name: Build and Deploy
- uses: JamesIves/github-pages-deploy-action@releases/v3
+ uses: JamesIves/github-pages-deploy-action@releases/v4
with:
- ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
- BRANCH: gh-pages
- FOLDER: integration
- CLEAN: true
+ token: ${{ secrets.ACCESS_TOKEN }}
+ branch: gh-pages
+ folder: integration
+ clean: true
# Deploys to a branch that doesn't exist with SINGLE_COMMIT.
integration-branch-creation:
@@ -181,12 +179,12 @@ jobs:
persist-credentials: false
- name: Build and Deploy
- uses: JamesIves/github-pages-deploy-action@releases/v3
+ uses: JamesIves/github-pages-deploy-action@releases/v4
with:
- ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
- BRANCH: integration-test-delete-prod
- FOLDER: integration
- SINGLE_COMMIT: true
+ token: ${{ secrets.ACCESS_TOKEN }}
+ branch: integration-test-delete-prod
+ folder: integration
+ single_commit: true
- name: Cleanup Generated Branch
uses: dawidd6/action-delete-branch@v2.0.1
diff --git a/README.md b/README.md
index e5a017a59..36e1cc3db 100644
--- a/README.md
+++ b/README.md
@@ -64,8 +64,8 @@ jobs:
- name: Deploy 🚀
uses: JamesIves/github-pages-deploy-action@3.7.1
with:
- BRANCH: gh-pages # The branch the action should deploy to.
- FOLDER: build # The folder the action should deploy.
+ branch: gh-pages # The branch the action should deploy to.
+ folder: build # The folder the action should deploy.
```
If you'd like to make it so the workflow only triggers on push events to specific branches then you can modify the `on` section.
@@ -124,37 +124,37 @@ The following options must be configured in order to make a deployment.
| Key | Value Information | Type | Required |
| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | -------- |
-| `BRANCH` | This is the branch you wish to deploy to, for example `gh-pages` or `docs`. | `with` | **Yes** |
-| `FOLDER` | The folder in your repository that you want to deploy. If your build script compiles into a directory named `build` you'd put it here. If you wish to deploy the root directory you can place a `.` here. You can also utilize absolute file paths by appending `~` to your folder path. | `with` | **Yes** |
+| `branch` | This is the branch you wish to deploy to, for example `gh-pages` or `docs`. | `with` | **Yes** |
+| `folder` | The folder in your repository that you want to deploy. If your build script compiles into a directory named `build` you'd put it here. If you wish to deploy the root directory you can place a `.` here. You can also utilize absolute file paths by appending `~` to your folder path. | `with` | **Yes** |
By default the action does not need any token configuration and uses the provided repository scoped GitHub token to make the deployment. If you require most customization you can modify the deployment type using the following options.
| Key | Value Information | Type | Required |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | -------- |
-| `TOKEN` | This option defaults to the repository scoped GitHub Token. However if you need more permissions for things such as deploying to another repository, you can add a Personal Access Token (PAT) here. This should be stored in the `secrets / with` menu **as a secret**. We reccomend using a service account with the least permissions neccersary and recommend when generating a new PAT that you select the least permission scopes neccersary. [Learn more about creating and using encrypted secrets here.](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets) | **No** |
-| `SSH` | You can configure the action to deploy using SSH by setting this option to `true`. For more information on how to add your ssh key pair please refer to the [Using a Deploy Key section of this README](https://github.com/JamesIves/github-pages-deploy-action/tree/dev#using-an-ssh-deploy-key-). | `with` | **No** |
+| `token` | This option defaults to the repository scoped GitHub Token. However if you need more permissions for things such as deploying to another repository, you can add a Personal Access Token (PAT) here. This should be stored in the `secrets / with` menu **as a secret**. We reccomend using a service account with the least permissions neccersary and recommend when generating a new PAT that you select the least permission scopes neccersary. [Learn more about creating and using encrypted secrets here.](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets) | **No** |
+| `ssh` | You can configure the action to deploy using SSH by setting this option to `true`. For more information on how to add your ssh key pair please refer to the [Using a Deploy Key section of this README](https://github.com/JamesIves/github-pages-deploy-action/tree/dev#using-an-ssh-deploy-key-). | `with` | **No** |
#### Optional Choices
| Key | Value Information | Type | Required |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | -------- |
-| `GIT_CONFIG_NAME` | Allows you to customize the name that is attached to the git config which is used when pushing the deployment commits. If this is not included it will use the name in the GitHub context, followed by the name of the action. | `with` | **No** |
-| `GIT_CONFIG_EMAIL` | Allows you to customize the email that is attached to the git config which is used when pushing the deployment commits. If this is not included it will use the email in the GitHub context, followed by a generic noreply GitHub email. | `with` | **No** |
-| `REPOSITORY_NAME` | Allows you to specify a different repository path so long as you have permissions to push to it. This should be formatted like so: `JamesIves/github-pages-deploy-action`. You'll need to use a PAT in the `TOKEN` input for this configuration option to work properly. | `with` | **No** |
-| `TARGET_FOLDER` | If you'd like to push the contents of the deployment folder into a specific directory on the deployment branch you can specify it here. | `with` | **No** |
-| `COMMIT_MESSAGE` | If you need to customize the commit message for an integration you can do so. | `with` | **No** |
-| `CLEAN` | If your project generates hashed files on build you can use this option to automatically delete them from the deployment branch with each deploy. This option is turned on by default, and can be toggled off by setting it to `false`. | `with` | **No** |
-| `CLEAN_EXCLUDE` | If you need to use `CLEAN` but you'd like to preserve certain files or folders you can use this option. This should be formatted as an array but stored as a string. For example: `'["filename.js", "folder"]'` | `with` | **No** |
-| `DRY_RUN` | Do not actually push back, but use `--dry-run` on `git push` invocations insead. | `with` | **No** |
-| `SINGLE_COMMIT` | This option can be toggled to `true` if you'd prefer to have a single commit on the deployment branch instead of maintaining the full history. **Using this option will also cause any existing history to be wiped from the deployment branch**. | `with` | **No** |
-| `SILENT` | Silences the action output preventing it from displaying git messages. | `with` | **No** |
-| `WORKSPACE` | This should point to where your project lives on the virtual machine. The GitHub Actions environment will set this for you. It is only necessary to set this variable if you're using the node module. | `with` | **No** |
+| `git_config_name` | Allows you to customize the name that is attached to the git config which is used when pushing the deployment commits. If this is not included it will use the name in the GitHub context, followed by the name of the action. | `with` | **No** |
+| `git_config_email` | Allows you to customize the email that is attached to the git config which is used when pushing the deployment commits. If this is not included it will use the email in the GitHub context, followed by a generic noreply GitHub email. | `with` | **No** |
+| `repository_name` | Allows you to specify a different repository path so long as you have permissions to push to it. This should be formatted like so: `JamesIves/github-pages-deploy-action`. You'll need to use a PAT in the `token` input for this configuration option to work properly. | `with` | **No** |
+| `target_folder` | If you'd like to push the contents of the deployment folder into a specific directory on the deployment branch you can specify it here. | `with` | **No** |
+| `commit_message` | If you need to customize the commit message for an integration you can do so. | `with` | **No** |
+| `clean` | If your project generates hashed files on build you can use this option to automatically delete them from the deployment branch with each deploy. This option is turned on by default, and can be toggled off by setting it to `false`. | `with` | **No** |
+| `clean_exclude` | If you need to use `clean` but you'd like to preserve certain files or folders you can use this option. This should be formatted as an array but stored as a string. For example: `'["filename.js", "folder"]'` | `with` | **No** |
+| `dry_run` | Do not actually push back, but use `--dry-run` on `git push` invocations insead. | `with` | **No** |
+| `single_commit` | This option can be toggled to `true` if you'd prefer to have a single commit on the deployment branch instead of maintaining the full history. **Using this option will also cause any existing history to be wiped from the deployment branch**. | `with` | **No** |
+| `silent` | Silences the action output preventing it from displaying git messages. | `with` | **No** |
+| `workspace` | This should point to where your project lives on the virtual machine. The GitHub Actions environment will set this for you. It is only necessary to set this variable if you're using the node module. | `with` | **No** |
With the action correctly configured you should see the workflow trigger the deployment under the configured conditions.
#### Deployment Status
-The action will export an environment variable called `DEPLOYMENT_STATUS` that you can use in your workflow to determine if the deployment was successful or not. You can find an explanation of each status type below.
+The action will export an environment variable called `deployment_status` that you can use in your workflow to determine if the deployment was successful or not. You can find an explanation of each status type below.
| Status | Description |
| ------------- |-------------|
@@ -174,7 +174,7 @@ ssh-keygen -t rsa -m pem -b 4096 -C "youremailhere@example.com" -N ""
Once you've generated the key pair you must add the contents of the public key within your repository's [deploy keys menu](https://developer.github.com/v3/guides/managing-deploy-keys/). You can find this option by going to `Settings > Deploy Keys`, you can name the public key whatever you want, but you **do** need to give it write access. Afterwards add the contents of the private key to the `Settings > Secrets` menu as `DEPLOY_KEY`.
-With this configured you must add the `ssh-agent` step to your workflow and set `SSH` to `true` within the deploy action. There are several SSH actions available on the [GitHub marketplace](https://github.com/marketplace?type=actions) for you to choose from.
+With this configured you must add the `ssh-agent` step to your workflow and set `ssh` to `true` within the deploy action. There are several SSH actions available on the [GitHub marketplace](https://github.com/marketplace?type=actions) for you to choose from.
```yml
- name: Install SSH Client 🔑
@@ -185,9 +185,9 @@ With this configured you must add the `ssh-agent` step to your workflow and set
- name: Deploy 🚀
uses: JamesIves/github-pages-deploy-action@3.7.1
with:
- SSH: true
- BRANCH: gh-pages
- FOLDER: site
+ ssh: true
+ branch: gh-pages
+ folder: site
```
You can view a full example of this here.
@@ -221,10 +221,10 @@ jobs:
- name: Deploy 🚀
uses: JamesIves/github-pages-deploy-action@3.7.1
with:
- BRANCH: gh-pages
- FOLDER: build
- CLEAN: true
- SSH: true # SSH must be set to true so the deploy action knows which protocol to deploy with.
+ branch: gh-pages
+ folder: build
+ clean: true
+ ssh: true # SSH must be set to true so the deploy action knows which protocol to deploy with.
```
@@ -287,9 +287,9 @@ jobs:
- name: Deploy 🚀
uses: JamesIves/github-pages-deploy-action@3.7.1
with:
- ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
- BRANCH: gh-pages
- FOLDER: "site" # The deployment folder should match the name of the artifact. Even though our project builds into the 'build' folder the artifact name of 'site' must be placed here.
+ token: ${{ secrets.ACCESS_TOKEN }}
+ branch: gh-pages
+ folder: "site" # The deployment folder should match the name of the artifact. Even though our project builds into the 'build' folder the artifact name of 'site' must be placed here.
```
diff --git a/src/constants.ts b/src/constants.ts
index 91a323050..4a6b12ed3 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -74,47 +74,47 @@ export interface NodeActionInterface {
/* Required action data that gets initialized when running within the GitHub Actions environment. */
export const action: ActionInterface = {
- folder: getInput('FOLDER'),
- branch: getInput('BRANCH'),
- commitMessage: getInput('COMMIT_MESSAGE'),
- dryRun: !isNullOrUndefined(getInput('DRY_RUN'))
- ? getInput('DRY_RUN').toLowerCase() === 'true'
+ folder: getInput('folder'),
+ branch: getInput('branch'),
+ commitMessage: getInput('commit_message'),
+ dryRun: !isNullOrUndefined(getInput('dry_run'))
+ ? getInput('dry_run').toLowerCase() === 'true'
: false,
- clean: !isNullOrUndefined(getInput('CLEAN'))
- ? getInput('CLEAN').toLowerCase() === 'true'
+ clean: !isNullOrUndefined(getInput('clean'))
+ ? getInput('clean').toLowerCase() === 'true'
: false,
- cleanExclude: getInput('CLEAN_EXCLUDE'),
+ cleanExclude: getInput('clean_exclude'),
isTest: TestFlag.NONE,
- email: !isNullOrUndefined(getInput('GIT_CONFIG_EMAIL'))
- ? getInput('GIT_CONFIG_EMAIL')
+ email: !isNullOrUndefined(getInput('git_config_email'))
+ ? getInput('git_config_email')
: pusher && pusher.email
? pusher.email
: `${
process.env.GITHUB_ACTOR || 'github-pages-deploy-action'
}@users.noreply.github.com`,
- name: !isNullOrUndefined(getInput('GIT_CONFIG_NAME'))
- ? getInput('GIT_CONFIG_NAME')
+ name: !isNullOrUndefined(getInput('git_config_name'))
+ ? getInput('git_config_name')
: pusher && pusher.name
? pusher.name
: process.env.GITHUB_ACTOR
? process.env.GITHUB_ACTOR
: 'GitHub Pages Deploy Action',
- repositoryName: !isNullOrUndefined(getInput('REPOSITORY_NAME'))
- ? getInput('REPOSITORY_NAME')
+ repositoryName: !isNullOrUndefined(getInput('repository_name'))
+ ? getInput('repository_name')
: repository && repository.full_name
? repository.full_name
: process.env.GITHUB_REPOSITORY,
- token: getInput('TOKEN'),
- singleCommit: !isNullOrUndefined(getInput('SINGLE_COMMIT'))
- ? getInput('SINGLE_COMMIT').toLowerCase() === 'true'
+ token: getInput('token'),
+ singleCommit: !isNullOrUndefined(getInput('single_commit'))
+ ? getInput('single_commit').toLowerCase() === 'true'
: false,
- silent: !isNullOrUndefined(getInput('SILENT'))
- ? getInput('SILENT').toLowerCase() === 'true'
+ silent: !isNullOrUndefined(getInput('silent'))
+ ? getInput('silent').toLowerCase() === 'true'
: false,
- ssh: !isNullOrUndefined(getInput('SSH'))
- ? getInput('SSH').toLowerCase() === 'true'
+ ssh: !isNullOrUndefined(getInput('ssh'))
+ ? getInput('ssh').toLowerCase() === 'true'
: false,
- targetFolder: getInput('TARGET_FOLDER'),
+ targetFolder: getInput('target_folder'),
workspace: process.env.GITHUB_WORKSPACE || ''
}
diff --git a/src/lib.ts b/src/lib.ts
index 8f7b2c5c7..072b24eac 100644
--- a/src/lib.ts
+++ b/src/lib.ts
@@ -59,6 +59,6 @@ export default async function run(
}`
)
- exportVariable('DEPLOYMENT_STATUS', status)
+ exportVariable('deployment_status', status)
}
}
From 7bf80b4b88484642613dcd34fe2cd1f6911f222b Mon Sep 17 00:00:00 2001
From: Axel Hecht
Date: Tue, 29 Dec 2020 16:34:23 +0100
Subject: [PATCH 13/32] Use multi-line string for clean-exclude patterns.
(#553)
As this change is subtle, I'm taking the opportunity to change
the underscore for the hyphen, which makes it less likely that
users of this action will just pass in an old json array.
---
README.md | 5 ++++-
__tests__/git.test.ts | 8 +++-----
action.yml | 4 ++--
src/constants.ts | 6 ++++--
src/git.ts | 15 ++-------------
5 files changed, 15 insertions(+), 23 deletions(-)
diff --git a/README.md b/README.md
index 36e1cc3db..ddeeb655c 100644
--- a/README.md
+++ b/README.md
@@ -144,7 +144,7 @@ By default the action does not need any token configuration and uses the provide
| `target_folder` | If you'd like to push the contents of the deployment folder into a specific directory on the deployment branch you can specify it here. | `with` | **No** |
| `commit_message` | If you need to customize the commit message for an integration you can do so. | `with` | **No** |
| `clean` | If your project generates hashed files on build you can use this option to automatically delete them from the deployment branch with each deploy. This option is turned on by default, and can be toggled off by setting it to `false`. | `with` | **No** |
-| `clean_exclude` | If you need to use `clean` but you'd like to preserve certain files or folders you can use this option. This should be formatted as an array but stored as a string. For example: `'["filename.js", "folder"]'` | `with` | **No** |
+| `clean-exclude` | If you need to use `clean` but you'd like to preserve certain files or folders you can use this option. This should contain each pattern as a single line in a multiline string. | `with` | **No** |
| `dry_run` | Do not actually push back, but use `--dry-run` on `git push` invocations insead. | `with` | **No** |
| `single_commit` | This option can be toggled to `true` if you'd prefer to have a single commit on the deployment branch instead of maintaining the full history. **Using this option will also cause any existing history to be wiped from the deployment branch**. | `with` | **No** |
| `silent` | Silences the action output preventing it from displaying git messages. | `with` | **No** |
@@ -224,6 +224,9 @@ jobs:
branch: gh-pages
folder: build
clean: true
+ clean-exclude: |
+ special-file.txt
+ some/*.txt
ssh: true # SSH must be set to true so the deploy action knows which protocol to deploy with.
```
diff --git a/__tests__/git.test.ts b/__tests__/git.test.ts
index 4b8bc7e2e..e621385ab 100644
--- a/__tests__/git.test.ts
+++ b/__tests__/git.test.ts
@@ -231,7 +231,6 @@ describe('git', () => {
email: 'as@cat'
},
clean: true,
- cleanExclude: '["cat", "montezuma"]',
workspace: 'other',
isTest: TestFlag.NONE
})
@@ -244,7 +243,7 @@ describe('git', () => {
})
})
- it('should execute commands with clean options stored as an array instead', async () => {
+ it('should execute commands with clean options stored as an array', async () => {
Object.assign(action, {
silent: false,
folder: 'assets',
@@ -267,7 +266,7 @@ describe('git', () => {
expect(rmRF).toBeCalledTimes(1)
})
- it('should gracefully handle incorrectly formatted clean exclude items', async () => {
+ it('should gracefully handle target folder', async () => {
Object.assign(action, {
silent: false,
folder: '.',
@@ -277,8 +276,7 @@ describe('git', () => {
clean: true,
targetFolder: 'new_folder',
commitMessage: 'Hello!',
- isTest: TestFlag.NONE,
- cleanExclude: '["cat, "montezuma"]' // There is a syntax errror in the string.
+ isTest: TestFlag.NONE
})
await deploy(action)
diff --git a/action.yml b/action.yml
index 746943031..50aaba54e 100644
--- a/action.yml
+++ b/action.yml
@@ -46,8 +46,8 @@ inputs:
required: false
default: 'true'
- CLEAN_EXCLUDE:
- description: "If you need to use CLEAN but you would like to preserve certain files or folders you can use this option. This should be formatted as an array but stored as a string."
+ clean-exclude:
+ description: "If you need to use CLEAN but you would like to preserve certain files or folders you can use this option. This should contain each pattern as a single line in a multiline string."
required: false
DRY_RUN:
diff --git a/src/constants.ts b/src/constants.ts
index 4a6b12ed3..097612f55 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -20,7 +20,7 @@ export interface ActionInterface {
/** If your project generates hashed files on build you can use this option to automatically delete them from the deployment branch with each deploy. This option can be toggled on by setting it to true. */
clean?: boolean | null
/** If you need to use CLEAN but you'd like to preserve certain files or folders you can use this option. */
- cleanExclude?: string | string[]
+ cleanExclude?: string[]
/** If you need to customize the commit message for an integration you can do so. */
commitMessage?: string
/** The git config email. */
@@ -83,7 +83,9 @@ export const action: ActionInterface = {
clean: !isNullOrUndefined(getInput('clean'))
? getInput('clean').toLowerCase() === 'true'
: false,
- cleanExclude: getInput('clean_exclude'),
+ cleanExclude: (getInput('clean-exclude') || '')
+ .split('\n')
+ .filter(l => l !== ''),
isTest: TestFlag.NONE,
email: !isNullOrUndefined(getInput('git_config_email'))
? getInput('git_config_email')
diff --git a/src/git.ts b/src/git.ts
index 1c931c188..ea432fbf4 100644
--- a/src/git.ts
+++ b/src/git.ts
@@ -65,19 +65,8 @@ export async function deploy(action: ActionInterface): Promise {
// Ensures that items that need to be excluded from the clean job get parsed.
let excludes = ''
if (action.clean && action.cleanExclude) {
- try {
- const excludedItems =
- typeof action.cleanExclude === 'string'
- ? JSON.parse(action.cleanExclude)
- : action.cleanExclude
-
- for (const item of excludedItems) {
- excludes += `--exclude ${item} `
- }
- } catch {
- info(
- 'There was an error parsing your CLEAN_EXCLUDE items. Please refer to the README for more details. ❌'
- )
+ for (const item of action.cleanExclude) {
+ excludes += `--exclude ${item} `
}
}
From 291c5c792e28160dd5f6cdb7b1137477d15641cd Mon Sep 17 00:00:00 2001
From: Axel Hecht
Date: Tue, 5 Jan 2021 16:39:10 +0100
Subject: [PATCH 14/32] Hyphenate inputs and outputs, add step output, fix #558
(#559)
* Hyphenate inputs and outputs, add step output, fix #558
I've also tried to make the clean docs a bit clearer, and consistent
about clean being on my default. Still not totally happy with the intro
of the docs there, though.
* Add testing of step outputs to build integration tests
---
.github/workflows/build.yml | 27 +++++++++++++++++++----
.github/workflows/integration.yml | 16 +++++++-------
README.md | 18 +++++++++-------
__tests__/git.test.ts | 1 +
__tests__/main.test.ts | 1 +
action.yml | 36 +++++++++++++++----------------
src/constants.ts | 24 ++++++++++-----------
src/lib.ts | 3 ++-
8 files changed, 75 insertions(+), 51 deletions(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index ad8d45367..199f72911 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -91,12 +91,22 @@ jobs:
name: dist
- name: Deploy
+ id: unmodified
uses: ./
with:
folder: integration
branch: ${{ matrix.branch }}
- SINGLE_COMMIT: ${{ matrix.commit == 'singleCommit' }}
- dry_run: true
+ single-commit: ${{ matrix.commit == 'singleCommit' }}
+ dry-run: true
+
+ # Usually, this should be skipped, but if the upstream gh-pages
+ # branch doesn't match ours, it should still be a success.
+ - name: Check step output
+ run: |
+ [[ \
+ ${{steps.unmodified.outputs.deployment-status}} = skipped || \
+ ${{steps.unmodified.outputs.deployment-status}} = success \
+ ]]
- name: Tweak content to publish to existing branch
if: ${{ matrix.branch == 'gh-pages' }}
@@ -104,10 +114,19 @@ jobs:
echo "" >> integration/index.html
- name: Deploy with modifications to existing branch
+ id: modified
uses: ./
if: ${{ matrix.branch == 'gh-pages' }}
with:
folder: integration
branch: ${{ matrix.branch }}
- single_commit: ${{ matrix.commit == 'singleCommit' }}
- dry_run: true
+ single-commit: ${{ matrix.commit == 'singleCommit' }}
+ dry-run: true
+
+ # The modified deployment should be a success, and not skipped.
+ - name: Check step output
+ if: ${{ matrix.branch == 'gh-pages' }}
+ run: |
+ [[ \
+ ${{steps.modified.outputs.deployment-status}} = success \
+ ]]
diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index 2d1de3f65..f46ad8b63 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -23,9 +23,9 @@ jobs:
token: ${{ secrets.ACCESS_TOKEN }}
branch: gh-pages
folder: integration
- target_folder: cat/montezuma
- git_config_name: Montezuma
- git_config_email: montezuma@jamesiv.es
+ target-folder: cat/montezuma
+ git-config-name: Montezuma
+ git-config-email: montezuma@jamesiv.es
- name: Cleanup Generated Branch
uses: dawidd6/action-delete-branch@v2.0.1
@@ -48,7 +48,7 @@ jobs:
with:
branch: gh-pages
folder: integration
- target_folder: cat/montezuma2
+ target-folder: cat/montezuma2
- name: Cleanup Generated Branch
uses: dawidd6/action-delete-branch@v2.0.1
@@ -80,7 +80,7 @@ jobs:
with:
branch: gh-pages
folder: integration
- target_folder: cat/montezuma2
+ target-folder: cat/montezuma2
- name: Cleanup Generated Branch
uses: dawidd6/action-delete-branch@v2.0.1
@@ -109,7 +109,7 @@ jobs:
ssh: true
branch: gh-pages
folder: integration
- target_folder: cat/montezuma3
+ target-folder: cat/montezuma3
- name: Cleanup Generated Branch
uses: dawidd6/action-delete-branch@v2.0.1
@@ -142,7 +142,7 @@ jobs:
ssh: true
branch: gh-pages
folder: integration
- target_folder: cat/montezuma4
+ target-folder: cat/montezuma4
- name: Cleanup Generated Branch
uses: dawidd6/action-delete-branch@v2.0.1
@@ -184,7 +184,7 @@ jobs:
token: ${{ secrets.ACCESS_TOKEN }}
branch: integration-test-delete-prod
folder: integration
- single_commit: true
+ single-commit: true
- name: Cleanup Generated Branch
uses: dawidd6/action-delete-branch@v2.0.1
diff --git a/README.md b/README.md
index ddeeb655c..9de870103 100644
--- a/README.md
+++ b/README.md
@@ -138,15 +138,15 @@ By default the action does not need any token configuration and uses the provide
| Key | Value Information | Type | Required |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | -------- |
-| `git_config_name` | Allows you to customize the name that is attached to the git config which is used when pushing the deployment commits. If this is not included it will use the name in the GitHub context, followed by the name of the action. | `with` | **No** |
-| `git_config_email` | Allows you to customize the email that is attached to the git config which is used when pushing the deployment commits. If this is not included it will use the email in the GitHub context, followed by a generic noreply GitHub email. | `with` | **No** |
-| `repository_name` | Allows you to specify a different repository path so long as you have permissions to push to it. This should be formatted like so: `JamesIves/github-pages-deploy-action`. You'll need to use a PAT in the `token` input for this configuration option to work properly. | `with` | **No** |
-| `target_folder` | If you'd like to push the contents of the deployment folder into a specific directory on the deployment branch you can specify it here. | `with` | **No** |
-| `commit_message` | If you need to customize the commit message for an integration you can do so. | `with` | **No** |
-| `clean` | If your project generates hashed files on build you can use this option to automatically delete them from the deployment branch with each deploy. This option is turned on by default, and can be toggled off by setting it to `false`. | `with` | **No** |
+| `git-config-name` | Allows you to customize the name that is attached to the git config which is used when pushing the deployment commits. If this is not included it will use the name in the GitHub context, followed by the name of the action. | `with` | **No** |
+| `git-config-email` | Allows you to customize the email that is attached to the git config which is used when pushing the deployment commits. If this is not included it will use the email in the GitHub context, followed by a generic noreply GitHub email. | `with` | **No** |
+| `repository-name` | Allows you to specify a different repository path so long as you have permissions to push to it. This should be formatted like so: `JamesIves/github-pages-deploy-action`. You'll need to use a PAT in the `token` input for this configuration option to work properly. | `with` | **No** |
+| `target-folder` | If you'd like to push the contents of the deployment folder into a specific directory on the deployment branch you can specify it here. | `with` | **No** |
+| `commit-message` | If you need to customize the commit message for an integration you can do so. | `with` | **No** |
+| `clean` | If your project generates hashed files on build you can use this option to automatically delete them from the target folder on the deployment branch with each deploy. This option is turned on by default, and can be toggled off by setting it to `false`. | `with` | **No** |
| `clean-exclude` | If you need to use `clean` but you'd like to preserve certain files or folders you can use this option. This should contain each pattern as a single line in a multiline string. | `with` | **No** |
-| `dry_run` | Do not actually push back, but use `--dry-run` on `git push` invocations insead. | `with` | **No** |
-| `single_commit` | This option can be toggled to `true` if you'd prefer to have a single commit on the deployment branch instead of maintaining the full history. **Using this option will also cause any existing history to be wiped from the deployment branch**. | `with` | **No** |
+| `dry-run` | Do not actually push back, but use `--dry-run` on `git push` invocations insead. | `with` | **No** |
+| `single-commit` | This option can be toggled to `true` if you'd prefer to have a single commit on the deployment branch instead of maintaining the full history. **Using this option will also cause any existing history to be wiped from the deployment branch**. | `with` | **No** |
| `silent` | Silences the action output preventing it from displaying git messages. | `with` | **No** |
| `workspace` | This should point to where your project lives on the virtual machine. The GitHub Actions environment will set this for you. It is only necessary to set this variable if you're using the node module. | `with` | **No** |
@@ -162,6 +162,8 @@ The action will export an environment variable called `deployment_status` that y
| `failed` | The `failed` status indicates that the action encountered an error while trying to deploy. |
| `skipped` | The `skipped` status indicates that the action exited early as there was nothing new to deploy. |
+This value is also set as a step output as `deployment-status`.
+
---
### Using an SSH Deploy Key 🔑
diff --git a/__tests__/git.test.ts b/__tests__/git.test.ts
index e621385ab..b6d38e761 100644
--- a/__tests__/git.test.ts
+++ b/__tests__/git.test.ts
@@ -18,6 +18,7 @@ jest.mock('fs', () => ({
jest.mock('@actions/core', () => ({
setFailed: jest.fn(),
getInput: jest.fn(),
+ setOutput: jest.fn(),
isDebug: jest.fn(),
info: jest.fn()
}))
diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts
index 4d02492fd..fc5b9d3b4 100644
--- a/__tests__/main.test.ts
+++ b/__tests__/main.test.ts
@@ -24,6 +24,7 @@ jest.mock('@actions/io', () => ({
jest.mock('@actions/core', () => ({
setFailed: jest.fn(),
getInput: jest.fn(),
+ setOutput: jest.fn(),
exportVariable: jest.fn(),
isDebug: jest.fn(),
info: jest.fn()
diff --git a/action.yml b/action.yml
index 50aaba54e..00e1398e9 100644
--- a/action.yml
+++ b/action.yml
@@ -8,11 +8,11 @@ branding:
icon: 'git-commit'
color: 'orange'
inputs:
- SSH:
+ ssh:
description: 'You can configure the action to deploy using SSH by setting this option to true. More more information on how to add your ssh key pair please refer to the Using a Deploy Key section of this README.'
required: false
- TOKEN:
+ token:
description: >
This option defaults to the repository scoped GitHub Token.
However if you need more permissions for things such as deploying to another repository, you can add a Personal Access Token (PAT) here.
@@ -25,59 +25,59 @@ inputs:
required: false
default: ${{ github.token }}
- BRANCH:
+ branch:
description: 'This is the branch you wish to deploy to, for example gh-pages or docs.'
required: true
- FOLDER:
+ folder:
description: 'The folder in your repository that you want to deploy. If your build script compiles into a directory named build you would put it here. Folder paths cannot have a leading / or ./. If you wish to deploy the root directory you can place a . here.'
required: true
- TARGET_FOLDER:
+ target-folder:
description: 'If you would like to push the contents of the deployment folder into a specific directory on the deployment branch you can specify it here.'
required: false
- COMMIT_MESSAGE:
+ commit-message:
description: 'If you need to customize the commit message for an integration you can do so.'
required: false
- CLEAN:
- description: 'If your project generates hashed files on build you can use this option to automatically delete them from the deployment branch with each deploy. This option can be toggled on by setting it to true.'
+ clean:
+ description: 'If your project generates hashed files on build you can use this option to automatically delete them from the target folder on the deployment branch with each deploy. This option is on by default and can be toggled off by setting it to false.'
required: false
- default: 'true'
+ default: true
clean-exclude:
- description: "If you need to use CLEAN but you would like to preserve certain files or folders you can use this option. This should contain each pattern as a single line in a multiline string."
+ description: "If you need to use clean but you would like to preserve certain files or folders you can use this option. This should contain each pattern as a single line in a multiline string."
required: false
- DRY_RUN:
+ dry-run:
description: "Do not actually push back, but use `--dry-run` on `git push` invocations insead."
required: false
- GIT_CONFIG_NAME:
+ git-config-name:
description: "Allows you to customize the name that is attached to the GitHub config which is used when pushing the deployment commits. If this is not included it will use the name in the GitHub context, followed by the name of the action."
required: false
- GIT_CONFIG_EMAIL:
+ git-config-email:
description: "Allows you to customize the email that is attached to the GitHub config which is used when pushing the deployment commits. If this is not included it will use the email in the GitHub context, followed by a generic noreply GitHub email."
required: false
- REPOSITORY_NAME:
+ repository-name:
description: "Allows you to speicfy a different repository path so long as you have permissions to push to it. This should be formatted like so: JamesIves/github-pages-deploy-action"
required: false
- WORKSPACE:
+ workspace:
description: "This should point to where your project lives on the virtual machine. The GitHub Actions environment will set this for you. It is only neccersary to set this variable if you're using the node module."
required: false
- SINGLE_COMMIT:
+ single-commit:
description: "This option can be used if you'd prefer to have a single commit on the deployment branch instead of maintaining the full history."
required: false
- SILENT:
+ silent:
description: "Silences the action output preventing it from displaying git messages."
required: false
outputs:
- DEPLOYMENT_STATUS:
+ deployment-status:
description: 'The status of the deployment that indicates if the run failed or passed. Possible outputs include: success|failed|skipped'
diff --git a/src/constants.ts b/src/constants.ts
index 097612f55..2990871e6 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -76,9 +76,9 @@ export interface NodeActionInterface {
export const action: ActionInterface = {
folder: getInput('folder'),
branch: getInput('branch'),
- commitMessage: getInput('commit_message'),
- dryRun: !isNullOrUndefined(getInput('dry_run'))
- ? getInput('dry_run').toLowerCase() === 'true'
+ commitMessage: getInput('commit-message'),
+ dryRun: !isNullOrUndefined(getInput('dry-run'))
+ ? getInput('dry-run').toLowerCase() === 'true'
: false,
clean: !isNullOrUndefined(getInput('clean'))
? getInput('clean').toLowerCase() === 'true'
@@ -87,28 +87,28 @@ export const action: ActionInterface = {
.split('\n')
.filter(l => l !== ''),
isTest: TestFlag.NONE,
- email: !isNullOrUndefined(getInput('git_config_email'))
- ? getInput('git_config_email')
+ email: !isNullOrUndefined(getInput('git-config-email'))
+ ? getInput('git-config-email')
: pusher && pusher.email
? pusher.email
: `${
process.env.GITHUB_ACTOR || 'github-pages-deploy-action'
}@users.noreply.github.com`,
- name: !isNullOrUndefined(getInput('git_config_name'))
- ? getInput('git_config_name')
+ name: !isNullOrUndefined(getInput('git-config-name'))
+ ? getInput('git-config-name')
: pusher && pusher.name
? pusher.name
: process.env.GITHUB_ACTOR
? process.env.GITHUB_ACTOR
: 'GitHub Pages Deploy Action',
- repositoryName: !isNullOrUndefined(getInput('repository_name'))
- ? getInput('repository_name')
+ repositoryName: !isNullOrUndefined(getInput('repository-name'))
+ ? getInput('repository-name')
: repository && repository.full_name
? repository.full_name
: process.env.GITHUB_REPOSITORY,
token: getInput('token'),
- singleCommit: !isNullOrUndefined(getInput('single_commit'))
- ? getInput('single_commit').toLowerCase() === 'true'
+ singleCommit: !isNullOrUndefined(getInput('single-commit'))
+ ? getInput('single-commit').toLowerCase() === 'true'
: false,
silent: !isNullOrUndefined(getInput('silent'))
? getInput('silent').toLowerCase() === 'true'
@@ -116,7 +116,7 @@ export const action: ActionInterface = {
ssh: !isNullOrUndefined(getInput('ssh'))
? getInput('ssh').toLowerCase() === 'true'
: false,
- targetFolder: getInput('target_folder'),
+ targetFolder: getInput('target-folder'),
workspace: process.env.GITHUB_WORKSPACE || ''
}
diff --git a/src/lib.ts b/src/lib.ts
index 072b24eac..9b3a3434a 100644
--- a/src/lib.ts
+++ b/src/lib.ts
@@ -1,4 +1,4 @@
-import {exportVariable, info, setFailed} from '@actions/core'
+import {exportVariable, info, setFailed, setOutput} from '@actions/core'
import {ActionInterface, Status, NodeActionInterface} from './constants'
import {deploy, init} from './git'
import {
@@ -60,5 +60,6 @@ export default async function run(
)
exportVariable('deployment_status', status)
+ setOutput('deployment-status', status)
}
}
From 58cb667763d2eb5808facaf19631635d2cd18099 Mon Sep 17 00:00:00 2001
From: James Ives
Date: Mon, 18 Jan 2021 09:54:40 -0500
Subject: [PATCH 15/32] Security Docs
---
SECURITY.md | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/SECURITY.md b/SECURITY.md
index 3890d4eed..a0811c8f0 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -2,12 +2,12 @@
## Supported Versions
-The current versions are actively maintained and will receieve frequent updates and security patches.
+The current version is actively maintained and will receive frequent updates and security patches.
| Version | Supported |
| ------- | ------------------ |
-| 3.0.x | :white_check_mark: |
-| < 2.0 | :x: |
+| 4.0.x | :white_check_mark: |
+| < 3.0 | :x: |
## Reporting a Vulnerability
From 639ff537d5bf88b668efb3a56d361e2a03f008c9 Mon Sep 17 00:00:00 2001
From: James Ives
Date: Mon, 18 Jan 2021 09:56:42 -0500
Subject: [PATCH 16/32] Integration tests
---
.github/workflows/integration.yml | 14 ++------------
1 file changed, 2 insertions(+), 12 deletions(-)
diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index f46ad8b63..331ab91a3 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -98,15 +98,10 @@ jobs:
with:
persist-credentials: false
- - name: Install SSH Client
- uses: webfactory/ssh-agent@v0.4.1
- with:
- ssh-private-key: ${{ secrets.DEPLOY_KEY }}
-
- name: Build and Deploy
uses: JamesIves/github-pages-deploy-action@releases/v4
with:
- ssh: true
+ ssh-key: ${{ secrets.DEPLOY_KEY }}
branch: gh-pages
folder: integration
target-folder: cat/montezuma3
@@ -131,15 +126,10 @@ jobs:
with:
persist-credentials: false
- - name: Install SSH Client
- uses: webfactory/ssh-agent@v0.4.1
- with:
- ssh-private-key: ${{ secrets.DEPLOY_KEY }}
-
- name: Build and Deploy
uses: JamesIves/github-pages-deploy-action@releases/v4
with:
- ssh: true
+ ssh-key: ${{ secrets.DEPLOY_KEY }}
branch: gh-pages
folder: integration
target-folder: cat/montezuma4
From e00d6bfda7446e15323592103729e8a25d931d1a Mon Sep 17 00:00:00 2001
From: James Ives
Date: Mon, 18 Jan 2021 09:56:53 -0500
Subject: [PATCH 17/32] Revert "Integration tests"
This reverts commit 639ff537d5bf88b668efb3a56d361e2a03f008c9.
---
.github/workflows/integration.yml | 14 ++++++++++++--
1 file changed, 12 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index 331ab91a3..f46ad8b63 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -98,10 +98,15 @@ jobs:
with:
persist-credentials: false
+ - name: Install SSH Client
+ uses: webfactory/ssh-agent@v0.4.1
+ with:
+ ssh-private-key: ${{ secrets.DEPLOY_KEY }}
+
- name: Build and Deploy
uses: JamesIves/github-pages-deploy-action@releases/v4
with:
- ssh-key: ${{ secrets.DEPLOY_KEY }}
+ ssh: true
branch: gh-pages
folder: integration
target-folder: cat/montezuma3
@@ -126,10 +131,15 @@ jobs:
with:
persist-credentials: false
+ - name: Install SSH Client
+ uses: webfactory/ssh-agent@v0.4.1
+ with:
+ ssh-private-key: ${{ secrets.DEPLOY_KEY }}
+
- name: Build and Deploy
uses: JamesIves/github-pages-deploy-action@releases/v4
with:
- ssh-key: ${{ secrets.DEPLOY_KEY }}
+ ssh: true
branch: gh-pages
folder: integration
target-folder: cat/montezuma4
From 64eb7112e478f1fea16def2dabef32ec9119c3e8 Mon Sep 17 00:00:00 2001
From: James Ives
Date: Thu, 21 Jan 2021 09:08:31 -0500
Subject: [PATCH 18/32] Native SSH Key Support (#569)
* SSH Key Support :key:
* Update ssh.ts
* Update src/ssh.ts
Co-authored-by: Axel Hecht
* README fixes/etc
* Unit Tests & README
* ssh key
* Update README.md
* Update ssh.test.ts
* Update ssh.test.ts
* Update ssh.test.ts
* Update ssh.test.ts
* Update ssh.test.ts
* Update ssh.test.ts
* Update integration.yml
Co-authored-by: Axel Hecht
---
.github/workflows/integration.yml | 33 ++++++++--
README.md | 20 ++----
__tests__/main.test.ts | 3 +-
__tests__/ssh.test.ts | 103 ++++++++++++++++++++++++++++++
__tests__/util.test.ts | 18 +++---
action.yml | 10 ++-
src/constants.ts | 20 +++---
src/lib.ts | 9 ++-
src/ssh.ts | 47 ++++++++++++++
src/util.ts | 8 +--
10 files changed, 224 insertions(+), 47 deletions(-)
create mode 100644 __tests__/ssh.test.ts
create mode 100644 src/ssh.ts
diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index f46ad8b63..5ced59be3 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -90,6 +90,30 @@ jobs:
# Deploys using an SSH key.
integration-ssh:
+ needs: integration-container
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+ with:
+ persist-credentials: false
+
+ - name: Build and Deploy
+ uses: JamesIves/github-pages-deploy-action@releases/v4
+ with:
+ ssh-key: ${{ secrets.DEPLOY_KEY }}
+ branch: gh-pages
+ folder: integration
+ target-folder: cat/montezuma3
+
+ - name: Cleanup Generated Branch
+ uses: dawidd6/action-delete-branch@v2.0.1
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ branches: gh-pages
+
+ # Deploys using an SSH key.
+ integration-ssh-third-party-client:
needs: integration-container
runs-on: ubuntu-latest
steps:
@@ -109,7 +133,7 @@ jobs:
ssh: true
branch: gh-pages
folder: integration
- target-folder: cat/montezuma3
+ target-folder: cat/montezuma4
- name: Cleanup Generated Branch
uses: dawidd6/action-delete-branch@v2.0.1
@@ -131,15 +155,10 @@ jobs:
with:
persist-credentials: false
- - name: Install SSH Client
- uses: webfactory/ssh-agent@v0.4.1
- with:
- ssh-private-key: ${{ secrets.DEPLOY_KEY }}
-
- name: Build and Deploy
uses: JamesIves/github-pages-deploy-action@releases/v4
with:
- ssh: true
+ ssh-key: ${{ secrets.DEPLOY_KEY }}
branch: gh-pages
folder: integration
target-folder: cat/montezuma4
diff --git a/README.md b/README.md
index 9de870103..9325313d7 100644
--- a/README.md
+++ b/README.md
@@ -132,7 +132,7 @@ By default the action does not need any token configuration and uses the provide
| Key | Value Information | Type | Required |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | -------- |
| `token` | This option defaults to the repository scoped GitHub Token. However if you need more permissions for things such as deploying to another repository, you can add a Personal Access Token (PAT) here. This should be stored in the `secrets / with` menu **as a secret**. We reccomend using a service account with the least permissions neccersary and recommend when generating a new PAT that you select the least permission scopes neccersary. [Learn more about creating and using encrypted secrets here.](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets) | **No** |
-| `ssh` | You can configure the action to deploy using SSH by setting this option to `true`. For more information on how to add your ssh key pair please refer to the [Using a Deploy Key section of this README](https://github.com/JamesIves/github-pages-deploy-action/tree/dev#using-an-ssh-deploy-key-). | `with` | **No** |
+| `ssh-key` | You can configure the action to deploy using SSH by setting this option to a private SSH key stored **as a secret**. It can also be set to `true` to use an existing SSH client configuration. For more detailed information on how to add your ssh key pair please refer to the [Using a Deploy Key section of this README](https://github.com/JamesIves/github-pages-deploy-action/tree/dev#using-an-ssh-deploy-key-). | `with` | **No** |
#### Optional Choices
@@ -176,20 +176,15 @@ ssh-keygen -t rsa -m pem -b 4096 -C "youremailhere@example.com" -N ""
Once you've generated the key pair you must add the contents of the public key within your repository's [deploy keys menu](https://developer.github.com/v3/guides/managing-deploy-keys/). You can find this option by going to `Settings > Deploy Keys`, you can name the public key whatever you want, but you **do** need to give it write access. Afterwards add the contents of the private key to the `Settings > Secrets` menu as `DEPLOY_KEY`.
-With this configured you must add the `ssh-agent` step to your workflow and set `ssh` to `true` within the deploy action. There are several SSH actions available on the [GitHub marketplace](https://github.com/marketplace?type=actions) for you to choose from.
+With this configured you can then set the `ssh-key` part of the action to your private key stored as a secret.
```yml
-- name: Install SSH Client 🔑
- uses: webfactory/ssh-agent@v0.4.1
- with:
- ssh-private-key: ${{ secrets.DEPLOY_KEY }}
-
- name: Deploy 🚀
uses: JamesIves/github-pages-deploy-action@3.7.1
with:
- ssh: true
branch: gh-pages
folder: site
+ ssh-key: ${{ secrets.DEPLOY_KEY }}
```
You can view a full example of this here.
@@ -215,11 +210,6 @@ jobs:
npm install
npm run build
- - name: Install SSH Client 🔑
- uses: webfactory/ssh-agent@v0.4.1 # This step installs the ssh client into the workflow run. There's many options available for this on the action marketplace.
- with:
- ssh-private-key: ${{ secrets.DEPLOY_KEY }}
-
- name: Deploy 🚀
uses: JamesIves/github-pages-deploy-action@3.7.1
with:
@@ -229,12 +219,14 @@ jobs:
clean-exclude: |
special-file.txt
some/*.txt
- ssh: true # SSH must be set to true so the deploy action knows which protocol to deploy with.
+ ssh-key: ${{ secrets.DEPLOY_KEY }}
```
+Alternatively if you've already configured the SSH client within a previous step you can set the `ssh-key` option to `true` to allow it to deploy using an existing SSH client. Instead of adjusting the client configuration it will simply switch to using GitHub's SSH endpoints.
+
---
### Operating System Support 💿
diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts
index fc5b9d3b4..3f3f499fe 100644
--- a/__tests__/main.test.ts
+++ b/__tests__/main.test.ts
@@ -60,6 +60,7 @@ describe('main', () => {
folder: 'assets',
branch: 'branch',
token: '123',
+ sshKey: true,
pusher: {
name: 'asd',
email: 'as@cat'
@@ -77,7 +78,7 @@ describe('main', () => {
folder: 'assets',
branch: 'branch',
token: null,
- ssh: null,
+ sshKey: null,
pusher: {
name: 'asd',
email: 'as@cat'
diff --git a/__tests__/ssh.test.ts b/__tests__/ssh.test.ts
new file mode 100644
index 000000000..b1d97a480
--- /dev/null
+++ b/__tests__/ssh.test.ts
@@ -0,0 +1,103 @@
+import {mkdirP} from '@actions/io'
+import {appendFileSync} from 'fs'
+import {action, TestFlag} from '../src/constants'
+import {execute} from '../src/execute'
+import {configureSSH} from '../src/ssh'
+
+const originalAction = JSON.stringify(action)
+
+jest.mock('fs', () => ({
+ appendFileSync: jest.fn(),
+ existsSync: jest.fn()
+}))
+
+jest.mock('@actions/io', () => ({
+ rmRF: jest.fn(),
+ mkdirP: jest.fn()
+}))
+
+jest.mock('@actions/core', () => ({
+ setFailed: jest.fn(),
+ getInput: jest.fn(),
+ setOutput: jest.fn(),
+ isDebug: jest.fn(),
+ info: jest.fn()
+}))
+
+jest.mock('../src/execute', () => ({
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ __esModule: true,
+ execute: jest.fn()
+}))
+
+describe('configureSSH', () => {
+ afterEach(() => {
+ Object.assign(action, JSON.parse(originalAction))
+ })
+
+ it('should skip client configuration if sshKey is set to true', async () => {
+ Object.assign(action, {
+ silent: false,
+ folder: 'assets',
+ branch: 'branch',
+ sshKey: true,
+ pusher: {
+ name: 'asd',
+ email: 'as@cat'
+ },
+ isTest: TestFlag.HAS_CHANGED_FILES
+ })
+
+ await configureSSH(action)
+
+ expect(execute).toBeCalledTimes(0)
+ expect(mkdirP).toBeCalledTimes(0)
+ expect(appendFileSync).toBeCalledTimes(0)
+ })
+
+ it('should configure the ssh client if a key is defined', async () => {
+ Object.assign(action, {
+ silent: false,
+ folder: 'assets',
+ branch: 'branch',
+ sshKey: '?=-----BEGIN 123 456\n 789',
+ pusher: {
+ name: 'asd',
+ email: 'as@cat'
+ },
+ isTest: TestFlag.HAS_CHANGED_FILES
+ })
+
+ await configureSSH(action)
+
+ expect(execute).toBeCalledTimes(4)
+ expect(mkdirP).toBeCalledTimes(1)
+ expect(appendFileSync).toBeCalledTimes(2)
+ })
+
+ it('should throw if something errors', async () => {
+ ;(execute as jest.Mock).mockImplementationOnce(() => {
+ throw new Error('Mocked throw')
+ })
+
+ Object.assign(action, {
+ silent: false,
+ folder: 'assets',
+ branch: 'branch',
+ sshKey: 'real_key',
+ pusher: {
+ name: 'asd',
+ email: 'as@cat'
+ },
+ isTest: TestFlag.HAS_CHANGED_FILES
+ })
+
+ try {
+ await configureSSH(action)
+ } catch (error) {
+ expect(error.message).toBe(
+ 'The ssh client configuration encountered an error: Mocked throw ❌'
+ )
+ }
+ })
+})
diff --git a/__tests__/util.test.ts b/__tests__/util.test.ts
index 7fa92024d..c06798494 100644
--- a/__tests__/util.test.ts
+++ b/__tests__/util.test.ts
@@ -38,7 +38,7 @@ describe('util', () => {
workspace: 'src/',
folder: 'build',
token: null,
- ssh: true,
+ sshKey: 'real_token',
silent: false,
isTest: TestFlag.NONE
}
@@ -51,7 +51,7 @@ describe('util', () => {
workspace: 'src/',
folder: 'build',
token: '123',
- ssh: null,
+ sshKey: null,
silent: false,
isTest: TestFlag.NONE
}
@@ -64,7 +64,7 @@ describe('util', () => {
workspace: 'src/',
folder: 'build',
token: null,
- ssh: null,
+ sshKey: null,
silent: false,
isTest: TestFlag.NONE
}
@@ -80,7 +80,7 @@ describe('util', () => {
workspace: 'src/',
folder: 'build',
token: null,
- ssh: true,
+ sshKey: 'real_token',
silent: false,
isTest: TestFlag.NONE
}
@@ -96,7 +96,7 @@ describe('util', () => {
workspace: 'src/',
folder: 'build',
token: '123',
- ssh: null,
+ sshKey: null,
silent: false,
isTest: TestFlag.NONE
}
@@ -155,7 +155,7 @@ describe('util', () => {
workspace: 'src/',
folder: 'build',
token: null,
- ssh: null,
+ sshKey: null,
silent: false,
isTest: TestFlag.NONE
}
@@ -168,7 +168,7 @@ describe('util', () => {
workspace: 'src/',
folder: '/home/user/repo/build',
token: null,
- ssh: null,
+ sshKey: null,
silent: false,
isTest: TestFlag.NONE
}
@@ -181,7 +181,7 @@ describe('util', () => {
workspace: 'src/',
folder: './build',
token: null,
- ssh: null,
+ sshKey: null,
silent: false,
isTest: TestFlag.NONE
}
@@ -194,7 +194,7 @@ describe('util', () => {
workspace: 'src/',
folder: '~/repo/build',
token: null,
- ssh: null,
+ sshKey: null,
silent: false,
isTest: TestFlag.NONE
}
diff --git a/action.yml b/action.yml
index 00e1398e9..8c1786c35 100644
--- a/action.yml
+++ b/action.yml
@@ -8,8 +8,14 @@ branding:
icon: 'git-commit'
color: 'orange'
inputs:
- ssh:
- description: 'You can configure the action to deploy using SSH by setting this option to true. More more information on how to add your ssh key pair please refer to the Using a Deploy Key section of this README.'
+ ssh-key:
+ description: >
+ This option allows you to define a private SSH key to be used in conjunction with a repository deployment key to deploy using SSH.
+ The private key should be stored in the `secrets / with` menu **as a secret**. The public should be stored in the repositories deployment
+ keys menu and be given write access.
+
+ Alternatively you can set this field to `true` to enable SSH endpoints for deployment without configuring the ssh client. This can be useful if you've
+ already setup the SSH client using another package or action in a previous step.
required: false
token:
diff --git a/src/constants.ts b/src/constants.ts
index 2990871e6..6f3a66ef8 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -41,8 +41,8 @@ export interface ActionInterface {
singleCommit?: boolean | null
/** Determines if the action should run in silent mode or not. */
silent: boolean
- /** Set to true if you're using an ssh client in your build step. */
- ssh?: boolean | null
+ /** Defines an SSH private key that can be used during deployment. This can also be set to true to use SSH deployment endpoints if you've already configured the SSH client outside of this package. */
+ sshKey?: string | boolean | null
/** If you'd like to push the contents of the deployment folder into a specific directory on the deployment branch you can specify it here. */
targetFolder?: string
/** Deployment token. */
@@ -65,10 +65,11 @@ export interface NodeActionInterface {
token?: string | null
/** Determines if the action should run in silent mode or not. */
silent: boolean
- /** Set to true if you're using an ssh client in your build step. */
- ssh?: boolean | null
+ /** Defines an SSH private key that can be used during deployment. This can also be set to true to use SSH deployment endpoints if you've already configured the SSH client outside of this package. */
+ sshKey?: string | boolean | null
/** The folder where your deployment project lives. */
workspace: string
+ /** Determines test scenarios the action is running in. */
isTest: TestFlag
}
@@ -113,9 +114,12 @@ export const action: ActionInterface = {
silent: !isNullOrUndefined(getInput('silent'))
? getInput('silent').toLowerCase() === 'true'
: false,
- ssh: !isNullOrUndefined(getInput('ssh'))
- ? getInput('ssh').toLowerCase() === 'true'
- : false,
+ sshKey: isNullOrUndefined(getInput('ssh-key'))
+ ? false
+ : !isNullOrUndefined(getInput('ssh-key')) &&
+ getInput('ssh-key').toLowerCase() === 'true'
+ ? true
+ : getInput('ssh-key'),
targetFolder: getInput('target-folder'),
workspace: process.env.GITHUB_WORKSPACE || ''
}
@@ -123,7 +127,7 @@ export const action: ActionInterface = {
/** Types for the required action parameters. */
export type RequiredActionParameters = Pick<
ActionInterface,
- 'token' | 'ssh' | 'branch' | 'folder' | 'isTest'
+ 'token' | 'sshKey' | 'branch' | 'folder' | 'isTest'
>
/** Status codes for the action. */
diff --git a/src/lib.ts b/src/lib.ts
index 9b3a3434a..94738cf8b 100644
--- a/src/lib.ts
+++ b/src/lib.ts
@@ -1,9 +1,10 @@
import {exportVariable, info, setFailed, setOutput} from '@actions/core'
-import {ActionInterface, Status, NodeActionInterface} from './constants'
+import {ActionInterface, NodeActionInterface, Status} from './constants'
import {deploy, init} from './git'
+import {configureSSH} from './ssh'
import {
- generateFolderPath,
checkParameters,
+ generateFolderPath,
generateRepositoryPath,
generateTokenType
} from './util'
@@ -43,6 +44,10 @@ export default async function run(
settings.repositoryPath = generateRepositoryPath(settings)
settings.tokenType = generateTokenType(settings)
+ if (settings.sshKey) {
+ await configureSSH(settings)
+ }
+
await init(settings)
status = await deploy(settings)
} catch (error) {
diff --git a/src/ssh.ts b/src/ssh.ts
new file mode 100644
index 000000000..ebe6780a0
--- /dev/null
+++ b/src/ssh.ts
@@ -0,0 +1,47 @@
+import {info} from '@actions/core'
+import {mkdirP} from '@actions/io'
+import {appendFileSync} from 'fs'
+import {ActionInterface} from './constants'
+import {execute} from './execute'
+import {suppressSensitiveInformation} from './util'
+
+export async function configureSSH(action: ActionInterface): Promise {
+ try {
+ if (typeof action.sshKey === 'string') {
+ const sshDirectory = `${process.env['HOME']}/.ssh`
+ const sshKnownHostsDirectory = `${sshDirectory}/known_hosts`
+
+ // SSH fingerprints provided by GitHub: https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/githubs-ssh-key-fingerprints
+ const sshGitHubKnownHostRsa =
+ '\ngithub.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==\n'
+ const sshGitHubKnownHostDss =
+ '\ngithub.com ssh-dss AAAAB3NzaC1kc3MAAACBANGFW2P9xlGU3zWrymJgI/lKo//ZW2WfVtmbsUZJ5uyKArtlQOT2+WRhcg4979aFxgKdcsqAYW3/LS1T2km3jYW/vr4Uzn+dXWODVk5VlUiZ1HFOHf6s6ITcZvjvdbp6ZbpM+DuJT7Bw+h5Fx8Qt8I16oCZYmAPJRtu46o9C2zk1AAAAFQC4gdFGcSbp5Gr0Wd5Ay/jtcldMewAAAIATTgn4sY4Nem/FQE+XJlyUQptPWMem5fwOcWtSXiTKaaN0lkk2p2snz+EJvAGXGq9dTSWHyLJSM2W6ZdQDqWJ1k+cL8CARAqL+UMwF84CR0m3hj+wtVGD/J4G5kW2DBAf4/bqzP4469lT+dF2FRQ2L9JKXrCWcnhMtJUvua8dvnwAAAIB6C4nQfAA7x8oLta6tT+oCk2WQcydNsyugE8vLrHlogoWEicla6cWPk7oXSspbzUcfkjN3Qa6e74PhRkc7JdSdAlFzU3m7LMkXo1MHgkqNX8glxWNVqBSc0YRdbFdTkL0C6gtpklilhvuHQCdbgB3LBAikcRkDp+FCVkUgPC/7Rw==\n'
+
+ info(`Configuring SSH client… 🔑`)
+
+ await mkdirP(sshDirectory)
+
+ appendFileSync(sshKnownHostsDirectory, sshGitHubKnownHostRsa)
+ appendFileSync(sshKnownHostsDirectory, sshGitHubKnownHostDss)
+
+ // Initializes SSH agent.
+ await execute(`ssh-agent`, sshDirectory, action.silent)
+
+ // Adds the SSH key to the agent.
+ action.sshKey.split(/(?=-----BEGIN)/).map(async line => {
+ await execute(`ssh-add - ${line.trim()}\n`, sshDirectory, action.silent)
+ })
+
+ await execute(`ssh-add -l`, sshDirectory, action.silent)
+ } else {
+ info(`Skipping SSH client configuration… ⌚`)
+ }
+ } catch (error) {
+ throw new Error(
+ `The ssh client configuration encountered an error: ${suppressSensitiveInformation(
+ error.message,
+ action
+ )} ❌`
+ )
+ }
+}
diff --git a/src/util.ts b/src/util.ts
index d43a1e09b..8db99d65a 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -1,6 +1,6 @@
+import {isDebug} from '@actions/core'
import {existsSync} from 'fs'
import path from 'path'
-import {isDebug} from '@actions/core'
import {ActionInterface, RequiredActionParameters} from './constants'
/* Replaces all instances of a match in a string. */
@@ -13,11 +13,11 @@ export const isNullOrUndefined = (value: any): boolean =>
/* Generates a token type used for the action. */
export const generateTokenType = (action: ActionInterface): string =>
- action.ssh ? 'SSH Deploy Key' : action.token ? 'Deploy Token' : '…'
+ action.sshKey ? 'SSH Deploy Key' : action.token ? 'Deploy Token' : '…'
/* Generates a the repository path used to make the commits. */
export const generateRepositoryPath = (action: ActionInterface): string =>
- action.ssh
+ action.sshKey
? `git@github.com:${action.repositoryName}`
: `https://${`x-access-token:${action.token}`}@github.com/${
action.repositoryName
@@ -46,7 +46,7 @@ const hasRequiredParameters = (
/* Verifies the action has the required parameters to run, otherwise throw an error. */
export const checkParameters = (action: ActionInterface): void => {
- if (!hasRequiredParameters(action, ['token', 'ssh'])) {
+ if (!hasRequiredParameters(action, ['token', 'sshKey'])) {
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.'
)
From a099e5db8b1ed2751daff5653efcfcc377d918d1 Mon Sep 17 00:00:00 2001
From: James Ives
Date: Wed, 3 Feb 2021 21:26:28 -0500
Subject: [PATCH 19/32] Deployment Issues (#583)
* Update git.ts
* Tests
* Update git.ts
* Formatting
* Update src/git.ts
Co-authored-by: Axel Hecht
* TestFlag
* Logging
* Update git.ts
Co-authored-by: Axel Hecht
---
__tests__/git.test.ts | 37 +++++++++++++++++++++++++++++++++++++
__tests__/main.test.ts | 4 ++--
src/constants.ts | 3 ++-
src/git.ts | 23 +++++++++++++++++++++--
4 files changed, 62 insertions(+), 5 deletions(-)
diff --git a/__tests__/git.test.ts b/__tests__/git.test.ts
index b6d38e761..4b882304c 100644
--- a/__tests__/git.test.ts
+++ b/__tests__/git.test.ts
@@ -40,6 +40,24 @@ describe('git', () => {
})
describe('init', () => {
+ it('should execute commands', async () => {
+ Object.assign(action, {
+ silent: false,
+ repositoryPath: 'JamesIves/github-pages-deploy-action',
+ token: '123',
+ branch: 'branch',
+ folder: '.',
+ pusher: {
+ name: 'asd',
+ email: 'as@cat'
+ },
+ isTest: TestFlag.HAS_CHANGED_FILES
+ })
+
+ await init(action)
+ expect(execute).toBeCalledTimes(4)
+ })
+
it('should catch when a function throws an error', async () => {
;(execute as jest.Mock).mockImplementationOnce(() => {
throw new Error('Mocked throw')
@@ -66,6 +84,24 @@ describe('git', () => {
)
}
})
+
+ it('should correctly continue when it cannot remove origin', async () => {
+ Object.assign(action, {
+ silent: false,
+ repositoryPath: 'JamesIves/github-pages-deploy-action',
+ token: '123',
+ branch: 'branch',
+ folder: '.',
+ pusher: {
+ name: 'asd',
+ email: 'as@cat'
+ },
+ isTest: TestFlag.UNABLE_TO_REMOVE_ORIGIN
+ })
+
+ await init(action)
+ expect(execute).toBeCalledTimes(4)
+ })
})
describe('deploy', () => {
@@ -75,6 +111,7 @@ describe('git', () => {
folder: 'assets',
branch: 'branch',
token: '123',
+ repositoryName: 'JamesIves/montezuma',
pusher: {
name: 'asd',
email: 'as@cat'
diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts
index 3f3f499fe..87c3a98d0 100644
--- a/__tests__/main.test.ts
+++ b/__tests__/main.test.ts
@@ -49,7 +49,7 @@ describe('main', () => {
debug: true
})
await run(action)
- expect(execute).toBeCalledTimes(10)
+ expect(execute).toBeCalledTimes(12)
expect(rmRF).toBeCalledTimes(1)
expect(exportVariable).toBeCalledTimes(1)
})
@@ -68,7 +68,7 @@ describe('main', () => {
isTest: TestFlag.HAS_CHANGED_FILES
})
await run(action)
- expect(execute).toBeCalledTimes(13)
+ expect(execute).toBeCalledTimes(15)
expect(rmRF).toBeCalledTimes(1)
expect(exportVariable).toBeCalledTimes(1)
})
diff --git a/src/constants.ts b/src/constants.ts
index 6f3a66ef8..648eae36a 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -8,7 +8,8 @@ const {pusher, repository} = github.context.payload
export enum TestFlag {
NONE = 0,
HAS_CHANGED_FILES = 1 << 1, // Assume changes to commit
- HAS_REMOTE_BRANCH = 1 << 2 // Assume remote repository has existing commits
+ HAS_REMOTE_BRANCH = 1 << 2, // Assume remote repository has existing commits
+ UNABLE_TO_REMOVE_ORIGIN = 1 << 3 // Assume we can't remove origin
}
/* For more information please refer to the README: https://github.com/JamesIves/github-pages-deploy-action */
diff --git a/src/git.ts b/src/git.ts
index ea432fbf4..272b9b1e1 100644
--- a/src/git.ts
+++ b/src/git.ts
@@ -23,6 +23,22 @@ export async function init(action: ActionInterface): Promise {
action.silent
)
+ try {
+ await execute(`git remote rm origin`, action.workspace, action.silent)
+
+ if (action.isTest === TestFlag.UNABLE_TO_REMOVE_ORIGIN) {
+ throw new Error()
+ }
+ } catch {
+ info('Attempted to remove origin but failed, continuing…')
+ }
+
+ await execute(
+ `git remote add origin ${action.repositoryPath}`,
+ action.workspace,
+ action.silent
+ )
+
info('Git configured… 🔧')
} catch (error) {
throw new Error(
@@ -48,7 +64,9 @@ export async function deploy(action: ActionInterface): Promise {
const commitMessage = !isNullOrUndefined(action.commitMessage)
? (action.commitMessage as string)
: `Deploying to ${action.branch}${
- process.env.GITHUB_SHA ? ` from @ ${process.env.GITHUB_SHA}` : ''
+ process.env.GITHUB_SHA
+ ? ` from @ ${process.env.GITHUB_REPOSITORY}@${process.env.GITHUB_SHA}`
+ : ''
} 🚀`
// Checks to see if the remote exists prior to deploying.
@@ -113,12 +131,13 @@ export async function deploy(action: ActionInterface): Promise {
branchExists && action.singleCommit
? `git diff origin/${action.branch}`
: `git status --porcelain`
+ info(`Checking if there are files to commit…`)
const hasFilesToCommit =
action.isTest & TestFlag.HAS_CHANGED_FILES ||
(await execute(
checkGitStatus,
`${action.workspace}/${temporaryDeploymentDirectory}`,
- action.silent
+ true // This output is always silenced due to the large output it creates.
))
if (!hasFilesToCommit) {
From eace78eb8288477dd52e8923bed96b9004019ae0 Mon Sep 17 00:00:00 2001
From: James Ives
Date: Wed, 3 Feb 2021 21:27:35 -0500
Subject: [PATCH 20/32] Codespace Support (#584)
---
.devcontainer/Dockerfile | 2 ++
.devcontainer/base.Dockerfile | 7 +++++++
.devcontainer/devcontainer.json | 18 ++++++++++++++++++
3 files changed, 27 insertions(+)
create mode 100644 .devcontainer/Dockerfile
create mode 100644 .devcontainer/base.Dockerfile
create mode 100644 .devcontainer/devcontainer.json
diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
new file mode 100644
index 000000000..9cb1cdc9f
--- /dev/null
+++ b/.devcontainer/Dockerfile
@@ -0,0 +1,2 @@
+ARG VARIANT=12
+FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:${VARIANT}
\ No newline at end of file
diff --git a/.devcontainer/base.Dockerfile b/.devcontainer/base.Dockerfile
new file mode 100644
index 000000000..16313f77e
--- /dev/null
+++ b/.devcontainer/base.Dockerfile
@@ -0,0 +1,7 @@
+ARG VARIANT=12-buster
+FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:dev-${VARIANT}
+
+# Install tslint, typescript. eslint is installed by javascript image
+ARG NODE_MODULES="tslint-to-eslint-config typescript"
+RUN su node -c "umask 0002 && npm install -g ${NODE_MODULES}" \
+ && npm cache clean --force > /dev/null 2>&1
\ No newline at end of file
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 000000000..b72d84679
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,18 @@
+// https://github.com/microsoft/vscode-dev-containers/tree/master/containers/typescript-node
+{
+ "name": "Node.js & TypeScript",
+ "build": {
+ "dockerfile": "Dockerfile",
+ // Update 'VARIANT' to pick a Node version: 10, 12, 14
+ "args": {
+ "VARIANT": "14"
+ }
+ },
+ "settings": {
+ "terminal.integrated.shell.linux": "/bin/bash"
+ },
+ "extensions": [
+ "dbaeumer.vscode-eslint"
+ ],
+ "remoteUser": "node"
+}
\ No newline at end of file
From 9b18761c06a24a109330ca37f6f89a9081c441a4 Mon Sep 17 00:00:00 2001
From: James Ives
Date: Wed, 3 Feb 2021 22:21:36 -0500
Subject: [PATCH 21/32] Add files via upload
---
.github/screenshot.png | Bin 0 -> 10800 bytes
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 .github/screenshot.png
diff --git a/.github/screenshot.png b/.github/screenshot.png
new file mode 100644
index 0000000000000000000000000000000000000000..086a0a43bcc1fd07d1bb7f1921fd97e88873882f
GIT binary patch
literal 10800
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
Ku2jbC#eV?`^fQwH
literal 0
HcmV?d00001
From d0c0cc0d21c4cc6a626c2ad575b4e02a656014a9 Mon Sep 17 00:00:00 2001
From: James Ives
Date: Wed, 3 Feb 2021 22:22:10 -0500
Subject: [PATCH 22/32] Update README.md
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 9325313d7..7c20ff2d1 100644
--- a/README.md
+++ b/README.md
@@ -35,7 +35,7 @@
-
+
## Getting Started :airplane:
From 156d91fbff900d4326443628b4b58d936438c770 Mon Sep 17 00:00:00 2001
From: James Ives
Date: Wed, 3 Feb 2021 22:36:41 -0500
Subject: [PATCH 23/32] Add files via upload
---
.github/logo.png | Bin 0 -> 64156 bytes
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 .github/logo.png
diff --git a/.github/logo.png b/.github/logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..f1b729fd1fdb1b8d2a930a04f0e2d4eec14fd2d2
GIT binary patch
literal 64156
zcmcF~Wl)?!(;#la-Ge0%++p$HB(S&!UnID@Zg7X-Zb5=e2ohv*w*=S4-Q92V+Ff1U
zefRyjt=igo=IQC_?&<02>F$kC0m@-vkYm8X!C@)LORK@b!3V*8Cj%oaL8v?8al2zN{T`t2Rn8XGY3<1c27G;7&IK5h`6Vt3CPCW
zmB!TE(#l?x?&Mbo9gUTlD4jN+5~q@*l=){Xd2eTPb#I^s$lC@4FryO}qY?2Gf+4Uo
zcQv8$w6nE$5%LtJ`v+bjSo?1`2OZ5nKwND^>E8dHkVZ#Ig+|K3*_?)-os$j3$;nM4
z0AT0h=j9a?WToNe`Bd
z)BMF~V(Q@LDoO{V^q(x)IsON&z01GE1QQsCr->s67dz))mi`T>r1W2d+S&aF+Qn7I
z9X89q=>0z;cG2*1H0Mw=cX4oY2ARvao7=n6|9dhs(0|4{x;fkagHtmQhqGyu56jd_tU@{{&Tn>4BMvtI2;4
z*ul)o!t1{Ss-z^OVDI8;Vh=J`kQSwb5yft0WhP_>;^pHv=QCw97cc{`@$mBsumLPA
z0Bj&ZZb4H%fC(3`1onvDAzheDMl^G19g$c-vA7E<1X3k|{
z22*=pel}B1E;BY>UJ$pr8Gw(Ao6CZZ#tbB6;oxj%0@D*KI}=ND4o7=SI-37Ujg*6}
zgR_!@8BFFd9{y*)f|Qhsvx9||E$oGhnw%t!f{YYD7l5Cijhmh8uiC@NfUzrYhCZNBYLX-~lR~yXD=>FAh^?wlae~+PmqV@P}4nz81Xvsgp
zTpTQ1JxrX<-&?|#`7dMhxVvE{Z(|aWlPb8n6hZeewI4p
zaa-4~m4=j3W+S#H?}{_Yj$qo*+q$!#emt)AS+b<@->!I-fwU_(btbI*&VbP
zq0`SZ>!FI8hiO0En`N5{2TpB3f_7iCzqY$CCi@{ONn2{5O
z7n}%z+a(+eG$MoEz*fW(RA&-9YYo{
zOWVx6Vo&+Lt++Yg=v`1y{m#<`)%p_GU#XmltRD%CL8lAORD$)Oq@Emx3EkP?)#XyC
z=D6%mW;vd~$l`OY@065?6lQiOu2dFk>s9D75=E1*#1M~?;BfOntar$~{}=7F
zd6B{V=8#D37{r7*bcHGnq-&wIaP4-gG-SQB_Z@_3DEuS=jo^32D2_Tk9KFhwx4*EZ
z)9eiUxI^g?HYAXF&KDzhcHv%OfJ|Q+)gDhwjD@!;fy6J3n7&AJs!I;KYte*bUFb*p
zRad&~B{MBugHKlnBc|h!a8eTx=&>afWhscrLT&NX8{yX)fy=VpZxy
zNwN{9!^?toL!_a6X`NJ>qjDFIkh@3${iN~|Qkp>1vEq)BkLw>WN
zGF#MpFbsYZV!g&X6M3-1C3KKTnmpZ2
zxeU*^w2^Fo)~v9tDWFMzY_Na&BI0~b=KD%YgoIQmh&EJEZmA^zN8D?ccEx`K^X}fN
zeo`f6ERWY-m}|@rV@`YG?dlh@OOzVqs3!3!r(12})Q@QdOjxerWRnT&LM0Z_AG@D4
zk`6V}v68Bl1(tqJ;@x5eIJDX!4qts+9HtoVpbF`JSEa1$!Ad4eyVdI_3^5?}n?hQl
z67mY5`HJbRyeZwcq4HX1@B9EK+cTq4pyv)UL59iIL-L;NF7@6Uc~JApQ^3UK%{OY4
zqTVbNI%J}`II@6K2=d1zb2Hijhx-r6?i5)NK&ULm%-%xus6Bx
za`&Ta)o0pAy?XCiJAmrsF0+HGEGEY?nB(#lscHBtB8qo;l0)AS_Q7-Lb~cYKNQG{O
zA!ko4kd8h&DB4`N_nJE90|2ijD2df_Hty`5VNeMo-c
zVLHw<0hSoG$FBRoS>|P0k%}Zcoy1BHv~@(XtpBq9^PA@?-ZpMa8c30=dzD2j?86x)
zJSW@fg2cBqgyxGCW&euCHtN(9yYL-!DND{CuinM$(RdQ>A=ow3mx|dp;wB=Afa8U1+bY?|TPjrXmyZ-
zo^^M4)M~e?X!Ry)oB6=ed-#Hh>#^C!BkH*qe3)w
z@H~RA3LLHE*9J8dBfiGIqdz#miJ6l}OqTu0dXCb2Lf>m6Uy~E4S;2Wwi*Vso@eJh(
z`owa4;gJR(Kw9|oL-KF6vkPaaVTdx^D%+{b89C6z4{b5>rXw0a_HN~bG(~fSB-QIK
zols`$ZTxUcMZ>qRU)J4hd$Xjp*QRDN-bkySx|%1@=Gb7`ph$@@dOpPPwC>p_YRLi}
zo!#xfAZ+rVOJEdMBfl6rpIHKqVu;Uug+q+mOau}L&e!jvb~L%VwYu4>;o7nO+?;e{
zo*7oK%z{E)F?p;3AxQO}Suws(vfVEl?ExU!c#=oIyPLiTY)<-Bc(G_Q15{the0Imz
z7HI}fxs_>Bo}|P2!ESf^n!Our#@I)C(AR;Xl9f!r0I4%EmkN_3c2){wzhf{pIcm2@~i}Rndr1rb_0(!)L7OtWNG64
zjWl{sjf%0!PP(fTHue|zTZM{_{7`d@OfJ2O5Oa7Oc%=~cGtyaGPus|cn404rAaPth
zvI{2O=`4hi==iGa0)SM>Iq~AS(`kaWCIXTtz&eZmyR!%df0G$+5R8$xf89Zuqa{`i
zP*$aRdc)|_(|Ee8!
z)GGGT4qF*w>m{mTsbgwT5JkBgMG}1x0EOPGjU*IR$jy9_`AM5qyfp_jUacis*ENJrLeII=N}}aQSdpjaWZXIGy*!k
ziqw{_X)K^}fj#Jc)wX?f&viUrB&T#Eoxv3zQYT$mecab~;;Q;L4oJ`9jOVl1T~iN)
zhfNgj?{dZeo`mD1TS>zoGl;xCH$W#EUi*L~3Qexaa8o986MQ
zm9l60qi49ynNc=i>HQkv_QQ9UtigJ2l*Yr%d?IAaoejbXx|WsoWOOcUUzA&mk@v}b
zUuv3`J}-Uj1rDyMx2ML;qbL`9Ac(!_3u7;7`O1c6Ii_lRBg_0j0kV7ptDJp!&C`d6
z>p7kL0s8g*u}=a)Ye9WmaD?Cp0-lu&m^oaViuFx8P_xj?<&BvQ-&ZZt%SGL>>?oU7
z(+l|kI&8r7$1KTe@}>|iQpZ-|JUkRa-7bt%5(o>D{^qxA&zM(ZEUXy)^*Y%FdS)SH
zvQlnb4ZnF0_<~DsSs!}clvokTySd}()9U9KWL2ksrHjiriC{WCbHaQFzT|*km#(h&?~=_J)qZ`?zXS<`C}0mRn<}Nb+x#6B>8o<
zPib6tNT=als8QiJGLV37rgZ!w
z{`N4Zf!CdpIRubgI2@9zuzJduKSb#PccIHuh;sXybc}kW!U!fkEZWAQUM$=3RS?0G
zSrD&GjN7~2R3R?@nHEtBg!1((Dr9WqzEg+DVfr+^VuVY?{7e?w_@Xj!kS{Ct(}%Bv
zeK($?&zYWw5Ce$!#-i!hy^SMkj>V}i7FGtb7>FtsC8IaGcJ!2vBDZsg7`$7HJw~=4
z$JZi0ZGe#m9eBHB+%BOPDb_5;%&UD%TDiW3v`KAe~Y(juaXC)kYWn$QnamVOa#f?uuR)
z0AVAH(0oh#`Lv;PUGs&AP@L6*G>@~ZhQWTGgiMGkgn??ei(6bKp-;R*Yf=dvaHvCt(T>$5LnDpmg$FXhj?Otv#!0
z1>$Su=T^?Z5+d)SWLAr7R5(3C{jEXO8@@DfI>h4ou)mcJA8woXNFKnYJNpkA)wUu&9l9{*3{M}h#=6-AOJ9DSBS_&C8@Pk9R%oD
ztoi&CffcY8F?CQuFi(ya)x6$j{WHzX-{nm~s*jarb8ImD7{~wzTljR^xUvU9*bxdu^5(
z>_G=uC%f(Tp_0~)Q7e&*DLP8kvAQR#hdhN$qjucAy{VN#os$irF$_pexta3;8k*ke
z)dh3aRC8{N_>v@@3K@OS|1>le@sB^Fav45tk!GL!q?xq;K}U5Kwb>djF004UjkRab
z7?QSaS0)U9lv8ZK>nAnfLHA{m>bd39&x$4blxS=NSUT8yp4TLLK%zKx`kstMn?;C3qf82J|_wnMkCYGdvVa`D`0F6|->$T688q~9!3gs17AMI=E8~{0C@OWX<wp6gQ?1{Bh5pF<0xCndVogNC)8M-k>O%O=lqEW;G5#qyPQ+)NvRTAq4^q8MU@
z_f|t1h?wYVFxzAfQXur+7i)vOl
z;_Jt3I6(C`QqhL@-@4=?TcrBYpK1j$@uz>_H8Ir6MokeZ%^GTY@Y#s8wib7|Ca{A8
zTxv`ESv2ezdW=ldwa|P;PahyoYlr?p^^SDXW4BNGgLI6`Bjqk#7%&HnP+S0nybaC!wLk5Av`T1?
z@!IXwi3DO8tt7WkU8Kb;7@?gSeAg^iIS%E$kD245PGjRQDTraiR(Z_O7TxE3S<`$iB3L)@UWhe@9~g{)OXxVf6Xd#bz9Ikddf1Qj(_%IZ+
zUXW+aPh2tn^_MCw(Nn|)q);0{j^jq}RaP(%SY()s%kuT4I~cJ;k}DKcxYI=|pF0?^kJ0XFyArCebjk*FW)q+DUH>rWZ}Gt{I_7VCj(TiM6k
zB|+$FDM}nNsj`U$xdBby@@^xoB$TgEhnt1GaRy4eOfN-nQiqKX;3W;rNC_L^AlAsA
zauABqo_1rbzAd(?hQp~5!`Z<+h*~L6+hE!Y%_hyIBD?`J3WU-sv%{q>+*21L>Gf)`
zp1?nsr9Q|}phydP(024D}uccmEBR1^QW<_V*lcG2RD3UK?A#
zOZ608bb4Bh%U;JZ@+QNu+GHq6v&Gm1>M+ZGcC9_xW|+C0HETn6A{-uJ?{5FIY`toA
zJN5H~Hb)qC?r6ggl~yKU5+~~-7s!(O5)|X#xgYjCu%5a=eq~LJ&e$Mil;2yBPK-XK
z@<|_$qGIQ=>5-(%exCy%K4w5(`dp91!>94Jxah@)TizOQmE`pvT&KOCmuIpR@e%PgDZVK+{pulp!hN&{9kKi%x&GuD;I
zm4|DZvLOc~yaz(1rv~SHxejgw8dBl#BX?mc{UdRUwFK~TG^|5Q7Ktk~`Tn?w2)TTD
zWHrbfMH@~$x%|0bq6e8yxs2-4#R#~3@mi!|>Y;DXDc3s&AwR}1u23gQyzjp05R>KH
zp*M1{q~_%E1#PL~nyOOtK!6A4&ff#VoeN7h3q^3!|SP
zh;0VH>?QlF!nrjU7-|OG=g?=_+??>d2W*c0j8M>RWs6y6SMjk+_iyVqNkjq$f{Zd^LFu(De3AZyjs-B%(e
zyO`|=XXOHi#rT5JTJg?bOF1mA9q>KrPaSI4_uC*ZNY5Wk@;O!Dp~AXVl}w(7X1-ge
zzp(V1nLMk>%h*KjsG>`9Se|=93I51Y>;jTH9w=M<)%+Heue%6U0}w?@Yslw!)-lFb
z#p@P8mW5p(zKR}*w!&`vq0X+-dq^0g{$yjy5$}Ew-l|4#|E5HL@3AGX)_Tn(E5+2z
zMk`B3)2w|JQHSDcYEr3%E)M+ahcWE9Kp`Ht4rmx~z1_2vpijpBmFFRH8V43Ixf}}v
zzh*iv3l!6oKmEngo>V)U3BP&!{g2bW8%E(Rdq1V$9U!`@#efZ`ri8J3Y^XsRn5oe$ndj_6^4+(JS@?o=
zwOYwbN@87qy|q9{!tXP}^gz=xrk3U%?RHFL6@>tzCuH+eqZ~803T2Rh7OQd~8O}6i
z1fey&w4Kv$jerz^eIo2U+wgEzwPAxClDMfl*3lOHkhq8;D&1cXpR5xlgx^r@
zAT{LRn8ZK0Xay3D=|zs)swv)GwdtJQP!K%>XLzc|eU`+1_slgim9WP-Waf$+Q;pNd
z1wguA`m^2x0B(VRZZF&TWh17gujLlC1YUoZyGNkSeKi)RM4_KxAw%!vxs;!u*&Kq*~bz&+gubSj1e9Dd6?exm($?*EO69HYG;USI|wMDsL`>
zY=cKu8EV8St$Z1q$BMI$g|i19vN&f4TYG?LifE^bjGbi!4BFe^RkoE`erPIKIBMiK
zeEXx=aR2k#;_Hp&)d%nMW|=?{!aWmS^`3j;pTE-uXXFwB-^I@K6vvs}O<-k_z)Y@8
z?AJtK>(_ZlF7#~A>PSeH%BFBl3xqwI{Ei@U9xx3_maS!qUjT`%E@wM@I)R0p+BS%O
zzIr5_iJvjce&J|7&ARGk`vM4JtXb%nlVUvS7Ie2O0pt?}BSz{v^zKGpoEs|fMVYz<
zrkY~g2@qdtiP#=&_y!v;%3uudIH3j6rSH;5uPH<=F%jj5%Hb`b(MzyL3er}DFi9@#
z6`E${c_+6D%uynBZZB-!RyQ*}KOgVSH$JW9^Spje^(oOAb(ZZ41g7MwUpMA6BG-Zr
zHMXymYg>jp@I4t%$BF8G(~3;bZ)Wp|(jTJOc}
za~?0Am%0L|(1So2dWs0sE`zv-g0(rQ4jAQOHR#`Rdy|Zg)X_VPfQ?9Jp2te@a6^dKZ4U>C57IQ^60Yq#Ql&K0Q|P&)DC}YAM7E!KOdr
z1!N&;6m|6&ZCT&7nZCA?!?iYJJITUc*cS)
z-U*H^+-S)x_=ztTdU%DK&$v;CFJ!VOkF1V$q#8_t&)ZSg&W@FF+O95qzo&T}K)Mpq
ze!1qg9ZSfyU%vMZ`HiV#99td(oUiA|)|+7geSK-BhW((WjjKZow%&rWgBmXQ7Yv)Z
zd)CvJVa)&z+&$Ep@B4(lfa|-EF7G&a4FtyTe5`g=D95NZV>=WzRU2Qa*h$8PEAnp9
z-U>WE3=A$CHvArLpd#6zLHc1>`knY|v_q3w#>vaX^3Kx}To2vX(w}X!%G!t?9x8Di
zMO3n@X^jsX>3+{hOa={|ehEQL*ra%J1@U3AN)7QAo8fOMG!D2HuNCnriBOiQ2@(r~
zW5R#T7LseD7Y6nwvfM%vZq)C~5q;OuMN65zz802+Gb6zZkxorFJ7B$Ky!p_vnq2u10;Ys5*l|m!OEy3@Ggr*Pya#
zwXm&lI(kg-@_vf0(sp+Gc~VQxIUhOrHS7Dww>r9pZ=9)_;nm{MY#dEZQfkvx-`?17
z)XK%--z6nLCSAx(G_9fq=6SVBR+Z(o9&SIxJ=2uTRQv|iMq5Pou@ifbbuCNcf!8&!iC89hh|n5EV*;&q>f
z`eKi4y4lMP-)xI4^l^9CsC#|2L;0au`M@OJc+|$}4zJGe<=vZ3>;pMcvYD81j?j%4
zcE?9+F6_KqpV*3uGGiz>&><<)30X(
z>&!zJnO8|}I5QooEf9)At)mAUbx?KrQC15-x!ojr`J{C)uN>W%NKUN~3pl4uol54C
zbLviwkk2BLh`X*c1J26FN$*=^bFfJ$H+JJLLn>n+yIg0!QdrMptRBV5&G*ZI=43x
zr=kYVp`SoL*Rso=9rj7X`EXZ@UG1-2%;dV
zvbDBF>2@OAdDSfz^op?PLusbhetvg^{J!R^wp*;ZvcXi-A49Yw?;i1LR9y2u>`Kr6
z&hp;PaY5AOkLbKVZRX(qnW3LtrofJ9E2}ik(h+PmEl|JW#zLy3@i}Cez
zX>ss*wHff%1x@cm4Jt4fmL8x^t;$W?=ar>s!z?UF>z{vQR8(Ni{VeLeoN_;IeIb>L
z8?QI77~4vEnu_qFSj+H^lX-VN-AFDfNcLi$^p
zgl6TZLuhF!mIlHOwZqNR7CqlpSt5AA!xZIGmNn*?ts%QBE@`!
z_@v=&0p@~O;~3cha)6iM1-W4z$mb`gPe$Sq_WQ|@`{QADpZAoVi!%q*Tb>c*r{|uw
z6{~ZAr-f40wh_(X=XE;WYB|3hq_&1P1q^w0F!le~$i+P)3EYGwJ%pa3&mvGPEmFy>
z_y9bOlJ$G@&34gEhL&d5Uoy!-n7@5=40U>^Go?p`*buCnEsK_E%cj!OC>hIUbqx0M
z+ha7OfK&kBDDsrt!R8gl+MwpS|ChI9(zIrS!P1k;xJdQ=#3?IO0IRUEFL5O<=MI3H
zcIzL4bktq@T8%7^kvdMdi#4Hr4}l&Sl%IeXMwr3<7!lTiGEekrYa9ng=vutggaL7g
z9vGKmJgeK9nPJhBe+v-wC750%L@;|U3T_hx{17%)uD@>p55$?O&Jd*)KbK^kHom!%
z%Nj
za+}>5V<33p4cD^#Uul_cUnsyvJ(0_2p|fmh@H*`50iV`x?wggJlR2+4M7<9M;ewXb
zkF}nw#9tmPAE`U^ZAT*UHQ59&0x)lfQj}IXViuzQd{g$eNUtHU+gmW;72wlJ$12q-
zZ~7o#mP3Op+n>=y19JYu^yz18J!hk01YwDSq
z`@?o?j`ELGDZtU+M>n65Vw<=lqnI@^;VUDE>%*|?%w3}e!>w?n(I`{ug0j#@})1+I4fUb#P5ipqX^M!ADj#S
zOdI_&zkH}+vLWGLe0p*3dO00pmC>k#M!6-Ad)j{lOvIYzNF)BzqXnAfSsw~fAXf~qz=
zEM=B$6F8N!=;kyO1j+}G#*oLcHr(Ak#ESiiy9G#{_4S~WGjZ|TR`U4&!J3`%u$^BO
z_Rq+}#~)YZYHx7_8M~A1pqNpY
zCBF?~m2)2W6J7gViT4wq7I7+VP>mXGl}7jm^U*Z5E`NX3upC)BrHRQkq3RL?X=RPO
zGkh+#d{ENRHeQ#vw7X-cUZf_iLg#_&+ehi}M}w~q4?JPP1G@bWt^=BU?sSFwfr
zi8mI8HV_UIEGq}}>{j-Rdt&HKn{y_l8YEm*-CdlN5(AD@4JqnzC2V>RF=+E)M2*Xx
zW6C342f!lwtq`VqFx^73seg1_c8|=)+_)*;3|Vy0)9}Qgk?tpWn7^@=&b(-Smiq1E
z5ZZ~jL0eFSpvdrDvEiU5AM5tcY`4UHvq#{CmPW=<>o%Dl(cD(@$DV6iL|bfIk}e~J
zlVVb?ir%-i?~ncX9Gkm5!r9bTyA$id&Us6er+82*$Yh)FPXgtW&HV9|*q~ST=q<*=
zXp#x#@LxtV`-NVyh|f`c1KHaKYb9|x4GOB}eQV=mP#*N_(yRXHVw=T;x$7pJH&iA1
zXUXY=c8qn?cG2&hT-;KxyiFFvLCp-6iVgn+O#uMEzNTjAHU-@^o^v2MPqlxV;bB
zg?L6M%OG#MtnR7Irj)O5kKIGZP*iE%8y9`CQBkdYHr+_Sv#4-D?RCfR5C+t8T6=};
zUk$`f0lDAB)LJ$D7TeP~NX`D9v}&6wC~&VJp9OAgG#OzbrHDEG?=Q(-%LuJl2ly}W
zhjTd=epB+ug`r60w9z%}bb>lqOVEf7M(NO4XshuqrXh{3z9fgrSiLH1%Zj>+q=1Qa
z)MD-ErxQY*%d&N%ENDOI0iz?S_R&GWKvLkw+~oed%O!f{SviJAqr+Q
z7SX=;NuE}9d`0Et(PcPT@6FqrNGT^L#kwEzMfmtL+az*6oiWcMW^d?Kug_3iM(%yuPy#QrRtLk042$`->#3@43Xf7R-Qg7^jb
z3?hk0&F@hTJFOQw-0!A9=u4-DY`_<)KOHgVQ3la($&DLQiDoD!kysRG399jRP%r-(
zwnxZ^tw3>6Y7xUl14I)c;WZ%z^UYOS{v!bt(fYhi>?a7aNF(H@_x-c(Ez7Iyz}d4`f#Jhm8DRnzugD
zujWtRD?*7nnnrqJQq{@OcdLFXp!&6LZ#v1B^GQM})4hF*X^7&}_UDi?OZZ(*6BwJW
z-^`vx&bCJ{ialBpr~djTCQ}ff*bn5MRNnKr&jPfC-%}6QTaxB3$dt@lnq4pmTe#PW
zc%YC-n<>cVSoXw_s|)|E_q|6_rHsFtJsk}EUW5-!%{_zvUf!SA2k@;RgCunWDLLjqf
z?kvrC%p}-ybhwg(oaC#FJO>sWmVD8dM-sh~Vg920ZcjA>*)+szvuEqWZZg|kd}D@=t3OuF
zYYG-$%EF={*#aI&KiA!UR@bCP!;2j1_~lFQ%J+{o&w;Nyf%}?=W;p~0(rskrI7xVh
zUUVFU9pd??Je5qhyBRS<@i^Ir*GBc`Ndtq(a#%$1refo;Y(=95od9yiY+s{&fm$3o
zzrgO#)ZOYTiIjrPXonT)CF?b59T9i(1orY_yZtM?fX4nb
zRD$48iTpp>k{maXYF=X4T8+h2DCe?9^+wA0in204cI#io_A(@v5x6TU+c?{TjXdF_
zx|PF9M!LTn4}W%7FK_3fe!X0zE4g9-S{$~|o7JTsCX@{Citighi>ZAJOyaD}(I`gc_$+x;G6XccP
z&2Lt+C2g|eAxu>SQ!cWNwseufdWX9VL%-GHrnzRxHCl-(K`xmDggGIs9k4K-m3-P%p3z@S+aCelj}LB`!;sfc>K?y(p=gGZHb0H>HV+0vfg)e7ik~U3K2~n
zxC7j*Q)+ARe%fcs8ejqiy_^%8d&Lsf)sdW%XXcnHEB|DPhz*~Gk$64!aOe75@-n8(
z?7I0NAxxLS0u)WQE+OA*nbkp98I|EfUChZL*!Goi2bR*klkUk9ZrZ`;T&mEV-mn
zkWW=>BqOzV{4_5|GOng=Qjd7IH6m5C^3!V!G2D4N$Flg#Y&WE4<__QPW62i>*%|bV
zgZoepFjeiqM{u?&)ezZ1)*<2?+%IvDuH|NKBR%Uz_SYg5y*?rK+kD#P^Ip&nLa7l9
z)SY43#)2^DTSYEc^aD_dNt?v;nt+qkHJDkGp_cpFb30c-{3MsA|r*q#~=5qmC0|KuRe!L(z|OE#xJo%6lZyh
zl>BN%p(o`{0K!S$+!egg=tH-#S^C0*}TyuO`*M)8dGr`SPkhjn~SH<2nHcTtdQ*
z+jhJbORvo(+y$7uTQj4Ll>+)(u4=6h>t(5#g0zC#X6@hqREmGVO!eVaQ23hFxzD`3
zQGaQnZFQr*J?q@Eht~FEaK}Eg|D~JN2|BM&;vTKpWY&+AnMqIqvrue$Am?wrgPP-e
zhH(l;=2lqSvAJsl>HLh_!qJBd?^}uMQ<(Q?-V811^A{{#{{Gl(e`3Mku+Li%Uv&3}
z6?V?L6KDyMWeC43F$R|i6S{sR`i~=m}(%{FU-DZ|y
z{9=yzo4Nrqi7Dxv@cdF(vZ%p-Vv(QfGQLp)@O2JB1u|$*
zpK$#yG}@Z+A>;{el5#Yv8GM
zw@%?2_6H}HpAHsvM2VNbDE)77l_`ZfmmZmImc?{)@Q@@c@zoq;<(be_>~6>ml$>Cx
zlUUBd_Yr|7r$a`vz~jNzmc~`Vja(&vu{o@Gn
zmSw}V+=pC(aeohE+$~8DPXA_n!J{2RWJ3+XFDIRw;khXOCHPI^2NnQuIHK=ik4K$8
z##s9Z!`P4nSBvF^vSG6yNAXDjAMw*{&6=?Xd~P|iy%SW_FrRup76fmgz%#1h$0WI)
zoa+_279hml}2{)3->^s!CDmN32nEruAkiO_q>r@Jxd++7v+~=#8?D5-P(Om-jR^^TjLManVJO
zV^fAuA=G)lJBG!Z!?lq|W|xm2m#!fSV%Jc77tw()%!u0NwW#X9ZYG@k7OZ;oIHWY3
zOr-f@BQ}^VW5xRT)M+_7EJ*U1N`N_=lRQx(XSe8MuJ=FG^Aq1({^;-doV~?ErPrX;
zYPRTJ+%LLMKzt!HL)?PtO_s=BU@yZ&Bz)Y?huh33mD@1Zk?Nxh#)wl0-35(keG>bQ>t72gcb*@ky6-t7$k$2X
z@o~Mlt%`i5*BRjzyHXSO@d8sdABk&C_cjA*I*%Y
zkYR3evSvmlGXdyNw8Gf7R#1mmY5$>f*l6b4T@P(t14CIqX-XNS_u=D&?;XeHBRN&C
zZC53WbAS#Ieao1BEuRsC+SIXQg9`sg?6=CVEuH@e3OB|&-)+(M$6n9Zx<%U*hgh@?
z9i})@zh+_tvO5>2)&L?)qr%e)1I(MN*XAp-Swj~TpwdM4goQcIhjYY-OK7^&wF$Dja-d8j6)nM;V;XZ7tXpV}j3GXBt;fKZ@6NBzEoiaRBU
zF^#zPvQ(lH4}~Y~`h~A|unA`vJ4fYU^YO?Vx?nIYGIYlU5B=3VE7=Aw;@9(1q{2_u
z3xpN-2n;vip%B%6|K1T8lLM@s)s}tGYmF_0d?H*)_F~hvEAu6*-U<1F0E>#y;OcYX
z?C+BwhxVN_M*&YOQIZ+D$H(wKNK-fL65!%-Q(uBH<+6v0t)#INT~T@8zD>`WAJuCN
zJ1L=|jR=B?rXc%{T!yi*X7
zeY&U6(LZq6-Bq~%17Eej%TXDREy9jT>cA)eC)nm1$VJ95uz)b84CTq6Jt>?H)T@^r
zM|Smj*8oX~`hhr?=-r0lIl*$A;>Yux?i**$KZ%LBIY>XO41$kC1R7tePz;kxDr$+o
zlFj7)7BNQgSICeLros#6en=Ncr|u(x)y6+2eu0%qE*^`RT2e~Aw)W>rzm?uLDBA13a=56~%euXnHf*io7rqD|?v-I~i@$8-x`+!xMGw07%g;hJU_
zm}`(Agb_YRKuUCSRjVD(00_`SYNMIvBMUU))Z!8y@1u{T&37m*X{X
z7DA6PK!fbPPF5M{lD???W+ljP(fnkNnx4gn*`~jdB~;8%3wP9-yJ?UCiUv
z1{F}`Sx~r`yoZ({C`<5TsVp@k{KdsErLc3xbg!mse1hQnqC*1K8QyMKrA`oVGpk=6
z($dQNqYc$~Lyo$4-$EFyG{@Bz3HX8{OV4Hu(pg9~^bw6)@i>w3demqAYoy^+p&twl
zTI*gHy8JX|oD9o$jMCnYlwm63b-<@sJw2DyJ>#y(i8tk)kV-?m)i3szWBMIbpPx_|
z@837phxxQ=5H=Qz=-5f*^swFz$pZYwDT1wZnB=K1L*j3efaDv+y3gO=Cm!eg^~iZSD^glE437IPK9
zct<#4x%|`q5GgokT0G)gM*itJ7`-C`+tC9v97I8n1z1ePXQ*pN^|<&1hx(X+B|V
z90?eoz%&u^o*$u=`oLTH-DiPA(iVId6l~!XwLEIHiyKNo2{ux83z1xyusIMZ=%@#y
zDHhoH)l5m;9be+4uNuqg_ME?Rk%uX9k?l6k?S_5iJic^h{oRhkf*_7BbSJ;t?D9_s)2Bmjp0*NidxE7gWP-I+&j)#W6N>ySqI^%#l-8teixROG3g@#TPo-
zE`a;Ir2SnRo3Bd0n_M}J?#Bwg>fJk|q5fF8@XuP)I?Z=-&UsuU$!?q65z9^W-Vr%B*`CQw?
z$p-pVLQUhh1#C=X(|BD?Ey$=herHZA%&fj2LD$Ti+3*|fZAt&6r1372rKexgudLH>
zDLSE?`8F^;b@jNFWGXA?QXyHLIdw!!(;lOkh!ukAhDoIn4*OYNF(%)KE{X~gQS!DH
zo}8rHv>AGg5QXj74hfkgrN+esubkPnc1PAto0azhY|Ivc;0uhw{|&M^ub_@F;uZa?
z&kbi+8$n?msZG^V%^*vcQwh}Td_nPJ=!rWlguzv$HCgl&0x38z
zyrJ#}WM^T+{37eJ!O1Y8KNRFq+xHsV>R)8PCAf^uOQPqfodH9^4}ozTjL(8HU5mK8H{W2w^{ZRe}4SEa&p0c@CCm
zfh5yLQ=f=Dw%OHOVCBzOtd$)(*gK#p$DQm1kn9XiTh)c{YvUHi`=4_YWACr;1fIF){8d1~O+IY_}jw6USrOQb32T^dO+sME}
zlnG3wbX^@Hd0dfzT=rNfF69i$zILdRbgOO7o~~H?T*f;H
zCbV`i*f@1fH}kp2YBs$d3#n5L>b8#Gf=Kn;t%j^y$2WZMF;{m9sDW;vwFj{r&s}5t
zmVIYcRT|YG3e&<&$BH%jImol8q>ve3GU3_cROSi9yh*}X6QYD|CfqvO$sW158eQ5%
zGQTZ^hI#va4zpSTBS9p>T|S#^GI?epui)=qYayXv-u$8P1b%)h*(T%*uC+A1b&jDRt+Xy_VziWdzKKw
zYoXTbHbwt%je%pwKeL30a>L6#r{~v*EjN+3>>w^s=GfFQ!JJKAL>lzZ>t_ktrVKH`
z;g+PXz!|jaagh-RMHM{PG7M4I^QB|OLAAvUTPSTJqx|L0eOLs;4%lO^9!j4
zD{1f76d-4+)+P}2NfvBO8DX01?5tw|XGsr&!3IGzBY(2LeIFiA#eliIdWmznouy*d
zi5tD=(#iS;tee}OjbsTfB)h8!mNV56t?RqC9&yGYKnVV
z<;KUuLC?3i$dJ_5%x`6sGV0CK-i_#bi&p3EY$MHiAzy^jDCnG^$^4C(6>r@D8kfp5
z!`8*nYS1yW+aD>I#6Lt0L|>X0
ziti!x5RM!UL&nO%a;(51y4t2!z2qg)+h`%>xo3U*GufxIQorezkqg9AeJ7WHj9dQGnqm|14X0^Gc92wc4k
z4qa4A*D+QLS5Asi#25Jx`yc%|&bG#}Cyk$F;Gm!1AlcbK9$u#uUB?9REAib_B59li&u
zC}VkP(rrp9NoG4j>Xg8WyL13Eav(_O^=oc*Sv62
zHv?;*k>x1frf;i|?DGYgaocnkEaf_LyI(HDSo)-D53jlPi6)%R#8v;!*r!VNW0ERO
zYDWxuzGN>RXeY&dF|f**^T5p-x^ZCt9_CleEB^rKJVZ+f2A;LXJ!+-YdM5Ae0diUcoNqhp-^2Y-b^tJ{;Rds+%-r?%5CuE9>H$TP~_LX
zJeK08KS4;Pj6T_m`*n~~sB-}ybE9vSs7`_Q1iT)3{4EDqrCO*G0}!!LpDkhks^jW<
zyZUI8-Frlu(}C
zo437DPM{(Qsz)`ZEzmJX&sd`Rfjoiv3LYi>J1Mlbs`e>kFKRoB?1UDqZexkLZ&n9T
zrw!mxC~U4ZA1lp5(Hq#o{JBoPK;TghA^V1!!D{xNrx4iY%rcx_*05OX&1vNkd;dz<
zvW8NI5srB$6*>j`$VICFziTyM6i>~P&o2C>K!;u)TM>XhM1&0>^U6w9>bE5^XrD+u#izS6y7Rw*}^$5T^BQKPcgdIcA2&pWuCe@Y*3zBIFM}$k+hqD4Z^NG?)
zBgWwp7_=)GZ8mn$u*_(zhsM`mY(d<5aA~{0;d}r%35q#Lh!Z(V&mafd`vppwq87BCI)dVub2^
z(SF^}`gRU4>_S{YJW%Z^4PhazA|vv!j1Vjt$egLD8As2TaQpod6yCIxfc;g;!T4cb
zp-EKsnN*MCrv@gI6GEAUQz%)
zIqa>!q;6i!%<&d6++`td5y1ffVvI1#OmOmkNvOEbTfYqaG+JuacT~vpKuYF#cwFf-
zGg@WjYLZcy%(JE#XU|G2c_nP-;~Z47E&-3&&`)cvBNu@2i}8-DH!=ozoO>=olJf1o
zEA|-WX;%n-Zgj2yh0
z64p$#K2_&&_<(&m-!OY0OY(W>@6YBf#j#)wIxZGFW04?vZ06(bwI>U9jD$w)c)f7jlf!
z>fGh0%MUn~A-zbxm&>$nSIpS|T`yc;ti-u=PO)B3)nl14eH|t3Oiv(^IKW>eh!ZS>mbcbfUL2Ex@Jhi9X?;ZnISkBDO#A%#+S_TVVC
z#hr|hldaIL*B}<&`eIDij7K}Epu*BI|_
zIaK1`y48LyRFG;$DaGRb-nMp-XntqbWpLUwQbS>i%;QJZ8x)7<7bw|lm#kBv*a#!qV3_yYk>n>uAlvwFyr?hPW6&NI
zH;rSEo}o7XvKd@vUGdM?j-^n|HuDv4&SxMXHxXcUgn
zu|T=Ho14=!5>N%>MBW~DTLU(%7Fz8=xBC${!ZyM24;#K2=)TWn9XA6C02Y9g95dGd
z{Zgwp^S93$R;<5EXNlrXf4BNx(-af&{>Gd+JNRV~=D*w0IoL<8%KScX^C|wsTy5
ze@*Uw8p^fj@4wB0maVinVSPS9sy^5FFQ3
zK7%g4f7|gF_CFy15Du(BhVQ@_=?JK7ScIp-);?qGNAV{XIOnJctAV}|VAggBgy&E0
za)WQiulec|aTAxv*)()DPwcLSy1H~l0M2aFwdUxnN87#_+3eqO%oo(a$oCrtj{oql
zAdpH0mU`i94{M0*k6|)&Fx@wOwVVLJ-D0gVcvs_n4!N}o*-{8Q_s;MfbiZ<)&10s{*dAqi&ODhdmVk-w`%nuLiASgU;c8Zg}pmE>Imbixr}QEIHu^4gp~
z$VGQr2j>jl^`e9_W_M$o1su>;l28}p>$QdhX
z;gWXhfWmy~3rAeM2FkLe$!}jZ#6Wds#UaY$q}AL93v;Ny4Ut>fIMz^k_1Y`QdfSlD
z>=R%%dpTl6VTDqv5KuA&>~|2FY!C4K-r)^<_A+7Uaq&{|2unl9vIRp;
zv2E9cW1~aHlRo+U1yVYW^w2pn+0yr1f6Vtibq
zwq8ZqW@b&%n;-mvr`qWC`qX6%az!nO?5Ybo0h7^c6#S%tn0IB@bLy#HdH;?Ja^x;a
z#N%wDUPqKcRaI11S@hFpo!EJCpjMQmF89y!8io@AbBBQpZ?LM|opm3>-DIU01iTA^
z-So|5N-+G&(e3Mo8`Fu=k`6y*y(y8$Ttj3L+^R;uurD(;);d;Q;Yc^Gqb8;-Q3K1)
zLQy3lk=JX1IL=^Vz5=uH6#MeMH=0E}kxaI8^3geo0fkZtu@~cxFPWi;A1kou(Gq?Z
z8;*PV26y5X9_23zzI2R3wvqJ;uZqU-OHM}IWi!13*56;zNip(&9JcH9GmlQ!#tI$;
z2afi!#sS6Y`>tiUy4k`RG+Z+=dcT5?nJ%jo<_?X|GNVqTVPUM!O9%5dv-lH6Ym53J
zY&b^QiYc_)e`>z24T8pD0SGFv&ZP68T%~`SxB#JZGOiybdd^)RrUNy
zw^JYd#tZ*u8fZ;|)fCZoOh{}OSX5ETQs1^pH!k@0gqy(t{H}<_E0wzP=a()nYa-0N
z;S}8aWI}^*EGs+SHn?|!;|uxh>;Iwy9uRRM#q<1X0{R(En!Cq-0A^_r#NJZy;IK1i
z2u`ke@bw*liykaSKM1?uq?Oo+!ofoF<97apGA&=eOSi7*fZ#=0Tp;}%wKkIhtUHi?
zhbBeH@=C2czkx}Ld@WE#Mj;d(VU>dI`;Ev4JKF1(!%4F;jG^SzYS6pHmJ8~EuqVgO
z%l&|unc?AgOIpcx3k;Y(r?2~zT5-@Y-R1H7WaG}lcT4cRzSmsU%blqwENhDK_ZT8Z
z&bC>}p^|Nr;9(o!FYcf|N~cAdaqN6AQZ3m0G{Iob;(e!N{-y}UV)52B={al(_Danp
z-m1Eq-*VdJiN6rPH!8Jj(DF7!NXJ7!Y3S`^YE+lqXwSa$ZFYMRbn(H!#InWDmcMVi
zwaOuunhehIRLs}(Jv$w+dzFOqg;yfk*y!YNhu=ixcz|sPHs)*chR?JC%;BEFOS1!%
z;f01_NVXtz8YP%8`2139yJ^GD;tmzvQC3mBjlC#~XH0KGu7(!c52TR7{sfhI@%Xmk
zwKi;UooCnJ>FMnJC;scS@27+$&V_{nkq=Mr8voVdMijxeGP>1u4fFb#DzAJtIy7jE
zZoY8K2#F|k!MR%Yb3V@ufYRX|oE~?5=+PBirFpWCGHsOjz~CTa|CUOYOFZ91mJXGI
zvpSVe<(GxtYm;!eo`eGWp}!%T1&Rea1-g|OnkGyQ1WrLpFSp)8u2#u`*fhhX_t@Z4
z)n;~9RQ)S~=IYy>?xt7!YIIVu)_vRAFtZWZji#2n)dsr}(j%#v1Rtr+FEsJg+HR?A
z0-KMoEpZ~Sr;V>3pZ=-ietOzWFI;-I4pNs3b(oJH+SfA1x!JCJg9_KTA>0LXXlY8VC2TBm3YKMEn7ci`p64X%$dK%NCgxarEg}?rU(&
z!9(4Bysb}cmbK4$UT||u11QN26|-2zTFk3jyz#{)ow1R#zmxWQir#{BDvf=A@t(4I-L=CDGB|8
z^O7Ge;Ow;NPSZ9mFg!govwCwmN@Jv3XFg|_GaFq>^ZIzM}>;e1~{rh#tGs+
z^OQ7fH*-zGw!a=hMXHDL2l0hv)?&`M*@pLU;EfB#On-T)O8J$$`lBIjhA)IsBcLiG~HUd%rs_Tu+?uXca
zrbOz3ojRk5h`uD6QgU|Gkn%XEjRG=_s|0ZsEL_wqj}O^kJY|V}&YL+;yvNc$ORB!R
zG?p2+kdT#21ed4W5~%a6QF(t|`gQH`gdbb|qswtGQR~nnAA=FBD$Qd0^P@-T$Jd0Lq`(P6Uvjy=kG)qRn8966Sbf(3U~dQVR3*F@MD^2Q1T_m8-a?WbozW>W-*_ee1wLjimZe)+ViyploV69&eWVW1*<_{m2qoQgQUGI6bfAbMt~G
z!B-&H9$8Gz7@IBwD~=sdTc|vc)WHq_Y!}~JymNTeBzxR^36C{rg#ahhP!h(bGw)U0S4Kq`9eb
zHiL&??R|DwE{^Jhwuk$2ShKKA4Q`BtAz3=Ak0;MW{m-QE)lK=<#S5;qo4xYTWO6Rg
z0WMz(v9NjB!5z1e!#)P)Y;L@imR+j}XYbe~m-3iKqOTyHMGj4il|F=I29uwlz4ZWd
zn;1J_rWJRYOv=xUpqtq;v|(s3X1hASaKHngvZ|(Z4G4&dfX+K65j&UFaN0&_6EV=&
zdTM*f(DFSaMfX1jR;_Z~yMlg_2x0OSX9K8FcIh@-*?_ERE{qhqh1(PPl
zL6L9h#y&s1oxOw)E+T_ROq(3@6n|zvJ(&}bV5uQ0n1sD~0oHQN+bjJWnkvKjKo~Aq
zKy4Uz6!|rw%hs;eICuC5g#{CsB3ubd%uMtfHb-3dL>9Ng$7NAUo4E~+vP(~+f@Zoc{p!-_(Jfp+fhHm*qGMozuF8OvRGOi7
z{+g2P%nvbwK}oP5<;v2$UF1w->LD{l@u(-T#AG+
zzSn_uLTWTo7#d}KFy9r4c-y?Jb=FbOnslJw<+l9EAN!GRl?e%
zJW2r;Wwb?vcTiK+`pt+1@K(|;x3;cz!d!Qep&!0<22gfTNI+Jff+1$_+pB4i2GO|D
z)xdV%M#LS%?!QiPk|wD0#R!wY*AIVqzug@RO+Uz{YFs!ND(;`uY)t!r2_wvJ$S3lT
zEEL2n1mC@E)4ml#U00PC$BkROi>!Ane_Zyig+N%s^^C`+Igm#$?NE@?(8x+(aRKZW0fIm-11+JkAA*-9~wn|vGDs240@o|fM=v?FbPphqA7kb(8HXu;}$
z44GCg;T=rQWb&!VzOqb7pn)WUtA9tz;jUUc=
zGXjfB!*eN!`Z{htHfoO`xe*Pq2}>H!i_-byub^1W%_|*qJPr)&T4fi;+KVlCySjI#
z>;@j@Q9JX=F-~vmlu&H?^wtwdP2mTIr&`=0!Kr3Z5>cM3-2D+
zrB$d16j^n6pe6gxv3#D@3+ZhXu>+J^t}pV5q)9kmI?1Ri5b#YV+J7>t^J5+nK2y>m
zP$2w(bolh?krK{QDHBWYM}O<06bVYWo-YF`EV1N`|h;X3iN)D
zyL+s_eyVLGH*E6|L|LyBLh~qKBSOHnKu&U(lfpL^#zKP!%>b!?5ApJKc6A}AqaV>T
z%I+RMkWw1wSB~
z*2*XNF%f@xB6KuOe1J>$mKM#sQNoT3q$&*8QeP$2&1)G?fCQ5ny!oxRp8shM0DjO&KIIfc`Dkw~2bDb+8CrTuB3F3-#W%-1^>Y!{7RrWwtY
z(pFG)*1(&=ksxrxT+t6@!&lYN-Hz8oRB6LjrKdY&WqL2gMu2jbR+WDn7EMS0@ls3F
z?F}P*7Se_wHbKT^IEMK;0j|vt3k9#rQ{?9jMF&(v+7BKpYK%Rm-tM5BxI~l@%q>B^
zqoh3`J<|@DpgX<)ORWh;?ta{LSsmZ;p91uOzAeC@?5`dW{^5MV-hr_7kU?4}m`u^$
z2Fd0)KHX=wcU^q7GF7FQ=q7GaI!3ZvOtlCXyItO4jIYgkG4>l^E%Z-q7T3#1#HY_I
zgTm9}uZ{kWr4KvQfiK3*#JuP0KjV=Z+kZ=KIukk3ksTCNvUxvKsal}!b=ruPK*|FDUJl=L))Qaw(
zL(M?lX}#&Z`FnZC`_wcWiVP!qy9$=Sc^Qz-Q9!UQzw-$mO--U5BEwTM?*?_Jp|Rce
z*L5BB+pa>K+lOM{Vj<^s1~4_3f67e~c=I!MxmcGR;@
zF+Yx>)7;eG**iD!453LHh{W2}z36WOq3B&SzOT)*V^!;tp&xKS%AiOR?B^2R;)dA(OcE&PR#0LDMUhUsm6Wa-U1EbpC^$Z+A-SGJ1s|^3@-C$?_7t15in~C9v)emS{ZGlcky)!}
z8ijuW(=Mu|Q5cAQyjZz2
z6KZe+`J*Zm*tH9j#)A`t6*4{&V+_#q?0U!~B2Ej&
zoAMSg5FhVRR#ce766&9zJyE5!QtI}z?r3=srQE8F(DF__N+IIlzX)h0>xDN2A%n?%
zj)9aIyBoeUC$|Z$7bLxkIZBWl;f0H21@`yky7nS@Y~*;7ofb(ZhTQBu!wpprWoLTd
zl>q`xcxo(nHH0$Mq7Qk=RR-L;sd-Je>A-?KZ?F)bbT`L(^H=ts4z(kFmEAz+{N!6M
z1Tc=qPl@83ywT~KYVz>eHme=8K?pAb0`8FdoLFKxIn1_KJ|N;)4w_F@MZcRNt+xxP
z$UFbgf)~B(r!ONa;!`O&+NjhXy0h2wli?%x1b
zz;oWDQENk+&%x0%vC)vTV*K-4aMhPx`W_fjw>86g-Ze?(F%?At3vg7qC#Fw6+-S17
zW;TwkB5ddd0OVQDIRge~^{rkLRErOZQ<(QLT|C_Ca`2
zqP&~8RdQI+x;FB~$^LuNW^rHsF@9{*B?kh!iR6Mp!0#X<>>@H|q>L*%Z2|BgL6o#P%VTulFS4RG>I`b>ymKX(gsi8
z`u^T52w^gWaF+(Z3~(sR_K(a%#&su0BDZj|^yi(vCaF?Y4+OoTjvoA?S`D4EH}=y#
zWi(+zv~gh@j}14X!gyLQ+GI7fhdrW&DWicndjz=x^}w#SDV}jhJ1nwhN{ILzO!xh!)G4YOEYLCq5Gp|^byRg53>t?qjIiXzH7X%P}8e0
z-`e3WqCARf_mE7iL3u#Eb#dTIg>eTTpb$s@nZJrAM}+Px!Djqn#urc0a#Ifx4W=}(
zU4-mt!#`J~MbQf_$^Uc&17w{gwf^e;!{@!GdV{jm6gH2hDm0Jw4Tu5@FyW}vf)1@u
z39|b*7O=H_O$f2cfB3Q+>5PeOK0s~BEbR%7A+eBv?I{%bB1C*~_#WaC1Fu%#>p&`0
zrSA?ZKHpVsQvFKN1eo^J6B(kdn|thaDH2yX<|9;N5&vRWs&!=AjDjmUDde}GL)`{$
z-Tey&_;sqxKD22ajrt83P=qK97W`S?i^!IX_RI+^IgvcU?8VoSx519nh5Lz27P0&x
z^{@MRelP+o6A<*Z1p8q0%wiO2ro48FPXDo@@s+Lv^9s398m^wqf1nL2`dy^vIvPg^
zfvf$RakHJp;k&yopX0k3B+^iewqk80xsmdF=!AefjWhu^vPUAonn)F!^V6Kx2{yVA
zFUtM|r{B>HYSTYDQ%A(FQN%h)3%=MLGqXBAM1-{ox^SRv7pvabcX5erLa2avBc
zCOk!p!D2efb0*%91uQ<=9`0hmR`UJ$VW9o5!hzzz2OivgxR{>xhDt!g%X_HJ3FaBt
zV>MCSBHA=-B}zsoH8_D5-0As=t*v-%%7sRbnUn%Dq5kCjb>f+76K->Bkj$rMuTM%1
z3}CnHxoz+>wemzEQ;Wi~sX-c7yirOAY_L6~J?`&uh?$P?@BU}_X%);CF#)p%O6@qnuxBD0lWH~2c{h^Wcyo>l`n
zb{S`US4>h)yxX+~p1OUU4t)6gPzHO(-3R+G9TmllkP$<6MFLAxZN=63K>1OOT=saL
z;-?`^L#>m^GptVWYL6ulxkLm^OX%|vZFCpk2~fyS2yTb~IGd}`H;!o}Tr
z2tS-r9P)lI1H9UvOTfwPlz4xqrZy{p6}s`Q$E_8DlV8Lo$FlPOf&-{NF!9d(0(6$a
z9HMAmd-bQ#L1*4^sgdN_uhr+rDaK5UcuGsZ8t^omSK?47o)M>pCLpdNF9*Vur2F4
z%-wJV6mz<=VAFa^!at2a*=$^3;z*CxXgV63fNR*w2%4Nj8j)EfIP!xc;kXG(#;|*Q
zV`5`acVj1JAL9(UEl&!1Eu;qU7Y
z1j{4JRh>Y}rZ|se02$>9PG2Y<DJ4+8<5l10ekHX^HT_hNle(kL$y89RJ
zfDcq3f)j_PI!^^K*pM0HVzCqcAf%th1^Oa6pG9MJwHws=^RfMuG!5u0JWY^-=|hqv
z`N2-d+JEBs^)569Y-s@r-)V1{CQ9<9YlMS=+UTM|zW$5@8|Sbc_APiydpz&Do2!TU
zU^t{Ux#wgngOqWuX*;hc`MzaQO^m5p9#a(I1pRYVFL=yqC4~?kgx%f;T@`eIO15Th5n$%xm4jkX#W;Ir1+Y&wROw@ii0$oK(iB1RjTFI3SyhF)a!0TyY^GE60(t_}smRdq%U=KmhzKaB9R*
zc|kO7Am)}3vvPsoF06ky5aj&3Z*s%qM|2)J#Sv%?-cbZ-J>#$(FoL=TrkL4!=Ly
zB7qU~UUQg%{!!C`3&4p@gcQbk#*`U|I2C|!57Gdl<^n&O&uk=3e@Bo@K2x~o*1tpw
z^E^{OzFEvmRpH}<;LnI-%pGnplFujiM1w@;J9w>@j-)-qYu6YF#~|LM4^r5azLJ0`
zW>)BXV1JK3k~-8vx|OS$XwnUCOTaLwosK6*NNuXej5#r_~V_57~4StfRF7%LekuJ_Ax@LkA4
z+7!B_+~7OA6eQSVrzlq_DtB8$DFDd
zxUM94w&UGEm9M3M3ui*ut;o#;#>EDEa!W~mH!4i4de-ZlqnzN(r}_qvQV&mG&@#q+
zk6pZ~L$u+*`C5r*45I8000fu=)8mHTok?!$%NjA_>rcF
zfv7jfU+Sm3V`Vke>%e$wR_ABoK9_&+S7lU^=CctnY#RxH-Gow(&)}{nslCsbR%c15
zhJMz~5vjB3yZGzxgQ}(EuCa$t0fVc7(5Xrj#)FhzScUz-|b;ix3yq2U{L#3C^n)vVCAJ*WK$wbOAv*$yFUtS8$)cD=FV!>u61qHmzH9L+1nVp9L$gaK|N(*>L%QkX_SYb#z04kKVrLqcuC9#V#AJn_^O}AE@)=NRd
z*Vp^~@9#hU&`&
z{o5oDQYH)dh%}R5B=L>#1qh0ejgT(^T2NZZr&n^nGxkjl!qulkQ6X2q7F<)sAhn>l
zAhmDb7j8eVX^)qXNHyxMg3BP&O3Xem+IFFVI8hD8(~ndGU^fxMFmw6jH)4Ue{ANO|
zD~qjBtJ_mtizCvN|0KKV-G0sewpxyz;L!4{`nNdY1Gb-kZ;wVmre1t_VM~Fv?*|pK
z%zW2ksJWqfGTB1S!*2GwN#oxt!A@zb!!F3zWLx(nRLg4<+nVEy9hsw5xGYyH
z&?ee`8vItZ-tC;hKGZIuSj0wu2_};$pmSc9Yts?nb_UO7vvd!LU(8$cL9AIQx&az&H#1?12iyj#Hj~RFYD|M2mCOWWv8SO3zF1B59E}ZXb
zIV_ILWpLt!vS38EU-N?j$PV4xk+1;fG!x$z%hWqb9$Z`3J0?wfPYiG+HSzlU`(EL<
zaLJu&h<~>U^xN_#CTk3TB!93bV#2r~$O@2;T$|*D^XM1o;0lX-TV?3Y9Q-=0`?HRR
zoe1_XLbjGRt_zFM>32o^xYh{15RAo*@z)~1a}}d}tnA4~;l9)^-He$@p#l|(aRvw*
znb$@FSoNom#)oyM#B8nfkB7F#vKIb_+mIZRWbVH)G^0x?fR!|(MA_DWrT*RDB3&@S
zscmpJ?P0L)_=}+G}KAX1U}9*9G8NiwS4=8A`h_S;VXH1|3V0L%uI>#RUpA16MxVI|h(<_)fY!
zw=oF{lw@^B6TnJm;w8EgcY}TgT|wS%fqrlYQvoe03ofG!Zs(eNLwglYBo4H4^Dl|V
ze5&vv(TX#=n}&y9KW!(WKC(qIb>cmO%<<}akx4U;Z#@qt6eibUOD=yrj-c+m&YBTh
z?Ii^2np59Z%tv=%KcY&UWPiB}^t<(AxYr;NZ;_xbaTEk)z4_g~KZOV1oDD*buDu2N
z!k6)Pe-9ZUY#UNR@se67VhT@C|NMxNK_w(N{D8*^KV#`M{6K)R3#zu9MVYYw{7Lo0
z9B<6^5gP5y0F8uH0?zDH6a(twRBVSE8?p{_+`I6%2>HxP+f;mx94NsbYTC39#(U?
zW6n=6Zo^%M!>rsk8`e9c?86TDVQ3O575&;#ccH{Y-lUF6u&H+3|CyTTrT
zapt<(Y(LJh5|+WUUef;}e2Y$u$^9GA4CWIKg&ycwPUy#x7$LwmZ`JaizRQret(v$!
znWma_c3p4lRcsGsf?C4kWp_ATN3CyNf_xua=Da0M#F7$PS(;Qz#O3U9YSg-0(4$c;
zdtIj=Lb!;`gZ`Y
zrGP=`4RSIkFM3U%(aWeN>ds!xBB46UE5h@pgA^wez49-Fd2}YDSDa41{=F^J%#^JbkZ?ZxvnUW~
z_ahp;I-kKUM}5LRNsE~ci>Z?Z3AA@{Q2`JC{8AyJG>uW~
z2`%-BO~GDKJb(vlRFD!yf&LIIFD>h;7{~}=Shcf(EM`mh4{xaEEP7Pw-z9_dfKR(u
zK)SLcXBulfjQ*+cA}vEuN%-eB>$@SN#HJdJU}&GSqU6bQSZ%2$A6E$YeXoQ@TV29@
zZy^t_lNaIz2$67uhcCk0bf%l$CH9*P@(d+;iqGF+9(n&3Ja+{?-5c`M$4gfZ+|DPp
zOfY4Mln8CmD{rml$+cGemOvg
z+r@YV(KdwGyB?d*2B~tsXn#3xf2%N^Z6`ine++-o=1q?L{TF`wAx8_RPz7bL
z3wac;v$JTu8?JfxTBHt1w^8#DxSb2o8Uy@u>{U0WJ`fnvNimc>G7^?CTeXou!sI0M&9Yd
zx+bWK9;j7Gw$y7?n}pX
z#K5u63ca3?-kx`I-hK$FEolCkzgR#h-{wyq-l?MNs$1#aElma=LW%h$hlEnw@aD)%
z(LQ*ab{<&_d8=#h!Rm0E-t-vM$NBahSg`8YvR3Hw2=TOvLcgN#eKEIkd}NlEcD8>Z
z$_a!0V`<5WE3ku1C5V|3_TX6=Z%T4@3Q+cm1QHJay6GUYImMzV=BKV&eVa00@C}ZL
zYi3`s-}AYz&*S3n#_LBmM+V=nn_CC&07iL;cJldSEj?5pS;_?^%@3^11IJ=gD@gvF^(tf@
z?MW2Rj}+YH8$$SCmkpALqe4CWwDBS2I0ZilEqMA5cO~C!NpKTaEa3`49H9kWRfl@1
zt!jXV>YD!!o}j-*o83ErA<%?*84yMAxxn(8(o}*9U_ECD^a^k2t{}|gnL7#vR-V@xMBIGnx8)eP
zKG$^-(@9k(51uBMhl%$Bh|A1+MJvtk=ej<^v$`qDAO(X^o@p+@=q2ksogjGS`OndH
z0amxmBw`aEVz)Wk>7WO<^d+RO*6H$$2@df+loUwo?h^Q$R=NWdSAP=1mvF{Cc$Ojn
zgA!FRdvI32`)2q{-6bv9sFehe@Hj(EKAuMXp0#Aq4tQ02Pbtd|J>m->`BGy`+@0FH
zHBjpnzJ+DfvLcTaAI*#S&Gr9}q^k^Q^8MO0NFzBqe$q%wHw={y5l~7}Kyq}0bSo;O
z83>3H(wzd69$lkr#0E0D-tm9G?c1K`e(w96>s;4$&W*XZ)t_VSgB-X2^>?Ps{VnuV
zrAyTtIXdQ=!0M9TYDCMe2n4yE?#ZTs9+x%7gf*G)BIsNdzGYNR^craK*-=zLWBHktXnrXd)SIw!&$r}rhzxiTz-xw@u#-oPuO>M5^k
ztWnL*0?GAEGAl(B4s`zWgEpb7V|Nie4sX<}6JxJ?Rat9oNpeY#hFvSV-$bw}yYO3)
zw2|9;@KA0&OUOCN0%dC%l+5Zh%A}Cq^Q{WQknv%lXBU
zi4xHC;c1m88gV%CK!oe85#x=L%Kl3w{H3pSG`ZRXj2N~pD1yn$(Ht_<*e&e^Z?(1*QKxi0pR6$F{e>2&X
z$#3eNpzZ_)UVd1eiDfNnSZ8<3`~f(c5r-r
zUK$d7207Rlwnzz8@(3CWNqt{%Aiam?WGT|Gg-7m@h4i5V>SNzs7GOdtQ6Jhvhye!I
zmX)|pQ_BQ;(Uwk(FhQT!rEi!j#{?0InF!-D!cMvOzK7r|eU{CPTqE#ZD@hKRz5PtC
zSA6w{0m56UK|QAQ@mXbsD2sDrH!{mCOh9;WQslmv%vG2>cqMxOVsPaqBqmFz1=hT^
znG|vwb1ayNPZ)-dAbh6lchZQ{8C!WMA~u>@1Ws1qm19y(S^dwMgAC!TQ><_*GRTSq
zMz1SjBR!;jOTMuU4gcCfb~{0`dK@SC9H0pKxCB_=CnnWbK+9(sm?NfU^Q&kh#O!Ez
zE|vFVJFk(y^ldPOvp7o$P3=DCj)&;c39Omh?|gVzRb)FTeWl4kP6@P_2N{B%v&!O)
zyX+i`ms~4FFpu+4lKJpJq_@Lg7eM_O{58Z$n3#8gje!e|*Y6UI!`2c;T-o+EB7Vq_
z=Yi`{&DI~9e7E#Jl;yz&ZT%XutrGFJL$NGS?^e{w09(^g>?*aoMUMY~el{&1M3w!0
zT1G-aAxb!#uvoj&qVCLddLA2{Q>XGXsxSW|fYMVv?+p3?&OnSE+0
zH0P*KbrOb)(u5s2Om%zV9&xVK}kX8!22tU-a^GQ4OKHzW*OW0{^(xpH4
zq5R%Ns>fb;T%I;G%tN2DBC6ak_rj(Z>wG!09Hj5414mOx!&o%#Qkd-wC@!co0By`N
zLy?MmkY^L57q%HxZ7VioTeEPFb7diy!l;1Pk!rqUn>nS|IvhJ<3A{hF(sQ6M3uZ=b
z5Bdh#1nG^3K78e4j)NT!Lkspjpo4up`;GaW%HQF@6OTr7vrXZzcji8~xtWxB~
zT^#8N-4ubjrtvtr-6~2oWBeyfvQz0clyWYH`Gm?COL4OtfDX5XPxAbQgHc;a=hN?Z
z4E9IRK8)`TH8KuqAwJjRr|xL`!tBu30UzzLysZ?gPV8-?^6!B|M4cXe4xz~9+qR%{
zlOec}c28ULkNA@`>n#NTch{Z
zmC3orv7rxNO@}r6!@PQ}rBs>y!sFR};L3-iL@3qFcs+B$lCVgkY{U%vf`f3N&a(p5
zutI_$0k1!eX@3xJbvONyin-u`+1$?+Pdo39*7{Cvwk+PvNsJF*icM3IpnxpfxqQ~0
zpW`mU#257`*S%!o*MD2jp0aWMPBO437-CTJLxp0IwkeS;CwCA6O0exR8`#UCFdA2S
zG9HVM#9t<%GQm$g?0y-@?Rs#E|1$yQTh$0lF{sB@%8dx3z0P9;VWs*p)c}o>AOp=|
zC2Xt2nvX_Z=AYA@$);2pD(!jnK!L3A4<*^xtq=IgI7IPl_01o9CIA%YXZC{Xv&j*e
zrt!Zs=TeN}_A&Uig_dZk>}`SsaJUa|f_ovfgAFFBiQ&2ua)2?Fr_A=btIuWoIbsLM
z&bE*PS!S8}s7=yY@nz51j#60*T=`55Bhh;|v=3Ly-D}9{_poa8+0LfwD}R-ua)5DH
zM6|rv^6Py_B+`NxN8w{qHyC{_N&{osx?3WhrtV1tx4Qqj%HcP(X+dsKd7<+lr>3s&YDy0aFYVoys3AqL!ND
z<4brm>u!Y(n3;B&+^4Sgx*;kGekSLTFp8t6#&weFxRU9$OqxDml})Ux$uaukMY%yb
z0s}gcFK&Kv{4`2?huBbd=-2CfkgEC?-Uh8+Ckq#0OV|Q+3
zsD>`;56s|srIn`rR!V@JcS4vM