├── .gitignore ├── .travis.yml ├── CHANGES.md ├── Cakefile ├── LICENSE.md ├── README.md ├── docs └── .gitkeep ├── index.coffee ├── package.json ├── src ├── hook.coffee ├── patterns.coffee ├── script.coffee └── version.coffee └── test ├── hook_test.coffee ├── index_test.coffee ├── mocha.opts ├── pattern_test.coffee ├── test_apps.json └── test_helper.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | script: 5 | - npm test 6 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | 0.1.0 2 | ===== 3 | 4 | -------------------------------------------------------------------------------- /Cakefile: -------------------------------------------------------------------------------- 1 | # Cakefile 2 | 3 | {exec} = require "child_process" 4 | 5 | REPORTER = "min" 6 | 7 | task "test", "run tests", -> 8 | exec "NODE_ENV=test ./node_modules/.bin/mocha", (err, output) -> 9 | throw err if err 10 | console.log output 11 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Corey Donohoe 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hubot-auto-deploy [![Build Status](https://travis-ci.org/atmos/hubot-auto-deploy.png?branch=master)](https://travis-ci.org/atmos/hubot-auto-deploy) 2 | 3 | [GitHub Flow][1] via [hubot][3]. Chatting with hubot configures auto-deployment on GitHub and dispatches [Deployment Events][4] when criteria is met. 4 | 5 | This script interacts with the GitHub API to manage the [Automated Deployment service][6] built in to GitHub services. 6 | 7 | ![chat config](https://cloud.githubusercontent.com/assets/38/3846663/b8ef9cbc-1e5e-11e4-88b8-16bcff97c9db.jpg) 8 | 9 | 10 | ## Installation 11 | 12 | * Add hubot-auto-deploy to your `package.json` file. 13 | * Add hubot-auto-deploy to your `external-scripts.json` file. 14 | 15 | ## Runtime Environment 16 | 17 | You need to set the following environmental variables. 18 | 19 | | Environmental Variables | | 20 | |-------------------------|-------------------------------------------------| 21 | | HUBOT_GITHUB_TOKEN |A [GitHub token](https://github.com/settings/applications#personal-access-tokens) with [repo:deployment](https://developer.github.com/v3/oauth/#scopes). The owner of this token creates [Deployments][5]. 22 | 23 | ## TODO 24 | 25 | * Handle automated deployment of non-default branches. 26 | 27 | ## See Also 28 | 29 | * [hubot](https://github.com/github/hubot) - A chat robot with support for a lot of networks. 30 | * [heaven](https://github.com/atmos/heaven) - Listens for Deployment events from GitHub and executes the deployment for you. 31 | * [hubot-deploy](https://github.com/atmos/hubot-deploy) - Request deployments on GitHub from your chat client. 32 | 33 | [1]: https://guides.github.com/overviews/flow/ 34 | [2]: https://developer.github.com/v3/repos/hooks/ 35 | [3]: https://hubot.github.com 36 | [4]: https://developer.github.com/v3/activity/events/types/#deploymentevent 37 | [5]: https://developer.github.com/v3/repos/deployments/ 38 | [6]: http://www.atmos.org/github-services/auto-deployment/ 39 | -------------------------------------------------------------------------------- /docs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atmos/hubot-auto-deploy/cb2dbadc14b971ac2f03620fde2cafca94ea1116/docs/.gitkeep -------------------------------------------------------------------------------- /index.coffee: -------------------------------------------------------------------------------- 1 | Path = require 'path' 2 | 3 | module.exports = (robot, scripts) -> 4 | robot.loadFile(Path.resolve(__dirname, "src"), "script.coffee") 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hubot-auto-deploy", 3 | "version": "0.1.9", 4 | "author": "Corey Donohoe ", 5 | "description": "hubot script for enabling auto-deployment for GitHub Flow", 6 | "main": "index.coffee", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/atmos/hubot-auto-deploy" 10 | }, 11 | "keywords": [ 12 | "flow", 13 | "github", 14 | "deploy", 15 | "auto-deployment", 16 | "continuous deployment", 17 | "cd", 18 | "hubot", 19 | "chatops", 20 | "deployment" 21 | ], 22 | "homepage": "https://github.com/atmos/hubot-auto-deploy", 23 | "scripts": { 24 | "test": "cake test" 25 | }, 26 | "contributors": [ 27 | { 28 | "name": "Corey Donohoe", 29 | "email": "atmos@atmos.org" 30 | } 31 | ], 32 | "dependencies": { 33 | "hubot": ">=2.7.2", 34 | "timeago": "0.1.0", 35 | "sprintf": "0.1.3", 36 | "octonode": "0.5.0", 37 | "inflection": "1.3.6" 38 | }, 39 | "devDependencies": { 40 | "chai": "~1.0.3", 41 | "mocha": "~1.1.0", 42 | "coffee-script": ">=1.6.3", 43 | "hubot-test-helper": "0.0.2" 44 | }, 45 | "engines": { 46 | "node": ">=0.8.0" 47 | }, 48 | "bugs": { 49 | "url": "https://github.com/atmos/hubot-auto-deploy/issues" 50 | }, 51 | "licenses": [ 52 | { 53 | "type": "MIT", 54 | "url": "http://github.com/atmos/hubot-auto-deploy/raw/master/LICENSE.md" 55 | } 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /src/hook.coffee: -------------------------------------------------------------------------------- 1 | Fs = require "fs" 2 | Path = require "path" 3 | Version = require(Path.join(__dirname, "version")).Version 4 | ########################################################################### 5 | 6 | api = require("octonode").client(process.env.HUBOT_GITHUB_TOKEN or 'unknown') 7 | api.requestDefaults.headers['Accept'] = 'application/vnd.github.cannonball-preview+json' 8 | ########################################################################### 9 | 10 | Array::unique = -> 11 | output = {} 12 | output[@[key]] = @[key] for key in [0...@length] 13 | value for key, value of output 14 | 15 | class Hook 16 | @APPS_FILE = process.env['HUBOT_DEPLOY_APPS_JSON'] or "apps.json" 17 | 18 | constructor: (@name, @token) -> 19 | @environments = [ "production" ] 20 | @requiredContexts = [ ] 21 | 22 | @active = true 23 | @config = null 24 | @deployOnStatus = false 25 | 26 | @token or= process.env.HUBOT_GITHUB_TOKEN 27 | 28 | try 29 | applications = JSON.parse(Fs.readFileSync(@constructor.APPS_FILE).toString()) 30 | catch 31 | throw new Error("Unable to parse your apps.json file in hubot-auto-deploy") 32 | 33 | @application = applications[@name] 34 | 35 | if @application? 36 | @repository = @application['repository'] 37 | 38 | @configureRequiredContexts() 39 | 40 | isValidApp: -> 41 | @application? 42 | 43 | statusContexts: -> 44 | if @requiredContexts 45 | @requiredContexts.unique().join(',') 46 | else 47 | "" 48 | 49 | toggle: (cb) -> 50 | @active = not @active 51 | @save(cb) 52 | 53 | removeEnvironment: (environment) -> 54 | index = @environments.indexOf(environment) 55 | if index > -1 56 | @environments.splice(index, 1) 57 | 58 | addEnvironment: (environment) -> 59 | @environments.push(environment) 60 | 61 | enableStatusDeployment: (cb) -> 62 | @deployOnStatus = true 63 | @save(cb) 64 | 65 | enablePushDeployment: (cb) -> 66 | @deployOnStatus = false 67 | @save(cb) 68 | 69 | statusLine: -> 70 | str = "#{@name} is " 71 | if @active 72 | str += "auto-deploying on" 73 | if @deployOnStatus 74 | str += " green commit statuses to the master branch." 75 | else 76 | str += " pushes to the master branch." 77 | str += " Environments: #{@environments.unique().join(',')}." 78 | else 79 | str += "not auto-deploying." 80 | str 81 | 82 | save: (cb) -> 83 | @post (err, data) -> 84 | cb(err, data) 85 | 86 | requestBody: -> 87 | name: "autodeploy" 88 | events: ["push", "status"] 89 | active: @active 90 | config: 91 | github_token: @token 92 | environments: @environments.unique().join(',') 93 | status_contexts: @statusContexts() 94 | deploy_on_status: @deployOnStatus 95 | 96 | get: (cb) -> 97 | path = "repos/#{@repository}/hooks" 98 | api.get path, {}, (err, status, body, headers) => 99 | unless err 100 | hooks = (hook for hook in body when hook.name is "autodeploy") 101 | if hooks.length > 0 102 | @config = hooks[0] 103 | @environments = @config.config.environments.split(',') 104 | @deployOnStatus = @config.config.deploy_on_status == '1' 105 | @active = @config.active == true 106 | 107 | cb(err) 108 | 109 | # Private Methods 110 | post: (cb) -> 111 | path = "repos/#{@repository}/hooks" 112 | postBody = @requestBody() 113 | 114 | if @config 115 | path += "/#{@config.id}" 116 | 117 | api.post path, postBody, (err, status, body, headers) -> 118 | unless err 119 | @environments = body.config.environments.split(',') 120 | @deployOnStatus = body.config.deploy_on_status == '1' 121 | @active = body.active == true 122 | 123 | cb(err, body) 124 | 125 | configureRequiredContexts: -> 126 | if @application['required_contexts']? 127 | @requiredContexts = @application['required_contexts'] 128 | 129 | exports.Hook = Hook 130 | -------------------------------------------------------------------------------- /src/patterns.coffee: -------------------------------------------------------------------------------- 1 | repository = "([-_\.0-9a-z]+)" 2 | 3 | scriptPrefix = process.env['HUBOT_AUTO_DEPLOY_PREFIX'] || "auto-deploy" 4 | 5 | # The :hammer: regex that handles all /auto-deploy requests 6 | AUTO_DEPLOY_SYNTAX = /// 7 | (#{scriptPrefix}(?:\:[^\s]+)?) # / prefix 8 | \s+#{repository} # application name, from apps.json 9 | (?:\s+(?:to|in|on)\s+ # http://i.imgur.com/3KqMoRi.gif 10 | #{repository} # Environment to release to 11 | (?:\/([^\s]+))?)? # Host filter to try 12 | ///i 13 | 14 | 15 | exports.AutoDeployPrefix = scriptPrefix 16 | exports.AutoDeployPattern = AUTO_DEPLOY_SYNTAX 17 | -------------------------------------------------------------------------------- /src/script.coffee: -------------------------------------------------------------------------------- 1 | # Description 2 | # Configure auto-deployment of GitHub repos from chat - https://github.com/atmos/hubot-auto-deploy 3 | # 4 | # Commands: 5 | # hubot auto-deploy:status - Check the status of auto-deployment for the app in environment 6 | # hubot auto-deploy:toggle - Toggle on/off auto-deployment for the app in all environments. 7 | # hubot auto-deploy:enable in - enable auto-deployment for the app on push in environment 8 | # hubot auto-deploy:disable in - disable auto-deployment for the app in environment 9 | # hubot auto-deploy:enable:push - enable auto-deployment for the app on push 10 | # hubot auto-deploy:enable:status - enable auto-deployment for the app on commit status 11 | # 12 | supported_tasks = [ "auto-deploy:enable", "auto-deploy:disable" ] 13 | 14 | Path = require("path") 15 | Hook = require(Path.join(__dirname, "hook")).Hook 16 | Patterns = require(Path.join(__dirname, "patterns")) 17 | 18 | module.exports = (robot) -> 19 | failureMessage = (hook, err) -> 20 | "Unable to access #{hook.repository} hooks. #{err.message}(#{err.statusCode})" 21 | 22 | robot.respond Patterns.AutoDeployPattern, (msg) -> 23 | task = msg.match[1] 24 | name = msg.match[2] 25 | environment = msg.match[3] or 'production' 26 | 27 | hook = new Hook(name) 28 | unless hook.isValidApp() 29 | msg.reply "Never heard of the #{name} app, sorry" 30 | return 31 | 32 | switch task 33 | when "auto-deploy:status" 34 | hook.get (err) -> 35 | if err 36 | msg.reply failureMessage(hook, err) 37 | else 38 | msg.reply hook.statusLine() 39 | when "auto-deploy:toggle" 40 | hook.get (err) -> 41 | if err 42 | msg.reply failureMessage(hook, err) 43 | else 44 | hook.toggle (err, data) -> 45 | msg.reply hook.statusLine() 46 | when "auto-deploy:enable" 47 | hook.get (err) -> 48 | if err 49 | msg.reply failureMessage(hook, err) 50 | else 51 | hook.addEnvironment(environment) 52 | hook.save (err, data) -> 53 | msg.reply hook.statusLine() 54 | when "auto-deploy:disable" 55 | hook.get (err) -> 56 | if err 57 | msg.reply failureMessage(hook, err) 58 | else 59 | hook.removeEnvironment(environment) 60 | hook.save (err, data) -> 61 | msg.reply hook.statusLine() 62 | when "auto-deploy:enable:status" 63 | hook.get (err) -> 64 | if err 65 | msg.reply failureMessage(hook, err) 66 | else 67 | hook.enableStatusDeployment (err, data) -> 68 | msg.reply hook.statusLine() 69 | when "auto-deploy:enable:push" 70 | hook.get (err) -> 71 | if err 72 | msg.reply failureMessage(hook, err) 73 | else 74 | hook.enablePushDeployment (err, data) -> 75 | msg.reply hook.statusLine() 76 | else 77 | msg.reply "#{task} is unavailable. Sorry." 78 | -------------------------------------------------------------------------------- /src/version.coffee: -------------------------------------------------------------------------------- 1 | Path = require "path" 2 | 3 | pkg = require Path.join __dirname, "..", 'package.json' 4 | 5 | exports.Version = pkg.version 6 | -------------------------------------------------------------------------------- /test/hook_test.coffee: -------------------------------------------------------------------------------- 1 | Path = require('path') 2 | 3 | Hook = require(Path.join(__dirname, "..", "src", "hook")).Hook 4 | 5 | describe "Hook fixtures", () -> 6 | describe "#isValidApp()", () -> 7 | it "is invalid if the app can't be found", () -> 8 | hook = new Hook("hubot-reloaded") 9 | assert.equal(hook.isValidApp(), false) 10 | 11 | it "is valid if the app can be found", () -> 12 | hook = new Hook("hubot-deploy") 13 | assert.equal(hook.isValidApp(), true) 14 | 15 | describe "#requiredContexts", () -> 16 | it "works with required contexts", () -> 17 | hook = new Hook("hubot") 18 | expectedContexts = ["ci/janky", "ci/travis-ci"] 19 | 20 | assert.deepEqual(expectedContexts, hook.requiredContexts) 21 | 22 | describe "#requestBody()", () -> 23 | it "verifies deploy on attributes", () -> 24 | hook = new Hook("hubot") 25 | body = hook.requestBody() 26 | assert.equal("1", body.active) 27 | assert.equal("autodeploy", body.name) 28 | assert.equal("0", body.config.deploy_on_status) 29 | assert.equal("production", body.config.environments) 30 | assert.equal("ci/janky,ci/travis-ci", body.config.status_contexts) 31 | 32 | it "handles unique environments etc", () -> 33 | hook = new Hook("hubot") 34 | hook.requiredContexts = null 35 | hook.active = false 36 | hook.deployOnStatus = true 37 | hook.environments.push("staging") 38 | hook.environments.push("production") 39 | 40 | body = hook.requestBody() 41 | assert.equal("0", body.active) 42 | assert.equal("autodeploy", body.name) 43 | assert.equal("1", body.config.deploy_on_status) 44 | assert.equal("production,staging", body.config.environments) 45 | assert.equal("", body.config.status_contexts) 46 | 47 | -------------------------------------------------------------------------------- /test/index_test.coffee: -------------------------------------------------------------------------------- 1 | Path = require("path") 2 | Helper = require('hubot-test-helper') 3 | 4 | pkg = require Path.join __dirname, "..", 'package.json' 5 | pkgVersion = pkg.version 6 | 7 | room = null 8 | helper = new Helper(Path.join(__dirname, "..", "src", "script.coffee")) 9 | 10 | describe "The Hubot Script", () -> 11 | beforeEach () -> 12 | room = helper.createRoom() 13 | 14 | it "displays the version", () -> 15 | room.user.say "atmos", "hubot auto-deploy:help hubot" 16 | expected = "@atmos auto-deploy:help is unavailable. Sorry." 17 | assert.deepEqual ["atmos", "hubot auto-deploy:help hubot"], room.messages[0] 18 | assert.deepEqual ["hubot", expected], room.messages[1] 19 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --compilers coffee:coffee-script/register 2 | --reporter dot 3 | --require coffee-script/register 4 | --require test/test_helper.coffee 5 | --colors 6 | -------------------------------------------------------------------------------- /test/pattern_test.coffee: -------------------------------------------------------------------------------- 1 | Path = require('path') 2 | 3 | Patterns = require(Path.join(__dirname, "..", "src", "patterns")) 4 | 5 | Pattern = Patterns.AutoDeployPattern 6 | 7 | describe "Patterns", () -> 8 | describe "AutoDeployPattern", () -> 9 | it "rejects things that don't start with auto-deploy", () -> 10 | assert !"ping".match(Pattern) 11 | assert !"image me pugs".match(Pattern) 12 | 13 | it "handles simple auto-deployment disabling", () -> 14 | matches = "auto-deploy:disable hubot".match(Pattern) 15 | assert.equal "auto-deploy:disable", matches[1], "incorrect task" 16 | assert.equal "hubot", matches[2], "incorrect app name" 17 | assert.equal undefined, matches[3], "incorrect environment" 18 | assert.equal undefined, matches[4], "incorrect host specification" 19 | 20 | it "handles simple auto-deployment enabling", () -> 21 | matches = "auto-deploy:enable hubot".match(Pattern) 22 | assert.equal "auto-deploy:enable", matches[1], "incorrect task" 23 | assert.equal "hubot", matches[2], "incorrect app name" 24 | assert.equal undefined, matches[3], "incorrect environment" 25 | assert.equal undefined, matches[4], "incorrect host specification" 26 | 27 | it "handles simple auto-deployment enabling for pushes", () -> 28 | matches = "auto-deploy:enable:push hubot".match(Pattern) 29 | assert.equal "auto-deploy:enable:push", matches[1], "incorrect task" 30 | assert.equal "hubot", matches[2], "incorrect app name" 31 | assert.equal undefined, matches[3], "incorrect environment" 32 | assert.equal undefined, matches[4], "incorrect host specification" 33 | 34 | it "handles simple auto-deployment enabling for statuses", () -> 35 | matches = "auto-deploy:enable:status hubot".match(Pattern) 36 | assert.equal "auto-deploy:enable:status", matches[1], "incorrect task" 37 | assert.equal "hubot", matches[2], "incorrect app name" 38 | assert.equal undefined, matches[3], "incorrect environment" 39 | assert.equal undefined, matches[4], "incorrect host specification" 40 | 41 | it "handles simple auto-deployment enabling in specific environments", () -> 42 | matches = "auto-deploy:enable hubot in staging".match(Pattern) 43 | assert.equal "auto-deploy:enable", matches[1], "incorrect task" 44 | assert.equal "hubot", matches[2], "incorrect app name" 45 | assert.equal "staging", matches[3], "incorrect environment" 46 | assert.equal undefined, matches[4], "incorrect host specification" 47 | -------------------------------------------------------------------------------- /test/test_apps.json: -------------------------------------------------------------------------------- 1 | { 2 | "hubot": { 3 | "auto_merge": false, 4 | "repository": "MyOrg/my-org-hubot", 5 | "environments": ["production"], 6 | "required_contexts": ["ci/janky", "ci/travis-ci"], 7 | 8 | "provider": "heroku", 9 | "heroku_name": "my-orgs-hubot" 10 | }, 11 | 12 | "hubot-deploy": { 13 | "repository": "atmos/hubot-deploy", 14 | "environments": ["production"], 15 | 16 | "provider": "npm" 17 | }, 18 | 19 | "github": { 20 | "repository": "github/github", 21 | "environments": ["production","staging"], 22 | 23 | "provider": "capistrano" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/test_helper.coffee: -------------------------------------------------------------------------------- 1 | global.assert = require("chai").assert 2 | require("chai").Assertion.includeStack = true 3 | process.env.NODE_ENV = 'test' 4 | process.env.HUBOT_DEPLOY_APPS_JSON = require("path").join(__dirname, "test_apps.json") 5 | --------------------------------------------------------------------------------