Shortcuts: s show h hide n next p prev

(dcxix) metacpan weekly report

r/perl
Thank you Team PWC for your continuous support and encouragement.
Welcome to the Week #348 of The Weekly Challenge.
Thank you Team PWC for your continuous support and encouragement.
Don't treat class attributes as sub attributes in perly.y

We need to use a dedicated grammar type; or at the very least, not use
`subattrlist` for doing this. The previous use of `subattrlist` had the
side-effect of passing every attribute list for a class via the
`apply_builtin_cv_attributes()` function. That was pointlessly
redundant, though currently harmless. It may stop being harmless in a
future improvement to the way attributes are handled, so best not to do
that any more.

The new structure repurposes the previous `myattrlist` to be a
non-optional attribute list, and defines `optattrlist` to use for both
class and field attributes.

The Act website for LPW 2025 is up and running

r/perl
The Act website for LPW 2025 is up and running

Extremely short notice, but the website is up and registrations and talk submissions are open.

The conference will take place in two weeks (!), on 29 November, at International Student House in Great Portland Street.

Hope to see some of you there!

submitted by /u/jjatria
[link] [comments]

In part 1, we had created a GET endpoint for the backend with static data. We will now move to connect the backend API to database, so we…

Weekly Challenge: The one about formatting

dev.to #perl

Weekly Challenge 347

Each week Mohammad S. Anwar sends out The Weekly Challenge, a chance for all of us to come up with solutions to two weekly tasks. My solutions are written in Python first, and then converted to Perl. It's a great way for us all to practice some coding.

Challenge, My solutions

Task 1: Format Date

Task

You are given a date in the form: 10th Nov 2025.

Write a script to format the given date in the form: 2025-11-10 using the sets below.

@DAYS   = ("1st", "2nd", "3rd", ....., "30th", "31st")
@MONTHS = ("Jan", "Feb", "Mar", ....., "Nov",  "Dec")
@YEARS  = (1900..2100)

My solution

Both of these weeks tasks are pretty straight forward so don't require too much explanation. For this task, I start by defined the lists (arrays in Perl) for DAYS, MONTHS and YEARS. As the years are actually a string, I use the map function to convert them to a string.

DAYS = [
    "1st", "2nd", "3rd", "4th", "5th", "6th", "7th", "8th", "9th", "10th",
    "11th", "12th", "13th", "14th", "15th", "16th", "17th", "18th", "19th",
    "20th", "21st", "22nd", "23rd", "24th", "25th", "26th", "27th", "28th",
    "29th", "30th", "31st"
]

MONTHS = [
    "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct",
    "Nov", "Dec"
]

YEARS = list(map(str, range(1900, 2101)))

I start the function by defining the three fields. Each list is the description of the field, the list to use, and the offset value.

def format_date(input_string: str) -> str:
    fields = [
        ("day of month", DAYS, 1),
        ("month", MONTHS, 1),
        ("year", YEARS, 1900)
    ]

I then split the input on spaces and store this as input_list. I create an empty list called output_list. I also check that there are three items in the list.

    input_list = input_string.split()
    output_list = []

    if len(input_list) != 3:
        raise ValueError("Input must contain day, month, and year")

I then loop through each field, getting the index (position) of the part of index_list that we are looking at, and check it is in the list. If it isn't, I'll raise an error (e.g. "Invalid day of month: 23th"). The offset is applied and the value is added to the output_list list. I use an offset as 'Jan' is at position zero, but it is the 1st month (as written as a date).

    for i in (range(3)):
        name, values, offset = fields[i]
        value = input_list[i]
        if value not in values:
            raise ValueError(f"Invalid {name}: {value}")
        index = values.index(value) + offset
        output_list.append(f"{index:02d}")

Finally, I print the date. I reverse the list and separate it with dashes.

    return "-".join(reversed(output_list))

The Perl code follows the same logic.

Examples

$ ./ch-1.py "1st Jan 2025"
2025-01-01

$ ./ch-1.py "22nd Feb 2025"
2025-02-22

$ ./ch-1.py "15th Apr 2025"
2025-04-15

$ ./ch-1.py "23rd Oct 2025"
2025-10-23

$ ./ch-1.py "31st Dec 2025"
2025-12-31

Task 2: Format Phone Number

Task

You are given a phone number as a string containing digits, space and dash only.

Write a script to format the given phone number using the below rules:

  1. Removing all spaces and dashes
  2. Grouping digits into blocks of length 3 from left to right
  3. Handling the final digits (4 or fewer) specially:
    • 2 digits: one block of length 2
    • 3 digits: one block of length 3
    • 4 digits: two blocks of length 2
  4. Joining all blocks with dashes

My solution

For this task, I start by using a regular expression removing anything that isn't a digit from the input_string variable. I create an empty list called parts.

I use a while loop that runs if input_string is not empty. I then determine how many characters to remove from the front of the string. If the string is four characters long, I set this to 2. In other cases, I set it to the minimum of 3 or the length of the string. I remove the characters from the start of the string and add it to the parts list. The loop runs again until all characters are removed.

def format_phone(input_string: str) -> str:
    # Strip all non-digit characters
    input_string = re.sub(r'\D', '', input_string)

    parts = []
    while input_string:
        # Decide length of next part
        l = 2 if len(input_string) == 4 else min(3, len(input_string))
        parts.append(input_string[:l])
        input_string = input_string[l:]

    return '-'.join(parts)

The Perl solution follows the same logic. It uses the substr function which can both remove the leading characters and add to the parts list in a single call.

sub main ($input_string) {
    # Strip all non-digit characters
    $input_string =~ s/\D//g;

    my @parts = ();
    while ($input_string) {
        # Decide length of next part
        my $l = length($input_string) == 4 ? 2 : min( 3, length($input_string) );
        push @parts, substr( $input_string, 0, $l, "" );
    }

    say join( "-", @parts );
}

Examples

$ ./ch-2.py "1-23-45-6"
123-456

$ ./ch-2.py "1234"
12-34

$ ./ch-2.py "12 345-6789"
123-456-789

$ ./ch-2.py "123 4567"
123-45-67

$ ./ch-2.py "123 456-78"
123-456-78

fix typos

Perl commits on GitHub
fix typos

reported in #23926

Get them from the usual place.
And no, I have still not had time to update CPAN::MetaCustodian so that it properly parses these wikis. But that time is approaching...

(dlxxiv) 12 great CPAN modules released last week

r/perl
Updates for great CPAN modules released last week. A module is considered great if its favorites count is greater or equal than 12.

  1. App::cpm - a fast CPAN module installer
    • Version: 0.998001 on 2025-11-13, with 176 votes
    • Previous CPAN version: 0.998000 was 5 days before
    • Author: SKAJI
  2. App::Netdisco - An open source web-based network management tool.
    • Version: 2.095001 on 2025-11-15, with 794 votes
    • Previous CPAN version: 2.095000
    • Author: OLIVER
  3. App::Rakubrew - Raku environment manager
    • Version: 45 on 2025-11-13, with 28 votes
    • Previous CPAN version: 44 was 2 days before
    • Author: PATRICKB
  4. Bitcoin::Crypto - Bitcoin cryptography in Perl
    • Version: 4.002 on 2025-11-14, with 14 votes
    • Previous CPAN version: 4.001 was 2 days before
    • Author: BRTASTIC
  5. CPANSA::DB - the CPAN Security Advisory data as a Perl data structure, mostly for CPAN::Audit
    • Version: 20251116.001 on 2025-11-16, with 25 votes
    • Previous CPAN version: 20251109.001 was 6 days before
    • Author: BRIANDFOY
  6. Dist::Zilla - distribution builder; installer not included!
    • Version: 6.036 on 2025-11-09, with 189 votes
    • Previous CPAN version: 6.035 was before
    • Author: RJBS
  7. JSON::Schema::Modern - Validate data against a schema using a JSON Schema
    • Version: 0.622 on 2025-11-08, with 12 votes
    • Previous CPAN version: 0.621 was 9 days before
    • Author: ETHER
  8. Net::SIP - Framework SIP (Voice Over IP, RFC3261)
    • Version: 0.840 on 2025-11-10, with 16 votes
    • Previous CPAN version: 0.839 was 2 months, 5 days before
    • Author: SULLR
  9. SPVM - The SPVM Language
    • Version: 0.990106 on 2025-11-11, with 36 votes
    • Previous CPAN version: 0.990105 was 26 days before
    • Author: KIMOTO
  10. Test::Simple - Basic utilities for writing tests.
    • Version: 1.302216 on 2025-11-16, with 199 votes
    • Previous CPAN version: 1.302215 was 2 days before
    • Author: EXODIST
  11. Time::Piece - Object Oriented time objects
    • Version: 1.41 on 2025-11-12, with 65 votes
    • Previous CPAN version: 1.40 was 4 days before
    • Author: ESAYM
  12. Workflow - Simple, flexible system to implement workflows
    • Version: 2.08 on 2025-11-12, with 34 votes
    • Previous CPAN version: 2.07 was 4 days before
    • Author: JONASBN

(dcxix) metacpan weekly report

Niceperl

This is the weekly favourites list of CPAN distributions. Votes count: 25

This week there isn't any remarkable distribution

Build date: 2025/11/16 11:03:31 GMT


Clicked for first time:


Increasing its reputation:


The Perl and Raku Foundation has announced a £1,000 sponsorship of the upcoming London Perl and Raku Workshop, reinforcing its ongoing commitment to supporting community-driven technical events. The workshop, one of the longest-running grassroots Perl gatherings in the UK, brings together developers, educators, and open-source enthusiasts for a day of talks, hands-on sessions, and collaborative learning centered on Perl, Raku, and related technologies.

The foundation’s contribution will help cover venue expenses, accessibility measures, and attendee resources. Organizers intend to use the support received from sponsros to keep the event free to attend, maintaining its tradition of lowering barriers for both newcomers and experienced programmers.

This year’s workshop is expected to feature a broad program, including presentations on language internals, modern development practices, and applied use cases within industry and research. Community members from across Europe are anticipated to participate, reflecting the workshop’s reputation as a focal point for Perl activity. The workshop is scheduled for November 29, 2025, in the heart of London at ISH Venues, near Regent's Park. Several speakers are already confirmed for this year's workshop, including Stevan Little and TPRF White Camel Award winners Sawyer X and Mohammad Sajid Anwar. For more information about the event, visit https://www.londonperlworkshop.com/.

By backing the event, The Perl and Raku Foundation continues its broader mission to foster growth, education, and innovation across both language communities. The London Perl Workshop remains one of the foundation’s key community touchpoints, offering a collaborative space for developers to share knowledge and help shape the future of the languages.

cpan/Term-Table - Update to version 0.027

0.027     2025-11-14 10:04:51-08:00 America/Los_Angeles

    - No changes from trial

0.026     2025-11-12 20:02:15-08:00 America/Los_Angeles (TRIAL RELEASE)

    - Fix terminal size detection

Grafana + Prometheus

blogs.perl.org


For all DevOps enthusiasts, here is a quick introduction to Grafana and Prometheus.
Please check out the link for more information:
https://theweeklychallenge.org/blog/grafana-prometheus

As you know, The Weekly Challenge, primarily focus on Perl and Raku. During the Week #018, we received solutions to The Weekly Challenge - 018 by Orestis Zekai in Python. It was pleasant surprise to receive solutions in something other than Perl and Raku. Ever since regular team members also started contributing in other languages like Ada, APL, Awk, BASIC, Bash, Bc, Befunge-93, Bourne Shell, BQN, Brainfuck, C3, C, CESIL, Chef, COBOL, Coconut, C Shell, C++, Clojure, Crystal, CUDA, D, Dart, Dc, Elixir, Elm, Emacs Lisp, Erlang, Excel VBA, F#, Factor, Fennel, Fish, Forth, Fortran, Gembase, Gleam, GNAT, Go, GP, Groovy, Haskell, Haxe, HTML, Hy, Idris, IO, J, Janet, Java, JavaScript, Julia, K, Kap, Korn Shell, Kotlin, Lisp, Logo, Lua, M4, Maxima, Miranda, Modula 3, MMIX, Mumps, Myrddin, Nelua, Nim, Nix, Node.js, Nuweb, Oberon, Octave, OCaml, Odin, Ook, Pascal, PHP, PicoLisp, Python, PostgreSQL, Postscript, PowerShell, Prolog, R, Racket, Rexx, Ring, Roc, Ruby, Rust, Scala, Scheme, Sed, Smalltalk, SQL, Standard ML, SVG, Swift, Tcl, TypeScript, Typst, Uiua, V, Visual BASIC, WebAssembly, Wolfram, XSLT, YaBasic and Zig.

