├── .gitignore ├── Gemfile ├── Gemfile.lock ├── README.md ├── Rakefile ├── bin └── capistrano-calendar ├── capistrano-calendar.gemspec ├── lib └── capistrano │ ├── calendar.rb │ └── calendar │ ├── client.rb │ ├── client │ ├── base.rb │ └── google.rb │ ├── configuration.rb │ ├── recipes.rb │ ├── runner.rb │ └── version.rb └── spec ├── google_calendar_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | .credentials.yml 4 | pkg/* 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem 'capistrano', '2.9.0' 6 | gem 'json', '1.8.6' 7 | gem "gdata_19", "1.1.3" 8 | gem 'daemons', '1.1.4' 9 | 10 | gem 'rspec', '2.6.0' 11 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | capistrano-calendar (0.1.3) 5 | capistrano (>= 2.5.5) 6 | gdata_19 (>= 1.1.0) 7 | json (~> 1.8.6) 8 | 9 | GEM 10 | remote: http://rubygems.org/ 11 | specs: 12 | capistrano (2.9.0) 13 | highline 14 | net-scp (>= 1.0.0) 15 | net-sftp (>= 2.0.0) 16 | net-ssh (>= 2.0.14) 17 | net-ssh-gateway (>= 1.1.0) 18 | daemons (1.1.4) 19 | diff-lcs (1.1.2) 20 | gdata_19 (1.1.3) 21 | highline (1.6.11) 22 | json (1.8.6) 23 | net-scp (1.0.4) 24 | net-ssh (>= 1.99.1) 25 | net-sftp (2.0.5) 26 | net-ssh (>= 2.0.9) 27 | net-ssh (2.3.0) 28 | net-ssh-gateway (1.1.0) 29 | net-ssh (>= 1.99.1) 30 | rspec (2.6.0) 31 | rspec-core (~> 2.6.0) 32 | rspec-expectations (~> 2.6.0) 33 | rspec-mocks (~> 2.6.0) 34 | rspec-core (2.6.4) 35 | rspec-expectations (2.6.0) 36 | diff-lcs (~> 1.1.2) 37 | rspec-mocks (2.6.0) 38 | 39 | PLATFORMS 40 | ruby 41 | 42 | DEPENDENCIES 43 | capistrano (= 2.9.0) 44 | capistrano-calendar! 45 | daemons (= 1.1.4) 46 | gdata_19 (= 1.1.3) 47 | json (= 1.8.6) 48 | rspec (= 2.6.0) 49 | 50 | BUNDLED WITH 51 | 1.16.2 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Capistrano Calendar 2 | 3 | ## Introduction 4 | 5 | capistrano-calendar is extension for capistrano that allows your create different deployment notification in calendar service. Currently supported services: 6 | 7 | * google calendar 8 | 9 | ## Installation 10 | 11 | gem install capistrano-calendar 12 | 13 | Add to your Capfile 14 | 15 | require 'capistrano/calendar/recipes' 16 | 17 | ## How it works 18 | 19 | This gem provides: 20 | 21 | * capistrano recipe *calendar:create_event* 22 | * ruby binary script *capistrano-calendar* 23 | 24 | Capistrano recipe should be used in some :after hooks (e.g. after deploy). Recipe reads calendar configurations and run capistrano-calendar binary on some host specified in *capistrano_runner* option. So you **need to install** *capistrano-calendar* gem also on that calendar host. Also please ensure that you properly configured calendar_runner otherwise you may have **duplicated events** in calender. 25 | 26 | ## Calendar configuration 27 | 28 | * *:calendar_verbose (nil) - if true display calendar data actions 29 | * *:calendar_logfile (/tmp/capistrano-calendar-PID.log) - calendar log 30 | * *:calendar_foreground (nil) - don't daemonize calendar process if true 31 | * *:calendar_service* (:google) - calendar service to use e.g. ':google' 32 | * **:calendar_username** (nil) - calendar service username e.g 'vasya.pupkin@gmail.com' 33 | * **:calendar_password** (nil) - calendar service password e.g 'qwery123456' 34 | * *:calendar_name* (default) - calendar name to use for events 35 | * *:calendar_summary* (nil) - calendar summary for new calendar 36 | * *:calendar_location* (nil) - calendar location for new calendar 37 | * *:calendar_timezone* ('UTC') - calendar timezone for new calendar 38 | * *:calendar_color* ('#A32929') - calendar color for new calendar 39 | * **:calendar_event_name** (nil) - calendar event name 40 | * *:calendar_event_summary* (nil) - calendar event summary 41 | * *:calendar_event_location* (nil) - calendar event location 42 | * *:calendar_event_time* (Time.now) - calendar event time 43 | * *:calendar_event_status* ('confirmed') - calendar event status 44 | 45 | Bold means required options. Value in brackets means default value. 46 | 47 | ## Calendar runner configuration 48 | 49 | Calendar event creation will be executed by default on the one of application servers (specified in :app roles) 50 | You can change this behavior via *calendar_runner* option. Examples: 51 | 52 | set(:calendar_runner, { :roles => :calendar_notifier, :once => true }) 53 | # or 54 | set(:calendar_runner, { :hosts => 'notification.example.com' }) 55 | 56 | Be sure to pass `:once => true` option if you use *:roles* ! 57 | 58 | ## Configuration example 59 | 60 | set :calendar_service, :google 61 | set :calendar_username, 'vasya.pupkin@my.company.com' 62 | set :calendar_password, '123456' 63 | 64 | set(:calendar_name) { stage } 65 | set(:calendar_summary) { "" } 66 | set :calendar_timezone, 'UTC' 67 | set(:calendar_color), { stage == 'production' ? '#ff000' : '#00ff00' } 68 | 69 | set(:calendar_event_summary) { "Bla" } 70 | set(:calendar_event_time) { Time.now } 71 | 72 | after 'deploy' do 73 | set :calendar_event_name, "[Deployed] #{application} #{branch}: #{real_revision}" 74 | top.calendar.create_event 75 | end 76 | 77 | after 'deploy:rollback' do 78 | set :calendar_event_name, "[Rollback] #{application} #{branch}: #{real_revision}" 79 | top.calendar.create_event 80 | end 81 | 82 | # 83 | # Extra configurations if you are using capistrano-patch: 84 | # 85 | after 'patch:apply' do 86 | set :calendar_event_name, "[Pathed] #{application} #{branch}: #{patch_strategy.revision_to}" 87 | calendar_client.create_event 88 | end 89 | 90 | after 'patch:revert' do 91 | set :calendar_event_name, "[Patch rollback] #{application} #{branch}: #{patch_strategy.revision_from}" 92 | top.calendar.create_event 93 | end 94 | 95 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | -------------------------------------------------------------------------------- /bin/capistrano-calendar: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'capistrano/calendar' 4 | 5 | Capistrano::Calendar::Runner.new(ARGV).run! 6 | -------------------------------------------------------------------------------- /capistrano-calendar.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | 4 | require "capistrano/calendar/version" 5 | 6 | Gem::Specification.new do |s| 7 | s.name = "capistrano-calendar" 8 | s.version = Capistrano::Calendar::VERSION 9 | s.authors = ["Andriy Yanko"] 10 | s.email = ["andriy.yanko@gmail.com"] 11 | s.homepage = "https://github.com/railsware/capistrano-calendar/" 12 | s.summary = %q{Deployment event creation on (google) calendar service} 13 | 14 | s.rubyforge_project = "capistrano-calendar" 15 | 16 | s.files = `git ls-files`.split("\n") 17 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 18 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 19 | s.require_paths = ["lib"] 20 | 21 | s.add_runtime_dependency "capistrano", ">=2.5.5" 22 | s.add_runtime_dependency "gdata_19", ">=1.1.0" 23 | s.add_runtime_dependency "json", "~>1.8.6" 24 | end 25 | -------------------------------------------------------------------------------- /lib/capistrano/calendar.rb: -------------------------------------------------------------------------------- 1 | require 'capistrano/calendar/version' 2 | 3 | module Capistrano 4 | module Calendar 5 | autoload :Client, 'capistrano/calendar/client' 6 | autoload :Configuration, 'capistrano/calendar/configuration' 7 | autoload :Runner, 'capistrano/calendar/runner' 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/capistrano/calendar/client.rb: -------------------------------------------------------------------------------- 1 | require 'capistrano/errors' 2 | 3 | module Capistrano 4 | module Calendar 5 | module Client 6 | def self.new(config) 7 | name = config[:calendar_service] || :google 8 | 9 | require "capistrano/calendar/client/#{name}" 10 | 11 | client_const = name.to_s.capitalize.gsub(/_(.)/) { $1.upcase } 12 | 13 | const_get(client_const).new(config) 14 | 15 | rescue LoadError 16 | raise Capistrano::Error, "could not find any client named '#{name}'" 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/capistrano/calendar/client/base.rb: -------------------------------------------------------------------------------- 1 | module Capistrano 2 | module Calendar 3 | module Client 4 | class Base 5 | def initialize(config) 6 | @configuration = config 7 | end 8 | 9 | attr_reader :configuration 10 | 11 | def calendar_username 12 | configuration.fetch(:calendar_username) 13 | end 14 | 15 | def calendar_password 16 | configuration.fetch(:calendar_password) 17 | end 18 | 19 | def authenticate 20 | raise NotImplementedError, "`authenticate' is not implemented by #{self.class.name}" 21 | end 22 | 23 | def create_event 24 | raise NotImplementedError, "`create_event' is not implemented by #{self.class.name}" 25 | end 26 | 27 | end 28 | 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/capistrano/calendar/client/google.rb: -------------------------------------------------------------------------------- 1 | require 'capistrano/calendar/client/base' 2 | 3 | require 'gdata' 4 | require 'json' 5 | require 'time' 6 | 7 | module Capistrano 8 | module Calendar 9 | module Client 10 | class Google < Base 11 | 12 | # 13 | # http://code.google.com/apis/calendar/data/2.0/developers_guide_protocol.html 14 | # 15 | 16 | OWN_CALENDARS_URL = "https://www.google.com/calendar/feeds/default/owncalendars/full" 17 | CALENDAR_EVENTS_URL = "https://www.google.com/calendar/feeds/default/private/full" 18 | 19 | def authenticate 20 | client.clientlogin(calendar_username, calendar_password) 21 | end 22 | 23 | def create_event 24 | response = client.post(events_url, json_event_entity) 25 | JSON.parse(response.body)['data'] 26 | end 27 | 28 | def events_url 29 | if configuration[:calendar_name] 30 | calendar = find_or_create_calendar(configuration[:calendar_name]) 31 | calendar['eventFeedLink'] 32 | else 33 | CALENDAR_EVENTS_URL 34 | end 35 | end 36 | 37 | def find_or_create_calendar(calendar_name) 38 | find_calendar(calendar_name) || create_calendar(calendar_name) 39 | end 40 | 41 | def find_calendar(calendar_name) 42 | response = client.get(OWN_CALENDARS_URL + '?alt=jsonc') 43 | JSON.parse(response.body)['data']['items'].find { |item| 44 | item['title'] == calendar_name 45 | } 46 | end 47 | 48 | def create_calendar(calendar_name) 49 | response = client.post(OWN_CALENDARS_URL, json_calendar_entity) 50 | JSON.parse(response.body)['data'] 51 | end 52 | 53 | def delete_calendar(calendar) 54 | calendar.is_a?(Hash) or calendar = find_calendar(calendar) 55 | calendar or raise Capistrano::Error, "Calendar #{calendar.inspect} not found" 56 | client.delete(calendar['selfLink']) 57 | end 58 | 59 | protected 60 | 61 | def client 62 | @client ||= GData::Client::Calendar.new({ 63 | :headers => { 'Content-Type' => 'application/json' }, 64 | :source => 'capistrano-calendar' 65 | }) 66 | end 67 | 68 | def json_calendar_entity 69 | data = { 70 | 'title' => configuration.fetch(:calendar_name), 71 | 'timeZone' => configuration.fetch(:calendar_timezone, 'UTC'), 72 | 'color' => configuration.fetch(:calendar_color, '#A32929'), 73 | 'hidden' => false 74 | } 75 | 76 | value = configuration[:calendar_summary] and data['details'] = value 77 | value = configuration[:calendar_location] and data['location'] = value 78 | 79 | { 'data' => data }.to_json 80 | end 81 | 82 | def json_event_entity 83 | event_time = configuration.fetch(:calendar_event_time, Time.now).utc.xmlschema 84 | 85 | data = { 86 | 'title' => configuration.fetch(:calendar_event_name), 87 | 'status' => configuration.fetch(:calendar_event_status, 'confirmed'), 88 | 'when' => [ 89 | { 90 | 'start' => event_time, 91 | 'stop' => event_time 92 | } 93 | ] 94 | } 95 | 96 | value = configuration[:calendar_event_summary] and data['details'] = value 97 | value = configuration[:calendar_event_location] and data['location'] = value 98 | 99 | { 'data' => data }.to_json 100 | end 101 | 102 | end 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /lib/capistrano/calendar/configuration.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | require 'base64' 3 | 4 | module Capistrano 5 | module Calendar 6 | class Configuration 7 | 8 | def self.collect(hash) 9 | [ 10 | :calendar_verbose, 11 | :calendar_logfile, 12 | :calendar_foreground, 13 | 14 | :calendar_service, 15 | :calendar_username, 16 | :calendar_password, 17 | 18 | :calendar_name, 19 | :calendar_summary, 20 | :calendar_location, 21 | :calendar_timezone, 22 | :calendar_color, 23 | 24 | :calendar_event_name, 25 | :calendar_event_summary, 26 | :calendar_event_location, 27 | :calendar_event_time, 28 | :calendar_event_status, 29 | ].inject({}) { |result, key| 30 | result[key] = hash[key] if hash.exists?(key) 31 | result 32 | } 33 | end 34 | 35 | def self.encode(hash) 36 | Base64.encode64(hash.to_yaml).gsub("\n", '') 37 | end 38 | 39 | def self.decode(string) 40 | YAML.load(Base64.decode64(string)) 41 | end 42 | 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/capistrano/calendar/recipes.rb: -------------------------------------------------------------------------------- 1 | require 'capistrano/calendar' 2 | 3 | Capistrano::Configuration.instance.load do 4 | 5 | set(:calendar_runner) do 6 | { 7 | :roles => :app, 8 | :once => true 9 | } 10 | end 11 | 12 | namespace :calendar do 13 | desc "Create calendar event" 14 | task :create_event do 15 | configuration = Capistrano::Calendar::Configuration.collect(self) 16 | string = Capistrano::Calendar::Configuration.encode(configuration) 17 | 18 | run("capistrano-calendar _#{Capistrano::Calendar::VERSION}_ create_event #{string}", calendar_runner) 19 | end 20 | 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/capistrano/calendar/runner.rb: -------------------------------------------------------------------------------- 1 | require 'optparse' 2 | 3 | module Capistrano 4 | module Calendar 5 | class Runner 6 | def self.run(*args) 7 | new(*args).run! 8 | end 9 | 10 | def initialize(argv) 11 | @argv = argv 12 | end 13 | 14 | attr_reader :client 15 | 16 | def run! 17 | parser = OptionParser.new do |o| 18 | o.banner = "Usage: capistrano-calendar [options] COMMAND [CONFIGURATION_DATA]" 19 | o.separator "" 20 | o.separator "Commands:" 21 | o.separator "" 22 | o.separator " create_event - create calendar event" 23 | o.separator "" 24 | o.separator "CONFIGURATION_DATA is base64 encoded yaml data" 25 | o.separator "" 26 | o.separator "Common options:" 27 | o.on_tail("-h", "--help", "Show this message") { puts o; exit } 28 | o.on_tail('-v', '--version', "Show version") { puts Capistrano::Calendar::VERSION; exit } 29 | end 30 | 31 | parser.parse!(@argv) 32 | 33 | @command = @argv.shift 34 | @configuration = @argv.shift 35 | 36 | case @command 37 | when 'create_event' 38 | @configuration or abort(parser.help) 39 | @configuration = Capistrano::Calendar::Configuration.decode(@configuration) 40 | @configuration.is_a?(Hash) or abort("Bad configuration given") 41 | 42 | @client = Capistrano::Calendar::Client.new(@configuration) 43 | 44 | if @configuration[:calendar_foreground] 45 | create_event 46 | else 47 | daemonize { create_event } 48 | end 49 | else 50 | abort parser.help 51 | end 52 | 53 | end 54 | 55 | protected 56 | 57 | def create_event 58 | verbose("Creating event with configuration: #{@configuration.inspect}") 59 | 60 | client.authenticate 61 | event = client.create_event 62 | 63 | verbose("Created event: #{event.inspect}") 64 | event 65 | end 66 | 67 | def daemonize 68 | logfile = @configuration[:calendar_logfile] || "/tmp/capistrano-calendar-#{Process.pid}.log" 69 | 70 | Process.fork do 71 | # Detach the child process from the parent's session. 72 | Process.setsid 73 | 74 | # Re-fork. This is recommended for System V-based 75 | # system as it guarantees that the daemon is not a 76 | # session leader, which prevents it from aquiring 77 | # a controlling terminal under the System V rules. 78 | exit if fork 79 | 80 | # Rename the process 81 | $0 = "capistrano-calendar" 82 | 83 | # Flush all buffers 84 | $stdout.sync = $stderr.sync = true 85 | 86 | # Set all standard files to `/dev/null/` in order 87 | # to be sure that any output from the daemon will 88 | # not appear in any terminals. 89 | $stdin.reopen("/dev/null") 90 | $stdout.reopen(logfile, "a") 91 | $stderr.reopen(logfile, "a") 92 | 93 | yield 94 | end 95 | 96 | puts "Logging to: #{logfile}" 97 | end 98 | 99 | def verbose(message) 100 | puts(message) if @configuration[:calendar_verbose] 101 | end 102 | end 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /lib/capistrano/calendar/version.rb: -------------------------------------------------------------------------------- 1 | module Capistrano 2 | module Calendar 3 | VERSION = "0.1.3" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/google_calendar_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Capistrano::Calendar::Client, "#create_event for google calendar" do 4 | 5 | let(:configuration) do 6 | { 7 | :calendar_service => :google, 8 | :calendar_username => Helper.credentials[:google][:username], 9 | :calendar_password => Helper.credentials[:google][:password], 10 | :calendar_name => "Capistrano Calendar", 11 | :calendar_event_name => 'Test Deploy!' 12 | } 13 | end 14 | 15 | let(:client) { 16 | client = Capistrano::Calendar::Client.new(configuration) 17 | client.authenticate 18 | client 19 | } 20 | 21 | before(:each) do 22 | sleep(1) 23 | configuration[:calendar_name] << " #{Time.now.to_f}" 24 | end 25 | 26 | it "should create calendar and event" do 27 | event = client.create_event 28 | 29 | event.should be_kind_of(Hash) 30 | event['title'].should == configuration[:calendar_event_name] 31 | 32 | calendar = client.find_calendar(configuration[:calendar_name]) 33 | calendar.should be_kind_of(Hash) 34 | calendar['title'].should == configuration[:calendar_name] 35 | calendar['timeZone'].should == 'UTC' 36 | end 37 | 38 | context "when calendar exists" do 39 | before(:each) do 40 | @calendar = client.create_calendar(configuration[:calendar_name]) 41 | end 42 | 43 | it "should create event and does NOT create another calendar" do 44 | event = client.create_event 45 | 46 | event.should be_kind_of(Hash) 47 | event['title'].should == configuration[:calendar_event_name] 48 | 49 | calendar = client.find_calendar(configuration[:calendar_name]) 50 | calendar.should be_kind_of(Hash) 51 | calendar['id'].should == @calendar['id'] 52 | end 53 | end 54 | 55 | context "when calendar_name is nil" do 56 | before(:each) do 57 | configuration.delete(:calendar_name) 58 | end 59 | 60 | it "should create event in default calendar" do 61 | event = client.create_event 62 | 63 | event.should be_kind_of(Hash) 64 | event['title'].should == configuration[:calendar_event_name] 65 | event['selfLink'].should include(Capistrano::Calendar::Client::Google::CALENDAR_EVENTS_URL) 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'capistrano' 2 | require 'capistrano/calendar/client' 3 | 4 | require 'yaml' 5 | 6 | module Helper 7 | def self.load_credentials! 8 | @credentials = YAML.load_file('.credentials.yml') 9 | rescue Errno::ENOENT => e 10 | puts "Put your credentials into '.credentials.yml'. Example:" 11 | puts({ 12 | :google => { 13 | :username => 'vasya.pukin@gmail.com', 14 | :password => '123456' 15 | } 16 | }.to_yaml) 17 | 18 | exit(1) 19 | end 20 | 21 | def self.credentials 22 | @credentials 23 | end 24 | end 25 | 26 | Helper.load_credentials! 27 | --------------------------------------------------------------------------------