Add ability to sign commits through Import GPG GitHub Action

Add author input
Fix default committer and author for GitHub Actions bot
Replace GITHUB_PAT env var with GH_PAT (#53)
Replace committer_name and committer_email inputs with committer
This commit is contained in:
CrazyMax 2020-05-11 14:59:39 +02:00
parent f1d4f1ae95
commit 077e8dcf81
No known key found for this signature in database
GPG Key ID: 3248E46B6BB8C7F7
10 changed files with 793 additions and 124 deletions

View File

@ -12,8 +12,21 @@ If you are interested, [check out](https://git.io/Je09Y) my other :octocat: GitH
![GitHub Pages](.github/ghaction-github-pages.png) ![GitHub Pages](.github/ghaction-github-pages.png)
___
* [Usage](#usage)
* [Workflow](#workflow)
* [Sign commits](#sign-commits)
* [Customizing](#customizing)
* [inputs](#inputs)
* [environment variables](#environment-variables)
* [How can I help?](#how-can-i-help)
* [License](#license)
## Usage ## Usage
### Workflow
Below is a simple snippet to deploy to GitHub Pages with a dummy HTML page. Below is a simple snippet to deploy to GitHub Pages with a dummy HTML page.
A [workflow](https://github.com/crazy-max/ghaction-github-pages/actions?query=workflow%3Aci) is also available for this repository and deploys [everyday to GitHub pages](https://crazy-max.github.io/ghaction-github-pages/). A [workflow](https://github.com/crazy-max/ghaction-github-pages/actions?query=workflow%3Aci) is also available for this repository and deploys [everyday to GitHub pages](https://crazy-max.github.io/ghaction-github-pages/).
@ -56,6 +69,31 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
``` ```
### Sign commits
You can use the [Import GPG](https://github.com/crazy-max/ghaction-import-gpg) GitHub Action along with this one to sign commits:
```yaml
-
name: Import GPG key
uses: crazy-max/ghaction-import-gpg@v1
with:
git_user_signingkey: true
git_commit_gpgsign: true
env:
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
PASSPHRASE: ${{ secrets.PASSPHRASE }}
-
name: Deploy to GitHub Pages
if: success()
uses: crazy-max/ghaction-github-pages@v1
with:
target_branch: gh-pages
build_dir: public
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```
## Customizing ## Customizing
### inputs ### inputs
@ -69,8 +107,8 @@ Following inputs can be used as `step.with` keys
| `keep_history` | Bool | Create incremental commit instead of doing push force (default `false`) | | `keep_history` | Bool | Create incremental commit instead of doing push force (default `false`) |
| `allow_empty_commit` | Bool | Allow an empty commit to be created (default `true`) | | `allow_empty_commit` | Bool | Allow an empty commit to be created (default `true`) |
| `build_dir` | String | Build directory to deploy (**required**) | | `build_dir` | String | Build directory to deploy (**required**) |
| `committer_name` | String | Commit author's name (default [GITHUB_ACTOR](https://help.github.com/en/github/automating-your-workflow-with-github-actions/using-environment-variables#default-environment-variables) or `github-actions`) | | `committer` | String | Committer name and email address as `Display Name <joe@foo.bar>` (defaults to the GitHub Actions bot user) |
| `committer_email` | String | Commit author's email (default `<committer_name>@users.noreply.github.com`) | | `author` | String | Author name and email address as `Display Name <joe@foo.bar>` (defaults to the GitHub Actions bot user) |
| `commit_message` | String | Commit message (default `Deploy to GitHub pages`) | | `commit_message` | String | Commit message (default `Deploy to GitHub pages`) |
| `fqdn` | String | Write the given domain name to the CNAME file | | `fqdn` | String | Write the given domain name to the CNAME file |
@ -81,7 +119,7 @@ Following environment variables can be used as `step.env` keys
| Name | Description | | Name | Description |
|----------------|---------------------------------------| |----------------|---------------------------------------|
| `GITHUB_TOKEN` | [GITHUB_TOKEN](https://help.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token) as provided by `secrets` | | `GITHUB_TOKEN` | [GITHUB_TOKEN](https://help.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token) as provided by `secrets` |
| `GITHUB_PAT` | Use a [Personal Access Token](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/) if you want to deploy to another repo | | `GH_PAT` | Use a [Personal Access Token](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/) if you want to deploy to another repo |
## How can I help? ## How can I help?

View File

@ -25,11 +25,11 @@ inputs:
build_dir: build_dir:
description: 'Build directory to deploy' description: 'Build directory to deploy'
required: true required: true
committer_name: committer:
description: 'Commit author''s name' description: 'The committer name and email address'
required: false required: false
committer_email: author:
description: 'Commit author''s email' description: 'The author name and email address'
required: false required: false
commit_message: commit_message:
description: 'Commit message' description: 'Commit message'

594
dist/index.js generated vendored
View File

@ -1752,6 +1752,9 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
step((generator = generator.apply(thisArg, _arguments || [])).next()); step((generator = generator.apply(thisArg, _arguments || [])).next());
}); });
}; };
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
var __importStar = (this && this.__importStar) || function (mod) { var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod; if (mod && mod.__esModule) return mod;
var result = {}; var result = {};
@ -1760,98 +1763,99 @@ var __importStar = (this && this.__importStar) || function (mod) {
return result; return result;
}; };
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
const child_process = __importStar(__webpack_require__(129)); const addressparser_1 = __importDefault(__webpack_require__(977));
const core = __importStar(__webpack_require__(470));
const exec = __importStar(__webpack_require__(986));
const fs_extra_1 = __webpack_require__(226); const fs_extra_1 = __webpack_require__(226);
const fs = __importStar(__webpack_require__(747)); const fs = __importStar(__webpack_require__(747));
const os = __importStar(__webpack_require__(87)); const os = __importStar(__webpack_require__(87));
const path = __importStar(__webpack_require__(622)); const path = __importStar(__webpack_require__(622));
const core = __importStar(__webpack_require__(470));
const git = __importStar(__webpack_require__(453));
function run() { function run() {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
try { try {
const repo = core.getInput('repo') || process.env['GITHUB_REPOSITORY'] || ''; const repo = core.getInput('repo') || process.env['GITHUB_REPOSITORY'] || '';
const target_branch = core.getInput('target_branch') || 'gh-pages'; const targetBranch = core.getInput('target_branch') || git.defaults.targetBranch;
const keep_history = /true/i.test(core.getInput('keep_history')); const keepHistory = /true/i.test(core.getInput('keep_history'));
const allow_empty_commit = /true/i.test(core.getInput('allow_empty_commit')); const allowEmptyCommit = /true/i.test(core.getInput('allow_empty_commit'));
const build_dir = core.getInput('build_dir', { required: true }); const buildDir = core.getInput('build_dir', { required: true });
const committer_name = core.getInput('committer_name') || process.env['GITHUB_ACTOR'] || 'github-actions'; const committer = core.getInput('committer') || git.defaults.committer;
const committer_email = core.getInput('committer_email') || `${committer_name}@users.noreply.github.com`; const author = core.getInput('author') || git.defaults.author;
const commit_message = core.getInput('commit_message') || 'Deploy to GitHub pages'; const commitMessage = core.getInput('commit_message') || git.defaults.message;
const fqdn = core.getInput('fqdn'); const fqdn = core.getInput('fqdn');
if (!fs.existsSync(build_dir)) { if (!fs.existsSync(buildDir)) {
core.setFailed('⛔️ Build dir does not exist'); core.setFailed('Build dir does not exist');
return; return;
} }
let remote_url = String('https://'); let remoteURL = String('https://');
if (process.env['GITHUB_PAT']) { if (process.env['GH_PAT']) {
core.info(`✅ Use GITHUB_PAT`); core.info(`✅ Use GH_PAT`);
remote_url = remote_url.concat(process.env['GITHUB_PAT'].trim()); remoteURL = remoteURL.concat(process.env['GH_PAT'].trim());
} }
else if (process.env['GITHUB_TOKEN']) { else if (process.env['GITHUB_TOKEN']) {
core.info(`✅ Use GITHUB_TOKEN`); core.info(`✅ Use GITHUB_TOKEN`);
remote_url = remote_url.concat('x-access-token:', process.env['GITHUB_TOKEN'].trim()); remoteURL = remoteURL.concat('x-access-token:', process.env['GITHUB_TOKEN'].trim());
} }
else { else {
core.setFailed('❌️ You have to provide a GITHUB_TOKEN or GITHUB_PAT'); core.setFailed('You have to provide a GITHUB_TOKEN or GH_PAT');
return; return;
} }
remote_url = remote_url.concat('@github.com/', repo, '.git'); remoteURL = remoteURL.concat('@github.com/', repo, '.git');
core.debug(`remoteURL=${remoteURL}`);
const remoteBranchExists = yield git.remoteBranchExists(remoteURL, targetBranch);
core.debug(`remoteBranchExists=${remoteBranchExists}`);
const tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), 'github-pages-')); const tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), 'github-pages-'));
core.debug(`tmpdir=${tmpdir}`);
const currentdir = path.resolve('.'); const currentdir = path.resolve('.');
core.debug(`currentdir=${currentdir}`);
process.chdir(tmpdir); process.chdir(tmpdir);
const remote_branch_exists = child_process.execSync(`git ls-remote --heads ${remote_url} ${target_branch}`, { encoding: 'utf8' }).trim().length > if (keepHistory && remoteBranchExists) {
0; core.info('🌀 Cloning ${repo}');
if (keep_history && remote_branch_exists) { yield git.clone(remoteURL, targetBranch, '.');
yield exec.exec('git', ['clone', '--quiet', '--branch', target_branch, '--depth', '1', remote_url, '.']);
} }
else { else {
core.info(`🏃 Initializing local git repo`); core.info(` Initializing local git repo`);
yield exec.exec('git', ['init', '.']); yield git.init('.');
yield exec.exec('git', ['checkout', '--orphan', target_branch]); yield git.checkout(targetBranch);
} }
core.info(`🏃 Copying ${path.join(currentdir, build_dir)} contents to ${tmpdir}`); core.info(`🏃 Copying ${path.join(currentdir, buildDir)} contents to ${tmpdir}`);
fs_extra_1.copySync(path.join(currentdir, build_dir), tmpdir); yield fs_extra_1.copySync(path.join(currentdir, buildDir), tmpdir);
if (fqdn) { if (fqdn) {
core.info(`✍️ Writing ${fqdn} domain name to ${path.join(tmpdir, 'CNAME')}`); core.info(`✍️ Writing ${fqdn} domain name to ${path.join(tmpdir, 'CNAME')}`);
fs.writeFileSync(path.join(tmpdir, 'CNAME'), fqdn.trim()); yield fs.writeFileSync(path.join(tmpdir, 'CNAME'), fqdn.trim());
} }
const dirty = child_process.execSync(`git status --short`, { encoding: 'utf8' }).trim().length > 0; const isDirty = yield git.isDirty();
if (keep_history && remote_branch_exists && !dirty) { core.debug(`isDirty=${isDirty}`);
core.info('⚠️ There are no changes to commit, stopping.'); if (keepHistory && remoteBranchExists && !isDirty) {
core.info('⚠️ No changes to commit');
return; return;
} }
core.info(`🔨 Configuring git committer to be ${committer_name} <${committer_email}>`); const committerPrs = addressparser_1.default(committer)[0];
yield exec.exec('git', ['config', 'user.name', committer_name]); core.info(`🔨 Configuring git committer as ${committerPrs.name} <${committerPrs.address}>`);
yield exec.exec('git', ['config', 'user.email', committer_email]); yield git.setConfig('user.name', committerPrs.name);
try { yield git.setConfig('user.email', committerPrs.address);
child_process.execSync('git status --porcelain').toString(); if (!(yield git.hasChanges())) {
}
catch (err) {
core.info('⚠️ Nothing to deploy'); core.info('⚠️ Nothing to deploy');
return; return;
} }
yield exec.exec('git', ['add', '--all', '.']); core.info(`📐 Updating index of working tree`);
let gitCommitCmd = []; yield git.add('.');
gitCommitCmd.push('commit'); core.info(`📦 Committing changes`);
if (allow_empty_commit) { if (allowEmptyCommit) {
core.info(`✅ Allow empty commit`); core.info(`✅ Allow empty commit`);
gitCommitCmd.push('--allow-empty');
} }
gitCommitCmd.push('-m', commit_message); const authorPrs = addressparser_1.default(author)[0];
yield exec.exec('git', gitCommitCmd); core.info(`🔨 Configuring git author as ${authorPrs.name} <${authorPrs.address}>`);
yield exec.exec('git', ['show', '--stat-count=10', 'HEAD']); yield git.commit(allowEmptyCommit, `${authorPrs.name} <${authorPrs.address}>`, commitMessage);
let gitPushCmd = []; yield git.showStat(10).then(output => {
gitPushCmd.push('push', '--quiet'); core.info(output);
if (!keep_history) { });
core.info(`🏃 Pushing ${buildDir} directory to ${targetBranch} branch on ${repo} repo`);
if (!keepHistory) {
core.info(`✅ Force push`); core.info(`✅ Force push`);
gitPushCmd.push('--force');
} }
gitPushCmd.push(remote_url, target_branch); yield git.push(remoteURL, targetBranch, !keepHistory);
core.info(`🏃 Deploying ${build_dir} directory to ${target_branch} branch on ${repo} repo`);
yield exec.exec('git', gitPushCmd);
process.chdir(currentdir); process.chdir(currentdir);
core.info(`🎉 Content of ${build_dir} has been deployed to GitHub Pages.`); core.info(`🎉 Content of ${buildDir} has been deployed to GitHub Pages.`);
} }
catch (error) { catch (error) {
core.setFailed(error.message); core.setFailed(error.message);
@ -1859,7 +1863,7 @@ function run() {
}); });
} }
run(); run();
//# sourceMappingURL=main.js.map
/***/ }), /***/ }),
@ -2416,6 +2420,136 @@ function escapeProperty(s) {
/***/ }), /***/ }),
/***/ 453:
/***/ (function(__unusedmodule, exports, __webpack_require__) {
"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 exec = __importStar(__webpack_require__(807));
exports.defaults = {
targetBranch: 'gh-pages',
committer: 'GitHub <noreply@github.com>',
author: 'github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>',
message: 'Deploy to GitHub pages'
};
const git = (args = []) => __awaiter(void 0, void 0, void 0, function* () {
return yield exec.exec(`git`, args, true).then(res => {
if (res.stderr != '' && !res.success) {
throw new Error(res.stderr);
}
return res.stdout.trim();
});
});
function remoteBranchExists(remoteURL, branch) {
return __awaiter(this, void 0, void 0, function* () {
return yield git(['ls-remote', '--heads', remoteURL, branch]).then(output => {
return output.trim().length > 0;
});
});
}
exports.remoteBranchExists = remoteBranchExists;
function clone(remoteURL, branch, dest) {
return __awaiter(this, void 0, void 0, function* () {
yield git(['clone', '--quiet', '--branch', branch, '--depth', '1', remoteURL, dest]);
});
}
exports.clone = clone;
function init(dest) {
return __awaiter(this, void 0, void 0, function* () {
yield git(['init', dest]);
});
}
exports.init = init;
function checkout(branch) {
return __awaiter(this, void 0, void 0, function* () {
yield git(['checkout', '--orphan', branch]);
});
}
exports.checkout = checkout;
function isDirty() {
return __awaiter(this, void 0, void 0, function* () {
return yield git(['status', '--short']).then(output => {
return output.trim().length > 0;
});
});
}
exports.isDirty = isDirty;
function hasChanges() {
return __awaiter(this, void 0, void 0, function* () {
return yield git(['status', '--porcelain']).then(output => {
return output.trim().length > 0;
});
});
}
exports.hasChanges = hasChanges;
function setConfig(key, value) {
return __awaiter(this, void 0, void 0, function* () {
yield git(['config', key, value]);
});
}
exports.setConfig = setConfig;
function add(pattern) {
return __awaiter(this, void 0, void 0, function* () {
yield git(['add', '--all', pattern]);
});
}
exports.add = add;
function commit(allowEmptyCommit, author, message) {
return __awaiter(this, void 0, void 0, function* () {
let args = [];
args.push('commit');
if (allowEmptyCommit) {
args.push('--allow-empty');
}
if (author !== '') {
args.push('--author', author);
}
args.push('--message', message);
yield git(args);
});
}
exports.commit = commit;
function showStat(count) {
return __awaiter(this, void 0, void 0, function* () {
return yield git(['show', `--stat-count=${count}`, 'HEAD']).then(output => {
return output;
});
});
}
exports.showStat = showStat;
function push(remoteURL, branch, force) {
return __awaiter(this, void 0, void 0, function* () {
let args = [];
args.push('push', '--quiet');
if (force) {
args.push('--force');
}
args.push(remoteURL, branch);
yield git(args);
});
}
exports.push = push;
//# sourceMappingURL=git.js.map
/***/ }),
/***/ 469: /***/ 469:
/***/ (function(module, __unusedexports, __webpack_require__) { /***/ (function(module, __unusedexports, __webpack_require__) {
@ -4268,6 +4402,55 @@ module.exports = {
} }
/***/ }),
/***/ 807:
/***/ (function(__unusedmodule, exports, __webpack_require__) {
"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 actionsExec = __importStar(__webpack_require__(986));
exports.exec = (command, args = [], silent) => __awaiter(void 0, void 0, void 0, function* () {
let stdout = '';
let stderr = '';
const options = {
silent: silent,
ignoreReturnCode: true
};
options.listeners = {
stdout: (data) => {
stdout += data.toString();
},
stderr: (data) => {
stderr += data.toString();
}
};
const returnCode = yield actionsExec.exec(command, args, options);
return {
success: returnCode === 0,
stdout: stdout.trim(),
stderr: stderr.trim()
};
});
//# sourceMappingURL=exec.js.map
/***/ }), /***/ }),
/***/ 849: /***/ 849:
@ -4737,6 +4920,305 @@ module.exports = {
} }
/***/ }),
/***/ 977:
/***/ (function(module) {
"use strict";
// expose to the world
module.exports = addressparser;
/**
* Parses structured e-mail addresses from an address field
*
* Example:
*
* 'Name <address@domain>'
*
* will be converted to
*
* [{name: 'Name', address: 'address@domain'}]
*
* @param {String} str Address field
* @return {Array} An array of address objects
*/
function addressparser(str) {
var tokenizer = new Tokenizer(str);
var tokens = tokenizer.tokenize();
var addresses = [];
var address = [];
var parsedAddresses = [];
tokens.forEach(function (token) {
if (token.type === 'operator' && (token.value === ',' || token.value === ';')) {
if (address.length) {
addresses.push(address);
}
address = [];
} else {
address.push(token);
}
});
if (address.length) {
addresses.push(address);
}
addresses.forEach(function (address) {
address = _handleAddress(address);
if (address.length) {
parsedAddresses = parsedAddresses.concat(address);
}
});
return parsedAddresses;
}
/**
* Converts tokens for a single address into an address object
*
* @param {Array} tokens Tokens object
* @return {Object} Address object
*/
function _handleAddress(tokens) {
var token;
var isGroup = false;
var state = 'text';
var address;
var addresses = [];
var data = {
address: [],
comment: [],
group: [],
text: []
};
var i;
var len;
// Filter out <addresses>, (comments) and regular text
for (i = 0, len = tokens.length; i < len; i++) {
token = tokens[i];
if (token.type === 'operator') {
switch (token.value) {
case '<':
state = 'address';
break;
case '(':
state = 'comment';
break;
case ':':
state = 'group';
isGroup = true;
break;
default:
state = 'text';
}
} else if (token.value) {
if (state === 'address') {
// handle use case where unquoted name includes a "<"
// Apple Mail truncates everything between an unexpected < and an address
// and so will we
token.value = token.value.replace(/^[^<]*<\s*/, '');
}
data[state].push(token.value);
}
}
// If there is no text but a comment, replace the two
if (!data.text.length && data.comment.length) {
data.text = data.comment;
data.comment = [];
}
if (isGroup) {
// http://tools.ietf.org/html/rfc2822#appendix-A.1.3
data.text = data.text.join(' ');
addresses.push({
name: data.text || (address && address.name),
group: data.group.length ? addressparser(data.group.join(',')) : []
});
} else {
// If no address was found, try to detect one from regular text
if (!data.address.length && data.text.length) {
for (i = data.text.length - 1; i >= 0; i--) {
if (data.text[i].match(/^[^@\s]+@[^@\s]+$/)) {
data.address = data.text.splice(i, 1);
break;
}
}
var _regexHandler = function (address) {
if (!data.address.length) {
data.address = [address.trim()];
return ' ';
} else {
return address;
}
};
// still no address
if (!data.address.length) {
for (i = data.text.length - 1; i >= 0; i--) {
// fixed the regex to parse email address correctly when email address has more than one @
data.text[i] = data.text[i].replace(/\s*\b[^@\s]+@[^\s]+\b\s*/, _regexHandler).trim();
if (data.address.length) {
break;
}
}
}
}
// If there's still is no text but a comment exixts, replace the two
if (!data.text.length && data.comment.length) {
data.text = data.comment;
data.comment = [];
}
// Keep only the first address occurence, push others to regular text
if (data.address.length > 1) {
data.text = data.text.concat(data.address.splice(1));
}
// Join values with spaces
data.text = data.text.join(' ');
data.address = data.address.join(' ');
if (!data.address && isGroup) {
return [];
} else {
address = {
address: data.address || data.text || '',
name: data.text || data.address || ''
};
if (address.address === address.name) {
if ((address.address || '').match(/@/)) {
address.name = '';
} else {
address.address = '';
}
}
addresses.push(address);
}
}
return addresses;
}
/**
* Creates a Tokenizer object for tokenizing address field strings
*
* @constructor
* @param {String} str Address field string
*/
function Tokenizer(str) {
this.str = (str || '').toString();
this.operatorCurrent = '';
this.operatorExpecting = '';
this.node = null;
this.escaped = false;
this.list = [];
}
/**
* Operator tokens and which tokens are expected to end the sequence
*/
Tokenizer.prototype.operators = {
'"': '"',
'(': ')',
'<': '>',
',': '',
':': ';',
// Semicolons are not a legal delimiter per the RFC2822 grammar other
// than for terminating a group, but they are also not valid for any
// other use in this context. Given that some mail clients have
// historically allowed the semicolon as a delimiter equivalent to the
// comma in their UI, it makes sense to treat them the same as a comma
// when used outside of a group.
';': ''
};
/**
* Tokenizes the original input string
*
* @return {Array} An array of operator|text tokens
*/
Tokenizer.prototype.tokenize = function () {
var chr, list = [];
for (var i = 0, len = this.str.length; i < len; i++) {
chr = this.str.charAt(i);
this.checkChar(chr);
}
this.list.forEach(function (node) {
node.value = (node.value || '').toString().trim();
if (node.value) {
list.push(node);
}
});
return list;
};
/**
* Checks if a character is an operator or text and acts accordingly
*
* @param {String} chr Character from the address field
*/
Tokenizer.prototype.checkChar = function (chr) {
if ((chr in this.operators || chr === '\\') && this.escaped) {
this.escaped = false;
} else if (this.operatorExpecting && chr === this.operatorExpecting) {
this.node = {
type: 'operator',
value: chr
};
this.list.push(this.node);
this.node = null;
this.operatorExpecting = '';
this.escaped = false;
return;
} else if (!this.operatorExpecting && chr in this.operators) {
this.node = {
type: 'operator',
value: chr
};
this.list.push(this.node);
this.node = null;
this.operatorExpecting = this.operators[chr];
this.escaped = false;
return;
}
if (!this.escaped && chr === '\\') {
this.escaped = true;
return;
}
if (!this.node) {
this.node = {
type: 'text',
value: ''
};
this.list.push(this.node);
}
if (this.escaped && chr !== '\\') {
this.node.value += '\\';
}
this.node.value += chr;
this.escaped = false;
};
/***/ }), /***/ }),
/***/ 986: /***/ 986:

