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:
co-authored by
Jatin K Malik
parent
d29c884782
commit
c9ea6c7de7
+286
-14
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user