From d9d495a53ea561b89cf51a746070670fbaea76c3 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Thu, 18 Jun 2026 21:07:27 +0000 Subject: [PATCH] feat(flatpak): host a signed OSTree repo at flatpak.unom.io for `flatpak update` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CI only shipped a single-file .flatpak bundle, which has no remote — users couldn't `flatpak update`. Keep the bundle (Decky fallback) but also sign the OSTree repo flatpak-builder already produces and publish it to a shared, reusable unom-wide remote. - flatpak.yml: pin --default-branch=stable; import the signing key and build-update-repo --gpg-sign; generate unom.flatpakrepo + the app .flatpakref + index.html; rsync the repo to unom-1 and bring up a static Caddy container. The step no-ops until FLATPAK_GPG_PRIVATE_KEY/DEPLOY_* exist (build stays green). - packaging/flatpak/server/: compose.production.yml + Caddyfile (static file server on :3230, mirrors docker.yml deploy-docs). - unom-flatpak.gpg: committed public signing key (base64 -> GPGKey= in the descriptors). - README: hosted repo is now the recommended install; documents the one-time infra (edge Caddy vhost, infra port 3230, DNS, the GPG secret). Edge Caddy vhost + infra port allowlist + the secret are applied out-of-band. Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitea/workflows/flatpak.yml | 73 +++++++++++++- packaging/flatpak/README.md | 94 ++++++++++++++---- packaging/flatpak/server/Caddyfile | 15 +++ .../flatpak/server/compose.production.yml | 14 +++ packaging/flatpak/unom-flatpak.gpg | Bin 0 -> 1158 bytes 5 files changed, 176 insertions(+), 20 deletions(-) create mode 100644 packaging/flatpak/server/Caddyfile create mode 100644 packaging/flatpak/server/compose.production.yml create mode 100644 packaging/flatpak/unom-flatpak.gpg diff --git a/.gitea/workflows/flatpak.yml b/.gitea/workflows/flatpak.yml index eb755af..b81fbc0 100644 --- a/.gitea/workflows/flatpak.yml +++ b/.gitea/workflows/flatpak.yml @@ -63,7 +63,9 @@ jobs: - name: Tooling run: | # flatpak-cargo-generator.py (master) needs aiohttp + tomlkit (NOT the old `toml`). - dnf -y install flatpak flatpak-builder git python3 python3-aiohttp python3-tomlkit curl jq + # gnupg2/rsync/openssh-clients: sign the OSTree repo + rsync it to unom-1 (see the deploy step). + dnf -y install flatpak flatpak-builder git python3 python3-aiohttp python3-tomlkit curl jq \ + gnupg2 rsync openssh-clients # Flathub provides the GNOME runtime/SDK + the rust-stable + ffmpeg-full extensions. flatpak remote-add --user --if-not-exists flathub \ https://dl.flathub.org/repo/flathub.flatpakrepo @@ -106,7 +108,10 @@ jobs: # runtime/SDK + the rust-stable (//25.08, rustc 1.96) and llvm20 SDK extensions, plus # the runtime's auto codecs-extra (HEVC libavcodec). --disable-rofiles-fuse is the # container-safe path (no FUSE). + # --default-branch=stable pins the ref to app/io.unom.Punktfunk/x86_64/stable so the + # hosted .flatpakref (Branch=stable) matches deterministically (manifest sets no branch). flatpak-builder --user --force-clean --disable-rofiles-fuse \ + --default-branch=stable \ --install-deps-from=flathub \ --repo="$PWD/repo" \ "$PWD/build-dir" "$MANIFEST" @@ -134,6 +139,72 @@ jobs: "$BASE/latest/punktfunk-client.flatpak" echo "published $BASE/latest/punktfunk-client.flatpak" + # Sign the OSTree repo flatpak-builder already produced and publish it to flatpak.unom.io on + # unom-1, so users get `flatpak update` (the single-file bundle above has no remote). Mirrors + # docker.yml's deploy-docs (DEPLOY_* = the unom-ci-deploy key). No-ops cleanly until the GPG + # secret + DEPLOY_* exist, so the bundle build stays green during setup. + - name: Sign + deploy the OSTree repo to unom-1 (flatpak.unom.io) + env: + FLATPAK_GPG_PRIVATE_KEY: ${{ secrets.FLATPAK_GPG_PRIVATE_KEY }} + DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }} + DEPLOY_USER: ${{ secrets.DEPLOY_USER }} + DEPLOY_PORT: ${{ secrets.DEPLOY_PORT }} + DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }} + run: | + set -euo pipefail + if [ -z "${FLATPAK_GPG_PRIVATE_KEY:-}" ] || [ -z "${DEPLOY_HOST:-}" ]; then + echo "::warning::FLATPAK_GPG_PRIVATE_KEY/DEPLOY_* not set — skipping repo deploy (bundle still published)." + exit 0 + fi + # 1) Import the signing key into a throwaway keyring; sign the repo (commits + summary). + export GNUPGHOME="$(mktemp -d)"; chmod 700 "$GNUPGHOME" + echo "$FLATPAK_GPG_PRIVATE_KEY" | base64 -d | gpg --batch --import + KEYID="$(gpg --list-keys --with-colons | awk -F: '/^fpr:/{print $10; exit}')" + flatpak build-update-repo --generate-static-deltas \ + --gpg-sign="$KEYID" --gpg-homedir="$GNUPGHOME" "$PWD/repo" + # 2) Build the install descriptors (GPGKey = the committed public key, base64). + GPGKEY="$(base64 -w0 packaging/flatpak/unom-flatpak.gpg)" + rm -rf site && mkdir -p site + cat > site/unom.flatpakrepo < "site/${APP_ID}.flatpakref" < site/index.html <unom flatpak repo +