5
package-lock.json generated
View File

@ -42,6 +42,11 @@
"integrity": "sha512-Qq3bMuonkcnV/96jhy9SQYdh39NXHxNMJ1O31ZFzWG9n52fR2DLtgrNzhj/ahlEjnBziMLGVWDbaS9sf03/fEw==", "integrity": "sha512-Qq3bMuonkcnV/96jhy9SQYdh39NXHxNMJ1O31ZFzWG9n52fR2DLtgrNzhj/ahlEjnBziMLGVWDbaS9sf03/fEw==",
"dev": true "dev": true
}, },
"addressparser": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/addressparser/-/addressparser-1.0.1.tgz",
"integrity": "sha1-R6++GiqSYhkdtoOOT9HTm0CCF0Y="
},
"at-least-node": { "at-least-node": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",

View File

@ -11,7 +11,7 @@
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/crazy-max/ghaction-docker-buildx.git" "url": "git+https://github.com/crazy-max/ghaction-github-pages.git"
}, },
"keywords": [ "keywords": [
"actions", "actions",
@ -24,6 +24,7 @@
"dependencies": { "dependencies": {
"@actions/core": "^1.2.4", "@actions/core": "^1.2.4",
"@actions/exec": "^1.0.4", "@actions/exec": "^1.0.4",
"addressparser": "^1.0.1",
"fs-extra": "^9.0.0" "fs-extra": "^9.0.0"
}, },
"devDependencies": { "devDependencies": {
@ -36,4 +37,4 @@
"typescript": "^3.8.3", "typescript": "^3.8.3",
"typescript-formatter": "^7.2.2" "typescript-formatter": "^7.2.2"
} }
} }

