robopigeon 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/.gitlab-ci.yml +27 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +65 -0
  6. data/.ruby-version +1 -0
  7. data/.version +1 -0
  8. data/Gemfile +4 -0
  9. data/README.md +256 -0
  10. data/Rakefile +11 -0
  11. data/bin/console +14 -0
  12. data/bin/rake +29 -0
  13. data/bin/rspec +29 -0
  14. data/bin/rubocop +29 -0
  15. data/bin/setup +8 -0
  16. data/exe/robopigeon +22 -0
  17. data/lib/robopigeon/documentarian.rb +39 -0
  18. data/lib/robopigeon/dsl/helpers.rb +17 -0
  19. data/lib/robopigeon/dsl/initial_jobs.rb +36 -0
  20. data/lib/robopigeon/dsl/job.rb +34 -0
  21. data/lib/robopigeon/dsl.rb +46 -0
  22. data/lib/robopigeon/git/helper_dsl.rb +60 -0
  23. data/lib/robopigeon/git.rb +7 -0
  24. data/lib/robopigeon/gitlab/client.rb +42 -0
  25. data/lib/robopigeon/gitlab/dsl.rb +174 -0
  26. data/lib/robopigeon/gitlab/helper_dsl.rb +44 -0
  27. data/lib/robopigeon/gitlab/jira.rb +0 -0
  28. data/lib/robopigeon/gitlab/merge_request.rb +78 -0
  29. data/lib/robopigeon/gitlab.rb +29 -0
  30. data/lib/robopigeon/jira/client.rb +17 -0
  31. data/lib/robopigeon/jira/dsl.rb +81 -0
  32. data/lib/robopigeon/jira/helper_dsl.rb +24 -0
  33. data/lib/robopigeon/jira/ticket.rb +186 -0
  34. data/lib/robopigeon/jira/ticket_dsl.rb +155 -0
  35. data/lib/robopigeon/jira.rb +37 -0
  36. data/lib/robopigeon/markdown/helper_dsl.rb +73 -0
  37. data/lib/robopigeon/markdown.rb +11 -0
  38. data/lib/robopigeon/resources/initial_robopigeon.rb +156 -0
  39. data/lib/robopigeon/slack/attachments_dsl.rb +63 -0
  40. data/lib/robopigeon/slack/client.rb +46 -0
  41. data/lib/robopigeon/slack/dsl.rb +74 -0
  42. data/lib/robopigeon/slack/helper_dsl.rb +26 -0
  43. data/lib/robopigeon/slack/message.rb +35 -0
  44. data/lib/robopigeon/slack.rb +30 -0
  45. data/lib/robopigeon/version.rb +3 -0
  46. data/lib/robopigeon.rb +13 -0
  47. data/robopigeon +0 -0
  48. data/robopigeon.gemspec +47 -0
  49. data/robopigeon.rb +156 -0
  50. metadata +264 -0
