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.
| 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! [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 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.
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:
- Removing all spaces and dashes
- Grouping digits into blocks of length 3 from left to right
- 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
- 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 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...
-
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
-
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
-
App::Rakubrew - Raku environment manager
- Version: 45 on 2025-11-13, with 28 votes
- Previous CPAN version: 44 was 2 days before
- Author: PATRICKB
-
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
-
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
-
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
-
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
-
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
-
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
-
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
-
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
-
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
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:
- App::jsonvalidate - App harness for the jsonvalidate CLI
- Bitcoin::Secp256k1 - Perl interface to libsecp256k1
- minion::task - A task boilerplate for Minion
- Pod::Abstract - Abstract document tree for Perl POD documents
Increasing its reputation:
- BioX::Seq (+1=3)
- Bitcoin::Crypto (+1=8)
- CGI::Tiny (+1=9)
- Dancer2 (+1=139)
- DBD::DuckDB (+1=6)
- Devel::MAT (+1=30)
- File::Slurp (+1=78)
- Git::Repository (+1=27)
- IO::Compress (+1=19)
- mojo::debugbar (+1=2)
- mojo::util::collection (+1=2)
- Mojolicious::Plugin::Debugbar (+1=2)
- Net::OpenSSH (+1=43)
- Perl::Critic (+1=134)
- Perl::Tidy (+1=146)
- Pod::Parser (+1=14)
- Readonly (+1=24)
- Scalar::List::Utils (+1=183)
- Set::Object (+1=13)
- Task::Kensho (+1=121)
- Time::Piece (+1=65)
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

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
| submitted by /u/AndrewMD5 [link] [comments] |
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?
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
1.41 2025-11-12
- strptime: parse micro seconds (RT165677, RT133599)
- add to_gmtime and to_localtime (RT113979)
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
makecan 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-scriptletstarget:perl -c -e '$(s_create_requires)'checks syntax without running it. - Some editors (Emacs
mmm-mode, Vimpolyglot) can treat marked sections as sub-languages to enable localized language specific editing features. - Use
includeto include a scriptlet into yourMakefile
…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:
-
Package it as a Docker container.
-
Build that container automatically in GitHub Actions.
-
Deploy it to Google Cloud Run, where it runs securely and scales automatically.
-
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— starts from an official Perl image on Docker Hub. -
Cartonkeeps dependencies consistent between environments. -
The app is copied into
/app, andcarton install --deploymentinstalls exactly what’s in yourcpanfile.snapshot. -
The container exposes port 8080 (Cloud Run’s default).
-
The
CMDruns Starman, serving your Dancer2 app.
To test it locally:
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):
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:
Once that’s done, you can deploy manually from the command line:
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:
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:
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
sshanywhere 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:
Package it as a Docker container.
Build that container automatically in GitHub Actions.
Deploy it to Google Cloud Run , where it runs securely and scales automatically.
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.Cartonkeeps dependencies consistent between environments.The app is copied into
/app, andcarton install --deploymentinstalls exactly what’s in yourcpanfile.snapshot.The container exposes port 8080 (Cloud Run’s default).
The
CMDruns 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
sshanywhere 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 bymake.- The Advantage: This is the only clean way to write readable,
indented code (like your
whileloop andifstatements) directly inside aMakefile.
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 theMakefile.
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 firstDARKPAN_TEMPLATE=passes the newly created shell variable’s value as an environment variable to theperlprocess. 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, forcingmaketo 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/exporttechnique works perfectly withpython -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
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:
header()with attributes-status,-charsetand-typestart_html()with attributes-head,-lang,-metaand-title- Add content using
divs,spans,ps, and some navigation links 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.
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
- Issue: #271 reported by @andk. Fixed via PR: #272 by @ehuelsmann This issue was discovered by the CPAN smoke testers, the report can be found on CPAN Testers
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?
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.
![]()
- **Sign up to attend**
- **Submit Talk** (virtual talks permitted!)
A complete beginner-friendly roadmap to access bonuses and benefits from PERL.
Notes from the live-coding session (part of the Perl Maven live events.
-
SVG the module for which we wrote tests.
-
Devl-Cover to generate test coverage report run
cover -test. -
done_testing()ordone_testing(2)
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.
One of the most important thing in my work regarding software development is testing. That’s why whenever I picked up a new programming…
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.
(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.
Originally published at [Perl Weekly 746](https://perlweekly.com/archive/746.html)
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.
-
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
-
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
-
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
-
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
-
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
-
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
-
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
-
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
-
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
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:
- App::Test::Generator - Generate fuzz and corpus-driven test harnesses
- Chart::ECharts - Apache ECharts wrapper for Perl
- Cwd::Guard - Temporary changing working directory (chdir)
- Eval::Context - Evalute perl code in context wrapper
- Image::WebP - binding to Google's libwebp.
- JSON::Schema::Validate - Lean, recursion-safe JSON Schema validator (Draft 2020-12)
- LaTeX::Replicase - Perl extension implementing a minimalistic engine for filling real TeX-LaTeX files that act as templates.
- OpenAPI::Linter - Lint and validate OpenAPI specification files
- Plack::Middleware::ConsoleLogger - Write logs to Firebug or Webkit Inspector
- Win32API::Process - Perl extension for handling the processes using the plain Win32 API
Increasing its reputation:
- abbreviation (+1=2)
- Alien (+1=5)
- App::cpanminus (+1=285)
- App::cpm (+1=77)
- App::perlimports (+1=21)
- Const::Fast (+1=36)
- CPAN::Meta (+1=27)
- Crypt::URandom (+1=7)
- Data::Dump (+1=42)
- Data::Password::zxcvbn (+1=8)
- DBD::DuckDB (+4=6)
- Devel::Cycle (+1=17)
- Devel::NYTProf (+1=195)
- Dist::Milla (+1=39)
- Dist::Zilla (+1=188)
- experimentals (+1=5)
- HTML::T5 (+1=2)
- HTTP::BrowserDetect (+1=26)
- HTTP::Message (+1=71)
- import (+1=3)
- Import::Into (+1=39)
- IO::Compress (+1=18)
- JQ::Lite (+3=7)
- Keyword::Declare (+1=24)
- Lexical::Persistence (+1=4)
- libwww::perl (+1=173)
- Mojo::PDF (+1=9)
- Mojo::Reactor::UV (+1=3)
- Mojolicious (+1=509)
- MooseX::Types::Set::Object (+1=2)
- namespace (+1=3)
- PAR::Packer (+1=45)
- Path::Iterator::Rule (+1=25)
- Path::Tiny (+1=193)
- Plack::Middleware::Debug (+1=20)
- Plack::Middleware::ETag (+1=4)
- Plack::Middleware::ReverseProxy (+1=9)
- Regexp::Common::net::CIDR (+1=2)
- Regexp::Debugger (+1=59)
- Reply (+1=61)
- Rex (+1=88)
- RT::Extension::CustomFieldsOnUpdate (+1=4)
- Safe (+1=13)
- strictures (+1=26)
- String::Random (+1=24)
- Syntax::Keyword::Match (+1=14)
- Syntax::Keyword::MultiSub (+1=5)
- Syntax::Keyword::Try (+1=46)
- Test2::Plugin::NoWarnings (+1=4)
- Test2::Plugin::SubtestFilter (+2=2)
- Test::Class (+1=24)
- Test::Simple (+1=199)
- Test::Simpler (+1=4)
- WWW::Mechanize (+1=103)

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.
-
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
-
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
-
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
-
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
-
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
-
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
-
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
-
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
-
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
-
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
-
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
-
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
-
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
-
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
-
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

