diff --git a/mklink.sh b/mklink.sh new file mode 100755 index 0000000..617c06a --- /dev/null +++ b/mklink.sh @@ -0,0 +1,23 @@ +#! /bin/sh + +cfgdir="./config" +src="src" +if [ "$#" -ge 1 ] +then + src=$1 +fi + +if [ ! -d $cfgdir ] +then + mkdir $cfgdir +fi + +if [ -h ${cfgdir}/Config.js ] +then + rm ${cfgdir}/Config.js +fi + +cd $cfgdir +ln -s ../${src}/Config.js Config.js +cd - > /dev/null + diff --git a/src/GitHubUtil.js b/src/GitHubUtil.js index cc4fc91..349981a 100644 --- a/src/GitHubUtil.js +++ b/src/GitHubUtil.js @@ -3,7 +3,7 @@ const GitHub = require('@octokit/rest')({ host: 'api.github.com', version: '3.0.0' }); -const Config = require('./Config.js'); +const Config = require('../config/Config.js'); const Util = require('./Util.js'); const Log = require('./Logger.js'); diff --git a/src/Logger.js b/src/Logger.js index 78db0bb..b8d7301 100644 --- a/src/Logger.js +++ b/src/Logger.js @@ -1,6 +1,6 @@ const assert = require('assert'); const bunyan = require('bunyan'); -const Config = require('./Config.js'); +const Config = require('../config/Config.js'); const Logger = bunyan.createLogger(Config.loggerParams()); diff --git a/src/Util.js b/src/Util.js index a51b04e..38bfdb4 100644 --- a/src/Util.js +++ b/src/Util.js @@ -1,5 +1,5 @@ const assert = require('assert'); -const Config = require('./Config.js'); +const Config = require('../config/Config.js'); function sleep(msec) { return new Promise((resolve) => setTimeout(resolve, msec)); diff --git a/tests/CiEmulator.js b/tests/CiEmulator.js new file mode 100644 index 0000000..68e787e --- /dev/null +++ b/tests/CiEmulator.js @@ -0,0 +1,118 @@ +const assert = require('assert'); +const http = require('http'); +const createHandler = require('github-webhook-handler'); +const Config = require('../config/Config.js'); +const Log = require('../src/Logger.js'); +const GH = require('../src/GitHubUtil.js'); + +const Logger = Log.Logger; + +const WebhookHandler = createHandler({ path: Config.githubWebhookPath(), secret: Config.githubWebhookSecret() }); + +// Emulates a CI. +// Listens for GitHub events coming on a listening port (GitHub webhook) and extracts SHA. +// For the SHA, creates/updates GitHub statuses, according to the configuration. +class CiEmulator { + + constructor() { + this._server = null; + this._sha = null; + this._handler = null; + } + + _createServer() { + assert(!this._server); + + this._server = http.createServer((req, res) => { + assert(this._handler); + this._handler(req, res, () => { + res.statusCode = 404; + res.end('no such location'); + }); + }); + + this._server.on('error', (e) => { + Logger.error("HTTP server error: " + e.code); + } + ); + + Logger.info("Location: " + Config.githubWebhookPath()); + return new Promise((resolve) => { + const params = {port: Config.port()}; + if (Config.host()) + params.host = Config.host(); + this._server.listen(params, () => { + let hostStr = Config.host() ? Config.host() : "unspecified"; + Log.Logger.info("HTTP server started and listening on " + hostStr + ":" + Config.port()); + resolve(true); + }); + }); + } + + async start(handler) { + assert(handler); + assert(!this.server); + this._handler = handler; + await this._createServer(); + } + + async run(sha, scope) { + this.sha = sha; + const statuses = Config.prStatuses(scope); + assert(statuses); + for (const st of statuses) { + const combinedStatus = await GH.getStatuses(sha); + const existingStatus = combinedStatus.statuses ? + combinedStatus.statuses.find(el => el.context.trim() === st.context) : null; + + if (existingStatus && + (existingStatus.state === st.state && existingStatus.description === st.description)) { + Logger.info("skipping existing status: " + st.context + " " + st.state); + continue; + } + + Logger.info("applying status: " + st.context + " " + st.state); + await GH.createStatus(sha, st.state, Config.statusUrl(), st.description, st.context); + } + } +} + +const Emulator = new CiEmulator(); + +// events + +WebhookHandler.on('error', (err) => { + Logger.error('Error:', err.message); +}); + +// https://developer.github.com/v3/activity/events/types/#pullrequestreviewevent +WebhookHandler.on('pull_request_review', (ev) => { + const pr = ev.payload.pull_request; + Logger.info("pull_request_review event:", pr.number, pr.head.sha, pr.state, pr.merge_commit_sha); + Emulator.run(pr.head.sha, "pr_status"); +}); + +// https://developer.github.com/v3/activity/events/types/#pullrequestevent +WebhookHandler.on('pull_request', (ev) => { + const pr = ev.payload.pull_request; + Logger.info("pull_request event:", pr.number, pr.head.sha, pr.state, pr.merge_commit_sha); + Emulator.run(pr.head.sha, "pr_status"); +}); + +// https://developer.github.com/v3/activity/events/types/#pushevent +WebhookHandler.on('push', (ev) => { + const e = ev.payload; + if (!e.head_commit) { + Logger.info("Push event ", e.ref, ",no head_commit, skipping"); + return; + } + + Logger.info("push event:", e.ref, e.head_commit.id); + if (e.ref.endsWith(Config.stagingBranchPath())) + Emulator.run(e.head_commit.id, "staged_status"); + else + Logger.info("skipping", e.ref); +}) + +Emulator.start(WebhookHandler); + diff --git a/tests/Config.js b/tests/Config.js new file mode 100644 index 0000000..edc33b4 --- /dev/null +++ b/tests/Config.js @@ -0,0 +1,65 @@ +const fs = require('fs'); +const assert = require('assert'); + +class ConfigOptions { + constructor(fname) { + const conf = JSON.parse(fs.readFileSync(fname), (key, value) => { + if (value === "process.stdout") + return process.stdout; + if (value === "process.stderr") + return process.stderr; + return value; + }); + + this._githubUserLogin = conf.github_login; + this._githubToken = conf.github_token; + this._githubWebhookPath = conf.github_webhook_path; + this._githubWebhookSecret = conf.github_webhook_secret; + this._repo = conf.repo; + this._host = conf.host; + this._port = conf.port; + this._owner = conf.owner; + this._stagingBranch = conf.staging_branch; + this._loggerParams = conf.logger_params; + this._statusParams = conf.status_params; + + this._githubUserName = null; + + const allOptions = Object.values(this); + for (let v of allOptions) { + assert(v !== undefined ); + } + } + + githubUserLogin() { return this._githubUserLogin; } + githubUserName(name) { + if (name !== undefined) + this._githubUserName = name; + return this._githubUserName; + } + githubToken() { return this._githubToken; } + githubWebhookPath() { return this._githubWebhookPath; } + githubWebhookSecret() { return this._githubWebhookSecret; } + repo() { return this._repo; } + host() { return this._host; } + port() { return this._port; } + owner() { return this._owner; } + stagingBranchPath() { return "heads/" + this._stagingBranch; } + loggerParams() { return this._loggerParams; } + statusParams() { return this._statusParams; } + statusUrl() { return "http://example.com"; } + dryRun() { return false; } + + prStatuses(scope) { + for (let p of this._statusParams) { + if (p.scope === scope) + return p.statuses; + } + return null; + } +} + +const configFile = process.argv.length > 2 ? process.argv[2] : './config.json'; +const Config = new ConfigOptions(configFile); + +module.exports = Config;