From 1d8bb0764bb14072c2bac953b21fd9516bbc9baa Mon Sep 17 00:00:00 2001
From: Eelco Dolstra <eelco.dolstra@logicblox.com>
Date: Wed, 8 May 2013 17:30:30 +0200
Subject: [PATCH] Add a plugin mechanism

You can now add plugins to Hydra by writing a module called
Hydra::Plugin::<whatever> and putting it in Perl's search path.  The
only plugin operation currently supported in buildFinished, called
when hydra-build has finished doing a build.

For instance, a Twitter notification plugin would look like this:

  package Hydra::Plugin::TwitterNotification;

  sub buildFinished {
      my ($self, $db, $config, $build, $dependents) = @_;
      print STDERR "tweeting about build ", $build->id, "\n";
      # send tweet...
  }

  1;
---
 src/lib/Hydra/Plugin.pm                   |  14 ++
 src/lib/Hydra/Plugin/EmailNotification.pm | 166 +++++++++++++++++++++
 src/script/hydra-build                    | 170 ++--------------------
 3 files changed, 191 insertions(+), 159 deletions(-)
 create mode 100644 src/lib/Hydra/Plugin.pm
 create mode 100644 src/lib/Hydra/Plugin/EmailNotification.pm

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;
 }