AETHERPAK

Flatpak repositories on GitHub

GitHub Actions that build Flatpak apps and host them as a Flatpak repository, using GHCR for blobs and GitHub Pages for the index.

What it is

A static-HTTP OSTree repository serves an app as many small objects, so clients make hundreds of requests. That is slow, and it trips GitHub Pages rate limits. AetherPak uses Flatpak's native OCI support instead:

  • Application layers (blobs) live in GHCR as OCI images.
  • A small JSON index and a landing page are served from GitHub Pages.
  • Clients read the index from Pages and pull layers from GHCR in large chunks.

What your users get

Every repository gets a landing page like this, with a one-click .flatpakref install button and copy-paste commands for each app and channel.

Live AetherPak actions-demo registry page showing GNOME Sudoku with install commands

Usage

1

Enable GitHub Pages & public packages

Turn on Pages, and make the published GHCR package public so anyone can install without authenticating (). Organization repos must allow public packages first.

2

Add a signing key (optional)

Generate a GPG key and store it as your repo's AETHERPAK_GPG_KEY secret (). With signing: auto (the default) the workflow signs every image once the key is present, publishing the signature and public key beside the index so clients verify on install. With no key set, nothing changes.

3

Add the workflow

Pick the shape that matches your repository: one Flatpak per repo, or several Flatpaks declared in aetherpak.yaml.

Create .github/workflows/publish.yml. It builds from your manifest for x86_64 and aarch64 and deploys to Pages.

name: Publish Flatpak
on: { push: { branches: [main] } }
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
    secrets:
      gpg-private-key: ${{ secrets.AETHERPAK_GPG_KEY }}  # signs when the key is set

Pin to a specific commit for reproducible builds:

uses: aetherpak/actions/.github/workflows/publish.yml@69bf04d09fb3d4168d0a1be87840921bce027b9d

Configuration

manifest-pathSingle-app mode: the Flatpak manifest to build.
configMulti-app mode: path to aetherpak.yaml. Default aetherpak.yaml.
archesSingle-app: architectures to build. Default x86_64 aarch64.
branchFlatpak branch (channel). In config mode, this overrides the branch of all planned apps. Defaults to stable on tags, beta on the default branch, else the ref name.
run-linterSingle-app: run flatpak-builder-lint. Default true.
cacheSingle-app: cache flatpak runtimes and builder files. Default true.
cache-keyCustom cache key prefix. When unset, a default including the manifest hash is used.
cache-stateCache flatpak-builder state (downloads and built modules). Default true.
cache-ccacheCache ccache compiler artifacts. Default true.
cache-build-dirCache flatpak-builder build directories (requires --keep-build-dirs). Default false.
builder-argsSingle-app: extra flatpak-builder flags, one per line. Default --install-deps-from=flathub.
appMulti-app: rebuild only this app id (skip change detection).
force-allMulti-app: rebuild every app. Default false.
base-shaMulti-app: commit to diff against for change detection. Defaults to github.event.pull_request.base.sha (on PRs) or github.event.before.
workflow-pathMulti-app: caller workflow path; touching it triggers a rebuild-all.
signingGPG-sign images: auto (sign when a key is set), gpg, or off. Default auto.
registryOCI registry host. Default ghcr.io.
oci-repositoryImage repository path. Defaults to this repository.
remote-nameFlatpak remote name and .flatpakrepo filename. Resolution priority: input value > AETHERPAK_REMOTE_NAME env var > remote_name in aetherpak.yaml > sanitized repo slug.
pages-urlPublic URL of the index. Defaults to the project's Pages URL.
runtime-repoRuntimeRepo in each one-click .flatpakref. Defaults to Flathub; empty omits it.
landing-pageWrite the static index.html. Set false to render your own page.
index-templatePath to a custom HTML index template. Overrides branding.index_template.
deployDeploy to Pages. Set false to host the site yourself.
artifact-nameSite artifact name when deploy: false. Default aetherpak-site.
submodulesSubmodule fetch mode for the build checkout. Default recursive.
concurrency-groupOverride the per-repository publish lock. Only for repos publishing several independent sites.
site-subpathOptional subdirectory under site-dir to structure the repository files.
prebuilt-bundle-artifactName or pattern of prebuilt bundle workflow artifacts to download and ingest. Supports `{arch}`, `{app-id}`, and `{branch}` placeholders. If the artifact contains multiple bundles, the workflow automatically matches by matrix cell app-id and arch.
dry-runDry run mode: build and verify everything, but skip pushing to the registry and publishing the site. Default false.
reconcile-onlySkip build and push; only reconcile the index against the registry and re-deploy. Use after deleting an image.
cli-versionaetherpak CLI release / container image tag. Default v0.33.3.
Change detection (multi-app): a manifest entry rebuilds when its manifest directory changes (including submodule gitlink bumps); any entry rebuilds when its aetherpak.yaml dict changes (sha256 bumps, arch additions, runtime updates). force-all or app override it.

Set pages-url to your domain (also set it under Settings, then Pages):

with:
  pages-url: https://flatpak.example.com

Learn more

Repository — turn on Pages: Settings, then Pages, then Source: GitHub Actions. After the first publish, make the image public so flatpak install works without authentication: the package's page, then Package settings, then Change visibility, then Public.

Organization — for an org-owned repo, an owner must allow public packages before the first publish, or the image is created private and can't be switched: Organization, then Settings, then Packages, then Package creation, then enable Public.

Personal-account repos skip the organization step. The package appears only after the workflow's first successful publish.

Create a passphrase-less 4096-bit RSA key and store the armored private key as your repository's AETHERPAK_GPG_KEY secret with the GitHub CLI. Run it once from a clone of the repo, with gh auth login done:

NAME="Example Releases"
EMAIL="[email protected]"
EXPIRE="0"  # 0 = never; or 1y, 2y, 18m, 2026-12-31

# Generate a signing key
gpg --batch --gen-key <<EOF
%no-protection
Key-Type: RSA
Key-Length: 4096
Name-Real: $NAME
Name-Email: $EMAIL
Expire-Date: $EXPIRE
%commit
EOF

# Store the private key as the repo secret the workflow reads
gpg --armor --export-secret-keys "$EMAIL" \
  | gh secret set AETHERPAK_GPG_KEY

The workflow reads AETHERPAK_GPG_KEY and, with signing: auto, signs every image as soon as the secret exists. To rotate, re-run this and re-publish each channel.

A meta-repository with one manifest-source app (vendored as a submodule) and one bundle-source app (imported from an upstream GitHub release):

apps:
  - id: org.example.App
    manifest: apps/org.example.App/manifest.yaml
    runtime: gnome-50
    arches: [x86_64, aarch64]
    branch: stable

  - id: com.example.Other
    branch: stable
    bundles:
      x86_64:
        url: https://github.com/upstream/.../Other_x86_64.flatpak
        sha256: 2159fc...
      aarch64:
        url: https://github.com/upstream/.../Other_aarch64.flatpak
        sha256: a74e98...

See Configuration → Config file for the full schema.