File Coverage

File:blib/lib/App/GHGen/Generator.pm
Coverage:98.8%

linestmtbrancondsubtimecode
1package App::GHGen::Generator;
2
3
5
5
1073
9
use v5.36;
4
5
5
5
5
6
4
31
use strict;
6
5
5
5
6
2
83
use warnings;
7
8
5
5
5
676
5
120
use App::GHGen::PerlCustomizer qw(detect_perl_requirements generate_custom_perl_workflow);
9
10
5
5
5
8
3
1012
use Exporter 'import';
11our @EXPORT_OK = qw(
12        generate_workflow
13        list_workflow_types
14);
15
16our $VERSION = '0.10';
17
18 - 88
=head1 NAME

App::GHGen::Generator - Generate GitHub Actions workflows

=head1 SYNOPSIS

    use App::GHGen::Generator qw(generate_workflow);
    use Path::Tiny;

    my $yaml = generate_workflow('perl');
    path('.github/workflows/ci.yml')->spew_utf8($yaml);

=head1 FUNCTIONS

=head2 generate_workflow($type)

Generate a complete GitHub Actions workflow YAML string for the given project type.

=head3 Purpose

Dispatch to the appropriate language-specific workflow generator and return
the resulting YAML string ready to be written to a C<.github/workflows/> file.

=head3 Arguments

=over 4

=item C<$type> (Str, required)

A project-type identifier.  Supported values: C<perl>, C<node>, C<python>,
C<rust>, C<go>, C<ruby>, C<java>, C<cpp>, C<php>, C<docker>, C<static>.

=back

=head3 Returns

A multi-line YAML string beginning with C<---> when C<$type> is recognised,
or C<undef> when the type is not supported.

=head3 Side Effects

The C<perl> type reads the current directory to detect project requirements
(via L<App::GHGen::PerlCustomizer>).

=head3 Usage Example

    use App::GHGen::Generator qw(generate_workflow);
    my $yaml = generate_workflow('node') // die "unsupported type";
    path('.github/workflows/ci.yml')->spew_utf8($yaml);

=head3 API SPECIFICATION

=head4 Input

    { type => { type => 'scalar', required => 1 } }

=head4 Output

    known type   â†’ { type => 'scalar' }   # YAML string
    unknown type → undef

=head3 FORMAL SPECIFICATION

    SupportedTypes ≔ { perl, node, python, rust, go, ruby, java, cpp, php, docker, static }

    generate_workflow : ℤ* → ℤ* ∪ { ⊥ }

    t ∈ SupportedTypes → generators[t]()    (non-empty YAML string)
    t ∉ SupportedTypes → ⊥                  (undef)

=cut
89
90
38
38
38
27156
28
34
sub generate_workflow($type) {
91
38
100
    my %generators = (
92        perl   => \&_generate_perl_workflow,
93        node   => \&_generate_node_workflow,
94        python => \&_generate_python_workflow,
95        rust   => \&_generate_rust_workflow,
96        go     => \&_generate_go_workflow,
97        ruby   => \&_generate_ruby_workflow,
98        java   => \&_generate_java_workflow,
99        cpp    => \&_generate_cpp_workflow,
100        php    => \&_generate_php_workflow,
101        docker => \&_generate_docker_workflow,
102        static => \&_generate_static_workflow,
103    );
104
105
38
45
        return undef unless exists $generators{$type};
106
35
39
        return $generators{$type}->();
107}
108
109 - 154
=head2 list_workflow_types()

Return a flat hash of all supported workflow types and their descriptions.

=head3 Purpose

Enumerate every type that C<generate_workflow> can handle, with a one-line
human-readable description for each.

=head3 Arguments

None.

=head3 Returns

A flat hash (not a reference) mapping type strings to description strings.
Contains exactly eleven entries: C<perl>, C<node>, C<python>, C<rust>,
C<go>, C<ruby>, C<java>, C<cpp>, C<php>, C<docker>, C<static>.

=head3 Side Effects

None.  Pure function.

=head3 Usage Example

    my %types = list_workflow_types();
    say "$_: $types{$_}" for sort keys %types;

=head3 API SPECIFICATION

=head4 Input

    # No parameters.

=head4 Output

    { type => 'hashref', keys => { '*' => { type => 'scalar' } } }

=head3 FORMAL SPECIFICATION

    list_workflow_types : → ℤ* → ℤ*

    result ≔ { t ↦ desc(t) ∣ t ∈ SupportedTypes }
    |result| = 11

