File Coverage

blib/lib/App/GHGen/PerlCustomizer.pm
Criterion Covered Total %
statement 164 170 96.4
branch 7 16 43.7
condition 13 25 52.0
subroutine 9 9 100.0
pod 2 2 100.0
total 195 222 87.8


line stmt bran cond sub pod time code
1             package App::GHGen::PerlCustomizer;
2              
3 1     1   13 use v5.36;
  1         6  
4 1     1   6 use strict;
  1         1  
  1         26  
5 1     1   4 use warnings;
  1         2  
  1         49  
6              
7 1     1   1094 use Path::Tiny;
  1         18027  
  1         90  
8              
9 1     1   11 use Exporter 'import';
  1         2  
  1         1996  
10             our @EXPORT_OK = qw(
11             detect_perl_requirements
12             generate_custom_perl_workflow
13             );
14              
15             our $VERSION = '0.05';
16              
17             =head1 NAME
18              
19             App::GHGen::PerlCustomizer - Customize Perl workflows based on project requirements
20              
21             =head1 SYNOPSIS
22              
23             use App::GHGen::PerlCustomizer qw(detect_perl_requirements);
24              
25             my $requirements = detect_perl_requirements();
26             # Returns: { min_version => '5.036', has_cpanfile => 1, ... }
27              
28             =head1 FUNCTIONS
29              
30             =head2 detect_perl_requirements()
31              
32             Detect Perl version requirements from cpanfile, Makefile.PL, or dist.ini.
33              
34             =cut
35              
36 1     1 1 2 sub detect_perl_requirements() {
  1         2  
37 1         7 my %reqs = (
38             min_version => undef,
39             has_cpanfile => 0,
40             has_makefile_pl => 0,
41             has_dist_ini => 0,
42             has_build_pl => 0,
43             );
44              
45             # Check for dependency files
46 1         5 $reqs{has_cpanfile} = path('cpanfile')->exists;
47 1         136 $reqs{has_makefile_pl} = path('Makefile.PL')->exists;
48 1         57 $reqs{has_dist_ini} = path('dist.ini')->exists;
49 1         107 $reqs{has_build_pl} = path('Build.PL')->exists;
50              
51             # Try to detect minimum Perl version
52 1 50       52 if ($reqs{has_cpanfile}) {
53 1         9 my $content = path('cpanfile')->slurp_utf8;
54 1 50       2025 if ($content =~ /requires\s+['"]perl['"],?\s+['"]([0-9.]+)['"]/) {
55 1         15 $reqs{min_version} = $1;
56             }
57             }
58              
59 1 0 33     5 if (!$reqs{min_version} && $reqs{has_makefile_pl}) {
60 0         0 my $content = path('Makefile.PL')->slurp_utf8;
61 0 0       0 if ($content =~ /MIN_PERL_VERSION\s*=>\s*['"]([0-9.]+)['"]/) {
62 0         0 $reqs{min_version} = $1;
63             }
64             }
65              
66 1         4 return \%reqs;
67             }
68              
69             =head2 generate_custom_perl_workflow($options)
70              
71             Generate a customized Perl workflow based on options hash.
72              
73             Options:
74             - perl_versions: Array ref of explicit Perl versions (e.g., ['5.40', '5.38'])
75             - min_perl_version: Minimum Perl version (e.g., '5.036')
76             - max_perl_version: Maximum Perl version to test (e.g., '5.40')
77             - os: Array ref of operating systems ['ubuntu', 'macos', 'windows']
78             - enable_critic: Boolean
79             - enable_coverage: Boolean
80              
81             If perl_versions is provided, it takes precedence over min/max versions.
82              
83             =cut
84              
85 1     1 1 3 sub generate_custom_perl_workflow($opts = {}) {
  1         2  
  1         31  
86 1   50     5 my $min_version = $opts->{min_perl_version} // '5.36';
87 1   50     3 my $max_version = $opts->{max_perl_version} // '5.40';
88 1   50     5 my $timeout = $opts->{timeout} // 30;
89 1   50     2 my @os = @{$opts->{os} // ['ubuntu-latest', 'macos-latest', 'windows-latest']};
  1         6  
90 1   50     3 my $enable_critic = $opts->{enable_critic} // 1;
91 1   50     6 my $enable_coverage = $opts->{enable_coverage} // 1;
92              
93             # Generate Perl version list - use explicit list if provided, otherwise min/max
94 1         3 my @perl_versions;
95 1 50 33     6 if ($opts->{perl_versions} && @{$opts->{perl_versions}}) {
  0         0  
96 0         0 @perl_versions = @{$opts->{perl_versions}};
  0         0  
97             } else {
98 1         19 @perl_versions = _get_perl_versions($min_version, $max_version);
99             }
100              
101 1         4 my $yaml = "---\n";
102 1         3 $yaml .= '# Created by ' . __PACKAGE__ . "\n";
103              
104 1         2 $yaml .= "name: Perl CI\n\n";
105 1         3 $yaml .= "'on':\n";
106 1         13 $yaml .= " push:\n";
107 1         3 $yaml .= " branches:\n";
108 1         1 $yaml .= " - main\n";
109 1         3 $yaml .= " - master\n";
110 1         3 $yaml .= " pull_request:\n";
111 1         3 $yaml .= " branches:\n";
112 1         28 $yaml .= " - main\n";
113 1         3 $yaml .= " - master\n\n";
114              
115 1         2 $yaml .= "concurrency:\n";
116 1         3 $yaml .= " group: \${{ github.workflow }}-\${{ github.ref }}\n";
117 1         2 $yaml .= " cancel-in-progress: true\n\n";
118              
119 1         2 $yaml .= "permissions:\n";
120 1         2 $yaml .= " contents: read\n\n";
121              
122 1         2 $yaml .= "jobs:\n";
123 1         2 $yaml .= " test:\n";
124 1         2 $yaml .= " runs-on: \${{ matrix.os }}\n";
125 1         3 $yaml .= " timeout-minutes: $timeout\n";
126 1         2 $yaml .= " strategy:\n";
127 1         2 $yaml .= " fail-fast: false\n";
128 1         3 $yaml .= " matrix:\n";
129 1         2 $yaml .= " os:\n";
130 1         3 for my $os (@os) {
131 3         6 $yaml .= " - $os\n";
132             }
133 1         2 $yaml .= " perl:\n";
134 1         2 for my $version (@perl_versions) {
135 3         7 $yaml .= " - '$version'\n";
136             }
137 1         3 $yaml .= " name: Perl \${{ matrix.perl }} on \${{ matrix.os }}\n";
138 1         2 $yaml .= " env:\n";
139 1         2 $yaml .= " AUTOMATED_TESTING: 1\n";
140 1         2 $yaml .= " NO_NETWORK_TESTING: 1\n";
141 1         3 $yaml .= " NONINTERACTIVE_TESTING: 1\n";
142 1         2 $yaml .= " steps:\n";
143 1         10 $yaml .= " - uses: actions/checkout\@v6\n\n";
144              
145 1         2 $yaml .= " - name: Setup Perl\n";
146 1         7 $yaml .= " uses: shogo82148/actions-setup-perl\@v1\n";
147 1         3 $yaml .= " with:\n";
148 1         1 $yaml .= " perl-version: \${{ matrix.perl }}\n\n";
149              
150 1         3 $yaml .= " - name: Cache CPAN modules\n";
151 1         2 $yaml .= " uses: actions/cache\@v5\n";
152 1         2 $yaml .= " with:\n";
153 1         3 $yaml .= " path: ~/perl5\n";
154 1         2 $yaml .= " key: \${{ runner.os }}-\${{ matrix.perl }}-\${{ hashFiles('cpanfile') }}\n";
155 1         2 $yaml .= " restore-keys: |\n";
156 1         2 $yaml .= " \${{ runner.os }}-\${{ matrix.perl }}-\n\n";
157              
158 1         15 $yaml .= " - name: Install cpanm and local::lib\n";
159 1         3 $yaml .= " if: runner.os != 'Windows'\n";
160 1         2 $yaml .= " run: cpanm --notest --local-lib=~/perl5 local::lib\n\n";
161              
162 1         2 $yaml .= " - name: Install cpanm and local::lib (Windows)\n";
163 1         10 $yaml .= " if: runner.os == 'Windows'\n";
164 1         2 $yaml .= " run: cpanm --notest App::cpanminus local::lib\n\n";
165              
166 1         2 $yaml .= " - name: Install dependencies\n";
167 1         2 $yaml .= " if: runner.os != 'Windows'\n";
168 1         2 $yaml .= " shell: bash\n";
169 1         2 $yaml .= " run: |\n";
170 1         9 $yaml .= " eval \$(perl -I ~/perl5/lib/perl5 -Mlocal::lib)\n";
171 1         3 $yaml .= " cpanm --notest --installdeps .\n\n";
172              
173 1         2 $yaml .= " - name: Install dependencies (Windows)\n";
174 1         2 $yaml .= " if: runner.os == 'Windows'\n";
175 1         1 $yaml .= " shell: cmd\n";
176 1         2 $yaml .= " run: |\n";
177 1         2 $yaml .= " \@echo off\n";
178 1         2 $yaml .= " set \"PATH=%USERPROFILE%\\perl5\\bin;%PATH%\"\n";
179 1         3 $yaml .= " set \"PERL5LIB=%USERPROFILE%\\perl5\\lib\\perl5\"\n";
180 1         1 $yaml .= " cpanm --notest --installdeps .\n\n";
181              
182 1         3 $yaml .= " - name: Run tests\n";
183 1         2 $yaml .= " if: runner.os != 'Windows'\n";
184 1         1 $yaml .= " shell: bash\n";
185 1         2 $yaml .= " run: |\n";
186 1         2 $yaml .= " eval \$(perl -I ~/perl5/lib/perl5 -Mlocal::lib)\n";
187 1         2 $yaml .= " prove -lr t/\n\n";
188              
189 1         2 $yaml .= " - name: Run tests (Windows)\n";
190 1         2 $yaml .= " if: runner.os == 'Windows'\n";
191 1         2 $yaml .= " shell: cmd\n";
192 1         2 $yaml .= " run: |\n";
193 1         2 $yaml .= " \@echo off\n";
194 1         1 $yaml .= " set \"PATH=%USERPROFILE%\\perl5\\bin;%PATH%\"\n";
195 1         3 $yaml .= " set \"PERL5LIB=%USERPROFILE%\\perl5\\lib\\perl5\"\n";
196 1         1 $yaml .= " prove -lr t/\n\n";
197              
198 1 50       4 if ($enable_critic) {
199 1         3 my $latest = $perl_versions[-1];
200 1         2 $yaml .= " - name: Run Perl::Critic\n";
201 1         8 $yaml .= " if: matrix.perl == '$latest' && matrix.os == 'ubuntu-latest'\n";
202 1         3 $yaml .= " continue-on-error: true\n";
203 1         2 $yaml .= " run: |\n";
204 1         1 $yaml .= " eval \$(perl -I ~/perl5/lib/perl5 -Mlocal::lib)\n";
205 1         3 $yaml .= " cpanm --notest Perl::Critic\n";
206 1         1 $yaml .= " perlcritic --severity 3 lib/ || true\n";
207 1         3 $yaml .= " shell: bash\n\n";
208             }
209              
210 1 50       3 if ($enable_coverage) {
211 1         3 my $latest = $perl_versions[-1];
212 1         2 $yaml .= " - name: Test coverage\n";
213 1         2 $yaml .= " if: matrix.perl == '$latest' && matrix.os == 'ubuntu-latest'\n";
214 1         3 $yaml .= " run: |\n";
215 1         2 $yaml .= " eval \$(perl -I ~/perl5/lib/perl5 -Mlocal::lib)\n";
216 1         2 $yaml .= " cpanm --notest Devel::Cover\n";
217 1         2 $yaml .= " cover -delete\n";
218 1         1 $yaml .= " HARNESS_PERL_SWITCHES=-MDevel::Cover prove -lr t/\n";
219 1         2 $yaml .= " cover\n";
220 1         2 $yaml .= " shell: bash\n";
221             }
222              
223 1         3 $yaml .= <<'YAML';
224              
225             - name: Show cpanm build log on failure (Windows)
226             if: runner.os == 'Windows' && failure()
227             shell: pwsh
228             run: Get-Content "$env:USERPROFILE\.cpanm\work\*\build.log" -Tail 100
229              
230             - name: Show cpanm build log on failure (non-Windows)
231             if: runner.os != 'Windows' && failure()
232             run: tail -100 "$HOME/.cpanm/work/*/build.log"
233             YAML
234              
235 1         21 return $yaml;
236             }
237              
238 1     1   2 sub _get_perl_versions($min, $max) {
  1         3  
  1         2  
  1         2  
239             # All available Perl versions in descending order
240 1         13 my @all_versions = qw(5.42 5.40 5.38 5.36 5.34 5.32 5.30 5.28 5.26 5.24 5.22);
241              
242             # Normalize version strings for comparison
243 1         24 my $min_normalized = _normalize_version($min);
244 1         3 my $max_normalized = _normalize_version($max);
245              
246 1         2 my @selected;
247 1         4 for my $version (@all_versions) {
248 11         18 my $v_normalized = _normalize_version($version);
249 11 100 100     56 if ($v_normalized >= $min_normalized && $v_normalized <= $max_normalized) {
250 3         8 push @selected, $version;
251             }
252             }
253              
254 1         5 return reverse @selected; # Return in ascending order
255             }
256              
257 13     13   20 sub _normalize_version($version) {
  13         21  
  13         20  
258             # Convert "5.036" or "5.36" to comparable number
259 13         41 $version =~ s/^v?//;
260 13         34 my @parts = split /\./, $version;
261 13   50     63 return sprintf("%d.%03d", $parts[0] // 5, $parts[1] // 0);
      50        
262             }
263              
264             =head1 AUTHOR
265              
266             Nigel Horne Enjh@nigelhorne.comE
267              
268             L
269              
270             =head1 LICENSE
271              
272             This is free software; you can redistribute it and/or modify it under
273             the same terms as the Perl 5 programming language system itself.
274              
275             =cut
276              
277             1;