diff --git a/configs/.env b/configs/.env index 33a814431..3520a509e 100644 --- a/configs/.env +++ b/configs/.env @@ -36,12 +36,11 @@ HELLO_RUNNABLE_GITHUB_ID=10224339 SLACK_BOT_IMAGE="https://avatars0.githubusercontent.com/u/2335750?v=3&s=200" SLACK_BOT_USERNAME="runnabot" HIPCHAT_BOT_USERNAME="runnabot" -ENABLE_BUILDS_ON_GIT_PUSH=false +ENABLE_GITHUB_HOOKS=false ENABLE_NOTIFICATIONS_ON_GIT_PUSH=false -ENABLE_NEW_BRANCH_BUILDS_ON_GIT_PUSH=false -ENABLE_GITHUB_PR_COMMENTS=false +ENABLE_GITHUB_PR_STATUSES=false POLL_MONGO_TIMEOUT="30 minutes" -GITHUB_SCOPE="user:email,repo,repo_deployment,read:repo_hook" +GITHUB_SCOPE="user:email,repo,repo_deployment,read:repo_hook, repo:status" GITHUB_HOOK_SECRET="3V3RYTHINGisAW3S0ME!" DOCKER_IMAGE_BUILDER_CACHE="/git-cache" MONITOR_INTERVAL="1 minute" @@ -49,5 +48,4 @@ RUNNABOT_GITHUB_ACCESS_TOKEN="14ed935a9413a29dbb28774d0521a4fd958571c8" RUNNABOT_GITHUB_USERNAME="runnabot" MESSENGER_NAMESPACE="runnable:api:messenger:" BODY_PARSER_SIZE_LIMIT="600kb" -GRAPH_DATABASE_TYPE="cayley" - +GRAPH_DATABASE_TYPE="cayley" \ No newline at end of file diff --git a/configs/.env.production b/configs/.env.production index 739fdc6ff..ad38508e7 100644 --- a/configs/.env.production +++ b/configs/.env.production @@ -29,10 +29,8 @@ ROUTE53_HOSTEDZONEID="Z3IHYG8VH3VMIJ" DNS_IPADDRESS=205.251.195.203 CAYLEY="http://cayley.runnable.io" DOCKER_IMAGE_BUILDER_CACHE="/git-cache" -ENABLE_BUILDS_ON_GIT_PUSH=true +USER_CONTENT_DOMAIN=true ENABLE_NOTIFICATIONS_ON_GIT_PUSH=true -ENABLE_NEW_BRANCH_BUILDS_ON_GIT_PUSH=false -ENABLE_GITHUB_PR_COMMENTS=true GITHUB_DEPLOY_KEYS_POOL_SIZE=100 HEAP_APP_ID=3017229846 DOCKER_IMAGE_BUILDER_LAYER_CACHE="/layer-cache" diff --git a/lib/models/apis/github.js b/lib/models/apis/github.js index 6b7fafaaa..cb4004601 100644 --- a/lib/models/apis/github.js +++ b/lib/models/apis/github.js @@ -639,6 +639,56 @@ Github.prototype.getPullRequestHeadCommit = function (shortRepo, number, cb) { }); }; + +Github.prototype.createDeployment = function (shortRepo, query, cb) { + debug('createDeployment', formatArgs(arguments)); + var split = shortRepo.split('/'); + query.user = split[0]; + query.repo = split[1]; + this.deployments.create(query, function (err, deployment) { + if (err) { + err = (err.code === 404) ? + Boom.notFound('Cannot find repo or ref: ' + shortRepo, { err: err, report: false }) : + Boom.create(502, 'Failed to repo or ref ' + shortRepo, { err: err }); + return cb(err); + } + cb(null, deployment); + }); +}; + +Github.prototype.createDeploymentStatus = function (shortRepo, query, cb) { + debug('createDeploymentStatus', formatArgs(arguments)); + var split = shortRepo.split('/'); + query.user = split[0]; + query.repo = split[1]; + this.deployments.createStatus(query, function (err, deployment) { + if (err) { + err = (err.code === 404) ? + Boom.notFound('Cannot find repo, ref or deployment: ' + shortRepo, + { err: err, report: false }) : + Boom.create(502, 'Failed to repo, ref or deployment ' + shortRepo, { err: err }); + return cb(err); + } + cb(null, deployment); + }); +}; + +Github.prototype.createBuildStatus = function (shortRepo, query, cb) { + debug('createBuildStatus', formatArgs(arguments)); + var split = shortRepo.split('/'); + query.user = split[0]; + query.repo = split[1]; + this.statuses.create(query, function (err, status) { + if (err) { + err = (err.code === 404) ? + Boom.notFound('Cannot find repo or sha: ' + shortRepo, { err: err, report: false }) : + Boom.create(502, 'Failed to repo or sha ' + shortRepo, { err: err }); + return cb(err); + } + cb(null, status); + }); +}; + // Check if user is collaborator for the repo Github.prototype.isCollaborator = function (shortRepo, username, cb) { debug('isCollaborator', formatArgs(arguments)); diff --git a/lib/models/apis/pullrequest.js b/lib/models/apis/pullrequest.js new file mode 100644 index 000000000..194752e32 --- /dev/null +++ b/lib/models/apis/pullrequest.js @@ -0,0 +1,145 @@ +'use strict'; +var Github = require('models/apis/github'); +var debug = require('debug')('runnable-api:models:pullrequest'); +var formatArgs = require('format-args'); + +function PullRequest (githubToken) { + this.github = new Github({token: githubToken}); +} + +PullRequest.prototype.buildStarted = function (pullRequestInfo, targetUrl, cb) { + debug('buildStarted', formatArgs(arguments)); + if (process.env.ENABLE_GITHUB_PR_STATUSES !== 'true') { + return cb(null); + } + var payload = { + state: 'pending', + description: 'PR-' + pullRequestInfo.number + ' is building on Runnable.', + // we use url to differentiate between several runnable builds + context: targetUrl, + target_url: targetUrl, + sha: pullRequestInfo.commit + }; + this.github.createBuildStatus(pullRequestInfo.repo, payload, cb); +}; + +PullRequest.prototype.buildSucceeded = function (pullRequestInfo, targetUrl, cb) { + debug('buildSucceeded', formatArgs(arguments)); + if (process.env.ENABLE_GITHUB_PR_STATUSES !== 'true') { + return cb(null); + } + var payload = { + state: 'success', + description: 'PR-' + pullRequestInfo.number + ' is ready to run on Runnable.', + // we use url to differentiate between several runnable builds + context: targetUrl, + target_url: targetUrl, + sha: pullRequestInfo.commit + }; + this.github.createBuildStatus(pullRequestInfo.repo, payload, cb); +}; + +PullRequest.prototype.buildErrored = function (pullRequestInfo, targetUrl, cb) { + debug('buildErrored', formatArgs(arguments)); + if (process.env.ENABLE_GITHUB_PR_STATUSES !== 'true') { + return cb(null); + } + var payload = { + state: 'error', + description: 'PR-' + pullRequestInfo.number + ' has failed to build on Runnable.', + // we use url to differentiate between several runnable builds + context: targetUrl, + target_url: targetUrl, + sha: pullRequestInfo.commit + }; + this.github.createBuildStatus(pullRequestInfo.repo, payload, cb); +}; + +PullRequest.prototype.serverSelectionStatus = function (pullRequestInfo, targetUrl, cb) { + debug('buildStarted', formatArgs(arguments)); + if (process.env.ENABLE_GITHUB_PR_STATUSES !== 'true') { + return cb(null); + } + var payload = { + state: 'pending', + description: 'Select a server to build PR-' + pullRequestInfo.number, + // we use url to differentiate between several runnable builds + context: targetUrl, + target_url: targetUrl, + sha: pullRequestInfo.commit + }; + this.github.createBuildStatus(pullRequestInfo.repo, payload, cb); +}; + + +PullRequest.prototype.createDeployment = function (pullRequestInfo, serverName, payload, cb) { + debug('createDeployment', formatArgs(arguments)); + if (process.env.ENABLE_GITHUB_PR_STATUSES !== 'true') { + return cb(null); + } + var description = 'Deploying PR-' + pullRequestInfo.number + ' to ' + + serverName + ' on Runnable.'; + var query = { + auto_merge: false, + environment: 'runnable', + description: description, + ref: pullRequestInfo.commit, + payload: JSON.stringify(payload || {}), + required_contexts: [] // we skip check on all `contexts` since we still can deploy + }; + this.github.createDeployment(pullRequestInfo.repo, query, cb); +}; + + +PullRequest.prototype.deploymentStarted = +function (pullRequestInfo, deploymentId, serverName, targetUrl, cb) { + debug('deploymentStarted', formatArgs(arguments)); + if (process.env.ENABLE_GITHUB_PR_STATUSES !== 'true') { + return cb(null); + } + var description = 'Deploying PR-' + pullRequestInfo.number + ' to ' + + serverName + ' on Runnable.'; + var payload = { + id: deploymentId, + state: 'pending', + target_url: targetUrl, + description: description + }; + this.github.createDeploymentStatus(pullRequestInfo.repo, payload, cb); +}; + +PullRequest.prototype.deploymentSucceeded = +function (pullRequestInfo, deploymentId, serverName, targetUrl, cb) { + debug('deploymentSucceeded', formatArgs(arguments)); + if (process.env.ENABLE_GITHUB_PR_STATUSES !== 'true') { + return cb(null); + } + var description = 'Deployed PR-' + pullRequestInfo.number + + ' to ' + serverName + ' on Runnable.'; + var payload = { + id: deploymentId, + state: 'success', + target_url: targetUrl, + description: description + }; + this.github.createDeploymentStatus(pullRequestInfo.repo, payload, cb); +}; + +PullRequest.prototype.deploymentErrored = +function (pullRequestInfo, deploymentId, serverName, targetUrl, cb) { + debug('deploymentErrored', formatArgs(arguments)); + if (process.env.ENABLE_GITHUB_PR_STATUSES !== 'true') { + return cb(null); + } + var description = 'Failed to deploy PR-' + pullRequestInfo.number + + ' to ' + serverName + ' on Runnable.'; + var payload = { + id: deploymentId, + state: 'error', + target_url: targetUrl, + description: description + }; + this.github.createDeploymentStatus(pullRequestInfo.repo, payload, cb); +}; + +module.exports = PullRequest; \ No newline at end of file diff --git a/lib/models/notifications/github.js b/lib/models/notifications/github.js deleted file mode 100644 index 253d8d5b3..000000000 --- a/lib/models/notifications/github.js +++ /dev/null @@ -1,248 +0,0 @@ -'use strict'; -var fs = require('fs'); -var async = require('async'); -var GitHubAPI = require('models/apis/github'); -var Instance = require('models/mongo/instance'); -var User = require('models/mongo/user'); -var debug = require('debug')('runnable-notifications:github'); -var Handlebars = require('handlebars'); -var noop = require('101/noop'); -var pluck = require('101/pluck'); -var formatArgs = require('format-args'); -var toSentence = require('underscore.string/toSentence'); -var Boom = require('dat-middleware').Boom; - -Handlebars.registerHelper('encode', function (str) { - // we do double encoding here for angular because - // browser would automatically replace `%2F` to `/` and angular router will fail - return encodeURIComponent(encodeURIComponent(str)); -}); - -var chooseServerTpl = createTpl('./templates/github_pull_request_choose_server.hbs'); -var serverLinkTpl = createTpl('./templates/github_pull_request_server_link.hbs'); - -function GitHub () { - this.github = new GitHubAPI({token: process.env.RUNNABOT_GITHUB_ACCESS_TOKEN}); -} - -// Notify when PR was created -GitHub.prototype.notifyOnPullRequest = function (gitInfo, instances, cb) { - debug('notifyOnGitHubPullRequest', formatArgs(arguments)); - cb = cb || noop; - // our env parsing cannot parse boolean correctly atm - if (process.env.ENABLE_GITHUB_PR_COMMENTS !== 'true') { - return cb(null); - } - var message = this._renderMessage(gitInfo, instances); - // ensure org membership - async.series([ - this._ensurePermissions.bind(this, gitInfo.repo, gitInfo.org.login), - this.github.addComment.bind(this.github, gitInfo.repo, gitInfo.number, message) - ], cb); -}; - -// Update PR's comment -GitHub.prototype.updatePullRequestsComments = function (instance, origInstance, cb) { - debug('updatePullRequestsComments', formatArgs(arguments)); - cb = cb || noop; - // our env parsing cannot parse boolean correctly atm - if (process.env.ENABLE_GITHUB_PR_COMMENTS !== 'true') { - return cb(null); - } - if (origInstance) { - // we don't want to fetch origInstance owner - // and we know for sure that owner for two instances is the same - origInstance.owner = instance.owner; - } - async.series([ - this._updatePullRequestsComments.bind(this, instance), - this._updatePullRequestsComments.bind(this, origInstance) - ], cb); -}; - - -GitHub.prototype.deletePullRequestComment = function (gitInfo, cb) { - debug('deletePullRequestComment', formatArgs(arguments)); - cb = cb || noop; - // our env parsing cannot parse boolean correctly atm - if (process.env.ENABLE_GITHUB_PR_COMMENTS !== 'true') { - return cb(null); - } - var self = this; - this.github.findCommentByUser(gitInfo.repo, gitInfo.number, process.env.RUNNABOT_GITHUB_USERNAME, - function (err, comment) { - if (err || !comment) { - return cb(err, null); - } - self.github.deleteComment(gitInfo.repo, comment.id, cb); - }); -}; - - -// this method will check if runnabot has proper permissions for the repo. -// there are 4 main cases: -// 1. repo owner is user and repo is public. -// 2. repo owner is user and repo is private. -// Check if runnabot is collaborator for the repo. -// 3. repo owner is org and repo is public. -// 4. repo owner is org and repo is private. -// Check if runnabot is org member and invitation is accepted. -GitHub.prototype._ensurePermissions = function (shortRepo, orgName, callback) { - debug('_ensurePermissions', formatArgs(arguments)); - var self = this; - self.github.isPublicRepo(shortRepo, function (err, isPublic) { - // error is 404 we still want to process with checking access. - // github returns 404 even if resource is there but access is denied - if (err && err.output && err.output.statusCode !== 404) { return callback(err); } - // case 1, 3 - if (isPublic) { - return callback(null); - } - // case 4 - if (orgName) { - self.github.isOrgMember(orgName, function (err, isMember) { - if (err) { return callback(err); } - if (isMember) { - return callback(null); - } - // user can have pending membership in the org: - // e.x. invitation was sent but not accepted yet. - // by setting `state` to `active` we are trying to accept membership. - self.github.user.updateOrgMembership({org: orgName, state: 'active'}, callback); - }); - } - // case 2 - else { - self.github.isCollaborator(shortRepo, process.env.RUNNABOT_GITHUB_USERNAME, - function (err, isCollaborator) { - if (err) { return callback(err); } - if (isCollaborator) { - return callback(null); - } - callback(Boom.forbidden('Runnabot is not collaborator on a private repo: ' + shortRepo)); - }); - } - }); -}; - -GitHub.prototype._updatePullRequestsComments = function (instance, cb) { - debug('_updatePullRequestsComments', formatArgs(arguments)); - if (!instance || !instance.contextVersion) { - return cb(null); - } - var self = this; - - var repos = instance.contextVersion.appCodeVersions; - if (!repos || repos.length === 0) { - return cb(null); - } - async.each(repos, function (repo, callback) { - self._updatePullRequestsCommentsForRepo(repo.lowerRepo, repo.lowerBranch, instance, callback); - }, cb); -}; - - -GitHub.prototype._updatePullRequestsCommentsForRepo = function (shortRepo, branch, instance, cb) { - debug('_updatePullRequestsCommentsForRepo', formatArgs(arguments)); - var self = this; - - var tasks = { - instances: Instance.findInstancesLinkedToBranch.bind(this, shortRepo, branch), - prs: this.github.listOpenPullRequestsForBranch.bind(this.github, shortRepo, branch) - }; - async.parallel(tasks, function (err, results) { - if (err) { return cb(err); } - var instances = results.instances; - var prs = results.prs; - if (!prs) { - return cb(null); - } - - var ids = prs.map(pluck('number')); - async.map(ids, function (number, callback) { - var gitInfo = { - owner: { - login: instance.owner.login || instance.owner.username - }, - number: number, - repoName: shortRepo.split('/')[1], - shortRepo: shortRepo, - branch: branch - }; - if (!instances || instances.length === 0) { - self._getHeadCommit(instance.createdBy.github, shortRepo, number, - function (err, headCommit) { - if (err) {return cb(err);} - gitInfo.headCommit = headCommit.commit; - gitInfo.commit = headCommit.sha; - self._updateComment(gitInfo, instances, callback); - }); - } else { - self._updateComment(gitInfo, instances, callback); - } - }, cb); - }); -}; - -GitHub.prototype._getHeadCommit = function (githubId, repo, number, cb) { - debug('_getHeadCommit', formatArgs(arguments)); - User.findByGithubId(githubId, function (err, user) { - if (err || !user) { return cb(err); } - var gh = new GitHubAPI({token: user.accounts.github.accessToken}); - gh.getPullRequestHeadCommit(repo, number, cb); - }); -}; - -GitHub.prototype._updateComment = function (gitInfo, instances, callback) { - debug('_updateComment', formatArgs(arguments)); - var self = this; - this.github.findCommentByUser(gitInfo.shortRepo, gitInfo.number, - process.env.RUNNABOT_GITHUB_USERNAME, - function (err, comment) { - if (err || !comment) { - return callback(err); - } - var oldMessage = comment.body; - var newMessage = self._renderMessage(gitInfo, instances); - if (newMessage === oldMessage) { - return callback(null); - } - self.github.updateComment(gitInfo.shortRepo, comment.id, newMessage, callback); - }); -}; - -GitHub.prototype._renderMessage = function (gitInfo, instances) { - if (instances && instances.length > 0) { - var message; - if (instances.length === 1) { - message = renderServerLink(gitInfo, instances[0]) + ' is'; - } - else { - var links = instances.map(renderServerLink.bind(null, gitInfo)); - message = toSentence(links, ', ', ' and ') + ' are'; - } - return message + ' updated with the latest changes to PR-' + gitInfo.number + '.'; - } - else { - gitInfo.domain = process.env.DOMAIN; - return chooseServerTpl(gitInfo).trim(); - } -}; - -function renderServerLink (gitInfo, instance) { - var data = { - domain: process.env.DOMAIN, - name: instance.name, - owner: { - login: gitInfo.owner.login - } - }; - return serverLinkTpl(data); -} - -function createTpl (tplPath) { - var content = fs.readFileSync(tplPath, {encoding: 'utf8'}); - return Handlebars.compile(content); -} - -module.exports = GitHub; \ No newline at end of file diff --git a/lib/models/notifications/notifier.js b/lib/models/notifications/notifier.js index 785cf08b4..374ae68f9 100644 --- a/lib/models/notifications/notifier.js +++ b/lib/models/notifications/notifier.js @@ -90,7 +90,6 @@ Notifier.prototype.notifyOnBuild = function (githubPushInfo, cb) { Notifier.prototype.notifyOnInstances = function (githubPushInfo, instances, cb) { debug('notifyOnInstances', githubPushInfo); if (instances && instances.length > 0) { - debug('notify on instances', instances); var message = this.makeOnInstancesMessage(githubPushInfo, instances); this.send(message, cb); } diff --git a/lib/routes/actions/github.js b/lib/routes/actions/github.js index 17ec3fb93..8f15ff745 100644 --- a/lib/routes/actions/github.js +++ b/lib/routes/actions/github.js @@ -17,18 +17,15 @@ var contextVersions = mongoMiddlewares.contextVersions; var contexts = mongoMiddlewares.contexts; var instances = mongoMiddlewares.instances; var users = mongoMiddlewares.users; -var settings = mongoMiddlewares.settings; var runnable = require('middlewares/apis').runnable; -var notifications = require('middlewares/notifications').index; -var githubNotifications = require('middlewares/notifications').github; +var github = require('middlewares/apis').github; var heap = require('middlewares/apis').heap; +var pullRequest = require('middlewares/apis').pullrequest; var timers = require('middlewares/apis').timers; var validations = require('middlewares/validations'); -var equals = require('101/equals'); -var pluck = require('101/pluck'); var noop = require('101/noop'); +var find = require('101/find'); var error = require('error'); -var github = require('middlewares/apis').github; var dogstatsd = require('models/datadog'); /** Receive the Github hooks @@ -38,7 +35,7 @@ var pushSessionUser = { permissionLevel: 5, accounts: { github: { - id: 'githubPushInfo.user.id' + id: 'githubPullRequest.user.id' } } }; @@ -52,93 +49,34 @@ app.post('/actions/github/', mw.res.send('Hello, Github Ping!')), function (req, res, next) { // our env parsing cannot parse boolean correctly atm - if (process.env.ENABLE_BUILDS_ON_GIT_PUSH !== 'true') { + if (process.env.ENABLE_GITHUB_HOOKS !== 'true') { res.status(202); - res.send('hooks are currently disabled. but we gotchu!'); + res.send('Hooks are currently disabled. but we gotchu!'); } else { next(); } }, - // handle pull request events. we care about `opened` and `closed` for now + // handle pull request events. we care about `synchronize` and `opened` for now mw.headers('x-github-event').matches(/^pull_request$/).then( - mw.body('action').validate(validations.equals('opened')) - .else( - mw.body('action').validate(validations.equals('closed')) - .else( - mw.res.status(202), - mw.res.send('Do not handle pull request with actions not equal opened')) - .then( - parseGitHubPRData, - githubNotifications.create(), - githubNotifications.model.deletePullRequestComment('githubPushInfo'), - mw.res.status(201), - mw.res.send('We processed PR closed event'))) - .then( - parseGitHubPRData, - instances.findContextVersionsForRepo('githubPushInfo.repo'), - mw.req().set('contextVersionIds', 'instances'), - mw.req('contextVersionIds.length').validate(validations.equals(0)) - .then( - mw.res.status(202), - mw.res.send('No appropriate work to be done; finishing.')), - // find instances that follow branch - instances.findInstancesLinkedToBranch('githubPushInfo.repo', 'githubPushInfo.branch'), - - contextVersions.findByIds('contextVersionIds', { _id:1, context:1, createdBy:1, owner:1 }), - // fetch context to get owner to get settings for - contexts.findById('contextVersions[0].context'), - mw.req().set('ownerGithubId', 'context.owner.github'), - // FIXME: - // we are getting creator of the context version here - // we will use GitHub API using creator account. This is not ideal. - // can we use runnabot personal token? - mw.req().set('creatorGithubId', 'contextVersions[0].createdBy.github'), - users.findByGithubId('creatorGithubId'), - // session user is needed for findGithubUserByGithubId - mw.req().set('versionCreator', 'user'), - users.create('versionCreator'), - users.model.findGithubUserByGithubId('ownerGithubId'), - mw.req().set('contextOwner', 'user'), - mw.req().set('githubPushInfo.owner', 'contextOwner'), - - - github.create({token: 'versionCreator.accounts.github.accessToken'}), - github.model.getPullRequestHeadCommit('githubPushInfo.repo', 'githubPushInfo.number'), - - mw.req().set('githubPushInfo.headCommit', 'githubResult.commit'), - - githubNotifications.create(), - githubNotifications.model.notifyOnPullRequest('githubPushInfo', 'instances'), - mw.res.status(201), - mw.res.send('We processed PR opened event') - ) - ), - // handle push events - mw.headers('x-github-event').matches(/^push$/).then( - mw.body('deleted').validate(equals(true)) + mw.body('action').matches(/synchronize|opened/) .else( mw.res.status(202), - mw.res.send('Deleted the branch; no work to be done.')), - parseGitHubPushData, - mw.req().set('hookStartTime', new Date()), - timers.create(), - timers.model.startTimer('github_push_event'), - mw.req('githubPushInfo.commitLog.length').validate(validations.equals(0)) + mw.res.send('Do not handle pull request with actions not equal synchronize or opened.')) .then( - mw.res.status(202), - mw.res.send('No commits pushed; no work to be done.')) - .else( - instances.findInstancesLinkedToBranch('githubPushInfo.repo', 'githubPushInfo.branch'), + parseGitHubPullRequest, + instances.findInstancesLinkedToBranch('githubPullRequest.repo', 'githubPullRequest.branch'), // check if there are instances that follow specific branch mw.req('instances.length').validate(validations.equals(0)) .then( - // no instances found. This can be push to the new branch - newBranch() + // no servers found with this branch/pr + // send server selection status + noServersForPullRequest() ) .else( - // instances following particular branch were found. Redeploy them with the new code + // servers following particular branch were found. Redeploy them with the new code followBranch('instances') - )) + ) + ) ), mw.res.status(202), mw.res.send('No action set up for that payload.')); @@ -150,123 +88,66 @@ function reportDatadogEvent (req, res, next) { next(); } -function parseGitHubPRData (req, res, next) { +function parseGitHubPullRequest (req, res, next) { var repository = keypather.get(req, 'body.repository'); if (!repository) { return next(Boom.badRequest('Unexpected PR hook format', { req: req })); } - req.githubPushInfo = { + var pullRequest = keypather.get(req, 'body.pull_request'); + if (!pullRequest) { + return next(Boom.badRequest('Unexpected PR hook format', { req: req })); + } + var head = keypather.get(req, 'body.pull_request.head'); + if (!head) { + return next(Boom.badRequest('Unexpected PR hook format', { req: req })); + } + req.githubPullRequest = { number : req.body.number, - repo : req.body.repository.full_name, - repoName: req.body.repository.name, - branch : req.body.pull_request.head.ref, - commit : req.body.pull_request.head.sha, + repo : repository.full_name, + repoName: repository.name, + branch : head.ref, + commit : head.sha, + creator : pullRequest.user, user : req.body.sender, org : req.body.organization || {} }; next(); } -function parseGitHubPushData (req, res, next) { - var repository = keypather.get(req, 'body.repository'); - if (!repository) { - return next(Boom.badRequest('Unexpected commit hook format', { req: req })); - } - req.headCommit = req.body.head_commit; - req.commitLog = req.body.commits; - req.githubPushInfo = { - repo : req.body.repository.full_name, - repoName : req.body.repository.name, - branch : req.body.ref.replace('refs/heads/', ''), - commit : req.body.head_commit.id, - headCommit: req.body.head_commit, - commitLog : req.body.commits || [], - user : req.body.sender - }; - next(); -} -function newBranch () { +function noServersForPullRequest () { return flow.series( - function (req, res, next) { - // our env parsing cannot parse boolean correctly atm - if (process.env.ENABLE_NEW_BRANCH_BUILDS_ON_GIT_PUSH !== 'true') { - res.status(202); - res.send('New branch builds are disabled for now'); - } else { - next(); - } - }, - - instances.findContextVersionsForRepo('githubPushInfo.repo'), + instances.findContextVersionsForRepo('githubPullRequest.repo'), mw.req().set('contextVersionIds', 'instances'), mw.req('contextVersionIds.length').validate(validations.equals(0)) .then( mw.res.status(202), mw.res.send('No appropriate work to be done; finishing.')), - // for each of the context versions, make a deep copy and build them! contextVersions.findByIds('contextVersionIds', { _id:1, context:1, createdBy:1, owner:1 }), - mw.req('contextVersions').each( - function (contextVersion, req, eachReq, res, next) { - req.newContextVersionIds = []; - eachReq.contextVersion = contextVersion; - next(); - }, - newContextVersion('contextVersion'), // replaces context version! - // Note: pushSessionUser has moderator permissions, - // can only be used for loopback methods that don't require a githubToken - runnable.create({}, pushSessionUser), - // build the new context version - runnable.model.buildVersion('contextVersion', { json: { - message: 'headCommit.message', - triggeredAction: { - manual: false, - appCodeVersion: { - repo: 'githubPushInfo.repo', - commit: 'githubPushInfo.headCommit.id', - commitLog: 'githubPushInfo.commitLog' - } - } - }}), - function (contextVersion, req, eachReq, res, next) { - var newContextVersionId = eachReq.runnableResult.id(); - req.newContextVersionIds.push(newContextVersionId); - next(); - } - ), - // RESPOND - resSendAndNext('newContextVersionIds'), - // background - flow.try( - waitForContextVersionBuildCompleted('newContextVersionIds'), - // fetch context to get owner to get settings for - contexts.findById('contextVersions[0].context'), - mw.req().set('ownerGithubId', 'context.owner.github'), - - // FIXME: - // we are getting creator of the context version here - // we will use GitHub API using creator account. This is not ideal. - mw.req().set('creatorGithubId', 'contextVersions[0].createdBy.github'), - users.findByGithubId('creatorGithubId'), - // session user is needed for findGithubUserByGithubId - mw.req().set('versionCreator', 'user'), - users.create('versionCreator'), - users.model.findGithubUserByGithubId('ownerGithubId'), - mw.req().set('contextOwner', 'user'), - mw.req().set('githubPushInfo.owner', 'contextOwner'), - // get settings from the owner of the first contextVersion - settings.findOneByGithubId('ownerGithubId'), - mw.req('setting').require().then( - notifications.create('setting.notifications'), - notifications.model.notifyOnBuild('githubPushInfo')) - ).catch( - error.logIfErrMw - ), - noop + // we take just first one since we want to send only one Status API request + contexts.findById('contextVersions[0].context'), + mw.req().set('ownerGithubId', 'context.owner.github'), + mw.req().set('creatorGithubId', 'contextVersions[0].createdBy.github'), + users.findByGithubId('creatorGithubId'), + // session user is needed for findGithubUserByGithubId + mw.req().set('versionCreator', 'user'), + users.model.findGithubUserByGithubId('ownerGithubId'), + mw.req().set('contextOwner', 'user'), + github.create({token: 'versionCreator.accounts.github.accessToken'}), + github.model.getPullRequestHeadCommit('githubPullRequest.repo', 'githubPullRequest.number'), + mw.req().set('githubPullRequest.headCommit', 'githubResult.commit'), + function (req, res, next) { + var selectionUrl = createServerSelectionUrl(req.contextOwner.login, req.githubPullRequest); + req.serverSelectionUrl = selectionUrl; + next(); + }, + pullRequest.create('versionCreator.accounts.github.accessToken'), + pullRequest.model.serverSelectionStatus('githubPullRequest', 'serverSelectionUrl'), + mw.res.status(201), + mw.res.json('contextVersionIds') ); } - function newContextVersion (contextVersionKey) { return flow.series( mw.req().set('contextVersion', contextVersionKey), @@ -278,154 +159,214 @@ function newContextVersion (contextVersionKey) { // find new deep copied context version contextVersions.modifyAppCodeVersionByRepo( 'contextVersionId', - 'githubPushInfo.repo', - 'githubPushInfo.branch', - 'githubPushInfo.commit') + 'githubPullRequest.repo', + 'githubPullRequest.branch', + 'githubPullRequest.commit') ); } function followBranch (instancesKey) { return flow.series( mw.req().set('instances', instancesKey), - // get settings from the owner of the first instance - mw.req().set('ownerGithubId', 'instances[0].owner.github'), - // FIXME: - // we are getting creator of the instance here - // we will use GitHub API using creator account. This is not ideal. - mw.req().set('creatorGithubId', 'instances[0].createdBy.github'), - users.findByGithubId('creatorGithubId'), - // session user is needed for getGithubUsername and patchInstance - mw.req().set('instanceCreator', 'user'), function (req, res, next) { // dat-middleware set creates closures when reference values are used! (objects) req.newContextVersionIds = []; req.instanceIds = []; - req.instanceNewInfo = {}; + req.contextVersionNewInfo = {}; next(); }, mw.req('instances').each( function (instance, req, eachReq, res, next) { eachReq.instance = instance; eachReq.contextVersion = instance.contextVersion; + eachReq.creatorGithubId = instance.createdBy.github; next(); }, + users.findByGithubId('creatorGithubId'), + mw.req().set('instanceCreator', 'user'), + instances.model.populateOwnerAndCreatedBy('instanceCreator'), + function (req, res, next) { + var instance = req.instance; + req.targetUrl = createTargetUrl(instance.name, instance.owner.username); + next(); + }, + mw.req('creatorGithubId').validate(validations.equalsKeypath('githubPullRequest.creator.id')) + .then( + pullRequest.create('instanceCreator.accounts.github.accessToken'), + pullRequest.model.buildStarted('githubPullRequest', 'targetUrl') + ), newContextVersion('contextVersion'), // replaces context version! - // Note: pushSessionUser has moderator permissions, - // can only be used for loopback methods that don't require a githubToken - runnable.create({}, pushSessionUser), - runnable.model.createBuild({ json: { - contextVersions: ['contextVersion._id'], - owner: { - github: 'ownerGithubId' - } - }}), - mw.req().set('jsonNewBuild', 'runnableResult'), - // we cannot use pushSessionUser, bc redeploy requires token - // we must reinstantiate runnable model for each call bc of a bug - runnable.create({}, 'instanceCreator'), - runnable.model.buildBuild('jsonNewBuild', { json: { - message: 'headCommit.message', - triggeredAction: { - manual: false, - appCodeVersion: { - repo: 'githubPushInfo.repo', - commit: 'githubPushInfo.headCommit.id', - commitLog: 'githubPushInfo.commitLog' + flow.try( + // Note: pushSessionUser has moderator permissions, + // can only be used for loopback methods that don't require a githubToken + runnable.create({}, pushSessionUser), + runnable.model.createBuild({ json: { + contextVersions: ['contextVersion._id'], + owner: { + github: 'instance.owner.github' + } + }}), + mw.req().set('jsonNewBuild', 'runnableResult'), + // we cannot use pushSessionUser, bc redeploy requires token + // we must reinstantiate runnable model for each call bc of a bug + runnable.create({}, 'instanceCreator'), + runnable.model.buildBuild('jsonNewBuild', { json: { + triggeredAction: { + manual: false, + appCodeVersion: { + repo: 'githubPullRequest.repo', + commit: 'githubPullRequest.commit' + } } + }}), + mw.req().set('jsonNewBuild', 'runnableResult'), + function (req, res, next) { + if (req.jsonNewBuild) { + var newContextVersionId = req.jsonNewBuild.contextVersions[0]; + var newBuildId = req.jsonNewBuild._id; + var oldContextVersion = find(req.newContextVersionIds, function (val) { + return val === newContextVersionId; + }); + var infoObject = { + instance: req.instance, + buildId: newBuildId, + targetUrl: req.targetUrl, + instanceCreator: req.instanceCreator + }; + if (!oldContextVersion) { + req.newContextVersionIds.push(newContextVersionId); + req.contextVersionNewInfo[newContextVersionId] = [infoObject]; + } + else { + req.contextVersionNewInfo[newContextVersionId].push(infoObject); + } + req.instanceIds.push(req.instance._id.toString()); + } + next(); } - }}), - mw.req().set('jsonNewBuild', 'runnableResult'), - function (instance, req, eachReq, res, next) { - var newContextVersionId = eachReq.jsonNewBuild.contextVersions[0]; - var newBuildId = eachReq.jsonNewBuild._id; - req.newContextVersionIds.push(newContextVersionId); - req.instanceIds.push(instance._id.toString()); - req.instanceNewInfo[instance._id.toString()] = { - contextVersionId: newContextVersionId, - buildId: newBuildId - }; - next(); - } + ).catch( + error.logIfErrMw, + mw.req('creatorGithubId').validate( + validations.equalsKeypath('githubPullRequest.creator.id')) + .then( + pullRequest.create('instanceCreator.accounts.github.accessToken'), + pullRequest.model.buildErrored('githubPullRequest', 'targetUrl') + ) + ) ), - // RESPOND + // send http response & continue in background resSendAndNext('instanceIds'), // background - flow.try( - waitForContextVersionBuildCompleted('newContextVersionIds'), - function (req, res, next) { - req.instanceIds = req.instances.map(pluck('_id')); + mw.req('newContextVersionIds').each( + function (contextVersionId, req, eachReq, res, next) { + eachReq.contextVersionId = contextVersionId; + var infoObjects = req.contextVersionNewInfo[contextVersionId]; + eachReq.infoObjects = infoObjects; next(); }, - function (req, res, next) { - req.instancesToWaitFor = req.instances.filter(function (instance) { - var contextVersionId = req.instanceNewInfo[instance._id.toString()].contextVersionId; - var buildSuccessful = req.pollMongoResults[contextVersionId]; - return buildSuccessful; - }); - next(); - }, - function (req, res, next) { - // dat-middleware set creates closures when reference values are used! (objects) - req.deployedInstances = []; - next(); - }, - mw.req('instancesToWaitFor').each( - function (instance, req, eachReq, res, next) { + pollMongo({ + idPath: 'contextVersionId', + database: require('models/mongo/context-version'), + successKeyPath: 'build.completed', + failureKeyPath: 'build.error', + failureCb: function (failureKeyPathValue, req, res, next) { + req.infoObjects.forEach(function (infoObject) { + var instanceCreator = infoObject.instanceCreator; + var prData = req.githubPullRequest; + pullRequest.create(instanceCreator.accounts.github.accessToken); + pullRequest.model.buildErrored(prData, infoObject.targetUrl)(req, res, next); + }); + } + }), + + mw.req('infoObjects').each( + function (infoObject, req, eachReq, res, next) { + var instance = infoObject.instance; eachReq.instance = instance; - eachReq.buildId = req.instanceNewInfo[instance._id.toString()].buildId; + eachReq.buildId = infoObject.buildId; + eachReq.targetUrl = infoObject.targetUrl; + eachReq.timerId = 'github_push_event:' + instance.shortHash; + eachReq.creatorGithubId = instance.createdBy.github; + eachReq.instanceCreator = infoObject.instanceCreator; next(); }, - // we cannot use pushSessionUser, bc patch requires token - // we must reinstantiate runnable model for each call bc of a bug - runnable.create({}, 'instanceCreator'), - runnable.model.updateInstance('instance.shortHash', { - json: {build: 'buildId'} + pullRequest.create('instanceCreator.accounts.github.accessToken'), + mw.req('creatorGithubId').validate( + validations.equalsKeypath('githubPullRequest.creator.id')) + .then( + pullRequest.model.buildSucceeded('githubPullRequest', 'targetUrl') + ), + timers.create(), + timers.model.startTimer('timerId'), + pullRequest.model.createDeployment('githubPullRequest', 'instance.name', { + instanceId: 'instance._id.toString()', }), - // we must reinstantiate runnable model for each call bc of a bug - runnable.create({}, 'instanceCreator'), - runnable.model.waitForInstanceDeployed('instance.shortHash'), - mw.req().set('instanceDeployed', 'runnableResult'), - function (instance, req, eachReq, res, next) { - if (eachReq.instanceDeployed) { - req.deployedInstances.push(instance); - } - next(); - } - ), - mw.req('deployedInstances.length').validate(validations.notEquals(0)) - .then( - function (req, res, next) { - var instancesNames = req.deployedInstances.map(pluck('name')) || []; - req.instancesNamesStr = JSON.stringify(instancesNames); - next(); - }, - timers.model.stopTimer('github_push_event'), - instances.populateOwnerAndCreatedByForInstances( - 'instanceCreator', 'deployedInstances'), - heap.create(), - heap.model.track('githubPushInfo.user.id', 'github_hook_autodeploy', { - repo: 'githubPushInfo.repo', - branch: 'githubPushInfo.branch', - commit: 'githubPushInfo.headCommit.id', - githubUsername: 'githubPushInfo.user.login', - instancesNames: 'instancesNamesStr', - boxOwnerGithubId: 'deployedInstances[0].owner.github', - boxOwnerGithubUsername: 'deployedInstances[0].owner.username', - duration: 'timersResult[0]' - }, { - githubUsername: 'githubPushInfo.user.login' + mw.req().set('deploymentId', 'pullrequestResult.id'), + flow.try( + pullRequest.model.deploymentStarted('githubPullRequest', + 'deploymentId', 'instance.name', 'targetUrl'), + // we cannot use pushSessionUser, bc patch requires token + // we must reinstantiate runnable model for each call bc of a bug + runnable.create({}, 'instanceCreator'), + runnable.model.updateInstance('instance.shortHash', { + json: {build: 'buildId'} }), - settings.findOneByGithubId('ownerGithubId'), - mw.req('setting').require().then( - notifications.create('setting.notifications'), - notifications.model.notifyOnInstances('githubPushInfo', 'deployedInstances'))) - ).catch( - error.logIfErrMw + // we must reinstantiate runnable model for each call bc of a bug + runnable.create({}, 'instanceCreator'), + runnable.model.waitForInstanceDeployed('instance.shortHash'), + pullRequest.model.deploymentSucceeded('githubPullRequest', + 'deploymentId', 'instance.name', 'targetUrl') + ).catch( + error.logIfErrMw, + pullRequest.model.deploymentErrored('githubPullRequest', + 'deploymentId', 'instance.name', 'targetUrl') + ), + instanceAutoDeployDone() + ) ), noop ); } +function instanceAutoDeployDone () { + return flow.series( + timers.model.stopTimer('timerId'), + heap.create(), + heap.model.track('githubPullRequest.user.id', 'github_hook_autodeploy', { + repo: 'githubPullRequest.repo', + branch: 'githubPullRequest.branch', + commit: 'githubPullRequest.commit', + githubUsername: 'githubPullRequest.user.login', + instanceName: 'instance.name', + boxOwnerGithubId: 'instance.owner.github', + boxOwnerGithubUsername: 'instance.owner.username', + duration: 'timersResult[0]' + }, { + githubUsername: 'githubPullRequest.user.login' + })); +} + +// TODO protocol should be in env later +function createTargetUrl (instanceName, owner) { + return 'https://' + process.env.DOMAIN + '/' + owner + '/' + instanceName; +} + +function doubleEncode (str) { + // we do double encoding here for angular because + // browser would automatically replace `%2F` to `/` and angular router will fail + return encodeURIComponent(encodeURIComponent(str)); +} + +function createServerSelectionUrl (owner, gitInfo) { + return 'https://' + process.env.DOMAIN + '/' + owner + '/serverSelection/' + + gitInfo.repoName + '?branch=' + doubleEncode(gitInfo.branch) + + '&pr=' + gitInfo.number + + '&commit=' + gitInfo.commit + + '&message=' + doubleEncode(gitInfo.headCommit.message); +} + + + function resSendAndNext (sendKey) { return function (req, res, next) { flow.series( @@ -433,24 +374,4 @@ function resSendAndNext (sendKey) { mw.res.json(sendKey))(req, res, noop); next(); // continue to background tasks }; -} - -function waitForContextVersionBuildCompleted (contextVersionIdsKey) { - return mw.req(contextVersionIdsKey).each( - function (contextVersionId, req, eachReq, res, next) { - eachReq.contextVersionId = contextVersionId; - req.pollMongoResults = {}; - next(); - }, - pollMongo({ - idPath: 'contextVersionId', - database: require('models/mongo/context-version'), - successKeyPath: 'build.completed', - failureKeyPath: 'build.error' - }), - function (contextVersionId, req, eachReq, res, next) { - req.pollMongoResults[contextVersionId] = eachReq.pollMongoResult; - next(); - } - ); -} +} \ No newline at end of file diff --git a/lib/routes/instances/index.js b/lib/routes/instances/index.js index 66c54a3cb..42fdbb26b 100644 --- a/lib/routes/instances/index.js +++ b/lib/routes/instances/index.js @@ -35,8 +35,6 @@ var checkFound = require('middlewares/check-found'); var error = require('error'); var Boom = mw.Boom; var isInternalRequest = require('middlewares/is-internal-request'); -var githubNotifications = require('middlewares/notifications').github; -var noop = require('101/noop'); var findInstance = flow.series( instances.findByShortHash('params.id'), @@ -563,25 +561,8 @@ app.patch('/instances/:id', ), instances.model.populateModels(), messenger.emitInstanceUpdate('instance', 'patch'), - resSendAndNext(200, 'instance'), - // handle pr comment in bg - mw.body('build').require() - .then( - githubNotifications.create(), - githubNotifications.model.updatePullRequestsComments('instance', 'origInstance')), - noop); - -function resSendAndNext (statusCode, sendKey) { - return function (req, res, next) { - if (sendKey) { - res.send(statusCode, req[sendKey]); - } - else { - res.sendStatus(statusCode); - } - next(); - }; -} + + mw.res.json('instance')); /** Delete in a instance @@ -617,12 +598,7 @@ app.delete('/instances/:id', messenger.emitInstanceUpdate('instance', 'delete'), // TODO: if deleting last instance for an org we can delete the network // beware of race with create - resSendAndNext(204), - // update github prs' comments ib background - mw.req().set('instance.owner', 'user'), - githubNotifications.create(), - githubNotifications.model.updatePullRequestsComments('instance', null), - noop); + mw.res.send(204)); /** * Fork should deep copy an instance, as well as deep copy its current build. We don't want @@ -653,10 +629,10 @@ app.post('/instances/:id/actions/copy', runnable.model.copyInstance('sessionUser', 'build', 'instance', 'body'), // Now return the new instance - resSendAndNext(201, 'runnableResult'), - githubNotifications.create(), - githubNotifications.model.updatePullRequestsComments('runnableResult', 'instance'), - noop); + // Now return the new instance + mw.res.status(201), + mw.res.json('runnableResult') +); /** Creates a container (instance.container) from the current instance build (instance.build) * @event PUT rest/instances/:id diff --git a/package.json b/package.json index 3dd128622..d3fcdf183 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "express-session": "^1.6.4", "extend": "^1.3.0", "fn-object": "~0.2.2", - "github": "git://github.com/podviaznikov/node-github#ffc3ea93df46746594ef8d69c1cd955b37c81ecb", + "github": "git://github.com/podviaznikov/node-github#3cc3eaaf0777732060d3125e9b782015df07fafb", "handlebars": "^2.0.0", "hashids": "^0.3.3", "hipchat-client": "^1.0.2", diff --git a/templates/github_pull_request_choose_server.hbs b/templates/github_pull_request_choose_server.hbs deleted file mode 100644 index dbc23e327..000000000 --- a/templates/github_pull_request_choose_server.hbs +++ /dev/null @@ -1 +0,0 @@ -[Choose a server](http://{{domain}}/{{owner.login}}/serverSelection/{{repoName}}?branch={{encode branch}}&message={{encode headCommit.message}}&commit={{commit}}&source=pr) to run PR-{{number}}. diff --git a/templates/github_pull_request_server_link.hbs b/templates/github_pull_request_server_link.hbs deleted file mode 100644 index 891ef54e7..000000000 --- a/templates/github_pull_request_server_link.hbs +++ /dev/null @@ -1 +0,0 @@ -[{{name}}](http://{{domain}}/{{owner.login}}/{{name}}?source=pr) \ No newline at end of file diff --git a/test/actions-github.js b/test/actions-github.js index d9be829e8..755094d51 100644 --- a/test/actions-github.js +++ b/test/actions-github.js @@ -8,14 +8,17 @@ var afterEach = Lab.afterEach; var beforeEach = Lab.beforeEach; var expect = Lab.expect; var request = require('request'); +var Boom = require('dat-middleware').Boom; var expects = require('./fixtures/expects'); var exists = require('101/exists'); -var ContextVersion = require('models/mongo/context-version'); var api = require('./fixtures/api-control'); var hooks = require('./fixtures/github-hooks'); var multi = require('./fixtures/multi-factory'); var dock = require('./fixtures/dock'); -var tailBuildStream = require('./fixtures/tail-build-stream'); +var Runnable = require('models/apis/runnable'); +var PullRequest = require('models/apis/pullrequest'); +var Github = require('models/apis/github'); +var cbCount = require('callback-count'); var nock = require('nock'); var generateKey = require('./fixtures/key-factory'); @@ -58,16 +61,16 @@ describe('Github - /actions/github', function () { describe('disabled hooks', function () { beforeEach(function (done) { - ctx.originalBuildsOnPushSetting = process.env.ENABLE_BUILDS_ON_GIT_PUSH; - delete process.env.ENABLE_BUILDS_ON_GIT_PUSH; + ctx.originalBuildsOnPushSetting = process.env.ENABLE_GITHUB_HOOKS; + delete process.env.ENABLE_GITHUB_HOOKS; done(); }); afterEach(function (done) { - process.env.ENABLE_BUILDS_ON_GIT_PUSH = ctx.originalBuildsOnPushSetting; + process.env.ENABLE_GITHUB_HOOKS = ctx.originalBuildsOnPushSetting; done(); }); it('should send response immediately if hooks are disabled', function (done) { - var options = hooks().push; + var options = hooks().pull_request_sync; options.json.ref = 'refs/heads/someotherbranch'; require('./fixtures/mocks/github/users-username')(101, 'bkendall'); request.post(options, function (err, res) { @@ -76,7 +79,7 @@ describe('Github - /actions/github', function () { } else { expect(res.statusCode).to.equal(202); - expect(res.body).to.match(/hooks are currently disabled\. but we gotchu/); + expect(res.body).to.match(/Hooks are currently disabled\. but we gotchu/); done(); } }); @@ -86,13 +89,13 @@ describe('Github - /actions/github', function () { describe('not supported event type', function () { beforeEach(function (done) { - ctx.originalBuildsOnPushSetting = process.env.ENABLE_BUILDS_ON_GIT_PUSH; - process.env.ENABLE_BUILDS_ON_GIT_PUSH = 'true'; + ctx.originalBuildsOnPushSetting = process.env.ENABLE_GITHUB_HOOKS; + process.env.ENABLE_GITHUB_HOOKS = 'true'; done(); }); afterEach(function (done) { - process.env.ENABLE_BUILDS_ON_GIT_PUSH = ctx.originalBuildsOnPushSetting; + process.env.ENABLE_GITHUB_HOOKS = ctx.originalBuildsOnPushSetting; done(); }); @@ -111,377 +114,336 @@ describe('Github - /actions/github', function () { describe('not supported action for pull_request event', function () { beforeEach(function (done) { - ctx.originalBuildsOnPushSetting = process.env.ENABLE_BUILDS_ON_GIT_PUSH; - process.env.ENABLE_BUILDS_ON_GIT_PUSH = 'true'; + ctx.originalBuildsOnPushSetting = process.env.ENABLE_GITHUB_HOOKS; + process.env.ENABLE_GITHUB_HOOKS = 'true'; done(); }); afterEach(function (done) { - process.env.ENABLE_BUILDS_ON_GIT_PUSH = ctx.originalBuildsOnPushSetting; + process.env.ENABLE_GITHUB_HOOKS = ctx.originalBuildsOnPushSetting; done(); }); it('should return OKAY', function (done) { - var options = hooks().pull_request; - options.action = 'delete'; + var options = hooks().pull_request_closed; request.post(options, function (err, res, body) { if (err) { return done(err); } expect(res.statusCode).to.equal(202); - expect(body).to.equal('No appropriate work to be done; finishing.'); + expect(body).to.equal('Do not handle pull request with actions not equal synchronize or opened.'); done(); }); }); }); - describe('when a branch was deleted', function () { + describe('pull_request synchronize', function () { + var ctx = {}; beforeEach(function (done) { - process.env.ENABLE_BUILDS_ON_GIT_PUSH = 'true'; + ctx.originalBuildsOnPushSetting = process.env.ENABLE_GITHUB_HOOKS; + ctx.originaUpdateInstance = Runnable.prototype.updateInstance; + ctx.originaCreateBuild = Runnable.prototype.createBuild; + ctx.originaBuildBuild = Runnable.prototype.buildBuild; + ctx.originalWaitForInstanceDeployed = Runnable.prototype.waitForInstanceDeployed; + ctx.originalBuildErrored = PullRequest.prototype.buildErrored; + ctx.originalDeploymentErrored = PullRequest.prototype.deploymentErrored; + ctx.originalDeploymentSucceeded = PullRequest.prototype.deploymentSucceeded; + ctx.originalCreateDeployment = PullRequest.prototype.createDeployment; + process.env.ENABLE_GITHUB_HOOKS = 'true'; done(); }); - it('should return 202 with thing to do', function (done) { - var options = hooks().push_delete; - require('./fixtures/mocks/github/users-username')(101, 'bkendall'); - request.post(options, function (err, res) { - if (err) { - done(err); - } - else { - expect(res.statusCode).to.equal(202); - expect(res.body).to.match(/Deleted the branch\; no work.+/); - done(); - } - }); - }); - }); - - describe('ignore hooks without commits data', function () { - beforeEach(function (done) { - process.env.ENABLE_BUILDS_ON_GIT_PUSH = 'true'; + afterEach(function (done) { + process.env.ENABLE_GITHUB_HOOKS = ctx.originalBuildsOnPushSetting; + Runnable.prototype.updateInstance = ctx.originaUpdateInstance; + Runnable.prototype.createBuild = ctx.originaCreateBuild; + Runnable.prototype.buildBuild = ctx.originaBuildBuild; + Runnable.prototype.waitForInstanceDeployed = ctx.originalWaitForInstanceDeployed; + PullRequest.prototype.buildErrored = ctx.originalBuildErrored; + PullRequest.prototype.deploymentErrored = ctx.originalDeploymentErrored; + PullRequest.prototype.deploymentSucceeded = ctx.originalDeploymentSucceeded; + PullRequest.prototype.createDeployment = ctx.originalCreateDeployment; done(); }); - it('should send response immediately there are no commits data ([]) in the payload ', function (done) { - var options = hooks().push; - options.json.ref = 'refs/heads/someotherbranch'; - options.json.commits = []; - require('./fixtures/mocks/github/users-username')(101, 'bkendall'); - request.post(options, function (err, res) { - if (err) { - done(err); - } - else { - expect(res.statusCode).to.equal(202); - expect(res.body).to.equal('No commits pushed; no work to be done.'); - done(); - } - }); - }); - it('should send response immediately there are no commits data (null) in the payload ', function (done) { - var options = hooks().push; - options.json.ref = 'refs/heads/someotherbranch'; - options.json.commits = null; - require('./fixtures/mocks/github/users-username')(101, 'bkendall'); - request.post(options, function (err, res) { - if (err) { - done(err); - } - else { - expect(res.statusCode).to.equal(202); - expect(res.body).to.equal('No commits pushed; no work to be done.'); + describe('errored cases', function () { + + beforeEach(function (done) { + multi.createInstance(function (err, instance, build, user, modelsArr) { + ctx.contextVersion = modelsArr[0]; + ctx.context = modelsArr[1]; + ctx.build = build; + ctx.user = user; + ctx.instance = instance; done(); - } + }); }); - }); - }); - describe('push new branch', function () { - var ctx = {}; - describe('disabled by default', function () { - it('should return 202 which means processing of new branches is disabled', function (done) { - var data = { - branch: 'feature-1' - }; - var options = hooks(data).push; - require('./fixtures/mocks/github/users-username')(101, 'podviaznikov'); - require('./fixtures/mocks/docker/container-id-attach')(); - (options.json.repository.owner.name, options.json.repository.name, options.json.head_commit.id); - request.post(options, function (err, res) { - if (err) { - done(err); - } - else { - expect(res.statusCode).to.equal(202); - expect(res.body).to.equal('New branch builds are disabled for now'); + + // it('should set build status to error if error happened build create', {timeout: 6000}, + // function (done) { + + + // Runnable.prototype.createBuild = function (opts, cb) { + // cb(Boom.notFound('Build create failed')); + // }; + + // PullRequest.prototype.buildErrored = function (pullRequest, targetUrl, cb) { + // expect(pullRequest).to.exist(); + // expect(targetUrl).to.include('https://runnable.io/'); + // cb(); + // done(); + // }; + + // var acv = ctx.contextVersion.attrs.appCodeVersions[0]; + // var data = { + // branch: 'master', + // repo: acv.repo, + // ownerId: 2 + // }; + // var options = hooks(data).pull_request_sync; + // require('./fixtures/mocks/github/users-username')(101, 'podviaznikov'); + // require('./fixtures/mocks/docker/container-id-attach')(); + // request.post(options, function (err, res, instancesIds) { + // if (err) { return done(err); } + // expect(instancesIds.length).to.equal(0); + // }); + // }); + + + it('should set deployment status to error if error happened during instance update', {timeout: 6000}, + function (done) { + var baseDeploymentId = 100000; + PullRequest.createDeployment = function (pullRequest, serverName, payload, cb) { + cb(null, {id: baseDeploymentId}); + }; + + + Runnable.prototype.updateInstance = function (id, opts, cb) { + cb(Boom.notFound('Instance update failed')); + }; + + PullRequest.prototype.deploymentErrored = function (pullRequest, deploymentId, serverName, targetUrl) { + expect(pullRequest).to.exist(); + expect(serverName).to.exist(); + expect(targetUrl).to.include('https://runnable.io/'); done(); - } + }; + + var acv = ctx.contextVersion.attrs.appCodeVersions[0]; + var data = { + branch: 'master', + repo: acv.repo + }; + var options = hooks(data).pull_request_sync; + require('./fixtures/mocks/github/users-username')(101, 'podviaznikov'); + require('./fixtures/mocks/docker/container-id-attach')(); + request.post(options, function (err, res, instancesIds) { + if (err) { return done(err); } + expect(res.statusCode).to.equal(201); + expect(instancesIds).to.be.okay; + expect(instancesIds).to.be.an('array'); + expect(instancesIds).to.have.a.lengthOf(1); + expect(instancesIds).to.include(ctx.instance.attrs._id); + }); + }); + + + it('should set deployment status to error if error happened during instance deployment', {timeout: 6000}, + function (done) { + var baseDeploymentId = 100000; + PullRequest.createDeployment = function (pullRequest, serverName, payload, cb) { + cb(null, {id: baseDeploymentId}); + }; + + + Runnable.prototype.waitForInstanceDeployed = function (id, cb) { + cb(Boom.notFound('Instance deploy failed')); + }; + + PullRequest.prototype.deploymentErrored = function (pullRequest, deploymentId, serverName, targetUrl) { + expect(pullRequest).to.exist(); + expect(serverName).to.exist(); + expect(targetUrl).to.include('https://runnable.io/'); + done(); + }; + + var acv = ctx.contextVersion.attrs.appCodeVersions[0]; + var data = { + branch: 'master', + repo: acv.repo + }; + var options = hooks(data).pull_request_sync; + require('./fixtures/mocks/github/users-username')(101, 'podviaznikov'); + require('./fixtures/mocks/docker/container-id-attach')(); + request.post(options, function (err, res, instancesIds) { + if (err) { return done(err); } + expect(res.statusCode).to.equal(201); + expect(instancesIds).to.be.okay; + expect(instancesIds).to.be.an('array'); + expect(instancesIds).to.have.a.lengthOf(1); + expect(instancesIds).to.include(ctx.instance.attrs._id); + }); }); - }); }); - describe('enabled auto builds', function () { + describe('success cases', function () { - before(function (done) { - process.env.ENABLE_NEW_BRANCH_BUILDS_ON_GIT_PUSH = 'true'; - done(); - }); beforeEach(function (done) { + ctx.originalServerSelectionStatus = PullRequest.prototype.serverSelectionStatus; + ctx.originalGetPullRequestHeadCommit = Github.prototype.getPullRequestHeadCommit; multi.createInstance(function (err, instance, build, user, modelsArr) { ctx.contextVersion = modelsArr[0]; ctx.context = modelsArr[1]; ctx.build = build; ctx.user = user; ctx.instance = instance; - done(err); + done(); }); }); - it('should do nothing if there were no context versions found', function (done) { - var data = { - branch: 'feature-1', - repo: 'some-user/some-repo' - }; - var options = hooks(data).push; - require('./fixtures/mocks/github/users-username')(101, 'podviaznikov'); - require('./fixtures/mocks/docker/container-id-attach')(); - request.post(options, function (err, res) { - if (err) { return done(err); } - expect(res.statusCode).to.equal(202); - expect(res.body).to.equal('No appropriate work to be done; finishing.'); - done(); - }); + afterEach(function (done) { + PullRequest.prototype.serverSelectionStatus = ctx.originalServerSelectionStatus; + Github.prototype.getPullRequestHeadCommit = ctx.originalGetPullRequestHeadCommit; + done(); }); - it('should create a build for an existing branch if instance is locked', {timeout: 500}, function (done) { - ctx.instance.update({locked: true}, function (err) { - if (err) { return done(err); } + it('should set server selection status for the branch without instance - pull_request:synchronize', + {timeout: 6000}, function (done) { + + Github.prototype.getPullRequestHeadCommit = function (repo, number, cb) { + cb(null, {commit: { + message: 'hello' + }}); + }; + + PullRequest.prototype.serverSelectionStatus = function (pullRequest, targetUrl, cb) { + expect(pullRequest.number).to.equal(2); + expect(pullRequest.headCommit.message).to.equal('hello'); + expect(pullRequest).to.exist(); + expect(targetUrl).to.include('https://runnable.io/'); + expect(targetUrl).to.include('/serverSelection/'); + cb(); + done(); + }; + var acv = ctx.contextVersion.attrs.appCodeVersions[0]; var data = { - branch: 'master', + branch: 'feature-1', repo: acv.repo }; - var options = hooks(data).push; + var options = hooks(data).pull_request_sync; require('./fixtures/mocks/github/users-username')(101, 'podviaznikov'); require('./fixtures/mocks/docker/container-id-attach')(); - request.post(options, function (err, res, cvs) { + request.post(options, function (err, res, contextVersionIds) { if (err) { return done(err); } expect(res.statusCode).to.equal(201); - expect(cvs).to.be.okay; - expect(cvs).to.be.an('array'); - expect(cvs).to.have.a.lengthOf(1); - var cvId = cvs[0]; - // immediately returned context version with started build - ContextVersion.findById(cvId, function (err, contextVersion) { - if (err) { return done(err); } - expect(contextVersion.build.started).to.exist(); - expect(contextVersion.build.completed).to.not.exist(); - expect(contextVersion.build.duration).to.not.exist(); - expect(contextVersion.build.triggeredBy.github).to.exist(); - expect(contextVersion.build.triggeredAction.manual).to.equal(false); - expect(contextVersion.appCodeVersions[0].lowerRepo).to.equal(options.json.repository.full_name); - expect(contextVersion.appCodeVersions[0].commit).to.equal(options.json.head_commit.id); - expect(contextVersion.appCodeVersions[0].branch).to.equal(data.branch); - expect(contextVersion.build.triggeredAction.appCodeVersion.repo) - .to.equal(options.json.repository.full_name); - expect(contextVersion.build.triggeredAction.appCodeVersion.commit) - .to.equal(options.json.head_commit.id); - expect(contextVersion.build.triggeredAction.appCodeVersion.commitLog) - .to.have.lengthOf(1); - expect(contextVersion.build.triggeredAction.appCodeVersion.commitLog[0].id) - .to.equal(options.json.head_commit.id); - // wait until cv is build. - tailBuildStream(cvId, function (err) { - if (err) { return done(err); } - ContextVersion.findById(cvId, function (err, contextVersion) { - if (err) { return done(err); } - expect(contextVersion.build.started).to.exist(); - expect(contextVersion.build.completed).to.exist(); - expect(contextVersion.build.duration).to.exist(); - expect(contextVersion.build.triggeredBy.github).to.exist(); - expect(contextVersion.build.triggeredAction.manual).to.equal(false); - expect(contextVersion.appCodeVersions[0].lowerRepo).to.equal(options.json.repository.full_name); - expect(contextVersion.appCodeVersions[0].commit).to.equal(options.json.head_commit.id); - expect(contextVersion.appCodeVersions[0].branch).to.equal(data.branch); - expect(contextVersion.build.triggeredAction.appCodeVersion.repo) - .to.equal(options.json.repository.full_name); - expect(contextVersion.build.triggeredAction.appCodeVersion.commit) - .to.equal(options.json.head_commit.id); - expect(contextVersion.build.triggeredAction.appCodeVersion.commitLog) - .to.have.lengthOf(1); - expect(contextVersion.build.triggeredAction.appCodeVersion.commitLog[0].id) - .to.equal(options.json.head_commit.id); - done(); - }); - }); - - }); + expect(contextVersionIds).to.be.okay; + expect(contextVersionIds).to.be.an('array'); + expect(contextVersionIds).to.have.a.lengthOf(1); }); }); - }); - it('should create a build for push on new branch', {timeout: 3000}, function (done) { - var acv = ctx.contextVersion.attrs.appCodeVersions[0]; - var data = { - branch: 'feature-1', - repo: acv.repo - }; - var options = hooks(data).push; - require('./fixtures/mocks/github/users-username')(101, 'podviaznikov'); - require('./fixtures/mocks/docker/container-id-attach')(); - request.post(options, function (err, res, cvs) { - if (err) { return done(err); } - expect(res.statusCode).to.equal(201); - expect(cvs).to.be.okay; - expect(cvs).to.be.an('array'); - expect(cvs).to.have.a.lengthOf(1); - var cvId = cvs[0]; - // immediately returned context version with started build - ContextVersion.findById(cvId, function (err, contextVersion) { - if (err) { return done(err); } - expect(contextVersion.build.started).to.exist(); - expect(contextVersion.build.completed).to.not.exist(); - expect(contextVersion.build.duration).to.not.exist(); - expect(contextVersion.build.triggeredBy.github).to.exist(); - expect(contextVersion.build.triggeredAction.manual).to.equal(false); - expect(contextVersion.appCodeVersions[0].lowerRepo).to.equal(options.json.repository.full_name); - expect(contextVersion.appCodeVersions[0].commit).to.equal(options.json.head_commit.id); - expect(contextVersion.appCodeVersions[0].branch).to.equal(data.branch); - expect(contextVersion.build.triggeredAction.appCodeVersion.repo) - .to.equal(options.json.repository.full_name); - expect(contextVersion.build.triggeredAction.appCodeVersion.commit) - .to.equal(options.json.head_commit.id); - expect(contextVersion.build.triggeredAction.appCodeVersion.commitLog) - .to.have.lengthOf(1); - expect(contextVersion.build.triggeredAction.appCodeVersion.commitLog[0].id) - .to.equal(options.json.head_commit.id); - // wait until cv is build. - tailBuildStream(cvId, function (err) { - if (err) { return done(err); } - ContextVersion.findById(cvId, function (err, contextVersion) { - if (err) { return done(err); } - expect(contextVersion.build.started).to.exist(); - expect(contextVersion.build.completed).to.exist(); - expect(contextVersion.build.duration).to.exist(); - expect(contextVersion.build.triggeredBy.github).to.exist(); - expect(contextVersion.build.triggeredAction.manual).to.equal(false); - expect(contextVersion.appCodeVersions[0].lowerRepo).to.equal(options.json.repository.full_name); - expect(contextVersion.appCodeVersions[0].commit).to.equal(options.json.head_commit.id); - expect(contextVersion.appCodeVersions[0].branch).to.equal(data.branch); - expect(contextVersion.build.triggeredAction.appCodeVersion.repo) - .to.equal(options.json.repository.full_name); - expect(contextVersion.build.triggeredAction.appCodeVersion.commit) - .to.equal(options.json.head_commit.id); - expect(contextVersion.build.triggeredAction.appCodeVersion.commitLog) - .to.have.lengthOf(1); - expect(contextVersion.build.triggeredAction.appCodeVersion.commitLog[0].id) - .to.equal(options.json.head_commit.id); - done(); - }); - }); - - }); - }); - }); - - }); - - }); + it('should set server selection status for the branch without instance - pull_request:opened', + {timeout: 6000}, function (done) { + Github.prototype.getPullRequestHeadCommit = function (repo, number, cb) { + cb(null, {commit: { + message: 'hello' + }}); + }; - describe('push follow branch', function () { - var ctx = {}; + PullRequest.prototype.serverSelectionStatus = function (pullRequest, targetUrl, cb) { + expect(pullRequest.number).to.equal(2); + expect(pullRequest.headCommit.message).to.equal('hello'); + expect(pullRequest).to.exist(); + expect(targetUrl).to.include('https://runnable.io/'); + expect(targetUrl).to.include('/serverSelection/'); + cb(); + done(); + }; - before(function (done) { - process.env.ENABLE_NEW_BRANCH_BUILDS_ON_GIT_PUSH = 'true'; - multi.createInstance(function (err, instance, build, user, modelsArr) { - ctx.contextVersion = modelsArr[0]; - ctx.context = modelsArr[1]; - ctx.build = build; - ctx.user = user; - ctx.instance = instance; - var settings = { - owner: { - github: instance.attrs.owner.github - }, - notifications: { - slack: { - webhookUrl: 'http://slack.com/some-web-hook-url' - }, - hipchat: { - authToken: 'some-hipchat-token', - roomId: 123123 - } - } - }; - user.createSetting({json: settings}, done); - }); - }); + var acv = ctx.contextVersion.attrs.appCodeVersions[0]; + var data = { + branch: 'feature-1', + repo: acv.repo + }; + var options = hooks(data).pull_request_opened; + require('./fixtures/mocks/github/users-username')(101, 'podviaznikov'); + require('./fixtures/mocks/docker/container-id-attach')(); + request.post(options, function (err, res, contextVersionIds) { + if (err) { return done(err); } + expect(res.statusCode).to.equal(201); + expect(contextVersionIds).to.be.okay; + expect(contextVersionIds).to.be.an('array'); + expect(contextVersionIds).to.have.a.lengthOf(1); + }); + }); - it('should redeploy two instances with new build', {timeout: 6000}, function (done) { - ctx.user.copyInstance(ctx.instance.id(), {}, function (err, instance2) { - if (err) { return done(err); } + it('should redeploy two instances with new build', {timeout: 6000}, function (done) { + ctx.user.copyInstance(ctx.instance.id(), {}, function (err, instance2) { + if (err) { return done(err); } - var spyOnClassMethod = require('function-proxy').spyOnClassMethod; - spyOnClassMethod(require('models/notifications/index'), 'notifyOnInstances', - function (githubPushInfo, deployedInstances) { - expect(deployedInstances).to.be.okay; - expect(deployedInstances).to.be.an('array'); - expect(deployedInstances).to.have.a.lengthOf(2); - var hashes = [deployedInstances[0].shortHash, deployedInstances[1].shortHash]; - expect(hashes).to.include(ctx.instance.id()); - expect(hashes).to.include(instance2.shortHash); - expect(githubPushInfo.commitLog).to.have.a.lengthOf(1); + var spyOnClassMethod = require('function-proxy').spyOnClassMethod; + var baseDeploymentId = 1234567; + spyOnClassMethod(require('models/apis/pullrequest'), 'createDeployment', + function (pullRequest, serverName, payload, cb) { + baseDeploymentId++; + cb(null, {id: baseDeploymentId}); + }); + var count = cbCount(2, function () { var expected = { 'contextVersion.build.started': exists, 'contextVersion.build.completed': exists, 'contextVersion.build.duration': exists, 'contextVersion.build.triggeredBy.github': exists, - 'contextVersion.appCodeVersions[0].lowerRepo': options.json.repository.full_name, - 'contextVersion.appCodeVersions[0].commit': options.json.head_commit.id, + 'contextVersion.appCodeVersions[0].lowerRepo': options.json.pull_request.head.repo.full_name, + 'contextVersion.appCodeVersions[0].commit': options.json.pull_request.head.sha, 'contextVersion.appCodeVersions[0].branch': data.branch, 'contextVersion.build.triggeredAction.manual': false, 'contextVersion.build.triggeredAction.appCodeVersion.repo': - options.json.repository.full_name, + options.json.pull_request.head.repo.full_name, 'contextVersion.build.triggeredAction.appCodeVersion.commit': - options.json.head_commit.id, - 'contextVersion.build.triggeredAction.appCodeVersion.commitLog': - function (commitLog) { - expect(commitLog).to.be.an('array'); - expect(commitLog).to.have.lengthOf(1); - expect(commitLog[0].id).to.equal(options.json.head_commit.id); - return true; - } + options.json.pull_request.head.sha }; ctx.instance.fetch(expects.success(200, expected, function (err) { if (err) { return done(err); } ctx.user.newInstance(instance2.shortHash).fetch(expects.success(200, expected, done)); })); }); + spyOnClassMethod(require('models/apis/pullrequest'), 'deploymentSucceeded', + function (pullRequest, deploymentId, serverName, targetUrl) { + expect(pullRequest).to.exist(); + expect(serverName).to.exist(); + expect([1234568, 1234569]).to.contain(deploymentId); + expect(targetUrl).to.include('https://runnable.io/'); + count.next(); + }); - - var acv = ctx.contextVersion.attrs.appCodeVersions[0]; - var data = { - branch: 'master', - repo: acv.repo - }; - var options = hooks(data).push; - require('./fixtures/mocks/github/users-username')(101, 'podviaznikov'); - require('./fixtures/mocks/docker/container-id-attach')(); - request.post(options, function (err, res, instancesIds) { - if (err) { return done(err); } - expect(res.statusCode).to.equal(201); - expect(instancesIds).to.be.okay; - expect(instancesIds).to.be.an('array'); - expect(instancesIds).to.have.a.lengthOf(2); - expect(instancesIds).to.include(ctx.instance.attrs._id); - expect(instancesIds).to.include(instance2._id); + var acv = ctx.contextVersion.attrs.appCodeVersions[0]; + var data = { + branch: 'master', + repo: acv.repo + }; + var options = hooks(data).pull_request_sync; + require('./fixtures/mocks/github/users-username')(101, 'podviaznikov'); + require('./fixtures/mocks/docker/container-id-attach')(); + request.post(options, function (err, res, instancesIds) { + if (err) { return done(err); } + expect(res.statusCode).to.equal(201); + expect(instancesIds).to.be.okay; + expect(instancesIds).to.be.an('array'); + expect(instancesIds).to.have.a.lengthOf(2); + expect(instancesIds).to.include(ctx.instance.attrs._id); + expect(instancesIds).to.include(instance2._id); + }); }); }); + + }); }); diff --git a/test/fixtures/github-hooks.js b/test/fixtures/github-hooks.js index d74964699..9c147e20b 100644 --- a/test/fixtures/github-hooks.js +++ b/test/fixtures/github-hooks.js @@ -6,6 +6,7 @@ module.exports = function (data) { var owner = data.owner || 'podviaznikov'; var repo = data.name || 'hellonode'; var branch = data.branch || 'master'; + var ownerId = data.ownerId || 429706; return { ping: { url: url.format({ @@ -70,7 +71,7 @@ module.exports = function (data) { 'title': 'Feature 1 - progress', 'user': { 'login': owner, - 'id': 429706, + 'id': ownerId, 'avatar_url': 'https://avatars.githubusercontent.com/u/429706?v=3', 'gravatar_id': '', 'url': 'https://api.github.com/users/' + owner, @@ -113,7 +114,7 @@ module.exports = function (data) { 'id': 71727909, 'user': { 'login': owner, - 'id': 429706, + 'id': ownerId, 'avatar_url': 'https://avatars.githubusercontent.com/u/429706?v=3', 'gravatar_id': '', 'url': 'https://api.github.com/users/' + owner, @@ -245,7 +246,7 @@ module.exports = function (data) { } } }, - pull_request: { + pull_request_closed: { url: url.format({ protocol: 'http:', slashes: true, @@ -261,7 +262,7 @@ module.exports = function (data) { 'content-type': 'application/json' }, json: { - 'action': 'opened', + 'action': 'closed', 'number': 2, 'pull_request': { 'url': 'https://api.github.com/repos/' + fullRepo + '/pulls/2', @@ -676,6 +677,868 @@ module.exports = function (data) { } } }, + pull_request_opened: { + url: url.format({ + protocol: 'http:', + slashes: true, + host: process.env.ROOT_DOMAIN, + pathname: 'actions/github' + }), + headers: { + host: process.env.ROOT_DOMAIN, + accept: '*/*', + 'user-agent': 'GitHub Hookshot 3e70583', + 'x-github-event': 'pull_request', + 'x-github-delivery': 'e05eb1f2-fbc7-11e3-8e1d-423f213c5718', + 'content-type': 'application/json' + }, + json: { + 'action': 'opened', + 'number': 2, + 'pull_request': { + 'url': 'https://api.github.com/repos/' + fullRepo + '/pulls/2', + 'id': 28062652, + 'html_url': 'https://github.com/' + fullRepo + '/pull/2', + 'diff_url': 'https://github.com/' + fullRepo + '/pull/2.diff', + 'patch_url': 'https://github.com/' + fullRepo + '/pull/2.patch', + 'issue_url': 'https://api.github.com/repos/' + fullRepo + '/issues/2', + 'number': 2, + 'state': 'open', + 'locked': false, + 'title': 'Feature 2', + 'user': { + 'login': owner, + 'id': 429706, + 'avatar_url': 'https://avatars.githubusercontent.com/u/429706?v=3', + 'gravatar_id': '', + 'url': 'https://api.github.com/users/' + owner, + 'html_url': 'https://github.com/' + owner, + 'followers_url': 'https://api.github.com/users/' + owner + '/followers', + 'following_url': 'https://api.github.com/users/' + owner + '/following{/other_user}', + 'gists_url': 'https://api.github.com/users/' + owner + '/gists{/gist_id}', + 'starred_url': 'https://api.github.com/users/' + owner + '/starred{/owner}{/repo}', + 'subscriptions_url': 'https://api.github.com/users/' + owner + '/subscriptions', + 'organizations_url': 'https://api.github.com/users/' + owner + '/orgs', + 'repos_url': 'https://api.github.com/users/' + owner + '/repos', + 'events_url': 'https://api.github.com/users/' + owner + '/events{/privacy}', + 'received_events_url': 'https://api.github.com/users/' + owner + '/received_events', + 'type': 'User', + 'site_admin': false + }, + 'body': '', + 'created_at': '2015-01-26T21:39:11Z', + 'updated_at': '2015-01-26T21:39:11Z', + 'closed_at': null, + 'merged_at': null, + 'merge_commit_sha': null, + 'assignee': null, + 'milestone': null, + 'commits_url': 'https://api.github.com/repos/' + fullRepo + '/pulls/2/commits', + 'review_comments_url': 'https://api.github.com/repos/' + fullRepo + '/pulls/2/comments', + 'review_comment_url': 'https://api.github.com/repos/' + fullRepo + '/pulls/comments/{number}', + 'comments_url': 'https://api.github.com/repos/' + fullRepo + '/issues/2/comments', + 'statuses_url': 'https://api.github.com/repos/' + fullRepo + + '/statuses/023a0cba6759ed69cbcb91e4a7f61aa3b8ca6fd3', + 'head': { + 'label': '' + owner + ':feature-2', + 'ref': 'feature-2', + 'sha': '023a0cba6759ed69cbcb91e4a7f61aa3b8ca6fd3', + 'user': { + 'login': owner, + 'id': 429706, + 'avatar_url': 'https://avatars.githubusercontent.com/u/429706?v=3', + 'gravatar_id': '', + 'url': 'https://api.github.com/users/' + owner, + 'html_url': 'https://github.com/' + owner, + 'followers_url': 'https://api.github.com/users/' + owner + '/followers', + 'following_url': 'https://api.github.com/users/' + owner + '/following{/other_user}', + 'gists_url': 'https://api.github.com/users/' + owner + '/gists{/gist_id}', + 'starred_url': 'https://api.github.com/users/' + owner + '/starred{/owner}{/repo}', + 'subscriptions_url': 'https://api.github.com/users/' + owner + '/subscriptions', + 'organizations_url': 'https://api.github.com/users/' + owner + '/orgs', + 'repos_url': 'https://api.github.com/users/' + owner + '/repos', + 'events_url': 'https://api.github.com/users/' + owner + '/events{/privacy}', + 'received_events_url': 'https://api.github.com/users/' + owner + '/received_events', + 'type': 'User', + 'site_admin': false + }, + 'repo': { + 'id': 28254077, + 'name': 'hellonode', + 'full_name': fullRepo , + 'owner': { + 'login': owner, + 'id': 429706, + 'avatar_url': 'https://avatars.githubusercontent.com/u/429706?v=3', + 'gravatar_id': '', + 'url': 'https://api.github.com/users/' + owner, + 'html_url': 'https://github.com/' + owner, + 'followers_url': 'https://api.github.com/users/' + owner + '/followers', + 'following_url': 'https://api.github.com/users/' + owner + '/following{/other_user}', + 'gists_url': 'https://api.github.com/users/' + owner + '/gists{/gist_id}', + 'starred_url': 'https://api.github.com/users/' + owner + '/starred{/owner}{/repo}', + 'subscriptions_url': 'https://api.github.com/users/' + owner + '/subscriptions', + 'organizations_url': 'https://api.github.com/users/' + owner + '/orgs', + 'repos_url': 'https://api.github.com/users/' + owner + '/repos', + 'events_url': 'https://api.github.com/users/' + owner + '/events{/privacy}', + 'received_events_url': 'https://api.github.com/users/' + owner + '/received_events', + 'type': 'User', + 'site_admin': false + }, + 'private': false, + 'html_url': 'https://github.com/' + fullRepo, + 'description': '', + 'fork': false, + 'url': 'https://api.github.com/repos/' + fullRepo, + 'forks_url': 'https://api.github.com/repos/' + fullRepo + '/forks', + 'keys_url': 'https://api.github.com/repos/' + fullRepo + '/keys{/key_id}', + 'collaborators_url': 'https://api.github.com/repos/' + fullRepo + '/collaborators{/collaborator}', + 'teams_url': 'https://api.github.com/repos/' + fullRepo + '/teams', + 'hooks_url': 'https://api.github.com/repos/' + fullRepo + '/hooks', + 'issue_events_url': 'https://api.github.com/repos/' + fullRepo + '/issues/events{/number}', + 'events_url': 'https://api.github.com/repos/' + fullRepo + '/events', + 'assignees_url': 'https://api.github.com/repos/' + fullRepo + '/assignees{/user}', + 'branches_url': 'https://api.github.com/repos/' + fullRepo + '/branches{/branch}', + 'tags_url': 'https://api.github.com/repos/' + fullRepo + '/tags', + 'blobs_url': 'https://api.github.com/repos/' + fullRepo + '/git/blobs{/sha}', + 'git_tags_url': 'https://api.github.com/repos/' + fullRepo + '/git/tags{/sha}', + 'git_refs_url': 'https://api.github.com/repos/' + fullRepo + '/git/refs{/sha}', + 'trees_url': 'https://api.github.com/repos/' + fullRepo + '/git/trees{/sha}', + 'statuses_url': 'https://api.github.com/repos/' + fullRepo + '/statuses/{sha}', + 'languages_url': 'https://api.github.com/repos/' + fullRepo + '/languages', + 'stargazers_url': 'https://api.github.com/repos/' + fullRepo + '/stargazers', + 'contributors_url': 'https://api.github.com/repos/' + fullRepo + '/contributors', + 'subscribers_url': 'https://api.github.com/repos/' + fullRepo + '/subscribers', + 'subscription_url': 'https://api.github.com/repos/' + fullRepo + '/subscription', + 'commits_url': 'https://api.github.com/repos/' + fullRepo + '/commits{/sha}', + 'git_commits_url': 'https://api.github.com/repos/' + fullRepo + '/git/commits{/sha}', + 'comments_url': 'https://api.github.com/repos/' + fullRepo + '/comments{/number}', + 'issue_comment_url': 'https://api.github.com/repos/' + fullRepo + '/issues/comments/{number}', + 'contents_url': 'https://api.github.com/repos/' + fullRepo + '/contents/{+path}', + 'compare_url': 'https://api.github.com/repos/' + fullRepo + '/compare/{base}...{head}', + 'merges_url': 'https://api.github.com/repos/' + fullRepo + '/merges', + 'archive_url': 'https://api.github.com/repos/' + fullRepo + '/{archive_format}{/ref}', + 'downloads_url': 'https://api.github.com/repos/' + fullRepo + '/downloads', + 'issues_url': 'https://api.github.com/repos/' + fullRepo + '/issues{/number}', + 'pulls_url': 'https://api.github.com/repos/' + fullRepo + '/pulls{/number}', + 'milestones_url': 'https://api.github.com/repos/' + fullRepo + '/milestones{/number}', + 'notifications_url': 'https://api.github.com/repos/' + fullRepo + + '/notifications{?since,all,participating}', + 'labels_url': 'https://api.github.com/repos/' + fullRepo + '/labels{/name}', + 'releases_url': 'https://api.github.com/repos/' + fullRepo + '/releases{/id}', + 'created_at': '2014-12-20T02:22:29Z', + 'updated_at': '2015-01-26T20:09:00Z', + 'pushed_at': '2015-01-26T20:09:00Z', + 'git_url': 'git://github.com/' + fullRepo + '.git', + 'ssh_url': 'git@github.com:' + fullRepo + '.git', + 'clone_url': 'https://github.com/' + fullRepo + '.git', + 'svn_url': 'https://github.com/' + fullRepo, + 'homepage': null, + 'size': 1631, + 'stargazers_count': 0, + 'watchers_count': 0, + 'language': 'JavaScript', + 'has_issues': true, + 'has_downloads': true, + 'has_wiki': true, + 'has_pages': false, + 'forks_count': 1, + 'mirror_url': null, + 'open_issues_count': 2, + 'forks': 1, + 'open_issues': 2, + 'watchers': 0, + 'default_branch': 'master' + } + }, + 'base': { + 'label': '' + owner + ':master', + 'ref': 'master', + 'sha': '3c8e16a66aca7a98dd93bde0840f2aa6f0da427b', + 'user': { + 'login': owner, + 'id': 429706, + 'avatar_url': 'https://avatars.githubusercontent.com/u/429706?v=3', + 'gravatar_id': '', + 'url': 'https://api.github.com/users/' + owner, + 'html_url': 'https://github.com/' + owner, + 'followers_url': 'https://api.github.com/users/' + owner + '/followers', + 'following_url': 'https://api.github.com/users/' + owner + '/following{/other_user}', + 'gists_url': 'https://api.github.com/users/' + owner + '/gists{/gist_id}', + 'starred_url': 'https://api.github.com/users/' + owner + '/starred{/owner}{/repo}', + 'subscriptions_url': 'https://api.github.com/users/' + owner + '/subscriptions', + 'organizations_url': 'https://api.github.com/users/' + owner + '/orgs', + 'repos_url': 'https://api.github.com/users/' + owner + '/repos', + 'events_url': 'https://api.github.com/users/' + owner + '/events{/privacy}', + 'received_events_url': 'https://api.github.com/users/' + owner + '/received_events', + 'type': 'User', + 'site_admin': false + }, + 'repo': { + 'id': 28254077, + 'name': 'hellonode', + 'full_name': fullRepo, + 'owner': { + 'login': owner, + 'id': 429706, + 'avatar_url': 'https://avatars.githubusercontent.com/u/429706?v=3', + 'gravatar_id': '', + 'url': 'https://api.github.com/users/' + owner, + 'html_url': 'https://github.com/' + owner, + 'followers_url': 'https://api.github.com/users/' + owner + '/followers', + 'following_url': 'https://api.github.com/users/' + owner + '/following{/other_user}', + 'gists_url': 'https://api.github.com/users/' + owner + '/gists{/gist_id}', + 'starred_url': 'https://api.github.com/users/' + owner + '/starred{/owner}{/repo}', + 'subscriptions_url': 'https://api.github.com/users/' + owner + '/subscriptions', + 'organizations_url': 'https://api.github.com/users/' + owner + '/orgs', + 'repos_url': 'https://api.github.com/users/' + owner + '/repos', + 'events_url': 'https://api.github.com/users/' + owner + '/events{/privacy}', + 'received_events_url': 'https://api.github.com/users/' + owner + '/received_events', + 'type': 'User', + 'site_admin': false + }, + 'private': false, + 'html_url': 'https://github.com/' + fullRepo, + 'description': '', + 'fork': false, + 'url': 'https://api.github.com/repos/' + fullRepo, + 'forks_url': 'https://api.github.com/repos/' + fullRepo + '/forks', + 'keys_url': 'https://api.github.com/repos/' + fullRepo + '/keys{/key_id}', + 'collaborators_url': 'https://api.github.com/repos/' + fullRepo + '/collaborators{/collaborator}', + 'teams_url': 'https://api.github.com/repos/' + fullRepo + '/teams', + 'hooks_url': 'https://api.github.com/repos/' + fullRepo + '/hooks', + 'issue_events_url': 'https://api.github.com/repos/' + fullRepo + '/issues/events{/number}', + 'events_url': 'https://api.github.com/repos/' + fullRepo + '/events', + 'assignees_url': 'https://api.github.com/repos/' + fullRepo + '/assignees{/user}', + 'branches_url': 'https://api.github.com/repos/' + fullRepo + '/branches{/branch}', + 'tags_url': 'https://api.github.com/repos/' + fullRepo + '/tags', + 'blobs_url': 'https://api.github.com/repos/' + fullRepo + '/git/blobs{/sha}', + 'git_tags_url': 'https://api.github.com/repos/' + fullRepo + '/git/tags{/sha}', + 'git_refs_url': 'https://api.github.com/repos/' + fullRepo + '/git/refs{/sha}', + 'trees_url': 'https://api.github.com/repos/' + fullRepo + '/git/trees{/sha}', + 'statuses_url': 'https://api.github.com/repos/' + fullRepo + '/statuses/{sha}', + 'languages_url': 'https://api.github.com/repos/' + fullRepo + '/languages', + 'stargazers_url': 'https://api.github.com/repos/' + fullRepo + '/stargazers', + 'contributors_url': 'https://api.github.com/repos/' + fullRepo + '/contributors', + 'subscribers_url': 'https://api.github.com/repos/' + fullRepo + '/subscribers', + 'subscription_url': 'https://api.github.com/repos/' + fullRepo + '/subscription', + 'commits_url': 'https://api.github.com/repos/' + fullRepo + '/commits{/sha}', + 'git_commits_url': 'https://api.github.com/repos/' + fullRepo + '/git/commits{/sha}', + 'comments_url': 'https://api.github.com/repos/' + fullRepo + '/comments{/number}', + 'issue_comment_url': 'https://api.github.com/repos/' + fullRepo + '/issues/comments/{number}', + 'contents_url': 'https://api.github.com/repos/' + fullRepo + '/contents/{+path}', + 'compare_url': 'https://api.github.com/repos/' + fullRepo + '/compare/{base}...{head}', + 'merges_url': 'https://api.github.com/repos/' + fullRepo + '/merges', + 'archive_url': 'https://api.github.com/repos/' + fullRepo + '/{archive_format}{/ref}', + 'downloads_url': 'https://api.github.com/repos/' + fullRepo + '/downloads', + 'issues_url': 'https://api.github.com/repos/' + fullRepo + '/issues{/number}', + 'pulls_url': 'https://api.github.com/repos/' + fullRepo + '/pulls{/number}', + 'milestones_url': 'https://api.github.com/repos/' + fullRepo + '/milestones{/number}', + 'notifications_url': 'https://api.github.com/repos/' + fullRepo + + '/notifications{?since,all,participating}', + 'labels_url': 'https://api.github.com/repos/' + fullRepo + '/labels{/name}', + 'releases_url': 'https://api.github.com/repos/' + fullRepo + '/releases{/id}', + 'created_at': '2014-12-20T02:22:29Z', + 'updated_at': '2015-01-26T20:09:00Z', + 'pushed_at': '2015-01-26T20:09:00Z', + 'git_url': 'git://github.com/' + fullRepo + '.git', + 'ssh_url': 'git@github.com:' + fullRepo + '.git', + 'clone_url': 'https://github.com/' + fullRepo + '.git', + 'svn_url': 'https://github.com/' + fullRepo, + 'homepage': null, + 'size': 1631, + 'stargazers_count': 0, + 'watchers_count': 0, + 'language': 'JavaScript', + 'has_issues': true, + 'has_downloads': true, + 'has_wiki': true, + 'has_pages': false, + 'forks_count': 1, + 'mirror_url': null, + 'open_issues_count': 2, + 'forks': 1, + 'open_issues': 2, + 'watchers': 0, + 'default_branch': 'master' + } + }, + '_links': { + 'self': { + 'href': 'https://api.github.com/repos/' + fullRepo + '/pulls/2' + }, + 'html': { + 'href': 'https://github.com/' + fullRepo + '/pull/2' + }, + 'issue': { + 'href': 'https://api.github.com/repos/' + fullRepo + '/issues/2' + }, + 'comments': { + 'href': 'https://api.github.com/repos/' + fullRepo + '/issues/2/comments' + }, + 'review_comments': { + 'href': 'https://api.github.com/repos/' + fullRepo + '/pulls/2/comments' + }, + 'review_comment': { + 'href': 'https://api.github.com/repos/' + fullRepo + '/pulls/comments/{number}' + }, + 'commits': { + 'href': 'https://api.github.com/repos/' + fullRepo + '/pulls/2/commits' + }, + 'statuses': { + 'href': 'https://api.github.com/repos/' + fullRepo + '/statuses/023a0cba6759ed69cbcb91e4a7f61aa3b8ca6fd3' + } + }, + 'merged': false, + 'mergeable': null, + 'mergeable_state': 'unknown', + 'merged_by': null, + 'comments': 0, + 'review_comments': 0, + 'commits': 18, + 'additions': 1, + 'deletions': 1, + 'changed_files': 1 + }, + 'repository': { + 'id': 28254077, + 'name': 'hellonode', + 'full_name': fullRepo, + 'owner': { + 'login': owner, + 'id': 429706, + 'avatar_url': 'https://avatars.githubusercontent.com/u/429706?v=3', + 'gravatar_id': '', + 'url': 'https://api.github.com/users/' + owner, + 'html_url': 'https://github.com/' + owner, + 'followers_url': 'https://api.github.com/users/' + owner + '/followers', + 'following_url': 'https://api.github.com/users/' + owner + '/following{/other_user}', + 'gists_url': 'https://api.github.com/users/' + owner + '/gists{/gist_id}', + 'starred_url': 'https://api.github.com/users/' + owner + '/starred{/owner}{/repo}', + 'subscriptions_url': 'https://api.github.com/users/' + owner + '/subscriptions', + 'organizations_url': 'https://api.github.com/users/' + owner + '/orgs', + 'repos_url': 'https://api.github.com/users/' + owner + '/repos', + 'events_url': 'https://api.github.com/users/' + owner + '/events{/privacy}', + 'received_events_url': 'https://api.github.com/users/' + owner + '/received_events', + 'type': 'User', + 'site_admin': false + }, + 'private': false, + 'html_url': 'https://github.com/' + fullRepo, + 'description': '', + 'fork': false, + 'url': 'https://api.github.com/repos/' + fullRepo, + 'forks_url': 'https://api.github.com/repos/' + fullRepo + '/forks', + 'keys_url': 'https://api.github.com/repos/' + fullRepo + '/keys{/key_id}', + 'collaborators_url': 'https://api.github.com/repos/' + fullRepo + '/collaborators{/collaborator}', + 'teams_url': 'https://api.github.com/repos/' + fullRepo + '/teams', + 'hooks_url': 'https://api.github.com/repos/' + fullRepo + '/hooks', + 'issue_events_url': 'https://api.github.com/repos/' + fullRepo + '/issues/events{/number}', + 'events_url': 'https://api.github.com/repos/' + fullRepo + '/events', + 'assignees_url': 'https://api.github.com/repos/' + fullRepo + '/assignees{/user}', + 'branches_url': 'https://api.github.com/repos/' + fullRepo + '/branches{/branch}', + 'tags_url': 'https://api.github.com/repos/' + fullRepo + '/tags', + 'blobs_url': 'https://api.github.com/repos/' + fullRepo + '/git/blobs{/sha}', + 'git_tags_url': 'https://api.github.com/repos/' + fullRepo + '/git/tags{/sha}', + 'git_refs_url': 'https://api.github.com/repos/' + fullRepo + '/git/refs{/sha}', + 'trees_url': 'https://api.github.com/repos/' + fullRepo + '/git/trees{/sha}', + 'statuses_url': 'https://api.github.com/repos/' + fullRepo + '/statuses/{sha}', + 'languages_url': 'https://api.github.com/repos/' + fullRepo + '/languages', + 'stargazers_url': 'https://api.github.com/repos/' + fullRepo + '/stargazers', + 'contributors_url': 'https://api.github.com/repos/' + fullRepo + '/contributors', + 'subscribers_url': 'https://api.github.com/repos/' + fullRepo + '/subscribers', + 'subscription_url': 'https://api.github.com/repos/' + fullRepo + '/subscription', + 'commits_url': 'https://api.github.com/repos/' + fullRepo + '/commits{/sha}', + 'git_commits_url': 'https://api.github.com/repos/' + fullRepo + '/git/commits{/sha}', + 'comments_url': 'https://api.github.com/repos/' + fullRepo + '/comments{/number}', + 'issue_comment_url': 'https://api.github.com/repos/' + fullRepo + '/issues/comments/{number}', + 'contents_url': 'https://api.github.com/repos/' + fullRepo + '/contents/{+path}', + 'compare_url': 'https://api.github.com/repos/' + fullRepo + '/compare/{base}...{head}', + 'merges_url': 'https://api.github.com/repos/' + fullRepo + '/merges', + 'archive_url': 'https://api.github.com/repos/' + fullRepo + '/{archive_format}{/ref}', + 'downloads_url': 'https://api.github.com/repos/' + fullRepo + '/downloads', + 'issues_url': 'https://api.github.com/repos/' + fullRepo + '/issues{/number}', + 'pulls_url': 'https://api.github.com/repos/' + fullRepo + '/pulls{/number}', + 'milestones_url': 'https://api.github.com/repos/' + fullRepo + '/milestones{/number}', + 'notifications_url': 'https://api.github.com/repos/' + fullRepo + '/notifications{?since,all,participating}', + 'labels_url': 'https://api.github.com/repos/' + fullRepo + '/labels{/name}', + 'releases_url': 'https://api.github.com/repos/' + fullRepo + '/releases{/id}', + 'created_at': '2014-12-20T02:22:29Z', + 'updated_at': '2015-01-26T20:09:00Z', + 'pushed_at': '2015-01-26T20:09:00Z', + 'git_url': 'git://github.com/' + fullRepo + '.git', + 'ssh_url': 'git@github.com:' + fullRepo + '.git', + 'clone_url': 'https://github.com/' + fullRepo + '.git', + 'svn_url': 'https://github.com/' + fullRepo, + 'homepage': null, + 'size': 1631, + 'stargazers_count': 0, + 'watchers_count': 0, + 'language': 'JavaScript', + 'has_issues': true, + 'has_downloads': true, + 'has_wiki': true, + 'has_pages': false, + 'forks_count': 1, + 'mirror_url': null, + 'open_issues_count': 2, + 'forks': 1, + 'open_issues': 2, + 'watchers': 0, + 'default_branch': 'master' + }, + 'sender': { + 'login': owner, + 'id': 429706, + 'avatar_url': 'https://avatars.githubusercontent.com/u/429706?v=3', + 'gravatar_id': '', + 'url': 'https://api.github.com/users/' + owner, + 'html_url': 'https://github.com/' + owner, + 'followers_url': 'https://api.github.com/users/' + owner + '/followers', + 'following_url': 'https://api.github.com/users/' + owner + '/following{/other_user}', + 'gists_url': 'https://api.github.com/users/' + owner + '/gists{/gist_id}', + 'starred_url': 'https://api.github.com/users/' + owner + '/starred{/owner}{/repo}', + 'subscriptions_url': 'https://api.github.com/users/' + owner + '/subscriptions', + 'organizations_url': 'https://api.github.com/users/' + owner + '/orgs', + 'repos_url': 'https://api.github.com/users/' + owner + '/repos', + 'events_url': 'https://api.github.com/users/' + owner + '/events{/privacy}', + 'received_events_url': 'https://api.github.com/users/' + owner + '/received_events', + 'type': 'User', + 'site_admin': false + } + } + }, + pull_request_sync: { + url: url.format({ + protocol: 'http:', + slashes: true, + host: process.env.ROOT_DOMAIN, + pathname: 'actions/github' + }), + headers: { + host: process.env.ROOT_DOMAIN, + accept: '*/*', + 'user-agent': 'GitHub Hookshot 3e70583', + 'x-github-event': 'pull_request', + 'x-github-delivery': 'e05eb1f2-fbc7-11e3-8e1d-423f213c5718', + 'content-type': 'application/json' + }, + json: { + 'action': 'synchronize', + 'number': 2, + 'pull_request': { + 'url': 'https://api.github.com/repos/' + fullRepo + '/pulls/2', + 'id': 28062652, + 'html_url': 'https://github.com/' + fullRepo + '/pull/2', + 'diff_url': 'https://github.com/' + fullRepo + '/pull/2.diff', + 'patch_url': 'https://github.com/' + fullRepo + '/pull/2.patch', + 'issue_url': 'https://api.github.com/repos/' + fullRepo + '/issues/2', + 'number': 2, + 'state': 'open', + 'locked': false, + 'title': 'Feature 2', + 'user': { + 'login': owner, + 'id': ownerId, + 'avatar_url': 'https://avatars.githubusercontent.com/u/429706?v=3', + 'gravatar_id': '', + 'url': 'https://api.github.com/users/' + owner, + 'html_url': 'https://github.com/' + owner, + 'followers_url': 'https://api.github.com/users/' + owner + '/followers', + 'following_url': 'https://api.github.com/users/' + owner + '/following{/other_user}', + 'gists_url': 'https://api.github.com/users/' + owner + '/gists{/gist_id}', + 'starred_url': 'https://api.github.com/users/' + owner + '/starred{/owner}{/repo}', + 'subscriptions_url': 'https://api.github.com/users/' + owner + '/subscriptions', + 'organizations_url': 'https://api.github.com/users/' + owner + '/orgs', + 'repos_url': 'https://api.github.com/users/' + owner + '/repos', + 'events_url': 'https://api.github.com/users/' + owner + '/events{/privacy}', + 'received_events_url': 'https://api.github.com/users/' + owner + '/received_events', + 'type': 'User', + 'site_admin': false + }, + 'body': '', + 'created_at': '2015-01-26T21:39:11Z', + 'updated_at': '2015-01-26T21:39:11Z', + 'closed_at': null, + 'merged_at': null, + 'merge_commit_sha': null, + 'assignee': null, + 'milestone': null, + 'commits_url': 'https://api.github.com/repos/' + fullRepo + '/pulls/2/commits', + 'review_comments_url': 'https://api.github.com/repos/' + fullRepo + '/pulls/2/comments', + 'review_comment_url': 'https://api.github.com/repos/' + fullRepo + '/pulls/comments/{number}', + 'comments_url': 'https://api.github.com/repos/' + fullRepo + '/issues/2/comments', + 'statuses_url': 'https://api.github.com/repos/' + fullRepo + + '/statuses/023a0cba6759ed69cbcb91e4a7f61aa3b8ca6fd3', + 'head': { + 'label': '' + owner + ':feature-2', + 'ref': branch, + 'sha': '023a0cba6759ed69cbcb91e4a7f61aa3b8ca6fd3', + 'user': { + 'login': owner, + 'id': ownerId, + 'avatar_url': 'https://avatars.githubusercontent.com/u/429706?v=3', + 'gravatar_id': '', + 'url': 'https://api.github.com/users/' + owner, + 'html_url': 'https://github.com/' + owner, + 'followers_url': 'https://api.github.com/users/' + owner + '/followers', + 'following_url': 'https://api.github.com/users/' + owner + '/following{/other_user}', + 'gists_url': 'https://api.github.com/users/' + owner + '/gists{/gist_id}', + 'starred_url': 'https://api.github.com/users/' + owner + '/starred{/owner}{/repo}', + 'subscriptions_url': 'https://api.github.com/users/' + owner + '/subscriptions', + 'organizations_url': 'https://api.github.com/users/' + owner + '/orgs', + 'repos_url': 'https://api.github.com/users/' + owner + '/repos', + 'events_url': 'https://api.github.com/users/' + owner + '/events{/privacy}', + 'received_events_url': 'https://api.github.com/users/' + owner + '/received_events', + 'type': 'User', + 'site_admin': false + }, + 'repo': { + 'id': 28254077, + 'name': 'hellonode', + 'full_name': fullRepo , + 'owner': { + 'login': owner, + 'id': ownerId, + 'avatar_url': 'https://avatars.githubusercontent.com/u/429706?v=3', + 'gravatar_id': '', + 'url': 'https://api.github.com/users/' + owner, + 'html_url': 'https://github.com/' + owner, + 'followers_url': 'https://api.github.com/users/' + owner + '/followers', + 'following_url': 'https://api.github.com/users/' + owner + '/following{/other_user}', + 'gists_url': 'https://api.github.com/users/' + owner + '/gists{/gist_id}', + 'starred_url': 'https://api.github.com/users/' + owner + '/starred{/owner}{/repo}', + 'subscriptions_url': 'https://api.github.com/users/' + owner + '/subscriptions', + 'organizations_url': 'https://api.github.com/users/' + owner + '/orgs', + 'repos_url': 'https://api.github.com/users/' + owner + '/repos', + 'events_url': 'https://api.github.com/users/' + owner + '/events{/privacy}', + 'received_events_url': 'https://api.github.com/users/' + owner + '/received_events', + 'type': 'User', + 'site_admin': false + }, + 'private': false, + 'html_url': 'https://github.com/' + fullRepo, + 'description': '', + 'fork': false, + 'url': 'https://api.github.com/repos/' + fullRepo, + 'forks_url': 'https://api.github.com/repos/' + fullRepo + '/forks', + 'keys_url': 'https://api.github.com/repos/' + fullRepo + '/keys{/key_id}', + 'collaborators_url': 'https://api.github.com/repos/' + fullRepo + '/collaborators{/collaborator}', + 'teams_url': 'https://api.github.com/repos/' + fullRepo + '/teams', + 'hooks_url': 'https://api.github.com/repos/' + fullRepo + '/hooks', + 'issue_events_url': 'https://api.github.com/repos/' + fullRepo + '/issues/events{/number}', + 'events_url': 'https://api.github.com/repos/' + fullRepo + '/events', + 'assignees_url': 'https://api.github.com/repos/' + fullRepo + '/assignees{/user}', + 'branches_url': 'https://api.github.com/repos/' + fullRepo + '/branches{/branch}', + 'tags_url': 'https://api.github.com/repos/' + fullRepo + '/tags', + 'blobs_url': 'https://api.github.com/repos/' + fullRepo + '/git/blobs{/sha}', + 'git_tags_url': 'https://api.github.com/repos/' + fullRepo + '/git/tags{/sha}', + 'git_refs_url': 'https://api.github.com/repos/' + fullRepo + '/git/refs{/sha}', + 'trees_url': 'https://api.github.com/repos/' + fullRepo + '/git/trees{/sha}', + 'statuses_url': 'https://api.github.com/repos/' + fullRepo + '/statuses/{sha}', + 'languages_url': 'https://api.github.com/repos/' + fullRepo + '/languages', + 'stargazers_url': 'https://api.github.com/repos/' + fullRepo + '/stargazers', + 'contributors_url': 'https://api.github.com/repos/' + fullRepo + '/contributors', + 'subscribers_url': 'https://api.github.com/repos/' + fullRepo + '/subscribers', + 'subscription_url': 'https://api.github.com/repos/' + fullRepo + '/subscription', + 'commits_url': 'https://api.github.com/repos/' + fullRepo + '/commits{/sha}', + 'git_commits_url': 'https://api.github.com/repos/' + fullRepo + '/git/commits{/sha}', + 'comments_url': 'https://api.github.com/repos/' + fullRepo + '/comments{/number}', + 'issue_comment_url': 'https://api.github.com/repos/' + fullRepo + '/issues/comments/{number}', + 'contents_url': 'https://api.github.com/repos/' + fullRepo + '/contents/{+path}', + 'compare_url': 'https://api.github.com/repos/' + fullRepo + '/compare/{base}...{head}', + 'merges_url': 'https://api.github.com/repos/' + fullRepo + '/merges', + 'archive_url': 'https://api.github.com/repos/' + fullRepo + '/{archive_format}{/ref}', + 'downloads_url': 'https://api.github.com/repos/' + fullRepo + '/downloads', + 'issues_url': 'https://api.github.com/repos/' + fullRepo + '/issues{/number}', + 'pulls_url': 'https://api.github.com/repos/' + fullRepo + '/pulls{/number}', + 'milestones_url': 'https://api.github.com/repos/' + fullRepo + '/milestones{/number}', + 'notifications_url': 'https://api.github.com/repos/' + fullRepo + + '/notifications{?since,all,participating}', + 'labels_url': 'https://api.github.com/repos/' + fullRepo + '/labels{/name}', + 'releases_url': 'https://api.github.com/repos/' + fullRepo + '/releases{/id}', + 'created_at': '2014-12-20T02:22:29Z', + 'updated_at': '2015-01-26T20:09:00Z', + 'pushed_at': '2015-01-26T20:09:00Z', + 'git_url': 'git://github.com/' + fullRepo + '.git', + 'ssh_url': 'git@github.com:' + fullRepo + '.git', + 'clone_url': 'https://github.com/' + fullRepo + '.git', + 'svn_url': 'https://github.com/' + fullRepo, + 'homepage': null, + 'size': 1631, + 'stargazers_count': 0, + 'watchers_count': 0, + 'language': 'JavaScript', + 'has_issues': true, + 'has_downloads': true, + 'has_wiki': true, + 'has_pages': false, + 'forks_count': 1, + 'mirror_url': null, + 'open_issues_count': 2, + 'forks': 1, + 'open_issues': 2, + 'watchers': 0, + 'default_branch': 'master' + } + }, + 'base': { + 'label': '' + owner + ':master', + 'ref': 'master', + 'sha': '3c8e16a66aca7a98dd93bde0840f2aa6f0da427b', + 'user': { + 'login': owner, + 'id': ownerId, + 'avatar_url': 'https://avatars.githubusercontent.com/u/429706?v=3', + 'gravatar_id': '', + 'url': 'https://api.github.com/users/' + owner, + 'html_url': 'https://github.com/' + owner, + 'followers_url': 'https://api.github.com/users/' + owner + '/followers', + 'following_url': 'https://api.github.com/users/' + owner + '/following{/other_user}', + 'gists_url': 'https://api.github.com/users/' + owner + '/gists{/gist_id}', + 'starred_url': 'https://api.github.com/users/' + owner + '/starred{/owner}{/repo}', + 'subscriptions_url': 'https://api.github.com/users/' + owner + '/subscriptions', + 'organizations_url': 'https://api.github.com/users/' + owner + '/orgs', + 'repos_url': 'https://api.github.com/users/' + owner + '/repos', + 'events_url': 'https://api.github.com/users/' + owner + '/events{/privacy}', + 'received_events_url': 'https://api.github.com/users/' + owner + '/received_events', + 'type': 'User', + 'site_admin': false + }, + 'repo': { + 'id': 28254077, + 'name': 'hellonode', + 'full_name': fullRepo, + 'owner': { + 'login': owner, + 'id': 429706, + 'avatar_url': 'https://avatars.githubusercontent.com/u/429706?v=3', + 'gravatar_id': '', + 'url': 'https://api.github.com/users/' + owner, + 'html_url': 'https://github.com/' + owner, + 'followers_url': 'https://api.github.com/users/' + owner + '/followers', + 'following_url': 'https://api.github.com/users/' + owner + '/following{/other_user}', + 'gists_url': 'https://api.github.com/users/' + owner + '/gists{/gist_id}', + 'starred_url': 'https://api.github.com/users/' + owner + '/starred{/owner}{/repo}', + 'subscriptions_url': 'https://api.github.com/users/' + owner + '/subscriptions', + 'organizations_url': 'https://api.github.com/users/' + owner + '/orgs', + 'repos_url': 'https://api.github.com/users/' + owner + '/repos', + 'events_url': 'https://api.github.com/users/' + owner + '/events{/privacy}', + 'received_events_url': 'https://api.github.com/users/' + owner + '/received_events', + 'type': 'User', + 'site_admin': false + }, + 'private': false, + 'html_url': 'https://github.com/' + fullRepo, + 'description': '', + 'fork': false, + 'url': 'https://api.github.com/repos/' + fullRepo, + 'forks_url': 'https://api.github.com/repos/' + fullRepo + '/forks', + 'keys_url': 'https://api.github.com/repos/' + fullRepo + '/keys{/key_id}', + 'collaborators_url': 'https://api.github.com/repos/' + fullRepo + '/collaborators{/collaborator}', + 'teams_url': 'https://api.github.com/repos/' + fullRepo + '/teams', + 'hooks_url': 'https://api.github.com/repos/' + fullRepo + '/hooks', + 'issue_events_url': 'https://api.github.com/repos/' + fullRepo + '/issues/events{/number}', + 'events_url': 'https://api.github.com/repos/' + fullRepo + '/events', + 'assignees_url': 'https://api.github.com/repos/' + fullRepo + '/assignees{/user}', + 'branches_url': 'https://api.github.com/repos/' + fullRepo + '/branches{/branch}', + 'tags_url': 'https://api.github.com/repos/' + fullRepo + '/tags', + 'blobs_url': 'https://api.github.com/repos/' + fullRepo + '/git/blobs{/sha}', + 'git_tags_url': 'https://api.github.com/repos/' + fullRepo + '/git/tags{/sha}', + 'git_refs_url': 'https://api.github.com/repos/' + fullRepo + '/git/refs{/sha}', + 'trees_url': 'https://api.github.com/repos/' + fullRepo + '/git/trees{/sha}', + 'statuses_url': 'https://api.github.com/repos/' + fullRepo + '/statuses/{sha}', + 'languages_url': 'https://api.github.com/repos/' + fullRepo + '/languages', + 'stargazers_url': 'https://api.github.com/repos/' + fullRepo + '/stargazers', + 'contributors_url': 'https://api.github.com/repos/' + fullRepo + '/contributors', + 'subscribers_url': 'https://api.github.com/repos/' + fullRepo + '/subscribers', + 'subscription_url': 'https://api.github.com/repos/' + fullRepo + '/subscription', + 'commits_url': 'https://api.github.com/repos/' + fullRepo + '/commits{/sha}', + 'git_commits_url': 'https://api.github.com/repos/' + fullRepo + '/git/commits{/sha}', + 'comments_url': 'https://api.github.com/repos/' + fullRepo + '/comments{/number}', + 'issue_comment_url': 'https://api.github.com/repos/' + fullRepo + '/issues/comments/{number}', + 'contents_url': 'https://api.github.com/repos/' + fullRepo + '/contents/{+path}', + 'compare_url': 'https://api.github.com/repos/' + fullRepo + '/compare/{base}...{head}', + 'merges_url': 'https://api.github.com/repos/' + fullRepo + '/merges', + 'archive_url': 'https://api.github.com/repos/' + fullRepo + '/{archive_format}{/ref}', + 'downloads_url': 'https://api.github.com/repos/' + fullRepo + '/downloads', + 'issues_url': 'https://api.github.com/repos/' + fullRepo + '/issues{/number}', + 'pulls_url': 'https://api.github.com/repos/' + fullRepo + '/pulls{/number}', + 'milestones_url': 'https://api.github.com/repos/' + fullRepo + '/milestones{/number}', + 'notifications_url': 'https://api.github.com/repos/' + fullRepo + + '/notifications{?since,all,participating}', + 'labels_url': 'https://api.github.com/repos/' + fullRepo + '/labels{/name}', + 'releases_url': 'https://api.github.com/repos/' + fullRepo + '/releases{/id}', + 'created_at': '2014-12-20T02:22:29Z', + 'updated_at': '2015-01-26T20:09:00Z', + 'pushed_at': '2015-01-26T20:09:00Z', + 'git_url': 'git://github.com/' + fullRepo + '.git', + 'ssh_url': 'git@github.com:' + fullRepo + '.git', + 'clone_url': 'https://github.com/' + fullRepo + '.git', + 'svn_url': 'https://github.com/' + fullRepo, + 'homepage': null, + 'size': 1631, + 'stargazers_count': 0, + 'watchers_count': 0, + 'language': 'JavaScript', + 'has_issues': true, + 'has_downloads': true, + 'has_wiki': true, + 'has_pages': false, + 'forks_count': 1, + 'mirror_url': null, + 'open_issues_count': 2, + 'forks': 1, + 'open_issues': 2, + 'watchers': 0, + 'default_branch': 'master' + } + }, + '_links': { + 'self': { + 'href': 'https://api.github.com/repos/' + fullRepo + '/pulls/2' + }, + 'html': { + 'href': 'https://github.com/' + fullRepo + '/pull/2' + }, + 'issue': { + 'href': 'https://api.github.com/repos/' + fullRepo + '/issues/2' + }, + 'comments': { + 'href': 'https://api.github.com/repos/' + fullRepo + '/issues/2/comments' + }, + 'review_comments': { + 'href': 'https://api.github.com/repos/' + fullRepo + '/pulls/2/comments' + }, + 'review_comment': { + 'href': 'https://api.github.com/repos/' + fullRepo + '/pulls/comments/{number}' + }, + 'commits': { + 'href': 'https://api.github.com/repos/' + fullRepo + '/pulls/2/commits' + }, + 'statuses': { + 'href': 'https://api.github.com/repos/' + fullRepo + '/statuses/023a0cba6759ed69cbcb91e4a7f61aa3b8ca6fd3' + } + }, + 'merged': false, + 'mergeable': null, + 'mergeable_state': 'unknown', + 'merged_by': null, + 'comments': 0, + 'review_comments': 0, + 'commits': 18, + 'additions': 1, + 'deletions': 1, + 'changed_files': 1 + }, + 'repository': { + 'id': 28254077, + 'name': 'hellonode', + 'full_name': fullRepo, + 'owner': { + 'login': owner, + 'id': ownerId, + 'avatar_url': 'https://avatars.githubusercontent.com/u/429706?v=3', + 'gravatar_id': '', + 'url': 'https://api.github.com/users/' + owner, + 'html_url': 'https://github.com/' + owner, + 'followers_url': 'https://api.github.com/users/' + owner + '/followers', + 'following_url': 'https://api.github.com/users/' + owner + '/following{/other_user}', + 'gists_url': 'https://api.github.com/users/' + owner + '/gists{/gist_id}', + 'starred_url': 'https://api.github.com/users/' + owner + '/starred{/owner}{/repo}', + 'subscriptions_url': 'https://api.github.com/users/' + owner + '/subscriptions', + 'organizations_url': 'https://api.github.com/users/' + owner + '/orgs', + 'repos_url': 'https://api.github.com/users/' + owner + '/repos', + 'events_url': 'https://api.github.com/users/' + owner + '/events{/privacy}', + 'received_events_url': 'https://api.github.com/users/' + owner + '/received_events', + 'type': 'User', + 'site_admin': false + }, + 'private': false, + 'html_url': 'https://github.com/' + fullRepo, + 'description': '', + 'fork': false, + 'url': 'https://api.github.com/repos/' + fullRepo, + 'forks_url': 'https://api.github.com/repos/' + fullRepo + '/forks', + 'keys_url': 'https://api.github.com/repos/' + fullRepo + '/keys{/key_id}', + 'collaborators_url': 'https://api.github.com/repos/' + fullRepo + '/collaborators{/collaborator}', + 'teams_url': 'https://api.github.com/repos/' + fullRepo + '/teams', + 'hooks_url': 'https://api.github.com/repos/' + fullRepo + '/hooks', + 'issue_events_url': 'https://api.github.com/repos/' + fullRepo + '/issues/events{/number}', + 'events_url': 'https://api.github.com/repos/' + fullRepo + '/events', + 'assignees_url': 'https://api.github.com/repos/' + fullRepo + '/assignees{/user}', + 'branches_url': 'https://api.github.com/repos/' + fullRepo + '/branches{/branch}', + 'tags_url': 'https://api.github.com/repos/' + fullRepo + '/tags', + 'blobs_url': 'https://api.github.com/repos/' + fullRepo + '/git/blobs{/sha}', + 'git_tags_url': 'https://api.github.com/repos/' + fullRepo + '/git/tags{/sha}', + 'git_refs_url': 'https://api.github.com/repos/' + fullRepo + '/git/refs{/sha}', + 'trees_url': 'https://api.github.com/repos/' + fullRepo + '/git/trees{/sha}', + 'statuses_url': 'https://api.github.com/repos/' + fullRepo + '/statuses/{sha}', + 'languages_url': 'https://api.github.com/repos/' + fullRepo + '/languages', + 'stargazers_url': 'https://api.github.com/repos/' + fullRepo + '/stargazers', + 'contributors_url': 'https://api.github.com/repos/' + fullRepo + '/contributors', + 'subscribers_url': 'https://api.github.com/repos/' + fullRepo + '/subscribers', + 'subscription_url': 'https://api.github.com/repos/' + fullRepo + '/subscription', + 'commits_url': 'https://api.github.com/repos/' + fullRepo + '/commits{/sha}', + 'git_commits_url': 'https://api.github.com/repos/' + fullRepo + '/git/commits{/sha}', + 'comments_url': 'https://api.github.com/repos/' + fullRepo + '/comments{/number}', + 'issue_comment_url': 'https://api.github.com/repos/' + fullRepo + '/issues/comments/{number}', + 'contents_url': 'https://api.github.com/repos/' + fullRepo + '/contents/{+path}', + 'compare_url': 'https://api.github.com/repos/' + fullRepo + '/compare/{base}...{head}', + 'merges_url': 'https://api.github.com/repos/' + fullRepo + '/merges', + 'archive_url': 'https://api.github.com/repos/' + fullRepo + '/{archive_format}{/ref}', + 'downloads_url': 'https://api.github.com/repos/' + fullRepo + '/downloads', + 'issues_url': 'https://api.github.com/repos/' + fullRepo + '/issues{/number}', + 'pulls_url': 'https://api.github.com/repos/' + fullRepo + '/pulls{/number}', + 'milestones_url': 'https://api.github.com/repos/' + fullRepo + '/milestones{/number}', + 'notifications_url': 'https://api.github.com/repos/' + fullRepo + '/notifications{?since,all,participating}', + 'labels_url': 'https://api.github.com/repos/' + fullRepo + '/labels{/name}', + 'releases_url': 'https://api.github.com/repos/' + fullRepo + '/releases{/id}', + 'created_at': '2014-12-20T02:22:29Z', + 'updated_at': '2015-01-26T20:09:00Z', + 'pushed_at': '2015-01-26T20:09:00Z', + 'git_url': 'git://github.com/' + fullRepo + '.git', + 'ssh_url': 'git@github.com:' + fullRepo + '.git', + 'clone_url': 'https://github.com/' + fullRepo + '.git', + 'svn_url': 'https://github.com/' + fullRepo, + 'homepage': null, + 'size': 1631, + 'stargazers_count': 0, + 'watchers_count': 0, + 'language': 'JavaScript', + 'has_issues': true, + 'has_downloads': true, + 'has_wiki': true, + 'has_pages': false, + 'forks_count': 1, + 'mirror_url': null, + 'open_issues_count': 2, + 'forks': 1, + 'open_issues': 2, + 'watchers': 0, + 'default_branch': 'master' + }, + 'sender': { + 'login': owner, + 'id': ownerId, + 'avatar_url': 'https://avatars.githubusercontent.com/u/429706?v=3', + 'gravatar_id': '', + 'url': 'https://api.github.com/users/' + owner, + 'html_url': 'https://github.com/' + owner, + 'followers_url': 'https://api.github.com/users/' + owner + '/followers', + 'following_url': 'https://api.github.com/users/' + owner + '/following{/other_user}', + 'gists_url': 'https://api.github.com/users/' + owner + '/gists{/gist_id}', + 'starred_url': 'https://api.github.com/users/' + owner + '/starred{/owner}{/repo}', + 'subscriptions_url': 'https://api.github.com/users/' + owner + '/subscriptions', + 'organizations_url': 'https://api.github.com/users/' + owner + '/orgs', + 'repos_url': 'https://api.github.com/users/' + owner + '/repos', + 'events_url': 'https://api.github.com/users/' + owner + '/events{/privacy}', + 'received_events_url': 'https://api.github.com/users/' + owner + '/received_events', + 'type': 'User', + 'site_admin': false + } + } + }, push: { url: url.format({ protocol: 'http:', diff --git a/unit/github-notifier.js b/unit/github-notifier.js deleted file mode 100644 index e2b6b8075..000000000 --- a/unit/github-notifier.js +++ /dev/null @@ -1,263 +0,0 @@ -'use strict'; -var Lab = require('lab'); -var describe = Lab.experiment; -var it = Lab.test; -var before = Lab.before; -var after = Lab.after; -var expect = Lab.expect; -var GitHub = require('models/notifications/github'); - -var repoMock = require('../test/fixtures/mocks/github/repo'); -var isCollaboratorMock = - require('../test/fixtures/mocks/github/repos-username-repo-collaborators-collaborator'); - -var userMembershipMock = - require('../test/fixtures/mocks/github/user-memberships-org'); - - -describe('GitHub Notifier', function () { - - describe('_renderMessage', function () { - - it('should render proper text for PR comment if no runnable boxes found', function (done) { - var github = new GitHub(); - - var githubPushInfo = { - repo: 'CodeNow/api', - repoName: 'api', - number: 2, - branch: 'fix/1', - commit: 'a240edf982d467201845b3bf10ccbe16f6049ea9', - user: { - login: 'podviaznikov' - }, - owner: { - login: 'podviaznikov' - }, - headCommit: { - id: 'a240edf982d467201845b3bf10ccbe16f6049ea9', - message: 'hey there', - url: 'https://github.com/Runnable/api/commit/a240edf982d467201845b3bf10ccbe16f6049ea9' - } - }; - - var message = github._renderMessage(githubPushInfo, []); - var msg = '[Choose a server]'; - msg += '(http://runnable3.net/podviaznikov/serverSelection/api?branch='; - msg += 'fix%252F1&message=hey%2520there&commit=a240edf982d467201845b3bf10ccbe16f6049ea9&source=pr)'; - msg += ' to run PR-2.'; - expect(message).to.equal(msg); - done(); - }); - - - it('should render proper text for PR comment if 2 runnable boxes found', function (done) { - var github = new GitHub(); - - var githubPushInfo = { - repo: 'CodeNow/api', - repoName: 'api', - branch: 'fix/1', - commit: 'a240edf982d467201845b3bf10ccbe16f6049ea9', - user: { - login: 'podviaznikov' - }, - owner: { - login: 'podviaznikov' - }, - number: 5 - }; - var instances = [ - { - name: 'box-1', - owner: { - login: 'podviaznikov' - } - }, - { - name: 'box-2', - owner: { - login: 'podviaznikov' - } - } - ]; - var message = github._renderMessage(githubPushInfo, instances); - var msg = '[box-1](http://runnable3.net/podviaznikov/box-1?source=pr) and '; - msg += '[box-2](http://runnable3.net/podviaznikov/box-2?source=pr)'; - msg += ' are updated with the latest changes to PR-5.'; - expect(message).to.equal(msg); - done(); - }); - - }); - - describe('disabled PR comments', function () { - var ctx = {}; - - before(function (done) { - ctx.originalENABLE_GITHUB_PR_COMMENTS = process.env.ENABLE_GITHUB_PR_COMMENTS; - process.env.ENABLE_GITHUB_PR_COMMENTS = false; - done(); - }); - - after(function (done) { - process.env.ENABLE_GITHUB_PR_COMMENTS = ctx.originalENABLE_GITHUB_PR_COMMENTS; - done(); - }); - - it('should not add new comment', function (done) { - var github = new GitHub(); - github.notifyOnPullRequest({}, [], function (err, resp) { - if (err) { return done(err); } - expect(resp).to.be.undefined(); - done(); - }); - }); - - it('should not update comment', function (done) { - var github = new GitHub(); - github.updatePullRequestsComments({}, {}, function (err, resp) { - if (err) { return done(err); } - expect(resp).to.be.undefined(); - done(); - }); - }); - - it('should not delete comment', function (done) { - var github = new GitHub(); - github.deletePullRequestComment({}, function (err, resp) { - if (err) { return done(err); } - expect(resp).to.be.undefined(); - done(); - }); - }); - - }); - - - describe('_updatePullRequestsComments', function () { - - it('should do nothing if instance is null', function (done) { - var github = new GitHub(); - github._updatePullRequestsComments(null, function (err) { - expect(err).to.be.null(); - done(); - }); - }); - - it('should do nothing if instance.contextVersion is null', function (done) { - var github = new GitHub(); - github._updatePullRequestsComments({name: 'a1'}, function (err) { - expect(err).to.be.null(); - done(); - }); - }); - - it('should do nothing if appCodeVersions is null', function (done) { - var github = new GitHub(); - var instance = { - contextVersion: { - appCodeVersions: null - } - }; - github._updatePullRequestsComments(instance, function (err) { - expect(err).to.be.null(); - done(); - }); - }); - - it('should do nothing if appCodeVersions is []', function (done) { - var github = new GitHub(); - var instance = { - contextVersion: { - appCodeVersions: [] - } - }; - github._updatePullRequestsComments(instance, function (err) { - expect(err).to.be.null(); - done(); - }); - }); - - - }); - - describe('_ensurePermissions', function () { - - it('should be success for user\s public repo', function (done) { - var github = new GitHub(); - repoMock.standardRepo({}); - github._ensurePermissions('cflynn07/clubbingowl_brochure', null, function (err, resp) { - if (err) { return done(err); } - expect(resp).to.be.undefined(); - done(); - }); - }); - - it('should be success for org public repo', function (done) { - var github = new GitHub(); - repoMock.standardRepo({}); - github._ensurePermissions('cflynn07/clubbingowl_brochure', 'cflynn07', function (err, resp) { - if (err) { return done(err); } - expect(resp).to.be.undefined(); - done(); - }); - }); - - it('should fail for for user\s private repo without configured collaborator', function (done) { - var github = new GitHub(); - repoMock.privateRepo({}); - isCollaboratorMock.notCollaborator('cflynn07', 'private_clubbingowl_brochure', 'runnabot'); - github._ensurePermissions('cflynn07/private_clubbingowl_brochure', null, function (err) { - expect(err.output.statusCode).to.equal(403); - expect(err.output.payload.message) - .to.equal('Runnabot is not collaborator on a private repo: cflynn07/private_clubbingowl_brochure'); - done(); - }); - }); - - it('should success for for user\s private repo with configured collaborator', function (done) { - var github = new GitHub(); - repoMock.privateRepo({}); - isCollaboratorMock.isCollaborator('cflynn07', 'private_clubbingowl_brochure', 'runnabot'); - github._ensurePermissions('cflynn07/private_clubbingowl_brochure', null, function (err, resp) { - if (err) { return done(err); } - expect(resp).to.be.undefined(); - done(); - }); - }); - - it('should try to accept membership for org private repo', function (done) { - var github = new GitHub(); - repoMock.privateRepo({}); - userMembershipMock.pendingMember(11, 'runnabot', 'cflynn07'); - github._ensurePermissions('cflynn07/private_clubbingowl_brochure', 'cflynn07', function (err) { - expect(err.message).to.match(/No match for request patch/); - done(); - }); - }); - - it('should try to accept membership for org private repo', function (done) { - var github = new GitHub(); - repoMock.privateRepo({}); - userMembershipMock.notMember(11, 'runnabot', 'cflynn07'); - github._ensurePermissions('cflynn07/private_clubbingowl_brochure', 'cflynn07', function (err) { - expect(err.message).to.match(/No match for request patch/); - done(); - }); - }); - - it('should work for org repo where runnabot is member', function (done) { - var github = new GitHub(); - repoMock.privateRepo({}); - userMembershipMock.isMember(11, 'runnabot', 'cflynn07'); - github._ensurePermissions('cflynn07/private_clubbingowl_brochure', 'cflynn07', function (err, resp) { - if (err) { return done(err); } - expect(resp).to.be.undefined(); - done(); - }); - }); - - }); - -}); \ No newline at end of file diff --git a/unit/github.js b/unit/github.js deleted file mode 100644 index 96f856d88..000000000 --- a/unit/github.js +++ /dev/null @@ -1,134 +0,0 @@ -'use strict'; - -var Lab = require('lab'); -var describe = Lab.experiment; -var it = Lab.test; -var expect = Lab.expect; - -var GitHub = require('models/apis/github'); -var repoMock = require('../test/fixtures/mocks/github/repo'); -var isCollaboratorMock = - require('../test/fixtures/mocks/github/repos-username-repo-collaborators-collaborator'); - -var userMembershipMock = - require('../test/fixtures/mocks/github/user-memberships-org'); - -var prsMock = - require('../test/fixtures/mocks/github/repos-username-repo-pulls'); - -describe('GitHub API', function () { - - - describe('listOpenPullRequestsForBranch', function () { - - it('should get one pr branch', function (done) { - var github = new GitHub({token: process.env.RUNNABOT_GITHUB_ACCESS_TOKEN}); - prsMock.openPulls('podviaznikov', 'hellonode', 'test'); - github.listOpenPullRequestsForBranch('podviaznikov/hellonode', 'test', function (err, prs) { - if (err) { return done(err); } - expect(prs.length).to.equal(1); - expect(prs[0].head.ref).to.equal('test'); - done(); - }); - }); - - it('should get 0 prs for master branch', function (done) { - var github = new GitHub({token: process.env.RUNNABOT_GITHUB_ACCESS_TOKEN}); - prsMock.openPulls('podviaznikov', 'hellonode', 'test'); - github.listOpenPullRequestsForBranch('podviaznikov/hellonode', 'master', function (err, prs) { - if (err) { return done(err); } - expect(prs.length).to.equal(0); - done(); - }); - }); - }); - - - - describe('isPublicRepo', function () { - - it('should return true for the public repo', function (done) { - repoMock.standardRepo({}); - var github = new GitHub({token: process.env.RUNNABOT_GITHUB_ACCESS_TOKEN}); - github.isPublicRepo('cflynn07/clubbingowl_brochure', function (err, isPublic) { - if (err) { return done(err); } - expect(isPublic).to.be.true(); - done(); - }); - }); - - it('should return false for the private repo', function (done) { - repoMock.privateRepo({}); - var github = new GitHub({token: process.env.RUNNABOT_GITHUB_ACCESS_TOKEN}); - github.isPublicRepo('cflynn07/private_clubbingowl_brochure', function (err, isPublic) { - if (err) { return done(err); } - expect(isPublic).to.be.false(); - done(); - }); - }); - - }); - - describe('isCollaborator', function () { - - it('should return true if user is collaborator', function (done) { - isCollaboratorMock.isCollaborator('podviaznikov', 'hellonode', 'runnabot'); - var github = new GitHub({token: process.env.RUNNABOT_GITHUB_ACCESS_TOKEN}); - github.isCollaborator('podviaznikov/hellonode', 'runnabot', function (err, isCollaborator) { - if (err) { return done(err); } - expect(isCollaborator).to.be.true(); - done(); - }); - }); - - it('should return false if user is not collaborator', function (done) { - isCollaboratorMock.notCollaborator('podviaznikov', 'hellonode', 'tj'); - var github = new GitHub({token: process.env.RUNNABOT_GITHUB_ACCESS_TOKEN}); - github.isCollaborator('podviaznikov/hellonode', 'tj', function (err, isCollaborator) { - if (err) { return done(err); } - expect(isCollaborator).to.be.false(); - done(); - }); - }); - - }); - - - describe('isOrgMember', function () { - - it('should return true if user is an org members', function (done) { - userMembershipMock.isMember(1, 'runnabot', 'CodeNow'); - var github = new GitHub({token: process.env.RUNNABOT_GITHUB_ACCESS_TOKEN}); - github.isOrgMember('CodeNow', function (err, isMember) { - if (err) { return done(err); } - expect(isMember).to.be.true(); - done(); - }); - }); - - it('should return false if user is a pending org member', function (done) { - userMembershipMock.pendingMember(2, 'runnabot', 'Runnable'); - var github = new GitHub({token: process.env.RUNNABOT_GITHUB_ACCESS_TOKEN}); - github.isOrgMember('Runnable', function (err, isMember) { - if (err) { return done(err); } - expect(isMember).to.be.false(); - done(); - }); - }); - - it('should return false if user is not an org member', function (done) { - userMembershipMock.notMember(3, 'runnabot', 'hashobject'); - var github = new GitHub({token: process.env.RUNNABOT_GITHUB_ACCESS_TOKEN}); - github.isOrgMember('hashobject', function (err, isMember) { - if (err) { return done(err); } - expect(isMember).to.be.false(); - done(); - }); - }); - - }); - - - - -}); \ No newline at end of file diff --git a/unit/notifier.js b/unit/notifier.js index ff205f029..8be2e6d54 100644 --- a/unit/notifier.js +++ b/unit/notifier.js @@ -6,8 +6,6 @@ var expect = Lab.expect; var Notifier = require('models/notifications/notifier'); var Slack = require('models/notifications/slack'); var HipChat = require('models/notifications/hipchat'); -var HipChatClient = require('hipchat-client'); -var uuid = require('uuid'); describe('Notifier', function () { @@ -246,57 +244,6 @@ describe('Notifier', function () { hipchat.notifyOnInstances(githubPushInfo, instances, done); }); - it('should send message to HipChat', {timeout: 4000}, function (done) { - var hipchat = new HipChat({authToken: 'a4bcd2c7007379398f5158d7785fa0', roomId: '1076330'}); - var randomUsername = 'user' + uuid(); - var instances = [ - { - name: 'instance1', - owner: { - username: 'podviaznikov' - } - } - ]; - var headCommit = { - id: 'a240edf982d467201845b3bf10ccbe16f6049ea9', - author: randomUsername, - url: 'https://github.com/CodeNow/api/commit/a240edf982d467201845b3bf10ccbe16f6049ea9', - message: 'some commit' - }; - var githubPushInfo = { - commitLog: [headCommit], - repo: 'CodeNow/api', - repoName: 'api', - branch: 'develop', - commit: 'a240edf982d467201845b3bf10ccbe16f6049ea9', - headCommit: headCommit, - user: { - login: randomUsername - } - }; - hipchat.notifyOnInstances(githubPushInfo, instances, function (err) { - if (err) { return done(err); } - var hc = new HipChatClient('388add7b19c83cc9f970d6b97a5642'); - setTimeout(function () { - hc.api.rooms.history({ - room_id: '1076330', - date: 'recent' - }, function (err, resp) { - if (err) { return done(err); } - var messages = resp.messages; - expect(messages.length).to.be.above(1); - var properMessages = messages.filter(function (message) { - return message.message.indexOf(randomUsername) > -1; - }); - expect(properMessages.length).to.be.equal(1); - properMessages.forEach(function (message) { - expect(message.from.name).to.equal(process.env.HIPCHAT_BOT_USERNAME); - }); - done(); - }); - }, 2500); - }); - }); });