=cut
155
156
4
4
7799
4
sub list_workflow_types() {
157    return (
158
4
22
        node   => 'Node.js/npm projects with testing and linting',
159        python => 'Python projects with pytest and coverage',
160        rust   => 'Rust projects with cargo, clippy, and formatting',
161        go     => 'Go projects with testing and race detection',
162        ruby   => 'Ruby projects with bundler and rake',
163        perl   => 'Perl projects with cpanm, prove, and coverage',
164        java   => 'Java projects with Maven or Gradle',
165        cpp    => 'C++ projects with CMake',
166        php    => 'PHP projects with Composer and PHPUnit',
167        docker => 'Docker image build and push workflow',
168        static => 'Static site deployment to GitHub Pages',
169    );
170}
171
172# Private workflow generators
173
174
5
5
1005
5
sub _generate_perl_workflow() {
175        # Try to detect requirements from project
176
5
9
        my $reqs = detect_perl_requirements();
177
178        # Use detected min version or default to 5.36
179
5
11
        my $min_version = $reqs->{min_version} // '5.36';
180
181        # Generate custom workflow with detected settings
182
5
22
        return generate_custom_perl_workflow({
183                min_perl_version => $min_version,
184                max_perl_version => '5.40',
185                os => ['macos-latest', 'ubuntu-latest', 'windows-latest'],
186                enable_critic => 1,
187                enable_coverage => 1,
188        });
189}
190
191
4
4
3
2
sub _generate_node_workflow() {
192
4
8
        return <<'YAML';
193---
194name: Node.js CI
195
196'on':
197  push:
198    branches:
199      - main
200      - develop
201  pull_request:
202    branches:
203      - main
204      - develop
205
206concurrency:
207  group: ${{ github.workflow }}-${{ github.ref }}
208  cancel-in-progress: true
209
210permissions:
211  contents: read
212
213jobs:
214  test:
215    runs-on: ubuntu-latest
216    strategy:
217      matrix:
218        node-version:
219          - 18.x
220          - 20.x
221          - 22.x
222    steps:
223      - name: Checkout code
224        uses: actions/checkout@76
225
226      - name: Setup Node.js ${{ matrix.node-version }}
227        uses: actions/setup-node@v4
228        with:
229          node-version: ${{ matrix.node-version }}
230
231      - name: Cache dependencies
232        uses: actions/cache@v6
233        with:
234          path: ~/.npm
235          key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
236          restore-keys: |
237            ${{ runner.os }}-node-
238
239      - name: Install dependencies
240        run: npm ci
241
242      - name: Run linter
243        run: npm run lint --if-present
244
245      - name: Run tests
246        run: npm test
247
248      - name: Build project
249        run: npm run build --if-present
250YAML
251}
252
253
3
3
3
3
sub _generate_python_workflow() {
254
3
5
    return <<'YAML';
255---
256name: Python CI
257
258'on':
259  push:
260    branches:
261      - main
262      - develop
263  pull_request:
264    branches:
265      - main
266      - develop
267
268concurrency:
269  group: ${{ github.workflow }}-${{ github.ref }}
270  cancel-in-progress: true
271
272permissions:
273  contents: read
274
275jobs:
276  test:
277    runs-on: ubuntu-latest
278    strategy:
279      matrix:
280        python-version:
281          - '3.9'
282          - '3.10'
283          - '3.11'
284          - '3.12'
285    steps:
286      - name: Checkout code
287        uses: actions/checkout@v7
288
289      - name: Set up Python ${{ matrix.python-version }}
290        uses: actions/setup-python@v5
291        with:
292          python-version: ${{ matrix.python-version }}
293
294      - name: Cache pip packages
295        uses: actions/cache@v6
296        with:
297          path: ~/.cache/pip
298          key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
299          restore-keys: |
300            ${{ runner.os }}-pip-
301
302      - name: Install dependencies
303        run: |
304          pip install -r requirements.txt
305          pip install pytest pytest-cov flake8
306
307      - name: Lint with flake8
308        run: flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
309
310      - name: Run tests with coverage
311        run: pytest --cov=. --cov-report=xml
312YAML
313}
314
315
3
3
2
3
sub _generate_rust_workflow() {
316
3
5
    return <<'YAML';
317---
318name: Rust CI
319
320'on':
321  push:
322    branches:
323      - main
324  pull_request:
325    branches:
326      - main
327
328concurrency:
329  group: ${{ github.workflow }}-${{ github.ref }}
330  cancel-in-progress: true
331
332permissions:
333  contents: read
334
335jobs:
336  test:
337    runs-on: ubuntu-latest
338    steps:
339      - name: Checkout code
340        uses: actions/checkout@v7
341
342      - name: Setup Rust
343        uses: dtolnay/rust-toolchain@stable
344
345      - name: Cache cargo
346        uses: actions/cache@v6
347        with:
348          path: |
349            ~/.cargo/bin/
350            ~/.cargo/registry/index/
351            ~/.cargo/registry/cache/
352            ~/.cargo/git/db/
353            target/
354          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
355
356      - name: Check formatting
357        run: cargo fmt -- --check
358
359      - name: Run clippy
360        run: cargo clippy -- -D warnings
361
362      - name: Run tests
363        run: cargo test --verbose
364
365      - name: Build release
366        run: cargo build --release --verbose
367YAML
368}
369
370
3
3
2
2
sub _generate_go_workflow() {
371
3
6
    return <<'YAML';
372---
373name: Go CI
374
375'on':
376  push:
377    branches:
378      - main
379  pull_request:
380    branches:
381      - main
382
383concurrency:
384  group: ${{ github.workflow }}-${{ github.ref }}
385  cancel-in-progress: true
386
387permissions:
388  contents: read
389
390jobs:
391  test:
392    runs-on: ubuntu-latest
393    steps:
394      - name: Checkout code
395        uses: actions/checkout@v7
396
397      - name: Setup Go
398        uses: actions/setup-go@v5
399        with:
400          go-version: '1.22'
401
402      - name: Cache Go modules
403        uses: actions/cache@v6
404        with:
405          path: ~/go/pkg/mod
406          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
407          restore-keys: |
408            ${{ runner.os }}-go-
409
410      - name: Download dependencies
411        run: go mod download
412
413      - name: Run go vet
414        run: go vet ./...
415
416      - name: Run tests
417        run: go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
418
419      - name: Build
420        run: go build -v ./...
421YAML
422}
423
424
3
3
3
1
sub _generate_ruby_workflow() {
425
3
6
    return <<'YAML';
426---
427name: Ruby CI
428
429'on':
430  push:
431    branches:
432      - main
433  pull_request:
434    branches:
435      - main
436
437concurrency:
438  group: ${{ github.workflow }}-${{ github.ref }}
439  cancel-in-progress: true
440
441permissions:
442  contents: read
443
444jobs:
445  test:
446    runs-on: ubuntu-latest
447    strategy:
448      matrix:
449        ruby-version:
450          - '3.1'
451          - '3.2'
452          - '3.3'
453    steps:
454      - name: Checkout code
455        uses: actions/checkout@v7
456
457      - name: Set up Ruby
458        uses: ruby/setup-ruby@v1
459        with:
460          ruby-version: ${{ matrix.ruby-version }}
461          bundler-cache: true
462
463      - name: Run tests
464        run: bundle exec rake test
465YAML
466}
467
468
3
3
3
1
sub _generate_docker_workflow() {
469
3
6
    return <<'YAML';
470---
471name: Docker Build
472
473'on':
474  push:
475    branches:
476      - main
477    tags:
478      - v*
479  pull_request:
480    branches:
481      - main
482
483concurrency:
484  group: ${{ github.workflow }}-${{ github.ref }}
485  cancel-in-progress: true
486
487permissions:
488  contents: read
489  packages: write
490
491jobs:
492  build:
493    runs-on: ubuntu-latest
494    steps:
495      - name: Checkout code
496        uses: actions/checkout@v7
497
498      - name: Set up Docker Buildx
499        uses: docker/setup-buildx-action@v3
500
501      - name: Log in to Docker Hub
502        uses: docker/login-action@v3
503        with:
504          username: ${{ secrets.DOCKER_USERNAME }}
505          password: ${{ secrets.DOCKER_PASSWORD }}
506        if: github.event_name != 'pull_request'
507
508      - name: Extract metadata
509        id: meta
510        uses: docker/metadata-action@v5
511        with:
512          images: your-username/your-image
513
514      - name: Build and push
515        uses: docker/build-push-action@v5
516        with:
517          context: .
518          push: ${{ github.event_name != 'pull_request' }}
519          tags: ${{ steps.meta.outputs.tags }}
520          labels: ${{ steps.meta.outputs.labels }}
521          cache-from: type=gha
522          cache-to: type=gha,mode=max
523YAML
524}
525
526
3
3
1
3
sub _generate_static_workflow() {
527
3
5
    return <<'YAML';
528---
529name: Deploy Static Site
530
531'on':
532  push:
533    branches:
534      - main
535
536permissions:
537  contents: read
538  pages: write
539  id-token: write
540
541concurrency:
542  group: pages
543  cancel-in-progress: false
544
545jobs:
546  build:
547    runs-on: ubuntu-latest
548    steps:
549      - name: Checkout
550        uses: actions/checkout@v7
551
552      - name: Setup Pages
553        uses: actions/configure-pages@v4
554
555      - name: Build
556        run: echo "Add your build command here"
557
558      - name: Upload artifact
559        uses: actions/upload-pages-artifact@v3
560        with:
561          path: ./public
562
563  deploy:
564    environment:
565      name: github-pages
566      url: ${{ steps.deployment.outputs.page_url }}
567    runs-on: ubuntu-latest
568    needs: build
569    steps:
570      - name: Deploy to GitHub Pages
571        id: deployment
572        uses: actions/deploy-pages@v4
573YAML
574}
575
576
3
3
3
2
sub _generate_java_workflow() {
577
3
4
    return <<'YAML';
578---
579name: Java CI
580
581'on':
582  push:
583    branches:
584      - main
585      - develop
586  pull_request:
587    branches:
588      - main
589      - develop
590
591concurrency:
592  group: ${{ github.workflow }}-${{ github.ref }}
593  cancel-in-progress: true
594
595permissions:
596  contents: read
597
598jobs:
599  test:
600    runs-on: ubuntu-latest
601    strategy:
602      matrix:
603        java-version:
604          - '11'
605          - '17'
606          - '21'
607    steps:
608      - name: Checkout code
609        uses: actions/checkout@v7
610
611      - name: Set up JDK ${{ matrix.java-version }}
612        uses: actions/setup-java@v4
613        with:
614          java-version: ${{ matrix.java-version }}
615          distribution: 'temurin'
616
617      - name: Cache Maven packages
618        uses: actions/cache@v6
619        with:
620          path: ~/.m2/repository
621          key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
622          restore-keys: |
623            ${{ runner.os }}-maven-
624
625      - name: Build with Maven
626        run: mvn -B package --file pom.xml
627
628      - name: Run tests
629        run: mvn -B test
630
631      - name: Generate test report
632        if: always()
633        run: mvn surefire-report:report
634YAML
635}
636
637
3
3
3
31
sub _generate_cpp_workflow() {
638
3
6
    return <<'YAML';
639---
640name: C++ CI
641
642'on':
643  push:
644    branches:
645      - main
646  pull_request:
647    branches:
648      - main
649
650concurrency:
651  group: ${{ github.workflow }}-${{ github.ref }}
652  cancel-in-progress: true
653
654permissions:
655  contents: read
656
657jobs:
658  test:
659    runs-on: ${{ matrix.os }}
660    strategy:
661      matrix:
662        os:
663          - ubuntu-latest
664          - macos-latest
665          - windows-latest
666        build-type:
667          - Debug
668          - Release
669    steps:
670      - name: Checkout code
671        uses: actions/checkout@v7
672
673      - name: Install CMake
674        uses: lukka/get-cmake@latest
675
676      - name: Cache build artifacts
677        uses: actions/cache@v6
678        with:
679          path: |
680            build
681            ~/.cache/ccache
682          key: ${{ runner.os }}-${{ matrix.build-type }}-${{ hashFiles('**/CMakeLists.txt') }}
683
684      - name: Configure CMake
685        run: cmake -B build -DCMAKE_BUILD_TYPE=${{ matrix.build-type }}
686
687      - name: Build
688        run: cmake --build build --config ${{ matrix.build-type }}
689
690      - name: Run tests
691        working-directory: build
692        run: ctest -C ${{ matrix.build-type }} --output-on-failure
693YAML
694}
695
696
3
3
3
2
sub _generate_php_workflow() {
697
3
5
    return <<'YAML';
698---
699name: PHP CI
700
701'on':
702  push:
703    branches:
704      - main
705      - develop
706  pull_request:
707    branches:
708      - main
709      - develop
710
711concurrency:
712  group: ${{ github.workflow }}-${{ github.ref }}
713  cancel-in-progress: true
714
715permissions:
716  contents: read
717
718jobs:
719  test:
720    runs-on: ubuntu-latest
721    strategy:
722      matrix:
723        php-version:
724          - '8.1'
725          - '8.2'
726          - '8.3'
727    steps:
728      - name: Checkout code
729        uses: actions/checkout@v7
730
731      - name: Setup PHP ${{ matrix.php-version }}
732        uses: shivammathur/setup-php@v2
733        with:
734          php-version: ${{ matrix.php-version }}
735          extensions: mbstring, xml, ctype, json
736          coverage: xdebug
737
738      - name: Validate composer.json
739        run: composer validate --strict
740
741      - name: Cache Composer packages
742        uses: actions/cache@v6
743        with:
744          path: vendor
745          key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
746          restore-keys: |
747            ${{ runner.os }}-php-
748
749      - name: Install dependencies
750        run: composer install --prefer-dist --no-progress
751
752      - name: Run PHPUnit tests
753        run: vendor/bin/phpunit --coverage-text
754
755      - name: Run PHP CodeSniffer
756        run: vendor/bin/phpcs --standard=PSR12 src tests
757        continue-on-error: true
758YAML
759}
760
761 - 772
=head1 AUTHOR

Nigel Horne E<lt>njh@nigelhorne.comE<gt>

L<https://github.com/nigelhorne>

=head1 LICENSE

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut
773
7741;