codebuild-notifier 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.rspec +1 -0
- data/.rubocop.yml +58 -0
- data/COPYING +674 -0
- data/Gemfile +3 -0
- data/README.md +261 -0
- data/bin/update-build-status +129 -0
- data/codebuild-notifier.gemspec +50 -0
- data/lib/codebuild-notifier.rb +24 -0
- data/lib/codebuild-notifier/build_history.rb +110 -0
- data/lib/codebuild-notifier/config.rb +52 -0
- data/lib/codebuild-notifier/current_build.rb +69 -0
- data/lib/codebuild-notifier/git.rb +25 -0
- data/lib/codebuild-notifier/slack_message.rb +98 -0
- data/lib/codebuild-notifier/slack_sender.rb +97 -0
- data/lib/codebuild-notifier/version.rb +20 -0
- data/lib/codebuild_notifier.rb +1 -0
- metadata +201 -0
@@ -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
|