TypeScript x Perl

r/perl

Grafana + Prometheus

The Weekly Challenge
Grafana is an open-source analytics and visualisation platform. It lets you create dashboards that visualise data from various sources such as:

I want to create an asynchronuous OpenAPI interface. Async jobs return a 202 and a location header to query later.

This is my OpenAPI document:

---
components:
  headers:
    JobLocation:
      description: Job status URL
      schema:
        type: string
    JobRetryAfter:
      description: Query delay in seconds
      schema:
        oneOf:
          - minimum: 1
            type: integer
          - type: string
  schemas:
    ErrorMessage:
      description: An error descriptopm
      example:
        customer_error: You did not provide a password
        message: 'Request error: Password is missing'
      properties:
        customer_error:
          description: Error for consumer
          type: string
        message:
          description: Technical error description
          type: string
      required:
        - message
      type: object
    Job:
      properties:
        code:
          type: integer
        completedAt:
          format: date-time
          nullable: true
          type: string
        createdAt:
          format: date-time
          type: string
        jobId:
          type: string
        result:
          $ref: '#/components/schemas/Result'
        status:
          enum:
            - initiated
            - pending
            - completed
            - failed
          type: string
      required:
        - jobId
        - code
        - status
        - createdAt
      type: object
    JobAccepted:
      example:
        jobId: job-12345
        statusUrl: /jobs/job-12345
      properties:
        jobId:
          type: string
        statusUrl:
          type: string
      type: object
    Notification:
      properties:
        message:
          description: Beschreibung der Benachrichtigung
          type: string
        notificationID:
          description: ein eindeutiger Bezeichner dieser Benachrichtigung
          type: string
        path:
          description: Aktuell unbenutzt
          type: string
        severity:
          description: Einschätzung der Benachrichtigung
          enum:
            - CRITICAL
            - ERROR
            - WARNING
            - INFO
          type: string
      required:
        - notificationID
        - severity
        - message
    PayloadBaseObject:
      properties:
        verb:
          type: string
      required:
        - verb
      type: object
    Result:
      properties:
        notifications:
          items:
            $ref: '#/components/schemas/Notification'
          type: array
        payload:
          items:
            discriminator:
              propertyName: entityType
            oneOf:
              - $ref: '#/components/schemas/SyncMock'
          type: array
      type: object
    SyncMock:
      allOf:
        - $ref: '#/components/schemas/PayloadBaseObject'
        - properties:
            entityType:
              enum:
                - mock-details
              type: string
            reqkey:
              type: string
            result:
              properties:
                async:
                  description: demo
                  type: integer
              type: object
          required:
            - entityType
            - reqkey
          type: object
      description: Demo of a synchronuous API call
  securitySchemes:
    http:
      description: Basic Auth credentials
      scheme: basic
      type: http
info:
  title: mock service
  version: 0.0.1
openapi: 3.0.4
paths:
  /sync:
    get:
      description: Makes a synchronuous API call and waits for the result. There is no timeout.
      operationId: getMockSync
      responses:
        200:
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Job'
          description: Synchronuous job response
      security:
        - http: []
      summary: Mocks a synchronuous REST call
      tags:
        - mock
      x-mojo-to: Mock#sync
  /async:
    get:
      description: Makes an asynchronuous API call and does NOT wait for the result.
      operationId: getMockAsync
      responses:
        202:
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobAccepted'
          description: Asynchronuous job response
      security:
        - http: []
      summary: Mocks an asynchronuous REST call
      tags:
        - mock
      x-mojo-to: Mock#async
  '/job/{jobid}':
    get:
      summary: Get job state
      description: Get job state
      operationId: retrieveJobStatus
      parameters:
        - description: A job ID
          in: path
          name: jobid
          required: true
          schema:
            type: string
      responses:
        200:
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Job'
          description: 'Job done'
        202:
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobAccepted'
          description: 'Job not done yet'
          headers:
            Location:
              $ref: '#/components/headers/JobLocation'
            Retry-After:
              $ref: '#/components/headers/JobRetryAfter'
        404:
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorMessage'
          description: 'Job not found (anymore)'
      x-mojo-to: Job#status
security:
  - http: []
servers:
  - description: Mock APIv2 service
    url: /mock
tags:
  - description: Just mocking
    name: mock

Here is a complete test script to reproduce my issue:

use strict;
use warnings;

use File::Spec;
use FindBin qw< $Bin >;

use Test::More;
use Test::Exception;
use Test::Mojo;

my $api = File::Spec->catfile($Bin => q(test.yaml));
my $t   = Test::Mojo->new(q(Test::APIv2));

my $prefix = q(/mock);
$t->get_ok(qq($prefix/sync))->status_is(200);
$t->get_ok(qq($prefix/async))->status_is(202)->header_exists(q(Location))
    ->header_like(Location => qr/\/job\/[0-9a-f]+$/)
    ->header_exists(q(Retry-after));
my $job = $t->tx->res->json(q(/jobId));
##note(qq(Job ID: $job));

$t->get_ok(qq($prefix/job/$job))->status_is(202);
$t->get_ok(qq($prefix/job/$job))->status_is(200);

done_testing();

package My::App;

use 5.020;    # -signatures flag for Mojolicious
use Mojo::Base 'Mojolicious' => -signatures;
use Carp;
use HTTP::Status qw<>;
use Try::Tiny;

sub startup ($self) {
    ##  avoid (currently) useless warning
    $self->secrets([qw< abc cde >]);

    $self->plugin(q(Config));

    ##  initialize UI
    $self->helper(ui => sub { state $ui = $self->init_ui() });

    ##  initialize OpenAPI plugin
    $self->plugin(
        OpenAPI => {
            ##  TODO make this a factory call to be better testable
            url => $self->config->{api_description},
            ##  make sure the response obeys the scheme
            validate_response => 1,
            log_level         => q(trace),
        }
    );

    $self->helper(
        myrender => sub ($c, $data, %opts) {
            my $status = $data->{code}
                or croak(q("code" is missing in response));

            if (grep({ $status == $_ } (HTTP::Status::HTTP_ACCEPTED))
                and my $jobid = $data->{jobId})
            {
                my $r
                    = $c->url_for(q(retrieveJobStatus) => { jobid => $jobid })
                    ->to_abs;
                $c->res->headers->header(Location => $r);

                my $ra = 3;    ## TODO static here?
                $c->res->headers->header(q(Retry-after) => $ra);
            } ## end if (grep({ $status == ...}))
            $c->render(openapi => $data, status => $status);
        }
    );
} ## end sub startup

sub init_ui ($self) { croak(q(Not interesting here)) }

package My::App::Controller::Job;

use 5.020;    # -signatures flag for Mojolicious
use Mojo::Base "Mojolicious::Controller" => -signatures;

{
    my $c;
    BEGIN { $c = 0 }
    sub get_c { $c++ }
}

sub status ($self) {
    $self = $self->openapi->valid_input
        or return;

    my $jobid  = $self->param(q(jobid));
    my $status = get_c() ? 200 : 202;
    my $r
        = $status == 200
        ? {
        'jobId'       => $jobid,
        'code'        => $status,
        'createdAt'   => '2025-11-14T16:24:44Z',
        'completedAt' => '2025-11-14T16:24:46Z',
        'status'      => 'completed',
        'result'      => {
            'payload' => [
                {
                    'reqkey'     => 'ent1',
                    'result'     => { 'async' => 0 },
                    'entityType' => 'mock-details',
                    'verb'       => 'sync'
                }
            ]
        },
        }
        : {
        'jobId'     => $jobid,
        'code'      => $status,
        'createdAt' => '2025-11-14T16:24:44Z',
        'status'    => 'initiated',
        };

    $self->myrender($r, status => $status,);
} ## end sub status

package Test::APIv2;

use 5.020;    # -signatures flag for Mojolicious
use Mojo::Base 'My::App' => -signatures;

sub startup ($self) {
    $self->SUPER::startup;
    $self->routes->namespaces(
        [
            qw<
                My::App::Controller
                Test::APIv2::Controller
            >
        ]
    );
} ## end sub startup

sub init_ui ($self) {return}

package Test::APIv2::Controller::Mock;

use 5.020;    # -signatures flag for Mojolicious
use Mojo::Base "Mojolicious::Controller" => -signatures;

use Carp;
use Try::Tiny;

sub sync ($self) {
    $self = $self->openapi->valid_input
        or return;

    $self->myrender(
        {
            'createdAt'   => '2025-11-14T16:24:44Z',
            'code'        => 200,
            'completedAt' => '2025-11-14T16:24:46Z',
            'jobId'       =>
                '7092005578957c4aa8695cf304a8f15eea34c92bd22ec62cc6b5721efaa74676',
            'result' => {
                'payload' => [
                    {
                        'reqkey'     => 'ent1',
                        'result'     => { 'async' => 0 },
                        'entityType' => 'mock-details',
                        'verb'       => 'sync'
                    }
                ]
            },
            'status' => 'completed'
        }
    );
} ## end sub sync

sub async ($self) {
    $self = $self->openapi->valid_input
        or return;

    $self->myrender(
        {
            "code"      => 202,
            "createdAt" => "2025-11-14T16:24:46Z",
            "jobId"     =>
                "58b074fbf5e421e0d79df4a5d38ef2e970a49ff51d06bd52718bacec483007f2",
            "status" => "initiated"
        }
    );
} ## end sub async

Put this into a directory (e.g. /tmp/mojo) and add the YAML (test.yaml) from above and a configfile test-a_p_iv2.conf to this directory:

{ api_description => q(test.yaml), }
##  vim:set filetype=perl:

Then execute

prove -v mock.t

The first tests (sync and async) work as expected, but when I call the job endpoint, there is a stacktrace:

[2025-11-14 18:01:49.92515] [1276658] [error] [DyHG8MMADFrN] You have to call resolve() before validate() to lookup "#/components/headers/JobLocation". at /usr/share/perl5/JSON/Validator/Schema/Draft201909.pm line 61.
    JSON::Validator::Schema::Draft201909::_state(JSON::Validator::Schema::OpenAPIv3=HASH(0x5ffda54c88a8), HASH(0x5ffda58eb580), "schema", HASH(0x5ffda5fb4b28)) called at /usr/share/perl5/JSON/Validator/Schema.pm line 155
    JSON::Validator::Schema::validate(JSON::Validator::Schema::OpenAPIv3=HASH(0x5ffda54c88a8), Mojo::URL=HASH(0x5ffda5fb2d98), HASH(0x5ffda5fb4b28)) called at /usr/share/perl5/JSON/Validator/Schema/OpenAPIv2.pm line 374
    JSON::Validator::Schema::OpenAPIv2::_validate_request_or_response(JSON::Validator::Schema::OpenAPIv3=HASH(0x5ffda54c88a8), "response", ARRAY(0x5ffda5fb24f8), HASH(0x5ffda584c138)) called at /usr/share/perl5/JSON/Validator/Schema/OpenAPIv2.pm line 174
    JSON::Validator::Schema::OpenAPIv2::validate_response(JSON::Validator::Schema::OpenAPIv3=HASH(0x5ffda54c88a8), ARRAY(0x5ffda5fb2f60), HASH(0x5ffda5fb51e8)) called at /usr/share/perl5/Mojolicious/Plugin/OpenAPI.pm line 256
    Mojolicious::Plugin::OpenAPI::_render(Mojolicious::Renderer=HASH(0x5ffda50396e8), My::App::Controller::Job=HASH(0x5ffda5fb2318), SCALAR(0x5ffda521f090), HASH(0x5ffda5fc4c40)) called at /usr/share/perl5/Mojolicious/Renderer.pm line 229
    Mojolicious::Renderer::_render_template(Mojolicious::Renderer=HASH(0x5ffda50396e8), My::App::Controller::Job=HASH(0x5ffda5fb2318), SCALAR(0x5ffda521f090), HASH(0x5ffda5fc4c40)) called at /usr/share/perl5/Mojolicious/Renderer.pm line 108
    Mojolicious::Renderer::render(Mojolicious::Renderer=HASH(0x5ffda50396e8), My::App::Controller::Job=HASH(0x5ffda5fb2318)) called at /usr/share/perl5/Mojolicious/Controller.pm line 149
    Mojolicious::Controller::render(My::App::Controller::Job=HASH(0x5ffda5fb2318), "openapi", HASH(0x5ffda5fb2f30), "status", 202) called at mock.t line 73
    My::App::__ANON__(My::App::Controller::Job=HASH(0x5ffda5fb2318), HASH(0x5ffda5fb2f30), "status", 202) called at /usr/share/perl5/Mojolicious/Controller.pm line 25
    Mojolicious::Controller::_Dynamic::myrender(My::App::Controller::Job=HASH(0x5ffda5fb2318), HASH(0x5ffda5fb2f30), "status", 202) called at mock.t line 123
    My::App::Controller::Job::status(My::App::Controller::Job=HASH(0x5ffda5fb2318)) called at /usr/share/perl5/Mojolicious.pm line 193
    Mojolicious::_action(undef, My::App::Controller::Job=HASH(0x5ffda5fb2318), CODE(0x5ffda53ca6f0), 1) called at /usr/share/perl5/Mojolicious/Plugins.pm line 15
    Mojolicious::Plugins::__ANON__() called at /usr/share/perl5/Mojolicious/Plugins.pm line 18
    Mojolicious::Plugins::emit_chain(Mojolicious::Plugins=HASH(0x5ffda537a810), "around_action", My::App::Controller::Job=HASH(0x5ffda5fb2318), CODE(0x5ffda53ca6f0), 1) called at /usr/share/perl5/Mojolicious/Routes.pm line 88
    Mojolicious::Routes::_action(Test::APIv2=HASH(0x5ffda5039430), My::App::Controller::Job=HASH(0x5ffda5fb2318), CODE(0x5ffda53ca6f0), 1) called at /usr/share/perl5/Mojolicious/Routes.pm line 161
    Mojolicious::Routes::_controller(Mojolicious::Routes=HASH(0x5ffda3666430), Mojolicious::Controller=HASH(0x5ffda5fb1750), HASH(0x5ffda5fbb368), 1) called at /usr/share/perl5/Mojolicious/Routes.pm line 44
    Mojolicious::Routes::continue(Mojolicious::Routes=HASH(0x5ffda3666430), Mojolicious::Controller=HASH(0x5ffda5fb1750)) called at /usr/share/perl5/Mojolicious/Routes.pm line 52
    Mojolicious::Routes::dispatch(Mojolicious::Routes=HASH(0x5ffda3666430), Mojolicious::Controller=HASH(0x5ffda5fb1750)) called at /usr/share/perl5/Mojolicious.pm line 127
    Mojolicious::dispatch(Test::APIv2=HASH(0x5ffda5039430), Mojolicious::Controller=HASH(0x5ffda5fb1750)) called at /usr/share/perl5/Mojolicious.pm line 136
    Mojolicious::__ANON__(undef, Mojolicious::Controller=HASH(0x5ffda5fb1750)) called at /usr/share/perl5/Mojolicious/Plugins.pm line 15
    Mojolicious::Plugins::__ANON__() called at /usr/share/perl5/Mojolicious.pm line 203
    eval {...} called at /usr/share/perl5/Mojolicious.pm line 203
    Mojolicious::_exception(CODE(0x5ffda5fb2b58), Mojolicious::Controller=HASH(0x5ffda5fb1750)) called at /usr/share/perl5/Mojolicious/Plugins.pm line 15
    Mojolicious::Plugins::__ANON__() called at /usr/share/perl5/Mojolicious/Plugins.pm line 18
    Mojolicious::Plugins::emit_chain(Mojolicious::Plugins=HASH(0x5ffda537a810), "around_dispatch", Mojolicious::Controller=HASH(0x5ffda5fb1750)) called at /usr/share/perl5/Mojolicious.pm line 141
    Mojolicious::handler(Test::APIv2=HASH(0x5ffda5039430), Mojo::Transaction::HTTP=HASH(0x5ffda5f3a4c8)) called at /usr/share/perl5/Mojo/Server.pm line 72
    Mojo::Server::__ANON__(Mojo::Server::Daemon=HASH(0x5ffda5f24a00), Mojo::Transaction::HTTP=HASH(0x5ffda5f3a4c8)) called at /usr/share/perl5/Mojo/EventEmitter.pm line 15
    Mojo::EventEmitter::emit(Mojo::Server::Daemon=HASH(0x5ffda5f24a00), "request", Mojo::Transaction::HTTP=HASH(0x5ffda5f3a4c8)) called at /usr/share/perl5/Mojo/Server/Daemon.pm line 103
    Mojo::Server::Daemon::__ANON__(Mojo::Transaction::HTTP=HASH(0x5ffda5f3a4c8)) called at /usr/share/perl5/Mojo/EventEmitter.pm line 15
    Mojo::EventEmitter::emit(Mojo::Transaction::HTTP=HASH(0x5ffda5f3a4c8), "request") called at /usr/share/perl5/Mojo/Transaction/HTTP.pm line 60
    Mojo::Transaction::HTTP::server_read(Mojo::Transaction::HTTP=HASH(0x5ffda5f3a4c8), "GET /mock/job/58b074fbf5e421e0d79df4a5d38ef2e970a49ff51d06bd5"...) called at /usr/share/perl5/Mojo/Server/Daemon.pm line 224
    Mojo::Server::Daemon::_read(Mojo::Server::Daemon=HASH(0x5ffda5f24a00), "508eabecdacc338f5072fe4f078ab562", "GET /mock/job/58b074fbf5e421e0d79df4a5d38ef2e970a49ff51d06bd5"...) called at /usr/share/perl5/Mojo/Server/Daemon.pm line 202
    Mojo::Server::Daemon::__ANON__(Mojo::IOLoop::Stream=HASH(0x5ffda5f41d70)) called at /usr/share/perl5/Mojo/EventEmitter.pm line 15
    Mojo::EventEmitter::emit(Mojo::IOLoop::Stream=HASH(0x5ffda5f41d70), "read", "GET /mock/job/58b074fbf5e421e0d79df4a5d38ef2e970a49ff51d06bd5"...) called at /usr/share/perl5/Mojo/IOLoop/Stream.pm line 109
    Mojo::IOLoop::Stream::_read(Mojo::IOLoop::Stream=HASH(0x5ffda5f41d70)) called at /usr/share/perl5/Mojo/IOLoop/Stream.pm line 57
    Mojo::IOLoop::Stream::__ANON__(Mojo::Reactor::EV=HASH(0x5ffda4b14e50)) called at /usr/share/perl5/Mojo/Reactor/Poll.pm line 141
    eval {...} called at /usr/share/perl5/Mojo/Reactor/Poll.pm line 141
    Mojo::Reactor::Poll::_try(Mojo::Reactor::EV=HASH(0x5ffda4b14e50), "I/O watcher", CODE(0x5ffda5f422c8), 0) called at /usr/share/perl5/Mojo/Reactor/EV.pm line 54
    Mojo::Reactor::EV::__ANON__(EV::IO=SCALAR(0x5ffda5f42118), 1) called at /usr/share/perl5/Mojo/Reactor/EV.pm line 32
    eval {...} called at /usr/share/perl5/Mojo/Reactor/EV.pm line 32
    Mojo::Reactor::EV::start(Mojo::Reactor::EV=HASH(0x5ffda4b14e50)) called at /usr/share/perl5/Mojo/IOLoop.pm line 134
    Mojo::IOLoop::start(Mojo::IOLoop=HASH(0x5ffda4ae5fd0)) called at /usr/share/perl5/Mojo/UserAgent.pm line 67
    Mojo::UserAgent::start(Mojo::UserAgent=HASH(0x5ffda5031418), Mojo::Transaction::HTTP=HASH(0x5ffda5f6fba8)) called at /usr/share/perl5/Test/Mojo.pm line 400
    Test::Mojo::_request_ok(Test::Mojo=HASH(0x5ffda2da6970), Mojo::Transaction::HTTP=HASH(0x5ffda5f6fba8), "/mock/job/58b074fbf5e421e0d79df4a5d38ef2e970a49ff51d06bd52718"...) called at /usr/share/perl5/Test/Mojo.pm line 343
    Test::Mojo::_build_ok(Test::Mojo=HASH(0x5ffda2da6970), "GET", "/mock/job/58b074fbf5e421e0d79df4a5d38ef2e970a49ff51d06bd52718"...) called at /usr/share/perl5/Test/Mojo.pm line 131
    Test::Mojo::get_ok(Test::Mojo=HASH(0x5ffda2da6970), "/mock/job/58b074fbf5e421e0d79df4a5d38ef2e970a49ff51d06bd52718"...) called at mock.t line 24

I understand it comes from JSON::Validator, but why does it bite me only on the job endpoint?

Episode 7 - CPAN Security Group

The Underbar
This is the last of the interviews recorded during the Perl Toolchain Summit 2025 in Leipzig, this time with the CPAN Security Group. We talked about how the group was formed, the security landscape for Perl and CPAN, and how volunteers are always needed.

cpan/Test-Simple - Update to version 1.302215

Perl commits on GitHub
cpan/Test-Simple - Update to version 1.302215

1.302215  2025-11-13 16:41:55-08:00 America/Los_Angeles

cpan/Time-Piece - Update to version 1.41

Perl commits on GitHub
cpan/Time-Piece - Update to version 1.41

1.41  2025-11-12
        - strptime: parse micro seconds (RT165677, RT133599)
        - add to_gmtime and to_localtime (RT113979)

Go Ahead ‘make’ My Day (Part III)

Perl on Medium

This is the last in a 3 part series on . You can catch up by reading our introduction and dissection of Scriptlets.

Go Ahead ‘make’ My Day (Part III)

This is the last in a 3 part series on Scriptlets. You can catch up by reading our introduction and dissection of Scriptlets.

In this final part, we talk about restraint - the discipline that keeps a clever trick from turning into a maintenance hazard.

That uneasy feeling…

So you are starting to write a few scriptlets and it seems pretty cool. But something doesn’t feel quite right…

You’re editing a Makefile and suddenly you feel anxious. Ah, you expected syntax highlighting, linting, proper indentation, and maybe that warm blanket of static analysis. So when we drop a 20 - line chunk of Perl or Python into our Makefile, our inner OCD alarms go off. No highlighting. No linting. Just raw text.

The discomfort isn’t a flaw - it’s feedback. It tells you when you’ve added too much salt to the soup.

A scriptlet is not a script!

A scriptlet is a small, focused snippet of code embedded inside a Makefile that performs one job quickly and deterministically. The “-let” suffix matters. It’s not a standalone program. It’s a helper function, a convenience, a single brushstroke that belongs in the same canvas as the build logic it supports.

If you ever feel the urge to bite your nails, pick at your skin, or start counting the spaces in your indentation - stop. You’ve crossed the line. What you’ve written is no longer a scriptlet; it’s a script. Give it a real file, a shebang, and a test harness. Keep the build clean.

Why we use them

Scriptlets shine where proximity and simplicity matter more than reuse (not that we can’t throw it in a separate file and include it in our Makefile).

  • Cleanliness: prevents a recipe from looking like a shell script.
  • Locality: live where they’re used. No path lookups, no installs.
  • Determinism: transform well-defined input into output. Nothing more.
  • Portability (of the idea): every CI/CD system that can run make can run a one-liner.

A Makefile that can generate its own dependency file, extract version numbers, or rewrite a cpanfile doesn’t need a constellation of helper scripts. It just needs a few lines of inline glue.

Why they’re sometimes painful

We lose the comforts that make us feel like professional developers:

  • No syntax highlighting.
  • No linting or type hints.
  • No indentation guides.
  • No “Format on Save.”

The trick is to accept that pain as a necessary check on the limits of the scriptlet. If you’re constantly wishing for linting and editor help, it’s your subconscious telling you: this doesn’t belong inline anymore. You’ve outgrown the -let.

When to promote your scriplet to a script

Promote a scriptlet to a full-blown script when:

  • It exceeds 30-50 lines.
  • It gains conditionals or error handling.
  • You need to test it independently.
  • It uses more than 1 or 2 non-core features.
  • It’s used by more than one target or project.
  • You’re debugging quoting more than logic.
  • You’re spending more time fixing indentation, than working on the build

At that point, you’re writing software, not glue. Give it a name, a shebang, and a home in your tools/ directory.

When to keep it inside your Makefile

Keep it inline when:

  • It’s short, pure, and single-use.
  • It depends primarily on the environment already assumed by your build (Perl, Python, awk, etc.).
  • It’s faster to read than to reference.

A good scriptlet reads like a make recipe: do this transformation right here, right now.

