#!/usr/bin/env ruby # codebuild-notifier # Copyright © 2018 Adam Alboyadjian # Copyright © 2018 Vista Higher Learning, Inc. # # codebuild-notifier is free software: you can redistribute it # and/or modify it under the terms of the GNU General Public # License as published by the Free Software Foundation, either # version 3 of the License, or (at your option) any later version. # # codebuild-notifier is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with codebuild-notifier. If not, see . require 'codebuild-notifier' require 'optparse' # This script updates a DynamoDb table with the last codebuild build status # for the current project and branch or pr. If the current build status is # different from the previous status, the email address of the author of the # last commit is extracted from the git commit, and a notification is sent # via slack. def quit(message) cb_puts(message) exit end def cb_puts(message) puts "CODEBUILD NOTIFIER: #{message}" end def not_latest_commit_in_branch_message(build, config) "Commit #{build.commit_hash} in project #{build.project_code} did " \ 'not match the most recent commit for any Pull Requests or the ' \ "whitelisted branches: #{config.whitelist}. Skipping status updates." end def not_pr_or_whitelisted_branch_message(config) 'Build is neither for a Pull Request nor for one of whitelisted ' \ "branches: #{config.whitelist}. Skipping status updates." end def no_status_diff_message(build) "Current status #{build.status} is same as last status. " \ 'Skipping slack notifications.' end command_line_opts = {} # rubocop:disable Metrics/BlockLength OptionParser.new do |opts| opts.banner = 'Usage: update-build-status [OPTIONS]' opts.on( '--additional-channel=CHANNEL', 'status notifications for whitelisted branches will be sent here ' \ 'as well as to author/committer' ) do |usernames| command_line_opts[:slack_admin_users] = usernames end opts.on( '--default-notify-strategy=STRATEGY', 'when to send notifications, in the absence of a branch-specific ' \ 'override -- valid options are: status_change (the default), ' \ 'every_build, fail_or_status_change' ) do |default_strategy| command_line_opts[:default_strategy] = default_strategy end opts.on( '--dynamo-table=TABLE', 'table for storing build statuses' ) do |table| command_line_opts[:dynamo_table] = table end opts.on( '--override-notify-strategy=BRANCH_STRATEGY_PAIRS', 'overrides default notify strategy; specify pairs of branch:strategy ' \ 'Valid strategies are: status_change, every_build, fail_or_status_change ' \ 'separate pairs with a comma' \ 'e.g. master:every_build,jira-15650:status_change' ) do |overrides| command_line_opts[:strategy_overrides] = overrides end opts.on( '--slack-admin-usernames=USERS', 'comma-separated list of slack users to be notified if build status ' \ 'notifications fail to send' ) do |usernames| command_line_opts[:slack_admin_users] = usernames end opts.on( '--slack-alias-table=TABLE', 'optional dynamodb table for storing alternate email addresses for when ' \ 'the commit author email is different from the address associated with ' \ 'their slack account; can also help with failed lookups for ' \ 'someuser@noreply.github.com' ) do |table| command_line_opts[:slack_alias_table] = table end opts.on( '--slack-secret-name=SECRET', 'name of Secrets Manager secret with slack app/bot auth token' ) do |slack_secret| command_line_opts[:slack_secret_name] = slack_secret end opts.on( '--whitelist-branches=BRANCHES', 'comma-separated list of branches that will have build notifications ' \ 'sent even if there is no open Pull Request' ) do |whitelist| command_line_opts[:whitelist_branches] = whitelist end opts.on('--region=REGION', 'AWS region') do |region| command_line_opts[:region] = region end end.parse! # rubocop:enable Metrics/BlockLength config = CodeBuildNotifier::Config.new(command_line_opts) build = CodeBuildNotifier::CurrentBuild.new history = CodeBuildNotifier::BuildHistory.new(config, build) last_build = history.last_entry build.previous_build = last_build if build.launched_by_retry? # Whenever a build is triggered by a PR or whitelisted branch, we update # the record for that trigger with the commit hash. If a build is # launched using Retry, and there is no history entry for the current # commit hash, then the build is not for a PR or a whitelisted branch, # and should not be tracked. quit(not_pr_or_whitelisted_branch_message(config)) unless last_build else # We only want to track information for whitelisted branches and branches # with open Pull Requests. unless config.non_pr_branch_ids.include?(build.trigger) || build.for_pr? quit(not_pr_or_whitelisted_branch_message(config)) end end history.write_entry(build.source_id) do |new_item| cb_puts "Updating dynamo table #{config.dynamo_table} with: #{new_item}" end status_changed = last_build&.status != build.status strategy = config.strategy_for_branch(build.branch_name) unless strategy == 'every_build' if strategy == 'status_change' quit(no_status_diff_message(build)) unless status_changed elsif strategy == 'fail_or_status_change' # TODO: Find good wording for a different exit message for this case. quit(no_status_diff_message(build)) if build.status == 'SUCCEEDED' && !status_changed end end slack_message = CodeBuildNotifier::SlackMessage.new(build, config) sender = CodeBuildNotifier::SlackSender.new(config) sender.send(slack_message)