| File: | blib/lib/App/Project/Doctor/Check/CI.pm |
| Coverage: | 87.5% |
| line | stmt | bran | cond | sub | time | code |
|---|---|---|---|---|---|---|
| 1 | package 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 | ||||||
| 21 | our $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. | |||||
| 25 | Readonly::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 | ||||||
| 41 | sub 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. | |||||
| 91 | sub _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 | ||||||
| 97 | 1; | |||||
| 98 | ||||||