24
src/addressparser.d.ts vendored Normal file
View File

@ -0,0 +1,24 @@
declare namespace addressparser {
interface Address {
name: string;
address: string;
}
}
/**
* Parses structured e-mail addresses from an address field
*
* Example:
*
* 'Name <address@domain>'
*
* will be converted to
*
* [{name: 'Name', address: 'address@domain'}]
*
* @param str Address field
* @return An array of address objects
*/
declare function addressparser(address: string): addressparser.Address[];
export = addressparser;

34
src/exec.ts Normal file
View File

@ -0,0 +1,34 @@
import * as actionsExec from '@actions/exec';
import {ExecOptions} from '@actions/exec';
export interface ExecResult {
success: boolean;
stdout: string;
stderr: string;
}
export const exec = async (command: string, args: string[] = [], silent: boolean): Promise<ExecResult> => {
let stdout: string = '';
let stderr: string = '';
const options: ExecOptions = {
silent: silent,
ignoreReturnCode: true
};
options.listeners = {
stdout: (data: Buffer) => {
stdout += data.toString();
},
stderr: (data: Buffer) => {
stderr += data.toString();
}
};
const returnCode: number = await actionsExec.exec(command, args, options);
return {
success: returnCode === 0,
stdout: stdout.trim(),
stderr: stderr.trim()
};
};

