File Coverage

File:blib/lib/App/Project/Doctor/Check/CI.pm
Coverage:87.5%

linestmtbrancondsubtimecode
1package App::Project::Doctor::Check::CI;
2
3# This check answers a single question: does the distribution have any CI
4# configuration at all?  The detailed per-file validation of GitHub Actions
5# YAML is handled separately by Check::GitHubActions.
6
7
4
4
4
2108
3
49
use strict;
8
4
4
4
6
4
90
use warnings;
9
4
4
4
6
2
10
use autodie qw(:all);
10
11# Inherit the standard check interface (name, description, order, can_fix, check).
12
4
4
4
8164
4
11
use parent -norequire, 'App::Project::Doctor::Check::Base';
13
14# croak dies with the caller's file/line instead of this module's line.
15
4
4
4
110
1
86
use Carp qw(croak);
16# File::Spec builds OS-portable paths so the fix works on Windows too.
17
4
4
4
4
4
41
use File::Spec;
18# Readonly creates true constants; assigning to them throws at runtime.
19
4
4
4
5
3
853
use Readonly;
20
21our $VERSION = '0.02';
22
23# Map human-readable CI system names to the path that indicates each one.
24# The check passes as soon as any of these paths exists under the distro root.
25Readonly::Hash my %CI_PATHS => (
26        'GitHub Actions' => '.github/workflows',
27        'Travis CI'      => '.travis.yml',
28        'CircleCI'       => '.circleci/config.yml',
29        'AppVeyor'       => 'appveyor.yml',
30);
31
32# Return the canonical name used in Finding check_name and report headings.
33
2
187
sub name        { 'CI' }
34# One-line description shown in --help and verbose output.
35
2
6
sub description { 'At least one CI configuration is present.' }
36# Signal that this check can offer an automated fix.
37
2
5
sub can_fix     { 1 }
38# Lower number = runs earlier; CI runs after Tests (10) but before GitHubActions (25).
39
5
7
sub order       { 20 }
40
41sub check {
42
12
13
        my ($self, $ctx) = @_;
43        # Guard: $ctx must be an object with file-system helpers.
44
12
27
        croak 'check requires an App::Project::Doctor::Context' unless ref $ctx;
45
46        # Walk through each known CI system and return a pass finding as soon as
47        # we spot one.  We sort the keys so the result is deterministic.
48
11
31
        for my $label (sort keys %CI_PATHS) {
49
33
186
                if ($ctx->has_file($CI_PATHS{$label})) {
50                        # Found a CI config: report success and stop checking.
51
7
13
                        return _f(
52                                severity => 'pass',
53                                message  => "CI configuration found ($label).",
54                        );
55                }
56        }
57
58        # No CI config was found at all; offer to generate a GitHub Actions workflow.
59        return _f(
60                severity => 'error',
61                message  => 'No CI configuration found (GitHub Actions, Travis, CircleCI, AppVeyor).',
62                fix      => sub {
63                        # The fix creates .github/workflows/perl-ci.yml using the
64                        # App::GHGen::Generator functional API.
65
1
3
                        my $root = $_[0]->root;
66
1
1
                        require App::GHGen::Generator;
67                        # generate_workflow returns a YAML string or undef if it fails.
68
1
2
                        my $yaml = App::GHGen::Generator::generate_workflow('perl');
69
1
5
                        return unless $yaml;    # Nothing to write if generation failed.
70                        # Build the target directory path in a cross-platform way.
71
0
0
                        my $wf_dir = File::Spec->catdir($root, '.github', 'workflows');
72
0
0
                        require File::Path;
73                        # make_path creates the directory and all missing parents.
74
0
0
                        File::Path::make_path($wf_dir);
75                        # Write the generated YAML to the standard workflow file.
76
0
0
                        open my $fh, '>', File::Spec->catfile($wf_dir, 'perl-ci.yml');
77
0
0
0
0
                        print {$fh} $yaml;
78
0
0
                        close $fh;
79                },
80
4
18
        );
81}
82
83# ---------------------------------------------------------------------------
84# Private helper
85# ---------------------------------------------------------------------------
86
87# Purpose:    Build a Finding object pre-filled with check_name => 'CI'.
88# Entry:      %args is a valid Finding constructor argument list.
89# Exit:       App::Project::Doctor::Finding object.
90# Side effects: None.
91sub _f {
92
11
31
        require App::Project::Doctor::Finding;
93        # Prepend check_name so callers don't have to repeat it every time.
94
11
35
        return App::Project::Doctor::Finding->new(check_name => 'CI', @_);
95}
96
971;
98