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 )