CI
NAME
App::Ariza::CI - the GitHub Actions workflows that build, smoke and publish an app's bundles
SYNOPSIS
use App::Ariza::CI;
use App::Ariza::Config;
my $cfg = App::Ariza::Config.load('/path/to/App-Moneymoor'.IO);
my @done = App::Ariza::CI.write(
:out-dir('/path/to/App-Moneymoor/.github/workflows'.IO),
:config($cfg),
);
say @done.map({ "{.<action>} {.<output>}" });
# (skipped test.yml wrote release.yml)
# Just the text, for a diff or a test:
say App::Ariza::CI.render(
:template<release.yml.j2>,
|App::Ariza::CI.context(:config($cfg)),
);
# What this app's release workflow will contain:
say App::Ariza::CI.lanes-for($cfg).map(*.<job>);
# (bundle-macos-arm64 bundle-linux-x86_64-glibc bundle-windows-x86_64)
DESCRIPTION
App::Ariza::Bundle builds one bundle on one machine. A release needs one per platform, each built on that platform, each proved to run, and all of them published together ā which is a CI problem, and this is the CI.
ariza scaffold-ci writes two files into an app's
.github/workflows/:
test.yml the ordinary Raku test workflow, written once
release.yml build every declared platform, smoke each, publish on a tag
Neither is ariza's to run. They are committed to the app's repository
and run by GitHub, exactly as install.sh is committed and run by a
user ā ariza's job is to make sure the file says the right thing about
this app.
What release.yml does
One build job per slug in the app's bundle.platforms, in parallel:
bundle-macos-arm64āmacos-latest, Rakudo fromRaku/setup-raku, SQLCipher frombrew install sqlcipher.bundle-linux-x86_64-glibcāubuntu-latestinside thequay.io/pypa/manylinux_2_28_x86_64container, so the archive's glibc floor is 2.28 rather than whatever the runner image ships this month. Rakudo comes from the rakudo.org release index by hand (setup-rakuinstalls into the runner's tool cache, which is not in the container's filesystem) and SQLCipher is built from source at the pinned version.bundle-windows-x86_64āwindows-latest,setup-raku, and SQLCipher from MSYS2'smingw-w64-ucrt-x86_64-sqlcipher, installed with thepacmanevery windows runner already has, withSQLCIPHER_LIB_DIRpointed at the result ā which is the sourcing contract App::Ariza::Native documents for Windows. The UCRT package rather than vcpkg's port, because an MSVC-built SQLCipher importsvcruntime140.dll, which is not part of Windows and which the PE audit therefore refuses to ship.
Each of them then runs the same three steps ā ariza bundle,
ariza smoke, upload the archive and its .sha256 ā because the
difference between platforms is entirely in what has to be installed
before ariza can start.
Then, on a tag only:
publishā collects every lane's artefact, flattens them, recomputes a combinedchecksums.txt, checks that against the sidecars ariza wrote, and creates the GitHub release with a body that says what a bundle is, which machines each archive runs on, and how to verify a download.smoke-installer-macos-arm64,smoke-installer-linux-x86_64-glibc,smoke-installer-windows-x86_64ā one per declared platform with a clean-machine smoke recipe (%LANES'ssmokekey), each on a plain runner with nothing installed on it: it downloads the archive that was just published, installs it with the repository's own committedinstall.shorinstall.ps1, and runs the installed launcher under a stripped environment (env -ion POSIX; a from-scratchSystem.Diagnostics.Processon Windows, which has noenv -i). These are the only jobs that test the artefact a user will actually receive, through the path they will actually take ā macOS's incidentally being the only placeinstall.sh's BSD branches (shasum -a 256, bsdtar) ever run in CI. Scaffolded only for an app with aninstaller.repoto publish to; a declared platform with a build lane but no smoke recipe yet renders no job for it, and a comment in the workflow says so instead of leaving the gap silent.
Dispatch before you tag
The workflow triggers on workflow_dispatch as well as on
push: tags: ['v*'], and the dispatch run stops after the build lanes:
publish and every smoke-installer-* job are gated on
startsWith(github.ref, 'refs/tags/').
That is the iteration loop, and it is the reason the dispatch trigger
exists. A recipe that is wrong ā a package that has been renamed, a
runner image that has moved on ā costs a run and a push to a branch,
rather than a burnt tag and a deleted release. The dispatch input
ref takes a branch, so the lane being fixed does not have to be on
the default branch to be tried.
Which Raku is which
Every lane installs a Raku to run ariza with. It is not the runtime
that ends up in the bundle: ariza downloads the pinned Rakudo
([rakudo] in versions.toml) for itself, verifies it, and unpacks
that into the archive. The lane's own Rakudo can be any version at all,
which is why setup-raku is pinned to latest rather than to
anything.
The manylinux lane is the exception in mechanism rather than in
principle: setup-raku cannot install into a container, so the lane
resolves the pin against the same rakudo.org JSON index
App::Ariza::Rakudo reads and unpacks the archive itself. There is no
URL to construct ā upstream filenames carry a toolchain suffix ā so the
index is the only honest route.
What is regenerated, and what is yours
release.yml is derived from bundle.platforms: add a platform to
ariza.toml, re-run ariza scaffold-ci, and the file gains a lane and
a needs: entry. It is rewritten in place every time, so hand edits to
it are edits you will make twice.
test.yml is written only when it is absent. A test workflow grows
system dependencies, extra jobs and skip conditions that no generator
can infer from a manifest ā this one is a starting point in the house
shape, and the moment it is committed it belongs to the repository.
:force overwrites it anyway, for when the house shape has moved on
and the local edits are known to be nothing.
The generated header of each file says which of the two it is, so nobody has to remember.
Where ariza comes from
By default the lanes run
zef install --/test 'App::Ariza:ver<0.2.2+>:auth<zef:apogee>',
with the repository URL beside it as a comment. ci.ariza-source in
the app's ariza.toml swaps them:
[ci]
ariza-source = "https://github.com/m-doughty/App-Ariza.git"
That is the bootstrap case ā an app whose release workflow has to exist before ariza is published ā and the case where a change to ariza is being tested against a real release before it is cut. Whichever is not in use is rendered as a comment on the next line, along with the version of ariza that scaffolded the file, so a reader can tell what produced it and swap without looking anything up.
METHODS
write(:$out-dir!, :$config!, :$versions, :$ariza-version, :$force --> List)
Render the workflows and return one
{ path, output, action } per file, action being 'wrote' or
'skipped'.
The output directory is created if it does not exist ā unlike
App::Ariza::Installer's, which refuses. .github/workflows has one
spelling and, in a repository with no CI, is by definition not there
yet, so a missing one is the expected case rather than a typo.
render(:$template!, *%ctx --> Str)
One template, as text, always with LF line endings. Exposed separately because that is what a golden-file test compares.
context(:$config!, :$versions, :$ariza-version --> Hash)
The render context: the app's names, the pins the workflows quote, the
declared lanes and their job ids, the platform floors for the release
body, and lane_jobs ā release.yml's whole jobs: section, rendered
from one template per lane and pasted in. A macOS lane and a manylinux
one share their last three steps and nothing else, so they are separate
files rather than a template full of conditionals.
Dies when the app declares no platforms, when versions.toml has no
[rakudo] pin (or a revision that is not a number, which the
manylinux lane compares numerically against the release index), or when
a linux-x86_64-glibc lane is wanted and there is no sqlcipher pin
for it to build.
lanes-for(App::Ariza::Config $config --> List) / lane-for(Str $slug --> Hash) / lane-slugs(--> List)
The lanes an app gets in manifest order, one slug's recipe, and every slug ariza can scaffold a lane for.
An undeclarable slug is a die rather than a skip, for the same reason
an unknown platform in bundle.platforms is: silently dropping a
platform the author asked for produces a release quietly missing it.
The set is deliberately the same shape as App::Ariza::Rakudo's ā a
platform with no official Rakudo build has no bundle to publish, so a
lane for it would be a guess.
workflows(--> List)
The { template, output, overwrite } entries, in write order.
install-lines(App::Ariza::Config $config, :$ariza-version! --> List)
(command, @note-lines): the zef install a lane runs, and the
comment rendered beside it ā which names the alternative source and the
ariza version that scaffolded the file, wrapped, because a workflow is
read as text.
ariza-version(--> Str)
ariza's own version for the provenance line, or 'dev' from a
checkout.
SEE ALSO
App::Ariza::Bundle and App::Ariza::Smoke, which are what every
build lane runs; App::Ariza::Installer, whose install.sh the
installer smoke job drives; App::Ariza::Native, whose Windows
sourcing contract the pacman step satisfies; App::Ariza::Rakudo, whose
release-index lookup the manylinux lane repeats in shell.
AUTHOR
Matt Doughty
COPYRIGHT AND LICENSE
Copyright 2026 Matt Doughty
This library is free software; you can redistribute it and/or modify it under the Artistic License 2.0.