Version 3 (#44)

* Version 3 Beta

* README Updates

* More readme updates

* lock

* Restoring license/contrib

* One set of tests

* linting

* More tests

* Finish up unit tests

* Update action.yml

* Update main.yml

* Update README.md

* Update README.md

* README Changes

* Update README.md

* Unit testing job

* Some more stuff

* More cleanup

* Update README.md

* Unrequired

* Update CONTRIBUTING.md

* no guess

* Using Git Switch as opposed to checkout

* Force Commit

* Remove git

* Some more simplification

* Update git.ts

* changes

* Options

* w

* Debugging

* More debugging

* More debugging

* Debugging

* Debugging

* Debugging

* Debugging

* Debugging

* Debugging

* Debugging

* Debugging

* Debugging

* Debugging

* Debugging

* Debugging

* Debugging

* Debug

* Debugging

* Another 1

* Update git.ts

* Fixes root deployment

* Quiet Mode

* Quiet

* Excluding .github

* Update README.md
This commit is contained in:
James Ives 2019-11-19 10:06:27 -05:00 committed by GitHub
parent ab80c0965e
commit 4b2f84d384
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 4151 additions and 133 deletions

16
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,16 @@
name: unit
on:
push:
branches:
- master
jobs:
unit-test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@master
- name: Install and Test
run: |
npm install
npm run-script test

View File

@ -14,5 +14,4 @@ jobs:
env:
ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
BRANCH: gh-pages
FOLDER: 'test/build'
BUILD_SCRIPT: npm run-script integrationTest
FOLDER: 'integration'

3
.gitignore vendored
View File

@ -5,4 +5,5 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
test/build
node_modules

View File

@ -1,10 +1,28 @@
# Contributing
# Contributing
When contributing to this repository, please first discuss the change you wish to make via issue,
email, or any other method with the owners of this repository before making a change.
When contributing to this repository, please first discuss the change you wish to make via issue,
email, or any other method with the owners of this repository before making a change.
## Pull Request Best Practices
## Pull Request Best Practices
1. Ensure that you've tested your feature/change yourself. As the primary focus of this project is deployment, providing a link to a deployed repository using your branch is preferred. You can reference the forked action using your GitHub username, for example `yourname/github-pages-deplpy-action@master`.
2. Make sure you update the README if you've made a change that requires documentation.
1. Ensure that you've tested your feature/change yourself. As the primary focus of this project is deployment, providing a link to a deployed repository using your branch is preferred. You can reference the forked action using your GitHub username, for example `yourname/github-pages-deplpy-action@master`.
2. Make sure you update the README if you've made a change that requires documentation.
3. When making a pull request, highlight any areas that may cause a breaking change so the maintainer can update the version number accordingly on the GitHub marketplace.
# Deploying
In order to deploy and test your own fork of this action, you must commit the required `node_modules` dependencies.
To do this you can follow the instructions below:
```
# comment out in distribution branches
# node_modules/
```
```
$ git checkout -b branchnamehere
$ git commit -a -m "prod dependencies"
```
The `node_modules` folder should _not_ be included when making a pull request.

View File

@ -1,13 +0,0 @@
FROM node:10
LABEL "com.github.actions.name"="Deploy to GitHub Pages"
LABEL "com.github.actions.description"="This action will handle the building and deploying process of your project to GitHub Pages."
LABEL "com.github.actions.icon"="git-commit"
LABEL "com.github.actions.color"="orange"
LABEL "repository"="http://github.com/JamesIves/gh-pages-github-action"
LABEL "homepage"="http://github.com/JamesIves/gh-pages-gh-action"
LABEL "maintainer"="James Ives <iam@jamesiv.es>"
ADD entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

View File

@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SOFTWARE.

View File

@ -1,8 +1,8 @@
# GitHub Pages Deploy Action :rocket:
[![Actions Status](https://github.com/JamesIves/github-pages-deploy-action/workflows/integration/badge.svg)](https://github.com/JamesIves/github-pages-deploy-action/actions) [![View Action](https://img.shields.io/badge/view-action-blue.svg)](https://github.com/marketplace/actions/deploy-to-github-pages) [![Issues](https://img.shields.io/github/issues/JamesIves/github-pages-deploy-action.svg)](https://github.com/JamesIves/github-pages-deploy-action/issues)
[![Build Status](https://github.com/JamesIves/github-pages-deploy-action/workflows/unit/badge.svg)](https://github.com/JamesIves/github-pages-deploy-action/actions) [![Actions Status](https://github.com/JamesIves/github-pages-deploy-action/workflows/integration/badge.svg)](https://github.com/JamesIves/github-pages-deploy-action/actions) [![View Action](https://img.shields.io/badge/view-action-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)](hhttps://github.com/JamesIves/github-pages-deploy-action/releases)
This [GitHub action](https://github.com/features/actions) will handle the building and 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 action is built on [Node](https://nodejs.org/en/), which means that you can call any optional build scripts your project requires prior to deploying.
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`.
## 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.
@ -20,13 +20,12 @@ jobs:
uses: actions/checkout@v1
- name: Build and Deploy
uses: JamesIves/github-pages-deploy-action@releases/v2
env:
uses: JamesIves/github-pages-deploy-action@releases/v3
with:
ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
BASE_BRANCH: master # The branch the action should deploy from.
BRANCH: gh-pages # The branch the action should deploy to.
FOLDER: build # The folder the action should deploy.
BUILD_SCRIPT: npm install && npm run-script build # The build script the action should run prior to deploying.
```
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. You'll still need to specify a `BASE_BRANCH` if you're deploying from a branch other than `master`.
@ -40,7 +39,7 @@ on:
## Configuration 📁
The `env` portion of the workflow **must** be configured before the action will work. You can add these in the `env` 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).
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).
Below you'll find a description of what each option does.
@ -48,12 +47,14 @@ Below you'll find a description of what each option does.
| ------------- | ------------- | ------------- | ------------- |
| `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 }}`. Only required if an access token is **not** provided. **Please note there is currently an issue affecting the use of this token, [you can learn more here](https://github.com/JamesIves/github-pages-deploy-action/issues/5)**. | `secrets` | **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` | **No** |
| `BRANCH` | This is the branch you wish to deploy to, for example `gh-pages` or `docs`. | `env` | **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 `./`**. | `env` | **Yes** |
| `BASE_BRANCH` | The base branch of your repository which you'd like to checkout prior to deploying. This defaults to `master`. | `env` | **No** |
| `BUILD_SCRIPT` | If you require a build script to compile your code prior to pushing it you can add the script here. The Docker container which powers the action runs Node which means `npm` commands are valid. If you're using a static site generator such as Jekyll I'd suggest compiling the code prior to pushing it to your base branch. | `env` | **No** |
| `CNAME` | If you're using a [custom domain](https://help.github.com/en/articles/using-a-custom-domain-with-github-pages), you will need to add the domain name to the `CNAME` environment variable. If you don't do this GitHub will wipe out your domain configuration after each deploy. This value will look something like this: `jives.dev`. | `env` | **No** |
| `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 `./`**. | `with` | **Yes** |
| `BASE_BRANCH` | The base branch of your repository which you'd like to checkout prior to deploying. This defaults to `master`. | `with` | **No** |
With the action correctly configured you should see the workflow trigger the deployment under the configured conditions.
### Additional Build Files
This action maintains the full Git history of the deployment branch. Therefore if you're using a custom domain and require a `CNAME` file, or if you require the use of a `.nojekyll` file, you can safely commit these files directly into deployment branch without them being overridden after each deployment.
![Example](screenshot.png)

161
__tests__/git.test.ts Normal file
View File

@ -0,0 +1,161 @@
// Initial env variable setup for tests.
process.env["INPUT_FOLDER"] = "build";
import { execute } from "../src/util";
import { init, generateBranch, deploy } from "../src/git";
import {action} from '../src/constants'
import {cp} from '@actions/io';
import _ from 'lodash';
const originalAction = _.cloneDeep(action);
jest.mock("../src/util", () => ({
execute: jest.fn()
}));
jest.mock("@actions/io", () => ({
cp: jest.fn()
}));
describe("git", () => {
afterEach(() => {
_.assignIn(action, originalAction);
});
describe("init", () => {
it("should execute three commands if a GitHub token is provided", async () => {
Object.assign(action, {
build: 'build',
gitHubToken: '123',
pusher: {
name: 'asd',
email: 'as@cat'
}})
const call = await init();
expect(execute).toBeCalledTimes(3);
expect(call).toBe('Initialization step complete...')
});
it("should execute three commands if a Access token is provided", async () => {
Object.assign(action, {
build: 'build',
accessToken: '123',
pusher: {
name: 'asd',
email: 'as@cat'
}})
const call = await init();
expect(execute).toBeCalledTimes(3);
expect(call).toBe('Initialization step complete...')
});
it("should fail if there is no provided GitHub Token or Access Token", async () => {
Object.assign(action, {
build: 'build',
pusher: {
name: 'asd',
email: 'as@cat'
}})
const call = await init()
expect(execute).toBeCalledTimes(0);
expect(call).toBe('Initialization step complete...')
})
it("should fail if the build folder begins with a /", async () => {
Object.assign(action, {
accessToken: '123',
build: '/',
pusher: {
name: 'asd',
email: 'as@cat'
}})
const call = await init()
expect(execute).toBeCalledTimes(0);
expect(call).toBe('Initialization step complete...')
})
it("should fail if the build folder begins with a ./", async () => {
Object.assign(action, {
accessToken: '123',
build: './',
pusher: {
name: 'asd',
email: 'as@cat'
}})
const call = await init()
expect(execute).toBeCalledTimes(0);
expect(call).toBe('Initialization step complete...')
})
it("should not fail if root is used", async () => {
Object.assign(action, {
accessToken: '123',
build: '.',
pusher: {
name: 'asd',
email: 'as@cat'
}})
const call = await init()
expect(execute).toBeCalledTimes(3);
expect(call).toBe('Initialization step complete...')
})
});
describe('generateBranch', () => {
it('should execute five commands', async () => {
const call = await generateBranch();
expect(execute).toBeCalledTimes(6);
expect(call).toBe('Deployment branch creation step complete...')
})
})
describe('deploy', () => {
it('should execute five commands', async () => {
Object.assign(action, {
build: 'build',
gitHubToken: '123',
pusher: {
name: 'asd',
email: 'as@cat'
}})
const call = await deploy();
// Includes the call to generateBranch
expect(execute).toBeCalledTimes(14);
expect(cp).toBeCalledTimes(1)
expect(call).toBe('Commit step complete...')
})
it('should execute six commands if root is used', async () => {
Object.assign(action, {
build: '.',
gitHubToken: '123',
pusher: {
name: 'asd',
email: 'as@cat'
}})
const call = await deploy();
// Includes the call to generateBranch
expect(execute).toBeCalledTimes(15);
expect(cp).toBeCalledTimes(0)
expect(call).toBe('Commit step complete...')
})
})
});

23
__tests__/util.test.ts Normal file
View File

@ -0,0 +1,23 @@
import {execute} from '../src/util';
import {exec} from '@actions/exec';
jest.mock('@actions/exec', () => ({
exec: jest.fn()
}))
describe('util', () => {
describe('execute', () => {
it('should be called with the correct arguements', async() => {
await execute('echo Montezuma', './')
expect(exec).toBeCalledWith(
"echo Montezuma", [], {
cwd: "./",
listeners: {
stdout: expect.any(Function)
}
}
)
});
})
})

View File

@ -1,9 +1,9 @@
name: 'Deploy to GitHub Pages'
description: 'This action will handle the building and deploying process of your project to GitHub Pages.'
description: 'This action will handle the deployment process of your project to GitHub Pages.'
author: 'James Ives <iam@jamesiv.es>'
runs:
using: 'docker'
image: 'Dockerfile'
using: 'node12'
main: 'lib/main.js'
branding:
icon: 'git-commit'
color: 'orange'

View File

@ -1,92 +0,0 @@
#!/bin/sh -l
set -e
if [ -z "$ACCESS_TOKEN" ] && [ -z "$GITHUB_TOKEN" ]
then
echo "You must provide the action with either a Personal Access Token or the GitHub Token secret in order to deploy."
exit 1
fi
if [ -z "$BRANCH" ]
then
echo "You must provide the action with a branch name it should deploy to, for example gh-pages or docs."
exit 1
fi
if [ -z "$FOLDER" ]
then
echo "You must provide the action with the folder name in the repository where your compiled page lives."
exit 1
fi
case "$FOLDER" in /*|./*)
echo "The deployment folder cannot be prefixed with '/' or './'. Instead reference the folder name directly."
exit 1
esac
# Installs Git and jq.
apt-get update && \
apt-get install -y git && \
apt-get install -y jq && \
# Gets the commit email/name if it exists in the push event payload.
COMMIT_EMAIL=`jq '.pusher.email' ${GITHUB_EVENT_PATH}`
COMMIT_NAME=`jq '.pusher.name' ${GITHUB_EVENT_PATH}`
# If the commit email/name is not found in the event payload then it falls back to the actor.
if [ -z "$COMMIT_EMAIL" ]
then
COMMIT_EMAIL="${GITHUB_ACTOR:-github-pages-deploy-action}@users.noreply.github.com"
fi
if [ -z "$COMMIT_NAME" ]
then
COMMIT_NAME="${GITHUB_ACTOR:-GitHub Pages Deploy Action}"
fi
# Directs the action to the the Github workspace.
cd $GITHUB_WORKSPACE && \
# Configures Git.
git init && \
git config --global user.email "${COMMIT_EMAIL}" && \
git config --global user.name "${COMMIT_NAME}" && \
## Initializes the repository path using the access token.
REPOSITORY_PATH="https://${ACCESS_TOKEN:-"x-access-token:$GITHUB_TOKEN"}@github.com/${GITHUB_REPOSITORY}.git" && \
# Checks to see if the remote exists prior to deploying.
# If the branch doesn't exist it gets created here as an orphan.
if [ "$(git ls-remote --heads "$REPOSITORY_PATH" "$BRANCH" | wc -l)" -eq 0 ];
then
echo "Creating remote branch ${BRANCH} as it doesn't exist..."
git checkout "${BASE_BRANCH:-master}" && \
git checkout --orphan $BRANCH && \
git rm -rf . && \
touch README.md && \
git add README.md && \
git commit -m "Initial ${BRANCH} commit" && \
git push $REPOSITORY_PATH $BRANCH
fi
# Checks out the base branch to begin the deploy process.
git checkout "${BASE_BRANCH:-master}" && \
# Builds the project if a build script is provided.
echo "Running build scripts... $BUILD_SCRIPT" && \
eval "$BUILD_SCRIPT" && \
if [ "$CNAME" ]; then
echo "Generating a CNAME file in in the $FOLDER directory..."
echo $CNAME > $FOLDER/CNAME
fi
# Commits the data to Github.
echo "Deploying to GitHub..." && \
git add -f $FOLDER && \
git commit -m "Deploying to ${BRANCH} from ${BASE_BRANCH:-master} ${GITHUB_SHA}" --quiet && \
git push $REPOSITORY_PATH `git subtree split --prefix $FOLDER ${BASE_BRANCH:-master}`:$BRANCH --force && \
echo "Deployment succesful!"

View File

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

11
jest.config.js Normal file
View File

@ -0,0 +1,11 @@
module.exports = {
clearMocks: true,
moduleFileExtensions: ['js', 'ts'],
testEnvironment: 'node',
testMatch: ['**/*.test.ts'],
testRunner: 'jest-circus/runner',
transform: {
'^.+\\.ts$': 'ts-jest'
},
verbose: true
}

28
lib/constants.js Normal file
View File

@ -0,0 +1,28 @@
"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 = ".";
// Required action data.
exports.action = {
build: exports.folder,
gitHubRepository: repository ? repository.full_name : "",
gitHubToken: core.getInput("GITHUB_TOKEN"),
accessToken: core.getInput("ACCESS_TOKEN"),
branch: core.getInput("BRANCH"),
baseBranch: core.getInput("BASE_BRANCH") || "master",
pusher
};
// Repository path used for commits/pushes.
exports.repositoryPath = `https://${exports.action.accessToken ||
`x-access-token:${exports.action.gitHubToken}`}@github.com/${exports.action.gitHubRepository}.git`;

113
lib/git.js Normal file
View File

@ -0,0 +1,113 @@
"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 io_1 = require("@actions/io");
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 (!constants_1.action.accessToken && !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 util_1.execute(`git init`, constants_1.workspace);
yield util_1.execute(`git config user.name ${constants_1.action.pusher.name}`, constants_1.workspace);
yield util_1.execute(`git config user.email ${constants_1.action.pusher.email}`, 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;
/** 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 util_1.execute(`git switch ${constants_1.action.baseBranch || "master"}`, constants_1.workspace);
yield util_1.execute(`git switch --orphan ${constants_1.action.branch}`, constants_1.workspace);
yield util_1.execute(`git reset --hard`, constants_1.workspace);
yield util_1.execute(`git commit --allow-empty -m "Initial ${constants_1.action.branch} commit."`, constants_1.workspace);
yield util_1.execute(`git push ${constants_1.repositoryPath} ${constants_1.action.branch}`, constants_1.workspace);
// Switches back to the base branch.
yield util_1.execute(`git switch ${constants_1.action.baseBranch || "master"}`, 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 util_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 util_1.execute(`git checkout ${constants_1.action.baseBranch || "master"}`, constants_1.workspace);
yield util_1.execute(`git fetch origin`, constants_1.workspace);
yield util_1.execute(`git worktree add --checkout ${temporaryDeploymentDirectory} origin/${constants_1.action.branch}`, constants_1.workspace);
/*
Pushes all of the build files into the deployment directory.
Allows the user to specify the root if '.' is provided. */
if (constants_1.action.build === constants_1.root) {
// rsync is executed here so the .git and temporary deployment directories don't get duplicated.
yield util_1.execute(`rsync -q -av --progress ${constants_1.action.build}/. ${temporaryDeploymentDirectory} --exclude .git --exclude .github --exclude ${temporaryDeploymentDirectory}`, constants_1.workspace);
}
else {
yield io_1.cp(`${constants_1.action.build}/.`, temporaryDeploymentDirectory, {
recursive: true,
force: true
});
}
// Commits to GitHub.
yield util_1.execute(`git add --all .`, temporaryDeploymentDirectory);
yield util_1.execute(`git switch -c ${temporaryDeploymentBranch}`, temporaryDeploymentDirectory);
yield util_1.execute(`git commit -m "Deploying to ${constants_1.action.branch} from ${constants_1.action.baseBranch} ${process.env.GITHUB_SHA}" --quiet`, temporaryDeploymentDirectory);
yield util_1.execute(`git push --force ${constants_1.repositoryPath} ${temporaryDeploymentBranch}:${constants_1.action.branch}`, temporaryDeploymentDirectory);
return Promise.resolve("Commit step complete...");
});
}
exports.deploy = deploy;

35
lib/main.js Normal file
View File

@ -0,0 +1,35 @@
"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) {
core.setFailed(error.message);
}
finally {
console.log("Completed Deployment");
}
});
})();

33
lib/util.js Normal file
View File

@ -0,0 +1,33 @@
"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,9 +1,51 @@
{
"name": "github-pages-deploy-action",
"description": "GitHub action for building a project and deploying it to GitHub pages.",
"license": "MIT",
"repository": "https://github.com/JamesIves/github-pages-deploy-action",
"private": true,
"main": "lib/main.js",
"scripts": {
"integrationTest": "mkdir test/build && cp test/image.jpg test/build/image.jpg && cp test/index.html test/build/index.html"
"build": "tsc",
"test": "jest",
"lint": "tslint -p tsconfig.json --project '.' || (echo Project needs formatting)",
"format": "prettier --write './src/*.ts'"
},
"repository": {
"type": "git",
"url": "git+https://github.com/JamesIves/github-pages-deploy-action.git"
},
"bugs": {
"url": "https://github.com/JamesIves/github-pages-deploy-action/issues"
},
"homepage": "https://github.com/JamesIves/github-pages-deploy-action",
"keywords": [
"actions",
"node",
"setup",
"build",
"deploy",
"gh-pages",
"pages",
"github",
"deploy",
"deployment"
],
"author": "James Ives",
"license": "MIT",
"dependencies": {
"@actions/core": "^1.0.0",
"@actions/exec": "^1.0.1",
"@actions/github": "^1.1.0",
"@actions/io": "^1.0.1"
},
"devDependencies": {
"@types/jest": "^24.0.13",
"@types/node": "^12.11.0",
"jest": "^24.8.0",
"jest-circus": "^24.7.1",
"lodash": "^4.17.15",
"prettier": "^1.18.2",
"ts-jest": "^24.0.2",
"tslint": "^5.20.0",
"typescript": "^3.5.1"
}
}
}

25
src/constants.ts Normal file
View File

@ -0,0 +1,25 @@
import * as core from "@actions/core";
import * as github from "@actions/github";
const { pusher, repository } = github.context.payload;
export const workspace: any = process.env.GITHUB_WORKSPACE;
export const folder = core.getInput("FOLDER", { required: true });
export const root = ".";
// Required action data.
export const action = {
build: folder,
gitHubRepository: repository ? repository.full_name : "",
gitHubToken: core.getInput("GITHUB_TOKEN"),
accessToken: core.getInput("ACCESS_TOKEN"),
branch: core.getInput("BRANCH"),
baseBranch: core.getInput("BASE_BRANCH") || "master",
pusher
};
// Repository path used for commits/pushes.
export const repositoryPath = `https://${action.accessToken ||
`x-access-token:${action.gitHubToken}`}@github.com/${
action.gitHubRepository
}.git`;

118
src/git.ts Normal file
View File

@ -0,0 +1,118 @@
import * as core from "@actions/core";
import { cp } from "@actions/io";
import { execute } from "./util";
import { workspace, action, root, repositoryPath } from "./constants";
/** Generates the branch if it doesn't exist on the remote.
* @returns {Promise}
*/
export async function init(): Promise<any> {
try {
if (!action.accessToken && !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 (action.build.startsWith("/") || action.build.startsWith("./")) {
return core.setFailed(
`The deployment folder cannot be prefixed with '/' or './'. Instead reference the folder name directly.`
);
}
await execute(`git init`, workspace);
await execute(`git config user.name ${action.pusher.name}`, workspace);
await execute(`git config user.email ${action.pusher.email}`, workspace);
} catch (error) {
core.setFailed(`There was an error initializing the repository: ${error}`);
} finally {
return Promise.resolve("Initialization step complete...");
}
}
/** Generates the branch if it doesn't exist on the remote.
* @returns {Promise}
*/
export async function generateBranch(): Promise<any> {
try {
console.log(`Creating ${action.branch} branch...`);
await execute(`git switch ${action.baseBranch || "master"}`, workspace);
await execute(`git switch --orphan ${action.branch}`, workspace);
await execute(`git reset --hard`, workspace);
await execute(
`git commit --allow-empty -m "Initial ${action.branch} commit."`,
workspace
);
await execute(`git push ${repositoryPath} ${action.branch}`, workspace);
// Switches back to the base branch.
await execute(`git switch ${action.baseBranch || "master"}`, workspace);
} catch (error) {
core.setFailed(
`There was an error creating the deployment branch: ${error}`
);
} finally {
return Promise.resolve("Deployment branch creation step complete...");
}
}
/** Runs the necessary steps to make the deployment.
* @returns {Promise}
*/
export async function deploy(): Promise<any> {
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) {
console.log("Deployment branch does not exist. Creating....");
await generateBranch();
}
// Checks out the base branch to begin the deployment process.
await execute(`git checkout ${action.baseBranch || "master"}`, workspace);
await execute(`git fetch origin`, workspace);
await execute(
`git worktree add --checkout ${temporaryDeploymentDirectory} origin/${action.branch}`,
workspace
);
/*
Pushes all of the build files into the deployment directory.
Allows the user to specify the root if '.' is provided. */
if (action.build === root) {
// rsync is executed here so the .git and temporary deployment directories don't get duplicated.
await execute(
`rsync -q -av --progress ${action.build}/. ${temporaryDeploymentDirectory} --exclude .git --exclude .github --exclude ${temporaryDeploymentDirectory}`,
workspace
);
} else {
await cp(`${action.build}/.`, temporaryDeploymentDirectory, {
recursive: true,
force: true
});
}
// Commits to GitHub.
await execute(`git add --all .`, temporaryDeploymentDirectory);
await execute(
`git switch -c ${temporaryDeploymentBranch}`,
temporaryDeploymentDirectory
);
await execute(
`git commit -m "Deploying to ${action.branch} from ${action.baseBranch} ${process.env.GITHUB_SHA}" --quiet`,
temporaryDeploymentDirectory
);
await execute(
`git push --force ${repositoryPath} ${temporaryDeploymentBranch}:${action.branch}`,
temporaryDeploymentDirectory
);
return Promise.resolve("Commit step complete...");
}

14
src/main.ts Normal file
View File

@ -0,0 +1,14 @@
import * as core from "@actions/core";
import { init, deploy } from "./git";
/** Initializes and runs the action. */
(async function() {
try {
await init();
await deploy();
} catch (error) {
core.setFailed(error.message);
} finally {
console.log("Completed Deployment");
}
})();

22
src/util.ts Normal file
View File

@ -0,0 +1,22 @@
import { exec } from "@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.
*/
export async function execute(cmd: string, cwd: string): Promise<any> {
let output = "";
await exec(cmd, [], {
cwd,
listeners: {
stdout: (data: Buffer) => {
output += data.toString().trim();
}
}
});
return Promise.resolve(output);
}

63
tsconfig.json Normal file
View File

@ -0,0 +1,63 @@
{
"compilerOptions": {
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./lib", /* Redirect output structure to the directory. */
"rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
"noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
},
"exclude": ["node_modules", "**/*.test.ts"]
}

37
tslint.json Normal file
View File

@ -0,0 +1,37 @@
{
"rules": {
"adjacent-overload-signatures": true,
"ban-comma-operator": true,
"no-namespace": true,
"no-parameter-reassignment": true,
"no-reference": true,
"no-unnecessary-type-assertion": true,
"label-position": true,
"no-conditional-assignment": true,
"no-construct": true,
"no-duplicate-super": true,
"no-duplicate-switch-case": true,
"no-duplicate-variable": [true, "check-parameters"],
"no-shadowed-variable": true,
"no-empty": [true, "allow-empty-catch"],
"no-floating-promises": false,
"no-implicit-dependencies": true,
"no-invalid-this": true,
"no-string-throw": true,
"no-unsafe-finally": true,
"no-use-before-declare": true,
"no-void-expression": [true, "ignore-arrow-function-shorthand"],
"no-duplicate-imports": true,
"no-empty-interface": {"severity": "warning"},
"no-import-side-effect": {"severity": "warning"},
"no-var-keyword": {"severity": "warning"},
"triple-equals": {"severity": "warning"},
"deprecation": {"severity": "warning"},
"prefer-for-of": {"severity": "warning"},
"unified-signatures": {"severity": "warning"},
"prefer-const": {"severity": "warning"},
"trailing-comma": {"severity": "warning"}
},
"defaultSeverity": "error"
}

3363
yarn.lock Normal file

File diff suppressed because it is too large Load Diff