File Coverage

blib/lib/App/GitHooks/Plugin/NotifyReleasesToSlack.pm
Criterion Covered Total %
statement 33 125 26.4
branch 0 18 0.0
condition 0 6 0.0
subroutine 11 19 57.8
pod 6 6 100.0
total 50 174 28.7


line stmt bran cond sub pod time code
1             package App::GitHooks::Plugin::NotifyReleasesToSlack;
2              
3 1     1   14019 use strict;
  1         2  
  1         28  
4 1     1   3 use warnings;
  1         2  
  1         22  
5              
6 1     1   3 use feature 'state';
  1         5  
  1         73  
7              
8 1     1   3 use base 'App::GitHooks::Plugin';
  1         1  
  1         475  
9              
10             # External dependencies.
11 1     1   615 use CPAN::Changes;
  1         9583  
  1         39  
12 1     1   765 use Data::Dumper;
  1         6775  
  1         60  
13 1     1   511 use JSON qw();
  1         7606  
  1         23  
14 1     1   515 use LWP::UserAgent;
  1         27449  
  1         32  
15 1     1   459 use Log::Any qw($log);
  1         1149  
  1         2  
16 1     1   502 use Try::Tiny;
  1         910  
  1         42  
17              
18             # Internal dependencies.
19 1     1   371 use App::GitHooks::Constants qw( :PLUGIN_RETURN_CODES );
  1         2577  
  1         1006  