unom Flatpak repository

+

Install the Punktfunk Linux client (auto-adds Flathub for the GNOME runtime, then tracks updates):

+
flatpak install --user $REPO_URL/${APP_ID}.flatpakref
+          flatpak run $APP_ID
+

Or add the whole remote: flatpak remote-add --user --if-not-exists unom $REPO_URL/unom.flatpakrepo

+ EOF + # 3) Ship to unom-1 and (re)start the static server. rsync WITHOUT --delete keeps old + # objects so clients mid-update aren't broken; the fresh signed summary advertises latest. + install -d -m700 ~/.ssh + printf '%s\n' "$DEPLOY_SSH_KEY" > ~/.ssh/deploy; chmod 600 ~/.ssh/deploy + SSH="ssh -i $HOME/.ssh/deploy -p ${DEPLOY_PORT:-22} -o StrictHostKeyChecking=accept-new" + DEST="${DEPLOY_USER}@${DEPLOY_HOST}" + $SSH "$DEST" "mkdir -p ~/$DEPLOY_DIR/site/repo" + rsync -az --info=stats1 -e "$SSH" repo/ "$DEST:$DEPLOY_DIR/site/repo/" + rsync -az -e "$SSH" site/unom.flatpakrepo "site/${APP_ID}.flatpakref" site/index.html "$DEST:$DEPLOY_DIR/site/" + rsync -az -e "$SSH" packaging/flatpak/server/compose.production.yml packaging/flatpak/server/Caddyfile "$DEST:$DEPLOY_DIR/" + $SSH "$DEST" "cd ~/$DEPLOY_DIR && docker compose -f compose.production.yml up -d" + echo "deployed → $REPO_URL/${APP_ID}.flatpakref" + - name: Attach bundle to the Gitea release (tags only) if: startsWith(gitea.ref, 'refs/tags/') env: diff --git a/packaging/flatpak/README.md b/packaging/flatpak/README.md index 77bdfeb..d718d62 100644 --- a/packaging/flatpak/README.md +++ b/packaging/flatpak/README.md @@ -1,10 +1,16 @@ # punktfunk client — Flatpak (Steam Deck / SteamOS, and any flatpak distro) The native Linux **client** (crate `punktfunk-client-linux`, binary `punktfunk-client`) is -published as a single-file **`.flatpak` bundle** to **Gitea's generic package registry** in -the public `unom` org. CI (`.gitea/workflows/flatpak.yml`) builds and publishes on every push -to `main` (a rolling `0.0.1-ciN.` build) and on `v*` tags (a clean `X.Y.Z`), and on tags -also attaches the bundle to the Gitea release. +published two ways by CI (`.gitea/workflows/flatpak.yml`), on every push to `main` (a rolling +`0.0.1-ciN.` build) and on `v*` tags (a clean `X.Y.Z`): + +1. **Hosted OSTree repo at `https://flatpak.unom.io`** (recommended) — a GPG-signed Flatpak + remote served by a static Caddy container on unom-1, so users **install once and then + `flatpak update`**. Shared unom-wide repo (remote name `unom`), reusable by other unom apps + under the same signing key. See "Install (recommended)" below. +2. **Single-file `.flatpak` bundle** in **Gitea's generic package registry** (`unom` org) — the + no-remote fallback the **Decky plugin** consumes (stable `latest/punktfunk-client.flatpak` + URL) and the offline/manual path. On tags it's also attached to the Gitea release. > The **host** is NOT a flatpak (it needs unsandboxed `/dev/uinput` + zero-copy NVENC — see > [`../README.md`](../README.md) "Why not Flatpak"). Only the **client** is sandbox-friendly. @@ -20,10 +26,34 @@ with HEVC-capable FFmpeg supplied automatically by the runtime's `codecs-extra` App id: **`io.unom.Punktfunk`** (matches the Apple bundle id family and the Decky plugin's flatpak fallback). -## Install on the Deck (one-time) +## Install (recommended): the hosted repo + +One command adds the signed `unom` remote and installs the client; it auto-adds Flathub for the +GNOME runtime, and `flatpak update` tracks new builds from then on: + +```sh +flatpak install --user https://flatpak.unom.io/io.unom.Punktfunk.flatpakref +flatpak run io.unom.Punktfunk +``` + +Equivalent two-step (add the whole remote, then install by app id): + +```sh +flatpak remote-add --user --if-not-exists unom https://flatpak.unom.io/unom.flatpakrepo +flatpak install --user unom io.unom.Punktfunk +``` + +Updates — the whole point of the hosted repo: + +```sh +flatpak update # or: flatpak update io.unom.Punktfunk +``` + +## Install on the Deck via the bundle (no-remote fallback) The generic registry is a plain HTTP file store, so just download the bundle and install it -per-user (no root, survives SteamOS updates): +per-user (no root, survives SteamOS updates). This is what the Decky plugin uses; the hosted +repo above is the better path for a human on the Deck: ```sh # Pick a version: a tag like 1.2.3, or the newest main build's 0.0.1-ciN.gSHA. @@ -47,16 +77,17 @@ flatpak run io.unom.Punktfunk --connect HOST:PORT The **Decky plugin** launches exactly this (`flatpak run io.unom.Punktfunk --connect …`) once installed — see [`../../clients/decky/README.md`](../../clients/decky/README.md). -## Updates +## Updating the bundle install -A bundle has no remote to track, so updates are "download the newer bundle and reinstall": +If you installed from the **bundle** (not the hosted repo), it has no remote to track, so updates +are "download the newer bundle and reinstall": ```sh flatpak install --user --bundle /tmp/punktfunk-client.flatpak # same command, newer file ``` -(If you want `flatpak update` to track new builds automatically you'd need a hosted OSTree -repo, which Gitea cannot serve — see "Alternatives" below. The bundle is the simplest path.) +Installs from `https://flatpak.unom.io` instead just take `flatpak update` (see "Install +(recommended)" above). ## Build locally / the CI fallback @@ -110,13 +141,38 @@ installed by the manifest). `cargo-sources.json` (the offline crate cache) is a network + `python3`/`aiohttp`/`tomlkit` (`build-flatpak.sh` does this automatically) and, for a build host that lacks those (the Deck), rsync the generated file in alongside the manifest. -## Alternatives considered (and why the bundle wins) +## Hosting the repo (unom-1) + one-time setup -- **Generic registry bundle (chosen):** one curl to publish, one `flatpak install --bundle` to - consume; mirrors the existing deb/rpm curl-upload pattern exactly. No auto-update. -- **Release attachment:** also done on tags (the bundle is attached to the Gitea release), good - for a human-facing download page; the generic registry gives the stable per-version URL the - Decky fallback and scripts use. -- **Self-hosted OSTree repo (rejected):** would enable `flatpak update`, but Gitea has no - flatpak/ostree registry, so it would mean serving a static OSTree tree over Gitea raw/Pages — - more moving parts than the appliance needs today. +The OSTree repo flatpak-builder produces is GPG-signed in CI and rsynced to unom-1, where a tiny +static **Caddy container** (`server/compose.production.yml` + `server/Caddyfile`, port **3230**) +serves the `./site` tree (`repo/` + `unom.flatpakrepo` + `io.unom.Punktfunk.flatpakref` + +`index.html`). The edge Caddy on home-reverse-proxy-1 fronts it at `https://flatpak.unom.io`. +The CI deploy step **no-ops until the secret + infra exist**, so it won't redden builds mid-setup. + +**Signing key:** dedicated RSA-4096 key `unom Flatpak Repo `. Public key committed +at [`unom-flatpak.gpg`](unom-flatpak.gpg) (its base64 goes into the `.flatpakrepo`/`.flatpakref` +`GPGKey=`); private key (ASCII-armored, then base64) lives only in the CI secret. + +One-time setup (mirrors any new unom DMZ service — see the deploy-infra notes): + +1. **Secret** `FLATPAK_GPG_PRIVATE_KEY` on this repo = base64 of the armored private key + (`gpg --armor --export-secret-keys | base64 -w0`). `DEPLOY_*` + `REGISTRY_TOKEN` already exist. +2. **Edge Caddy** on home-reverse-proxy-1 (`/home/caddy/caddy/Caddyfile`, apply by hand + `./reload.sh`): + `flatpak.unom.io { reverse_proxy 192.168.50.50:3230 }` +3. **Port allowlist:** add `3230` to `caddy_target_ports` in `unom/infra` (proxmox/unom-1) + terraform apply. +4. **DNS:** ensure `flatpak.unom.io` resolves to the edge proxy. + +Re-signing/rotation: regenerate the key, replace `unom-flatpak.gpg` + the secret; every client must +re-add the remote (the `GPGKey` changed), so rotate rarely. + +## Alternatives considered + +- **Hosted OSTree repo (chosen):** the only option that gives `flatpak update`. We self-host the + static tree on unom-1 behind Caddy (Gitea has no flatpak/ostree registry); the build already + produces the repo, so the marginal cost is GPG signing + an rsync + a 10-line static container. +- **Generic registry bundle (kept as fallback):** one curl to publish, one `flatpak install + --bundle` to consume; mirrors the deb/rpm curl-upload pattern. No auto-update — this is what the + **Decky plugin** pulls (stable `latest/punktfunk-client.flatpak`), plus the offline/manual path. +- **Release attachment:** also done on tags, good for a human-facing download page. +- **Flathub (deferred):** best discoverability + zero hosting, but a separate submission/review + process and less control; revisit once the client is past scaffold quality. diff --git a/packaging/flatpak/server/Caddyfile b/packaging/flatpak/server/Caddyfile new file mode 100644 index 0000000..b56f78d --- /dev/null +++ b/packaging/flatpak/server/Caddyfile @@ -0,0 +1,15 @@ +# Inner Caddy (plain HTTP on :3230); the edge proxy on home-reverse-proxy-1 does TLS. +:3230 { + root * /srv + file_server browse + + # OSTree summary/refs change every publish — keep them fresh; objects are immutable. + @mutable path /repo/summary* /repo/refs/* + header @mutable Cache-Control "public, max-age=30" + @objects path /repo/objects/* /repo/deltas/* + header @objects Cache-Control "public, max-age=31536000, immutable" + + # Serve the install descriptors as text so browsers show them / flatpak fetches cleanly. + @descriptors path *.flatpakref *.flatpakrepo + header @descriptors Content-Type "text/plain; charset=utf-8" +} diff --git a/packaging/flatpak/server/compose.production.yml b/packaging/flatpak/server/compose.production.yml new file mode 100644 index 0000000..10750dc --- /dev/null +++ b/packaging/flatpak/server/compose.production.yml @@ -0,0 +1,14 @@ +# Static file server for the punktfunk Flatpak OSTree repo, on unom-1 (the DMZ services VM). +# Caddy on home-reverse-proxy-1 terminates TLS for flatpak.punktfunk.unom.io and reverse_proxies +# to 192.168.50.50:3230 (port must be in unom/infra caddy_target_ports). This inner Caddy just +# serves the bind-mounted ./site tree (repo/ + the .flatpakrepo/.flatpakref + index.html) over +# plain HTTP. CI rsyncs into ./site and runs `docker compose up -d` (idempotent). +services: + flatpak: + image: caddy:2-alpine + restart: unless-stopped + ports: + - "3230:3230" + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile:ro + - ./site:/srv:ro diff --git a/packaging/flatpak/unom-flatpak.gpg b/packaging/flatpak/unom-flatpak.gpg new file mode 100644 index 0000000000000000000000000000000000000000..f033154a31a0a64db7335a632227be8fc36bdf99 GIT binary patch literal 1158 zcmV;11bO?J0u2OeG+jIa5CEl$q9bEeb7eqxHWj;Z(PCIVer$}hNQ9REsTFH}4)3F2 zp7GqRMP|_+BS@_{fkq9^+UlI(wq=__p76T~cS(8jJwK-HBeCRD{swOCU%QlseBqe{ zxBGEPSLN|G7URuh=_;NbljfGdH-&)Qf#AWRPG_kH9~z15j@6eMZa8T(ofg~vXaujd zg-bAURR?A$zMFg&qU=6)5?PJ=Pr(!eu4I6BeIFJk&L}Uk%kMLk8-I#)HhxXo$V`;y7t&LbEm`P$#(Rl*7+bK3(9VMu?dEt4o= zIf>&ui67$9Yaf=nA7iD)KScjzFz9uj77j-Cu_xYY%07wwfmIm#S=`28)<+5ufmN9EzCEdB<0Gy1BN_U@?>uzJ9~ z6d&nB6KN$O3f({x-8u z+6lFjKm^x^lvb)~^QWe3PFxPsM=n5nXlI7z?xtC643*=a3Sn|pl6~oJWVqa7rLXMq z9RCw@YY)LEqsCq~_Z;D{^ccCZM`Y#UF~O%*`3$Hn;p7`0jH7 z*^j_ttO#H2_i9IOL7^L1?;;9fODfE0D%4yDUCExxdsKm<6b97~goZA%apg!}iTHs8 zzb4@HWSFqXsD)&&&N7HMrnk(B+kttrs4jFE7o5i=EM5O@gaQb=po&$Zt{yeGO^YK9 zwBm(5nZWRe{LM>Nk;Yg{q6k^ChZPuG9#gED17~J)U*?J0Ear7E@xYIPlPuvQW`qzUznWSn> Y{w!BVS?L13^bb#|8J!`&>Inbqj^A4%>Hq)$ literal 0 HcmV?d00001