@@ -0,0 +1,46 @@
1
+ require 'slack-ruby-client'
2
+
3
+ module RoboPigeon::Slack
4
+ class Client
5
+ @name = 'RoboPigeon'
6
+ @emoji = ':robot_face:'
7
+ @enabled = true
8
+
9
+ class << self
10
+ attr_accessor :name, :emoji, :enabled
11
+
12
+ def client
13
+ raise 'client requires API key be set' unless @client
14
+
15
+ @client
16
+ end
17
+
18
+ attr_writer :client
19
+
20
+ def api_key
21
+ raise 'api_key requires API key be set' unless client.token
22
+
23
+ client.token
24
+ end
25
+
26
+ def api_key=(api_key)
27
+ @client = Slack::Web::Client.new(token: api_key)
28
+ end
29
+
30
+ def get_user(search)
31
+ begin
32
+ users = client.users_search(user: search.downcase).try(:members)
33
+ rescue ::Faraday::Error => e
34
+ puts "Giving up on slack user lookup because the slack client raised a #{e.class}:\n#{e.message}"
35
+ users = nil
36
+ end
37
+
38
+ if users.nil? || users.empty? || users.length != 1
39
+ nil
40
+ else
41
+ users.try(:first)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,74 @@
1
+ module RoboPigeon::Dsl
2
+ class SlackRoot
3
+ def self.run(&block)
4
+ slack = RoboPigeon::Dsl::SlackRoot.new
5
+ slack.instance_eval(&block)
6
+ end
7
+
8
+ RoboPigeon::Documentarian.add_command(
9
+ 'enabled',
10
+ block: ['slack'],
11
+ params: [
12
+ { name: 'enabled', type: 'boolean', desc: 'should it run slack commands', example: 'false', default: 'true' }
13
+ ],
14
+ desc: 'Set whether or not to run slack commands, defaults to true'
15
+ )
16
+ def enabled(bool)
17
+ RoboPigeon::Slack::Client.enabled = bool
18
+ end
19
+
20
+ RoboPigeon::Documentarian.add_command('api_key', block: ['slack'], params: [{ name: 'api_key', type: 'String', desc: 'the api key for your slackbot', example: "xoxb-don't-actually-store-this-here", default: "ENV['SLACK_API_KEY']" }], desc: 'set your slack api key globally')
21
+ def api_key(key)
22
+ RoboPigeon::Slack::Client.api_key = key
23
+ end
24
+
25
+ RoboPigeon::Documentarian.add_command('name', block: ['slack'], params: [{ name: 'name', type: 'String', desc: 'the name your slackbot displays', example: 'RoboPigeon 9000', default: 'RoboPigeon' }], desc: 'set your slack bot name globally')
26
+ def name(name)
27
+ RoboPigeon::Slack::Client.name = name
28
+ end
29
+
30
+ RoboPigeon::Documentarian.add_command('emoji', block: ['slack'], params: [{ name: 'emoji', type: 'String', desc: 'the emoji icon your slackbot displays', example: 'RoboPigeon 9000', default: 'RoboPigeon' }], desc: 'set your slack bot emoji icon globally')
31
+ def emoji(emoji)
32
+ RoboPigeon::Slack::Client.emoji = emoji
33
+ end
34
+ end
35
+
36
+ class Slack < SlackRoot
37
+ include RoboPigeon::Dsl::Helpers
38
+ attr_accessor :message
39
+
40
+ def self.run(&block)
41
+ if RoboPigeon::Slack::Client.enabled
42
+ slack = RoboPigeon::Dsl::Slack.new
43
+ slack.instance_eval(&block)
44
+ slack.message.send!
45
+ else
46
+ puts 'Slack is disabled, please remove `enabled false` from your global slack config'
47
+ end
48
+ end
49
+
50
+ def initialize
51
+ self.message = RoboPigeon::Slack::Message.new
52
+ end
53
+
54
+ RoboPigeon::Documentarian.add_command('user', block: %w[job slack], params: [{ name: 'search', type: 'String', desc: 'the name, email, or slack handle of a user to search for', example: 'robopigeon@ives.dev' }], desc: 'add a single user recipient to your message, can be used multiple times')
55
+ def user(user)
56
+ message.users.push(RoboPigeon::Slack::Client.get_user(user).try(:id))
57
+ end
58
+
59
+ RoboPigeon::Documentarian.add_command('channel', block: %w[job slack], params: [{ name: 'channel', type: 'String', desc: 'the name of a channel to send the message to', example: '#team-pigeon' }], desc: 'add a single channel recipient to your message, can be used multiple times')
60
+ def channel(channel)
61
+ message.channels.push(channel)
62
+ end
63
+
64
+ RoboPigeon::Documentarian.add_command('text', block: %w[job slack], singular: true, params: [{ name: 'text', type: 'Text', desc: 'the text of the message you want to send', example: 'RoboPigeon failed a job! Oh no!' }], desc: 'Add a text message to the message you plan to send')
65
+ def text(text)
66
+ message.text = text
67
+ end
68
+
69
+ RoboPigeon::Documentarian.add_block('attachment', helpers: true, block: %w[job slack], desc: 'add an attachment to your slack message, can have multiple attachemnts')
70
+ def attachment(&block)
71
+ message.attachments.push(RoboPigeon::Dsl::SlackAttachment.run(&block))
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,26 @@
1
+ module RoboPigeon::Dsl
2
+ module Helpers
3
+ module Slack
4
+ RoboPigeon::Documentarian.add_command('slack_user_for', block: ['helpers'], params: [{ name: 'search', type: 'String', desc: 'the name, email, or slack handle of a user to search for', example: 'robopigeon@ives.dev' }], desc: 'Searches for a given user and returns a formatted slack message mention')
5
+ def slack_user_for(search)
6
+ uid = RoboPigeon::Slack::Client.get_user(search).try(:id)
7
+ return '' if uid.nil?
8
+
9
+ "<@#{uid}>"
10
+ end
11
+
12
+ RoboPigeon::Documentarian.add_command('slack_name_for', block: ['helpers'], params: [{ name: 'search', type: 'String', desc: 'the name, email, or slack handle of a user to search for', example: 'robopigeon@ives.dev' }], desc: 'Searches for a given user and returns their slack handle with an @')
13
+ def slack_name_for(search)
14
+ name = RoboPigeon::Slack::Client.get_user(search).try(:name)
15
+ return '' if uid.nil?
16
+
17
+ "@#{name}"
18
+ end
19
+
20
+ RoboPigeon::Documentarian.add_command('slack_user_group', block: ['helpers'], params: [{ name: 'id', type: 'String', desc: 'the usergroup id to mention', example: 'robopigeon@ives.dev' }], desc: 'a message formatted mention of the given slack group id')
21
+ def slack_user_group(id)
22
+ "<!subteam^#{id}>"
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,35 @@
1
+ module RoboPigeon::Slack
2
+ class Message
3
+ attr_accessor :users, :channels, :text, :attachments
4
+ def initialize
5
+ self.attachments = []
6
+ self.users = []
7
+ self.channels = []
8
+ end
9
+
10
+ def client
11
+ RoboPigeon::Slack::Client
12
+ end
13
+
14
+ def send_message(recipient)
15
+ raise 'No text or attachments set' if (text.nil? || text.empty?) && attachments.empty?
16
+
17
+ client.client.chat_postMessage(
18
+ channel: recipient,
19
+ text: text,
20
+ icon_emoji: client.emoji,
21
+ username: client.name,
22
+ attachments: attachments
23
+ )
24
+ end
25
+
26
+ def send!
27
+ users.each do |user|
28
+ send_message(user)
29
+ end
30
+ channels.each do |channel|
31
+ send_message(channel)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,30 @@
1
+ RoboPigeon::Documentarian.add_block('slack', helpers: true, block: [], desc: 'a configuration block for slack')
2
+ RoboPigeon::Documentarian.add_block('slack', helpers: true, block: ['job'], desc: 'configure a slack notification, sends on end, can have multiple')
3
+
4
+ require 'robopigeon/slack/client'
5
+ require 'robopigeon/slack/message'
6
+ require 'robopigeon/slack/dsl'
7
+ require 'robopigeon/slack/helper_dsl'
8
+ require 'robopigeon/slack/attachments_dsl'
9
+
10
+ module RoboPigeon::Dsl
11
+ module Helpers
12
+ include RoboPigeon::Dsl::Helpers::Slack
13
+ end
14
+ end
15
+
16
+ module RoboPigeon::Dsl
17
+ class Root
18
+ def slack(&block)
19
+ RoboPigeon::Dsl::SlackRoot.run(&block)
20
+ end
21
+ end
22
+ end
23
+
24
+ module RoboPigeon::Dsl
25
+ class Job
26
+ def slack(&block)
27
+ RoboPigeon::Dsl::Slack.run(&block)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,3 @@
1
+ module RoboPigeon
2
+ VERSION = File.read(File.join(__dir__, '../../.version')).freeze
3
+ end
data/lib/robopigeon.rb ADDED
@@ -0,0 +1,13 @@
1
+ require 'robopigeon/documentarian'
2
+ require 'robopigeon/dsl'
3
+ require 'robopigeon/git'
4
+ require 'robopigeon/gitlab'
5
+ require 'robopigeon/jira'
6
+ require 'robopigeon/markdown'
7
+ require 'robopigeon/slack'
8
+ require 'robopigeon/version'
9
+
10
+ module RoboPigeon
11
+ DEFAULT_FILE = "#{FileUtils.pwd}/robopigeon.rb".freeze
12
+ class SkippedJob < StandardError; end
13
+ end
data/robopigeon ADDED
File without changes
@@ -0,0 +1,47 @@
1
+ lib = File.expand_path('../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'robopigeon/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'robopigeon'
7
+ spec.version = RoboPigeon::VERSION
8
+ spec.authors = ['Alex Ives']
9
+ spec.email = ['alex.ives@granicus.com']
10
+
11
+ spec.summary = 'A set of tools for gitlab and other ci piplines'
12
+ spec.description = 'Gitlab ci, jenkins, bamboo, circleci all leave something to be desired - a tigher integration with notifications and git actions. Pigeon bot is a new tool to help you craft pipelines that give you notifications and get you the functionality of a full time bot, without having to run one.'
13
+ spec.homepage = 'https://gitlab.com/robopigeon/robopigeon'
14
+
15
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
16
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
17
+ if spec.respond_to?(:metadata)
18
+ spec.metadata['homepage_uri'] = spec.homepage
19
+ spec.metadata['source_code_uri'] = 'https://gitlab.com/robopigeon/robopigeon'
20
+ else
21
+ raise 'RubyGems 2.0 or newer is required to protect against ' \
22
+ 'public gem pushes.'
23
+ end
24
+
25
+ # Specify which files should be added to the gem when it is released.
26
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
27
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
28
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
29
+ end
30
+ spec.bindir = 'exe'
31
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
32
+ spec.require_paths = ['lib']
33
+
34
+ spec.add_dependency 'gitlab'
35
+ spec.add_dependency 'markdown2confluence'
36
+ spec.add_dependency 'picky'
37
+ spec.add_dependency 'slack-ruby-client'
38
+ spec.add_dependency 'slackdown', '~> 0.2'
39
+
40
+ spec.add_development_dependency 'bundler'
41
+ spec.add_development_dependency 'pry'
42
+ spec.add_development_dependency 'rake', '~> 10.0'
43
+ spec.add_development_dependency 'rspec', '~> 3.0'
44
+ spec.add_development_dependency 'rubocop'
45
+ spec.add_development_dependency 'vcr'
46
+ spec.add_development_dependency 'webmock'
47
+ end
data/robopigeon.rb ADDED
@@ -0,0 +1,156 @@
1
+ slack do
2
+ # Slack is currently disabled, make sure you have a valid bot token, then remove
3
+ # the following line
4
+ enabled false
5
+ # This is where you configure your global slack preferences, like bot name and api key
6
+ # You should always keep the api key either encrypted with something like `encryptatron`
7
+ # or in a ci environment variable (which is probably better)
8
+ api_key ENV['SLACK_API_KEY']
9
+ name 'RoboPigeon'
10
+ end
11
+
12
+ gitlab do
13
+ # GitLab is currently disabled, make sure you have a valid api token with acces to
14
+ # to your repo, then remove the following line
15
+ enabled false
16
+ # This is where you configure your global gitlab settings, like api url and api key
17
+ # always keep your api key either in an environment variable or encrypted with something
18
+ # something like `encryptatron` or rails secrets.
19
+ api_url ENV['CI_API_V4_URL']
20
+ api_key ENV['GITLAB_API_KEY'] # Always use an envrionment variable, don't check in secrets.
21
+ end
22
+
23
+ jira do
24
+ # Jira is currently disabled, make sure you have a valid api token with acces to
25
+ # to your repo, then remove the following line
26
+ enabled false
27
+ # This is where you configure your global jira settings, like api url and api token
28
+ # always keep your api key either in an environment variable or encrypted with
29
+ # something like `encryptatron` or rails secrets.
30
+ api_url ENV['JIRA_API_URL']
31
+ api_key ENV['JIRA_API_TOKEN'] # Always use an envrionment variable, don't check in secrets.
32
+ end
33
+
34
+ job 'deploy_complete', 'Notify slack and comment in gitlab that a deploy is complete' do
35
+ # This configures a slack notification, at the end of the block it sends to
36
+ # the users and channels in the body. Un-comment the channels and fill in the
37
+ # appropriate ones to have them sent there.
38
+ slack do
39
+ # channel '#your-team-channel'
40
+ # channel '#your-release-notifications-channel'
41
+ user ENV['GITLAB_USER_EMAIL']
42
+ # `git_committer_email` looks at the commit log and returns the email address
43
+ # for the last person to make a non-merge commit
44
+ user git_committer_email
45
+ # `git_merger_email` looks at the commit log and returns the email address
46
+ # for the last person to make a merge commit
47
+ user git_merger_email
48
+ # This configures the overall message to send, it comes first, before attachments.
49
+ # If you want to send multiple messages, you can use attachments with pretext in them
50
+ # It's not actually required to that you set a message in order to send attachments
51
+ message "#{slack_user_for(ENV['GITLAB_USER_EMAIL'])} deployed #{ENV['CI_PROJECT_NAME']} #{ENV['CI_COMMIT_TAG']} to #{ENV['CI_ENVIRONMENT_NAME']}"
52
+ attachment do
53
+ # You have to set a fallback text for the actions
54
+ fallback "<#{ENV['CI_PIPELINE_URL']}|Pipeline>"
55
+ # Actions show up as little buttons under the message, this one will be
56
+ # a green button that links to the pipeline.
57
+ action 'button', 'Pipeline', ENV['CI_PIPELINE_URL'], 'primary'
58
+ action 'button', 'Rollback', "#{ENV['CI_PROJECT_URL']}/environments", 'danger'
59
+ color 'good'
60
+ end
61
+ end
62
+
63
+ # This configures an action taken in gitlab. In this case, we're saying to use
64
+ # the last branch merged into this one as the branch name, and then comment on
65
+ # each merge request with that branch that someone has started deploying
66
+ gitlab do
67
+ merge_request_comment "#{ENV['GITLAB_USER_EMAIL']} deployed branch", git_branch_merged_source
68
+ end
69
+
70
+ # This configures an action taken in jira, In this case, we're looping over all
71
+ # of the tickets since the last deployment, and then adding a comment to them
72
+ # with what version it was released in.
73
+ jira do
74
+ # This helper gets all of the tickets referenced in the git log since the last
75
+ # deployment to the referenced environment, that contain the provided key
76
+ tickets_in_log_since_deployment_to('production', 'TIC').each do |ticket_id|
77
+ # Then it comments on them with the project that was deployed and the version
78
+ ticket ticket_id do
79
+ # Then it comments on them with the project that was deployed and the version
80
+ comment "#{ENV['CI_PROJECT_NAME']} had changes related to this deployed to production in version #{ENV['CI_COMMIT_TAG']}"
81
+ # Runs a transition called 'Release'
82
+ transition 'Release'
83
+ # and sets the fix version to the tag of the deployment
84
+ field 'Fix Version', ENV['CI_COMMIT_TAG']
85
+ end
86
+ end
87
+ end
88
+ end
89
+
90
+ # This example job creates a ticket, assignes it to whoever runs the pipeline, and
91
+ # then sends them a slack message with the id of the ticket
92
+ job 'create ticket' do
93
+ jira do
94
+ ticket do
95
+ # Set the project key
96
+ project 'PIG'
97
+ # Set the issue type
98
+ issuetype 'Bug'
99
+ # Set the summary of the ticket
100
+ summary 'Detecetd a defect! Fix it!'
101
+ # Assign the ticket to the user who is running the pipeline
102
+ assign ENV['GITLAB_USER_EMAIL']
103
+ # Set a description from a file template, that template can
104
+ # use any of the other dsl helpers like the git or deployment
105
+ # operations
106
+ description jira_from_md('path/to/file.erb.md')
107
+ # Create the ticket and store the id in a global variable
108
+ create!
109
+ # Print out the ticket number for logging/deubgging
110
+ puts 'Created ticket:'
111
+ print_id
112
+ end
113
+ end
114
+
115
+ slack do
116
+ user ENV['GITLAB_USER_EMAIL']
117
+ message "Created #{jira_last_created_ticket_slack_link}"
118
+ end
119
+ end
120
+
121
+ # Below are a number of pre-configured jobs that some people find useful!
122
+ job 'deploy_started', 'Notfiy slack and comment in gitlab that a deploy is starting' do
123
+ slack do
124
+ user ENV['GITLAB_USER_EMAIL']
125
+ user git_committer_email
126
+ user git_merger_email
127
+ message "#{slack_user_for(ENV['GITLAB_USER_EMAIL'])} deployed #{ENV['CI_PROJECT_NAME']} #{ENV['CI_COMMIT_TAG']} to #{ENV['CI_ENVIRONMENT_NAME']}"
128
+ attachment do
129
+ fallback "<#{ENV['CI_PIPELINE_URL']}|Pipeline>"
130
+ action 'button', 'Pipeline', ENV['CI_PIPELINE_URL'], 'primary'
131
+ color 'good'
132
+ end
133
+ end
134
+
135
+ gitlab do
136
+ merge_request_comment "#{ENV['GITLAB_USER_EMAIL']} has started deyploying this branch", git_branch_merged_source
137
+ end
138
+ end
139
+
140
+ job 'notify_failure', 'Notify slack and gitlab that a job has failed' do
141
+ slack do
142
+ # channel '#your-team-channel'
143
+ user ENV['GITLAB_USER_EMAIL']
144
+ user git_committer_email
145
+ attachment do
146
+ fallback "#{ENV['CI_PROJECT_NAME']} - #{ENV['CI_COMMIT_REF_NAME']} has a failure - <#{ENV['CI_PIPELINE_URL']}|Pipeline>"
147
+ title "#{ENV['CI_PROJECT_NAME']} - #{ENV['CI_COMMIT_REF_NAME']} has a failure"
148
+ action 'button', 'Pipeline', ENV['CI_PIPELINE_URL'], 'primary'
149
+ color 'danger'
150
+ end
151
+ end
152
+
153
+ gitlab do
154
+ merge_request_comment "The branch from this merge request has a failure - ENV['CI_PIPELINE_URL']", git_branch_merged_source
155
+ end
156
+ end