Releasing Stylo
Last updated: 2026-10-12 · Change: minor (RPM runtime capabilities and metadata checks, #1275)
Every push to the release branch runs
.github/workflows/release.yml. The workflow builds
installers for macOS, Windows, Linux (including Flatpak bundles and a
RISC-V tarball) and FreeBSD, plus the web build, and creates or updates
a draft GitHub Release named
Stylo v<version>. Nobody sees a draft until a
maintainer publishes it.
User-facing names say Stylo. Files, binaries and ids
stay lowercase
(stylo-<version>-<platform>-<arch>.<ext>,
com.ncdev.stylo).
Cutting a release
Bump the version on
main. It lives in exactly one place,[workspace.package] versionin the rootCargo.toml; every crate inherits it and the packaging scripts read it from there:cargo xtask version # prints the current version, e.g. 0.3.1 cargo xtask version set 0.4.0 # or 0.4.0-rc.1; updates Cargo.toml and Cargo.lockCommit the change (
Cargo.toml+Cargo.lock) through the normal review flow, as aRelease: Stylo v0.4.0commit whose message says what changed for users.Merge
mainintorelease(or fast-forward it) and push. The workflow starts by itself.Wait for the draft. When every job is done (macOS notarization is the slow part), the Releases page has a draft
Stylo v0.4.0, targeting the pushed commit, with every artifact andSHA256SUMS.txt. The notes are generated from the merged pull requests.Check it. Download an installer or two and read the job summaries. A
::warning::there means a signing secret was missing and that artifact is unsigned.Publish the draft in the GitHub UI. Publishing creates the
v0.4.0tag. A version with a pre-release suffix (-rc.1) is marked as a pre-release.
Pushing to release again before you publish rebuilds the
same draft and replaces its assets. Once the draft is published, the
workflow refuses to touch that version again: bump it first.
Test runs: Actions › Release › Run workflow
runs the whole pipeline by hand. The optional version input
(such as 0.4.0-rc.1) overrides Cargo.toml for
that run only; each build job applies it with
cargo xtask version set before building. The signing jobs
(macOS, Windows) and the draft-release job run in the
release environment, which only the release
branch can use, so pick that branch for a full run. Run on any other
branch, it is a dry run: the Linux, Flatpak, FreeBSD and web jobs build
and upload their artifacts, the environment refuses the signing jobs,
and no draft release is created.
What gets built
| Platform | Artifacts | Built on |
|---|---|---|
| macOS 11+ (universal: Apple silicon and Intel) | stylo-<v>-macos-universal.dmg,
stylo-cli-<v>-macos-universal.zip |
macos-15 |
| Windows x64 | stylo-<v>-windows-x64.msi,
stylo-<v>-windows-x64-portable.zip |
windows-latest |
| Windows x86 (32-bit) | stylo-<v>-windows-x86.msi,
stylo-<v>-windows-x86-portable.zip |
windows-latest |
| Windows on ARM64 | stylo-<v>-windows-arm64.msi,
stylo-<v>-windows-arm64-portable.zip |
windows-latest (cross-compiled) |
| Linux x86_64 | stylo-<v>-linux-x86_64.{AppImage,deb,rpm,tar.gz} |
ubuntu-22.04 |
| Linux aarch64 | stylo-<v>-linux-aarch64.{AppImage,deb,rpm,tar.gz} |
ubuntu-22.04-arm |
| Linux riscv64 | stylo-<v>-linux-riscv64.tar.gz (cross-compiled,
glibc >= 2.39; CLI smoke-tested under QEMU) |
ubuntu-24.04 |
| Linux AppImage updates | stylo-<v>-linux-{x86_64,aarch64}.AppImage.zsync |
with the AppImages |
| Flatpak x86_64, aarch64 | stylo-<v>-linux-{x86_64,aarch64}.flatpak |
ubuntu-24.04, ubuntu-24.04-arm (repackages
the Linux tarball) |
| FreeBSD 14 x86_64 | stylo-<v>-freebsd-x86_64.tar.gz |
a FreeBSD 14.3 VM on ubuntu-latest |
| Web | stylo-web-<v>.zip, a static site (see packaging/web/README.md) |
ubuntu-latest |
stylo --version and stylo-cli --version
print the version from Cargo.toml.
Fonts. Every build job checks out craft-fonts at a
pinned commit (the ref: of its craft-fonts checkout step)
and builds with CRAFT_FONTS_DIR and
CRAFT_FONTS_REQUIRED=1, so releases embed its Japanese
fonts and fail rather than ship without them (the web build embeds only
the UI font, BIZ UDPGothic Regular, to stay small). The packages carry
each embedded font's licence as OFL-<family>.txt
(copy_font_licences in packaging/env.sh). Bump
the pins deliberately, in every job at once. The rules are in craftrules
standards/fonts.md, the build option in docs/development.md › Fonts.
macOS
packaging/macos/package.sh builds
aarch64-apple-darwin and x86_64-apple-darwin
with MACOSX_DEPLOYMENT_TARGET=11.0, joins them with
lipo and assembles Stylo.app:
Info.plistis generated fromInfo.plist.in(bundle idcom.ncdev.stylo,LSMinimumSystemVersion11.0, the version and the build commit). The icon isassets/app-icon/stylo.icns.cargo xtask bundlefills in the same template for its development app.- Document types:
Info.plist.indeclares every format File › Open reads, so Finder opens them with Stylo (double-click, Open With, a drop on the Dock icon). Stylo owns its own formats and is an alternate for the others. A test keeps the list in step with the engine'sOPEN_EXTS. AppKit hands the files to the app as open-document events, whichapps/stylo/src/open_documents.rsopens like files named on the command line. - Signing goes inside-out with the hardened runtime
and a secure timestamp: the executable first, then the bundle, with no
--deepon the final signature. The entitlements (entitlements.plist) are deliberately empty. - Notarization: the app is zipped and sent with
xcrun notarytool submit --wait, then the ticket is stapled and checked withstapler validateandspctl. The app goes on a DMG (hdiutil create -srcfolder, retried when a CI runner's device is busy, with anApplicationslink to drag onto), which is signed, notarized and stapled too; the app mounted from it must passcodesign --verify --strict --deep(#1085). Its Finder window (background, icon size and positions) comes frompackaging/macos/dmg/, and its volume is namedStylowithout the version, which the window's background needs; the DMG file name keeps the version. - CLI: the universal
stylo-cliis signed the same way, zipped, and the zip is notarized. A bare executable can't hold a stapled ticket, so Gatekeeper looks the CLI's ticket up online the first time a downloaded copy runs.
Locally, without certificates, the script signs ad hoc
(codesign -s -) and skips notarization, which is enough to
check the bundle and the DMG on your own Mac:
packaging/macos/package.sh --arch universal # needs both rustup targets
packaging/macos/package.sh --arch aarch64 # quicker, host only
Windows
packaging/windows/package.ps1 -Arch x64|x86|arm64 builds
with -C target-feature=+crt-static, so neither the MSI nor
the portable zip needs the Visual C++ redistributable. The flag is
scoped to the target triple, so host build scripts aren't affected.
Before packaging, the script checks each binary's PE machine type against
-Arch(an x64 build can never ship labelled arm64), thatstylo.exeis a GUI program (no console window) and thatstylo-cli.exeis a console program.Portable zip: it ships
packaging/windows/portable.txtbesidestylo.exe. That marker (or aStylo.portablefile) switches on portable mode, as in Picto:ui.jsonand the folders next to it (Swatches, Graphic Styles, Fonts, Data Recovery, logs…) go toStyloData\next to the exe instead of%APPDATA%\Stylo. The first timeStyloDatais created, the settings already in%APPDATA%\Styloare copied into it (the originals stay), so someone updating a portable zip from before the marker keeps their settings. If that folder isn't writable the app logs a warning and uses%APPDATA%. Deletingportable.txtreturns settings to the user profile (theStyloDatafolder is left as it is). The MSI has no marker.STYLO_CONFIG_DIRstill wins,STYLO_NO_PREFSnever looks for the marker, and a portable copy skips the one-time read of the old DrawCraft settings. The logic is inapps/stylo/src/profile.rs. On the other platforms the marker goes where the user sees the app, never inside it: on macOS next toStylo.app(nothing is written inside the signed bundle), for an AppImage next to the.AppImagefile ($APPIMAGE; the mounted executable's own folder is read-only). Flatpak isn't supported:/appis read-only and the sandbox has no folder "beside the app", so a Flatpak install always uses its own config folder.stylo.wxs(WiX) is a per-machine install into Program Files with a Start Menu shortcut and an App Paths entry (Win+Rstylo). Same-version upgrades are allowed, so release candidates replace each other.Double-clicking the MSI opens a setup wizard (WixUI_InstallDir without the licence page): Welcome, install folder (remembered for upgrades in
HKLM\Software\Stylo\InstallDir), Shortcuts (a ticked "Create a desktop shortcut" box), Ready, a progress page, and a Finish page that says where to find the app, with a ticked "Launch Stylo" box. The pages name the full version (stylo.en-us.wxl).package.ps1draws the wizard's banner and side bitmaps from the app icon, so no WiX stock art ships.msiexec /qnstill installs silently, with the desktop shortcut unlessINSTALLDESKTOPSHORTCUT=""is passed. Building needs the WiX UI and Util extensions:wix extension add -g WixToolset.UI.wixext/5.0.2 WixToolset.Util.wixext/5.0.2.The ARM64 build is cross-compiled on the x64 runner.
.github/workflows/windows-arm64.ymlinstalls that MSI on a Windows 11 ARM64 runner, checks that both installed programs are ARM64, runsstylo-cli --versionnatively and uninstalls.Signing:
packaging/windows/sign.ps1signs both.exefiles and then the.msiwithsigntool(SHA-256, RFC 3161 timestamp), using whichever material is present:- a
.pfxcertificate (WINDOWS_CERTIFICATE, base64, andWINDOWS_CERTIFICATE_PASSWORD), or - Azure Trusted Signing (
AZURE_TENANT_ID,AZURE_CLIENT_ID,AZURE_CLIENT_SECRET,AZURE_SIGNING_ENDPOINT,AZURE_SIGNING_ACCOUNT,AZURE_CERT_PROFILE).
With neither, it warns and leaves the files unsigned, so test builds still produce installers. It is the one place to adapt when the signing setup changes;
WINDOWS_TIMESTAMP_URLandSIGNTOOLoverride the timestamp server and thesigntool.exepath.- a
Linux
packaging/linux/package.sh builds the release binaries
and packages them as an AppImage, a .deb and an
.rpm (with nfpm,
from nfpm.yaml) and a plain .tar.gz tree
(bin/, share/). The packages install both
programs, the desktop entry, the AppStream metainfo, the MIME type for
Stylo documents, the icons and the licence files.
Document types. The desktop entry's
MimeType lists every format File › Open reads but PSD, PSB,
.ps PostScript and Targa, so file managers list Stylo under
Open With for them. com.ncdev.stylo.mime.xml declares the
types that shared-mime-info lacks: Stylo documents and templates,
.ait templates and Affinity files
(application/x-affinity). An .af file gets the
Affinity type by its content, and .afdesign and
.afpub files by their extension. A test keeps both files in
step with the engine's OPEN_EXTS.
The jobs run on ubuntu-22.04, the oldest GitHub-hosted
image, so the binaries only need glibc 2.35 or newer: Ubuntu 22.04+,
Debian 12+, Fedora 36+ and RHEL 10. Moving the job to a newer image
raises that floor, so do it deliberately. After packaging, the job
prints the .deb's metadata and contents, runs
ldd on the binary and runs each AppImage with
--version.
RPM dependencies. The x86_64 and aarch64 RPMs
require 64-bit shared-library capabilities rather than
distribution-specific package names: libc, libm's
GLIBC_2.35, libgcc_s,
libxkbcommon, and either Vulkan or EGL. Windowing and GPU
libraries are loaded at runtime, so ldd alone does not list
every requirement. The glibc capability enforces the same floor as the
.deb; a distribution with an older glibc is not supported
by these binaries. It names libm, whose hypot and
hypotf are the binaries' GLIBC_2.35 symbols: RHEL 9's glibc
2.34 provides libc.so.6(GLIBC_2.35) through backports, so a
libc capability let it install binaries that can't start. Weak
recommendations still use package names and do not block
installation.
Before publishing a Linux release:
RISC-V. The linux-riscv64 job
cross-compiles riscv64gc-unknown-linux-gnu on
ubuntu-24.04 against riscv64 libraries from
ports.ubuntu.com (so the floor there is glibc 2.39) and runs
package.sh --formats tar with CROSS_ARCH,
CROSS_TARGET, CROSS_COMPILE and
EMULATOR=qemu-riscv64 set: a .tar.gz only,
with stylo-cli --version run under QEMU.
AppImage updates. Each AppImage embeds the update
information
gh-releases-zsync|storytold|stylo|latest|stylo-*-linux-<arch>.AppImage.zsync
(appimagetool -u), and package.sh writes the
matching .zsync beside it when zsyncmake (the
zsync package) is installed. AppImageUpdate
and AppImageLauncher then download only the changed blocks from the
newest published, non-pre-release release. The job checks both are
there.
Flatpak. The flatpak jobs (x86_64 on
ubuntu-24.04, aarch64 on ubuntu-24.04-arm)
take the Linux job's tarball and repackage it with
packaging/linux/flatpak-bundle.sh and
packaging/linux/flatpak/com.ncdev.stylo.bundle.yml into a
single-file bundle
(flatpak install --user stylo-<v>-linux-<arch>.flatpak),
then install it and run stylo-cli --version in the sandbox.
No Rust build happens there, so the bundle carries the same binaries as
the AppImage. packaging/linux/flatpak/com.ncdev.stylo.yml
is the from-source manifest for a Flathub submission (its header says
how to build it); packaging-lint keeps the runtime and sandbox
permissions of the two manifests identical.
FreeBSD
GitHub has no FreeBSD runners, so the job builds in a FreeBSD 14.3 VM
on ubuntu-latest (the same image as
.github/workflows/freebsd.yml).
packaging/freebsd/package.sh writes a
/usr/local-style tree; users install it with
tar -xzf stylo-*-freebsd-x86_64.tar.gz --strip-components 1 -C /usr/local.
Web
The web job builds apps/stylo-web with trunk and zips
the static site (packaging/web/package.sh). packaging/web/README.md
covers hosting: any path works, .wasm must be served as
application/wasm, and the sample header files for common
hosts.
The draft release
The last job waits for every build, downloads their artifacts, writes
SHA256SUMS.txt and creates the draft
Stylo v<version> with notes generated from the merged
pull requests. If the draft already exists, it replaces its assets and
keeps it a draft. If that version is already published, the job fails
and asks for a version bump (cargo xtask version set).
Secrets
The jobs that sign (macOS, Windows) and the draft-release job run in
the release environment, which only the
release branch can use and which holds the signing secrets.
The other jobs get no secrets. Every secret is optional: a missing one
produces unsigned artifacts and a warning, never a failed build.
| Secret | Used for |
|---|---|
APPLE_CERTIFICATE,
APPLE_CERTIFICATE_PASSWORD,
KEYCHAIN_PASSWORD |
the Developer ID certificate (base64 .p12), imported
into a temporary keychain by
packaging/macos/import-cert.sh |
APPLE_ID, APPLE_PASSWORD,
APPLE_TEAM_ID |
notarization (APPLE_PASSWORD is an app-specific
password) |
WINDOWS_CERTIFICATE,
WINDOWS_CERTIFICATE_PASSWORD |
Windows signing with a .pfx |
AZURE_TENANT_ID, AZURE_CLIENT_ID,
AZURE_CLIENT_SECRET, AZURE_SIGNING_ENDPOINT,
AZURE_SIGNING_ACCOUNT, AZURE_CERT_PROFILE |
Windows signing with Azure Trusted Signing |
Revision history
| Date | Change | Summary |
|---|---|---|
| 2026-10-14 | minor | RPM glibc floor names libm's GLIBC_2.35: RHEL 9's glibc
2.34 provides libc's through backports |
| 2026-10-12 | minor | RPM dependencies use 64-bit runtime capabilities and enforce glibc 2.35 (#1275); real distro install/start-up checks remain pending |
| 2026-10-12 | minor | Portable mode (#1366): the Windows portable zip ships
portable.txt; where the marker goes on macOS and Linux, and
what Flatpak can't do |