├── test ├── fixtures │ ├── cafile.txt │ ├── pushes │ │ ├── multiple.json │ │ └── single.json │ ├── deployments │ │ ├── staging.json │ │ └── production.json │ ├── deployment_statuses │ │ ├── success.json │ │ ├── failure.json │ │ └── pending.json │ ├── deployments.json │ └── statuses │ │ ├── success.json │ │ └── pending.json ├── mocha.opts ├── helper.coffee ├── support │ └── cassettes │ │ ├── github_deployments.coffee │ │ ├── github_user.coffee │ │ ├── github_statuses.coffee │ │ ├── github_latest_deployments.coffee │ │ └── github_deployment_create.coffee ├── test_apps.json ├── scripts │ ├── http_test.coffee │ ├── latest_deploys_test.coffee │ ├── deployment_test.coffee │ └── tokens_test.coffee ├── github │ ├── api │ │ ├── deployment_status_test.coffee │ │ └── deployment_test.coffee │ ├── api_test.coffee │ └── webhooks │ │ ├── commit_status_test.coffee │ │ ├── push_test.coffee │ │ ├── pull_request_test.coffee │ │ ├── deployment_test.coffee │ │ └── deployment_status_test.coffee └── models │ ├── deployments │ ├── latest_test.coffee │ ├── instance_methods_test.coffee │ ├── creating_deployments_test.coffee │ └── creating_deployment_messages_test.coffee │ ├── formatter_test.coffee │ ├── verifiers_test.coffee │ ├── handler_test.coffee │ └── pattern_test.coffee ├── .gitignore ├── src ├── version.coffee ├── github │ ├── webhooks.coffee │ ├── webhooks │ │ ├── pull_request.coffee │ │ ├── commit_status.coffee │ │ ├── deployment_status.coffee │ │ ├── deployment.coffee │ │ └── push.coffee │ ├── api │ │ ├── deployment_status.coffee │ │ └── deployment.coffee │ └── api.coffee ├── hubot │ └── vault.coffee ├── models │ ├── handler.coffee │ ├── patterns.coffee │ ├── formatters.coffee │ └── verifiers.coffee └── scripts │ ├── token.coffee │ ├── deploy.coffee │ └── http.coffee ├── .travis.yml ├── index.coffee ├── Cakefile ├── CHANGES.md ├── LICENSE.md ├── package.json ├── README.md ├── docs ├── examples │ ├── slack.coffee │ └── hipchat.coffee ├── configuration.md ├── config-file.md └── chatops.md └── .coffeelint.json /test/fixtures/cafile.txt: -------------------------------------------------------------------------------- 1 | Sample CAFile 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | .node-version 4 | test/models/*.js 5 | test/scripts/*.js 6 | -------------------------------------------------------------------------------- /src/version.coffee: -------------------------------------------------------------------------------- 1 | Path = require "path" 2 | 3 | pkg = require Path.join __dirname, "..", 'package.json' 4 | 5 | exports.Version = pkg.version 6 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --compilers coffee:coffee-script/register 2 | --reporter dot 3 | --require coffee-script/register 4 | --require test/helper.coffee 5 | --colors 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | 4 | node_js: 5 | - "0.10" 6 | - "0.12" 7 | - "4" 8 | 9 | before_install: 10 | - npm install "hubot@$HUBOT_VERSION" --save 11 | 12 | cache: 13 | directories: 14 | - node_modules 15 | -------------------------------------------------------------------------------- /test/support/cassettes/github_deployments.coffee: -------------------------------------------------------------------------------- 1 | module.exports.cassettes = 2 | '/repos-atmos-my-robot-deployments-1875476-success': 3 | host: 'https://api.github.com:443' 4 | path: '/repos/atmos/my-robot/deployments' 5 | method: 'post' 6 | code: 200 7 | path: '/repos/atmos/my-robot/deployments' 8 | body: { } 9 | -------------------------------------------------------------------------------- /index.coffee: -------------------------------------------------------------------------------- 1 | Path = require 'path' 2 | Vault = require('./src/hubot/vault.coffee').Vault 3 | 4 | module.exports = (robot, scripts) -> 5 | robot.vault = 6 | forUser: (user) -> 7 | new Vault(user) 8 | 9 | robot.loadFile(Path.resolve(__dirname, "src", "scripts"), "http.coffee") 10 | robot.loadFile(Path.resolve(__dirname, "src", "scripts"), "token.coffee") 11 | robot.loadFile(Path.resolve(__dirname, "src", "scripts"), "deploy.coffee") 12 | -------------------------------------------------------------------------------- /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 test/**/*_test.coffee test/**/**/*_test.coffee test/**/**/**/*_test.coffee", (err, output) -> 9 | console.log output 10 | throw err if err 11 | 12 | task "lint", "run linter", -> 13 | exec "NODE_ENV=test coffeelint -f .coffeelint.json src/", (err, output) -> 14 | console.log output 15 | throw err if err 16 | -------------------------------------------------------------------------------- /src/github/webhooks.coffee: -------------------------------------------------------------------------------- 1 | Path = require "path" 2 | 3 | exports.Push = require(Path.join(__dirname, "webhooks", "push")).Push 4 | exports.Deployment = require(Path.join(__dirname, "webhooks", "deployment")).Deployment 5 | exports.PullRequest = require(Path.join(__dirname, "webhooks", "pull_request")).PullRequest 6 | exports.CommitStatus = require(Path.join(__dirname, "webhooks", "commit_status")).CommitStatus 7 | exports.DeploymentStatus = require(Path.join(__dirname, "webhooks", "deployment_status")).DeploymentStatus 8 | -------------------------------------------------------------------------------- /src/github/webhooks/pull_request.coffee: -------------------------------------------------------------------------------- 1 | class PullRequest 2 | constructor: (@id, @payload) -> 3 | @name = @payload.repository.name 4 | @actor = @payload.sender.login 5 | @title = @payload.pull_request.title 6 | @branch = @payload.pull_request.head.ref 7 | @state = @payload.pull_request.state 8 | @merged = @payload.pull_request.merged 9 | @action = @payload.action 10 | @number = @payload.number 11 | @repoName = @payload.repository.full_name 12 | 13 | toSimpleString: -> 14 | "hubot-deploy: #{@actor} #{@action} pull request ##{@number}: #{@branch} " + 15 | "https://github.com/#{@repoName}/pull/#{@number}/files" 16 | 17 | exports.PullRequest = PullRequest 18 | -------------------------------------------------------------------------------- /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", "staging"], 15 | 16 | "provider": "npm" 17 | }, 18 | 19 | "github": { 20 | "repository": "github/github", 21 | "environments": ["production","staging"], 22 | 23 | "provider": "capistrano" 24 | }, 25 | 26 | "restricted-app": { 27 | "repository": "acme/example", 28 | "enironments": ["production"], 29 | "provider": "capistrano", 30 | "allowed_rooms": ["ops"] 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /test/scripts/http_test.coffee: -------------------------------------------------------------------------------- 1 | Path = require "path" 2 | Robot = require "hubot/src/robot" 3 | TextMessage = require("hubot/src/message").TextMessage 4 | 5 | describe "Deployment Status HTTP Callbacks", () -> 6 | user = null 7 | robot = null 8 | adapter = null 9 | 10 | beforeEach (done) -> 11 | robot = new Robot(null, "mock-adapter", true, "Hubot") 12 | 13 | robot.adapter.on "connected", () -> 14 | process.env.HUBOT_DEPLOY_RANDOM_REPLY = "sup-dude" 15 | 16 | require("../../index")(robot) 17 | 18 | userInfo = 19 | name: "atmos", 20 | room: "#zf-promo" 21 | 22 | user = robot.brain.userForId "1", userInfo 23 | adapter = robot.adapter 24 | 25 | done() 26 | 27 | robot.run() 28 | 29 | afterEach () -> 30 | robot.server.close() 31 | robot.shutdown() 32 | -------------------------------------------------------------------------------- /src/github/webhooks/commit_status.coffee: -------------------------------------------------------------------------------- 1 | class CommitStatus 2 | constructor: (@id, @payload) -> 3 | @state = @payload.state 4 | @targetUrl = @payload.target_url 5 | @description = @payload.description 6 | @context = @payload.context 7 | @ref = @payload.branches[0].name 8 | @sha = @payload.sha.substring(0,7) 9 | @name = @payload.repository.name 10 | @repoName = @payload.repository.full_name 11 | 12 | toSimpleString: -> 13 | msg = "hubot-deploy: Build for #{@name}/#{@ref} (#{@context}) " 14 | switch @state 15 | when "success" 16 | msg += "was successful." 17 | when "failure", "error" 18 | msg += "failed." 19 | else 20 | msg += "is running." 21 | 22 | if @targetUrl? 23 | msg += " " + @targetUrl 24 | 25 | msg 26 | 27 | exports.CommitStatus = CommitStatus 28 | -------------------------------------------------------------------------------- /test/support/cassettes/github_user.coffee: -------------------------------------------------------------------------------- 1 | module.exports.cassettes = 2 | '/user-invalid-auth': 3 | host: 'https://api.github.com:443' 4 | path: '/user' 5 | code: 401 6 | body: 7 | message: "Bad credentials" 8 | documentation_url: "https://developer.github.com/v3" 9 | 10 | '/user-invalid-scopes': 11 | host: 'https://api.github.com:443' 12 | path: '/user' 13 | code: 200 14 | headers: 15 | "x-oauth-scopes": "public" 16 | body: 17 | id: 1 18 | login: "octocat" 19 | avatar_url: "https://github.com/images/error/octocat_happy.gif" 20 | 21 | 22 | '/user-valid': 23 | host: 'https://api.github.com:443' 24 | path: '/user' 25 | code: 200 26 | headers: 27 | "x-oauth-scopes": "gist, repo" 28 | body: 29 | id: 1 30 | login: "octocat" 31 | avatar_url: "https://github.com/images/error/octocat_happy.gif" 32 | -------------------------------------------------------------------------------- /src/github/api/deployment_status.coffee: -------------------------------------------------------------------------------- 1 | Path = require "path" 2 | Version = require(Path.join(__dirname, "..", "..", "version")).Version 3 | ScopedClient = require "scoped-http-client" 4 | 5 | class DeploymentStatus 6 | constructor: (@apiToken, @repoName, @number) -> 7 | @state = undefined 8 | @targetUrl = undefined 9 | @description = undefined 10 | 11 | postParams: () -> 12 | data = 13 | state: @state 14 | target_url: @targetUrl 15 | description: @description 16 | JSON.stringify(data) 17 | 18 | create: (callback) -> 19 | ScopedClient.create("https://api.github.com"). 20 | header("Accept", "application/vnd.github+json"). 21 | header("User-Agent", "hubot-deploy-v#{Version}"). 22 | header("Authorization", "token #{@apiToken}"). 23 | path("/repos/#{@repoName}/deployments/#{@number}/statuses"). 24 | post(@postParams()) (err, res, body) -> 25 | callback(err, res, body) 26 | 27 | exports.DeploymentStatus = DeploymentStatus 28 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | 0.7.1 2 | ===== 3 | * Support incoming deployment status hooks. Emit events that can be handled programatically in the bot. 4 | 5 | 6 | 0.7.0 7 | ===== 8 | 9 | * Support different api endpoints(enterprise) on a per application basis. 10 | * Support `deploy-token:set` commands to have chat user specific tokens for 11 | deployment creation. 12 | * The `task` attribute is first class and not in the payload anymore. 13 | * Support recent deployments listing in chat `/deploys hubot` 14 | * Support required_contexts in deployment API 15 | * Use user tokens if present for fetching recent deployments 16 | 17 | 0.6.x 18 | ===== 19 | 20 | * Loosen required hubot version >= 2.7.2 21 | * Loosin node engine requirements to work with 0.8 and 0.10 22 | * Customizable script prefix, defaults to 'deploy' still 23 | 24 | 0.4.1 25 | ===== 26 | 27 | * Use robot.adapterName made available in https://github.com/github/hubot/pull/663 28 | * Explicitly load the hubot script so it supports the help command. 29 | * Break up the docs into separate files for examples. 30 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/hubot/vault.coffee: -------------------------------------------------------------------------------- 1 | fernet = require 'fernet' 2 | 3 | class Vault 4 | constructor: (@user) -> 5 | @user?.vault or= {} 6 | @vault = @user.vault 7 | @secrets = @getSecrets() 8 | 9 | set: (key, value) -> 10 | token = new fernet.Token(secret: @currentSecret()) 11 | @vault[key] = token.encode(JSON.stringify(value)) 12 | 13 | get: (key) -> 14 | return undefined unless @vault[key] 15 | for secret in @secrets 16 | token = new fernet.Token 17 | secret: secret 18 | token: @vault[key] 19 | ttl: 0 20 | try 21 | value = JSON.parse(token.decode()) 22 | return value 23 | catch error 24 | continue 25 | 26 | unset: (key) -> 27 | delete @vault[key] 28 | 29 | currentSecret: -> 30 | @secrets[0] 31 | 32 | getSecrets: -> 33 | unless process.env.HUBOT_DEPLOY_FERNET_SECRETS? 34 | throw new Error("Please set a HUBOT_DEPLOY_FERNET_SECRETS string in the environment") 35 | fernetSecrets = process.env.HUBOT_DEPLOY_FERNET_SECRETS.split(",") 36 | (new fernet.Secret(secret) for secret in fernetSecrets) 37 | 38 | exports.Vault = Vault 39 | -------------------------------------------------------------------------------- /src/models/handler.coffee: -------------------------------------------------------------------------------- 1 | Crypto = require "crypto" 2 | Fernet = require "fernet" 3 | 4 | GitHubDeploymentStatus = require("../github/api").GitHubDeploymentStatus 5 | 6 | class Handler 7 | constructor: (@robot, @deployment) -> 8 | @ref = @deployment.ref 9 | @sha = @deployment.sha 10 | @repoName = @deployment.repoName 11 | @environment = @deployment.environment 12 | @notify = @deployment.notify 13 | @actualDeployment = @deployment.payload.deployment 14 | @provider = @actualDeployment.payload?.config?.provider 15 | @number = @actualDeployment.id 16 | @task = @actualDeployment.task 17 | 18 | run: (callback) -> 19 | try 20 | unless @robot.name is @actualDeployment.payload.robotName 21 | throw new Error "Received request for unintended robot #{@actualDeployment.payload.robotName}." 22 | unless @notify? 23 | throw new Error("Not deploying #{@repoName}/#{@ref} to #{@environment}. Not chat initiated.") 24 | callback undefined, @ 25 | catch err 26 | callback err, @ 27 | 28 | exports.Handler = Handler 29 | -------------------------------------------------------------------------------- /src/github/api.coffee: -------------------------------------------------------------------------------- 1 | Url = require "url" 2 | Path = require "path" 3 | ########################################################################### 4 | class GitHubApi 5 | constructor: (@userToken, @application) -> 6 | @token = @apiToken() 7 | 8 | @parsedApiUrl = Url.parse(@apiUri()) 9 | @hostname = @parsedApiUrl.host 10 | 11 | apiUri: -> 12 | (@application? and @application['github_api']) or 13 | process.env.HUBOT_GITHUB_API or 14 | 'https://api.github.com' 15 | 16 | apiToken: -> 17 | (@application? and @application['github_token']) or 18 | (@userToken? and @userToken) or 19 | process.env.HUBOT_GITHUB_TOKEN 20 | 21 | filterPaths: -> 22 | newArr = @pathParts().filter (word) -> word isnt "" 23 | 24 | pathParts: -> 25 | @parsedApiUrl.path.split("/") 26 | 27 | path: (suffix) -> 28 | if suffix?.length > 0 29 | parts = @filterPaths() 30 | parts.push(suffix) 31 | "/#{parts.join('/')}" 32 | else 33 | @parsedApiUrl.path 34 | 35 | exports.Api = GitHubApi 36 | exports.Deployment = require(Path.join(__dirname, "api", "deployment")).Deployment 37 | exports.DeploymentStatus = require(Path.join(__dirname, "api", "deployment_status")).DeploymentStatus 38 | -------------------------------------------------------------------------------- /test/github/api/deployment_status_test.coffee: -------------------------------------------------------------------------------- 1 | Fs = require "fs" 2 | Path = require "path" 3 | 4 | GitHubRequests = require(Path.join(__dirname, "..", "..", "..", "src", "github", "api")) 5 | DeploymentStatus = GitHubRequests.DeploymentStatus 6 | 7 | describe "GitHubRequests.GitHubDeploymentStatus", () -> 8 | describe "basic variables", () -> 9 | it "knows the state and repo", () -> 10 | status = new DeploymentStatus("token", "atmos/hubot-deploy", "42") 11 | status.targetUrl = "https://gist.github.com/my-sweet-gist" 12 | status.description = "Deploying from chat, wooo" 13 | status.state = "success" 14 | 15 | assert.equal "42", status.number 16 | assert.equal "token", status.apiToken 17 | assert.equal "atmos/hubot-deploy", status.repoName 18 | assert.equal "success", status.state 19 | 20 | it "posts well formed parameters", () -> 21 | status = new DeploymentStatus("token", "atmos/hubot-deploy", "42") 22 | status.targetUrl = "https://gist.github.com/my-sweet-gist" 23 | status.description = "Deploying from chat, wooo" 24 | status.state = "success" 25 | 26 | postParams = 27 | state: status.state 28 | target_url: status.targetUrl 29 | description: status.description 30 | 31 | assert.equal JSON.stringify(postParams), status.postParams() 32 | -------------------------------------------------------------------------------- /test/models/deployments/latest_test.coffee: -------------------------------------------------------------------------------- 1 | VCR = require "ys-vcr" 2 | Path = require "path" 3 | 4 | srcDir = Path.join(__dirname, "..", "..", "..", "src") 5 | 6 | Version = require(Path.join(srcDir, "version")).Version 7 | Deployment = require(Path.join(srcDir, "github", "api")).Deployment 8 | 9 | describe "Deployment#latest", () -> 10 | beforeEach () -> 11 | VCR.playback() 12 | afterEach () -> 13 | VCR.stop() 14 | 15 | it "gets the latest deployments from the api", (done) -> 16 | VCR.play '/github-deployments-latest-production-success' 17 | deployment = new Deployment("hubot-deploy", "master", "deploy", "production", "", "") 18 | deployment.latest (err, deployments) -> 19 | throw err if err 20 | assert.equal "hubot-deploy", deployment.name 21 | assert.equal "production", deployment.env 22 | assert.equal 2, deployments.length 23 | done() 24 | 25 | it "gets the latest deployments from the api", (done) -> 26 | VCR.play '/github-deployments-latest-staging-success' 27 | deployment = new Deployment("hubot-deploy", "master", "deploy", "staging", "", "") 28 | deployment.latest (err, deployments) -> 29 | throw err if err 30 | assert.equal "hubot-deploy", deployment.name 31 | assert.equal "staging", deployment.env 32 | assert.equal 2, deployments.length 33 | done() 34 | 35 | -------------------------------------------------------------------------------- /src/github/webhooks/deployment_status.coffee: -------------------------------------------------------------------------------- 1 | class DeploymentStatus 2 | constructor: (@id, @payload) -> 3 | deployment = @payload.deployment 4 | @name = deployment?.payload?.name or @payload.repository.name 5 | @repoName = @payload.repository.full_name 6 | 7 | @number = deployment.id 8 | @sha = deployment.sha.substring(0,7) 9 | @ref = deployment.ref 10 | @environment = deployment.environment 11 | @notify = deployment.payload.notify 12 | if @notify? and @notify.user? 13 | @actorName = @notify.user 14 | else 15 | @actorName = deployment.creator.login 16 | 17 | deployment_status = @payload.deployment_status 18 | @state = deployment_status.state 19 | @targetUrl = deployment_status.target_url 20 | @description = deployment_status.description 21 | 22 | if deployment.sha is @ref 23 | @ref = @sha 24 | 25 | toSimpleString: -> 26 | msg = "hubot-deploy: #{@actorName}'s deployment ##{@number} of #{@name}/#{@ref} to #{@environment} " 27 | switch @state 28 | when "success" 29 | msg += "was successful." 30 | when "failure", "error" 31 | msg += "failed." 32 | else 33 | msg += "is running." 34 | 35 | if @targetUrl? 36 | msg += " " + @targetUrl 37 | 38 | msg 39 | 40 | exports.DeploymentStatus = DeploymentStatus 41 | -------------------------------------------------------------------------------- /src/github/webhooks/deployment.coffee: -------------------------------------------------------------------------------- 1 | Fernet = require "fernet" 2 | 3 | class Deployment 4 | constructor: (@id, @payload) -> 5 | deployment = @payload.deployment 6 | @name = @payload.repository.name 7 | @repoName = @payload.repository.full_name 8 | 9 | @number = deployment.id 10 | @sha = deployment.sha.substring(0,7) 11 | @ref = deployment.ref 12 | @task = deployment.task 13 | @environment = deployment.environment 14 | 15 | if process.env.HUBOT_DEPLOY_ENCRYPT_PAYLOAD and proccess.env.HUBOT_DEPLOY_FERNET_SECRETS 16 | fernetSecret = new Fernet.Secret(process.env.HUBOT_DEPLOY_FERNET_SECRETS) 17 | fernetToken = new Fernet.Token(secret: fernetSecret, token: deployment.payload, ttl: 0) 18 | 19 | payload = deployment.payload 20 | deployment.payload = fernetToken.decode(payload) 21 | 22 | @notify = deployment.payload.notify 23 | 24 | if @notify? and @notify.user? 25 | @actorName = @notify.user 26 | else 27 | @actorName = deployment.creator.login 28 | 29 | if deployment.payload.yubikey? 30 | @yubikey = deployment.payload.yubikey 31 | 32 | if @payload.deployment.sha is @ref 33 | @ref = @sha 34 | 35 | toSimpleString: -> 36 | "hubot-deploy: #{@actorName}'s deployment ##{@number} of #{@name}/#{@ref} to #{@environment} requested." 37 | 38 | exports.Deployment = Deployment 39 | 40 | -------------------------------------------------------------------------------- /test/github/api_test.coffee: -------------------------------------------------------------------------------- 1 | Path = require('path') 2 | 3 | GitHubApi = require(Path.join(__dirname, "..", "..", "src", "github", "api")) 4 | 5 | describe "GitHubApi", () -> 6 | describe "defaults", () -> 7 | apiConfig = new GitHubApi.Api("xxx", null) 8 | 9 | it "fetches the GitHub API token provided", () -> 10 | assert.equal "xxx", apiConfig.token 11 | it "defaults to api.github.com", () -> 12 | assert.equal "api.github.com", apiConfig.hostname 13 | it "handles no path suffix requests", () -> 14 | assert.equal "/", apiConfig.path("") 15 | it "handles path suffixes", () -> 16 | assert.equal "/repos/atmos/heaven/deployments", apiConfig.path("repos/atmos/heaven/deployments") 17 | 18 | describe "custom application and enterprise url", () -> 19 | config = 20 | application = 21 | github_api: "https://enterprise.mycompany.com/api/v3/" 22 | github_token: "yyy" 23 | apiConfig = new GitHubApi.Api("xxx", application) 24 | 25 | it "fetches the custom GitHub API token", () -> 26 | assert.equal "yyy", apiConfig.token 27 | it "uses the application api_url field for hostname", () -> 28 | assert.equal "enterprise.mycompany.com", apiConfig.hostname 29 | it "handles no path suffix requests", () -> 30 | assert.equal "/api/v3/", apiConfig.path("") 31 | it "handles path suffixes", () -> 32 | assert.equal "/api/v3/repos/atmos/heaven/deployments", apiConfig.path("repos/atmos/heaven/deployments") 33 | 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hubot-deploy", 3 | "version": "0.13.27", 4 | "author": "Corey Donohoe ", 5 | "description": "hubot script for GitHub Flow", 6 | "main": "index.coffee", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/atmos/hubot-deploy" 10 | }, 11 | "keywords": [ 12 | "flow", 13 | "github", 14 | "deploy", 15 | "hubot", 16 | "chatops", 17 | "deployment" 18 | ], 19 | "homepage": "https://github.com/atmos/hubot-deploy", 20 | "scripts": { 21 | "test": "NODE_ENV=test mocha test/**/*_test.coffee test/**/**/*_test.coffee test/**/**/**/*_test.coffee", 22 | "posttest": "NODE_ENV=test coffeelint -f .coffeelint.json src/" 23 | }, 24 | "contributors": [ 25 | { 26 | "name": "Corey Donohoe", 27 | "email": "atmos@atmos.org" 28 | } 29 | ], 30 | "dependencies": { 31 | "coffeelint": ">= 2.1", 32 | "fernet": "0.3.1", 33 | "hubot": ">= 2.13.2", 34 | "inflection": "1.3.6", 35 | "ip-address": "5.0.2", 36 | "octonode": "0.9.5", 37 | "scoped-http-client": "0.11.0", 38 | "sprintf": "0.1.3", 39 | "timeago": "0.1.0" 40 | }, 41 | "devDependencies": { 42 | "chai": "~1.0.3", 43 | "mocha": "~2.2.5", 44 | "ys-vcr": "0.4.2", 45 | "coffee-script": ">=1.6.3", 46 | "hubot-mock-adapter": "1.1.1" 47 | }, 48 | "engines": { 49 | "node": ">=0.8.0" 50 | }, 51 | "bugs": { 52 | "url": "https://github.com/atmos/hubot-deploy/issues" 53 | }, 54 | "license": "MIT" 55 | } 56 | -------------------------------------------------------------------------------- /src/models/patterns.coffee: -------------------------------------------------------------------------------- 1 | Inflection = require "inflection" 2 | 3 | validSlug = "([-_\.0-9a-z]+)" 4 | 5 | scriptPrefix = process.env['HUBOT_DEPLOY_PREFIX'] || "deploy" 6 | 7 | # The :hammer: regex that handles all /deploy requests 8 | DEPLOY_SYNTAX = /// 9 | (#{scriptPrefix}(?:\:[^\s]+)?) # / prefix 10 | (!)?\s+ # Whether or not it was a forced deployment 11 | #{validSlug} # application name, from apps.json 12 | (?:\/([^\s]+))? # Branch or sha to deploy 13 | (?:\s+(?:to|in|on)\s+ # http://i.imgur.com/3KqMoRi.gif 14 | #{validSlug} # Environment to release to 15 | (?:\/([^\s]+))?)?\s* # Host filter to try 16 | (?:([cbdefghijklnrtuv]{32,64}|\d{6})?\s*)?$ # Optional Yubikey 17 | ///i 18 | 19 | 20 | # Supports tasks like 21 | # /deploys github 22 | # 23 | # and 24 | # 25 | # /deploys github in staging 26 | inflectedScriptPrefix = Inflection.pluralize(scriptPrefix) 27 | DEPLOYS_SYNTAX = /// 28 | (#{inflectedScriptPrefix}) # / prefix 29 | \s+ # hwhitespace 30 | #{validSlug} # application name, from apps.json 31 | (?:\/([^\s]+))? # Branch or sha to deploy 32 | (?:\s+(?:to|in|on)\s+ # http://i.imgur.com/3KqMoRi.gif 33 | #{validSlug})? # Environment to release to 34 | ///i 35 | 36 | exports.DeployPrefix = scriptPrefix 37 | exports.DeployPattern = DEPLOY_SYNTAX 38 | exports.DeploysPattern = DEPLOYS_SYNTAX 39 | -------------------------------------------------------------------------------- /test/models/formatter_test.coffee: -------------------------------------------------------------------------------- 1 | Path = require('path') 2 | 3 | Deployment = require(Path.join(__dirname, "..", "..", "src", "github", "api")).Deployment 4 | Formatter = require(Path.join(__dirname, "..", "..", "src", "models", "formatters")) 5 | 6 | describe "Formatter", () -> 7 | describe "LatestFormatter", () -> 8 | it "displays recent deployments", () -> 9 | deployment = new Deployment("hubot", null, null, "production") 10 | deployments = require(Path.join(__dirname, "..", "fixtures", "deployments")) 11 | formatter = new Formatter.LatestFormatter(deployment, deployments) 12 | 13 | message = formatter.message() 14 | 15 | assert.match message, /Recent production Deployments for hubot/im 16 | assert.match message, /atmos \| master\(8efb8c88\)\s+\| (.*) 2014-06-13T20:55:21Z/ 17 | assert.match message, /atmos \| 8efb8c88\(auto-deploy\)\s+\| (.*) 2014-06-13T20:52:13Z/ 18 | assert.match message, /atmos \| master\(ffcabfea\)\s+\| (.*) 2014-06-11T22:47:34Z/ 19 | 20 | describe "WhereFormatter", () -> 21 | it "displays deployment environments", () -> 22 | deployment = new Deployment("hubot", null, null, "production") 23 | deployments = require(Path.join(__dirname, "..", "fixtures", "deployments")) 24 | formatter = new Formatter.WhereFormatter(deployment) 25 | 26 | message = formatter.message() 27 | 28 | assert.match message, /Environments for hubot/im 29 | assert.match message, /production/im 30 | assert.notMatch message, /staging/im 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hubot-deploy [![Build Status](https://travis-ci.org/atmos/hubot-deploy.png?branch=master)](https://travis-ci.org/atmos/hubot-deploy) 2 | 3 | [GitHub Flow][1] via [hubot][3]. Chatting with hubot creates [deployments][2] on GitHub and dispatches [Deployment Events][4]. 4 | 5 | ![](https://f.cloud.github.com/assets/38/2331137/77036ef8-a444-11e3-97f6-68dab6975eeb.jpg) 6 | 7 | ## Installation 8 | 9 | * Add hubot-deploy to your `package.json` file. 10 | * Add hubot-deploy to your `external-scripts.json` file. 11 | * [Configure](https://github.com/atmos/hubot-deploy/blob/master/docs/configuration.md) your runtime environment to interact with the GitHub API. 12 | * Understand how [apps.json](https://github.com/atmos/hubot-deploy/blob/master/docs/config-file.md) works. 13 | * Learn about [ChatOps](https://github.com/atmos/hubot-deploy/blob/master/docs/chatops.md) deploys. 14 | 15 | ## See Also 16 | 17 | * [hubot](https://github.com/github/hubot) - A chat robot with support for a lot of networks. 18 | * [heaven](https://github.com/atmos/heaven) - Listens for Deployment events from GitHub and executes the deployment for you. 19 | * [hubot-auto-deploy](https://github.com/atmos/hubot-auto-deploy) - Manage automated deployments on GitHub from chat. 20 | * [github-credentials](https://github.com/github/hubot-scripts/blob/master/src/scripts/github-credentials.coffee) - Map your chat username to your GitHub username if they differ 21 | 22 | [1]: https://guides.github.com/overviews/flow/ 23 | [2]: https://developer.github.com/v3/repos/deployments/ 24 | [3]: https://hubot.github.com 25 | [4]: https://developer.github.com/v3/activity/events/types/#deploymentevent 26 | [5]: https://developer.github.com/v3/repos/deployments/ 27 | -------------------------------------------------------------------------------- /test/github/webhooks/commit_status_test.coffee: -------------------------------------------------------------------------------- 1 | Fs = require "fs" 2 | Path = require "path" 3 | 4 | srcDir = Path.join(__dirname, "..", "..", "..", "src") 5 | 6 | GitHubEvents = require(Path.join(srcDir, "github", "webhooks")) 7 | CommitStatus = GitHubEvents.CommitStatus 8 | 9 | describe "GitHubEvents.CommitStatus fixtures", () -> 10 | deploymentStatusFor = (fixtureName) -> 11 | fixtureData = Path.join __dirname, "..", "..", "fixtures", "statuses", "#{fixtureName}.json" 12 | fixturePayload = JSON.parse(Fs.readFileSync(fixtureData)) 13 | status = new CommitStatus "uuid", fixturePayload 14 | 15 | describe "pending", () -> 16 | it "knows the status and repo", () -> 17 | status = deploymentStatusFor "pending" 18 | assert.equal status.state, "pending" 19 | assert.equal status.repoName, "atmos/my-robot" 20 | assert.equal status.toSimpleString(), "hubot-deploy: Build for atmos/master (Janky (github)) is running. https://ci.atmos.org/1123112/output" 21 | 22 | describe "failure", () -> 23 | it "knows the status and repo", () -> 24 | status = deploymentStatusFor "failure" 25 | assert.equal status.state, "failure" 26 | assert.equal status.repoName, "atmos/my-robot" 27 | assert.equal status.toSimpleString(), "hubot-deploy: Build for my-robot/jroes-patch-4 (Janky (github)) failed. https://baller.com/target_stuff" 28 | 29 | describe "success", () -> 30 | it "knows the status and repo", () -> 31 | status = deploymentStatusFor "success" 32 | assert.equal status.state, "success" 33 | assert.equal status.repoName, "atmos/my-robot" 34 | assert.equal status.toSimpleString(), "hubot-deploy: Build for github/master (Janky (github)) was successful. https://ci.atmos.org/1123112/output" 35 | -------------------------------------------------------------------------------- /test/github/api/deployment_test.coffee: -------------------------------------------------------------------------------- 1 | Path = require "path" 2 | 3 | Version = require(Path.join(__dirname, "..", "..", "..", "src", "version")).Version 4 | Deployment = require(Path.join(__dirname, "..", "..", "..", "src", "github", "api")).Deployment 5 | 6 | describe "Deployment fixtures", () -> 7 | describe "#autoMerge", () -> 8 | it "works with auto-merging", () -> 9 | deployment = new Deployment("hubot", "master", "deploy", "production", "", "") 10 | assert.equal(false, deployment.autoMerge) 11 | 12 | describe "#api", () -> 13 | context "with no ca file", () -> 14 | it "doesnt set agentOptions", () -> 15 | deployment = new Deployment("hubot", "master", "deploy", "production", "", "") 16 | api = deployment.api() 17 | assert.equal(api.requestDefaults.agentOptions, null) 18 | 19 | context "with ca file", () -> 20 | it "sets agentOptions.ca", () -> 21 | process.env.HUBOT_CA_FILE = Path.join(__dirname, "..", "..", "fixtures", "cafile.txt") 22 | deployment = new Deployment("hubot", "master", "deploy", "production", "", "") 23 | api = deployment.api() 24 | assert(api.requestDefaults.agentOptions.ca) 25 | 26 | #describe "#latest()", () -> 27 | # it "fetches the latest deployments", (done) -> 28 | # deployment = new Deployment("hubot") 29 | # deployment.latest (deployments) -> 30 | # done() 31 | 32 | #describe "#post()", () -> 33 | # it "404s with a handy message", (done) -> 34 | # failureMessage = "Unable to create deployments for github/github. Check your scopes for this token." 35 | # deployment = new Deployment("github", "master", "deploy", "garage", "", "") 36 | # deployment.post (responseMessage) -> 37 | # assert.equal(responseMessage, failureMessage) 38 | # done() 39 | -------------------------------------------------------------------------------- /test/scripts/latest_deploys_test.coffee: -------------------------------------------------------------------------------- 1 | VCR = require "ys-vcr" 2 | Path = require "path" 3 | Robot = require "hubot/src/robot" 4 | TextMessage = require("hubot/src/message").TextMessage 5 | Verifiers = require(Path.join(__dirname, "..", "..", "src", "models", "verifiers")) 6 | 7 | describe "Latest deployments", () -> 8 | user = null 9 | robot = null 10 | adapter = null 11 | 12 | beforeEach (done) -> 13 | VCR.playback() 14 | process.env.HUBOT_DEPLOY_FERNET_SECRETS or= "HSfTG4uWzw9whtlLEmNAzscHh96eHUFt3McvoWBXmHk=" 15 | robot = new Robot(null, "mock-adapter", true, "Hubot") 16 | 17 | robot.adapter.on "connected", () -> 18 | require("../../index")(robot) 19 | 20 | userInfo = 21 | name: "atmos", 22 | room: "#my-room" 23 | 24 | user = robot.brain.userForId "1", userInfo 25 | adapter = robot.adapter 26 | 27 | done() 28 | 29 | robot.run() 30 | 31 | afterEach () -> 32 | VCR.stop() 33 | robot.server.close() 34 | robot.shutdown() 35 | 36 | it "tells you the latest production deploys", (done) -> 37 | VCR.play '/github-deployments-latest-production-success' 38 | robot.on "hubot_deploy_recent_deployments", (msg, deployment, deployments, formatter) -> 39 | assert.equal "hubot-deploy", deployment.name 40 | assert.equal "production", deployment.env 41 | assert.equal 2, deployments.length 42 | done() 43 | 44 | adapter.receive(new TextMessage(user, "Hubot deploys hubot-deploy in production")) 45 | 46 | it "tells you the latest staging deploys", (done) -> 47 | VCR.play '/github-deployments-latest-staging-success' 48 | robot.on "hubot_deploy_recent_deployments", (msg, deployment, deployments, formatter) -> 49 | assert.equal "hubot-deploy", deployment.name 50 | assert.equal "staging", deployment.env 51 | assert.equal 2, deployments.length 52 | done() 53 | 54 | adapter.receive(new TextMessage(user, "Hubot deploys hubot-deploy in staging")) 55 | -------------------------------------------------------------------------------- /test/github/webhooks/push_test.coffee: -------------------------------------------------------------------------------- 1 | Fs = require "fs" 2 | Path = require "path" 3 | 4 | srcDir = Path.join(__dirname, "..", "..", "..", "src") 5 | 6 | GitHubEvents = require(Path.join(srcDir, "github", "webhooks")) 7 | 8 | describe "GitHubEvents.Push fixtures", () -> 9 | pushFor = (fixtureName) -> 10 | fixtureData = Path.join __dirname, "..", "..", "fixtures", "pushes", "#{fixtureName}.json" 11 | fixturePayload = JSON.parse(Fs.readFileSync(fixtureData)) 12 | push = new GitHubEvents.Push "uuid", fixturePayload 13 | 14 | describe "single commit", () -> 15 | it "knows the state and repo", (done) -> 16 | push = pushFor("single") 17 | message = "hubot-deploy: atmos pushed a commit" 18 | assert.equal message, push.toSimpleString() 19 | summaryMessage = "[hubot-deploy] atmos pushed 1 new commit to changes" 20 | assert.equal summaryMessage, push.summaryMessage() 21 | actorLink = "atmos" 22 | assert.equal actorLink, push.actorLink 23 | branchUrl = "https://github.com/atmos/hubot-deploy/commits/changes" 24 | assert.equal branchUrl, push.branchUrl 25 | firstMessage = "- Update README.md - atmos - (0d1a26e6)" 26 | assert.equal firstMessage, push.firstMessage 27 | summaryUrl = "https://github.com/atmos/hubot-deploy/commit/0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c" 28 | assert.equal summaryUrl, push.summaryUrl() 29 | done() 30 | 31 | describe "multiple commits", () -> 32 | it "knows the state and repo", (done) -> 33 | push = pushFor("multiple") 34 | message = "hubot-deploy: atmos pushed 3 commits" 35 | assert.equal message, push.toSimpleString() 36 | summaryMessage = "[hubot-deploy] atmos pushed 3 new commits to master" 37 | assert.equal summaryMessage, push.summaryMessage() 38 | summaryUrl = "http://github.com/atmos/hubot-deploy/compare/4c8124f...a47fd41" 39 | assert.equal summaryUrl, push.summaryUrl() 40 | done() 41 | -------------------------------------------------------------------------------- /test/scripts/deployment_test.coffee: -------------------------------------------------------------------------------- 1 | VCR = require "ys-vcr" 2 | Path = require "path" 3 | Robot = require "hubot/src/robot" 4 | TextMessage = require("hubot/src/message").TextMessage 5 | Verifiers = require(Path.join(__dirname, "..", "..", "src", "models", "verifiers")) 6 | 7 | describe "Deploying from chat", () -> 8 | user = null 9 | robot = null 10 | adapter = null 11 | 12 | beforeEach (done) -> 13 | VCR.playback() 14 | process.env.HUBOT_DEPLOY_FERNET_SECRETS or= "HSfTG4uWzw9whtlLEmNAzscHh96eHUFt3McvoWBXmHk=" 15 | process.env.HUBOT_DEPLOY_EMIT_GITHUB_DEPLOYMENTS = true 16 | robot = new Robot(null, "mock-adapter", true, "Hubot") 17 | 18 | robot.adapter.on "connected", () -> 19 | require("../../index")(robot) 20 | 21 | userInfo = 22 | name: "atmos", 23 | room: "#my-room" 24 | 25 | user = robot.brain.userForId "1", userInfo 26 | adapter = robot.adapter 27 | 28 | done() 29 | 30 | robot.run() 31 | 32 | afterEach () -> 33 | delete(process.env.HUBOT_DEPLOY_DEFAULT_ENVIRONMENT) 34 | VCR.stop() 35 | robot.server.close() 36 | robot.shutdown() 37 | 38 | it "creates deployments when requested from chat", (done) -> 39 | VCR.play '/repos-atmos-hubot-deploy-deployment-production-create-success' 40 | robot.on "github_deployment", (msg, deployment) -> 41 | assert.equal "hubot-deploy", deployment.name 42 | assert.equal "production", deployment.env 43 | done() 44 | 45 | adapter.receive(new TextMessage(user, "Hubot deploy hubot-deploy")) 46 | 47 | it "allows for the default environment to be overridden by an env var", (done) -> 48 | process.env.HUBOT_DEPLOY_DEFAULT_ENVIRONMENT = "staging" 49 | VCR.play '/repos-atmos-hubot-deploy-deployment-staging-create-success' 50 | robot.on "github_deployment", (msg, deployment) -> 51 | assert.equal "hubot-deploy", deployment.name 52 | assert.equal "staging", deployment.env 53 | done() 54 | 55 | adapter.receive(new TextMessage(user, "Hubot deploy hubot-deploy")) 56 | -------------------------------------------------------------------------------- /src/models/formatters.coffee: -------------------------------------------------------------------------------- 1 | Sprintf = require("sprintf").sprintf 2 | Timeago = require("timeago") 3 | 4 | class Formatter 5 | constructor: (@deployment, @extras) -> 6 | 7 | class WhereFormatter extends Formatter 8 | message: -> 9 | output = "Environments for #{@deployment.name}\n" 10 | output += "-----------------------------------------------------------------\n" 11 | output += Sprintf "%-15s\n", "Environment" 12 | output += "-----------------------------------------------------------------\n" 13 | 14 | for environment in @deployment.environments 15 | output += "#{environment}\n" 16 | output += "-----------------------------------------------------------------\n" 17 | 18 | output 19 | 20 | class LatestFormatter extends Formatter 21 | delimiter: -> 22 | "-----------------------------------------------------------------------------------\n" 23 | 24 | loginForDeployment: (deployment) -> 25 | result = null 26 | if deployment.payload? 27 | if deployment.payload.notify 28 | result or= deployment.payload.notify.user_name 29 | result or= deployment.payload.actor 30 | 31 | result or= deployment.creator.login 32 | 33 | message: -> 34 | output = "Recent #{@deployment.env} Deployments for #{@deployment.name}\n" 35 | output += @delimiter() 36 | output += Sprintf "%-15s | %-21s | %-38s\n", "Who", "What", "When" 37 | output += @delimiter() 38 | 39 | if @extras? 40 | for deployment in @extras[0..10] 41 | if deployment.ref is deployment.sha[0..7] 42 | ref = deployment.ref 43 | if deployment.description.match(/auto deploy triggered by a commit status change/) 44 | ref += "(auto-deploy)" 45 | 46 | else 47 | ref = "#{deployment.ref}(#{deployment.sha[0..7]})" 48 | 49 | login = @loginForDeployment(deployment) 50 | timestamp = Sprintf "%18s / %-21s", Timeago(deployment.created_at), deployment.created_at 51 | 52 | output += Sprintf "%-15s | %-21s | %38s\n", login, ref, timestamp 53 | 54 | output += @delimiter() 55 | output 56 | 57 | exports.WhereFormatter = WhereFormatter 58 | exports.LatestFormatter = LatestFormatter 59 | -------------------------------------------------------------------------------- /test/support/cassettes/github_statuses.coffee: -------------------------------------------------------------------------------- 1 | module.exports.cassettes = 2 | '/repos-atmos-my-robot-deployments-1875476-statuses-success': 3 | host: 'https://api.github.com:443' 4 | path: '/repos/atmos/my-robot/deployments/1875476/statuses' 5 | method: 'post' 6 | code: 200 7 | body: 8 | id: 1 9 | url: "https://api.github.com/repos/atmos/my-robot/deployments/1875476/statuses/1" 10 | state: "success" 11 | creator: 12 | id: 1 13 | login: "octocat" 14 | avatar_url: "https://github.com/images/error/octocat_happy.gif" 15 | description: "Deployment finished successfully.", 16 | target_url: "https://example.com/deployment/1875476/output", 17 | created_at: "2012-07-20T01:19:13Z", 18 | updated_at: "2012-07-20T01:19:13Z", 19 | 20 | '/repos-atmos-my-robot-deployments-1875476-statuses-failure': 21 | host: 'https://api.github.com:443' 22 | path: '/repos/atmos/my-robot/deployments/1875476/statuses' 23 | method: 'post' 24 | code: 200 25 | body: 26 | id: 1 27 | url: "https://api.github.com/repos/atmos/my-robot/deployments/1875476/statuses/1" 28 | state: "failure" 29 | creator: 30 | id: 1 31 | login: "octocat" 32 | avatar_url: "https://github.com/images/error/octocat_happy.gif" 33 | description: "Deployment failed to complete.", 34 | target_url: "https://example.com/deployment/1875476/output", 35 | created_at: "2012-07-20T01:19:13Z", 36 | updated_at: "2012-07-20T01:19:13Z", 37 | 38 | '/repos-atmos-my-robot-deployments-1875476-statuses-pending': 39 | host: 'https://api.github.com:443' 40 | path: '/repos/atmos/my-robot/deployments/1875476/statuses' 41 | method: 'post' 42 | code: 200 43 | body: 44 | id: 1 45 | url: "https://api.github.com/repos/atmos/my-robot/deployments/1875476/statuses/1" 46 | state: "pending" 47 | creator: 48 | id: 1 49 | login: "octocat" 50 | avatar_url: "https://github.com/images/error/octocat_happy.gif" 51 | description: "Deployment running.", 52 | target_url: "https://example.com/deployment/1875476/output", 53 | created_at: "2012-07-20T01:19:13Z", 54 | updated_at: "2012-07-20T01:19:13Z", 55 | -------------------------------------------------------------------------------- /test/models/verifiers_test.coffee: -------------------------------------------------------------------------------- 1 | VCR = require "ys-vcr" 2 | Path = require('path') 3 | 4 | Verifiers = require(Path.join(__dirname, "..", "..", "src", "models", "verifiers")) 5 | 6 | describe "GitHubWebHookIpVerifier", () -> 7 | afterEach () -> 8 | delete process.env.HUBOT_DEPLOY_GITHUB_SUBNETS 9 | 10 | it "verifies correct ip addresses", () -> 11 | verifier = new Verifiers.GitHubWebHookIpVerifier 12 | 13 | assert.isTrue verifier.ipIsValid("192.30.252.1") 14 | assert.isTrue verifier.ipIsValid("192.30.253.1") 15 | assert.isTrue verifier.ipIsValid("192.30.254.1") 16 | assert.isTrue verifier.ipIsValid("192.30.255.1") 17 | 18 | it "rejects incorrect ip addresses", () -> 19 | verifier = new Verifiers.GitHubWebHookIpVerifier 20 | 21 | assert.isFalse verifier.ipIsValid("192.30.250.1") 22 | assert.isFalse verifier.ipIsValid("192.30.251.1") 23 | assert.isFalse verifier.ipIsValid("192.168.1.1") 24 | assert.isFalse verifier.ipIsValid("127.0.0.1") 25 | 26 | it "verifies correct ip addresses with custom subnets", () -> 27 | process.env.HUBOT_DEPLOY_GITHUB_SUBNETS = '207.97.227.0/22,, 198.41.190.0/22' 28 | verifier = new Verifiers.GitHubWebHookIpVerifier 29 | 30 | assert.isTrue verifier.ipIsValid("207.97.224.1") 31 | assert.isTrue verifier.ipIsValid("198.41.188.1") 32 | 33 | assert.isFalse verifier.ipIsValid("192.30.252.1") 34 | assert.isFalse verifier.ipIsValid("207.97.228.1") 35 | assert.isFalse verifier.ipIsValid("198.41.194.1") 36 | assert.isFalse verifier.ipIsValid("192.168.1.1") 37 | assert.isFalse verifier.ipIsValid("127.0.0.1") 38 | 39 | describe "ApiTokenVerifier", () -> 40 | it "returns false when the GitHub token is invalid", (done) -> 41 | VCR.play "/user-invalid-auth" 42 | verifier = new Verifiers.ApiTokenVerifier("123456789") 43 | verifier.valid (result) -> 44 | assert.isFalse result 45 | done() 46 | 47 | it "returns false when the GitHub token has incorrect scopes", (done) -> 48 | VCR.play "/user-invalid-scopes" 49 | verifier = new Verifiers.ApiTokenVerifier("123456789") 50 | verifier.valid (result) -> 51 | assert.isFalse result 52 | done() 53 | 54 | it "tells you when your provided GitHub token is valid", (done) -> 55 | VCR.play "/user-valid" 56 | verifier = new Verifiers.ApiTokenVerifier("123456789") 57 | verifier.valid (result) -> 58 | assert.isTrue result 59 | done() 60 | -------------------------------------------------------------------------------- /test/fixtures/pushes/multiple.json: -------------------------------------------------------------------------------- 1 | { 2 | "after": "a47fd41f3aa4610ea527dcc1669dfdb9c15c5425", 3 | "ref": "refs/heads/master", 4 | "before": "4c8124ffcf4039d292442eeccabdeca5af5c5017", 5 | "compare": "http://github.com/atmos/hubot-deploy/compare/4c8124f...a47fd41", 6 | "repository": { 7 | "name": "hubot-deploy", 8 | "url": "http://github.com/atmos/hubot-deploy", 9 | "owner": { 10 | "name": "atmos", 11 | "email": "atmos@atmos.com" 12 | }, 13 | "master_branch": "master", 14 | "default_branch": "master", 15 | "private": false 16 | }, 17 | "pusher": { 18 | "name": "atmos" 19 | }, 20 | "commits": [ 21 | { 22 | "distinct": true, 23 | "removed": [ 24 | 25 | ], 26 | "message": "stub git call for Grit#heads test f:15 Case#1", 27 | "added": [ 28 | 29 | ], 30 | "timestamp": "2007-10-10T00:11:02-07:00", 31 | "modified": [ 32 | "lib/hubot-deploy/hubot-deploy.rb", 33 | "test/helper.rb", 34 | "test/test_hubot-deploy.rb" 35 | ], 36 | "url": "http://github.com/atmos/hubot-deploy/commit/06f63b43050935962f84fe54473a7c5de7977325", 37 | "author": { 38 | "name": "Corey Donohoe", 39 | "email": "atmos@atmos.com" 40 | }, 41 | "id": "06f63b43050935962f84fe54473a7c5de7977325" 42 | }, 43 | { 44 | "distinct": true, 45 | "removed": [ 46 | 47 | ], 48 | "message": "clean up heads test f:2hrs", 49 | "added": [ 50 | 51 | ], 52 | "timestamp": "2007-10-10T00:18:20-07:00", 53 | "modified": [ 54 | "test/test_hubot-deploy.rb" 55 | ], 56 | "url": "http://github.com/atmos/hubot-deploy/commit/5057e76a11abd02e83b7d3d3171c4b68d9c88480", 57 | "author": { 58 | "name": "Corey Donohoe", 59 | "email": "atmos@atmos.com" 60 | }, 61 | "id": "5057e76a11abd02e83b7d3d3171c4b68d9c88480" 62 | }, 63 | { 64 | "distinct": true, 65 | "removed": [ 66 | 67 | ], 68 | "message": "add more comments throughout", 69 | "added": [ 70 | 71 | ], 72 | "timestamp": "2007-10-10T00:50:39-07:00", 73 | "modified": [ 74 | "lib/hubot-deploy.rb", 75 | "lib/hubot-deploy/commit.rb", 76 | "lib/hubot-deploy/hubot-deploy.rb" 77 | ], 78 | "url": "http://github.com/atmos/hubot-deploy/commit/a47fd41f3aa4610ea527dcc1669dfdb9c15c5425", 79 | "author": { 80 | "name": "Corey Donohoe", 81 | "email": "atmos@atmos.com" 82 | }, 83 | "id": "a47fd41f3aa4610ea527dcc1669dfdb9c15c5425" 84 | } 85 | ] 86 | } 87 | -------------------------------------------------------------------------------- /test/models/deployments/instance_methods_test.coffee: -------------------------------------------------------------------------------- 1 | Path = require "path" 2 | 3 | srcDir = Path.join(__dirname, "..", "..", "..", "src") 4 | 5 | Version = require(Path.join(srcDir, "version")).Version 6 | Deployment = require(Path.join(srcDir, "github", "api")).Deployment 7 | 8 | describe "Deployment fixtures", () -> 9 | describe "#isValidApp()", () -> 10 | it "is invalid if the app can't be found", () -> 11 | deployment = new Deployment("hubot-reloaded", "master", "deploy", "production", "", "") 12 | assert.equal(deployment.isValidApp(), false) 13 | 14 | it "is valid if the app can be found", () -> 15 | deployment = new Deployment("hubot-deploy", "master", "deploy", "production", "", "") 16 | assert.equal(deployment.isValidApp(), true) 17 | 18 | describe "#isValidEnv()", () -> 19 | it "is invalid if the env can't be found", () -> 20 | deployment = new Deployment("hubot", "master", "deploy", "garage", "", "") 21 | assert.equal(deployment.isValidEnv(), false) 22 | 23 | it "is valid if the env can be found", () -> 24 | deployment = new Deployment("hubot", "master", "deploy", "production", "", "") 25 | assert.equal(deployment.isValidEnv(), true) 26 | 27 | describe "#requiredContexts", () -> 28 | it "works with required contexts", () -> 29 | deployment = new Deployment("hubot", "master", "deploy", "production", "", "") 30 | expectedContexts = ["ci/janky", "ci/travis-ci"] 31 | 32 | assert.deepEqual(expectedContexts, deployment.requiredContexts) 33 | 34 | describe "#isAllowedRoom()", () -> 35 | it "allows everything when there is no configuration", -> 36 | deployment = new Deployment("hubot", "master", "deploy", "production", "", "") 37 | assert.equal(deployment.isAllowedRoom('anything'), true) 38 | it "is allowed with room that is in configuration", -> 39 | deployment = new Deployment("restricted-app", "master", "deploy", "production", "", "") 40 | assert.equal(deployment.isAllowedRoom('ops'), true) 41 | it "is not allowed with room that is not in configuration", -> 42 | deployment = new Deployment("restricted-app", "master", "deploy", "production", "", "") 43 | assert.equal(deployment.isAllowedRoom('watercooler'), false) 44 | 45 | describe "#requestBody()", () -> 46 | it "shouldn't blow up", () -> 47 | deployment = new Deployment("hubot", "master", "deploy", "garage", "", "") 48 | deployment.requestBody() 49 | assert.equal(true, true) 50 | it "should have the right description", () -> 51 | deployment = new Deployment("hubot", "master", "deploy", "production", "", "") 52 | body = deployment.requestBody() 53 | assert.equal(body.description, "deploy on production from hubot-deploy-v#{Version}") 54 | 55 | 56 | -------------------------------------------------------------------------------- /test/scripts/tokens_test.coffee: -------------------------------------------------------------------------------- 1 | VCR = require "ys-vcr" 2 | Path = require "path" 3 | Robot = require "hubot/src/robot" 4 | TextMessage = require("hubot/src/message").TextMessage 5 | Verifiers = require(Path.join(__dirname, "..", "..", "src", "models", "verifiers")) 6 | 7 | describe "Setting tokens and such", () -> 8 | user = null 9 | robot = null 10 | adapter = null 11 | 12 | beforeEach (done) -> 13 | VCR.playback() 14 | process.env.HUBOT_DEPLOY_PRIVATE_MESSAGE_TOKEN_MANAGEMENT = "true" 15 | process.env.HUBOT_DEPLOY_FERNET_SECRETS or= "HSfTG4uWzw9whtlLEmNAzscHh96eHUFt3McvoWBXmHk=" 16 | robot = new Robot(null, "mock-adapter", true, "Hubot") 17 | 18 | robot.adapter.on "connected", () -> 19 | require("../../index")(robot) 20 | 21 | userInfo = 22 | name: "atmos", 23 | room: "#my-room" 24 | 25 | user = robot.brain.userForId "1", userInfo 26 | adapter = robot.adapter 27 | 28 | done() 29 | 30 | robot.run() 31 | 32 | afterEach () -> 33 | VCR.stop() 34 | robot.server.close() 35 | robot.shutdown() 36 | 37 | it "tells you when your provided GitHub token is invalid", (done) -> 38 | VCR.play "/user-invalid-auth" 39 | adapter.on "send", (envelope, strings) -> 40 | assert.match strings[0], /Your GitHub token is invalid/ 41 | done() 42 | adapter.receive(new TextMessage(user, "Hubot deploy-token:set:github 123456789")) 43 | 44 | it "tells you when your provided GitHub token is valid", (done) -> 45 | VCR.play "/user-valid" 46 | expectedResponse = /Your GitHub token is valid. I stored it for future use./ 47 | adapter.on "send", (envelope, strings) -> 48 | assert.match strings[0], expectedResponse 49 | assert robot.vault.forUser(user).get(Verifiers.VaultKey) 50 | assert.equal robot.vault.forUser(user).get(Verifiers.VaultKey), "123456789" 51 | done() 52 | adapter.receive(new TextMessage(user, "Hubot deploy-token:set:github 123456789")) 53 | 54 | it "tells you when your stored GitHub token is invalid", (done) -> 55 | VCR.play "/user-invalid-auth" 56 | robot.vault.forUser(user).set(Verifiers.VaultKey, "123456789") 57 | adapter.on "send", (envelope, strings) -> 58 | assert.match strings[0], /Your GitHub token is invalid, verify that it has \'repo\' scope./ 59 | done() 60 | adapter.receive(new TextMessage(user, "Hubot deploy-token:verify:github")) 61 | 62 | it "tells you when your stored GitHub token is valid", (done) -> 63 | VCR.play "/user-valid" 64 | robot.vault.forUser(user).set(Verifiers.VaultKey, "123456789") 65 | adapter.on "send", (envelope, strings) -> 66 | assert.match strings[0], /Your GitHub token is valid on api.github.com./ 67 | done() 68 | adapter.receive(new TextMessage(user, "Hubot deploy-token:verify:github")) 69 | -------------------------------------------------------------------------------- /docs/examples/slack.coffee: -------------------------------------------------------------------------------- 1 | # Description 2 | # Custom hubot-deploy scripts for slack 3 | # 4 | process.env.HUBOT_DEPLOY_EMIT_GITHUB_DEPLOYMENTS = "true" 5 | 6 | module.exports = (robot) -> 7 | # This is what happens with a '/deploy' request is accepted. 8 | # 9 | # msg - The hubot message that triggered the deployment. msg.reply and msg.send post back immediately 10 | # deployment - The deployment captured from a chat interaction. You can modify it before it's passed on to the GitHub API. 11 | robot.on "github_deployment", (msg, deployment) -> 12 | user = robot.brain.userForId deployment.user 13 | 14 | vault = robot.vault.forUser(user) 15 | githubDeployToken = vault.get "hubot-deploy-github-secret" 16 | if githubDeployToken? 17 | deployment.setUserToken(githubDeployToken) 18 | 19 | 20 | deployment.post (err, status, body, headers, responseMessage) -> 21 | msg.send responseMessage if responseMessage? 22 | 23 | # Reply with the most recent deployments that the api is aware of 24 | # 25 | # msg - The hubot message that triggered the deployment. msg.reply and msg.send post back immediately 26 | # deployment - The deployed app that matched up with the request. 27 | # deployments - The list of the most recent deployments from the GitHub API. 28 | # formatter - A basic formatter for the deployments that should work everywhere even though it looks gross. 29 | robot.on "hubot_deploy_recent_deployments", (msg, deployment, deployments, formatter) -> 30 | msg.send formatter.message() 31 | 32 | # Reply with the environments that hubot-deploy knows about for a specific application. 33 | # 34 | # msg - The hubot message that triggered the deployment. msg.reply and msg.send post back immediately 35 | # deployment - The deployed app that matched up with the request. 36 | # formatter - A basic formatter for the deployments that should work everywhere even though it looks gross. 37 | robot.on "hubot_deploy_available_environments", (msg, deployment) -> 38 | msg.send "#{deployment.name} can be deployed to #{deployment.environments.join(', ')}." 39 | 40 | # An incoming webhook from GitHub for a deployment. 41 | # 42 | # deployment - A Deployment from github_events.coffee 43 | robot.on "github_deployment_event", (deployment) -> 44 | robot.logger.info JSON.stringify(deployment) 45 | 46 | # An incoming webhook from GitHub for a deployment status. 47 | # 48 | # status - A DeploymentStatus from github_events.coffee 49 | robot.on "github_deployment_status_event", (status) -> 50 | if status.notify 51 | user = robot.brain.userForId status.notify.user 52 | status.actorName = user.name 53 | 54 | messageBody = status.toSimpleString().replace(/^hubot-deploy: /i, '') 55 | robot.logger.info messageBody 56 | if status?.notify?.room? 57 | robot.messageRoom status.notify.room, messageBody 58 | -------------------------------------------------------------------------------- /test/github/webhooks/pull_request_test.coffee: -------------------------------------------------------------------------------- 1 | Fs = require "fs" 2 | Path = require "path" 3 | 4 | srcDir = Path.join(__dirname, "..", "..", "..", "src") 5 | 6 | GitHubEvents = require(Path.join(srcDir, "github", "webhooks")) 7 | PullRequest = GitHubEvents.PullRequest 8 | 9 | describe "GitHubEvents.PullRequest fixtures", () -> 10 | pullRequestFor = (fixtureName) -> 11 | fixtureData = Path.join __dirname, "..", "..", "fixtures", "pull_requests", "pull_request_#{fixtureName}.json" 12 | fixturePayload = JSON.parse(Fs.readFileSync(fixtureData)) 13 | status = new PullRequest "uuid", fixturePayload 14 | 15 | describe "opened", () -> 16 | it "knows the state, number, and repo", () -> 17 | pullRequest = pullRequestFor("opened") 18 | assert.equal 32, pullRequest.number 19 | assert.equal "open", pullRequest.state 20 | assert.equal "hubot-deploy", pullRequest.name 21 | assert.equal "atmos/hubot-deploy", pullRequest.repoName 22 | 23 | describe "merged", () -> 24 | it "knows the state, number, and repo", () -> 25 | pullRequest = pullRequestFor("merged") 26 | assert.equal 32, pullRequest.number 27 | assert.equal "closed", pullRequest.state 28 | assert.equal "hubot-deploy", pullRequest.name 29 | assert.equal "atmos/hubot-deploy", pullRequest.repoName 30 | 31 | describe "closed", () -> 32 | it "knows the state, number, and repo", () -> 33 | pullRequest = pullRequestFor("closed") 34 | assert.equal 32, pullRequest.number 35 | assert.equal "closed", pullRequest.state 36 | assert.equal "hubot-deploy", pullRequest.name 37 | assert.equal "atmos/hubot-deploy", pullRequest.repoName 38 | 39 | describe "reopened", () -> 40 | it "knows the state, number, and repo", () -> 41 | pullRequest = pullRequestFor("reopened") 42 | assert.equal 32, pullRequest.number 43 | assert.equal "open", pullRequest.state 44 | assert.equal "hubot-deploy", pullRequest.name 45 | assert.equal "atmos/hubot-deploy", pullRequest.repoName 46 | 47 | describe "synchronize", () -> 48 | it "knows the state, number, and repo", () -> 49 | pullRequest = pullRequestFor("reopened") 50 | assert.equal 32, pullRequest.number 51 | assert.equal "open", pullRequest.state 52 | assert.equal "hubot-deploy", pullRequest.name 53 | assert.equal "atmos/hubot-deploy", pullRequest.repoName 54 | 55 | describe "toSimpleString", () -> 56 | it "works", () -> 57 | pullRequest = pullRequestFor("reopened") 58 | assert.equal 32, pullRequest.number 59 | assert.equal "open", pullRequest.state 60 | assert.equal "hubot-deploy", pullRequest.name 61 | assert.equal "atmos/hubot-deploy", pullRequest.repoName 62 | expectedOutput = "hubot-deploy: atmos reopened pull request #32: webhooks-events-generator " + 63 | "https://github.com/atmos/hubot-deploy/pull/32/files" 64 | assert.equal pullRequest.toSimpleString(), expectedOutput 65 | -------------------------------------------------------------------------------- /src/models/verifiers.coffee: -------------------------------------------------------------------------------- 1 | Path = require "path" 2 | Octonode = require "octonode" 3 | Address4 = require("ip-address").Address4 4 | GitHubApi = require(Path.join(__dirname, "..", "github", "api")).Api 5 | ScopedClient = require "scoped-http-client" 6 | ########################################################################### 7 | 8 | VaultKey = "hubot-deploy-github-secret" 9 | 10 | class ApiTokenVerifier 11 | constructor: (token) -> 12 | @token = token?.trim() 13 | 14 | @config = new GitHubApi(@token, null) 15 | 16 | hostname = @config.hostname 17 | path = @config.path() 18 | 19 | if path isnt "/" 20 | hostname += path 21 | 22 | @api = Octonode.client(@config.token, {hostname: hostname}) 23 | 24 | valid: (cb) -> 25 | @api.get "/user", (err, status, data, headers) -> 26 | scopes = headers?['x-oauth-scopes'] 27 | if scopes?.indexOf('repo') >= 0 28 | cb(true) 29 | else 30 | cb(false) 31 | 32 | class GitHubWebHookIpVerifier 33 | constructor: () -> 34 | gitHubSubnets = process.env.HUBOT_DEPLOY_GITHUB_SUBNETS || "192.30.252.0/22,185.199.108.0/22,140.82.112.0/20" 35 | @subnets = (new Address4(subnet.trim()) for subnet in gitHubSubnets.split(',')) 36 | 37 | ipIsValid: (ipAddress) -> 38 | address = new Address4("#{ipAddress}/24") 39 | for subnet in @subnets 40 | return true if address.isInSubnet(subnet) 41 | false 42 | 43 | class GitHubTokenVerifier 44 | constructor: (token) -> 45 | @token = token?.trim() 46 | 47 | valid: (cb) -> 48 | return cb(false) unless token? 49 | ScopedClient.create("https://api.github.com"). 50 | header("User-Agent", "hubot-deploy/0.13.1"). 51 | header("Authorization", "token #{@token}"). 52 | path("/user"). 53 | get() (err, res, body) -> 54 | scopes = res.headers?['x-oauth-scopes'] 55 | if err 56 | cb(false) 57 | else if res.statusCode isnt 200 58 | cb(false) 59 | else if scopes?.indexOf("repo") < 0 60 | cb(false) 61 | else 62 | user = JSON.parse(body) 63 | cb(user) 64 | 65 | class HerokuTokenVerifier 66 | constructor: (token) -> 67 | @token = token?.trim() 68 | 69 | valid: (cb) -> 70 | return cb(false) unless token? 71 | ScopedClient.create("https://api.heroku.com"). 72 | header("Accept", "application/vnd.heroku+json; version=3"). 73 | header("Authorization", "Bearer #{@token}"). 74 | path("/account"). 75 | get() (err, res, body) -> 76 | if err 77 | cb(false) 78 | else if res.statusCode isnt 200 79 | cb(false) 80 | else 81 | user = JSON.parse(body) 82 | cb(user) 83 | 84 | exports.VaultKey = VaultKey 85 | exports.ApiTokenVerifier = ApiTokenVerifier 86 | exports.GitHubTokenVerifier = GitHubTokenVerifier 87 | exports.HerokuTokenVerifier = HerokuTokenVerifier 88 | exports.GitHubWebHookIpVerifier = GitHubWebHookIpVerifier 89 | -------------------------------------------------------------------------------- /src/scripts/token.coffee: -------------------------------------------------------------------------------- 1 | # Description 2 | # Enable deployments from chat that correctly attribute you as the creator - https://github.com/atmos/hubot-deploy 3 | # 4 | # Commands: 5 | # hubot deploy-token:set:github - Sets your user's GitHub deployment token. Requires repo scope. 6 | # hubot deploy-token:reset:github - Resets your user's GitHub deployment token. 7 | # hubot deploy-token:verify:github - Verifies that your GitHub deployment token is valid. 8 | # 9 | supported_tasks = [ "#{DeployPrefix}-token" ] 10 | 11 | Path = require("path") 12 | Patterns = require(Path.join(__dirname, "..", "models", "patterns")) 13 | Deployment = require(Path.join(__dirname, "..", "github", "api")).Deployment 14 | DeployPrefix = Patterns.DeployPrefix 15 | DeployPattern = Patterns.DeployPattern 16 | DeploysPattern = Patterns.DeploysPattern 17 | 18 | Verifiers = require(Path.join(__dirname, "..", "models", "verifiers")) 19 | 20 | TokenForBrain = Verifiers.VaultKey 21 | ApiTokenVerifier = Verifiers.ApiTokenVerifier 22 | ########################################################################### 23 | module.exports = (robot) -> 24 | if process.env.HUBOT_DEPLOY_PRIVATE_MESSAGE_TOKEN_MANAGEMENT is "true" 25 | robot.respond ///#{DeployPrefix}-token:set:github\s+(.*)///i, id: "hubot-deploy.token.set", (msg) -> 26 | user = robot.brain.userForId msg.envelope.user.id 27 | token = msg.match[1] 28 | 29 | # Versions of hubot-deploy < 0.9.0 stored things unencrypted, encrypt them. 30 | delete(user.githubDeployToken) 31 | 32 | verifier = new ApiTokenVerifier(token) 33 | verifier.valid (result) -> 34 | if result 35 | robot.vault.forUser(user).set(TokenForBrain, verifier.token) 36 | msg.send "Your GitHub token is valid. I stored it for future use." 37 | else 38 | msg.send "Your GitHub token is invalid, verify that it has 'repo' scope." 39 | 40 | robot.respond ///#{DeployPrefix}-token:reset:github$///i, id: "hubot-deploy.token.reset", (msg) -> 41 | user = robot.brain.userForId msg.envelope.user.id 42 | robot.vault.forUser(user).unset(TokenForBrain) 43 | # Versions of hubot-deploy < 0.9.0 stored things unencrypted, encrypt them. 44 | delete(user.githubDeployToken) 45 | msg.reply "I nuked your GitHub token. I'll try to use my default token until you configure another." 46 | 47 | robot.respond ///#{DeployPrefix}-token:verify:github$///i, id: "hubot-deploy.token.verify", (msg) -> 48 | user = robot.brain.userForId msg.envelope.user.id 49 | # Versions of hubot-deploy < 0.9.0 stored things unencrypted, encrypt them. 50 | delete(user.githubDeployToken) 51 | token = robot.vault.forUser(user).get(TokenForBrain) 52 | verifier = new ApiTokenVerifier(token) 53 | verifier.valid (result) -> 54 | if result 55 | msg.send "Your GitHub token is valid on #{verifier.config.hostname}." 56 | else 57 | msg.send "Your GitHub token is invalid, verify that it has 'repo' scope." 58 | -------------------------------------------------------------------------------- /test/models/handler_test.coffee: -------------------------------------------------------------------------------- 1 | Fs = require "fs" 2 | VCR = require "ys-vcr" 3 | Path = require('path') 4 | Robot = require "hubot/src/robot" 5 | TextMessage = require("hubot/src/message").TextMessage 6 | GitHubEvents = require(Path.join(__dirname, "..", "..", "src", "github", "webhooks")) 7 | Deployment = GitHubEvents.Deployment 8 | 9 | Handler = require(Path.join(__dirname, "..", "..", "src", "models", "handler")) 10 | 11 | describe "Deployment Handlers", () -> 12 | user = null 13 | robot = null 14 | adapter = null 15 | deployment = null 16 | 17 | beforeEach (done) -> 18 | VCR.playback() 19 | process.env.HUBOT_DEPLOY_FERNET_SECRETS or= "HSfTG4uWzw9whtlLEmNAzscHh96eHUFt3McvoWBXmHk=" 20 | process.env.HUBOT_DEPLOY_EMIT_GITHUB_DEPLOYMENTS = true 21 | robot = new Robot(null, "mock-adapter", true, "hubot") 22 | 23 | robot.adapter.on "connected", () -> 24 | require("../../index")(robot) 25 | 26 | userInfo = 27 | name: "atmos", 28 | room: "#my-room" 29 | 30 | user = robot.brain.userForId "1", userInfo 31 | adapter = robot.adapter 32 | 33 | done() 34 | 35 | robot.run() 36 | 37 | afterEach () -> 38 | delete(process.env.HUBOT_DEPLOY_DEFAULT_ENVIRONMENT) 39 | VCR.stop() 40 | robot.server.close() 41 | robot.shutdown() 42 | 43 | deploymentFixtureFor = (fixtureName) -> 44 | fixtureData = Path.join __dirname, "..", "..", "test", "fixtures", "deployments", "#{fixtureName}.json" 45 | JSON.parse(Fs.readFileSync(fixtureData)) 46 | 47 | it "only responds to the currently running bot name", (done) -> 48 | fixturePayload = deploymentFixtureFor "production" 49 | fixturePayload.deployment.payload.robotName = "evilbot" 50 | deployment = new Deployment "uuid", fixturePayload 51 | 52 | handler = new Handler.Handler robot, deployment 53 | handler.run (err, handler) -> 54 | assert.equal err.message, "Received request for unintended robot evilbot." 55 | done() 56 | 57 | it "ignores deployments that have no notify attrs in their payload", (done) -> 58 | fixturePayload = deploymentFixtureFor "production" 59 | delete fixturePayload.deployment.payload.notify 60 | deployment = new Deployment "uuid", fixturePayload 61 | 62 | handler = new Handler.Handler robot, deployment 63 | handler.run (err, handler) -> 64 | assert.equal err.message, "Not deploying atmos/my-robot/heroku to production. Not chat initiated." 65 | done() 66 | 67 | it "dispatches to specific providers", (done) -> 68 | fixturePayload = deploymentFixtureFor "production" 69 | deployment = new Deployment "uuid", fixturePayload 70 | 71 | handler = new Handler.Handler robot, deployment 72 | handler.run (err, handler) -> 73 | throw err if err 74 | assert.equal "heroku", handler.provider 75 | assert.equal "heroku", handler.ref 76 | assert.equal "3c9f42c", handler.sha 77 | assert.equal "1875476", handler.number 78 | assert.equal "production", handler.environment 79 | assert.equal "atmos/my-robot", handler.repoName 80 | done() 81 | -------------------------------------------------------------------------------- /.coffeelint.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrow_spacing": { 3 | "level": "ignore" 4 | }, 5 | "braces_spacing": { 6 | "level": "ignore", 7 | "spaces": 0, 8 | "empty_object_spaces": 0 9 | }, 10 | "camel_case_classes": { 11 | "level": "error" 12 | }, 13 | "coffeescript_error": { 14 | "level": "error" 15 | }, 16 | "colon_assignment_spacing": { 17 | "level": "ignore", 18 | "spacing": { 19 | "left": 0, 20 | "right": 0 21 | } 22 | }, 23 | "cyclomatic_complexity": { 24 | "level": "ignore", 25 | "value": 10 26 | }, 27 | "duplicate_key": { 28 | "level": "error" 29 | }, 30 | "empty_constructor_needs_parens": { 31 | "level": "ignore" 32 | }, 33 | "ensure_comprehensions": { 34 | "level": "warn" 35 | }, 36 | "eol_last": { 37 | "level": "ignore" 38 | }, 39 | "indentation": { 40 | "value": 2, 41 | "level": "error" 42 | }, 43 | "line_endings": { 44 | "level": "ignore", 45 | "value": "unix" 46 | }, 47 | "max_line_length": { 48 | "value": 120, 49 | "level": "error", 50 | "limitComments": true 51 | }, 52 | "missing_fat_arrows": { 53 | "level": "ignore", 54 | "is_strict": false 55 | }, 56 | "newlines_after_classes": { 57 | "value": 3, 58 | "level": "ignore" 59 | }, 60 | "no_backticks": { 61 | "level": "error" 62 | }, 63 | "no_debugger": { 64 | "level": "warn", 65 | "console": false 66 | }, 67 | "no_empty_functions": { 68 | "level": "ignore" 69 | }, 70 | "no_empty_param_list": { 71 | "level": "ignore" 72 | }, 73 | "no_implicit_braces": { 74 | "level": "ignore", 75 | "strict": true 76 | }, 77 | "no_implicit_parens": { 78 | "level": "ignore", 79 | "strict": true 80 | }, 81 | "no_interpolation_in_single_quotes": { 82 | "level": "ignore" 83 | }, 84 | "no_nested_string_interpolation": { 85 | "level": "warn" 86 | }, 87 | "no_plusplus": { 88 | "level": "ignore" 89 | }, 90 | "no_private_function_fat_arrows": { 91 | "level": "warn" 92 | }, 93 | "no_stand_alone_at": { 94 | "level": "ignore" 95 | }, 96 | "no_tabs": { 97 | "level": "error" 98 | }, 99 | "no_this": { 100 | "level": "ignore" 101 | }, 102 | "no_throwing_strings": { 103 | "level": "error" 104 | }, 105 | "no_trailing_semicolons": { 106 | "level": "error" 107 | }, 108 | "no_trailing_whitespace": { 109 | "level": "error", 110 | "allowed_in_comments": false, 111 | "allowed_in_empty_lines": true 112 | }, 113 | "no_unnecessary_double_quotes": { 114 | "level": "ignore" 115 | }, 116 | "no_unnecessary_fat_arrows": { 117 | "level": "warn" 118 | }, 119 | "non_empty_constructor_needs_parens": { 120 | "level": "ignore" 121 | }, 122 | "prefer_english_operator": { 123 | "level": "ignore", 124 | "doubleNotLevel": "ignore" 125 | }, 126 | "space_operators": { 127 | "level": "ignore" 128 | }, 129 | "spacing_after_comma": { 130 | "level": "ignore" 131 | }, 132 | "transform_messes_up_line_numbers": { 133 | "level": "warn" 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /docs/examples/hipchat.coffee: -------------------------------------------------------------------------------- 1 | # Description 2 | # Custom hubot-deploy scripts for hipchat 3 | # 4 | process.env.HUBOT_DEPLOY_EMIT_GITHUB_DEPLOYMENTS = "true" 5 | 6 | module.exports = (robot) -> 7 | # This is what happens with a '/deploy' request is accepted. 8 | # 9 | # msg - The hubot message that triggered the deployment. msg.reply and msg.send post back immediately 10 | # deployment - The deployment captured from a chat interaction. You can modify it before it's passed on to the GitHub API. 11 | robot.on "github_deployment", (msg, deployment) -> 12 | # Handle the difference between userIds and roomIds in hipchat 13 | user = robot.brain.userForId deployment.user 14 | unless user.id?.match(/\d+/) 15 | user = robot.brain.userByNameOrMention(user.id) 16 | deployment.user = user.id if user.id? 17 | 18 | vault = robot.vault.forUser(user) 19 | githubDeployToken = vault.get "hubot-deploy-github-secret" 20 | if githubDeployToken? 21 | deployment.setUserToken(githubDeployToken) 22 | 23 | if deployment.application.provider in [ "heroku", "capistrano" ] 24 | deployment.post (err, status, body, headers, responseMessage) -> 25 | msg.send responseMessage if responseMessage? 26 | else 27 | msg.send "Sorry, I can't deploy #{deployment.name}, the provider is unsupported" 28 | 29 | # Reply with the most recent deployments that the api is aware of 30 | # 31 | # msg - The hubot message that triggered the deployment. msg.reply and msg.send post back immediately 32 | # deployment - The deployed app that matched up with the request. 33 | # deployments - The list of the most recent deployments from the GitHub API. 34 | # formatter - A basic formatter for the deployments that should work everywhere even though it looks gross. 35 | robot.on "hubot_deploy_recent_deployments", (msg, deployment, deployments, formatter) -> 36 | msg.send formatter.message() 37 | 38 | # Reply with the environments that hubot-deploy knows about for a specific application. 39 | # 40 | # msg - The hubot message that triggered the deployment. msg.reply and msg.send post back immediately 41 | # deployment - The deployed app that matched up with the request. 42 | # formatter - A basic formatter for the deployments that should work everywhere even though it looks gross. 43 | robot.on "hubot_deploy_available_environments", (msg, deployment) -> 44 | msg.send "#{deployment.name} can be deployed to #{deployment.environments.join(', ')}." 45 | 46 | # An incoming webhook from GitHub for a deployment. 47 | # 48 | # deployment - A Deployment from github_events.coffee 49 | robot.on "github_deployment_event", (deployment) -> 50 | robot.logger.info JSON.stringify(deployment) 51 | 52 | # An incoming webhook from GitHub for a deployment status. 53 | # 54 | # status - A DeploymentStatus from github_events.coffee 55 | robot.on "github_deployment_status_event", (status) -> 56 | if status.notify 57 | user = robot.brain.userForId status.notify.user 58 | status.actorName = user.name 59 | 60 | messageBody = status.toSimpleString().replace(/^hubot-deploy: /i, '') 61 | robot.logger.info messageBody 62 | if status?.notify?.room? 63 | robot.messageRoom status.notify.room, messageBody 64 | -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | ## Configuration 2 | 3 | In order to create deployments on GitHub you need to configure a few things. Fallback configurations are configured by environmental variables. 4 | 5 | | Common Attributes | | 6 | |-------------------------|-------------------------------------------------| 7 | | HUBOT_GITHUB_API | A String of the full URL to the GitHub API. Default: "https://api.github.com" | 8 | | HUBOT_GITHUB_TOKEN | A [personal oauth token][1] with repo_deployment scope. This is normally a bot account. | 9 | | HUBOT_DEPLOY_PREFIX | The thing to prefix your deployment commands with. Defaults to 'deploy' | 10 | | HUBOT_DEPLOY_FERNET_SECRETS | The key used for encrypting your tokens in the hubot's brain. A comma delimited set of different key tokens. To create one run `dd if=/dev/urandom bs=32 count=1 2>/dev/null | openssl base64` on a UNIX system. | 11 | | HUBOT_DEPLOY_EMIT_GITHUB_DEPLOYMENTS | If set to true a `github_deployment` event emit emitted instead of posting directly to the GitHub API. This allows for customization, check out the examples. | 12 | | HUBOT_DEPLOY_DEFAULT_ENVIRONMENT | Allow for specifying which environment should be the default when it is omitted from the deployment request in chat. | 13 | | HUBOT_DEPLOY_GITHUB_SUBNETS | Allow for specifying the subnets for your GitHub install, useful for GitHub Enterprise. Defaults to github.com's IP range. | 14 | | HUBOT_DEPLOY_PRIVATE_MESSAGE_TOKEN_MANAGEMENT | Allow for messaging tokens to hubot in chat. This is going away. | 15 | | HUBOT_DEPLOY_WEBHOOK_SECRET | The shared webhook secret to check payload signatures from GitHub. | 16 | | HUBOT_DEPLOY_ENCRYPT_PAYLOAD | Encrypt the entire deployment payload in the GitHub API. | 17 | | HUBOT_DEPLOY_WEBHOOK_PREFIX | The URL prefix to be used for receiving webhooks. Default: "/hubot-deploy" 18 | 19 | ### Robot Users 20 | 21 | If you already have a user on GitHub that is essentially a bot account you can create a [personal OAuth token][1] for that user with the `user` and `repo` scopes. Unfortunately GitHub won't be able to differentiate between different users deploying, they'll all be created in the API as the bot user. 22 | 23 | ### User Tokens 24 | 25 | The hubot-deploy script provides a way to have user specific tokens for interacting with the API. You need to be using a chat service that supports private messages like [SlackHQ][2] or [Hipchat][3]. 26 | 27 | To configure your own token, make a [personal OAuth token][1] with both `user`, `repo` scopes. Then provide it to hubot via private message. 28 | 29 | deploy-token:set:github 30 | 31 | Hubot will respond and tell you whether the token is sufficient or not. If your token is good future deployments will be properly attributed to your user in the API. 32 | 33 | If things are being weird you can verify your token. 34 | 35 | deploy-token:verify:github 36 | 37 | If you want to go back having the highlander token create your deployments you can reset things like. 38 | 39 | deploy-token:reset:github 40 | 41 | Hubot will respond and tell you that your token has been forgotten and removed from the robot's brain. 42 | 43 | [1]: https://github.com/settings/tokens 44 | [2]: https://slack.com/is 45 | [3]: https://www.hipchat.com 46 | -------------------------------------------------------------------------------- /test/github/webhooks/deployment_test.coffee: -------------------------------------------------------------------------------- 1 | Fs = require "fs" 2 | Path = require "path" 3 | 4 | srcDir = Path.join(__dirname, "..", "..", "..", "src") 5 | GitHubEvents = require(Path.join(srcDir, "github", "webhooks")) 6 | Deployment = GitHubEvents.Deployment 7 | DeploymentStatus = GitHubEvents.DeploymentStatus 8 | 9 | describe "GitHubEvents.DeploymentStatus fixtures", () -> 10 | deploymentStatusFor = (fixtureName) -> 11 | fixtureData = Path.join __dirname, "..", "..", "fixtures", "deployment_statuses", "#{fixtureName}.json" 12 | fixturePayload = JSON.parse(Fs.readFileSync(fixtureData)) 13 | status = new DeploymentStatus "uuid", fixturePayload 14 | 15 | describe "pending", () -> 16 | it "knows the statue and repo", () -> 17 | status = deploymentStatusFor "pending" 18 | assert.equal status.state, "pending" 19 | assert.equal status.repoName, "atmos/my-robot" 20 | assert.equal status.toSimpleString(), "hubot-deploy: atmos\'s deployment #123456 of my-robot/break-up-notifiers to production is running. https://gist.github.com/fa77d9fb1fe41c3bb3a3ffb2c" 21 | 22 | describe "failure", () -> 23 | it "knows the statue and repo", () -> 24 | status = deploymentStatusFor "failure" 25 | assert.equal status.state, "failure" 26 | assert.equal status.repoName, "atmos/my-robot" 27 | assert.equal status.toSimpleString(), "hubot-deploy: atmos\'s deployment #123456 of my-robot/break-up-notifiers to production failed. https://gist.github.com/fa77d9fb1fe41c3bb3a3ffb2c" 28 | 29 | describe "success", () -> 30 | it "knows the statue and repo", () -> 31 | status = deploymentStatusFor "success" 32 | assert.equal status.state, "success" 33 | assert.equal status.repoName, "atmos/my-robot" 34 | assert.equal status.toSimpleString(), "hubot-deploy: atmos\'s deployment #11627 of my-robot/break-up-notifiers to production was successful. https://gist.github.com/fa77d9fb1fe41c3bb3a3ffb2c" 35 | 36 | describe "GitHubEvents.Deployment fixtures", () -> 37 | deploymentFor = (fixtureName) -> 38 | fixtureData = Path.join __dirname, "..", "..", "fixtures", "deployments", "#{fixtureName}.json" 39 | fixturePayload = JSON.parse(Fs.readFileSync(fixtureData)) 40 | new Deployment "uuid", fixturePayload 41 | 42 | describe "production", () -> 43 | it "works", () -> 44 | deployment = deploymentFor "production" 45 | assert.equal deployment.number, 1875476 46 | assert.equal deployment.repoName, "atmos/my-robot" 47 | assert.equal deployment.ref, "heroku" 48 | assert.equal deployment.sha, "3c9f42c" 49 | assert.equal deployment.name, "my-robot" 50 | assert.equal deployment.environment, "production" 51 | assert.isDefined deployment.notify 52 | assert.isNotNull deployment.notify 53 | assert.equal deployment.toSimpleString(), "hubot-deploy: atmos\'s deployment #1875476 of my-robot/heroku to production requested." 54 | 55 | describe "staging", () -> 56 | it "works", () -> 57 | deployment = deploymentFor "staging" 58 | assert.equal deployment.number, 1875476 59 | assert.equal deployment.name, "heaven" 60 | assert.equal deployment.repoName, "atmos/heaven" 61 | assert.equal deployment.ref, "heroku" 62 | assert.equal deployment.sha, "3c9f42c" 63 | assert.equal deployment.environment, "staging" 64 | assert.isDefined deployment.notify 65 | assert.isNotNull deployment.notify 66 | assert.equal deployment.toSimpleString(), "hubot-deploy: atmos\'s deployment #1875476 of heaven/heroku to staging requested." 67 | -------------------------------------------------------------------------------- /test/models/deployments/creating_deployments_test.coffee: -------------------------------------------------------------------------------- 1 | VCR = require "ys-vcr" 2 | Path = require "path" 3 | 4 | srcDir = Path.join(__dirname, "..", "..", "..", "src") 5 | 6 | Version = require(Path.join(srcDir, "version")).Version 7 | Deployment = require(Path.join(srcDir, "github", "api")).Deployment 8 | 9 | describe "Deployment#rawPost", () -> 10 | beforeEach () -> 11 | VCR.playback() 12 | afterEach () -> 13 | VCR.stop() 14 | 15 | it "does not create a deployment due to bad authentication", (done) -> 16 | VCR.play '/repos-atmos-hubot-deploy-deployment-production-create-bad-auth' 17 | deployment = new Deployment("hubot-deploy", "master", "deploy", "production", "", "") 18 | deployment.rawPost (err, status, body, headers) -> 19 | unless err 20 | throw new Error("Should've thrown bad auth") 21 | 22 | assert.equal "Bad credentials", err.message 23 | assert.equal 401, err.statusCode 24 | done() 25 | 26 | it "does not create a deployment due to missing required commit statuses", (done) -> 27 | VCR.play '/repos-atmos-hubot-deploy-deployment-production-create-required-status-missing' 28 | deployment = new Deployment("hubot-deploy", "master", "deploy", "production", "", "") 29 | deployment.rawPost (err, status, body, headers) -> 30 | throw err if err 31 | assert.equal 409, status 32 | assert.equal "Conflict: Commit status checks failed for master", body.message 33 | done() 34 | 35 | it "does not create a deployment due to failing required commit statuses", (done) -> 36 | VCR.play '/repos-atmos-hubot-deploy-deployment-production-create-required-status-failing' 37 | deployment = new Deployment("hubot-deploy", "master", "deploy", "production", "", "") 38 | deployment.rawPost (err, status, body, headers) -> 39 | throw err if err 40 | assert.equal 409, status 41 | assert.equal "Conflict: Commit status checks failed for master", body.message 42 | assert.equal "continuous-integration/travis-ci/push", body.errors[0].contexts[0].context 43 | assert.equal "code-climate", body.errors[0].contexts[1].context 44 | done() 45 | 46 | it "sometimes can't auto-merge when the requested ref is behind the default branch", (done) -> 47 | VCR.play '/repos-atmos-hubot-deploy-deployment-production-create-auto-merged-failed' 48 | deployment = new Deployment("hubot-deploy", "topic", "deploy", "production", "", "") 49 | deployment.rawPost (err, status, body, headers) -> 50 | throw err if err 51 | assert.equal 409, status 52 | assert.equal "Conflict merging master into topic.", body.message 53 | done() 54 | 55 | it "successfully auto-merges when the requested ref is behind the default branch", (done) -> 56 | VCR.play '/repos-atmos-hubot-deploy-deployment-production-create-auto-merged' 57 | deployment = new Deployment("hubot-deploy", "topic", "deploy", "production", "", "") 58 | deployment.rawPost (err, status, body, headers) -> 59 | throw err if err 60 | assert.equal 202, status 61 | assert.equal "Auto-merged master into topic on deployment.", body.message 62 | done() 63 | 64 | it "successfully created deployment", (done) -> 65 | VCR.play '/repos-atmos-hubot-deploy-deployment-production-create-success' 66 | deployment = new Deployment("hubot-deploy", "master", "deploy", "production", "", "") 67 | deployment.rawPost (err, status, body, headers) -> 68 | throw err if err 69 | assert.equal 201, status 70 | assert.equal "deploy", body.deployment.task 71 | assert.equal "production", body.deployment.environment 72 | done() 73 | -------------------------------------------------------------------------------- /docs/config-file.md: -------------------------------------------------------------------------------- 1 | ## Configuration File 2 | 3 | `hubot-deploy` looks for an `apps.json` file in the root of your deployed hubot to map names to specific repos that should be deployed. Here's what the format looks like. 4 | 5 | It's a javascript object where application aliases are used as the names(keys). This allows you to easily reference projects even if the repository name is long, hard to type, or easy to forget. The object itself tells GitHub enough information for deployment systems to handle the who(name), what(repository), where(environment), and how(provider). 6 | 7 | | Common Attributes | | 8 | |-------------------------|-------------------------------------------------| 9 | | provider | Either 'heroku' or 'capistrano'. **Required** | 10 | | repository | The name with owner path to a repo on github. e.g. "atmos/heaven". **Required** | 11 | | environments | An array of environments that you can deploy to. Default: "production" | 12 | | required_contexts | An array of commit status context names to verify against.| 13 | | github_api | A String with the full URL to the API. Useful for enterprise installs. Default: "https://api.github.com" | 14 | | github_token | A token for creating deployments in the GitHub API specific to the specific repository.| 15 | | allowed_rooms | An array of room id's for restricting the deployments to be started only in those rooms.| 16 | 17 | Any extra parameters will be passed along to GitHub in the `payload` field. This should allow for a decent amount of customization. 18 | 19 | ```JSON 20 | { 21 | "hubot": { 22 | "provider": "heroku", 23 | "auto_merge": false, 24 | "repository": "MyOrg/my-org-hubot", 25 | "environments": ["production"], 26 | 27 | "heroku_production_name": "my-orgs-hubot" 28 | }, 29 | 30 | "dotcom": { 31 | "provider": "heroku", 32 | "repository": "MyOrg/www", 33 | "environments": ["production","staging"], 34 | "required_contexts": ["ci/janky", "security/brakeman"], 35 | 36 | "heroku_staging_name": "my-org-www-staging", 37 | "heroku_production_name": "my-org-www" 38 | } 39 | } 40 | ``` 41 | 42 | ## Providers 43 | 44 | ### Heroku 45 | 46 | There's another plugin for hubot called [hubot-deploy-heroku](https://github.com/atmos/hubot-deploy-heroku). 47 | 48 | 49 | ## Deprecated GitHub Services 50 | 51 | GitHub has service integrations for Heroku and OpsWorks. This allows you to get deployment support without having to setup an service of your own. 52 | 53 | ### Heroku 54 | 55 | GitHub provides a repo integration for deploying to [heroku](https:///heroku.com). [Docs](http://www.atmos.org/github-services/heroku/). 56 | 57 | | Provider Attributes | | 58 | |-------------------------|-------------------------------------------------| 59 | | heroku__name | The name of the heroku app to push to. Multiple environments are available with things like 'heroku_production_name' and 'heroku_staging_name'. | 60 | 61 | ### OpsWorks 62 | 63 | There's also a repo integration for deploying to [Aws OpsWorks](http://aws.amazon.com/opsworks/). OpsWorks supports multi-environment deployments by specifying different stack and app ids. [Docs](http://www.atmos.org/github-services/aws-opsworks/). 64 | 65 | ## Supported Heaven Services 66 | 67 | There is also [heaven](https://github.com/atmos/heaven) which exists if you need to write your own custom stuff or don't want to share your keys with GitHub. This requires you to stand up your own service, but deploys to heroku relatively easily. 68 | -------------------------------------------------------------------------------- /src/github/webhooks/push.coffee: -------------------------------------------------------------------------------- 1 | class Push 2 | constructor: (@id, @payload) -> 3 | @ref = @payload.ref 4 | @actor = @payload.pusher.name 5 | @count = @payload.commits.length 6 | @isTag = @ref.match(/^refs\/tags\//) 7 | 8 | @commits = @payload.commits 9 | 10 | @refName = @ref.replace(/^refs\/(heads|tags)\//, "") 11 | 12 | @afterSha = @payload.after[0..7] 13 | @beforeSha = @payload.before[0..7] 14 | @repoName = @payload.repository.name 15 | @ownerName = @payload.repository.owner.name 16 | 17 | @baseRef = @payload.base_ref 18 | @baseRefName = @payload.base_ref_name 19 | 20 | @forced = @payload.forced or false 21 | @deleted = @payload.deleted or @payload.after.match(/0{40}/) 22 | @created = @payload.created or @payload.before.match(/0{40}/) 23 | 24 | @repoUrl = @payload.repository.url 25 | @branchUrl = "#{@repoUrl}/commits/#{@refName}" 26 | @compareUrl = @payload.compare 27 | @afterShaUrl = "#{@repoUrl}/commit/#{@afterSha}" 28 | @beforeShaUrl = "#{@repoUrl}/commit/#{@beforeSha}" 29 | @nameWithOwner = "#{@ownerName}/#{@repoName}" 30 | 31 | @actorLink = "#{@actor}" 32 | 33 | @distinctCommits = (commit for commit in @commits when commit.distinct and commit.message.length > 0) 34 | 35 | @firstMessage = @formatCommitMessage(@commits[0]) 36 | 37 | if @count > 1 38 | @commitMessage = "#{@count} commits" 39 | else 40 | @commitMessage = "a commit" 41 | 42 | formatCommitMessage: (commit) -> 43 | short = commit.message.split("\n", 2)[0] 44 | "- #{short} - #{commit.author.name} - (#{@afterSha})" 45 | 46 | summaryUrl: -> 47 | if @created 48 | if @distinctCommits.length is 0 49 | @branchUrl 50 | else 51 | @compareUrl 52 | else if @deleted 53 | @beforeShaUrl 54 | else if @forced 55 | @branchUrl 56 | else if @commits.length is 1 57 | @commits[0].url 58 | else 59 | @compareUrl 60 | 61 | summaryMessage: -> 62 | message = [] 63 | message.push("[#{@repoName}] #{@actor}") 64 | 65 | if @created 66 | if @isTag 67 | message.push("tagged #{@refName} at") 68 | message.push(if @baseRef? then @baseRefName else @afterSha) 69 | else 70 | message.push("created #{@refName}") 71 | 72 | if @baseRef 73 | message.push("from #{@baseRefName}") 74 | else if @distinctCommits.empty? 75 | message.push("at #{@afterSha}") 76 | 77 | if @distinctCommits.length > 0 78 | num = @distinctCommits.length 79 | message << "(+#{@commitMessage})" 80 | 81 | else if @deleted 82 | message.push("deleted #{@refName} at #{@beforeSha}") 83 | 84 | else if @forced 85 | message.push("force-pushed #{@refName} from #{@beforeSha} to #{@afterSha}") 86 | 87 | else if @commits.length > 0 and @distinctCommits.length is 0 88 | if @baseRef 89 | message.push("merged #{baseRefName} into #{@refName}") 90 | else 91 | message.push("fast-forwarded #{@refName} from #{@beforeSha} to #{@afterSha}") 92 | 93 | else if @distinctCommits.length > 0 94 | num = @distinctCommits.length 95 | message.push("pushed #{num} new commit#{if num > 1 then 's' else ''} to #{@refName}") 96 | else 97 | message.push("pushed nothing") 98 | 99 | message.join(" ") 100 | 101 | toSimpleString: -> 102 | "hubot-deploy: #{@actor} pushed #{@commitMessage}" 103 | 104 | exports.Push = Push 105 | -------------------------------------------------------------------------------- /test/github/webhooks/deployment_status_test.coffee: -------------------------------------------------------------------------------- 1 | Fs = require "fs" 2 | Path = require "path" 3 | 4 | srcDir = Path.join(__dirname, "..", "..", "..", "src") 5 | 6 | GitHubEvents = require(Path.join(srcDir, "github", "webhooks")) 7 | Deployment = GitHubEvents.Deployment 8 | DeploymentStatus = GitHubEvents.DeploymentStatus 9 | 10 | describe "GitHubEvents.DeploymentStatus fixtures", () -> 11 | deploymentStatusFor = (fixtureName) -> 12 | fixtureData = Path.join __dirname, "..", "..", "fixtures", "deployment_statuses", "#{fixtureName}.json" 13 | fixturePayload = JSON.parse(Fs.readFileSync(fixtureData)) 14 | status = new DeploymentStatus "uuid", fixturePayload 15 | 16 | describe "pending", () -> 17 | it "knows the state, description and repo", () -> 18 | status = deploymentStatusFor "pending" 19 | assert.equal status.state, "pending" 20 | assert.equal status.repoName, "atmos/my-robot" 21 | assert.equal status.description, "Deploying from Heaven v0.5.5" 22 | assert.equal status.toSimpleString(), "hubot-deploy: atmos\'s deployment #123456 of my-robot/break-up-notifiers to production is running. https://gist.github.com/fa77d9fb1fe41c3bb3a3ffb2c" 23 | 24 | describe "failure", () -> 25 | it "knows the state, description and repo", () -> 26 | status = deploymentStatusFor "failure" 27 | assert.equal status.state, "failure" 28 | assert.equal status.repoName, "atmos/my-robot" 29 | assert.equal status.description, "Deploying from Heaven v0.5.5" 30 | assert.equal status.toSimpleString(), "hubot-deploy: atmos\'s deployment #123456 of my-robot/break-up-notifiers to production failed. https://gist.github.com/fa77d9fb1fe41c3bb3a3ffb2c" 31 | 32 | describe "success", () -> 33 | it "knows the state, description and repo", () -> 34 | status = deploymentStatusFor "success" 35 | assert.equal status.state, "success" 36 | assert.equal status.repoName, "atmos/my-robot" 37 | assert.equal status.description, "Deploying from Heaven v0.5.5" 38 | assert.equal status.toSimpleString(), "hubot-deploy: atmos\'s deployment #11627 of my-robot/break-up-notifiers to production was successful. https://gist.github.com/fa77d9fb1fe41c3bb3a3ffb2c" 39 | 40 | describe "GitHubEvents.Deployment fixtures", () -> 41 | deploymentFor = (fixtureName) -> 42 | fixtureData = Path.join __dirname, "..", "..", "fixtures", "deployments", "#{fixtureName}.json" 43 | fixturePayload = JSON.parse(Fs.readFileSync(fixtureData)) 44 | new Deployment "uuid", fixturePayload 45 | 46 | describe "production", () -> 47 | it "works", () -> 48 | deployment = deploymentFor "production" 49 | assert.equal deployment.number, 1875476 50 | assert.equal deployment.repoName, "atmos/my-robot" 51 | assert.equal deployment.ref, "heroku" 52 | assert.equal deployment.sha, "3c9f42c" 53 | assert.equal deployment.name, "my-robot" 54 | assert.equal deployment.environment, "production" 55 | assert.isDefined deployment.notify 56 | assert.isNotNull deployment.notify 57 | assert.equal deployment.toSimpleString(), "hubot-deploy: atmos\'s deployment #1875476 of my-robot/heroku to production requested." 58 | 59 | describe "staging", () -> 60 | it "works", () -> 61 | deployment = deploymentFor "staging" 62 | assert.equal deployment.number, 1875476 63 | assert.equal deployment.name, "heaven" 64 | assert.equal deployment.repoName, "atmos/heaven" 65 | assert.equal deployment.ref, "heroku" 66 | assert.equal deployment.sha, "3c9f42c" 67 | assert.equal deployment.environment, "staging" 68 | assert.isDefined deployment.notify 69 | assert.isNotNull deployment.notify 70 | assert.equal deployment.toSimpleString(), "hubot-deploy: atmos\'s deployment #1875476 of heaven/heroku to staging requested." 71 | -------------------------------------------------------------------------------- /test/models/deployments/creating_deployment_messages_test.coffee: -------------------------------------------------------------------------------- 1 | VCR = require "ys-vcr" 2 | Path = require "path" 3 | 4 | srcDir = Path.join(__dirname, "..", "..", "..", "src") 5 | 6 | Version = require(Path.join(srcDir, "version")).Version 7 | Deployment = require(Path.join(srcDir, "github", "api")).Deployment 8 | 9 | describe "Deployment#post", () -> 10 | beforeEach () -> 11 | VCR.playback() 12 | afterEach () -> 13 | VCR.stop() 14 | 15 | it "does not create a deployment due to bad authentication", (done) -> 16 | VCR.play '/repos-atmos-hubot-deploy-deployment-production-create-bad-auth' 17 | deployment = new Deployment("hubot-deploy", "master", "deploy", "production", "", "") 18 | deployment.post (err, status, body, headers, message) -> 19 | unless err 20 | throw new Error("Should've thrown bad auth") 21 | 22 | assert.equal "Bad credentials", err.message 23 | assert.equal 401, err.statusCode 24 | assert.equal "Bad credentials", message 25 | done() 26 | 27 | it "does not create a deployment due to missing required commit statuses", (done) -> 28 | VCR.play '/repos-atmos-hubot-deploy-deployment-production-create-required-status-missing' 29 | deployment = new Deployment("hubot-deploy", "master", "deploy", "production", "", "") 30 | deployment.post (err, status, body, headers, message) -> 31 | throw err if err 32 | assert.equal 409, status 33 | assert.equal "Conflict: Commit status checks failed for master", body.message 34 | assert.equal "Unmet required commit status contexts for hubot-deploy: continuous-integration/travis-ci/push failed.", message 35 | done() 36 | 37 | it "does not create a deployment due to failing required commit statuses", (done) -> 38 | VCR.play '/repos-atmos-hubot-deploy-deployment-production-create-required-status-failing' 39 | deployment = new Deployment("hubot-deploy", "master", "deploy", "production", "", "") 40 | deployment.post (err, status, body, headers, message) -> 41 | throw err if err 42 | assert.equal 409, status 43 | assert.equal "Conflict: Commit status checks failed for master", body.message 44 | assert.equal "continuous-integration/travis-ci/push", body.errors[0].contexts[0].context 45 | assert.equal "code-climate", body.errors[0].contexts[1].context 46 | assert.equal "Unmet required commit status contexts for hubot-deploy: continuous-integration/travis-ci/push,code-climate failed.", message 47 | done() 48 | 49 | it "sometimes can't auto-merge when the requested ref is behind the default branch", (done) -> 50 | VCR.play '/repos-atmos-hubot-deploy-deployment-production-create-auto-merged-failed' 51 | deployment = new Deployment("hubot-deploy", "topic", "deploy", "production", "", "") 52 | deployment.post (err, status, body, headers, message) -> 53 | throw err if err 54 | assert.equal 409, status 55 | assert.equal "Conflict merging master into topic.", body.message 56 | assert.equal "Conflict merging master into topic.", message 57 | done() 58 | 59 | it "successfully auto-merges when the requested ref is behind the default branch", (done) -> 60 | VCR.play '/repos-atmos-hubot-deploy-deployment-production-create-auto-merged' 61 | deployment = new Deployment("hubot-deploy", "topic", "deploy", "production", "", "") 62 | deployment.post (err, status, body, headers, message) -> 63 | throw err if err 64 | assert.equal 202, status 65 | assert.equal "Auto-merged master into topic on deployment.", body.message 66 | assert.equal "Auto-merged master into topic on deployment.", message 67 | done() 68 | 69 | it "successfully created deployment", (done) -> 70 | VCR.play '/repos-atmos-hubot-deploy-deployment-production-create-success' 71 | deployment = new Deployment("hubot-deploy", "master", "deploy", "production", "", "") 72 | deployment.post (err, status, body, headers, message) -> 73 | throw err if err 74 | assert.equal 201, status 75 | assert.equal "deploy", body.deployment.task 76 | assert.equal "production", body.deployment.environment 77 | assert.equal undefined, message 78 | done() 79 | -------------------------------------------------------------------------------- /docs/chatops.md: -------------------------------------------------------------------------------- 1 | ## Chatops 2 | 3 | There are quite a few variants of this, but here are the basics. 4 | 5 | You can always check the version that you're running against. 6 | 7 | $ hubot deploy:version 8 | hubot-deploy v0.6.43/hubot v2.7.4/node v0.10.26 9 | 10 | You can also trigger a variety of deployments with custom payloads. 11 | 12 | $ hubot deploy hubot 13 | ... Deploys the master branch of hubot to the default environment 14 | 15 | If you already have `/deploy` style syntax you can override the deploy command prefix with the `HUBOT_DEPLOY_PREFIX` environmental variable. 16 | 17 | ```JSON 18 | { 19 | "url": "https://api.github.com/repos/MyOrg/my-org-hubot/deployments/1077", 20 | "id": 1077, 21 | "sha": "cfbc1c744e106c2aa869fae6452ed249f12d8713", 22 | "payload": { 23 | "task": "deploy", 24 | "hosts": "", 25 | "branch": "master", 26 | "notify": { 27 | "room": "danger", 28 | "user": "atmos", 29 | "adapter": "unknown" 30 | }, 31 | "environment": "production", 32 | "config": { 33 | "provider": "heroku", 34 | "repository": "MyOrg/my-org-hubot", 35 | "environments": [ 36 | "production" 37 | ], 38 | "heroku_name": "my-org-hubot", 39 | } 40 | }, 41 | "description": "Deploying from hubot-deploy-v0.5.1", 42 | "creator": { 43 | "login": "fakeatmos" 44 | }, 45 | "statuses_url": "https://api.github.com/repos/MyOrg/my-org-hubot/deployments/1077/statuses" 46 | } 47 | ``` 48 | 49 | $ hubot deploy hubot/topic 50 | ... Deploys the topic branch of hubot to the default environment 51 | 52 | ```JSON 53 | { 54 | "url": "https://api.github.com/repos/MyOrg/my-org-hubot/deployments/1078", 55 | "id": 1078, 56 | "sha": "03ed31c1312478561d677bfe743eb13290b10d42", 57 | "payload": { 58 | "task": "deploy", 59 | "hosts": "", 60 | "branch": "topic", 61 | "notify": { 62 | "room": "danger", 63 | "user": "atmos", 64 | "adapter": "unknown" 65 | }, 66 | "environment": "production", 67 | "config": { 68 | "provider": "heroku", 69 | "repository": "MyOrg/my-org-hubot", 70 | "environments": [ 71 | "production" 72 | ], 73 | "heroku_name": "my-org-hubot", 74 | } 75 | }, 76 | "description": "Deploying from hubot-deploy-v0.5.1", 77 | "creator": { 78 | "login": "fakeatmos" 79 | }, 80 | "statuses_url": "https://api.github.com/repos/MyOrg/my-org-hubot/deployments/1078/statuses" 81 | } 82 | ``` 83 | $ hubot deploy:migrate hubot 84 | ... Create a deployment where the task is set to `deploy:migrate` for the master branch of hubot 85 | 86 | ```JSON 87 | { 88 | "url": "https://api.github.com/repos/MyOrg/my-org-hubot/deployments/1079", 89 | "id": 1079, 90 | "sha": "cfbc1c744e106c2aa869fae6452ed249f12d8713", 91 | "payload": { 92 | "task": "deploy:migrate", 93 | "hosts": "", 94 | "branch": "master", 95 | "notify": { 96 | "room": "danger", 97 | "user": "atmos", 98 | "adapter": "unknown" 99 | }, 100 | "environment": "production", 101 | "config": { 102 | "provider": "heroku", 103 | "repository": "MyOrg/my-org-hubot", 104 | "environments": [ 105 | "production" 106 | ], 107 | "heroku_name": "my-org-hubot", 108 | } 109 | }, 110 | "description": "Deploying from hubot-deploy-v0.5.1", 111 | "creator": { 112 | "login": "fakeatmos" 113 | }, 114 | "statuses_url": "https://api.github.com/repos/MyOrg/my-org-hubot/deployments/1079/statuses" 115 | } 116 | ``` 117 | 118 | $ hubot deploy! hubot 119 | ... Bypass all CI and ahead/behind checks on GitHub 120 | 121 | ```JSON 122 | { 123 | "url": "https://api.github.com/repos/MyOrg/my-org-hubot/deployments/1080", 124 | "id": 1080, 125 | "sha": "cfbc1c744e106c2aa869fae6452ed249f12d8713", 126 | "payload": { 127 | "task": "deploy", 128 | "hosts": "", 129 | "branch": "master", 130 | "notify": { 131 | "room": "danger", 132 | "user": "atmos", 133 | "adapter": "unknown" 134 | }, 135 | "environment": "production", 136 | "config": { 137 | "provider": "heroku", 138 | "repository": "MyOrg/my-org-hubot", 139 | "environments": [ 140 | "production" 141 | ], 142 | "heroku_name": "my-org-hubot", 143 | } 144 | }, 145 | "description": "Deploying from hubot-deploy-v0.5.1", 146 | "creator": { 147 | "login": "fakeatmos" 148 | }, 149 | "statuses_url": "https://api.github.com/repos/MyOrg/my-org-hubot/deployments/1080/statuses" 150 | } 151 | ``` 152 | 153 | $ hubot deploy hubot/topic to staging/fe 154 | ... Deploy the topic branch of hubot to the staging environment for the host class of `fe`. 155 | 156 | ```JSON 157 | { 158 | "url": "https://api.github.com/repos/MyOrg/my-org-hubot/deployments/1081", 159 | "id": 1081, 160 | "sha": "03ed31c1312478561d677bfe743eb13290b10d42", 161 | "payload": { 162 | "task": "deploy", 163 | "hosts": "fe", 164 | "branch": "topic", 165 | "notify": { 166 | "room": "danger", 167 | "user": "atmos", 168 | "adapter": "unknown" 169 | }, 170 | "environment": "production", 171 | "config": { 172 | "provider": "heroku", 173 | "repository": "MyOrg/my-org-hubot", 174 | "environments": [ 175 | "production" 176 | ], 177 | "heroku_name": "my-org-hubot", 178 | } 179 | }, 180 | "description": "Deploying from hubot-deploy-v0.5.1", 181 | "creator": { 182 | "login": "fakeatmos" 183 | }, 184 | "statuses_url": "https://api.github.com/repos/MyOrg/my-org-hubot/deployments/1081/statuses" 185 | } 186 | ``` 187 | -------------------------------------------------------------------------------- /test/support/cassettes/github_latest_deployments.coffee: -------------------------------------------------------------------------------- 1 | module.exports.cassettes = 2 | '/github-deployments-latest-production-success': 3 | host: 'https://api.github.com:443' 4 | path: '/repos/atmos/hubot-deploy/deployments' 5 | code: 200 6 | params: 7 | environment: 'production' 8 | path: '/repos/atmos/hubot-deploy/deployments' 9 | body: [ 10 | { 11 | id: 2332339, 12 | url: "https://api.github.com/repos/atmos/hubot-deploy/deployments/2332339" 13 | sha: "afa77dd02e61d86e73795796207e128206d670e2" 14 | ref: "master" 15 | task: "deploy" 16 | environment: "production", 17 | description: "deploy on production from hubot-deploy-v0.10.1", 18 | payload: 19 | name: "hubot" 20 | robotName: "hubot" 21 | notify: 22 | adapter: "slack" 23 | room: "#general" 24 | user: "341" 25 | config: 26 | provider: "heroku", 27 | repository: "atmos/hubot-deploy", 28 | environments: [ 29 | "production" 30 | "staging" 31 | ] 32 | required_contexts: [ 33 | "continuous-integration/travis-ci/push" 34 | ] 35 | heroku_production_name: "hubot-deploy-production" 36 | heroku_staging_name: "hubot-deploy-staging" 37 | creator: 38 | id: 38, 39 | login: "atmos", 40 | avatar_url: "https://avatars.githubusercontent.com/u/38?v=3" 41 | } 42 | { 43 | id: 2332369, 44 | url: "https://api.github.com/repos/atmos/hubot-deploy/deployments/2332369" 45 | sha: "afa77dd02e61d86e73795796207e128206d670e2" 46 | ref: "master" 47 | task: "deploy" 48 | environment: "production", 49 | description: "deploy on production from hubot-deploy-v0.10.1", 50 | payload: 51 | name: "hubot" 52 | robotName: "hubot" 53 | notify: 54 | adapter: "slack" 55 | room: "#general" 56 | user: "341" 57 | config: 58 | provider: "heroku", 59 | repository: "atmos/hubot-deploy", 60 | environments: [ 61 | "production" 62 | "staging" 63 | ] 64 | required_contexts: [ 65 | "continuous-integration/travis-ci/push" 66 | ] 67 | heroku_production_name: "hubot-deploy-production" 68 | heroku_staging_name: "hubot-deploy-staging" 69 | creator: 70 | id: 38, 71 | login: "atmos", 72 | avatar_url: "https://avatars.githubusercontent.com/u/38?v=3" 73 | } 74 | ] 75 | '/github-deployments-latest-staging-success': 76 | host: 'https://api.github.com:443' 77 | path: '/repos/atmos/hubot-deploy/deployments' 78 | params: 79 | environment: 'staging' 80 | code: 200 81 | path: '/repos/atmos/hubot-deploy/deployments' 82 | body: [ 83 | { 84 | id: 2332339, 85 | url: "https://api.github.com/repos/atmos/hubot-deploy/deployments/2332339" 86 | sha: "afa77dd02e61d86e73795796207e128206d670e2" 87 | ref: "master" 88 | task: "deploy" 89 | environment: "staging", 90 | description: "deploy on staging from hubot-deploy-v0.10.1", 91 | payload: 92 | name: "hubot" 93 | robotName: "hubot" 94 | notify: 95 | adapter: "slack" 96 | room: "#general" 97 | user: "341" 98 | config: 99 | provider: "heroku", 100 | repository: "atmos/hubot-deploy", 101 | environments: [ 102 | "production" 103 | "staging" 104 | ] 105 | required_contexts: [ 106 | "continuous-integration/travis-ci/push" 107 | ] 108 | heroku_production_name: "hubot-deploy-production" 109 | heroku_staging_name: "hubot-deploy-staging" 110 | creator: 111 | id: 38, 112 | login: "atmos", 113 | avatar_url: "https://avatars.githubusercontent.com/u/38?v=3" 114 | } 115 | { 116 | id: 2332369, 117 | url: "https://api.github.com/repos/atmos/hubot-deploy/deployments/2332369" 118 | sha: "afa77dd02e61d86e73795796207e128206d670e2" 119 | ref: "master" 120 | task: "deploy" 121 | environment: "staging", 122 | description: "deploy on staging from hubot-deploy-v0.10.1", 123 | payload: 124 | name: "hubot" 125 | robotName: "hubot" 126 | notify: 127 | adapter: "slack" 128 | room: "#general" 129 | user: "341" 130 | config: 131 | provider: "heroku", 132 | repository: "atmos/hubot-deploy", 133 | environments: [ 134 | "production" 135 | "staging" 136 | ] 137 | required_contexts: [ 138 | "continuous-integration/travis-ci/push" 139 | ] 140 | heroku_production_name: "hubot-deploy-production" 141 | heroku_staging_name: "hubot-deploy-staging" 142 | creator: 143 | id: 38, 144 | login: "atmos", 145 | avatar_url: "https://avatars.githubusercontent.com/u/38?v=3" 146 | } 147 | ] 148 | '/github-deployments-latest-production-bad-auth': 149 | host: 'https://api.github.com:443' 150 | path: '/repos/atmos/hubot-deploy/deployments' 151 | code: 401 152 | path: '/repos/atmos/hubot-deploy/deployments' 153 | body: 154 | message: "Bad credentials" 155 | documentation_url: "https://developer.github.com/v3" 156 | -------------------------------------------------------------------------------- /test/support/cassettes/github_deployment_create.coffee: -------------------------------------------------------------------------------- 1 | module.exports.cassettes = 2 | '/repos-atmos-hubot-deploy-deployment-production-create-bad-auth': 3 | host: 'https://api.github.com:443' 4 | path: '/repos/atmos/hubot-deploy/deployments' 5 | method: 'post' 6 | code: 401 7 | path: '/repos/atmos/hubot-deploy/deployments' 8 | body: 9 | message: 'Bad credentials' 10 | documentation_url: 'https://developer.github.com/v3' 11 | '/repos-atmos-hubot-deploy-deployment-production-create-required-status-missing': 12 | host: 'https://api.github.com:443' 13 | path: '/repos/atmos/hubot-deploy/deployments' 14 | method: 'post' 15 | code: 409 16 | path: '/repos/atmos/hubot-deploy/deployments' 17 | body: 18 | message: 'Conflict: Commit status checks failed for master' 19 | errors: [ 20 | { 21 | contexts: [ 22 | { 23 | context: "continuous-integration/travis-ci/push" 24 | state: "failure" 25 | }, 26 | { 27 | context: "code-climate" 28 | state: "success" 29 | } 30 | ] 31 | } 32 | ] 33 | '/repos-atmos-hubot-deploy-deployment-production-create-required-status-failing': 34 | host: 'https://api.github.com:443' 35 | path: '/repos/atmos/hubot-deploy/deployments' 36 | method: 'post' 37 | code: 409 38 | path: '/repos/atmos/hubot-deploy/deployments' 39 | body: 40 | message: "Conflict: Commit status checks failed for master" 41 | errors: [ 42 | { 43 | contexts: [ 44 | { 45 | context: "continuous-integration/travis-ci/push" 46 | state: "failure" 47 | }, 48 | { 49 | context: "code-climate" 50 | state: "failure" 51 | } 52 | ] 53 | } 54 | ] 55 | '/repos-atmos-hubot-deploy-deployment-production-create-auto-merged-failed': 56 | host: 'https://api.github.com:443' 57 | path: '/repos/atmos/hubot-deploy/deployments' 58 | method: 'post' 59 | code: 409 60 | path: '/repos/atmos/hubot-deploy/deployments' 61 | body: 62 | message: "Conflict merging master into topic." 63 | '/repos-atmos-hubot-deploy-deployment-production-create-auto-merged': 64 | host: 'https://api.github.com:443' 65 | path: '/repos/atmos/hubot-deploy/deployments' 66 | method: 'post' 67 | code: 202 68 | path: '/repos/atmos/hubot-deploy/deployments' 69 | body: 70 | message: "Auto-merged master into topic on deployment." 71 | '/repos-atmos-hubot-deploy-deployment-production-create-success': 72 | host: 'https://api.github.com:443' 73 | path: '/repos/atmos/hubot-deploy/deployments' 74 | method: 'post' 75 | code: 201 76 | path: '/repos/atmos/hubot-deploy/deployments' 77 | body: 78 | deployment: 79 | url: "https://api.github.com/repos/atmos/hubot-deploy/deployments/1875476" 80 | id: 1875476 81 | sha: "3c9f42c76ce057eaabc3762e3ec46dd830976963" 82 | ref: "heroku" 83 | task: "deploy" 84 | environment: "production" 85 | description: "Deploying from hubot-deploy-v0.6.53" 86 | payload: 87 | name: "hubot-deploy" 88 | hosts: "" 89 | notify: 90 | room: "ops", 91 | user: "atmos", 92 | adapter: "slack", 93 | config: 94 | provider: "heroku", 95 | auto_merge: true, 96 | repository: "atmos/hubot-deploy", 97 | environments: [ "production", "staging" ] 98 | allowed_rooms: [] 99 | heroku_name: "zero-fucks-hubot" 100 | creator: 101 | id: 6626297, 102 | login: "atmos", 103 | avatar_url: "https://avatars.githubusercontent.com/u/6626297?v=3" 104 | repository: 105 | id: 42524818 106 | name: "hubot-deploy" 107 | private: true 108 | full_name: "atmos/hubot-deploy" 109 | owner: 110 | login: "atmos", 111 | type: "User", 112 | site_admin: false 113 | sender: 114 | id: 6626297 115 | login: "atmos" 116 | avatar_url: "https://avatars.githubusercontent.com/u/6626297?v=3" 117 | 118 | '/repos-atmos-hubot-deploy-deployment-staging-create-success': 119 | host: 'https://api.github.com:443' 120 | path: '/repos/atmos/hubot-deploy/deployments' 121 | method: 'post' 122 | code: 201 123 | path: '/repos/atmos/hubot-deploy/deployments' 124 | body: 125 | deployment: 126 | url: "https://api.github.com/repos/atmos/hubot-deploy/deployments/1875476" 127 | id: 1875476 128 | sha: "3c9f42c76ce057eaabc3762e3ec46dd830976963" 129 | ref: "heroku" 130 | task: "deploy" 131 | environment: "staging" 132 | description: "Deploying from hubot-deploy-v0.6.53" 133 | payload: 134 | name: "hubot-deploy" 135 | hosts: "" 136 | notify: 137 | room: "ops", 138 | user: "atmos", 139 | adapter: "slack", 140 | config: 141 | provider: "heroku", 142 | auto_merge: true, 143 | repository: "atmos/hubot-deploy", 144 | environments: [ "production", "staging" ] 145 | allowed_rooms: [] 146 | heroku_name: "zero-fucks-hubot" 147 | creator: 148 | id: 6626297, 149 | login: "atmos", 150 | avatar_url: "https://avatars.githubusercontent.com/u/6626297?v=3" 151 | repository: 152 | id: 42524818 153 | name: "hubot-deploy" 154 | private: true 155 | full_name: "atmos/hubot-deploy" 156 | owner: 157 | login: "atmos", 158 | type: "User", 159 | site_admin: false 160 | sender: 161 | id: 6626297 162 | login: "atmos" 163 | avatar_url: "https://avatars.githubusercontent.com/u/6626297?v=3" 164 | -------------------------------------------------------------------------------- /src/scripts/deploy.coffee: -------------------------------------------------------------------------------- 1 | # Description 2 | # Cut GitHub deployments from chat that deploy via hooks - https://github.com/atmos/hubot-deploy 3 | # 4 | # Commands: 5 | # hubot where can I deploy - see what environments you can deploy app 6 | # hubot deploy:version - show the script version and node/environment info 7 | # hubot deploy / to / - deploys 's to the environment's servers 8 | # hubot deploys / in - Displays recent deployments for 's in the environment 9 | # 10 | supported_tasks = [ DeployPrefix ] 11 | 12 | Path = require("path") 13 | Version = require(Path.join(__dirname, "..", "version")).Version 14 | Patterns = require(Path.join(__dirname, "..", "models", "patterns")) 15 | Formatters = require(Path.join(__dirname, "..", "models", "formatters")) 16 | Deployment = require(Path.join(__dirname, "..", "github", "api")).Deployment 17 | 18 | DeployPrefix = Patterns.DeployPrefix 19 | DeployPattern = Patterns.DeployPattern 20 | DeploysPattern = Patterns.DeploysPattern 21 | 22 | Verifiers = require(Path.join(__dirname, "..", "models", "verifiers")) 23 | TokenForBrain = Verifiers.VaultKey 24 | 25 | defaultDeploymentEnvironment = () -> 26 | process.env.HUBOT_DEPLOY_DEFAULT_ENVIRONMENT || 'production' 27 | 28 | ########################################################################### 29 | module.exports = (robot) -> 30 | ########################################################################### 31 | # where can i deploy 32 | # 33 | # Displays the available environments for an application 34 | robot.respond ///where\s+can\s+i\s+#{DeployPrefix}\s+([-_\.0-9a-z]+)///i, id: "hubot-deploy.wcid", (msg) -> 35 | name = msg.match[1] 36 | 37 | try 38 | deployment = new Deployment(name) 39 | formatter = new Formatters.WhereFormatter(deployment) 40 | 41 | robot.emit "hubot_deploy_available_environments", msg, deployment, formatter 42 | 43 | catch err 44 | robot.logger.info "Exploded looking for deployment locations: #{err}" 45 | 46 | ########################################################################### 47 | # deploys in 48 | # 49 | # Displays the recent deployments for an application in an environment 50 | robot.respond DeploysPattern, id: "hubot-deploy.recent", hubotDeployAuthenticate: true, (msg) -> 51 | name = msg.match[2] 52 | environment = msg.match[4] || "" 53 | 54 | try 55 | deployment = new Deployment(name, null, null, environment) 56 | unless deployment.isValidApp() 57 | msg.reply "#{name}? Never heard of it." 58 | return 59 | unless deployment.isValidEnv() 60 | if environment.length > 0 61 | msg.reply "#{name} doesn't seem to have an #{environment} environment." 62 | return 63 | 64 | user = robot.brain.userForId msg.envelope.user.id 65 | token = robot.vault.forUser(user).get(TokenForBrain) 66 | if token? 67 | deployment.setUserToken(token) 68 | 69 | deployment.user = user.id 70 | deployment.room = msg.message.user.room 71 | 72 | if robot.adapterName is "flowdock" 73 | deployment.threadId = msg.message.metadata.thread_id 74 | deployment.messageId = msg.message.id 75 | 76 | if robot.adapterName is "hipchat" 77 | if msg.envelope.user.reply_to? 78 | deployment.room = msg.envelope.user.reply_to 79 | 80 | if robot.adapterName is "slack" 81 | deployment.user = user.name 82 | deployment.room = robot.adapter.client.rtm.dataStore.getChannelGroupOrDMById(msg.message.user.room).name 83 | 84 | deployment.adapter = robot.adapterName 85 | deployment.robotName = robot.name 86 | 87 | deployment.latest (err, deployments) -> 88 | formatter = new Formatters.LatestFormatter(deployment, deployments) 89 | robot.emit "hubot_deploy_recent_deployments", msg, deployment, deployments, formatter 90 | 91 | catch err 92 | robot.logger.info "Exploded looking for recent deployments: #{err}" 93 | 94 | ########################################################################### 95 | # deploy hubot/topic-branch to staging 96 | # 97 | # Actually dispatch deployment requests to GitHub 98 | robot.respond DeployPattern, id: "hubot-deploy.create", hubotDeployAuthenticate: true, (msg) -> 99 | task = msg.match[1].replace(DeployPrefix, "deploy") 100 | force = msg.match[2] == '!' 101 | name = msg.match[3] 102 | ref = (msg.match[4]||'master') 103 | env = (msg.match[5]||defaultDeploymentEnvironment()) 104 | hosts = (msg.match[6]||'') 105 | yubikey = msg.match[7] 106 | 107 | deployment = new Deployment(name, ref, task, env, force, hosts) 108 | 109 | unless deployment.isValidApp() 110 | msg.reply "#{name}? Never heard of it." 111 | return 112 | unless deployment.isValidEnv() 113 | msg.reply "#{name} doesn't seem to have an #{env} environment." 114 | return 115 | unless deployment.isAllowedRoom(msg.message.user.room) 116 | msg.reply "#{name} is not allowed to be deployed from this room." 117 | return 118 | 119 | user = robot.brain.userForId msg.envelope.user.id 120 | token = robot.vault.forUser(user).get(TokenForBrain) 121 | if token? 122 | deployment.setUserToken(token) 123 | 124 | deployment.user = user.id 125 | deployment.room = msg.message.user.room 126 | 127 | if robot.adapterName is "flowdock" 128 | deployment.threadId = msg.message.metadata.thread_id 129 | deployment.messageId = msg.message.id 130 | 131 | if robot.adapterName is "hipchat" 132 | if msg.envelope.user.reply_to? 133 | deployment.room = msg.envelope.user.reply_to 134 | 135 | if robot.adapterName is "slack" 136 | deployment.user = user.name 137 | deployment.room = robot.adapter.client.rtm.dataStore.getChannelGroupOrDMById(msg.message.user.room).name 138 | 139 | deployment.yubikey = yubikey 140 | deployment.adapter = robot.adapterName 141 | deployment.userName = user.name 142 | deployment.robotName = robot.name 143 | 144 | if process.env.HUBOT_DEPLOY_EMIT_GITHUB_DEPLOYMENTS 145 | robot.emit "github_deployment", msg, deployment 146 | else 147 | deployment.post (err, status, body, headers, responseMessage) -> 148 | msg.reply responseMessage if responseMessage? 149 | 150 | ########################################################################### 151 | # deploy:version 152 | # 153 | # Useful for debugging 154 | robot.respond ///#{DeployPrefix}\:version$///i, id: "hubot-deploy.version", (msg) -> 155 | msg.send "hubot-deploy v#{Version}/hubot v#{robot.version}/node #{process.version}" 156 | -------------------------------------------------------------------------------- /src/github/api/deployment.coffee: -------------------------------------------------------------------------------- 1 | Fs = require "fs" 2 | Url = require "url" 3 | Path = require "path" 4 | Fernet = require "fernet" 5 | Version = require(Path.join(__dirname, "..", "..", "version")).Version 6 | Octonode = require "octonode" 7 | GitHubApi = require(Path.join(__dirname, "..", "api")).Api 8 | ########################################################################### 9 | 10 | class Deployment 11 | @APPS_FILE = process.env['HUBOT_DEPLOY_APPS_JSON'] or "apps.json" 12 | 13 | constructor: (@name, @ref, @task, @env, @force, @hosts) -> 14 | @room = 'unknown' 15 | @user = 'unknown' 16 | @adapter = 'unknown' 17 | @userName = 'unknown' 18 | @robotName = 'hubot' 19 | @autoMerge = true 20 | @environments = [ "production" ] 21 | @requiredContexts = null 22 | @caFile = Fs.readFileSync(process.env['HUBOT_CA_FILE']) if process.env['HUBOT_CA_FILE'] 23 | 24 | @messageId = undefined 25 | @threadId = undefined 26 | 27 | try 28 | applications = JSON.parse(Fs.readFileSync(@constructor.APPS_FILE).toString()) 29 | catch 30 | throw new Error("Unable to parse your apps.json file in hubot-deploy") 31 | 32 | @application = applications[@name] 33 | 34 | if @application? 35 | @repository = @application['repository'] 36 | 37 | @configureAutoMerge() 38 | @configureRequiredContexts() 39 | @configureEnvironments() 40 | 41 | @allowedRooms = @application['allowed_rooms'] 42 | 43 | isValidApp: -> 44 | @application? 45 | 46 | isValidEnv: -> 47 | @env in @environments 48 | 49 | isAllowedRoom: (room) -> 50 | !@allowedRooms? || room in @allowedRooms 51 | 52 | # Retrieves a fully constructed request body and removes sensitive config info 53 | # A hash to be converted into the body of the post to create a GitHub Deployment 54 | requestBody: -> 55 | body = JSON.parse(JSON.stringify(@unfilteredRequestBody())) 56 | if body?.payload?.config? 57 | delete(body.payload.config.github_api) 58 | delete(body.payload.config.github_token) 59 | if process.env.HUBOT_DEPLOY_ENCRYPT_PAYLOAD and process.env.HUBOT_DEPLOY_FERNET_SECRETS 60 | payload = body.payload 61 | fernetSecret = new Fernet.Secret(process.env.HUBOT_DEPLOY_FERNET_SECRETS) 62 | fernetToken = new Fernet.Token(secret: fernetSecret) 63 | 64 | body.payload = fernetToken.encode(payload) 65 | 66 | body 67 | 68 | unfilteredRequestBody: -> 69 | ref: @ref 70 | task: @task 71 | force: @force 72 | auto_merge: @autoMerge 73 | environment: @env 74 | required_contexts: @requiredContexts 75 | description: "#{@task} on #{@env} from hubot-deploy-v#{Version}" 76 | payload: 77 | name: @name 78 | robotName: @robotName 79 | hosts: @hosts 80 | yubikey: @yubikey 81 | notify: 82 | adapter: @adapter 83 | room: @room 84 | user: @user 85 | user_name: @userName 86 | message_id: @messageId 87 | thread_id: @threadId 88 | config: @application 89 | 90 | setUserToken: (token) -> 91 | @userToken = token.trim() 92 | 93 | apiConfig: -> 94 | new GitHubApi(@userToken, @application) 95 | 96 | api: -> 97 | api = Octonode.client(@apiConfig().token, { hostname: @apiConfig().hostname }) 98 | api.requestDefaults.agentOptions = { ca: @caFile } if @caFile 99 | api 100 | 101 | latest: (callback) -> 102 | path = @apiConfig().path("repos/#{@repository}/deployments") 103 | params = 104 | environment: @env 105 | 106 | @api().get path, params, (err, status, body, headers) -> 107 | callback(err, body) 108 | 109 | post: (callback) -> 110 | name = @name 111 | repository = @repository 112 | env = @env 113 | ref = @ref 114 | 115 | requiredContexts = @requiredContexts 116 | 117 | @rawPost (err, status, body, headers) -> 118 | data = body 119 | 120 | if err 121 | data = err 122 | 123 | if data['message'] 124 | bodyMessage = data['message'] 125 | 126 | if bodyMessage.match(/No successful commit statuses/) 127 | message = """ 128 | I don't see a successful build for #{repository} that covers the latest \"#{ref}\" branch. 129 | """ 130 | 131 | if bodyMessage.match(/Conflict merging ([-_\.0-9a-z]+)/) 132 | default_branch = data.message.match(/Conflict merging ([-_\.0-9a-z]+)/)[1] 133 | message = """ 134 | There was a problem merging the #{default_branch} for #{repository} into #{ref}. 135 | You'll need to merge it manually, or disable auto-merging. 136 | """ 137 | 138 | if bodyMessage.match(/Merged ([-_\.0-9a-z]+) into/) 139 | tmpMessage = """ 140 | Successfully merged the default branch for #{repository} into #{ref}. 141 | Normal push notifications should provide feedback. 142 | """ 143 | console.log tmpMessage 144 | 145 | if bodyMessage.match(/Conflict: Commit status checks/) 146 | errors = data['errors'][0] 147 | commitContexts = errors.contexts 148 | 149 | namedContexts = (context.context for context in commitContexts ) 150 | failedContexts = (context.context for context in commitContexts when context.state isnt 'success') 151 | if requiredContexts? 152 | failedContexts.push(context) for context in requiredContexts when context not in namedContexts 153 | 154 | bodyMessage = """ 155 | Unmet required commit status contexts for #{name}: #{failedContexts.join(',')} failed. 156 | """ 157 | 158 | if bodyMessage == "Not Found" 159 | message = "Unable to create deployments for #{repository}. Check your scopes for this token." 160 | else 161 | message = bodyMessage 162 | 163 | callback(err, status, body, headers, message) 164 | 165 | rawPost: (callback) -> 166 | path = @apiConfig().path("repos/#{@repository}/deployments") 167 | repository = @repository 168 | env = @env 169 | ref = @ref 170 | 171 | @api().post path, @requestBody(), (err, status, body, headers) -> 172 | callback(err, status, body, headers) 173 | 174 | # Private Methods 175 | configureEnvironments: -> 176 | if @application['environments']? 177 | @environments = @application['environments'] 178 | 179 | @env = 'staging' if @env == 'stg' 180 | @env = 'production' if @env == 'prod' 181 | 182 | configureAutoMerge: -> 183 | if @application['auto_merge']? 184 | @autoMerge = @application['auto_merge'] 185 | if @force 186 | @autoMerge = false 187 | 188 | configureRequiredContexts: -> 189 | if @application['required_contexts']? 190 | @requiredContexts = @application['required_contexts'] 191 | if @force 192 | @requiredContexts = [ ] 193 | 194 | exports.Deployment = Deployment 195 | -------------------------------------------------------------------------------- /test/models/pattern_test.coffee: -------------------------------------------------------------------------------- 1 | Path = require('path') 2 | 3 | Patterns = require(Path.join(__dirname, "..", "..", "src", "models", "patterns")) 4 | 5 | DeployPattern = Patterns.DeployPattern 6 | DeploysPattern = Patterns.DeploysPattern 7 | 8 | describe "Patterns", () -> 9 | describe "DeployPattern", () -> 10 | it "rejects things that don't start with deploy", () -> 11 | assert !"ping".match(DeployPattern) 12 | assert !"image me pugs".match(DeployPattern) 13 | 14 | it "handles simple deployment", () -> 15 | matches = "deploy hubot".match(DeployPattern) 16 | assert.equal "deploy", matches[1], "incorrect task" 17 | assert.equal "hubot", matches[3], "incorrect app name" 18 | assert.equal undefined, matches[4], "incorrect branch" 19 | assert.equal undefined, matches[5], "incorrect environment" 20 | assert.equal undefined, matches[6], "incorrect host specification" 21 | 22 | it "handles ! operations", () -> 23 | matches = "deploy! hubot".match(DeployPattern) 24 | assert.equal "deploy", matches[1], "incorrect task" 25 | assert.equal "!", matches[2], "incorrect task" 26 | assert.equal "hubot", matches[3], "incorrect app name" 27 | assert.equal undefined, matches[4], "incorrect branch" 28 | assert.equal undefined, matches[5], "incorrect environment" 29 | assert.equal undefined, matches[6], "incorrect host specification" 30 | 31 | it "handles custom tasks", () -> 32 | matches = "deploy:migrate hubot".match(DeployPattern) 33 | assert.equal "deploy:migrate", matches[1], "incorrect task" 34 | assert.equal "hubot", matches[3], "incorrect app name" 35 | assert.equal undefined, matches[4], "incorrect branch" 36 | assert.equal undefined, matches[5], "incorrect environment" 37 | assert.equal undefined, matches[6], "incorrect host specification" 38 | 39 | it "handles deploying branches", () -> 40 | matches = "deploy hubot/mybranch to production".match(DeployPattern) 41 | assert.equal "deploy", matches[1], "incorrect task" 42 | assert.equal "hubot", matches[3], "incorrect app name" 43 | assert.equal "mybranch", matches[4], "incorrect branch name" 44 | assert.equal "production", matches[5], "incorrect environment name" 45 | assert.equal undefined, matches[6], "incorrect branch name" 46 | 47 | it "handles deploying to environments", () -> 48 | matches = "deploy hubot to production".match(DeployPattern) 49 | assert.equal "deploy", matches[1], "incorrect task" 50 | assert.equal "hubot", matches[3], "incorrect app name" 51 | assert.equal undefined, matches[4], "incorrect branch name" 52 | assert.equal "production", matches[5], "incorrect environment name" 53 | assert.equal undefined, matches[6], "incorrect branch name" 54 | 55 | it "handles environments with hosts", () -> 56 | matches = "deploy hubot to production/fe".match(DeployPattern) 57 | assert.equal "deploy", matches[1], "incorrect task" 58 | assert.equal "hubot", matches[3], "incorrect app name" 59 | assert.equal undefined, matches[4], "incorrect branch name" 60 | assert.equal "production", matches[5], "incorrect environment name" 61 | assert.equal "fe", matches[6], "incorrect host name" 62 | assert.equal undefined, matches[7], "incorrect yubikey pattern" 63 | 64 | it "handles branch deploys with slashes and environments with hosts", () -> 65 | matches = "deploy hubot/atmos/branch to production/fe".match(DeployPattern) 66 | assert.equal "deploy", matches[1], "incorrect task" 67 | assert.equal "hubot", matches[3], "incorrect app name" 68 | assert.equal "atmos/branch", matches[4], "incorrect branch name" 69 | assert.equal "production", matches[5], "incorrect environment name" 70 | assert.equal "fe", matches[6], "incorrect host name" 71 | assert.equal undefined, matches[7], "incorrect yubikey pattern" 72 | 73 | it "handles branch deploys with slashes and environments with hosts plus yubikeys", () -> 74 | matches = "deploy hubot/atmos/branch to production/fe ccccccdlnncbtuevhdbctrccukdciveuclhbkvehbeve".match(DeployPattern) 75 | assert.equal "deploy", matches[1], "incorrect task" 76 | assert.equal "hubot", matches[3], "incorrect app name" 77 | assert.equal "atmos/branch", matches[4], "incorrect branch name" 78 | assert.equal "production", matches[5], "incorrect environment name" 79 | assert.equal "fe", matches[6], "incorrect host name" 80 | assert.equal "ccccccdlnncbtuevhdbctrccukdciveuclhbkvehbeve", matches[7], "incorrect yubikey pattern" 81 | 82 | it "handles branch deploys with slashes and environments with hosts plus 2fa keys", () -> 83 | matches = "deploy hubot/atmos/branch to production/fe 123456".match(DeployPattern) 84 | assert.equal "deploy", matches[1], "incorrect task" 85 | assert.equal "hubot", matches[3], "incorrect app name" 86 | assert.equal "atmos/branch", matches[4], "incorrect branch name" 87 | assert.equal "production", matches[5], "incorrect environment name" 88 | assert.equal "fe", matches[6], "incorrect host name" 89 | assert.equal "123456", matches[7], "incorrect authenticator token" 90 | 91 | it "doesn't match on malformed yubikeys", () -> 92 | matches = "deploy hubot/atmos/branch to production/fe burgers".match(DeployPattern) 93 | assert.equal null, matches 94 | 95 | it "does not match typos", () -> 96 | matches = "deploy hubot/branch tos taging".match(DeployPattern) 97 | assert.equal matches, null 98 | 99 | describe "DeploysPattern", () -> 100 | it "rejects things that don't start with deploy", () -> 101 | assert !"ping".match(DeploysPattern) 102 | assert !"image me pugs".match(DeploysPattern) 103 | 104 | it "handles simple deploys listing", () -> 105 | matches = "deploys hubot".match(DeploysPattern) 106 | assert.equal "deploys", matches[1], "incorrect task" 107 | assert.equal "hubot", matches[2], "incorrect app name" 108 | assert.equal undefined, matches[3], "incorrect branch" 109 | assert.equal undefined, matches[4], "incorrect environment" 110 | 111 | it "handles deploys with environments", () -> 112 | matches = "deploys hubot in production".match(DeploysPattern) 113 | assert.equal "deploys", matches[1], "incorrect task" 114 | assert.equal "hubot", matches[2], "incorrect app name" 115 | assert.equal undefined, matches[3], "incorrect branch name" 116 | assert.equal "production", matches[4], "incorrect environment name" 117 | 118 | it "handles deploys with branches", () -> 119 | matches = "deploys hubot/mybranch to production".match(DeploysPattern) 120 | assert.equal "deploys", matches[1], "incorrect task" 121 | assert.equal "hubot", matches[2], "incorrect app name" 122 | assert.equal "mybranch", matches[3], "incorrect branch name" 123 | assert.equal "production", matches[4], "incorrect environment name" 124 | -------------------------------------------------------------------------------- /test/fixtures/pushes/single.json: -------------------------------------------------------------------------------- 1 | { 2 | "ref": "refs/heads/changes", 3 | "before": "9049f1265b7d61be4a8904a9a27120d2064dab3b", 4 | "after": "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c", 5 | "created": false, 6 | "deleted": false, 7 | "forced": false, 8 | "base_ref": null, 9 | "compare": "https://github.com/atmos/hubot-deploy/compare/9049f1265b7d...0d1a26e67d8f", 10 | "commits": [ 11 | { 12 | "id": "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c", 13 | "distinct": true, 14 | "message": "Update README.md", 15 | "timestamp": "2015-05-05T19:40:15-04:00", 16 | "url": "https://github.com/atmos/hubot-deploy/commit/0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c", 17 | "author": { 18 | "name": "atmos", 19 | "email": "atmos@users.noreply.github.com", 20 | "username": "atmos" 21 | }, 22 | "committer": { 23 | "name": "atmos", 24 | "email": "atmos@users.noreply.github.com", 25 | "username": "atmos" 26 | }, 27 | "added": [ 28 | 29 | ], 30 | "removed": [ 31 | 32 | ], 33 | "modified": [ 34 | "README.md" 35 | ] 36 | } 37 | ], 38 | "head_commit": { 39 | "id": "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c", 40 | "distinct": true, 41 | "message": "Update README.md", 42 | "timestamp": "2015-05-05T19:40:15-04:00", 43 | "url": "https://github.com/atmos/hubot-deploy/commit/0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c", 44 | "author": { 45 | "name": "atmos", 46 | "email": "atmos@users.noreply.github.com", 47 | "username": "atmos" 48 | }, 49 | "committer": { 50 | "name": "atmos", 51 | "email": "atmos@users.noreply.github.com", 52 | "username": "atmos" 53 | }, 54 | "added": [ 55 | 56 | ], 57 | "removed": [ 58 | 59 | ], 60 | "modified": [ 61 | "README.md" 62 | ] 63 | }, 64 | "repository": { 65 | "id": 35129377, 66 | "name": "hubot-deploy", 67 | "full_name": "atmos/hubot-deploy", 68 | "owner": { 69 | "name": "atmos", 70 | "email": "atmos@users.noreply.github.com" 71 | }, 72 | "private": false, 73 | "html_url": "https://github.com/atmos/hubot-deploy", 74 | "description": "", 75 | "fork": false, 76 | "url": "https://github.com/atmos/hubot-deploy", 77 | "forks_url": "https://api.github.com/repos/atmos/hubot-deploy/forks", 78 | "keys_url": "https://api.github.com/repos/atmos/hubot-deploy/keys{/key_id}", 79 | "collaborators_url": "https://api.github.com/repos/atmos/hubot-deploy/collaborators{/collaborator}", 80 | "teams_url": "https://api.github.com/repos/atmos/hubot-deploy/teams", 81 | "hooks_url": "https://api.github.com/repos/atmos/hubot-deploy/hooks", 82 | "issue_events_url": "https://api.github.com/repos/atmos/hubot-deploy/issues/events{/number}", 83 | "events_url": "https://api.github.com/repos/atmos/hubot-deploy/events", 84 | "assignees_url": "https://api.github.com/repos/atmos/hubot-deploy/assignees{/user}", 85 | "branches_url": "https://api.github.com/repos/atmos/hubot-deploy/branches{/branch}", 86 | "tags_url": "https://api.github.com/repos/atmos/hubot-deploy/tags", 87 | "blobs_url": "https://api.github.com/repos/atmos/hubot-deploy/git/blobs{/sha}", 88 | "git_tags_url": "https://api.github.com/repos/atmos/hubot-deploy/git/tags{/sha}", 89 | "git_refs_url": "https://api.github.com/repos/atmos/hubot-deploy/git/refs{/sha}", 90 | "trees_url": "https://api.github.com/repos/atmos/hubot-deploy/git/trees{/sha}", 91 | "statuses_url": "https://api.github.com/repos/atmos/hubot-deploy/statuses/{sha}", 92 | "languages_url": "https://api.github.com/repos/atmos/hubot-deploy/languages", 93 | "stargazers_url": "https://api.github.com/repos/atmos/hubot-deploy/stargazers", 94 | "contributors_url": "https://api.github.com/repos/atmos/hubot-deploy/contributors", 95 | "subscribers_url": "https://api.github.com/repos/atmos/hubot-deploy/subscribers", 96 | "subscription_url": "https://api.github.com/repos/atmos/hubot-deploy/subscription", 97 | "commits_url": "https://api.github.com/repos/atmos/hubot-deploy/commits{/sha}", 98 | "git_commits_url": "https://api.github.com/repos/atmos/hubot-deploy/git/commits{/sha}", 99 | "comments_url": "https://api.github.com/repos/atmos/hubot-deploy/comments{/number}", 100 | "issue_comment_url": "https://api.github.com/repos/atmos/hubot-deploy/issues/comments{/number}", 101 | "contents_url": "https://api.github.com/repos/atmos/hubot-deploy/contents/{+path}", 102 | "compare_url": "https://api.github.com/repos/atmos/hubot-deploy/compare/{base}...{head}", 103 | "merges_url": "https://api.github.com/repos/atmos/hubot-deploy/merges", 104 | "archive_url": "https://api.github.com/repos/atmos/hubot-deploy/{archive_format}{/ref}", 105 | "downloads_url": "https://api.github.com/repos/atmos/hubot-deploy/downloads", 106 | "issues_url": "https://api.github.com/repos/atmos/hubot-deploy/issues{/number}", 107 | "pulls_url": "https://api.github.com/repos/atmos/hubot-deploy/pulls{/number}", 108 | "milestones_url": "https://api.github.com/repos/atmos/hubot-deploy/milestones{/number}", 109 | "notifications_url": "https://api.github.com/repos/atmos/hubot-deploy/notifications{?since,all,participating}", 110 | "labels_url": "https://api.github.com/repos/atmos/hubot-deploy/labels{/name}", 111 | "releases_url": "https://api.github.com/repos/atmos/hubot-deploy/releases{/id}", 112 | "created_at": 1430869212, 113 | "updated_at": "2015-05-05T23:40:12Z", 114 | "pushed_at": 1430869217, 115 | "git_url": "git://github.com/atmos/hubot-deploy.git", 116 | "ssh_url": "git@github.com:atmos/hubot-deploy.git", 117 | "clone_url": "https://github.com/atmos/hubot-deploy.git", 118 | "svn_url": "https://github.com/atmos/hubot-deploy", 119 | "homepage": null, 120 | "size": 0, 121 | "stargazers_count": 0, 122 | "watchers_count": 0, 123 | "language": null, 124 | "has_issues": true, 125 | "has_downloads": true, 126 | "has_wiki": true, 127 | "has_pages": true, 128 | "forks_count": 0, 129 | "mirror_url": null, 130 | "open_issues_count": 0, 131 | "forks": 0, 132 | "open_issues": 0, 133 | "watchers": 0, 134 | "default_branch": "master", 135 | "stargazers": 0, 136 | "master_branch": "master" 137 | }, 138 | "pusher": { 139 | "name": "atmos", 140 | "email": "atmos@users.noreply.github.com" 141 | }, 142 | "sender": { 143 | "login": "atmos", 144 | "id": 6752317, 145 | "avatar_url": "https://avatars.githubusercontent.com/u/6752317?v=3", 146 | "gravatar_id": "", 147 | "url": "https://api.github.com/users/atmos", 148 | "html_url": "https://github.com/atmos", 149 | "followers_url": "https://api.github.com/users/atmos/followers", 150 | "following_url": "https://api.github.com/users/atmos/following{/other_user}", 151 | "gists_url": "https://api.github.com/users/atmos/gists{/gist_id}", 152 | "starred_url": "https://api.github.com/users/atmos/starred{/owner}{/repo}", 153 | "subscriptions_url": "https://api.github.com/users/atmos/subscriptions", 154 | "organizations_url": "https://api.github.com/users/atmos/orgs", 155 | "repos_url": "https://api.github.com/users/atmos/repos", 156 | "events_url": "https://api.github.com/users/atmos/events{/privacy}", 157 | "received_events_url": "https://api.github.com/users/atmos/received_events", 158 | "type": "User", 159 | "site_admin": false 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/scripts/http.coffee: -------------------------------------------------------------------------------- 1 | # Description 2 | # Enable deployment statuses from the GitHub API 3 | # 4 | # Commands: 5 | # 6 | 7 | Fs = require "fs" 8 | Path = require "path" 9 | Crypto = require "crypto" 10 | 11 | GitHubEvents = require(Path.join(__dirname, "..", "github", "webhooks")) 12 | Push = GitHubEvents.Push 13 | Deployment = GitHubEvents.Deployment 14 | PullRequest = GitHubEvents.PullRequest 15 | DeploymentStatus = GitHubEvents.DeploymentStatus 16 | CommitStatus = GitHubEvents.CommitStatus 17 | 18 | DeployPrefix = require(Path.join(__dirname, "..", "models", "patterns")).DeployPrefix 19 | 20 | GitHubSecret = process.env.HUBOT_DEPLOY_WEBHOOK_SECRET 21 | 22 | WebhookPrefix = process.env.HUBOT_DEPLOY_WEBHOOK_PREFIX or "/hubot-deploy" 23 | 24 | supported_tasks = [ "#{DeployPrefix}-hooks:sync" ] 25 | 26 | Verifiers = require(Path.join(__dirname, "..", "models", "verifiers")) 27 | 28 | AppsJsonFile = process.env['HUBOT_DEPLOY_APPS_JSON'] or "apps.json" 29 | AppsJsonData = JSON.parse(Fs.readFileSync(AppsJsonFile)) 30 | ########################################################################### 31 | module.exports = (robot) -> 32 | ipVerifier = new Verifiers.GitHubWebHookIpVerifier 33 | 34 | process.env.HUBOT_DEPLOY_WEBHOOK_SECRET or= "459C1E17-AAA9-4ABF-9120-92E8385F9949" 35 | if GitHubSecret 36 | robot.router.get WebhookPrefix + "/apps", (req, res) -> 37 | token = req.headers['authorization']?.match(/Bearer (.+){1,256}/)?[1] 38 | if token is process.env["HUBOT_DEPLOY_WEBHOOK_SECRET"] 39 | res.writeHead 200, {'content-type': 'application/json' } 40 | return res.end(JSON.stringify(AppsJsonData)) 41 | else 42 | res.writeHead 404, {'content-type': 'application/json' } 43 | return res.end(JSON.stringify({message: "Not Found"})) 44 | 45 | robot.router.post WebhookPrefix + "/repos/:owner/:repo/messages", (req, res) -> 46 | token = req.headers['authorization']?.match(/Bearer (.+){1,256}/)?[1] 47 | if token is process.env["HUBOT_DEPLOY_WEBHOOK_SECRET"] 48 | emission = 49 | body: req.body 50 | repo: req.params.repo 51 | owner: req.params.owner 52 | 53 | robot.emit "hubot_deploy_repo_message", emission 54 | res.writeHead 202, {'content-type': 'application/json' } 55 | return res.end("{}") 56 | else 57 | res.writeHead 404, {'content-type': 'application/json' } 58 | return res.end(JSON.stringify({message: "Not Found"})) 59 | 60 | robot.router.post WebhookPrefix + "/teams/:team/messages", (req, res) -> 61 | token = req.headers['authorization']?.match(/Bearer (.+){1,256}/)?[1] 62 | if token is process.env["HUBOT_DEPLOY_WEBHOOK_SECRET"] 63 | emission = 64 | team: req.params.team 65 | body: req.body 66 | 67 | robot.emit "hubot_deploy_team_message", emission 68 | res.writeHead 202, {'content-type': 'application/json' } 69 | return res.end("{}") 70 | else 71 | res.writeHead 404, {'content-type': 'application/json' } 72 | return res.end(JSON.stringify({message: "Not Found"})) 73 | 74 | robot.router.get WebhookPrefix + "/apps/:name", (req, res) -> 75 | try 76 | token = req.headers['authorization']?.match(/Bearer (.+){1,256}/)?[1] 77 | if token isnt process.env["HUBOT_DEPLOY_WEBHOOK_SECRET"] 78 | throw new Error("Bad auth headers") 79 | else 80 | app = AppsJsonData[req.params["name"]] 81 | if app? 82 | res.writeHead 200, {'content-type': 'application/json' } 83 | return res.end(JSON.stringify(app)) 84 | else 85 | throw new Error("App not found") 86 | catch 87 | res.writeHead 404, {'content-type': 'application/json' } 88 | return res.end(JSON.stringify({message: "Not Found"})) 89 | 90 | robot.router.post WebhookPrefix, (req, res) -> 91 | try 92 | remoteIp = req.headers['x-forwarded-for'] or req.connection.remoteAddress 93 | unless ipVerifier.ipIsValid(remoteIp) 94 | res.writeHead 400, {'content-type': 'application/json' } 95 | return res.end(JSON.stringify({error: "Webhook requested from a non-GitHub IP address."})) 96 | 97 | payloadSignature = req.headers['x-hub-signature'] 98 | unless payloadSignature? 99 | res.writeHead 400, {'content-type': 'application/json' } 100 | return res.end(JSON.stringify({error: "No GitHub payload signature headers present"})) 101 | 102 | expectedSignature = Crypto.createHmac("sha1", GitHubSecret).update(JSON.stringify(req.body)).digest("hex") 103 | if payloadSignature is not "sha1=#{expectedSignature}" 104 | res.writeHead 400, {'content-type': 'application/json' } 105 | return res.end(JSON.stringify({error: "X-Hub-Signature does not match blob signature"})) 106 | 107 | deliveryId = req.headers['x-github-delivery'] 108 | switch req.headers['x-github-event'] 109 | when "ping" 110 | res.writeHead 204, {'content-type': 'application/json' } 111 | return res.end(JSON.stringify({message: "Hello from #{robot.name}. :D"})) 112 | 113 | when "push" 114 | push = new Push deliveryId, req.body 115 | 116 | robot.emit "github_push_event", push 117 | 118 | res.writeHead 202, {'content-type': 'application/json' } 119 | return res.end(JSON.stringify({message: push.toSimpleString()})) 120 | 121 | when "deployment" 122 | deployment = new Deployment deliveryId, req.body 123 | 124 | robot.emit "github_deployment_event", deployment 125 | 126 | res.writeHead 202, {'content-type': 'application/json' } 127 | return res.end(JSON.stringify({message: deployment.toSimpleString()})) 128 | 129 | when "deployment_status" 130 | status = new DeploymentStatus deliveryId, req.body 131 | 132 | robot.emit "github_deployment_status_event", status 133 | 134 | res.writeHead 202, {'content-type': 'application/json' } 135 | return res.end(JSON.stringify({message: status.toSimpleString()})) 136 | 137 | when "status" 138 | status = new CommitStatus deliveryId, req.body 139 | 140 | robot.emit "github_commit_status_event", status 141 | 142 | res.writeHead 202, {'content-type': 'application/json' } 143 | return res.end(JSON.stringify({message: status.toSimpleString()})) 144 | 145 | when "pull_request" 146 | pullRequest = new PullRequest deliveryId, req.body 147 | 148 | robot.emit "github_pull_request", pullRequest 149 | 150 | res.writeHead 202, {'content-type': 'application/json' } 151 | return res.end(JSON.stringify({message: pullRequest.toSimpleString()})) 152 | 153 | else 154 | res.writeHead 204, {'content-type': 'application/json' } 155 | return res.end(JSON.stringify({message: "Received but not processed."})) 156 | 157 | catch err 158 | robot.logger.error err 159 | res.writeHead 500, {'content-type': 'application/json' } 160 | return res.end(JSON.stringify({error: "Something went crazy processing the request."})) 161 | 162 | else if process.env.NODE_ENV is not "test" 163 | robot.logger.error "You're using hubot-deploy without specifying the shared webhook secret" 164 | robot.logger.error "Take a second to learn about them: https://developer.github.com/webhooks/securing/" 165 | robot.logger.error "Then set the HUBOT_DEPLOY_WEBHOOK_SECRET variable in the robot environment" 166 | -------------------------------------------------------------------------------- /test/fixtures/deployments/staging.json: -------------------------------------------------------------------------------- 1 | { 2 | "deployment": { 3 | "url": "https://api.github.com/repos/atmos/heaven/deployments/1875476", 4 | "id": 1875476, 5 | "sha": "3c9f42c76ce057eaabc3762e3ec46dd830976963", 6 | "ref": "heroku", 7 | "task": "deploy", 8 | "payload": { 9 | "name": "heaven", 10 | "robotName": "hubot", 11 | "hosts": "", 12 | "notify": { 13 | "room": "ops", 14 | "user": "atmos", 15 | "adapter": "slack", 16 | "message_id": "unknown", 17 | "thread_id": "unknown" 18 | }, 19 | "config": { 20 | "provider": "heroku", 21 | "auto_merge": true, 22 | "repository": "atmos/heaven", 23 | "environments": [ 24 | "staging" 25 | ], 26 | "allowed_rooms": [], 27 | "heroku_staging_name": "zero-fucks-hubot" 28 | } 29 | }, 30 | "environment": "staging", 31 | "description": "Deploying from hubot-deploy-v0.12.5", 32 | "creator": { 33 | "login": "atmos", 34 | "id": 6626297, 35 | "avatar_url": "https://avatars.githubusercontent.com/u/6626297?v=3", 36 | "gravatar_id": "", 37 | "url": "https://api.github.com/users/atmos", 38 | "html_url": "https://github.com/atmos", 39 | "followers_url": "https://api.github.com/users/atmos/followers", 40 | "following_url": "https://api.github.com/users/atmos/following{/other_user}", 41 | "gists_url": "https://api.github.com/users/atmos/gists{/gist_id}", 42 | "starred_url": "https://api.github.com/users/atmos/starred{/owner}{/repo}", 43 | "subscriptions_url": "https://api.github.com/users/atmos/subscriptions", 44 | "organizations_url": "https://api.github.com/users/atmos/orgs", 45 | "repos_url": "https://api.github.com/users/atmos/repos", 46 | "events_url": "https://api.github.com/users/atmos/events{/privacy}", 47 | "received_events_url": "https://api.github.com/users/atmos/received_events", 48 | "type": "User", 49 | "site_admin": false 50 | }, 51 | "created_at": "2015-09-24T03:36:33Z", 52 | "updated_at": "2015-09-24T03:36:33Z", 53 | "statuses_url": "https://api.github.com/repos/atmos/heaven/deployments/1875476/statuses", 54 | "repository_url": "https://api.github.com/repos/atmos/heaven" 55 | }, 56 | "repository": { 57 | "id": 42524818, 58 | "name": "heaven", 59 | "full_name": "atmos/heaven", 60 | "owner": { 61 | "login": "atmos", 62 | "id": 6626297, 63 | "avatar_url": "https://avatars.githubusercontent.com/u/6626297?v=3", 64 | "gravatar_id": "", 65 | "url": "https://api.github.com/users/atmos", 66 | "html_url": "https://github.com/atmos", 67 | "followers_url": "https://api.github.com/users/atmos/followers", 68 | "following_url": "https://api.github.com/users/atmos/following{/other_user}", 69 | "gists_url": "https://api.github.com/users/atmos/gists{/gist_id}", 70 | "starred_url": "https://api.github.com/users/atmos/starred{/owner}{/repo}", 71 | "subscriptions_url": "https://api.github.com/users/atmos/subscriptions", 72 | "organizations_url": "https://api.github.com/users/atmos/orgs", 73 | "repos_url": "https://api.github.com/users/atmos/repos", 74 | "events_url": "https://api.github.com/users/atmos/events{/privacy}", 75 | "received_events_url": "https://api.github.com/users/atmos/received_events", 76 | "type": "User", 77 | "site_admin": false 78 | }, 79 | "private": true, 80 | "html_url": "https://github.com/atmos/heaven", 81 | "description": "SlackHQ hubot for atmos", 82 | "fork": false, 83 | "url": "https://api.github.com/repos/atmos/heaven", 84 | "forks_url": "https://api.github.com/repos/atmos/heaven/forks", 85 | "keys_url": "https://api.github.com/repos/atmos/heaven/keys{/key_id}", 86 | "collaborators_url": "https://api.github.com/repos/atmos/heaven/collaborators{/collaborator}", 87 | "teams_url": "https://api.github.com/repos/atmos/heaven/teams", 88 | "hooks_url": "https://api.github.com/repos/atmos/heaven/hooks", 89 | "issue_events_url": "https://api.github.com/repos/atmos/heaven/issues/events{/number}", 90 | "events_url": "https://api.github.com/repos/atmos/heaven/events", 91 | "assignees_url": "https://api.github.com/repos/atmos/heaven/assignees{/user}", 92 | "branches_url": "https://api.github.com/repos/atmos/heaven/branches{/branch}", 93 | "tags_url": "https://api.github.com/repos/atmos/heaven/tags", 94 | "blobs_url": "https://api.github.com/repos/atmos/heaven/git/blobs{/sha}", 95 | "git_tags_url": "https://api.github.com/repos/atmos/heaven/git/tags{/sha}", 96 | "git_refs_url": "https://api.github.com/repos/atmos/heaven/git/refs{/sha}", 97 | "trees_url": "https://api.github.com/repos/atmos/heaven/git/trees{/sha}", 98 | "statuses_url": "https://api.github.com/repos/atmos/heaven/statuses/{sha}", 99 | "languages_url": "https://api.github.com/repos/atmos/heaven/languages", 100 | "stargazers_url": "https://api.github.com/repos/atmos/heaven/stargazers", 101 | "contributors_url": "https://api.github.com/repos/atmos/heaven/contributors", 102 | "subscribers_url": "https://api.github.com/repos/atmos/heaven/subscribers", 103 | "subscription_url": "https://api.github.com/repos/atmos/heaven/subscription", 104 | "commits_url": "https://api.github.com/repos/atmos/heaven/commits{/sha}", 105 | "git_commits_url": "https://api.github.com/repos/atmos/heaven/git/commits{/sha}", 106 | "comments_url": "https://api.github.com/repos/atmos/heaven/comments{/number}", 107 | "issue_comment_url": "https://api.github.com/repos/atmos/heaven/issues/comments{/number}", 108 | "contents_url": "https://api.github.com/repos/atmos/heaven/contents/{+path}", 109 | "compare_url": "https://api.github.com/repos/atmos/heaven/compare/{base}...{head}", 110 | "merges_url": "https://api.github.com/repos/atmos/heaven/merges", 111 | "archive_url": "https://api.github.com/repos/atmos/heaven/{archive_format}{/ref}", 112 | "downloads_url": "https://api.github.com/repos/atmos/heaven/downloads", 113 | "issues_url": "https://api.github.com/repos/atmos/heaven/issues{/number}", 114 | "pulls_url": "https://api.github.com/repos/atmos/heaven/pulls{/number}", 115 | "milestones_url": "https://api.github.com/repos/atmos/heaven/milestones{/number}", 116 | "notifications_url": "https://api.github.com/repos/atmos/heaven/notifications{?since,all,participating}", 117 | "labels_url": "https://api.github.com/repos/atmos/heaven/labels{/name}", 118 | "releases_url": "https://api.github.com/repos/atmos/heaven/releases{/id}", 119 | "created_at": "2015-09-15T14:32:13Z", 120 | "updated_at": "2015-09-15T15:28:11Z", 121 | "pushed_at": "2015-09-24T02:04:35Z", 122 | "git_url": "git://github.com/atmos/heaven.git", 123 | "ssh_url": "git@github.com:atmos/heaven.git", 124 | "clone_url": "https://github.com/atmos/heaven.git", 125 | "svn_url": "https://github.com/atmos/heaven", 126 | "homepage": null, 127 | "size": 4924, 128 | "stargazers_count": 0, 129 | "watchers_count": 0, 130 | "language": "Python", 131 | "has_issues": true, 132 | "has_downloads": true, 133 | "has_wiki": false, 134 | "has_pages": false, 135 | "forks_count": 0, 136 | "mirror_url": null, 137 | "open_issues_count": 1, 138 | "forks": 0, 139 | "open_issues": 1, 140 | "watchers": 0, 141 | "default_branch": "develop" 142 | }, 143 | "sender": { 144 | "login": "atmos", 145 | "id": 6626297, 146 | "avatar_url": "https://avatars.githubusercontent.com/u/6626297?v=3", 147 | "gravatar_id": "", 148 | "url": "https://api.github.com/users/atmos", 149 | "html_url": "https://github.com/atmos", 150 | "followers_url": "https://api.github.com/users/atmos/followers", 151 | "following_url": "https://api.github.com/users/atmos/following{/other_user}", 152 | "gists_url": "https://api.github.com/users/atmos/gists{/gist_id}", 153 | "starred_url": "https://api.github.com/users/atmos/starred{/owner}{/repo}", 154 | "subscriptions_url": "https://api.github.com/users/atmos/subscriptions", 155 | "organizations_url": "https://api.github.com/users/atmos/orgs", 156 | "repos_url": "https://api.github.com/users/atmos/repos", 157 | "events_url": "https://api.github.com/users/atmos/events{/privacy}", 158 | "received_events_url": "https://api.github.com/users/atmos/received_events", 159 | "type": "User", 160 | "site_admin": false 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /test/fixtures/deployments/production.json: -------------------------------------------------------------------------------- 1 | { 2 | "deployment": { 3 | "url": "https://api.github.com/repos/atmos/my-robot/deployments/1875476", 4 | "id": 1875476, 5 | "sha": "3c9f42c76ce057eaabc3762e3ec46dd830976963", 6 | "ref": "heroku", 7 | "task": "deploy", 8 | "payload": { 9 | "name": "my-robot", 10 | "robotName": "hubot", 11 | "hosts": "", 12 | "notify": { 13 | "room": "ops", 14 | "user": "atmos", 15 | "adapter": "slack", 16 | "message_id": "unknown", 17 | "thread_id": "unknown" 18 | }, 19 | "config": { 20 | "provider": "heroku", 21 | "auto_merge": true, 22 | "repository": "atmos/my-robot", 23 | "environments": [ 24 | "production" 25 | ], 26 | "allowed_rooms": [], 27 | "heroku_name": "zero-fucks-hubot" 28 | } 29 | }, 30 | "environment": "production", 31 | "description": "Deploying from hubot-deploy-v0.12.5", 32 | "creator": { 33 | "login": "atmos", 34 | "id": 6626297, 35 | "avatar_url": "https://avatars.githubusercontent.com/u/6626297?v=3", 36 | "gravatar_id": "", 37 | "url": "https://api.github.com/users/atmos", 38 | "html_url": "https://github.com/atmos", 39 | "followers_url": "https://api.github.com/users/atmos/followers", 40 | "following_url": "https://api.github.com/users/atmos/following{/other_user}", 41 | "gists_url": "https://api.github.com/users/atmos/gists{/gist_id}", 42 | "starred_url": "https://api.github.com/users/atmos/starred{/owner}{/repo}", 43 | "subscriptions_url": "https://api.github.com/users/atmos/subscriptions", 44 | "organizations_url": "https://api.github.com/users/atmos/orgs", 45 | "repos_url": "https://api.github.com/users/atmos/repos", 46 | "events_url": "https://api.github.com/users/atmos/events{/privacy}", 47 | "received_events_url": "https://api.github.com/users/atmos/received_events", 48 | "type": "User", 49 | "site_admin": false 50 | }, 51 | "created_at": "2015-09-24T03:36:33Z", 52 | "updated_at": "2015-09-24T03:36:33Z", 53 | "statuses_url": "https://api.github.com/repos/atmos/my-robot/deployments/1875476/statuses", 54 | "repository_url": "https://api.github.com/repos/atmos/my-robot" 55 | }, 56 | "repository": { 57 | "id": 42524818, 58 | "name": "my-robot", 59 | "full_name": "atmos/my-robot", 60 | "owner": { 61 | "login": "atmos", 62 | "id": 6626297, 63 | "avatar_url": "https://avatars.githubusercontent.com/u/6626297?v=3", 64 | "gravatar_id": "", 65 | "url": "https://api.github.com/users/atmos", 66 | "html_url": "https://github.com/atmos", 67 | "followers_url": "https://api.github.com/users/atmos/followers", 68 | "following_url": "https://api.github.com/users/atmos/following{/other_user}", 69 | "gists_url": "https://api.github.com/users/atmos/gists{/gist_id}", 70 | "starred_url": "https://api.github.com/users/atmos/starred{/owner}{/repo}", 71 | "subscriptions_url": "https://api.github.com/users/atmos/subscriptions", 72 | "organizations_url": "https://api.github.com/users/atmos/orgs", 73 | "repos_url": "https://api.github.com/users/atmos/repos", 74 | "events_url": "https://api.github.com/users/atmos/events{/privacy}", 75 | "received_events_url": "https://api.github.com/users/atmos/received_events", 76 | "type": "User", 77 | "site_admin": false 78 | }, 79 | "private": true, 80 | "html_url": "https://github.com/atmos/my-robot", 81 | "description": "SlackHQ hubot for atmos", 82 | "fork": false, 83 | "url": "https://api.github.com/repos/atmos/my-robot", 84 | "forks_url": "https://api.github.com/repos/atmos/my-robot/forks", 85 | "keys_url": "https://api.github.com/repos/atmos/my-robot/keys{/key_id}", 86 | "collaborators_url": "https://api.github.com/repos/atmos/my-robot/collaborators{/collaborator}", 87 | "teams_url": "https://api.github.com/repos/atmos/my-robot/teams", 88 | "hooks_url": "https://api.github.com/repos/atmos/my-robot/hooks", 89 | "issue_events_url": "https://api.github.com/repos/atmos/my-robot/issues/events{/number}", 90 | "events_url": "https://api.github.com/repos/atmos/my-robot/events", 91 | "assignees_url": "https://api.github.com/repos/atmos/my-robot/assignees{/user}", 92 | "branches_url": "https://api.github.com/repos/atmos/my-robot/branches{/branch}", 93 | "tags_url": "https://api.github.com/repos/atmos/my-robot/tags", 94 | "blobs_url": "https://api.github.com/repos/atmos/my-robot/git/blobs{/sha}", 95 | "git_tags_url": "https://api.github.com/repos/atmos/my-robot/git/tags{/sha}", 96 | "git_refs_url": "https://api.github.com/repos/atmos/my-robot/git/refs{/sha}", 97 | "trees_url": "https://api.github.com/repos/atmos/my-robot/git/trees{/sha}", 98 | "statuses_url": "https://api.github.com/repos/atmos/my-robot/statuses/{sha}", 99 | "languages_url": "https://api.github.com/repos/atmos/my-robot/languages", 100 | "stargazers_url": "https://api.github.com/repos/atmos/my-robot/stargazers", 101 | "contributors_url": "https://api.github.com/repos/atmos/my-robot/contributors", 102 | "subscribers_url": "https://api.github.com/repos/atmos/my-robot/subscribers", 103 | "subscription_url": "https://api.github.com/repos/atmos/my-robot/subscription", 104 | "commits_url": "https://api.github.com/repos/atmos/my-robot/commits{/sha}", 105 | "git_commits_url": "https://api.github.com/repos/atmos/my-robot/git/commits{/sha}", 106 | "comments_url": "https://api.github.com/repos/atmos/my-robot/comments{/number}", 107 | "issue_comment_url": "https://api.github.com/repos/atmos/my-robot/issues/comments{/number}", 108 | "contents_url": "https://api.github.com/repos/atmos/my-robot/contents/{+path}", 109 | "compare_url": "https://api.github.com/repos/atmos/my-robot/compare/{base}...{head}", 110 | "merges_url": "https://api.github.com/repos/atmos/my-robot/merges", 111 | "archive_url": "https://api.github.com/repos/atmos/my-robot/{archive_format}{/ref}", 112 | "downloads_url": "https://api.github.com/repos/atmos/my-robot/downloads", 113 | "issues_url": "https://api.github.com/repos/atmos/my-robot/issues{/number}", 114 | "pulls_url": "https://api.github.com/repos/atmos/my-robot/pulls{/number}", 115 | "milestones_url": "https://api.github.com/repos/atmos/my-robot/milestones{/number}", 116 | "notifications_url": "https://api.github.com/repos/atmos/my-robot/notifications{?since,all,participating}", 117 | "labels_url": "https://api.github.com/repos/atmos/my-robot/labels{/name}", 118 | "releases_url": "https://api.github.com/repos/atmos/my-robot/releases{/id}", 119 | "created_at": "2015-09-15T14:32:13Z", 120 | "updated_at": "2015-09-15T15:28:11Z", 121 | "pushed_at": "2015-09-24T02:04:35Z", 122 | "git_url": "git://github.com/atmos/my-robot.git", 123 | "ssh_url": "git@github.com:atmos/my-robot.git", 124 | "clone_url": "https://github.com/atmos/my-robot.git", 125 | "svn_url": "https://github.com/atmos/my-robot", 126 | "homepage": null, 127 | "size": 4924, 128 | "stargazers_count": 0, 129 | "watchers_count": 0, 130 | "language": "Python", 131 | "has_issues": true, 132 | "has_downloads": true, 133 | "has_wiki": false, 134 | "has_pages": false, 135 | "forks_count": 0, 136 | "mirror_url": null, 137 | "open_issues_count": 1, 138 | "forks": 0, 139 | "open_issues": 1, 140 | "watchers": 0, 141 | "default_branch": "develop" 142 | }, 143 | "sender": { 144 | "login": "atmos", 145 | "id": 6626297, 146 | "avatar_url": "https://avatars.githubusercontent.com/u/6626297?v=3", 147 | "gravatar_id": "", 148 | "url": "https://api.github.com/users/atmos", 149 | "html_url": "https://github.com/atmos", 150 | "followers_url": "https://api.github.com/users/atmos/followers", 151 | "following_url": "https://api.github.com/users/atmos/following{/other_user}", 152 | "gists_url": "https://api.github.com/users/atmos/gists{/gist_id}", 153 | "starred_url": "https://api.github.com/users/atmos/starred{/owner}{/repo}", 154 | "subscriptions_url": "https://api.github.com/users/atmos/subscriptions", 155 | "organizations_url": "https://api.github.com/users/atmos/orgs", 156 | "repos_url": "https://api.github.com/users/atmos/repos", 157 | "events_url": "https://api.github.com/users/atmos/events{/privacy}", 158 | "received_events_url": "https://api.github.com/users/atmos/received_events", 159 | "type": "User", 160 | "site_admin": false 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /test/fixtures/deployment_statuses/success.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 12345, 3 | "state": "success", 4 | "deployment": { 5 | "url": "https://api.github.com/repos/atmos/my-robot/deployments/11627", 6 | "id": 11627, 7 | "sha": "daf81923c94f7513ac840fa4fcc0dfcc11f32f74", 8 | "ref": "break-up-notifiers", 9 | "payload": { 10 | "name": "my-robot", 11 | "task": "deploy", 12 | "hosts": "", 13 | "notify": { 14 | "room": "danger", 15 | "user": "atmos", 16 | "adapter": "slack" 17 | }, 18 | "config": { 19 | "provider": "heroku", 20 | "repository": "atmos/my-robot", 21 | "environments": [ 22 | "production" 23 | ], 24 | "heroku_production_name": "zero-fucks-my-robot" 25 | } 26 | }, 27 | "environment": "production", 28 | "description": "Deploying from hubot-deploy-v0.6.0", 29 | "creator": { 30 | "login": "fakeatmos", 31 | "id": 704696, 32 | "avatar_url": "https://avatars.githubusercontent.com/u/704696?", 33 | "gravatar_id": "7310f196129141a0517f1fe8f0689472", 34 | "url": "https://api.github.com/users/fakeatmos", 35 | "html_url": "https://github.com/fakeatmos", 36 | "followers_url": "https://api.github.com/users/fakeatmos/followers", 37 | "following_url": "https://api.github.com/users/fakeatmos/following{/other_user}", 38 | "gists_url": "https://api.github.com/users/fakeatmos/gists{/gist_id}", 39 | "starred_url": "https://api.github.com/users/fakeatmos/starred{/owner}{/repo}", 40 | "subscriptions_url": "https://api.github.com/users/fakeatmos/subscriptions", 41 | "organizations_url": "https://api.github.com/users/fakeatmos/orgs", 42 | "repos_url": "https://api.github.com/users/fakeatmos/repos", 43 | "events_url": "https://api.github.com/users/fakeatmos/events{/privacy}", 44 | "received_events_url": "https://api.github.com/users/fakeatmos/received_events", 45 | "type": "User", 46 | "site_admin": false 47 | }, 48 | "created_at": "2014-05-20T08:07:39Z", 49 | "updated_at": "2014-05-20T08:07:39Z", 50 | "statuses_url": "https://api.github.com/repos/atmos/my-robot/deployments/11627/statuses" 51 | }, 52 | "target_url": "https://gist.github.com/fa77d9fb1fe41c3bb3a3ffb2c", 53 | "description": "Deploying from Heaven v0.5.5", 54 | "deployment_status": { 55 | "id": 12345, 56 | "state": "success", 57 | "target_url": "https://gist.github.com/fa77d9fb1fe41c3bb3a3ffb2c", 58 | "description": "Deploying from Heaven v0.5.5" 59 | }, 60 | "repository": { 61 | "id": 1749093424, 62 | "name": "my-robot", 63 | "full_name": "atmos/my-robot", 64 | "owner": { 65 | "login": "atmos", 66 | "id": 6626297, 67 | "avatar_url": "https://avatars.githubusercontent.com/u/6626297?", 68 | "gravatar_id": null, 69 | "url": "https://api.github.com/users/atmos", 70 | "html_url": "https://github.com/atmos", 71 | "followers_url": "https://api.github.com/users/atmos/followers", 72 | "following_url": "https://api.github.com/users/atmos/following{/other_user}", 73 | "gists_url": "https://api.github.com/users/atmos/gists{/gist_id}", 74 | "starred_url": "https://api.github.com/users/atmos/starred{/owner}{/repo}", 75 | "subscriptions_url": "https://api.github.com/users/atmos/subscriptions", 76 | "organizations_url": "https://api.github.com/users/atmos/orgs", 77 | "repos_url": "https://api.github.com/users/atmos/repos", 78 | "events_url": "https://api.github.com/users/atmos/events{/privacy}", 79 | "received_events_url": "https://api.github.com/users/atmos/received_events", 80 | "type": "Organization", 81 | "site_admin": false 82 | }, 83 | "private": false, 84 | "html_url": "https://github.com/atmos/my-robot", 85 | "description": "Rails app for GitHub Flow Notifications - :walking: :tada:", 86 | "fork": true, 87 | "url": "https://api.github.com/repos/atmos/my-robot", 88 | "forks_url": "https://api.github.com/repos/atmos/my-robot/forks", 89 | "keys_url": "https://api.github.com/repos/atmos/my-robot/keys{/key_id}", 90 | "collaborators_url": "https://api.github.com/repos/atmos/my-robot/collaborators{/collaborator}", 91 | "teams_url": "https://api.github.com/repos/atmos/my-robot/teams", 92 | "hooks_url": "https://api.github.com/repos/atmos/my-robot/hooks", 93 | "issue_events_url": "https://api.github.com/repos/atmos/my-robot/issues/events{/number}", 94 | "events_url": "https://api.github.com/repos/atmos/my-robot/events", 95 | "assignees_url": "https://api.github.com/repos/atmos/my-robot/assignees{/user}", 96 | "branches_url": "https://api.github.com/repos/atmos/my-robot/branches{/branch}", 97 | "tags_url": "https://api.github.com/repos/atmos/my-robot/tags", 98 | "blobs_url": "https://api.github.com/repos/atmos/my-robot/git/blobs{/sha}", 99 | "git_tags_url": "https://api.github.com/repos/atmos/my-robot/git/tags{/sha}", 100 | "git_refs_url": "https://api.github.com/repos/atmos/my-robot/git/refs{/sha}", 101 | "trees_url": "https://api.github.com/repos/atmos/my-robot/git/trees{/sha}", 102 | "statuses_url": "https://api.github.com/repos/atmos/my-robot/statuses/{sha}", 103 | "languages_url": "https://api.github.com/repos/atmos/my-robot/languages", 104 | "stargazers_url": "https://api.github.com/repos/atmos/my-robot/stargazers", 105 | "contributors_url": "https://api.github.com/repos/atmos/my-robot/contributors", 106 | "subscribers_url": "https://api.github.com/repos/atmos/my-robot/subscribers", 107 | "subscription_url": "https://api.github.com/repos/atmos/my-robot/subscription", 108 | "commits_url": "https://api.github.com/repos/atmos/my-robot/commits{/sha}", 109 | "git_commits_url": "https://api.github.com/repos/atmos/my-robot/git/commits{/sha}", 110 | "comments_url": "https://api.github.com/repos/atmos/my-robot/comments{/number}", 111 | "issue_comment_url": "https://api.github.com/repos/atmos/my-robot/issues/comments/{number}", 112 | "contents_url": "https://api.github.com/repos/atmos/my-robot/contents/{+path}", 113 | "compare_url": "https://api.github.com/repos/atmos/my-robot/compare/{base}...{head}", 114 | "merges_url": "https://api.github.com/repos/atmos/my-robot/merges", 115 | "archive_url": "https://api.github.com/repos/atmos/my-robot/{archive_format}{/ref}", 116 | "downloads_url": "https://api.github.com/repos/atmos/my-robot/downloads", 117 | "issues_url": "https://api.github.com/repos/atmos/my-robot/issues{/number}", 118 | "pulls_url": "https://api.github.com/repos/atmos/my-robot/pulls{/number}", 119 | "milestones_url": "https://api.github.com/repos/atmos/my-robot/milestones{/number}", 120 | "notifications_url": "https://api.github.com/repos/atmos/my-robot/notifications{?since,all,participating}", 121 | "labels_url": "https://api.github.com/repos/atmos/my-robot/labels{/name}", 122 | "releases_url": "https://api.github.com/repos/atmos/my-robot/releases{/id}", 123 | "created_at": "2014-02-26T07:43:34Z", 124 | "updated_at": "2014-05-20T08:04:10Z", 125 | "pushed_at": "2014-05-20T08:04:10Z", 126 | "git_url": "git://github.com/atmos/my-robot.git", 127 | "ssh_url": "git@github.com:atmos/my-robot.git", 128 | "clone_url": "https://github.com/atmos/my-robot.git", 129 | "svn_url": "https://github.com/atmos/my-robot", 130 | "homepage": "", 131 | "size": 15649, 132 | "stargazers_count": 0, 133 | "watchers_count": 0, 134 | "language": "Ruby", 135 | "has_issues": false, 136 | "has_downloads": true, 137 | "has_wiki": false, 138 | "forks_count": 0, 139 | "mirror_url": null, 140 | "open_issues_count": 0, 141 | "forks": 0, 142 | "open_issues": 0, 143 | "watchers": 0, 144 | "default_branch": "master" 145 | }, 146 | "sender": { 147 | "login": "fakeatmos", 148 | "id": 704696, 149 | "avatar_url": "https://avatars.githubusercontent.com/u/704696?", 150 | "gravatar_id": "7310f196129141a0517f1fe8f0689472", 151 | "url": "https://api.github.com/users/fakeatmos", 152 | "html_url": "https://github.com/fakeatmos", 153 | "followers_url": "https://api.github.com/users/fakeatmos/followers", 154 | "following_url": "https://api.github.com/users/fakeatmos/following{/other_user}", 155 | "gists_url": "https://api.github.com/users/fakeatmos/gists{/gist_id}", 156 | "starred_url": "https://api.github.com/users/fakeatmos/starred{/owner}{/repo}", 157 | "subscriptions_url": "https://api.github.com/users/fakeatmos/subscriptions", 158 | "organizations_url": "https://api.github.com/users/fakeatmos/orgs", 159 | "repos_url": "https://api.github.com/users/fakeatmos/repos", 160 | "events_url": "https://api.github.com/users/fakeatmos/events{/privacy}", 161 | "received_events_url": "https://api.github.com/users/fakeatmos/received_events", 162 | "type": "User", 163 | "site_admin": false 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /test/fixtures/deployment_statuses/failure.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 12345, 3 | "state": "failure", 4 | "deployment": { 5 | "url": "https://api.github.com/repos/atmos/my-robot/deployments/123456", 6 | "id": 123456, 7 | "sha": "daf81923c94f7513ac840fa4fcc0dfcc11f32f74", 8 | "ref": "break-up-notifiers", 9 | "payload": { 10 | "name": "my-robot", 11 | "task": "deploy", 12 | "hosts": "", 13 | "notify": { 14 | "room": "danger", 15 | "user": "atmos", 16 | "adapter": "slack" 17 | }, 18 | "config": { 19 | "provider": "heroku", 20 | "repository": "atmos/my-robot", 21 | "environments": [ 22 | "production" 23 | ], 24 | "heroku_production_name": "zero-fucks-my-robot" 25 | } 26 | }, 27 | "environment": "production", 28 | "description": "Deploying from hubot-deploy-v0.6.0", 29 | "creator": { 30 | "login": "fakeatmos", 31 | "id": 704696, 32 | "avatar_url": "https://avatars.githubusercontent.com/u/704696?", 33 | "gravatar_id": "7310f196129141a0517f1fe8f0689472", 34 | "url": "https://api.github.com/users/fakeatmos", 35 | "html_url": "https://github.com/fakeatmos", 36 | "followers_url": "https://api.github.com/users/fakeatmos/followers", 37 | "following_url": "https://api.github.com/users/fakeatmos/following{/other_user}", 38 | "gists_url": "https://api.github.com/users/fakeatmos/gists{/gist_id}", 39 | "starred_url": "https://api.github.com/users/fakeatmos/starred{/owner}{/repo}", 40 | "subscriptions_url": "https://api.github.com/users/fakeatmos/subscriptions", 41 | "organizations_url": "https://api.github.com/users/fakeatmos/orgs", 42 | "repos_url": "https://api.github.com/users/fakeatmos/repos", 43 | "events_url": "https://api.github.com/users/fakeatmos/events{/privacy}", 44 | "received_events_url": "https://api.github.com/users/fakeatmos/received_events", 45 | "type": "User", 46 | "site_admin": false 47 | }, 48 | "created_at": "2014-05-20T08:07:39Z", 49 | "updated_at": "2014-05-20T08:07:39Z", 50 | "statuses_url": "https://api.github.com/repos/atmos/my-robot/deployments/123456/statuses" 51 | }, 52 | "target_url": "https://gist.github.com/fa77d9fb1fe41c3bb3a3ffb2c", 53 | "description": "Deploying from Heaven v0.5.5", 54 | "deployment_status": { 55 | "id": 12345, 56 | "state": "failure", 57 | "target_url": "https://gist.github.com/fa77d9fb1fe41c3bb3a3ffb2c", 58 | "description": "Deploying from Heaven v0.5.5" 59 | }, 60 | "repository": { 61 | "id": 1749093424, 62 | "name": "my-robot", 63 | "full_name": "atmos/my-robot", 64 | "owner": { 65 | "login": "atmos", 66 | "id": 6626297, 67 | "avatar_url": "https://avatars.githubusercontent.com/u/6626297?", 68 | "gravatar_id": null, 69 | "url": "https://api.github.com/users/atmos", 70 | "html_url": "https://github.com/atmos", 71 | "followers_url": "https://api.github.com/users/atmos/followers", 72 | "following_url": "https://api.github.com/users/atmos/following{/other_user}", 73 | "gists_url": "https://api.github.com/users/atmos/gists{/gist_id}", 74 | "starred_url": "https://api.github.com/users/atmos/starred{/owner}{/repo}", 75 | "subscriptions_url": "https://api.github.com/users/atmos/subscriptions", 76 | "organizations_url": "https://api.github.com/users/atmos/orgs", 77 | "repos_url": "https://api.github.com/users/atmos/repos", 78 | "events_url": "https://api.github.com/users/atmos/events{/privacy}", 79 | "received_events_url": "https://api.github.com/users/atmos/received_events", 80 | "type": "Organization", 81 | "site_admin": false 82 | }, 83 | "private": false, 84 | "html_url": "https://github.com/atmos/my-robot", 85 | "description": "Rails app for GitHub Flow Notifications - :walking: :tada:", 86 | "fork": true, 87 | "url": "https://api.github.com/repos/atmos/my-robot", 88 | "forks_url": "https://api.github.com/repos/atmos/my-robot/forks", 89 | "keys_url": "https://api.github.com/repos/atmos/my-robot/keys{/key_id}", 90 | "collaborators_url": "https://api.github.com/repos/atmos/my-robot/collaborators{/collaborator}", 91 | "teams_url": "https://api.github.com/repos/atmos/my-robot/teams", 92 | "hooks_url": "https://api.github.com/repos/atmos/my-robot/hooks", 93 | "issue_events_url": "https://api.github.com/repos/atmos/my-robot/issues/events{/number}", 94 | "events_url": "https://api.github.com/repos/atmos/my-robot/events", 95 | "assignees_url": "https://api.github.com/repos/atmos/my-robot/assignees{/user}", 96 | "branches_url": "https://api.github.com/repos/atmos/my-robot/branches{/branch}", 97 | "tags_url": "https://api.github.com/repos/atmos/my-robot/tags", 98 | "blobs_url": "https://api.github.com/repos/atmos/my-robot/git/blobs{/sha}", 99 | "git_tags_url": "https://api.github.com/repos/atmos/my-robot/git/tags{/sha}", 100 | "git_refs_url": "https://api.github.com/repos/atmos/my-robot/git/refs{/sha}", 101 | "trees_url": "https://api.github.com/repos/atmos/my-robot/git/trees{/sha}", 102 | "statuses_url": "https://api.github.com/repos/atmos/my-robot/statuses/{sha}", 103 | "languages_url": "https://api.github.com/repos/atmos/my-robot/languages", 104 | "stargazers_url": "https://api.github.com/repos/atmos/my-robot/stargazers", 105 | "contributors_url": "https://api.github.com/repos/atmos/my-robot/contributors", 106 | "subscribers_url": "https://api.github.com/repos/atmos/my-robot/subscribers", 107 | "subscription_url": "https://api.github.com/repos/atmos/my-robot/subscription", 108 | "commits_url": "https://api.github.com/repos/atmos/my-robot/commits{/sha}", 109 | "git_commits_url": "https://api.github.com/repos/atmos/my-robot/git/commits{/sha}", 110 | "comments_url": "https://api.github.com/repos/atmos/my-robot/comments{/number}", 111 | "issue_comment_url": "https://api.github.com/repos/atmos/my-robot/issues/comments/{number}", 112 | "contents_url": "https://api.github.com/repos/atmos/my-robot/contents/{+path}", 113 | "compare_url": "https://api.github.com/repos/atmos/my-robot/compare/{base}...{head}", 114 | "merges_url": "https://api.github.com/repos/atmos/my-robot/merges", 115 | "archive_url": "https://api.github.com/repos/atmos/my-robot/{archive_format}{/ref}", 116 | "downloads_url": "https://api.github.com/repos/atmos/my-robot/downloads", 117 | "issues_url": "https://api.github.com/repos/atmos/my-robot/issues{/number}", 118 | "pulls_url": "https://api.github.com/repos/atmos/my-robot/pulls{/number}", 119 | "milestones_url": "https://api.github.com/repos/atmos/my-robot/milestones{/number}", 120 | "notifications_url": "https://api.github.com/repos/atmos/my-robot/notifications{?since,all,participating}", 121 | "labels_url": "https://api.github.com/repos/atmos/my-robot/labels{/name}", 122 | "releases_url": "https://api.github.com/repos/atmos/my-robot/releases{/id}", 123 | "created_at": "2014-02-26T07:43:34Z", 124 | "updated_at": "2014-05-20T08:04:10Z", 125 | "pushed_at": "2014-05-20T08:04:10Z", 126 | "git_url": "git://github.com/atmos/my-robot.git", 127 | "ssh_url": "git@github.com:atmos/my-robot.git", 128 | "clone_url": "https://github.com/atmos/my-robot.git", 129 | "svn_url": "https://github.com/atmos/my-robot", 130 | "homepage": "", 131 | "size": 15649, 132 | "stargazers_count": 0, 133 | "watchers_count": 0, 134 | "language": "Ruby", 135 | "has_issues": false, 136 | "has_downloads": true, 137 | "has_wiki": false, 138 | "forks_count": 0, 139 | "mirror_url": null, 140 | "open_issues_count": 0, 141 | "forks": 0, 142 | "open_issues": 0, 143 | "watchers": 0, 144 | "default_branch": "master" 145 | }, 146 | "sender": { 147 | "login": "fakeatmos", 148 | "id": 704696, 149 | "avatar_url": "https://avatars.githubusercontent.com/u/704696?", 150 | "gravatar_id": "7310f196129141a0517f1fe8f0689472", 151 | "url": "https://api.github.com/users/fakeatmos", 152 | "html_url": "https://github.com/fakeatmos", 153 | "followers_url": "https://api.github.com/users/fakeatmos/followers", 154 | "following_url": "https://api.github.com/users/fakeatmos/following{/other_user}", 155 | "gists_url": "https://api.github.com/users/fakeatmos/gists{/gist_id}", 156 | "starred_url": "https://api.github.com/users/fakeatmos/starred{/owner}{/repo}", 157 | "subscriptions_url": "https://api.github.com/users/fakeatmos/subscriptions", 158 | "organizations_url": "https://api.github.com/users/fakeatmos/orgs", 159 | "repos_url": "https://api.github.com/users/fakeatmos/repos", 160 | "events_url": "https://api.github.com/users/fakeatmos/events{/privacy}", 161 | "received_events_url": "https://api.github.com/users/fakeatmos/received_events", 162 | "type": "User", 163 | "site_admin": false 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /test/fixtures/deployment_statuses/pending.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 12344, 3 | "state": "pending", 4 | "deployment": { 5 | "url": "https://api.github.com/repos/atmos/my-robot/deployments/123456", 6 | "id": 123456, 7 | "sha": "daf81923c94f7513ac840fa4fcc0dfcc11f32f74", 8 | "ref": "break-up-notifiers", 9 | "payload": { 10 | "name": "my-robot", 11 | "task": "deploy", 12 | "hosts": "", 13 | "notify": { 14 | "room": "danger", 15 | "user": "atmos", 16 | "adapter": "slack" 17 | }, 18 | "config": { 19 | "provider": "heroku", 20 | "repository": "atmos/my-robot", 21 | "environments": [ 22 | "production" 23 | ], 24 | "heroku_production_name": "zero-fucks-my-robot" 25 | } 26 | }, 27 | "environment": "production", 28 | "description": "Deploying from hubot-deploy-v0.6.0", 29 | "creator": { 30 | "login": "fakeatmos", 31 | "id": 704696, 32 | "avatar_url": "https://avatars.githubusercontent.com/u/704696?", 33 | "gravatar_id": "7310f196129141a0517f1fe8f0689472", 34 | "url": "https://api.github.com/users/fakeatmos", 35 | "html_url": "https://github.com/fakeatmos", 36 | "followers_url": "https://api.github.com/users/fakeatmos/followers", 37 | "following_url": "https://api.github.com/users/fakeatmos/following{/other_user}", 38 | "gists_url": "https://api.github.com/users/fakeatmos/gists{/gist_id}", 39 | "starred_url": "https://api.github.com/users/fakeatmos/starred{/owner}{/repo}", 40 | "subscriptions_url": "https://api.github.com/users/fakeatmos/subscriptions", 41 | "organizations_url": "https://api.github.com/users/fakeatmos/orgs", 42 | "repos_url": "https://api.github.com/users/fakeatmos/repos", 43 | "events_url": "https://api.github.com/users/fakeatmos/events{/privacy}", 44 | "received_events_url": "https://api.github.com/users/fakeatmos/received_events", 45 | "type": "User", 46 | "site_admin": false 47 | }, 48 | "created_at": "2014-05-20T08:07:39Z", 49 | "updated_at": "2014-05-20T08:07:39Z", 50 | "statuses_url": "https://api.github.com/repos/atmos/my-robot/deployments/123456/statuses" 51 | }, 52 | "target_url": "https://gist.github.com/fa77d9fb1fe41c3bb3a3ffb2c", 53 | "description": "Deploying from Heaven v0.5.5", 54 | "deployment_status": { 55 | "id": 12344, 56 | "state": "pending", 57 | "target_url": "https://gist.github.com/fa77d9fb1fe41c3bb3a3ffb2c", 58 | "description": "Deploying from Heaven v0.5.5" 59 | }, 60 | "repository": { 61 | "id": 1749093424, 62 | "name": "my-robot", 63 | "full_name": "atmos/my-robot", 64 | "owner": { 65 | "login": "atmos", 66 | "id": 6626297, 67 | "avatar_url": "https://avatars.githubusercontent.com/u/6626297?", 68 | "gravatar_id": null, 69 | "url": "https://api.github.com/users/atmos", 70 | "html_url": "https://github.com/atmos", 71 | "followers_url": "https://api.github.com/users/atmos/followers", 72 | "following_url": "https://api.github.com/users/atmos/following{/other_user}", 73 | "gists_url": "https://api.github.com/users/atmos/gists{/gist_id}", 74 | "starred_url": "https://api.github.com/users/atmos/starred{/owner}{/repo}", 75 | "subscriptions_url": "https://api.github.com/users/atmos/subscriptions", 76 | "organizations_url": "https://api.github.com/users/atmos/orgs", 77 | "repos_url": "https://api.github.com/users/atmos/repos", 78 | "events_url": "https://api.github.com/users/atmos/events{/privacy}", 79 | "received_events_url": "https://api.github.com/users/atmos/received_events", 80 | "type": "Organization", 81 | "site_admin": false 82 | }, 83 | "private": false, 84 | "html_url": "https://github.com/atmos/my-robot", 85 | "description": "Rails app for GitHub Flow Notifications - :walking: :tada:", 86 | "fork": true, 87 | "url": "https://api.github.com/repos/atmos/my-robot", 88 | "forks_url": "https://api.github.com/repos/atmos/my-robot/forks", 89 | "keys_url": "https://api.github.com/repos/atmos/my-robot/keys{/key_id}", 90 | "collaborators_url": "https://api.github.com/repos/atmos/my-robot/collaborators{/collaborator}", 91 | "teams_url": "https://api.github.com/repos/atmos/my-robot/teams", 92 | "hooks_url": "https://api.github.com/repos/atmos/my-robot/hooks", 93 | "issue_events_url": "https://api.github.com/repos/atmos/my-robot/issues/events{/number}", 94 | "events_url": "https://api.github.com/repos/atmos/my-robot/events", 95 | "assignees_url": "https://api.github.com/repos/atmos/my-robot/assignees{/user}", 96 | "branches_url": "https://api.github.com/repos/atmos/my-robot/branches{/branch}", 97 | "tags_url": "https://api.github.com/repos/atmos/my-robot/tags", 98 | "blobs_url": "https://api.github.com/repos/atmos/my-robot/git/blobs{/sha}", 99 | "git_tags_url": "https://api.github.com/repos/atmos/my-robot/git/tags{/sha}", 100 | "git_refs_url": "https://api.github.com/repos/atmos/my-robot/git/refs{/sha}", 101 | "trees_url": "https://api.github.com/repos/atmos/my-robot/git/trees{/sha}", 102 | "statuses_url": "https://api.github.com/repos/atmos/my-robot/statuses/{sha}", 103 | "languages_url": "https://api.github.com/repos/atmos/my-robot/languages", 104 | "stargazers_url": "https://api.github.com/repos/atmos/my-robot/stargazers", 105 | "contributors_url": "https://api.github.com/repos/atmos/my-robot/contributors", 106 | "subscribers_url": "https://api.github.com/repos/atmos/my-robot/subscribers", 107 | "subscription_url": "https://api.github.com/repos/atmos/my-robot/subscription", 108 | "commits_url": "https://api.github.com/repos/atmos/my-robot/commits{/sha}", 109 | "git_commits_url": "https://api.github.com/repos/atmos/my-robot/git/commits{/sha}", 110 | "comments_url": "https://api.github.com/repos/atmos/my-robot/comments{/number}", 111 | "issue_comment_url": "https://api.github.com/repos/atmos/my-robot/issues/comments/{number}", 112 | "contents_url": "https://api.github.com/repos/atmos/my-robot/contents/{+path}", 113 | "compare_url": "https://api.github.com/repos/atmos/my-robot/compare/{base}...{head}", 114 | "merges_url": "https://api.github.com/repos/atmos/my-robot/merges", 115 | "archive_url": "https://api.github.com/repos/atmos/my-robot/{archive_format}{/ref}", 116 | "downloads_url": "https://api.github.com/repos/atmos/my-robot/downloads", 117 | "issues_url": "https://api.github.com/repos/atmos/my-robot/issues{/number}", 118 | "pulls_url": "https://api.github.com/repos/atmos/my-robot/pulls{/number}", 119 | "milestones_url": "https://api.github.com/repos/atmos/my-robot/milestones{/number}", 120 | "notifications_url": "https://api.github.com/repos/atmos/my-robot/notifications{?since,all,participating}", 121 | "labels_url": "https://api.github.com/repos/atmos/my-robot/labels{/name}", 122 | "releases_url": "https://api.github.com/repos/atmos/my-robot/releases{/id}", 123 | "created_at": "2014-02-26T07:43:34Z", 124 | "updated_at": "2014-05-20T08:04:10Z", 125 | "pushed_at": "2014-05-20T08:04:10Z", 126 | "git_url": "git://github.com/atmos/my-robot.git", 127 | "ssh_url": "git@github.com:atmos/my-robot.git", 128 | "clone_url": "https://github.com/atmos/my-robot.git", 129 | "svn_url": "https://github.com/atmos/my-robot", 130 | "homepage": "", 131 | "size": 15649, 132 | "stargazers_count": 0, 133 | "watchers_count": 0, 134 | "language": "Ruby", 135 | "has_issues": false, 136 | "has_downloads": true, 137 | "has_wiki": false, 138 | "forks_count": 0, 139 | "mirror_url": null, 140 | "open_issues_count": 0, 141 | "forks": 0, 142 | "open_issues": 0, 143 | "watchers": 0, 144 | "default_branch": "master" 145 | }, 146 | "sender": { 147 | "login": "fakeatmos", 148 | "id": 704696, 149 | "avatar_url": "https://avatars.githubusercontent.com/u/704696?", 150 | "gravatar_id": "7310f196129141a0517f1fe8f0689472", 151 | "url": "https://api.github.com/users/fakeatmos", 152 | "html_url": "https://github.com/fakeatmos", 153 | "followers_url": "https://api.github.com/users/fakeatmos/followers", 154 | "following_url": "https://api.github.com/users/fakeatmos/following{/other_user}", 155 | "gists_url": "https://api.github.com/users/fakeatmos/gists{/gist_id}", 156 | "starred_url": "https://api.github.com/users/fakeatmos/starred{/owner}{/repo}", 157 | "subscriptions_url": "https://api.github.com/users/fakeatmos/subscriptions", 158 | "organizations_url": "https://api.github.com/users/fakeatmos/orgs", 159 | "repos_url": "https://api.github.com/users/fakeatmos/repos", 160 | "events_url": "https://api.github.com/users/fakeatmos/events{/privacy}", 161 | "received_events_url": "https://api.github.com/users/fakeatmos/received_events", 162 | "type": "User", 163 | "site_admin": false 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /test/fixtures/deployments.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "url": "https://api.github.com/repos/MyOrg/my-org-hubot/deployments/18731", 4 | "id": 18731, 5 | "sha": "8efb8c881eae523e41717b2c3dc88703adc46666", 6 | "ref": "master", 7 | "payload": { 8 | "name": "hubot", 9 | "task": "deploy", 10 | "hosts": "", 11 | "notify": { 12 | "room": "danger", 13 | "user_name": "atmos", 14 | "adapter": "slack" 15 | }, 16 | "config": { 17 | "provider": "heroku", 18 | "repository": "MyOrg/my-org-hubot", 19 | "auto_deploy": [ 20 | "production" 21 | ], 22 | "environments": [ 23 | "production" 24 | ], 25 | "required_contexts": [ 26 | "ci/circleci", 27 | "continuous-integration/travis-ci" 28 | ], 29 | "heroku_production_name": "my-org-hubot", 30 | "heroku_staging_name": "my-org-hubot-staging" 31 | } 32 | }, 33 | "environment": "production", 34 | "description": "deploy on production from hubot-deploy-v0.6.15", 35 | "creator": { 36 | "login": "fakeatmos", 37 | "id": 704696, 38 | "avatar_url": "https://avatars.githubusercontent.com/u/704696?", 39 | "gravatar_id": "7310f196129141a0517f1fe8f0689472", 40 | "url": "https://api.github.com/users/fakeatmos", 41 | "html_url": "https://github.com/fakeatmos", 42 | "followers_url": "https://api.github.com/users/fakeatmos/followers", 43 | "following_url": "https://api.github.com/users/fakeatmos/following{/other_user}", 44 | "gists_url": "https://api.github.com/users/fakeatmos/gists{/gist_id}", 45 | "starred_url": "https://api.github.com/users/fakeatmos/starred{/owner}{/repo}", 46 | "subscriptions_url": "https://api.github.com/users/fakeatmos/subscriptions", 47 | "organizations_url": "https://api.github.com/users/fakeatmos/orgs", 48 | "repos_url": "https://api.github.com/users/fakeatmos/repos", 49 | "events_url": "https://api.github.com/users/fakeatmos/events{/privacy}", 50 | "received_events_url": "https://api.github.com/users/fakeatmos/received_events", 51 | "type": "User", 52 | "site_admin": false 53 | }, 54 | "created_at": "2014-06-13T20:55:21Z", 55 | "updated_at": "2014-06-13T20:55:21Z", 56 | "statuses_url": "https://api.github.com/repos/MyOrg/my-org-hubot/deployments/18731/statuses" 57 | }, 58 | { 59 | "url": "https://api.github.com/repos/MyOrg/my-org-hubot/deployments/18730", 60 | "id": 18730, 61 | "sha": "8efb8c881eae523e41717b2c3dc88703adc46666", 62 | "ref": "8efb8c88", 63 | "payload": { 64 | "name": "hubot", 65 | "task": "deploy", 66 | "hosts": "", 67 | "notify": { 68 | "room": "danger", 69 | "user_name": "atmos", 70 | "adapter": "slack" 71 | }, 72 | "config": { 73 | "provider": "heroku", 74 | "repository": "MyOrg/my-org-hubot", 75 | "auto_deploy": [ 76 | "production" 77 | ], 78 | "environments": [ 79 | "production" 80 | ], 81 | "required_contexts": [ 82 | "ci/circleci", 83 | "continuous-integration/travis-ci" 84 | ], 85 | "heroku_production_name": "my-org-hubot", 86 | "heroku_staging_name": "my-org-hubot-staging" 87 | }, 88 | "actor": null, 89 | "sha": "8efb8c88" 90 | }, 91 | "environment": "production", 92 | "description": "Heaven auto deploy triggered by a commit status change", 93 | "creator": { 94 | "login": "fakeatmos", 95 | "id": 704696, 96 | "avatar_url": "https://avatars.githubusercontent.com/u/704696?", 97 | "gravatar_id": "7310f196129141a0517f1fe8f0689472", 98 | "url": "https://api.github.com/users/fakeatmos", 99 | "html_url": "https://github.com/fakeatmos", 100 | "followers_url": "https://api.github.com/users/fakeatmos/followers", 101 | "following_url": "https://api.github.com/users/fakeatmos/following{/other_user}", 102 | "gists_url": "https://api.github.com/users/fakeatmos/gists{/gist_id}", 103 | "starred_url": "https://api.github.com/users/fakeatmos/starred{/owner}{/repo}", 104 | "subscriptions_url": "https://api.github.com/users/fakeatmos/subscriptions", 105 | "organizations_url": "https://api.github.com/users/fakeatmos/orgs", 106 | "repos_url": "https://api.github.com/users/fakeatmos/repos", 107 | "events_url": "https://api.github.com/users/fakeatmos/events{/privacy}", 108 | "received_events_url": "https://api.github.com/users/fakeatmos/received_events", 109 | "type": "User", 110 | "site_admin": false 111 | }, 112 | "created_at": "2014-06-13T20:52:13Z", 113 | "updated_at": "2014-06-13T20:52:13Z", 114 | "statuses_url": "https://api.github.com/repos/MyOrg/my-org-hubot/deployments/18730/statuses" 115 | }, 116 | { 117 | "url": "https://api.github.com/repos/MyOrg/my-org-hubot/deployments/17866", 118 | "id": 17866, 119 | "sha": "f3d28e4c5339a6ce73f659650e4d7b371a2d795d", 120 | "ref": "f3d28e4c", 121 | "payload": { 122 | "name": "hubot", 123 | "task": "deploy", 124 | "hosts": "", 125 | "notify": { 126 | "room": "danger", 127 | "user_name": "atmos", 128 | "adapter": "slack" 129 | }, 130 | "config": { 131 | "provider": "heroku", 132 | "repository": "MyOrg/my-org-hubot", 133 | "auto_deploy": [ 134 | "production" 135 | ], 136 | "environments": [ 137 | "production" 138 | ], 139 | "required_contexts": [ 140 | "ci/circleci", 141 | "continuous-integration/travis-ci" 142 | ], 143 | "heroku_production_name": "my-org-hubot", 144 | "heroku_staging_name": "my-org-hubot-staging" 145 | }, 146 | "actor": null, 147 | "sha": "f3d28e4c" 148 | }, 149 | "environment": "production", 150 | "description": "Heaven auto deploy triggered by a commit status change", 151 | "creator": { 152 | "login": "fakeatmos", 153 | "id": 704696, 154 | "avatar_url": "https://avatars.githubusercontent.com/u/704696?", 155 | "gravatar_id": "7310f196129141a0517f1fe8f0689472", 156 | "url": "https://api.github.com/users/fakeatmos", 157 | "html_url": "https://github.com/fakeatmos", 158 | "followers_url": "https://api.github.com/users/fakeatmos/followers", 159 | "following_url": "https://api.github.com/users/fakeatmos/following{/other_user}", 160 | "gists_url": "https://api.github.com/users/fakeatmos/gists{/gist_id}", 161 | "starred_url": "https://api.github.com/users/fakeatmos/starred{/owner}{/repo}", 162 | "subscriptions_url": "https://api.github.com/users/fakeatmos/subscriptions", 163 | "organizations_url": "https://api.github.com/users/fakeatmos/orgs", 164 | "repos_url": "https://api.github.com/users/fakeatmos/repos", 165 | "events_url": "https://api.github.com/users/fakeatmos/events{/privacy}", 166 | "received_events_url": "https://api.github.com/users/fakeatmos/received_events", 167 | "type": "User", 168 | "site_admin": false 169 | }, 170 | "created_at": "2014-06-11T22:50:24Z", 171 | "updated_at": "2014-06-11T22:50:24Z", 172 | "statuses_url": "https://api.github.com/repos/MyOrg/my-org-hubot/deployments/17866/statuses" 173 | }, 174 | { 175 | "url": "https://api.github.com/repos/MyOrg/my-org-hubot/deployments/17863", 176 | "id": 17863, 177 | "sha": "ffcabfea4f6de1ccb3353291f5c346178a6d847d", 178 | "ref": "master", 179 | "payload": { 180 | "name": "hubot", 181 | "task": "deploy", 182 | "hosts": "", 183 | "notify": { 184 | "room": "danger", 185 | "user_name": "atmos", 186 | "adapter": "slack" 187 | }, 188 | "config": { 189 | "provider": "heroku", 190 | "repository": "MyOrg/my-org-hubot", 191 | "auto_deploy": [ 192 | "production" 193 | ], 194 | "environments": [ 195 | "production" 196 | ], 197 | "required_contexts": [ 198 | "ci/circleci", 199 | "continuous-integration/travis-ci" 200 | ], 201 | "heroku_production_name": "my-org-hubot", 202 | "heroku_staging_name": "my-org-hubot-staging" 203 | } 204 | }, 205 | "environment": "production", 206 | "description": "deploy on production from hubot-deploy-v0.6.13", 207 | "creator": { 208 | "login": "atmos", 209 | "id": 38, 210 | "avatar_url": "https://avatars.githubusercontent.com/u/38?", 211 | "gravatar_id": "a86224d72ce21cd9f5bee6784d4b06c7", 212 | "url": "https://api.github.com/users/atmos", 213 | "html_url": "https://github.com/atmos", 214 | "followers_url": "https://api.github.com/users/atmos/followers", 215 | "following_url": "https://api.github.com/users/atmos/following{/other_user}", 216 | "gists_url": "https://api.github.com/users/atmos/gists{/gist_id}", 217 | "starred_url": "https://api.github.com/users/atmos/starred{/owner}{/repo}", 218 | "subscriptions_url": "https://api.github.com/users/atmos/subscriptions", 219 | "organizations_url": "https://api.github.com/users/atmos/orgs", 220 | "repos_url": "https://api.github.com/users/atmos/repos", 221 | "events_url": "https://api.github.com/users/atmos/events{/privacy}", 222 | "received_events_url": "https://api.github.com/users/atmos/received_events", 223 | "type": "User", 224 | "site_admin": true 225 | }, 226 | "created_at": "2014-06-11T22:47:34Z", 227 | "updated_at": "2014-06-11T22:47:34Z", 228 | "statuses_url": "https://api.github.com/repos/MyOrg/my-org-hubot/deployments/17863/statuses" 229 | } 230 | ] 231 | -------------------------------------------------------------------------------- /test/fixtures/statuses/success.json: -------------------------------------------------------------------------------- 1 | { 2 | "sha": "1333c018defc50bfe123e2e7acbc83bb", 3 | "name": "atmos/my-robot", 4 | "target_url": "https://ci.atmos.org/1123112/output", 5 | "description": "Build #1123112 succeeded in 60s", 6 | "state": "success", 7 | "branches": [ 8 | { 9 | "name": "master", 10 | "commit": { 11 | "sha": "1333c018defc50bfe123e2e7acbc83bb", 12 | "url": "https://api.github.com/repos/atmos/my-robot/commits/1333c018defc50bfe123e2e7acbc83bb" 13 | } 14 | } 15 | ], 16 | "commit": { 17 | "sha": "1333c018defc50bfe123e2e7acbc83bb", 18 | "commit": { 19 | "author": { 20 | "name": "Charlie Somerville", 21 | "email": "charlie@charliesomerville.com", 22 | "date": "2014-03-12T04:11:37Z" 23 | }, 24 | "committer": { 25 | "name": "Charlie Somerville", 26 | "email": "charlie@charliesomerville.com", 27 | "date": "2014-03-12T04:11:37Z" 28 | }, 29 | "message": "Merge pull request #22820 from atmos/exclusive-bootstrap\n\nOnly run one script/bootstrap at a time", 30 | "tree": { 31 | "sha": "966f796e9e0e85aba08aa21a73960d9ef13a828c", 32 | "url": "https://api.github.com/repos/atmos/my-robot/git/trees/966f796e9e0e85aba08aa21a73960d9ef13a828c" 33 | }, 34 | "url": "https://api.github.com/repos/atmos/my-robot/git/commits/1333c018defc50bfe123e2e7acbc83bb", 35 | "comment_count": 0 36 | }, 37 | "url": "https://api.github.com/repos/atmos/my-robot/commits/1333c018defc50bfe123e2e7acbc83bb", 38 | "html_url": "https://github.com/atmos/my-robot/commit/1333c018defc50bfe123e2e7acbc83bb", 39 | "comments_url": "https://api.github.com/repos/atmos/my-robot/commits/1333c018defc50bfe123e2e7acbc83bb/comments", 40 | "author": { 41 | "login": "charliesome", 42 | "id": 179065, 43 | "avatar_url": "https://gravatar.com/avatar/bcb6acc9d0d9bef99e033b36c3d32ca9?d=https%3A%2F%2Fidenticons.github.com%2F9b5cccda2e7df730754dbb4164e1b17a.png&r=x", 44 | "gravatar_id": "bcb6acc9d0d9bef99e033b36c3d32ca9", 45 | "url": "https://api.github.com/users/charliesome", 46 | "html_url": "https://github.com/charliesome", 47 | "followers_url": "https://api.github.com/users/charliesome/followers", 48 | "following_url": "https://api.github.com/users/charliesome/following{/other_user}", 49 | "gists_url": "https://api.github.com/users/charliesome/gists{/gist_id}", 50 | "starred_url": "https://api.github.com/users/charliesome/starred{/owner}{/repo}", 51 | "subscriptions_url": "https://api.github.com/users/charliesome/subscriptions", 52 | "organizations_url": "https://api.github.com/users/charliesome/orgs", 53 | "repos_url": "https://api.github.com/users/charliesome/repos", 54 | "events_url": "https://api.github.com/users/charliesome/events{/privacy}", 55 | "received_events_url": "https://api.github.com/users/charliesome/received_events", 56 | "type": "User" 57 | }, 58 | "committer": { 59 | "login": "charliesome", 60 | "id": 179065, 61 | "avatar_url": "https://gravatar.com/avatar/bcb6acc9d0d9bef99e033b36c3d32ca9?d=https%3A%2F%2Fidenticons.github.com%2F9b5cccda2e7df730754dbb4164e1b17a.png&r=x", 62 | "gravatar_id": "bcb6acc9d0d9bef99e033b36c3d32ca9", 63 | "url": "https://api.github.com/users/charliesome", 64 | "html_url": "https://github.com/charliesome", 65 | "followers_url": "https://api.github.com/users/charliesome/followers", 66 | "following_url": "https://api.github.com/users/charliesome/following{/other_user}", 67 | "gists_url": "https://api.github.com/users/charliesome/gists{/gist_id}", 68 | "starred_url": "https://api.github.com/users/charliesome/starred{/owner}{/repo}", 69 | "subscriptions_url": "https://api.github.com/users/charliesome/subscriptions", 70 | "organizations_url": "https://api.github.com/users/charliesome/orgs", 71 | "repos_url": "https://api.github.com/users/charliesome/repos", 72 | "events_url": "https://api.github.com/users/charliesome/events{/privacy}", 73 | "received_events_url": "https://api.github.com/users/charliesome/received_events", 74 | "type": "User" 75 | }, 76 | "parents": [ 77 | { 78 | "sha": "b2646d01c9ef5627023f96d3d8410c4a", 79 | "url": "https://api.github.com/repos/atmos/my-robot/commits/b2646d01c9ef5627023f96d3d8410c4a", 80 | "html_url": "https://github.com/atmos/my-robot/commit/b2646d01c9ef5627023f96d3d8410c4a" 81 | }, 82 | { 83 | "sha": "0ab50e368ccafe91b39cf9f311230f67", 84 | "url": "https://api.github.com/repos/atmos/my-robot/commits/0ab50e368ccafe91b39cf9f311230f67", 85 | "html_url": "https://github.com/atmos/my-robot/commit/0ab50e368ccafe91b39cf9f311230f67" 86 | } 87 | ] 88 | }, 89 | "context": "Janky (github)", 90 | "repository": { 91 | "id": 3, 92 | "name": "github", 93 | "full_name": "atmos/my-robot", 94 | "owner": { 95 | "login": "atmos", 96 | "id": 9919, 97 | "avatar_url": "https://gravatar.com/avatar/61024896f291303615bcd4f7a0dcfb74?d=https%3A%2F%2Fidenticons.github.com%2Fae816a80e4c1c56caa2eb4e1819cbb2f.png&r=x", 98 | "gravatar_id": "61024896f291303615bcd4f7a0dcfb74", 99 | "url": "https://api.github.com/users/atmos", 100 | "html_url": "https://github.com/github", 101 | "followers_url": "https://api.github.com/users/followers", 102 | "following_url": "https://api.github.com/users/following{/other_user}", 103 | "gists_url": "https://api.github.com/users/gists{/gist_id}", 104 | "starred_url": "https://api.github.com/users/starred{/owner}{/repo}", 105 | "subscriptions_url": "https://api.github.com/users/subscriptions", 106 | "organizations_url": "https://api.github.com/users/orgs", 107 | "repos_url": "https://api.github.com/users/repos", 108 | "events_url": "https://api.github.com/users/events{/privacy}", 109 | "received_events_url": "https://api.github.com/users/received_events", 110 | "type": "User" 111 | }, 112 | "private": true, 113 | "html_url": "https://github.com/atmos/my-robot", 114 | "description": "You’re lookin’ at it.", 115 | "fork": false, 116 | "url": "https://api.github.com/repos/atmos/my-robot", 117 | "forks_url": "https://api.github.com/repos/atmos/my-robot/forks", 118 | "keys_url": "https://api.github.com/repos/atmos/my-robot/keys{/key_id}", 119 | "collaborators_url": "https://api.github.com/repos/atmos/my-robot/collaborators{/collaborator}", 120 | "teams_url": "https://api.github.com/repos/atmos/my-robot/teams", 121 | "hooks_url": "https://api.github.com/repos/atmos/my-robot/hooks", 122 | "issue_events_url": "https://api.github.com/repos/atmos/my-robot/issues/events{/number}", 123 | "events_url": "https://api.github.com/repos/atmos/my-robot/events", 124 | "assignees_url": "https://api.github.com/repos/atmos/my-robot/assignees{/user}", 125 | "branches_url": "https://api.github.com/repos/atmos/my-robot/branches{/branch}", 126 | "tags_url": "https://api.github.com/repos/atmos/my-robot/tags", 127 | "blobs_url": "https://api.github.com/repos/atmos/my-robot/git/blobs{/sha}", 128 | "git_tags_url": "https://api.github.com/repos/atmos/my-robot/git/tags{/sha}", 129 | "git_refs_url": "https://api.github.com/repos/atmos/my-robot/git/refs{/sha}", 130 | "trees_url": "https://api.github.com/repos/atmos/my-robot/git/trees{/sha}", 131 | "statuses_url": "https://api.github.com/repos/atmos/my-robot/statuses/{sha}", 132 | "languages_url": "https://api.github.com/repos/atmos/my-robot/languages", 133 | "stargazers_url": "https://api.github.com/repos/atmos/my-robot/stargazers", 134 | "contributors_url": "https://api.github.com/repos/atmos/my-robot/contributors", 135 | "subscribers_url": "https://api.github.com/repos/atmos/my-robot/subscribers", 136 | "subscription_url": "https://api.github.com/repos/atmos/my-robot/subscription", 137 | "commits_url": "https://api.github.com/repos/atmos/my-robot/commits{/sha}", 138 | "git_commits_url": "https://api.github.com/repos/atmos/my-robot/git/commits{/sha}", 139 | "comments_url": "https://api.github.com/repos/atmos/my-robot/comments{/number}", 140 | "issue_comment_url": "https://api.github.com/repos/atmos/my-robot/issues/comments/{number}", 141 | "contents_url": "https://api.github.com/repos/atmos/my-robot/contents/{+path}", 142 | "compare_url": "https://api.github.com/repos/atmos/my-robot/compare/{base}...{head}", 143 | "merges_url": "https://api.github.com/repos/atmos/my-robot/merges", 144 | "archive_url": "https://api.github.com/repos/atmos/my-robot/{archive_format}{/ref}", 145 | "downloads_url": "https://api.github.com/repos/atmos/my-robot/downloads", 146 | "issues_url": "https://api.github.com/repos/atmos/my-robot/issues{/number}", 147 | "pulls_url": "https://api.github.com/repos/atmos/my-robot/pulls{/number}", 148 | "milestones_url": "https://api.github.com/repos/atmos/my-robot/milestones{/number}", 149 | "notifications_url": "https://api.github.com/repos/atmos/my-robot/notifications{?since,all,participating}", 150 | "labels_url": "https://api.github.com/repos/atmos/my-robot/labels{/name}", 151 | "releases_url": "https://api.github.com/repos/atmos/my-robot/releases{/id}", 152 | "created_at": "2007-10-29T14:37:16Z", 153 | "updated_at": "2014-03-12T04:11:40Z", 154 | "pushed_at": "2014-03-12T04:11:41Z", 155 | "git_url": "git://github.com/atmos/my-robot.git", 156 | "ssh_url": "git@github.com:atmos/my-robot.git", 157 | "clone_url": "https://github.com/atmos/my-robot.git", 158 | "svn_url": "https://github.com/atmos/my-robot", 159 | "homepage": "https://github.com", 160 | "size": 1635911, 161 | "stargazers_count": 45, 162 | "watchers_count": 45, 163 | "language": "Ruby", 164 | "has_issues": true, 165 | "has_downloads": false, 166 | "has_wiki": false, 167 | "forks_count": 38, 168 | "mirror_url": null, 169 | "open_issues_count": 1188, 170 | "forks": 38, 171 | "open_issues": 1188, 172 | "watchers": 45, 173 | "default_branch": "master" 174 | }, 175 | "sender": { 176 | "login": "hubot", 177 | "id": 480938, 178 | "avatar_url": "https://gravatar.com/avatar/64afcfdf8e40d081a80961eae290890c?d=https%3A%2F%2Fidenticons.github.com%2F69e03cf56053aff3d27500963b3f6828.png&r=x", 179 | "gravatar_id": "64afcfdf8e40d081a80961eae290890c", 180 | "url": "https://api.github.com/users/hubot", 181 | "html_url": "https://github.com/hubot", 182 | "followers_url": "https://api.github.com/users/hubot/followers", 183 | "following_url": "https://api.github.com/users/hubot/following{/other_user}", 184 | "gists_url": "https://api.github.com/users/hubot/gists{/gist_id}", 185 | "starred_url": "https://api.github.com/users/hubot/starred{/owner}{/repo}", 186 | "subscriptions_url": "https://api.github.com/users/hubot/subscriptions", 187 | "organizations_url": "https://api.github.com/users/hubot/orgs", 188 | "repos_url": "https://api.github.com/users/hubot/repos", 189 | "events_url": "https://api.github.com/users/hubot/events{/privacy}", 190 | "received_events_url": "https://api.github.com/users/hubot/received_events", 191 | "type": "User" 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /test/fixtures/statuses/pending.json: -------------------------------------------------------------------------------- 1 | { 2 | "sha": "1333c018defc50bfe123e2e7acbc83bb", 3 | "name": "atmos/my-robot", 4 | "target_url": "https://ci.atmos.org/1123112/output", 5 | "description": "Build #1123112 started", 6 | "state": "pending", 7 | "branches": [ 8 | { 9 | "name": "master", 10 | "commit": { 11 | "sha": "1333c018defc50bfe123e2e7acbc83bb", 12 | "url": "https://api.github.com/repos/atmos/my-robot/commits/1333c018defc50bfe123e2e7acbc83bb" 13 | } 14 | } 15 | ], 16 | "commit": { 17 | "sha": "1333c018defc50bfe123e2e7acbc83bb", 18 | "commit": { 19 | "author": { 20 | "name": "Charlie Somerville", 21 | "email": "charlie@charliesomerville.com", 22 | "date": "2014-03-12T04:11:37Z" 23 | }, 24 | "committer": { 25 | "name": "Charlie Somerville", 26 | "email": "charlie@charliesomerville.com", 27 | "date": "2014-03-12T04:11:37Z" 28 | }, 29 | "message": "Merge pull request #22820 from atmos/exclusive-bootstrap\n\nOnly run one script/bootstrap at a time", 30 | "tree": { 31 | "sha": "966f796e9e0e85aba08aa21a73960d9ef13a828c", 32 | "url": "https://api.github.com/repos/atmos/my-robot/git/trees/966f796e9e0e85aba08aa21a73960d9ef13a828c" 33 | }, 34 | "url": "https://api.github.com/repos/atmos/my-robot/git/commits/1333c018defc50bfe123e2e7acbc83bb", 35 | "comment_count": 0 36 | }, 37 | "url": "https://api.github.com/repos/atmos/my-robot/commits/1333c018defc50bfe123e2e7acbc83bb", 38 | "html_url": "https://github.com/atmos/my-robot/commit/1333c018defc50bfe123e2e7acbc83bb", 39 | "comments_url": "https://api.github.com/repos/atmos/my-robot/commits/1333c018defc50bfe123e2e7acbc83bb/comments", 40 | "author": { 41 | "login": "charliesome", 42 | "id": 179065, 43 | "avatar_url": "https://gravatar.com/avatar/bcb6acc9d0d9bef99e033b36c3d32ca9?d=https%3A%2F%2Fidenticons.github.com%2F9b5cccda2e7df730754dbb4164e1b17a.png&r=x", 44 | "gravatar_id": "bcb6acc9d0d9bef99e033b36c3d32ca9", 45 | "url": "https://api.github.com/users/charliesome", 46 | "html_url": "https://github.com/charliesome", 47 | "followers_url": "https://api.github.com/users/charliesome/followers", 48 | "following_url": "https://api.github.com/users/charliesome/following{/other_user}", 49 | "gists_url": "https://api.github.com/users/charliesome/gists{/gist_id}", 50 | "starred_url": "https://api.github.com/users/charliesome/starred{/owner}{/repo}", 51 | "subscriptions_url": "https://api.github.com/users/charliesome/subscriptions", 52 | "organizations_url": "https://api.github.com/users/charliesome/orgs", 53 | "repos_url": "https://api.github.com/users/charliesome/repos", 54 | "events_url": "https://api.github.com/users/charliesome/events{/privacy}", 55 | "received_events_url": "https://api.github.com/users/charliesome/received_events", 56 | "type": "User" 57 | }, 58 | "committer": { 59 | "login": "charliesome", 60 | "id": 179065, 61 | "avatar_url": "https://gravatar.com/avatar/bcb6acc9d0d9bef99e033b36c3d32ca9?d=https%3A%2F%2Fidenticons.github.com%2F9b5cccda2e7df730754dbb4164e1b17a.png&r=x", 62 | "gravatar_id": "bcb6acc9d0d9bef99e033b36c3d32ca9", 63 | "url": "https://api.github.com/users/charliesome", 64 | "html_url": "https://github.com/charliesome", 65 | "followers_url": "https://api.github.com/users/charliesome/followers", 66 | "following_url": "https://api.github.com/users/charliesome/following{/other_user}", 67 | "gists_url": "https://api.github.com/users/charliesome/gists{/gist_id}", 68 | "starred_url": "https://api.github.com/users/charliesome/starred{/owner}{/repo}", 69 | "subscriptions_url": "https://api.github.com/users/charliesome/subscriptions", 70 | "organizations_url": "https://api.github.com/users/charliesome/orgs", 71 | "repos_url": "https://api.github.com/users/charliesome/repos", 72 | "events_url": "https://api.github.com/users/charliesome/events{/privacy}", 73 | "received_events_url": "https://api.github.com/users/charliesome/received_events", 74 | "type": "User" 75 | }, 76 | "parents": [ 77 | { 78 | "sha": "b2646d01c9ef5627023f96d3d8410c4a", 79 | "url": "https://api.github.com/repos/atmos/my-robot/commits/b2646d01c9ef5627023f96d3d8410c4a", 80 | "html_url": "https://github.com/atmos/my-robot/commit/b2646d01c9ef5627023f96d3d8410c4a" 81 | }, 82 | { 83 | "sha": "0ab50e368ccafe91b39cf9f311230f67", 84 | "url": "https://api.github.com/repos/atmos/my-robot/commits/0ab50e368ccafe91b39cf9f311230f67", 85 | "html_url": "https://github.com/atmos/my-robot/commit/0ab50e368ccafe91b39cf9f311230f67" 86 | } 87 | ] 88 | }, 89 | "context": "Janky (github)", 90 | "repository": { 91 | "id": 3, 92 | "name": "atmos", 93 | "full_name": "atmos/my-robot", 94 | "owner": { 95 | "login": "github", 96 | "id": 9919, 97 | "avatar_url": "https://gravatar.com/avatar/61024896f291303615bcd4f7a0dcfb74?d=https%3A%2F%2Fidenticons.github.com%2Fae816a80e4c1c56caa2eb4e1819cbb2f.png&r=x", 98 | "gravatar_id": "61024896f291303615bcd4f7a0dcfb74", 99 | "url": "https://api.github.com/users/atmos", 100 | "html_url": "https://github.com/github", 101 | "followers_url": "https://api.github.com/users/atmos/followers", 102 | "following_url": "https://api.github.com/users/atmos/following{/other_user}", 103 | "gists_url": "https://api.github.com/users/atmos/gists{/gist_id}", 104 | "starred_url": "https://api.github.com/users/atmos/starred{/owner}{/repo}", 105 | "subscriptions_url": "https://api.github.com/users/atmos/subscriptions", 106 | "organizations_url": "https://api.github.com/users/atmos/orgs", 107 | "repos_url": "https://api.github.com/users/atmos/repos", 108 | "events_url": "https://api.github.com/users/atmos/events{/privacy}", 109 | "received_events_url": "https://api.github.com/users/atmos/received_events", 110 | "type": "User" 111 | }, 112 | "private": true, 113 | "html_url": "https://github.com/atmos/my-robot", 114 | "description": "My Robawt", 115 | "fork": false, 116 | "url": "https://api.github.com/repos/atmos/my-robot", 117 | "forks_url": "https://api.github.com/repos/atmos/my-robot/forks", 118 | "keys_url": "https://api.github.com/repos/atmos/my-robot/keys{/key_id}", 119 | "collaborators_url": "https://api.github.com/repos/atmos/my-robot/collaborators{/collaborator}", 120 | "teams_url": "https://api.github.com/repos/atmos/my-robot/teams", 121 | "hooks_url": "https://api.github.com/repos/atmos/my-robot/hooks", 122 | "issue_events_url": "https://api.github.com/repos/atmos/my-robot/issues/events{/number}", 123 | "events_url": "https://api.github.com/repos/atmos/my-robot/events", 124 | "assignees_url": "https://api.github.com/repos/atmos/my-robot/assignees{/user}", 125 | "branches_url": "https://api.github.com/repos/atmos/my-robot/branches{/branch}", 126 | "tags_url": "https://api.github.com/repos/atmos/my-robot/tags", 127 | "blobs_url": "https://api.github.com/repos/atmos/my-robot/git/blobs{/sha}", 128 | "git_tags_url": "https://api.github.com/repos/atmos/my-robot/git/tags{/sha}", 129 | "git_refs_url": "https://api.github.com/repos/atmos/my-robot/git/refs{/sha}", 130 | "trees_url": "https://api.github.com/repos/atmos/my-robot/git/trees{/sha}", 131 | "statuses_url": "https://api.github.com/repos/atmos/my-robot/statuses/{sha}", 132 | "languages_url": "https://api.github.com/repos/atmos/my-robot/languages", 133 | "stargazers_url": "https://api.github.com/repos/atmos/my-robot/stargazers", 134 | "contributors_url": "https://api.github.com/repos/atmos/my-robot/contributors", 135 | "subscribers_url": "https://api.github.com/repos/atmos/my-robot/subscribers", 136 | "subscription_url": "https://api.github.com/repos/atmos/my-robot/subscription", 137 | "commits_url": "https://api.github.com/repos/atmos/my-robot/commits{/sha}", 138 | "git_commits_url": "https://api.github.com/repos/atmos/my-robot/git/commits{/sha}", 139 | "comments_url": "https://api.github.com/repos/atmos/my-robot/comments{/number}", 140 | "issue_comment_url": "https://api.github.com/repos/atmos/my-robot/issues/comments/{number}", 141 | "contents_url": "https://api.github.com/repos/atmos/my-robot/contents/{+path}", 142 | "compare_url": "https://api.github.com/repos/atmos/my-robot/compare/{base}...{head}", 143 | "merges_url": "https://api.github.com/repos/atmos/my-robot/merges", 144 | "archive_url": "https://api.github.com/repos/atmos/my-robot/{archive_format}{/ref}", 145 | "downloads_url": "https://api.github.com/repos/atmos/my-robot/downloads", 146 | "issues_url": "https://api.github.com/repos/atmos/my-robot/issues{/number}", 147 | "pulls_url": "https://api.github.com/repos/atmos/my-robot/pulls{/number}", 148 | "milestones_url": "https://api.github.com/repos/atmos/my-robot/milestones{/number}", 149 | "notifications_url": "https://api.github.com/repos/atmos/my-robot/notifications{?since,all,participating}", 150 | "labels_url": "https://api.github.com/repos/atmos/my-robot/labels{/name}", 151 | "releases_url": "https://api.github.com/repos/atmos/my-robot/releases{/id}", 152 | "created_at": "2007-10-29T14:37:16Z", 153 | "updated_at": "2014-03-12T04:11:40Z", 154 | "pushed_at": "2014-03-12T04:11:41Z", 155 | "git_url": "git://github.com/atmos/my-robot.git", 156 | "ssh_url": "git@github.com:atmos/my-robot.git", 157 | "clone_url": "https://github.com/atmos/my-robot.git", 158 | "svn_url": "https://github.com/atmos/my-robot", 159 | "homepage": "https://github.com", 160 | "size": 1635911, 161 | "stargazers_count": 45, 162 | "watchers_count": 45, 163 | "language": "Ruby", 164 | "has_issues": true, 165 | "has_downloads": false, 166 | "has_wiki": false, 167 | "forks_count": 38, 168 | "mirror_url": null, 169 | "open_issues_count": 1188, 170 | "forks": 38, 171 | "open_issues": 1188, 172 | "watchers": 45, 173 | "default_branch": "master" 174 | }, 175 | "sender": { 176 | "login": "hubot", 177 | "id": 480938, 178 | "avatar_url": "https://gravatar.com/avatar/64afcfdf8e40d081a80961eae290890c?d=https%3A%2F%2Fidenticons.github.com%2F69e03cf56053aff3d27500963b3f6828.png&r=x", 179 | "gravatar_id": "64afcfdf8e40d081a80961eae290890c", 180 | "url": "https://api.github.com/users/hubot", 181 | "html_url": "https://github.com/hubot", 182 | "followers_url": "https://api.github.com/users/hubot/followers", 183 | "following_url": "https://api.github.com/users/hubot/following{/other_user}", 184 | "gists_url": "https://api.github.com/users/hubot/gists{/gist_id}", 185 | "starred_url": "https://api.github.com/users/hubot/starred{/owner}{/repo}", 186 | "subscriptions_url": "https://api.github.com/users/hubot/subscriptions", 187 | "organizations_url": "https://api.github.com/users/hubot/orgs", 188 | "repos_url": "https://api.github.com/users/hubot/repos", 189 | "events_url": "https://api.github.com/users/hubot/events{/privacy}", 190 | "received_events_url": "https://api.github.com/users/hubot/received_events", 191 | "type": "User" 192 | } 193 | } 194 | --------------------------------------------------------------------------------