File Coverage

blib/lib/Dist/Zilla/Role/TravisYML.pm
Criterion Covered Total %
statement 27 221 12.2
branch 0 100 0.0
condition 0 53 0.0
subroutine 9 16 56.2
pod n/a
total 36 390 9.2


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