codebuild-notifier 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,110 @@
1
+ # codebuild-notifier
2
+ # Copyright © 2018 Adam Alboyadjian <adam@cassia.tech>
3
+ # Copyright © 2018 Vista Higher Learning, Inc.
4
+ #
5
+ # codebuild-notifier is free software: you can redistribute it
6
+ # and/or modify it under the terms of the GNU General Public
7
+ # License as published by the Free Software Foundation, either
8
+ # version 3 of the License, or (at your option) any later version.
9
+ #
10
+ # codebuild-notifier is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13
+ # General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with codebuild-notifier. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+ require 'active_support'
19
+ require 'active_support/core_ext'
20
+ require 'aws-sdk-dynamodb'
21
+ require 'hashie'
22
+
23
+ module CodeBuildNotifier
24
+ class BuildHistory
25
+ attr_reader :config, :current_build
26
+
27
+ delegate :dynamo_table, to: :config
28
+ delegate :launched_by_retry?, to: :current_build
29
+
30
+ def initialize(config, current_build)
31
+ @config = config
32
+ @current_build = current_build
33
+ end
34
+
35
+ def last_entry
36
+ # If this build was launched using the Retry command from the console
37
+ # or api we don't have a Pull Request or branch name to use in the
38
+ # primary key, so we query by commit hash and project instead.
39
+ item = launched_by_retry? ? find_by_commit_and_project : find_by_id
40
+
41
+ # Provide .dot access to hash values from Dynamo item.
42
+ item && Hashie::Mash.new(item)
43
+ end
44
+
45
+ def write_entry(source_id)
46
+ updates = hash_to_dynamo_update(new_entry).merge(
47
+ key: { source_id: source_id }
48
+ )
49
+
50
+ yield updates if block_given?
51
+
52
+ dynamo_client.update_item(
53
+ updates.merge(table_name: dynamo_table)
54
+ )
55
+ end
56
+
57
+ # The commit hash and project code are used to find which Pull Request
58
+ # or branch the current build belongs to, and the previous build status
59
+ # for that Pull Request or branch.
60
+ private def find_by_commit_and_project
61
+ dynamo_client.query(
62
+ expression_attribute_values: {
63
+ ':commit_hash' => current_build.commit_hash,
64
+ ':project_code' => current_build.project_code
65
+ },
66
+ filter_expression: 'project_code = :project_code',
67
+ index_name: 'commit_hash_index',
68
+ key_condition_expression: 'commit_hash = :commit_hash',
69
+ table_name: dynamo_table
70
+ ).items.first
71
+ end
72
+
73
+ private def find_by_id
74
+ dynamo_client.get_item(
75
+ key: { 'source_id' => current_build.source_id },
76
+ table_name: dynamo_table
77
+ ).item
78
+ end
79
+
80
+ private def new_entry
81
+ {
82
+ commit_hash: current_build.commit_hash,
83
+ project_code: current_build.project_code,
84
+ status: current_build.status
85
+ }.tap do |memo|
86
+ # If launched via manual re-try instead of via a webhook, we don't
87
+ # want to overwrite the current source_ref value that tells us which
88
+ # branch or pull request originally created the dynamo record.
89
+ memo[:source_ref] = current_build.trigger unless launched_by_retry?
90
+ end
91
+ end
92
+
93
+ private def hash_to_dynamo_update(hash)
94
+ update = hash.each_with_object(
95
+ expression_attribute_names: {},
96
+ expression_attribute_values: {},
97
+ update_expression: []
98
+ ) do |(key, value), memo|
99
+ memo[:expression_attribute_names]["##{key}"] = key.to_s
100
+ memo[:expression_attribute_values][":#{key}"] = value
101
+ memo[:update_expression] << "##{key} = :#{key}"
102
+ end
103
+ update.merge(update_expression: "SET #{update[:update_expression].join(', ')}")
104
+ end
105
+
106
+ private def dynamo_client
107
+ @dynamo_client || Aws::DynamoDB::Client.new(region: config.region)
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,52 @@
1
+ # codebuild-notifier
2
+ # Copyright © 2018 Adam Alboyadjian <adam@cassia.tech>
3
+ # Copyright © 2018 Vista Higher Learning, Inc.
4
+ #
5
+ # codebuild-notifier is free software: you can redistribute it
6
+ # and/or modify it under the terms of the GNU General Public
7
+ # License as published by the Free Software Foundation, either
8
+ # version 3 of the License, or (at your option) any later version.
9
+ #
10
+ # codebuild-notifier is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13
+ # General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with codebuild-notifier. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+ module CodeBuildNotifier
19
+ class Config
20
+ DEFAULT_WHITELIST = %w[master release]
21
+
22
+ attr_reader :additional_channel, :dynamo_table, :region, :slack_admins,
23
+ :slack_secret_name, :whitelist_branches
24
+
25
+ # Configuration values specific to CodeBuild Notifier. CBN_ prefix is
26
+ # used because ENV vars with CODEBUILD_ prefix are reserved for use by AWS.
27
+ def initialize(
28
+ additional_channel: ENV['CBN_ADDITIONAL_CHANNEL'],
29
+ dynamo_table: ENV['CBN_DYNAMO_TABLE'] || 'branch-build-status',
30
+ region: ENV['CBN_AWS_REGION'] || ENV['AWS_REGION'],
31
+ slack_admins: ENV['CBN_SLACK_ADMIN_USERNAMES'],
32
+ slack_secret_name: ENV['CBN_SLACK_SECRET_NAME'] || 'slack/codebuild',
33
+ whitelist_branches: ENV['CBN_WHITELIST_BRANCHES']
34
+ )
35
+ @additional_channel = additional_channel
36
+ @dynamo_table = dynamo_table
37
+ @region = region
38
+ @slack_admins = slack_admins&.split(',') || []
39
+ @slack_secret_name = slack_secret_name
40
+ @whitelist_branches = whitelist_branches&.split(',') || DEFAULT_WHITELIST
41
+ end
42
+
43
+ # Match the format of the CodeBuild trigger variable
44
+ def non_pr_branch_ids
45
+ whitelist_branches.map { |name| "branch/#{name}" }
46
+ end
47
+
48
+ def whitelist
49
+ whitelist_branches.join(', ')
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,69 @@
1
+ # codebuild-notifier
2
+ # Copyright © 2018 Adam Alboyadjian <adam@cassia.tech>
3
+ # Copyright © 2018 Vista Higher Learning, Inc.
4
+ #
5
+ # codebuild-notifier is free software: you can redistribute it
6
+ # and/or modify it under the terms of the GNU General Public
7
+ # License as published by the Free Software Foundation, either
8
+ # version 3 of the License, or (at your option) any later version.
9
+ #
10
+ # codebuild-notifier is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13
+ # General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with codebuild-notifier. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+ module CodeBuildNotifier
19
+ class CurrentBuild
20
+ attr_reader :build_id, :commit_hash, :git_repo_url, :status_code, :trigger
21
+
22
+ # Default values are extracted from CODEBUILD_* ENV vars present in each
23
+ # CodeBuild # job container.
24
+ def initialize(
25
+ build_id: ENV['CODEBUILD_BUILD_ID'],
26
+ commit_hash: ENV['CODEBUILD_RESOLVED_SOURCE_VERSION'],
27
+ git_repo: ENV['CODEBUILD_SOURCE_REPO_URL'],
28
+ status_code: ENV['CODEBUILD_BUILD_SUCCEEDING'],
29
+ trigger: ENV['CODEBUILD_WEBHOOK_TRIGGER']
30
+ )
31
+ @build_id = build_id
32
+ @commit_hash = commit_hash
33
+ # Handle repos specified with and without optional .git suffix.
34
+ @git_repo_url = git_repo.to_s.gsub(/\.git\z/, '')
35
+ @status_code = status_code
36
+ @trigger = trigger
37
+ end
38
+
39
+ def status
40
+ status_code.to_s == '1' ? 'SUCCEEDED' : 'FAILED'
41
+ end
42
+
43
+ def project_code
44
+ @project_code ||= build_id.split(':').first
45
+ end
46
+
47
+ # If trigger is empty, this build was launched using the Retry command from
48
+ # the console or api.
49
+ def launched_by_retry?
50
+ trigger.to_s.empty?
51
+ end
52
+
53
+ def for_pr?
54
+ %r{^pr/}.match?(trigger.to_s)
55
+ end
56
+
57
+ # source_id, the primary key, is a composite of project_code and
58
+ # trigger.
59
+ # e.g.:
60
+ # my-app_ruby2-4:branch/master
61
+ # my-app_ruby2-3:pr/4056
62
+ # project_code forms part of the key to support having repos with
63
+ # multiple projects, for example, with different buildspec files for
64
+ # different ruby versions, or for rspec vs cucumber.
65
+ def source_id
66
+ "#{project_code}:#{trigger}"
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,25 @@
1
+ # codebuild-notifier
2
+ # Copyright © 2018 Adam Alboyadjian <adam@cassia.tech>
3
+ # Copyright © 2018 Vista Higher Learning, Inc.
4
+ #
5
+ # codebuild-notifier is free software: you can redistribute it
6
+ # and/or modify it under the terms of the GNU General Public
7
+ # License as published by the Free Software Foundation, either
8
+ # version 3 of the License, or (at your option) any later version.
9
+ #
10
+ # codebuild-notifier is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13
+ # General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with codebuild-notifier. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+ module CodeBuildNotifier
19
+ module Git
20
+ def current_commit
21
+ `git show -s --format='%h|%aN|%aE|%cE|%s'`.chomp.split('|')
22
+ end
23
+ module_function :current_commit
24
+ end
25
+ end
@@ -0,0 +1,98 @@
1
+ # codebuild-notifier
2
+ # Copyright © 2018 Adam Alboyadjian <adam@cassia.tech>
3
+ # Copyright © 2018 Vista Higher Learning, Inc.
4
+ #
5
+ # codebuild-notifier is free software: you can redistribute it
6
+ # and/or modify it under the terms of the GNU General Public
7
+ # License as published by the Free Software Foundation, either
8
+ # version 3 of the License, or (at your option) any later version.
9
+ #
10
+ # codebuild-notifier is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13
+ # General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with codebuild-notifier. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+ module CodeBuildNotifier
19
+ class SlackMessage
20
+ attr_reader :author_email, :author_name, :build, :committer_email,
21
+ :commit_message_subject, :config, :short_hash, :source_ref
22
+
23
+ def initialize(build, config, source_ref)
24
+ @build = build
25
+ @config = config
26
+ @source_ref = source_ref
27
+ @short_hash, @author_name, @author_email,
28
+ @committer_email, @commit_message_subject = git_info
29
+ end
30
+
31
+ def payload
32
+ {
33
+ color: slack_color,
34
+ fallback: [title, body].join("\n"),
35
+ title: title,
36
+ text: body
37
+ }
38
+ end
39
+
40
+ def recipients
41
+ [author_email, committer_email].uniq
42
+ end
43
+
44
+ def additional_channel
45
+ !build.for_pr? && config.additional_channel
46
+ end
47
+
48
+ private def git_info
49
+ Git.current_commit
50
+ end
51
+
52
+ private def slack_color
53
+ {
54
+ 'FAILED' => 'danger',
55
+ 'SUCCEEDED' => 'good'
56
+ }[build.status]
57
+ end
58
+
59
+ private def title
60
+ "#{slack_icon} #{author_name}'s " \
61
+ "<#{details_url}|#{build.project_code} build> - " \
62
+ "#{build.status.downcase}"
63
+ end
64
+
65
+ private def slack_icon
66
+ {
67
+ 'FAILED' => ':broken_heart:',
68
+ 'SUCCEEDED' => ':green_heart:'
69
+ }[build.status]
70
+ end
71
+
72
+ private def details_url
73
+ 'https://console.aws.amazon.com/codesuite/codebuild/projects/' \
74
+ "#{build.project_code}/build/#{build.build_id}/log?region=#{config.region}"
75
+ end
76
+
77
+ private def body
78
+ "commit #{commit_link} (#{commit_message_subject}) in " \
79
+ "#{source_ref_link}"
80
+ end
81
+
82
+ private def commit_link
83
+ "<#{build.git_repo_url}/commit/#{build.commit_hash}|#{short_hash}>"
84
+ end
85
+
86
+ private def source_ref_link
87
+ "<#{build.git_repo_url}/#{url_path}|#{source_ref}>"
88
+ end
89
+
90
+ private def url_path
91
+ if %r{\Apr/}.match?(source_ref)
92
+ "pull/#{source_ref[3..-1]}"
93
+ else
94
+ "tree/#{source_ref.gsub(%r{\Abranch/}, '')}"
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,97 @@
1
+ # codebuild-notifier
2
+ # Copyright © 2018 Adam Alboyadjian <adam@cassia.tech>
3
+ # Copyright © 2018 Vista Higher Learning, Inc.
4
+ #
5
+ # codebuild-notifier is free software: you can redistribute it
6
+ # and/or modify it under the terms of the GNU General Public
7
+ # License as published by the Free Software Foundation, either
8
+ # version 3 of the License, or (at your option) any later version.
9
+ #
10
+ # codebuild-notifier is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13
+ # General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with codebuild-notifier. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+ require 'aws-sdk-secretsmanager'
19
+ require 'slack-ruby-client'
20
+
21
+ module CodeBuildNotifier
22
+ class SlackSender
23
+ attr_reader :config
24
+
25
+ def initialize(config)
26
+ @config = config
27
+ end
28
+
29
+ def send(message)
30
+ Slack.configure { |slack_config| slack_config.token = app_token }
31
+ channel = message.additional_channel
32
+ if channel
33
+ channel = "##{channel}" unless /\A#/.match?(channel)
34
+ post_message(message, channel)
35
+ end
36
+
37
+ message.recipients.each do |email|
38
+ slack_user_id = find_slack_user(email)
39
+ slack_user_id && post_message(message, slack_user_id)
40
+ end
41
+ end
42
+
43
+ private def post_message(message, channel)
44
+ slack_client.chat_postMessage(
45
+ as_user: app_is_bot_user?,
46
+ attachments: [message.payload],
47
+ channel: channel
48
+ )
49
+ end
50
+
51
+ private def admin_send(message)
52
+ config.slack_admins.each do |username|
53
+ username = "@#{username}" unless /\A@/.match?(username)
54
+ slack_client.chat_postMessage(
55
+ as_user: false,
56
+ text: message,
57
+ channel: username
58
+ )
59
+ end
60
+ end
61
+
62
+ private def find_slack_user(email)
63
+ lookup_response = slack_client.users_lookupByEmail(email: email)
64
+ lookup_response.user.id
65
+ rescue Slack::Web::Api::Errors::SlackError => e
66
+ admin_send(
67
+ "Slack user lookup by email for #{email} failed with " \
68
+ "error: #{e.message}"
69
+ )
70
+ nil
71
+ end
72
+
73
+ # If the app token starts with xoxb- then it is a Bot User Oauth token
74
+ # and slack notifications should be posted with as_user: true. If it
75
+ # starts with xoxp- then it's an app token not associated with a user,
76
+ # and as_user: should be false.
77
+ private def app_is_bot_user?
78
+ /\Axoxb/.match?(app_token)
79
+ end
80
+
81
+ private def secrets_client
82
+ Aws::SecretsManager::Client.new(region: config.region)
83
+ end
84
+
85
+ private def slack_client
86
+ @slack_client ||= Slack::Web::Client.new
87
+ end
88
+
89
+ private def app_token
90
+ @app_token ||= JSON.parse(secret.secret_string)['token']
91
+ end
92
+
93
+ private def secret
94
+ secrets_client.get_secret_value(secret_id: config.slack_secret_name)
95
+ end
96
+ end
97
+ end