From 16175d9ea3dfb9343b0df683d5895a2cd5e08a33 Mon Sep 17 00:00:00 2001 From: Jatin K Malik Date: Sat, 5 Sep 2026 20:10:13 -0700 Subject: [PATCH] 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. --- .github/workflows/flatpak.yml | 3 +- .github/workflows/release.yml | 135 ++++++++++++++++++++++++++++++-- README.md | 18 +++-- packaging/flatpak/README.md | 49 +++++++----- tests/test_release_integrity.py | 70 ++++++++++++++++- 5 files changed, 237 insertions(+), 38 deletions(-) diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml index 84ac929..806dc11 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f446f5c..b9f2c78 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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. diff --git a/README.md b/README.md index fd42f33..ff2a6b9 100644 --- a/README.md +++ b/README.md @@ -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--x86_64.flatpak` and +`Vocalinux--aarch64.flatpak`. After the Flathub GNOME runtime is +present, install with `flatpak install --user ./Vocalinux--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 diff --git a/packaging/flatpak/README.md b/packaging/flatpak/README.md index a9f56d4..8e87a9f 100644 --- a/packaging/flatpak/README.md +++ b/packaging/flatpak/README.md @@ -2,6 +2,26 @@ Manifest and AppStream metadata for building Vocalinux as a Flatpak. +## GitHub Release bundles + +Each `v*` GitHub Release attaches `Vocalinux--x86_64.flatpak` and +`Vocalinux--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--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: - ``` - -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 diff --git a/tests/test_release_integrity.py b/tests/test_release_integrity.py index cc6e695..fa5c192 100644 --- a/tests/test_release_integrity.py +++ b/tests/test_release_integrity.py @@ -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