AetherPak Maintainer Guide: Building and Publishing Flatpak Apps with GitHub Actions#
1. Introduction#
AetherPak is a GitHub Actions-based system for building Flatpak applications and hosting them as self-hosted Flatpak repositories — entirely within GitHub's free infrastructure. It turns your existing Flatpak manifest into a fully functional, publicly installable app repository, complete with a landing page and one-click install support, with no self-hosted servers required.
How it works#
AetherPak splits the hosting responsibilities between two GitHub services :
- GitHub Container Registry (GHCR) stores the application layers as OCI images — the large blobs that clients download when installing or updating an app.
- GitHub Pages serves a lightweight JSON index (
index/static), a landing page (index.html), a.flatpakreporemote-config file, and per-app.flatpakreffiles.
When a user installs your app, the Flatpak client reads the index from Pages and pulls the actual application layers directly from GHCR in large chunks. This design solves a real scaling problem: a traditional static-HTTP OSTree repository would serve an app as hundreds of tiny objects, which is slow and quickly trips GitHub Pages' rate limits. The OCI-backed approach avoids both problems .
The full pipeline looks like this :
manifest ──build──▶ OSTree repo ──publish──▶ OCI image in GHCR
└──▶ index/static + index.html on Pages
client: Pages index ──(digest)──▶ GHCR blobs
Why use AetherPak?#
| Benefit | Details |
|---|---|
| Free hosting | GHCR and GitHub Pages are free for public repositories |
| Automated builds | Push to your main branch; the workflow builds, signs, and deploys automatically |
| Multi-arch support | Builds for x86_64 and aarch64 in parallel |
| OCI-backed distribution | Reliable, chunked downloads that won't hit Pages rate limits |
| Optional GPG signing | Cryptographically verified installs with --signature-lookaside |
| One-click install | Generated .flatpakref files let users install with a single button click |
| Multi-app repos | One repository can host many apps in a shared index |
| Zero infrastructure | Everything runs in GitHub Actions; no servers to manage |
Live demo#
You can see a working AetherPak deployment at https://aetherpak.github.io/actions-demo/, which publishes GNOME Sudoku for both x86_64 and aarch64 from a 20-line workflow file .
2. Prerequisites#
Before you set up AetherPak, make sure the following are in place:
A GitHub repository with a Flatpak manifest#
You need a GitHub repository containing a Flatpak manifest file (.json or .yaml). This is the same manifest you would pass to flatpak-builder. If you're starting from scratch, Flathub's app submission guide is a good reference for writing one.
Enable GitHub Pages with "GitHub Actions" source#
AetherPak deploys your repository index and landing page to GitHub Pages. To enable this:
- Go to your repository Settings → Pages
- Under Source, select GitHub Actions
This grants the workflow permission to deploy Pages directly. Without this, the publish-site job will fail at the deployment step .
Allow public package creation (organization repositories only)#
If your repository belongs to a GitHub organization, an organization owner must allow public packages before your first workflow run. Otherwise the GHCR image is created as private and cannot be switched to public after the fact.
To enable it: Organization → Settings → Packages → Package creation → enable Public .
After the first successful workflow run, you will also need to make the package public:
- Go to your profile or organization → Packages
- Open the newly created package
- Package settings → Change visibility → Public
Required GitHub Actions permissions#
Your workflow must declare these permissions :
permissions:
contents: read # checkout the repository
packages: write # push OCI images to GHCR
pages: write # deploy to GitHub Pages
id-token: write # OIDC token for Pages deployment
Supported platforms#
AetherPak only supports Linux runners. Builds are supported for x86_64 (amd64) and aarch64 (arm64) architectures — these are the only architectures available in the builder container image .
Basic familiarity with GitHub Actions#
You don't need to understand every detail of GitHub Actions, but knowing how to create a workflow file under .github/workflows/, how to set repository secrets, and how reusable workflows work will help you follow this guide.
3. Available Shared Workflows and Actions#
AetherPak provides both a high-level reusable workflow that orchestrates the full pipeline and a set of modular composite actions you can use individually for custom setups.
Reusable workflow: publish.yml#
aetherpak/actions/.github/workflows/publish.yml@v3
This is the primary entry point for most maintainers. It runs the complete build-publish-deploy pipeline and supports two modes :
- Manifest mode (
manifest-path): builds a single Flatpak app from a manifest - Config mode (
config): readsaetherpak.yamlto build multiple apps with change detection
Internally, it runs five jobs in sequence :
| Job | What it does |
|---|---|
plan | Resolves the build matrix from a manifest or aetherpak.yaml |
build-manifest | Compiles each (app, arch) cell from a manifest (parallel) |
prep-bundle | Fetches, verifies, and imports prebuilt bundle cells (parallel) |
publish-oci | Pushes each OSTree repo as a signed OCI image to GHCR (parallel) |
publish-site | Aggregates all records into the shared index and deploys to Pages (single, concurrency-locked) |
Reusable workflow: prune-github-container-registry.yml#
aetherpak/actions/.github/workflows/prune-github-container-registry.yml@v3
Prunes inactive container versions from GitHub Container Registry (GHCR) to save storage and maintain clean registries. This workflow fetches the active Flatpak index from GitHub Pages, compares it with container versions in GHCR, and deletes versions that are no longer referenced in the active index.
Usage: Called via workflow_call as a reusable workflow.
Inputs:
| Input | Default | Description |
|---|---|---|
app-id | (empty) | Specific flatpak App ID to prune (e.g. org.example.App). If omitted, prunes all apps managed by aetherpak. |
registry | ghcr.io | Target container registry host. |
oci-repository | (empty) | OCI repository path without registry host. Defaults to GITHUB_REPOSITORY (lowercased). |
pages-url | (empty) | Public URL of the flatpak static index page. Defaults to the GitHub Pages URL. |
dry-run | false | Only list versions that would be pruned without actually deleting them. |
Secrets:
| Secret | Description |
|---|---|
token | GitHub token (e.g. GitHub App token or PAT) with packages:read and packages:delete permissions. |
Safety features:
- Will not prune versions that are still in the active index (by digest or tag matching)
- Has dry-run mode for testing
- If the index cannot be fetched and no
app-idis specified, the workflow fails to prevent accidental deletion of all images
Example:
name: Prune Registry
on:
schedule:
- cron: "0 2 * * 0" # weekly on Sunday at 2am
workflow_dispatch:
jobs:
prune:
uses: aetherpak/actions/.github/workflows/prune-github-container-registry.yml@v3
secrets:
token: ${{ secrets.GHCR_PRUNE_TOKEN }}
Composite actions#
These are individual building blocks you can call from your own workflows. They automatically install the aetherpak CLI via aetherpak/setup-cli@v1 if it isn't already on PATH .
aetherpak/actions@v3 — Root action #
Chains build and publish in a single step. Best suited for prebuilt inputs (a .flatpak bundle or an existing OSTree repository). For manifest builds, use the reusable workflow instead — it supplies the required builder container .
aetherpak/actions/build@v3 — Build or import #
Compiles a Flatpak app from a manifest using flatpak-builder, or imports a prebuilt .flatpak bundle or an existing OSTree repository. Outputs repo-path, app-id, branch, and arch.
Key inputs:
manifest-path— path to the Flatpak manifest (JSON/YAML)prebuilt-bundle-path— path to a pre-built.flatpakfileprebuilt-repo-path— path to an existing OSTree repositoryapp-id,branch,arch— coordinates (resolved from the repo when building)run-linter— runflatpak-builder-lint(defaulttrue)cache,cache-state,cache-ccache,cache-build-dir— caching options (see Build Caching)
aetherpak/actions/plan@v3 — Build matrix planner #
Expands an aetherpak.yaml or a single manifest path into a GitHub Actions build matrix, narrowed to apps that changed since base-sha. Outputs matrix, matrix-manifest, matrix-bundle, and per-type counts.
Key inputs:
config— path toaetherpak.yaml(default:aetherpak.yaml)manifest— single manifest path (bypasses config)arch— space-separated architectures for manifest modeforce—''for change detection,'<app-id>'for a single app, or'all'for everythingbase-sha— commit SHA to diff against
aetherpak/actions/prep-bundle@v3 — Bundle fetcher #
Fetches a .flatpak bundle from a URL, verifies its SHA-256 checksum, imports it into an OSTree repository, and rebinds the commit to the consumer-declared channel. This rebind is important: upstream bundles often carry app/<id>/<arch>/master as the branch; prep-bundle rewrites the ref to the branch you declared in aetherpak.yaml .
Key inputs: url, sha256, branch (default stable), repo-path, bundle-path
aetherpak/actions/publish-oci@v3 — OCI publisher #
Pushes an OSTree repository as a signed OCI image to GHCR (or another registry) and writes a per-cell record containing the digest, labels, and optional signature. Parallel-safe — multiple cells can run simultaneously without conflicting.
Key inputs: repo-path, app-id, branch, arch, registry, oci-repository, registry-token, signing, gpg-private-key, records-dir
aetherpak/actions/publish-site@v3 — Site builder #
Aggregates per-cell records into the complete static site: merges index/static, writes .flatpakrepo and .flatpakref files, generates index.html, and assembles the signature lookaside. This is the one job that must run with a concurrency lock.
Key inputs: records-dir, pages-url (required), site-dir, signing, gpg-private-key, remote-name, landing-page, index-template
aetherpak/actions/publish@v3 — Combined publisher #
A thin wrapper over publish-oci + publish-site. Use this when you already have an OSTree repo or .flatpak bundle and want a simple one-action publish step with Pages deployment.
aetherpak/setup-cli@v1 — CLI installer #
Downloads and installs the aetherpak CLI binary on the runner. All composite actions call this automatically if needed, but you can invoke it manually to pin to a specific CLI version.
Key inputs: version (default latest), repo (default aetherpak/cli), install-dependencies (default true — installs flatpak, ostree, flatpak-builder, and related tools)
Tip: If you prefer to use the CLI container images directly in your CI pipeline instead of the setup action, see CLI Container Images: Standard and Builder Variants for detailed usage instructions.
4. Quick Start: Single App#
This section walks you through publishing your first Flatpak app using AetherPak in a few minutes.
Step 1: Enable GitHub Pages#
In your repository, go to Settings → Pages → Source and select GitHub Actions. This allows the workflow to deploy your repository index and landing page directly .
Step 2: Create the workflow file#
Create .github/workflows/publish.yml with the following content, replacing org.example.App.json with the path to your manifest:
name: Publish Flatpak
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
packages: write
pages: write
id-token: write
jobs:
publish:
uses: aetherpak/actions/.github/workflows/publish.yml@v3
with:
manifest-path: org.example.App.json
That's the minimal configuration. By default, the workflow will:
- Build your app for both
x86_64andaarch64 - Set the Flatpak branch to
stableon tag pushes,betaon your default branch, or the git ref name otherwise - Run
flatpak-builder-linton the build - Push OCI images to GHCR under your repository name
- Deploy the index and landing page to GitHub Pages
Step 3: Make the GHCR package public#
After the first successful run, you need to make the container package publicly accessible so users can install without authenticating :
- Go to your profile or organization → Packages
- Open the package created by AetherPak (named after your repository)
- Go to Package settings → Change visibility → Public
Org repositories: An organization owner must first enable public package creation before your first run at Organization → Settings → Packages → Package creation → Public. If this step is skipped, the image is created private and cannot be switched .
Step 4: What happens on each push#
Every push to main triggers the full pipeline :
- Plan — parses your manifest and emits a
(app, arch)matrix - Build — compiles the app inside a
flatpak-buildercontainer for each architecture - Publish-OCI — converts each OSTree repo to an OCI image and pushes it to GHCR (signed if you've configured a GPG key)
- Publish-site — merges the per-arch records into
index/static, writes.flatpakreffiles, regeneratesindex.html, and deploys to GitHub Pages
Your landing page is published at https://<owner>.github.io/<repo>/.
Common workflow options#
| Input | Default | Purpose |
|---|---|---|
manifest-path | (required) | Flatpak manifest to build |
arches | x86_64 aarch64 | Architectures to build |
branch | auto | stable on tags, beta on default branch, else ref name |
run-linter | true | Run flatpak-builder-lint |
deploy | true | Deploy to Pages; false uploads the site as an artifact |
pages-url | project Pages URL | Override for custom domains |
signing | auto | auto, gpg, or off — see GPG Signing |
cache | true | Cache Flatpak runtimes and builder state |
builder-args | --install-deps-from=flathub | Extra flatpak-builder flags |
upload-bundle | false | Export the built app as a .flatpak bundle artifact |
dry-run | false | Build and verify, but skip pushing and deploying |
reconcile-only | false | Skip builds; only reconcile the index against the registry |
Adding GPG signing#
To enable GPG signing, pass your private key as a secret (see GPG Signing for key generation):
jobs:
publish:
uses: aetherpak/actions/.github/workflows/publish.yml@v3
with:
manifest-path: org.example.App.json
signing: gpg
secrets:
gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
5. Multi-App Repository Setup#
One repository can host any number of apps in a single shared index. Instead of specifying manifest-path, you declare all your apps in an aetherpak.yaml file and pass config to the reusable workflow .
Workflow configuration#
# .github/workflows/publish.yml
name: Publish Flatpaks
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
packages: write
pages: write
id-token: write
jobs:
publish:
uses: aetherpak/actions/.github/workflows/publish.yml@v3
with:
config: aetherpak.yaml
secrets:
gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
aetherpak.yaml schema#
The configuration file has the following structure :
# aetherpak.yaml
# Global settings
registry: ghcr.io # OCI registry (default: ghcr.io)
pages_url: https://owner.github.io/repo # Public hosting URL
oci_repository: owner/repo # Registry path (defaults to GitHub repo)
remote_name: my-apps # Flatpak remote name and .flatpakrepo filename
repo_title: My App Repository
repo_homepage: https://example.org
# Optional channel mappings (glob patterns → Flatpak branch names)
channel_mappings:
main: beta
"release/*": stable
# Global build defaults (inherited by all apps)
defaults:
ccache: true
run_linter: false
builder_args:
- --install-deps-from=flathub
# Custom landing page branding
branding:
logo_url: https://example.org/logo.png
favicon_url: https://example.org/favicon.ico
accent_color: "#3584e4"
footer_text: "© 2025 My Project"
index_template: custom-template.html # path to a custom HTML template
# Linter configuration (optional)
linter:
strict: false
ignore_rules:
- no-appstream-screenshot-urls
# Application list
apps:
- id: org.example.AppOne
manifest: apps/org.example.AppOne/org.example.AppOne.json
arches: [x86_64, aarch64]
branch: stable
run-linter: true
- id: com.example.AppTwo
bundles:
x86_64:
url: https://upstream.example.com/AppTwo_x86_64.flatpak
sha256: "abc123...64hexchars"
aarch64:
url: https://upstream.example.com/AppTwo_aarch64.flatpak
sha256: "def456...64hexchars"
branch: stable
Each app requires exactly one of manifest or bundles — not both .
Key configuration fields#
| Field | Description |
|---|---|
registry | OCI registry host (default ghcr.io) |
pages_url | Public URL where the index is served |
oci_repository | Registry image path (defaults to GITHUB_REPOSITORY) |
remote_name | Flatpak remote name and .flatpakrepo filename |
channel_mappings | Map git refs/branches to Flatpak channel names (supports glob wildcards) |
defaults | Global settings inherited by all apps (ccache, state_dir, builder_args, remotes, flatpaks) |
branding | Landing page appearance (logo, favicon, accent color, footer, custom template) |
linter | Linter strictness and ignored rules |
apps | List of application definitions |
Per-app fields :
| Field | Description |
|---|---|
id | Reverse-DNS app ID (e.g. org.gnome.Sudoku) |
manifest | Relative path to Flatpak manifest (mutually exclusive with bundles) |
bundles | Map of arch → {url, sha256} for prebuilt bundles |
arches | List of architectures to build (default [x86_64] for manifest apps) |
branch | Flatpak channel/branch (default stable) |
run-linter | Override global linter setting |
ccache | Enable ccache for this app |
builder_args | Extra flatpak-builder flags for this app |
remotes | Additional Flatpak remotes to add before building |
flatpaks | Flatpak refs to pre-install from declared remotes |
Change detection#
In config mode, the workflow only rebuilds apps that changed since the previous commit. For manifest-based apps, change detection uses gitlink tree diffs against the manifest's directory; for bundle-based apps, it diffs the aetherpak.yaml entry itself .
You can control this behavior with workflow inputs :
| Input | Effect |
|---|---|
| (default) | Rebuild only changed apps |
app: org.example.AppOne | Force rebuild of one specific app |
force-all: true | Rebuild every app in the config |
base-sha: <sha> | Override the diff base commit |
workflow-path: .github/workflows/publish.yml | Touching this file triggers a rebuild-all |
Pipeline for multi-app builds#
The five-stage pipeline runs in parallel where possible :
plan— expandsaetherpak.yamlinto a matrix of(app, arch)cells, narrowed by change detectionbuild-manifest— parallel per-cell compilation inside the builder container; each cell uploads arepo-<app-id>-<arch>artifactprep-bundle— parallel per-cell bundle fetch + import + channel rebind; uploads the same artifact shapepublish-oci— parallel OCI push per cell; each writes aaetherpak-record-<app-id>-<arch>artifactpublish-site— single, concurrency-locked job that downloads all records, merges them into the sharedindex/static, reconciles, writes.flatpakreffiles, and deploys to Pages
The concurrency lock on publish-site is important: it prevents two concurrent workflow runs from racing while reading, merging, and writing back the shared index .
6. Publishing Pre-built Bundles#
AetherPak doesn't require you to build apps from source. If you already have a .flatpak bundle — whether produced by another CI system, a third-party tool, or a cross-compilation setup — AetherPak can import, sign, and publish it directly.
Method 1: Local bundle file with the publish action#
If you produce a .flatpak bundle in a previous job and want to publish it directly, use the aetherpak/actions/publish composite action with bundle-path :
name: Publish Pre-built Flatpak
on:
push:
branches: [main]
permissions:
contents: read
packages: write
pages: write
id-token: write
jobs:
publish:
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deploy.outputs.page_url }}
steps:
- uses: actions/checkout@v4
# Download or build your .flatpak bundle here
- name: Download bundle
run: curl -fLO https://example.org/releases/app.flatpak
- uses: aetherpak/actions/publish@v3
with:
bundle-path: app.flatpak
pages-url: https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}
- id: deploy
uses: actions/deploy-pages@v5
Multiple bundles at once#
You can publish several bundles in a single step by providing a multiline or comma-separated list of paths or glob patterns. When doing this, do not provide app-id or arch — AetherPak reads these coordinates directly from each bundle's internal metadata :
- uses: aetherpak/actions/publish@v3
with:
bundle-path: |
build/*.flatpak
pages-url: https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}
Method 2: Prebuilt workflow artifacts with the reusable workflow#
If your build job uploads .flatpak bundles as GitHub Actions artifacts, you can pass the artifact name pattern to the reusable workflow via prebuilt-bundle-artifact. The pattern supports {arch}, {app-id}, and {branch} placeholders :
name: Build and Publish
on:
push:
branches: [main]
permissions:
contents: read
packages: write
pages: write
id-token: write
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
arch: [x86_64, aarch64]
steps:
- uses: actions/checkout@v4
# ... your build steps produce out/make/app.flatpak ...
- uses: actions/upload-artifact@v4
with:
name: flatpak-bundle-${{ matrix.arch }}
path: out/make/*.flatpak
publish:
needs: build
uses: aetherpak/actions/.github/workflows/publish.yml@v3
with:
config: aetherpak.yaml
prebuilt-bundle-artifact: "flatpak-bundle-{arch}"
The workflow resolves the {arch} placeholder for each matrix cell, downloads the artifact, and uses flatpak info --show-ref to select the matching .flatpak file when the artifact contains multiple bundles .
Method 3: URL bundles in aetherpak.yaml#
For third-party bundles hosted at stable URLs, declare them in aetherpak.yaml using the bundles field :
apps:
- id: com.example.ThirdPartyApp
branch: stable
bundles:
x86_64:
url: https://releases.example.com/ThirdPartyApp_x86_64.flatpak
sha256: "2159fc643175dcf54f8b9293f48fb8b11c41a87fe31684ee4c5d4e94c6f37a42"
aarch64:
url: https://releases.example.com/ThirdPartyApp_aarch64.flatpak
sha256: "a7b3c9d2e1f04586738c9d2b4f1e3a6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2"
The SHA-256 must be exactly 64 lowercase hex characters. AetherPak uses the prep-bundle action to fetch each bundle, verify its checksum, import it into an OSTree repository, and rebind the commit to the declared branch . This rebind step is critical — upstream bundles often carry app/<id>/<arch>/master as their embedded branch name, and prep-bundle rewrites both the ref name and the xa.ref binding to match what you declared, so flatpak install accepts it.
Use cases#
- Third-party apps: publish and redistribute an upstream
.flatpakunder your own repository - Electron/Tauri apps: your framework produces a
.flatpakbundle; feed it straight to AetherPak withoutflatpak-builder - Multi-system CI: build on a specialized runner (e.g., native Raspberry Pi), upload the artifact, and publish from a standard
ubuntu-latestrunner - Import from release assets: mirror another project's GitHub Releases
.flatpakto your own Flatpak repository
Targeting a custom registry#
The publish action also supports non-GHCR registries. Pass registry, oci-repository, and registry-token directly :
- uses: aetherpak/setup-cli@v1 # installs aetherpak CLI on PATH
- uses: aetherpak/actions/publish@v3
with:
bundle-path: app.flatpak
registry: registry.example.com
oci-repository: my-org/my-app
registry-token: ${{ secrets.REGISTRY_TOKEN }}
pages-url: https://flatpak.example.com
Add insecure-registry: true for local or HTTP-only registries.
7. GPG Signing#
Signing is entirely optional. Without a GPG key, AetherPak publishes unsigned repositories and users install with --no-gpg-verify. Configure a key and every OCI image is cryptographically signed; users can then install with verified signatures using Flatpak's --signature-lookaside mechanism .
Note: AetherPak uses GPG-based signing only — this is what Flatpak's OCI verifier supports (via the
containers/imagesimple-signing lookaside). Cosign / keyless signing is not used .
Step 1: Generate a GPG key#
For CI use, generate a passphrase-less RSA key :
gpg --batch --gen-key <<EOF
%no-protection
Key-Type: RSA
Key-Length: 4096
Name-Real: My App Releases
Name-Email: releases@example.org
Expire-Date: 0
%commit
EOF
# Export the private key
gpg --armor --export-secret-keys releases@example.org
Copy the entire output including the -----BEGIN PGP PRIVATE KEY BLOCK----- and -----END PGP PRIVATE KEY BLOCK----- lines.
If you prefer a passphrase-protected key, omit the %no-protection line and store the passphrase as an additional secret.
Step 2: Store the key as a GitHub secret#
- Go to your repository Settings → Secrets and variables → Actions
- Click New repository secret
- Name it
GPG_PRIVATE_KEY(orAETHERPAK_GPG_KEYas used in the demo) - Paste the armored private key as the value
If your key has a passphrase, create a second secret named GPG_PRIVATE_KEY_PASSPHRASE .
Step 3: Pass the secret to the workflow#
jobs:
publish:
uses: aetherpak/actions/.github/workflows/publish.yml@v3
with:
manifest-path: org.example.App.json
signing: gpg # 'gpg' = require key; 'auto' = sign if key is present
secrets:
gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
# gpg-private-key-passphrase: ${{ secrets.GPG_PRIVATE_KEY_PASSPHRASE }}
Signing modes#
| Mode | Behavior |
|---|---|
auto (default) | Sign if gpg-private-key is provided; skip silently if not |
gpg | Require a key; fail the workflow if no key is set |
off | Never sign, even if a key is provided |
What gets published#
When signing is enabled, aetherpak build-site assembles a signature lookaside directory alongside your index :
| File | Contents |
|---|---|
sigs/<repo>@sha256=<digest>/signature-1 | Detached GPG signature for each OCI image |
sigs/key.asc | Exported public key for import |
sigs/signing.json | Manifest read by the landing page to show verified-install commands |
Each .flatpakref file embeds the public key in base64 (GPGKey field) and the SignatureLookaside URL, so installs via the Install button are automatically verified. This is something the .flatpakrepo format cannot carry, which is why .flatpakref files offer the most seamless verified install experience .
Key rotation#
To rotate your signing key :
- Generate a new key following the steps above
- Replace the
GPG_PRIVATE_KEYrepository secret with the new key - Re-publish every channel — rotation only fully takes effect once every image still listed in the index has been re-signed with the new key
The new public key replaces the old one on the next deploy. Existing images remain accessible but will show as unverified under the new key until they are re-published.
Technical note: OCI tag encoding#
Flatpak's OCI verifier strips a [0-9A-Za-z_-]-only regex from the embedded docker-reference tag. App IDs containing dots (e.g. org.gnome.Sudoku) would fail this strip and reject otherwise-valid signatures. AetherPak works around this by encoding . as _ in the OCI image tag portion of the reference; the canonical app ID is preserved in the org.flatpak.ref label .
8. Independent Actions for Custom Pipelines#
The reusable workflow covers most use cases, but AetherPak's composite actions can also be used individually when you need finer-grained control — for example, inserting custom build steps, integrating with an existing workflow structure, or targeting a different publishing target.
Auto-installation of the CLI#
All composite actions automatically invoke aetherpak/setup-cli@v1 to install the aetherpak CLI if it isn't already on PATH. Non-build actions (plan, publish-site) skip system dependency installation to keep setup fast .
To pin a specific CLI version, call setup-cli explicitly before any action step:
- uses: aetherpak/setup-cli@v1
with:
version: v0.15.1
Using build for custom build steps#
Use aetherpak/actions/build when you want to control the runner, inject custom environment variables, or run additional steps between build and publish:
jobs:
build-and-sign:
runs-on: ubuntu-latest
container:
image: ghcr.io/aetherpak/cli:v0.15.1-builder
options: --privileged
steps:
- uses: actions/checkout@v4
- name: Build Flatpak
id: build
uses: aetherpak/actions/build@v3
with:
manifest-path: org.example.App.json
arch: x86_64
branch: stable
run-linter: true
- name: Custom post-build step
run: echo "Built ${{ steps.build.outputs.app-id }} at ${{ steps.build.outputs.repo-path }}"
- name: Publish
uses: aetherpak/actions/publish@v3
with:
repo-path: ${{ steps.build.outputs.repo-path }}
pages-url: https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}
Important: Manifest builds require the
-buildercontainer image with--privilegedsoflatpak-buildercan install SDK/runtime from the baked Flathub remote without the polkit/dbus system helper .
Using plan for matrix generation#
Use aetherpak/actions/plan to drive a dynamic matrix in your own workflow without using the full reusable workflow:
jobs:
plan:
runs-on: ubuntu-latest
container:
image: ghcr.io/aetherpak/cli:v0.15.1
outputs:
matrix: ${{ steps.plan.outputs.matrix }}
count: ${{ steps.plan.outputs.count }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- id: plan
uses: aetherpak/actions/plan@v3
with:
config: aetherpak.yaml
base-sha: ${{ github.event.before }}
build:
needs: plan
if: needs.plan.outputs.count != '0'
strategy:
matrix: ${{ fromJSON(needs.plan.outputs.matrix) }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build ${{ matrix.app-id }} (${{ matrix.arch }})
uses: aetherpak/actions/build@v3
with:
manifest-path: ${{ matrix.manifest }}
app-id: ${{ matrix.app-id }}
arch: ${{ matrix.arch }}
branch: ${{ matrix.branch }}
Using prep-bundle for bundle import workflows#
- name: Fetch and import bundle
uses: aetherpak/actions/prep-bundle@v3
with:
url: https://releases.example.com/app_x86_64.flatpak
sha256: "abc123...64hexchars"
branch: stable
repo-path: _repo
For local bundle files (e.g., from a previous build step), use bundle-path instead of url + sha256:
- uses: aetherpak/actions/prep-bundle@v3
with:
bundle-path: ./out/app.flatpak
branch: stable
repo-path: _repo
Using publish-oci and publish-site separately#
Staged publishing lets you push OCI images in parallel and then run a single, concurrency-controlled site build:
jobs:
push-oci:
strategy:
matrix:
arch: [x86_64, aarch64]
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
name: repo-${{ matrix.arch }}
path: _repo
- name: Push OCI image
id: push
uses: aetherpak/actions/publish-oci@v3
with:
repo-path: _repo
arch: ${{ matrix.arch }}
branch: stable
registry-token: ${{ github.token }}
signing: auto
gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
records-dir: _records
- uses: actions/upload-artifact@v4
with:
name: record-${{ matrix.arch }}
path: _records
include-hidden-files: true
publish-site:
needs: push-oci
concurrency:
group: publish-${{ github.repository }}
cancel-in-progress: false
runs-on: ubuntu-latest
permissions:
pages: write
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
pattern: record-*
path: _records_in
merge-multiple: true
- uses: aetherpak/actions/publish-site@v3
with:
records-dir: _records_in
pages-url: https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}
signing: auto
gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
- uses: actions/deploy-pages@v5
Using aetherpak/setup-cli directly#
Use setup-cli when you want to call aetherpak CLI commands directly in your workflow steps :
- uses: aetherpak/setup-cli@v1
with:
version: v0.15.1 # pin to a specific version
install-dependencies: true # install flatpak, ostree, etc.
- name: Check repository status
run: aetherpak status
- name: Plan changed apps
run: aetherpak plan --config aetherpak.yaml --base-sha ${{ github.event.before }}
The setup-cli action supports Linux only (amd64 and arm64) and installs the binary to $RUNNER_TEMP/aetherpak-bin, adding it to $GITHUB_PATH. It is idempotent: if the correct version is already on PATH, it skips the download .
9. Build Caching#
Compiling a Flatpak app from source can take several minutes per architecture, especially when the runtime and SDK need to be downloaded. AetherPak provides several independent cache controls to dramatically reduce subsequent build times.
Cache inputs#
| Input | Default | Description |
|---|---|---|
cache | true | Master toggle — enables caching of Flatpak runtimes and builder state |
cache-key | (empty) | Custom prefix for the cache key; when unset, a default prefix including the manifest hash is used |
cache-state | true | Cache flatpak-builder state: downloaded modules and their build artifacts |
cache-ccache | true | Cache ccache compiler artifacts — dramatically speeds up C/C++ recompiles |
cache-build-dir | false | Cache flatpak-builder build directories; requires --keep-build-dirs in builder-args |
Configuring caches#
In manifest mode (reusable workflow), pass these as top-level inputs:
jobs:
publish:
uses: aetherpak/actions/.github/workflows/publish.yml@v3
with:
manifest-path: org.example.App.json
cache: true
cache-key: "my-app-v2" # bump this to invalidate all caches
cache-state: true
cache-ccache: true
cache-build-dir: false
In multi-app mode, these inputs apply globally to all manifest builds. Per-app ccache control is available in aetherpak.yaml via the ccache field.
What each cache covers#
cache-state is typically the most impactful cache. It stores the flatpak-builder state directory (.state/ by default), which contains downloaded sources and already-built modules. On a cache hit, flatpak-builder skips modules that haven't changed, potentially reducing a 10-minute build to under a minute.
cache-ccache speeds up C/C++ compilation by caching compiled object files. Very effective for apps with large C/C++ codebases. The ccache directory (.ccache/ by default) is shared across builds, so even a full rebuild is faster on cache hits.
cache-build-dir stores flatpak-builder's per-module build directories. This is off by default because it requires --keep-build-dirs in builder-args to be useful, and the cache can grow large. Only enable it if you have modules that benefit from incremental builds:
jobs:
publish:
uses: aetherpak/actions/.github/workflows/publish.yml@v3
with:
manifest-path: org.example.App.json
cache-build-dir: true
builder-args: |
--install-deps-from=flathub
--keep-build-dirs
Cache key strategy#
The default cache key is derived from the manifest file's hash, so changing any dependency or module in your manifest automatically invalidates the cache. Use cache-key when you want to:
- Force a cache bust (e.g., after a major dependency upgrade): change the prefix to any new string
- Isolate caches per branch: use
cache-key: "${{ github.ref_name }}"to keep stable and beta caches separate - Share a cache across related apps: point multiple apps at the same key prefix (use with care)
Recommendations#
For most apps, the defaults (cache-state: true, cache-ccache: true, cache-build-dir: false) provide the best balance of build speed and storage. Only disable caches if you experience cache corruption or are debugging build reproducibility issues.
10. End-User Installation#
Once you've published your app, users can install it in several ways. AetherPak generates all the necessary files automatically — you don't need to write installation instructions by hand.
Method 1: One-click install via .flatpakref (easiest)#
The landing page deployed to your GitHub Pages URL lists every app and channel with an Install button. Clicking it downloads a .flatpakref file, which when opened adds the Flatpak remote and installs the app in a single step .
The .flatpakref files are located at refs/<app-id>-<channel>.flatpakref under your pages URL. When signing is enabled, each .flatpakref embeds the public key (GPGKey field) and the signature lookaside URL (SignatureLookaside field), so the install is automatically verified.
Each .flatpakref also includes RuntimeRepo: https://dl.flathub.org/repo/flathub.flatpakrepo by default, so the app's runtime is fetched from Flathub if not already installed. Override this with the runtime-repo input, or set it to empty to omit it .
Method 2: Command-line installation#
For users who prefer the terminal, AetherPak publishes the exact commands on the landing page. The remote name defaults to <owner>-<repo> (override with the remote-name workflow input).
Without signing (or flatpak < 1.17):
flatpak remote-add --if-not-exists --user --no-gpg-verify \
<owner>-<repo> oci+https://<owner>.github.io/<repo>
flatpak install --user <owner>-<repo> org.example.App
With signing (flatpak ≥ 1.17, recommended):
# Fetch the public key (shown on the landing page)
curl -fsSLO https://<owner>.github.io/<repo>/sigs/key.asc
flatpak remote-add --user \
--gpg-import=key.asc \
--signature-lookaside=https://<owner>.github.io/<repo>/sigs \
<owner>-<repo> oci+https://<owner>.github.io/<repo>
flatpak install --user <owner>-<repo> org.example.App
For the live demo repository, the commands look like :
curl -fsSLO https://aetherpak.github.io/actions-demo/sigs/key.asc
flatpak remote-add --user \
--gpg-import=key.asc \
--signature-lookaside=https://aetherpak.github.io/actions-demo/sigs \
actions-demo oci+https://aetherpak.github.io/actions-demo
flatpak install --user actions-demo org.gnome.Sudoku
Method 3: .flatpakrepo file#
The landing page also links to a <remote-name>.flatpakrepo file that configures the remote for all apps and channels in your repository at once. Users can click the link to add the remote in their Flatpak GUI client .
Limitation: The .flatpakrepo format cannot embed a SignatureLookaside URL. Adding the remote through a GUI therefore leaves signature verification incomplete. The landing page shows a remote-modify command to fix this:
flatpak remote-modify --user \
--signature-lookaside=https://<owner>.github.io/<repo>/sigs \
<owner>-<repo>
Troubleshooting installation issues#
"Package not found" after install:
Make sure the GHCR package is set to Public (see Prerequisites). Anonymous Flatpak installs fail against private packages .
"Signature verification failed":
The signature lookaside may not be configured on the remote. Run the remote-modify command from the landing page to add it. Also confirm the GHCR package was re-published after any key rotation.
"flatpak: unrecognized option '--signature-lookaside'":
The --signature-lookaside flag requires Flatpak ≥ 1.17. On older versions, use the unsigned remote-add command with --no-gpg-verify.
App runtime not found:
The .flatpakref includes RuntimeRepo pointing to Flathub by default. If this is empty or points elsewhere, the user may need to add Flathub manually:
flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
Updates not appearing:
Flatpak checks for updates via the OCI index. Run flatpak update to pull the latest version. If the index is stale, confirm that the latest workflow run completed the publish-site job successfully.
11. Repository Maintenance#
Removing apps, channels, or architectures#
AetherPak uses reconcile-based removal: there is no explicit delete command. To remove an app, channel, or architecture from your published repository :
- Delete the OCI image from GHCR
- Re-run the publish workflow — the next run reconciles
index/staticagainst the registry and automatically drops any entries whose image no longer exists
The index only removes entries when the registry returns a definitive "not found" response. Transient errors or authentication failures leave entries intact to avoid accidental removal .
Deleting images from GHCR#
Via the web UI (no token needed) :
- Go to your profile or organization → Packages
- Open the package (named after your repository)
- Find the version tagged
<branch>-<arch>(e.g.stable-x86_64) or locate it by digest - Click the version → Delete version → confirm
Via the GitHub CLI:
# Delete by version ID (requires a token with delete:packages scope)
gh api -X DELETE /orgs/OWNER/packages/container/PKG/versions/ID
Via Skopeo (registries that support the OCI delete API):
skopeo login ghcr.io
skopeo delete docker://ghcr.io/OWNER/REPO@sha256:<digest>
Reconcile-only mode#
After deleting one or more images, you can update the landing page without running a full rebuild by triggering the workflow with reconcile-only: true :
# Via workflow_dispatch, adding this input:
jobs:
publish:
uses: aetherpak/actions/.github/workflows/publish.yml@v3
with:
config: aetherpak.yaml
reconcile-only: true
Or trigger it manually from Actions → your workflow → Run workflow and enable the reconcile-only toggle if you've defined it as a workflow_dispatch input:
on:
push:
branches: [main]
workflow_dispatch:
inputs:
reconcile-only:
description: "Skip builds; only reconcile the index"
type: boolean
default: false
jobs:
publish:
uses: aetherpak/actions/.github/workflows/publish.yml@v3
with:
config: aetherpak.yaml
reconcile-only: ${{ inputs.reconcile-only || false }}
Dry-run mode#
Use dry-run: true to build and validate your manifest without publishing anything. All build, lint, and import steps run normally, but the publish-oci and publish-site jobs are skipped entirely :
jobs:
publish:
uses: aetherpak/actions/.github/workflows/publish.yml@v3
with:
manifest-path: org.example.App.json
dry-run: true
This is useful for validating manifest changes in pull requests without modifying any remote state.
How the index works#
The index/static file is the heart of your repository — it's a JSON document that Flatpak reads to discover available apps and their OCI image digests :
{
"Registry": "https://ghcr.io",
"Results": [
{
"Name": "owner/repo",
"Images": [
{
"Digest": "sha256:...",
"Architecture": "amd64",
"Tags": ["stable"],
"Labels": {
"org.flatpak.ref": "app/org.example.App/x86_64/stable",
"org.flatpak.commit": "...",
"org.flatpak.metadata": "..."
}
}
]
}
]
}
Each publish run seeds this index from the current deployed site, then merges in new records. The index accumulates entries across architectures, channels, and apps — all from separate publish-oci cells — into a single file. When the index is deployed, any entry whose OCI image has been deleted from the registry is dropped from the next version of the index.
OCI tags follow the pattern <app-id>-<branch>-<arch> (with dots in the app-id encoded as underscores), so multiple apps in the same repository share a single OCI repository path without overwriting each other's tags .
12. Customization#
AetherPak generates a functional, clean landing page by default, but you have several options to brand and customize the output to match your project's identity.
Custom branding via aetherpak.yaml#
The branding section of aetherpak.yaml lets you tune the default landing page appearance without replacing it :
branding:
logo_url: https://example.org/logo.png # full URL to your project logo
favicon_url: https://example.org/favicon.ico # browser tab favicon
accent_color: "#3584e4" # primary accent color (CSS hex)
footer_text: "© 2025 My App Project" # footer text on the landing page
index_template: templates/custom-index.html # path to a fully custom HTML template
Branding via workflow inputs#
In single-app manifest mode, you can pass branding overrides as workflow inputs without modifying aetherpak.yaml :
| Input | Description |
|---|---|
landing-page | true (default) to render index.html; false to skip it and serve your own page from index/static |
index-template | Path to a custom HTML template file; overrides branding.index_template in aetherpak.yaml |
remote-name | Friendlier name for the Flatpak remote and .flatpakrepo filename (e.g. my-apps instead of owner-repo) |
pages-url | Override the default Pages URL — use this for custom domains |
site-subpath | Serve the Flatpak repository under a subdirectory (e.g. flatpak) |
Custom domain#
To serve your repository from a custom domain (e.g. https://apps.example.org):
- Configure the custom domain in your repository Settings → Pages → Custom domain
- Pass it explicitly via the
pages-urlinput :
jobs:
publish:
uses: aetherpak/actions/.github/workflows/publish.yml@v3
with:
manifest-path: org.example.App.json
pages-url: https://apps.example.org
Hosting under a subpath#
If your Pages site is shared with other content (e.g. a project website), you can publish the Flatpak repository under a subdirectory using site-subpath:
jobs:
publish:
uses: aetherpak/actions/.github/workflows/publish.yml@v3
with:
manifest-path: org.example.App.json
pages-url: https://owner.github.io/repo/flatpak
site-subpath: flatpak
Self-hosting the site#
Set deploy: false to skip GitHub Pages deployment. The workflow uploads the built site as an artifact instead, which you can download and serve from any web host :
jobs:
publish:
uses: aetherpak/actions/.github/workflows/publish.yml@v3
with:
manifest-path: org.example.App.json
deploy: false
pages-url: https://flatpak.example.org # where you'll host it
artifact-name: my-flatpak-site # name of the uploaded artifact
Channel mappings#
Map git branches or tags to Flatpak channel names using channel_mappings in aetherpak.yaml. Glob patterns are supported :
channel_mappings:
main: beta # pushes to 'main' branch → 'beta' channel
"release/*": stable # 'release/1.0' → 'stable' channel
"v*": stable # version tags → 'stable' channel
Concurrency group#
By default, the publish-site job uses the repository name as its concurrency group key, so only one publish-site job runs at a time per repository. If you have multiple independent AetherPak deployments in the same repository (e.g. publishing to different subpaths), give each its own concurrency group :
jobs:
publish-stable:
uses: aetherpak/actions/.github/workflows/publish.yml@v3
with:
config: aetherpak.yaml
concurrency-group: publish-stable
publish-nightly:
uses: aetherpak/actions/.github/workflows/publish.yml@v3
with:
config: aetherpak-nightly.yaml
concurrency-group: publish-nightly
Custom landing page template#
For complete control over the landing page HTML, supply a custom template via index-template (or branding.index_template). AetherPak's build-site command passes the index data to the template, letting you render it however you like. When landing-page: false is set, index.html is omitted entirely and you can consume index/static directly to build your own page.
13. Container Images#
AetherPak's CI pipeline runs inside pre-built container images that ship all the necessary Flatpak tooling. Understanding which image is used where helps you when debugging builds or setting up custom workflows.
Images in use#
The reusable workflow uses CLI container images from ghcr.io/aetherpak/cli, not the flatpak-containers images directly. These CLI images bundle the aetherpak binary alongside the Flatpak toolchain :
| Image | Tag | Used by | Contents |
|---|---|---|---|
ghcr.io/aetherpak/cli | :<cli-version> | plan, prep-bundle, publish-oci, publish-site | aetherpak, flatpak, ostree, git, jq, gpg |
ghcr.io/aetherpak/cli | :<cli-version>-builder | build-manifest | Everything above + flatpak-builder, flatpak-builder-lint, elfutils, ccache tools |
The cli-version workflow input controls which version is pulled (default v0.15.1) . Pin to a specific version tag to ensure reproducible builds:
jobs:
publish:
uses: aetherpak/actions/.github/workflows/publish.yml@v3
with:
manifest-path: org.example.App.json
cli-version: v0.15.1
Underlying base images#
The CLI images are built on top of the Fedora-based container images from aetherpak/flatpak-containers :
ghcr.io/aetherpak/flatpak — the base image :
- Base:
registry.fedoraproject.org/fedora-minimal(pinned by digest for reproducibility) - Includes:
flatpak,ostree,git,jq,curl,tar,gzip,gnupg2,ca-certificates,shared-mime-tools - Pre-configured Flathub repository remote
ghcr.io/aetherpak/flatpak-builder — the builder image :
- Extends
ghcr.io/aetherpak/flatpakvia multi-stage build - Adds:
flatpak-builder,elfutils,appstream,desktop-file-utils,python3,gobject-introspection,cairo,patch,unzip,bzip2,zstd - Includes
flatpak-builder-lint(compiled from source)
Both images are published for linux/amd64 and linux/arm64 as multi-arch manifests, enabling automatic architecture selection .
Why --privileged for builds#
The build-manifest job runs with --privileged . This is required because flatpak-builder installs the app's runtime and SDK from the baked Flathub remote. Running as root inside a privileged container lets flatpak-builder write directly to the system install location without needing the polkit/dbus system helper — which is not available in a container environment. Non-build jobs (plan, publish-oci, publish-site) do not need --privileged.
Architecture limitations#
Build support is limited to x86_64 and aarch64 — these are the only architectures for which the builder container image is published . There is no cross-compilation support; each architecture is built on a native runner.
What happens if cli-version has no matching container#
If you specify a cli-version that doesn't have a corresponding published container image tag, the non-build jobs fall back to aetherpak/setup-cli for CLI installation instead of pulling the CLI container . This fallback runs on ubuntu-latest and installs dependencies via apt-get.
14. Reference#
Live demos#
| Demo | Landing page | Repository |
|---|---|---|
| Single app (GNOME Sudoku) | aetherpak.github.io/actions-demo | aetherpak/actions-demo |
| Multi-app repository | abn.github.io/flatpakrepo | abn/flatpakrepo |
Key repositories#
| Repository | Description |
|---|---|
| aetherpak/actions | Composite actions and reusable workflow |
| aetherpak/cli | The aetherpak CLI tool |
| aetherpak/setup-cli | GitHub Action to install the CLI |
| aetherpak/flatpak-containers | Base container images |
| aetherpak/actions-demo | Minimal working demo |
Additional documentation#
- ARCHITECTURE.md — deep dive into how the pipeline, index, OCI signing, and multi-app orchestration work
- CONTRIBUTING.md — development setup, testing procedures, and code style guidelines
- aetherpak/actions README — quick-start reference, full input tables, and additional usage examples
Getting help and reporting issues#
- Bug reports and feature requests: github.com/aetherpak/actions/issues
- CLI issues: github.com/aetherpak/cli/issues
- Setup CLI issues: github.com/aetherpak/setup-cli/issues
Quick reference: reusable workflow inputs#
The full input reference for aetherpak/actions/.github/workflows/publish.yml@v3 :
Manifest mode inputs (single app):
| Input | Default | Description |
|---|---|---|
manifest-path | Path to the Flatpak manifest | |
arches | x86_64 aarch64 | Architectures to build |
branch | auto | stable on tags, beta on default branch, else ref name |
run-linter | true | Run flatpak-builder-lint |
builder-args | --install-deps-from=flathub | Extra flatpak-builder flags |
Config mode inputs (multi-app):
| Input | Default | Description |
|---|---|---|
config | aetherpak.yaml | Path to aetherpak.yaml |
app | Force rebuild of one specific app ID | |
force-all | false | Rebuild every app |
base-sha | github.event.before | Diff base commit for change detection |
workflow-path | Caller workflow path; touching it triggers rebuild-all |
Shared inputs:
| Input | Default | Description |
|---|---|---|
reconcile-only | false | Skip builds; only reconcile index |
registry | ghcr.io | OCI registry host |
oci-repository | GitHub repo | Registry image path |
remote-name | <owner>-<repo> | Flatpak remote name |
pages-url | project Pages URL | Public hosting URL |
landing-page | true | Write index.html |
deploy | true | Deploy to GitHub Pages |
signing | auto | auto, gpg, or off — see GPG Signing |
runtime-repo | Flathub URL | RuntimeRepo in .flatpakref files |
cache | true | Enable build caching |
cache-key | Custom cache key prefix | |
cache-state | true | Cache builder state |
cache-ccache | true | Cache ccache artifacts |
cache-build-dir | false | Cache build directories |
site-subpath | Subdirectory under pages-url | |
cli-version | v0.15.1 | aetherpak CLI version to use |
upload-bundle | false | Upload .flatpak bundle as artifact |
prebuilt-bundle-artifact | Artifact name pattern for prebuilt bundles | |
dry-run | false | Build only; skip push and deploy |
submodules | recursive | Submodule fetch mode |
Secrets:
| Secret | Description |
|---|---|
gpg-private-key | ASCII-armored GPG private key for signing |
gpg-private-key-passphrase | Passphrase for the GPG key (if protected) |
registry-token | Registry auth token (defaults to github.token for GHCR) |