File Coverage

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

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