robopigeon 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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