Skip to main content
Ryoku ships two release artifacts:
  • The signed [ryoku] pacman repository at https://repo.ryoku.dev/stable/$arch/, which holds every Ryoku package (ryoku-shell, ryoku-hub, ryoku, ryoku-blobs, ryoku-desktop, ryoku-keyring, plus the gpk package manager).
  • The live ISO (/docs/iso-build-recipe) signed with the same release key.
Both are produced from the monorepo by scripts under release/ and installation/iso/, and published by workflows under .github/workflows/. This page describes only what those scripts and workflow files actually do.

Versioning

VERSION at the repo root holds the tracked semver (currently 0.1.0-beta.13). The release helpers under bin/ derive everything from it:
  • bin/ryoku-release-version --channel main prints the stable display version.
  • bin/ryoku-release-version --channel unstable-dev --build-number <run> --sha <sha> prints the rolling preview version (e.g. vX.Y.Z-...dev.<run>+g<short-sha>).
  • bin/ryoku-release-version --pkgver prints the per-build pacman version used in the monorepo PKGBUILDs (semver plus commit count and short sha), so every published build is a strictly newer, commit-identifiable pacman version and pacman -Syu actually sees an upgrade after each push.
  • bin/ryoku-release-bump <patch|minor|major> <alpha|beta|stable> prints the next tracked release version.
.github/workflows/release-channel-versions.yml validates these on PRs and pushes against main and unstable-dev. It compiles all four forms above, sanity-checks their shape with regexes, and on an unstable-dev push moves the unstable-dev-latest tag onto the pushed commit. That tag is a marker for the newest rolling build, not a stable release tag.

Cutting a stable release

.github/workflows/stable-release.yml is the stable release entry point. Run it from main with three inputs:
  • bump_type: patch, minor, or major.
  • release_stage: alpha, beta, or stable.
  • dispatch_iso: whether to dispatch the ISO build after tagging.
The workflow:
  1. Computes the new version with bin/ryoku-release-bump and refuses to continue if the resulting tag already exists on origin.
  2. Writes the new value into VERSION, commits as github-actions[bot] with subject release: v<version>, and pushes HEAD + the annotated tag v<version> to origin.
  3. Dispatches publish-repo.yml against the new tag explicitly (a tag pushed by GITHUB_TOKEN does not fire the tag trigger, so the dispatch is required to republish the signed [ryoku] repo).
  4. If dispatch_iso is true, dispatches build-iso.yml against the tag with the chosen release_stage.
Concurrency is gated on the stable-release group so two cuts cannot race.

Building the [ryoku] pacman repo

