File Coverage

blib/lib/Dist/Zilla/Role/TravisYML.pm
Criterion Covered Total %
statement 24 217 11.0
branch 0 116 0.0
condition 0 56 0.0
subroutine 8 15 53.3
pod n/a
total 32 404 7.9


line stmt bran cond sub pod time code
1             package Dist::Zilla::Role::TravisYML;
2              
3             our $AUTHORITY = 'cpan:BBYRD'; # AUTHORITY
4             our $VERSION = '1.15'; # VERSION
5             # ABSTRACT: Role for .travis.yml creation
6              
7 1     1   663 use v5.10;
  1         4  
  1         55  
8 1     1   6 use Moose::Role;
  1         2  
  1         9  
9              
10 1     1   5093 use MooseX::Has::Sugar;
  1         546  
  1         4  
11 1     1   90 use MooseX::Types::Moose qw{ ArrayRef Str Bool is_Bool };
  1         2  
  1         11  
12              
13 1     1   4946 use List::AllUtils qw{ first sum uniq };
  1         2  
  1         92  
14 1     1   4 use YAML qw{ Dump };
  1         2  
  1         49  
15              
16 1     1   2271 use Module::CoreList;
  1         35553  
  1         13  
17 1     1   656 use version 0.77;
  1         31  
  1         8  
