├── log └── .keep ├── .rspec ├── app ├── mailers │ └── .keep ├── models │ ├── .keep │ ├── concerns │ │ ├── .keep │ │ ├── deployment_timeout.rb │ │ ├── api_client.rb │ │ └── local_log_file.rb │ ├── repository.rb │ ├── deployment.rb │ ├── commit_status.rb │ ├── auto_deployment.rb │ └── deployment │ │ ├── status.rb │ │ ├── output.rb │ │ └── credentials.rb ├── controllers │ ├── concerns │ │ ├── .keep │ │ └── webhook_validations.rb │ ├── application_controller.rb │ └── events_controller.rb ├── helpers │ └── application_helper.rb ├── views │ └── layouts │ │ └── application.html.erb ├── validators │ └── github_source_validator.rb ├── receivers │ ├── lock_receiver.rb │ └── receiver.rb └── services │ └── environment_locker.rb ├── lib ├── assets │ └── .keep ├── tasks │ ├── .keep │ └── resque.rake ├── heaven │ ├── version.rb │ ├── jobs │ │ ├── deployment_status.rb │ │ ├── status.rb │ │ ├── environment_lock.rb │ │ ├── environment_unlock.rb │ │ ├── locked_error.rb │ │ ├── environment_locked_error.rb │ │ └── deployment.rb │ ├── jobs.rb │ ├── notifier │ │ ├── campfire.rb │ │ ├── hipchat.rb │ │ ├── flowdock │ │ │ ├── api.rb │ │ │ └── message_helper.rb │ │ ├── slack.rb │ │ ├── default.rb │ │ └── flowdock.rb │ ├── notifier.rb │ ├── provider │ │ ├── fabric.rb │ │ ├── capistrano.rb │ │ ├── ansible.rb │ │ ├── bundler_capistrano.rb │ │ ├── dpl.rb │ │ ├── shell.rb │ │ ├── elastic_beanstalk.rb │ │ ├── heroku.rb │ │ └── default_provider.rb │ ├── comparison │ │ ├── linked.rb │ │ └── default.rb │ └── provider.rb └── heaven.rb ├── .ruby-version ├── vendor ├── assets │ ├── javascripts │ │ └── .keep │ └── stylesheets │ │ └── .keep └── cache │ ├── arel-6.0.0.gem │ ├── ast-2.0.0.gem │ ├── dpl-1.5.7.gem │ ├── i18n-0.7.0.gem │ ├── json-1.8.2.gem │ ├── kgio-2.9.2.gem │ ├── mail-2.6.3.gem │ ├── rack-1.6.2.gem │ ├── slop-3.6.0.gem │ ├── tilt-1.4.1.gem │ ├── crack-0.4.2.gem │ ├── dotenv-0.9.0.gem │ ├── erubis-2.7.0.gem │ ├── loofah-2.0.2.gem │ ├── pry-0.9.12.6.gem │ ├── rails-4.2.2.gem │ ├── rake-10.4.2.gem │ ├── redis-3.0.7.gem │ ├── sawyer-0.5.5.gem │ ├── sshkit-1.8.1.gem │ ├── thor-0.19.1.gem │ ├── tzinfo-1.2.2.gem │ ├── vegas-0.1.11.gem │ ├── warden-1.2.3.gem │ ├── activejob-4.2.2.gem │ ├── astrolabe-1.3.0.gem │ ├── aws-sdk-1.51.0.gem │ ├── builder-3.2.2.gem │ ├── callsite-0.0.11.gem │ ├── campfiyah-0.0.6.gem │ ├── coderay-1.1.0.gem │ ├── diff-lcs-1.2.5.gem │ ├── faraday-0.9.0.gem │ ├── flowdock-0.5.0.gem │ ├── foreman-0.63.0.gem │ ├── globalid-0.3.5.gem │ ├── hipchat-1.1.0.gem │ ├── httparty-0.13.1.gem │ ├── mime-types-2.5.gem │ ├── minitest-5.6.1.gem │ ├── multi_xml-0.5.5.gem │ ├── net-scp-1.2.1.gem │ ├── net-ssh-3.0.2.gem │ ├── octokit-3.4.0.gem │ ├── parser-2.2.2.5.gem │ ├── powerpack-0.0.9.gem │ ├── rack-test-0.6.3.gem │ ├── railties-4.2.2.gem │ ├── rainbow-2.0.0.gem │ ├── resque-1.25.1.gem │ ├── rubocop-0.26.0.gem │ ├── safe_yaml-1.0.4.gem │ ├── simplecov-0.7.1.gem │ ├── sinatra-1.4.4.gem │ ├── sprockets-3.0.3.gem │ ├── sqlite3-1.3.10.gem │ ├── unicorn-4.8.2.gem │ ├── webmock-1.17.3.gem │ ├── yajl-ruby-1.2.0.gem │ ├── actionpack-4.2.2.gem │ ├── actionview-4.2.2.gem │ ├── activemodel-4.2.2.gem │ ├── addressable-2.3.6.gem │ ├── capistrano-3.4.0.gem │ ├── mono_logger-1.1.0.gem │ ├── multi_json-1.11.0.gem │ ├── nokogiri-1.6.6.2.gem │ ├── posix-spawn-0.3.8.gem │ ├── raindrops-0.13.0.gem │ ├── rspec-core-2.14.7.gem │ ├── thread_safe-0.3.5.gem │ ├── actionmailer-4.2.2.gem │ ├── activerecord-4.2.2.gem │ ├── activesupport-4.2.2.gem │ ├── better_errors-1.1.0.gem │ ├── meta_request-0.2.8.gem │ ├── method_source-0.8.2.gem │ ├── mini_portile-0.6.2.gem │ ├── multipart-post-2.0.0.gem │ ├── rack-contrib-1.1.0.gem │ ├── rspec-mocks-2.14.5.gem │ ├── rspec-rails-2.14.1.gem │ ├── simplecov-html-0.7.1.gem │ ├── slack-notifier-1.0.0.gem │ ├── warden-github-1.0.2.gem │ ├── capistrano-rails-1.1.3.gem │ ├── debug_inspector-0.0.2.gem │ ├── rack-protection-1.5.2.gem │ ├── redis-namespace-1.4.1.gem │ ├── resque-scheduler-4.0.0.gem │ ├── ruby-progressbar-1.5.1.gem │ ├── rufus-scheduler-3.2.0.gem │ ├── sprockets-rails-2.2.4.gem │ ├── binding_of_caller-0.7.2.gem │ ├── capistrano-bundler-1.1.4.gem │ ├── capistrano-resque-0.2.2.gem │ ├── faraday_middleware-0.9.1.gem │ ├── rails-dom-testing-1.0.6.gem │ ├── resque-lock-timeout-0.4.4.gem │ ├── rspec-expectations-2.14.5.gem │ ├── warden-github-rails-1.1.0.gem │ ├── capistrano-passenger-0.2.0.gem │ ├── rails-html-sanitizer-1.0.2.gem │ ├── capistrano-rails-console-1.0.0.gem │ └── rails-deprecated_sanitizer-1.0.3.gem ├── requirements.txt ├── public ├── favicon.ico ├── robots.txt ├── 500.html ├── 422.html └── 404.html ├── .travis.yml ├── spec ├── fixtures │ ├── ping.json │ └── meta.json ├── models │ ├── deployment │ │ ├── status_spec.rb │ │ ├── credentials_spec.rb │ │ └── output_spec.rb │ ├── repository_spec.rb │ ├── concerns │ │ └── api_client_spec.rb │ ├── deployment_spec.rb │ └── reciever_spec.rb ├── support │ ├── matchers │ │ ├── environment_locker_matchers.rb │ │ └── deployment_status_matchers.rb │ └── helpers │ │ ├── fixture_helper.rb │ │ ├── comparison_helper.rb │ │ ├── gist_helper.rb │ │ ├── meta_helper.rb │ │ └── deployment_status_helper.rb ├── app_spec.rb ├── requests │ ├── site_spec.rb │ └── events_spec.rb ├── lib │ └── heaven │ │ ├── provider │ │ ├── capistrano_spec.rb │ │ ├── dpl_spec.rb │ │ └── default_provider_spec.rb │ │ ├── notifier │ │ ├── default_spec.rb │ │ ├── hipchat_spec.rb │ │ ├── campfire_spec.rb │ │ └── slack_spec.rb │ │ ├── provider_spec.rb │ │ ├── jobs │ │ ├── environment_lock_spec.rb │ │ ├── environment_unlock_spec.rb │ │ └── environment_locked_error_spec.rb │ │ └── comparison │ │ ├── default_spec.rb │ │ └── linked_spec.rb ├── validators │ └── github_source_validator_spec.rb ├── request_spec_helper.rb ├── controllers │ └── concerns │ │ └── webhook_validations_spec.rb ├── spec_helper.rb └── services │ └── environment_locker_spec.rb ├── Procfile ├── bin ├── rake ├── bundle ├── rails ├── dpl ├── cap └── capify ├── config ├── initializers │ ├── flowdock.rb │ ├── resque.rb │ ├── wrap_parameters.rb │ ├── session_store.rb │ ├── mime_types.rb │ ├── filter_parameter_logging.rb │ ├── libraries.rb │ ├── warden-github.rb │ ├── backtrace_silencers.rb │ ├── inflections.rb │ └── secret_token.rb ├── environment.rb ├── boot.rb ├── routes.rb ├── database.yml ├── unicorn.rb ├── locales │ └── en.yml ├── application.rb ├── environments │ ├── development.rb │ ├── test.rb │ ├── staging.rb │ └── production.rb ├── deploy.rb └── deploy │ └── production.rb ├── CONTRIBUTING.md ├── config.ru ├── CHANGES.md ├── script ├── console ├── bootstrap └── cibuild ├── Rakefile ├── db ├── seeds.rb ├── migrate │ ├── 20140329200427_create_deployments.rb │ └── 20140728040201_create_repositories.rb └── schema.rb ├── .gitignore ├── Gemfile ├── doc ├── locking.md ├── installation.md ├── overview.md └── notifications.md ├── Capfile ├── LICENSE.md ├── README.md ├── app.json ├── .rubocop.yml └── Gemfile.lock /log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /app/mailers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.2.3 2 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Fabric==1.8.3 2 | ansible 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/public/favicon.ico -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | rvm: 2 | - 2.2.3 3 | script: 4 | - script/cibuild 5 | services: 6 | - redis-server -------------------------------------------------------------------------------- /spec/fixtures/ping.json: -------------------------------------------------------------------------------- 1 | { 2 | "zen": "Practicality beats purity.", 3 | "hook_id": 1834813 4 | } 5 | -------------------------------------------------------------------------------- /vendor/cache/arel-6.0.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/arel-6.0.0.gem -------------------------------------------------------------------------------- /vendor/cache/ast-2.0.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/ast-2.0.0.gem -------------------------------------------------------------------------------- /vendor/cache/dpl-1.5.7.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/dpl-1.5.7.gem -------------------------------------------------------------------------------- /vendor/cache/i18n-0.7.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/i18n-0.7.0.gem -------------------------------------------------------------------------------- /vendor/cache/json-1.8.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/json-1.8.2.gem -------------------------------------------------------------------------------- /vendor/cache/kgio-2.9.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/kgio-2.9.2.gem -------------------------------------------------------------------------------- /vendor/cache/mail-2.6.3.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/mail-2.6.3.gem -------------------------------------------------------------------------------- /vendor/cache/rack-1.6.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/rack-1.6.2.gem -------------------------------------------------------------------------------- /vendor/cache/slop-3.6.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/slop-3.6.0.gem -------------------------------------------------------------------------------- /vendor/cache/tilt-1.4.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/tilt-1.4.1.gem -------------------------------------------------------------------------------- /lib/heaven/version.rb: -------------------------------------------------------------------------------- 1 | # The top-level Heaven module 2 | module Heaven 3 | VERSION = "0.8.0" 4 | end 5 | -------------------------------------------------------------------------------- /vendor/cache/crack-0.4.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/crack-0.4.2.gem -------------------------------------------------------------------------------- /vendor/cache/dotenv-0.9.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/dotenv-0.9.0.gem -------------------------------------------------------------------------------- /vendor/cache/erubis-2.7.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/erubis-2.7.0.gem -------------------------------------------------------------------------------- /vendor/cache/loofah-2.0.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/loofah-2.0.2.gem -------------------------------------------------------------------------------- /vendor/cache/pry-0.9.12.6.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/pry-0.9.12.6.gem -------------------------------------------------------------------------------- /vendor/cache/rails-4.2.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/rails-4.2.2.gem -------------------------------------------------------------------------------- /vendor/cache/rake-10.4.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/rake-10.4.2.gem -------------------------------------------------------------------------------- /vendor/cache/redis-3.0.7.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/redis-3.0.7.gem -------------------------------------------------------------------------------- /vendor/cache/sawyer-0.5.5.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/sawyer-0.5.5.gem -------------------------------------------------------------------------------- /vendor/cache/sshkit-1.8.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/sshkit-1.8.1.gem -------------------------------------------------------------------------------- /vendor/cache/thor-0.19.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/thor-0.19.1.gem -------------------------------------------------------------------------------- /vendor/cache/tzinfo-1.2.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/tzinfo-1.2.2.gem -------------------------------------------------------------------------------- /vendor/cache/vegas-0.1.11.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/vegas-0.1.11.gem -------------------------------------------------------------------------------- /vendor/cache/warden-1.2.3.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/warden-1.2.3.gem -------------------------------------------------------------------------------- /vendor/cache/activejob-4.2.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/activejob-4.2.2.gem -------------------------------------------------------------------------------- /vendor/cache/astrolabe-1.3.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/astrolabe-1.3.0.gem -------------------------------------------------------------------------------- /vendor/cache/aws-sdk-1.51.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/aws-sdk-1.51.0.gem -------------------------------------------------------------------------------- /vendor/cache/builder-3.2.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/builder-3.2.2.gem -------------------------------------------------------------------------------- /vendor/cache/callsite-0.0.11.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/callsite-0.0.11.gem -------------------------------------------------------------------------------- /vendor/cache/campfiyah-0.0.6.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/campfiyah-0.0.6.gem -------------------------------------------------------------------------------- /vendor/cache/coderay-1.1.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/coderay-1.1.0.gem -------------------------------------------------------------------------------- /vendor/cache/diff-lcs-1.2.5.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/diff-lcs-1.2.5.gem -------------------------------------------------------------------------------- /vendor/cache/faraday-0.9.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/faraday-0.9.0.gem -------------------------------------------------------------------------------- /vendor/cache/flowdock-0.5.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/flowdock-0.5.0.gem -------------------------------------------------------------------------------- /vendor/cache/foreman-0.63.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/foreman-0.63.0.gem -------------------------------------------------------------------------------- /vendor/cache/globalid-0.3.5.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/globalid-0.3.5.gem -------------------------------------------------------------------------------- /vendor/cache/hipchat-1.1.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/hipchat-1.1.0.gem -------------------------------------------------------------------------------- /vendor/cache/httparty-0.13.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/httparty-0.13.1.gem -------------------------------------------------------------------------------- /vendor/cache/mime-types-2.5.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/mime-types-2.5.gem -------------------------------------------------------------------------------- /vendor/cache/minitest-5.6.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/minitest-5.6.1.gem -------------------------------------------------------------------------------- /vendor/cache/multi_xml-0.5.5.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/multi_xml-0.5.5.gem -------------------------------------------------------------------------------- /vendor/cache/net-scp-1.2.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/net-scp-1.2.1.gem -------------------------------------------------------------------------------- /vendor/cache/net-ssh-3.0.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/net-ssh-3.0.2.gem -------------------------------------------------------------------------------- /vendor/cache/octokit-3.4.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/octokit-3.4.0.gem -------------------------------------------------------------------------------- /vendor/cache/parser-2.2.2.5.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/parser-2.2.2.5.gem -------------------------------------------------------------------------------- /vendor/cache/powerpack-0.0.9.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/powerpack-0.0.9.gem -------------------------------------------------------------------------------- /vendor/cache/rack-test-0.6.3.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/rack-test-0.6.3.gem -------------------------------------------------------------------------------- /vendor/cache/railties-4.2.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/railties-4.2.2.gem -------------------------------------------------------------------------------- /vendor/cache/rainbow-2.0.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/rainbow-2.0.0.gem -------------------------------------------------------------------------------- /vendor/cache/resque-1.25.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/resque-1.25.1.gem -------------------------------------------------------------------------------- /vendor/cache/rubocop-0.26.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/rubocop-0.26.0.gem -------------------------------------------------------------------------------- /vendor/cache/safe_yaml-1.0.4.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/safe_yaml-1.0.4.gem -------------------------------------------------------------------------------- /vendor/cache/simplecov-0.7.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/simplecov-0.7.1.gem -------------------------------------------------------------------------------- /vendor/cache/sinatra-1.4.4.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/sinatra-1.4.4.gem -------------------------------------------------------------------------------- /vendor/cache/sprockets-3.0.3.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/sprockets-3.0.3.gem -------------------------------------------------------------------------------- /vendor/cache/sqlite3-1.3.10.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/sqlite3-1.3.10.gem -------------------------------------------------------------------------------- /vendor/cache/unicorn-4.8.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/unicorn-4.8.2.gem -------------------------------------------------------------------------------- /vendor/cache/webmock-1.17.3.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/webmock-1.17.3.gem -------------------------------------------------------------------------------- /vendor/cache/yajl-ruby-1.2.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/yajl-ruby-1.2.0.gem -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bundle exec unicorn -p $PORT -c config/unicorn.rb 2 | worker: bundle exec rake resque:work QUEUE=* 3 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /vendor/cache/actionpack-4.2.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/actionpack-4.2.2.gem -------------------------------------------------------------------------------- /vendor/cache/actionview-4.2.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/actionview-4.2.2.gem -------------------------------------------------------------------------------- /vendor/cache/activemodel-4.2.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/activemodel-4.2.2.gem -------------------------------------------------------------------------------- /vendor/cache/addressable-2.3.6.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/addressable-2.3.6.gem -------------------------------------------------------------------------------- /vendor/cache/capistrano-3.4.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/capistrano-3.4.0.gem -------------------------------------------------------------------------------- /vendor/cache/mono_logger-1.1.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/mono_logger-1.1.0.gem -------------------------------------------------------------------------------- /vendor/cache/multi_json-1.11.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/multi_json-1.11.0.gem -------------------------------------------------------------------------------- /vendor/cache/nokogiri-1.6.6.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/nokogiri-1.6.6.2.gem -------------------------------------------------------------------------------- /vendor/cache/posix-spawn-0.3.8.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/posix-spawn-0.3.8.gem -------------------------------------------------------------------------------- /vendor/cache/raindrops-0.13.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/raindrops-0.13.0.gem -------------------------------------------------------------------------------- /vendor/cache/rspec-core-2.14.7.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/rspec-core-2.14.7.gem -------------------------------------------------------------------------------- /vendor/cache/thread_safe-0.3.5.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/thread_safe-0.3.5.gem -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | # A helper module to be shared with controllers 2 | module ApplicationHelper 3 | end 4 | -------------------------------------------------------------------------------- /vendor/cache/actionmailer-4.2.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/actionmailer-4.2.2.gem -------------------------------------------------------------------------------- /vendor/cache/activerecord-4.2.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/activerecord-4.2.2.gem -------------------------------------------------------------------------------- /vendor/cache/activesupport-4.2.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/activesupport-4.2.2.gem -------------------------------------------------------------------------------- /vendor/cache/better_errors-1.1.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/better_errors-1.1.0.gem -------------------------------------------------------------------------------- /vendor/cache/meta_request-0.2.8.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/meta_request-0.2.8.gem -------------------------------------------------------------------------------- /vendor/cache/method_source-0.8.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/method_source-0.8.2.gem -------------------------------------------------------------------------------- /vendor/cache/mini_portile-0.6.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/mini_portile-0.6.2.gem -------------------------------------------------------------------------------- /vendor/cache/multipart-post-2.0.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/multipart-post-2.0.0.gem -------------------------------------------------------------------------------- /vendor/cache/rack-contrib-1.1.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/rack-contrib-1.1.0.gem -------------------------------------------------------------------------------- /vendor/cache/rspec-mocks-2.14.5.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/rspec-mocks-2.14.5.gem -------------------------------------------------------------------------------- /vendor/cache/rspec-rails-2.14.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/rspec-rails-2.14.1.gem -------------------------------------------------------------------------------- /vendor/cache/simplecov-html-0.7.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/simplecov-html-0.7.1.gem -------------------------------------------------------------------------------- /vendor/cache/slack-notifier-1.0.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/slack-notifier-1.0.0.gem -------------------------------------------------------------------------------- /vendor/cache/warden-github-1.0.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/warden-github-1.0.2.gem -------------------------------------------------------------------------------- /vendor/cache/capistrano-rails-1.1.3.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/capistrano-rails-1.1.3.gem -------------------------------------------------------------------------------- /vendor/cache/debug_inspector-0.0.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/debug_inspector-0.0.2.gem -------------------------------------------------------------------------------- /vendor/cache/rack-protection-1.5.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/rack-protection-1.5.2.gem -------------------------------------------------------------------------------- /vendor/cache/redis-namespace-1.4.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/redis-namespace-1.4.1.gem -------------------------------------------------------------------------------- /vendor/cache/resque-scheduler-4.0.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/resque-scheduler-4.0.0.gem -------------------------------------------------------------------------------- /vendor/cache/ruby-progressbar-1.5.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/ruby-progressbar-1.5.1.gem -------------------------------------------------------------------------------- /vendor/cache/rufus-scheduler-3.2.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/rufus-scheduler-3.2.0.gem -------------------------------------------------------------------------------- /vendor/cache/sprockets-rails-2.2.4.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/sprockets-rails-2.2.4.gem -------------------------------------------------------------------------------- /config/initializers/flowdock.rb: -------------------------------------------------------------------------------- 1 | if ENV['FLOWDOCK_API_URL'] 2 | ::Flowdock::FLOWDOCK_API_URL = ENV['FLOWDOCK_API_URL'] 3 | end 4 | -------------------------------------------------------------------------------- /vendor/cache/binding_of_caller-0.7.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/binding_of_caller-0.7.2.gem -------------------------------------------------------------------------------- /vendor/cache/capistrano-bundler-1.1.4.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/capistrano-bundler-1.1.4.gem -------------------------------------------------------------------------------- /vendor/cache/capistrano-resque-0.2.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/capistrano-resque-0.2.2.gem -------------------------------------------------------------------------------- /vendor/cache/faraday_middleware-0.9.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/faraday_middleware-0.9.1.gem -------------------------------------------------------------------------------- /vendor/cache/rails-dom-testing-1.0.6.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/rails-dom-testing-1.0.6.gem -------------------------------------------------------------------------------- /vendor/cache/resque-lock-timeout-0.4.4.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/resque-lock-timeout-0.4.4.gem -------------------------------------------------------------------------------- /vendor/cache/rspec-expectations-2.14.5.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/rspec-expectations-2.14.5.gem -------------------------------------------------------------------------------- /vendor/cache/warden-github-rails-1.1.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/warden-github-rails-1.1.0.gem -------------------------------------------------------------------------------- /lib/tasks/resque.rake: -------------------------------------------------------------------------------- 1 | require "resque/tasks" 2 | 3 | namespace :resque do 4 | task :setup => [:environment] do 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /vendor/cache/capistrano-passenger-0.2.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/capistrano-passenger-0.2.0.gem -------------------------------------------------------------------------------- /vendor/cache/rails-html-sanitizer-1.0.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/rails-html-sanitizer-1.0.2.gem -------------------------------------------------------------------------------- /vendor/cache/capistrano-rails-console-1.0.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/capistrano-rails-console-1.0.0.gem -------------------------------------------------------------------------------- /config/initializers/resque.rb: -------------------------------------------------------------------------------- 1 | Resque.redis = Redis::Namespace.new( 2 | "#{Heaven::REDIS_PREFIX}:resque", 3 | :redis => Heaven.redis 4 | ) 5 | -------------------------------------------------------------------------------- /vendor/cache/rails-deprecated_sanitizer-1.0.3.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/heaven/master/vendor/cache/rails-deprecated_sanitizer-1.0.3.gem -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | ActiveSupport.on_load(:action_controller) do 2 | wrap_parameters format: [] if respond_to?(:wrap_parameters) 3 | end 4 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../../config/application', __FILE__) 3 | require_relative '../config/boot' 4 | require 'rails/commands' 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | If you'd like to submit changes: 2 | 3 | * Create a branch with your code 4 | * Make sure the tests work by running `script/cibuild` 5 | * Open a pull request 6 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Rails.application 5 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Heaven::Application.config.session_store :cookie_store, key: '_heaven_session' 4 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | 0.8.0 2 | ===== 3 | 4 | * Support Elastic Beanstalk deployments of configured apps. 5 | * Repository can be flagged as inactive if heaven isn't responsible for deploying them. 6 | -------------------------------------------------------------------------------- /app/models/repository.rb: -------------------------------------------------------------------------------- 1 | # A database record for a repository 2 | class Repository < ActiveRecord::Base 3 | validates_presence_of :name, :owner 4 | 5 | has_many :deployments 6 | end 7 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the Rails application. 5 | Heaven::Application.initialize! 6 | -------------------------------------------------------------------------------- /spec/fixtures/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "verifiable_password_authentication": true, 3 | "hooks": [ 4 | "192.30.252.0/22" 5 | ], 6 | "git": [ 7 | "192.30.252.0/22" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | # The top-level application controller 2 | class ApplicationController < ActionController::Base 3 | protect_from_forgery :with => :null_session 4 | end 5 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | # Set up gems listed in the Gemfile. 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | 4 | require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) 5 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /script/console: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # GitHub CI paths 4 | if [ -d /usr/share/rbenv/shims ]; then 5 | export PATH=/usr/share/rbenv/shims:$PATH 6 | export RBENV_VERSION="1.9.3-p231-tcs-github" 7 | fi 8 | 9 | bundle exec rails c 10 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | # Mime::Type.register_alias "text/html", :iphone 6 | -------------------------------------------------------------------------------- /script/bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # GitHub CI paths 4 | if [ -d /usr/share/rbenv/shims ]; then 5 | export PATH=/usr/share/rbenv/shims:$PATH 6 | export RBENV_VERSION="1.9.3-p231-tcs-github" 7 | fi 8 | 9 | bundle install --local --path vendor/gems 10 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path("../config/application", __FILE__) 5 | 6 | Heaven::Application.load_tasks 7 | -------------------------------------------------------------------------------- /spec/models/deployment/status_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Deployment::Status do 4 | it "knows whether or not it completed" do 5 | status = Deployment::Status.new("atmos/heaven", 42) 6 | 7 | expect(status).to_not be_completed 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/support/matchers/environment_locker_matchers.rb: -------------------------------------------------------------------------------- 1 | module EnvironmentLocker::Matchers 2 | extend RSpec::Matchers::DSL 3 | 4 | matcher :be_locked do 5 | match do |environment_key| 6 | Heaven.redis.exists("#{environment_key}-lock") 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | param_keys = [:branches, :commit, :config, :context, :deployment, :description, :environment, :id, :name, :password, :payload, :ref, :repository, :sender, :sha, :state, :target_url] 2 | Rails.application.config.filter_parameters += param_keys 3 | -------------------------------------------------------------------------------- /spec/support/helpers/fixture_helper.rb: -------------------------------------------------------------------------------- 1 | module FixtureHelper 2 | def fixture_data(name) 3 | path = Rails.root.join("spec", "fixtures", "#{name}.json") 4 | File.read(path) 5 | end 6 | 7 | def decoded_fixture_data(name) 8 | JSON.parse(fixture_data(name)) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/app_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe "app.json config for heroku" do 4 | it "doesn't blow up parsing" do 5 | app_file = File.expand_path("../../app.json", __FILE__) 6 | data = JSON.parse(File.read(app_file)) 7 | expect(data["name"]).to eql("Heaven") 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/models/repository_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Repository do 4 | it "creates simple repositories" do 5 | repository = Repository.create :name => "heaven", :owner => "atmos" 6 | 7 | expect(repository).to be_valid 8 | expect(repository).to be_active 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Heaven::Application.routes.draw do 2 | get "/" => redirect(ENV["ROOT_REDIRECT_URL"] || "https://github.com/atmos/heaven") 3 | 4 | github_authenticate(:team => :employees) do 5 | mount Resque::Server.new, :at => "/resque" 6 | end 7 | 8 | post "/events" => "events#create" 9 | end 10 | -------------------------------------------------------------------------------- /config/initializers/libraries.rb: -------------------------------------------------------------------------------- 1 | # General requires you have for various things 2 | require 'base64' 3 | require 'timeout' 4 | require 'resque/server' 5 | require 'resque/plugins/lock_timeout' 6 | require 'yajl/json_gem' 7 | require 'aws-sdk' 8 | require 'heaven' 9 | require "active_support/core_ext/hash/indifferent_access" 10 | -------------------------------------------------------------------------------- /lib/heaven/jobs/deployment_status.rb: -------------------------------------------------------------------------------- 1 | module Heaven 2 | module Jobs 3 | # A deployment status handler 4 | class DeploymentStatus 5 | @queue = :deployment_statuses 6 | 7 | def self.perform(payload) 8 | notifier = Heaven::Notifier.for(payload) 9 | notifier.post! if notifier 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }]) 7 | # Mayor.create(name: 'Emanuel', city: cities.first) 8 | -------------------------------------------------------------------------------- /spec/requests/site_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe "Visiting in a browser" do 4 | describe "GET /" do 5 | it "redirects to github.com" do 6 | get "/" 7 | 8 | expect(last_response).to be_redirect 9 | expect(last_response.headers["Location"]).to eq( 10 | "https://github.com/atmos/heaven" 11 | ) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/lib/heaven/provider/capistrano_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Heaven::Provider::Capistrano do 4 | include FixtureHelper 5 | 6 | let(:deployment) { Heaven::Provider::Capistrano.new(SecureRandom.uuid, decoded_fixture_data("deployment-capistrano")) } 7 | 8 | it "finds deployment task" do 9 | expect(deployment.task).to eql "deploy:migrations" 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | HerokuDeploy 5 | <%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => true %> 6 | <%= javascript_include_tag "application", "data-turbolinks-track" => true %> 7 | <%= csrf_meta_tags %> 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /lib/heaven/jobs.rb: -------------------------------------------------------------------------------- 1 | module Heaven 2 | # A job to handle commit statuses 3 | module Jobs 4 | end 5 | end 6 | 7 | require "heaven/jobs/deployment" 8 | require "heaven/jobs/deployment_status" 9 | require "heaven/jobs/status" 10 | require "heaven/jobs/locked_error" 11 | require "heaven/jobs/environment_lock" 12 | require "heaven/jobs/environment_unlock" 13 | require "heaven/jobs/environment_locked_error" 14 | -------------------------------------------------------------------------------- /spec/support/helpers/comparison_helper.rb: -------------------------------------------------------------------------------- 1 | module ComparisonHelper 2 | def build_commit_hash(message) 3 | { 4 | :sha => "sha", 5 | :commit => { 6 | :message => message 7 | }, 8 | :author => { 9 | :login => "login", 10 | :html_url => "https://github.com/login" 11 | }, 12 | :html_url => "https://github.com/org/repo/commit/sha" 13 | } 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /bin/dpl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # This file was generated by Bundler. 4 | # 5 | # The application 'dpl' is installed as part of a gem, and 6 | # this file is here to facilitate running it. 7 | # 8 | 9 | require 'pathname' 10 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile", 11 | Pathname.new(__FILE__).realpath) 12 | 13 | require 'rubygems' 14 | require 'bundler/setup' 15 | 16 | load Gem.bin_path('dpl', 'dpl') 17 | -------------------------------------------------------------------------------- /bin/cap: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # This file was generated by Bundler. 4 | # 5 | # The application 'cap' is installed as part of a gem, and 6 | # this file is here to facilitate running it. 7 | # 8 | 9 | require 'pathname' 10 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile", 11 | Pathname.new(__FILE__).realpath) 12 | 13 | require 'rubygems' 14 | require 'bundler/setup' 15 | 16 | load Gem.bin_path('capistrano', 'cap') 17 | -------------------------------------------------------------------------------- /bin/capify: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # This file was generated by Bundler. 4 | # 5 | # The application 'capify' is installed as part of a gem, and 6 | # this file is here to facilitate running it. 7 | # 8 | 9 | require 'pathname' 10 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile", 11 | Pathname.new(__FILE__).realpath) 12 | 13 | require 'rubygems' 14 | require 'bundler/setup' 15 | 16 | load Gem.bin_path('capistrano', 'capify') 17 | -------------------------------------------------------------------------------- /config/initializers/warden-github.rb: -------------------------------------------------------------------------------- 1 | require 'warden/github/rails' 2 | 3 | Warden::GitHub::Rails.setup do |config| 4 | config.add_scope :user, :client_id => ENV['GITHUB_CLIENT_ID'], 5 | :client_secret => ENV['GITHUB_CLIENT_SECRET'], 6 | :scope => ["read:org"] 7 | 8 | config.add_team :employees, ENV['GITHUB_TEAM_ID'] || '696075' 9 | 10 | config.default_scope = :user 11 | end 12 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /lib/heaven/jobs/status.rb: -------------------------------------------------------------------------------- 1 | module Heaven 2 | module Jobs 3 | # A job to handle commit statuses 4 | class Status 5 | @queue = :statuses 6 | 7 | attr_accessor :guid, :payload 8 | 9 | def initialize(guid, payload) 10 | @guid = guid 11 | @payload = payload 12 | end 13 | 14 | def self.perform(guid, payload) 15 | CommitStatus.new(guid, payload).run! 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /script/cibuild: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | if [ -d /usr/share/rbenv/shims ]; then 3 | export PATH=/usr/share/rbenv/shims:$PATH 4 | export RBENV_VERSION="1.9.3-p231-tcs-github" 5 | fi 6 | 7 | set -x 8 | git log -n 1 HEAD | cat 9 | ruby -v 10 | bundle -v 11 | set +x 12 | 13 | export RACK_ENV=test 14 | export RAILS_ENV=test 15 | 16 | script/bootstrap 17 | bundle exec rake db:migrate 18 | bundle exec rake assets:precompile 19 | bundle exec rspec spec 20 | bundle exec rubocop 21 | -------------------------------------------------------------------------------- /spec/validators/github_source_validator_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe GithubSourceValidator do 4 | include MetaHelper 5 | 6 | before { stub_meta } 7 | 8 | context "verifies IPs" do 9 | it "returns production" do 10 | expect(GithubSourceValidator.new("127.0.0.1")).to_not be_valid 11 | expect(GithubSourceValidator.new("192.30.252.41")).to be_valid 12 | expect(GithubSourceValidator.new("192.30.252.46")).to be_valid 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/models/deployment/credentials_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Deployment::Credentials do 4 | it "correctly sets up the environment" do 5 | root = "#{Dir.pwd}/tmp" 6 | credentials = Deployment::Credentials.new(root) 7 | 8 | expect { credentials.setup! }.to_not raise_error 9 | expect(File.exist?("#{root}/.netrc")).to be_true 10 | expect(File.exist?("#{root}/.ssh/config")).to be_true 11 | expect(File.exist?("#{root}/.ssh/id_rsa")).to be_true 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/support/matchers/deployment_status_matchers.rb: -------------------------------------------------------------------------------- 1 | module Deployment::Status::Matchers 2 | extend RSpec::Matchers::DSL 3 | 4 | def event_match?(event, options) 5 | options.deep_stringify_keys!.all? do |key, value| 6 | event.fetch(key) == value 7 | end 8 | end 9 | 10 | matcher :have_event do |options| 11 | match do |deployment_status| 12 | deployment_status.deliveries.any? do |event| 13 | event_match?(event, options) 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/lib/heaven/notifier/default_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe "Heaven::Notifier::Default" do 4 | it "does not deliver changes unless an environment opt-in is present" do 5 | notifier = Heaven::Notifier::Default.new("{}") 6 | 7 | expect(notifier.change_delivery_enabled?).to be_false 8 | 9 | ENV["HEAVEN_NOTIFIER_DISPLAY_COMMITS"] = "true" 10 | 11 | notifier = Heaven::Notifier::Default.new("{}") 12 | 13 | expect(notifier.change_delivery_enabled?).to be_true 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/support/helpers/gist_helper.rb: -------------------------------------------------------------------------------- 1 | module GistHelper 2 | def stub_gists 3 | stub_request(:post, "https://api.github.com/gists") 4 | .to_return( 5 | :status => 200, 6 | :body => double( 7 | "id" => "cd520d99c3087f2d18b4", 8 | :html_url => "https://gist.github.com/atmos/cd520d99c3087f2d18b4" 9 | ) 10 | ) 11 | 12 | stub_request(:patch, "https://api.github.com/gists/cd520d99c3087f2d18b4") 13 | .to_return(:status => 200, :body => "", :headers => {}) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/controllers/concerns/webhook_validations.rb: -------------------------------------------------------------------------------- 1 | # A bunch of validations for incoming webhooks to ensure github is sending them 2 | module WebhookValidations 3 | extend ActiveSupport::Concern 4 | 5 | def verify_incoming_webhook_address! 6 | if valid_incoming_webhook_address? 7 | true 8 | else 9 | render :json => {}, :status => :forbidden 10 | end 11 | end 12 | 13 | def valid_incoming_webhook_address? 14 | if Octokit.api_endpoint == "https://api.github.com/" 15 | GithubSourceValidator.new(request.ip).valid? 16 | else 17 | true 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore the default SQLite database. 11 | /db/*.sqlite3 12 | /db/*.sqlite3-journal 13 | 14 | # Ignore all logfiles and tempfiles. 15 | /log/*.log 16 | /tmp 17 | vendor/gems 18 | public/assets 19 | release.sh 20 | coverage 21 | .ruby-version 22 | -------------------------------------------------------------------------------- /app/models/concerns/deployment_timeout.rb: -------------------------------------------------------------------------------- 1 | # A module to handle deployment timeouts 2 | module DeploymentTimeout 3 | extend ActiveSupport::Concern 4 | 5 | def timeout 6 | Integer(ENV["DEPLOYMENT_TIMEOUT"] || "300") 7 | end 8 | 9 | def deployment_time_elapsed 10 | (Time.now - deployment_start_time).ceil 11 | end 12 | 13 | def deployment_time_remaining 14 | timeout - deployment_time_elapsed 15 | end 16 | 17 | def deployment_start_time 18 | @deployment_start_time || Time.now 19 | end 20 | 21 | def start_deployment_timeout! 22 | @deployment_start_time = Time.now 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/lib/heaven/provider_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Heaven::Provider do 4 | include FixtureHelper 5 | 6 | describe ".from" do 7 | it "returns an initialized provider based on the payload config" do 8 | data = decoded_fixture_data("deployment") 9 | data["deployment"]["payload"]["config"]["provider"] = "capistrano" 10 | 11 | provider = Heaven::Provider.from("1", data) 12 | 13 | expect(provider).to be_a(Heaven::Provider::Capistrano) 14 | 15 | provider = Heaven::Provider.from("1", {}) 16 | 17 | expect(provider).to be_nil 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /db/migrate/20140329200427_create_deployments.rb: -------------------------------------------------------------------------------- 1 | class CreateDeployments < ActiveRecord::Migration 2 | def change 3 | create_table :deployments do |t| 4 | t.text :custom_payload 5 | t.string :environment, :required => true, :default => "production" 6 | t.string :guid, :required => true 7 | t.string :name, :required => true 8 | t.string :name_with_owner, :required => true 9 | t.string :output 10 | t.string :ref, :required => true 11 | t.string :sha, :required => true 12 | 13 | t.timestamps 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/heaven/jobs/environment_lock.rb: -------------------------------------------------------------------------------- 1 | module Heaven 2 | module Jobs 3 | # A job for unlocking the environment 4 | class EnvironmentLock 5 | @queue = :locks 6 | 7 | def self.perform(lock_params) 8 | lock_params.symbolize_keys! 9 | locker = EnvironmentLocker.new(lock_params) 10 | locker.lock! 11 | 12 | status = ::Deployment::Status.new(lock_params[:name_with_owner], lock_params[:deployment_id]) 13 | status.description = "#{locker.name_with_owner} locked on #{locker.environment} by #{locker.actor}" 14 | 15 | status.success! 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/heaven/jobs/environment_unlock.rb: -------------------------------------------------------------------------------- 1 | module Heaven 2 | module Jobs 3 | # A job for unlocking the environment 4 | class EnvironmentUnlock 5 | @queue = :locks 6 | 7 | def self.perform(lock_params) 8 | lock_params.symbolize_keys! 9 | locker = EnvironmentLocker.new(lock_params) 10 | locker.unlock! 11 | 12 | status = ::Deployment::Status.new(lock_params[:name_with_owner], lock_params[:deployment_id]) 13 | status.description = "#{locker.name_with_owner} unlocked on #{locker.environment} by #{locker.actor}" 14 | 15 | status.success! 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/support/helpers/meta_helper.rb: -------------------------------------------------------------------------------- 1 | module MetaHelper 2 | def stub_meta 3 | request_params = { 4 | "headers" => { 5 | "Accept" => "application/vnd.github.v3+json", 6 | "User-Agent" => "Octokit Ruby Gem #{Octokit::VERSION}", 7 | "Accept-Encoding" => "gzip;q=1.0,deflate;q=0.6,identity;q=0.3" 8 | } 9 | } 10 | 11 | get_url = "https://api.github.com/meta?client_id=%3Cunknown-client-id%3E&client_secret=%3Cunknown-client-secret%3E" 12 | 13 | stub_request(:get, get_url).with(request_params) 14 | .to_return(:status => 200, :body => double("hooks" => ["192.30.252.0/22"])) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | development: 7 | adapter: sqlite3 8 | database: db/development.sqlite3 9 | pool: 5 10 | timeout: 5000 11 | 12 | # Warning: The database defined as "test" will be erased and 13 | # re-generated from your development database when you run "rake". 14 | # Do not set this db to the same as development or production. 15 | test: 16 | adapter: sqlite3 17 | database: db/test.sqlite3 18 | pool: 5 19 | timeout: 5000 20 | 21 | production: 22 | adapter: sqlite3 23 | database: db/production.sqlite3 24 | pool: 5 25 | timeout: 5000 26 | -------------------------------------------------------------------------------- /config/unicorn.rb: -------------------------------------------------------------------------------- 1 | # config/unicorn.rb 2 | worker_processes Integer(ENV["WEB_CONCURRENCY"] || 3) 3 | timeout 15 4 | preload_app true 5 | 6 | before_fork do |server, worker| 7 | Signal.trap "TERM" do 8 | puts "Unicorn master intercepting TERM and sending myself QUIT instead" 9 | Process.kill "QUIT", Process.pid 10 | end 11 | 12 | defined?(ActiveRecord::Base) and 13 | ActiveRecord::Base.connection.disconnect! 14 | end 15 | 16 | after_fork do |server, worker| 17 | Signal.trap "TERM" do 18 | puts "Unicorn worker intercepting TERM and doing nothing. Wait for master to send QUIT" 19 | end 20 | 21 | defined?(ActiveRecord::Base) and 22 | ActiveRecord::Base.establish_connection 23 | end 24 | -------------------------------------------------------------------------------- /lib/heaven/jobs/locked_error.rb: -------------------------------------------------------------------------------- 1 | module Heaven 2 | module Jobs 3 | # An error that's raised when two deployments trigger simultaneously 4 | class LockedError 5 | @queue = :deployment_statuses 6 | 7 | attr_accessor :guid, :payload 8 | 9 | def initialize(guid, payload) 10 | @guid = guid 11 | @payload = payload 12 | end 13 | 14 | def self.perform(guid, payload) 15 | provider = Heaven::Provider::DefaultProvider.new(guid, payload) 16 | provider.status.description = "Already deploying." 17 | provider.status.error! 18 | 19 | Rails.logger.info "Deployment errored out, run was locked." 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/heaven/jobs/environment_locked_error.rb: -------------------------------------------------------------------------------- 1 | module Heaven 2 | module Jobs 3 | # An error returned when a repo's environment is locked 4 | class EnvironmentLockedError 5 | @queue = :locks 6 | 7 | def self.perform(lock_params) 8 | lock_params.symbolize_keys! 9 | locker = EnvironmentLocker.new(lock_params) 10 | 11 | status = ::Deployment::Status.new(lock_params[:name_with_owner], lock_params[:deployment_id]) 12 | status.description = "#{locker.name_with_owner} is locked on #{locker.environment} by #{locker.locked_by}" 13 | 14 | status.error! 15 | 16 | Rails.logger.info "Deployment errored out, environment was locked." 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more, please read the Rails Internationalization guide 20 | # available at http://guides.rubyonrails.org/i18n.html. 21 | 22 | en: 23 | hello: "Hello world" 24 | -------------------------------------------------------------------------------- /db/migrate/20140728040201_create_repositories.rb: -------------------------------------------------------------------------------- 1 | class CreateRepositories < ActiveRecord::Migration 2 | def change 3 | create_table :repositories do |t| 4 | t.string :owner, :required => true, :nullable => false 5 | t.string :name, :required => true, :nullable => false 6 | t.boolean :active, :required => true, :default => true 7 | 8 | t.timestamps 9 | end 10 | 11 | add_column :deployments, :repository_id, :integer 12 | 13 | Deployment.all.each do |deployment| 14 | owner, name = deployment.name_with_owner.split('/') 15 | repository = Repository.find_or_create_by(owner: owner, name: name) 16 | deployment.repository = repository 17 | deployment.save 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/heaven/notifier/campfire.rb: -------------------------------------------------------------------------------- 1 | module Heaven 2 | module Notifier 3 | # A notifier for campfire 4 | class Campfire < Default 5 | def deliver(message) 6 | message << " #{output_link("Output")}" 7 | Rails.logger.info "campfire: #{message}" 8 | room = campfire_account.room_by_id(chat_room) 9 | room.message(message) 10 | end 11 | 12 | def campfire_token 13 | ENV["CAMPFIRE_TOKEN"] || "0xdeadbeef" 14 | end 15 | 16 | def campfire_subdomain 17 | ENV["CAMPFIRE_SUBDOMAIN"] || "unknown" 18 | end 19 | 20 | def campfire_account 21 | @campfire_account ||= ::Campfiyah::Account.new(campfire_subdomain, campfire_token) 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/request_spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | module ApiHelper 4 | def send_and_accept_json 5 | header "Accept", "application/json" 6 | header "Content-Type", "application/json" 7 | end 8 | 9 | def request_env(remote_ip = "192.30.252.41") 10 | { "REMOTE_ADDR" => remote_ip, 11 | "X_FORWARDED_FOR" => remote_ip } 12 | end 13 | 14 | def github_event(event) 15 | header "X-Github-Event", event 16 | header "X-Github-Delivery", SecureRandom.uuid 17 | end 18 | end 19 | 20 | RSpec.configure do |config| 21 | config.include Rack::Test::Methods 22 | config.include ApiHelper 23 | config.include MetaHelper 24 | 25 | config.before :type => :request do 26 | send_and_accept_json 27 | stub_meta 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | 10 | # Make sure your secret_key_base is kept private 11 | # if you're sharing your code publicly. 12 | if Rails.env.development? 13 | Heaven::Application.config.secret_key_base = '8d788e2c-b4b4-4013-9909-1364d53d0aa2' 14 | else 15 | Heaven::Application.config.secret_key_base = ENV['RAILS_SECRET_KEY_BASE'] 16 | end 17 | -------------------------------------------------------------------------------- /spec/lib/heaven/jobs/environment_lock_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Heaven::Jobs::EnvironmentLock do 4 | include Deployment::Status::Matchers, EnvironmentLocker::Matchers 5 | 6 | describe ".perform" do 7 | let(:lock_params) do 8 | { 9 | :name_with_owner => "atmos/heaven", 10 | :environment => "production", 11 | :actor => "atmos", 12 | :deployment_id => "12345" 13 | } 14 | end 15 | 16 | it "locks the environment and sends a success status" do 17 | job = Heaven::Jobs::EnvironmentLock 18 | 19 | job.perform(lock_params) 20 | 21 | expect("atmos/heaven-production").to be_locked 22 | expect(Deployment::Status).to have_event("status" => "success") 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/controllers/concerns/webhook_validations_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe WebhookValidations do 4 | include MetaHelper 5 | 6 | before { stub_meta } 7 | 8 | class WebhookValidationsTester 9 | class Request 10 | def initialize(ip) 11 | @ip = ip 12 | end 13 | attr_accessor :ip 14 | end 15 | include WebhookValidations 16 | 17 | def initialize(ip) 18 | @ip = ip 19 | end 20 | 21 | def request 22 | Request.new(@ip) 23 | end 24 | end 25 | 26 | it "makes methods available" do 27 | klass = WebhookValidationsTester.new("192.30.252.41") 28 | expect(klass).to be_valid_incoming_webhook_address 29 | klass = WebhookValidationsTester.new("127.0.0.1") 30 | expect(klass).to_not be_valid_incoming_webhook_address 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/lib/heaven/jobs/environment_unlock_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Heaven::Jobs::EnvironmentUnlock do 4 | include Deployment::Status::Matchers, 5 | EnvironmentLocker::Matchers 6 | 7 | describe ".perform" do 8 | let(:lock_params) do 9 | { 10 | :name_with_owner => "atmos/heaven", 11 | :environment => "production", 12 | :actor => "atmos", 13 | :deployment_id => "12345" 14 | } 15 | end 16 | 17 | before do 18 | EnvironmentLocker.new(lock_params).lock! 19 | end 20 | 21 | it "unlocks the environment and sends a success status" do 22 | job = Heaven::Jobs::EnvironmentUnlock 23 | 24 | job.perform(lock_params) 25 | 26 | expect("atmos/heaven-production").to_not be_locked 27 | expect(Deployment::Status).to have_event("status" => "success") 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /app/models/deployment.rb: -------------------------------------------------------------------------------- 1 | # A record of a deployment processes 2 | class Deployment < ActiveRecord::Base 3 | validates_presence_of :name, :name_with_owner 4 | 5 | belongs_to :repository 6 | 7 | def self.latest_for_name_with_owner(name_with_owner) 8 | sets = self.select(:name, :environment) 9 | .where(:name_with_owner => name_with_owner) 10 | .group("name,environment") 11 | 12 | sets.map do |deployment| 13 | params = { 14 | :name => deployment.name, 15 | :environment => deployment.environment, 16 | :name_with_owner => name_with_owner 17 | } 18 | Deployment.where(params).order("created_at desc").limit(1) 19 | end.flatten 20 | end 21 | 22 | def payload 23 | @payload ||= JSON.parse(custom_payload) 24 | end 25 | 26 | def auto_deploy_payload(actor, sha) 27 | payload.merge(:actor => actor, :sha => sha) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /app/controllers/events_controller.rb: -------------------------------------------------------------------------------- 1 | # A controller to handle incoming webhook events 2 | class EventsController < ApplicationController 3 | include WebhookValidations 4 | 5 | before_filter :verify_incoming_webhook_address! 6 | skip_before_filter :verify_authenticity_token, :only => [:create] 7 | 8 | def create 9 | event = request.headers["HTTP_X_GITHUB_EVENT"] 10 | delivery = request.headers["HTTP_X_GITHUB_DELIVERY"] 11 | 12 | if valid_events.include?(event) 13 | request.body.rewind 14 | 15 | Resque.enqueue(Receiver, event, delivery, event_params) 16 | 17 | render :json => {}, :status => :created 18 | else 19 | render :json => {}, :status => :unprocessable_entity 20 | end 21 | end 22 | 23 | def valid_events 24 | %w{deployment deployment_status status ping} 25 | end 26 | 27 | private 28 | 29 | def event_params 30 | params.permit! 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "rails", "~>4.2.2" 4 | gem "resque" 5 | gem "resque-lock-timeout" 6 | gem "octokit" 7 | gem "unicorn" 8 | gem "yajl-ruby" 9 | gem "posix-spawn" 10 | gem "warden-github-rails" 11 | gem "faraday" 12 | gem "faraday_middleware" 13 | 14 | # Providers 15 | gem "dpl", "1.5.7" 16 | gem "aws-sdk" 17 | gem "capistrano" 18 | gem 'capistrano-rails', '~> 1.1' 19 | gem 'capistrano-passenger' 20 | gem 'capistrano-rails-console' 21 | gem "capistrano-resque", "~> 0.2.2", require: false 22 | 23 | # Notifiers 24 | gem "hipchat" 25 | gem "campfiyah" 26 | gem "slack-notifier" 27 | gem "flowdock" 28 | 29 | # Database 30 | gem "sqlite3", "1.3.10" 31 | 32 | group :test do 33 | gem "webmock" 34 | gem "simplecov", "0.7.1" 35 | gem "rubocop" 36 | gem "rspec-rails" 37 | end 38 | 39 | group :development do 40 | gem "pry" 41 | gem "foreman" 42 | gem "meta_request" 43 | gem "better_errors" 44 | gem "binding_of_caller" 45 | end 46 | -------------------------------------------------------------------------------- /app/models/concerns/api_client.rb: -------------------------------------------------------------------------------- 1 | # A module to include for easy access to the GitHub API 2 | module ApiClient 3 | extend ActiveSupport::Concern 4 | 5 | def github_token 6 | ENV["GITHUB_TOKEN"] || "" 7 | end 8 | 9 | def github_client_id 10 | ENV["GITHUB_CLIENT_ID"] || "" 11 | end 12 | 13 | def github_client_secret 14 | ENV["GITHUB_CLIENT_SECRET"] || "" 15 | end 16 | 17 | def github_api_endpoint 18 | ENV["OCTOKIT_API_ENDPOINT"] || "https://api.github.com/" 19 | end 20 | 21 | def api 22 | @api ||= Octokit::Client.new(:access_token => github_token, 23 | :api_endpoint => github_api_endpoint) 24 | end 25 | 26 | def oauth_client_api 27 | @oauth_client_api ||= Octokit::Client.new( 28 | :client_id => github_client_id, 29 | :client_secret => github_client_secret, 30 | :api_endpoint => github_api_endpoint 31 | ) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/heaven/notifier.rb: -------------------------------------------------------------------------------- 1 | require "heaven/notifier/default" 2 | require "heaven/notifier/campfire" 3 | require "heaven/notifier/hipchat" 4 | require "heaven/notifier/flowdock" 5 | require "heaven/notifier/slack" 6 | 7 | module Heaven 8 | # The Notifier module 9 | module Notifier 10 | def self.for(payload) 11 | if slack? 12 | ::Heaven::Notifier::Slack.new(payload) 13 | elsif hipchat? 14 | ::Heaven::Notifier::Hipchat.new(payload) 15 | elsif flowdock? 16 | ::Heaven::Notifier::Flowdock.new(payload) 17 | elsif Rails.env.test? 18 | # noop on posting 19 | else 20 | ::Heaven::Notifier::Campfire.new(payload) 21 | end 22 | end 23 | 24 | def self.slack? 25 | !ENV["SLACK_WEBHOOK_URL"].nil? 26 | end 27 | 28 | def self.hipchat? 29 | !ENV["HIPCHAT_TOKEN"].nil? 30 | end 31 | 32 | def self.flowdock? 33 | !ENV["FLOWDOCK_USER_API_TOKEN"].nil? 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /doc/locking.md: -------------------------------------------------------------------------------- 1 | # Environment Locking 2 | 3 | Most deployment commands are passed through to the deployment provider. One 4 | exception to this norm, is when locking or unlocking an app environment. 5 | 6 | `deploy:lock` and `deploy:unlock` tasks are intercepted and handled within 7 | Heaven, never touching a provider. This allows for uniform locking behavior, 8 | regardless of which providers you're using. 9 | 10 | ## Locking an environment 11 | 12 | To lock an environment, set the `deploy:lock` task on a new deployment. 13 | 14 | Once an environment is locked, any following deployments to the same app 15 | environment will error out. If you have enabled a notifier, you will be sent an 16 | error description including who locked the environment. 17 | 18 | ## Unlocking an environment 19 | 20 | When you're ready to unlock an environment, set the `deploy:unlock` task on a 21 | new deployment. 22 | 23 | Once the deployment succeeds, the app environment will be available for new 24 | deployments! 25 | -------------------------------------------------------------------------------- /app/validators/github_source_validator.rb: -------------------------------------------------------------------------------- 1 | # A class to validate if a given ip is coming from GitHub. 2 | class GithubSourceValidator 3 | include ApiClient 4 | attr_accessor :ip 5 | 6 | def initialize(ip) 7 | @ip = ip 8 | end 9 | 10 | def valid? 11 | hook_source_ips.any? { |block| IPAddr.new(block).include?(ip) } 12 | end 13 | 14 | private 15 | 16 | VERIFIER_KEY = "hook-sources-#{Rails.env}" 17 | 18 | def source_key 19 | VERIFIER_KEY 20 | end 21 | 22 | def default_ttl 23 | %w{staging production}.include?(Rails.env) ? 60 : 2 24 | end 25 | 26 | def meta_info 27 | @meta_info ||= Heaven.redis.get(source_key) 28 | end 29 | 30 | def hook_source_ips 31 | if meta_info 32 | JSON.parse(meta_info) 33 | else 34 | addresses = oauth_client_api.get("/meta").hooks 35 | Heaven.redis.set(source_key, JSON.dump(addresses)) 36 | Heaven.redis.expire(source_key, default_ttl) 37 | Rails.logger.info "Refreshed GitHub hook sources" 38 | addresses 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /Capfile: -------------------------------------------------------------------------------- 1 | # Load DSL and set up stages 2 | require 'capistrano/setup' 3 | 4 | # Include default deployment tasks 5 | require 'capistrano/deploy' 6 | 7 | # Include tasks from other gems included in your Gemfile 8 | # 9 | # For documentation on these, see for example: 10 | # 11 | # https://github.com/capistrano/rvm 12 | # https://github.com/capistrano/rbenv 13 | # https://github.com/capistrano/chruby 14 | # https://github.com/capistrano/bundler 15 | # https://github.com/capistrano/rails 16 | # https://github.com/capistrano/passenger 17 | # 18 | # require 'capistrano/rvm' 19 | # require 'capistrano/rbenv' 20 | # require 'capistrano/chruby' 21 | require 'capistrano/bundler' 22 | require 'capistrano/rails/assets' 23 | require 'capistrano/rails/migrations' 24 | require 'capistrano/passenger' 25 | require 'capistrano/rails/console' 26 | 27 | # Load custom tasks from `lib/capistrano/tasks` if you have any defined 28 | Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r } 29 | 30 | # Load some resque tasks for deploys 31 | require "capistrano-resque" 32 | -------------------------------------------------------------------------------- /lib/heaven/notifier/hipchat.rb: -------------------------------------------------------------------------------- 1 | module Heaven 2 | module Notifier 3 | # A notifier for Hipchat 4 | class Hipchat < Notifier::Default 5 | def deliver(message) 6 | filtered_message = message + " #{ascii_face}" 7 | Rails.logger.info "hipchat: #{filtered_message}" 8 | 9 | hipchat_client["#{hipchat_room}"].send "hubot", filtered_message, 10 | :color => green? ? "green" : "red", 11 | :notify => 1, 12 | :message_format => "text" 13 | end 14 | 15 | def hipchat_token 16 | ENV["HIPCHAT_TOKEN"] 17 | end 18 | 19 | def hipchat_room 20 | ENV["HIPCHAT_ROOM"] || "Developers" 21 | end 22 | 23 | def hipchat_client 24 | @hipchat_client ||= ::HipChat::Client.new(hipchat_token) 25 | end 26 | 27 | def repository_link(path = "") 28 | repo_url(path) 29 | end 30 | 31 | def user_link 32 | "@#{chat_user}" 33 | end 34 | 35 | def output_link 36 | target_url 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/lib/heaven/provider/dpl_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Heaven::Provider::Dpl do 4 | include FixtureHelper 5 | 6 | context "production environment" do 7 | let(:data) { decoded_fixture_data("deployment") } 8 | let!(:deployment) { Heaven::Provider::Dpl.new(SecureRandom.uuid, data) } 9 | 10 | it "returns production" do 11 | expect(deployment.environment).to eq("production") 12 | end 13 | 14 | it "returns heroku_name" do 15 | expect(deployment.app_name).to eq(data["deployment"]["payload"]["config"]["heroku_name"]) 16 | end 17 | end 18 | 19 | context "staging environment" do 20 | let(:data) { decoded_fixture_data("deployment_staging") } 21 | let!(:deployment) { Heaven::Provider::Dpl.new(SecureRandom.uuid, data) } 22 | 23 | it "returns staging" do 24 | expect(deployment.environment).to eq("staging") 25 | end 26 | 27 | it "returns heroku_staging_name" do 28 | expect(deployment.app_name).to eq(data["deployment"]["payload"]["config"]["heroku_staging_name"]) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/lib/heaven/notifier/hipchat_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe "Heaven::Notifier::Hipchat" do 4 | include FixtureHelper 5 | 6 | it "handles pending notifications" do 7 | data = decoded_fixture_data("deployment-pending") 8 | 9 | n = Heaven::Notifier::Hipchat.new(data) 10 | expect(n.default_message).to \ 11 | eql "@atmos is deploying https://github.com/atmos/my-robot/tree/break-up-notifiers to production" 12 | end 13 | 14 | it "handles successful deployment statuses" do 15 | data = decoded_fixture_data("deployment-success") 16 | 17 | n = Heaven::Notifier::Hipchat.new(data) 18 | expect(n.default_message).to \ 19 | eql "@atmos's production deployment of https://github.com/atmos/my-robot is done! " 20 | end 21 | 22 | it "handles failure deployment statuses" do 23 | data = decoded_fixture_data("deployment-failure") 24 | 25 | n = Heaven::Notifier::Hipchat.new(data) 26 | expect(n.default_message).to \ 27 | eql "@atmos's production deployment of https://github.com/atmos/my-robot failed. " 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require 'rails/all' 4 | 5 | # Require the gems listed in Gemfile, including any gems 6 | # you've limited to :test, :development, or :production. 7 | Bundler.require(:default, Rails.env) 8 | 9 | module Heaven 10 | class Application < Rails::Application 11 | # Settings in config/environments/* take precedence over those specified here. 12 | # Application configuration should go into files in config/initializers 13 | # -- all .rb files in that directory are automatically loaded. 14 | 15 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 16 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 17 | # config.time_zone = 'Central Time (US & Canada)' 18 | 19 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 20 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 21 | # config.i18n.default_locale = :de 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/heaven/notifier/flowdock/api.rb: -------------------------------------------------------------------------------- 1 | module Heaven 2 | module Notifier 3 | # Flowdock Api interactions 4 | module FlowdockApi 5 | def thread_client 6 | @thread_client ||= Faraday.new ::Flowdock::FLOWDOCK_API_URL do |connection| 7 | connection.request :json 8 | connection.response :json, :content_type => /\bjson$/ 9 | connection.use Faraday::Response::RaiseError 10 | connection.use Faraday::Response::Logger, Rails.logger if Rails.logger.debug? 11 | connection.adapter Faraday.default_adapter 12 | end 13 | end 14 | 15 | def auth_client 16 | @auth_client ||= ::Flowdock::Client.new(:api_token => flowdock_user_api_token) 17 | end 18 | 19 | def flowdock_user_api_token 20 | ENV["FLOWDOCK_USER_API_TOKEN"] 21 | end 22 | 23 | def flow_token 24 | JSON.parse(ENV["FLOWDOCK_FLOW_TOKENS"])[chat_room] 25 | rescue JSON::ParserError => e 26 | Rails.logger.error "Failed parsing FLOWDOCK_FLOW_TOKENS: #{e}" 27 | nil 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /app/receivers/lock_receiver.rb: -------------------------------------------------------------------------------- 1 | # A class to handle incoming lock/unlock webhooks 2 | class LockReceiver 3 | attr_reader :data 4 | 5 | def initialize(data) 6 | @data = data 7 | end 8 | 9 | def run! 10 | if locker.lock? 11 | Resque.enqueue(Heaven::Jobs::EnvironmentLock, lock_params) 12 | elsif locker.unlock? 13 | Resque.enqueue(Heaven::Jobs::EnvironmentUnlock, lock_params) 14 | elsif locker.locked? 15 | Resque.enqueue(Heaven::Jobs::EnvironmentLockedError, lock_params) 16 | end 17 | end 18 | 19 | private 20 | 21 | def locker 22 | @locker ||= EnvironmentLocker.new(lock_params) 23 | end 24 | 25 | def lock_params 26 | {}.tap do |hash| 27 | hash[:name_with_owner] = data["repository"]["full_name"] 28 | hash[:environment] = deployment_data["environment"] 29 | hash[:actor] = deployment_data["creator"]["login"] 30 | hash[:deployment_id] = deployment_data["id"] 31 | hash[:task] = deployment_data["task"] 32 | end 33 | end 34 | 35 | def deployment_data 36 | data["deployment"] || data 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Corey Donohoe 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /lib/heaven.rb: -------------------------------------------------------------------------------- 1 | # The top-level Heaven module 2 | module Heaven 3 | REDIS_PREFIX = "heaven:#{Rails.env}" 4 | 5 | class << self 6 | attr_writer :testing, :redis 7 | 8 | def testing? 9 | @testing.present? 10 | end 11 | 12 | def redis 13 | @redis ||= if ENV["REDIS_PROVIDER"] 14 | Redis.new(:url => ENV[ENV["REDIS_PROVIDER"]]) 15 | elsif ENV["REDISCLOUD_URL"] 16 | Redis.new(:url => ENV["REDISCLOUD_URL"]) 17 | elsif ENV["OPENREDIS_URL"] 18 | Redis.new(:url => ENV["OPENREDIS_URL"]) 19 | elsif ENV["BOXEN_REDIS_URL"] 20 | Redis.new(:url => ENV["BOXEN_REDIS_URL"]) 21 | else 22 | Redis.new 23 | end 24 | 25 | @redis 26 | end 27 | 28 | def redis_reconnect! 29 | @redis = nil 30 | redis 31 | end 32 | end 33 | end 34 | 35 | # initialize early to ensure proper resque prefixes 36 | Heaven.redis 37 | 38 | require "heaven/version" 39 | require "heaven/jobs" 40 | require "heaven/provider" 41 | require "heaven/notifier" 42 | -------------------------------------------------------------------------------- /spec/lib/heaven/jobs/environment_locked_error_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Heaven::Jobs::EnvironmentLockedError do 4 | include Deployment::Status::Matchers, 5 | EnvironmentLocker::Matchers 6 | 7 | describe ".perform" do 8 | let(:lock_params) do 9 | { 10 | :name_with_owner => "atmos/heaven", 11 | :environment => "production", 12 | :actor => "atmos", 13 | :deployment_id => "12345" 14 | } 15 | end 16 | 17 | it "triggers an error status with information about the lock" do 18 | job = Heaven::Jobs::EnvironmentLockedError 19 | 20 | job.perform(lock_params) 21 | 22 | expect(Deployment::Status).to have_event( 23 | "status" => "error", 24 | "description" => "atmos/heaven is locked on production by Unknown" 25 | ) 26 | 27 | EnvironmentLocker.new(lock_params).lock! 28 | 29 | job.perform(lock_params) 30 | 31 | expect(Deployment::Status).to have_event( 32 | "status" => "error", 33 | "description" => "atmos/heaven is locked on production by atmos" 34 | ) 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/heaven/jobs/deployment.rb: -------------------------------------------------------------------------------- 1 | module Heaven 2 | module Jobs 3 | # A class for kicking off deployment processes 4 | class Deployment 5 | extend Resque::Plugins::LockTimeout 6 | 7 | @queue = :deployments 8 | @lock_timeout = Integer(ENV["DEPLOYMENT_TIMEOUT"] || "300") 9 | 10 | # Only allow one deployment per-environment at a time 11 | def self.redis_lock_key(guid, data) 12 | deployment_data = data["deployment"] 13 | if deployment_data["payload"] && deployment_data["payload"]["name"] 14 | name = deployment_data["payload"]["name"] 15 | return "#{name}-#{deployment_data["environment"]}-deployment" 16 | end 17 | guid 18 | end 19 | 20 | def self.identifier(guid, data) 21 | redis_lock_key(guid, data) 22 | end 23 | 24 | attr_accessor :guid, :data 25 | 26 | def initialize(guid, data) 27 | @guid = guid 28 | @data = data 29 | end 30 | 31 | def self.perform(guid, data) 32 | provider = Heaven::Provider.from(guid, data) 33 | provider.run! if provider 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/lib/heaven/notifier/campfire_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe "Heaven::Notifier::Campfire" do 4 | include FixtureHelper 5 | 6 | it "handles pending notifications" do 7 | data = decoded_fixture_data("deployment-pending") 8 | 9 | n = Heaven::Notifier::Campfire.new(data) 10 | expect(n.default_message).to \ 11 | eql "[atmos](https://github.com/atmos) is deploying [my-robot](https://github.com/atmos/my-robot/tree/break-up-notifiers) to production" 12 | end 13 | 14 | it "handles successful deployment statuses" do 15 | data = decoded_fixture_data("deployment-success") 16 | 17 | n = Heaven::Notifier::Campfire.new(data) 18 | expect(n.default_message).to \ 19 | eql "[atmos](https://github.com/atmos)'s production deployment of [my-robot](https://github.com/atmos/my-robot) is done! " 20 | end 21 | 22 | it "handles failure deployment statuses" do 23 | data = decoded_fixture_data("deployment-failure") 24 | 25 | n = Heaven::Notifier::Campfire.new(data) 26 | expect(n.default_message).to \ 27 | eql "[atmos](https://github.com/atmos)'s production deployment of [my-robot](https://github.com/atmos/my-robot) failed. " 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Heaven::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports and disable caching. 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send. 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger. 20 | config.active_support.deprecation = :log 21 | 22 | # Raise an error on page load if there are pending migrations 23 | config.active_record.migration_error = :page_load 24 | 25 | # Debug mode disables concatenation and preprocessing of assets. 26 | # This option may cause significant delays in view rendering with a large 27 | # number of complex assets. 28 | config.assets.debug = true 29 | end 30 | -------------------------------------------------------------------------------- /spec/models/deployment/output_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Deployment::Output do 4 | let(:gist) { Octokit::Gist.new("deadbeef") } 5 | 6 | it "creates a gist for storing output" do 7 | params = { 8 | :files => { :stdout => { :content => "Deployment 42 pending" } }, 9 | :public => false, 10 | :description => "Heaven number 42 for heaven" 11 | } 12 | 13 | stub_request(:post, "https://api.github.com/gists") 14 | .with(:body => params.to_json) 15 | .to_return(:status => 200, :body => gist, :headers => {}) 16 | 17 | output = Deployment::Output.new("heaven", 42, SecureRandom.uuid) 18 | expect { output.create }.to_not raise_error 19 | 20 | params = { 21 | :files => { 22 | :stderr => { :content => "chasing dreams" }, 23 | :stdout => { :content => "push to limit" } 24 | }, 25 | :public => false 26 | } 27 | 28 | stub_request(:patch, "https://api.github.com/gists/#{gist.id}") 29 | .with(:body => params.to_json) 30 | .to_return(:status => 200, :body => "", :headers => {}) 31 | 32 | output.stderr = "chasing dreams" 33 | output.stdout = "push to limit" 34 | 35 | expect { output.update }.to_not raise_error 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /app/services/environment_locker.rb: -------------------------------------------------------------------------------- 1 | # A class for lock/unlocking a repo's environment 2 | class EnvironmentLocker 3 | LOCK_TASK = "lock".freeze 4 | UNLOCK_TASK = "unlock".freeze 5 | UNKNOWN_ACTOR = "Unknown".freeze 6 | 7 | attr_reader :name_with_owner, :environment, :actor, :task 8 | attr_writer :redis 9 | 10 | def initialize(lock_params) 11 | @name_with_owner = lock_params.fetch(:name_with_owner) 12 | @environment = lock_params.fetch(:environment) 13 | @actor = lock_params[:actor] 14 | @task = lock_params[:task] 15 | end 16 | 17 | def lock? 18 | task == "#{prefix}:#{LOCK_TASK}" 19 | end 20 | 21 | def unlock? 22 | task == "#{prefix}:#{UNLOCK_TASK}" 23 | end 24 | 25 | def lock! 26 | redis.set(redis_key, actor) 27 | end 28 | 29 | def unlock! 30 | redis.del(redis_key) 31 | end 32 | 33 | def locked? 34 | redis.exists(redis_key) 35 | end 36 | 37 | def locked_by 38 | redis.get(redis_key) || UNKNOWN_ACTOR 39 | end 40 | 41 | private 42 | 43 | def redis 44 | @redis ||= Heaven.redis 45 | end 46 | 47 | def redis_key 48 | [name_with_owner, environment, "lock"].join("-") 49 | end 50 | 51 | def prefix 52 | ENV["HUBOT_DEPLOY_PREFIX"] || "deploy" 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/models/concerns/api_client_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe ApiClient do 4 | class ApiClientTester 5 | include ApiClient 6 | end 7 | 8 | let(:tester) { ApiClientTester.new } 9 | 10 | describe "#api" do 11 | it "#api uses #github_token to auth requests" do 12 | ENV["GITHUB_TOKEN"] = "secret" 13 | stub_request(:get, "https://api.github.com/user") 14 | .with(:headers => octokit_request_headers) 15 | .to_return(:status => 200, :body => "atmos") 16 | expect(tester.api.user).to eql("atmos") 17 | end 18 | end 19 | 20 | describe "#oauth_client_api" do 21 | it "#oauth_client_api uses #github_client_id and #github_client_secret" do 22 | ENV["GITHUB_CLIENT_ID"] = "id" 23 | ENV["GITHUB_CLIENT_SECRET"] = "secret" 24 | 25 | stub_request(:get, "https://api.github.com/meta?client_id=id&client_secret=secret") 26 | .with(:headers => octokit_request_headers) 27 | .to_return(:status => 200, :body => "ok") 28 | 29 | expect(tester.oauth_client_api.meta).to eql("ok") 30 | end 31 | end 32 | 33 | def octokit_request_headers 34 | { "Accept" => "application/vnd.github.v3+json", 35 | "User-Agent" => "Octokit Ruby Gem #{Octokit::VERSION}", 36 | "Accept-Encoding" => "gzip;q=1.0,deflate;q=0.6,identity;q=0.3" } 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/heaven/provider/fabric.rb: -------------------------------------------------------------------------------- 1 | module Heaven 2 | # Top-level module for providers. 3 | module Provider 4 | # The fabric provider. 5 | class Fabric < DefaultProvider 6 | def initialize(guid, payload) 7 | super 8 | @name = "fabric" 9 | end 10 | 11 | def task 12 | deployment_data["task"] || "deploy" 13 | end 14 | 15 | def deploy_command_format 16 | ENV["DEPLOY_COMMAND_FORMAT"] || "fab -R %{environment} %{task}:branch_name=%{ref}" 17 | end 18 | 19 | def execute 20 | return execute_and_log(["/usr/bin/true"]) if Rails.env.test? 21 | 22 | unless File.exist?(checkout_directory) 23 | log "Cloning #{repository_url} into #{checkout_directory}" 24 | execute_and_log(["git", "clone", clone_url, checkout_directory]) 25 | end 26 | 27 | Dir.chdir(checkout_directory) do 28 | log "Fetching the latest code" 29 | execute_and_log(%w{git fetch}) 30 | execute_and_log(["git", "reset", "--hard", sha]) 31 | deploy_string = deploy_command_format % { 32 | :environment => environment, 33 | :task => task, 34 | :ref => ref 35 | } 36 | 37 | log "Executing fabric: #{deploy_string}" 38 | execute_and_log([deploy_string]) 39 | end 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/heaven/provider/capistrano.rb: -------------------------------------------------------------------------------- 1 | module Heaven 2 | # Top-level module for providers. 3 | module Provider 4 | # The capistrano provider. 5 | class Capistrano < DefaultProvider 6 | def initialize(guid, payload) 7 | super 8 | @name = "capistrano" 9 | end 10 | 11 | def cap_path 12 | gem_executable_path("cap") 13 | end 14 | 15 | def task 16 | name = deployment_data["task"] || "deploy" 17 | unless name =~ /deploy(?:\:[\w+:]+)?/ 18 | fail "Invalid capistrano taskname: #{name.inspect}" 19 | end 20 | name 21 | end 22 | 23 | def execute 24 | return execute_and_log(["/usr/bin/true"]) if Rails.env.test? 25 | 26 | unless File.exist?(checkout_directory) 27 | log "Cloning #{repository_url} into #{checkout_directory}" 28 | execute_and_log(["git", "clone", clone_url, checkout_directory]) 29 | end 30 | 31 | Dir.chdir(checkout_directory) do 32 | log "Fetching the latest code" 33 | execute_and_log(%w{git fetch}) 34 | execute_and_log(["git", "reset", "--hard", sha]) 35 | deploy_command = [cap_path, environment, task] 36 | log "Executing capistrano: #{deploy_command.join(" ")}" 37 | execute_and_log(deploy_command, "BRANCH" => ref) 38 | end 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/heaven/notifier/flowdock/message_helper.rb: -------------------------------------------------------------------------------- 1 | module Heaven 2 | module Notifier 3 | # Helpers for generating flowdock messages 4 | module FlowdockMessageHelper 5 | def tags 6 | ["deploy", environment, repo_name, state].compact 7 | end 8 | 9 | def build_status_avatar 10 | if %(success pending).include?(state) 11 | "https://d2ph5hv9wbwvla.cloudfront.net/heaven/build_ok.png" 12 | else 13 | "https://d2ph5hv9wbwvla.cloudfront.net/heaven/build_fail.png" 14 | end 15 | end 16 | 17 | def thread_status_color 18 | case state 19 | when "success" 20 | "green" 21 | when "error", "failure" 22 | "red" 23 | when "pending" 24 | "yellow" 25 | else 26 | nil 27 | end 28 | end 29 | 30 | def activity_title 31 | case state 32 | when "success" 33 | "#{repo_name} deployed with ref #{ref} to #{environment}." 34 | when "error" 35 | "Error deploying #{repo_name} to #{environment}." 36 | when "failure" 37 | "Failed deploying #{repo_name} to #{environment}." 38 | when "pending" 39 | "Started deploying #{repo_name} to #{environment}." 40 | else 41 | puts "Unhandled deployment state, #{state}" 42 | end 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /app/models/commit_status.rb: -------------------------------------------------------------------------------- 1 | # An object representing a commit status event from GitHub 2 | class CommitStatus 3 | include ApiClient 4 | 5 | attr_accessor :guid, :data 6 | 7 | def initialize(guid, data) 8 | @guid = guid 9 | @data = data 10 | end 11 | 12 | def successful? 13 | state == "success" 14 | end 15 | 16 | def sha 17 | data["sha"][0..7] 18 | end 19 | 20 | def state 21 | data["state"] 22 | end 23 | 24 | def branches 25 | @branches ||= data["branches"] 26 | end 27 | 28 | def default_branch 29 | data["repository"]["default_branch"] 30 | end 31 | 32 | def default_branch? 33 | branches.any? { |branch| branch["name"] == default_branch } 34 | end 35 | 36 | def name_with_owner 37 | data["repository"]["full_name"] 38 | end 39 | 40 | def author 41 | data["commit"]["commit"]["author"]["login"] 42 | end 43 | 44 | def run! 45 | return unless successful? 46 | if default_branch? 47 | Deployment.latest_for_name_with_owner(name_with_owner).each do |deployment| 48 | Rails.logger.info "tryna deploy #{name_with_owner}@#{sha} to #{deployment.environment}" 49 | AutoDeployment.new(deployment, self).execute 50 | end 51 | else 52 | branch = branches && branches.any? && branches.first["name"] 53 | Rails.logger.info "Ignoring commit status(#{state}) for #{name_with_owner}+#{branch}@#{sha}" 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/models/deployment_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Deployment do 4 | include FixtureHelper 5 | 6 | let(:payload) { fixture_data("deployment") } 7 | let!(:data) { JSON.parse(payload)["payload"] } 8 | 9 | let!(:create_data) do 10 | { 11 | :custom_payload => JSON.dump(data), 12 | :environment => "production", 13 | :guid => SecureRandom.uuid, 14 | :name => "hubot", 15 | :name_with_owner => "github/hubot", 16 | :output => "https://gist.github.com/1", 17 | :ref => "master", 18 | :sha => "f24b8008" 19 | } 20 | end 21 | 22 | it "works with dynamic finders" do 23 | deployment = Deployment.create create_data 24 | expect(deployment).to be_valid 25 | end 26 | 27 | it "#latest_for_name_with_owner" do 28 | present = [] 29 | Deployment.create create_data 30 | present << Deployment.create(create_data) 31 | 32 | Deployment.create create_data.merge(:name => "mybot") 33 | present << Deployment.create(create_data.merge(:name => "mybot")) 34 | 35 | Deployment.create create_data.merge(:name_with_owner => "atmos/heaven") 36 | 37 | present << Deployment.create(create_data.merge(:environment => "staging")) 38 | 39 | deployments = Deployment.latest_for_name_with_owner("github/hubot") 40 | 41 | expect(deployments.size).to be 3 42 | expect(deployments).to match_array(present) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # This file is copied to spec/ when you run 'rails generate rspec:install' 2 | require 'securerandom' 3 | ENV["RAILS_ENV"] ||= "test" 4 | ENV["RAILS_SECRET_KEY_BASE"] ||= SecureRandom.hex 5 | 6 | require File.expand_path("../../config/environment", __FILE__) 7 | require "simplecov" 8 | SimpleCov.start "rails" 9 | 10 | require "rspec/rails" 11 | require "rspec/autorun" 12 | require "webmock/rspec" 13 | 14 | Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } 15 | 16 | ActiveRecord::Migration.check_pending! if defined?(ActiveRecord::Migration) 17 | 18 | ENV["DEPLOYMENT_PRIVATE_KEY"] = "private\nkey\n" 19 | 20 | RSpec.configure do |config| 21 | config.include GistHelper 22 | config.include DeploymentStatusHelper 23 | 24 | config.order = "random" 25 | config.fixture_path = "#{::Rails.root}/spec/fixtures" 26 | config.use_transactional_fixtures = true 27 | config.infer_base_class_for_anonymous_controllers = false 28 | config.treat_symbols_as_metadata_keys_with_true_values = true 29 | 30 | config.before do 31 | ENV["GITHUB_CLIENT_ID"] = "" 32 | ENV["GITHUB_CLIENT_SECRET"] = "" 33 | Resque.inline = true 34 | end 35 | 36 | config.around do |example| 37 | original = Heaven.redis.client.db 38 | Heaven.redis.select(15) 39 | example.run 40 | Heaven.redis.flushall 41 | Heaven.redis.select(original) 42 | end 43 | end 44 | 45 | Heaven.testing = true 46 | -------------------------------------------------------------------------------- /lib/heaven/comparison/linked.rb: -------------------------------------------------------------------------------- 1 | require "heaven/comparison/default" 2 | 3 | module Heaven 4 | module Comparison 5 | # Formats a comparison between two commits 6 | class Linked < Default 7 | attr_reader :name_with_owner 8 | 9 | def initialize(comparison, name_with_owner) 10 | @comparison = comparison.with_indifferent_access 11 | @name_with_owner = name_with_owner 12 | end 13 | 14 | private 15 | 16 | def changes_header 17 | <<-CHANGES.strip_heredoc 18 | Total Commits: #{total_commits} 19 | #{file_sum(:additions)} Additions, #{file_sum(:deletions)} Deletions, #{file_sum(:changes)} Changes 20 | CHANGES 21 | end 22 | 23 | def n_more_commits_link(number) 24 | "[And #{number} more #{"commit".pluralize(number)}...](#{comparison[:html_url]})" 25 | end 26 | 27 | def formatted_commits(commits) 28 | commits.reverse.map do |commit| 29 | "#{sha_link(commit)} by #{author_link(commit[:author])}: #{commit_message(commit[:commit])}" 30 | end 31 | end 32 | 33 | def commit_message(commit) 34 | super.gsub(/#(\d+)/, "[#\\1](https://github.com/#{name_with_owner}/issues/\\1)") 35 | end 36 | 37 | def sha_link(commit) 38 | "[#{commit[:sha][0..7]}](#{commit[:html_url]})" 39 | end 40 | 41 | def author_link(author) 42 | "[#{author[:login]}](#{author[:html_url]})" 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /app/models/auto_deployment.rb: -------------------------------------------------------------------------------- 1 | # An object representing when auto-deployment should occur 2 | class AutoDeployment 3 | include ApiClient 4 | 5 | attr_accessor :commit_status, :deployment 6 | 7 | def initialize(deployment, commit_status) 8 | @commit_status = commit_status 9 | @deployment = deployment 10 | end 11 | 12 | delegate :author, :branches, :default_branch, :name_with_owner, :sha, 13 | :to => :commit_status 14 | 15 | def combined_status_green? 16 | aggregate["state"] == "success" 17 | end 18 | 19 | def aggregate 20 | @aggregate ||= api.combined_status(name_with_owner, sha) 21 | end 22 | 23 | def updated_payload 24 | deployment.auto_deploy_payload(author, sha) 25 | end 26 | 27 | def compare 28 | @compare ||= api.compare(name_with_owner, deployment.sha, sha) 29 | end 30 | 31 | def ahead? 32 | compare.ahead_by > 0 33 | end 34 | 35 | def create_deployment 36 | description = "Heaven auto deploy triggered by a commit status change" 37 | api.create_deployment(name_with_owner, sha, 38 | :payload => updated_payload, 39 | :environment => deployment.environment, 40 | :description => description 41 | ) 42 | end 43 | 44 | def execute 45 | return unless combined_status_green? 46 | if ahead? 47 | Rails.logger.info "Trying to deploy #{sha}" 48 | create_deployment 49 | else 50 | Rails.logger.info "#{sha} isn't ahead of #{deployment.sha} and in the #{default_branch}" 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 48 | 49 | 50 | 51 | 52 |
53 |

