lib/App/Project/Doctor/Check/CI.pm

Structural Coverage (Approximate)

TER1 (Statement): 84.09%
TER2 (Branch): 83.33%
TER3 (LCSAJ): 100.0% (1/1)
Approximate LCSAJ segments: 7

LCSAJ Legend

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.

Mutant Testing Legend

Survived (tests missed this) Killed (tests detected this) No mutation
    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