File Coverage

File:blib/lib/App/GHGen/PerlCustomizer.pm
Coverage:87.7%

linestmtbrancondsubtimecode
1package App::GHGen::PerlCustomizer;
2
3
1
1
5
1
use v5.36;
4
1
1
1
1
1
7
use strict;
5
1
1
1
1
1
16
use warnings;
6
7
1
1
1
375
5156
26
use Path::Tiny;
8
9
1
1
1
2
0
670
use Exporter 'import';
10our @EXPORT_OK = qw(
11        detect_perl_requirements
12        generate_custom_perl_workflow
13);
14
15our $VERSION = '0.05';
16
17 - 34
=head1 NAME

App::GHGen::PerlCustomizer - Customize Perl workflows based on project requirements

=head1 SYNOPSIS

        use App::GHGen::PerlCustomizer qw(detect_perl_requirements);

        my $requirements = detect_perl_requirements();
        # Returns: { min_version => '5.036', has_cpanfile => 1, ... }

=head1 FUNCTIONS

=head2 detect_perl_requirements()

Detect Perl version requirements from cpanfile, Makefile.PL, or dist.ini.

=cut
35
36
1
1
0
1
sub detect_perl_requirements() {
37
1
1
        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
2
        $reqs{has_cpanfile} = path('cpanfile')->exists;
47
1
35
        $reqs{has_makefile_pl} = path('Makefile.PL')->exists;
48
1
17
        $reqs{has_dist_ini} = path('dist.ini')->exists;
49
1
14
        $reqs{has_build_pl} = path('Build.PL')->exists;
50
51        # Try to detect minimum Perl version
52
1
13
        if ($reqs{has_cpanfile}) {
53
1
3
                my $content = path('cpanfile')->slurp_utf8;
54
1
603
                if ($content =~ /requires\s+['"]perl['"],?\s+['"]([0-9.]+)['"]/) {
55
1
2
                        $reqs{min_version} = $1;
56                }
57        }
58
59
1
1
        if (!$reqs{min_version} && $reqs{has_makefile_pl}) {
60
0
0
                my $content = path('Makefile.PL')->slurp_utf8;
61
0
0
                if ($content =~ /MIN_PERL_VERSION\s*=>\s*['"]([0-9.]+)['"]/) {
62
0
0
                        $reqs{min_version} = $1;
63                }
64        }
65
66
1
1
        return \%reqs;
67}
68
69 - 83
=head2 generate_custom_perl_workflow($options)

Generate a customized Perl workflow based on options hash.

Options:
  - perl_versions: Array ref of explicit Perl versions (e.g., ['5.40', '5.38'])
  - min_perl_version: Minimum Perl version (e.g., '5.036')
  - max_perl_version: Maximum Perl version to test (e.g., '5.40')
  - os: Array ref of operating systems ['ubuntu', 'macos', 'windows']
  - enable_critic: Boolean
  - enable_coverage: Boolean

If perl_versions is provided, it takes precedence over min/max versions.

=cut
84
85
1
1
1
1
1
0
sub generate_custom_perl_workflow($opts = {}) {
86
1
2
        my $min_version = $opts->{min_perl_version} // '5.36';
87
1
1
        my $max_version = $opts->{max_perl_version} // '5.40';
88
1
2
        my $timeout = $opts->{timeout} // 30;
89
1
1
0
2
        my @os = @{$opts->{os} // ['ubuntu-latest', 'macos-latest', 'windows-latest']};
90
1
1
        my $enable_critic = $opts->{enable_critic} // 1;
91
1
5
        my $enable_coverage = $opts->{enable_coverage} // 1;
92
93        # Generate Perl version list - use explicit list if provided, otherwise min/max
94
1
1
        my @perl_versions;
95
1
0
2
0
        if ($opts->{perl_versions} && @{$opts->{perl_versions}}) {
96
0
0
0
0
                @perl_versions = @{$opts->{perl_versions}};
97        } else {
98
1
1
                @perl_versions = _get_perl_versions($min_version, $max_version);
99        }
100
101
1
1
        my $yaml = "---\n";
102
1
1
        $yaml .= '# Created by ' . __PACKAGE__ . "\n";
103
104
1
1
        $yaml .= "name: Perl CI\n\n";
105
1
0
        $yaml .= "'on':\n";
106
1
1
        $yaml .= "  push:\n";
107
1
1
        $yaml .= "    branches:\n";
108
1
0
        $yaml .= "      - main\n";
109
1
1
        $yaml .= "      - master\n";
110
1
0
        $yaml .= "  pull_request:\n";
111
1
7
        $yaml .= "    branches:\n";
112
1
1
        $yaml .= "      - main\n";
113
1
1
        $yaml .= "      - master\n\n";
114
115
1
1
        $yaml .= "concurrency:\n";
116
1
0
        $yaml .= "  group: \${{ github.workflow }}-\${{ github.ref }}\n";
117
1
1
        $yaml .= "  cancel-in-progress: true\n\n";
118
119
1
1
        $yaml .= "permissions:\n";
120
1
0
        $yaml .= "  contents: read\n\n";
121
122
1
1
        $yaml .= "jobs:\n";
123
1
0
        $yaml .= "  test:\n";
124
1
1
        $yaml .= "    runs-on: \${{ matrix.os }}\n";
125
1
1
        $yaml .= "    timeout-minutes: $timeout\n";
126
1
1
        $yaml .= "    strategy:\n";
127
1
0
        $yaml .= "      fail-fast: false\n";
128
1
1
        $yaml .= "      matrix:\n";
129
1
0
        $yaml .= "        os:\n";
130
1
1
        for my $os (@os) {
131
3
2
                $yaml .= "          - $os\n";
132        }
133
1
1
        $yaml .= "        perl:\n";
134
1
1
        for my $version (@perl_versions) {
135
3
2
                $yaml .= "          - '$version'\n";
136        }
137
1
1
        $yaml .= "    name: Perl \${{ matrix.perl }} on \${{ matrix.os }}\n";
138
1
0
        $yaml .= "    env:\n";
139
1
1
        $yaml .= "      AUTOMATED_TESTING: 1\n";
140
1
0
        $yaml .= "      NO_NETWORK_TESTING: 1\n";
141
1
1
        $yaml .= "      NONINTERACTIVE_TESTING: 1\n";
142
1
3
        $yaml .= "    steps:\n";
143
1
0
        $yaml .= "      - uses: actions/checkout\@v6\n\n";
144
145
1
1
        $yaml .= "      - name: Setup Perl\n";
146
1
3
        $yaml .= "        uses: shogo82148/actions-setup-perl\@v1\n";
147
1
0
        $yaml .= "        with:\n";
148
1
1
        $yaml .= "          perl-version: \${{ matrix.perl }}\n\n";
149
150
1
0
        $yaml .= "      - name: Cache CPAN modules\n";
151
1
1
        $yaml .= "        uses: actions/cache\@v5\n";
152
1
0
        $yaml .= "        with:\n";
153
1
1
        $yaml .= "          path: ~/perl5\n";
154
1
1
        $yaml .= "          key: \${{ runner.os }}-\${{ matrix.perl }}-\${{ hashFiles('cpanfile') }}\n";
155
1
0
        $yaml .= "          restore-keys: |\n";
156
1
6
        $yaml .= "            \${{ runner.os }}-\${{ matrix.perl }}-\n\n";
157
158
1
1
        $yaml .= "      - name: Install cpanm and local::lib\n";
159
1
0
        $yaml .= "        if: runner.os != 'Windows'\n";
160
1
1
        $yaml .= "        run: cpanm --notest --local-lib=~/perl5 local::lib\n\n";
161
162
1
1
        $yaml .= "      - name: Install cpanm and local::lib (Windows)\n";
163
1
0
        $yaml .= "        if: runner.os == 'Windows'\n";
164
1
1
        $yaml .= "        run: cpanm --notest App::cpanminus local::lib\n\n";
165
166
1
0
        $yaml .= "      - name: Install dependencies\n";
167
1
1
        $yaml .= "        if: runner.os != 'Windows'\n";
168
1
1
        $yaml .= "        shell: bash\n";
169
1
0
        $yaml .= "        run: |\n";
170
1
6
        $yaml .= "          eval \$(perl -I ~/perl5/lib/perl5 -Mlocal::lib)\n";
171
1
1
        $yaml .= "          cpanm --notest --installdeps .\n\n";
172
173
1
1
        $yaml .= "      - name: Install dependencies (Windows)\n";
174
1
0
        $yaml .= "        if: runner.os == 'Windows'\n";
175
1
1
        $yaml .= "        shell: cmd\n";
176
1
1
        $yaml .= "        run: |\n";
177
1
0
        $yaml .= "          \@echo off\n";
178
1
1
        $yaml .= "          set \"PATH=%USERPROFILE%\\perl5\\bin;%PATH%\"\n";
179
1
0
        $yaml .= "          set \"PERL5LIB=%USERPROFILE%\\perl5\\lib\\perl5\"\n";
180
1
0
        $yaml .= "          cpanm --notest --installdeps .\n\n";
181
182
1
1
        $yaml .= "      - name: Run tests\n";
183
1
1
        $yaml .= "        if: runner.os != 'Windows'\n";
184
1
0
        $yaml .= "        shell: bash\n";
185
1
1
        $yaml .= "        run: |\n";
186
1
0
        $yaml .= "          eval \$(perl -I ~/perl5/lib/perl5 -Mlocal::lib)\n";
187
1
1
        $yaml .= "          prove -lr t/\n\n";
188
189
1
0
        $yaml .= "      - name: Run tests (Windows)\n";
190
1
1
        $yaml .= "        if: runner.os == 'Windows'\n";
191
1
1
        $yaml .= "        shell: cmd\n";
192
1
0
        $yaml .= "        run: |\n";
193
1
1
        $yaml .= "          \@echo off\n";
194
1
0
        $yaml .= "          set \"PATH=%USERPROFILE%\\perl5\\bin;%PATH%\"\n";
195
1
1
        $yaml .= "          set \"PERL5LIB=%USERPROFILE%\\perl5\\lib\\perl5\"\n";
196
1
0
        $yaml .= "          prove -lr t/\n\n";
197
198
1
1
        if ($enable_critic) {
199
1
1
                my $latest = $perl_versions[-1];
200
1
1
                $yaml .= "      - name: Run Perl::Critic\n";
201
1
2
                $yaml .= "        if: matrix.perl == '$latest' && matrix.os == 'ubuntu-latest'\n";
202
1
1
                $yaml .= "        continue-on-error: true\n";
203
1
1
                $yaml .= "        run: |\n";
204
1
0
                $yaml .= "          eval \$(perl -I ~/perl5/lib/perl5 -Mlocal::lib)\n";
205
1
1
                $yaml .= "          cpanm --notest Perl::Critic\n";
206
1
0
                $yaml .= "          perlcritic --severity 3 lib/ || true\n";
207
1
1
                $yaml .= "        shell: bash\n\n";
208        }
209
210
1
1
        if ($enable_coverage) {
211
1
1
                my $latest = $perl_versions[-1];
212
1
0
                $yaml .= "      - name: Test coverage\n";
213
1
1
                $yaml .= "        if: matrix.perl == '$latest' && matrix.os == 'ubuntu-latest'\n";
214
1
0
                $yaml .= "        run: |\n";
215
1
1
                $yaml .= "          eval \$(perl -I ~/perl5/lib/perl5 -Mlocal::lib)\n";
216
1
0
                $yaml .= "          cpanm --notest Devel::Cover\n";
217
1
1
                $yaml .= "          cover -delete\n";
218
1
0
                $yaml .= "          HARNESS_PERL_SWITCHES=-MDevel::Cover prove -lr t/\n";
219
1
1
                $yaml .= "          cover\n";
220
1
0
                $yaml .= "        shell: bash\n";
221        }
222
223
1
1
        $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"
233YAML
234
235
1
6
        return $yaml;
236}
237
238
1
1
1
1
0
1
1
0
sub _get_perl_versions($min, $max) {
239        # All available Perl versions in descending order
240
1
1
        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
1
        my $min_normalized = _normalize_version($min);
244
1
1
        my $max_normalized = _normalize_version($max);
245
246
1
1
        my @selected;
247
1
1
        for my $version (@all_versions) {
248
11
9
                my $v_normalized = _normalize_version($version);
249
11
14
                if ($v_normalized >= $min_normalized && $v_normalized <= $max_normalized) {
250
3
2
                        push @selected, $version;
251                }
252        }
253
254
1
1
        return reverse @selected;       # Return in ascending order
255}
256
257
13
13
13
5
8
4
sub _normalize_version($version) {
258        # Convert "5.036" or "5.36" to comparable number
259
13
11
        $version =~ s/^v?//;
260
13
10
        my @parts = split /\./, $version;
261
13
19
        return sprintf("%d.%03d", $parts[0] // 5, $parts[1] // 0);
262}
263
264 - 275
=head1 AUTHOR

Nigel Horne E<lt>njh@nigelhorne.comE<gt>

L<https://github.com/nigelhorne>

=head1 LICENSE

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut
276
2771;