-
Notifications
You must be signed in to change notification settings - Fork 39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Travis Build Failure monitor(ing) #469
Changes from all commits
dad56ec
23487ec
c4a7323
945c53b
c550b0a
b7440e6
d68b6a4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
require 'travis' | ||
|
||
class TravisBranchMonitor | ||
include Sidekiq::Worker | ||
sidekiq_options :queue => :miq_bot, :retry => false | ||
|
||
include Sidetiq::Schedulable | ||
recurrence { hourly.minute_of_hour(0, 15, 30, 45) } | ||
|
||
include SidekiqWorkerMixin | ||
|
||
class << self | ||
private | ||
|
||
# For this class, sometimes the repo needs to be mapped to a specific | ||
# gitter room, so a hash is required. | ||
# | ||
# This override allows for doing this in the config | ||
# | ||
# travis_branch_monitor: | ||
# included_repos: | ||
# ManageIQ/manageiq-ui-classic: ManageIQ/ui | ||
# ManageIQ/manageiq-gems-pending: ManageIQ/core | ||
# ManageIQ/manageiq: | ||
# ManageIQ/miq_bot: | ||
# | ||
# Which you are allowed to leave the value empty, and the key will be used | ||
# where appropriate (not used in this class). | ||
# | ||
# The result from the above for this method will then be: | ||
# | ||
# [ | ||
# [ | ||
# "ManageIQ/manageiq-ui-classic", | ||
# "ManageIQ/manageiq-gems-pending", | ||
# "ManageIQ/manageiq", | ||
# "ManageIQ/miq_bot" | ||
# ], | ||
# [] | ||
# ] | ||
# | ||
def included_and_excluded_repos | ||
super # just used for error handling... | ||
|
||
[ | ||
settings.included_repos.try(:to_h).try(:stringify_keys).try(:keys), | ||
settings.excluded_repos.try(:to_h).try(:stringify_keys).try(:keys) | ||
] | ||
end | ||
end | ||
|
||
def perform | ||
if !first_unique_worker? | ||
logger.info("#{self.class} is already running, skipping") | ||
else | ||
process_repos | ||
end | ||
end | ||
|
||
def process_repos | ||
enabled_repos.each do |repo| | ||
process_repo(repo) | ||
end | ||
end | ||
|
||
def process_repo(repo) | ||
repo.regular_branch_names.each do |branch_record| | ||
process_branch(repo, branch_record) | ||
end | ||
end | ||
|
||
def process_branch(repo, branch_record) | ||
# If we already have a failure record, call notify with that record | ||
return branch_record.notify_of_failure if branch_record.previously_failing? | ||
|
||
# otherwise, check if any builds exist with a failures, and if so, update | ||
# the branch_record to add the `travis_build_failure_id`. | ||
v3_client = TravisV3Client.new(:repo => Travis::Repository.find(repo.name)) | ||
branch_builds = v3_client.repo_branch_builds(branch_record.name) | ||
|
||
if branch_builds.first.failed? | ||
first_failure = find_first_recent_failure(branch_builds) | ||
branch_record.update(:travis_build_failure_id => first_failure.id) | ||
|
||
branch_record.notify_of_failure | ||
end | ||
end | ||
|
||
private | ||
|
||
def find_first_recent_failure(builds) | ||
builds.take_while(&:failed?).last | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
class AddBranchBuildFailures < ActiveRecord::Migration[5.2] | ||
def change | ||
add_column :branches, :travis_build_failure_id, :integer | ||
add_column :branches, :last_build_failure_notified_at, :datetime | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
require 'travis' | ||
require 'gitter/api' | ||
|
||
class BuildFailureNotifier | ||
def self.gitter | ||
@gitter ||= begin | ||
api_url = Settings.gitter.api_url | ||
client_settings = { | ||
:token => Settings.gitter_credentials.token, | ||
:api_prefix => Settings.gitter.api_prefix, | ||
:api_uri => api_url && URI(api_url) | ||
} | ||
|
||
Gitter::API::Client.new(client_settings) | ||
end | ||
end | ||
|
||
def self.repo_room_map | ||
settings = Settings.travis_branch_monitor | ||
repo_map = settings.included_repos.to_h.merge(settings.excluded_repos.to_h) | ||
|
||
repo_map.stringify_keys! | ||
repo_map.each do |repo, room_uri| | ||
repo_map[repo] = repo if room_uri.nil? | ||
end | ||
end | ||
|
||
attr_reader :branch, :build, :repo, :repo_path, :room | ||
|
||
def initialize(branch) | ||
@branch = branch | ||
@repo_path = branch.repo.name | ||
@repo = Travis::Repository.find(repo_path) | ||
@room = repo_room_map[repo_path] | ||
@build = repo.session.find_one(Travis::Client::Build, branch.travis_build_failure_id) | ||
end | ||
|
||
def post_failure | ||
notification_msg = <<~MSG | ||
> ### :red_circle: Build Failure in #{repo_branches_markdown_url}! | ||
> | ||
> **Travis Build**: #{travis_build_url} | ||
MSG | ||
notification_msg << "> **Failure PR**: #{offending_pr}\n" if offending_pr | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not always a PR to that repo which causes the failure. Sometimes a merge to core breaks the API repo, but it is only noticed after the API repo cron runs. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That is why the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. However, I am open to suggestions on how we can improve this to better provide context for the message. I realize that this could still display a false positive by displaying the "last merged commit" on something that was merged weeks ago, so maybe only display the PR if it was merged recently? Within the last 3 days? |
||
notification_msg << "> **Commit**: #{commit_url}\n" if commit_url | ||
notification_msg << "> **Compare**: #{compare_url}\n" if compare_url | ||
|
||
gitter_room.send_message(notification_msg) | ||
end | ||
|
||
def report_passing | ||
notification_msg = <<~MSG | ||
> ### :green_heart: #{repo_branches_markdown_url} now passing! | ||
MSG | ||
|
||
gitter_room.send_message(notification_msg) | ||
end | ||
|
||
private | ||
|
||
def gitter | ||
self.class.gitter | ||
end | ||
|
||
def repo_room_map | ||
self.class.repo_room_map | ||
end | ||
|
||
# join room if needed, otherwise returns room | ||
def gitter_room | ||
@gitter_room ||= gitter.join_room(room) | ||
end | ||
|
||
def travis_build_url | ||
"https://travis-ci.org/#{repo_path}/builds/#{build.id}" | ||
end | ||
|
||
# find the PR that caused this mess... | ||
def offending_pr | ||
if build.commit && build.commit.message =~ /^Merge pull request #(\d+)/ | ||
"https://github.com/#{repo_path}/issues/#{$1}" | ||
end | ||
end | ||
|
||
def commit_url | ||
if build.commit | ||
"https://github.com/#{repo_path}/commit/#{build.commit.sha[0, 8]}" | ||
end | ||
end | ||
|
||
def compare_url | ||
build.commit.compare_url if build.commit && build.commit.compare_url | ||
end | ||
|
||
def repo_branches_markdown_url | ||
"[`#{repo_path}`](https://travis-ci.org/#{repo_path}/branches)" | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
namespace :travis_branch_monitor do | ||
task :poll_single => :environment do | ||
TravisBranchMonitor.new.perform | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
require 'travis' | ||
|
||
# A helper class for making v3 API class to the TravisCI API, since the current | ||
# API mostly only makes v2 calls, and does a decent amount of inificient ones | ||
# at that... | ||
# | ||
# Expects for most calls that a base Travis::Entity has been set for the given | ||
# client call. Passed in as a hash during initialization. | ||
# | ||
class TravisV3Client | ||
attr_reader :connection, :repo, :user_agent | ||
|
||
def initialize(base_entities = {}) | ||
@connection = Faraday.new(:url => Travis::Client::ORG_URI) | ||
|
||
@connection.headers['Authorization'] = "token #{::Settings.travis.access_token}" | ||
@connection.headers['travis-api-version'] = '3' | ||
|
||
@repo = base_entities[:repo] | ||
|
||
set_user_agent | ||
end | ||
|
||
def repo_branch_builds(branch = "master", params = {}) | ||
query_params = { | ||
"branch.name" => branch, | ||
"build.event_type" => "push,api,cron" | ||
}.merge(params) | ||
|
||
data = connection.get("/repo/#{repo.id}/builds", query_params) | ||
repo.session.load(JSON.parse(data.body))["builds"] | ||
end | ||
|
||
private | ||
|
||
# Use the Travis user agent for this | ||
def set_user_agent | ||
base_model = repo | ||
if base_model | ||
agent_string = repo.session.headers['User-Agent'] | ||
@connection.headers['User-Agent'] = agent_string | ||
agent_string | ||
end | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we check that it's still failing first?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this probably is a "naming is hard" problem, but
.notify_of_failure
actually does that. It does the.passing?
check which will determine if a repo is still failing for that given branch.I do forget the rational for my logic as to "why" I split it up like this, so I will have to look at this again, but the logic does as you wish, it just is a little unclear.