feat(release): attach Flatpak bundles to v* GitHub Releases (#786)

* feat(release): attach Flatpak bundles to v* GitHub Releases

On each version tag, build x86_64 and aarch64 .flatpak assets with the
same builder pins as flatpak.yml, upload them as workflow artifacts, and
attach Vocalinux-${VERSION}-{arch}.flatpak next to the AppImages.
SHA256SUMS now checksums *.flatpak as well.

The Flathub builder image has no GitHub CLI, so attach is a follow-up
job on ubuntu-latest using gh release upload --clobber. Snap jobs are
unchanged.

Fixes #784

* fix(ci): pin privileged Flatpak builder to an immutable digest

The gnome-50 tag is mutable; both release Flatpak jobs and flatpak.yml
now pull the image by OCI index digest. The integrity test requires
@sha256 rather than the tag alone.
This commit is contained in:
Jatin K Malik
2026-09-05 20:10:13 -07:00
committed by GitHub
parent 50ea1109d7
commit 16175d9ea3
5 changed files with 237 additions and 38 deletions
+2 -1
View File
@@ -47,7 +47,8 @@ jobs:
matrix:
include: ${{ fromJSON(needs.matrix.outputs.include) }}
container:
image: ghcr.io/flathub-infra/flatpak-github-actions:gnome-50
# Privileged builder: pin the OCI index, not the mutable gnome-50 tag.
image: ghcr.io/flathub-infra/flatpak-github-actions:gnome-50@sha256:1fb2df10a57276f90806e1f35454048e30bf1855b7b4ff4808c9ee55887bd852
options: --privileged
steps:
- name: Checkout
+127 -8
View File
@@ -161,10 +161,25 @@ 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).
### Verifying what you downloaded
`SHA256SUMS` covers every file here and is attached once the aarch64 build finishes,
a few minutes after this release appears. The wheel and sdist below are the same bytes
published to PyPI.
`SHA256SUMS` covers every file here and is attached once the aarch64 AppImage and
both Flatpak builds finish, a few minutes after this release appears. The wheel and
sdist below are the same bytes published to PyPI.
```bash
sha256sum -c --ignore-missing SHA256SUMS
```
@@ -234,12 +249,116 @@ jobs:
--repo ${{ github.repository }} \
--clobber
# Runs last because the aarch64 AppImage lands after the release is created:
# a checksum manifest that covers three of the four files is worse than none,
# since the missing one looks like the tampered one.
# Pins match .github/workflows/flatpak.yml. These jobs start with the tag
# (no needs) so they overlap the wheel / x86_64 AppImage job. The builder
# image is Freedesktop SDK, not a GitHub-hosted runner: no `gh`, so attach
# is a follow-up job on ubuntu-latest, same `gh release upload --clobber`
# as the aarch64 AppImage. The image is digest-pinned: it runs privileged.
build-flatpak-amd64:
name: Build Flatpak (x86_64)
runs-on: ubuntu-latest
container:
image: ghcr.io/flathub-infra/flatpak-github-actions:gnome-50@sha256:1fb2df10a57276f90806e1f35454048e30bf1855b7b4ff4808c9ee55887bd852
options: --privileged
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Extract version from tag
id: get_version
run: |
VERSION=${GITHUB_REF#refs/tags/v}
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
- name: Build and bundle
uses: flatpak/flatpak-github-actions/flatpak-builder@79327416609af08178ad73b352877e51450790b3 # v6
with:
bundle: Vocalinux-${{ steps.get_version.outputs.VERSION }}-x86_64.flatpak
manifest-path: packaging/flatpak/com.vocalinux.Vocalinux.yml
cache-key: flatpak-builder-${{ hashFiles('packaging/flatpak/**') }}
arch: x86_64
# Pinned actions/upload-artifact below, same digest as AppImage.
upload-artifact: false
mirror-screenshots-url: https://dl.flathub.org/media
- name: Upload the x86_64 Flatpak
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: flatpak-x86_64
path: Vocalinux-${{ steps.get_version.outputs.VERSION }}-x86_64.flatpak
if-no-files-found: error
retention-days: 7
build-flatpak-arm64:
name: Build Flatpak (aarch64)
runs-on: ubuntu-24.04-arm
container:
image: ghcr.io/flathub-infra/flatpak-github-actions:gnome-50@sha256:1fb2df10a57276f90806e1f35454048e30bf1855b7b4ff4808c9ee55887bd852
options: --privileged
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Extract version from tag
id: get_version
run: |
VERSION=${GITHUB_REF#refs/tags/v}
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
- name: Build and bundle
uses: flatpak/flatpak-github-actions/flatpak-builder@79327416609af08178ad73b352877e51450790b3 # v6
with:
bundle: Vocalinux-${{ steps.get_version.outputs.VERSION }}-aarch64.flatpak
manifest-path: packaging/flatpak/com.vocalinux.Vocalinux.yml
cache-key: flatpak-builder-${{ hashFiles('packaging/flatpak/**') }}
arch: aarch64
upload-artifact: false
mirror-screenshots-url: https://dl.flathub.org/media
- name: Upload the aarch64 Flatpak
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: flatpak-aarch64
path: Vocalinux-${{ steps.get_version.outputs.VERSION }}-aarch64.flatpak
if-no-files-found: error
retention-days: 7
attach-flatpak:
name: Attach Flatpak bundles
needs: [build-and-release, build-flatpak-amd64, build-flatpak-arm64]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Download the x86_64 Flatpak
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: flatpak-x86_64
path: dist
- name: Download the aarch64 Flatpak
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: flatpak-aarch64
path: dist
- name: Attach to release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
ls -la dist/*.flatpak
gh release upload "${{ github.ref_name }}" \
dist/*.flatpak \
--repo ${{ github.repository }} \
--clobber
# Runs last because the aarch64 AppImage and both Flatpak bundles land after
# the release is created: a checksum manifest that covers some of the files
# is worse than none, since the missing one looks like the tampered one.
publish-checksums:
name: Checksums and Provenance
needs: [build-and-release, build-appimage-arm64]
needs: [build-and-release, build-appimage-arm64, build-flatpak-amd64, build-flatpak-arm64, attach-flatpak]
runs-on: ubuntu-latest
permissions:
contents: write
@@ -259,7 +378,7 @@ jobs:
ls -la
# Bare names, so `sha256sum -c` works in whatever directory the user
# downloaded the release into.
sha256sum -- *.whl *.tar.gz *.AppImage | tee SHA256SUMS
sha256sum -- *.whl *.tar.gz *.AppImage *.flatpak | tee SHA256SUMS
# Attesting from the manifest rather than a second glob: the file we
# publish and the set we attest cannot drift apart.
+11 -7
View File
@@ -204,8 +204,12 @@ See [docs/AUR.md](docs/AUR.md).
### Flatpak (any distro)
For a sandboxed, distro-independent install (great for NixOS, Fedora Silverblue,
Steam Deck, and anywhere else), build the Flatpak from the bundled manifest:
GitHub Releases attach `Vocalinux-<version>-x86_64.flatpak` and
`Vocalinux-<version>-aarch64.flatpak`. After the Flathub GNOME runtime is
present, install with `flatpak install --user ./Vocalinux-<version>-x86_64.flatpak`.
Bundles do not auto-update.
For a local build (contributors), use the bundled manifest:
```bash
flatpak install flathub org.gnome.Platform//50 org.gnome.Sdk//50
@@ -216,11 +220,11 @@ flatpak run com.vocalinux.Vocalinux
The Flatpak ships the whisper.cpp engine with Vulkan GPU support and runs through
XWayland on Wayland sessions. See [`packaging/flatpak/README.md`](packaging/flatpak/README.md)
for build details, permissions, and Flathub submission notes. It is **not on
Flathub**: the submission ([flathub/flathub#9368](https://github.com/flathub/flathub/pull/9368))
was closed on 2026-07-23 on policy grounds. The manifest is complete and builds
in CI on both arches, so build it yourself as above; where it gets published is
tracked in [#167](https://github.com/VocaHQ/vocalinux/issues/167).
for build details and permissions. It is **not on Flathub**: the submission
([flathub/flathub#9368](https://github.com/flathub/flathub/pull/9368)) was closed
on 2026-07-23 on policy grounds. Release `.flatpak` assets are tracked in
[#784](https://github.com/VocaHQ/vocalinux/issues/784); the longer-term channel
is [#167](https://github.com/VocaHQ/vocalinux/issues/167).
### Alternative: Install from Source
+28 -21
View File
@@ -2,6 +2,26 @@
Manifest and AppStream metadata for building Vocalinux as a Flatpak.
## GitHub Release bundles
Each `v*` GitHub Release attaches `Vocalinux-<version>-x86_64.flatpak` and
`Vocalinux-<version>-aarch64.flatpak`. That is the easy install path:
```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
```
Bundles do **not** auto-update. 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).
## Current Scope
The manifest ships the default `whisper_cpp` engine only. VOSK is omitted because
@@ -70,28 +90,15 @@ pipx run flatpak-pip-generator \
`python3-build-dependencies.yaml` is a small hand-maintained helper so
`--no-build-isolation` builds can import `mesonpy` before NumPy is built.
## Flathub Submission Notes
## Channel: GitHub Releases, not Flathub
Read this first: the submission,
[flathub/flathub#9368](https://github.com/flathub/flathub/pull/9368), was
**closed on 2026-07-23** on policy grounds — the generative-AI policy, plus a
"tray-only application" reading. Nothing below was the reason it failed, and
re-submitting without addressing that is a repeat. See
[#167](https://github.com/VocaHQ/vocalinux/issues/167) for the channel decision.
1. Change the `vocalinux` module source from `type: dir` to a tagged release:
```yaml
sources:
- type: git
url: https://github.com/VocaHQ/vocalinux.git
tag: v0.16.2
commit: <release-commit-sha>
```
2. Keep build-time network access disabled; only declared sources.
3. Re-run AppStream validation and `flatpak-builder` locally.
4. Be ready to justify sandbox permissions (IBus and StatusNotifier D-Bus talk names).
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.
## Manifest Details
+69 -1
View File
@@ -22,6 +22,22 @@ RELEASE = REPO_ROOT / ".github" / "workflows" / "release.yml"
#: The artifact holding the wheel and sdist that every other job consumes.
DIST_ARTIFACT = "python-dist"
#: Container and builder action pins must stay identical to flatpak.yml.
#: The builder runs privileged, so the image must be an immutable digest, not
#: the moving gnome-50 tag. Digest is the gnome-50 OCI index as of 2026-09-06.
_FLATPAK_CI = REPO_ROOT / ".github" / "workflows" / "flatpak.yml"
_FLATPAK_IMAGE = (
"ghcr.io/flathub-infra/flatpak-github-actions:gnome-50"
"@sha256:1fb2df10a57276f90806e1f35454048e30bf1855b7b4ff4808c9ee55887bd852"
)
_FLATPAK_BUILDER = (
"flatpak/flatpak-github-actions/flatpak-builder@79327416609af08178ad73b352877e51450790b3"
)
_FLATPAK_TAG_ONLY = re.compile(
r"image:\s*ghcr\.io/flathub-infra/flatpak-github-actions:gnome-50\s*$",
re.M,
)
def _text() -> str:
return RELEASE.read_text(encoding="utf-8")
@@ -120,7 +136,7 @@ def test_the_manifest_covers_every_kind_of_artifact_we_publish():
assert "merge-multiple: true" in block, "it collects one artifact, not all of them"
checksum_line = re.search(r"sha256sum -- (.+)$", block, re.M)
assert checksum_line, "publish-checksums does not generate SHA256SUMS"
for pattern in ("*.whl", "*.tar.gz", "*.AppImage"):
for pattern in ("*.whl", "*.tar.gz", "*.AppImage", "*.flatpak"):
assert pattern in checksum_line.group(1), f"{pattern} is published but unchecksummed"
@@ -160,3 +176,55 @@ def test_the_release_notes_tell_users_how_to_verify_the_download():
assert "SHA256SUMS" in body, "the release notes never mention the manifest"
assert "sha256sum -c" in body, "the notes do not show how to check it"
assert "gh attestation verify" in body, "the notes do not show how to check provenance"
def test_flatpak_release_jobs_reuse_ci_builder_pins():
"""Release bundles must be the same builder image and action commit as CI.
Attach is a separate ubuntu-latest job: the builder image is Freedesktop
SDK and does not ship GitHub CLI, unlike the AppImage runners.
"""
assert "@sha256:" in _FLATPAK_IMAGE, "the builder image pin must be a digest, not a tag"
ci = _FLATPAK_CI.read_text(encoding="utf-8")
assert _FLATPAK_IMAGE in ci, "flatpak.yml image pin moved; update this test"
assert not _FLATPAK_TAG_ONLY.search(ci), "flatpak.yml pins the mutable gnome-50 tag"
assert _FLATPAK_BUILDER in ci, "flatpak.yml action pin moved; update this test"
jobs = _jobs()
for name, arch, runner in (
("build-flatpak-amd64", "x86_64", "ubuntu-latest"),
("build-flatpak-arm64", "aarch64", "ubuntu-24.04-arm"),
):
block = jobs[name]
assert _FLATPAK_IMAGE in block, f"{name} does not use the CI builder image"
assert not _FLATPAK_TAG_ONLY.search(block), f"{name} pins the mutable gnome-50 tag"
assert "options: --privileged" in block, f"{name} is not a privileged container"
assert _FLATPAK_BUILDER in block, f"{name} does not use the pinned builder action"
assert f"arch: {arch}" in block
assert f"runs-on: {runner}" in block
assert f"Vocalinux-${{{{ steps.get_version.outputs.VERSION }}}}-{arch}.flatpak" in block
assert "actions/upload-artifact" in block, f"{name} does not upload a workflow artifact"
assert "gh release upload" not in block, f"{name} cannot run gh in the builder image"
assert "python -m build" not in _without_comments(block)
attach = jobs["attach-flatpak"]
assert _needs(attach) >= {
"build-and-release",
"build-flatpak-amd64",
"build-flatpak-arm64",
}
assert "gh release upload" in attach
assert "--clobber" in attach
assert "dist/*.flatpak" in attach
assert _permissions(attach).get("contents") == "write"
def test_the_release_notes_document_flatpak_bundles():
"""Users need the Flathub runtime, --user install, and the no-store caveats."""
body = _jobs()["build-and-release"]
assert "flatpak install --user" in body
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