App-GHGen
view release on metacpan or search on metacpan
lib/App/GHGen/Fixer.pm view on Meta::CPAN
=head4 Output
{ type => 'scalar' } # boolean: 1 or 0
=head3 FORMAL SPECIFICATION
can_auto_fix : Issue â ð¹
FixableTypes â { performance, security, cost, maintenance }
can_auto_fix(i) â¡ i.type â FixableTypes
=cut
sub can_auto_fix($issue) {
my %fixable = (
'performance' => 1, # Can add caching
'security' => 1, # Can update action versions and add permissions
'cost' => 1, # Can add concurrency, filters
'maintenance' => 1, # Can update runners
);
return $fixable{$issue->{type}} // 0;
}
=head2 apply_fixes($workflow, $issues)
Apply all auto-fixable changes from C<$issues> directly to C<$workflow>.
=head3 Purpose
Iterate over C<$issues>, skip issues that are not auto-fixable, and call
the appropriate internal fix routine for each fixable type/message
combination. Modifies C<$workflow> in place.
=head3 Arguments
=over 4
=item C<$workflow> (HashRef, required)
The parsed workflow hash to be mutated.
=item C<$issues> (ArrayRef[HashRef], required)
The issues to process. Each must have C<type> and C<message> keys.
=back
=head3 Returns
The number of individual fix operations applied (an integer ⥠0).
=head3 Side Effects
Modifies C<$workflow> in place.
=head3 Usage Example
my $n = apply_fixes($workflow, \@issues);
say "$n fix(es) applied.";
=head3 API SPECIFICATION
=head4 Input
{
workflow => { type => 'hashref', required => 1 },
issues => { type => 'arrayref', required => 1 },
}
=head4 Output
{ type => 'scalar' } # non-negative integer
=head3 FORMAL SPECIFICATION
apply_fixes : Workflow à seq Issue â â
applied â â { fix(w, i) ⣠i â issues, can_auto_fix(i) }
result â applied
Mutates w by applying each fix in sequence.
=cut
sub apply_fixes($workflow, $issues) {
my $modified = 0;
for my $issue (@$issues) {
next unless can_auto_fix($issue);
if ($issue->{type} eq 'performance' && $issue->{message} =~ /caching/) {
$modified += add_caching($workflow);
}
elsif ($issue->{type} eq 'security' && $issue->{message} =~ /unpinned/) {
$modified += fix_unpinned_actions($workflow);
}
elsif ($issue->{type} eq 'security' && $issue->{message} =~ /permissions/) {
$modified += add_permissions($workflow);
}
elsif ($issue->{type} eq 'maintenance' && $issue->{message} =~ /outdated action/) {
$modified += update_actions($workflow);
}
elsif ($issue->{type} eq 'cost' && $issue->{message} =~ /concurrency/) {
$modified += add_concurrency($workflow);
}
elsif ($issue->{type} eq 'cost' && $issue->{message} =~ /triggers/) {
$modified += add_trigger_filters($workflow);
}
elsif ($issue->{type} eq 'maintenance' && $issue->{message} =~ /runner/) {
$modified += update_runners($workflow);
} elsif ($issue->{type} eq 'performance' && $issue->{message} =~ /missing timeout-minutes/) {
$modified += add_missing_timeout($workflow);
}
}
return $modified;
}
=head2 fix_workflow($file, $issues)
Load a workflow YAML file, apply fixes, and write it back to disk.
=head3 Purpose
Persist the results of C<apply_fixes> by reading the workflow from C<$file>
with C<YAML::XS::LoadFile>, calling C<apply_fixes>, and rewriting the file
with C<YAML::XS::DumpFile> when at least one fix was applied.
=head3 Arguments
=over 4
=item C<$file> (Str, required)
Path to a YAML workflow file. Passed directly to C<YAML::XS::LoadFile>.
=item C<$issues> (ArrayRef[HashRef], required)
Issues to fix, each with C<type> and C<message> keys.
=back
=head3 Returns
The number of fixes applied (an integer ⥠0). The file is only rewritten
when the count is greater than zero.
=head3 Side Effects
Reads C<$file> from disk; rewrites C<$file> in place when fixes are applied.
=head3 Usage Example
my $n = fix_workflow('.github/workflows/ci.yml', \@issues);
say "$n fix(es) written to ci.yml.";
=head3 API SPECIFICATION
=head4 Input
{
file => { type => 'scalar', required => 1 },
issues => { type => 'arrayref', required => 1 },
}
=head4 Output
{ type => 'scalar' } # non-negative integer
=head3 FORMAL SPECIFICATION
fix_workflow : Path à seq Issue â â
w â LoadFile(file)
fixes â apply_fixes(w, issues)
fixes > 0 â DumpFile(file, w)
result â fixes
=cut
sub fix_workflow($file, $issues) {
my $workflow = LoadFile($file);
my $fixes = apply_fixes($workflow, $issues);
if ($fixes > 0) {
DumpFile($file, $workflow);
}
return $fixes;
}
# Fix implementations
sub add_caching($workflow) {
my $jobs = $workflow->{jobs} or return 0;
my $modified = 0;
for my $job (values %$jobs) {
my $steps = $job->{steps} or next;
# Check if already has caching
my $has_cache = grep { $_->{uses} && $_->{uses} =~ /actions\/cache/ } @$steps;
next if $has_cache;
# Detect project type and add appropriate cache
my $cache_step = detect_and_create_cache_step($steps);
next unless $cache_step;
# Insert cache step after checkout
my $insert_at = 0;
for my $i (0 .. $#$steps) {
if ($steps->[$i]->{uses} && $steps->[$i]->{uses} =~ /actions\/checkout/) {
$insert_at = $i + 1;
last;
( run in 0.545 second using v1.01-cache-2.11-cpan-9169edd2b0e )