diff --git a/src/lib/Hydra/Plugin.pm b/src/lib/Hydra/Plugin.pm new file mode 100644 index 00000000..5d7f5c4b --- /dev/null +++ b/src/lib/Hydra/Plugin.pm @@ -0,0 +1,14 @@ +package Hydra::Plugin; + +use Module::Pluggable + search_path => "Hydra::Plugin", + require => 1; + +# $plugin->buildFinished($db, $config, $build, $dependents): +# +# Called when build $build has finished. If the build failed, then +# $dependents is an array ref to a list of builds that have also +# failed as a result (i.e. because they depend on $build or a failed +# dependeny of $build). + +1; diff --git a/src/lib/Hydra/Plugin/EmailNotification.pm b/src/lib/Hydra/Plugin/EmailNotification.pm new file mode 100644 index 00000000..2bbe1dd9 --- /dev/null +++ b/src/lib/Hydra/Plugin/EmailNotification.pm @@ -0,0 +1,166 @@ +package Hydra::Plugin::EmailNotification; + +use strict; +use feature qw/switch/; +use POSIX qw(strftime); +use Email::Sender::Simple qw(sendmail); +use Email::Sender::Transport::SMTP; +use Email::Simple; +use Email::Simple::Creator; +use Sys::Hostname::Long; +use Text::Table; +use File::Slurp; +use Hydra::Helper::Nix; + + +sub statusDescription { + my ($buildstatus) = @_; + + my $status = "Failed"; + given ($buildstatus) { + when (0) { $status = "Success"; } + when (1) { $status = "Failed with non-zero exit code"; } + when (2) { $status = "Dependency failed"; } + when (4) { $status = "Cancelled"; } + } + + return $status; +} + + +sub buildFinished { + my ($self, $db, $config, $build, $dependents) = @_; + + die unless $build->finished; + + my $prevBuild; + ($prevBuild) = $db->resultset('Builds')->search( + { project => $build->project->name + , jobset => $build->jobset->name + , job => $build->job->name + , system => $build->system + , finished => 1 + , id => { '<', $build->id } + , -not => { buildstatus => { -in => [4, 3]} } + }, { order_by => ["id DESC"] } + ); + + # Do we want to send mail? + unless ($ENV{'HYDRA_FORCE_SEND_MAIL'}) { + return unless $build->jobset->enableemail && ($build->maintainers ne "" || $build->jobset->emailoverride ne ""); + + # If build is cancelled or aborted, do not send email. + return if $build->buildstatus == 4 || $build->buildstatus == 3; + + # If there is a previous (that is not cancelled or aborted) build + # with same buildstatus, do not send email. + return if defined $prevBuild && ($build->buildstatus == $prevBuild->buildstatus); + } + + # Send mail. + # !!! should use the Template Toolkit here. + + my $to = (!$build->jobset->emailoverride eq "") ? $build->jobset->emailoverride : $build->maintainers; + print STDERR "sending mail notification to ", $to, "\n"; + + my $jobName = $build->project->name . ":" . $build->jobset->name . ":" . $build->job->name; + + my $status = statusDescription($build->buildstatus); + + my $baseurl = hostname_long; + my $sender = $config->{'notification_sender'} || + (($ENV{'USER'} || "hydra") . "@" . $baseurl); + + my $selfURI = $config->{'base_uri'} || "http://localhost:3000"; + + sub showTime { my ($x) = @_; return strftime('%Y-%m-%d %H:%M:%S', localtime($x)); } + + my $infoTable = Text::Table->new({ align => "left" }, \ " | ", { align => "left" }); + my @lines = ( + [ "Build ID:", $build->id ], + [ "Nix name:", $build->nixname ], + [ "Short description:", $build->description || '(not given)' ], + [ "Maintainer(s):", $build->maintainers ], + [ "System:", $build->system ], + [ "Derivation store path:", $build->drvpath ], + [ "Output store path:", join(", ", map { $_->path } $build->buildoutputs) ], + [ "Time added:", showTime $build->timestamp ], + ); + push @lines, ( + [ "Build started:", showTime $build->starttime ], + [ "Build finished:", showTime $build->stoptime ], + [ "Duration:", $build->stoptime - $build->starttime . "s" ], + ) if $build->starttime; + $infoTable->load(@lines); + + my $inputsTable = Text::Table->new( + { title => "Name", align => "left" }, \ " | ", + { title => "Type", align => "left" }, \ " | ", + { title => "Value", align => "left" }); + @lines = (); + foreach my $input ($build->inputs) { + my $type = $input->type; + push @lines, + [ $input->name + , $input->type + , ( $input->type eq "build" || $input->type eq "sysbuild") + ? $input->dependency->id + : ($input->type eq "string" || $input->type eq "boolean") + ? $input->value : ($input->uri . ':' . $input->revision) + ]; + } + $inputsTable->load(@lines); + + my $loglines = 50; + my $logtext = logContents($build->drvpath, $loglines); + $logtext = removeAsciiEscapes($logtext); + + my $body = "Hi,\n" + . "\n" + . "This is to let you know that Hydra build " . $build->id + . " of job " . $jobName . " " . (defined $prevBuild ? "has changed from '" . statusDescription($prevBuild->buildstatus) . "' to '$status'" : "is '$status'" ) .".\n" + . "\n" + . "Complete build information can be found on this page: " + . "$selfURI/build/" . $build->id . "\n" + . ($build->buildstatus != 0 ? "\nThe last $loglines lines of the build log are shown at the bottom of this email.\n" : "") + . "\n" + . "A summary of the build information follows:\n" + . "\n" + . $infoTable->body + . "\n" + . "The build inputs were:\n" + . "\n" + . $inputsTable->title + . $inputsTable->rule('-', '+') + . $inputsTable->body + . "\n" + . "Regards,\n\nThe Hydra build daemon.\n" + . ($build->buildstatus != 0 ? "\n---\n$logtext" : ""); + + # stripping trailing spaces from lines + $body =~ s/[\ ]+$//gm; + + my $email = Email::Simple->create( + header => [ + To => $to, + From => "Hydra Build Daemon <$sender>", + Subject => "$status: Hydra job $jobName on " . $build->system . ", build " . $build->id, + 'X-Hydra-Instance' => $baseurl, + 'X-Hydra-Project' => $build->project->name, + 'X-Hydra-Jobset' => $build->jobset->name, + 'X-Hydra-Job' => $build->job->name, + 'X-Hydra-System' => $build->system + ], + body => "", + ); + $email->body_set($body); + + if (defined $ENV{'HYDRA_MAIL_SINK'}) { + # For testing, redirect all mail to a file. + write_file($ENV{'HYDRA_MAIL_SINK'}, { append => 1 }, $email->as_string . "\n"); + } else { + sendmail($email); + } +} + +1; diff --git a/src/script/hydra-build b/src/script/hydra-build index 5c17e652..91cfa3b4 100755 --- a/src/script/hydra-build +++ b/src/script/hydra-build @@ -5,20 +5,11 @@ use List::MoreUtils qw(all); use File::Basename; use File::stat; use Nix::Store; +use Hydra::Plugin; use Hydra::Schema; use Hydra::Helper::Nix; use Hydra::Model::DB; use Hydra::Helper::AddBuilds; -use Email::Sender::Simple qw(sendmail); -use Email::Sender::Transport::SMTP; -use Email::Simple; -use Email::Simple::Creator; -use Sys::Hostname::Long; -use Config::General; -use Text::Table; -use POSIX qw(strftime); -use Data::Dump qw(dump); -use feature qw/switch/; STDOUT->autoflush(); @@ -27,153 +18,6 @@ my $db = Hydra::Model::DB->new(); my $config = getHydraConfig(); -sub statusDescription { - my ($buildstatus) = @_; - - my $status = "Failed"; - given ($buildstatus) { - when (0) { $status = "Success"; } - when (1) { $status = "Failed with non-zero exit code"; } - when (2) { $status = "Dependency failed"; } - when (4) { $status = "Cancelled"; } - } - - return $status; -} - - -sub sendEmailNotification { - my ($build) = @_; - - die unless $build->finished; - - return unless $build->jobset->enableemail && ($build->maintainers ne "" || $build->jobset->emailoverride ne ""); - - # Do we want to send mail? - - my $prevBuild; - ($prevBuild) = $db->resultset('Builds')->search( - { project => $build->project->name - , jobset => $build->jobset->name - , job => $build->job->name - , system => $build->system - , finished => 1 - , id => { '<', $build->id } - , -not => { buildstatus => { -in => [4, 3]} } - }, { order_by => ["id DESC"] } - ); - - # If build is cancelled or aborted, do not send email. - return if $build->buildstatus == 4 || $build->buildstatus == 3; - - # If there is a previous (that is not cancelled or aborted) build - # with same buildstatus, do not send email. - return if defined $prevBuild && ($build->buildstatus == $prevBuild->buildstatus); - - # Send mail. - # !!! should use the Template Toolkit here. - - print STDERR "sending mail notification to ", $build->maintainers, "\n"; - - my $jobName = $build->project->name . ":" . $build->jobset->name . ":" . $build->job->name; - - my $status = statusDescription($build->buildstatus); - - my $baseurl = hostname_long; - my $sender = $config->{'notification_sender'} || - (($ENV{'USER'} || "hydra") . "@" . $baseurl); - - my $selfURI = $config->{'base_uri'} || "http://localhost:3000"; - - sub showTime { my ($x) = @_; return strftime('%Y-%m-%d %H:%M:%S', localtime($x)); } - - my $infoTable = Text::Table->new({ align => "left" }, \ " | ", { align => "left" }); - my @lines = ( - [ "Build ID:", $build->id ], - [ "Nix name:", $build->nixname ], - [ "Short description:", $build->description || '(not given)' ], - [ "Maintainer(s):", $build->maintainers ], - [ "System:", $build->system ], - [ "Derivation store path:", $build->drvpath ], - [ "Output store path:", join(", ", map { $_->path } $build->buildoutputs) ], - [ "Time added:", showTime $build->timestamp ], - ); - push @lines, ( - [ "Build started:", showTime $build->starttime ], - [ "Build finished:", showTime $build->stoptime ], - [ "Duration:", $build->stoptime - $build->starttime . "s" ], - ) if $build->starttime; - $infoTable->load(@lines); - - my $inputsTable = Text::Table->new( - { title => "Name", align => "left" }, \ " | ", - { title => "Type", align => "left" }, \ " | ", - { title => "Value", align => "left" }); - @lines = (); - foreach my $input ($build->inputs) { - my $type = $input->type; - push @lines, - [ $input->name - , $input->type - , ( $input->type eq "build" || $input->type eq "sysbuild") - ? $input->dependency->id - : ($input->type eq "string" || $input->type eq "boolean") - ? $input->value : ($input->uri . ':' . $input->revision) - ]; - } - $inputsTable->load(@lines); - - my $loglines = 50; - my $logtext = logContents($build->drvpath, $loglines); - $logtext = removeAsciiEscapes($logtext); - - my $body = "Hi,\n" - . "\n" - . "This is to let you know that Hydra build " . $build->id - . " of job " . $jobName . " " . (defined $prevBuild ? "has changed from '" . statusDescription($prevBuild->buildstatus) . "' to '$status'" : "is '$status'" ) .".\n" - . "\n" - . "Complete build information can be found on this page: " - . "$selfURI/build/" . $build->id . "\n" - . ($build->buildstatus != 0 ? "\nThe last $loglines lines of the build log are shown at the bottom of this email.\n" : "") - . "\n" - . "A summary of the build information follows:\n" - . "\n" - . $infoTable->body - . "\n" - . "The build inputs were:\n" - . "\n" - . $inputsTable->title - . $inputsTable->rule('-', '+') - . $inputsTable->body - . "\n" - . "Regards,\n\nThe Hydra build daemon.\n" - . ($build->buildstatus != 0 ? "\n---\n$logtext" : ""); - - # stripping trailing spaces from lines - $body =~ s/[\ ]+$//gm; - - my $to = (!$build->jobset->emailoverride eq "") ? $build->jobset->emailoverride : $build->maintainers; - my $email = Email::Simple->create( - header => [ - To => $to, - From => "Hydra Build Daemon <$sender>", - Subject => "$status: Hydra job $jobName on " . $build->system . ", build " . $build->id, - 'X-Hydra-Instance' => $baseurl, - 'X-Hydra-Project' => $build->project->name, - 'X-Hydra-Jobset' => $build->jobset->name, - 'X-Hydra-Job' => $build->job->name, - 'X-Hydra-System' => $build->system - ], - body => "", - ); - $email->body_set($body); - - print $email->as_string if $ENV{'HYDRA_MAIL_TEST'}; - - sendmail($email); -} - - sub addBuildStepOutputs { my ($step) = @_; my $drv = derivationFromPath($step->drvpath); @@ -444,7 +288,14 @@ sub doBuild { }); - sendEmailNotification $build; + foreach my $plugin (Hydra::Plugin->plugins) { + eval { + $plugin->buildFinished($db, $config, $build, []); + }; + if ($@) { + print STDERR "$plugin->buildFinished: $@\n"; + } + } } @@ -452,7 +303,8 @@ my $buildId = $ARGV[0] or die "syntax: $0 BUILD-ID\n"; print STDERR "performing build $buildId\n"; if ($ENV{'HYDRA_MAIL_TEST'}) { - sendEmailNotification $db->resultset('Builds')->find($buildId); + my $build = $db->resultset('Builds')->find($buildId); + $_->buildFinished($db, $config, $build, []) foreach Hydra::Plugin->plugins; exit 0; }