TER1 (Statement): 84.09%
TER2 (Branch): 83.33%
TER3 (LCSAJ): 100.0% (1/1)
Approximate LCSAJ segments: 7
● Covered — this LCSAJ path was executed during testing.
● Not covered — this LCSAJ path was never executed. These are the paths to focus on.
Multiple dots on a line indicate that multiple control-flow paths begin at that line. Hovering over any dot shows:
start → end → jump
Uncovered paths show [NOT COVERED] in the tooltip.
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: use strict; 8: use warnings; 9: use autodie qw(:all); 10: 11: # Inherit the standard check interface (name, description, order, can_fix, check). 12: 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: use Carp qw(croak); 16: # File::Spec builds OS-portable paths so the fix works on Windows too. 17: use File::Spec; 18: # Readonly creates true constants; assigning to them throws at runtime. 19: 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: sub name { 'CI' } 34: # One-line description shown in --help and verbose output. 35: sub description { 'At least one CI configuration is present.' } 36: # Signal that this check can offer an automated fix. 37: sub can_fix { 1 } 38: # Lower number = runs earlier; CI runs after Tests (10) but before GitHubActions (25). 39: sub order { 20 } 40: 41: sub check { ●42 → 48 → 59 42: my ($self, $ctx) = @_; 43: # Guard: $ctx must be an object with file-system helpers. 44: 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: for my $label (sort keys %CI_PATHS) { 49: if ($ctx->has_file($CI_PATHS{$label})) {Mutants (Total: 1, Killed: 1, Survived: 0)
50: # Found a CI config: report success and stop checking. 51: return _f(
Mutants (Total: 2, Killed: 2, Survived: 0)
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(
Mutants (Total: 2, Killed: 2, Survived: 0)
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: my $root = $_[0]->root; 66: require App::GHGen::Generator; 67: # generate_workflow returns a YAML string or undef if it fails. 68: my $yaml = App::GHGen::Generator::generate_workflow('perl'); 69: return unless $yaml; # Nothing to write if generation failed. 70: # Build the target directory path in a cross-platform way. 71: my $wf_dir = File::Spec->catdir($root, '.github', 'workflows'); 72: require File::Path; 73: # make_path creates the directory and all missing parents. 74: File::Path::make_path($wf_dir); 75: # Write the generated YAML to the standard workflow file. 76: open my $fh, '>', File::Spec->catfile($wf_dir, 'perl-ci.yml'); 77: print {$fh} $yaml; 78: close $fh; 79: }, 80: ); 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: require App::Project::Doctor::Finding; 93: # Prepend check_name so callers don't have to repeat it every time. 94: return App::Project::Doctor::Finding->new(check_name => 'CI', @_);
Mutants (Total: 2, Killed: 2, Survived: 0)
95: } 96: 97: 1; 98: 99: __END__ 100: 101: =head1 NAME 102: 103: App::Project::Doctor::Check::CI - Check that a CI configuration exists 104: 105: =head1 DESCRIPTION 106: 107: Reports an error when no supported CI configuration is found. Detailed 108: GitHub Actions validation is handled by L<App::Project::Doctor::Check::GitHubActions>. 109: 110: =head1 METHODS 111: 112: =head2 check( $context ) 113: 114: Inspects the distro root for any recognised CI configuration. 115: 116: =head3 API SPECIFICATION 117: 118: =head4 Input 119: 120: $context : App::Project::Doctor::Context 121: 122: =head4 Output 123: 124: List of exactly one App::Project::Doctor::Finding -- 125: pass when at least one CI config file or directory is present, 126: error (fixable) when none are found. 127: 128: =head3 MESSAGES 129: 130: Code | Trigger | Resolution 131: -----|-------------------|------------------------------------ 132: C001 | No CI config | Fix generates a workflow via App::GHGen::Generator 133: 134: =head3 FORMAL SPECIFICATION 135: 136: check : Context -> [Finding] 137: check ctx == if exists any CI_PATH in ctx then [pass] else [error+fix] 138: 139: =head1 AUTHOR 140: 141: Nigel Horne C<< <njh@nigelhorne.com> >> 142: 143: =head1 LICENSE 144: 145: Copyright (C) 2026 Nigel Horne. 146: This library is free software; you can redistribute it and/or modify 147: it under the same terms as Perl itself. 148: 149: =cut