We're sorry, but something went wrong.

54 |
55 |

If you are the application owner check the logs for more information.

56 | 57 | 58 | -------------------------------------------------------------------------------- /app/models/deployment/status.rb: -------------------------------------------------------------------------------- 1 | # Top-level class for Deployments. 2 | class Deployment 3 | # A GitHub DeploymentStatus. 4 | class Status 5 | include ApiClient 6 | 7 | attr_accessor :description, :number, :nwo, :output, :completed 8 | alias_method :completed?, :completed 9 | 10 | def initialize(nwo, number) 11 | @nwo = nwo 12 | @number = number 13 | @completed = false 14 | @description = "Deploying from Heaven v#{Heaven::VERSION}" 15 | end 16 | 17 | class << self 18 | def deliveries 19 | @deliveries ||= [] 20 | end 21 | end 22 | 23 | def url 24 | "#{Octokit.api_endpoint}repos/#{nwo}/deployments/#{number}" 25 | end 26 | 27 | def payload 28 | { "target_url" => output, "description" => description } 29 | end 30 | 31 | def pending! 32 | create_status(:status => "pending", :completed => false) 33 | end 34 | 35 | def success! 36 | create_status(:status => "success") 37 | end 38 | 39 | def failure! 40 | create_status(:status => "failure") 41 | end 42 | 43 | def error! 44 | create_status(:status => "error") 45 | end 46 | 47 | private 48 | 49 | def create_status(status:, completed: true) 50 | if Heaven.testing? 51 | self.class.deliveries << payload.merge("status" => status) 52 | else 53 | api.create_deployment_status(url, status, payload) 54 | end 55 | 56 | @completed = completed 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /db/schema.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # This file is auto-generated from the current state of the database. Instead 3 | # of editing this file, please use the migrations feature of Active Record to 4 | # incrementally modify your database, and then regenerate this schema definition. 5 | # 6 | # Note that this schema.rb definition is the authoritative source for your 7 | # database schema. If you need to create the application database on another 8 | # system, you should be using db:schema:load, not running all the migrations 9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 10 | # you'll amass, the slower it'll run and the greater likelihood for issues). 11 | # 12 | # It's strongly recommended that you check this file into your version control system. 13 | 14 | ActiveRecord::Schema.define(version: 20140728040201) do 15 | 16 | create_table "deployments", force: :cascade do |t| 17 | t.text "custom_payload" 18 | t.string "environment", default: "production" 19 | t.string "guid" 20 | t.string "name" 21 | t.string "name_with_owner" 22 | t.string "output" 23 | t.string "ref" 24 | t.string "sha" 25 | t.datetime "created_at" 26 | t.datetime "updated_at" 27 | t.integer "repository_id" 28 | end 29 | 30 | create_table "repositories", force: :cascade do |t| 31 | t.string "owner" 32 | t.string "name" 33 | t.boolean "active", default: true 34 | t.datetime "created_at" 35 | t.datetime "updated_at" 36 | end 37 | 38 | end 39 | -------------------------------------------------------------------------------- /public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 48 | 49 | 50 | 51 | 52 |
53 |

The change you wanted was rejected.

54 |

Maybe you tried to change something you didn't have access to.

55 |
56 |

If you are the application owner check the logs for more information.

57 | 58 | 59 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 48 | 49 | 50 | 51 | 52 |
53 |

The page you were looking for doesn't exist.

54 |

You may have mistyped the address or the page may have moved.

55 |
56 |

If you are the application owner check the logs for more information.

57 | 58 | 59 | -------------------------------------------------------------------------------- /lib/heaven/provider/ansible.rb: -------------------------------------------------------------------------------- 1 | module Heaven 2 | # Top-level module for providers. 3 | module Provider 4 | # The capistrano provider. 5 | class Ansible < DefaultProvider 6 | def initialize(guid, payload) 7 | super 8 | @name = "ansible" 9 | end 10 | 11 | def ansible_root 12 | "#{checkout_directory}/ansible" 13 | end 14 | 15 | def execute 16 | return execute_and_log(["/usr/bin/true"]) if Rails.env.test? 17 | 18 | unless File.exist?(checkout_directory) 19 | log "Cloning #{repository_url} into #{checkout_directory}" 20 | execute_and_log(["git", "clone", clone_url, checkout_directory]) 21 | end 22 | 23 | Dir.chdir(checkout_directory) do 24 | log "Fetching the latest code" 25 | execute_and_log(%w{git fetch}) 26 | execute_and_log(["git", "reset", "--hard", sha]) 27 | 28 | ansible_hosts_file = "#{ansible_root}/hosts" 29 | ansible_site_file = "#{ansible_root}/site.yml" 30 | ansible_extra_vars = [ 31 | "heaven_deploy_sha=#{sha}", 32 | "ansible_ssh_private_key_file=#{working_directory}/.ssh/id_rsa" 33 | ].join(" ") 34 | 35 | deploy_string = ["ansible-playbook", "-i", ansible_hosts_file, ansible_site_file, 36 | "--verbose", "--extra-vars", ansible_extra_vars, "-vvvv"] 37 | log "Executing ansible: #{deploy_string.join(" ")}" 38 | execute_and_log(deploy_string, "ANSIBLE_HOST_KEY_CHECKING" => "false") 39 | end 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/heaven/provider.rb: -------------------------------------------------------------------------------- 1 | require "heaven/provider/default_provider" 2 | require "heaven/provider/capistrano" 3 | require "heaven/provider/heroku" 4 | require "heaven/provider/fabric" 5 | require "heaven/provider/elastic_beanstalk" 6 | require "heaven/provider/dpl" 7 | require "heaven/provider/bundler_capistrano" 8 | require "heaven/provider/ansible" 9 | require "heaven/provider/shell" 10 | 11 | # The top-level Heaven module 12 | module Heaven 13 | # A dispatcher for provider identification 14 | module Provider 15 | PROVIDERS ||= { 16 | "heroku" => HerokuHeavenProvider, 17 | "capistrano" => Capistrano, 18 | "fabric" => Fabric, 19 | "elastic_beanstalk" => ElasticBeanstalk, 20 | "bundler_capistrano" => BundlerCapistrano, 21 | "ansible" => Ansible, 22 | "shell" => Shell 23 | } 24 | 25 | def self.from(guid, data) 26 | klass = provider_class_for(data) 27 | klass.new(guid, data) if klass 28 | end 29 | 30 | def self.provider_class_for(data) 31 | name = provider_name_for(data) 32 | provider = PROVIDERS[name] 33 | 34 | Rails.logger.info "No deployment system for #{name}" unless provider 35 | 36 | provider 37 | end 38 | 39 | def self.provider_name_for(data) 40 | return unless data && 41 | data.key?("deployment") && 42 | data["deployment"].key?("payload") && 43 | data["deployment"]["payload"].key?("config") 44 | 45 | data["deployment"]["payload"]["config"]["provider"] 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /app/models/deployment/output.rb: -------------------------------------------------------------------------------- 1 | # Top-level class for Deployments. 2 | class Deployment 3 | # All of the process output from a deployment 4 | class Output 5 | include ApiClient 6 | attr_accessor :gist, :guid, :name, :number, :stderr, :stdout 7 | 8 | def initialize(name, number, guid) 9 | @guid = guid 10 | @name = name 11 | @number = number 12 | @stdout = "" 13 | @stderr = "" 14 | end 15 | 16 | def gist 17 | @gist ||= api.create_gist(create_params) 18 | end 19 | 20 | def create 21 | gist 22 | end 23 | 24 | def update 25 | api.edit_gist(gist.id, update_params) 26 | rescue Octokit::UnprocessableEntity 27 | Rails.logger.info "Unable to update #{gist.id}, shit's fucked up." 28 | rescue StandardError => e 29 | Rails.logger.info "Unable to update #{gist.id}, #{e.class.name} - #{e.message}." 30 | end 31 | 32 | def url 33 | gist.html_url 34 | end 35 | 36 | private 37 | 38 | def create_params 39 | { 40 | :files => { :stdout => { :content => "Deployment #{number} pending" } }, 41 | :public => false, 42 | :description => "Heaven number #{number} for #{name}" 43 | } 44 | end 45 | 46 | def update_params 47 | params = { 48 | :files => {}, 49 | :public => false 50 | } 51 | 52 | unless stderr.empty? 53 | params[:files].merge!(:stderr => { :content => stderr }) 54 | end 55 | 56 | unless stdout.empty? 57 | params[:files].merge!(:stdout => { :content => stdout }) 58 | end 59 | 60 | params 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /spec/requests/events_spec.rb: -------------------------------------------------------------------------------- 1 | require "request_spec_helper" 2 | 3 | describe "Receiving GitHub hooks", :request do 4 | include FixtureHelper 5 | 6 | before do 7 | stub_gists 8 | stub_deploy_statuses 9 | end 10 | 11 | describe "POST /events" do 12 | it "returns a forbidden error to invalid hosts" do 13 | github_event("ping") 14 | 15 | post "/events", fixture_data("ping"), request_env("74.125.239.105") 16 | 17 | expect(last_response).to be_forbidden 18 | expect(last_response.status).to eql(403) 19 | end 20 | 21 | it "returns a unprocessable error for invalid events" do 22 | github_event("invalid") 23 | 24 | post "/events", "{}", request_env 25 | 26 | expect(last_response.status).to eql(422) 27 | end 28 | 29 | it "handles ping events from valid hosts" do 30 | github_event("ping") 31 | 32 | post "/events", fixture_data("ping"), request_env 33 | 34 | expect(last_response).to be_successful 35 | expect(last_response.status).to eql(201) 36 | end 37 | 38 | it "handles deployment events from valid hosts" do 39 | github_event("deployment") 40 | 41 | post "/events", fixture_data("deployment"), request_env 42 | 43 | expect(last_response).to be_successful 44 | expect(last_response.status).to eql(201) 45 | end 46 | 47 | it "handles deployment status events from valid hosts" do 48 | github_event("deployment_status") 49 | 50 | post "/events", fixture_data("deployment-success"), request_env 51 | 52 | expect(last_response).to be_successful 53 | expect(last_response.status).to eql(201) 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Heaven::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure static asset server for tests with Cache-Control for performance. 16 | config.serve_static_files = true 17 | config.static_cache_control = "public, max-age=3600" 18 | 19 | # Show full error reports and disable caching. 20 | config.consider_all_requests_local = true 21 | config.action_controller.perform_caching = false 22 | 23 | # Raise exceptions instead of rendering exception templates. 24 | config.action_dispatch.show_exceptions = false 25 | 26 | # Disable request forgery protection in test environment. 27 | config.action_controller.allow_forgery_protection = false 28 | 29 | # Tell Action Mailer not to deliver emails to the real world. 30 | # The :test delivery method accumulates sent emails in the 31 | # ActionMailer::Base.deliveries array. 32 | config.action_mailer.delivery_method = :test 33 | 34 | # Print deprecation notices to the stderr. 35 | config.active_support.deprecation = :stderr 36 | end 37 | -------------------------------------------------------------------------------- /lib/heaven/comparison/default.rb: -------------------------------------------------------------------------------- 1 | module Heaven 2 | module Comparison 3 | # Formats a comparison between two commits 4 | class Default 5 | attr_reader :comparison 6 | 7 | def initialize(comparison) 8 | @comparison = comparison.with_indifferent_access 9 | end 10 | 11 | def changes(limit = nil) 12 | header = changes_header 13 | commits = comparison[:commits].reverse 14 | 15 | commit_list = formatted_commits(limit ? commits.take(limit) : commits) 16 | 17 | parts = [header, commit_list] 18 | 19 | if limit && limit < commits.length 20 | parts << n_more_commits_link(commits.length - limit) 21 | end 22 | 23 | parts.join("\n") 24 | end 25 | 26 | private 27 | 28 | def changes_header 29 | <<-CHANGES.strip_heredoc 30 | Total Commits: #{total_commits} 31 | #{file_sum(:additions)} Additions, #{file_sum(:deletions)} Deletions, #{file_sum(:changes)} Changes 32 | CHANGES 33 | end 34 | 35 | def n_more_commits_link(number) 36 | "And #{number} more #{"commit".pluralize(number)}... #{comparison[:html_url]}" 37 | end 38 | 39 | def total_commits 40 | comparison[:total_commits] 41 | end 42 | 43 | def file_sum(key) 44 | comparison[:files].map { |f| f[key] }.reduce(&:+) || 0 45 | end 46 | 47 | def formatted_commits(commits) 48 | commits.map do |commit| 49 | "#{commit[:sha][0..7]} by #{commit[:author][:login]}: #{commit_message(commit[:commit])}" 50 | end 51 | end 52 | 53 | def commit_message(commit) 54 | commit[:message].split("\n").first 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/lib/heaven/comparison/default_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "heaven/comparison/default" 3 | require "support/helpers/comparison_helper" 4 | 5 | describe "Heaven::Comparison::Default" do 6 | include ComparisonHelper 7 | 8 | let(:comparison) do 9 | { 10 | :html_url => "https://github.com/org/repo/compare/sha...sha", 11 | :total_commits => 1, 12 | :commits => [ 13 | build_commit_hash("Commit message #123"), 14 | build_commit_hash("Another commit") 15 | ], 16 | :files => [{ 17 | :additions => 1, 18 | :deletions => 2, 19 | :changes => 3 20 | }, { 21 | :additions => 1, 22 | :deletions => 2, 23 | :changes => 3 24 | }] 25 | }.with_indifferent_access 26 | end 27 | 28 | describe "#changes" do 29 | it "prints out a formatted list of commit changes" do 30 | formatter = Heaven::Comparison::Default.new(comparison) 31 | 32 | expect(formatter.changes).to eq( 33 | <<-CHANGES.strip_heredoc.strip 34 | Total Commits: 1 35 | 2 Additions, 4 Deletions, 6 Changes 36 | 37 | sha by login: Another commit 38 | sha by login: Commit message #123 39 | CHANGES 40 | ) 41 | end 42 | 43 | it "accepts a commit list limit" do 44 | formatter = Heaven::Comparison::Default.new(comparison) 45 | 46 | expect(formatter.changes(1)).to eq( 47 | <<-CHANGES.strip_heredoc.strip 48 | Total Commits: 1 49 | 2 Additions, 4 Deletions, 6 Changes 50 | 51 | sha by login: Another commit 52 | And 1 more commit... https://github.com/org/repo/compare/sha...sha 53 | CHANGES 54 | ) 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /app/receivers/receiver.rb: -------------------------------------------------------------------------------- 1 | # A class to handle incoming webhooks 2 | class Receiver 3 | @queue = :events 4 | 5 | attr_accessor :event, :guid, :data 6 | 7 | def initialize(event, guid, data) 8 | @guid = guid 9 | @event = event 10 | @data = data 11 | end 12 | 13 | def self.perform(event, guid, data) 14 | receiver = new(event, guid, data) 15 | 16 | if receiver.active_repository? 17 | receiver.run! 18 | else 19 | Rails.logger.info "Repository is not configured to deploy: #{receiver.full_name}" 20 | end 21 | end 22 | 23 | def full_name 24 | data["repository"] && data["repository"]["full_name"] 25 | end 26 | 27 | def active_repository? 28 | if data["repository"] 29 | name = data["repository"]["name"] 30 | owner = data["repository"]["owner"]["login"] 31 | repository = Repository.find_or_create_by(:name => name, :owner => owner) 32 | repository.active? 33 | else 34 | false 35 | end 36 | end 37 | 38 | def run_deployment! 39 | return if LockReceiver.new(data).run! 40 | 41 | if Heaven::Jobs::Deployment.locked?(guid, data) 42 | Rails.logger.info "Deployment locked for: #{Heaven::Jobs::Deployment.identifier(guid, data)}" 43 | Resque.enqueue(Heaven::Jobs::LockedError, guid, data) 44 | else 45 | Resque.enqueue(Heaven::Jobs::Deployment, guid, data) 46 | end 47 | end 48 | 49 | def run! 50 | if event == "deployment" 51 | run_deployment! 52 | elsif event == "deployment_status" 53 | Resque.enqueue(Heaven::Jobs::DeploymentStatus, data) 54 | elsif event == "status" 55 | Resque.enqueue(Heaven::Jobs::Status, guid, data) 56 | else 57 | Rails.logger.info "Unhandled event type, #{event}." 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /app/models/concerns/local_log_file.rb: -------------------------------------------------------------------------------- 1 | # A module to include for easy access to writing to a transient filesystem 2 | module LocalLogFile 3 | extend ActiveSupport::Concern 4 | include DeploymentTimeout 5 | 6 | def working_directory 7 | @working_directory ||= "/tmp/" + \ 8 | Digest::SHA1.hexdigest([name_with_owner, github_token].join) 9 | end 10 | 11 | def checkout_directory 12 | "#{working_directory}/checkout" 13 | end 14 | 15 | def stdout_file 16 | "#{working_directory}/stdout.#{guid}.log" 17 | end 18 | 19 | def stderr_file 20 | "#{working_directory}/stderr.#{guid}.log" 21 | end 22 | 23 | def log_stdout(out) 24 | File.open(stdout_file, "a") { |f| f.write(out.force_encoding("utf-8")) } 25 | end 26 | 27 | def log_stderr(err) 28 | File.open(stderr_file, "a") { |f| f.write(err.force_encoding("utf-8")) } 29 | end 30 | 31 | def execute_and_log(cmds, env = {}) 32 | # Don't add single/double quotes around to any cmd in cmds. 33 | # For example, 34 | # cmds = ["my_command", "'foo=bar lazy=true'"] will fail 35 | # The correct way is 36 | # cmds = ["my_command", "foo=bar lazy=true"] 37 | @last_child = POSIX::Spawn::Child.new(env.merge("HOME" => working_directory), *cmds, execute_options) 38 | 39 | log_stdout(last_child.out) 40 | log_stderr(last_child.err) 41 | 42 | unless last_child.success? 43 | fail StandardError, "Task failed: #{cmds.join(" ")}" 44 | end 45 | 46 | last_child 47 | end 48 | 49 | def execute_options 50 | if terminate_child_process_on_timeout 51 | { :timeout => deployment_time_remaining - 2 } 52 | else 53 | {} 54 | end 55 | end 56 | 57 | def terminate_child_process_on_timeout 58 | ENV["TERMINATE_CHILD_PROCESS_ON_TIMEOUT"] == "1" 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/heaven/provider/bundler_capistrano.rb: -------------------------------------------------------------------------------- 1 | require "heaven/provider/capistrano" 2 | 3 | module Heaven 4 | # Top-level module for providers. 5 | module Provider 6 | # A capistrano provider that installs gems. 7 | class BundlerCapistrano < Capistrano 8 | def initialize(guid, payload) 9 | super 10 | @name = "bundler_capistrano" 11 | end 12 | 13 | def execute 14 | return execute_and_log(["/usr/bin/true"]) if Rails.env.test? 15 | 16 | unless File.exist?(checkout_directory) 17 | log "Cloning #{repository_url} into #{checkout_directory}" 18 | execute_and_log(["git", "clone", clone_url, checkout_directory]) 19 | end 20 | 21 | Dir.chdir(checkout_directory) do 22 | log "Fetching the latest code" 23 | execute_and_log(%w{git fetch}) 24 | execute_and_log(["git", "reset", "--hard", sha]) 25 | Bundler.with_clean_env do 26 | bundler_string = ["bundle", "install", "--without", ignored_groups.join(" ")] 27 | log "Executing bundler: #{bundler_string.join(" ")}" 28 | execute_and_log(bundler_string) 29 | deploy_string = ["bundle", "exec", "cap", environment, task] 30 | log "Executing capistrano: #{deploy_string.join(" ")}" 31 | execute_and_log(deploy_string, "BRANCH" => ref) 32 | end 33 | end 34 | end 35 | 36 | private 37 | 38 | def ignored_groups 39 | bundle_definition.groups - [:heaven, :deployment] 40 | end 41 | 42 | def bundle_definition 43 | gemfile_path = File.expand_path("Gemfile", checkout_directory) 44 | lockfile_path = File.expand_path("Gemfile.lock", checkout_directory) 45 | Bundler::Definition.build(gemfile_path, lockfile_path, nil) 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/lib/heaven/provider/default_provider_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Heaven::Provider::DefaultProvider do 4 | 5 | let(:valid_git_ref) { Heaven::Provider::DefaultProvider::VALID_GIT_REF } 6 | 7 | describe "::VALID_GIT_REF" do 8 | it "matches master" do 9 | expect("master").to match(valid_git_ref) 10 | end 11 | it "matches dev/feature" do 12 | expect("dev/feature").to match(valid_git_ref) 13 | end 14 | it "matches short sha" do 15 | expect(SecureRandom.hex(4).first(7)).to match(valid_git_ref) 16 | end 17 | it "matches full sha" do 18 | expect(SecureRandom.hex(20)).to match(valid_git_ref) 19 | end 20 | it "matches branch with dashes and underscore" do 21 | expect("my_awesome-branch").to match(valid_git_ref) 22 | end 23 | it "matches name with single dot" do 24 | expect("some.feature").to match(valid_git_ref) 25 | end 26 | 27 | it "does not allow dot after slash" do 28 | expect("dev/.branch").not_to match(valid_git_ref) 29 | end 30 | it "does not allow space" do 31 | expect("dev branch").not_to match(valid_git_ref) 32 | end 33 | it "does not allow two consecutive dots" do 34 | expect("dev..branch").not_to match(valid_git_ref) 35 | end 36 | it "does not allow trailing /" do 37 | expect("branch/").not_to match(valid_git_ref) 38 | end 39 | it "does not allow trailing ." do 40 | expect("devbranch.").not_to match(valid_git_ref) 41 | end 42 | it "does not allow trailing .lock" do 43 | expect("devbranch.lock").not_to match(valid_git_ref) 44 | end 45 | it "does not allow @{" do 46 | expect("dev@{branch").not_to match(valid_git_ref) 47 | end 48 | it "does not allow \\" do 49 | expect("dev\\\\branch").not_to match(valid_git_ref) 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /app/models/deployment/credentials.rb: -------------------------------------------------------------------------------- 1 | # Top-level class for Deployments. 2 | class Deployment 3 | # Private: Setup ssh and netrc in a deployment specific directory 4 | # 5 | # This allows commands to be executed inside of the deployment directory with 6 | # the HOME environmental variable changed. This makes git and ssh work 7 | # without worrying about impacting other deployment processes. 8 | class Credentials 9 | include ApiClient 10 | 11 | attr_accessor :root 12 | 13 | def initialize(root) 14 | @root = root 15 | end 16 | 17 | # Public: Create ssh and netrc config files 18 | # 19 | # Returns nothing. 20 | def setup! 21 | setup_ssh 22 | setup_netrc 23 | end 24 | 25 | private 26 | 27 | def netrc_config 28 | "#{root}/.netrc" 29 | end 30 | 31 | def ssh_directory 32 | "#{root}/.ssh" 33 | end 34 | 35 | def ssh_config 36 | "#{ssh_directory}/config" 37 | end 38 | 39 | def ssh_key 40 | "#{ssh_directory}/id_rsa" 41 | end 42 | 43 | def ssh_private_key 44 | ENV["DEPLOYMENT_PRIVATE_KEY"] || "" 45 | end 46 | 47 | def setup_ssh 48 | FileUtils.mkdir_p ssh_directory 49 | FileUtils.chmod_R 0700, ssh_directory 50 | 51 | File.open(ssh_key, "w", 0600) do |fp| 52 | fp.puts(ssh_private_key.split('\n')) 53 | end 54 | 55 | File.open(ssh_config, "w", 0600) do |fp| 56 | fp.puts <<-EOF 57 | StrictHostKeyChecking no 58 | UserKnownHostsFile /dev/null 59 | ForwardAgent yes 60 | Host all 61 | Hostname * 62 | IdentityFile #{ssh_key} 63 | EOF 64 | end 65 | end 66 | 67 | def setup_netrc 68 | File.open(netrc_config, "w", 0600) do |fp| 69 | fp.puts <<-EOF 70 | machine github.com 71 | username #{github_token} 72 | password x-oauth-basic 73 | EOF 74 | end 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /spec/lib/heaven/notifier/slack_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe "Heaven::Notifier::Slack" do 4 | include FixtureHelper 5 | 6 | it "handles pending notifications" do 7 | Heaven.redis.set("atmos/my-robot-production-revision", "sha") 8 | 9 | data = decoded_fixture_data("deployment-pending") 10 | 11 | n = Heaven::Notifier::Slack.new(data) 12 | n.comparison = { 13 | "html_url" => "https://github.com/org/repo/compare/sha...sha" 14 | } 15 | 16 | result = [ 17 | "[#123456](https://gist.github.com/fa77d9fb1fe41c3bb3a3ffb2c) ", 18 | ": [atmos](https://github.com/atmos) is deploying ", 19 | "[my-robot](https://github.com/atmos/my-robot/tree/break-up-notifiers) ", 20 | "to production ([compare](https://github.com/org/repo/compare/sha...sha))" 21 | ] 22 | 23 | expect(n.default_message).to eql result.join("") 24 | end 25 | 26 | it "handles successful deployment statuses" do 27 | data = decoded_fixture_data("deployment-success") 28 | 29 | n = Heaven::Notifier::Slack.new(data) 30 | 31 | result = [ 32 | "[#11627](https://gist.github.com/fa77d9fb1fe41c3bb3a3ffb2c) ", 33 | ": [atmos](https://github.com/atmos)'s production deployment of ", 34 | "[my-robot](https://github.com/atmos/my-robot) ", 35 | "is done! " 36 | ] 37 | expect(n.default_message).to eql result.join("") 38 | end 39 | 40 | it "handles failure deployment statuses" do 41 | data = decoded_fixture_data("deployment-failure") 42 | 43 | n = Heaven::Notifier::Slack.new(data) 44 | 45 | result = [ 46 | "[#123456](https://gist.github.com/fa77d9fb1fe41c3bb3a3ffb2c) ", 47 | ": [atmos](https://github.com/atmos)'s production deployment of ", 48 | "[my-robot](https://github.com/atmos/my-robot) ", 49 | "failed. " 50 | ] 51 | expect(n.default_message).to eql result.join("") 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /config/deploy.rb: -------------------------------------------------------------------------------- 1 | # config valid only for current version of Capistrano 2 | lock '3.4.0' 3 | 4 | set :application, 'appdeploy' 5 | set :repo_url, 'https://github.com/iiif/heaven.git' 6 | 7 | # Default branch is :master 8 | # ask :branch, `git rev-parse --abbrev-ref HEAD`.chomp 9 | 10 | # Default deploy_to directory is /var/www/my_app_name 11 | set :deploy_to, '/data/appdeploy' 12 | 13 | # Default value for :scm is :git 14 | # set :scm, :git 15 | 16 | # Default value for :format is :pretty 17 | # set :format, :pretty 18 | 19 | # Default value for :log_level is :debug 20 | # set :log_level, :debug 21 | 22 | # Default value for :pty is false 23 | # set :pty, true 24 | 25 | # Default value for :linked_files is [] 26 | # set :linked_files, fetch(:linked_files, []).push('config/database.yml', 'config/secrets.yml') 27 | set :linked_files, fetch(:linked_files, []).push('db/production.sqlite3') 28 | 29 | # Default value for linked_dirs is [] 30 | set :linked_dirs, fetch(:linked_dirs, []).push('log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'vendor/bundle') 31 | 32 | # Default value for default_env is {} 33 | # set :default_env, { path: "/opt/ruby/bin:$PATH" } 34 | 35 | # Default value for keep_releases is 5 36 | set :keep_releases, 3 37 | 38 | # Restart Passenger with touch. 39 | set :passenger_restart_with_touch, true 40 | 41 | namespace :deploy do 42 | 43 | after :restart, :clear_cache do 44 | on roles(:web), in: :groups, limit: 3, wait: 10 do 45 | # Here we can do anything such as: 46 | # within release_path do 47 | # execute :rake, 'cache:clear' 48 | # end 49 | end 50 | end 51 | 52 | end 53 | 54 | role :resque_worker, "iiif.io" 55 | role :resque_scheduler, "iiif.io" 56 | set :workers, { "events" => 1, "failed" => 1, "deployments" => 1 } 57 | 58 | after 'deploy:restart', 'resque:restart' 59 | after 'deploy:reverted', 'resque:restart' 60 | after 'deploy:published', 'resque:restart' 61 | -------------------------------------------------------------------------------- /lib/heaven/provider/dpl.rb: -------------------------------------------------------------------------------- 1 | module Heaven 2 | # Top-level module for providers. 3 | module Provider 4 | # The dpl provider. 5 | class Dpl < DefaultProvider 6 | def initialize(guid, payload) 7 | super 8 | @name = "dpl" 9 | end 10 | 11 | def app_name 12 | return nil unless custom_payload_config 13 | if environment == "staging" 14 | custom_payload_config["heroku_staging_name"] 15 | else 16 | custom_payload_config["heroku_name"] 17 | end 18 | end 19 | 20 | def heroku_username 21 | ENV["HEROKU_USERNAME"] 22 | end 23 | 24 | def heroku_password 25 | ENV["HEROKU_PASSWORD"] 26 | end 27 | 28 | def heroku_api_key 29 | ENV["HEROKU_API_KEY"] 30 | end 31 | 32 | def dpl_path 33 | gem_executable_path("dpl") 34 | end 35 | 36 | def execute 37 | return execute_and_log(["/usr/bin/true"]) if Rails.env.test? 38 | 39 | unless File.exist?(checkout_directory) 40 | log "Cloning #{repository_url} into #{checkout_directory}" 41 | execute_and_log(["git", "clone", clone_url, checkout_directory]) 42 | end 43 | 44 | Dir.chdir(checkout_directory) do 45 | log "Fetching the latest code" 46 | execute_and_log(%w{git fetch}) 47 | execute_and_log(["git", "reset", "--hard", sha]) 48 | log "Pushing to heroku" 49 | deploy_string = ["#{dpl_path}", 50 | "--provider=heroku", 51 | "--strategy=git", 52 | "--api-key=#{heroku_api_key}", 53 | "--username=#{heroku_username}", 54 | "--password=#{heroku_password}", 55 | "--app=#{app_name}"] 56 | execute_and_log(deploy_string) 57 | end 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /spec/models/reciever_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Receiver do 4 | include FixtureHelper 5 | 6 | describe "#run!" do 7 | before { stub_gists } 8 | 9 | it "triggers a deployment for deployment events" do 10 | data = decoded_fixture_data("deployment") 11 | receiver = Receiver.new("deployment", "1", data) 12 | 13 | expect(Resque).to receive(:enqueue).with(Heaven::Jobs::Deployment, "1", data) 14 | 15 | receiver.run! 16 | end 17 | 18 | it "triggers a deployment status for deployment status events" do 19 | data = decoded_fixture_data("deployment-pending") 20 | receiver = Receiver.new("deployment_status", "1", data) 21 | 22 | expect(Resque).to receive(:enqueue).with(Heaven::Jobs::DeploymentStatus, data) 23 | 24 | receiver.run! 25 | end 26 | 27 | it "triggers a status for status events" do 28 | data = decoded_fixture_data("status_success") 29 | receiver = Receiver.new("status", "1", data) 30 | 31 | expect(Resque).to receive(:enqueue).with(Heaven::Jobs::Status, "1", data) 32 | 33 | receiver.run! 34 | end 35 | end 36 | 37 | describe "#active_repository?" do 38 | let(:data) { decoded_fixture_data("deployment") } 39 | let(:receiver) { Receiver.new("deployment", "1", data) } 40 | 41 | it "is true if the repository is active" do 42 | Repository.create(:name => "my-robot", :owner => "atmos", :active => true) 43 | 44 | expect(receiver).to be_active_repository 45 | end 46 | 47 | it "is false if the repository is inactive" do 48 | Repository.create(:name => "my-robot", :owner => "atmos", :active => false) 49 | 50 | expect(receiver).to_not be_active_repository 51 | end 52 | 53 | it "is false if a repository is missing from the payload" do 54 | receiver.data.delete("repository") 55 | 56 | expect(receiver).to_not be_active_repository 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/heaven/provider/shell.rb: -------------------------------------------------------------------------------- 1 | module Heaven 2 | # Top-level module for providers. 3 | module Provider 4 | # The shell provider. 5 | class Shell < DefaultProvider 6 | def initialize(guid, payload) 7 | super 8 | @name = "shell" 9 | end 10 | 11 | def execute 12 | return execute_and_log(["/usr/bin/true"]) if Rails.env.test? 13 | 14 | unless File.exist?(checkout_directory) 15 | log "Cloning #{repository_url} into #{checkout_directory}" 16 | execute_and_log(["git", "clone", clone_url, checkout_directory]) 17 | end 18 | 19 | Dir.chdir(checkout_directory) do 20 | log "Fetching the latest code" 21 | execute_and_log(%w{git fetch}) 22 | execute_and_log(["git", "reset", "--hard", sha]) 23 | Bundler.with_clean_env do 24 | log "Executing script: #{deployment_command}" 25 | execute_and_log([deployment_command], deployment_environment) 26 | end 27 | end 28 | end 29 | 30 | private 31 | 32 | def deployment_command 33 | script = custom_payload_config.try(:[], "deploy_script") 34 | fail "No deploy script configured." unless script 35 | fail "Only deploy scripts from the repo are allowed." unless script =~ /\A([\w-]+\/)*[\w-]+(\.\w+)?\Z/ 36 | fail "Deploy script #{script} not found or not executable" unless File.executable?("./" + script) 37 | "./" + script 38 | end 39 | 40 | def deployment_environment 41 | { 42 | "BRANCH" => ref, 43 | "SHA" => sha, 44 | "DEPLOY_ENV" => environment, 45 | "DEPLOY_TASK" => task 46 | } 47 | end 48 | 49 | def task 50 | name = deployment_data["task"] || "deploy" 51 | unless name =~ /deploy(?:\:[\w+:]+)?/ 52 | fail "Invalid taskname: #{name.inspect}" 53 | end 54 | name 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/support/helpers/deployment_status_helper.rb: -------------------------------------------------------------------------------- 1 | module DeploymentStatusHelper 2 | class StubDeploymentRel 3 | attr_reader :nwo, :number, :type 4 | def initialize(nwo, number, type) 5 | @nwo = nwo 6 | @number = number 7 | @type = type 8 | end 9 | 10 | def href 11 | "https://api.github.com/repos/#{nwo}/deployments/#{number}/#{type}" 12 | end 13 | end 14 | 15 | class StubDeployment 16 | attr_reader :nwo, :number 17 | def initialize(nwo, number) 18 | @nwo = nwo 19 | @number = number 20 | end 21 | 22 | def rels 23 | { :statuses => StubDeploymentRel.new(nwo, number, "statuses") } 24 | end 25 | end 26 | 27 | def deployment_url(path = "") 28 | "https://api.github.com/repos/atmos/my-robot/deployments/721#{path}" 29 | end 30 | 31 | def stub_deploy_statuses 32 | deployment_results = { 33 | :body => StubDeployment.new("atmos/my-robot", 721), 34 | :status => 200, 35 | :headers => {} 36 | } 37 | stub_request(:get, deployment_url) 38 | .to_return(deployment_results) 39 | 40 | extra_params = { 41 | "target_url" => "https://gist.github.com/cd520d99c3087f2d18b4", 42 | "description" => "Deploying from Heaven v#{Heaven::VERSION}" 43 | } 44 | 45 | stub_request(:post, deployment_url("/statuses")) 46 | .with(:body => extra_params.merge("state" => "pending").to_json) 47 | .to_return(:status => 201, :body => {}, :headers => {}) 48 | 49 | stub_request(:post, deployment_url("/statuses")) 50 | .with(:body => extra_params.merge("state" => "failure").to_json) 51 | .to_return(:status => 201, :body => {}, :headers => {}) 52 | 53 | stub_request(:post, deployment_url("/statuses")) 54 | .with(:body => extra_params.merge("state" => "success").to_json) 55 | .to_return(:status => 201, :body => {}, :headers => {}) 56 | end 57 | 58 | ::RSpec.configure do |config| 59 | config.include self 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /spec/lib/heaven/comparison/linked_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "heaven/comparison/linked" 3 | require "support/helpers/comparison_helper" 4 | 5 | describe "Heaven::Comparison::Linked" do 6 | include ComparisonHelper 7 | 8 | let(:comparison) do 9 | { 10 | :html_url => "https://github.com/org/repo/compare/sha...sha", 11 | :total_commits => 1, 12 | :commits => [ 13 | build_commit_hash("Commit message #123"), 14 | build_commit_hash("Another commit") 15 | ], 16 | :files => [{ 17 | :additions => 1, 18 | :deletions => 2, 19 | :changes => 3 20 | }, { 21 | :additions => 1, 22 | :deletions => 2, 23 | :changes => 3 24 | }] 25 | }.with_indifferent_access 26 | end 27 | 28 | describe "#changes" do 29 | it "prints out a formatted and linked list of commit changes" do 30 | formatter = Heaven::Comparison::Linked.new(comparison, "org/repo") 31 | 32 | expect(formatter.changes).to eq( 33 | <<-CHANGES.strip_heredoc.strip 34 | Total Commits: 1 35 | 2 Additions, 4 Deletions, 6 Changes 36 | 37 | [sha](https://github.com/org/repo/commit/sha) by [login](https://github.com/login): Commit message [#123](https://github.com/org/repo/issues/123) 38 | [sha](https://github.com/org/repo/commit/sha) by [login](https://github.com/login): Another commit 39 | CHANGES 40 | ) 41 | end 42 | 43 | it "accepts a commit list limit" do 44 | formatter = Heaven::Comparison::Linked.new(comparison, "org/repo") 45 | 46 | expect(formatter.changes(1)).to eq( 47 | <<-CHANGES.strip_heredoc.strip 48 | Total Commits: 1 49 | 2 Additions, 4 Deletions, 6 Changes 50 | 51 | [sha](https://github.com/org/repo/commit/sha) by [login](https://github.com/login): Another commit 52 | [And 1 more commit...](https://github.com/org/repo/compare/sha...sha) 53 | CHANGES 54 | ) 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Heaven [![Build Status](https://travis-ci.org/atmos/heaven.png?branch=master)](https://travis-ci.org/atmos/heaven) 2 | 3 | Heaven is an API that integrates with GitHub's [Deployment API][1]. It receives [deployment events][5] from GitHub and pushes code to your servers. 4 | 5 | Heaven currently supports [Capistrano][15], [Fabric][10], and [Heroku][22] deployments. It also has a notification system for broadcasting [deployment status events][6] to chat services(e.g. [Campfire][7], [Hipchat][8], [SlackHQ][9], and [Flowdock][21]). It can be hosted on Heroku for a few dollars a month. 6 | 7 | # Documentation 8 | 9 | * [Overview](/doc/overview.md) 10 | * [Installation](/doc/installation.md) 11 | * [Deployment Providers](/doc/providers.md) 12 | * [Deployment Notifications](/doc/notifications.md) 13 | * [Environment Locking](/doc/locking.md) 14 | 15 | # Launch on Heroku 16 | 17 | [![Launch on Heroku](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) 18 | 19 | [1]: http://developer.github.com/v3/repos/deployments/ 20 | [2]: https://github.com/blog/1778-webhooks-level-up 21 | [3]: https://github.com/resque/resque 22 | [4]: https://gist.github.com/ 23 | [5]: https://developer.github.com/v3/repos/deployments/#create-a-deployment 24 | [6]: https://developer.github.com/v3/repos/deployments/#create-a-deployment-status 25 | [7]: https://campfirenow.com/ 26 | [8]: https://www.hipchat.com/ 27 | [9]: https://slack.com/ 28 | [10]: http://www.fabfile.org/ 29 | [11]: http://www.getchef.com/ 30 | [12]: http://puppetlabs.com/ 31 | [13]: https://devcenter.heroku.com/articles/build-and-release-using-the-api 32 | [14]: https://developer.github.com/v3/repos/contents/#get-archive-link 33 | [15]: http://capistranorb.com/ 34 | [16]: https://github.com/settings/applications 35 | [17]: https://devcenter.heroku.com/articles/oauth#direct-authorization 36 | [18]: https://www.phusionpassenger.com/ 37 | [19]: https://devcenter.heroku.com/articles/releases 38 | [20]: https://github.com/atmos/hubot-deploy 39 | [21]: https://www.flowdock.com/ 40 | [22]: https://www.heroku.com 41 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Heaven", 3 | "logo": "https://cloud.githubusercontent.com/assets/704696/3822434/60e8b060-1d25-11e4-9baf-7fdb7f467e2b.gif", 4 | "description": "GitHub flow with capistrano, fabric, and heroku.", 5 | 6 | "keywords": [ 7 | "deployment", 8 | "capistrano", 9 | "fabric", 10 | "continuous", 11 | "hubot", 12 | "github" 13 | ], 14 | 15 | "website": "http://github.com/atmos/heaven", 16 | "repository": "https://github.com/atmos/heaven", 17 | "success_url": "/resque", 18 | "addons": ["heroku-postgresql", "heroku-redis"], 19 | "env": { 20 | "GITHUB_TOKEN": { 21 | "description": "A personal OAuth token from GitHub with repo scope." 22 | }, 23 | "GITHUB_CLIENT_ID": { 24 | "description": "A client id from a GitHub OAuth app you created." 25 | }, 26 | "GITHUB_CLIENT_SECRET": { 27 | "description": "A client secret from a GitHub OAuth app you created." 28 | }, 29 | "GITHUB_TEAM_ID": { 30 | "description": "A GitHub team id number to restrict resque access to. Optional.", 31 | "required": false 32 | }, 33 | "DEPLOYMENT_TIMEOUT": { 34 | "description": "The maximum amount of time a deployment can take in seconds.", 35 | "value": "300" 36 | }, 37 | "RAILS_ENV": { 38 | "description": "This is what the RAILS_ENV unix environmental variable is set to.", 39 | "value": "production" 40 | }, 41 | "RAILS_SECRET_KEY_BASE": { 42 | "description": "Unique token for signing cookies. This is generated.", 43 | "generator": "secret" 44 | }, 45 | "HEROKU_API_KEY": { 46 | "description": "A direct authorization token from heroku. Optional.", 47 | "required": false 48 | }, 49 | "REDIS_PROVIDER": { 50 | "description": "Redis config var to use", 51 | "value": "REDIS_URL" 52 | } 53 | }, 54 | "formation": [ 55 | { "process": "web", "quantity": 1 }, 56 | { "process": "worker", "quantity": 1 } 57 | ], 58 | "scripts": { 59 | "postdeploy": "bundle exec rake db:migrate" 60 | }, 61 | "buildpacks": [ 62 | { "url": "https://github.com/heroku/heroku-buildpack-ruby.git" }, 63 | { "url": "https://github.com/heroku/heroku-buildpack-python.git" } 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /config/deploy/production.rb: -------------------------------------------------------------------------------- 1 | # server-based syntax 2 | # ====================== 3 | # Defines a single server with a list of roles and multiple properties. 4 | # You can define all roles on a single server, or split them: 5 | 6 | # server 'example.com', user: 'deploy', roles: %w{app db web}, my_property: :my_value 7 | # server 'example.com', user: 'deploy', roles: %w{app web}, other_property: :other_value 8 | # server 'db.example.com', user: 'deploy', roles: %w{db} 9 | server 'iiif.io', user: 'iiif', roles: %w{app db web} 10 | 11 | 12 | 13 | # role-based syntax 14 | # ================== 15 | 16 | # Defines a role with one or multiple servers. The primary server in each 17 | # group is considered to be the first unless any hosts have the primary 18 | # property set. Specify the username and a domain or IP for the server. 19 | # Don't use `:all`, it's a meta role. 20 | 21 | # role :app, %w{deploy@example.com}, my_property: :my_value 22 | # role :web, %w{user1@primary.com user2@additional.com}, other_property: :other_value 23 | # role :db, %w{deploy@example.com} 24 | 25 | 26 | 27 | # Configuration 28 | # ============= 29 | # You can set any configuration variable like in config/deploy.rb 30 | # These variables are then only loaded and set in this stage. 31 | # For available Capistrano configuration variables see the documentation page. 32 | # http://capistranorb.com/documentation/getting-started/configuration/ 33 | # Feel free to add new variables to customise your setup. 34 | 35 | 36 | 37 | # Custom SSH Options 38 | # ================== 39 | # You may pass any option but keep in mind that net/ssh understands a 40 | # limited set of options, consult the Net::SSH documentation. 41 | # http://net-ssh.github.io/net-ssh/classes/Net/SSH.html#method-c-start 42 | # 43 | # Global options 44 | # -------------- 45 | # set :ssh_options, { 46 | # keys: %w(/home/rlisowski/.ssh/id_rsa), 47 | # forward_agent: false, 48 | # auth_methods: %w(password) 49 | # } 50 | # 51 | # The server-based syntax can be used to override options: 52 | # ------------------------------------ 53 | # server 'example.com', 54 | # user: 'user_name', 55 | # roles: %w{web app}, 56 | # ssh_options: { 57 | # user: 'user_name', # overrides user setting above 58 | # keys: %w(/home/user_name/.ssh/id_rsa), 59 | # forward_agent: false, 60 | # auth_methods: %w(publickey password) 61 | # # password: 'please use keys' 62 | # } 63 | -------------------------------------------------------------------------------- /lib/heaven/notifier/slack.rb: -------------------------------------------------------------------------------- 1 | require "heaven/comparison/linked" 2 | 3 | module Heaven 4 | module Notifier 5 | # A notifier for Slack 6 | class Slack < Notifier::Default 7 | def deliver(message) 8 | output_message = "" 9 | filtered_message = slack_formatted(message) 10 | 11 | Rails.logger.info "slack: #{filtered_message}" 12 | Rails.logger.info "message: #{message}" 13 | 14 | output_message << "##{deployment_number} - #{repo_name} / #{ref} / #{environment}" 15 | slack_account.ping "", 16 | :channel => "##{chat_room}", 17 | :username => slack_bot_name, 18 | :icon_url => slack_bot_icon, 19 | :attachments => [{ 20 | :text => filtered_message, 21 | :color => green? ? "good" : "danger", 22 | :pretext => pending? ? output_message : " " 23 | }] 24 | end 25 | 26 | def default_message 27 | message = output_link("##{deployment_number}") 28 | message << " : #{user_link}" 29 | case state 30 | when "success" 31 | message << "'s #{environment} deployment of #{repository_link} is done! " 32 | when "failure" 33 | message << "'s #{environment} deployment of #{repository_link} failed. " 34 | when "error" 35 | message << "'s #{environment} deployment of #{repository_link} has errors. #{ascii_face} " 36 | message << description unless description =~ /Deploying from Heaven/ 37 | when "pending" 38 | message << " is deploying #{repository_link("/tree/#{ref}")} to #{environment} #{compare_link}" 39 | else 40 | puts "Unhandled deployment state, #{state}" 41 | end 42 | end 43 | 44 | def slack_formatted(message) 45 | ::Slack::Notifier::LinkFormatter.format(message) 46 | end 47 | 48 | def changes 49 | Heaven::Comparison::Linked.new(comparison, name_with_owner).changes(commit_change_limit) 50 | end 51 | 52 | def compare_link 53 | "([compare](#{comparison["html_url"]}))" if last_known_revision 54 | end 55 | 56 | def slack_webhook_url 57 | ENV["SLACK_WEBHOOK_URL"] 58 | end 59 | 60 | def slack_bot_name 61 | ENV["SLACK_BOT_NAME"] || "hubot" 62 | end 63 | 64 | def slack_bot_icon 65 | ENV["SLACK_BOT_ICON"] || "https://octodex.github.com/images/labtocat.png" 66 | end 67 | 68 | def slack_account 69 | @slack_account ||= ::Slack::Notifier.new(slack_webhook_url) 70 | end 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | # Ruby linting configuration. 2 | # We only worry about two kinds of issues: 'error' and anything less than that. 3 | # Error is not about severity, but about taste. Simple style choices that 4 | # never have a great excuse to be broken, such as 1.9 JSON-like hash syntax, 5 | # are errors. Choices that tend to have good exceptions in practice, such as 6 | # line length, are warnings. 7 | 8 | # If you'd like to make changes, a full list of available issues is at 9 | # https://github.com/bbatsov/rubocop/blob/master/config/enabled.yml 10 | # A list of configurable issues is at: 11 | # https://github.com/bbatsov/rubocop/blob/master/config/default.yml 12 | # 13 | # If you disable a check, document why. 14 | 15 | 16 | StringLiterals: 17 | EnforcedStyle: double_quotes 18 | Severity: error 19 | 20 | HashSyntax: 21 | EnforcedStyle: hash_rockets 22 | Severity: error 23 | Exclude: 24 | - !ruby/regexp /db\/schema.rb/ 25 | 26 | AlignHash: 27 | SupportedLastArgumentHashStyles: always_ignore 28 | 29 | AlignParameters: 30 | Enabled: false # This is usually true, but we often want to roll back to 31 | # the start of a line. 32 | 33 | Attr: 34 | Enabled: false # We have no styleguide guidance here, and it seems to be 35 | # in frequent use. 36 | 37 | ClassAndModuleChildren: 38 | Enabled: false # module X<\n>module Y is just as good as module X::Y. 39 | 40 | Documentation: 41 | Exclude: 42 | - !ruby/regexp /spec\/*\/*/ 43 | 44 | ClassLength: 45 | Max: 145 46 | Exclude: 47 | - !ruby/regexp /spec\/*\/*/ 48 | 49 | PercentLiteralDelimiters: 50 | PreferredDelimiters: 51 | '%w': '{}' 52 | 53 | LineLength: 54 | Max: 158 55 | Severity: warning 56 | 57 | MultilineTernaryOperator: 58 | Severity: error 59 | 60 | UnreachableCode: 61 | Severity: error 62 | 63 | AndOr: 64 | Severity: error 65 | 66 | EndAlignment: 67 | Severity: error 68 | 69 | IndentationWidth: 70 | Severity: error 71 | 72 | MethodLength: 73 | CountComments: false # count full line comments? 74 | Max: 20 75 | Severity: error 76 | 77 | Alias: 78 | Enabled: false # We have no guidance on alias vs alias_method 79 | 80 | RedundantSelf: 81 | Enabled: false # Sometimes a self.field is a bit more clear 82 | 83 | IfUnlessModifier: 84 | Enabled: false 85 | 86 | AllCops: 87 | # Include gemspec and Rakefile 88 | Include: 89 | - '**/*.gemspec' 90 | - '**/*.rake' 91 | - '**/Gemfile' 92 | - '**/Rakefile' 93 | - '**/Capfile' 94 | - '**/Guardfile' 95 | - '**/Podfile' 96 | - '**/Vagrantfile' 97 | Exclude: 98 | - 'db/**/*' 99 | - 'bin/*' 100 | - 'script/**/*' 101 | - 'config/**/*' 102 | - 'vendor/**/*' 103 | # By default, the rails cops are not run. Override in project or home 104 | # directory .rubocop.yml files, or by giving the -R/--rails option. 105 | RunRailsCops: false 106 | -------------------------------------------------------------------------------- /doc/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | Heaven is a rails app that was designed to be hosted on heroku. 4 | 5 | ## Process Management 6 | 7 | You need redis for resque and as many workers as you think you'll need. I'd keep it at two until you start to notice queuing. 8 | 9 | $ heroku ps:scale worker=2 10 | 11 | ## Configuration 12 | 13 | Everything should have been configured via the heroku template. 14 | 15 | | Environmental Variables | | 16 | |-------------------------|-------------------------------------------------| 17 | | DATABASE_URL | A uri for to connect to a postgresql database. | 18 | | GITHUB_TOKEN | A personal access token from your [account settings][16], for API interaction. | 19 | | GITHUB_CLIENT_ID | The client id of the OAuth application. | 20 | | GITHUB_CLIENT_SECRET | The client secret of the OAuth application. | 21 | | GITHUB_TEAM_ID | A GitHub team id to restrict resque access to. | 22 | | BUILDPACK_URL | Heroku support for multiple runtimes. [Link][20] | 23 | | RAILS_SECRET_KEY_BASE | The secret key for signing session cookies. This should be unique per domain. | 24 | | OCTOKIT_API_ENDPOINT | The full url to the GitHub API for enterprise installs. Optional. e.g. https://enterprise.myorg.com/api/v3 | 25 | | OCTOKIT_WEB_ENDPOINT | The full url to the GitHub UI for enterprise installs. Optional. e.g. https://enterprise.myorg.com/ | 26 | 27 | 28 | ## Optional Configuration 29 | 30 | | Environmental Variables | | 31 | |-------------------------|-------------------------------------------------| 32 | | DEPLOYMENT_PRIVATE_KEY | An ssh private key used to login to your remote servers via SSH. Put it all on one line with `\n` in it.| 33 | | DEPLOYMENT_TIMEOUT | A timeout in seconds that the deployment should take. Deployments are aborted if they exceed this value. Defaults to 300 seconds | 34 | | HEROKU_API_KEY | A [direct authorization][17] token from heroku | 35 | | REDIS_PROVIDER | If you use a different provider than OpenRedis, set this to the name of the env var with Redis' URL (e.g. `REDISTOGO_URL`) | 36 | 37 | [1]: http://developer.github.com/v3/repos/deployments/ 38 | [2]: https://github.com/blog/1778-webhooks-level-up 39 | [3]: https://github.com/resque/resque 40 | [4]: https://gist.github.com/ 41 | [5]: https://developer.github.com/v3/repos/deployments/#create-a-deployment 42 | [6]: https://developer.github.com/v3/repos/deployments/#create-a-deployment-status 43 | [7]: https://campfirenow.com/ 44 | [8]: https://www.hipchat.com/ 45 | [9]: https://slack.com/ 46 | [10]: http://www.fabfile.org/ 47 | [11]: http://www.getchef.com/ 48 | [12]: http://puppetlabs.com/ 49 | [13]: https://devcenter.heroku.com/articles/build-and-release-using-the-api 50 | [14]: https://developer.github.com/v3/repos/contents/#get-archive-link 51 | [15]: http://capistranorb.com/ 52 | [16]: https://github.com/settings/applications 53 | [17]: https://devcenter.heroku.com/articles/oauth#direct-authorization 54 | [18]: https://www.phusionpassenger.com/ 55 | [19]: https://devcenter.heroku.com/articles/releases 56 | [20]: https://github.com/ddollar/heroku-buildpack-multi 57 | -------------------------------------------------------------------------------- /config/environments/staging.rb: -------------------------------------------------------------------------------- 1 | Heaven::Application.configure do 2 | # config.force_ssl = true 3 | # Settings specified here will take precedence over those in config/application.rb. 4 | 5 | # Code is not reloaded between requests. 6 | config.cache_classes = true 7 | 8 | # Eager load code on boot. This eager loads most of Rails and 9 | # your application in memory, allowing both thread web servers 10 | # and those relying on copy on write to perform better. 11 | # Rake tasks automatically ignore this option for performance. 12 | config.eager_load = true 13 | 14 | # Full error reports are disabled and caching is turned on. 15 | config.consider_all_requests_local = false 16 | config.action_controller.perform_caching = true 17 | 18 | # Enable Rack::Cache to put a simple HTTP cache in front of your application 19 | # Add `rack-cache` to your Gemfile before enabling this. 20 | # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid. 21 | # config.action_dispatch.rack_cache = true 22 | 23 | # Compress JavaScripts and CSS. 24 | config.assets.js_compressor = :uglifier 25 | # config.assets.css_compressor = :sass 26 | 27 | # Do not fallback to assets pipeline if a precompiled asset is missed. 28 | config.assets.compile = false 29 | 30 | # Generate digests for assets URLs. 31 | config.assets.digest = true 32 | 33 | # Version of your assets, change this if you want to expire all your assets. 34 | config.assets.version = '1.0' 35 | 36 | # Specifies the header that your server uses for sending files. 37 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache 38 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx 39 | 40 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 41 | # config.force_ssl = true 42 | 43 | # Set to :debug to see everything in the log. 44 | config.log_level = :info 45 | 46 | # Prepend all log lines with the following tags. 47 | # config.log_tags = [ :subdomain, :uuid ] 48 | 49 | # Use a different logger for distributed setups. 50 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 51 | 52 | # Use a different cache store in production. 53 | # config.cache_store = :mem_cache_store 54 | 55 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 56 | # config.action_controller.asset_host = "http://assets.example.com" 57 | 58 | # Precompile additional assets. 59 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 60 | # config.assets.precompile += %w( search.js ) 61 | 62 | # Ignore bad email addresses and do not raise email delivery errors. 63 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 64 | # config.action_mailer.raise_delivery_errors = false 65 | 66 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 67 | # the I18n.default_locale when a translation can not be found). 68 | config.i18n.fallbacks = true 69 | 70 | # Send deprecation notices to registered listeners. 71 | config.active_support.deprecation = :notify 72 | 73 | # Disable automatic flushing of the log to improve performance. 74 | # config.autoflush_log = false 75 | 76 | # Use default logging formatter so that PID and timestamp are not suppressed. 77 | config.log_formatter = ::Logger::Formatter.new 78 | end 79 | -------------------------------------------------------------------------------- /spec/services/environment_locker_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe EnvironmentLocker do 4 | let(:redis) { double(:redis) } 5 | 6 | let(:lock_params) do 7 | { 8 | :name_with_owner => "atmos/heaven", 9 | :environment => "production", 10 | :actor => "atmos" 11 | } 12 | end 13 | 14 | describe "#lock?" do 15 | it "is true if the task is deploy:lock" do 16 | locker = EnvironmentLocker.new(lock_params.merge(:task => "deploy:lock")) 17 | locker.redis = redis 18 | 19 | expect(locker.lock?).to be_true 20 | end 21 | 22 | context "with hubot deploy prefix" do 23 | before { stub_const("ENV", "HUBOT_DEPLOY_PREFIX" => "ship") } 24 | 25 | it "is true if the task is ship:lock" do 26 | locker = EnvironmentLocker.new(lock_params.merge(:task => "ship:lock")) 27 | expect(locker.lock?).to be_true 28 | end 29 | end 30 | end 31 | 32 | describe "#unlock?" do 33 | it "is true if the task is deploy:unlock" do 34 | locker = EnvironmentLocker.new(lock_params.merge(:task => "deploy:unlock")) 35 | locker.redis = redis 36 | 37 | expect(locker.unlock?).to be_true 38 | end 39 | 40 | context "with hubot deploy prefix" do 41 | before { stub_const("ENV", "HUBOT_DEPLOY_PREFIX" => "ship") } 42 | 43 | it "is true if the task is ship:unlock" do 44 | locker = EnvironmentLocker.new(lock_params.merge(:task => "ship:unlock")) 45 | expect(locker.unlock?).to be_true 46 | end 47 | end 48 | end 49 | 50 | describe "#lock!" do 51 | it "locks the environment for the repo and records the locker" do 52 | locker = EnvironmentLocker.new(lock_params) 53 | locker.redis = redis 54 | 55 | expect(locker.actor).to eq("atmos") 56 | 57 | expect(redis).to receive(:set).with("atmos/heaven-production-lock", "atmos") 58 | 59 | locker.lock! 60 | 61 | expect(redis).to receive(:get).with("atmos/heaven-production-lock").and_return("atmos") 62 | 63 | expect(locker.locked_by).to eq("atmos") 64 | end 65 | end 66 | 67 | describe "#unlock!" do 68 | it "unlocks the environment for the repo" do 69 | locker = EnvironmentLocker.new(lock_params) 70 | locker.redis = redis 71 | 72 | expect(redis).to receive(:del).with("atmos/heaven-production-lock") 73 | 74 | locker.unlock! 75 | end 76 | end 77 | 78 | describe "#locked?" do 79 | let(:locker) do 80 | EnvironmentLocker.new(lock_params).tap do |locker| 81 | locker.redis = redis 82 | end 83 | end 84 | 85 | it "is true if the repo/environment pair exists" do 86 | expect(redis).to receive(:exists).with("atmos/heaven-production-lock").and_return(true) 87 | 88 | expect(locker.locked?).to be_true 89 | end 90 | 91 | it "is false if the repo/environment pair exists" do 92 | expect(redis).to receive(:exists).with("atmos/heaven-production-lock").and_return(false) 93 | 94 | expect(locker.locked?).to be_false 95 | end 96 | end 97 | 98 | describe "#locked_by" do 99 | it "returns the user who locked the environment" do 100 | locker = EnvironmentLocker.new(lock_params) 101 | locker.redis = redis 102 | 103 | expect(redis).to receive(:get).with("atmos/heaven-production-lock").and_return("atmos") 104 | 105 | expect(locker.locked_by).to eq("atmos") 106 | end 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Heaven::Application.configure do 2 | # config.force_ssl = true 3 | config.force_ssl = false 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # Code is not reloaded between requests. 7 | config.cache_classes = true 8 | 9 | # Eager load code on boot. This eager loads most of Rails and 10 | # your application in memory, allowing both thread web servers 11 | # and those relying on copy on write to perform better. 12 | # Rake tasks automatically ignore this option for performance. 13 | config.eager_load = true 14 | 15 | # Full error reports are disabled and caching is turned on. 16 | config.consider_all_requests_local = false 17 | config.action_controller.perform_caching = true 18 | 19 | # Enable Rack::Cache to put a simple HTTP cache in front of your application 20 | # Add `rack-cache` to your Gemfile before enabling this. 21 | # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid. 22 | # config.action_dispatch.rack_cache = true 23 | 24 | # Disable Rails's static asset server (Apache or nginx will already do this). 25 | config.serve_static_files = false 26 | 27 | # Compress JavaScripts and CSS. 28 | config.assets.js_compressor = :uglifier 29 | # config.assets.css_compressor = :sass 30 | 31 | # Do not fallback to assets pipeline if a precompiled asset is missed. 32 | config.assets.compile = false 33 | 34 | # Generate digests for assets URLs. 35 | config.assets.digest = true 36 | 37 | # Version of your assets, change this if you want to expire all your assets. 38 | config.assets.version = '1.0' 39 | 40 | # Specifies the header that your server uses for sending files. 41 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache 42 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx 43 | 44 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 45 | # config.force_ssl = true 46 | 47 | # Set to :debug to see everything in the log. 48 | config.log_level = :info 49 | 50 | # Prepend all log lines with the following tags. 51 | # config.log_tags = [ :subdomain, :uuid ] 52 | 53 | # Use a different logger for distributed setups. 54 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 55 | 56 | # Use a different cache store in production. 57 | # config.cache_store = :mem_cache_store 58 | 59 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 60 | # config.action_controller.asset_host = "http://assets.example.com" 61 | 62 | # Precompile additional assets. 63 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 64 | # config.assets.precompile += %w( search.js ) 65 | 66 | # Ignore bad email addresses and do not raise email delivery errors. 67 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 68 | # config.action_mailer.raise_delivery_errors = false 69 | 70 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 71 | # the I18n.default_locale when a translation can not be found). 72 | config.i18n.fallbacks = true 73 | 74 | # Send deprecation notices to registered listeners. 75 | config.active_support.deprecation = :notify 76 | 77 | # Disable automatic flushing of the log to improve performance. 78 | # config.autoflush_log = false 79 | 80 | # Use default logging formatter so that PID and timestamp are not suppressed. 81 | config.log_formatter = ::Logger::Formatter.new 82 | end 83 | -------------------------------------------------------------------------------- /doc/overview.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | Heaven is a rails app that receives [Deployment][1] events from GitHub and deploys your code. It works best with a [hubot](https://hubot.github.com), and give you a [chat-ops][20] style workflow. It receives [GitHub webhooks][2] and runs deployment jobs as background tasks with [resque][3]. Heaven captures the standard input and output streams and posts the results to a [gist][4]. 4 | 5 | ``` 6 | +---------+ +--------+ +----------+ +-------------+ 7 | | Hubot | | GitHub | | Heaven | | Your Server | 8 | +---------+ +--------+ +----------+ +-------------+ 9 | | | | | 10 | | Create Deployment | | | 11 | |--------------------->| | | 12 | | | | | 13 | | Deployment Created | | | 14 | |<---------------------| | | 15 | | | | | 16 | | | Deployment Event | | 17 | | |---------------------->| | 18 | | | | SSH+Deploys | 19 | | | |-------------------->| 20 | | | | | 21 | | | Deployment Status | | 22 | | |<----------------------| | 23 | | | | | 24 | | | | Deploy Completed | 25 | | | |<--------------------| 26 | | | | | 27 | | | Deployment Status | | 28 | | |<----------------------| | 29 | | | | | 30 | ``` 31 | 32 | Heaven aims to give you [GitHub Flow](https://guides.github.com/introduction/flow/index.html). It allows you to deploy branches easily and always roll back to master. 33 | 34 | The goal of heaven is to be one of many deployment systems that you can use with the Deployments API. It's a small app that receives [webhooks][2] for specific repositories. You can set up as many systems as you need to handle your web, mobile, native, compiled, and docker images while keeping the tooling the same through GitHub. You can also start evaluating new systems on a per-repo basis without introducing widespread breakage across a deployment systems userbase. 35 | 36 | If you deploy from chat then this all starts to feel pretty natural. 37 | 38 | ![](https://f.cloud.github.com/assets/38/2330090/208fce50-a42a-11e3-94e6-46beaac78bfb.jpg) 39 | 40 | 41 | [1]: http://developer.github.com/v3/repos/deployments/ 42 | [2]: https://github.com/blog/1778-webhooks-level-up 43 | [3]: https://github.com/resque/resque 44 | [4]: https://gist.github.com/ 45 | [5]: https://developer.github.com/v3/repos/deployments/#create-a-deployment 46 | [6]: https://developer.github.com/v3/repos/deployments/#create-a-deployment-status 47 | [7]: https://campfirenow.com/ 48 | [8]: https://www.hipchat.com/ 49 | [9]: https://slack.com/ 50 | [10]: http://www.fabfile.org/ 51 | [11]: http://www.getchef.com/ 52 | [12]: http://puppetlabs.com/ 53 | [13]: https://devcenter.heroku.com/articles/build-and-release-using-the-api 54 | [14]: https://developer.github.com/v3/repos/contents/#get-archive-link 55 | [15]: http://capistranorb.com/ 56 | [16]: https://github.com/settings/applications 57 | [17]: https://devcenter.heroku.com/articles/oauth#direct-authorization 58 | [18]: https://www.phusionpassenger.com/ 59 | [19]: https://devcenter.heroku.com/articles/releases 60 | [20]: https://github.com/atmos/hubot-deploy 61 | -------------------------------------------------------------------------------- /lib/heaven/provider/elastic_beanstalk.rb: -------------------------------------------------------------------------------- 1 | module Heaven 2 | # Top-level module for Providers. 3 | module Provider 4 | # The Amazon elastic beanstalk provider. 5 | class ElasticBeanstalk < DefaultProvider 6 | def initialize(guid, payload) 7 | super 8 | @name = "elastic_beanstalk" 9 | end 10 | 11 | def archive_name 12 | "#{name}-#{sha}.zip" 13 | end 14 | 15 | def archive_link 16 | @archive_link ||= api.archive_link(name_with_owner, :ref => sha) 17 | end 18 | 19 | def archive_zip 20 | archive_link.gsub(/legacy\.tar\.gz/, "deploy.zip") 21 | end 22 | 23 | def archive_path 24 | @archive_path ||= "#{working_directory}/#{archive_name}" 25 | end 26 | 27 | def fetch_source_code 28 | execute_and_log(["curl", archive_zip, "-o", archive_path]) 29 | end 30 | 31 | def execute 32 | return execute_and_log(["/usr/bin/true"]) if Rails.env.test? 33 | 34 | configure_s3_bucket 35 | log_stdout "Beanstalk: Fetching source code from GitHub:\n" 36 | fetch_source_code 37 | log_stdout "Beanstalk: Uploading source code: #{archive_path}\n" 38 | upload = upload_source_code(archive_name, archive_path) 39 | log_stdout "Beanstalk: Creating application: #{app_name}\n" 40 | app_version = create_app_version(upload.key) 41 | log_stdout "Beanstalk: Updating application: #{app_name}-#{environment}.\n" 42 | app_update = update_app(app_version) 43 | status.output = "#{base_url}?region=#{custom_aws_region}#/environment" 44 | status.output << "/dashboard?applicationName=#{app_name}&environmentId" 45 | status.output << "=#{app_update[:environment_id]}" 46 | end 47 | 48 | def base_url 49 | "https://console.aws.amazon.com/elasticbeanstalk/home" 50 | end 51 | 52 | def notify 53 | update_output 54 | 55 | status.success! 56 | end 57 | 58 | def upload_source_code(key, file) 59 | obj = s3.buckets[bucket_name].objects[key] 60 | obj.write(Pathname.new(file)) 61 | obj 62 | end 63 | 64 | def bucket_name 65 | ENV["BEANSTALK_S3_BUCKET"] || 66 | "heaven-elasticbeanstalk-builds-#{custom_aws_region}" 67 | end 68 | 69 | private 70 | 71 | def app_name 72 | custom_payload_config && custom_payload_config["app_name"] 73 | end 74 | 75 | def configure_s3_bucket 76 | return if s3.buckets.map(&:name).include?(bucket_name) 77 | s3.buckets.create(bucket_name) 78 | end 79 | 80 | def create_app_version(s3_key) 81 | options = { 82 | :application_name => app_name, 83 | :version_label => version_label, 84 | :description => description, 85 | :source_bundle => { 86 | :s3_key => s3_key, 87 | :s3_bucket => bucket_name 88 | }, 89 | :auto_create_application => false 90 | } 91 | eb.create_application_version(options) 92 | end 93 | 94 | def update_app(version) 95 | options = { 96 | :environment_name => environment, 97 | :version_label => version[:application_version][:version_label] 98 | } 99 | eb.update_environment(options) 100 | end 101 | 102 | def version_label 103 | "heaven-#{sha}-#{Time.now.to_i}" 104 | end 105 | 106 | def custom_aws_region 107 | (custom_payload && 108 | custom_payload["aws"] && 109 | custom_payload["aws"]["region"]) || "us-east-1" 110 | end 111 | 112 | def aws_config 113 | { 114 | "region" => custom_aws_region, 115 | "logger" => Logger.new(stdout_file), 116 | "access_key_id" => ENV["BEANSTALK_ACCESS_KEY_ID"], 117 | "secret_access_key" => ENV["BEANSTALK_SECRET_ACCESS_KEY"] 118 | } 119 | end 120 | 121 | def s3 122 | @s3 ||= AWS::S3.new(aws_config) 123 | end 124 | 125 | def eb 126 | @eb ||= AWS::ElasticBeanstalk::Client.new(aws_config) 127 | end 128 | end 129 | end 130 | end 131 | -------------------------------------------------------------------------------- /lib/heaven/provider/heroku.rb: -------------------------------------------------------------------------------- 1 | module Heaven 2 | # Top-level module for providers. 3 | module Provider 4 | # A heroku API client. 5 | module HerokuApiClient 6 | def http_options 7 | { 8 | :url => "https://api.heroku.com", 9 | :headers => { 10 | "Accept" => "application/vnd.heroku+json; version=3", 11 | "Content-Type" => "application/json", 12 | "Authorization" => Base64.encode64(":#{ENV["HEROKU_API_KEY"]}") 13 | } 14 | } 15 | end 16 | 17 | def http 18 | @http ||= Faraday.new(http_options) do |faraday| 19 | faraday.request :url_encoded 20 | faraday.adapter Faraday.default_adapter 21 | faraday.response :logger unless %w{staging production}.include?(Rails.env) 22 | end 23 | end 24 | end 25 | 26 | # A heroku build object. 27 | class HerokuBuild 28 | include HerokuApiClient 29 | 30 | attr_accessor :id, :info, :name 31 | def initialize(name, id) 32 | @id = id 33 | @name = name 34 | @info = info! 35 | end 36 | 37 | def info! 38 | response = http.get do |req| 39 | req.url "/apps/#{name}/builds/#{id}" 40 | end 41 | Rails.logger.info "#{response.status} response for Heroku build info for #{id}" 42 | @info = JSON.parse(response.body) 43 | end 44 | 45 | def output 46 | response = http.get do |req| 47 | req.url "/apps/#{name}/builds/#{id}/result" 48 | end 49 | Rails.logger.info "#{response.status} response for Heroku build output for #{id}" 50 | @output = JSON.parse(response.body) 51 | end 52 | 53 | def lines 54 | @lines ||= output["lines"] 55 | end 56 | 57 | def stdout 58 | lines.map do |line| 59 | line["line"] if line["stream"] == "STDOUT" 60 | end.join 61 | end 62 | 63 | def stderr 64 | lines.map do |line| 65 | line["line"] if line["stream"] == "STDERR" 66 | end.join 67 | end 68 | 69 | def refresh! 70 | Rails.logger.info "Refreshing build #{id}" 71 | info! 72 | end 73 | 74 | def completed? 75 | success? || failed? 76 | end 77 | 78 | def success? 79 | info["status"] == "succeeded" 80 | end 81 | 82 | def failed? 83 | info["status"] == "failed" 84 | end 85 | end 86 | 87 | # The heroku provider. 88 | class HerokuHeavenProvider < DefaultProvider 89 | include HerokuApiClient 90 | 91 | attr_accessor :build 92 | def initialize(guid, payload) 93 | super 94 | @name = "heroku" 95 | end 96 | 97 | def app_name 98 | return nil unless custom_payload_config 99 | 100 | app_key = "heroku_#{environment}_name" 101 | if custom_payload_config.key?(app_key) 102 | custom_payload_config[app_key] 103 | else 104 | puts "Specify a There is no heroku specific app #{app_key} for the environment #{environment}" 105 | custom_payload_config["heroku_name"] # default app name 106 | end 107 | end 108 | 109 | def archive_link 110 | @archive_link ||= api.archive_link(name_with_owner, :ref => sha) 111 | end 112 | 113 | def execute 114 | response = build_request 115 | return unless response.success? 116 | body = JSON.parse(response.body) 117 | @build = HerokuBuild.new(app_name, body["id"]) 118 | 119 | until build.completed? 120 | sleep 10 121 | build.refresh! 122 | end 123 | end 124 | 125 | def notify 126 | if build 127 | output.stderr = build.stderr 128 | output.stdout = build.stdout 129 | else 130 | output.stderr = "Unable to create a build" 131 | end 132 | 133 | output.update 134 | if build && build.success? 135 | status.success! 136 | else 137 | status.failure! 138 | end 139 | end 140 | 141 | private 142 | 143 | def build_request 144 | http.post do |req| 145 | req.url "/apps/#{app_name}/builds" 146 | body = { 147 | :source_blob => { 148 | :url => archive_link, 149 | :version => sha 150 | } 151 | } 152 | req.body = JSON.dump(body) 153 | end 154 | end 155 | end 156 | end 157 | end 158 | -------------------------------------------------------------------------------- /doc/notifications.md: -------------------------------------------------------------------------------- 1 | # Chat Notifications 2 | 3 | Heaven looks for information in the environments and tries to give feedback to chat rooms to everyone knows that code has been deployed. Right now it supports three networks. 4 | 5 | All notifications run inside of [resque][3] jobs and need to define a deliver method. 6 | 7 | ### Example Provider 8 | 9 | ```ruby 10 | module Heaven 11 | module Notifier 12 | class MyChat < Default 13 | def deliver(message) 14 | end 15 | end 16 | end 17 | end 18 | ``` 19 | 20 | ## SlackHQ 21 | 22 | ### Configuration 23 | 24 | | Environmental Variables | | 25 | |-------------------------|-------------------------------------------------| 26 | | SLACK_WEBHOOK_URL | A Slack Webhook URL from [incoming-webhook][21] section of Slack| 27 | | SLACK_BOT_NAME | The name to post to Slack as. Defaults to `hubot`| 28 | | SLACK_BOT_ICON | The icon to use for the notification. Defaults to `https://octodex.github.com/images/labtocat.png`| 29 | 30 | ## Campfire 31 | 32 | ### Configuration 33 | 34 | | Environmental Variables | | 35 | |-------------------------|-------------------------------------------------| 36 | | CAMPFIRE_TOKEN | A campfire API token from the 'my info' section of [campfire][7]. | 37 | | CAMPFIRE_SUBDOMAIN | This subdomain for the campfire. For example https://atmos.campfirenow.com would be 'atmos' | 38 | 39 | ## HipChat 40 | 41 | ### Configuration 42 | 43 | | Environmental Variables | | 44 | |-------------------------|-------------------------------------------------| 45 | | HIPCHAT_TOKEN | The notification token to send messages to hipchat. | 46 | | HIPCHAT_ROOM | The room to post deployment messages to. | 47 | 48 | ## Flowdock 49 | 50 | ### Configuration 51 | 52 | | Environmental Variables | | 53 | |-------------------------|-------------------------------------------------| 54 | | FLOWDOCK_USER_API_TOKEN | A user's api token from Flowdock [account page][22]. This is used to access the api and post some messages. | 55 | | FLOWDOCK_FLOW_TOKENS | A JSON string that has flow ids and [flow source tokens][23] as key-value pairs. You need to generate the source tokens using OAuth api. Note that these are part of the new [Flowdock threads api][24] and thus the old flow tokens will not work. | 56 | | FLOWDOCK_USER_NAME | (Optional) A name that is displayed as the author for deployment activities. Defaults to `Heaven` | 57 | | FLOWDOCK_USER_EMAIL | (Optional) An email address for the above user. Defaults to dummy `build@flowdock.com`. | 58 | | FLOWDOCK_USER_AVATAR | (Optional) A url to an image that is shown for the above user as an avatar. By default a success and failure icon are used depending on the build status. | 59 | 60 | ## Commit Change Notifications 61 | 62 | Successful deployments to any environment are always compared with the last 63 | successful deployment to the production environment. 64 | 65 | | Environmental Variables | | 66 | | ----------------------- | -------------------------------------------------------------------------------- | 67 | | HEAVEN_NOTIFIER_DISPLAY_COMMITS | Add to send a list of commit changes after a succesful deployment. | 68 | | HEAVEN_NOTIFIER_DISPLAY_COMMITS_LIMIT | Limit the number of commits listed in the notification. | 69 | 70 | [1]: http://developer.github.com/v3/repos/deployments/ 71 | [2]: https://github.com/blog/1778-webhooks-level-up 72 | [3]: https://github.com/resque/resque 73 | [4]: https://gist.github.com/ 74 | [5]: https://developer.github.com/v3/repos/deployments/#create-a-deployment 75 | [6]: https://developer.github.com/v3/repos/deployments/#create-a-deployment-status 76 | [7]: https://campfirenow.com/ 77 | [8]: https://www.hipchat.com/ 78 | [9]: https://slack.com/ 79 | [10]: http://www.fabfile.org/ 80 | [11]: http://www.getchef.com/ 81 | [12]: http://puppetlabs.com/ 82 | [13]: https://devcenter.heroku.com/articles/build-and-release-using-the-api 83 | [14]: https://developer.github.com/v3/repos/contents/#get-archive-link 84 | [15]: http://capistranorb.com/ 85 | [16]: https://github.com/settings/applications 86 | [17]: https://devcenter.heroku.com/articles/oauth#direct-authorization 87 | [18]: https://www.phusionpassenger.com/ 88 | [19]: https://devcenter.heroku.com/articles/releases 89 | [20]: https://github.com/atmos/hubot-deploy 90 | [21]: https://my.slack.com/services/new/incoming-webhook 91 | [22]: https://www.flowdock.com/account/tokens 92 | [23]: https://gist.github.com/Mumakil/1d184a3f06bcd087c5e2 93 | [24]: https://www.flowdock.com/api/how-to-integrate 94 | -------------------------------------------------------------------------------- /lib/heaven/notifier/default.rb: -------------------------------------------------------------------------------- 1 | require "heaven/comparison/default" 2 | 3 | module Heaven 4 | module Notifier 5 | # The class that all notifiers inherit from 6 | class Default 7 | DISPLAY_COMMITS_KEY = "HEAVEN_NOTIFIER_DISPLAY_COMMITS" 8 | DISPLAY_COMMITS_LIMIT_KEY = "HEAVEN_NOTIFIER_DISPLAY_COMMITS_LIMIT" 9 | 10 | include ApiClient 11 | 12 | attr_accessor :data 13 | attr_writer :comparison 14 | 15 | def initialize(data) 16 | @data = data 17 | end 18 | 19 | def deliver(message) 20 | fail "Unable to deliver, write your own #deliver(#{message}) method." 21 | end 22 | 23 | def ascii_face 24 | case state 25 | when "pending" then "•̀.̫•́✧" 26 | when "success" then "(◕‿◕)" 27 | when "failure" then "ಠﭛಠ" 28 | when "error" then "¯_(ツ)_/¯" 29 | else 30 | "٩◔̯◔۶" 31 | end 32 | end 33 | 34 | def pending? 35 | state == "pending" 36 | end 37 | 38 | def success? 39 | state == "success" 40 | end 41 | 42 | def deploy? 43 | task == "deploy" 44 | end 45 | 46 | def change_delivery_enabled? 47 | ENV.key?(DISPLAY_COMMITS_KEY) 48 | end 49 | 50 | def green? 51 | %w{pending success}.include?(state) 52 | end 53 | 54 | def deployment_status_data 55 | data["deployment_status"] || data 56 | end 57 | 58 | def state 59 | deployment_status_data["state"] 60 | end 61 | 62 | def number 63 | deployment_status_data["id"] 64 | end 65 | 66 | def target_url 67 | deployment_status_data["target_url"] 68 | end 69 | 70 | def description 71 | deployment_status_data["description"] 72 | end 73 | 74 | def deployment 75 | data["deployment"] 76 | end 77 | 78 | def environment 79 | deployment["environment"] 80 | end 81 | 82 | def task 83 | deployment["task"] 84 | end 85 | 86 | def sha 87 | deployment["sha"][0..7] 88 | end 89 | 90 | def ref 91 | deployment["ref"] 92 | end 93 | 94 | def deployment_number 95 | deployment["id"] 96 | end 97 | 98 | def deployment_payload 99 | @deployment_payload ||= deployment["payload"] 100 | end 101 | 102 | def chat_user 103 | deployment_payload["notify"]["user"] || "unknown" 104 | end 105 | 106 | def chat_room 107 | deployment_payload["notify"]["room"] 108 | end 109 | 110 | def repo_name 111 | deployment_payload["name"] || data["repository"]["name"] 112 | end 113 | 114 | def name_with_owner 115 | data["repository"]["full_name"] 116 | end 117 | 118 | def repo_url(path = "") 119 | data["repository"]["html_url"] + path 120 | end 121 | 122 | def repository_link(path = "") 123 | "[#{repo_name}](#{repo_url(path)})" 124 | end 125 | 126 | def octokit_web_endpoint 127 | ENV["OCTOKIT_WEB_ENDPOINT"] || "https://github.com/" 128 | end 129 | 130 | def default_message 131 | message = user_link 132 | case state 133 | when "success" 134 | message << "'s #{environment} deployment of #{repository_link} is done! " 135 | when "failure" 136 | message << "'s #{environment} deployment of #{repository_link} failed. " 137 | when "error" 138 | message << "'s #{environment} deployment of #{repository_link} has errors. " 139 | when "pending" 140 | message << " is deploying #{repository_link("/tree/#{ref}")} to #{environment}" 141 | else 142 | puts "Unhandled deployment state, #{state}" 143 | end 144 | end 145 | 146 | def changes 147 | Heaven::Comparison::Default.new(comparison).changes(commit_change_limit) 148 | end 149 | 150 | def commit_change_limit 151 | ENV.key?(DISPLAY_COMMITS_LIMIT_KEY) ? ENV[DISPLAY_COMMITS_LIMIT_KEY].to_i : nil 152 | end 153 | 154 | def comparison 155 | @comparison ||= api.compare(name_with_owner, last_known_revision, sha).as_json 156 | end 157 | 158 | def last_known_revision 159 | Heaven.redis.get("#{name_with_owner}-#{environment}-revision") 160 | end 161 | 162 | def record_revision 163 | Heaven.redis.set("#{name_with_owner}-#{environment}-revision", sha) 164 | end 165 | 166 | def post! 167 | deliver(default_message) 168 | 169 | return unless success? && deploy? 170 | 171 | deliver(changes) if deliver_changes? 172 | 173 | record_revision 174 | end 175 | 176 | def deliver_changes? 177 | change_delivery_enabled? && last_known_revision.present? 178 | end 179 | 180 | def user_link 181 | "[#{chat_user}](#{octokit_web_endpoint}#{chat_user})" 182 | end 183 | 184 | def output_link(link_title = "deployment") 185 | if target_url 186 | "[#{link_title}](#{target_url})" 187 | else 188 | link_title 189 | end 190 | end 191 | end 192 | end 193 | end 194 | -------------------------------------------------------------------------------- /lib/heaven/provider/default_provider.rb: -------------------------------------------------------------------------------- 1 | module Heaven 2 | # Top-level module for providers. 3 | module Provider 4 | # The super class provider, all providers inherit from this. 5 | class DefaultProvider 6 | include ApiClient 7 | include DeploymentTimeout 8 | include LocalLogFile 9 | 10 | attr_accessor :credentials, :guid, :last_child, :name, :data 11 | 12 | # See http://stackoverflow.com/questions/12093748/how-do-i-check-for-valid-git-branch-names 13 | # and http://linux.die.net/man/1/git-check-ref-format 14 | VALID_GIT_REF = %r{\A(?!/)(?!.*(?:/\.|//|@\{|\\|\.\.))[\040-\176&&[^ ~\^:?*\[]]+(? JSON.dump(custom_payload), 133 | :environment => environment, 134 | :guid => guid, 135 | :name => name, 136 | :name_with_owner => name_with_owner, 137 | :output => output.url, 138 | :ref => ref, 139 | :sha => sha) 140 | end 141 | 142 | def update_output 143 | output.stderr = File.read(stderr_file) if File.exist?(stderr_file) 144 | output.stdout = File.read(stdout_file) if File.exist?(stdout_file) 145 | 146 | output.update 147 | end 148 | 149 | def notify 150 | update_output 151 | 152 | last_child.success? ? status.success! : status.failure! 153 | end 154 | 155 | def run! 156 | Timeout.timeout(timeout) do 157 | start_deployment_timeout! 158 | setup 159 | execute unless Rails.env.test? 160 | notify 161 | record 162 | end 163 | rescue POSIX::Spawn::TimeoutExceeded, Timeout::Error => e 164 | Rails.logger.info e.message 165 | Rails.logger.info e.backtrace 166 | output.stderr += "\n\nDEPLOYMENT TIMED OUT AFTER #{timeout} SECONDS" 167 | rescue StandardError => e 168 | Rails.logger.info e.message 169 | Rails.logger.info e.backtrace 170 | ensure 171 | update_output 172 | status.failure! unless completed? 173 | end 174 | end 175 | end 176 | end 177 | -------------------------------------------------------------------------------- /lib/heaven/notifier/flowdock.rb: -------------------------------------------------------------------------------- 1 | require "heaven/notifier/flowdock/api" 2 | require "heaven/notifier/flowdock/message_helper" 3 | 4 | module Heaven 5 | module Notifier 6 | # A notifier for flowdock 7 | class Flowdock < Notifier::Default 8 | include ApiClient 9 | include FlowdockApi 10 | include FlowdockMessageHelper 11 | 12 | def deliver(message) 13 | Rails.logger.info "flowdock: #{message}" 14 | if flow_token.nil? 15 | Rails.logger.error "Could not find flow token for flow #{chat_room}" 16 | return 17 | end 18 | response = thread_client.post "/messages", 19 | :flow_token => flow_token, 20 | :event => "activity", 21 | :external_thread_id => flowdock_thread_id, 22 | :thread => thread_data, 23 | :title => activity_title, 24 | :author => activity_author, 25 | :tags => tags 26 | return if state != "pending" || autodeploy? 27 | answer_to_chat(response.body["thread_id"]) 28 | end 29 | 30 | def answer_to_chat(deployment_thread_id) 31 | flow = auth_client.get("/flows/find", :id => chat_room) 32 | params = { 33 | :content => "Deployment started: #{thread_url(flow, deployment_thread_id)}" 34 | } 35 | if !thread_id.blank? 36 | params[:thread_id] = thread_id 37 | elsif !message_id.blank? 38 | params[:message_id] = message_id 39 | end 40 | params[:flow] = chat_room 41 | auth_client.chat_message(params) 42 | end 43 | 44 | def thread_id 45 | deployment_payload["notify"]["thread_id"] 46 | end 47 | 48 | def message_id 49 | deployment_payload["notify"]["message_id"] 50 | end 51 | 52 | def repo_default_branch 53 | data["repository"]["default_branch"] 54 | end 55 | 56 | def autodeploy? 57 | deployment["description"].start_with?("Auto-Deployed") 58 | end 59 | 60 | def push_api_content 61 | "

#{deployment["description"]}

" 62 | end 63 | 64 | def thread_data 65 | data = { 66 | :title => "Deployment ##{deployment_number} of #{repo_name} to #{environment}", 67 | :body => "

#{deployment["description"]}

", 68 | :external_url => target_url, 69 | :status => { 70 | :value => state, 71 | :color => thread_status_color 72 | }, 73 | :fields => thread_fields 74 | } 75 | data 76 | end 77 | 78 | def thread_fields 79 | [ 80 | { :label => "Repository", :value => "#{data["repository"]["full_name"]}" }, 81 | { :label => "Deployment", :value => "#{deployment_number} (output)" }, 82 | { 83 | :label => "Deployed ref", 84 | :value => "#{ref} @ " + \ 85 | "#{sha}" 86 | }, 87 | { :label => "Environment", :value => environment }, 88 | { :label => "Previous deployment", :value => previous_deployment_link }, 89 | { :label => "Application", :value => repo_name } 90 | ] 91 | end 92 | 93 | def previous_deployment_link 94 | deployed_sha = fetch_previous_deployment 95 | if deployed_sha.nil? 96 | "No previous deployments" 97 | else 98 | diff_link = "Show diff" 99 | "#{deployed_sha} (#{diff_link})" 100 | end 101 | end 102 | 103 | def activity_author 104 | { 105 | :name => ENV["FLOWDOCK_USER_NAME"] || "Heaven", 106 | :avatar => ENV["FLOWDOCK_USER_AVATAR"] || build_status_avatar, 107 | :email => ENV["FLOWDOCK_USER_EMAIL"] || "build@flowdock.com" 108 | } 109 | end 110 | 111 | def fetch_previous_deployment(page = 1) 112 | deployments = api.deployments( 113 | data["repository"]["full_name"], 114 | :environment => environment, 115 | :page => page, 116 | :accept => "application/vnd.github.cannonball-preview+json" 117 | ) 118 | return nil if deployments.length == 0 119 | successfull = deployments.find do |deployment| 120 | deployment.id < deployment_number && 121 | api.deployment_statuses(deployment.url, :accept => "application/vnd.github.cannonball-preview+json") 122 | .any? { |status| status.state == "success" } 123 | end 124 | if successfull.nil? 125 | fetch_previous_deployment(page + 1) 126 | else 127 | successfull.sha[0..7] 128 | end 129 | rescue Octokit::Error => e 130 | Rails.logger.error "Error with github api: #{e}" 131 | nil 132 | end 133 | 134 | private 135 | 136 | def flowdock_thread_id 137 | "heaven:deployment:#{data["repository"]["full_name"].gsub("/", ":")}:#{deployment_number}" 138 | end 139 | 140 | def thread_url(flow, id) 141 | "#{flow["web_url"]}/threads/#{id}" 142 | end 143 | end 144 | end 145 | end 146 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | actionmailer (4.2.2) 5 | actionpack (= 4.2.2) 6 | actionview (= 4.2.2) 7 | activejob (= 4.2.2) 8 | mail (~> 2.5, >= 2.5.4) 9 | rails-dom-testing (~> 1.0, >= 1.0.5) 10 | actionpack (4.2.2) 11 | actionview (= 4.2.2) 12 | activesupport (= 4.2.2) 13 | rack (~> 1.6) 14 | rack-test (~> 0.6.2) 15 | rails-dom-testing (~> 1.0, >= 1.0.5) 16 | rails-html-sanitizer (~> 1.0, >= 1.0.1) 17 | actionview (4.2.2) 18 | activesupport (= 4.2.2) 19 | builder (~> 3.1) 20 | erubis (~> 2.7.0) 21 | rails-dom-testing (~> 1.0, >= 1.0.5) 22 | rails-html-sanitizer (~> 1.0, >= 1.0.1) 23 | activejob (4.2.2) 24 | activesupport (= 4.2.2) 25 | globalid (>= 0.3.0) 26 | activemodel (4.2.2) 27 | activesupport (= 4.2.2) 28 | builder (~> 3.1) 29 | activerecord (4.2.2) 30 | activemodel (= 4.2.2) 31 | activesupport (= 4.2.2) 32 | arel (~> 6.0) 33 | activesupport (4.2.2) 34 | i18n (~> 0.7) 35 | json (~> 1.7, >= 1.7.7) 36 | minitest (~> 5.1) 37 | thread_safe (~> 0.3, >= 0.3.4) 38 | tzinfo (~> 1.1) 39 | addressable (2.3.6) 40 | arel (6.0.0) 41 | ast (2.0.0) 42 | astrolabe (1.3.0) 43 | parser (>= 2.2.2.5, < 3.0) 44 | aws-sdk (1.51.0) 45 | json (~> 1.4) 46 | nokogiri (>= 1.4.4) 47 | better_errors (1.1.0) 48 | coderay (>= 1.0.0) 49 | erubis (>= 2.6.6) 50 | binding_of_caller (0.7.2) 51 | debug_inspector (>= 0.0.1) 52 | builder (3.2.2) 53 | callsite (0.0.11) 54 | campfiyah (0.0.6) 55 | faraday 56 | faraday_middleware 57 | yajl-ruby 58 | capistrano (3.4.0) 59 | i18n 60 | rake (>= 10.0.0) 61 | sshkit (~> 1.3) 62 | capistrano-bundler (1.1.4) 63 | capistrano (~> 3.1) 64 | sshkit (~> 1.2) 65 | capistrano-passenger (0.2.0) 66 | capistrano (~> 3.0) 67 | capistrano-rails (1.1.3) 68 | capistrano (~> 3.1) 69 | capistrano-bundler (~> 1.1) 70 | capistrano-rails-console (1.0.0) 71 | capistrano (>= 3.1.0, < 4.0.0) 72 | capistrano-resque (0.2.2) 73 | capistrano 74 | resque 75 | resque-scheduler 76 | coderay (1.1.0) 77 | crack (0.4.2) 78 | safe_yaml (~> 1.0.0) 79 | debug_inspector (0.0.2) 80 | diff-lcs (1.2.5) 81 | dotenv (0.9.0) 82 | dpl (1.5.7) 83 | erubis (2.7.0) 84 | faraday (0.9.0) 85 | multipart-post (>= 1.2, < 3) 86 | faraday_middleware (0.9.1) 87 | faraday (>= 0.7.4, < 0.10) 88 | flowdock (0.5.0) 89 | httparty (~> 0.7) 90 | multi_json 91 | foreman (0.63.0) 92 | dotenv (>= 0.7) 93 | thor (>= 0.13.6) 94 | globalid (0.3.5) 95 | activesupport (>= 4.1.0) 96 | hipchat (1.1.0) 97 | httparty 98 | httparty (0.13.1) 99 | json (~> 1.8) 100 | multi_xml (>= 0.5.2) 101 | i18n (0.7.0) 102 | json (1.8.2) 103 | kgio (2.9.2) 104 | loofah (2.0.2) 105 | nokogiri (>= 1.5.9) 106 | mail (2.6.3) 107 | mime-types (>= 1.16, < 3) 108 | meta_request (0.2.8) 109 | callsite 110 | rack-contrib 111 | railties 112 | method_source (0.8.2) 113 | mime-types (2.5) 114 | mini_portile (0.6.2) 115 | minitest (5.6.1) 116 | mono_logger (1.1.0) 117 | multi_json (1.11.0) 118 | multi_xml (0.5.5) 119 | multipart-post (2.0.0) 120 | net-scp (1.2.1) 121 | net-ssh (>= 2.6.5) 122 | net-ssh (3.0.2) 123 | nokogiri (1.6.6.2) 124 | mini_portile (~> 0.6.0) 125 | octokit (3.4.0) 126 | sawyer (~> 0.5.3) 127 | parser (2.2.2.5) 128 | ast (>= 1.1, < 3.0) 129 | posix-spawn (0.3.8) 130 | powerpack (0.0.9) 131 | pry (0.9.12.6) 132 | coderay (~> 1.0) 133 | method_source (~> 0.8) 134 | slop (~> 3.4) 135 | rack (1.6.2) 136 | rack-contrib (1.1.0) 137 | rack (>= 0.9.1) 138 | rack-protection (1.5.2) 139 | rack 140 | rack-test (0.6.3) 141 | rack (>= 1.0) 142 | rails (4.2.2) 143 | actionmailer (= 4.2.2) 144 | actionpack (= 4.2.2) 145 | actionview (= 4.2.2) 146 | activejob (= 4.2.2) 147 | activemodel (= 4.2.2) 148 | activerecord (= 4.2.2) 149 | activesupport (= 4.2.2) 150 | bundler (>= 1.3.0, < 2.0) 151 | railties (= 4.2.2) 152 | sprockets-rails 153 | rails-deprecated_sanitizer (1.0.3) 154 | activesupport (>= 4.2.0.alpha) 155 | rails-dom-testing (1.0.6) 156 | activesupport (>= 4.2.0.beta, < 5.0) 157 | nokogiri (~> 1.6.0) 158 | rails-deprecated_sanitizer (>= 1.0.1) 159 | rails-html-sanitizer (1.0.2) 160 | loofah (~> 2.0) 161 | railties (4.2.2) 162 | actionpack (= 4.2.2) 163 | activesupport (= 4.2.2) 164 | rake (>= 0.8.7) 165 | thor (>= 0.18.1, < 2.0) 166 | rainbow (2.0.0) 167 | raindrops (0.13.0) 168 | rake (10.4.2) 169 | redis (3.0.7) 170 | redis-namespace (1.4.1) 171 | redis (~> 3.0.4) 172 | resque (1.25.1) 173 | mono_logger (~> 1.0) 174 | multi_json (~> 1.0) 175 | redis-namespace (~> 1.2) 176 | sinatra (>= 0.9.2) 177 | vegas (~> 0.1.2) 178 | resque-lock-timeout (0.4.4) 179 | resque (~> 1.22) 180 | resque-scheduler (4.0.0) 181 | mono_logger (~> 1.0) 182 | redis (~> 3.0) 183 | resque (~> 1.25) 184 | rufus-scheduler (~> 3.0) 185 | rspec-core (2.14.7) 186 | rspec-expectations (2.14.5) 187 | diff-lcs (>= 1.1.3, < 2.0) 188 | rspec-mocks (2.14.5) 189 | rspec-rails (2.14.1) 190 | actionpack (>= 3.0) 191 | activemodel (>= 3.0) 192 | activesupport (>= 3.0) 193 | railties (>= 3.0) 194 | rspec-core (~> 2.14.0) 195 | rspec-expectations (~> 2.14.0) 196 | rspec-mocks (~> 2.14.0) 197 | rubocop (0.26.0) 198 | astrolabe (~> 1.3) 199 | parser (>= 2.2.0.pre.4, < 3.0) 200 | powerpack (~> 0.0.6) 201 | rainbow (>= 1.99.1, < 3.0) 202 | ruby-progressbar (~> 1.4) 203 | ruby-progressbar (1.5.1) 204 | rufus-scheduler (3.2.0) 205 | safe_yaml (1.0.4) 206 | sawyer (0.5.5) 207 | addressable (~> 2.3.5) 208 | faraday (~> 0.8, < 0.10) 209 | simplecov (0.7.1) 210 | multi_json (~> 1.0) 211 | simplecov-html (~> 0.7.1) 212 | simplecov-html (0.7.1) 213 | sinatra (1.4.4) 214 | rack (~> 1.4) 215 | rack-protection (~> 1.4) 216 | tilt (~> 1.3, >= 1.3.4) 217 | slack-notifier (1.0.0) 218 | slop (3.6.0) 219 | sprockets (3.0.3) 220 | rack (~> 1.0) 221 | sprockets-rails (2.2.4) 222 | actionpack (>= 3.0) 223 | activesupport (>= 3.0) 224 | sprockets (>= 2.8, < 4.0) 225 | sqlite3 (1.3.10) 226 | sshkit (1.8.1) 227 | net-scp (>= 1.1.2) 228 | net-ssh (>= 2.8.0) 229 | thor (0.19.1) 230 | thread_safe (0.3.5) 231 | tilt (1.4.1) 232 | tzinfo (1.2.2) 233 | thread_safe (~> 0.1) 234 | unicorn (4.8.2) 235 | kgio (~> 2.6) 236 | rack 237 | raindrops (~> 0.7) 238 | vegas (0.1.11) 239 | rack (>= 1.0.0) 240 | warden (1.2.3) 241 | rack (>= 1.0) 242 | warden-github (1.0.2) 243 | octokit (> 2.1.0) 244 | warden (> 1.0) 245 | warden-github-rails (1.1.0) 246 | railties (>= 3.1) 247 | warden-github (~> 1.0) 248 | webmock (1.17.3) 249 | addressable (>= 2.2.7) 250 | crack (>= 0.3.2) 251 | yajl-ruby (1.2.0) 252 | 253 | PLATFORMS 254 | ruby 255 | 256 | DEPENDENCIES 257 | aws-sdk 258 | better_errors 259 | binding_of_caller 260 | campfiyah 261 | capistrano 262 | capistrano-passenger 263 | capistrano-rails (~> 1.1) 264 | capistrano-rails-console 265 | capistrano-resque (~> 0.2.2) 266 | dpl (= 1.5.7) 267 | faraday 268 | faraday_middleware 269 | flowdock 270 | foreman 271 | hipchat 272 | meta_request 273 | octokit 274 | posix-spawn 275 | pry 276 | rails (~> 4.2.2) 277 | resque 278 | resque-lock-timeout 279 | rspec-rails 280 | rubocop 281 | simplecov (= 0.7.1) 282 | slack-notifier 283 | sqlite3 (= 1.3.10) 284 | unicorn 285 | warden-github-rails 286 | webmock 287 | yajl-ruby 288 | 289 | BUNDLED WITH 290 | 1.11.2 291 | --------------------------------------------------------------------------------