18              
19             requires 'zilla';
20             requires 'logger';
21              
22             with 'Dist::Zilla::Role::MetaCPANInterfacer';
23              
24 0     0     sub log { shift->logger->log(@_) }
25 0     0     sub log_debug { shift->logger->log_debug(@_) }
26 0     0     sub log_fatal { shift->logger->log_fatal(@_) }
27              
28             # needs our to pass to mvp_multivalue_args
29             our @phases = qw(
30             before_install
31             install
32             after_install
33             before_script
34             script
35             after_script
36             after_success
37             after_failure
38             after_deploy
39             );
40             my @yml_order = (qw(
41             sudo
42             language
43             perl
44             env
45             matrix
46             branches
47             ), @phases, qw(
48             notifications
49             ));
50              
51              
52             ### HACK: Need these rw for ChainSmoking ###
53             has $_ => ( rw, isa => ArrayRef[Str], default => sub { [] } ) for (
54             map { $_, $_.'_dzil', $_.'_build' }
55             map { $_, 'pre_'.$_, 'post_'.$_ }
56             @phases
57             );
58              
59             has dzil_branch => ( rw, isa => Str );
60             has build_branch => ( rw, isa => Str, default => '/^build\/.*/' );
61             has notify_email => ( rw, isa => ArrayRef[Str], default => sub { [ 1 ] } );
62             has notify_irc => ( rw, isa => ArrayRef[Str], default => sub { [ 0 ] } );
63             has mvdt => ( rw, isa => Bool, default => 0 );
64             has test_authordeps => ( rw, isa => Bool, default => 0 );
65             has test_deps => ( rw, isa => Bool, default => 1 );
66             has support_builddir => ( rw, isa => Bool, default => 0 );
67             has sudo => ( rw, isa => Bool, default => 0 );
68              
69             has irc_template => ( rw, isa => ArrayRef[Str], default => sub { [
70             "%{branch}#%{build_number} by %{author}: %{message} (%{build_url})",
71             ] } );
72              
73             has perl_version => ( rw, isa => Str, default => '-blead 5.20 5.18 5.16 5.14 5.12 5.10 -5.8' );
74             has perl_version_build => ( rw, isa => Str, lazy, default => sub { shift->perl_version } );
75              
76             has _releases => ( ro, isa => ArrayRef[Str], lazy, default => sub {
77             my $self = shift;
78              
79             # Find the lowest required dependencies and tell Travis-CI to install them
80             my (%releases, %versions);
81             if ($self->mvdt) {
82             my $prereqs = $self->zilla->prereqs;
83             $self->log("Searching for minimum dependency versions");
84              
85             my $minperl = version->parse(
86             $prereqs->requirements_for('runtime', 'requires')->requirements_for_module('perl') ||
87             v5.8.8 # released in 2006... C'mon, people! Don't make me lower this!
88             );
89             foreach my $phase (qw( runtime configure build test )) {
90             $self->logger->set_prefix("{Phase '$phase'} ");
91             my $req = $prereqs->requirements_for($phase, 'requires');
92              
93             foreach my $module ( sort ($req->required_modules) ) {
94             next if $module eq 'perl'; # obvious
95              
96             my $modver = $req->requirements_for_module($module);
97             my ($distro, $release, $minver) = $self->_mcpan_module_minrelease($module, $modver);
98             next unless $release;
99             my $mod_in_perlver = Module::CoreList->first_release($module, $minver);
100              
101             if ($mod_in_perlver && $minperl >= $mod_in_perlver) {
102             $self->log_debug(['Module %s v%s is already found in core Perl v%s (<= v%s)', $module, $minver, $mod_in_perlver, $minperl]);
103             next;
104             }
105              
106             # Only install the latest version, in cases of a conflict between phases
107             if (!$versions{$distro} || $minver > $versions{$distro}) {
108             $releases{$distro} = $release;
109             $versions{$distro} = $minver;
110             $self->log_debug(['Found minimum dep version for Module %s as %s', $module, $release]);
111             }
112             else {
113             $self->log_debug(['Module %s v%s has a higher version due to be installed in %s v%s', $module, $minver, $distro, ''.$versions{$distro}]);
114             }
115             }
116             }
117             $self->logger->clear_prefix;
118             }
119              
120             return [ map { $releases{$_} } sort keys %releases ];
121             });
122              
123             sub build_travis_yml {
124 0     0     my ($self, $is_build_branch) = @_;
125              
126 0 0         my %travis_yml = (
    0          
127             sudo => $self->sudo ? 'true' : 'false',
128             language => 'perl',
129             matrix => { fast_finish => 'true' },
130             $self->support_builddir ? (
131             env => [ 'BUILD=0', 'BUILD=1' ],
132             ) : (),
133             );
134              
135 0           my $email = $self->notify_email->[0];
136 0           my $irc = $self->notify_irc->[0];
137 0           my $rmeta = $self->zilla->distmeta->{resources};
138              
139 0           my %notifications;
140              
141             # Perl versions
142 0           my (@perls, @perls_allow_failures);
143 0 0 0       if ($self->support_builddir && !$is_build_branch) { # dual DZIL+build YAML
144 0           @perls = uniq map { s/^\-//; $_ } split(/\s+/, $self->perl_version.' '.$self->perl_version_build);
  0            
  0            
145 0           @perls_allow_failures = (
146             (
147 0           map { +{ perl => $_, env => 'BUILD=0' } }
148 0           grep { s/^\-// }
149             split(/\s+/, $self->perl_version)
150             ), (
151 0           map { +{ perl => $_, env => 'BUILD=1' } }
152 0           grep { s/^\-// }
153             split(/\s+/, $self->perl_version_build)
154             )
155             );
156             }
157             else {
158 0 0         @perls = split(/\s+/, $is_build_branch ? $self->perl_version : $self->perl_version_build);
159 0           @perls_allow_failures =
160 0           map { +{ perl => $_ } }
161 0           grep { s/^\-// } # also strips the dash from @perls
162             @perls
163             ;
164             }
165 0           $travis_yml{perl} = \@perls;
166 0 0         $travis_yml{matrix}{allow_failures} = \@perls_allow_failures if @perls_allow_failures;
167              
168             # IRC
169 0 0 0 0     $irc eq "1" and $irc = $self->notify_irc->[0] = $rmeta->{ first { /irc$/i } keys %$rmeta } || "0";
  0            
170 0           s#^irc:|/+##gi for @{$self->notify_irc};
  0            
171              
172 0 0         if ($irc) {
173 0           my %irc = (
174             on_success => 'change',
175             on_failure => 'always',
176             use_notice => 'true',
177             );
178 0           $irc{channels} = [grep { $_ } @{$self->notify_irc}];
  0            
  0            
179 0           $irc{template} = [grep { $_ } @{$self->irc_template}];
  0            
  0            
180 0           $notifications{irc} = \%irc;
181             }
182              
183             # Email
184 0 0         $notifications{email} = ($email eq "0") ? "false" : [ grep { $_ } @{$self->notify_email} ]
  0 0          
  0            
185             unless ($email eq "1");
186              
187 0 0         $travis_yml{notifications} = \%notifications if %notifications;
188              
189             ### Prior to the custom mangling by the user, establish a default .travis.yml to work from
190 0           my %travis_code = (
191             common => { # run for both dzil *and* build
192             before_install => [ # install haarg's perl travis helpers
193             'export AUTOMATED_TESTING=1 NONINTERACTIVE_TESTING=1 HARNESS_OPTIONS=j10:c HARNESS_TIMER=1',
194             'git clone git://github.com/haarg/perl-travis-helper',
195             'source perl-travis-helper/init',
196             'build-perl',
197             'perl -V',
198             ],
199             },
200             dzil => {},
201             build => {},
202             );
203              
204             # needed for MDVT
205 0           my @releases = @{$self->_releases};
  0            
206 0           my @releases_install;
207 0 0         if (@releases) {
208 0           @releases_install = (
209             # Install the lowest possible required version for the dependencies
210             'export OLD_CPANM_OPT=$PERL_CPANM_OPT',
211             "export PERL_CPANM_OPT='--mirror http://cpan.metacpan.org/ --mirror http://search.cpan.org/CPAN' \$PERL_CPANM_OPT",
212 0           (map { 'cpanm --verbose ' .$_ } @releases), # first pass to force minimum versions
213 0           (map { 'cpanm --verbose --installdeps '.$_ } @releases), # second pass to make sure conflicting deps are handled correctly
214             'export PERL_CPANM_OPT=$OLD_CPANM_OPT',
215             );
216             }
217              
218             # DZIL Travis YAML
219              
220             # verbosity/testing and parallelized installs don't mix
221 0           my $notest_cmd = 'xargs -n 5 -P 10 cpanm --quiet --notest';
222 0           my $test_cmd = 'cpanm --verbose';
223              
224 0           $travis_code{dzil}{before_install} = [
225             # Fix for https://github.com/travis-ci/travis-cookbooks/issues/159
226             'git config --global user.name "TravisCI"',
227             'git config --global user.email $HOSTNAME":not-for-mail@travis-ci.org"',
228             ];
229 0 0         $travis_code{dzil}{install} = scalar(@releases) ? \@releases_install : [
    0          
    0          
230             "cpanm --quiet --notest --skip-satisfied Dist::Zilla", # this should already exist anyway...
231             "dzil authordeps --missing | grep -vP '[^\\w:]' | ".($self->test_authordeps ? $test_cmd : $notest_cmd),
232             "dzil listdeps --author --missing | grep -vP '[^\\w:]' | ".($self->test_deps ? $test_cmd : $notest_cmd),
233             ];
234 0           $travis_code{dzil}{script} = [
235             "dzil smoke --release --author",
236             ];
237              
238             # Build Travis YAML
239              
240 0           $travis_code{build}{before_install} = [
241             # Prevent any test problems with this file
242             'rm .travis.yml',
243             # Build tests shouldn't be considered "author testing"
244             'export AUTHOR_TESTING=0',
245             ];
246 0 0         $travis_code{build}{install} = scalar(@releases) ? \@releases_install : [
    0          
247             'cpanm --installdeps --verbose '.($self->test_deps ? '' : '--notest').' .',
248             ];
249              
250 0 0         if (my $bbranch = $self->build_branch) {
251 0           $travis_code{build}{branches} = { only => $bbranch };
252             }
253              
254             ### See if any custom code is requested
255              
256 0           foreach my $phase (@phases) {
257             # First, replace any new blocks, then deal with pre/post blocks
258 0           foreach my $ft ('', '_dzil', '_build') { # YML file type; specific wins priority
259 0           my $method = $phase.$ft;
260 0           my $custom_cmds = $self->$method;
261              
262 0 0 0       if ($custom_cmds && @$custom_cmds) {
263 0           foreach my $key ('dzil', 'build') {
264 0 0 0       next unless (!$ft || substr($ft, 1) eq $key);
265 0           $travis_code{$key}{$phase} = [ @$custom_cmds ];
266             }
267             }
268             }
269              
270 0           foreach my $ft ('', '_dzil', '_build') {
271 0           foreach my $pos (qw(pre post)) {
272 0           my $method = $pos.'_'.$phase.$ft;
273 0           my $custom_cmds = $self->$method;
274              
275 0 0 0       if ($custom_cmds && @$custom_cmds) {
276 0           foreach my $key ('dzil', 'build') {
277 0 0 0       next unless (!$ft || substr($ft, 1) eq $key);
278 0   0       $travis_code{$key}{$phase} //= [];
279              
280 0           $pos eq 'pre' ?
281 0           unshift(@{$travis_code{$key}{$phase}}, @$custom_cmds) :
282 0 0         push (@{$travis_code{$key}{$phase}}, @$custom_cmds)
283             ;
284             }
285             }
286             }
287             }
288             }
289              
290             # Insert %travis_code into %travis_yml
291 0 0         unless ($is_build_branch) {
    0          
292             # Standard DZIL YAML
293 0 0         unless ($self->support_builddir) {
294 0           %travis_yml = (%travis_yml, %{ $travis_code{dzil} });
  0            
295             }
296             # Dual DZIL+build YAML
297             else {
298 0           foreach my $phase (@phases) { # skip branches as well
299 0 0         my @common = $travis_code{common}{$phase} ? @{ $travis_code{common} {$phase} } : ();
  0            
300 0 0         my @dzil = $travis_code{dzil} {$phase} ? @{ $travis_code{dzil} {$phase} } : ();
  0            
301 0 0         my @build = $travis_code{build} {$phase} ? @{ $travis_code{build} {$phase} } : ();
  0            
302              
303 0 0         if ($phase eq 'before_install') {
304 0           @build = grep { $_ ne 'rm .travis.yml' } @build; # this won't actually exist in .build/testing
  0            
305 0           unshift @build, 'cd .build/testing';
306             }
307              
308 0 0 0       if (@common || @dzil || @build) {
      0        
309 0           $travis_yml{$phase} = [
310             @common,
311 0           ( map { 'if [[ $BUILD == 0 ]]; then '.$_.'; fi' } @dzil ),
312 0           ( map { 'if [[ $BUILD == 1 ]]; then '.$_.'; fi' } @build ),
313             ];
314             }
315              
316             # if the directory doesn't exist, unset $BUILD, so that everything else is a no-op
317 0 0         unshift @{ $travis_yml{$phase} }, 'if [[ $BUILD == 1 && ! -d .build/testing ]]; then unset BUILD; fi'
  0            
318             if $phase eq 'before_install';
319              
320             # because {build}{script} normally doesn't have any lines, mimic the Travis default
321 0 0 0       if ($phase eq 'script' and not @build) {
322 0           push @{ $travis_yml{$phase} }, (
  0            
323             'if [[ $BUILD == 1 && -f Makefile.PL ]]; then perl Makefile.PL && make test; fi',
324             'if [[ $BUILD == 1 && -f Build.PL ]]; then perl Build.PL && ./Build test; fi',
325             'if [[ $BUILD == 1 && ! -f Makefile.PL && ! -f Build.PL ]]; then make test; fi',
326             );
327             }
328             }
329             }
330              
331             # Add 'only' option, if specified
332 0 0         $travis_code{build}{branches} = { only => $self->dzil_branch } if $self->dzil_branch;
333             }
334             # Build branch YAML
335             elsif ($self->build_branch) {
336 0           %travis_yml = (%travis_yml, %{ $travis_code{build} });
  0            
337             }
338             else {
339 0           return; # no point in staying here...
340             }
341              
342             ### Dump YML (in order)
343 0           local $YAML::Indent = 3;
344 0           local $YAML::UseHeader = 0;
345              
346 0           my $node = YAML::Bless(\%travis_yml);
347 0           $node->keys([grep { exists $travis_yml{$_} } @yml_order]);
  0            
348 0 0         $self->log( "Rebuilding .travis.yml".($is_build_branch ? ' (in build dir)' : '') );
349              
350             # Add quotes to perl version strings, as Travis tends to remove the zeroes
351 0           my $travis_yml = Dump \%travis_yml;
352 0           $travis_yml =~ s/^(\s+- )(5\.\d+|blead)$/$1'$2'/gm;
353 0           $travis_yml =~ s/^(\s+(?:- )?perl: )(5\.\d+|blead)$/$1'$2'/gm;
354              
355 0           my $file = Path::Class::File->new($self->zilla->built_in, '.travis.yml');
356 0           $file->spew($travis_yml);
357 0           return $file;
358             }
359              
360             sub _as_lucene_query {
361 0     0     my ($self, $ver_str) = @_;
362              
363             # simple versions short-circuits
364 0 0         return () if $ver_str eq '0';
365 0 0         return ('module.version_numified:['.version->parse($ver_str)->numify.' TO 999999]')
366             unless ($ver_str =~ /[\<\=\>]/);
367              
368 0           my ($min, $max, $is_min_inc, $is_max_inc, @num_conds, @str_conds);
369 0           foreach my $ver_cmp (split(qr{\s*,\s*}, $ver_str)) {
370 0           my ($cmp, $ver) = split(qr{(?<=[\<\=\>])\s*(?=\d)}, $ver_cmp, 2);
371              
372             # Normalize string, but keep originals for alphas
373 0           my $use_num = 1;
374 0           my $orig_ver = $ver;
375 0           $ver = version->parse($ver);
376 0           my $num_ver = $ver->numify;
377 0 0         if ($ver->is_alpha) {
378 0           $ver = $orig_ver;
379 0           $ver =~ s/^v//i;
380 0           $use_num = 0;
381             }
382 0           else { $ver = $num_ver; }
383              
384 0 0         if ($cmp eq '==') { return 'module.version'.($use_num ? '_numified' : '').':'.$ver; } # no need to look at anything else
  0 0          
385 0 0         if ($cmp eq '!=') { $use_num ? push(@num_conds, '-'.$ver) : push(@str_conds, '-'.$ver); }
  0 0          
386             ### XXX: Trying to do range-based searches on strings isn't a good idea, so we always use the number field ###
387 0 0         if ($cmp eq '>=') { ($min, $is_min_inc) = ($num_ver, 1); }
  0            
388 0 0         if ($cmp eq '<=') { ($max, $is_max_inc) = ($num_ver, 1); }
  0            
389 0 0         if ($cmp eq '>') { ($min, $is_min_inc) = ($num_ver, 0); }
  0            
390 0 0         if ($cmp eq '<') { ($max, $is_max_inc) = ($num_ver, 0); }
  0            
391 0           else { die 'Unable to parse complex module requirements with operator of '.$cmp.' !'; }
392             }
393              
394             # Min/Max parsing
395 0 0 0       if ($min || $max) {
396 0   0       $min ||= 0;
397 0   0       $max ||= 999999;
398 0           my $rng = $min.' TO '.$max;
399              
400             # Figure out the inclusive/exclusive status
401 0           my $inc = $is_min_inc.$is_max_inc; # (this is just easier to deal with as a combined form)
402 0 0 0       unshift @num_conds, '-'.($inc eq '01' ? $min : $max)
    0          
403             if ($inc =~ /0/ && $inc =~ /\d\d/); # has mismatch of inc/exc (reverse order due to unshift)
404 0 0         unshift @num_conds, '+'.($inc =~ /1/ ? '['.$rng.']' : '{'.$rng.'}'); # +[{ $min TO $max }]
405             }
406              
407             # Create the string
408 0           my @lq;
409 0 0         push @lq, 'module.version_numified:('.join(' ', @num_conds).')' if @num_conds;
410 0 0         push @lq, 'module.version:(' .join(' ', @str_conds).')' if @str_conds;
411 0           return @lq;
412             }
413              
414             sub _mcpan_module_minrelease {
415 0     0     my ($self, $module, $ver_str, $try_harder) = @_;
416              
417 0           my @lq = $self->_as_lucene_query($ver_str);
418 0 0         my $maturity_q = ($ver_str =~ /==/) ? undef : 'maturity:released'; # exact version may be a developer one
419              
420             ### XXX: This should be replaced with a ->file() method when those
421             ### two pull requests of mine are put into CPAN...
422 0           my $q = join(' AND ', 'module.name:"'.$module.'"', $maturity_q, 'module.authorized:true', @lq);
423 0           $self->log_debug("Checking module $module via MetaCPAN");
424             #$self->log_debug(" [q=$q]");
425 0 0         my $details = $self->mcpan->fetch("file/_search",
426             q => $q,
427             sort => 'module.version_numified',
428             fields => 'author,release,distribution,module.version,module.name',
429             size => $try_harder ? 20 : 1,
430             );
431 0 0 0       unless ($details && $details->{hits}{total}) {
432 0           $self->log("??? MetaCPAN can't even find a good version for $module!");
433 0           return undef;
434             }
435              
436             ### XXX: Figure out better ways to find these modules with multiple package names (ie: Moose::Autobox, EUMM)
437              
438             # Sometimes, MetaCPAN just gets highly confused...
439 0           my @hits = @{ $details->{hits}{hits} };
  0            
440 0           my $hit;
441 0           my $is_bad = 1;
442 0   0       while ($is_bad and @hits) {
443 0           $hit = shift @hits;
444             # (ie: we shouldn't have multiples of modules or versions, and sort should actually have a value)
445 0   0       $is_bad = !$hit->{sort}[0] || ref $hit->{fields}{'module.name'} || ref $hit->{fields}{'module.version'};
446             };
447              
448 0 0         if ($is_bad) {
449 0 0         if ($try_harder) {
450 0           $self->log("??? MetaCPAN is highly confused about $module!");
451 0           return undef;
452             }
453 0           $self->log_debug(" MetaCPAN got confused; trying harder...");
454 0           return $self->_mcpan_module_minrelease($module, $ver_str, 1)
455             }
456              
457 0           $hit = $hit->{fields};
458              
459             # This will almost always be .tar.gz, but TRIAL versions might have different names, etc.
460 0           my $fields = $self->mcpan->release(
461             search => {
462             q => 'author:'.$hit->{author}.' AND name:"'.$hit->{release}.'"',
463             fields => 'archive,tests',
464             size => 1,
465             },
466             )->{hits}{hits}[0]{fields};
467              
468             # Warn about test failures
469 0           my $t = $fields->{tests};
470 0           my $ttl = sum @$t{qw(pass fail unknown na)};
471 0 0         unless ($ttl) {
472 0           $self->log(['%s has no CPAN test results! You should consider upgrading the minimum dep version for %s...', $hit->{release}, $module]);
473             }
474             else {
475 0           my $per = $t->{pass} / $ttl * 100;
476 0           my $f_ttl = $ttl - $t->{pass};
477              
478 0 0 0       if ($per < 70 || $t->{fail} > 20 || $f_ttl > 30) {
      0        
479 0           $self->log(['CPAN Test Results for %s:', $hit->{release}]);
480 0           $self->log([' %7s: %4u (%3.1f)', $_, $t->{lc $_}, $t->{lc $_} / $ttl * 100]) for (qw(Pass Fail Unknown NA));
481 0           $self->log(['You should consider upgrading the minimum dep version for %s...', $module]);
482             }
483             }
484              
485 0           my $v = $hit->{'module.version'};
486 0   0       return ($hit->{distribution}, $hit->{author}.'/'.$fields->{archive}, $v && version->parse($v));
487             }
488              
489             42;
490              
491             __END__
492              
493             =pod
494              
495             =encoding UTF-8
496              
497             =head1 NAME
498              
499             Dist::Zilla::Role::TravisYML - Role for .travis.yml creation
500              
501             =head1 AVAILABILITY
502              
503             The project homepage is L<https://github.com/SineSwiper/Dist-Zilla-TravisCI>.
504              
505             The latest version of this module is available from the Comprehensive Perl
506             Archive Network (CPAN). Visit L<http://www.perl.com/CPAN/> to find a CPAN
507             site near you, or see L<https://metacpan.org/module/Dist::Zilla::TravisCI/>.
508              
509             =head1 AUTHOR
510              
511             Brendan Byrd <bbyrd@cpan.org>
512              
513             =head1 COPYRIGHT AND LICENSE
514              
515             This software is Copyright (c) 2015 by Brendan Byrd.
516              
517             This is free software, licensed under:
518              
519             The Artistic License 2.0 (GPL Compatible)
520              
521             =cut