├── .gitignore ├── .gitmodules ├── .rspec ├── .travis.yml ├── Gemfile ├── Gemfile.lock ├── Procfile ├── README.md ├── Rakefile ├── config ├── boot.rb ├── config.sample.rb └── slack-deploy-bot.eye.sample ├── console ├── images ├── changelog-command.png ├── deploy-command.png ├── deploy-failed.png └── help-command.png ├── logs └── .gitkeep ├── run.rb ├── slack-deploy-bot.rb ├── slack-deploy-bot ├── about.rb ├── app_config_validator.rb ├── bot.rb ├── commands.rb ├── commands │ ├── about.rb │ ├── changelog.rb │ └── deploy.rb ├── utils.rb └── version.rb └── spec ├── slack-deploy-bot ├── app_config_validator_spec.rb ├── bot_spec.rb └── commands │ ├── about_spec.rb │ ├── changelog_spec.rb │ └── deploy_spec.rb ├── spec_helper.rb ├── support ├── concurrency_mock.rb ├── connected_client.rb ├── remote_repo.git │ ├── HEAD │ ├── config │ ├── description │ ├── hooks │ │ ├── applypatch-msg.sample │ │ ├── commit-msg.sample │ │ ├── post-update.sample │ │ ├── pre-applypatch.sample │ │ ├── pre-commit.sample │ │ ├── pre-push.sample │ │ ├── pre-rebase.sample │ │ ├── prepare-commit-msg.sample │ │ └── update.sample │ ├── info │ │ └── exclude │ ├── objects │ │ ├── 10 │ │ │ └── c3096a19741a6ced5715569445f5072511f4de │ │ ├── 16 │ │ │ └── 935f996a17fb5013491b2560c6b3043e718e82 │ │ ├── 36 │ │ │ └── 80008f06aae8321faa9cf22c031c99b39d1cff │ │ ├── 37 │ │ │ └── 9a4c986e3265e08e9da42c56ec747619c56a33 │ │ ├── 43 │ │ │ └── 0ff1dcee43d2e8a12c3ba36f5ff20e46136152 │ │ ├── 47 │ │ │ ├── 65132ed192e9fb14fff274b364677315fd3629 │ │ │ └── b73a126cec824d08d6b734f5def7fc374751a2 │ │ ├── 50 │ │ │ └── 80851e4b508fe4e67478dca9c9f97c0176c4d0 │ │ ├── 51 │ │ │ └── a115a76f6a81b5f493142d69b12917d3a0516d │ │ ├── 72 │ │ │ └── dc5019af8538edc560f2ee6cf600154d5ff76a │ │ ├── 95 │ │ │ └── 99fdeb034597d90d72d2f58396dee096885b79 │ │ ├── 06 │ │ │ └── 0fc29c2a831131a46e11d4c492a92c0fd124d0 │ │ ├── 2f │ │ │ └── 5425622b1c148ca0b3c458430fa9e4c012b611 │ │ ├── 4b │ │ │ └── b84c00154e676be22ae837d534edb4ec5cecd1 │ │ ├── 4d │ │ │ └── c1968fb2fcba099965d54e2d1d1c4eb6041774 │ │ ├── 8d │ │ │ └── 629c409367c902311ec58a0aaa5e5b82a953bd │ │ ├── 9c │ │ │ └── e89a6b4ae562bbd30ccff8e2cd06b373cb813a │ │ ├── 9d │ │ │ └── f352c6fc26a5c8acf2b44b32b896e4e8031cf5 │ │ ├── a5 │ │ │ └── 09b88fc4b226e0599fb0d6bead1805b5489786 │ │ ├── aa │ │ │ └── 0f7cbbbc4d8c013daa2b5be8c14e0b4c0032b6 │ │ ├── ad │ │ │ └── 24149d789e59d4b5f9ce41cda90110ca0f98b7 │ │ ├── ae │ │ │ └── fde3a01f6e10d72fd4899ce14c8b2654d3eb45 │ │ ├── b8 │ │ │ └── a1fd79948bc23a151648eb49bd7cbf77405353 │ │ ├── be │ │ │ └── 1c293b15d3942015b75c39688ce1859bc238e5 │ │ ├── e0 │ │ │ └── 94ae5f14dc92b216a0dcb1a66200f6f5ba07c7 │ │ ├── e6 │ │ │ └── 9de29bb2d1d6434b8b29ae775ad8c2e48c5391 │ │ ├── f1 │ │ │ └── 73c70adcbe9e011b14213bc49e4135a5a20c4d │ │ └── f7 │ │ │ └── 053cb5cf49a82236e947a3c392f7bc76ccdc5c │ └── refs │ │ └── heads │ │ ├── master │ │ └── my-feature ├── respond_with_slack_message.rb ├── say_to_bot.rb └── slack_deploy_bot.rb └── vcr └── rtm_start.yml /.gitignore: -------------------------------------------------------------------------------- 1 | .slack-api-token 2 | *.log 3 | config/slack-deploy-bot.eye 4 | config/config.rb 5 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "spec/support/dummy_app"] 2 | path = spec/support/dummy_app 3 | url = https://github.com/accessd/sample_app.git 4 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | 3 | cache: bundler 4 | 5 | matrix: 6 | include: 7 | - rvm: 2.3.0 8 | - rvm: ruby-head 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | gem 'slack-ruby-bot' 4 | gem 'celluloid-io' 5 | gem 'dry-validation' 6 | 7 | group :development, :test do 8 | gem 'rake' 9 | gem 'pry' 10 | gem 'rb-readline' 11 | gem 'rspec' 12 | gem 'rack-test' 13 | gem 'vcr' 14 | gem 'webmock' 15 | end 16 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | activesupport (5.0.1) 5 | concurrent-ruby (~> 1.0, >= 1.0.2) 6 | i18n (~> 0.7) 7 | minitest (~> 5.1) 8 | tzinfo (~> 1.1) 9 | addressable (2.5.0) 10 | public_suffix (~> 2.0, >= 2.0.2) 11 | celluloid (0.17.3) 12 | celluloid-essentials 13 | celluloid-extras 14 | celluloid-fsm 15 | celluloid-pool 16 | celluloid-supervision 17 | timers (>= 4.1.1) 18 | celluloid-essentials (0.20.5) 19 | timers (>= 4.1.1) 20 | celluloid-extras (0.20.5) 21 | timers (>= 4.1.1) 22 | celluloid-fsm (0.20.5) 23 | timers (>= 4.1.1) 24 | celluloid-io (0.17.3) 25 | celluloid (>= 0.17.2) 26 | nio4r (>= 1.1) 27 | timers (>= 4.1.1) 28 | celluloid-pool (0.20.5) 29 | timers (>= 4.1.1) 30 | celluloid-supervision (0.20.6) 31 | timers (>= 4.1.1) 32 | coderay (1.1.1) 33 | concurrent-ruby (1.0.4) 34 | crack (0.4.3) 35 | safe_yaml (~> 1.0.0) 36 | diff-lcs (1.3) 37 | dry-configurable (0.6.1) 38 | concurrent-ruby (~> 1.0) 39 | dry-container (0.6.0) 40 | concurrent-ruby (~> 1.0) 41 | dry-configurable (~> 0.1, >= 0.1.3) 42 | dry-core (0.2.4) 43 | concurrent-ruby (~> 1.0) 44 | dry-equalizer (0.2.0) 45 | dry-logic (0.4.1) 46 | dry-container (~> 0.2, >= 0.2.6) 47 | dry-core (~> 0.2) 48 | dry-equalizer (~> 0.2) 49 | dry-types (0.9.4) 50 | concurrent-ruby (~> 1.0) 51 | dry-configurable (~> 0.1) 52 | dry-container (~> 0.3) 53 | dry-core (~> 0.2, >= 0.2.1) 54 | dry-equalizer (~> 0.2) 55 | dry-logic (~> 0.4, >= 0.4.0) 56 | inflecto (~> 0.0.0, >= 0.0.2) 57 | dry-validation (0.10.5) 58 | concurrent-ruby (~> 1.0) 59 | dry-configurable (~> 0.1, >= 0.1.3) 60 | dry-core (~> 0.2, >= 0.2.1) 61 | dry-equalizer (~> 0.2) 62 | dry-logic (~> 0.4, >= 0.4.0) 63 | dry-types (~> 0.9, >= 0.9.0) 64 | faraday (0.11.0) 65 | multipart-post (>= 1.2, < 3) 66 | faraday_middleware (0.11.0.1) 67 | faraday (>= 0.7.4, < 1.0) 68 | gli (2.15.0) 69 | hashdiff (0.3.2) 70 | hashie (3.5.5) 71 | hitimes (1.2.4) 72 | i18n (0.8.1) 73 | inflecto (0.0.2) 74 | json (2.0.3) 75 | method_source (0.8.2) 76 | minitest (5.10.1) 77 | multipart-post (2.0.0) 78 | nio4r (2.0.0) 79 | pry (0.10.4) 80 | coderay (~> 1.1.0) 81 | method_source (~> 0.8.1) 82 | slop (~> 3.4) 83 | public_suffix (2.0.5) 84 | rack (2.0.1) 85 | rack-test (0.6.3) 86 | rack (>= 1.0) 87 | rake (10.5.0) 88 | rb-readline (0.5.4) 89 | rspec (3.5.0) 90 | rspec-core (~> 3.5.0) 91 | rspec-expectations (~> 3.5.0) 92 | rspec-mocks (~> 3.5.0) 93 | rspec-core (3.5.4) 94 | rspec-support (~> 3.5.0) 95 | rspec-expectations (3.5.0) 96 | diff-lcs (>= 1.2.0, < 2.0) 97 | rspec-support (~> 3.5.0) 98 | rspec-mocks (3.5.0) 99 | diff-lcs (>= 1.2.0, < 2.0) 100 | rspec-support (~> 3.5.0) 101 | rspec-support (3.5.0) 102 | safe_yaml (1.0.4) 103 | slack-ruby-bot (0.10.1) 104 | hashie 105 | slack-ruby-client (>= 0.6.0) 106 | slack-ruby-client (0.7.9) 107 | activesupport 108 | faraday (>= 0.9) 109 | faraday_middleware 110 | gli 111 | hashie 112 | json 113 | websocket-driver 114 | slop (3.6.0) 115 | thread_safe (0.3.6) 116 | timers (4.1.2) 117 | hitimes 118 | tzinfo (1.2.2) 119 | thread_safe (~> 0.1) 120 | vcr (3.0.3) 121 | webmock (2.3.2) 122 | addressable (>= 2.3.6) 123 | crack (>= 0.3.2) 124 | hashdiff 125 | websocket-driver (0.6.5) 126 | websocket-extensions (>= 0.1.0) 127 | websocket-extensions (0.1.2) 128 | 129 | PLATFORMS 130 | ruby 131 | 132 | DEPENDENCIES 133 | celluloid-io 134 | dry-validation 135 | pry 136 | rack-test 137 | rake 138 | rb-readline 139 | rspec 140 | slack-ruby-bot 141 | vcr 142 | webmock 143 | 144 | BUNDLED WITH 145 | 1.11.2 146 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | bot: bundle exec ruby run.rb 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Slack Deploy Bot 2 | 3 | [![Build Status](https://travis-ci.org/accessd/slack-deploy-bot.svg?branch=master)](https://travis-ci.org/accessd/slack-deploy-bot) 4 | 5 | A Slack bot that helps you to deploy your apps. 6 | 7 | ## What it can do? 8 | 9 | For help just type **help**: 10 | 11 | ![ScreenShot](https://raw.github.com/accessd/slack-deploy-bot/master/images/help-command.png) 12 | 13 | 14 | Show **changelog** for your branch against *master* branch: 15 | 16 | changelog my-awesome-app#feature 17 | 18 | ![ScreenShot](https://raw.github.com/accessd/slack-deploy-bot/master/images/changelog-command.png) 19 | 20 | 21 | **Deploy** your apps branches to different environments: 22 | 23 | deploy my-awesome-app # deploy my-awesome-app default branch to default environment 24 | 25 | deploy my-awesome-app to prod # deploy my-awesome-app default branch to prod environment 26 | 27 | deploy my-awesome-app#my-feature to staging # deploy my-awesome-app my-feature branch to prod environment 28 | 29 | ![ScreenShot](https://raw.github.com/accessd/slack-deploy-bot/master/images/deploy-command.png) 30 | 31 | 32 | in case of deploy fail you'll see the error message: 33 | 34 | ![ScreenShot](https://raw.github.com/accessd/slack-deploy-bot/master/images/deploy-failed.png) 35 | 36 | ## Production 37 | 38 | ### How to setup 39 | 40 | 1. Clone the repo 41 | 2. `cp ./config/config.sample.rb ./config/config.rb` 42 | 3. Edit `./config/config.rb` to add configuration for your app 43 | 4. `bundle install --without development test` 44 | 5. You can try to launch bot with `SLACK_API_TOKEN=xxx bundle exec ruby run.rb` and test it 45 | 6. Launch bot as a daemon (you can use *eye, upstart, systemd*, etc.) 46 | 47 | I'd recommend to use **eye** gem for launching bot. But it's up to you. 48 | 49 | ### Launching with eye 50 | 51 | Please follow instruction on [https://github.com/kostya/eye](https://github.com/kostya/eye) for installing **eye** 52 | 53 | Put in the root *.slack-api-token* file which contains api token 54 | 55 | Create **config/slack-deploy-bot.eye** config using **config/slack-deploy-bot.eye.sample** as sample 56 | 57 | For first time or if slack-deploy-bot.eye config was changed than run: 58 | 59 | eye load config/slack-deploy-bot.eye 60 | 61 | Start bot: 62 | 63 | eye start deploybot 64 | 65 | Restart bot with command: 66 | 67 | eye r deploybot 68 | 69 | Info about bot process: 70 | 71 | eye i deploybot 72 | 73 | ## Configuration 74 | 75 | Copy *config/config.sample.rb* to *config/config.rb* 76 | 77 | Example of `config.rb`: 78 | 79 | ```ruby 80 | DeployBot.setup do |config| 81 | config.apps = { 82 | :'my-awesome-app' => { 83 | envs: [:staging, :prod], 84 | path: '~/projects/my-awesome-app', 85 | default_branch: :master, # default branch to deploy, is not required 86 | default_env: :prod, # default env to deploy, is not required 87 | deploy_cmd: ->(env, branch) { "./deploy.sh #{env} #{branch}" } # deploy with Ansible for example 88 | }, 89 | :'my-second-awesome-app': { 90 | envs: [:dev, :prod], 91 | path: '~/projects/my-second-awesome-app', 92 | deploy_cmd: ->(env, branch) { "BRANCH_NAME=#{branch} bundle exec cap #{env} deploy" } # deploy with Capistrano 93 | } 94 | } 95 | end 96 | ``` 97 | 98 | Required options for each app: *envs*, *path*, *deploy_cmd* 99 | 100 | ## Development & Testing 101 | 102 | Start bot with command: 103 | 104 | SLACK_API_TOKEN=xxx foreman start 105 | 106 | Start console with: 107 | 108 | bundle exec ./console 109 | 110 | Run specs with: 111 | 112 | bundle exec rake 113 | 114 | Before starting specs please run: 115 | 116 | git submodule update --init 117 | 118 | for fetching *spec/support/dummy_app* 119 | 120 | ## TODO 121 | 122 | 1. ~~Configuration (apps, envs, default branch, deploy command)~~ 123 | 2. ~~Notifications to channel about starting/ending/failing deploy events~~ 124 | 3. ~~Specs~~ 125 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler' 3 | 4 | Bundler.setup :default, :development 5 | 6 | require 'rspec/core' 7 | require 'rspec/core/rake_task' 8 | 9 | RSpec::Core::RakeTask.new(:spec) do |spec| 10 | spec.pattern = FileList['spec/**/*_spec.rb'] 11 | end 12 | 13 | task default: [:spec] 14 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | require 'slack-ruby-bot' 2 | 3 | module SlackDeployBot 4 | class << self 5 | attr_accessor :apps 6 | end 7 | 8 | def self.setup 9 | yield self 10 | end 11 | end 12 | 13 | SlackDeployBot.apps = {} 14 | 15 | unless ENV['ENV'] == 'test' 16 | require_relative 'config' 17 | end 18 | 19 | SlackDeployBot.apps.each do |app_name, app_config| 20 | SlackDeployBot::AppConfigValidator.validate(app_name, app_config) 21 | end 22 | -------------------------------------------------------------------------------- /config/config.sample.rb: -------------------------------------------------------------------------------- 1 | SlackDeployBot.setup do |config| 2 | config.apps = { 3 | :'my-awesome-app' => { 4 | envs: [:staging, :prod], 5 | path: '~/projects/my-awesome-app', 6 | default_branch: :master, 7 | default_env: :prod, 8 | deploy_cmd: ->(env, branch) { "./deploy.sh #{env} #{branch}" } 9 | }, 10 | :'my-second-awesome-app' => { 11 | envs: [:dev, :prod], 12 | path: '~/projects/my-second-awesome-app', 13 | deploy_cmd: ->(env, branch) { "BRANCH_NAME=#{branch} bundle exec cap #{env} deploy" } 14 | } 15 | } 16 | end 17 | -------------------------------------------------------------------------------- /config/slack-deploy-bot.eye.sample: -------------------------------------------------------------------------------- 1 | BUNDLE = 'bundle' 2 | BASE_PATH = '/path/to/slack-deploy-bot' 3 | ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..')) 4 | api_token_file = '.slack-api-token' 5 | api_token_file_path = "#{ROOT}/#{api_token_file}" 6 | fail "#{api_token_file_path} does not exist" unless File.exist?(api_token_file_path) 7 | SLACK_API_TOKEN = File.read(api_token_file_path).gsub(/\n/, '') 8 | 9 | Eye.config do 10 | logger "#{ROOT}/logs/eye.log" 11 | end 12 | 13 | Eye.application :deploybot do 14 | env 'BASE_PATH' => BASE_PATH 15 | env 'SLACK_API_TOKEN' => SLACK_API_TOKEN 16 | working_dir ROOT 17 | trigger :flapping, times: 10, within: 1.minute, retry_in: 10.minutes 18 | 19 | process :bot do 20 | daemonize true 21 | pid_file 'bot.pid' 22 | stdall "#{ROOT}/logs/deploybot.log" 23 | 24 | start_command "#{BUNDLE} exec ruby run.rb" 25 | stop_signals [:TERM, 5.seconds, :KILL] 26 | restart_command 'kill -USR2 {PID}' 27 | 28 | # just sleep this until process get up status 29 | restart_grace 2.seconds 30 | 31 | check :cpu, every: 30, below: 80, times: 3 32 | check :memory, every: 30, below: 100.megabytes, times: [3, 5] 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require_relative 'slack-deploy-bot' 4 | require 'pry' 5 | pry 6 | -------------------------------------------------------------------------------- /images/changelog-command.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/accessd/slack-deploy-bot/16a63d9fbcf988308c950def6991d2be614babe8/images/changelog-command.png -------------------------------------------------------------------------------- /images/deploy-command.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/accessd/slack-deploy-bot/16a63d9fbcf988308c950def6991d2be614babe8/images/deploy-command.png -------------------------------------------------------------------------------- /images/deploy-failed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/accessd/slack-deploy-bot/16a63d9fbcf988308c950def6991d2be614babe8/images/deploy-failed.png -------------------------------------------------------------------------------- /images/help-command.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/accessd/slack-deploy-bot/16a63d9fbcf988308c950def6991d2be614babe8/images/help-command.png -------------------------------------------------------------------------------- /logs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/accessd/slack-deploy-bot/16a63d9fbcf988308c950def6991d2be614babe8/logs/.gitkeep -------------------------------------------------------------------------------- /run.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 2 | 3 | require 'slack-deploy-bot' 4 | 5 | begin 6 | SlackDeployBot::Bot.run 7 | rescue Exception => e 8 | STDERR.puts "ERROR: #{e}" 9 | STDERR.puts e.backtrace 10 | raise e 11 | end 12 | -------------------------------------------------------------------------------- /slack-deploy-bot.rb: -------------------------------------------------------------------------------- 1 | require_relative 'slack-deploy-bot/app_config_validator' 2 | require_relative 'config/boot' 3 | require_relative 'slack-deploy-bot/utils' 4 | require_relative 'slack-deploy-bot/version' 5 | require_relative 'slack-deploy-bot/about' 6 | require_relative 'slack-deploy-bot/commands' 7 | require_relative 'slack-deploy-bot/bot' 8 | -------------------------------------------------------------------------------- /slack-deploy-bot/about.rb: -------------------------------------------------------------------------------- 1 | module SlackDeployBot 2 | ABOUT = <<-ABOUT 3 | #{SlackDeployBot::VERSION} 4 | https://github.com/accessd/slack-deploy-bot 5 | https://morskov.com 6 | ABOUT 7 | end 8 | -------------------------------------------------------------------------------- /slack-deploy-bot/app_config_validator.rb: -------------------------------------------------------------------------------- 1 | require 'dry-validation' 2 | 3 | module SlackDeployBot 4 | class InvalidAppConfig < StandardError; end 5 | class AppConfigValidator 6 | AppConfigSchema = Dry::Validation.Schema do 7 | required(:envs).filled 8 | required(:path).filled 9 | required(:deploy_cmd).filled 10 | end 11 | 12 | def self.validate(app_name, config) 13 | validation_result = AppConfigSchema.call(config) 14 | 15 | if validation_result.failure? 16 | errors = validation_result.errors(full: true).values 17 | raise InvalidAppConfig, "app: <#{app_name}> errors: <#{errors.join(', ')}>" 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /slack-deploy-bot/bot.rb: -------------------------------------------------------------------------------- 1 | SlackRubyBot.configure do |config| 2 | config.send_gifs = false 3 | end 4 | 5 | module SlackDeployBot 6 | MASTER_BRANCH = 'master'.freeze 7 | EMPTY_CHANGELOG = 'none'.freeze 8 | DEFAULT_BRANCH = MASTER_BRANCH 9 | 10 | def self.app_names 11 | @@app_names ||= self.apps.keys.map!(&:to_sym) 12 | end 13 | 14 | def self.check_app_present_in_config(app_name, client, data) 15 | app_names.include?(app_name.to_sym) || say_error(client, data, "Invalid app name `#{app_name}`, use one of the following: #{app_names.join(', ')}") 16 | end 17 | 18 | def self.say_error(client, data, text) 19 | client.say(channel: data.channel, text: ":warning: #{text}") 20 | throw(:exit) 21 | end 22 | 23 | class Bot < SlackRubyBot::Bot 24 | help do 25 | title 'Deploy Bot' 26 | 27 | desc 'This bot can deploy something to somewhere :)' 28 | 29 | command 'deploy' do 30 | desc "Command format is: *deploy|накати|задеплой (#{SlackDeployBot.apps.keys.join('|')})#branch_or_tag to|на production*" 31 | end 32 | 33 | command 'changelog' do 34 | desc "Shows changelog against #{SlackDeployBot::MASTER_BRANCH}: *changelog (#{SlackDeployBot.apps.keys.join('|')})#branch_or_tag*" 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /slack-deploy-bot/commands.rb: -------------------------------------------------------------------------------- 1 | require_relative 'commands/about' 2 | require_relative 'commands/changelog' 3 | require_relative 'commands/deploy' 4 | -------------------------------------------------------------------------------- /slack-deploy-bot/commands/about.rb: -------------------------------------------------------------------------------- 1 | module SlackDeployBot 2 | module Commands 3 | class Default < SlackRubyBot::Commands::Base 4 | command 'about' 5 | match(/^(?[[:alnum:][:punct:]@<>]*)$/u) 6 | 7 | def self.call(client, data, _match) 8 | client.say(text: SlackDeployBot::ABOUT, channel: data.channel) 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /slack-deploy-bot/commands/changelog.rb: -------------------------------------------------------------------------------- 1 | module SlackDeployBot 2 | module Commands 3 | class Changelog < SlackRubyBot::Commands::Base 4 | match(/changelog (?.+)#(?.+)$/i) do |client, data, match| 5 | catch(:exit) do 6 | app = match[:app] 7 | SlackDeployBot.check_app_present_in_config(app, client, data) 8 | branch = match[:branch] 9 | app_config = SlackDeployBot.apps[app.to_sym] 10 | 11 | git = Utils::Git.new(app_config[:path]) 12 | master_branch = SlackDeployBot::MASTER_BRANCH 13 | git.fetch_branches || SlackDeployBot.say_error(client, data, "Cannot fetch repo") 14 | git.branch_exists?(branch) || SlackDeployBot.say_error(client, data, "Unknown git branch `#{branch}`") 15 | git.branch_exists?(master_branch) || SlackDeployBot.say_error(client, data, "Unknown git branch `#{master_branch}`") 16 | git.sync_branch(branch) || SlackDeployBot.say_error(client, data, "Couldn't pull `#{branch}`") 17 | git.sync_branch(master_branch) || SlackDeployBot.say_error(client, data, "Couldn't pull `#{master_branch}`") 18 | 19 | changelog = git.changelog(branch, against: master_branch).presence || SlackDeployBot::EMPTY_CHANGELOG 20 | 21 | client.say(channel: data.channel, text: "Changelog for `#{branch}`:\n```#{changelog}```") 22 | end 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /slack-deploy-bot/commands/deploy.rb: -------------------------------------------------------------------------------- 1 | module SlackDeployBot 2 | def self.deploy_app(app_name:, branch: nil, env: nil, client:, data:) 3 | catch(:exit) do 4 | check_app_present_in_config(app_name, client, data) 5 | app_config = apps[app_name.to_sym] 6 | 7 | branch ||= app_config[:default_branch] 8 | branch ||= DEFAULT_BRANCH 9 | env ||= app_config[:default_env] 10 | env || say_error(client, data, "There's no default env") 11 | 12 | client.send(:logger).info "app: #{app_name}" 13 | client.send(:logger).info "branch: #{branch}" 14 | client.send(:logger).info "env: #{env}" 15 | 16 | envs = app_config[:envs].map(&:to_sym) 17 | envs.include?(env.to_sym) || say_error(client, data, "Invalid env `#{env}`, use: #{envs.join(', ')}") 18 | git = Utils::Git.new(app_config[:path]) 19 | git.fetch_branches || say_error(client, data, "Cannot fetch branches") 20 | git.branch_exists?(branch) || say_error(client, data, "Unknown git branch `#{branch}`") 21 | git.sync_branch(branch) || say_error(client, data, "Couldn't pull `#{branch}`") 22 | 23 | git.checkout(branch) 24 | username = client.users[data.user][:name] 25 | client.send(:logger).info "user: #{username}" 26 | client.say(channel: data.channel, text: "#{username} started deploying #{app_name}##{branch} to #{env}") 27 | cmd = "cd #{app_config[:path]}; #{app_config[:deploy_cmd].call(env, branch)}" 28 | client.send(:logger).info "command: #{cmd}" 29 | 30 | error_happened = false 31 | Utils::Subprocess.new(cmd) do |stdout, stderr, thread| 32 | if stdout && !stdout.empty? 33 | client.send(:logger).info stdout 34 | if stdout =~ /failed\:/ || stdout =~ /command not found/ 35 | error_happened = true 36 | say_error(client, data, "Deploy failed with error: #{stdout}. More info at logs/deploybot.log") 37 | end 38 | elsif stderr && !stderr.empty? 39 | client.send(:logger).error stderr 40 | error_happened = true 41 | say_error(client, data, "Deploy failed with error: #{stderr}. More info at logs/deploybot.log") 42 | end 43 | end 44 | 45 | unless error_happened 46 | client.say(channel: data.channel, text: "#{username} finished deploying #{app_name}##{branch} to #{env}") 47 | end 48 | end 49 | end 50 | 51 | module Commands 52 | class Deploy < SlackRubyBot::Commands::Base 53 | 54 | match(/(deploy|накати|задеплой) ((?.+)#(?.+)|(?.+)) (to|на) (?.+)/i) do |client, data, match| 55 | SlackDeployBot.deploy_app(app_name: match[:app], branch: match[:branch], env: match[:env], client: client, data: data) 56 | end 57 | 58 | match(/(deploy|накати|задеплой) ((?.+)#(?.+)|(?.+))/i) do |client, data, match| 59 | SlackDeployBot.deploy_app(app_name: match[:app], branch: match[:branch], client: client, data: data) 60 | end 61 | 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /slack-deploy-bot/utils.rb: -------------------------------------------------------------------------------- 1 | require 'open3' 2 | 3 | module SlackDeployBot 4 | module Utils 5 | class Subprocess 6 | def initialize(cmd, &block) 7 | Open3.popen3(cmd) do |stdin, stdout, stderr, thread| 8 | { :out => stdout, :err => stderr }.each do |key, stream| 9 | Thread.new do 10 | until (line = stream.gets).nil? do 11 | if key == :out 12 | yield line, nil, thread if block_given? 13 | else 14 | yield nil, line, thread if block_given? 15 | end 16 | end 17 | end 18 | end 19 | 20 | thread.join # don't exit until the external process is done 21 | end 22 | end 23 | end 24 | 25 | class Git 26 | attr_reader :base_path 27 | 28 | def initialize(base_path) 29 | @base_path = base_path 30 | @output_to = if ENV['ENV'] == 'test' 31 | File::NULL 32 | else 33 | STDOUT 34 | end 35 | end 36 | 37 | def branch_exists?(branch) 38 | `cd #{base_path}; git ls-remote --heads origin #{branch}`.present? 39 | end 40 | 41 | def sync_branch(branch) 42 | checkout(branch) && system("cd #{base_path}; git reset --hard origin/#{branch}", out: @output_to) 43 | end 44 | 45 | def fetch_branches 46 | system "cd #{base_path}; git fetch --all", out: @output_to 47 | end 48 | 49 | def checkout(branch) 50 | system "cd #{base_path}; git checkout #{branch}", out: @output_to 51 | end 52 | 53 | def changelog(branch, against:) 54 | `cd #{base_path} ; git log --no-merges --pretty=format:'%s (%an)' #{against}..#{branch} | cat` 55 | end 56 | 57 | def update_branch(branch, against:) 58 | result = checkout(branch) && system("cd #{base_path}; git merge --no-ff --quiet #{against} && git push origin #{branch}", out: @output_to) 59 | system("cd #{base_path}; git reset --hard HEAD", out: @output_to) unless result 60 | result 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /slack-deploy-bot/version.rb: -------------------------------------------------------------------------------- 1 | module SlackDeployBot 2 | VERSION = '1.0.0' 3 | end 4 | -------------------------------------------------------------------------------- /spec/slack-deploy-bot/app_config_validator_spec.rb: -------------------------------------------------------------------------------- 1 | describe SlackDeployBot::AppConfigValidator do 2 | describe 'validates app config' do 3 | it 'envs should be present' do 4 | config = { 5 | path: '/tmp', 6 | default_branch: :master, 7 | default_env: :prod, 8 | deploy_cmd: ->(env, branch) { "./deploy.sh #{env} #{branch}" } 9 | } 10 | expect{ described_class.validate('my-awesome-app', config) }.to raise_error( 11 | SlackDeployBot::InvalidAppConfig, 12 | 'app: errors: ' 13 | ) 14 | end 15 | 16 | it 'path should be present' do 17 | config = { 18 | envs: [:staging, :prod], 19 | default_branch: :master, 20 | default_env: :prod, 21 | deploy_cmd: ->(env, branch) { "./deploy.sh #{env} #{branch}" } 22 | } 23 | expect{ described_class.validate('my-awesome-app', config) }.to raise_error( 24 | SlackDeployBot::InvalidAppConfig, 25 | 'app: errors: ' 26 | ) 27 | end 28 | 29 | it 'deploy_cmd should be present' do 30 | config = { 31 | envs: [:staging, :prod], 32 | path: '/tmp', 33 | default_branch: :master, 34 | default_env: :prod, 35 | } 36 | expect{ described_class.validate('my-awesome-app', config) }.to raise_error( 37 | SlackDeployBot::InvalidAppConfig, 38 | 'app: errors: ' 39 | ) 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/slack-deploy-bot/bot_spec.rb: -------------------------------------------------------------------------------- 1 | describe SlackDeployBot::Bot do 2 | subject { SlackDeployBot::Bot.instance } 3 | 4 | it_behaves_like 'a slack ruby bot' 5 | end 6 | -------------------------------------------------------------------------------- /spec/slack-deploy-bot/commands/about_spec.rb: -------------------------------------------------------------------------------- 1 | describe SlackDeployBot::Commands::Default do 2 | def app 3 | SlackDeployBot::Bot.instance 4 | end 5 | 6 | it 'deploy-bot' do 7 | expect(message: 'deploy-bot').to respond_with_slack_message(messages: [SlackDeployBot::ABOUT]) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/slack-deploy-bot/commands/changelog_spec.rb: -------------------------------------------------------------------------------- 1 | describe SlackDeployBot::Commands::Changelog do 2 | def app 3 | SlackDeployBot::Bot.instance 4 | end 5 | 6 | it 'shows changelog for branch' do 7 | expect(message: 'deploy-bot changelog my-awesome-app#my-feature'). 8 | to respond_with_slack_message(message_includes: ['Changelog for `my-feature`', 'add deploy.sh (Accessd)']) 9 | end 10 | 11 | describe 'say error' do 12 | it 'in case of unknown app' do 13 | expect(message: 'deploy-bot changelog unknown-app#my-feature'). 14 | to respond_with_slack_message(messages: [':warning: Invalid app name `unknown-app`, use one of the following: my-awesome-app, my-second-awesome-app']) 15 | end 16 | 17 | it 'when branch does not exists' do 18 | expect(message: 'deploy-bot changelog my-awesome-app#unknown-feature'). 19 | to respond_with_slack_message(messages: [':warning: Unknown git branch `unknown-feature`']) 20 | end 21 | end 22 | 23 | it 'invalid command' do 24 | expect(message: 'deploy-bot changelog my-awesome-app#').to respond_with_slack_message(messages: ["Sorry <@user>, I don't understand that command!"]) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/slack-deploy-bot/commands/deploy_spec.rb: -------------------------------------------------------------------------------- 1 | describe SlackDeployBot::Commands::Deploy, vcr: { cassette_name: 'rtm_start' } do 2 | def app 3 | SlackDeployBot::Bot.instance 4 | end 5 | include_context 'connected client' 6 | 7 | before do 8 | set_slack_user('broskoski') 9 | end 10 | 11 | it 'executes deploy command' do 12 | say_to_bot(message: 'deploy-bot deploy my-awesome-app#my-feature to prod') 13 | expect_deploy_cmd_has_been_executed 14 | end 15 | 16 | it 'notifies about start and finish deploy' do 17 | expect(message: 'deploy-bot deploy my-awesome-app#my-feature to prod'). 18 | to respond_with_slack_message(messages: [ 19 | 'broskoski started deploying my-awesome-app#my-feature to prod', 20 | 'broskoski finished deploying my-awesome-app#my-feature to prod', 21 | ]) 22 | end 23 | 24 | describe 'without env' do 25 | it 'use default env' do 26 | expect(message: 'deploy-bot deploy my-awesome-app#my-feature'). 27 | to respond_with_slack_message(messages: [ 28 | 'broskoski started deploying my-awesome-app#my-feature to prod', 29 | 'broskoski finished deploying my-awesome-app#my-feature to prod', 30 | ]) 31 | 32 | say_to_bot(message: 'deploy-bot deploy my-awesome-app#my-feature') 33 | expect_deploy_cmd_has_been_executed 34 | end 35 | 36 | it 'default env is not set' do 37 | expect(message: 'deploy-bot deploy my-second-awesome-app#my-feature'). 38 | to respond_with_slack_message(messages: [ 39 | ":warning: There's no default env" 40 | ]) 41 | end 42 | end 43 | 44 | describe 'say error' do 45 | it 'when error happened' do 46 | expect(message: 'deploy-bot deploy my-second-awesome-app#my-feature to prod'). 47 | to respond_with_slack_message(messages: [":warning: Deploy failed with error: bundler: command not found: cap\n. More info at logs/deploybot.log"]) 48 | end 49 | 50 | it 'in case of unknown app' do 51 | expect(message: 'deploy-bot deploy unknown-app#my-feature to prod'). 52 | to respond_with_slack_message(messages: [':warning: Invalid app name `unknown-app`, use one of the following: my-awesome-app, my-second-awesome-app']) 53 | end 54 | 55 | it 'when branch does not exists' do 56 | expect(message: 'deploy-bot deploy my-awesome-app#unknown-feature to prod'). 57 | to respond_with_slack_message(messages: [':warning: Unknown git branch `unknown-feature`']) 58 | end 59 | end 60 | 61 | it 'invalid command' do 62 | expect(message: 'deploy-bot deploy'). 63 | to respond_with_slack_message(messages: ["Sorry <@#{ENV['SLACK_USER_ID']}>, I don't understand that command!"]) 64 | end 65 | 66 | begin 'Helper methods' 67 | def expect_deploy_cmd_has_been_executed 68 | file_created_by_deploy_cmd = '/tmp/slack-deploy-bot' 69 | expect(File.exist?(file_created_by_deploy_cmd)).to eq(true) 70 | expect(File.stat(file_created_by_deploy_cmd).atime).to be_within(5.second).of Time.now 71 | end 72 | 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..')) 2 | 3 | require 'slack-ruby-bot/rspec' 4 | require 'webmock/rspec' 5 | require 'vcr' 6 | 7 | VCR.configure do |config| 8 | config.cassette_library_dir = 'spec/vcr' 9 | config.hook_into :webmock 10 | config.default_cassette_options = { record: :new_episodes } 11 | config.configure_rspec_metadata! 12 | end 13 | 14 | ENV['ENV'] = 'test' 15 | 16 | Dir[File.join(File.dirname(__FILE__), 'support', '**/*.rb')].each do |file| 17 | require file 18 | end 19 | 20 | require 'slack-deploy-bot' 21 | 22 | SlackDeployBot.setup do |config| 23 | config.apps = { 24 | :'my-awesome-app' => { 25 | envs: [:staging, :prod], 26 | path: File.join(File.dirname(__FILE__), 'support', 'dummy_app'), 27 | default_branch: :master, 28 | default_env: :prod, 29 | deploy_cmd: ->(env, branch) { "./deploy.sh #{env} #{branch}" } 30 | }, 31 | :'my-second-awesome-app' => { 32 | envs: [:dev, :prod], 33 | path: File.join(File.dirname(__FILE__), 'support', 'dummy_app'), 34 | deploy_cmd: ->(env, branch) { "BRANCH_NAME=#{branch} bundle exec cap #{env} deploy" } 35 | }, 36 | } 37 | end 38 | 39 | ENV['SLACK_USER_ID'] = 'user' 40 | ENV['SLACK_CHANNEL'] = 'channel' 41 | 42 | def slack_users 43 | {'broskoski' => 'U092V4E9L'} 44 | end 45 | 46 | def set_slack_user(user_name) 47 | ENV['SLACK_USER_ID'] = slack_users[user_name] 48 | end 49 | -------------------------------------------------------------------------------- /spec/support/concurrency_mock.rb: -------------------------------------------------------------------------------- 1 | module Slack 2 | module RealTime 3 | module Concurrency 4 | module Mock 5 | class WebSocket 6 | end 7 | 8 | class Socket < ::Slack::RealTime::Socket 9 | def self.close 10 | end 11 | 12 | def start_async(client) 13 | reactor = Thread.new {} 14 | client.run_loop 15 | reactor 16 | end 17 | 18 | def send_data(message) 19 | driver.send(message) 20 | end 21 | 22 | protected 23 | 24 | def connect 25 | @driver = WebSocket.new(url, nil, options) 26 | end 27 | end 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/support/connected_client.rb: -------------------------------------------------------------------------------- 1 | RSpec.shared_context 'connected client' do |opts| 2 | let(:client) { SlackRubyBot::Client.new(opts || {}) } 3 | let(:ws) { double(Slack::RealTime::Concurrency::Mock::WebSocket) } 4 | let(:url) { 'wss://ms173.slack-msgs.com/websocket/lqcUiAvrKTP-uuid=' } 5 | let(:socket) { double(Slack::RealTime::Socket, connected?: true) } 6 | before do 7 | Slack::RealTime.configure do |config| 8 | config.concurrency = Slack::RealTime::Concurrency::Mock 9 | end 10 | allow(Slack::RealTime::Socket).to receive(:new).with(url, ping: 30, logger: instance_of(Logger)).and_return(socket) 11 | allow(socket).to receive(:start_sync) 12 | allow(socket).to receive(:connect!) 13 | allow(ws).to receive(:on) 14 | client.start! 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/support/remote_repo.git/HEAD: -------------------------------------------------------------------------------- 1 | ref: refs/heads/master 2 | -------------------------------------------------------------------------------- /spec/support/remote_repo.git/config: -------------------------------------------------------------------------------- 1 | [core] 2 | repositoryformatversion = 0 3 | filemode = true 4 | bare = true 5 | ignorecase = true 6 | -------------------------------------------------------------------------------- /spec/support/remote_repo.git/description: -------------------------------------------------------------------------------- 1 | Unnamed repository; edit this file 'description' to name the repository. 2 | -------------------------------------------------------------------------------- /spec/support/remote_repo.git/hooks/applypatch-msg.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to check the commit log message taken by 4 | # applypatch from an e-mail message. 5 | # 6 | # The hook should exit with non-zero status after issuing an 7 | # appropriate message if it wants to stop the commit. The hook is 8 | # allowed to edit the commit message file. 9 | # 10 | # To enable this hook, rename this file to "applypatch-msg". 11 | 12 | . git-sh-setup 13 | commitmsg="$(git rev-parse --git-path hooks/commit-msg)" 14 | test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"} 15 | : 16 | -------------------------------------------------------------------------------- /spec/support/remote_repo.git/hooks/commit-msg.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to check the commit log message. 4 | # Called by "git commit" with one argument, the name of the file 5 | # that has the commit message. The hook should exit with non-zero 6 | # status after issuing an appropriate message if it wants to stop the 7 | # commit. The hook is allowed to edit the commit message file. 8 | # 9 | # To enable this hook, rename this file to "commit-msg". 10 | 11 | # Uncomment the below to add a Signed-off-by line to the message. 12 | # Doing this in a hook is a bad idea in general, but the prepare-commit-msg 13 | # hook is more suited to it. 14 | # 15 | # SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') 16 | # grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" 17 | 18 | # This example catches duplicate Signed-off-by lines. 19 | 20 | test "" = "$(grep '^Signed-off-by: ' "$1" | 21 | sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { 22 | echo >&2 Duplicate Signed-off-by lines. 23 | exit 1 24 | } 25 | -------------------------------------------------------------------------------- /spec/support/remote_repo.git/hooks/post-update.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to prepare a packed repository for use over 4 | # dumb transports. 5 | # 6 | # To enable this hook, rename this file to "post-update". 7 | 8 | exec git update-server-info 9 | -------------------------------------------------------------------------------- /spec/support/remote_repo.git/hooks/pre-applypatch.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to verify what is about to be committed 4 | # by applypatch from an e-mail message. 5 | # 6 | # The hook should exit with non-zero status after issuing an 7 | # appropriate message if it wants to stop the commit. 8 | # 9 | # To enable this hook, rename this file to "pre-applypatch". 10 | 11 | . git-sh-setup 12 | precommit="$(git rev-parse --git-path hooks/pre-commit)" 13 | test -x "$precommit" && exec "$precommit" ${1+"$@"} 14 | : 15 | -------------------------------------------------------------------------------- /spec/support/remote_repo.git/hooks/pre-commit.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to verify what is about to be committed. 4 | # Called by "git commit" with no arguments. The hook should 5 | # exit with non-zero status after issuing an appropriate message if 6 | # it wants to stop the commit. 7 | # 8 | # To enable this hook, rename this file to "pre-commit". 9 | 10 | if git rev-parse --verify HEAD >/dev/null 2>&1 11 | then 12 | against=HEAD 13 | else 14 | # Initial commit: diff against an empty tree object 15 | against=4b825dc642cb6eb9a060e54bf8d69288fbee4904 16 | fi 17 | 18 | # If you want to allow non-ASCII filenames set this variable to true. 19 | allownonascii=$(git config --bool hooks.allownonascii) 20 | 21 | # Redirect output to stderr. 22 | exec 1>&2 23 | 24 | # Cross platform projects tend to avoid non-ASCII filenames; prevent 25 | # them from being added to the repository. We exploit the fact that the 26 | # printable range starts at the space character and ends with tilde. 27 | if [ "$allownonascii" != "true" ] && 28 | # Note that the use of brackets around a tr range is ok here, (it's 29 | # even required, for portability to Solaris 10's /usr/bin/tr), since 30 | # the square bracket bytes happen to fall in the designated range. 31 | test $(git diff --cached --name-only --diff-filter=A -z $against | 32 | LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 33 | then 34 | cat <<\EOF 35 | Error: Attempt to add a non-ASCII file name. 36 | 37 | This can cause problems if you want to work with people on other platforms. 38 | 39 | To be portable it is advisable to rename the file. 40 | 41 | If you know what you are doing you can disable this check using: 42 | 43 | git config hooks.allownonascii true 44 | EOF 45 | exit 1 46 | fi 47 | 48 | # If there are whitespace errors, print the offending file names and fail. 49 | exec git diff-index --check --cached $against -- 50 | -------------------------------------------------------------------------------- /spec/support/remote_repo.git/hooks/pre-push.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # An example hook script to verify what is about to be pushed. Called by "git 4 | # push" after it has checked the remote status, but before anything has been 5 | # pushed. If this script exits with a non-zero status nothing will be pushed. 6 | # 7 | # This hook is called with the following parameters: 8 | # 9 | # $1 -- Name of the remote to which the push is being done 10 | # $2 -- URL to which the push is being done 11 | # 12 | # If pushing without using a named remote those arguments will be equal. 13 | # 14 | # Information about the commits which are being pushed is supplied as lines to 15 | # the standard input in the form: 16 | # 17 | # 18 | # 19 | # This sample shows how to prevent push of commits where the log message starts 20 | # with "WIP" (work in progress). 21 | 22 | remote="$1" 23 | url="$2" 24 | 25 | z40=0000000000000000000000000000000000000000 26 | 27 | while read local_ref local_sha remote_ref remote_sha 28 | do 29 | if [ "$local_sha" = $z40 ] 30 | then 31 | # Handle delete 32 | : 33 | else 34 | if [ "$remote_sha" = $z40 ] 35 | then 36 | # New branch, examine all commits 37 | range="$local_sha" 38 | else 39 | # Update to existing branch, examine new commits 40 | range="$remote_sha..$local_sha" 41 | fi 42 | 43 | # Check for WIP commit 44 | commit=`git rev-list -n 1 --grep '^WIP' "$range"` 45 | if [ -n "$commit" ] 46 | then 47 | echo >&2 "Found WIP commit in $local_ref, not pushing" 48 | exit 1 49 | fi 50 | fi 51 | done 52 | 53 | exit 0 54 | -------------------------------------------------------------------------------- /spec/support/remote_repo.git/hooks/pre-rebase.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Copyright (c) 2006, 2008 Junio C Hamano 4 | # 5 | # The "pre-rebase" hook is run just before "git rebase" starts doing 6 | # its job, and can prevent the command from running by exiting with 7 | # non-zero status. 8 | # 9 | # The hook is called with the following parameters: 10 | # 11 | # $1 -- the upstream the series was forked from. 12 | # $2 -- the branch being rebased (or empty when rebasing the current branch). 13 | # 14 | # This sample shows how to prevent topic branches that are already 15 | # merged to 'next' branch from getting rebased, because allowing it 16 | # would result in rebasing already published history. 17 | 18 | publish=next 19 | basebranch="$1" 20 | if test "$#" = 2 21 | then 22 | topic="refs/heads/$2" 23 | else 24 | topic=`git symbolic-ref HEAD` || 25 | exit 0 ;# we do not interrupt rebasing detached HEAD 26 | fi 27 | 28 | case "$topic" in 29 | refs/heads/??/*) 30 | ;; 31 | *) 32 | exit 0 ;# we do not interrupt others. 33 | ;; 34 | esac 35 | 36 | # Now we are dealing with a topic branch being rebased 37 | # on top of master. Is it OK to rebase it? 38 | 39 | # Does the topic really exist? 40 | git show-ref -q "$topic" || { 41 | echo >&2 "No such branch $topic" 42 | exit 1 43 | } 44 | 45 | # Is topic fully merged to master? 46 | not_in_master=`git rev-list --pretty=oneline ^master "$topic"` 47 | if test -z "$not_in_master" 48 | then 49 | echo >&2 "$topic is fully merged to master; better remove it." 50 | exit 1 ;# we could allow it, but there is no point. 51 | fi 52 | 53 | # Is topic ever merged to next? If so you should not be rebasing it. 54 | only_next_1=`git rev-list ^master "^$topic" ${publish} | sort` 55 | only_next_2=`git rev-list ^master ${publish} | sort` 56 | if test "$only_next_1" = "$only_next_2" 57 | then 58 | not_in_topic=`git rev-list "^$topic" master` 59 | if test -z "$not_in_topic" 60 | then 61 | echo >&2 "$topic is already up-to-date with master" 62 | exit 1 ;# we could allow it, but there is no point. 63 | else 64 | exit 0 65 | fi 66 | else 67 | not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"` 68 | /usr/bin/perl -e ' 69 | my $topic = $ARGV[0]; 70 | my $msg = "* $topic has commits already merged to public branch:\n"; 71 | my (%not_in_next) = map { 72 | /^([0-9a-f]+) /; 73 | ($1 => 1); 74 | } split(/\n/, $ARGV[1]); 75 | for my $elem (map { 76 | /^([0-9a-f]+) (.*)$/; 77 | [$1 => $2]; 78 | } split(/\n/, $ARGV[2])) { 79 | if (!exists $not_in_next{$elem->[0]}) { 80 | if ($msg) { 81 | print STDERR $msg; 82 | undef $msg; 83 | } 84 | print STDERR " $elem->[1]\n"; 85 | } 86 | } 87 | ' "$topic" "$not_in_next" "$not_in_master" 88 | exit 1 89 | fi 90 | 91 | exit 0 92 | 93 | ################################################################ 94 | 95 | This sample hook safeguards topic branches that have been 96 | published from being rewound. 97 | 98 | The workflow assumed here is: 99 | 100 | * Once a topic branch forks from "master", "master" is never 101 | merged into it again (either directly or indirectly). 102 | 103 | * Once a topic branch is fully cooked and merged into "master", 104 | it is deleted. If you need to build on top of it to correct 105 | earlier mistakes, a new topic branch is created by forking at 106 | the tip of the "master". This is not strictly necessary, but 107 | it makes it easier to keep your history simple. 108 | 109 | * Whenever you need to test or publish your changes to topic 110 | branches, merge them into "next" branch. 111 | 112 | The script, being an example, hardcodes the publish branch name 113 | to be "next", but it is trivial to make it configurable via 114 | $GIT_DIR/config mechanism. 115 | 116 | With this workflow, you would want to know: 117 | 118 | (1) ... if a topic branch has ever been merged to "next". Young 119 | topic branches can have stupid mistakes you would rather 120 | clean up before publishing, and things that have not been 121 | merged into other branches can be easily rebased without 122 | affecting other people. But once it is published, you would 123 | not want to rewind it. 124 | 125 | (2) ... if a topic branch has been fully merged to "master". 126 | Then you can delete it. More importantly, you should not 127 | build on top of it -- other people may already want to 128 | change things related to the topic as patches against your 129 | "master", so if you need further changes, it is better to 130 | fork the topic (perhaps with the same name) afresh from the 131 | tip of "master". 132 | 133 | Let's look at this example: 134 | 135 | o---o---o---o---o---o---o---o---o---o "next" 136 | / / / / 137 | / a---a---b A / / 138 | / / / / 139 | / / c---c---c---c B / 140 | / / / \ / 141 | / / / b---b C \ / 142 | / / / / \ / 143 | ---o---o---o---o---o---o---o---o---o---o---o "master" 144 | 145 | 146 | A, B and C are topic branches. 147 | 148 | * A has one fix since it was merged up to "next". 149 | 150 | * B has finished. It has been fully merged up to "master" and "next", 151 | and is ready to be deleted. 152 | 153 | * C has not merged to "next" at all. 154 | 155 | We would want to allow C to be rebased, refuse A, and encourage 156 | B to be deleted. 157 | 158 | To compute (1): 159 | 160 | git rev-list ^master ^topic next 161 | git rev-list ^master next 162 | 163 | if these match, topic has not merged in next at all. 164 | 165 | To compute (2): 166 | 167 | git rev-list master..topic 168 | 169 | if this is empty, it is fully merged to "master". 170 | -------------------------------------------------------------------------------- /spec/support/remote_repo.git/hooks/prepare-commit-msg.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to prepare the commit log message. 4 | # Called by "git commit" with the name of the file that has the 5 | # commit message, followed by the description of the commit 6 | # message's source. The hook's purpose is to edit the commit 7 | # message file. If the hook fails with a non-zero status, 8 | # the commit is aborted. 9 | # 10 | # To enable this hook, rename this file to "prepare-commit-msg". 11 | 12 | # This hook includes three examples. The first comments out the 13 | # "Conflicts:" part of a merge commit. 14 | # 15 | # The second includes the output of "git diff --name-status -r" 16 | # into the message, just before the "git status" output. It is 17 | # commented because it doesn't cope with --amend or with squashed 18 | # commits. 19 | # 20 | # The third example adds a Signed-off-by line to the message, that can 21 | # still be edited. This is rarely a good idea. 22 | 23 | case "$2,$3" in 24 | merge,) 25 | /usr/bin/perl -i.bak -ne 's/^/# /, s/^# #/#/ if /^Conflicts/ .. /#/; print' "$1" ;; 26 | 27 | # ,|template,) 28 | # /usr/bin/perl -i.bak -pe ' 29 | # print "\n" . `git diff --cached --name-status -r` 30 | # if /^#/ && $first++ == 0' "$1" ;; 31 | 32 | *) ;; 33 | esac 34 | 35 | # SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') 36 | # grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" 37 | -------------------------------------------------------------------------------- /spec/support/remote_repo.git/hooks/update.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to block unannotated tags from entering. 4 | # Called by "git receive-pack" with arguments: refname sha1-old sha1-new 5 | # 6 | # To enable this hook, rename this file to "update". 7 | # 8 | # Config 9 | # ------ 10 | # hooks.allowunannotated 11 | # This boolean sets whether unannotated tags will be allowed into the 12 | # repository. By default they won't be. 13 | # hooks.allowdeletetag 14 | # This boolean sets whether deleting tags will be allowed in the 15 | # repository. By default they won't be. 16 | # hooks.allowmodifytag 17 | # This boolean sets whether a tag may be modified after creation. By default 18 | # it won't be. 19 | # hooks.allowdeletebranch 20 | # This boolean sets whether deleting branches will be allowed in the 21 | # repository. By default they won't be. 22 | # hooks.denycreatebranch 23 | # This boolean sets whether remotely creating branches will be denied 24 | # in the repository. By default this is allowed. 25 | # 26 | 27 | # --- Command line 28 | refname="$1" 29 | oldrev="$2" 30 | newrev="$3" 31 | 32 | # --- Safety check 33 | if [ -z "$GIT_DIR" ]; then 34 | echo "Don't run this script from the command line." >&2 35 | echo " (if you want, you could supply GIT_DIR then run" >&2 36 | echo " $0 )" >&2 37 | exit 1 38 | fi 39 | 40 | if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then 41 | echo "usage: $0 " >&2 42 | exit 1 43 | fi 44 | 45 | # --- Config 46 | allowunannotated=$(git config --bool hooks.allowunannotated) 47 | allowdeletebranch=$(git config --bool hooks.allowdeletebranch) 48 | denycreatebranch=$(git config --bool hooks.denycreatebranch) 49 | allowdeletetag=$(git config --bool hooks.allowdeletetag) 50 | allowmodifytag=$(git config --bool hooks.allowmodifytag) 51 | 52 | # check for no description 53 | projectdesc=$(sed -e '1q' "$GIT_DIR/description") 54 | case "$projectdesc" in 55 | "Unnamed repository"* | "") 56 | echo "*** Project description file hasn't been set" >&2 57 | exit 1 58 | ;; 59 | esac 60 | 61 | # --- Check types 62 | # if $newrev is 0000...0000, it's a commit to delete a ref. 63 | zero="0000000000000000000000000000000000000000" 64 | if [ "$newrev" = "$zero" ]; then 65 | newrev_type=delete 66 | else 67 | newrev_type=$(git cat-file -t $newrev) 68 | fi 69 | 70 | case "$refname","$newrev_type" in 71 | refs/tags/*,commit) 72 | # un-annotated tag 73 | short_refname=${refname##refs/tags/} 74 | if [ "$allowunannotated" != "true" ]; then 75 | echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2 76 | echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2 77 | exit 1 78 | fi 79 | ;; 80 | refs/tags/*,delete) 81 | # delete tag 82 | if [ "$allowdeletetag" != "true" ]; then 83 | echo "*** Deleting a tag is not allowed in this repository" >&2 84 | exit 1 85 | fi 86 | ;; 87 | refs/tags/*,tag) 88 | # annotated tag 89 | if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1 90 | then 91 | echo "*** Tag '$refname' already exists." >&2 92 | echo "*** Modifying a tag is not allowed in this repository." >&2 93 | exit 1 94 | fi 95 | ;; 96 | refs/heads/*,commit) 97 | # branch 98 | if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then 99 | echo "*** Creating a branch is not allowed in this repository" >&2 100 | exit 1 101 | fi 102 | ;; 103 | refs/heads/*,delete) 104 | # delete branch 105 | if [ "$allowdeletebranch" != "true" ]; then 106 | echo "*** Deleting a branch is not allowed in this repository" >&2 107 | exit 1 108 | fi 109 | ;; 110 | refs/remotes/*,commit) 111 | # tracking branch 112 | ;; 113 | refs/remotes/*,delete) 114 | # delete tracking branch 115 | if [ "$allowdeletebranch" != "true" ]; then 116 | echo "*** Deleting a tracking branch is not allowed in this repository" >&2 117 | exit 1 118 | fi 119 | ;; 120 | *) 121 | # Anything else (is there anything else?) 122 | echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2 123 | exit 1 124 | ;; 125 | esac 126 | 127 | # --- Finished 128 | exit 0 129 | -------------------------------------------------------------------------------- /spec/support/remote_repo.git/info/exclude: -------------------------------------------------------------------------------- 1 | # git ls-files --others --exclude-from=.git/info/exclude 2 | # Lines that start with '#' are comments. 3 | # For a project mostly in C, the following would be a good set of 4 | # exclude patterns (uncomment them if you want to use them): 5 | # *.[oa] 6 | # *~ 7 | -------------------------------------------------------------------------------- /spec/support/remote_repo.git/objects/06/0fc29c2a831131a46e11d4c492a92c0fd124d0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/accessd/slack-deploy-bot/16a63d9fbcf988308c950def6991d2be614babe8/spec/support/remote_repo.git/objects/06/0fc29c2a831131a46e11d4c492a92c0fd124d0 -------------------------------------------------------------------------------- /spec/support/remote_repo.git/objects/10/c3096a19741a6ced5715569445f5072511f4de: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/accessd/slack-deploy-bot/16a63d9fbcf988308c950def6991d2be614babe8/spec/support/remote_repo.git/objects/10/c3096a19741a6ced5715569445f5072511f4de -------------------------------------------------------------------------------- /spec/support/remote_repo.git/objects/16/935f996a17fb5013491b2560c6b3043e718e82: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/accessd/slack-deploy-bot/16a63d9fbcf988308c950def6991d2be614babe8/spec/support/remote_repo.git/objects/16/935f996a17fb5013491b2560c6b3043e718e82 -------------------------------------------------------------------------------- /spec/support/remote_repo.git/objects/2f/5425622b1c148ca0b3c458430fa9e4c012b611: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/accessd/slack-deploy-bot/16a63d9fbcf988308c950def6991d2be614babe8/spec/support/remote_repo.git/objects/2f/5425622b1c148ca0b3c458430fa9e4c012b611 -------------------------------------------------------------------------------- /spec/support/remote_repo.git/objects/36/80008f06aae8321faa9cf22c031c99b39d1cff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/accessd/slack-deploy-bot/16a63d9fbcf988308c950def6991d2be614babe8/spec/support/remote_repo.git/objects/36/80008f06aae8321faa9cf22c031c99b39d1cff -------------------------------------------------------------------------------- /spec/support/remote_repo.git/objects/37/9a4c986e3265e08e9da42c56ec747619c56a33: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/accessd/slack-deploy-bot/16a63d9fbcf988308c950def6991d2be614babe8/spec/support/remote_repo.git/objects/37/9a4c986e3265e08e9da42c56ec747619c56a33 -------------------------------------------------------------------------------- /spec/support/remote_repo.git/objects/43/0ff1dcee43d2e8a12c3ba36f5ff20e46136152: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/accessd/slack-deploy-bot/16a63d9fbcf988308c950def6991d2be614babe8/spec/support/remote_repo.git/objects/43/0ff1dcee43d2e8a12c3ba36f5ff20e46136152 -------------------------------------------------------------------------------- /spec/support/remote_repo.git/objects/47/65132ed192e9fb14fff274b364677315fd3629: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/accessd/slack-deploy-bot/16a63d9fbcf988308c950def6991d2be614babe8/spec/support/remote_repo.git/objects/47/65132ed192e9fb14fff274b364677315fd3629 -------------------------------------------------------------------------------- /spec/support/remote_repo.git/objects/47/b73a126cec824d08d6b734f5def7fc374751a2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/accessd/slack-deploy-bot/16a63d9fbcf988308c950def6991d2be614babe8/spec/support/remote_repo.git/objects/47/b73a126cec824d08d6b734f5def7fc374751a2 -------------------------------------------------------------------------------- /spec/support/remote_repo.git/objects/4b/b84c00154e676be22ae837d534edb4ec5cecd1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/accessd/slack-deploy-bot/16a63d9fbcf988308c950def6991d2be614babe8/spec/support/remote_repo.git/objects/4b/b84c00154e676be22ae837d534edb4ec5cecd1 -------------------------------------------------------------------------------- /spec/support/remote_repo.git/objects/4d/c1968fb2fcba099965d54e2d1d1c4eb6041774: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/accessd/slack-deploy-bot/16a63d9fbcf988308c950def6991d2be614babe8/spec/support/remote_repo.git/objects/4d/c1968fb2fcba099965d54e2d1d1c4eb6041774 -------------------------------------------------------------------------------- /spec/support/remote_repo.git/objects/50/80851e4b508fe4e67478dca9c9f97c0176c4d0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/accessd/slack-deploy-bot/16a63d9fbcf988308c950def6991d2be614babe8/spec/support/remote_repo.git/objects/50/80851e4b508fe4e67478dca9c9f97c0176c4d0 -------------------------------------------------------------------------------- /spec/support/remote_repo.git/objects/51/a115a76f6a81b5f493142d69b12917d3a0516d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/accessd/slack-deploy-bot/16a63d9fbcf988308c950def6991d2be614babe8/spec/support/remote_repo.git/objects/51/a115a76f6a81b5f493142d69b12917d3a0516d -------------------------------------------------------------------------------- /spec/support/remote_repo.git/objects/72/dc5019af8538edc560f2ee6cf600154d5ff76a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/accessd/slack-deploy-bot/16a63d9fbcf988308c950def6991d2be614babe8/spec/support/remote_repo.git/objects/72/dc5019af8538edc560f2ee6cf600154d5ff76a -------------------------------------------------------------------------------- /spec/support/remote_repo.git/objects/8d/629c409367c902311ec58a0aaa5e5b82a953bd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/accessd/slack-deploy-bot/16a63d9fbcf988308c950def6991d2be614babe8/spec/support/remote_repo.git/objects/8d/629c409367c902311ec58a0aaa5e5b82a953bd -------------------------------------------------------------------------------- /spec/support/remote_repo.git/objects/95/99fdeb034597d90d72d2f58396dee096885b79: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/accessd/slack-deploy-bot/16a63d9fbcf988308c950def6991d2be614babe8/spec/support/remote_repo.git/objects/95/99fdeb034597d90d72d2f58396dee096885b79 -------------------------------------------------------------------------------- /spec/support/remote_repo.git/objects/9c/e89a6b4ae562bbd30ccff8e2cd06b373cb813a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/accessd/slack-deploy-bot/16a63d9fbcf988308c950def6991d2be614babe8/spec/support/remote_repo.git/objects/9c/e89a6b4ae562bbd30ccff8e2cd06b373cb813a -------------------------------------------------------------------------------- /spec/support/remote_repo.git/objects/9d/f352c6fc26a5c8acf2b44b32b896e4e8031cf5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/accessd/slack-deploy-bot/16a63d9fbcf988308c950def6991d2be614babe8/spec/support/remote_repo.git/objects/9d/f352c6fc26a5c8acf2b44b32b896e4e8031cf5 -------------------------------------------------------------------------------- /spec/support/remote_repo.git/objects/a5/09b88fc4b226e0599fb0d6bead1805b5489786: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/accessd/slack-deploy-bot/16a63d9fbcf988308c950def6991d2be614babe8/spec/support/remote_repo.git/objects/a5/09b88fc4b226e0599fb0d6bead1805b5489786 -------------------------------------------------------------------------------- /spec/support/remote_repo.git/objects/aa/0f7cbbbc4d8c013daa2b5be8c14e0b4c0032b6: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/accessd/slack-deploy-bot/16a63d9fbcf988308c950def6991d2be614babe8/spec/support/remote_repo.git/objects/aa/0f7cbbbc4d8c013daa2b5be8c14e0b4c0032b6 -------------------------------------------------------------------------------- /spec/support/remote_repo.git/objects/ad/24149d789e59d4b5f9ce41cda90110ca0f98b7: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/accessd/slack-deploy-bot/16a63d9fbcf988308c950def6991d2be614babe8/spec/support/remote_repo.git/objects/ad/24149d789e59d4b5f9ce41cda90110ca0f98b7 -------------------------------------------------------------------------------- /spec/support/remote_repo.git/objects/ae/fde3a01f6e10d72fd4899ce14c8b2654d3eb45: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/accessd/slack-deploy-bot/16a63d9fbcf988308c950def6991d2be614babe8/spec/support/remote_repo.git/objects/ae/fde3a01f6e10d72fd4899ce14c8b2654d3eb45 -------------------------------------------------------------------------------- /spec/support/remote_repo.git/objects/b8/a1fd79948bc23a151648eb49bd7cbf77405353: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/accessd/slack-deploy-bot/16a63d9fbcf988308c950def6991d2be614babe8/spec/support/remote_repo.git/objects/b8/a1fd79948bc23a151648eb49bd7cbf77405353 -------------------------------------------------------------------------------- /spec/support/remote_repo.git/objects/be/1c293b15d3942015b75c39688ce1859bc238e5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/accessd/slack-deploy-bot/16a63d9fbcf988308c950def6991d2be614babe8/spec/support/remote_repo.git/objects/be/1c293b15d3942015b75c39688ce1859bc238e5 -------------------------------------------------------------------------------- /spec/support/remote_repo.git/objects/e0/94ae5f14dc92b216a0dcb1a66200f6f5ba07c7: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/accessd/slack-deploy-bot/16a63d9fbcf988308c950def6991d2be614babe8/spec/support/remote_repo.git/objects/e0/94ae5f14dc92b216a0dcb1a66200f6f5ba07c7 -------------------------------------------------------------------------------- /spec/support/remote_repo.git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/accessd/slack-deploy-bot/16a63d9fbcf988308c950def6991d2be614babe8/spec/support/remote_repo.git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 -------------------------------------------------------------------------------- /spec/support/remote_repo.git/objects/f1/73c70adcbe9e011b14213bc49e4135a5a20c4d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/accessd/slack-deploy-bot/16a63d9fbcf988308c950def6991d2be614babe8/spec/support/remote_repo.git/objects/f1/73c70adcbe9e011b14213bc49e4135a5a20c4d -------------------------------------------------------------------------------- /spec/support/remote_repo.git/objects/f7/053cb5cf49a82236e947a3c392f7bc76ccdc5c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/accessd/slack-deploy-bot/16a63d9fbcf988308c950def6991d2be614babe8/spec/support/remote_repo.git/objects/f7/053cb5cf49a82236e947a3c392f7bc76ccdc5c -------------------------------------------------------------------------------- /spec/support/remote_repo.git/refs/heads/master: -------------------------------------------------------------------------------- 1 | 4dc1968fb2fcba099965d54e2d1d1c4eb6041774 2 | -------------------------------------------------------------------------------- /spec/support/remote_repo.git/refs/heads/my-feature: -------------------------------------------------------------------------------- 1 | 060fc29c2a831131a46e11d4c492a92c0fd124d0 2 | -------------------------------------------------------------------------------- /spec/support/respond_with_slack_message.rb: -------------------------------------------------------------------------------- 1 | require 'rspec/expectations' 2 | 3 | RSpec::Matchers.define :message_args do |expected| 4 | match { |actual| actual[:channel] == expected[:channel] } 5 | expected[:text] = Array(expected[:text]) 6 | expected[:text].each do |e| 7 | match { |actual| actual[:text].include?(e) } 8 | end 9 | end 10 | 11 | RSpec::Matchers.define :respond_with_slack_message do |expected| 12 | match do |actual| 13 | client = if respond_to?(:client) 14 | send(:client) 15 | else 16 | SlackRubyBot::Client.new 17 | end 18 | 19 | message_command = SlackRubyBot::Hooks::Message.new 20 | channel, user, message = parse(actual) 21 | 22 | allow(Giphy).to receive(:random) if defined?(Giphy) 23 | 24 | if (expected[:message_includes]) 25 | expect(client).to receive(:message).with(message_args(channel: channel, text: expected[:message_includes])) 26 | end 27 | if (expected[:messages]) 28 | expected[:messages].each do |msg| 29 | expect(client).to receive(:message).with(channel: channel, text: msg) 30 | end 31 | end 32 | allow(client).to receive(:message).with(channel: channel, text: instance_of(String)) 33 | message_command.call(client, Hashie::Mash.new(text: message, channel: channel, user: user)) 34 | true 35 | end 36 | 37 | private 38 | 39 | def parse(actual) 40 | actual = { message: actual } unless actual.is_a?(Hash) 41 | [actual[:channel] || ENV['SLACK_CHANNEL'], actual[:user] || ENV['SLACK_USER_ID'], actual[:message]] 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/support/say_to_bot.rb: -------------------------------------------------------------------------------- 1 | def say_to_bot(message:, channel: ENV['SLACK_CHANNEL'], user: ENV['SLACK_USER_ID']) 2 | client = if respond_to?(:client) 3 | send(:client) 4 | else 5 | SlackRubyBot::Client.new 6 | end 7 | message_command = SlackRubyBot::Hooks::Message.new 8 | allow(Giphy).to receive(:random) if defined?(Giphy) 9 | allow(client).to receive(:message).with(channel: channel, text: instance_of(String)) 10 | message_command.call(client, Hashie::Mash.new(text: message, channel: channel, user: user)) 11 | true 12 | end 13 | -------------------------------------------------------------------------------- /spec/support/slack_deploy_bot.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | config.before do 3 | SlackRubyBot.config.user = 'deploy-bot' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/vcr/rtm_start.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: https://slack.com/api/rtm.start 6 | body: 7 | encoding: UTF-8 8 | string: token=token 9 | headers: 10 | Accept: 11 | - application/json; charset=utf-8 12 | User-Agent: 13 | - Slack Ruby Client/0.5.5 14 | Content-Type: 15 | - application/x-www-form-urlencoded 16 | Accept-Encoding: 17 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 18 | response: 19 | status: 20 | code: 200 21 | message: OK 22 | headers: 23 | Access-Control-Allow-Origin: 24 | - "*" 25 | Cache-Control: 26 | - private, no-cache, no-store, must-revalidate 27 | Content-Security-Policy: 28 | - referrer no-referrer; 29 | Content-Type: 30 | - application/json; charset=utf-8 31 | Date: 32 | - Sun, 31 Jan 2016 22:42:56 GMT 33 | Expires: 34 | - Mon, 26 Jul 1997 05:00:00 GMT 35 | Pragma: 36 | - no-cache 37 | Server: 38 | - Apache 39 | Strict-Transport-Security: 40 | - max-age=31536000; includeSubDomains; preload 41 | Vary: 42 | - Accept-Encoding 43 | X-Accepted-Oauth-Scopes: 44 | - rtm:stream,client 45 | X-Content-Type-Options: 46 | - nosniff 47 | X-Oauth-Scopes: 48 | - identify,read,post,client 49 | X-Xss-Protection: 50 | - '0' 51 | Content-Length: 52 | - '7152' 53 | Connection: 54 | - keep-alive 55 | body: 56 | encoding: ASCII-8BIT 57 | string: '{"ok":true,"self":{"id":"U07518DTL","name":"rubybot","prefs":{"highlight_words":"","user_colors":"","color_names_in_list":true,"growls_enabled":true,"tz":null,"push_dm_alert":true,"push_mention_alert":true,"msg_replies":"{ 58 | \"flexpane\":false }","push_everything":true,"push_idle_wait":2,"push_sound":"b2.mp3","push_loud_channels":"","push_mention_channels":"","push_loud_channels_set":"","email_alerts":"instant","email_alerts_sleep_until":0,"email_misc":true,"email_weekly":true,"welcome_message_hidden":false,"all_channels_loud":true,"loud_channels":"","never_channels":"","loud_channels_set":"","show_member_presence":true,"search_sort":"timestamp","expand_inline_imgs":true,"expand_internal_inline_imgs":true,"expand_snippets":false,"posts_formatting_guide":true,"seen_live_support_popup":false,"seen_welcome_2":false,"seen_ssb_prompt":false,"seen_spaces_new_xp_tooltip":false,"spaces_new_xp_banner_dismissed":false,"search_only_my_channels":false,"emoji_mode":"default","emoji_use":"","has_invited":false,"has_uploaded":false,"has_created_channel":false,"search_exclude_channels":"","messages_theme":"default","webapp_spellcheck":true,"no_joined_overlays":false,"no_created_overlays":false,"dropbox_enabled":false,"seen_domain_invite_reminder":false,"seen_member_invite_reminder":false,"mute_sounds":false,"arrow_history":false,"tab_ui_return_selects":true,"obey_inline_img_limit":true,"new_msg_snd":"knock_brush.mp3","collapsible":false,"collapsible_by_click":true,"require_at":false,"ssb_space_window":"","mac_ssb_bounce":"","mac_ssb_bullet":true,"expand_non_media_attachments":true,"show_typing":true,"pagekeys_handled":true,"last_snippet_type":"","display_real_names_override":0,"time24":false,"enter_is_special_in_tbt":false,"graphic_emoticons":false,"convert_emoticons":true,"autoplay_chat_sounds":true,"ss_emojis":true,"sidebar_behavior":"","seen_onboarding_start":false,"onboarding_cancelled":false,"seen_onboarding_slackbot_conversation":false,"seen_onboarding_channels":false,"seen_onboarding_direct_messages":false,"seen_onboarding_invites":false,"seen_onboarding_search":false,"seen_onboarding_recent_mentions":false,"seen_onboarding_starred_items":false,"seen_onboarding_private_groups":false,"onboarding_slackbot_conversation_step":0,"dnd_enabled":false,"dnd_start_hour":"22:00","dnd_end_hour":"08:00","mark_msgs_read_immediately":true,"start_scroll_at_oldest":true,"snippet_editor_wrap_long_lines":false,"ls_disabled":false,"sidebar_theme":"default","sidebar_theme_custom_values":"","f_key_search":false,"k_key_omnibox":true,"speak_growls":false,"mac_speak_voice":"com.apple.speech.synthesis.voice.Alex","mac_speak_speed":250,"comma_key_prefs":false,"at_channel_suppressed_channels":"","push_at_channel_suppressed_channels":"","prompted_for_email_disabling":false,"full_text_extracts":false,"no_text_in_notifications":false,"muted_channels":"","no_macssb1_banner":false,"no_winssb1_banner":false,"no_omnibox_in_channels":false,"k_key_omnibox_auto_hide_count":0,"hide_user_group_info_pane":false,"mentions_exclude_at_user_groups":false,"privacy_policy_seen":true,"search_exclude_bots":false,"fuzzy_matching":false,"load_lato_2":false,"fuller_timestamps":false,"last_seen_at_channel_warning":0,"flex_resize_window":false,"msg_preview":false,"msg_preview_displaces":true,"msg_preview_persistent":true,"emoji_autocomplete_big":false,"winssb_run_from_tray":true,"winssb_window_flash_behavior":"idle","two_factor_auth_enabled":false,"two_factor_type":null,"two_factor_backup_type":null,"mentions_exclude_at_channels":true,"confirm_clear_all_unreads":true,"confirm_user_marked_away":true,"box_enabled":false,"seen_single_emoji_msg":false,"confirm_sh_call_start":true,"preferred_skin_tone":"","show_all_skin_tones":false,"separate_private_channels":false,"whats_new_read":0,"hotness":false,"frecency_jumper":"","jumbomoji":false},"created":1435864712,"manual_presence":"active"},"team":{"id":"T04KB5WQH","name":"dblock","email_domain":"dblock.org","domain":"dblockdotorg","msg_edit_window_mins":-1,"prefs":{"default_channels":["C04KB5X4D","C04KB5X4Z"],"posts_migrating":0,"msg_edit_window_mins":-1,"allow_message_deletion":true,"hide_referers":true,"display_real_names":false,"disable_file_uploads":"allow_all","who_can_at_everyone":"regular","who_can_at_channel":"ra","warn_before_at_channel":"always","who_can_create_channels":"regular","who_can_create_shared_channels":"admin","who_can_archive_channels":"regular","who_can_create_groups":"ra","who_can_post_general":"ra","who_can_kick_channels":"admin","who_can_kick_groups":"regular","dnd_enabled":true,"dnd_start_hour":"22:00","dnd_end_hour":"08:00","allow_shared_channels":false,"who_has_team_visibility":"ra","who_can_create_delete_user_groups":"admin","who_can_edit_user_groups":"admin","who_can_change_team_profile":"admin","retention_type":0,"retention_duration":0,"group_retention_type":0,"group_retention_duration":0,"dm_retention_type":0,"dm_retention_duration":0,"file_retention_type":0,"file_retention_duration":0,"allow_retention_override":true,"require_at_for_mention":0,"compliance_export_start":0,"invites_only_admins":true,"auth_mode":"normal","who_can_manage_integrations":{"type":["regular"]}},"icon":{"image_34":"https:\/\/s3-us-west-2.amazonaws.com\/slack-files2\/avatars\/2015-04-28\/4657218807_d480d2ee610d2e8aacfe_34.jpg","image_44":"https:\/\/s3-us-west-2.amazonaws.com\/slack-files2\/avatars\/2015-04-28\/4657218807_d480d2ee610d2e8aacfe_44.jpg","image_68":"https:\/\/s3-us-west-2.amazonaws.com\/slack-files2\/avatars\/2015-04-28\/4657218807_d480d2ee610d2e8aacfe_68.jpg","image_88":"https:\/\/s3-us-west-2.amazonaws.com\/slack-files2\/avatars\/2015-04-28\/4657218807_d480d2ee610d2e8aacfe_88.jpg","image_102":"https:\/\/s3-us-west-2.amazonaws.com\/slack-files2\/avatars\/2015-04-28\/4657218807_d480d2ee610d2e8aacfe_102.jpg","image_132":"https:\/\/s3-us-west-2.amazonaws.com\/slack-files2\/avatars\/2015-04-28\/4657218807_d480d2ee610d2e8aacfe_132.jpg","image_original":"https:\/\/s3-us-west-2.amazonaws.com\/slack-files2\/avatars\/2015-04-28\/4657218807_d480d2ee610d2e8aacfe_original.jpg"},"over_storage_limit":false,"plan":"","over_integrations_limit":true},"latest_event_ts":"1454279576.000000","channels":[{"id":"C09C5GYHF","name":"a1","is_channel":true,"created":1440078580,"creator":"U04KB5WQR","is_archived":true,"is_general":false,"has_pins":false,"is_member":false},{"id":"C09C598QL","name":"a2","is_channel":true,"created":1440078625,"creator":"U04KB5WQR","is_archived":true,"is_general":false,"has_pins":false,"is_member":false},{"id":"C0ALZCCSC","name":"a3","is_channel":true,"created":1442284272,"creator":"U04KB5WQR","is_archived":true,"is_general":false,"has_pins":false,"is_member":false},{"id":"C09C5DS65","name":"aliases","is_channel":true,"created":1440078448,"creator":"U04KB5WQR","is_archived":true,"is_general":false,"has_pins":false,"is_member":false},{"id":"C092CEUTY","name":"art","is_channel":true,"created":1439479078,"creator":"U04KB5WQR","is_archived":true,"is_general":false,"has_pins":false,"is_member":false},{"id":"C0HLE6UMV","name":"botaliases","is_channel":true,"created":1451914370,"creator":"U04KB5WQR","is_archived":true,"is_general":false,"has_pins":false,"is_member":false},{"id":"C092FEFQU","name":"cab","is_channel":true,"created":1439481937,"creator":"U04KB5WQR","is_archived":true,"is_general":false,"has_pins":false,"is_member":false},{"id":"C0HNTD0CW","name":"chess","is_channel":true,"created":1452005790,"creator":"U04KB5WQR","is_archived":false,"is_general":false,"has_pins":false,"is_member":false},{"id":"C0HCU3R2S","name":"chess1","is_channel":true,"created":1451249172,"creator":"U04KB5WQR","is_archived":true,"is_general":false,"has_pins":false,"is_member":false},{"id":"C06JKEMJ8","name":"demo","is_channel":true,"created":1434735992,"creator":"U04KB5WQR","is_archived":false,"is_general":false,"has_pins":false,"is_member":false},{"id":"C06JKF1QQ","name":"example","is_channel":true,"created":1434736136,"creator":"U04KB5WQR","is_archived":true,"is_general":false,"has_pins":false,"is_member":false},{"id":"C0HU1L26L","name":"fs","is_channel":true,"created":1452177776,"creator":"U04KB5WQR","is_archived":false,"is_general":false,"has_pins":false,"is_member":false},{"id":"C04KB5X4D","name":"general","is_channel":true,"created":1430222230,"creator":"U04KB5WQR","is_archived":false,"is_general":true,"has_pins":false,"is_member":true,"last_read":"1435863302.000026","latest":{"type":"message","user":"U07518DTL","text":"Hello 59 | World","ts":"1453587706.000054"},"unread_count":320,"unread_count_display":281,"members":["U04JPQ0JS","U04JZBDQQ","U04KB5WQR","U06JGTU5V","U07518DTL","U07KECJ77","U092BDCLV","U092V4E9L","U0EA7TZJN","U0H701CJZ","U0HLFUZLJ","U0HPMN0GY"],"topic":{"value":"","creator":"","last_set":0},"purpose":{"value":"This 60 | channel is for team-wide communication and announcements. All team members 61 | are in this channel.","creator":"","last_set":0}},{"id":"C0HLE0BBL","name":"gifs","is_channel":true,"created":1451913914,"creator":"U04KB5WQR","is_archived":true,"is_general":false,"has_pins":false,"is_member":false},{"id":"C092Y488H","name":"hack1","is_channel":true,"created":1439498209,"creator":"U092V4E9L","is_archived":false,"is_general":false,"has_pins":false,"is_member":false},{"id":"C0HE7877F","name":"kasparov","is_channel":true,"created":1451342301,"creator":"U04KB5WQR","is_archived":true,"is_general":false,"has_pins":false,"is_member":false},{"id":"C06JH25U4","name":"math","is_channel":true,"created":1434733062,"creator":"U04KB5WQR","is_archived":false,"is_general":false,"has_pins":false,"is_member":false},{"id":"C04M42N6Z","name":"mattz","is_channel":true,"created":1430514740,"creator":"U04KB5WQR","is_archived":false,"is_general":false,"has_pins":false,"is_member":false},{"id":"C0HLFUZD5","name":"playplay","is_channel":true,"created":1451916478,"creator":"U0HLFUZLJ","is_archived":true,"is_general":false,"has_pins":false,"is_member":false},{"id":"C0HNSS6H5","name":"pong","is_channel":true,"created":1452005545,"creator":"U04KB5WQR","is_archived":false,"is_general":false,"has_pins":false,"is_member":false},{"id":"C0HNQ6KEF","name":"pool","is_channel":true,"created":1452005604,"creator":"U04KB5WQR","is_archived":false,"is_general":false,"has_pins":false,"is_member":false},{"id":"C04KB5X4Z","name":"random","is_channel":true,"created":1430222230,"creator":"U04KB5WQR","is_archived":false,"is_general":false,"has_pins":false,"is_member":false},{"id":"C0751A9HC","name":"rubybot-demo","is_channel":true,"created":1435864859,"creator":"U04KB5WQR","is_archived":true,"is_general":false,"has_pins":false,"is_member":false},{"id":"C0JHNAB5H","name":"rubybot-test","is_channel":true,"created":1452870939,"creator":"U04KB5WQR","is_archived":false,"is_general":false,"has_pins":false,"is_member":true,"last_read":"1452870938.000002","latest":{"type":"message","user":"U07518DTL","text":"```\n{\n \"ok\": 62 | true,\n \"args\": {\n \"token\": \"token\"\n }\n}```","ts":"1452871397.000067"},"unread_count":47,"unread_count_display":24,"members":["U04KB5WQR","U07518DTL"],"topic":{"value":"","creator":"","last_set":0},"purpose":{"value":"","creator":"","last_set":0}},{"id":"C0HTYU35K","name":"sh","is_channel":true,"created":1452178941,"creator":"U04KB5WQR","is_archived":false,"is_general":false,"has_pins":false,"is_member":true,"last_read":"1452178941.000002","latest":{"type":"message","user":"U07518DTL","text":"```\/test.txt```","ts":"1452296877.000020"},"unread_count":97,"unread_count_display":58,"members":["U04KB5WQR","U07518DTL"],"topic":{"value":"","creator":"","last_set":0},"purpose":{"value":"","creator":"","last_set":0}},{"id":"C0HPJ6XK2","name":"slackslack","is_channel":true,"created":1452022382,"creator":"U04KB5WQR","is_archived":true,"is_general":false,"has_pins":false,"is_member":false},{"id":"C0HPLJHU1","name":"slakslak","is_channel":true,"created":1452024884,"creator":"U04KB5WQR","is_archived":false,"is_general":false,"has_pins":false,"is_member":false},{"id":"C085YQLD8","name":"t1","is_channel":true,"created":1437855306,"creator":"U04KB5WQR","is_archived":true,"is_general":false,"has_pins":false,"is_member":false},{"id":"C085Z22NB","name":"t2","is_channel":true,"created":1437856697,"creator":"U04KB5WQR","is_archived":true,"is_general":false,"has_pins":false,"is_member":false},{"id":"C0860L4UU","name":"t3","is_channel":true,"created":1437863185,"creator":"U04KB5WQR","is_archived":true,"is_general":false,"has_pins":false,"is_member":false},{"id":"C0GPSJKJ8","name":"t4","is_channel":true,"created":1450273967,"creator":"U04KB5WQR","is_archived":true,"is_general":false,"has_pins":false,"is_member":false},{"id":"C0GPU3P53","name":"t5","is_channel":true,"created":1450274014,"creator":"U04KB5WQR","is_archived":true,"is_general":false,"has_pins":false,"is_member":false},{"id":"C0HPNR78C","name":"t6","is_channel":true,"created":1452030336,"creator":"U04KB5WQR","is_archived":true,"is_general":false,"has_pins":false,"is_member":false},{"id":"C0FEXLXGE","name":"tattletale","is_channel":true,"created":1448754718,"creator":"U04KB5WQR","is_archived":true,"is_general":false,"has_pins":false,"is_member":false},{"id":"C0HNTKC8G","name":"tictac","is_channel":true,"created":1452005621,"creator":"U04KB5WQR","is_archived":false,"is_general":false,"has_pins":false,"is_member":false},{"id":"C07S5SGGN","name":"weather","is_channel":true,"created":1437323916,"creator":"U04KB5WQR","is_archived":true,"is_general":false,"has_pins":false,"is_member":false},{"id":"C092V2M63","name":"whois","is_channel":true,"created":1439495329,"creator":"U092V4E9L","is_archived":false,"is_general":false,"has_pins":false,"is_member":false}],"groups":[{"id":"G0K7EV5A7","name":"mpdm-dblock--rubybot--player1-1","is_group":true,"created":1453561861,"creator":"U04KB5WQR","is_archived":false,"is_mpim":true,"has_pins":false,"is_open":false,"last_read":"0000000000.000000","latest":null,"unread_count":0,"unread_count_display":0,"members":["U04KB5WQR","U0HLFUZLJ","U07518DTL"],"topic":{"value":"Group 63 | messaging","creator":"U04KB5WQR","last_set":1453561861},"purpose":{"value":"Group 64 | messaging with: @dblock @rubybot @player1","creator":"U04KB5WQR","last_set":1453561861}}],"ims":[{"id":"D07519J57","is_im":true,"user":"USLACKBOT","created":1435864712,"has_pins":false,"last_read":"0000000000.000000","latest":{"text":"4576f752ce2734db8803d1689b896787","username":"bot","type":"message","subtype":"bot_message","ts":"1453474448.000004"},"unread_count":98,"unread_count_display":93,"is_open":true},{"id":"D0751GYBD","is_im":true,"user":"U04KB5WQR","created":1435864712,"has_pins":false,"last_read":"0000000000.000000","latest":{"type":"message","user":"U07518DTL","text":"Sorry 65 | <@U04KB5WQR>, I don''t understand that command!\n","attachments":[{"fallback":"267x281px 66 | image","image_url":"http:\/\/media1.giphy.com\/media\/nbaeOn3Ici544\/giphy.gif","image_width":267,"image_height":281,"image_bytes":848273,"from_url":"http:\/\/media1.giphy.com\/media\/nbaeOn3Ici544\/giphy.gif","id":1}],"ts":"1454103796.000015"},"unread_count":1000,"unread_count_display":1000,"is_open":true}],"cache_ts":1454280176,"subteams":{"self":[],"all":[]},"dnd":{"dnd_enabled":false,"next_dnd_start_ts":1,"next_dnd_end_ts":1,"snooze_enabled":false},"users":[{"id":"U092BDCLV","team_id":"T04KB5WQH","name":"artsy","deleted":true,"profile":{"bot_id":"B092B5DDG","image_24":"https:\/\/avatars.slack-edge.com\/2015-08-13\/9080316676_7bbab49314a0429c7f7d_24.jpg","image_32":"https:\/\/avatars.slack-edge.com\/2015-08-13\/9080316676_7bbab49314a0429c7f7d_32.jpg","image_48":"https:\/\/avatars.slack-edge.com\/2015-08-13\/9080316676_7bbab49314a0429c7f7d_48.jpg","image_72":"https:\/\/avatars.slack-edge.com\/2015-08-13\/9080316676_7bbab49314a0429c7f7d_48.jpg","image_192":"https:\/\/avatars.slack-edge.com\/2015-08-13\/9080316676_7bbab49314a0429c7f7d_48.jpg","image_512":"https:\/\/avatars.slack-edge.com\/2015-08-13\/9080316676_7bbab49314a0429c7f7d_48.jpg","image_1024":"https:\/\/avatars.slack-edge.com\/2015-08-13\/9080316676_7bbab49314a0429c7f7d_48.jpg","image_original":"https:\/\/avatars.slack-edge.com\/2015-08-13\/9080316676_7bbab49314a0429c7f7d_original.jpg","first_name":"Artsy","last_name":"Bot","title":"Searches 67 | Artsy.","real_name":"Artsy Bot","real_name_normalized":"Artsy Bot","fields":null},"is_bot":true,"presence":"away"},{"id":"U07KECJ77","team_id":"T04KB5WQH","name":"aws","deleted":true,"profile":{"bot_id":"B07KECJ6R","image_24":"https:\/\/avatars.slack-edge.com\/2015-07-14\/7660472085_4755a86e204c9706c84f_24.jpg","image_32":"https:\/\/avatars.slack-edge.com\/2015-07-14\/7660472085_4755a86e204c9706c84f_32.jpg","image_48":"https:\/\/avatars.slack-edge.com\/2015-07-14\/7660472085_4755a86e204c9706c84f_48.jpg","image_72":"https:\/\/avatars.slack-edge.com\/2015-07-14\/7660472085_4755a86e204c9706c84f_48.jpg","image_192":"https:\/\/avatars.slack-edge.com\/2015-07-14\/7660472085_4755a86e204c9706c84f_48.jpg","image_original":"https:\/\/avatars.slack-edge.com\/2015-07-14\/7660472085_4755a86e204c9706c84f_original.jpg","first_name":"AWS","last_name":"Cloud","real_name":"AWS 68 | Cloud","real_name_normalized":"AWS Cloud","fields":null},"is_bot":true,"presence":"away"},{"id":"U0EA7TZJN","team_id":"T04KB5WQH","name":"bar","deleted":true,"profile":{"bot_id":"B0EA31LQM","image_24":"https:\/\/avatars.slack-edge.com\/2015-11-11\/14346833313_f359ba59a1211d17912b_24.jpg","image_32":"https:\/\/avatars.slack-edge.com\/2015-11-11\/14346833313_f359ba59a1211d17912b_32.jpg","image_48":"https:\/\/avatars.slack-edge.com\/2015-11-11\/14346833313_f359ba59a1211d17912b_48.jpg","image_72":"https:\/\/avatars.slack-edge.com\/2015-11-11\/14346833313_f359ba59a1211d17912b_72.jpg","image_192":"https:\/\/avatars.slack-edge.com\/2015-11-11\/14346833313_f359ba59a1211d17912b_192.jpg","image_512":"https:\/\/avatars.slack-edge.com\/2015-11-11\/14346833313_f359ba59a1211d17912b_512.jpg","image_1024":"https:\/\/avatars.slack-edge.com\/2015-11-11\/14346833313_f359ba59a1211d17912b_512.jpg","image_original":"https:\/\/avatars.slack-edge.com\/2015-11-11\/14346833313_f359ba59a1211d17912b_original.jpg","real_name":"","real_name_normalized":"","fields":null},"is_bot":true,"presence":"away"},{"id":"U092V4E9L","team_id":"T04KB5WQH","name":"broskoski","deleted":true,"profile":{"first_name":"Cab","real_name":"Cab","real_name_normalized":"Cab","email":"cab@artsymail.com","image_24":"https:\/\/secure.gravatar.com\/avatar\/99621cbca6ea94706a8f5dcdc5e8af4a.jpg?s=24&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F0180%2Fimg%2Favatars%2Fava_0017-24.png","image_32":"https:\/\/secure.gravatar.com\/avatar\/99621cbca6ea94706a8f5dcdc5e8af4a.jpg?s=32&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F66f9%2Fimg%2Favatars%2Fava_0017-32.png","image_48":"https:\/\/secure.gravatar.com\/avatar\/99621cbca6ea94706a8f5dcdc5e8af4a.jpg?s=48&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F66f9%2Fimg%2Favatars%2Fava_0017-48.png","image_72":"https:\/\/secure.gravatar.com\/avatar\/99621cbca6ea94706a8f5dcdc5e8af4a.jpg?s=72&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F66f9%2Fimg%2Favatars%2Fava_0017-72.png","image_192":"https:\/\/secure.gravatar.com\/avatar\/99621cbca6ea94706a8f5dcdc5e8af4a.jpg?s=192&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F7fa9%2Fimg%2Favatars%2Fava_0017-192.png","image_512":"https:\/\/secure.gravatar.com\/avatar\/99621cbca6ea94706a8f5dcdc5e8af4a.jpg?s=512&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F7fa9%2Fimg%2Favatars%2Fava_0017-512.png","fields":null},"presence":"away"},{"id":"U0H701CJZ","team_id":"T04KB5WQH","name":"chessbot","deleted":false,"status":null,"color":"df3dc0","real_name":"PlayPlay.io 69 | - Chess for Slack","tz":null,"tz_label":"Pacific Standard Time","tz_offset":-28800,"profile":{"first_name":"PlayPlay.io 70 | - Chess for Slack","bot_id":"B0H74MK3J","api_app_id":"A0H70N5C3","avatar_hash":"5bb4aaddb73b","image_24":"https:\/\/avatars.slack-edge.com\/2015-12-25\/17392306678_5bb4aaddb73b7f2a86c3_24.png","image_32":"https:\/\/avatars.slack-edge.com\/2015-12-25\/17392306678_5bb4aaddb73b7f2a86c3_32.png","image_48":"https:\/\/avatars.slack-edge.com\/2015-12-25\/17392306678_5bb4aaddb73b7f2a86c3_48.png","image_72":"https:\/\/avatars.slack-edge.com\/2015-12-25\/17392306678_5bb4aaddb73b7f2a86c3_72.png","image_192":"https:\/\/avatars.slack-edge.com\/2015-12-25\/17392306678_5bb4aaddb73b7f2a86c3_192.png","image_512":"https:\/\/avatars.slack-edge.com\/2015-12-25\/17392306678_5bb4aaddb73b7f2a86c3_512.png","image_1024":"https:\/\/avatars.slack-edge.com\/2015-12-25\/17392306678_5bb4aaddb73b7f2a86c3_512.png","image_original":"https:\/\/avatars.slack-edge.com\/2015-12-25\/17392306678_5bb4aaddb73b7f2a86c3_original.png","real_name":"PlayPlay.io 71 | - Chess for Slack","real_name_normalized":"PlayPlay.io - Chess for Slack","fields":null},"is_admin":false,"is_owner":false,"is_primary_owner":false,"is_restricted":false,"is_ultra_restricted":false,"is_bot":true,"presence":"active"},{"id":"U04KB5WQR","team_id":"T04KB5WQH","name":"dblock","deleted":false,"status":null,"color":"9f69e7","real_name":"Dan","tz":"America\/Indiana\/Indianapolis","tz_label":"Eastern 72 | Standard Time","tz_offset":-18000,"profile":{"fields":[],"first_name":"Dan","last_name":"","title":"","phone":"","skype":"","real_name":"Dan","real_name_normalized":"Dan","email":"dblock@dblock.org","image_24":"https:\/\/secure.gravatar.com\/avatar\/3d925b45ac07ec0ae5bd04888f6c5b61.jpg?s=24&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F66f9%2Fimg%2Favatars%2Fava_0015-24.png","image_32":"https:\/\/secure.gravatar.com\/avatar\/3d925b45ac07ec0ae5bd04888f6c5b61.jpg?s=32&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F66f9%2Fimg%2Favatars%2Fava_0015-32.png","image_48":"https:\/\/secure.gravatar.com\/avatar\/3d925b45ac07ec0ae5bd04888f6c5b61.jpg?s=48&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F66f9%2Fimg%2Favatars%2Fava_0015-48.png","image_72":"https:\/\/secure.gravatar.com\/avatar\/3d925b45ac07ec0ae5bd04888f6c5b61.jpg?s=72&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F66f9%2Fimg%2Favatars%2Fava_0015-72.png","image_192":"https:\/\/secure.gravatar.com\/avatar\/3d925b45ac07ec0ae5bd04888f6c5b61.jpg?s=192&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F7fa9%2Fimg%2Favatars%2Fava_0015-192.png","image_512":"https:\/\/secure.gravatar.com\/avatar\/3d925b45ac07ec0ae5bd04888f6c5b61.jpg?s=512&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F7fa9%2Fimg%2Favatars%2Fava_0015-512.png"},"is_admin":true,"is_owner":true,"is_primary_owner":true,"is_restricted":false,"is_ultra_restricted":false,"is_bot":false,"presence":"active"},{"id":"U04JZBDQQ","team_id":"T04KB5WQH","name":"foo","deleted":true,"profile":{"bot_id":"B04JZBDQC","image_24":"https:\/\/avatars.slack-edge.com\/2015-04-28\/4645387956_44604ca32e800d947f22_24.jpg","image_32":"https:\/\/avatars.slack-edge.com\/2015-04-28\/4645387956_44604ca32e800d947f22_32.jpg","image_48":"https:\/\/avatars.slack-edge.com\/2015-04-28\/4645387956_44604ca32e800d947f22_48.jpg","image_72":"https:\/\/avatars.slack-edge.com\/2015-04-28\/4645387956_44604ca32e800d947f22_72.jpg","image_192":"https:\/\/avatars.slack-edge.com\/2015-04-28\/4645387956_44604ca32e800d947f22_192.jpg","image_original":"https:\/\/avatars.slack-edge.com\/2015-04-28\/4645387956_44604ca32e800d947f22_original.jpg","real_name":"","real_name_normalized":"","fields":null},"is_bot":true,"presence":"away"},{"id":"U04JPQ0JS","team_id":"T04KB5WQH","name":"gamebot","deleted":true,"profile":{"bot_id":"B04JPQ0JE","image_24":"https:\/\/avatars.slack-edge.com\/2015-04-28\/4635825662_28d3661bf00dc6013442_24.jpg","image_32":"https:\/\/avatars.slack-edge.com\/2015-04-28\/4635825662_28d3661bf00dc6013442_32.jpg","image_48":"https:\/\/avatars.slack-edge.com\/2015-04-28\/4635825662_28d3661bf00dc6013442_48.jpg","image_72":"https:\/\/avatars.slack-edge.com\/2015-04-28\/4635825662_28d3661bf00dc6013442_48.jpg","image_192":"https:\/\/avatars.slack-edge.com\/2015-04-28\/4635825662_28d3661bf00dc6013442_48.jpg","image_original":"https:\/\/avatars.slack-edge.com\/2015-04-28\/4635825662_28d3661bf00dc6013442_original.jpg","first_name":"Game","last_name":"Bot","title":"Game 73 | ons.","real_name":"Game Bot","real_name_normalized":"Game Bot","fields":null},"is_bot":true,"presence":"away"},{"id":"U06JGTU5V","team_id":"T04KB5WQH","name":"mathbot","deleted":true,"profile":{"bot_id":"B06JH1N93","image_24":"https:\/\/avatars.slack-edge.com\/2015-06-19\/6629501812_c507c80d735dfc75bbc8_24.jpg","image_32":"https:\/\/avatars.slack-edge.com\/2015-06-19\/6629501812_c507c80d735dfc75bbc8_32.jpg","image_48":"https:\/\/avatars.slack-edge.com\/2015-06-19\/6629501812_c507c80d735dfc75bbc8_48.jpg","image_72":"https:\/\/avatars.slack-edge.com\/2015-06-19\/6629501812_c507c80d735dfc75bbc8_48.jpg","image_192":"https:\/\/avatars.slack-edge.com\/2015-06-19\/6629501812_c507c80d735dfc75bbc8_48.jpg","image_original":"https:\/\/avatars.slack-edge.com\/2015-06-19\/6629501812_c507c80d735dfc75bbc8_original.jpg","first_name":"Math","last_name":"Bot","real_name":"Math 74 | Bot","real_name_normalized":"Math Bot","fields":null},"is_bot":true,"presence":"away"},{"id":"U0HLFUZLJ","team_id":"T04KB5WQH","name":"player1","deleted":false,"status":null,"color":"d58247","real_name":"Player 75 | 1","tz":"America\/Indiana\/Indianapolis","tz_label":"Eastern Standard Time","tz_offset":-18000,"profile":{"first_name":"Player","last_name":"1","avatar_hash":"gcde4e113736","real_name":"Player 76 | 1","real_name_normalized":"Player 1","email":"dblock+player1@dblock.org","image_24":"https:\/\/secure.gravatar.com\/avatar\/cde4e1137364cf971f15160113b04e86.jpg?s=24&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F66f9%2Fimg%2Favatars%2Fava_0009-24.png","image_32":"https:\/\/secure.gravatar.com\/avatar\/cde4e1137364cf971f15160113b04e86.jpg?s=32&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F66f9%2Fimg%2Favatars%2Fava_0009-32.png","image_48":"https:\/\/secure.gravatar.com\/avatar\/cde4e1137364cf971f15160113b04e86.jpg?s=48&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F66f9%2Fimg%2Favatars%2Fava_0009-48.png","image_72":"https:\/\/secure.gravatar.com\/avatar\/cde4e1137364cf971f15160113b04e86.jpg?s=72&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F66f9%2Fimg%2Favatars%2Fava_0009-72.png","image_192":"https:\/\/secure.gravatar.com\/avatar\/cde4e1137364cf971f15160113b04e86.jpg?s=192&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F7fa9%2Fimg%2Favatars%2Fava_0009-192.png","image_512":"https:\/\/secure.gravatar.com\/avatar\/cde4e1137364cf971f15160113b04e86.jpg?s=512&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F7fa9%2Fimg%2Favatars%2Fava_0009-512.png","fields":null},"is_admin":false,"is_owner":false,"is_primary_owner":false,"is_restricted":false,"is_ultra_restricted":false,"is_bot":false,"presence":"away"},{"id":"U0H15MV1R","team_id":"T04KB5WQH","name":"pongbot","deleted":false,"status":null,"color":"99a949","real_name":"PlayPlay.io 77 | - Ping-Pong for Slack","tz":null,"tz_label":"Pacific Standard Time","tz_offset":-28800,"profile":{"first_name":"PlayPlay.io 78 | - Ping-Pong for Slack","bot_id":"B0H184MLZ","api_app_id":"A0H0ZJHP0","avatar_hash":"7deb687e1284","image_24":"https:\/\/avatars.slack-edge.com\/2016-01-11\/18253837687_7deb687e1284211067e9_24.png","image_32":"https:\/\/avatars.slack-edge.com\/2016-01-11\/18253837687_7deb687e1284211067e9_32.png","image_48":"https:\/\/avatars.slack-edge.com\/2016-01-11\/18253837687_7deb687e1284211067e9_48.png","image_72":"https:\/\/avatars.slack-edge.com\/2016-01-11\/18253837687_7deb687e1284211067e9_72.png","image_192":"https:\/\/avatars.slack-edge.com\/2016-01-11\/18253837687_7deb687e1284211067e9_192.png","image_512":"https:\/\/avatars.slack-edge.com\/2016-01-11\/18253837687_7deb687e1284211067e9_512.png","image_1024":"https:\/\/avatars.slack-edge.com\/2016-01-11\/18253837687_7deb687e1284211067e9_512.png","image_original":"https:\/\/avatars.slack-edge.com\/2016-01-11\/18253837687_7deb687e1284211067e9_original.png","real_name":"PlayPlay.io 79 | - Ping-Pong for Slack","real_name_normalized":"PlayPlay.io - Ping-Pong for 80 | Slack","fields":null},"is_admin":false,"is_owner":false,"is_primary_owner":false,"is_restricted":false,"is_ultra_restricted":false,"is_bot":true,"presence":"active"},{"id":"U0HC7MSEA","team_id":"T04KB5WQH","name":"poolbot","deleted":false,"status":null,"color":"4cc091","real_name":"PlayPlay.io 81 | - Billiards for Slack","tz":null,"tz_label":"Pacific Standard Time","tz_offset":-28800,"profile":{"first_name":"PlayPlay.io 82 | - Billiards for Slack","bot_id":"B0HC7D1SN","api_app_id":"A0HC74EET","avatar_hash":"834b5aefa301","image_24":"https:\/\/avatars.slack-edge.com\/2015-12-26\/17415480311_834b5aefa3013ce926b2_24.png","image_32":"https:\/\/avatars.slack-edge.com\/2015-12-26\/17415480311_834b5aefa3013ce926b2_32.png","image_48":"https:\/\/avatars.slack-edge.com\/2015-12-26\/17415480311_834b5aefa3013ce926b2_48.png","image_72":"https:\/\/avatars.slack-edge.com\/2015-12-26\/17415480311_834b5aefa3013ce926b2_72.png","image_192":"https:\/\/avatars.slack-edge.com\/2015-12-26\/17415480311_834b5aefa3013ce926b2_192.png","image_512":"https:\/\/avatars.slack-edge.com\/2015-12-26\/17415480311_834b5aefa3013ce926b2_512.png","image_1024":"https:\/\/avatars.slack-edge.com\/2015-12-26\/17415480311_834b5aefa3013ce926b2_512.png","image_original":"https:\/\/avatars.slack-edge.com\/2015-12-26\/17415480311_834b5aefa3013ce926b2_original.png","real_name":"PlayPlay.io 83 | - Billiards for Slack","real_name_normalized":"PlayPlay.io - Billiards for 84 | Slack","fields":null},"is_admin":false,"is_owner":false,"is_primary_owner":false,"is_restricted":false,"is_ultra_restricted":false,"is_bot":true,"presence":"active"},{"id":"U07518DTL","team_id":"T04KB5WQH","name":"rubybot","deleted":false,"status":null,"color":"674b1b","real_name":"","tz":null,"tz_label":"Pacific 85 | Standard Time","tz_offset":-28800,"profile":{"bot_id":"B0751JU2H","image_24":"https:\/\/avatars.slack-edge.com\/2015-07-02\/7171641589_359bf095c1fe0841e582_24.jpg","image_32":"https:\/\/avatars.slack-edge.com\/2015-07-02\/7171641589_359bf095c1fe0841e582_32.jpg","image_48":"https:\/\/avatars.slack-edge.com\/2015-07-02\/7171641589_359bf095c1fe0841e582_48.jpg","image_72":"https:\/\/avatars.slack-edge.com\/2015-07-02\/7171641589_359bf095c1fe0841e582_48.jpg","image_192":"https:\/\/avatars.slack-edge.com\/2015-07-02\/7171641589_359bf095c1fe0841e582_48.jpg","image_original":"https:\/\/avatars.slack-edge.com\/2015-07-02\/7171641589_359bf095c1fe0841e582_original.jpg","title":"Also 86 | used in Travis-CI for slack-ruby-client.","real_name":"","real_name_normalized":"","fields":null},"is_admin":false,"is_owner":false,"is_primary_owner":false,"is_restricted":false,"is_ultra_restricted":false,"is_bot":true,"presence":"away"},{"id":"U0GPR6KDJ","team_id":"T04KB5WQH","name":"slackbotserver","deleted":false,"status":null,"color":"2b6836","real_name":"slack-bot-server","tz":null,"tz_label":"Pacific 87 | Standard Time","tz_offset":-28800,"profile":{"first_name":"slack-bot-server","bot_id":"B0GPWQWHY","api_app_id":"A0GPRTG9Z","avatar_hash":"g36f3834829d","real_name":"slack-bot-server","real_name_normalized":"slack-bot-server","image_24":"https:\/\/secure.gravatar.com\/avatar\/36f3834829d1ec429f7d6acebccc7641.jpg?s=24&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F66f9%2Fimg%2Favatars%2Fava_0008-24.png","image_32":"https:\/\/secure.gravatar.com\/avatar\/36f3834829d1ec429f7d6acebccc7641.jpg?s=32&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F66f9%2Fimg%2Favatars%2Fava_0008-32.png","image_48":"https:\/\/secure.gravatar.com\/avatar\/36f3834829d1ec429f7d6acebccc7641.jpg?s=48&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F66f9%2Fimg%2Favatars%2Fava_0008-48.png","image_72":"https:\/\/secure.gravatar.com\/avatar\/36f3834829d1ec429f7d6acebccc7641.jpg?s=72&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F66f9%2Fimg%2Favatars%2Fava_0008-72.png","image_192":"https:\/\/secure.gravatar.com\/avatar\/36f3834829d1ec429f7d6acebccc7641.jpg?s=192&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F7fa9%2Fimg%2Favatars%2Fava_0008-192.png","image_512":"https:\/\/secure.gravatar.com\/avatar\/36f3834829d1ec429f7d6acebccc7641.jpg?s=512&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F7fa9%2Fimg%2Favatars%2Fava_0008-512.png","fields":null},"is_admin":false,"is_owner":false,"is_primary_owner":false,"is_restricted":false,"is_ultra_restricted":false,"is_bot":true,"presence":"away"},{"id":"U0HPMN0GY","team_id":"T04KB5WQH","name":"slak","deleted":true,"profile":{"first_name":"API 88 | Explorer","bot_id":"B0HPLG4M7","api_app_id":"A0HP7340N","avatar_hash":"36be3c0d4513","image_24":"https:\/\/avatars.slack-edge.com\/2016-01-21\/19073458786_36be3c0d451350c9f03b_24.png","image_32":"https:\/\/avatars.slack-edge.com\/2016-01-21\/19073458786_36be3c0d451350c9f03b_32.png","image_48":"https:\/\/avatars.slack-edge.com\/2016-01-21\/19073458786_36be3c0d451350c9f03b_48.png","image_72":"https:\/\/avatars.slack-edge.com\/2016-01-21\/19073458786_36be3c0d451350c9f03b_72.png","image_192":"https:\/\/avatars.slack-edge.com\/2016-01-21\/19073458786_36be3c0d451350c9f03b_192.png","image_512":"https:\/\/avatars.slack-edge.com\/2016-01-21\/19073458786_36be3c0d451350c9f03b_512.png","image_1024":"https:\/\/avatars.slack-edge.com\/2016-01-21\/19073458786_36be3c0d451350c9f03b_512.png","image_original":"https:\/\/avatars.slack-edge.com\/2016-01-21\/19073458786_36be3c0d451350c9f03b_original.png","real_name":"API 89 | Explorer","real_name_normalized":"API Explorer","fields":null},"is_bot":true,"presence":"away"},{"id":"U0HF43KT7","team_id":"T04KB5WQH","name":"tictactoe","deleted":false,"status":null,"color":"9b3b45","real_name":"PlayPlay.io 90 | - Tic-Tac-Toe for Slack","tz":null,"tz_label":"Pacific Standard Time","tz_offset":-28800,"profile":{"first_name":"PlayPlay.io 91 | - Tic-Tac-Toe for Slack","bot_id":"B0HF56491","api_app_id":"A0HF3E0AH","avatar_hash":"0b97b85448da","image_24":"https:\/\/avatars.slack-edge.com\/2015-12-29\/17536998294_0b97b85448da1dc95c83_24.png","image_32":"https:\/\/avatars.slack-edge.com\/2015-12-29\/17536998294_0b97b85448da1dc95c83_32.png","image_48":"https:\/\/avatars.slack-edge.com\/2015-12-29\/17536998294_0b97b85448da1dc95c83_48.png","image_72":"https:\/\/avatars.slack-edge.com\/2015-12-29\/17536998294_0b97b85448da1dc95c83_72.png","image_192":"https:\/\/avatars.slack-edge.com\/2015-12-29\/17536998294_0b97b85448da1dc95c83_192.png","image_512":"https:\/\/avatars.slack-edge.com\/2015-12-29\/17536998294_0b97b85448da1dc95c83_512.png","image_1024":"https:\/\/avatars.slack-edge.com\/2015-12-29\/17536998294_0b97b85448da1dc95c83_512.png","image_original":"https:\/\/avatars.slack-edge.com\/2015-12-29\/17536998294_0b97b85448da1dc95c83_original.png","real_name":"PlayPlay.io 92 | - Tic-Tac-Toe for Slack","real_name_normalized":"PlayPlay.io - Tic-Tac-Toe 93 | for Slack","fields":null},"is_admin":false,"is_owner":false,"is_primary_owner":false,"is_restricted":false,"is_ultra_restricted":false,"is_bot":true,"presence":"active"},{"id":"U0J1GAHN1","team_id":"T04KB5WQH","name":"travis-ci","deleted":false,"status":null,"color":"5a4592","real_name":"","tz":null,"tz_label":"Pacific 94 | Standard Time","tz_offset":-28800,"profile":{"bot_id":"B0J1L75DY","api_app_id":"","avatar_hash":"dd0191970c25","image_24":"https:\/\/avatars.slack-edge.com\/2016-01-08\/18054871664_dd0191970c25e54ef23f_24.png","image_32":"https:\/\/avatars.slack-edge.com\/2016-01-08\/18054871664_dd0191970c25e54ef23f_32.png","image_48":"https:\/\/avatars.slack-edge.com\/2016-01-08\/18054871664_dd0191970c25e54ef23f_48.png","image_72":"https:\/\/avatars.slack-edge.com\/2016-01-08\/18054871664_dd0191970c25e54ef23f_72.png","image_192":"https:\/\/avatars.slack-edge.com\/2016-01-08\/18054871664_dd0191970c25e54ef23f_192.png","image_512":"https:\/\/avatars.slack-edge.com\/2016-01-08\/18054871664_dd0191970c25e54ef23f_512.png","image_1024":"https:\/\/avatars.slack-edge.com\/2016-01-08\/18054871664_dd0191970c25e54ef23f_512.png","image_original":"https:\/\/avatars.slack-edge.com\/2016-01-08\/18054871664_dd0191970c25e54ef23f_original.png","title":"Used 95 | for Travis-CI integration tests on https:\/\/github.com\/dblock\/slack-ruby-client.","real_name":"","real_name_normalized":"","fields":null},"is_admin":false,"is_owner":false,"is_primary_owner":false,"is_restricted":false,"is_ultra_restricted":false,"is_bot":true,"presence":"away"},{"id":"USLACKBOT","team_id":"T04KB5WQH","name":"slackbot","deleted":false,"status":null,"color":"757575","real_name":"slackbot","tz":null,"tz_label":"Pacific 96 | Standard Time","tz_offset":-28800,"profile":{"first_name":"slackbot","last_name":"","image_24":"https:\/\/slack.global.ssl.fastly.net\/0180\/img\/slackbot_24.png","image_32":"https:\/\/slack.global.ssl.fastly.net\/66f9\/img\/slackbot_32.png","image_48":"https:\/\/slack.global.ssl.fastly.net\/66f9\/img\/slackbot_48.png","image_72":"https:\/\/slack.global.ssl.fastly.net\/0180\/img\/slackbot_72.png","image_192":"https:\/\/slack.global.ssl.fastly.net\/66f9\/img\/slackbot_192.png","image_512":"https:\/\/slack.global.ssl.fastly.net\/7fa9\/img\/slackbot_512.png","avatar_hash":"sv1444671949","real_name":"slackbot","real_name_normalized":"slackbot","email":null,"fields":null},"is_admin":false,"is_owner":false,"is_primary_owner":false,"is_restricted":false,"is_ultra_restricted":false,"is_bot":false,"presence":"active"}],"cache_version":"v12-rats","cache_ts_version":"v1-cat","bots":[{"id":"B0751JU2H","deleted":false,"name":"bot","icons":{"image_36":"https:\/\/slack.global.ssl.fastly.net\/12b5a\/plugins\/bot\/assets\/service_36.png","image_48":"https:\/\/slack.global.ssl.fastly.net\/12b5a\/plugins\/bot\/assets\/service_48.png","image_72":"https:\/\/slack.global.ssl.fastly.net\/12b5a\/plugins\/bot\/assets\/service_72.png"}},{"id":"B0AFSEC0Z","deleted":false,"name":"slash-commands","icons":{"image_36":"https:\/\/slack.global.ssl.fastly.net\/12b5a\/plugins\/slash_commands\/assets\/service_36.png","image_48":"https:\/\/slack.global.ssl.fastly.net\/12b5a\/plugins\/slash_commands\/assets\/service_48.png","image_72":"https:\/\/slack.global.ssl.fastly.net\/12b5a\/plugins\/slash_commands\/assets\/service_72.png"}},{"id":"B0GPWQWHY","deleted":false,"name":"slack-bot-server","icons":{"image_36":"https:\/\/slack.global.ssl.fastly.net\/12b5a\/plugins\/app\/assets\/service_36.png","image_48":"https:\/\/slack.global.ssl.fastly.net\/12b5a\/plugins\/app\/assets\/service_48.png","image_72":"https:\/\/slack.global.ssl.fastly.net\/12b5a\/plugins\/app\/assets\/service_72.png"}},{"id":"B0H184MLZ","deleted":false,"name":"PlayPlay.io 97 | - Ping-Pong for Slack","icons":{"image_36":"https:\/\/s3-us-west-2.amazonaws.com\/slack-files2\/avatars\/2015-12-19\/17068126628_26197be640f5b341e017_36.png","image_48":"https:\/\/s3-us-west-2.amazonaws.com\/slack-files2\/avatars\/2015-12-19\/17068126628_26197be640f5b341e017_48.png","image_72":"https:\/\/s3-us-west-2.amazonaws.com\/slack-files2\/avatars\/2015-12-19\/17068126628_26197be640f5b341e017_72.png"}},{"id":"B0H74MK3J","deleted":false,"name":"PlayPlay.io 98 | - Chess for Slack","icons":{"image_36":"https:\/\/s3-us-west-2.amazonaws.com\/slack-files2\/avatars\/2015-12-22\/17242566018_86a79b66c6884f7b41b4_36.png","image_48":"https:\/\/s3-us-west-2.amazonaws.com\/slack-files2\/avatars\/2015-12-22\/17242566018_86a79b66c6884f7b41b4_48.png","image_72":"https:\/\/s3-us-west-2.amazonaws.com\/slack-files2\/avatars\/2015-12-22\/17242566018_86a79b66c6884f7b41b4_72.png"}},{"id":"B0HC7D1SN","deleted":false,"name":"PlayPlay.io 99 | - Billiards for Slack","icons":{"image_36":"https:\/\/s3-us-west-2.amazonaws.com\/slack-files2\/avatars\/2015-12-26\/17414662001_31808d5c91a3ec3d1352_36.png","image_48":"https:\/\/s3-us-west-2.amazonaws.com\/slack-files2\/avatars\/2015-12-26\/17414662001_31808d5c91a3ec3d1352_48.png","image_72":"https:\/\/s3-us-west-2.amazonaws.com\/slack-files2\/avatars\/2015-12-26\/17414662001_31808d5c91a3ec3d1352_72.png"}},{"id":"B0HF56491","deleted":false,"name":"PlayPlay.io 100 | - Tic-Tac-Toe for Slack","icons":{"image_36":"https:\/\/s3-us-west-2.amazonaws.com\/slack-files2\/avatars\/2015-12-29\/17517672695_9aa1fb32ea85b923e2ca_36.png","image_48":"https:\/\/s3-us-west-2.amazonaws.com\/slack-files2\/avatars\/2015-12-29\/17517672695_9aa1fb32ea85b923e2ca_48.png","image_72":"https:\/\/s3-us-west-2.amazonaws.com\/slack-files2\/avatars\/2015-12-29\/17517672695_9aa1fb32ea85b923e2ca_72.png"}},{"id":"B0J1L75DY","deleted":false,"name":"bot","icons":{"image_36":"https:\/\/slack.global.ssl.fastly.net\/12b5a\/plugins\/bot\/assets\/service_36.png","image_48":"https:\/\/slack.global.ssl.fastly.net\/12b5a\/plugins\/bot\/assets\/service_48.png","image_72":"https:\/\/slack.global.ssl.fastly.net\/12b5a\/plugins\/bot\/assets\/service_72.png"}},{"id":"B04JZBDQC","deleted":true,"name":"bot","icons":{"image_36":"https:\/\/slack.global.ssl.fastly.net\/12b5a\/plugins\/bot\/assets\/service_36.png","image_48":"https:\/\/slack.global.ssl.fastly.net\/12b5a\/plugins\/bot\/assets\/service_48.png","image_72":"https:\/\/slack.global.ssl.fastly.net\/12b5a\/plugins\/bot\/assets\/service_72.png"}},{"id":"B07KECJ6R","deleted":true,"name":"bot","icons":{"image_36":"https:\/\/slack.global.ssl.fastly.net\/12b5a\/plugins\/bot\/assets\/service_36.png","image_48":"https:\/\/slack.global.ssl.fastly.net\/12b5a\/plugins\/bot\/assets\/service_48.png","image_72":"https:\/\/slack.global.ssl.fastly.net\/12b5a\/plugins\/bot\/assets\/service_72.png"}},{"id":"B0EA31LQM","deleted":true,"name":"bot","icons":{"image_36":"https:\/\/slack.global.ssl.fastly.net\/12b5a\/plugins\/bot\/assets\/service_36.png","image_48":"https:\/\/slack.global.ssl.fastly.net\/12b5a\/plugins\/bot\/assets\/service_48.png","image_72":"https:\/\/slack.global.ssl.fastly.net\/12b5a\/plugins\/bot\/assets\/service_72.png"}},{"id":"B06JH1N93","deleted":true,"name":"bot","icons":{"image_36":"https:\/\/slack.global.ssl.fastly.net\/12b5a\/plugins\/bot\/assets\/service_36.png","image_48":"https:\/\/slack.global.ssl.fastly.net\/12b5a\/plugins\/bot\/assets\/service_48.png","image_72":"https:\/\/slack.global.ssl.fastly.net\/12b5a\/plugins\/bot\/assets\/service_72.png"}},{"id":"B04JPNS2Y","deleted":true,"name":"gdrive"},{"id":"B092B5DDG","deleted":true,"name":"bot","icons":{"image_36":"https:\/\/slack.global.ssl.fastly.net\/12b5a\/plugins\/bot\/assets\/service_36.png","image_48":"https:\/\/slack.global.ssl.fastly.net\/12b5a\/plugins\/bot\/assets\/service_48.png","image_72":"https:\/\/slack.global.ssl.fastly.net\/12b5a\/plugins\/bot\/assets\/service_72.png"}},{"id":"B04JPQ0JE","deleted":true,"name":"bot","icons":{"image_36":"https:\/\/slack.global.ssl.fastly.net\/12b5a\/plugins\/bot\/assets\/service_36.png","image_48":"https:\/\/slack.global.ssl.fastly.net\/12b5a\/plugins\/bot\/assets\/service_48.png","image_72":"https:\/\/slack.global.ssl.fastly.net\/12b5a\/plugins\/bot\/assets\/service_72.png"}},{"id":"B0HPLG4M7","deleted":true,"name":"API 101 | Explorer","icons":{"image_36":"https:\/\/s3-us-west-2.amazonaws.com\/slack-files2\/avatars\/2016-01-21\/19073538231_edf8d896148b6bfef790_36.png","image_48":"https:\/\/s3-us-west-2.amazonaws.com\/slack-files2\/avatars\/2016-01-21\/19073538231_edf8d896148b6bfef790_48.png","image_72":"https:\/\/s3-us-west-2.amazonaws.com\/slack-files2\/avatars\/2016-01-21\/19073538231_edf8d896148b6bfef790_72.png"}}],"url":"wss:\/\/ms173.slack-msgs.com\/websocket\/lqcUiAvrKTP-uuid="}' 102 | http_version: 103 | recorded_at: Sun, 31 Jan 2016 22:42:57 GMT 104 | recorded_with: VCR 3.0.0 105 | --------------------------------------------------------------------------------