Package Exporting (#181)

* Corrects exporting

* README Changes

* Forgot the compiled code.

* Configuration changes

* Moving action package

* Update README.md

* Update README.md

* Improving example

* Update README.md

* Update src/lib.ts

Co-Authored-By: XAMPPRocky <4464295+XAMPPRocky@users.noreply.github.com>

* Correctly building types

* Update README.md

* Configuration update

* Update README.md

* Re-assigning

* Missing chnage

* More changes

* Some more information

* Setting changes to repositoryPath and tokenType

* Compiling

* Update package.json

* Token hiding

* Package Exporting Changes (#185)

* Initiial Changes

* Changes to action

* Compiled

* Added better logging for when debug is off...

* Removing base branch logging as it's not really required

* throw new Error -> throw

* Debug flag as an variable

* Update README.md

* More README Changes

* Update README.md

* Update README.md

* Update README.md

* error.message

* Fixes the debug flag

* Changing the directory routing for shell scripting

* Tidying!

* Changing to const

* Promotion

Co-authored-by: XAMPPRocky <4464295+XAMPPRocky@users.noreply.github.com>
This commit is contained in:
James Ives 2020-03-02 07:52:38 -05:00 committed by GitHub
parent 1780f8795b
commit 71b19fd2fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1128 additions and 763 deletions

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
v10.19.0

133
README.md
View File

@ -1,12 +1,13 @@
# GitHub Pages Deploy Action :rocket:
# GitHub Pages Deploy Action :rocket:
[![Build Status](https://github.com/JamesIves/github-pages-deploy-action/workflows/unit-tests/badge.svg)](https://github.com/JamesIves/github-pages-deploy-action/actions) [![Actions Status](https://github.com/JamesIves/github-pages-deploy-action/workflows/integration-tests/badge.svg)](https://github.com/JamesIves/github-pages-deploy-action/actions) [![View Action](https://img.shields.io/badge/action-marketplace-blue.svg?logo=github&color=orange)](https://github.com/marketplace/actions/deploy-to-github-pages) [![Version](https://img.shields.io/github/v/release/JamesIves/github-pages-deploy-action.svg?logo=github)](https://github.com/JamesIves/github-pages-deploy-action/releases) [![Codecov Coverage](https://codecov.io/gh/JamesIves/github-pages-deploy-action/branch/dev/graph/badge.svg)](https://codecov.io/gh/JamesIves/github-pages-deploy-action/branch/dev)
[![Build Status](https://github.com/JamesIves/github-pages-deploy-action/workflows/unit-tests/badge.svg)](https://github.com/JamesIves/github-pages-deploy-action/actions) [![Actions Status](https://github.com/JamesIves/github-pages-deploy-action/workflows/integration-tests/badge.svg)](https://github.com/JamesIves/github-pages-deploy-action/actions) [![View Action](https://img.shields.io/badge/action-marketplace-blue.svg?logo=github&color=orange)](https://github.com/marketplace/actions/deploy-to-github-pages) [![Version](https://img.shields.io/github/v/release/JamesIves/github-pages-deploy-action.svg?logo=github)](https://github.com/JamesIves/github-pages-deploy-action/releases) [![Codecov Coverage](https://codecov.io/gh/JamesIves/github-pages-deploy-action/branch/dev/graph/badge.svg)](https://codecov.io/gh/JamesIves/github-pages-deploy-action/branch/dev)
This [GitHub action](https://github.com/features/actions) will handle the deploy process of your project to [GitHub Pages](https://pages.github.com/). It can be configured to upload your production-ready code into any branch you'd like, including `gh-pages` and `docs`.
This [GitHub action](https://github.com/features/actions) will handle the deploy process of your project to [GitHub Pages](https://pages.github.com/). It can be configured to upload your production-ready code into any branch you'd like, including `gh-pages` and `docs`.
![Example Screenshot](screenshot.png)
## Getting Started :airplane:
You can include the action in your workflow to trigger on any event that [GitHub actions supports](https://help.github.com/en/articles/events-that-trigger-workflows). If the remote branch that you wish to deploy to doesn't already exist the action will create it for you. Your workflow will also need to include the `actions/checkout` step before this workflow runs in order for the deployment to work.
You can view an example of this below.
@ -18,28 +19,62 @@ jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2 # If you're using actions/checkout@v2 you must set persist-credentials to false in most cases for the deployment to work correctly.
with:
persist-credentials: false
- name: Checkout
uses: actions/checkout@v2 # If you're using actions/checkout@v2 you must set persist-credentials to false in most cases for the deployment to work correctly.
with:
persist-credentials: false
- name: Build and Deploy
uses: JamesIves/github-pages-deploy-action@releases/v3
with:
ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
BRANCH: gh-pages # The branch the action should deploy to.
FOLDER: build # The folder the action should deploy.
- name: Build and Deploy
uses: JamesIves/github-pages-deploy-action@releases/v3
with:
ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
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.
```yml
on:
push:
branches:
push:
branches:
- master
```
#### Install as a Node Module 📦
If you'd like to use the functionality provided by this action in your own action you can install it using [yarn](https://yarnpkg.com/) by running the following command.
```
yarn add github-pages-deploy-action
```
It can then be imported into your project like so.
```javascript
import run, {
init,
deploy,
generateBranch,
actionInterface
} from "github-pages-deploy-action";
```
Calling the functions directly will require you to pass in an object containing the variables found in the configuration section, you'll also need to provide a `workspace` with a path to your project.
```javascript
import run from "github-pages-deploy-action";
run({
folder: "build",
branch: "gh-pages",
workspace: "src/project/location",
accessToken: process.env["ACCESS_TOKEN"]
});
```
For more information regarding the [action interface please click here](https://github.com/JamesIves/github-pages-deploy-action/blob/dev/src/constants.ts#L7).
## Configuration 📁
The `with` portion of the workflow **must** be configured before the action will work. You can add these in the `with` section found in the examples above. Any `secrets` must be referenced using the bracket syntax and stored in the GitHub repositories `Settings/Secrets` menu. You can learn more about setting environment variables with GitHub actions [here](https://help.github.com/en/articles/workflow-syntax-for-github-actions#jobsjob_idstepsenv).
@ -48,30 +83,33 @@ The `with` portion of the workflow **must** be configured before the action will
One of the following deployment options must be configured.
| Key | Value Information | Type | Required |
| ------------- | ------------- | ------------- | ------------- |
| `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 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** |
| `GITHUB_TOKEN` | In order for GitHub to trigger the rebuild of your page you must provide the action with the repositories provided GitHub token. This can be referenced in the workflow `yml` file by using `${{ secrets.GITHUB_TOKEN }}`. **Please note there is currently an issue affecting the use of this token which makes it so it only works with private repositories, [you can learn more here](https://github.com/JamesIves/github-pages-deploy-action/issues/5)**. | `secrets / with` | **Yes** |
| Key | Value Information | Type | Required |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | -------- |
| `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 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** |
| `GITHUB_TOKEN` | In order for GitHub to trigger the rebuild of your page you must provide the action with the repositories provided GitHub token. This can be referenced in the workflow `yml` file by using `${{ secrets.GITHUB_TOKEN }}`. **Please note there is currently an issue affecting the use of this token which makes it so it only works with private repositories, [you can learn more here](https://github.com/JamesIves/github-pages-deploy-action/issues/5)**. | `secrets / with` | **Yes** |
In addition to the deployment options you must also configure the following.
| 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. **Folder paths cannot have a leading `/` or `./`**. If you wish to deploy the root directory you can place a `.` here. | `with` | **Yes** |
| 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. **Folder paths cannot have a leading `/` or `./`**. If you wish to deploy the root directory you can place a `.` here. | `with` | **Yes** |
#### Optional Choices
| Key | Value Information | Type | Required |
| ------------- | ------------- | ------------- | ------------- |
| `GIT_CONFIG_NAME` | 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. | `with` | **No** |
| `GIT_CONFIG_EMAIL` | 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. | `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 can be toggled on by setting it to `true`. | `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** |
| Key | Value Information | Type | Required |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | -------- |
| `GIT_CONFIG_NAME` | 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. | `with` | **No** |
| `GIT_CONFIG_EMAIL` | 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. | `with` | **No** |
| `REPOSITORY_PATH` | Allows you to speicfy a different repository path so long as you have permissions to push to it. This shoul be formatted like so: `JamesIves/github-pages-deploy-action`. | `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 can be toggled on by setting it to `true`. | `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** |
| `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 neccersary to set this variable if you're using the node module. | `with` | **No** |
| `DEBUG` | By default the git commands are hidden from the log. If you'd like to turn them on you can toggle this to `true`. **If you're using this action in your own project as a node module via yarn or npm you may expose your secrets if you toggle this on in a production environment**. | `with` | **No** |
With the action correctly configured you should see the workflow trigger the deployment under the configured conditions.
@ -112,21 +150,20 @@ on:
push:
branches:
- master
jobs:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
with:
persist-credentials: false
- name: Install
run: |
npm install
npm run-script build
- name: Install SSH Client
uses: webfactory/ssh-agent@v0.2.0 # This step installs the ssh client into the workflow run. There's many options available for this on the action marketplace.
with:
@ -135,16 +172,17 @@ jobs:
- name: Build and Deploy Repo
uses: JamesIves/github-pages-deploy-action@releases/v3-test
with:
BASE_BRANCH: master
BASE_BRANCH: master
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.
```
</p>
</details>
------
---
### Operating System Support 💿
@ -156,7 +194,7 @@ jobs:
runs-on: ubuntu-latest
```
If you're using an operating system such as [Windows](https://www.microsoft.com/en-us/windows/) you can workaround this using [artifacts](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/persisting-workflow-data-using-artifacts). In your workflow configuration you can utilize the `actions/upload-artifact` and `actions/download-artifact` actions to move your project built on a Windows job to a secondary job that will handle the deployment.
If you're using an operating system such as [Windows](https://www.microsoft.com/en-us/windows/) you can workaround this using [artifacts](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/persisting-workflow-data-using-artifacts). In your workflow configuration you can utilize the `actions/upload-artifact` and `actions/download-artifact` actions to move your project built on a Windows job to a secondary job that will handle the deployment.
<details><summary>You can view an example of this pattern here.</summary>
<p>
@ -172,18 +210,18 @@ jobs:
uses: actions/checkout@v2
with:
persist-credentials: false
- name: Install # The project is built using npm and placed in the 'build' folder.
run: |
npm install
npm run-script build
- name: Upload Artifacts # The project is then uploaded as an artifact named 'site'.
uses: actions/upload-artifact@v1
with:
name: site
path: build
deploy:
needs: [build] # The second job must depend on the first one to complete before running, and uses ubuntu-latest instead of windows.
runs-on: ubuntu-latest
@ -192,7 +230,7 @@ jobs:
uses: actions/checkout@v2
with:
persist-credentials: false
- name: Download Artifacts # The built project is downloaded into the 'site' folder.
uses: actions/download-artifact@v1
with:
@ -203,14 +241,15 @@ jobs:
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.
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.
```
</p>
</details>
---
### Using a Container 📦
### Using a Container 🚢
If you use a [container](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idcontainer) in your workflow you may need to run an additional step to install `rsync` as this action depends on it. You can view an example of this below.
@ -218,7 +257,7 @@ If you use a [container](https://help.github.com/en/actions/automating-your-work
- name: Install rsync
run: |
apt-get update && apt-get install -y rsync
- name: Deploy
uses: JamesIves/github-pages-deploy-action@releases/v3
```

View File

@ -12,6 +12,22 @@ describe("execute", () => {
expect(exec).toBeCalledWith("echo Montezuma", [], {
cwd: "./",
silent: true,
listeners: {
stdout: expect.any(Function)
}
});
});
it("should not silence the input when INPUT_DEBUG is defined", async () => {
process.env["DEBUG_DEPLOY_ACTION"] = "yes";
await stdout("hello");
await execute("echo Montezuma", "./");
expect(exec).toBeCalledWith("echo Montezuma", [], {
cwd: "./",
silent: false,
listeners: {
stdout: expect.any(Function)
}

View File

@ -26,7 +26,8 @@ describe("git", () => {
describe("init", () => {
it("should execute commands if a GitHub token is provided", async () => {
Object.assign(action, {
build: "build",
repositoryPath: "JamesIves/github-pages-deploy-action",
folder: "build",
branch: "branch",
gitHubToken: "123",
pusher: {
@ -35,13 +36,14 @@ describe("git", () => {
}
});
await init();
await init(action);
expect(execute).toBeCalledTimes(6);
});
it("should execute commands if an Access Token is provided", async () => {
Object.assign(action, {
build: "build",
repositoryPath: "JamesIves/github-pages-deploy-action",
folder: "build",
branch: "branch",
accessToken: "123",
pusher: {
@ -50,13 +52,14 @@ describe("git", () => {
}
});
await init();
await init(action);
expect(execute).toBeCalledTimes(6);
});
it("should execute commands if SSH is true", async () => {
Object.assign(action, {
build: "build",
repositoryPath: "JamesIves/github-pages-deploy-action",
folder: "build",
branch: "branch",
ssh: true,
pusher: {
@ -65,14 +68,15 @@ describe("git", () => {
}
});
await init();
await init(action);
expect(execute).toBeCalledTimes(6);
});
it("should fail if there is no provided GitHub Token, Access Token or SSH bool", async () => {
Object.assign(action, {
build: "build",
repositoryPath: null,
folder: "build",
branch: "branch",
pusher: {
name: "asd",
@ -83,56 +87,120 @@ describe("git", () => {
ssh: null
});
await init();
expect(setFailed).toBeCalledTimes(1);
expect(execute).toBeCalledTimes(0);
try {
await init(action);
} catch (e) {
expect(execute).toBeCalledTimes(0);
expect(e.message).toMatch(
"There was an error initializing the repository: No deployment token/method was provided. You must provide the action with either a Personal Access Token or the GitHub Token secret in order to deploy. If you wish to use an ssh deploy token then you must set SSH to true. ❌"
);
}
});
it("should fail if there is no folder", async () => {
Object.assign(action, {
repositoryPath: "JamesIves/github-pages-deploy-action",
gitHubToken: "123",
branch: "branch",
pusher: {
name: "asd",
email: "as@cat"
},
folder: null,
ssh: true
});
try {
await init(action);
} catch (e) {
expect(execute).toBeCalledTimes(0);
expect(e.message).toMatch(
"There was an error initializing the repository: You must provide the action with a folder to deploy. ❌"
);
}
});
it("should fail if there is no provided repository path", async () => {
Object.assign(action, {
repositoryPath: null,
folder: "build",
branch: "branch",
pusher: {
name: "asd",
email: "as@cat"
},
gitHubToken: "123",
accessToken: null,
ssh: null
});
try {
await init(action);
} catch (e) {
expect(execute).toBeCalledTimes(0);
expect(e.message).toMatch(
"There was an error initializing the repository: No deployment token/method was provided. You must provide the action with either a Personal Access Token or the GitHub Token secret in order to deploy. If you wish to use an ssh deploy token then you must set SSH to true. "
);
}
});
it("should fail if the build folder begins with a /", async () => {
Object.assign(action, {
accessToken: "123",
repositoryPath: "JamesIves/github-pages-deploy-action",
branch: "branch",
build: "/",
folder: "/",
pusher: {
name: "asd",
email: "as@cat"
}
});
await init();
expect(setFailed).toBeCalledTimes(1);
expect(execute).toBeCalledTimes(0);
try {
await init(action);
} catch (e) {
expect(execute).toBeCalledTimes(0);
expect(e.message).toMatch(
"There was an error initializing the repository: Incorrectly formatted build folder. The deployment folder cannot be prefixed with '/' or './'. Instead reference the folder name directly. ❌"
);
}
});
it("should fail if the build folder begins with a ./", async () => {
Object.assign(action, {
accessToken: "123",
branch: "branch",
build: "./",
folder: "./",
pusher: {
name: "asd",
email: "as@cat"
}
});
await init();
expect(setFailed).toBeCalledTimes(1);
expect(execute).toBeCalledTimes(0);
try {
await init(action);
} catch (e) {
expect(execute).toBeCalledTimes(0);
expect(e.message).toMatch(
"There was an error initializing the repository: Incorrectly formatted build folder. The deployment folder cannot be prefixed with '/' or './'. Instead reference the folder name directly. ❌"
);
}
});
it("should not fail if root is used", async () => {
Object.assign(action, {
repositoryPath: "JamesIves/github-pages-deploy-action",
accessToken: "123",
branch: "branch",
build: ".",
folder: ".",
root: ".",
pusher: {
name: "asd",
email: "as@cat"
}
});
await init();
await init(action);
expect(execute).toBeCalledTimes(6);
});
@ -143,14 +211,14 @@ describe("git", () => {
Object.assign(action, {
accessToken: "123",
branch: "branch",
build: ".",
folder: ".",
pusher: {
name: "asd",
email: "as@cat"
}
});
await generateBranch();
await generateBranch(action);
expect(execute).toBeCalledTimes(6);
});
@ -158,16 +226,20 @@ describe("git", () => {
Object.assign(action, {
accessToken: "123",
branch: null,
build: ".",
folder: ".",
pusher: {
name: "asd",
email: "as@cat"
}
});
await generateBranch();
expect(execute).toBeCalledTimes(0);
expect(setFailed).toBeCalledTimes(1);
try {
await generateBranch(action);
} catch (e) {
expect(e.message).toMatch(
"There was an error creating the deployment branch: Branch is required. ❌"
);
}
});
});
@ -176,23 +248,62 @@ describe("git", () => {
Object.assign(action, {
accessToken: "123",
branch: "branch",
build: ".",
folder: ".",
pusher: {
name: "asd",
email: "as@cat"
}
});
const call = await switchToBaseBranch();
await switchToBaseBranch(action);
expect(execute).toBeCalledTimes(1);
expect(call).toBe("Switched to the base branch...");
});
it("should execute one command if using custom baseBranch", async () => {
Object.assign(action, {
baseBranch: "123",
accessToken: "123",
branch: "branch",
folder: ".",
pusher: {
name: "asd",
email: "as@cat"
}
});
await switchToBaseBranch(action);
expect(execute).toBeCalledTimes(1);
});
it("should fail if one of the required parameters is not available", async () => {
Object.assign(action, {
baseBranch: "123",
accessToken: null,
gitHubToken: null,
ssh: null,
branch: "branch",
folder: null,
pusher: {
name: "asd",
email: "as@cat"
}
});
try {
await switchToBaseBranch(action);
} catch (e) {
expect(execute).toBeCalledTimes(0);
expect(e.message).toMatch(
"There was an error switching to the base branch: No deployment token/method was provided. You must provide the action with either a Personal Access Token or the GitHub Token secret in order to deploy. If you wish to use an ssh deploy token then you must set SSH to true. ❌"
);
}
});
});
describe("deploy", () => {
it("should execute commands", async () => {
Object.assign(action, {
build: "build",
folder: "build",
branch: "branch",
gitHubToken: "123",
pusher: {
@ -201,16 +312,16 @@ describe("git", () => {
}
});
const call = await deploy();
await deploy(action);
// Includes the call to generateBranch
expect(execute).toBeCalledTimes(12);
expect(call).toBe("Commit step complete...");
});
it("should execute commands with clean options", async () => {
it("should execute commands with clean options, ommits sha commit message", async () => {
process.env.GITHUB_SHA = "";
Object.assign(action, {
build: "build",
folder: "build",
branch: "branch",
gitHubToken: "123",
pusher: {
@ -221,16 +332,34 @@ describe("git", () => {
cleanExclude: '["cat", "montezuma"]'
});
const call = await deploy();
await deploy(action);
// Includes the call to generateBranch
expect(execute).toBeCalledTimes(12);
});
it("should execute commands with clean options stored as an array instead", async () => {
Object.assign(action, {
folder: "build",
branch: "branch",
gitHubToken: "123",
pusher: {
name: "asd",
email: "as@cat"
},
clean: true,
cleanExclude: ["cat", "montezuma"]
});
await deploy(action);
// Includes the call to generateBranch
expect(execute).toBeCalledTimes(12);
expect(call).toBe("Commit step complete...");
});
it("should gracefully handle incorrectly formatted clean exclude items", async () => {
Object.assign(action, {
build: ".",
folder: ".",
branch: "branch",
gitHubToken: "123",
pusher: {},
@ -241,15 +370,14 @@ describe("git", () => {
cleanExclude: '["cat, "montezuma"]' // There is a syntax errror in the string.
});
const call = await deploy();
await deploy(action);
expect(execute).toBeCalledTimes(12);
expect(call).toBe("Commit step complete...");
});
it("should stop early if there is nothing to commit", async () => {
Object.assign(action, {
build: "build",
folder: "build",
branch: "branch",
gitHubToken: "123",
pusher: {
@ -259,8 +387,32 @@ describe("git", () => {
isTest: false // Setting this env variable to false means there will never be anything to commit and the action will exit early.
});
await deploy();
expect(execute).toBeCalledTimes(12);
await deploy(action);
expect(execute).toBeCalledTimes(13);
});
it("should throw an error if one of the required parameters is not available", async () => {
Object.assign(action, {
folder: "build",
branch: "branch",
ssh: null,
accessToken: null,
gitHubToken: null,
pusher: {
name: "asd",
email: "as@cat"
},
isTest: false // Setting this env variable to false means there will never be anything to commit and the action will exit early.
});
try {
await deploy(action);
} catch (e) {
expect(execute).toBeCalledTimes(1);
expect(e.message).toMatch(
"The deploy step encountered an error: No deployment token/method was provided. You must provide the action with either a Personal Access Token or the GitHub Token secret in order to deploy. If you wish to use an ssh deploy token then you must set SSH to true. ❌"
);
}
});
});
});

View File

@ -1,9 +1,11 @@
// Initial env variable setup for tests.
process.env["INPUT_FOLDER"] = "build";
process.env["GITHUB_SHA"] = "123";
process.env["INPUT_DEBUG"] = "debug";
import "../src/main";
import { action } from "../src/constants";
import main from "../src/main";
import run from "../src/lib";
import { execute } from "../src/execute";
import { setFailed } from "@actions/core";
@ -15,7 +17,8 @@ jest.mock("../src/execute", () => ({
jest.mock("@actions/core", () => ({
setFailed: jest.fn(),
getInput: jest.fn()
getInput: jest.fn(),
exportVariable: jest.fn()
}));
describe("main", () => {
@ -25,22 +28,24 @@ describe("main", () => {
it("should run through the commands", async () => {
Object.assign(action, {
build: "build",
repositoryPath: "JamesIves/github-pages-deploy-action",
folder: "build",
branch: "branch",
gitHubToken: "123",
pusher: {
name: "asd",
email: "as@cat"
},
isTest: true
isTest: false,
debug: true
});
await main();
expect(execute).toBeCalledTimes(30);
await run(action);
expect(execute).toBeCalledTimes(19);
});
it("should throw if an error is encountered", async () => {
Object.assign(action, {
build: "build",
folder: "build",
branch: "branch",
baseBranch: "master",
gitHubToken: null,
@ -52,8 +57,8 @@ describe("main", () => {
},
isTest: true
});
await main();
expect(execute).toBeCalledTimes(12);
await run(action);
expect(execute).toBeCalledTimes(0);
expect(setFailed).toBeCalledTimes(1);
});
});

View File

@ -1,4 +1,9 @@
import { isNullOrUndefined } from "../src/util";
import {
isNullOrUndefined,
generateTokenType,
generateRepositoryPath,
suppressSensitiveInformation
} from "../src/util";
describe("util", () => {
describe("isNullOrUndefined", () => {
@ -17,4 +22,150 @@ describe("util", () => {
expect(isNullOrUndefined(value)).toBeFalsy();
});
});
describe("generateTokenType", () => {
it("should return ssh if ssh is provided", async () => {
const action = {
branch: "123",
root: ".",
workspace: "src/",
folder: "build",
gitHubToken: null,
accessToken: null,
ssh: true
};
expect(generateTokenType(action)).toEqual("SSH Deploy Key");
});
it("should return access token if access token is provided", async () => {
const action = {
branch: "123",
root: ".",
workspace: "src/",
folder: "build",
gitHubToken: null,
accessToken: "123",
ssh: null
};
expect(generateTokenType(action)).toEqual("Access Token");
});
it("should return github token if github token is provided", async () => {
const action = {
branch: "123",
root: ".",
workspace: "src/",
folder: "build",
gitHubToken: "123",
accessToken: null,
ssh: null
};
expect(generateTokenType(action)).toEqual("GitHub Token");
});
it("should return ... if no token is provided", async () => {
const action = {
branch: "123",
root: ".",
workspace: "src/",
folder: "build",
gitHubToken: null,
accessToken: null,
ssh: null
};
expect(generateTokenType(action)).toEqual("...");
});
});
describe("generateRepositoryPath", () => {
it("should return ssh if ssh is provided", async () => {
const action = {
gitHubRepository: "JamesIves/github-pages-deploy-action",
branch: "123",
root: ".",
workspace: "src/",
folder: "build",
gitHubToken: null,
accessToken: null,
ssh: true
};
expect(generateRepositoryPath(action)).toEqual(
"git@github.com:JamesIves/github-pages-deploy-action"
);
});
it("should return https if access token is provided", async () => {
const action = {
gitHubRepository: "JamesIves/github-pages-deploy-action",
branch: "123",
root: ".",
workspace: "src/",
folder: "build",
gitHubToken: null,
accessToken: "123",
ssh: null
};
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 () => {
const action = {
gitHubRepository: "JamesIves/github-pages-deploy-action",
branch: "123",
root: ".",
workspace: "src/",
folder: "build",
gitHubToken: "123",
accessToken: null,
ssh: null
};
expect(generateRepositoryPath(action)).toEqual(
"https://x-access-token:123@github.com/JamesIves/github-pages-deploy-action.git"
);
});
describe("suppressSensitiveInformation", () => {
it("should replace any sensitive information with ***", async () => {
const action = {
gitHubRepository: "JamesIves/github-pages-deploy-action",
repositoryPath:
"https://x-access-token:supersecret999%%%@github.com/anothersecret123333",
branch: "123",
root: ".",
workspace: "src/",
folder: "build",
accessToken: "supersecret999%%%",
gitHubToken: "anothersecret123333"
};
const string = `This is an error message! It contains ${action.accessToken} and ${action.gitHubToken} and ${action.repositoryPath}`;
expect(suppressSensitiveInformation(string, action)).toBe(
"This is an error message! It contains *** and *** and ***"
);
});
it("should not suppress information when in debug mode", async () => {
const action = {
gitHubRepository: "JamesIves/github-pages-deploy-action",
repositoryPath:
"https://x-access-token:supersecret999%%%@github.com/anothersecret123333",
branch: "123",
root: ".",
workspace: "src/",
folder: "build",
accessToken: "supersecret999%%%",
gitHubToken: "anothersecret123333"
};
process.env["INPUT_DEBUG"] = "true";
const string = `This is an error message! It contains ${action.accessToken} and ${action.gitHubToken} 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"
);
});
});
});
});

View File

@ -1,2 +0,0 @@
"use strict";
process.env.UNIT_TEST = "true";

43
lib/constants.d.ts vendored Normal file
View File

@ -0,0 +1,43 @@
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. */
clean?: string | boolean;
/** If you need to use CLEAN but you'd like to preserve certain files or folders you can use this option. */
cleanExclude?: string | Array<string>;
/** If you need to customize the commit message for an integration you can do so. */
commitMessage?: string;
/** Unhides the Git commands from the function terminal. */
debug?: boolean | 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. */
folder: string;
/** The repository path, for example JamesIves/github-pages-deploy-action */
gitHubRepository?: string;
/** GitHub deployment token. */
gitHubToken?: string | null;
/** Determines if the action is running in test mode or not. */
isTest?: string | undefined | null;
/** The git config name. */
name?: string;
/** The fully qualified repositpory path, this gets auto generated if gitHubRepository is provided. */
repositoryPath?: string;
/** The root directory where your project lives. */
root?: string;
/** Set to true if you're using an ssh client in your build step. */
ssh?: 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;
/** The token type, ie ssh/github token/access token, this gets automatically generated. */
tokenType?: string;
/** The folder where your deployment project lives. */
workspace: string;
}
export declare const action: actionInterface;

View File

@ -11,18 +11,16 @@ const core_1 = require("@actions/core");
const github = __importStar(require("@actions/github"));
const util_1 = require("./util");
const { pusher, repository } = github.context.payload;
exports.workspace = process.env.GITHUB_WORKSPACE;
exports.folder = core_1.getInput("FOLDER", { required: true });
exports.root = ".";
// Required action data.
/* Required action data that gets initialized when running within the GitHub Actions environment. */
exports.action = {
accessToken: core_1.getInput("ACCESS_TOKEN"),
baseBranch: core_1.getInput("BASE_BRANCH"),
build: exports.folder,
folder: core_1.getInput("FOLDER"),
branch: core_1.getInput("BRANCH"),
commitMessage: core_1.getInput("COMMIT_MESSAGE"),
clean: core_1.getInput("CLEAN"),
cleanExclude: core_1.getInput("CLEAN_EXCLUDE"),
debug: core_1.getInput("DEBUG"),
defaultBranch: process.env.GITHUB_SHA ? process.env.GITHUB_SHA : "master",
isTest: process.env.UNIT_TEST,
ssh: core_1.getInput("SSH"),
@ -32,9 +30,11 @@ exports.action = {
? pusher.email
: `${process.env.GITHUB_ACTOR ||
"github-pages-deploy-action"}@users.noreply.github.com`,
gitHubRepository: repository && repository.full_name
? repository.full_name
: process.env.GITHUB_REPOSITORY,
gitHubRepository: !util_1.isNullOrUndefined(core_1.getInput("REPOSITORY_PATH"))
? core_1.getInput("REPOSITORY_PATH")
: repository && repository.full_name
? repository.full_name
: process.env.GITHUB_REPOSITORY,
gitHubToken: core_1.getInput("GITHUB_TOKEN"),
name: !util_1.isNullOrUndefined(core_1.getInput("GIT_CONFIG_NAME"))
? core_1.getInput("GIT_CONFIG_NAME")
@ -43,18 +43,7 @@ exports.action = {
: process.env.GITHUB_ACTOR
? process.env.GITHUB_ACTOR
: "GitHub Pages Deploy Action",
targetFolder: core_1.getInput("TARGET_FOLDER")
targetFolder: core_1.getInput("TARGET_FOLDER"),
workspace: process.env.GITHUB_WORKSPACE || "",
root: "."
};
// Token Types
exports.tokenType = exports.action.ssh
? "SSH Deploy Key"
: exports.action.accessToken
? "Access Token"
: exports.action.gitHubToken
? "GitHub Token"
: "...";
// Repository path used for commits/pushes.
exports.repositoryPath = exports.action.ssh
? `git@github.com:${exports.action.gitHubRepository}`
: `https://${exports.action.accessToken ||
`x-access-token:${exports.action.gitHubToken}`}@github.com/${exports.action.gitHubRepository}.git`;

8
lib/execute.d.ts vendored Normal file
View File

@ -0,0 +1,8 @@
/** Wrapper around the GitHub toolkit exec command which returns the output.
* Also allows you to easily toggle the current working directory.
* @param cmd = The command to execute.
* @param cwd - The current working directory.
* @returns - The output from the command.
*/
export declare function execute(cmd: string, cwd: string): Promise<any>;
export declare function stdout(data: any): void;

View File

@ -10,18 +10,19 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
};
Object.defineProperty(exports, "__esModule", { value: true });
const exec_1 = require("@actions/exec");
// Stores the output from execute.
let output;
/** Wrapper around the GitHub toolkit exec command which returns the output.
* Also allows you to easily toggle the current working directory.
* @param {String} cmd = The command to execute.
* @param {String} cwd - The current working directory.
* @returns {Promise} - The output from the command.
* @param cmd = The command to execute.
* @param cwd - The current working directory.
* @returns - The output from the command.
*/
function execute(cmd, cwd) {
return __awaiter(this, void 0, void 0, function* () {
output = "";
yield exec_1.exec(cmd, [], {
// Silences the input unless the INPUT_DEBUG flag is set.
silent: process.env.DEBUG_DEPLOY_ACTION ? false : true,
cwd,
listeners: {
stdout

5
lib/git.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
import { actionInterface } from "./constants";
export declare function init(action: actionInterface): Promise<void | Error>;
export declare function switchToBaseBranch(action: actionInterface): Promise<void>;
export declare function generateBranch(action: actionInterface): Promise<void>;
export declare function deploy(action: actionInterface): Promise<void>;

View File

@ -9,136 +9,130 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const core_1 = require("@actions/core");
const constants_1 = require("./constants");
const execute_1 = require("./execute");
const util_1 = require("./util");
/** Generates the branch if it doesn't exist on the remote.
* @returns {Promise}
*/
function init() {
/* Generates the branch if it doesn't exist on the remote. */
function init(action) {
return __awaiter(this, void 0, void 0, function* () {
try {
if (util_1.isNullOrUndefined(constants_1.action.accessToken) &&
util_1.isNullOrUndefined(constants_1.action.gitHubToken) &&
util_1.isNullOrUndefined(constants_1.action.ssh)) {
core_1.setFailed("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.");
throw Error("No deployment token/method was provided. You must provide the action with either a Personal Access Token or the GitHub Token secret in order to deploy. If you wish to use an ssh deploy token then you must set SSH to true.");
}
if (constants_1.action.build.startsWith("/") || constants_1.action.build.startsWith("./")) {
core_1.setFailed(`The deployment folder cannot be prefixed with '/' or './'. Instead reference the folder name directly.`);
throw Error("Incorrectly formatted build folder. The deployment folder cannot be prefixed with '/' or './'. Instead reference the folder name directly.");
}
console.log(`Deploying using ${constants_1.tokenType}... 🔑`);
yield execute_1.execute(`git init`, constants_1.workspace);
yield execute_1.execute(`git config user.name ${constants_1.action.name}`, constants_1.workspace);
yield execute_1.execute(`git config user.email ${constants_1.action.email}`, constants_1.workspace);
yield execute_1.execute(`git remote rm origin`, constants_1.workspace);
yield execute_1.execute(`git remote add origin ${constants_1.repositoryPath}`, constants_1.workspace);
yield execute_1.execute(`git fetch`, constants_1.workspace);
util_1.hasRequiredParameters(action);
console.log(`Deploying using ${action.tokenType}... 🔑`);
console.log("Configuring git...");
yield execute_1.execute(`git init`, action.workspace);
yield execute_1.execute(`git config user.name "${action.name}"`, action.workspace);
yield execute_1.execute(`git config user.email "${action.email}"`, action.workspace);
yield execute_1.execute(`git remote rm origin`, action.workspace);
yield execute_1.execute(`git remote add origin ${action.repositoryPath}`, action.workspace);
yield execute_1.execute(`git fetch`, action.workspace);
console.log("Git configured... 🔧");
}
catch (error) {
console.log(`There was an error initializing the repository: ${error}`);
}
finally {
return Promise.resolve("Initialization step complete...");
throw new Error(`There was an error initializing the repository: ${util_1.suppressSensitiveInformation(error.message, action)}`);
}
});
}
exports.init = init;
/** Switches to the base branch.
* @returns {Promise}
*/
function switchToBaseBranch() {
/* Switches to the base branch. */
function switchToBaseBranch(action) {
return __awaiter(this, void 0, void 0, function* () {
yield execute_1.execute(`git checkout --progress --force ${constants_1.action.baseBranch ? constants_1.action.baseBranch : constants_1.action.defaultBranch}`, constants_1.workspace);
return Promise.resolve("Switched to the base branch...");
try {
util_1.hasRequiredParameters(action);
yield execute_1.execute(`git checkout --progress --force ${action.baseBranch ? action.baseBranch : action.defaultBranch}`, action.workspace);
}
catch (error) {
throw new Error(`There was an error switching to the base branch: ${util_1.suppressSensitiveInformation(error.message, action)}`);
}
});
}
exports.switchToBaseBranch = switchToBaseBranch;
/** Generates the branch if it doesn't exist on the remote.
* @returns {Promise}
*/
function generateBranch() {
/* Generates the branch if it doesn't exist on the remote. */
function generateBranch(action) {
return __awaiter(this, void 0, void 0, function* () {
try {
if (util_1.isNullOrUndefined(constants_1.action.branch)) {
throw Error("Branch is required.");
}
console.log(`Creating ${constants_1.action.branch} branch... 🔧`);
yield switchToBaseBranch();
yield execute_1.execute(`git checkout --orphan ${constants_1.action.branch}`, constants_1.workspace);
yield execute_1.execute(`git reset --hard`, constants_1.workspace);
yield execute_1.execute(`git commit --allow-empty -m "Initial ${constants_1.action.branch} commit."`, constants_1.workspace);
yield execute_1.execute(`git push ${constants_1.repositoryPath} ${constants_1.action.branch}`, constants_1.workspace);
yield execute_1.execute(`git fetch`, constants_1.workspace);
util_1.hasRequiredParameters(action);
console.log(`Creating the ${action.branch} branch...`);
yield switchToBaseBranch(action);
yield execute_1.execute(`git checkout --orphan ${action.branch}`, action.workspace);
yield execute_1.execute(`git reset --hard`, action.workspace);
yield execute_1.execute(`git commit --allow-empty -m "Initial ${action.branch} commit."`, action.workspace);
yield execute_1.execute(`git push ${action.repositoryPath} ${action.branch}`, action.workspace);
yield execute_1.execute(`git fetch`, action.workspace);
console.log(`Created the ${action.branch} branch... 🔧`);
}
catch (error) {
core_1.setFailed(`There was an error creating the deployment branch: ${error}`);
}
finally {
return Promise.resolve("Deployment branch creation step complete... ✅");
throw new Error(`There was an error creating the deployment branch: ${util_1.suppressSensitiveInformation(error.message, action)}`);
}
});
}
exports.generateBranch = generateBranch;
/** Runs the necessary steps to make the deployment.
* @returns {Promise}
*/
function deploy() {
/* Runs the necessary steps to make the deployment. */
function deploy(action) {
return __awaiter(this, void 0, void 0, function* () {
const temporaryDeploymentDirectory = "gh-action-temp-deployment-folder";
const temporaryDeploymentBranch = "gh-action-temp-deployment-branch";
/*
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 = yield execute_1.execute(`git ls-remote --heads ${constants_1.repositoryPath} ${constants_1.action.branch} | wc -l`, constants_1.workspace);
if (!branchExists && !constants_1.action.isTest) {
console.log("Deployment branch does not exist. Creating....");
yield generateBranch();
}
// Checks out the base branch to begin the deployment process.
yield switchToBaseBranch();
yield execute_1.execute(`git fetch ${constants_1.repositoryPath}`, constants_1.workspace);
yield execute_1.execute(`git worktree add --checkout ${temporaryDeploymentDirectory} origin/${constants_1.action.branch}`, constants_1.workspace);
// Ensures that items that need to be excluded from the clean job get parsed.
let excludes = "";
if (constants_1.action.clean && constants_1.action.cleanExclude) {
try {
const excludedItems = JSON.parse(constants_1.action.cleanExclude);
excludedItems.forEach((item) => (excludes += `--exclude ${item} `));
console.log("Starting to commit changes...");
try {
util_1.hasRequiredParameters(action);
/*
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 = yield execute_1.execute(`git ls-remote --heads ${action.repositoryPath} ${action.branch} | wc -l`, action.workspace);
if (!branchExists && !action.isTest) {
yield generateBranch(action);
}
catch (_a) {
console.log("There was an error parsing your CLEAN_EXCLUDE items. Please refer to the README for more details. ❌");
// Checks out the base branch to begin the deployment process.
yield switchToBaseBranch(action);
yield execute_1.execute(`git fetch ${action.repositoryPath}`, action.workspace);
yield execute_1.execute(`git worktree add --checkout ${temporaryDeploymentDirectory} origin/${action.branch}`, action.workspace);
// 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;
excludedItems.forEach((item) => (excludes += `--exclude ${item} `));
}
catch (_a) {
console.log("There was an error parsing your CLEAN_EXCLUDE items. Please refer to the README for more details. ❌");
}
}
/*
Pushes all of the build files into the deployment directory.
Allows the user to specify the root if '.' is provided.
rsync is used to prevent file duplication. */
yield execute_1.execute(`rsync -q -av --progress ${action.folder}/. ${action.targetFolder
? `${temporaryDeploymentDirectory}/${action.targetFolder}`
: temporaryDeploymentDirectory} ${action.clean
? `--delete ${excludes} --exclude CNAME --exclude .nojekyll`
: ""} --exclude .ssh --exclude .git --exclude .github ${action.folder === action.root
? `--exclude ${temporaryDeploymentDirectory}`
: ""}`, action.workspace);
const hasFilesToCommit = yield execute_1.execute(`git status --porcelain`, `${action.workspace}/${temporaryDeploymentDirectory}`);
if (!hasFilesToCommit && !action.isTest) {
console.log("There is nothing to commit. Exiting early... 📭");
return;
}
// Commits to GitHub.
yield execute_1.execute(`git add --all .`, `${action.workspace}/${temporaryDeploymentDirectory}`);
yield execute_1.execute(`git checkout -b ${temporaryDeploymentBranch}`, `${action.workspace}/${temporaryDeploymentDirectory}`);
yield execute_1.execute(`git commit -m "${!util_1.isNullOrUndefined(action.commitMessage)
? action.commitMessage
: `Deploying to ${action.branch} from ${action.baseBranch}`} ${process.env.GITHUB_SHA ? `- ${process.env.GITHUB_SHA}` : ""} 🚀" --quiet`, `${action.workspace}/${temporaryDeploymentDirectory}`);
yield execute_1.execute(`git push --force ${action.repositoryPath} ${temporaryDeploymentBranch}:${action.branch}`, `${action.workspace}/${temporaryDeploymentDirectory}`);
console.log(`Changes committed to the ${action.branch} branch... 📦`);
// Cleans up temporary files/folders and restores the git state.
console.log("Running post deployment cleanup jobs...");
yield execute_1.execute(`git checkout --progress --force ${action.defaultBranch}`, action.workspace);
}
/*
Pushes all of the build files into the deployment directory.
Allows the user to specify the root if '.' is provided.
rysync is used to prevent file duplication. */
yield execute_1.execute(`rsync -q -av --progress ${constants_1.action.build}/. ${constants_1.action.targetFolder
? `${temporaryDeploymentDirectory}/${constants_1.action.targetFolder}`
: temporaryDeploymentDirectory} ${constants_1.action.clean
? `--delete ${excludes} --exclude CNAME --exclude .nojekyll`
: ""} --exclude .ssh --exclude .git --exclude .github ${constants_1.action.build === constants_1.root ? `--exclude ${temporaryDeploymentDirectory}` : ""}`, constants_1.workspace);
const hasFilesToCommit = yield execute_1.execute(`git status --porcelain`, temporaryDeploymentDirectory);
if (!hasFilesToCommit && !constants_1.action.isTest) {
console.log("There is nothing to commit. Exiting... ✅");
return Promise.resolve();
catch (error) {
throw new Error(`The deploy step encountered an error: ${util_1.suppressSensitiveInformation(error.message, action)}`);
}
finally {
// Ensures the deployment directory is safely removed.
yield execute_1.execute(`rm -rf ${temporaryDeploymentDirectory}`, action.workspace);
}
// Commits to GitHub.
yield execute_1.execute(`git add --all .`, temporaryDeploymentDirectory);
yield execute_1.execute(`git checkout -b ${temporaryDeploymentBranch}`, temporaryDeploymentDirectory);
yield execute_1.execute(`git commit -m "${!util_1.isNullOrUndefined(constants_1.action.commitMessage)
? constants_1.action.commitMessage
: `Deploying to ${constants_1.action.branch} from ${constants_1.action.baseBranch}`} - ${process.env.GITHUB_SHA} 🚀" --quiet`, temporaryDeploymentDirectory);
yield execute_1.execute(`git push --force ${constants_1.repositoryPath} ${temporaryDeploymentBranch}:${constants_1.action.branch}`, temporaryDeploymentDirectory);
// Cleans up temporary files/folders and restores the git state.
console.log("Running post deployment cleanup jobs... 🔧");
yield execute_1.execute(`rm -rf ${temporaryDeploymentDirectory}`, constants_1.workspace);
yield execute_1.execute(`git checkout --progress --force ${constants_1.action.defaultBranch}`, constants_1.workspace);
return Promise.resolve("Commit step complete...");
});
}
exports.deploy = deploy;

5
lib/lib.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
import { actionInterface } from "./constants";
import { deploy, generateBranch, init } from "./git";
/** Initializes and runs the action. */
export default function run(configuration: actionInterface): Promise<void>;
export { init, deploy, generateBranch, actionInterface };

47
lib/lib.js Normal file
View File

@ -0,0 +1,47 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const core_1 = require("@actions/core");
const constants_1 = require("./constants");
const git_1 = require("./git");
exports.deploy = git_1.deploy;
exports.generateBranch = git_1.generateBranch;
exports.init = git_1.init;
const util_1 = require("./util");
/** Initializes and runs the action. */
function run(configuration) {
return __awaiter(this, void 0, void 0, function* () {
let errorState = false;
try {
console.log("Checking configuration and starting deployment...🚦");
const settings = Object.assign(Object.assign({}, constants_1.action), configuration);
// Defines the repository paths and token types.
settings.repositoryPath = util_1.generateRepositoryPath(settings);
settings.tokenType = util_1.generateTokenType(settings);
if (settings.debug) {
// Sets the debug flag if passed as an arguement.
core_1.exportVariable("DEBUG_DEPLOY_ACTION", "debug");
}
yield git_1.init(settings);
yield git_1.deploy(settings);
}
catch (error) {
errorState = true;
core_1.setFailed(error.message);
}
finally {
console.log(`${errorState
? "Deployment Failed ❌"
: "Completed Deployment Successfully! ✅"}`);
}
});
}
exports.default = run;

1
lib/main.d.ts vendored Normal file
View File

@ -0,0 +1 @@
export {};

View File

@ -1,34 +1,9 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const core_1 = require("@actions/core");
const git_1 = require("./git");
/** Initializes and runs the action. */
function main() {
return __awaiter(this, void 0, void 0, function* () {
try {
yield git_1.init();
yield git_1.deploy();
}
catch (error) {
/* istanbul ignore next */
console.log("The deployment encountered an error. ❌");
/* istanbul ignore next */
core_1.setFailed(error);
}
finally {
console.log("Completed Deployment ✅");
}
});
}
exports.default = main;
// Init
main();
const constants_1 = require("./constants");
const lib_1 = __importDefault(require("./lib"));
// Runs the action within the GitHub actions environment.
lib_1.default(constants_1.action);

View File

@ -1,44 +0,0 @@
"use strict";
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
result["default"] = mod;
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const core = __importStar(require("@actions/core"));
const github = __importStar(require("@actions/github"));
const { pusher, repository } = github.context.payload;
exports.workspace = process.env.GITHUB_WORKSPACE;
exports.folder = core.getInput("FOLDER", { required: true });
exports.root = ".";
exports.isTest = process.env.UNIT_TEST;
// Required action data.
exports.action = {
accessToken: core.getInput("ACCESS_TOKEN"),
baseBranch: core.getInput("BASE_BRANCH"),
build: exports.folder,
branch: core.getInput("BRANCH"),
commitMessage: core.getInput("COMMIT_MESSAGE"),
clean: core.getInput("CLEAN"),
cleanExclude: core.getInput("CLEAN_EXCLUDE"),
defaultBranch: process.env.GITHUB_SHA ? process.env.GITHUB_SHA : "master",
email: pusher && pusher.email
? pusher.email
: `${process.env.GITHUB_ACTOR ||
"github-pages-deploy-action"}@users.noreply.github.com`,
gitHubRepository: repository && repository.full_name
? repository.full_name
: process.env.GITHUB_REPOSITORY,
gitHubToken: core.getInput("GITHUB_TOKEN"),
name: pusher && pusher.name
? pusher.name
: process.env.GITHUB_ACTOR
? process.env.GITHUB_ACTOR
: "GitHub Pages Deploy Action",
targetFolder: core.getInput("TARGET_FOLDER")
};
// Repository path used for commits/pushes.
exports.repositoryPath = `https://${exports.action.accessToken ||
`x-access-token:${exports.action.gitHubToken}`}@github.com/${exports.action.gitHubRepository}.git`;

View File

@ -1,33 +0,0 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const exec_1 = require("@actions/exec");
/** Wrapper around the GitHub toolkit exec command which returns the output.
* Also allows you to easily toggle the current working directory.
* @param {String} cmd = The command to execute.
* @param {String} cwd - The current working directory.
* @returns {Promise} - The output from the command.
*/
function execute(cmd, cwd) {
return __awaiter(this, void 0, void 0, function* () {
let output = "";
yield exec_1.exec(cmd, [], {
cwd,
listeners: {
stdout: (data) => {
output += data.toString().trim();
}
}
});
return Promise.resolve(output);
});
}
exports.execute = execute;

View File

@ -1,144 +0,0 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
result["default"] = mod;
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const core = __importStar(require("@actions/core"));
const execute_1 = require("./execute");
const util_1 = require("./util");
const constants_1 = require("./constants");
/** Generates the branch if it doesn't exist on the remote.
* @returns {Promise}
*/
function init() {
return __awaiter(this, void 0, void 0, function* () {
try {
if (util_1.isNullOrUndefined(constants_1.action.accessToken) &&
util_1.isNullOrUndefined(constants_1.action.gitHubToken)) {
return core.setFailed("You must provide the action with either a Personal Access Token or the GitHub Token secret in order to deploy.");
}
if (constants_1.action.build.startsWith("/") || constants_1.action.build.startsWith("./")) {
return core.setFailed(`The deployment folder cannot be prefixed with '/' or './'. Instead reference the folder name directly.`);
}
yield execute_1.execute(`git init`, constants_1.workspace);
yield execute_1.execute(`git config user.name ${constants_1.action.name}`, constants_1.workspace);
yield execute_1.execute(`git config user.email ${constants_1.action.email}`, constants_1.workspace);
yield execute_1.execute(`git remote rm origin`, constants_1.workspace);
yield execute_1.execute(`git remote add origin ${constants_1.repositoryPath}`, constants_1.workspace);
yield execute_1.execute(`git fetch`, constants_1.workspace);
}
catch (error) {
core.setFailed(`There was an error initializing the repository: ${error}`);
}
finally {
return Promise.resolve("Initialization step complete...");
}
});
}
exports.init = init;
/** Switches to the base branch.
* @returns {Promise}
*/
function switchToBaseBranch() {
return __awaiter(this, void 0, void 0, function* () {
yield execute_1.execute(`git checkout --progress --force ${constants_1.action.baseBranch ? constants_1.action.baseBranch : constants_1.action.defaultBranch}`, constants_1.workspace);
return Promise.resolve("Switched to the base branch...");
});
}
exports.switchToBaseBranch = switchToBaseBranch;
/** Generates the branch if it doesn't exist on the remote.
* @returns {Promise}
*/
function generateBranch() {
return __awaiter(this, void 0, void 0, function* () {
try {
console.log(`Creating ${constants_1.action.branch} branch... 🔧`);
yield switchToBaseBranch();
yield execute_1.execute(`git checkout --orphan ${constants_1.action.branch}`, constants_1.workspace);
yield execute_1.execute(`git reset --hard`, constants_1.workspace);
yield execute_1.execute(`git commit --allow-empty -m "Initial ${constants_1.action.branch} commit."`, constants_1.workspace);
yield execute_1.execute(`git push ${constants_1.repositoryPath} ${constants_1.action.branch}`, constants_1.workspace);
yield execute_1.execute(`git fetch`, constants_1.workspace);
}
catch (error) {
core.setFailed(`There was an error creating the deployment branch: ${error}`);
}
finally {
return Promise.resolve("Deployment branch creation step complete... ✅");
}
});
}
exports.generateBranch = generateBranch;
/** Runs the necessary steps to make the deployment.
* @returns {Promise}
*/
function deploy() {
return __awaiter(this, void 0, void 0, function* () {
const temporaryDeploymentDirectory = "gh-action-temp-deployment-folder";
const temporaryDeploymentBranch = "gh-action-temp-deployment-branch";
/*
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 = yield execute_1.execute(`git ls-remote --heads ${constants_1.repositoryPath} ${constants_1.action.branch} | wc -l`, constants_1.workspace);
if (!branchExists) {
console.log("Deployment branch does not exist. Creating....");
yield generateBranch();
}
// Checks out the base branch to begin the deployment process.
yield switchToBaseBranch();
yield execute_1.execute(`git fetch ${constants_1.repositoryPath}`, constants_1.workspace);
yield execute_1.execute(`git worktree add --checkout ${temporaryDeploymentDirectory} origin/${constants_1.action.branch}`, constants_1.workspace);
// Ensures that items that need to be excluded from the clean job get parsed.
let excludes = "";
if (constants_1.action.clean && constants_1.action.cleanExclude) {
try {
const excludedItems = JSON.parse(constants_1.action.cleanExclude);
excludedItems.forEach((item) => (excludes += `--exclude ${item} `));
}
catch (_a) {
console.log("There was an error parsing your CLEAN_EXCLUDE items. Please refer to the README for more details. ❌");
}
}
/*
Pushes all of the build files into the deployment directory.
Allows the user to specify the root if '.' is provided.
rysync is used to prevent file duplication. */
yield execute_1.execute(`rsync -q -av --progress ${constants_1.action.build}/. ${constants_1.action.targetFolder
? `${temporaryDeploymentDirectory}/${constants_1.action.targetFolder}`
: temporaryDeploymentDirectory} ${constants_1.action.clean
? `--delete ${excludes} --exclude CNAME --exclude .nojekyll`
: ""} --exclude .git --exclude .github ${constants_1.action.build === constants_1.root ? `--exclude ${temporaryDeploymentDirectory}` : ""}`, constants_1.workspace);
const hasFilesToCommit = yield execute_1.execute(`git status --porcelain`, temporaryDeploymentDirectory);
if (!hasFilesToCommit && !constants_1.isTest) {
console.log("There is nothing to commit. Exiting... ✅");
return Promise.resolve();
}
// Commits to GitHub.
yield execute_1.execute(`git add --all .`, temporaryDeploymentDirectory);
yield execute_1.execute(`git checkout -b ${temporaryDeploymentBranch}`, temporaryDeploymentDirectory);
yield execute_1.execute(`git commit -m "${constants_1.action.commitMessage
? constants_1.action.commitMessage
: `Deploying to ${constants_1.action.branch} from ${constants_1.action.baseBranch}`} ${process.env.GITHUB_SHA} 🚀" --quiet`, temporaryDeploymentDirectory);
yield execute_1.execute(`git push --force ${constants_1.repositoryPath} ${temporaryDeploymentBranch}:${constants_1.action.branch}`, temporaryDeploymentDirectory);
// Cleans up temporary files/folders and restores the git state.
console.log("Running post deployment cleanup jobs... 🔧");
yield execute_1.execute(`rm -rf ${temporaryDeploymentDirectory}`, constants_1.workspace);
yield execute_1.execute(`git checkout --progress --force ${constants_1.action.defaultBranch}`, constants_1.workspace);
return Promise.resolve("Commit step complete...");
});
}
exports.deploy = deploy;

View File

@ -1,36 +0,0 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
result["default"] = mod;
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const core = __importStar(require("@actions/core"));
const git_1 = require("./git");
/** Initializes and runs the action. */
(function () {
return __awaiter(this, void 0, void 0, function* () {
try {
yield git_1.init();
yield git_1.deploy();
}
catch (error) {
console.log("The deployment encountered an error. ❌");
core.setFailed(error.message);
}
finally {
console.log("Completed Deployment ✅");
}
});
})();

View File

@ -1,10 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
/** Utility function that checks to see if a value is undefined or not.
* @param {*} value = The value to check.
* @returns {boolean}
*/
function isNullOrUndefined(value) {
return typeof value === "undefined" || value === null || value === "";
}
exports.isNullOrUndefined = isNullOrUndefined;

6
lib/util.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
import { actionInterface } from "./constants";
export declare const isNullOrUndefined: (value: any) => boolean;
export declare const generateTokenType: (action: actionInterface) => string;
export declare const generateRepositoryPath: (action: actionInterface) => string;
export declare const hasRequiredParameters: (action: actionInterface) => void;
export declare const suppressSensitiveInformation: (str: string, action: actionInterface) => string;

View File

@ -1,10 +1,54 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
/** Utility function that checks to see if a value is undefined or not.
* @param {*} value = The value to check.
* @returns {boolean}
*/
function isNullOrUndefined(value) {
return typeof value === "undefined" || value === null || value === "";
}
exports.isNullOrUndefined = isNullOrUndefined;
const core_1 = require("@actions/core");
/* Utility function that checks to see if a value is undefined or not. */
exports.isNullOrUndefined = (value) => typeof value === "undefined" || value === null || value === "";
/* Generates a token type used for the action. */
exports.generateTokenType = (action) => action.ssh
? "SSH Deploy Key"
: action.accessToken
? "Access Token"
: action.gitHubToken
? "GitHub Token"
: "...";
/* Generates a the repository path used to make the commits. */
exports.generateRepositoryPath = (action) => action.ssh
? `git@github.com:${action.gitHubRepository}`
: `https://${action.accessToken ||
`x-access-token:${action.gitHubToken}`}@github.com/${action.gitHubRepository}.git`;
/* Checks for the required tokens and formatting. Throws an error if any case is matched. */
exports.hasRequiredParameters = (action) => {
if ((exports.isNullOrUndefined(action.accessToken) &&
exports.isNullOrUndefined(action.gitHubToken) &&
exports.isNullOrUndefined(action.ssh)) ||
exports.isNullOrUndefined(action.repositoryPath)) {
throw new Error("No deployment token/method was provided. You must provide the action with either a Personal Access Token or the GitHub Token secret in order to deploy. If you wish to use an ssh deploy token then you must set SSH to true.");
}
if (exports.isNullOrUndefined(action.branch)) {
throw new Error("Branch is required.");
}
if (!action.folder || exports.isNullOrUndefined(action.folder)) {
throw new Error("You must provide the action with a folder to deploy.");
}
if (action.folder.startsWith("/") || action.folder.startsWith("./")) {
throw new Error("Incorrectly formatted build folder. The deployment folder cannot be prefixed with '/' or './'. Instead reference the folder name directly.");
}
};
/* Suppresses sensitive information from being exposed in error messages. */
exports.suppressSensitiveInformation = (str, action) => {
let value = str;
if (core_1.getInput("DEBUG")) {
// Data is unmasked in debug mode.
return value;
}
if (action.accessToken) {
value = value.replace(action.accessToken, "***");
}
if (action.gitHubToken) {
value = value.replace(action.gitHubToken, "***");
}
if (action.repositoryPath) {
value = value.replace(action.repositoryPath, "***");
}
return value;
};

View File

@ -1,10 +1,13 @@
{
"name": "github-pages-deploy-action",
"description": "GitHub action for building a project and deploying it to GitHub pages.",
"version": "3.2.4",
"main": "lib/main.js",
"author": "James Ives <iam@jamesiv.es>",
"version": "3.3.0",
"license": "MIT",
"main": "lib/lib.js",
"types": "lib/lib.d.ts",
"scripts": {
"build": "tsc",
"build": "rm -rf lib && tsc --declaration",
"test": "jest",
"lint": "tslint -p tsconfig.json --project '.' || (echo Project needs formatting)",
"format": "prettier --write './**/*.ts'"
@ -29,8 +32,6 @@
"deploy",
"deployment"
],
"author": "James Ives <iam@jamesiv.es>",
"license": "MIT",
"dependencies": {
"@actions/core": "^1.2.0",
"@actions/exec": "^1.0.2",

View File

@ -4,19 +4,60 @@ import { isNullOrUndefined } from "./util";
const { pusher, repository } = github.context.payload;
export const workspace: any = process.env.GITHUB_WORKSPACE;
export const folder = getInput("FOLDER", { required: true });
export const root = ".";
/* 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 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. */
clean?: string | boolean;
/** If you need to use CLEAN but you'd like to preserve certain files or folders you can use this option. */
cleanExclude?: string | Array<string>;
/** If you need to customize the commit message for an integration you can do so. */
commitMessage?: string;
/** Unhides the Git commands from the function terminal. */
debug?: boolean | 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. */
folder: string;
/** The repository path, for example JamesIves/github-pages-deploy-action */
gitHubRepository?: string;
/** GitHub deployment token. */
gitHubToken?: string | null;
/** Determines if the action is running in test mode or not. */
isTest?: string | undefined | null;
/** The git config name. */
name?: string;
/** The fully qualified repositpory path, this gets auto generated if gitHubRepository is provided. */
repositoryPath?: string;
/** The root directory where your project lives. */
root?: string;
/** Set to true if you're using an ssh client in your build step. */
ssh?: 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;
/** The token type, ie ssh/github token/access token, this gets automatically generated. */
tokenType?: string;
/** The folder where your deployment project lives. */
workspace: string;
}
// Required action data.
export const action = {
/* 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"),
build: folder,
folder: getInput("FOLDER"),
branch: getInput("BRANCH"),
commitMessage: getInput("COMMIT_MESSAGE"),
clean: getInput("CLEAN"),
cleanExclude: getInput("CLEAN_EXCLUDE"),
debug: getInput("DEBUG"),
defaultBranch: process.env.GITHUB_SHA ? process.env.GITHUB_SHA : "master",
isTest: process.env.UNIT_TEST,
ssh: getInput("SSH"),
@ -26,10 +67,11 @@ export const action = {
? pusher.email
: `${process.env.GITHUB_ACTOR ||
"github-pages-deploy-action"}@users.noreply.github.com`,
gitHubRepository:
repository && repository.full_name
? repository.full_name
: process.env.GITHUB_REPOSITORY,
gitHubRepository: !isNullOrUndefined(getInput("REPOSITORY_PATH"))
? getInput("REPOSITORY_PATH")
: repository && repository.full_name
? repository.full_name
: process.env.GITHUB_REPOSITORY,
gitHubToken: getInput("GITHUB_TOKEN"),
name: !isNullOrUndefined(getInput("GIT_CONFIG_NAME"))
? getInput("GIT_CONFIG_NAME")
@ -38,22 +80,7 @@ export const action = {
: process.env.GITHUB_ACTOR
? process.env.GITHUB_ACTOR
: "GitHub Pages Deploy Action",
targetFolder: getInput("TARGET_FOLDER")
targetFolder: getInput("TARGET_FOLDER"),
workspace: process.env.GITHUB_WORKSPACE || "",
root: "."
};
// Token Types
export const tokenType = action.ssh
? "SSH Deploy Key"
: action.accessToken
? "Access Token"
: action.gitHubToken
? "GitHub Token"
: "...";
// Repository path used for commits/pushes.
export const repositoryPath = action.ssh
? `git@github.com:${action.gitHubRepository}`
: `https://${action.accessToken ||
`x-access-token:${action.gitHubToken}`}@github.com/${
action.gitHubRepository
}.git`;

View File

@ -1,18 +1,19 @@
import { exec } from "@actions/exec";
// Stores the output from execute.
let output: string;
/** Wrapper around the GitHub toolkit exec command which returns the output.
* Also allows you to easily toggle the current working directory.
* @param {String} cmd = The command to execute.
* @param {String} cwd - The current working directory.
* @returns {Promise} - The output from the command.
* @param cmd = The command to execute.
* @param cwd - The current working directory.
* @returns - The output from the command.
*/
export async function execute(cmd: string, cwd: string): Promise<any> {
output = "";
await exec(cmd, [], {
// Silences the input unless the INPUT_DEBUG flag is set.
silent: process.env.DEBUG_DEPLOY_ACTION ? false : true,
cwd,
listeners: {
stdout

View File

@ -1,194 +1,214 @@
import { setFailed } from "@actions/core";
import {
action,
repositoryPath,
root,
tokenType,
workspace
} from "./constants";
import { actionInterface } from "./constants";
import { execute } from "./execute";
import { isNullOrUndefined } from "./util";
import {
hasRequiredParameters,
isNullOrUndefined,
suppressSensitiveInformation
} from "./util";
/** Generates the branch if it doesn't exist on the remote.
* @returns {Promise}
*/
export async function init(): Promise<void> {
/* Generates the branch if it doesn't exist on the remote. */
export async function init(action: actionInterface): Promise<void | Error> {
try {
if (
isNullOrUndefined(action.accessToken) &&
isNullOrUndefined(action.gitHubToken) &&
isNullOrUndefined(action.ssh)
) {
setFailed(
"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."
);
hasRequiredParameters(action);
throw 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."
);
}
console.log(`Deploying using ${action.tokenType}... 🔑`);
console.log("Configuring git...");
if (action.build.startsWith("/") || action.build.startsWith("./")) {
setFailed(
`The deployment folder cannot be prefixed with '/' or './'. Instead reference the folder name directly.`
);
await execute(`git init`, action.workspace);
await execute(`git config user.name "${action.name}"`, action.workspace);
await execute(`git config user.email "${action.email}"`, action.workspace);
await execute(`git remote rm origin`, action.workspace);
await execute(
`git remote add origin ${action.repositoryPath}`,
action.workspace
);
await execute(`git fetch`, action.workspace);
throw Error(
"Incorrectly formatted build folder. The deployment folder cannot be prefixed with '/' or './'. Instead reference the folder name directly."
);
}
console.log(`Deploying using ${tokenType}... 🔑`);
await execute(`git init`, workspace);
await execute(`git config user.name ${action.name}`, workspace);
await execute(`git config user.email ${action.email}`, workspace);
await execute(`git remote rm origin`, workspace);
await execute(`git remote add origin ${repositoryPath}`, workspace);
await execute(`git fetch`, workspace);
console.log("Git configured... 🔧");
} catch (error) {
console.log(`There was an error initializing the repository: ${error}`);
} finally {
console.log("Initialization step complete...");
throw new Error(
`There was an error initializing the repository: ${suppressSensitiveInformation(
error.message,
action
)} `
);
}
}
/** Switches to the base branch.
* @returns {Promise}
*/
export async function switchToBaseBranch(): Promise<string> {
await execute(
`git checkout --progress --force ${
action.baseBranch ? action.baseBranch : action.defaultBranch
}`,
workspace
);
/* Switches to the base branch. */
export async function switchToBaseBranch(
action: actionInterface
): Promise<void> {
try {
hasRequiredParameters(action);
return Promise.resolve("Switched to the base branch...");
await execute(
`git checkout --progress --force ${
action.baseBranch ? action.baseBranch : action.defaultBranch
}`,
action.workspace
);
} 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.
* @returns {Promise}
*/
export async function generateBranch(): Promise<void> {
/* Generates the branch if it doesn't exist on the remote. */
export async function generateBranch(action: actionInterface): Promise<void> {
try {
if (isNullOrUndefined(action.branch)) {
throw Error("Branch is required.");
}
hasRequiredParameters(action);
console.log(`Creating ${action.branch} branch... 🔧`);
await switchToBaseBranch();
await execute(`git checkout --orphan ${action.branch}`, workspace);
await execute(`git reset --hard`, workspace);
console.log(`Creating the ${action.branch} branch...`);
await switchToBaseBranch(action);
await execute(`git checkout --orphan ${action.branch}`, action.workspace);
await execute(`git reset --hard`, action.workspace);
await execute(
`git commit --allow-empty -m "Initial ${action.branch} commit."`,
workspace
action.workspace
);
await execute(`git push ${repositoryPath} ${action.branch}`, workspace);
await execute(`git fetch`, workspace);
await execute(
`git push ${action.repositoryPath} ${action.branch}`,
action.workspace
);
await execute(`git fetch`, action.workspace);
console.log(`Created the ${action.branch} branch... 🔧`);
} catch (error) {
setFailed(`There was an error creating the deployment branch: ${error}`);
} finally {
console.log("Deployment branch creation step complete... ✅");
throw new Error(
`There was an error creating the deployment branch: ${suppressSensitiveInformation(
error.message,
action
)} `
);
}
}
/** Runs the necessary steps to make the deployment.
* @returns {Promise}
*/
export async function deploy(): Promise<string> {
/* Runs the necessary steps to make the deployment. */
export async function deploy(action: actionInterface): Promise<void> {
const temporaryDeploymentDirectory = "gh-action-temp-deployment-folder";
const temporaryDeploymentBranch = "gh-action-temp-deployment-branch";
/*
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 ${repositoryPath} ${action.branch} | wc -l`,
workspace
);
if (!branchExists && !action.isTest) {
console.log("Deployment branch does not exist. Creating....");
await generateBranch();
}
console.log("Starting to commit changes...");
// Checks out the base branch to begin the deployment process.
await switchToBaseBranch();
await execute(`git fetch ${repositoryPath}`, workspace);
await execute(
`git worktree add --checkout ${temporaryDeploymentDirectory} origin/${action.branch}`,
workspace
);
try {
hasRequiredParameters(action);
// Ensures that items that need to be excluded from the clean job get parsed.
let excludes = "";
if (action.clean && action.cleanExclude) {
try {
const excludedItems = JSON.parse(action.cleanExclude);
excludedItems.forEach(
(item: string) => (excludes += `--exclude ${item} `)
);
} catch {
console.log(
"There was an error parsing your CLEAN_EXCLUDE items. Please refer to the README for more details. ❌"
);
/*
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
);
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);
await execute(
`git worktree add --checkout ${temporaryDeploymentDirectory} origin/${action.branch}`,
action.workspace
);
// 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;
excludedItems.forEach(
(item: string) => (excludes += `--exclude ${item} `)
);
} catch {
console.log(
"There was an error parsing your CLEAN_EXCLUDE items. Please refer to the README for more details. ❌"
);
}
}
/*
Pushes all of the build files into the deployment directory.
Allows the user to specify the root if '.' is provided.
rsync is used to prevent file duplication. */
await execute(
`rsync -q -av --progress ${action.folder}/. ${
action.targetFolder
? `${temporaryDeploymentDirectory}/${action.targetFolder}`
: temporaryDeploymentDirectory
} ${
action.clean
? `--delete ${excludes} --exclude CNAME --exclude .nojekyll`
: ""
} --exclude .ssh --exclude .git --exclude .github ${
action.folder === action.root
? `--exclude ${temporaryDeploymentDirectory}`
: ""
}`,
action.workspace
);
const hasFilesToCommit = await execute(
`git status --porcelain`,
`${action.workspace}/${temporaryDeploymentDirectory}`
);
if (!hasFilesToCommit && !action.isTest) {
console.log("There is nothing to commit. Exiting early... 📭");
return;
}
// Commits to GitHub.
await execute(
`git add --all .`,
`${action.workspace}/${temporaryDeploymentDirectory}`
);
await execute(
`git checkout -b ${temporaryDeploymentBranch}`,
`${action.workspace}/${temporaryDeploymentDirectory}`
);
await execute(
`git commit -m "${
!isNullOrUndefined(action.commitMessage)
? action.commitMessage
: `Deploying to ${action.branch} from ${action.baseBranch}`
} ${
process.env.GITHUB_SHA ? `- ${process.env.GITHUB_SHA}` : ""
} 🚀" --quiet`,
`${action.workspace}/${temporaryDeploymentDirectory}`
);
await execute(
`git push --force ${action.repositoryPath} ${temporaryDeploymentBranch}:${action.branch}`,
`${action.workspace}/${temporaryDeploymentDirectory}`
);
console.log(`Changes committed to the ${action.branch} branch... 📦`);
// Cleans up temporary files/folders and restores the git state.
console.log("Running post deployment cleanup jobs...");
await execute(
`git checkout --progress --force ${action.defaultBranch}`,
action.workspace
);
} catch (error) {
throw new Error(
`The deploy step encountered an error: ${suppressSensitiveInformation(
error.message,
action
)} `
);
} finally {
// Ensures the deployment directory is safely removed.
await execute(`rm -rf ${temporaryDeploymentDirectory}`, action.workspace);
}
/*
Pushes all of the build files into the deployment directory.
Allows the user to specify the root if '.' is provided.
rysync is used to prevent file duplication. */
await execute(
`rsync -q -av --progress ${action.build}/. ${
action.targetFolder
? `${temporaryDeploymentDirectory}/${action.targetFolder}`
: temporaryDeploymentDirectory
} ${
action.clean
? `--delete ${excludes} --exclude CNAME --exclude .nojekyll`
: ""
} --exclude .ssh --exclude .git --exclude .github ${
action.build === root ? `--exclude ${temporaryDeploymentDirectory}` : ""
}`,
workspace
);
const hasFilesToCommit = await execute(
`git status --porcelain`,
temporaryDeploymentDirectory
);
if (!hasFilesToCommit && !action.isTest) {
console.log("There is nothing to commit. Exiting... ✅");
return Promise.resolve("Exiting early...");
}
// Commits to GitHub.
await execute(`git add --all .`, temporaryDeploymentDirectory);
await execute(
`git checkout -b ${temporaryDeploymentBranch}`,
temporaryDeploymentDirectory
);
await execute(
`git commit -m "${
!isNullOrUndefined(action.commitMessage)
? action.commitMessage
: `Deploying to ${action.branch} from ${action.baseBranch}`
} - ${process.env.GITHUB_SHA} 🚀" --quiet`,
temporaryDeploymentDirectory
);
await execute(
`git push --force ${repositoryPath} ${temporaryDeploymentBranch}:${action.branch}`,
temporaryDeploymentDirectory
);
// Cleans up temporary files/folders and restores the git state.
console.log("Running post deployment cleanup jobs... 🔧");
await execute(`rm -rf ${temporaryDeploymentDirectory}`, workspace);
await execute(
`git checkout --progress --force ${action.defaultBranch}`,
workspace
);
return Promise.resolve("Commit step complete...");
}

45
src/lib.ts Normal file
View File

@ -0,0 +1,45 @@
import { exportVariable, setFailed } from "@actions/core";
import { action, actionInterface } from "./constants";
import { deploy, generateBranch, init } from "./git";
import { generateRepositoryPath, generateTokenType } from "./util";
/** Initializes and runs the action. */
export default async function run(
configuration: actionInterface
): Promise<void> {
let errorState: boolean = false;
try {
console.log("Checking configuration and starting deployment...🚦");
const settings = {
...action,
...configuration
};
// Defines the repository paths and token types.
settings.repositoryPath = generateRepositoryPath(settings);
settings.tokenType = generateTokenType(settings);
if (settings.debug) {
// Sets the debug flag if passed as an arguement.
exportVariable("DEBUG_DEPLOY_ACTION", "debug");
}
await init(settings);
await deploy(settings);
} catch (error) {
errorState = true;
setFailed(error.message);
} finally {
console.log(
`${
errorState
? "Deployment Failed ❌"
: "Completed Deployment Successfully! ✅"
}`
);
}
}
export { init, deploy, generateBranch, actionInterface };

View File

@ -1,20 +1,5 @@
import { setFailed } from "@actions/core";
import { init, deploy } from "./git";
import { action } from "./constants";
import run from "./lib";
/** Initializes and runs the action. */
export default async function main(): Promise<void> {
try {
await init();
await deploy();
} catch (error) {
/* istanbul ignore next */
console.log("The deployment encountered an error. ❌");
/* istanbul ignore next */
setFailed(error);
} finally {
console.log("Completed Deployment ✅");
}
}
// Init
main();
// Runs the action within the GitHub actions environment.
run(action);

View File

@ -1,7 +1,80 @@
/** Utility function that checks to see if a value is undefined or not.
* @param {*} value = The value to check.
* @returns {boolean}
*/
export function isNullOrUndefined(value: any): boolean {
return typeof value === "undefined" || value === null || value === "";
}
import { getInput } from "@actions/core";
import { actionInterface } from "./constants";
/* Utility function that checks to see if a value is undefined or not. */
export const isNullOrUndefined = (value: any): boolean =>
typeof value === "undefined" || value === null || value === "";
/* 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"
: "...";
/* Generates a the repository path used to make the commits. */
export const generateRepositoryPath = (action: actionInterface): string =>
action.ssh
? `git@github.com:${action.gitHubRepository}`
: `https://${action.accessToken ||
`x-access-token:${action.gitHubToken}`}@github.com/${
action.gitHubRepository
}.git`;
/* Checks for the required tokens and formatting. Throws an error if any case is matched. */
export const hasRequiredParameters = (action: actionInterface): void => {
if (
(isNullOrUndefined(action.accessToken) &&
isNullOrUndefined(action.gitHubToken) &&
isNullOrUndefined(action.ssh)) ||
isNullOrUndefined(action.repositoryPath)
) {
throw new Error(
"No deployment token/method was provided. You must provide the action with either a Personal Access Token or the GitHub Token secret in order to deploy. If you wish to use an ssh deploy token then you must set SSH to true."
);
}
if (isNullOrUndefined(action.branch)) {
throw new Error("Branch is required.");
}
if (!action.folder || isNullOrUndefined(action.folder)) {
throw new Error("You must provide the action with a folder to deploy.");
}
if (action.folder.startsWith("/") || action.folder.startsWith("./")) {
throw new Error(
"Incorrectly formatted build folder. The deployment folder cannot be prefixed with '/' or './'. Instead reference the folder name directly."
);
}
};
/* Suppresses sensitive information from being exposed in error messages. */
export const suppressSensitiveInformation = (
str: string,
action: actionInterface
) => {
let value = str;
if (getInput("DEBUG")) {
// Data is unmasked in debug mode.
return value;
}
if (action.accessToken) {
value = value.replace(action.accessToken, "***");
}
if (action.gitHubToken) {
value = value.replace(action.gitHubToken, "***");
}
if (action.repositoryPath) {
value = value.replace(action.repositoryPath, "***");
}
return value;
};