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

@ -8,3 +8,21 @@ email, or any other method with the owners of this repository before making a ch
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

@ -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