84
src/git.ts Normal file
View File

@ -0,0 +1,84 @@
import * as exec from './exec';
export const defaults = {
targetBranch: 'gh-pages',
committer: 'GitHub <noreply@github.com>',
author: 'github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>',
message: 'Deploy to GitHub pages'
};
const git = async (args: string[] = []): Promise<string> => {
return await exec.exec(`git`, args, true).then(res => {
if (res.stderr != '' && !res.success) {
throw new Error(res.stderr);
}
return res.stdout.trim();
});
};
export async function remoteBranchExists(remoteURL: string, branch: string): Promise<boolean> {
return await git(['ls-remote', '--heads', remoteURL, branch]).then(output => {
return output.trim().length > 0;
});
}
export async function clone(remoteURL: string, branch: string, dest: string): Promise<void> {
await git(['clone', '--quiet', '--branch', branch, '--depth', '1', remoteURL, dest]);
}
export async function init(dest: string): Promise<void> {
await git(['init', dest]);
}
export async function checkout(branch: string): Promise<void> {
await git(['checkout', '--orphan', branch]);
}
export async function isDirty(): Promise<boolean> {
return await git(['status', '--short']).then(output => {
return output.trim().length > 0;
});
}
export async function hasChanges(): Promise<boolean> {
return await git(['status', '--porcelain']).then(output => {
return output.trim().length > 0;
});
}
export async function setConfig(key: string, value: string): Promise<void> {
await git(['config', key, value]);
}
export async function add(pattern: string): Promise<void> {
await git(['add', '--all', pattern]);
}
export async function commit(allowEmptyCommit: boolean, author: string, message: string): Promise<void> {
let args: Array<string> = [];
args.push('commit');
if (allowEmptyCommit) {
args.push('--allow-empty');
}
if (author !== '') {
args.push('--author', author);
}
args.push('--message', message);
await git(args);
}
export async function showStat(count: number): Promise<string> {
return await git(['show', `--stat-count=${count}`, 'HEAD']).then(output => {
return output;
});
}
export async function push(remoteURL: string, branch: string, force: boolean): Promise<void> {
let args: Array<string> = [];
args.push('push', '--quiet');
if (force) {
args.push('--force');
}
args.push(remoteURL, branch);
await git(args);
}

