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]
|
||||
|
||||
Reference in New Issue
Block a user