MVP (#5)
* Hey!
* Remove event option (its broken)
* Change up logging
* Adds pull_request opened test
* Log error
* Import logic
* This should be async
* Execute a module action
* Log moveResult
* Throw if failures exist
* Swapping API libs
* Well there's that
* Adds a bit of functionality
* 💄 eventModule handler
* Add payload log
* Change icon, fix ref type exclusion
* Fix typo
* Fixes body resolution
* Adds PR movement automations
* Nvm don't need this.
This commit is contained in:
parent
a15359d149
commit
9a63f16fbb
|
@ -3,7 +3,7 @@ FROM node:slim
|
||||||
LABEL "com.github.actions.name"="Zenhub Automations"
|
LABEL "com.github.actions.name"="Zenhub Automations"
|
||||||
LABEL "com.github.actions.description"="Automate zenhub with ease"
|
LABEL "com.github.actions.description"="Automate zenhub with ease"
|
||||||
# Here are all of the available icons: https://feathericons.com/
|
# Here are all of the available icons: https://feathericons.com/
|
||||||
LABEL "com.github.actions.icon"="move"
|
LABEL "com.github.actions.icon"="power"
|
||||||
# And all of the available colors: https://developer.github.com/actions/creating-github-actions/creating-a-docker-container/#label
|
# And all of the available colors: https://developer.github.com/actions/creating-github-actions/creating-a-docker-container/#label
|
||||||
LABEL "com.github.actions.color"="gray-dark"
|
LABEL "com.github.actions.color"="gray-dark"
|
||||||
|
|
||||||
|
|
35
index.js
35
index.js
|
@ -1,6 +1,33 @@
|
||||||
const { Toolkit } = require('actions-toolkit')
|
const { Toolkit } = require('actions-toolkit'),
|
||||||
|
{ join } = require('path');
|
||||||
|
|
||||||
|
const tools = new Toolkit();
|
||||||
|
|
||||||
// Run your GitHub Action!
|
|
||||||
Toolkit.run(async tools => {
|
Toolkit.run(async tools => {
|
||||||
tools.exit.success('We did it!')
|
const handlerRef = `${tools.context.event}.js`;
|
||||||
})
|
|
||||||
|
tools.log.info(`Trying to load handler: "${handlerRef}"...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
var eventModule = require(`./lib/handlers/${handlerRef}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
return tools.exit.neutral('Failed to load module for event. No action necessary.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const moduleAction = eventModule[tools.context.payload.action] || eventModule[tools.context.payload.ref_type];
|
||||||
|
|
||||||
|
console.log(tools.context.payload);
|
||||||
|
|
||||||
|
if (!moduleAction) {
|
||||||
|
return tools.exit.neutral('Failed to find sub handler. No action necessary.');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await moduleAction(tools);
|
||||||
|
} catch (e) {
|
||||||
|
return tools.exit.failure(`Failed to run event handler: ${e}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
tools.exit.success('Executed event handler.');
|
||||||
|
});
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
const { Toolkit } = require('actions-toolkit')
|
|
||||||
|
|
||||||
describe('Zenhub Automations', () => {
|
|
||||||
let action, tools
|
|
||||||
|
|
||||||
// Mock Toolkit.run to define `action` so we can call it
|
|
||||||
Toolkit.run = jest.fn((actionFn) => { action = actionFn })
|
|
||||||
// Load up our entrypoint file
|
|
||||||
require('.')
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
// Create a new Toolkit instance
|
|
||||||
tools = new Toolkit()
|
|
||||||
// Mock methods on it!
|
|
||||||
tools.exit.success = jest.fn()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('exits successfully', () => {
|
|
||||||
action(tools)
|
|
||||||
expect(tools.exit.success).toHaveBeenCalled()
|
|
||||||
expect(tools.exit.success).toHaveBeenCalledWith('We did it!')
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
const zh = require('../zh-client'),
|
||||||
|
resolveIssueNumbers = require('../resolve-issue-number'),
|
||||||
|
{ INPROG_COLUMN } = process.env;
|
||||||
|
|
||||||
|
exports.branch = async function handleCreatedBranch (tools) {
|
||||||
|
tools.log.info('Handling created branch...');
|
||||||
|
|
||||||
|
const failures = [],
|
||||||
|
{ payload } = tools.context;
|
||||||
|
|
||||||
|
let issueNumbers = resolveIssueNumbers(payload.ref);
|
||||||
|
|
||||||
|
if (!issueNumbers || issueNumbers.length < 1) {
|
||||||
|
return tools.exit.neutral('No issues detected to act upon.');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < issueNumbers.length; i++) {
|
||||||
|
let issueNo = issueNumbers[i];
|
||||||
|
|
||||||
|
// Assign sender to issue(s)
|
||||||
|
try {
|
||||||
|
tools.log.info(`Adding assignees for #${issueNo}...`);
|
||||||
|
|
||||||
|
await tools.github.issues.addAssignees({
|
||||||
|
owner: payload.repository.owner.login,
|
||||||
|
repo: payload.repository.name,
|
||||||
|
issue_number: issueNo,
|
||||||
|
assignees: [ payload.sender.login ]
|
||||||
|
});
|
||||||
|
|
||||||
|
tools.log.info(`Added assignees for #${issueNo}.`);
|
||||||
|
} catch (e) {
|
||||||
|
failures.push(`Failed to added assignees for #${issueNo}: ${e}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move issue(s) to INPROG_COLUMN
|
||||||
|
if (INPROG_COLUMN) {
|
||||||
|
try {
|
||||||
|
tools.log.info(`Moving issue #${issueNo} to in progress...`);
|
||||||
|
|
||||||
|
await zh.issues.moveIssueBetweenPipelines(payload.repository.id, issueNo, {
|
||||||
|
pipeline_id: INPROG_COLUMN,
|
||||||
|
position: 'top'
|
||||||
|
});
|
||||||
|
|
||||||
|
tools.log.info(`Moved #${issueNo} to in progress.`);
|
||||||
|
} catch (e) {
|
||||||
|
failures.push(`Failed to move #${issueNo} to in progress: ${e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failures.length) {
|
||||||
|
throw new Error(`Failed to execute some actions: ${failures.map(x => x.message || x).join(', ')}`);
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,50 @@
|
||||||
|
const zh = require('../zh-client'),
|
||||||
|
resolveIssueNumbers = require('../resolve-issue-number'),
|
||||||
|
{
|
||||||
|
PR_COLUMN,
|
||||||
|
REVIEW_COLUMN
|
||||||
|
} = process.env;
|
||||||
|
|
||||||
|
exports.opened = async function handleOpenedPR (tools) {
|
||||||
|
tools.log.info('Handling opened PR...');
|
||||||
|
|
||||||
|
const failures = [],
|
||||||
|
{ payload } = tools.context;
|
||||||
|
|
||||||
|
// Move PR to PR issue column specified by ENV var
|
||||||
|
if (PR_COLUMN) {
|
||||||
|
tools.log.info('ZH :: Moving opened PR...');
|
||||||
|
try {
|
||||||
|
await zh.issues.moveIssueBetweenPipelines(payload.repository.id, payload.number, {
|
||||||
|
pipeline_id: PR_COLUMN,
|
||||||
|
position: 'top'
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
failures.push(e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tools.log.info('ZH :: Skipped moving opened PR because `PR_COLUMN` is undefined.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move PR closes to REVIEW_COLUMN
|
||||||
|
if (REVIEW_COLUMN) {
|
||||||
|
let issueNumbers = resolveIssueNumbers(payload.pull_request.head.ref, payload.pull_request.body);
|
||||||
|
|
||||||
|
for (let i = 0; i < issueNumbers.length; i++) {
|
||||||
|
let issueNo = issueNumbers[i];
|
||||||
|
|
||||||
|
try {
|
||||||
|
await zh.issues.moveIssueBetweenPipelines(payload.repository.id, issueNo, {
|
||||||
|
pipeline_id: REVIEW_COLUMN,
|
||||||
|
position: 'top'
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
failures.push(`Failed to move issue ${issueNo}: ${e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failures.length) {
|
||||||
|
throw new Error(`Failed to execute some actions: ${failures.map(x => x.message || x).join(', ')}`);
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,32 @@
|
||||||
|
const { CLOSING_VERBS } = process.env;
|
||||||
|
|
||||||
|
const closingVerbs = CLOSING_VERBS ?
|
||||||
|
// converts CLOSING_VERBS=test,abc to [ 'test', 'abc' ]
|
||||||
|
CLOSING_VERBS.split(',') :
|
||||||
|
// default
|
||||||
|
[ 'closes', 'fixes', 'fixed', 'resolves', 'resolved', 'gotem' ];
|
||||||
|
|
||||||
|
const descMatching = new RegExp(`(?:${closingVerbs.join('|')}) +([\\d, #]+)`, 'i');
|
||||||
|
|
||||||
|
module.exports = function extractIssueNumbers (...str) {
|
||||||
|
return str.reduce((issueNumbers, s) => {
|
||||||
|
// branch mode
|
||||||
|
if (s.indexOf(' ') < 0) {
|
||||||
|
let brMatch = /#(\d+)/.exec(str);
|
||||||
|
|
||||||
|
if (brMatch && brMatch[1]) {
|
||||||
|
issueNumbers.push(brMatch[1]);
|
||||||
|
return issueNumbers;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// plain mode
|
||||||
|
let match = descMatching.exec(str);
|
||||||
|
|
||||||
|
if (!match || !match[1]) {
|
||||||
|
return issueNumbers;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [ ...issueNumbers, ...match[1].replace(/\s|#/g, '').split(',') ];
|
||||||
|
}, []);
|
||||||
|
};
|
|
@ -0,0 +1,59 @@
|
||||||
|
const chai = require('chai'),
|
||||||
|
expect = chai.expect;
|
||||||
|
|
||||||
|
const resolveNumbers = require('./resolve-issue-number');
|
||||||
|
|
||||||
|
describe('Unit :: Issue Number Resolution', () => {
|
||||||
|
it('should be a function', () => {
|
||||||
|
expect(resolveNumbers).to.be.a('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve a branch issue', () => {
|
||||||
|
const cases = {
|
||||||
|
'feature/#123-tester': [ '123' ],
|
||||||
|
'enhancement/#22234': [ '22234' ],
|
||||||
|
'bug/#888-someiss': [ '888' ],
|
||||||
|
'#122': [ '122' ]
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.keys(cases).forEach(branchName => {
|
||||||
|
let result = resolveNumbers(branchName),
|
||||||
|
expected = cases[branchName];
|
||||||
|
|
||||||
|
expect(result).to.have.lengthOf(expected.length);
|
||||||
|
expect(result).to.have.members(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve a description issue', () => {
|
||||||
|
const baseCases = {
|
||||||
|
'[verb] #123': [ '123' ],
|
||||||
|
'[verb] #123, #444': [ '123', '444' ],
|
||||||
|
'heyo [verb] #123': [ '123' ],
|
||||||
|
'heyo [verb] #123, #333, #456': [ '123', '333', '456' ]
|
||||||
|
};
|
||||||
|
|
||||||
|
const verbs = [ 'Closes', 'closes', 'fixes', 'fixed', 'resolves', 'resolved', 'gotem' ];
|
||||||
|
|
||||||
|
const cases = Object.keys(baseCases).reduce((c, desc) => {
|
||||||
|
verbs.forEach(verb => {
|
||||||
|
c[desc.replace('[verb]', verb)] = baseCases[desc];
|
||||||
|
})
|
||||||
|
|
||||||
|
return c;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
Object.keys(cases).forEach(desc => {
|
||||||
|
let result = resolveNumbers(desc),
|
||||||
|
expected = cases[desc];
|
||||||
|
|
||||||
|
expect(result).to.have.lengthOf(expected.length);
|
||||||
|
expect(result).to.have.members(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accept multiple args for resolution', () => {
|
||||||
|
let multiArgResult = resolveNumbers('feature/#123-test', 'also closes #455');
|
||||||
|
expect(multiArgResult).to.have.members([ '123', '455' ]);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,9 @@
|
||||||
|
const ZenhubAPI = require('zenhub-client'),
|
||||||
|
{ promisifyAll } = require('bluebird'),
|
||||||
|
{ ZENHUB_API_KEY } = process.env;
|
||||||
|
|
||||||
|
if (!ZENHUB_API_KEY) {
|
||||||
|
throw new Error('"ZENHUB_API_KEY" was not found, please set it in environment secrets.');
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new ZenhubAPI(ZENHUB_API_KEY);
|
File diff suppressed because it is too large
Load Diff
|
@ -4,12 +4,15 @@
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node ./index.js",
|
"start": "node ./index.js",
|
||||||
"test": "jest"
|
"test": "NODE_ENV=test mocha -R spec **/*.test.js --exit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"actions-toolkit": "^2.1.0"
|
"actions-toolkit": "^2.1.0",
|
||||||
|
"bluebird": "^3.5.5",
|
||||||
|
"zenhub-client": "^1.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"jest": "^24.5.0"
|
"chai": "^4.2.0",
|
||||||
|
"mocha": "^6.1.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue