├── .gitignore ├── .rspec ├── .travis.yml ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── capistrano-github.gemspec ├── lib └── capistrano │ ├── github.rb │ ├── github │ ├── api.rb │ └── version.rb │ └── tasks │ └── github.rake └── spec ├── github └── api_spec.rb ├── spec_helper.rb ├── support └── shared_contexts │ ├── capistrano.rb │ └── rake.rb └── tasks └── github_spec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.0 4 | - 2.1 5 | - 2.0 6 | script: bundle exec rspec 7 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in capistrano-github.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Kir Shatrov 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Capistrano::Github 2 | 3 | In January 2014 Github Team [announced Deployments API](http://developer.github.com/changes/2014-01-09-preview-the-new-deployments-api/) and you can use it with Capistrano 3. 4 | 5 | ## Installation 6 | 7 | Add this line to your application's Gemfile: 8 | 9 | gem 'capistrano-github', github: '3scale/capistrano-github' 10 | 11 | And then execute: 12 | 13 | $ bundle 14 | 15 | 16 | Require github tasks and set `github_access_token`: 17 | 18 | ```ruby 19 | # Capfile 20 | require 'capistrano/github' 21 | ``` 22 | 23 | ```ruby 24 | # deploy.rb 25 | set :github_access_token, '89c3be3d1f917b6ccf5e2c141dbc403f57bc140c' 26 | 27 | 28 | before 'deploy:starting', 'github:deployment:create' 29 | after 'deploy:starting', 'github:deployment:pending' 30 | after 'deploy:finished', 'github:deployment:success' 31 | after 'deploy:failed', 'github:deployment:failure' 32 | ``` 33 | 34 | You can get your personal GH token [here](https://github.com/settings/applications) 35 | 36 | ## Usage 37 | 38 | New deployment record will be created automatically on each `cap deploy` run. 39 | 40 | To see the list of deployments, execute 41 | 42 | ```bash 43 | cap production github:deployments 44 | ``` 45 | 46 | ## Contributing 47 | 48 | 1. Fork it ( http://github.com/3scale/capistrano-github/fork ) 49 | 2. Create your feature branch (`git checkout -b my-new-feature`) 50 | 3. Commit your changes (`git commit -am 'Add some feature'`) 51 | 4. Push to the branch (`git push origin my-new-feature`) 52 | 5. Create new Pull Request 53 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | -------------------------------------------------------------------------------- /capistrano-github.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'capistrano/github/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "capistrano-github" 8 | spec.version = Capistrano::Github::VERSION 9 | spec.authors = ["Kir Shatrov", 'Michal Cichra'] 10 | spec.email = ["shatrov@me.com", 'michal@3scale.net'] 11 | spec.summary = %q{Integrates Capistrano with Github Deployments API} 12 | spec.homepage = "http://github.com/3scale/capistrano-github" 13 | spec.license = "MIT" 14 | 15 | spec.files = `git ls-files -z`.split("\x0") 16 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 17 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 18 | spec.require_paths = ["lib"] 19 | 20 | spec.add_dependency "capistrano", "~> 3.1" 21 | spec.add_dependency "octokit", ">= 3.0", "< 4.0" 22 | 23 | spec.add_development_dependency "bundler", "~> 1.5" 24 | spec.add_development_dependency "rake" 25 | spec.add_development_dependency "rspec", '~> 3.1' 26 | end 27 | -------------------------------------------------------------------------------- /lib/capistrano/github.rb: -------------------------------------------------------------------------------- 1 | load File.expand_path("../tasks/github.rake", __FILE__) 2 | 3 | require "capistrano/github/version" 4 | require "capistrano/github/api" 5 | 6 | module Capistrano 7 | module Github 8 | # Your code goes here... 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/capistrano/github/api.rb: -------------------------------------------------------------------------------- 1 | require 'octokit' 2 | 3 | module Capistrano 4 | module Github 5 | class API 6 | class Deployment 7 | attr_accessor :created_at, :ref, :sha, :creator_login, :payload, :statuses_proc, :id, :environment 8 | 9 | def statuses 10 | @statuses ||= statuses_proc.call 11 | end 12 | 13 | def last_state 14 | @last_state ||= statuses.map(&:state).first || 'unknown' 15 | end 16 | 17 | class Status 18 | attr_accessor :created_at, :state 19 | end 20 | end 21 | 22 | REPO_FORMAT = /git@github.com:([\S]*)\/([\S]*).git/ 23 | 24 | attr_reader :client 25 | 26 | def initialize(repo_url, token) 27 | raise MissingAccessToken unless token 28 | @client = Octokit::Client.new(access_token: token) 29 | @repo = parse_repo_url(repo_url) 30 | end 31 | 32 | def create_deployment(branch, options = {}) 33 | @client.create_deployment(@repo, branch, options).id 34 | end 35 | 36 | def create_deployment_status(id, state) 37 | @client.create_deployment_status(deployment_url(id), state) 38 | end 39 | 40 | def deployments(options = {}) 41 | @client.deployments(@repo, options).map do |d| 42 | Deployment.new.tap do |dep| 43 | dep.created_at = d.created_at 44 | dep.sha = d.sha 45 | dep.ref = d.ref 46 | dep.creator_login = d.creator.login 47 | dep.payload = d.payload 48 | dep.id = d.id 49 | dep.environment = d.environment 50 | 51 | dep.statuses_proc = -> { deployment_statuses(d.id) } 52 | end 53 | end 54 | end 55 | 56 | private 57 | 58 | def deployment_statuses(id) 59 | @client.deployment_statuses(deployment_url(id)) 60 | end 61 | 62 | def deployment_url(id) 63 | "repos/#{@repo}/deployments/#{id}" 64 | end 65 | 66 | def parse_repo_url(url) 67 | repo_match = url && url.match(REPO_FORMAT) or raise InvalidRepoUrl, url 68 | "#{repo_match[1]}/#{repo_match[2]}" 69 | end 70 | 71 | InvalidRepoUrl = Class.new(StandardError) 72 | MissingAccessToken = Class.new(StandardError) 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/capistrano/github/version.rb: -------------------------------------------------------------------------------- 1 | module Capistrano 2 | module Github 3 | VERSION = '0.1.1' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/capistrano/tasks/github.rake: -------------------------------------------------------------------------------- 1 | require 'capistrano/github/api' 2 | 3 | set_if_empty :github_deployment_payload, -> do 4 | { 5 | 6 | } 7 | end 8 | 9 | set_if_empty :github_deployment, -> do 10 | { 11 | auto_merge: false, 12 | environment: fetch(:stage) 13 | } 14 | end 15 | 16 | set_if_empty :github_deployment_api, -> do 17 | Capistrano::Github::API.new(fetch(:repo_url), fetch(:github_access_token)) 18 | end 19 | 20 | set :github_deployment_required, false 21 | 22 | set :github_deployment_enabled, -> do 23 | begin 24 | fetch(:github_deployment_api) 25 | rescue Capistrano::Github::API::MissingAccessToken 26 | false 27 | end 28 | end 29 | 30 | set :github_deployment_skip, -> do 31 | !fetch(:github_deployment_enabled) && !fetch(:github_deployment_required) 32 | end 33 | 34 | namespace :github do 35 | 36 | namespace :deployment do 37 | desc 'Create new deployment' 38 | task :create do 39 | next if fetch(:github_deployment_skip) 40 | 41 | gh = fetch(:github_deployment_api) 42 | payload = fetch(:github_deployment_payload) 43 | config = fetch(:github_deployment).merge(payload: payload) 44 | branch = fetch(:branch) 45 | 46 | set :current_github_deployment, deployment = gh.create_deployment(branch, config) 47 | 48 | run_locally do 49 | info("Created GitHub Deployment #{deployment}") 50 | end 51 | end 52 | 53 | [:pending, :success, :error, :failure].each do |status| 54 | desc "Mark current deployment as #{status}" 55 | task status => 'github:deployment:create' do 56 | next if fetch(:github_deployment_skip) 57 | 58 | gh = fetch(:github_deployment_api) 59 | deployment = fetch(:current_github_deployment) 60 | 61 | run_locally do 62 | if deployment 63 | gh.create_deployment_status(deployment, status) 64 | info("Marked GitHub Deployment #{deployment} as #{status}") 65 | else 66 | info("No GitHub Deployment found, could not mark as #{status}") 67 | end 68 | end 69 | end 70 | end 71 | end 72 | 73 | print_deployment = -> d { puts "Deployment (#{d.last_state}): #{d.created_at} #{d.ref}@#{d.sha} to #{d.environment} by @#{d.creator_login}" } 74 | print_status = -> s { puts "\t#{s.created_at} state: #{s.state}" } 75 | 76 | desc 'List Github deployments' 77 | task :deployments do 78 | gh = fetch(:github_deployment_api) 79 | env = fetch(:github_deployment)[:environment] 80 | gh.deployments(environment: env).each do |deployment| 81 | deployment.tap(&print_deployment) 82 | deployment.statuses.each(&print_status) 83 | end 84 | end 85 | 86 | desc 'Show last Github deploy' 87 | task :last_deploy do 88 | gh = fetch(:github_deployment_api) 89 | env = fetch(:github_deployment)[:environment] 90 | gh.deployments(environment: env).first.tap(&print_deployment) 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /spec/github/api_spec.rb: -------------------------------------------------------------------------------- 1 | require 'capistrano/github/api' 2 | 3 | RSpec.describe Capistrano::Github::API do 4 | subject(:api) { described_class.new(repo_url, access_token) } 5 | 6 | let(:access_token) { 'some-token' } 7 | let(:repo_url) { 'git@github.com:3scale/capistrano-github.git' } 8 | 9 | context 'invalid repo url' do 10 | let(:repo_url) { 'some-invalid' } 11 | 12 | it do 13 | expect{ subject }.to raise_error(Capistrano::Github::API::InvalidRepoUrl) 14 | end 15 | end 16 | 17 | context 'missing repo url' do 18 | let(:repo_url) { nil } 19 | 20 | it do 21 | expect{ subject }.to raise_error(Capistrano::Github::API::InvalidRepoUrl) 22 | end 23 | end 24 | 25 | context 'missing access token' do 26 | let(:access_token) { nil } 27 | 28 | it do 29 | expect{ subject }.to raise_error(Capistrano::Github::API::MissingAccessToken) 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # This file was generated by the `rspec --init` command. Conventionally, all 2 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 3 | # The generated `.rspec` file contains `--require spec_helper` which will cause this 4 | # file to always be loaded, without a need to explicitly require it in any files. 5 | # 6 | # Given that it is always loaded, you are encouraged to keep this file as 7 | # light-weight as possible. Requiring heavyweight dependencies from this file 8 | # will add to the boot time of your test suite on EVERY test run, even for an 9 | # individual file that may not need all of that loaded. Instead, consider making 10 | # a separate helper file that requires the additional dependencies and performs 11 | # the additional setup, and require it from the spec files that actually need it. 12 | # 13 | # The `.rspec` file also contains a few flags that are not defaults but that 14 | # users commonly want. 15 | # 16 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 17 | 18 | require 'support/shared_contexts/capistrano' 19 | require 'support/shared_contexts/rake' 20 | require 'sshkit' 21 | require 'capistrano/dsl' 22 | 23 | RSpec.configure do |config| 24 | # rspec-expectations config goes here. You can use an alternate 25 | # assertion/expectation library such as wrong or the stdlib/minitest 26 | # assertions if you prefer. 27 | config.expect_with :rspec do |expectations| 28 | # This option will default to `true` in RSpec 4. It makes the `description` 29 | # and `failure_message` of custom matchers include text for helper methods 30 | # defined using `chain`, e.g.: 31 | # be_bigger_than(2).and_smaller_than(4).description 32 | # # => "be bigger than 2 and smaller than 4" 33 | # ...rather than: 34 | # # => "be bigger than 2" 35 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 36 | end 37 | 38 | # rspec-mocks config goes here. You can use an alternate test double 39 | # library (such as bogus or mocha) by changing the `mock_with` option here. 40 | config.mock_with :rspec do |mocks| 41 | # Prevents you from mocking or stubbing a method that does not exist on 42 | # a real object. This is generally recommended, and will default to 43 | # `true` in RSpec 4. 44 | mocks.verify_partial_doubles = true 45 | end 46 | 47 | # The settings below are suggested to provide a good initial experience 48 | # with RSpec, but feel free to customize to your heart's content. 49 | =begin 50 | # These two settings work together to allow you to limit a spec run 51 | # to individual examples or groups you care about by tagging them with 52 | # `:focus` metadata. When nothing is tagged with `:focus`, all examples 53 | # get run. 54 | config.filter_run :focus 55 | config.run_all_when_everything_filtered = true 56 | 57 | # Limits the available syntax to the non-monkey patched syntax that is recommended. 58 | # For more details, see: 59 | # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax 60 | # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 61 | # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching 62 | config.disable_monkey_patching! 63 | 64 | # This setting enables warnings. It's recommended, but in some cases may 65 | # be too noisy due to issues in dependencies. 66 | config.warnings = true 67 | 68 | # Many RSpec users commonly either run the entire suite or an individual 69 | # file, and it's useful to allow more verbose output when running an 70 | # individual spec file. 71 | if config.files_to_run.one? 72 | # Use the documentation formatter for detailed output, 73 | # unless a formatter has already been configured 74 | # (e.g. via a command-line flag). 75 | config.default_formatter = 'doc' 76 | end 77 | 78 | # Print the 10 slowest examples and example groups at the 79 | # end of the spec run, to help surface which specs are running 80 | # particularly slow. 81 | config.profile_examples = 10 82 | 83 | # Run specs in random order to surface order dependencies. If you find an 84 | # order dependency and want to debug it, you can fix the order by providing 85 | # the seed, which is printed after each run. 86 | # --seed 1234 87 | config.order = :random 88 | 89 | # Seed global randomization in this process using the `--seed` CLI option. 90 | # Setting this allows you to use `--seed` to deterministically reproduce 91 | # test failures related to randomization by passing the same `--seed` value 92 | # as the one that triggered the failure. 93 | Kernel.srand config.seed 94 | =end 95 | end 96 | -------------------------------------------------------------------------------- /spec/support/shared_contexts/capistrano.rb: -------------------------------------------------------------------------------- 1 | RSpec.shared_context :capistrano do 2 | let(:env) { Capistrano::Configuration.env } 3 | 4 | let(:repo_url) { 'git@github.com:capistrano/github.git' } 5 | let(:branch) { 'master' } 6 | let(:stage) { 'production' } 7 | 8 | before do 9 | Capistrano::Configuration.reset! 10 | 11 | env.set :repo_url, repo_url 12 | env.set :branch, branch 13 | env.set :stage, stage 14 | end 15 | end 16 | 17 | -------------------------------------------------------------------------------- /spec/support/shared_contexts/rake.rb: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | 3 | RSpec.shared_context :rake do 4 | let(:rake) { Rake::Application.new } 5 | let(:task_name) { self.class.top_level_description } 6 | let(:task_path) { "capistrano/tasks/#{task_name.split(":").first}" } 7 | subject(:task) { rake[task_name] } 8 | 9 | def loaded_files_excluding_current_rake_file 10 | $".reject {|file| file == "#{task_path}.rake" } 11 | end 12 | 13 | before do 14 | Rake.application = rake 15 | Rake.application.rake_require(task_path, $LOAD_PATH, loaded_files_excluding_current_rake_file) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/tasks/github_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe 'github:deployment:create' do 2 | include_context :capistrano 3 | include_context :rake 4 | 5 | it { is_expected.to be } 6 | 7 | context 'invoke' do 8 | subject(:invoke) { task.invoke } 9 | it { is_expected.to be } 10 | 11 | context 'with access token' do 12 | let(:api) { instance_double(Capistrano::Github::API) } 13 | 14 | before do 15 | env.set :github_deployment_api, api 16 | end 17 | 18 | after { invoke } 19 | 20 | it do 21 | expect(api).to receive(:create_deployment) 22 | .with(branch, auto_merge: false, environment: stage, payload: {}) 23 | .and_return('some-id') 24 | end 25 | end 26 | end 27 | end 28 | 29 | [:pending, :success, :error, :failure].each do |status| 30 | RSpec.describe "github:deployment:#{status}" do 31 | include_context :capistrano 32 | include_context :rake 33 | 34 | it { is_expected.to be } 35 | it { expect(task.prerequisites).to include('github:deployment:create') } 36 | 37 | context 'invoke' do 38 | subject(:invoke) { task.invoke } 39 | it { is_expected.to be } 40 | 41 | context 'with access token' do 42 | let(:api) { instance_double(Capistrano::Github::API) } 43 | 44 | let(:deployment) { rand(10000) } 45 | 46 | before do 47 | env.set :github_deployment_api, api 48 | expect(api).to receive(:create_deployment).and_return(deployment) 49 | end 50 | 51 | after { invoke } 52 | 53 | it do 54 | expect(api).to receive(:create_deployment_status) 55 | .with(deployment, status) 56 | end 57 | end 58 | end 59 | end 60 | end 61 | --------------------------------------------------------------------------------