release/repo/build-repo.sh is the local + CI builder. It expects the release signing key already imported into the active GNUPGHOME. The script:
  1. Refuses to run as root (makepkg refuses too).
  2. Sanity-checks for makepkg, repo-add, and gpg, and that the key EB6D3C0F55A7B3CABA6B2838847B274F025DD6E3 is available.
  3. Wipes out/<arch>/ so the database is rebuilt from exactly this run’s package set (no repo-add --new).
  4. Exports PKGDEST=out/<arch> and PKGEXT=.pkg.tar.zst, then exports RYOKU_PKGVER from bin/ryoku-release-version --pkgver. The monorepo PKGBUILDs (ryoku-shell, ryoku-hub, ryoku, ryoku-blobs, ryoku-desktop) read this so every build is uniquely versioned; ryoku-keyring and gpk keep their own pkgver (key-rotation date and upstream release).
  5. Runs each release/packages/*/PKGBUILD with makepkg --force --clean --nodeps --noconfirm --sign --key $KEY_ID. --nodeps is intentional: runtime depends and AUR depends are not resolvable here and not needed to compile; only the host build toolchain matters (base-devel, go, cmake, ninja, qt6-shadertools, qt6-declarative).
  6. Confirms every produced package has a .sig beside it.
  7. Runs repo-add -s -k $KEY_ID out/<arch>/ryoku.db.tar.gz <pkg>... to build the signed database from the actual package set.
  8. Materializes the bare names ryoku.db, ryoku.db.sig, ryoku.files, ryoku.files.sig as real files (not symlinks), because object storage has no symlinks and pacman fetches the bare names.
Env overrides: RYOKU_REPO_OUT, RYOKU_REPO_KEY, RYOKU_REPO_NAME, RYOKU_REPO_ARCH, RYOKU_PACKAGES_DIR, RYOKU_PKGVER. The final layout is exactly what the mirror serves:

Publishing the repo

.github/workflows/publish-repo.yml runs the same build-repo.sh and uploads the result. Triggers: push to main, push of a v* tag, and workflow_dispatch (including the dispatch issued by stable-release.yml). A push to unstable-dev never publishes; the repo only advances when main fast-forwards. The job runs inside archlinux:latest (makepkg, repo-add, and pacman only exist on Arch) and installs the build toolchain: base-devel git go cmake ninja qt6-shadertools qt6-declarative gnupg rclone. Secrets required (workflow fails early when any are missing): Build flow:
  1. A throwaway builder user is created and given ownership of the checkout (makepkg refuses to run as root).
  2. The armored private key is written to a 0600 file under $RUNNER_TEMP, chowned to builder, then gpg --batch --imported into a fresh ephemeral GNUPGHOME configured with allow-loopback-pinentry (the release key is passphraseless; loopback keeps a headless runner from blocking on a pinentry).
  3. release/repo/build-repo.sh runs as builder.
  4. The key file is removed.
Publish flow: rclone is configured with a Cloudflare provider entry (type = s3, provider = Cloudflare, no_check_bucket = true so the scoped Object Read+Write token does not 403 on HeadBucket/CreateBucket), then:
sync (not copy) is deliberate: superseded packages and rotated-out signatures are pruned, so the bucket only ever holds the current set.

Building and publishing the ISO

.github/workflows/build-iso.yml builds the live ISO with installation/iso/build.sh (/docs/iso-build-recipe) and publishes it. Triggers: workflow_dispatch (with a release_stage input of alpha, beta, or stable, defaulting to alpha) and a v* tag push. There is no branch-push trigger, so commits to unstable-dev never publish anything user-facing. The build runs on ubuntu-latest with timeout-minutes: 120 and walks through these steps:
  1. Checkout with fetch-depth: 0 and persist-credentials: false. build.sh stages the payload with git archive HEAD, so full history is cheap to keep around.
  2. Prepare release metadata. Computes tracking_id="r${GITHUB_RUN_NUMBER}-${short_sha}" and exports RYOKU_ISO_* env vars (commit, run URL, channel = the input release_stage, RYOKU_ISO_INSTALLER_REF=main).
  3. Verify required secrets are present (R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_ENDPOINT, GPG_PRIVATE_KEY). Fails fast with the missing list.
  4. Free disk space on the runner. Strips preinstalled toolchains (/opt/hostedtoolcache, /usr/share/dotnet, /opt/ghc, /usr/local/lib/android, /usr/local/share/boost) and prunes docker.
  5. Build ISO inside a --privileged archlinux:latest container, bind-mounting the repo at /repo and $RUNNER_TEMP/ryoku-iso-build at /work. The container refreshes archlinux-keyring, installs the build toolchain (archiso git go base-devel cmake ninja qt6-base qt6-declarative qt6-shadertools), and runs ./build.sh with RYOKU_ISO_OUT=/repo/installation/iso/out, RYOKU_ISO_WORK=/work/work, RYOKU_ISO_STAGE=/work/staging, and a reproducible SOURCE_DATE_EPOCH. After the build the out/ dir is chowned back to the runner user. The --privileged flag is required so pacstrap can bind-mount /proc, /sys, and /dev into the airootfs.
  6. Capture ISO path and rename the artifact to the tracked public name ryoku-<YYYY.MM.DD>-r<run>-<sha>-x86_64-<installer_ref>.iso.
  7. Mount the ISO live root for Trivy. Loop-mount the ISO read-only, find the squashfs (*.sfs), loop-mount it, and rsync the rootfs into $RUNNER_TEMP/ryoku-iso-rootfs so Trivy can read it as a regular tree.
  8. Trivy SARIF report with scan-type: rootfs, scanners vuln,secret,misconfig, severities HIGH,CRITICAL, ignore-unfixed: true. skip-dirs excludes runtime mounts, the pacman package cache, and the baked payload’s bulky brand and wallpaper assets at usr/share/ryoku/ryoku/assets. The SARIF is uploaded to the Security tab via github/codeql-action/upload-sarif@v4.
  9. Block on critical CVEs/misconfigurations. A second Trivy step with severity: CRITICAL, exit-code: 1 fails the workflow before any signing or upload if a critical finding is present.
  10. Sign the ISO with GPG. Imports GPG_PRIVATE_KEY into an ephemeral GNUPGHOME under mktemp -d, signs with --batch --pinentry-mode loopback --passphrase-fd 0 if GPG_PASSPHRASE is set or plain --batch --detach-sign otherwise, then verifies with gpg --verify. Exports the public key as ryoku-release-key.pub.asc next to the ISO; this mirrors the repo-canonical copy at keys/ryoku-release-key.pub.asc.
  11. Generate release manifests. bin/ryoku-iso-manifest <iso> writes <iso>.sha256, <iso>.json, <iso>.js, latest.json, and latest.js.
  12. Install rclone via the official installer script.
  13. Upload to Cloudflare R2. rclone is configured with the same Cloudflare provider entry as the repo workflow (no_check_bucket = true). The ISO, signature, sha256, per-ISO manifests, latest.* pointers, and the public key are copied to Ryoku:$dest/ where $dest=${R2_BUCKET:-ryoku}. The [ryoku] pacman repo owns per-arch prefixes in the same bucket; the ISO release lives at the root.
  14. Upload to the workflow run as well via actions/upload-artifact@v4 with retention-days: 14, as a fallback to the R2 publish.
  15. Notify Discord if DISCORD_WEBHOOK_URL is set. Sends an embed with title, tracking ID, channel, ref, image name, and the brand mark from ryoku/assets/brand/logo-mark.png. Never fails the build (if: always(), continue-on-error: true).

ISO build secrets

The required set above (R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_ENDPOINT, GPG_PRIVATE_KEY) plus the optional ones: R2_ENDPOINT MUST be the account-scoped S3 endpoint (https://<account>.r2.cloudflarestorage.com), not the bucket URL.

The signing key

release/packages/ryoku-keyring/PKGBUILD packages the public key material (ryoku.gpg, ryoku-trusted, ryoku-revoked) into /usr/share/pacman/keyrings/. The canonical public key for users to verify against lives in the repo at keys/ryoku-release-key.pub.asc. The release identity is Ryoku Releases <[email protected]>, and the key fingerprint build-repo.sh signs with is EB6D3C0F55A7B3CABA6B2838847B274F025DD6E3. Rotating the key: regenerate ryoku.gpg, ryoku-trusted, and ryoku-revoked, bump the pkgver date in ryoku-keyring/PKGBUILD, replace keys/ryoku-release-key.pub.asc, and update RYOKU_REPO_KEY (or the default in build-repo.sh) to the new fingerprint.

Verifying a published ISO

The public key ships alongside each ISO in the same R2 location and is mirrored in the repo:
The fingerprint MUST match EB6D3C0F55A7B3CABA6B2838847B274F025DD6E3 before you trust the imported key.
Last modified on June 24, 2026