20              
21             # Uncomment to see debug information.
22             #use Log::Any::Adapter ('Stderr');
23              
24              
25             =head1 NAME
26              
27             App::GitHooks::Plugin::NotifyReleasesToSlack - Notify Slack channels of new releases pushed from a repository.
28              
29              
30             =head1 DESCRIPTION
31              
32             If you maintain a changelog file, and tag your release commits, you can use
33             this plugin to send the release notes to Slack channels.
34              
35             Here is a practical scenario:
36              
37             =over 4
38              
39             =item 1.
40              
41             Install C.
42              
43             =item 2.
44              
45             Set up an incoming webhook in Slack. This should give you a URL to post
46             messages to, with a format similar to
47             C.
48              
49             =item 3.
50              
51             Configure the plugin in your C<.githooksrc> file:
52              
53             [NotifyReleasesToSlack]
54             slack_post_url = ...
55             slack_channels = #releases, #test
56             changelog_path = Changes
57              
58             =item 4.
59              
60             Add release notes in your changelog file:
61              
62             v1.0.0 2015-04-12
63             - Added first feature.
64             - Added second feature.
65              
66             =item 5.
67              
68             Commit your release notes:
69              
70             git commit Changelog -m 'Release version 1.0.0.'
71              
72             =item 6.
73              
74             Tag your release:
75              
76             git tag v1.0.0
77             git push origin v1.0.0
78              
79             =item 7.
80              
81             Watch the notification appear in the corresponding Slack channel(s):
82              
83             release-notes BOT: @channel - Release v1.0.0 of test_repo:
84             - Added first feature.
85             - Added second feature.
86              
87             =back
88              
89             =head1 VERSION
90              
91             Version 1.0.2
92              
93             =cut
94              
95             our $VERSION = '1.0.2';
96              
97              
98             =head1 CONFIGURATION OPTIONS
99              
100             This plugin supports the following options in the C<[NotifyReleasesToSlack]>
101             section of your C<.githooksrc> file.
102              
103             [NotifyReleasesToSlack]
104             slack_post_url = https://hooks.slack.com/services/.../.../...
105             slack_channels = #releases, #test
106             changelog_path = Changes
107              
108              
109             =head2 slack_post_url
110              
111             After you set up a new incoming webhook in Slack, check under "Integration
112             settings" for the following information: "Webhook URL", "Send your JSON
113             payloads to this URL". This is the URL you need to set as the value for the
114             C config option.
115              
116             slack_post_url = https://hooks.slack.com/services/.../.../...
117              
118              
119             =head2 slack_channels
120              
121             The comma-separated list of channels to send release notifications to.
122              
123             slack_channels = #releases, #test
124              
125             Don't forget to prefix the channel names with '#'. It may still work without
126             it, but some keywords are reserved by Slack and you may see inconsistent
127             behaviors between channels.
128              
129              
130             =head2 changelog_path
131              
132             The path to the changelog file, relative to the root of the repository.
133              
134             For example, if the changelog file is named C and lives at the root of
135             your repository:
136              
137             changelog_path = Changes
138              
139              
140             =head1 METHODS
141              
142             =head2 run_pre_push()
143              
144             Code to execute as part of the pre-push hook.
145              
146             my $plugin_return_code = App::GitHooks::Plugin::NotifyReleasesToSlack->run_pre_push(
147             app => $app,
148             stdin => $stdin,
149             );
150              
151             Arguments:
152              
153             =over 4
154              
155             =item * $app I<(mandatory)>
156              
157             An C object.
158              
159             =item * $stdin I<(mandatory)>
160              
161             The content provided by git on stdin, corresponding to a list of references
162             being pushed.
163              
164             =back
165              
166             =cut
167              
168             sub run_pre_push
169             {
170 0     0 1   my ( $class, %args ) = @_;
171 0           my $app = delete( $args{'app'} );
172 0           my $stdin = delete( $args{'stdin'} );
173 0           my $config = $app->get_config();
174              
175 0           $log->info( 'Entering NotifyReleasesToSlack.' );
176              
177             # Verify that the mandatory config options are present.
178 0           my $config_return = verify_config( $config );
179 0 0         return $config_return
180             if defined( $config_return );
181              
182             # Check if we are pushing any tags.
183 0           my @tags = get_pushed_tags( $app, $stdin );
184 0           $log->infof( "Found %s tag(s) to push.", scalar( @tags ) );
185 0 0         if ( scalar( @tags ) == 0 )
186             {
187 0           $log->info( "No tags were found in the list of references to push." );
188 0           return $PLUGIN_RETURN_SKIPPED;
189             }
190              
191             # Get the list of releases in the changelog.
192 0           my $releases = get_changelog_releases( $app );
193 0           $log->infof( "Found %s release(s) in the changelog file.", scalar( keys %$releases ) );
194              
195             # Determine the name of the repository.
196 0           my $remote_name = get_remote_name( $app );
197 0           $log->infof( "The repository's remote name is %s.", $remote_name );
198              
199             # Analyze tags.
200 0           foreach my $tag ( @tags )
201             {
202             # Check if there's an entry in the changelog.
203 0           my $release = $releases->{ $tag };
204 0 0         if ( !defined( $release ) )
205             {
206 0           $log->infof( "No release found in the changelog for tag '%s'.", $tag );
207 0           next;
208             }
209 0           $log->infof( "Found release notes for %s.", $tag );
210              
211             # Serialize release notes.
212 0           my $serialized_notes = join(
213             "\n",
214 0           map { $_->serialize() } $release->group_values()
215             );
216              
217             # Notify Slack.
218 0           notify_slack(
219             $app,
220             sprintf(
221             "*Release %s of %s:*\n%s",
222             $tag,
223             $remote_name,
224             $serialized_notes,
225             ),
226             );
227             }
228              
229 0           return $PLUGIN_RETURN_PASSED;
230             }
231              
232              
233             =head1 FUNCTIONS
234              
235             =head2 verify_config()
236              
237             Verify that the mandatory options are defined in the current githooksrc config.
238              
239             my $plugin_return_code = App::GitHooks::Plugin::NotifyReleasesToSlack::verify_config(
240             $config
241             );
242              
243             Arguments:
244              
245             =over 4
246              
247             =item * $config I<(mandatory)>
248              
249             An C object.
250              
251             =back
252              
253             =cut
254              
255             sub verify_config
256             {
257 0     0 1   my ( $config ) = @_;
258              
259             # Check if a Slack post url is defined in the config.
260 0           my $slack_post_url = $config->get( 'NotifyReleasesToSlack', 'slack_post_url' );
261 0 0         if ( !defined( $slack_post_url ) )
262             {
263 0           $log->info('No Slack post URL defined in the [NotifyReleasesToSlack] section, skipping plugin.');
264 0           return $PLUGIN_RETURN_SKIPPED;
265             }
266              
267             # Check if Slack channels are defined in the config.
268 0           my $slack_channels = $config->get( 'NotifyReleasesToSlack', 'slack_channels' );
269 0 0         if ( !defined( $slack_channels ) )
270             {
271 0           $log->info('No Slack channels to post to defined in the [NotifyReleasesToSlack] section, skipping plugin.');
272 0           return $PLUGIN_RETURN_SKIPPED;
273             }
274              
275             # Check if a changelog is defined in the config.
276 0           my $changelog_path = $config->get( 'NotifyReleasesToSlack', 'changelog_path' );
277 0 0         if ( !defined( $changelog_path ) )
278             {
279 0           $log->info( "'changelog_path' is not defined in the [NotifyReleasesToSlack] section of your .githooksrc config." );
280 0           return $PLUGIN_RETURN_SKIPPED;
281             }
282              
283 0           return undef;
284             }
285              
286              
287             =head2 get_remote_name()
288              
289             Get the name of the repository.
290              
291             my $remote_name = App::GitHooks::Plugin::NotifyReleasesToSlack::get_remote_name(
292             $app
293             );
294              
295             Arguments:
296              
297             =over 4
298              
299             =item * $app I<(mandatory)>
300              
301             An C object.
302              
303             =back
304              
305             =cut
306              
307             sub get_remote_name
308             {
309 0     0 1   my ( $app ) = @_;
310 0           my $repository = $app->get_repository();
311              
312             # Retrieve the remote path.
313 0           $log->info('run git');
314 0   0       my $remote = $repository->run( qw( config --get remote.origin.url ) ) // '';
315 0           $log->info('run git');
316              
317             # Extract the remote name.
318 0           my ( $remote_name ) = ( $remote =~ /\/(.*?)\.git$/i );
319 0   0       $remote_name //= '(no remote found)';
320 0           $log->info('run git');
321              
322 0           return $remote_name;
323             }
324              
325              
326             =head2 notify_slack()
327              
328             Display a notification in the Slack channels defined in the config file.
329              
330             App::GitHooks::Plugin::NotifyReleasesToSlack::notify_slack(
331             $app,
332             $message,
333             );
334              
335             Arguments:
336              
337             =over 4
338              
339             =item * $app I<(mandatory)>
340              
341             An C object.
342              
343             =item * $message I<(mandatory)>
344              
345             The message to display in Slack channels.
346              
347             =back
348              
349             =cut
350              
351             sub notify_slack
352             {
353 0     0 1   my ( $app, $message ) = @_;
354 0           my $config = $app->get_config();
355              
356             # Get the list of channels to notify
357 0           state $slack_channels =
358             [
359             split(
360             /\s*,\s*/,
361             $config->get( 'NotifyReleasesToSlack', 'slack_channels' )
362             )
363             ];
364              
365             # Notify Slack channels.
366 0           foreach my $channel ( @$slack_channels )
367             {
368 0           $log->infof( 'Notifying Slack channel %s: %s', $channel, $message );
369              
370             # Prepare payload for the request.
371 0           my $request_payload =
372             JSON::encode_json(
373             {
374             text => $message,
375             channel => $channel,
376             }
377             );
378              
379             # Prepare request.
380 0           my $request = HTTP::Request->new(
381             POST => $config->get( 'NotifyReleasesToSlack', 'slack_post_url' ),
382             );
383 0           $request->content( $request_payload );
384              
385             # Send request to Slack.
386 0           my $user_agent = LWP::UserAgent->new();
387 0           my $response = $user_agent->request( $request );
388              
389             # If the connection is down, or Slack is down, warn the user.
390 0 0         if ( !$response->is_success() )
391             {
392 0           my $error = sprintf(
393             "Failed to notify channel '%s' with message '%s'.\n>>> %s %s.",
394             $channel,
395             $message,
396             $response->code(),
397             $response->message(),
398             );
399              
400             # Notify any logging systems.
401 0           $log->error( $error );
402              
403             # Notify the user who is pushing tags.
404 0           print STDERR "$error\n";
405             }
406             }
407              
408 0           return;
409             }
410              
411              
412             =head2 get_changelog_releases()
413              
414             Retrieve a hashref of all the releases in the changelog file.
415              
416             my $releases = App::GitHooks::Plugin::NotifyReleasesToSlack::get_changelog_releases(
417             $app
418             );
419              
420             Arguments:
421              
422             =over 4
423              
424             =item * $app I<(mandatory)>
425              
426             An C object.
427              
428             =back
429              
430             =cut
431              
432             sub get_changelog_releases
433             {
434 0     0 1   my ( $app ) = @_;
435 0           my $repository = $app->get_repository();
436 0           my $config = $app->get_config();
437              
438             # Make sure the changelog file exists.
439 0           my $changelog_path = $config->get( 'NotifyReleasesToSlack', 'changelog_path' );
440 0           $changelog_path = $repository->work_tree() . '/' . $changelog_path;
441 0 0         die "The changelog '$changelog_path' specified in your .githooksrc config does not exist in the repository\n"
442             if ! -e $changelog_path;
443 0           $log->infof( "Using changelog '%s'.", $changelog_path );
444              
445             # Read the changelog.
446             my $changes =
447             try
448             {
449 0     0     return CPAN::Changes->load( $changelog_path );
450             }
451             catch
452             {
453 0     0     $log->error( "Unable to parse the change log" );
454 0           die "Unable to parse the change log\n";
455 0           };
456 0           $log->info( 'Successfully parsed the change log file.' );
457              
458             # Organize the releases into a hash for easy lookup.
459 0           my $releases =
460             {
461 0           map { $_->version() => $_ }
462             $changes->releases()
463             };
464              
465 0           return $releases;
466             }
467              
468              
469             =head2 get_pushed_tags()
470              
471             Retrieve a list of the tags being pushed with C.
472              
473             my @tags = App::GitHooks::Plugin::NotifyReleasesToSlack::get_pushed_tags(
474             $app,
475             $stdin,
476             );
477              
478             Arguments:
479              
480             =over 4
481              
482             =item * $app I<(mandatory)>
483              
484             An C object.
485              
486             =item * $stdin I<(mandatory)>
487              
488             The content provided by git on stdin, corresponding to a list of references
489             being pushed.
490              
491             =back
492              
493             =cut
494              
495             sub get_pushed_tags
496             {
497 0     0 1   my ( $app, $stdin ) = @_;
498 0           my $config = $app->get_config();
499              
500             # Tag pattern.
501 0   0       my $git_tag_regex = $config->get_regex( 'NotifyReleasesToSlack', 'git_tag_regex' )
502             // '(v\d+\.\d+\.\d+)';
503 0           $log->infof( "Using git tag regex '%s'.", $git_tag_regex );
504              
505             # Analyze each reference being pushed.
506 0           my $tags = {};
507 0           foreach my $line ( @$stdin )
508             {
509 0           chomp( $line );
510 0           $log->debugf( 'Parse STDIN line >%s<.', $line );
511              
512             # Extract the tag information.
513 0           my ( $tag ) = ( $line =~ /^refs\/tags\/$git_tag_regex\b/x );
514 0 0         next if !defined( $tag );
515 0           $log->infof( "Found tag '%s'.", $tag );
516 0           $tags->{ $tag } = 1;
517             }
518              
519 0           return keys %$tags;
520             }
521              
522              
523             =head1 BUGS
524              
525             Please report any bugs or feature requests through the web interface at
526             L.
527             I will be notified, and then you'll automatically be notified of progress on
528             your bug as I make changes.
529              
530              
531             =head1 SUPPORT
532              
533             You can find documentation for this module with the perldoc command.
534              
535             perldoc App::GitHooks::Plugin::NotifyReleasesToSlack
536              
537              
538             You can also look for information at:
539              
540             =over
541              
542             =item * GitHub's request tracker
543              
544             L
545              
546             =item * AnnoCPAN: Annotated CPAN documentation
547              
548             L
549              
550             =item * CPAN Ratings
551              
552             L
553              
554             =item * MetaCPAN
555              
556             L
557              
558             =back
559              
560              
561             =head1 AUTHOR
562              
563             L,
564             C<< >>.
565              
566              
567             =head1 COPYRIGHT & LICENSE
568              
569             Copyright 2013-2015 Guillaume Aubert.
570              
571             This program is free software: you can redistribute it and/or modify it under
572             the terms of the GNU General Public License version 3 as published by the Free
573             Software Foundation.
574              
575             This program is distributed in the hope that it will be useful, but WITHOUT ANY
576             WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
577             PARTICULAR PURPOSE. See the GNU General Public License for more details.
578              
579             You should have received a copy of the GNU General Public License along with
580             this program. If not, see http://www.gnu.org/licenses/
581              
582             =cut
583              
584             1;