define create_cpanfile =
    while (<STDIN>) {
        s/[#].*//; s/^\s+|\s+$//g; next if $_ eq q{};
        my ($mod,$v) = split /\s+/, $_, 2;
        print qq{requires "$mod", "$v";\n};
    }
endef

export s_create_cpanfile = $(value create_cpanfile)

That’s a perfect scriptlet: small, readable, deterministic, and local.

Rule of Thumb: If it fits on one screen, keep it inline. If it scrolls, promote it.

Tools for the OCD developer

If you must relieve the OCD symptoms without promotion of your scriptlet to a script

  • Add a lint-scriptlets target: perl -c -e '$(s_create_requires)' checks syntax without running it.
  • Some editors (Emacs mmm-mode, Vim polyglot) can treat marked sections as sub-languages to enable localized language specific editing features.
  • Use include to include a scriptlet into your Makefile

…however try to resist the urge to over-optimize the tooling. Feeling the uneasiness grow helps identify the boundary between scriptlets and scripts.

You’ve been warned!

Because scriptlets are powerful, flexible, and fast, it’s easy to reach for them too often or make them the focus of your project. They start as a cure for friction - a way to express a small transformation inline - but left unchecked, they can sometimes grow arms and legs. Before long, your Makefile turns into a Frankenstein monster.

The great philosopher Basho (or at least I think it was him) once said:

A single aspirin tablet eases pain. A whole bottle sends you to the hospital.

Thanks for reading.

Learn More

I am trying to implement a Mojolicious application that (also) acts as a proxy in front or rclone serve.

To that purpose I am trying to get Mojolicious to act as a proxy, and request from rclone serve and serve the response. Pure redirecting won't do for authentication reasons.

This is the minimal working example I got thus far:

use Mojolicious::Lite -signatures;
use Mojo::Util qw/dumper/;
use Time::HiRes qw/time/;

helper "handle" => sub {
    my $self = shift;

    my $start = time(); my $start_all = $start;
    my $req = $self->req->clone;
    $req->url->scheme("http")->host("127.0.0.1")->port("3002");

    my $ua = $self->ua;
    my $tx = $ua->start(Mojo::Transaction::HTTP->new(req => $req));

    $self->res->headers->from_hash($tx->res->headers->to_hash);
    my $body = $tx->res->body;
    $self->render(data => $body);
};


get '/*reqpath' => sub ($c) {
    return $c->handle();
};

app->start;

It works fine - but is very slow, as getting the same content directly vs. via Mojolicious takes about 5 times as long.

The culprit seems to be

my $tx = $ua->start(Mojo::Transaction::HTTP->new(req => $req));

What's the reason? what should I do differently?

I have also tried to do that asynchronously, but as I expected it did not speed up the single transaction, it just became more responsive across a bunch of them.

For years, most of my Perl web apps lived happily enough on a VPS. I had full control of the box, I could install whatever I liked, and I knew where everything lived.

In fact, over the last eighteen months or so, I wrote a series of blog posts explaining how I developed a system for deploying Dancer2 apps and, eventually, controlling them using systemd. I’m slightly embarrassed by those posts now.

Because the control that my VPS gave me also came with a price: I also had to worry about OS upgrades, SSL renewals, kernel updates, and the occasional morning waking up to automatic notifications that one of my apps had been offline since midnight.

Back in 2019, I started writing a series of blog posts called Into the Cloud that would follow my progress as I moved all my apps into Docker containers. But real life intruded and I never made much progress on the project.

Recently, I returned to this idea (yes, I’m at least five years late here!) I’ve been working on migrating those old Dancer2 applications from my IONOS VPS to Google Cloud Run. The difference has been amazing. My apps now run in their own containers, scale automatically, and the server infrastructure requires almost no maintenance.

This post walks through how I made the jump – and how you can too – using Perl, Dancer2, Docker, GitHub Actions, and Google Cloud Run.


Why move away from a VPS?

Running everything on a single VPS used to make sense. You could ssh in, restart services, and feel like you were in control. But over time, the drawbacks grow:

  • You have to maintain the OS and packages yourself.

  • One bad app or memory leak can affect everything else.

  • You’re paying for full-time CPU and RAM even when nothing’s happening.

  • Scaling means provisioning a new server — not something you do in a coffee break.

Cloud Run, on the other hand, runs each app as a container and only charges you while requests are being served. When no-one’s using your app, it scales to zero and costs nothing.

Even better: no servers to patch, no ports to open, no SSL certificates to renew — Google does all of that for you.


What we’ll build

Here’s the plan. We’ll take a simple Dancer2 app and:

  1. Package it as a Docker container.

  2. Build that container automatically in GitHub Actions.

  3. Deploy it to Google Cloud Run, where it runs securely and scales automatically.

  4. Map a custom domain to it and forget about server admin forever.

If you’ve never touched Docker or Cloud Run before, don’t worry – I’ll explain what’s going on as we go.


Why Cloud Run fits Perl surprisingly well

Perl’s ecosystem has always valued stability and control. Containers give you both: you can lock in a Perl version, CPAN modules, and any shared libraries your app needs. The image you build today will still work next year.

Cloud Run runs those containers on demand. It’s effectively a managed starman farm where Google handles the hard parts – scaling, routing, and HTTPS.

You pay for CPU and memory per request, not per server. For small or moderate-traffic Perl apps, it’s often well under £1/month.


Step 1: Dockerising a Dancer2 app

If you’re new to Docker, think of it as a way of bundling your whole environment — Perl, modules, and configuration — into a portable image. It’s like freezing a working copy of your app so it can run identically anywhere.

Here’s a minimal Dockerfile for a Dancer2 app:

FROM perl:5.42
LABEL maintainer="dave@perlhacks.com"

# Install Carton and Starman
RUN cpanm Carton Starman

# Copy the app into the container
COPY . /app
WORKDIR /app

# Install dependencies
RUN carton install --deployment

EXPOSE 8080
CMD ["carton", "exec", "starman", "--port", "8080", "bin/app.psgi"]

Let’s break that down:

  • FROM perl:5.42 — starts from an official Perl image on Docker Hub.

  • Carton keeps dependencies consistent between environments.

  • The app is copied into /app, and carton install --deployment installs exactly what’s in your cpanfile.snapshot.

  • The container exposes port 8080 (Cloud Run’s default).

  • The CMD runs Starman, serving your Dancer2 app.

To test it locally:

docker build -t myapp .
docker run -p 8080:8080 myapp

Then visit http://localhost:8080. If you see your Dancer2 homepage, you’ve successfully containerised your app.


Step 2: Building the image in GitHub Actions

Once it works locally, we can automate it. GitHub Actions will build and push our image to Google Artifact Registry whenever we push to main or tag a release.

Here’s a simplified workflow file (.github/workflows/build.yml):

name: Build container

on:
  push:
    branches: [ main ]
    tags: [ 'v*' ]
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: google-github-actions/setup-gcloud@v3
        with:
          project_id: ${{ secrets.GCP_PROJECT }}
          service_account_email: ${{ secrets.GCP_SA_EMAIL }}
          workload_identity_provider: ${{ secrets.GCP_WIF_PROVIDER }}

      - name: Build and push image
        run: |
          IMAGE="europe-west1-docker.pkg.dev/${{ secrets.GCP_PROJECT }}/containers/myapp:$GITHUB_SHA"
          docker build -t $IMAGE .
          docker push $IMAGE

You’ll notice a few secrets referenced in the workflow — things like your Google Cloud project ID and credentials. These are stored securely in GitHub Actions. When the workflow runs, GitHub uses those secrets to authenticate as you and access your Google Cloud account, so it can push the new container image or deploy your app.

You only set those secrets up once, and they’re encrypted and hidden from everyone else — even if your repository is public.

Once that’s set up, every push builds a fresh, versioned container image.


Step 3: Deploying to Cloud Run

Now we’re ready to run it in the cloud. We’ll do that using Google’s command line program, gcloud. It’s available from Google’s official downloads or through most Linux package managers — for example:

# Fedora, RedHat or similar
sudo dnf install google-cloud-cli
# or on Debian/Ubuntu:
sudo apt install google-cloud-cli

Once installed, authenticate it with your Google account:

gcloud auth login
gcloud config set project your-project-id

That links the CLI to your Google Cloud project and lets it perform actions like deploying to Cloud Run.

Once that’s done, you can deploy manually from the command line:

gcloud run deploy myapp \
--image=europe-west1-docker.pkg.dev/MY_PROJECT/containers/myapp:$GITHUB_SHA \
--region=europe-west1 \
--allow-unauthenticated \
--port=8080

This tells Cloud Run to start a new service called myapp, using the image we just built.

After a minute or two, Google will give you a live HTTPS URL, like:

    • https://myapp-abcdef12345-ew.a.run.app

Visit it — and if all went well, you’ll see your familiar Dancer2 app, running happily on Cloud Run.

To connect your own domain, run:

gcloud run domain-mappings create \
--service=myapp \
--domain=myapp.example.com

Then update your DNS records as instructed. Within an hour or so, Cloud Run will issue a free SSL certificate for you.


Step 4: Automating the deployment

Once the manual deployment works, we can automate it too.

Here’s a second GitHub Actions workflow (deploy.yml) that triggers after a successful build:

name: Deploy container

on:
  workflow_run:
    workflows: [ "Build container" ]
    types: [ completed ]

jobs:
  deploy:
    runs-on: ubuntu-latest
    if: ${{ github.event.workflow_run.conclusion == 'success' }}

    steps:
      - uses: google-github-actions/setup-gcloud@v3
        with:
          project_id: ${{ secrets.GCP_PROJECT }}
          service_account_email: ${{ secrets.GCP_SA_EMAIL }}
          workload_identity_provider: ${{ secrets.GCP_WIF_PROVIDER }}

      - name: Deploy to Cloud Run
        run: |
          gcloud run deploy myapp \
            --image=europe-west1-docker.pkg.dev/${{ secrets.GCP_PROJECT }}/containers/myapp:$GITHUB_SHA \
            --region=europe-west1 \
            --allow-unauthenticated \
            --port=8080

Now every successful push to main results in an automatic deployment to production.

You can take it further by splitting environments — e.g. main deploys to staging, tagged releases to production — but even this simple setup is a big step forward from ssh and git pull.


Step 5: Environment variables and configuration

Each Cloud Run service can have its own configuration and secrets. You can set these from the console or CLI:

gcloud run services update myapp \
  --set-env-vars="DANCER_ENV=production,DATABASE_URL=postgres://..."

In your Dancer2 app, you can then access them with:

$ENV{DATABASE_URL}

It’s a good idea to keep database credentials and API keys out of your code and inject them at deploy time like this.


Step 6: Monitoring and logs

Cloud Run integrates neatly with Google Cloud’s logging tools.

To see recent logs from your app:

gcloud logs read --project=$PROJECT_NAME --service=myapp

You’ll see your Dancer2 warn and die messages there too, because STDOUT and STDERR are automatically captured.

If you prefer a UI, you can use the Cloud Console’s Log Explorer to filter by service or severity.


Step 7: The payoff

Once you’ve done one migration, the next becomes almost trivial. Each Dancer2 app gets:

  • Its own Dockerfile and GitHub workflows.

  • Its own Cloud Run service and domain.

  • Its own scaling and logging.

And none of them share a single byte of RAM with each other.

Here’s how the experience compares:

Aspect Old VPS Cloud Run
OS maintenance Manual upgrades Managed
Scaling Fixed size Automatic
SSL Let’s Encrypt renewals Automatic
Deployment SSH + git pull Push to GitHub
Cost Fixed monthly Pay-per-request
Downtime risk One app can crash all Each isolated

For small apps with light traffic, Cloud Run often costs pennies per month – less than the price of a coffee for peace of mind.


Lessons learned

After a few migrations, a few patterns emerged:

  • Keep apps self-contained. Don’t share config or code across services; treat each app as a unit.

  • Use digest-based deploys. Deploy by image digest (@sha256:...) rather than tag for true immutability.

  • Logs are your friend. Cloud Run’s logs are rich; you rarely need to ssh anywhere again.

  • Cold starts exist, but aren’t scary. If your app is infrequently used, expect the first request after a while to take a second longer.

  • CI/CD is liberating. Once the pipeline’s in place, deployment becomes a non-event.


Costs and practicalities

One of the most pleasant surprises was the cost. My smallest Dancer2 app, which only gets a handful of requests each day, usually costs under £0.50/month on Cloud Run. Heavier ones rarely top a few pounds.

Compare that to the £10–£15/month I was paying for the old VPS — and the VPS didn’t scale, didn’t auto-restart cleanly, and didn’t come with HTTPS certificates for free.


What’s next

This post covers the essentials: containerising a Dancer2 app and deploying it to Cloud Run via GitHub Actions.

In future articles, I’ll look at:

  • Connecting to persistent databases.

  • Using caching.

  • Adding monitoring and dashboards.

  • Managing secrets with Google Secret Manager.


Conclusion

After two decades of running Perl web apps on traditional servers, Cloud Run feels like the future has finally caught up with me.

You still get to write your code in Dancer2 – the framework that’s made Perl web development fun for years – but you deploy it in a way that’s modern, repeatable, and blissfully low-maintenance.

No more patching kernels. No more 3 a.m. alerts. Just code, commit, and dance in the clouds.

The post Dancing in the Clouds: Moving Dancer2 Apps from a VPS to Cloud Run first appeared on Perl Hacks.

For years, most of my Perl web apps lived happily enough on a VPS. I had full control of the box, I could install whatever I liked, and I knew where everything lived.

In fact, over the last eighteen months or so, I wrote a series of blog posts explaining how I developed a system for deploying Dancer2 apps and, eventually, controlling them using systemd. I’m slightly embarrassed by those posts now.

Because the control that my VPS gave me also came with a price: I also had to worry about OS upgrades, SSL renewals, kernel updates, and the occasional morning waking up to automatic notifications that one of my apps had been offline since midnight.

Back in 2019, I started writing a series of blog posts called Into the Cloud that would follow my progress as I moved all my apps into Docker containers. But real life intruded and I never made much progress on the project.

Recently, I returned to this idea (yes, I’m at least five years late here!) I’ve been working on migrating those old Dancer2 applications from my IONOS VPS to Google Cloud Run. The difference has been amazing. My apps now run in their own containers, scale automatically, and the server infrastructure requires almost no maintenance.

This post walks through how I made the jump – and how you can too – using Perl, Dancer2, Docker, GitHub Actions, and Google Cloud Run.

Why move away from a VPS?

Running everything on a single VPS used to make sense. You could ssh in, restart services, and feel like you were in control. But over time, the drawbacks grow:

  • You have to maintain the OS and packages yourself.

  • One bad app or memory leak can affect everything else.

  • You’re paying for full-time CPU and RAM even when nothing’s happening.

  • Scaling means provisioning a new server — not something you do in a coffee break.

Cloud Run, on the other hand, runs each app as a container and only charges you while requests are being served. When no-one’s using your app, it scales to zero and costs nothing.

Even better: no servers to patch, no ports to open, no SSL certificates to renew — Google does all of that for you.

What we’ll build

Here’s the plan. We’ll take a simple Dancer2 app and:

  1. Package it as a Docker container.

  2. Build that container automatically in GitHub Actions.

  3. Deploy it to Google Cloud Run , where it runs securely and scales automatically.

  4. Map a custom domain to it and forget about server admin forever.

If you’ve never touched Docker or Cloud Run before, don’t worry – I’ll explain what’s going on as we go.

Why Cloud Run fits Perl surprisingly well

Perl’s ecosystem has always valued stability and control. Containers give you both: you can lock in a Perl version, CPAN modules, and any shared libraries your app needs. The image you build today will still work next year.

Cloud Run runs those containers on demand. It’s effectively a managed starman farm where Google handles the hard parts – scaling, routing, and HTTPS.

You pay for CPU and memory per request, not per server. For small or moderate-traffic Perl apps, it’s often well under £1/month.

Step 1: Dockerising a Dancer2 app

If you’re new to Docker, think of it as a way of bundling your whole environment — Perl, modules, and configuration — into a portable image. It’s like freezing a working copy of your app so it can run identically anywhere.

Here’s a minimal Dockerfile for a Dancer2 app:

FROM perl:5.42
LABEL maintainer="dave@perlhacks.com"

# Install Carton and Starman
RUN cpanm Carton Starman

# Copy the app into the container
COPY . /app
WORKDIR /app

# Install dependencies
RUN carton install --deployment

EXPOSE 8080
CMD ["carton", "exec", "starman", "--port", "8080", "bin/app.psgi"]

Let’s break that down:

  • FROM perl:5.42 — starts from an official Perl image on Docker Hub.

  • Carton keeps dependencies consistent between environments.

  • The app is copied into /app, and carton install --deployment installs exactly what’s in your cpanfile.snapshot.

  • The container exposes port 8080 (Cloud Run’s default).

  • The CMD runs Starman, serving your Dancer2 app.

To test it locally:

docker build -t myapp .
docker run -p 8080:8080 myapp

Then visit http://localhost:8080. If you see your Dancer2 homepage, you’ve successfully containerised your app.

Step 2: Building the image in GitHub Actions

Once it works locally, we can automate it. GitHub Actions will build and push our image to Google Artifact Registry whenever we push to main or tag a release.

Here’s a simplified workflow file (.github/workflows/build.yml):

name: Build container

on:
  push:
    branches: [main]
    tags: ['v*']
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: google-github-actions/setup-gcloud@v3
        with:
          project_id: ${{ secrets.GCP_PROJECT }}
          service_account_email: ${{ secrets.GCP_SA_EMAIL }}
          workload_identity_provider: ${{ secrets.GCP_WIF_PROVIDER }}

      - name: Build and push image
        run: |
          IMAGE="europe-west1-docker.pkg.dev/${{ secrets.GCP_PROJECT }}/containers/myapp:$GITHUB_SHA"
          docker build -t $IMAGE .
          docker push $IMAGE

You’ll notice a few secrets referenced in the workflow — things like your Google Cloud project ID and credentials. These are stored securely in GitHub Actions. When the workflow runs, GitHub uses those secrets to authenticate as you and access your Google Cloud account, so it can push the new container image or deploy your app.

You only set those secrets up once, and they’re encrypted and hidden from everyone else — even if your repository is public.

Once that’s set up, every push builds a fresh, versioned container image.

Step 3: Deploying to Cloud Run

Now we’re ready to run it in the cloud. We’ll do that using Google’s command line program, gcloud. It’s available from Google’s official downloads or through most Linux package managers — for example:

# Fedora, RedHat or similar
sudo dnf install google-cloud-cli
# or on Debian/Ubuntu:
sudo apt install google-cloud-cli

Once installed, authenticate it with your Google account:

gcloud auth login
gcloud config set project your-project-id

That links the CLI to your Google Cloud project and lets it perform actions like deploying to Cloud Run.

Once that’s done, you can deploy manually from the command line:

gcloud run deploy myapp \
--image=europe-west1-docker.pkg.dev/MY_PROJECT/containers/myapp:$GITHUB_SHA \
--region=europe-west1 \
--allow-unauthenticated \
--port=8080

This tells Cloud Run to start a new service called myapp, using the image we just built.

After a minute or two, Google will give you a live HTTPS URL, like:

Visit it — and if all went well, you’ll see your familiar Dancer2 app, running happily on Cloud Run.

To connect your own domain, run:

gcloud run domain-mappings create \
--service=myapp \
--domain=myapp.example.com

Then update your DNS records as instructed. Within an hour or so, Cloud Run will issue a free SSL certificate for you.

Step 4: Automating the deployment

Once the manual deployment works, we can automate it too.

Here’s a second GitHub Actions workflow (deploy.yml) that triggers after a successful build:

name: Deploy container

on:
  workflow_run:
    workflows: ["Build container"]
    types: [completed]

jobs:
  deploy:
    runs-on: ubuntu-latest
    if: ${{ github.event.workflow_run.conclusion == 'success' }}

    steps:
      - uses: google-github-actions/setup-gcloud@v3
        with:
          project_id: ${{ secrets.GCP_PROJECT }}
          service_account_email: ${{ secrets.GCP_SA_EMAIL }}
          workload_identity_provider: ${{ secrets.GCP_WIF_PROVIDER }}

      - name: Deploy to Cloud Run
        run: |
          gcloud run deploy myapp \
            --image=europe-west1-docker.pkg.dev/${{ secrets.GCP_PROJECT }}/containers/myapp:$GITHUB_SHA \
            --region=europe-west1 \
            --allow-unauthenticated \
            --port=8080

Now every successful push to main results in an automatic deployment to production.

You can take it further by splitting environments — e.g. main deploys to staging, tagged releases to production — but even this simple setup is a big step forward from ssh and git pull.

Step 5: Environment variables and configuration

Each Cloud Run service can have its own configuration and secrets. You can set these from the console or CLI:

gcloud run services update myapp \
  --set-env-vars="DANCER_ENV=production,DATABASE_URL=postgres://..."

In your Dancer2 app, you can then access them with:

$ENV{DATABASE_URL}

It’s a good idea to keep database credentials and API keys out of your code and inject them at deploy time like this.

Step 6: Monitoring and logs

Cloud Run integrates neatly with Google Cloud’s logging tools.

To see recent logs from your app:

gcloud logs read --project=$PROJECT_NAME --service=myapp

You’ll see your Dancer2 warn and die messages there too, because STDOUT and STDERR are automatically captured.

If you prefer a UI, you can use the Cloud Console’s Log Explorer to filter by service or severity.

Step 7: The payoff

Once you’ve done one migration, the next becomes almost trivial. Each Dancer2 app gets:

  • Its own Dockerfile and GitHub workflows.

  • Its own Cloud Run service and domain.

  • Its own scaling and logging.

And none of them share a single byte of RAM with each other.

Here’s how the experience compares:

Aspect Old VPS Cloud Run
OS maintenance Manual upgrades Managed
Scaling Fixed size Automatic
SSL Let’s Encrypt renewals Automatic
Deployment SSH + git pull Push to GitHub
Cost Fixed monthly Pay-per-request
Downtime risk One app can crash all Each isolated

For small apps with light traffic, Cloud Run often costs pennies per month – less than the price of a coffee for peace of mind.

Lessons learned

After a few migrations, a few patterns emerged:

  • Keep apps self-contained. Don’t share config or code across services; treat each app as a unit.

  • Use digest-based deploys. Deploy by image digest (@sha256:...) rather than tag for true immutability.

  • Logs are your friend. Cloud Run’s logs are rich; you rarely need to ssh anywhere again.

  • Cold starts exist, but aren’t scary. If your app is infrequently used, expect the first request after a while to take a second longer.

  • CI/CD is liberating. Once the pipeline’s in place, deployment becomes a non-event.

Costs and practicalities

One of the most pleasant surprises was the cost. My smallest Dancer2 app, which only gets a handful of requests each day, usually costs under £0.50/month on Cloud Run. Heavier ones rarely top a few pounds.

Compare that to the £10–£15/month I was paying for the old VPS — and the VPS didn’t scale, didn’t auto-restart cleanly, and didn’t come with HTTPS certificates for free.

What’s next

This post covers the essentials: containerising a Dancer2 app and deploying it to Cloud Run via GitHub Actions.

In future articles, I’ll look at:

  • Connecting to persistent databases.

  • Using caching.

  • Adding monitoring and dashboards.

  • Managing secrets with Google Secret Manager.

Conclusion

After two decades of running Perl web apps on traditional servers, Cloud Run feels like the future has finally caught up with me.

You still get to write your code in Dancer2 – the framework that’s made Perl web development fun for years – but you deploy it in a way that’s modern, repeatable, and blissfully low-maintenance.

No more patching kernels. No more 3 a.m. alerts. Just code, commit, and dance in the clouds.

The post Dancing in the Clouds: Moving Dancer2 Apps from a VPS to Cloud Run first appeared on Perl Hacks.

Go Ahead ‘make’ My Day (Part II)

In our previous blog post “Go Ahead ‘make’ My Day” we presented the scriptlet, an advanced make technique for spicing up your Makefile recipes. In this follow-up, we’ll deconstruct the scriptlet and detail the ingredients that make up the secret sauce.


Introducing the Scriptlet

Makefile scriptlets are an advanced technique that uses GNU make’s powerful functions to safely embed a multi-line script (Perl, in our example) into a single, clean shell command. It turns a complex block of logic into an easily executable template.

An Example Scriptlet

#-*- mode: makefile; -*-

DARKPAN_TEMPLATE="https://cpan.openbedrock.net/orepan2/authors/D/DU/DUMMY/%s-%s.tar.gz"

define create_requires =
 # scriptlet to create cpanfile from an list of required Perl modules
 # skip comments
 my $DARKPAN_TEMPLATE=$ENV{DARKPAN_TEMPLATE};

 while (s/^#[^\n]+\n//g){};

 # skip blank lines
 while (s/\n\n/\n/) {};

 for (split/\n/) { 
  my ($mod, $v) = split /\s+/;
  next if !$mod;

  my $dist = $mod;
  $dist =~s/::/\-/g;

  my $url = sprintf $DARKPAN_TEMPLATE, $dist, $v;

  print <<"EOF";
requires \"$mod\", \"$v\",
  url => \"$url\";
EOF
 }

endef

export s_create_requires = $(value create_requires)

cpanfile.darkpan: requires.darkpan
    DARKPAN_TEMPLATE=$(DARKPAN_TEMPLATE); \
    DARKPAN_TEMPLATE=$$DARKPAN_TEMPLATE perl -0ne "$$s_create_requires" $< > $@ || rm $@

Dissecting the Scriptlet

1. The Container: Defining the Script (define / endef)

This section creates the multi-line variable that holds your entire Perl program.

define create_requires =
# Perl code here...
endef
  • define ... endef: This is GNU Make’s mechanism for defining a recursively expanded variable that spans multiple lines. The content is not processed by the shell yet; it’s simply stored by make.
  • The Advantage: This is the only clean way to write readable, indented code (like your while loop and if statements) directly inside a Makefile.

2. The Bridge: Passing Environment Data (my $ENV{...})

This is a critical step for making your script template portable and configurable.

my $DARKPAN_TEMPLATE=$ENV{DARKPAN_TEMPLATE};
  • The Problem: Your Perl script needs dynamic values (like the template URL) that are set by make.
  • The Solution: Instead of hardcoding the URL, the Perl code is designed to read from the shell environment variable $ENV{DARKPAN_TEMPLATE}. This makes the script agnostic to its calling environment, delegating the data management back to the Makefile.

3. The Transformer: Shell Preparation (export and $(value))

This is the “magic” that turns the multi-line Make variable into a single, clean shell command.

export s_create_requires = $(value create_requires)
  • $(value create_requires): This is a specific Make function that performs a direct, single-pass expansion of the variable’s raw content. Crucially, it converts the entire multi-line block into a single-line string suitable for export, preserving special characters and line breaks that the shell will execute.
  • export s_create_requires = ...: This exports the multi-line Perl script content as an environment variable (s_create_requires) that will be accessible to any shell process running in the recipe’s environment.

4. The Execution: Atomic Execution ($$ and perl -0ne)

The final recipe executes the entire, complex process as a single, atomic operation, which is the goal of robust Makefiles.

cpanfile.darkpan: requires.darkpan
    DARKPAN_TEMPLATE=$(DARKPAN_TEMPLATE); \
    DARKPAN_TEMPLATE=$$DARKPAN_TEMPLATE perl -0ne "$$s_create_requires" $< > $@ || rm $@
  • DARKPAN_TEMPLATE=$(DARKPAN_TEMPLATE): This creates the local shell variable.
  • DARKPAN_TEMPLATE=$$DARKPAN_TEMPLATE perl...: This is the clean execution. The first DARKPAN_TEMPLATE= passes the newly created shell variable’s value as an environment variable to the perl process. The $$ ensures the shell variable is properly expanded before the Perl interpreter runs it.
  • perl -0ne "...": Runs the Perl script:
    * `-n` and `-e` (Execute script on input)
    * `-0`: Tells Perl to read the input as one single block
      (slurping the file), which is necessary for your multi-line
      regex and `split/\n/` logic.
    
  • || rm $@: This is the final mark of quality. It makes the entire command transactional—if the Perl script fails, the half-written target file ($@) is deleted, forcing make to try again later.

Hey Now! You’re a Rockstar!

(..get your game on!)

Mastering build automation using make will transform you from being an average DevOps engineer into a rockstar. GNU make is a Swiss Army knife with more tools than you might think! The knives are sharp and the tools are highly targeted to handle all the real-world issues build automation has encountered over the decades. Learning to use make effectively will put you head and shoulders above the herd (see what I did there? 😉).

Calling All Pythonistas!

The scriptlet technique creates a powerful, universal pattern for clean, atomic builds:

  • It’s Language Agnostic: Pythonistas! Join the fun! The same define/export technique works perfectly with python -c.
  • The Win: This ensures that every developer - regardless of their preferred language - can achieve the same clean, atomic build and avoid external script chaos.

Learn more about GNU make and move your Makefiles from simple shell commands to precision instruments of automation.

Thanks for reading.

Learn More

Shutter crashing

Perl Maven

A long time ago I used Shutter and found it as an excellent tool. Now I get all kinds of crashes.

Actually "Now" was a while ago, since then I upgraded Ubuntu and now I get all kinds of other error messages.

However, I wonder.

Why are there so many errors?

Who's fault is it?

  • A failure of the Perl community?

  • A failure of the Ubuntu or the Debian developers?

  • A failure of the whole idea of Open Source?

  • Maybe I broke the system?

It starts so badly and then it crashes. I don't want to spend time figuring out what is the problem. I don't even have the energy to open a ticket. I am not even sure where should I do it. On Ubuntu? On the Shutter project?

Here is the output:

$ shutter
Subroutine Pango::Layout::set_text redefined at /usr/share/perl5/Gtk3.pm line 2299.
	require Gtk3.pm called at /usr/bin/shutter line 72
	Shutter::App::BEGIN() called at /usr/bin/shutter line 72
	eval {...} called at /usr/bin/shutter line 72
Subroutine Pango::Layout::set_markup redefined at /usr/share/perl5/Gtk3.pm line 2305.
	require Gtk3.pm called at /usr/bin/shutter line 72
	Shutter::App::BEGIN() called at /usr/bin/shutter line 72
	eval {...} called at /usr/bin/shutter line 72
GLib-GObject-CRITICAL **: g_boxed_type_register_static: assertion 'g_type_from_name (name) == 0' failed at /usr/lib/x86_64-linux-gnu/perl5/5.36/Glib/Object/Introspection.pm line 110.
 at /usr/share/perl5/Gtk3.pm line 489.
	Gtk3::import("Gtk3", "-init") called at /usr/bin/shutter line 72
	Shutter::App::BEGIN() called at /usr/bin/shutter line 72
	eval {...} called at /usr/bin/shutter line 72
GLib-CRITICAL **: g_once_init_leave: assertion 'result != 0' failed at /usr/lib/x86_64-linux-gnu/perl5/5.36/Glib/Object/Introspection.pm line 110.
 at /usr/share/perl5/Gtk3.pm line 489.
	Gtk3::import("Gtk3", "-init") called at /usr/bin/shutter line 72
	Shutter::App::BEGIN() called at /usr/bin/shutter line 72
	eval {...} called at /usr/bin/shutter line 72
GLib-GObject-CRITICAL **: g_boxed_type_register_static: assertion 'g_type_from_name (name) == 0' failed at /usr/lib/x86_64-linux-gnu/perl5/5.36/Glib/Object/Introspection.pm line 110.
 at /usr/share/perl5/Gtk3.pm line 489.
	Gtk3::import("Gtk3", "-init") called at /usr/bin/shutter line 72
	Shutter::App::BEGIN() called at /usr/bin/shutter line 72
	eval {...} called at /usr/bin/shutter line 72
GLib-CRITICAL **: g_once_init_leave: assertion 'result != 0' failed at /usr/lib/x86_64-linux-gnu/perl5/5.36/Glib/Object/Introspection.pm line 110.
 at /usr/share/perl5/Gtk3.pm line 489.
	Gtk3::import("Gtk3", "-init") called at /usr/bin/shutter line 72
	Shutter::App::BEGIN() called at /usr/bin/shutter line 72
	eval {...} called at /usr/bin/shutter line 72
GLib-GObject-CRITICAL **: g_boxed_type_register_static: assertion 'g_type_from_name (name) == 0' failed at /usr/lib/x86_64-linux-gnu/perl5/5.36/Glib/Object/Introspection.pm line 110.
 at /usr/share/perl5/Gtk3.pm line 489.
	Gtk3::import("Gtk3", "-init") called at /usr/bin/shutter line 72
	Shutter::App::BEGIN() called at /usr/bin/shutter line 72
	eval {...} called at /usr/bin/shutter line 72
GLib-CRITICAL **: g_once_init_leave: assertion 'result != 0' failed at /usr/lib/x86_64-linux-gnu/perl5/5.36/Glib/Object/Introspection.pm line 110.
 at /usr/share/perl5/Gtk3.pm line 489.
	Gtk3::import("Gtk3", "-init") called at /usr/bin/shutter line 72
	Shutter::App::BEGIN() called at /usr/bin/shutter line 72
	eval {...} called at /usr/bin/shutter line 72
Variable "$progname_active" will not stay shared at /usr/bin/shutter line 2778.
Variable "$progname" will not stay shared at /usr/bin/shutter line 2779.
Variable "$im_colors_active" will not stay shared at /usr/bin/shutter line 2787.
Variable "$combobox_im_colors" will not stay shared at /usr/bin/shutter line 2788.
Variable "$trans_check" will not stay shared at /usr/bin/shutter line 2798.


... About 700 similar error messages ...


Name "Gtk3::Gdk::SELECTION_CLIPBOARD" used only once: possible typo at /usr/bin/shutter line 291.
WARNING: gnome-web-photo is missing --> screenshots of websites will be disabled!

 at /usr/bin/shutter line 9038.
	Shutter::App::fct_init_depend() called at /usr/bin/shutter line 195
Useless use of hash element in void context at /usr/share/perl5/Shutter/App/Common.pm line 77.
	require Shutter/App/Common.pm called at /usr/bin/shutter line 206
Useless use of hash element in void context at /usr/share/perl5/Shutter/App/Common.pm line 80.
	require Shutter/App/Common.pm called at /usr/bin/shutter line 206
Subroutine lookup redefined at /usr/share/perl5/Shutter/Draw/DrawingTool.pm line 28.
	require Shutter/Draw/DrawingTool.pm called at /usr/bin/shutter line 228
Variable "$self" will not stay shared at /usr/share/perl5/Shutter/Draw/DrawingTool.pm line 671.
	require Shutter/Draw/DrawingTool.pm called at /usr/bin/shutter line 228
Variable "$self" will not stay shared at /usr/share/perl5/Shutter/Screenshot/SelectorAdvanced.pm line 840.
	require Shutter/Screenshot/SelectorAdvanced.pm called at /usr/bin/shutter line 233
Failed to register: GDBus.Error:org.freedesktop.DBus.Error.NoReply: Message recipient disconnected from message bus without replying

I wrote a CGI script in Perl that handles a specific application completely and triggers a redirect to an error page for some errors.

The error page when being sent also sets the HTTP status code as requested.

It all works fine with the exception that Apache still appends it's own error page at the end, breaking XHTML.

How can I fix it (preferably inside the CGI script)?

Examples:

The resulting response as seen in Firefox:

<!DOCTYPE html
    PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="de-DE" xml:lang="de-DE">
<head>
<title>Parameterfehler</title>
<link href="/favicon.ico" rel="icon" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>

<div class="grid-1"><h1 class="g1-head error">Parameterfehler</h1>
<span class="g1-main"><p class="error">Fehlender Suchbegriff</p> <p class="error">Fehlende Datenquelle</p>
</span></div>
<span class="g1-foot"><div class="nav" role="navigation"><a href="https://.../" rel="external">...</a></div></span>

</body>
</html>
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>406 Unknown Reason</title>
</head><body>
<h1>Unknown Reason</h1>
<p>An appropriate representation of the requested resource could not be found on this server.</p>
<hr>
<address>Apache Server at www-tel Port 80</address>
</body></html>

Code to send the error page sets the HTTP status, charset and content type in the header, and then sends a HTML document constructed from some parameters passed to the error URL.

An error URL may look like view-source:http://www-tel/test/api-v1?et=Parameterfehler&um=3&em=Fehlender+Suchbegriff&em=Fehlende+Datenquelle&fn=70&es=406+ where esis the error status (code and optional message) and fn=70says to output an error page.

I won't present the actual coe to build the errorp age here, because I use a lot of tools and constants added to the script that would not be appropriate for a minimal example.

Basically I'm using CGI's functions:

  1. header() with attributes -status, -charset and -type
  2. start_html() with attributes -head, -lang, -meta and -title
  3. Add content using divs, spans, ps, and some navigation links
  4. end_html() to finalize the output stream

Ideas

One idea was to use a 200 success status for the error pages, but asking AI about it, it said: Drawbacks of Using a Success Status Code

Misleading Information: Serving an error page with a 200 OK status can mislead users into thinking that the page loaded correctly, even though the content may indicate an error.

Search Engine Impact: Search engines might not handle a page correctly if it is treated as successful even though it isn’t, affecting your site's SEO.

Bug fix release 2.08 for the Perl Distribution Workflow

dev.to #perl

We got an issue reported by long term Perl user and PAUSE gatekeeper @ANDK, an issue had been observed and was reported via the smoke testing framework available as CPAN-testers.

ANDK had debugged the issue, which is much appreciated, I was unable to reproduce it locally, but @ehuelsmann was fast to provide a patch that we believe fixes the issue.

This kind of bugs are tricky, especially if you cannot reproduce them, but the area of the code pointed out by ANDK made sense, and Erik's patch also made sense - it is a fragile area of the code and it might be related to tests, but we want our tests and main code to be as robust as possible, so we can trust it.

I am not sure this is a real heisenbug, but it does feel like it.

Change log from the lastest release:

2.08 2025-11-12 bug fix release, update not required

Fixed

Perl library to create static HTML

Perl questions on StackOverflow

Long story short I have written my own custom HTML template system that I am very happy with. However I have done that with manually concatenating HTML tags, and it is a bit messy. There has to be a perl library that will help you build a static HTML document and then output the result. But when I google for a solution I get plenty of options about parsing HTML documents, using templates to generate HTML, or frameworks to generate HTML pages dynamically, but I can't seem to find a perl library that you can generate an HTML document with. Does anybody have any suggestions?

Speakers Wanted! Dec. 17-18 PCC Winter 2025

blogs.perl.org

We have 7 high quality and exciting talks, we're looking for many more - as many as we can back into 2 days. Virtual presentations are accepted.

event-flyer.jpg




Simple PERL Bonus Guide — November 2025

Perl on Medium

A complete beginner-friendly roadmap to access bonuses and benefits from PERL.

Adding tests to legacy Perl code

Perl Maven

Notes from the live-coding session (part of the Perl Maven live events.

Meeting summary

Quick recap

The meeting began with informal introductions and discussions about Perl programming, including experiences with maintaining Perl codebases and the challenges of the language's syntax. The main technical focus was on testing and code coverage, with detailed demonstrations of using Devel::Cover and various testing modules in Perl, including examples of testing SVG functionality and handling exceptions. The session concluded with discussions about testing practices, code coverage implementation, and the benefits of automated testing, while also touching on practical aspects of Perl's object-oriented programming and error handling features.

SVG Test Coverage Analysis

Gabor demonstrated how to use Devel::Cover to generate test coverage reports for the SVG.pm module. He showed that the main module has 98% coverage, while some submodules have lower coverage. Gabor explained how to interpret the coverage reports, including statement, branch, and condition coverage. He also discussed the importance of identifying and removing unused code that appears uncovered by tests. Gabor then walked through some example tests in the SVG distribution, explaining how they verify different aspects of the SVG module's functionality.

Original announcement

Adding tests to legacy Perl code

During this live coding event we'll take a Perl module from CPAN and add some tests to it.

Further events

Register on the Perl Maven Luma calendar.

Testing in Perl

Perl on Medium

One of the most important thing in my work regarding software development is testing. That’s why whenever I picked up a new programming…

This week in PSC (206) | 2025-11-03

blogs.perl.org

Just Paul and Aristotle again.

We discussed the unlimited statement modifier chaining proposal. Both of us thought that unlimited chaining is a bad idea, though we agreed that being able to combine one loop and one conditional would on rare occasions make things slightly nicer. However we also agreed that in terms of conceptual complexity, only either allowing arbitrary chaining or disallowing chaining entirely is justifiable, as opposed to specifying and implementing rules to permit it but only in certain specific cases, especially considering the marginal benefit. So we settled on agreeing with Larry’s original design decision to disallow them.

[P5P posting of this summary]

(English below)

Hallo zusammen,

wir laden Euch herzlich ein
zum Deutschen Perl/Raku Workshop 2026.

Der Workshop findet nächstes Jahr vom Montag 16. März bis
Mittwoch 18. März in der Heilandskirche in Berlin. statt.

Die Webseite und der Call for Papers sind bereits online. Wir freuen uns auf viele interessante
Vorträge!

Über Unterstützung durch Sponsoren freuen wir uns immer. Wenn Ihr bzw. Eure
Firma den Workshop unterstützen möchtet, wendet Euch gerne an uns. Wir finden gemeinsam sicher eine Möglichkeit!

Wenn Ihr Fragen an die Organisatoren habt, erreicht Ihr uns am besten
direkt unter orga2026@german-perl-workshop.de .

Wir freuen uns auf Eure Teilnahme,
Max Maischein für die Organisatoren und Frankfurt.pm

Wir arbeiten noch an
Hotelempfehlungen und veröffentlichen diese auf der Webseite.

---

Hello everybody,

we cordially invite you
to the German Perl/Raku Workshop 2026.

The workshop will take place next year from Monday 16th May to
Wednesday 18th March in the Heilandskirche in Berlin..

The website and the call for papers are already online.
We are looking forward to many interesting
presentations!

We are always happy to receive support from sponsors. If you or your
company would like to support the workshop, please contact us. We will
find a way together!

If you have any questions for the organisers, please contact us directly at orga2026@german-perl-workshop.de .

We look forward to your participation,
Max Maischein for the organisers and Frankfurt.pm

We are still working on hotel recommendations and will publish them on the website.

Issue #746–2025–11–10 — YAPC::Fukuoka 2025

Perl on Medium

Originally published at [Perl Weekly 746](https://perlweekly.com/archive/746.html)

Perl 🐪 Weekly #746 - YAPC::Fukuoka 2025 🇯🇵

dev.to #perl

Originally published at Perl Weekly 746

Hi there,

YAPC::Fukuoka 2025 is happening on 14-15 Nov 2025.

I'm also excited about LPW 2025. From our latest update from the organisers, the event is moving forward. They're making every effort to ensure it happens, which is no small feat given the short notice. What's particularly remarkable is that they're managing this with just a two-person team.

They truly need our support, so please do reach out if you can help. The organisers are currently seeking sponsors, volunteers and speakers. I've tentatively submitted a talk proposal myself.

Hopefully, I'll see you all at the event. The venue is expected to be confirmed this week. For the latest updates, please check the website.

Enjoy rest of the newsletter.

--
Your editor: Mohammad Sajid Anwar.

Announcements

YAPC::Fukuoka 2025

It's 2 days event, 14-15 Nov 2025. YAPC::Fukuoka 2025 will be the first time the event is held in Kyushu since YAPC::Fukuoka 2017 HAKATA.

The corner of Gabor

A couple of entries sneaked in by Gabor.

Adding tests to legacy Perl code

Online live-coding event.

Articles

Implementing Bitcoin in a dead language

An exceptional piece of technical writing that balances satire, technical depth and human vulnerability while showcasing significant software engineering accomplishment. It now implements BIP340 (Schnorr signatures), BIP341 (spending rules) and BIP342 (script validation).

A polymorphic quine

An exceptional deep dive into esoteric programming concepts that demonstrates profound computer science knowledge and creative problem-solving.

AWS S3 Bucket Events

Back to my favourite topic: AWS. In this post, I am showing how to tackle S3 bucket events using AWS Lambda.

CPAN

App::Test::Generator

This is a major improvement of the fuzz testing framework, a building tool that automatically generates comprehensive test suites from formal specifications.

Grants

PEVANS Core Perl 5: Grant Report for October 2025

Maintaining Perl (Tony Cook) October 2025

The Weekly Challenge

The Weekly Challenge by Mohammad Sajid Anwar will help you step out of your comfort-zone. You can even win prize money of $50 by participating in the weekly challenge. We pick one champion at the end of the month from among all of the contributors during the month, thanks to the sponsor Lance Wicks.

The Weekly Challenge - 347

Welcome to a new week with a couple of fun tasks "Format Date" and "Format Phone Number". If you are new to the weekly challenge then why not join us and have fun every week. For more information, please read the FAQ.

RECAP - The Weekly Challenge - 346

Enjoy a quick recap of last week's contributions by Team PWC dealing with the "Longest Parenthesis" and "Magic Expression" tasks in Perl and Raku. You will find plenty of solutions to keep you busy.

TWC346

For The Weekly Challenge, this is a strong, organised solutions post. Working code for both challenges is provided by Ali, who exhibits a straightforward and useful approach to problem-solving. The answers are clear and accurate.

Parenthesised Magic

This blog post is technically sound, informative and well-written. It successfully illustrates a typical Raku problem-solving technique: beginning with a simple, imperative approach and then refining it into a more idiomatic, succinct and potent functional solution.

Perl Weekly Challenge: Week 346

Jaldhar doesn't just provide solutions but explores multiple approaches for each problem, analysing trade-offs between readability, performance and idiomatic style. The post is rich with technical insights, performance benchmarks and clever algorithmic thinking, making it both educational and intellectually stimulating.

Magic Parentheses

This is an excellent, technically complex post that demonstrates sophisticated Perl programming methods. Jorg exhibits a thorough understanding of mathematical optimization, regex engines and effective algorithm design.

really not inspired!

Welcome back Luca after the break, nice hack about tracking nesting levels in Raku.

Perl Weekly Challenge 346

This is an excellent technical blog post that demonstrates deep algorithmic creativity and strong problem-solving skills. It's a genuinely novel approach that demonstrates deep understanding of the problem domain rather than just implementing standard algorithms.

Recursive Parentheses - But no Recursive Magic

This is a solid and insightful write-up by Matthias - concise, technically sound and pedagogically clear.

Whoa-oh-oh! Sing about parens!

This is a superb technical blog post that exhibits profound pedagogical understanding and remarkable programming proficiency in several languages.

(Magic)

This post blends practical implementation knowledge with sophisticated algorithms. Peter demonstrates a solid foundation in computer science and carefully weighs the pros and cons of various strategies.

All Aboard The Magic Parenthesis

This is a solid, practical technical blog post that demonstrates good problem-solving skills across multiple languages.

Longest Expression

Both tasks are implemented correctly, pedagogically and safely. The code favors clarity and correctness over performance, which makes sense for a challenge-focused context.

Rakudo

2025.44 Jimmy Wales on Audrey Tang

This is a high-quality community newsletter that successfully serves both technical and social aspects of the Raku ecosystem. It demonstrates an active, thoughtful community with meaningful technical content and important community developments.

Weekly collections

NICEPERL's lists

Great CPAN modules released last week;
MetaCPAN weekly report.

Events

Perl Maven online: Adding tests to legacy Perl code

November 11, 2025

Paris.pm monthly meeting

November 12, 2025

YAPC::Fukuoka 2025

November 14-15, 2025

London Perl and Raku Workshop

November 29, 2025

Toronto.pm - online - How SUSE is using Perl

December 6, 2025

Paris.pm monthly meeting

December 10, 2025

You joined the Perl Weekly to get weekly e-mails about the Perl programming language and related topics.

Want to see more? See the archives of all the issues.

Not yet subscribed to the newsletter? Join us free of charge!

(C) Copyright Gabor Szabo
The articles are copyright the respective authors.


Tony writes:

``` [Hours] [Activity] 2025/10/02 Thursday 1.03 #23782 testing, comments 0.23 #23794 review change, research and comment 0.32 #23787 review and approve 0.27 #23777 review, research and comment 0.17 #23775 review and comment

0.48 #23608 research and comment

2.50

2025/10/03 Friday 1.30 #21877 code review, find another possible bug 0.08 #23787 review updates, has other approval, apply to blead 0.68 #21877 bug report rcatline - #23798 0.08 #23794 review updates and approve 0.08 #16865 follow-up

0.90 #23704 research and comment

3.12

2025/10/06 Monday 0.27 #23728 review and comment 0.60 #23752 review, testing and comment 0.15 #23813 review, but nothing further to say 0.18 #23809 comment 0.18 #21877 write more tests 0.07 #23817 review, got merged as I looked at it 0.22 #23795 start review

1.95 #23795 more review

3.62

2025/10/07 Tuesday 0.60 #23774 review 0.68 #23796 review and approve 0.37 #23797 review 0.08 #23797 finish review and approve 0.25 #23799 review and comment 0.12 #23800 review and approve 0.10 #23801 review and comment 0.17 #23802 comment 0.08 #23752 review and approve

0.60 #23795 more review

3.05

2025/10/08 Wednesday 0.55 #23795 more review 0.72 #23795 more review 0.12 #23782 marked resolved comments resolved

0.08 #23801 review updates and approve

1.47

2025/10/09 Thursday 0.45 #23799 comment 0.08 #23821 review and approve 0.08 #23824 review and approve 0.12 #23827 review, research and approve 0.10 #23820 review, research and comment 0.30 #23812 review, existing comment matches my opinion 0.10 #23805 briefly comment 2.30 #21877 add #23798 tests, testing, more work on re- implementation 1.67 #21877 work on re-implementation

0.55 #21877 more work

5.75

2025/10/10 Friday

0.23 #23828 review discussion and failing message, comment

0.23

2025/10/13 Monday 0.23 #23829 review discussion and comment 0.22 #23833 comment 0.20 #23834 review and approve with comment 0.42 #23837 review and approve 0.30 #23838 review and comment 0.42 #23840 review and comment 0.08 #23843 review and approve 0.15 #23842 review and comment 0.23 #23836 review test failures and comment 0.17 #23841 review discussion, research and comment 0.52 #23676 search for other possible modules, review and comment 0.13 #23833 review and comment 0.47 #21877 more tests, debugging 0.32 #23802 research, comment

1.18 #21877 debugging

5.04

2025/10/14 Tuesday 1.20 #23844 review, comments 0.15 #23845 review and approve 0.33 #21877 debugging 0.72 #21877 debugging, testing

0.67 #21877 debugging

3.07

2025/10/15 Wednesday

0.23 check coverity scan reports

0.23

2025/10/16 Thursday 0.55 #23847 review, #p5p discussion re 5.42.1, approve 0.53 #23851 review, research and comment, more maint-5.42 discussion 0.98 #23850 review, comments 0.53 #23852 research and comment

0.37 maint votes: vote and look for anything else

2.96

2025/10/20 Monday 0.35 #23840 review updates and approve 0.78 #23838 review updates, review build logs, comments 0.33 #23833 investigate build errors and warnings, restart mingw64 CI 0.08 #23818 review updates and approve 0.28 #23851 research and comments

1.97 #23795 more review

3.79

2025/10/21 Tuesday 0.12 #23838 check updates, restart failed CI job 0.38 #23853 review, research and comment 0.62 #23865 review, coverage testing and approve 1.13 #23858 review, testing, comments 0.08 #23838 check CI results and approve 1.07 #23795 more review and let dave know I’m done for now 1.08 #23852 work on re-working docs, research equivalence of

sigprocmask and pthread_sigmask, comment

4.48

2025/10/22 Wednesday 0.27 #23858 review updates and conditionally approve 0.27 #23868 review and approve 0.47 update t/test_pl.pod with the new PREAMBLE directive PR 23869 1.43 #23782 try to understand the code, minor change and testing

0.42 #23782 more testing, debugging

2.86

2025/10/23 Thursday

2.78 #23871 review, testing, comments

2.78

2025/10/27 Monday 2.80 #23871 review updates, comment, testing

0.43 #23795 comments

3.23

2025/10/28 Tuesday 0.40 #23879 review changes and research, comment on referred ticket 0.10 #23781 comment 0.08 #23809 briefly comment 0.67 #23867 review 0.45 #23867 comments

1.30 #23872 review

3.00

2025/10/29 Wednesday 1.10 #23782 testing and follow-up

0.53 #23781 re-check

1.63

2025/10/30 Thursday 0.35 #23882 review and comment 1.70 #23873 review, testing and approve 0.33 #23614 comment

1.55 #21877 debugging - fix one issue

3.93

Which I calculate is 56.74 hours.

Approximately 59 tickets were reviewed or worked on, and 1 patches were applied. ```

I'm running into a weird import error message while trying to create a commandline tool that references packages within a Perl web application. The web application itself is working fine. I can reproduce the issue with an MVE, but I'm mostly interested in understanding what is going on because it makes no sense to me. It's easy to "fix" this MVE by playing around with the imports but this isn't working with the real program, and I'd also prefer to avoid changing the application packages (i.e. the real AllZip.pm).

This is perl 5, version 40, subversion 1 (v5.40.1), running on Windows 11. This was locally compiled with the modules built using cpanm.

AllZip.pm

package AllZip;

use strict;
use warnings;

use IO::Uncompress::Unzip qw(unzip $UnzipError);
use Archive::Zip::SimpleZip qw($SimpleZipError);

1;

test.pl:

package test;

use strict;
use warnings;
use Carp;

use AllZip;     #Line 7

use IO::Compress::Gzip qw(:constants gzip $GzipError);      #Line 9
use IO::Compress::Zip 2.213 qw(:all);       #Line 10 (taken from Archive::Zip::SimpleZip)

print (($GzipError // '') . " is imported.\n");
print (($ZipError // '') . " is imported.\n");

Running this as-is gives the error:

"$GzipError" is not exported by the IO::Compress::Zip module
"gzip" is not exported by the IO::Compress::Zip module
Can't continue after import errors at test.pl line 10.
BEGIN failed--compilation aborted at test.pl line 10.

Which is strange, because I'm not asking for gzip or $GzipError from IO::Compress::Zip.

The print statements are just to verify that the exports are working. If I comment out line 9 I get a syntax error stating that $GzipError isn't declared, which is expected.

The error goes away if I remove line 7. Changing the order of the Zip/GZip use statements results in multiple prototype mismatch errors but otherwise runs to completion (the real program does not).

Does the prior import of IO::Compress::Gzip somehow clobber the :all keyword when IO::Compress::Zip is re-imported?


@ikegami can replicate on Linux, and whittled it down to the following:

$ perl -e'
   use IO::Compress::Zip  qw();
   use IO::Compress::Gzip qw();
   use IO::Compress::Zip  qw( :all );  # Line 4
'
"gzip" is not exported by the IO::Compress::Zip module
"$GzipError" is not exported by the IO::Compress::Zip module
Can't continue after import errors at -e line 4.
BEGIN failed--compilation aborted at -e line 4.

(dlxxiii) 9 great CPAN modules released last week

Niceperl
Updates for great CPAN modules released last week. A module is considered great if its favorites count is greater or equal than 12.

  1. App::cpm - a fast CPAN module installer
    • Version: 0.998000 on 2025-11-07, with 176 votes
    • Previous CPAN version: 0.997024 was 3 months, 27 days before
    • Author: SKAJI
  2. App::Netdisco - An open source web-based network management tool.
    • Version: 2.094003 on 2025-11-03, with 777 votes
    • Previous CPAN version: 2.094002 was 5 days before
    • Author: OLIVER
  3. CPANSA::DB - the CPAN Security Advisory data as a Perl data structure, mostly for CPAN::Audit
    • Version: 20251102.001 on 2025-11-02, with 25 votes
    • Previous CPAN version: 20251026.001 was 7 days before
    • Author: BRIANDFOY
  4. Dist::Zilla - distribution builder; installer not included!
    • Version: 6.034 on 2025-11-07, with 189 votes
    • Previous CPAN version: 6.033 was 6 months, 5 days before
    • Author: RJBS
  5. PerlPowerTools - BSD utilities written in pure Perl
    • Version: 1.053 on 2025-11-04, with 223 votes
    • Previous CPAN version: 1.052 was 3 months, 17 days before
    • Author: BRIANDFOY
  6. Sys::Virt - libvirt Perl API
    • Version: v11.8.0 on 2025-11-07, with 17 votes
    • Previous CPAN version: v11.6.0 was 3 months, 3 days before
    • Author: DANBERR
  7. Test::Fatal - incredibly simple helpers for testing code with exceptions
    • Version: 0.018 on 2025-11-06, with 40 votes
    • Previous CPAN version: 0.017 was 2 years, 10 months, 5 days before
    • Author: RJBS
  8. Time::Piece - Object Oriented time objects
    • Version: 1.40 on 2025-11-08, with 64 votes
    • Previous CPAN version: 1.39 was 14 days before
    • Author: ESAYM
  9. Workflow - Simple, flexible system to implement workflows
    • Version: 2.07 on 2025-11-08, with 34 votes
    • Previous CPAN version: 2.06 was 2 months, 26 days before
    • Author: JONASBN

(dcxviii) metacpan weekly report - DBD::DuckDB

Niceperl

This is the weekly favourites list of CPAN distributions. Votes count: 70

Week's winners (+5):  DBD::DuckDB 

Build date: 2025/11/08 17:43:57 GMT


Clicked for first time:


Increasing its reputation:


Paul writes:

The main event from this month has been getting the signature named parameters (PPC0024) branch finalised and merged.

  • 4 = Improvements to unit tests around subroutine signatures
    • https://github.com/Perl/perl5/pull/23822
    • https://github.com/Perl/perl5/pull/23868
  • 11 = Signature Named Parameters branch
    • https://github.com/Perl/perl5/pull/23871
  • 2 = Investigations into possible performance enhancements of pp_multiparam
    • https://www.nntp.perl.org/group/perl.perl5.porters/2025/10/msg270428.html
  • 1 = Improvements to named parameter error reporting
    • https://github.com/Perl/perl5/pull/23888
  • 2 = Other github code reviews

Total: 20 hours

My aim for November is to let the dust settle a bit on signature parameters, and turn my attention to either Magic v2 or Attributes v2, aiming to have something that can interact better with signatures in time for the 5.44 release.

(dlxxii) 15 great CPAN modules released last week

Niceperl
Updates for great CPAN modules released last week. A module is considered great if its favorites count is greater or equal than 12.

  1. App::Music::ChordPro - A lyrics and chords formatting program
    • Version: v6.090.0 on 2025-10-31, with 409 votes
    • Previous CPAN version: v6.080.1 was 2 months, 2 days before
    • Author: JV
  2. App::Netdisco - An open source web-based network management tool.
    • Version: 2.094002 on 2025-10-29, with 773 votes
    • Previous CPAN version: 2.094001 was before
    • Author: OLIVER
  3. App::perlimports - Make implicit imports explicit
    • Version: 0.000058 on 2025-10-28, with 20 votes
    • Previous CPAN version: 0.000057 was 5 months, 28 days before
    • Author: OALDERS
  4. CPANSA::DB - the CPAN Security Advisory data as a Perl data structure, mostly for CPAN::Audit
    • Version: 20251026.001 on 2025-10-26, with 25 votes
    • Previous CPAN version: 20251020.002 was 5 days before
    • Author: BRIANDFOY
  5. DBIx::DataModel - UML-based Object-Relational Mapping (ORM) framework
    • Version: 3.14 on 2025-10-28, with 13 votes
    • Previous CPAN version: 3.13 was 2 days before
    • Author: DAMI
  6. HTTP::BrowserDetect - Determine Web browser, version, and platform from an HTTP user agent string
    • Version: 3.45 on 2025-11-01, with 58 votes
    • Previous CPAN version: 3.44 
    • Author: OALDERS
  7. JSON::Schema::Modern - Validate data against a schema using a JSON Schema
    • Version: 0.621 on 2025-10-30, with 12 votes
    • Previous CPAN version: 0.620 was 14 days before
    • Author: ETHER
  8. Mojo::mysql - Mojolicious and Async MySQL/MariaDB
    • Version: 1.28 on 2025-10-30, with 36 votes
    • Previous CPAN version: 1.27 was 2 years, 3 days before
    • Author: JHTHORSEN
  9. Mouse - Moose minus the antlers
    • Version: v2.6.0 on 2025-10-30, with 63 votes
    • Previous CPAN version: v2.5.11 was 1 year, 2 months, 25 days before
    • Author: SYOHEX
  10. OrePAN2 - Yet another DarkPAN manager.
    • Version: 0.53 on 2025-10-31, with 22 votes
    • Previous CPAN version: 0.52 was 1 year, 8 months, 11 days before
    • Author: OALDERS
  11. Perlanet - command line interface to Perlanet.pm
    • Version: v3.3.4 on 2025-10-29, with 25 votes
    • Previous CPAN version: v3.3.3 was 5 months, 29 days before
    • Author: DAVECROSS
  12. PPI - Parse, Analyze and Manipulate Perl (without perl)
    • Version: 1.284 on 2025-10-28, with 64 votes
    • Previous CPAN version: 1.283 was 5 months, 24 days before
    • Author: OALDERS
  13. Time::Piece - Object Oriented time objects
    • Version: 1.39 on 2025-10-25, with 64 votes
    • Previous CPAN version: 1.38 was 6 days before
    • Author: ESAYM
  14. WebService::Fastly - an interface to most facets of the [Fastly API](https://www.fastly.com/documentation/reference/api/).
    • Version: 13.00 on 2025-10-31, with 18 votes
    • Previous CPAN version: 12.00 was 2 months, 2 days before
    • Author: FASTLY
  15. WWW::Mechanize::Chrome - automate the Chrome browser
    • Version: 0.75 on 2025-10-31, with 22 votes
    • Previous CPAN version: 0.74 was 25 days before
    • Author: CORION