codebuild-notifier 0.2.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.
- 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
|