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:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user