mourningdove/bin/worker/codebuild-notifier
2026-05-24 01:03:05 +00:00

211 lines
5.8 KiB
Perl
Executable file

#!/usr/bin/perl
#
# bin/worker/sns-notifier
#
# Listens on SNS queues and takes some action when a message comes in.
#
# Authors:
# Mark Smith <mark@dreamwidth.org>
#
# Copyright (c) 2019 by Dreamwidth Studios, LLC.
#
# This program is free software; you may redistribute it and/or modify it under
# the same terms as Perl itself. For a copy of the license, please reference
# 'perldoc perlartistic' or 'perldoc perlgpl'.
#
use strict;
use v5.10;
BEGIN {
require "$ENV{LJHOME}/cgi-bin/ljlib.pl";
}
use Log::Log4perl;
my $log = Log::Log4perl->get_logger(__PACKAGE__);
use List::Util qw/ sum /;
use LWP::UserAgent;
use Paws;
use POSIX qw/ strftime /;
use LJ::JSON;
my $HOOK_URL = $LJ::BUILD_NOTIFY_HOOK_URL or die;
my @ALL_PHASES = qw/
SUBMITTED QUEUED PROVISIONING DOWNLOAD_SOURCE INSTALL
PRE_BUILD POST_BUILD UPLOAD_ARTIFACTS FINALIZING COMPLETED
/;
my %PHASES = (
SUBMITTED => rgb( 255, 255, 128 ),
QUEUED => rgb( 128, 128, 255 ),
BUILD => rgb( 128, 128, 255 ),
COMPLETED => rgb( 128, 255, 128 ),
);
## Establish SQS connection
my $paws = Paws->new(
config => {
region => $LJ::SQS{region},
},
) or $log->logcroak('Failed to initialize Paws object.');
my $sqs = $paws->service('SQS')
or $log->logcroak('Failed to initialize Paws::SQS object.');
my $queue_url = $sqs->GetQueueUrl( QueueName => 'dw-codebuild-notifications' )->QueueUrl;
## Start listening for notifications, one by one
while (1) {
my $res = eval {
$sqs->ReceiveMessage(
QueueUrl => $queue_url,
MaxNumberOfMessages => 1,
WaitTimeSeconds => 20
);
};
if ( $@ && $@->isa('Paws::Exception') ) {
die $@->Message;
}
my $messages = $res->Messages;
$log->warn( 'Got ', scalar(@$messages), ' messages.' );
next unless @$messages;
foreach my $message (@$messages) {
my $msg = LJ::JSON::from_json( $message->Body );
my $d = $msg->{detail};
my $ai = $d->{'additional-information'};
my $project = $d->{'project-name'} // '(unknown)';
my $current = $d->{'current-phase'};
my $completed = $d->{'completed-phase'};
my $next =
$ai->{phases}
? $ai->{phases}[-1]{'phase-type'}
: undef;
my ( $phase, $message );
if ( $current eq 'SUBMITTED' ) {
# Job has been submitted (very first phase)
$phase = $current;
$message = 'Build request submitted, waiting for a VM...';
}
elsif ( $current eq 'COMPLETED' ) {
# Job is fully complete
$phase = $current;
$message = 'Upload completed, build was successful!';
}
elsif ( $completed eq 'BUILD' ) {
# Job has finished building, moving on to uploading
$phase = $completed;
$message = 'Build stage complete, uploading artifact.';
}
elsif ( $completed eq 'QUEUED' ) {
# Job has been assigned resources and is beginning to execute
#
# This might be nice if we ever see lag between SUBMITTED and QUEUED,
# but in all our experience so far, it's instant. Let's not send it
# for now.
#
# $phase = $completed;
# $message = 'Resources allocated, provisioning build environment.';
}
next unless $phase;
my $color = $PHASES{$phase};
my $seconds = $d->{'completed-phase-duration-seconds'};
my $elapsed =
$ai->{phases}
? sum( map { $_->{'duration-in-seconds'} + 0 } @{ $ai->{phases} } )
: undef;
my $embed = make_embed(
$project, $message, $color,
commit => github_url( $ai->{'source-version'} ),
duration => ( $seconds ? timestr($seconds) : '--' ),
elapsed => ( $elapsed ? timestr($elapsed) : '--' ),
);
send_webhook($embed) or die;
}
my $idx = 0;
$sqs->DeleteMessageBatch(
QueueUrl => $queue_url,
Entries => [ map { { Id => $idx++, ReceiptHandle => $_->ReceiptHandle } } @$messages ]
);
if ( $@ && $@->isa('Paws::Exception') ) {
die $@->Message;
}
}
sub make_embed {
my ( $project, $msg, $color, @fields ) = @_;
my @embed_fields;
while ( my @field = splice @fields, 0, 2 ) {
push @embed_fields,
{ name => $field[0], value => $field[1], inline => LJ::JSON->to_boolean(1) };
}
my $obj = {
'embeds' => [
{
'title' => sprintf( '[%s] build update', $project ),
'description' => $msg,
'fields' => \@embed_fields,
'color' => $color,
'footer' => {
'text' => 'AWS CodeBuild Notifier'
},
'timestamp' => strftime( '%Y-%m-%dT%H:%M:%S.000Z', gmtime ),
}
]
};
return $obj;
}
sub send_webhook {
my $obj = $_[0];
my $ua = LWP::UserAgent->new;
$ua->agent('Dreamwidth AWS CodeBuild Notifier <support@dreamwidth.org>');
my $res = $ua->post(
$HOOK_URL,
Content => LJ::JSON::to_json($obj),
'Content-Type' => 'application/json',
);
return 1 if $res->is_success;
die $res->decoded_content;
}
sub rgb {
return ( $_[0] << 16 ) + ( $_[1] << 8 ) + $_[2];
}
sub timestr {
my $secs = $_[0];
if ( $secs > 3600 ) {
return sprintf( '%dh%s', int( $secs / 3600 ), timestr( $secs % 3600 ) );
}
elsif ( $secs > 60 ) {
return sprintf( '%dm%s', int( $secs / 60 ), timestr( $secs % 60 ) );
}
else {
return sprintf( '%ds', $secs );
}
}
sub github_url {
my $hash = substr( $_[0], 0, 8 );
return qq|[$hash](https://github.com/dreamwidth/dreamwidth/commit/$hash)|;
}