feat(flatpak): publish a signed self-hosted OSTree remote on releases (#875)

* feat(flatpak): publish a signed self-hosted OSTree remote on releases (#785)

release.yml gains publish-flatpak-remote: it imports the per-arch
.flatpak bundles into an OSTree repo, signs commits and the summary
with a provisioned GPG key, regenerates static deltas and the
vocahq.flatpakrepo/index page, and pushes to VocaHQ/vocalinux-flatpak
served by GitHub Pages — the Homebrew-tap analogue, so installs
auto-update via flatpak update instead of per-release downloads.

The job is secret-gated like publish-aur (FLATPAK_GPG_PRIVATE_KEY +
FLATPAK_REPO_TOKEN) and skips itself while unconfigured. Prior repo
objects are pulled back in from gh-pages so clients get incremental
deltas. The manifest now declares branch: stable and the flatpakrepo
declares DefaultBranch=stable + RuntimeRepo for the Flathub GNOME
runtime.

Docs: new docs/FLATPAK_REMOTE.md (one-time setup, rotation,
verification); README, docs/INSTALL.md, packaging/flatpak/README.md
and the release notes now lead with the remote and keep release
bundles as the no-auto-update fallback. tests/test_release_integrity.py
pins the job's shape.

Closes #785

* fix(flatpak-remote): publish a .flatpakref and gate the remote to stable tags

- needs build-and-release so the remote cannot lead the GitHub Release
- stable tags only: prereleases must not reach `flatpak update`
- probe gh-pages before the empty-repo fallback: a transient checkout
  failure no longer discards remote history
- publish com.vocalinux.Vocalinux.flatpakref (RuntimeRepo lives there, not
  on .flatpakrepo) plus vocahq.gpg, so clean installs resolve the GNOME
  runtime and key rotation works via remote-modify --gpg-import
- gate the release-notes remote instructions on the publish actually
  running; the bundle path stays primary otherwise
- document rotation via remote-modify --gpg-import / remote-delete
  --force and the explicit Flathub setup step

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
This commit is contained in:
devin-ai-integration[bot]
2026-09-29 22:45:49 -07:00
committed by GitHub
co-authored by Jatin K Malik
parent d29c884782
commit c9ea6c7de7
9 changed files with 606 additions and 33 deletions
+286 -14
View File
@@ -14,6 +14,12 @@ permissions:
env:
# Build against the supported floor (requires-python in pyproject.toml).
PYTHON_VERSION: '3.11'
# The dedicated "tap" repo and the URL Pages serves it at. A custom
# domain (e.g. flatpak.vocalinux.com) overrides both via the vars.
FLATPAK_REMOTE_REPO: ${{ vars.FLATPAK_REMOTE_REPO || 'VocaHQ/vocalinux-flatpak' }}
FLATPAK_REMOTE_URL: ${{ vars.FLATPAK_REMOTE_URL || 'https://vocahq.github.io/vocalinux-flatpak/repo' }}
FLATPAKREPO_URL: ${{ vars.FLATPAKREPO_URL || 'https://vocahq.github.io/vocalinux-flatpak/vocahq.flatpakrepo' }}
FLATPAKREF_URL: ${{ vars.FLATPAKREF_URL || 'https://vocahq.github.io/vocalinux-flatpak/com.vocalinux.Vocalinux.flatpakref' }}
jobs:
build-and-release:
@@ -107,6 +113,88 @@ jobs:
if-no-files-found: error
retention-days: 7
# The remote install commands only belong in the notes when this tag
# will actually publish the remote — the same two gates
# publish-flatpak-remote applies: a stable tag and both secrets
# configured. Otherwise the notes would send users to a remote that
# never carries this release, so they get the bundle path alone.
- name: Compose the Flatpak install notes
id: flatpak_notes
env:
FLATPAK_GPG_PRIVATE_KEY: ${{ secrets.FLATPAK_GPG_PRIVATE_KEY }}
FLATPAK_REPO_TOKEN: ${{ secrets.FLATPAK_REPO_TOKEN }}
VERSION: ${{ steps.get_version.outputs.VERSION }}
run: |
set -euo pipefail
notes="$RUNNER_TEMP/flatpak-notes.md"
case "$GITHUB_REF_NAME" in
*alpha*|*beta*|*rc*) prerelease=true ;;
*) prerelease=false ;;
esac
if [ "$prerelease" = false ] && [ -n "$FLATPAK_GPG_PRIVATE_KEY" ] && [ -n "$FLATPAK_REPO_TOKEN" ]; then
cat >"$notes" <<'EOF'
### Flatpak
Install and auto-update via the self-hosted VocaHQ remote (`flatpak update`
picks up each release):
```bash
flatpak install __FLATPAKREF_URL__
```
The `.flatpakref` adds the `vocahq` remote and resolves `org.gnome.Platform//50`
through Flathub itself — no separate Flathub setup needed. To add the
pieces by hand instead:
```bash
flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
flatpak remote-add --if-not-exists vocahq __FLATPAKREPO_URL__
flatpak install vocahq com.vocalinux.Vocalinux
```
Or download the `.flatpak` matching your CPU (`x86_64` or `aarch64`, attached
shortly after this release is published) — a bundle with **no auto-update**:
```bash
# once: Flathub remote + GNOME runtime (the app itself is not on Flathub)
flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
flatpak install flathub org.gnome.Platform//50
flatpak install --user ./Vocalinux-__VERSION__-x86_64.flatpak
flatpak run com.vocalinux.Vocalinux
```
EOF
else
cat >"$notes" <<'EOF'
### Flatpak
Download the `.flatpak` matching your CPU (`x86_64` or `aarch64`, attached
shortly after this release is published) — a bundle with **no auto-update**:
```bash
# once: Flathub remote + GNOME runtime (the app itself is not on Flathub)
flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
flatpak install flathub org.gnome.Platform//50
flatpak install --user ./Vocalinux-__VERSION__-x86_64.flatpak
flatpak run com.vocalinux.Vocalinux
```
EOF
if [ "$prerelease" = true ]; then
cat >>"$notes" <<'EOF'
Prereleases are not pushed to the `vocahq` auto-update remote — `flatpak update`
there stays on the latest stable release.
EOF
fi
fi
cat >>"$notes" <<'EOF'
Whisper.cpp engine only. Hotkeys need device access as documented in
packaging/flatpak/README.md. Not on Flathub (https://github.com/flathub/flathub/pull/9368).
EOF
sed -i \
-e "s|__VERSION__|$VERSION|g" \
-e "s|__FLATPAKREPO_URL__|$FLATPAKREPO_URL|g" \
-e "s|__FLATPAKREF_URL__|$FLATPAKREF_URL|g" \
"$notes"
{
echo 'flatpak<<VOCA_NOTES_EOF'
cat "$notes"
echo 'VOCA_NOTES_EOF'
} >> "$GITHUB_OUTPUT"
- name: Create Release
id: create_release
uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
@@ -161,20 +249,7 @@ jobs:
```
Still requires `xdotool`/`wtype`/`ydotool` on the host for text injection, same as the PyPI path.
### Flatpak
Download the `.flatpak` matching your CPU (`x86_64` or `aarch64`, attached shortly
after this release is published). The app is **not on Flathub**; these are GitHub
Release bundles with **no auto-update**.
```bash
# once: Flathub remote + GNOME runtime (the app itself is not on Flathub)
flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
flatpak install flathub org.gnome.Platform//50
flatpak install --user ./Vocalinux-${{ steps.get_version.outputs.VERSION }}-x86_64.flatpak
flatpak run com.vocalinux.Vocalinux
```
Whisper.cpp engine only. Hotkeys need device access as documented in
packaging/flatpak/README.md. Not on Flathub (https://github.com/flathub/flathub/pull/9368).
${{ steps.flatpak_notes.outputs.flatpak }}
### Snap
Tagged releases also attach `vocalinux_${{ steps.get_version.outputs.VERSION }}_amd64.snap`
@@ -337,6 +412,203 @@ jobs:
if-no-files-found: error
retention-days: 7
# Self-hosted OSTree remote (#785), the Homebrew-tap analogue: the release
# bundles are imported into a signed repo and pushed to
# VocaHQ/vocalinux-flatpak, which GitHub Pages serves at FLATPAK_REMOTE_URL.
# Installs then auto-update via `flatpak update`. One-time setup — repo,
# Pages, signing key, PAT — is documented in docs/FLATPAK_REMOTE.md.
# build-and-release is a hard requirement: the remote is the update channel
# for the GitHub Release, so it must not publish when release creation
# failed. Stable tags only — a prerelease on `stable` would land in every
# user's `flatpak update`.
publish-flatpak-remote:
name: Publish Flatpak remote
needs: [build-and-release, build-flatpak-amd64, build-flatpak-arm64]
if: ${{ !contains(github.ref, 'alpha') && !contains(github.ref, 'beta') && !contains(github.ref, 'rc') }}
runs-on: ubuntu-latest
permissions:
contents: read
concurrency:
# Two tags publishing at once would race each other's gh-pages push.
group: flatpak-remote
steps:
# Same pattern as publish-aur: an unconfigured remote skips the publish
# instead of failing the release.
- name: Check remote credentials
id: creds
env:
FLATPAK_GPG_PRIVATE_KEY: ${{ secrets.FLATPAK_GPG_PRIVATE_KEY }}
FLATPAK_REPO_TOKEN: ${{ secrets.FLATPAK_REPO_TOKEN }}
run: |
if [ -z "$FLATPAK_GPG_PRIVATE_KEY" ] || [ -z "$FLATPAK_REPO_TOKEN" ]; then
echo "configured=false" >> "$GITHUB_OUTPUT"
echo "FLATPAK_GPG_PRIVATE_KEY and/or FLATPAK_REPO_TOKEN not set; skipping remote publish (docs/FLATPAK_REMOTE.md)"
else
echo "configured=true" >> "$GITHUB_OUTPUT"
fi
- name: Checkout
if: steps.creds.outputs.configured == 'true'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install repo tooling
if: steps.creds.outputs.configured == 'true'
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends flatpak ostree
- name: Download the Flatpak bundles
if: steps.creds.outputs.configured == 'true'
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: flatpak-*
merge-multiple: true
path: dist
# The import below falls back to `ostree init` when site/repo is absent,
# which is only safe on a confirmed first publish: a transient checkout
# failure taken as "no history" would publish an empty repo over the
# remote and drop every object and delta clients still need. Probe
# gh-pages directly — exit 2 means confirmed absent; anything else
# aborts instead of guessing.
- name: Check whether the remote was published before
id: published
if: steps.creds.outputs.configured == 'true'
env:
FLATPAK_REPO_TOKEN: ${{ secrets.FLATPAK_REPO_TOKEN }}
run: |
set -u
rc=0
git ls-remote --exit-code --heads \
"https://x-access-token:${FLATPAK_REPO_TOKEN}@github.com/${FLATPAK_REMOTE_REPO}.git" \
gh-pages >/dev/null 2>&1 || rc=$?
case $rc in
0) echo "published=true" >> "$GITHUB_OUTPUT" ;;
2) echo "published=false" >> "$GITHUB_OUTPUT" ;;
*)
echo "::error::Could not check ${FLATPAK_REMOTE_REPO} for gh-pages (git ls-remote exited $rc). Does the repo exist (docs/FLATPAK_REMOTE.md)?"
exit "$rc"
;;
esac
- name: Fetch the previously published repo
if: steps.creds.outputs.configured == 'true' && steps.published.outputs.published == 'true'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: ${{ env.FLATPAK_REMOTE_REPO }}
ref: gh-pages
token: ${{ secrets.FLATPAK_REPO_TOKEN }}
path: site
- name: Import the signing key
if: steps.creds.outputs.configured == 'true'
env:
FLATPAK_GPG_PRIVATE_KEY: ${{ secrets.FLATPAK_GPG_PRIVATE_KEY }}
FLATPAK_GPG_PASSPHRASE: ${{ secrets.FLATPAK_GPG_PASSPHRASE }}
run: |
set -euo pipefail
export GNUPGHOME="$RUNNER_TEMP/gnupg"
mkdir -m 700 "$GNUPGHOME"
gpg --batch --import <<<"$FLATPAK_GPG_PRIVATE_KEY"
KEYID=$(gpg --batch --with-colons --list-secret-keys | awk -F: '$1 == "sec" {print $5; exit}')
KEYGRIP=$(gpg --batch --with-colons --with-keygrip --list-secret-keys | awk -F: '$1 == "grp" {print $10; exit}')
if [ -n "${FLATPAK_GPG_PASSPHRASE:-}" ]; then
# libostree signs through gpg-agent, so the passphrase has to be
# preset on the keygrip — there is no --passphrase-fd to reach for.
PRESET=$(command -v gpg-preset-passphrase || find /usr/lib* /usr/libexec -name gpg-preset-passphrase 2>/dev/null | head -1 || true)
if [ -z "$PRESET" ]; then
echo "::error::FLATPAK_GPG_PASSPHRASE is set but gpg-preset-passphrase is unavailable; import a passphrase-less key instead"
exit 1
fi
echo "allow-preset-passphrase" >> "$GNUPGHOME/gpg-agent.conf"
gpgconf --kill gpg-agent || true
gpg-agent --daemon
printf '%s' "$FLATPAK_GPG_PASSPHRASE" | "$PRESET" --preset "$KEYGRIP"
fi
gpg --batch --export "$KEYID" > "$RUNNER_TEMP/pubkey.gpg"
echo "GNUPGHOME=$GNUPGHOME" >> "$GITHUB_ENV"
echo "KEYID=$KEYID" >> "$GITHUB_ENV"
# site/repo carries the prior objects back in, so clients pull deltas
# per release rather than re-downloading the app.
- name: Import bundles and sign the repo
if: steps.creds.outputs.configured == 'true'
run: |
set -euo pipefail
repo="$PWD/publish/repo"
if [ -d site/repo/objects ]; then
mkdir -p publish
cp -a site/repo "$repo"
else
mkdir -p "$repo"
ostree --repo="$repo" init --mode=archive-z2
fi
ls -la dist/*.flatpak
for bundle in dist/*.flatpak; do
flatpak build-import-bundle \
--gpg-sign="$KEYID" --gpg-homedir="$GNUPGHOME" \
--update-appstream --no-update-summary \
"$repo" "$bundle"
done
flatpak build-update-repo \
--gpg-sign="$KEYID" --gpg-homedir="$GNUPGHOME" \
--gpg-import="$RUNNER_TEMP/pubkey.gpg" \
--default-branch=stable \
--generate-static-deltas \
"$repo"
# RuntimeRepo lives on the .flatpakref — it is a "Flatpak Ref" key, not
# a "Flatpak Repo" key, so on the .flatpakrepo it was a silent no-op and
# clean installs without Flathub could not resolve the GNOME runtime.
# Installing the .flatpakref adds the remote and pulls the runtime in
# one command. The bare public key ships for remote-modify key rotation.
- name: Write the .flatpakrepo, .flatpakref, and index page
if: steps.creds.outputs.configured == 'true'
run: |
set -euo pipefail
touch publish/.nojekyll
{
echo '[Flatpak Repo]'
echo 'Title=VocaHQ'
echo "Url=${FLATPAK_REMOTE_URL}"
echo 'Homepage=https://vocalinux.com'
echo 'Comment=Official Vocalinux Flatpak remote'
echo 'Description=Self-hosted Vocalinux Flatpak repository'
echo 'Icon=https://vocalinux.com/icon-512x512.png'
echo 'DefaultBranch=stable'
echo "GPGKey=$(base64 -w0 "$RUNNER_TEMP/pubkey.gpg")"
} > publish/vocahq.flatpakrepo
{
echo '[Flatpak Ref]'
echo 'Version=1'
echo 'Title=Vocalinux'
echo 'Name=com.vocalinux.Vocalinux'
echo 'Branch=stable'
echo "Url=${FLATPAK_REMOTE_URL}"
echo 'SuggestRemoteName=vocahq'
echo 'RuntimeRepo=https://dl.flathub.org/repo/flathub.flatpakrepo'
echo 'IsRuntime=false'
echo 'Homepage=https://vocalinux.com'
echo 'Icon=https://vocalinux.com/icon-512x512.png'
echo "GPGKey=$(base64 -w0 "$RUNNER_TEMP/pubkey.gpg")"
} > publish/com.vocalinux.Vocalinux.flatpakref
cp "$RUNNER_TEMP/pubkey.gpg" publish/vocahq.gpg
grep -v GPGKey publish/vocahq.flatpakrepo
sed -e "s|__FLATPAKREPO_URL__|${FLATPAKREPO_URL}|g" \
-e "s|__FLATPAKREF_URL__|${FLATPAKREF_URL}|g" \
-e "s|__REMOTE_URL__|${FLATPAK_REMOTE_URL}|g" \
packaging/flatpak/index.html > publish/index.html
- name: Publish the remote
if: steps.creds.outputs.configured == 'true'
uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453 # v4
with:
personal_token: ${{ secrets.FLATPAK_REPO_TOKEN }}
external_repository: ${{ env.FLATPAK_REMOTE_REPO }}
publish_dir: ./publish
publish_branch: gh-pages
commit_message: 'Vocalinux ${{ github.ref_name }}'
attach-flatpak:
name: Attach Flatpak bundles
needs: [build-and-release, build-flatpak-amd64, build-flatpak-arm64]
+16 -2
View File
@@ -166,13 +166,27 @@ Download the `x86_64` or `aarch64` AppImage from [Releases](https://github.com/V
### Flatpak (any distro)
GitHub Releases attach `Vocalinux-<version>-x86_64.flatpak` and `Vocalinux-<version>-aarch64.flatpak`. After the Flathub GNOME runtime is present:
Install and auto-update via the self-hosted VocaHQ remote (`flatpak update` picks up each release):
```bash
flatpak install https://vocahq.github.io/vocalinux-flatpak/com.vocalinux.Vocalinux.flatpakref
```
The `.flatpakref` adds the `vocahq` remote and resolves the GNOME runtime through Flathub. Manually instead:
```bash
flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
flatpak remote-add --if-not-exists vocahq https://vocahq.github.io/vocalinux-flatpak/vocahq.flatpakrepo
flatpak install vocahq com.vocalinux.Vocalinux
```
Or sideload a GitHub Release bundle (`Vocalinux-<version>-x86_64.flatpak` or `-aarch64.flatpak`) after the Flathub GNOME runtime is present — bundles do not auto-update:
```bash
flatpak install --user ./Vocalinux-<version>-x86_64.flatpak
```
Bundles do not auto-update. Local build:
Local build:
```bash
flatpak install flathub org.gnome.Platform//50 org.gnome.Sdk//50
+140
View File
@@ -0,0 +1,140 @@
# Self-hosted Flatpak remote
Vocalinux publishes a signed OSTree Flatpak remote on every stable `v*` tag —
the Homebrew-tap analogue to `VocaHQ/homebrew-vocamac`
([issue #785](https://github.com/VocaHQ/vocalinux/issues/785)). The remote
lives in the dedicated repo `VocaHQ/vocalinux-flatpak`, served by GitHub
Pages:
```bash
flatpak install https://vocahq.github.io/vocalinux-flatpak/com.vocalinux.Vocalinux.flatpakref
flatpak update # picks up each new release
```
The `.flatpakref` adds the `vocahq` remote (via `SuggestRemoteName=`) and lets
flatpak resolve `org.gnome.Platform//50` through its `RuntimeRepo=` pointer to
Flathub — one command, no separate Flathub setup. The equivalent manual
steps:
```bash
flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
flatpak remote-add --if-not-exists vocahq https://vocahq.github.io/vocalinux-flatpak/vocahq.flatpakrepo
flatpak install vocahq com.vocalinux.Vocalinux
```
Only the app is ours; the GNOME runtime still comes from Flathub —
`RuntimeRepo=` is a `.flatpakref` key (it is ignored on `.flatpakrepo`, which
is why the app ref carries it, not the remote descriptor). GitHub Release
`.flatpak` bundles ([#784](https://github.com/VocaHQ/vocalinux/issues/784))
remain as a no-auto-update fallback, and are the only Flatpak path for
prerelease tags, which never reach the remote.
## Maintainer setup (one time)
1. **Repo** — create `VocaHQ/vocalinux-flatpak` (public).
2. **Pages** — Settings → Pages → "Deploy from a branch" → branch `gh-pages`,
folder `/ (root)`. The first publish creates the branch, so Pages can be
pointed at it afterwards; creating an empty `gh-pages` upfront works too.
3. **Signing key** — OSTree remotes sign the summary and commits with GPG:
```bash
gpg --quick-gen-key "Vocalinux Flatpak <flatpak@vocahq.com>" ed25519 sign never
gpg --list-secret-keys # note the KEYID
gpg --export-secret-keys --armor KEYID # -> FLATPAK_GPG_PRIVATE_KEY
gpg --gen-revoke KEYID > flatpak-remote-revoke.asc # store it somewhere safe
```
A passphrase-less key keeps CI simplest — libostree signs through
gpg-agent, which has no `--passphrase-fd` to reach for. A passphrase works
too: set `FLATPAK_GPG_PASSPHRASE` and the workflow presets it onto the
keygrip via `gpg-preset-passphrase`.
4. **Secrets on `VocaHQ/vocalinux`** (Settings → Secrets and variables →
Actions):
- `FLATPAK_GPG_PRIVATE_KEY` — the armored private key
- `FLATPAK_GPG_PASSPHRASE` — optional
- `FLATPAK_REPO_TOKEN` — fine-grained PAT scoped to
`VocaHQ/vocalinux-flatpak` with Contents: read and write
5. **Variables (optional)** — same settings page, Variables tab:
- `FLATPAK_REMOTE_REPO` — publish target (default
`VocaHQ/vocalinux-flatpak`)
- `FLATPAK_REMOTE_URL` — remote URL baked into the `.flatpakrepo`
(default `https://vocahq.github.io/vocalinux-flatpak/repo`; set
e.g. `https://flatpak.vocalinux.com/repo` for a custom domain)
- `FLATPAKREPO_URL` — where users fetch `vocahq.flatpakrepo` (default
`https://vocahq.github.io/vocalinux-flatpak/vocahq.flatpakrepo`)
- `FLATPAKREF_URL` — where users fetch the app `.flatpakref` (default
`https://vocahq.github.io/vocalinux-flatpak/com.vocalinux.Vocalinux.flatpakref`)
6. **Trigger** — push a stable `v*` tag (alpha/beta/rc tags do not publish to
the remote), or re-run `publish-flatpak-remote` on an existing release run
after configuring the secrets.
Without the secrets the job skips itself (same pattern as `publish-aur`), so
an unconfigured remote never fails a release.
## How a publish works
`release.yml → publish-flatpak-remote` runs on stable tags only, after the
GitHub Release exists and the two arch builds finish:
1. Downloads `Vocalinux-<version>-{x86_64,aarch64}.flatpak` from the build
jobs.
2. Probes the remote repo for an existing `gh-pages` branch; only a confirmed
first publish may skip the history checkout — a transient failure aborts
instead of seeding an empty repo over the live remote. It then checks out
`gh-pages` and copies `repo/` over, so existing objects and deltas
survive — clients pull an incremental delta each release, not the whole
app.
3. `flatpak build-import-bundle --gpg-sign` imports each bundle under the
signing key.
4. `flatpak build-update-repo --generate-static-deltas --gpg-sign
--gpg-import` rebuilds the summary, signs it, and generates static deltas.
5. Regenerates `vocahq.flatpakrepo` (URL + base64 public key),
`com.vocalinux.Vocalinux.flatpakref` (the one-command app ref carrying the
`RuntimeRepo=` pointer to Flathub), `vocahq.gpg` (the bare public key, for
the key-rotation path below), and the index page from
`packaging/flatpak/index.html`.
6. Publishes the directory back to `gh-pages` via
`peaceiris/actions-gh-pages`.
## Verifying a publish
```bash
flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
flatpak remote-add --if-not-exists vocahq https://vocahq.github.io/vocalinux-flatpak/vocahq.flatpakrepo
flatpak remote-info vocahq com.vocalinux.Vocalinux # signed commit, both arches
flatpak install vocahq com.vocalinux.Vocalinux
flatpak run com.vocalinux.Vocalinux
# after the next tag: flatpak update pulls the delta
```
## Key rotation and loss
Generate a fresh key, replace `FLATPAK_GPG_PRIVATE_KEY` (and
`FLATPAK_GPG_PASSPHRASE` if used), then re-run `publish-flatpak-remote`. The
published `.flatpakrepo`/`.flatpakref` then carry the new `GPGKey=`, so new
installs trust it immediately.
Existing installs must import the new key into the remote's trusted keyring —
re-running `remote-add` does **not** replace it (with `--if-not-exists` it
does nothing at all), and `flatpak update` fails verification until the key
is updated. Point users at the public key published beside the repo:
```bash
curl -fsSL https://vocahq.github.io/vocalinux-flatpak/vocahq.gpg -o /tmp/vocahq.gpg
flatpak remote-modify --gpg-import=/tmp/vocahq.gpg vocahq
flatpak update
```
`remote-modify --gpg-import` adds the key — the old one stays trusted, which
is what makes a routine rotation seamless. To *revoke* a leaked key, drop the
remote's keyring instead: delete and re-add the remote (installed apps keep
their `vocahq` origin and update normally afterwards):
```bash
flatpak remote-delete --force vocahq # --force: the remote is still in use
flatpak remote-add vocahq https://vocahq.github.io/vocalinux-flatpak/vocahq.flatpakrepo
flatpak update
```
Either way is worth a line in that release's notes. The revoke certificate is
for the day the key leaks, not for routine rotation.
+15 -3
View File
@@ -101,14 +101,26 @@ See [AUR.md](AUR.md).
## Flatpak
GitHub Releases attach `Vocalinux-<version>-x86_64.flatpak` and `Vocalinux-<version>-aarch64.flatpak`. After the Flathub GNOME runtime is present:
Install and auto-update via the self-hosted VocaHQ remote (`flatpak update` picks up each release):
```bash
flatpak install https://vocahq.github.io/vocalinux-flatpak/com.vocalinux.Vocalinux.flatpakref
```
The `.flatpakref` adds the `vocahq` remote and resolves the GNOME runtime through Flathub. Manually instead:
```bash
flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
flatpak remote-add --if-not-exists vocahq https://vocahq.github.io/vocalinux-flatpak/vocahq.flatpakrepo
flatpak install vocahq com.vocalinux.Vocalinux
```
Or sideload a GitHub Release bundle (`Vocalinux-<version>-x86_64.flatpak` or `-aarch64.flatpak`) after the Flathub GNOME runtime is present — bundles do not auto-update:
```bash
flatpak install --user ./Vocalinux-<version>-x86_64.flatpak
```
Bundles do not auto-update.
Local build (contributors):
```bash
+9 -3
View File
@@ -389,11 +389,15 @@ After pushing the tag, the GitHub Actions workflow will automatically:
so a partial manifest never gets published)
5. Publish to PyPI via trusted publishing
6. Publish the AUR package (when the `AUR_SSH_PRIVATE_KEY` secret is configured)
7. Deploy the website to vocalinux.com
8. Build the amd64 snap, attach it to the GitHub Release, and try Snap Store
7. Publish the signed self-hosted Flatpak remote to `VocaHQ/vocalinux-flatpak`
(stable tags only, once the GitHub Release exists, when
`FLATPAK_GPG_PRIVATE_KEY` + `FLATPAK_REPO_TOKEN` are configured — see
docs/FLATPAK_REMOTE.md)
8. Deploy the website to vocalinux.com
9. Build the amd64 snap, attach it to the GitHub Release, and try Snap Store
`edge`/`candidate`. Store human review (for example `uinput` allow-installation)
must not block the GitHub `.snap`. `stable` is still a manual promote
9. Mark as pre-release if version contains alpha/beta/rc
10. Mark as pre-release if version contains alpha/beta/rc
Monitor at: https://github.com/VocaHQ/vocalinux/actions
@@ -409,6 +413,8 @@ Monitor at: https://github.com/VocaHQ/vocalinux/actions
- [ ] Verify PyPI package was published (if applicable), and that its wheel sha256
matches the line for that wheel in `SHA256SUMS`
- [ ] Verify website was deployed (check vocalinux.com)
- [ ] If the Flatpak remote is configured, verify it picked up the tag:
`flatpak remote-info vocahq com.vocalinux.Vocalinux` (docs/FLATPAK_REMOTE.md)
- [ ] Announce on social media/communities
- [ ] Update any pinned issues or discussions
+29 -10
View File
@@ -2,10 +2,28 @@
Manifest and AppStream metadata for building Vocalinux as a Flatpak.
## VocaHQ remote (auto-update)
The recommended install is the self-hosted VocaHQ OSTree remote — the
Homebrew-tap analogue — which `release.yml` signs and publishes to
`VocaHQ/vocalinux-flatpak` (GitHub Pages) on every stable `v*` tag:
```bash
flatpak install https://vocahq.github.io/vocalinux-flatpak/com.vocalinux.Vocalinux.flatpakref
flatpak update # each new release arrives here
```
The `.flatpakref` adds the `vocahq` remote and resolves the GNOME runtime
through Flathub via `RuntimeRepo=` — the manual equivalent is `remote-add`
for `flathub` then `vocahq`, then `flatpak install vocahq
com.vocalinux.Vocalinux`. Only the app is ours; `org.gnome.Platform//50`
still comes from Flathub. One-time repo/key setup:
[docs/FLATPAK_REMOTE.md](../../docs/FLATPAK_REMOTE.md).
## GitHub Release bundles
Each `v*` GitHub Release attaches `Vocalinux-<version>-x86_64.flatpak` and
`Vocalinux-<version>-aarch64.flatpak`. That is the easy install path:
`Vocalinux-<version>-aarch64.flatpak` as a no-remote fallback:
```bash
# once: Flathub remote + GNOME runtime (the app itself is not on Flathub)
@@ -16,11 +34,11 @@ flatpak install --user ./Vocalinux-<version>-x86_64.flatpak
flatpak run com.vocalinux.Vocalinux
```
Bundles do **not** auto-update. Vocalinux is **not on Flathub** (submission
Bundles do **not** auto-update — the VocaHQ remote above is the path for
that. Vocalinux is **not on Flathub** (submission
[flathub/flathub#9368](https://github.com/flathub/flathub/pull/9368) closed on
policy grounds; we are not re-submitting). A self-hosted VocaHQ remote is the
long-term auto-update path. Until then, download a new bundle from the next
release. Local `flatpak-builder` remains for contributors (below).
policy grounds; we are not re-submitting). Local `flatpak-builder` remains for
contributors (below).
## Current Scope
@@ -109,15 +127,16 @@ symlinking of whisper.cpp's shared libraries onto the loader path.
`python3-build-dependencies.yaml` is a small hand-maintained helper so
`--no-build-isolation` builds can import `mesonpy` before NumPy is built.
## Channel: GitHub Releases, not Flathub
## Channel: VocaHQ remote + GitHub Releases, not Flathub
Flathub is **not pursued**. The submission,
[flathub/flathub#9368](https://github.com/flathub/flathub/pull/9368), was closed
on 2026-07-23 on policy grounds. See
[#167](https://github.com/VocaHQ/vocalinux/issues/167) for the channel decision
and [#784](https://github.com/VocaHQ/vocalinux/issues/784) for release bundles.
Do not re-submit. Local builds and GitHub Release `.flatpak` assets are the
supported paths.
[#167](https://github.com/VocaHQ/vocalinux/issues/167) for the channel decision,
[#784](https://github.com/VocaHQ/vocalinux/issues/784) for release bundles, and
[#785](https://github.com/VocaHQ/vocalinux/issues/785) for the self-hosted
remote. Do not re-submit. The `vocahq` remote, local builds, and GitHub
Release `.flatpak` assets are the supported paths.
## Manifest Details
@@ -1,4 +1,5 @@
app-id: com.vocalinux.Vocalinux
branch: stable
runtime: org.gnome.Platform
runtime-version: "50"
sdk: org.gnome.Sdk
+52
View File
@@ -0,0 +1,52 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>VocaHQ Flatpak remote</title>
<style>
body {
max-width: 42rem;
margin: 3rem auto;
padding: 0 1rem;
font-family: system-ui, sans-serif;
line-height: 1.6;
}
code,
pre {
background: #f2f2f2;
border-radius: 4px;
}
pre {
padding: 1rem;
overflow-x: auto;
}
</style>
</head>
<body>
<h1>VocaHQ Flatpak remote</h1>
<p>
Self-hosted, signed OSTree repository for
<a href="https://vocalinux.com">Vocalinux</a> — published on every stable
tagged release, so installs auto-update via <code>flatpak update</code>.
</p>
<pre><code>flatpak install __FLATPAKREF_URL__</code></pre>
<p>
The <code>.flatpakref</code> adds the <code>vocahq</code> remote and
resolves the GNOME runtime through Flathub in one step. To wire it up by
hand instead:
</p>
<pre><code>flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
flatpak remote-add --if-not-exists vocahq __FLATPAKREPO_URL__
flatpak install vocahq com.vocalinux.Vocalinux</code></pre>
<p>
The GNOME runtime still comes from Flathub — the
<code>.flatpakref</code> declares <code>RuntimeRepo</code> so flatpak
finds it there. Vocalinux itself is not on Flathub.
</p>
<p>
<a href="https://github.com/VocaHQ/vocalinux">Source code</a> ·
<a href="__REMOTE_URL__">Repository index</a>
</p>
</body>
</html>
+58 -1
View File
@@ -234,4 +234,61 @@ def test_the_release_notes_document_flatpak_bundles():
assert "org.gnome.Platform//50" in body
assert "no auto-update" in body
assert "not on Flathub" in body
assert "Vocalinux-${{ steps.get_version.outputs.VERSION }}-x86_64.flatpak" in body
assert "Vocalinux-__VERSION__-x86_64.flatpak" in body
def test_the_remote_install_guidance_only_runs_when_the_remote_publishes():
"""The notes recommend `flatpak install` of the `.flatpakref` only for a
tag the remote will actually carry: stable, with both publish secrets
configured — the same gates publish-flatpak-remote applies. Otherwise the
prominent command would point at a remote that never gets this release."""
body = _jobs()["build-and-release"]
assert (
"steps.flatpak_notes.outputs.flatpak" in body
), "the Flatpak notes must come from the conditional step, not inline text"
for secret in ("FLATPAK_GPG_PRIVATE_KEY", "FLATPAK_REPO_TOKEN"):
assert f"secrets.{secret}" in body, f"the notes do not gate on {secret}"
assert "*alpha*|*beta*|*rc*" in body, "the notes do not detect prerelease tags"
assert "__FLATPAKREF_URL__" in body, "the notes do not publish the .flatpakref path"
# Clean installs without Flathub cannot resolve the GNOME runtime from the
# remote alone: the manual path has to add flathub first.
assert "remote-add --if-not-exists flathub" in body
def test_the_flatpak_remote_publishes_signed_and_secret_gated():
"""#785's self-hosted OSTree remote signs with a provisioned GPG key and
pushes to VocaHQ/vocalinux-flatpak, never to the release — and it skips
itself instead of failing the release while secrets are unconfigured.
"""
block = _jobs()["publish-flatpak-remote"]
assert _needs(block) >= {
"build-and-release",
"build-flatpak-amd64",
"build-flatpak-arm64",
}, "the remote must not publish bundles still building or lead the GitHub Release"
for secret in ("FLATPAK_GPG_PRIVATE_KEY", "FLATPAK_REPO_TOKEN"):
assert f"secrets.{secret}" in block, f"{secret} is not wired into the job"
assert "build-import-bundle" in block
assert "--gpg-sign" in block, "the remote must sign what it publishes"
assert "--generate-static-deltas" in block, "updates would re-download whole apps"
assert "external_repository" in block, "the tap repo gets no push without it"
assert "gh release upload" not in block, "the remote is not a release asset"
assert _permissions(block).get("id-token") != "write"
def test_the_remote_publish_is_stable_gated_and_keeps_history():
"""A prerelease tag must not reach `flatpak update` users, and a transient
checkout failure must not be mistaken for a first publish — the empty-repo
path would drop the objects clients still delta from."""
block = _jobs()["publish-flatpak-remote"]
job_if = re.search(r"^ if: (.+)$", block, re.M)
assert job_if, "the job is not gated to stable tags"
for marker in ("alpha", "beta", "rc"):
assert f"contains(github.ref, '{marker}')" in job_if.group(1)
assert (
"ls-remote --exit-code" in block
), "no explicit check decides whether the remote was published before"
assert (
"continue-on-error" not in block
), "a tolerated checkout failure falls through to the empty-repo path"
assert ".flatpakref" in block, "no app ref is written for one-command installs"