View File

@ -1,107 +1,107 @@
import * as child_process from 'child_process'; import addressparser from 'addressparser';
import * as core from '@actions/core';
import * as exec from '@actions/exec';
import {copySync} from 'fs-extra'; import {copySync} from 'fs-extra';
import * as fs from 'fs'; import * as fs from 'fs';
import * as os from 'os'; import * as os from 'os';
import * as path from 'path'; import * as path from 'path';
import * as core from '@actions/core';
import * as git from './git';
async function run() { async function run() {
try { try {
const repo: string = core.getInput('repo') || process.env['GITHUB_REPOSITORY'] || ''; const repo: string = core.getInput('repo') || process.env['GITHUB_REPOSITORY'] || '';
const target_branch: string = core.getInput('target_branch') || 'gh-pages'; const targetBranch: string = core.getInput('target_branch') || git.defaults.targetBranch;
const keep_history: boolean = /true/i.test(core.getInput('keep_history')); const keepHistory: boolean = /true/i.test(core.getInput('keep_history'));
const allow_empty_commit: boolean = /true/i.test(core.getInput('allow_empty_commit')); const allowEmptyCommit: boolean = /true/i.test(core.getInput('allow_empty_commit'));
const build_dir: string = core.getInput('build_dir', {required: true}); const buildDir: string = core.getInput('build_dir', {required: true});
const committer_name: string = core.getInput('committer_name') || process.env['GITHUB_ACTOR'] || 'github-actions'; const committer: string = core.getInput('committer') || git.defaults.committer;
const committer_email: string = core.getInput('committer_email') || `${committer_name}@users.noreply.github.com`; const author: string = core.getInput('author') || git.defaults.author;
const commit_message: string = core.getInput('commit_message') || 'Deploy to GitHub pages'; const commitMessage: string = core.getInput('commit_message') || git.defaults.message;
const fqdn: string = core.getInput('fqdn'); const fqdn: string = core.getInput('fqdn');
if (!fs.existsSync(build_dir)) { if (!fs.existsSync(buildDir)) {
core.setFailed('⛔️ Build dir does not exist'); core.setFailed('Build dir does not exist');
return; return;
} }
let remote_url = String('https://'); let remoteURL = String('https://');
if (process.env['GITHUB_PAT']) { if (process.env['GH_PAT']) {
core.info(`✅ Use GITHUB_PAT`); core.info(`✅ Use GH_PAT`);
remote_url = remote_url.concat(process.env['GITHUB_PAT'].trim()); remoteURL = remoteURL.concat(process.env['GH_PAT'].trim());
} else if (process.env['GITHUB_TOKEN']) { } else if (process.env['GITHUB_TOKEN']) {
core.info(`✅ Use GITHUB_TOKEN`); core.info(`✅ Use GITHUB_TOKEN`);
remote_url = remote_url.concat('x-access-token:', process.env['GITHUB_TOKEN'].trim()); remoteURL = remoteURL.concat('x-access-token:', process.env['GITHUB_TOKEN'].trim());
} else { } else {
core.setFailed('❌️ You have to provide a GITHUB_TOKEN or GITHUB_PAT'); core.setFailed('You have to provide a GITHUB_TOKEN or GH_PAT');
return; return;
} }
remote_url = remote_url.concat('@github.com/', repo, '.git'); remoteURL = remoteURL.concat('@github.com/', repo, '.git');
core.debug(`remoteURL=${remoteURL}`);
const remoteBranchExists: boolean = await git.remoteBranchExists(remoteURL, targetBranch);
core.debug(`remoteBranchExists=${remoteBranchExists}`);
const tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), 'github-pages-')); const tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), 'github-pages-'));
core.debug(`tmpdir=${tmpdir}`);
const currentdir = path.resolve('.'); const currentdir = path.resolve('.');
core.debug(`currentdir=${currentdir}`);
process.chdir(tmpdir); process.chdir(tmpdir);
const remote_branch_exists = if (keepHistory && remoteBranchExists) {
child_process.execSync(`git ls-remote --heads ${remote_url} ${target_branch}`, {encoding: 'utf8'}).trim().length > core.info('🌀 Cloning ${repo}');
0; await git.clone(remoteURL, targetBranch, '.');
if (keep_history && remote_branch_exists) {
await exec.exec('git', ['clone', '--quiet', '--branch', target_branch, '--depth', '1', remote_url, '.']);
} else { } else {
core.info(`🏃 Initializing local git repo`); core.info(`✨ Initializing local git repo`);
await exec.exec('git', ['init', '.']); await git.init('.');
await exec.exec('git', ['checkout', '--orphan', target_branch]); await git.checkout(targetBranch);
} }
core.info(`🏃 Copying ${path.join(currentdir, build_dir)} contents to ${tmpdir}`); core.info(`🏃 Copying ${path.join(currentdir, buildDir)} contents to ${tmpdir}`);
copySync(path.join(currentdir, build_dir), tmpdir); await copySync(path.join(currentdir, buildDir), tmpdir);
if (fqdn) { if (fqdn) {
core.info(`✍️ Writing ${fqdn} domain name to ${path.join(tmpdir, 'CNAME')}`); core.info(`✍️ Writing ${fqdn} domain name to ${path.join(tmpdir, 'CNAME')}`);
fs.writeFileSync(path.join(tmpdir, 'CNAME'), fqdn.trim()); await fs.writeFileSync(path.join(tmpdir, 'CNAME'), fqdn.trim());
} }
const dirty = child_process.execSync(`git status --short`, {encoding: 'utf8'}).trim().length > 0; const isDirty: boolean = await git.isDirty();
if (keep_history && remote_branch_exists && !dirty) { core.debug(`isDirty=${isDirty}`);
core.info('⚠️ There are no changes to commit, stopping.'); if (keepHistory && remoteBranchExists && !isDirty) {
core.info('⚠️ No changes to commit');
return; return;
} }
core.info(`🔨 Configuring git committer to be ${committer_name} <${committer_email}>`); const committerPrs: addressparser.Address = addressparser(committer)[0];
await exec.exec('git', ['config', 'user.name', committer_name]); core.info(`🔨 Configuring git committer as ${committerPrs.name} <${committerPrs.address}>`);
await exec.exec('git', ['config', 'user.email', committer_email]); await git.setConfig('user.name', committerPrs.name);
await git.setConfig('user.email', committerPrs.address);
try { if (!(await git.hasChanges())) {
child_process.execSync('git status --porcelain').toString();
} catch (err) {
core.info('⚠️ Nothing to deploy'); core.info('⚠️ Nothing to deploy');
return; return;
} }
await exec.exec('git', ['add', '--all', '.']); core.info(`📐 Updating index of working tree`);
await git.add('.');
let gitCommitCmd: Array<string> = []; core.info(`📦 Committing changes`);
gitCommitCmd.push('commit'); if (allowEmptyCommit) {
if (allow_empty_commit) {
core.info(`✅ Allow empty commit`); core.info(`✅ Allow empty commit`);
gitCommitCmd.push('--allow-empty');
} }
gitCommitCmd.push('-m', commit_message); const authorPrs: addressparser.Address = addressparser(author)[0];
await exec.exec('git', gitCommitCmd); core.info(`🔨 Configuring git author as ${authorPrs.name} <${authorPrs.address}>`);
await git.commit(allowEmptyCommit, `${authorPrs.name} <${authorPrs.address}>`, commitMessage);
await git.showStat(10).then(output => {
core.info(output);
});
await exec.exec('git', ['show', '--stat-count=10', 'HEAD']); core.info(`🏃 Pushing ${buildDir} directory to ${targetBranch} branch on ${repo} repo`);
if (!keepHistory) {
let gitPushCmd: Array<string> = [];
gitPushCmd.push('push', '--quiet');
if (!keep_history) {
core.info(`✅ Force push`); core.info(`✅ Force push`);
gitPushCmd.push('--force');
} }
gitPushCmd.push(remote_url, target_branch); await git.push(remoteURL, targetBranch, !keepHistory);
core.info(`🏃 Deploying ${build_dir} directory to ${target_branch} branch on ${repo} repo`);
await exec.exec('git', gitPushCmd);
process.chdir(currentdir); process.chdir(currentdir);
core.info(`🎉 Content of ${build_dir} has been deployed to GitHub Pages.`); core.info(`🎉 Content of ${buildDir} has been deployed to GitHub Pages.`);
} catch (error) { } catch (error) {
core.setFailed(error.message); core.setFailed(error.message);
} }

View File

@ -10,7 +10,8 @@
"rootDir": "./src", "rootDir": "./src",
"strict": true, "strict": true,
"noImplicitAny": false, "noImplicitAny": false,
"esModuleInterop": true "esModuleInterop": true,
"sourceMap": true
}, },
"exclude": ["node_modules"] "exclude": ["node_modules", "**/*.test.ts"]
} }