name: Release on: push: tags: - 'v*' # Least privilege, granted per job. `id-token: write` at the top level handed an # OIDC token to every job in the workflow, including publish-aur, which runs a # third-party action with our AUR signing key. permissions: contents: read 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.voculinux.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/voculinux-flatpak/repo' }} FLATPAKREPO_URL: ${{ vars.FLATPAKREPO_URL || 'https://vocahq.github.io/voculinux-flatpak/vocahq.flatpakrepo' }} FLATPAKREF_URL: ${{ vars.FLATPAKREF_URL || 'https://vocahq.github.io/voculinux-flatpak/com.voculinux.Voculinux.flatpakref' }} jobs: build-and-release: name: Build and Create Release runs-on: ubuntu-latest permissions: contents: write steps: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 - name: Set up Python uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 with: python-version: ${{ env.PYTHON_VERSION }} cache: 'pip' - name: Set up pinned uv uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 with: version: '0.13.3' - name: Extract version from tag id: get_version run: | VERSION=${GITHUB_REF#refs/tags/v} echo "VERSION=$VERSION" >> $GITHUB_OUTPUT echo "Building version: $VERSION" - name: Verify version consistency run: | # Get version from version.py file directly (avoid importing full package which needs gi) PACKAGE_VERSION=$(grep -oP '__version__\s*=\s*"\K[^"]+' src/vocalinux/version.py) TAG_VERSION=${{ steps.get_version.outputs.VERSION }} echo "Package version: $PACKAGE_VERSION" echo "Tag version: $TAG_VERSION" # Allow alpha/beta suffixes in package version if [[ ! "$PACKAGE_VERSION" == *"$TAG_VERSION"* ]] && [[ ! "$TAG_VERSION" == *"$PACKAGE_VERSION"* ]]; then echo "Warning: Version mismatch between tag ($TAG_VERSION) and package ($PACKAGE_VERSION)" fi # Two independent builds verify reproducibility, but only the canonical # distributions enter dist/. Every publisher consumes those same bytes. # The script pins SOURCE_DATE_EPOCH, verifies exports, installs the hashed # toolchain, builds without isolation and runs twine check --strict. - name: Build and verify reproducible wheel run: python scripts/build_reproducible_wheel.py --python "$(command -v python)" - name: Preserve failed wheel builds and diagnostics if: failure() uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: wheel-reproducibility-diagnostics path: build/reproducible-wheel/ retention-days: 7 - name: Upload the built distributions uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: python-dist path: | dist/*.whl dist/*.tar.gz if-no-files-found: error retention-days: 7 # glslc and the pywhispercpp Vulkan rebuild are native artifacts. Neither # depends on this commit: the id script keys them to the base image, # tool_checksums.txt, and the pywhispercpp pin. - name: AppImage native cache id id: native-cache run: | set -euo pipefail id="$(bash packaging/appimage/native-cache-id.sh)" echo "id=${id}" >> "$GITHUB_OUTPUT" - name: Cache AppImage build tools uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/.cache/voculinux-appimage key: appimage-native-${{ runner.arch }}-${{ steps.native-cache.outputs.id }} - name: Build AppImage (x86_64) env: VOCULINUX_APPIMAGE_REQUIRE_VULKAN: "1" run: | bash packaging/appimage/docker-build.sh dist/*.whl "${{ steps.get_version.outputs.VERSION }}" dist ls -la dist/*.AppImage - name: Upload the x86_64 AppImage uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: appimage-x86_64 path: dist/*.AppImage 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.voculinux.Voculinux ``` 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 ./Voculinux-__VERSION__-x86_64.flatpak flatpak run com.voculinux.Voculinux ``` 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 ./Voculinux-__VERSION__-x86_64.flatpak flatpak run com.voculinux.Voculinux ``` 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<> "$GITHUB_OUTPUT" - name: Create Release id: create_release uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3 with: files: | dist/*.whl dist/*.tar.gz dist/*.AppImage generate_release_notes: true draft: false prerelease: ${{ contains(github.ref, 'alpha') || contains(github.ref, 'beta') || contains(github.ref, 'rc') }} body: | ## Installation ### Quick Install (Recommended) ```bash curl -fsSL https://raw.githubusercontent.com/VocaHQ/vocalinux/${{ github.ref_name }}/install.sh | bash ``` ### From Source ```bash git clone https://github.com/VocaHQ/vocalinux.git cd voculinux ./install.sh ``` ### From PyPI ```bash # Install the Linux system packages first: # GTK/AppIndicator, PortAudio, and xdotool/wtype are required. python3 -m venv ~/.local/share/voculinux-pypi/venv --system-site-packages source ~/.local/share/voculinux-pypi/venv/bin/activate pip install voculinux voculinux ``` PyPI installs the Python package only. For automatic system dependency, desktop integration, and model setup, use the Quick Install command above. See docs/INSTALL.md for distro-specific PyPI prerequisites. ### Arch Linux (AUR) ```bash yay -S voculinux ``` ### AppImage (no install, no root) Download the `.AppImage` matching your CPU (`x86_64` or `aarch64`, the latter attached shortly after this release is published) below: ```bash chmod +x Voculinux-${{ steps.get_version.outputs.VERSION }}-x86_64.AppImage ./Voculinux-${{ steps.get_version.outputs.VERSION }}-x86_64.AppImage ``` Still requires `xdotool`/`wtype`/`ydotool` on the host for text injection, same as the PyPI path. ### Distro packages (.deb / .rpm) Download the package matching your distro and CPU (`amd64`/`x86_64`, or `arm64`/`aarch64` attached shortly after this release is published): ```bash # Debian / Ubuntu sudo apt install ./voculinux_*_amd64.deb # Fedora sudo dnf install ./voculinux-*.x86_64.rpm ``` Thin packages: Python, GTK, PyGObject and the AppIndicator typelib come from the distro, and `apt`/`dnf` resolves them. Only `pywhispercpp` and `pynput` are vendored (the first is packaged by neither distro, the second is missing on Fedora). whisper.cpp runs on CPU in these packages; the Vulkan build is the AppImage. A PPA and a COPR are not published yet. On Wayland the default Right Alt hotkey uses evdev: `sudo usermod -aG input $USER`, then log out and back in (the `input` group can read every keyboard and pointer device; X11 needs nothing). ${{ steps.flatpak_notes.outputs.flatpak }} ### Snap Tagged releases also attach `voculinux_${{ steps.get_version.outputs.VERSION }}_amd64.snap` and try the Snap Store (`edge` and `candidate`). `stable` is promoted from candidate after QA by the `snap-promote.yml` dispatch workflow. Wayland typing runs through the RemoteDesktop portal; the snap ships no `uinput` plug. The GitHub `.snap` is also the sideload while a Store revision lags: ```bash sudo snap install --dangerous ./voculinux_${{ steps.get_version.outputs.VERSION }}_amd64.snap ``` `--dangerous` is required because this file is not a Store revision. It will not refresh from the Store. amd64 only. ### Void Linux (glibc x86_64, signed xbps repo) Also attached below: the signed repository files (`*.x86_64.xbps` with signatures, `x86_64-repodata`, and the client key `FINGERPRINT` plus the fingerprint-named `.plist`). This is a third-party repository published by this project, not upstream Void Linux: ```bash # as root (the files come from this release's assets): cp .plist /var/db/xbps/keys/ echo 'repository=https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/' > /etc/xbps.d/voculinux.conf sudo xbps-install -Sy voculinux ``` Answer `Y` at the fingerprint prompt only after checking it matches the attached `FINGERPRINT` file. ### Verifying what you downloaded `SHA256SUMS` covers every file here and is attached once the aarch64 AppImage, both Flatpak builds, and the snap finish, a few minutes after this release appears. The wheel and the sdist below are the same bytes published to PyPI. ```bash sha256sum -c --ignore-missing SHA256SUMS ``` Each artifact also carries GitHub build provenance, which ties it to this workflow run: ```bash gh attestation verify Voculinux-${{ steps.get_version.outputs.VERSION }}-x86_64.AppImage \ --repo ${{ github.repository }} ``` --- See [CHANGELOG](https://github.com/VocaHQ/vocalinux/releases) for detailed changes. build-appimage-arm64: name: Build and Attach AppImage (aarch64) needs: build-and-release runs-on: ubuntu-24.04-arm permissions: contents: write 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 # The wheel is py3-none-any, so this is the wheel the release and PyPI # carry. Building it again here made a third copy of the same version that # no published checksum could describe. - name: Download the built distributions uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: python-dist path: dist # glslc and the pywhispercpp Vulkan rebuild are native artifacts. Neither # depends on this commit: the id script keys them to the base image, # tool_checksums.txt, and the pywhispercpp pin. - name: AppImage native cache id id: native-cache run: | set -euo pipefail id="$(bash packaging/appimage/native-cache-id.sh)" echo "id=${id}" >> "$GITHUB_OUTPUT" - name: Cache AppImage build tools uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/.cache/voculinux-appimage key: appimage-native-${{ runner.arch }}-${{ steps.native-cache.outputs.id }} - name: Build AppImage (aarch64) env: VOCULINUX_APPIMAGE_REQUIRE_VULKAN: "1" run: | bash packaging/appimage/docker-build.sh dist/*.whl "${{ steps.get_version.outputs.VERSION }}" dist ls -la dist/*.AppImage - name: Upload the aarch64 AppImage uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: appimage-aarch64 path: dist/*.AppImage if-no-files-found: error retention-days: 7 - name: Attach to release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | gh release upload "${{ github.ref_name }}" \ dist/*.AppImage \ --repo ${{ github.repository }} \ --clobber # The .deb and .rpm are thin: they depend on the distro's Python, GTK, # PyGObject and typelibs and vendor only pywhispercpp and pynput (#600). # nfpm builds both from one config inside the pinned Debian 12 image, and # the same wheel PyPI ships is the payload. build-native: name: Build native packages (x86_64) needs: build-and-release runs-on: ubuntu-latest permissions: contents: write 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 # Same reason the aarch64 AppImage downloads the artifact: one wheel, # everywhere, or the checksums cannot describe what the packages hold. - name: Download the built distributions uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: python-dist path: dist - name: Build .deb and .rpm run: | bash packaging/native/docker-build.sh dist/*.whl "${{ steps.get_version.outputs.VERSION }}" dist ls -la dist/*.deb dist/*.rpm - name: Upload the x86_64 packages uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: native-x86_64 path: | dist/*.deb dist/*.rpm if-no-files-found: error retention-days: 7 - name: Attach to release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | gh release upload "${{ github.ref_name }}" \ dist/*.deb dist/*.rpm \ --repo ${{ github.repository }} \ --clobber build-native-arm64: name: Build native packages (aarch64) needs: build-and-release runs-on: ubuntu-24.04-arm permissions: contents: write 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: Download the built distributions uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: python-dist path: dist - name: Build .deb and .rpm (aarch64) run: | bash packaging/native/docker-build.sh dist/*.whl "${{ steps.get_version.outputs.VERSION }}" dist ls -la dist/*.deb dist/*.rpm - name: Upload the aarch64 packages uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: native-aarch64 path: | dist/*.deb dist/*.rpm if-no-files-found: error retention-days: 7 - name: Attach to release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | gh release upload "${{ github.ref_name }}" \ dist/*.deb dist/*.rpm \ --repo ${{ github.repository }} \ --clobber # 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: Voculinux-${{ steps.get_version.outputs.VERSION }}-x86_64.flatpak manifest-path: packaging/flatpak/com.voculinux.Voculinux.yml # Same prefix rule as flatpak.yml: restore-keys is # `flatpak-builder-${arch}`. The action appends -${arch} again. cache-key: flatpak-builder-x86_64-${{ 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@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: flatpak-x86_64 path: Voculinux-${{ 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: Voculinux-${{ steps.get_version.outputs.VERSION }}-aarch64.flatpak manifest-path: packaging/flatpak/com.voculinux.Voculinux.yml cache-key: flatpak-builder-aarch64-${{ 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@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: flatpak-aarch64 path: Voculinux-${{ steps.get_version.outputs.VERSION }}-aarch64.flatpak 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. The skip is loud — a quiet one let the # documented remote sit unpublished for releases at a time (#951). - 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" msg="FLATPAK_GPG_PRIVATE_KEY and/or FLATPAK_REPO_TOKEN not set; skipping remote publish — the documented auto-update remote will not carry this release (docs/FLATPAK_REMOTE.md)" echo "::warning::$msg" { echo "### :warning: Flatpak remote publish skipped" echo echo "$msg" } >> "$GITHUB_STEP_SUMMARY" 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@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 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 # git drops empty directories: a checked-out site/repo lacks # refs/remotes and refs/mirrors, and `build-update-repo # --generate-static-deltas` aborts with # "opendir(refs/remotes): No such file or directory" without them. mkdir -p "$repo/refs/remotes" "$repo/refs/mirrors" 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://voculinux.com' echo 'Comment=Official Voculinux Flatpak remote' echo 'Description=Self-hosted Voculinux Flatpak repository' echo 'Icon=https://voculinux.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=Voculinux' echo 'Name=com.voculinux.Voculinux' 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://voculinux.com' echo 'Icon=https://voculinux.com/icon-512x512.png' echo "GPGKey=$(base64 -w0 "$RUNNER_TEMP/pubkey.gpg")" } > publish/com.voculinux.Voculinux.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: 'Voculinux ${{ github.ref_name }}' 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@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: flatpak-x86_64 path: dist - name: Download the aarch64 Flatpak uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 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, both Flatpak bundles, the native # packages, and the snap 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, build-native, build-native-arm64, build-flatpak-amd64, build-flatpak-arm64, attach-flatpak, attach-snap, build-xbps] runs-on: ubuntu-latest # An attach-job failure must not skip checksums: this job hashes the build # artifacts, which exist regardless of whether an upload to the release # succeeded. A missing build artifact still fails the job via the guard # below, so a partial manifest can never publish. if: ${{ !cancelled() && needs.build-and-release.result == 'success' }} permissions: contents: write id-token: write attestations: write steps: - name: Collect every artifact this run published uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: # Verification-only artifacts must never enter the release manifest. pattern: '@(python-dist|appimage-*|native-*|flatpak-*|snap-amd64|xbps-repo)' merge-multiple: true path: dist - name: Require every expected artifact working-directory: dist run: | set -euo pipefail for pat in '*.whl' '*.tar.gz' '*.AppImage' '*.deb' '*.rpm' '*.flatpak' '*.snap'; do compgen -G "$pat" > /dev/null || { echo "missing artifact: $pat"; exit 1; } done for pat in 'repo/*.x86_64.xbps' 'repo/*.sig2' 'repo/*-repodata*' 'keys/FINGERPRINT' 'keys/*.plist'; do compgen -G "$pat" > /dev/null || { echo "missing artifact: $pat"; exit 1; } done test "$(compgen -G '*.AppImage' | wc -l)" -eq 2 test "$(compgen -G '*.deb' | wc -l)" -eq 2 test "$(compgen -G '*.rpm' | wc -l)" -eq 2 test "$(compgen -G '*.flatpak' | wc -l)" -eq 2 - name: Generate SHA256SUMS working-directory: dist run: | set -euo pipefail ls -la # Bare names, so `sha256sum -c` works in whatever directory the user # downloaded the release into. The xbps repo files hash from their # own directories for the same reason: the release attaches them # flat, and RSA signatures stay the primary trust root. sha256sum -- *.whl *.tar.gz *.AppImage *.deb *.rpm *.flatpak *.snap | tee SHA256SUMS (cd repo && sha256sum -- *.x86_64.xbps *.sig2 *-repodata*) >> SHA256SUMS (cd keys && sha256sum -- FINGERPRINT *.plist) >> SHA256SUMS # Attesting from the manifest rather than a second glob: the file we # publish and the set we attest cannot drift apart. - name: Attest build provenance uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4 with: subject-checksums: dist/SHA256SUMS - name: Attach SHA256SUMS to the release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | gh release upload "${{ github.ref_name }}" \ dist/SHA256SUMS \ --repo ${{ github.repository }} \ --clobber publish-pypi: name: Publish to PyPI needs: build-and-release runs-on: ubuntu-latest permissions: id-token: write # PyPI trusted publishing; there is no API token to leak environment: name: pypi url: https://pypi.org/project/voculinux/ steps: # Not a rebuild: the artifact attached to the GitHub release, so one # checksum describes both. - name: Download the built distributions uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: python-dist path: dist # twine check --strict already ran with the locked release toolchain # before these exact distributions were uploaded by build-and-release. - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # release/v1 with: skip-existing: true aur-source-test: name: Verify AUR release source (gate) needs: [build-and-release, publish-checksums] runs-on: ubuntu-latest timeout-minutes: 30 permissions: contents: read attestations: read steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 with: python-version: '3.11' - uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 with: version: '0.13.3' - name: Download the published sdist and checksum manifest env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | gh release download "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" \ --pattern 'voculinux-*.tar.gz' --pattern SHA256SUMS --dir release-source - name: Verify provenance and prepare the exact source PKGBUILD env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | uv run --python "$(command -v python)" --locked --only-group release-build \ python scripts/prepare_aur_source.py \ --sdist release-source/voculinux-*.tar.gz --manifest release-source/SHA256SUMS \ --tag "$GITHUB_REF_NAME" --source-commit "$GITHUB_SHA" --output build/aur-source - name: Build and install the exact source PKGBUILD on Arch run: | docker run --rm -v "$PWD:/repo:ro" -e AUR_SOURCE_ONLY=1 -e AUR_FETCH_SOURCE=1 \ archlinux:latest bash /repo/packaging/aur/build-test.sh - name: Retain only the verified source PKGBUILD for publication uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: aur-source-pkgbuild path: build/aur-source/PKGBUILD if-no-files-found: error retention-days: 7 publish-aur: name: Publish to AUR needs: [build-and-release, aur-source-test] runs-on: ubuntu-latest permissions: contents: read # Secrets: AUR_SSH_PRIVATE_KEY, AUR_USERNAME, AUR_EMAIL — see docs/AUR.md steps: - name: Check for AUR credentials id: aur env: AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }} run: | if [ -z "$AUR_SSH_PRIVATE_KEY" ]; then echo "configured=false" >> "$GITHUB_OUTPUT" echo "AUR_SSH_PRIVATE_KEY not set; skipping (docs/AUR.md)" else echo "configured=true" >> "$GITHUB_OUTPUT" fi - name: Download the verified source PKGBUILD if: steps.aur.outputs.configured == 'true' uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: aur-source-pkgbuild path: aur-source - name: Publish AUR package if: steps.aur.outputs.configured == 'true' uses: KSXGitHub/github-actions-deploy-aur@084b0d9b15415bf9cdb65d44dad1efe37a354050 # v4.2.0 with: pkgname: voculinux pkgbuild: aur-source/PKGBUILD updpkgsums: 'false' commit_username: ${{ secrets.AUR_USERNAME }} commit_email: ${{ secrets.AUR_EMAIL }} ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} commit_message: 'Release ${{ github.ref_name }}' # Separate job: the -bin PKGBUILD pins digests of BOTH AppImages, and the # aarch64 build attaches its asset after build-and-release returns. Waiting # for it here keeps the binary package from curling a 404. Do not depend on # source publication: its Arch gate and unrelated checksum producers must # not block ready AppImages. publish-aur-bin: name: Publish -bin to AUR needs: [build-and-release, build-appimage-arm64] runs-on: ubuntu-latest permissions: contents: read steps: - name: Check for AUR credentials id: aur env: AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }} run: | if [ -z "$AUR_SSH_PRIVATE_KEY" ]; then echo "configured=false" >> "$GITHUB_OUTPUT" echo "AUR_SSH_PRIVATE_KEY not set; skipping (docs/AUR.md)" else echo "configured=true" >> "$GITHUB_OUTPUT" fi - name: Checkout code if: steps.aur.outputs.configured == 'true' uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Bump PKGBUILD version if: steps.aur.outputs.configured == 'true' run: | set -euo pipefail TAG="${GITHUB_REF#refs/tags/v}" PKGBUILD=packaging/aur/voculinux-bin/PKGBUILD sed -i "s/^pkgver=.*/pkgver=${TAG//-/}/; s/^_tag=.*/_tag=${TAG}/; s/^pkgrel=.*/pkgrel=1/" "$PKGBUILD" grep -E '^(pkgver|_tag|pkgrel)=' "$PKGBUILD" # The -bin PKGBUILD carries per-arch sum arrays, but updpkgsums inside # the publish action only regenerates the x86_64 one — pin both here so # the published PKGBUILD never ships a stale aarch64 digest. - name: Pin per-arch AppImage digests in the -bin PKGBUILD if: steps.aur.outputs.configured == 'true' run: | set -euo pipefail tag="${GITHUB_REF_NAME#v}" for arch in x86_64 aarch64; do url="https://github.com/VocaHQ/vocalinux/releases/download/${GITHUB_REF_NAME}/Voculinux-${tag}-${arch}.AppImage" sum="$(curl -fsSL "$url" | sha256sum | cut -d' ' -f1)" sed -i "s/^sha256sums_${arch}=('[0-9a-f]*')/sha256sums_${arch}=('${sum}')/" \ packaging/aur/voculinux-bin/PKGBUILD done - name: Publish AUR -bin package if: steps.aur.outputs.configured == 'true' uses: KSXGitHub/github-actions-deploy-aur@084b0d9b15415bf9cdb65d44dad1efe37a354050 # v4.2.0 with: pkgname: voculinux-bin # The -bin PKGBUILD names local sources (voculinux.desktop, # voculinux.svg, LICENSE); publishing the whole directory mirrors # them into the AUR repo instead of shipping a lone PKGBUILD. asset_dir: packaging/aur/voculinux-bin # Sums are pinned in the step above for BOTH arches; regenerating # here would only refresh the x86_64 array. updpkgsums: 'false' commit_username: ${{ secrets.AUR_USERNAME }} commit_email: ${{ secrets.AUR_EMAIL }} ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} commit_message: 'Release ${{ github.ref_name }}' # The signed Void xbps repository (ticket #29): every template builds in # smoke-gate order in the pinned Void container via podman, the full # repository index is signed with the RSA4096 repository key, and the # packages, signatures, repodata, and client key land on this release. # XBPS_SIGNING_KEY is required — an unsigned repository can never # publish, so a missing key fails the job instead of skipping it (unlike # the opt-in Flatpak remote and AUR publishes). The voculinux template # builds the canonical release sdist, so this waits for build-and-release, # and the tag must match src/vocalinux/version.py or the job aborts. build-xbps: name: Build signed xbps repository needs: build-and-release runs-on: ubuntu-latest permissions: contents: write steps: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Verify tag matches the version source of truth run: | set -euo pipefail TAG="${GITHUB_REF#refs/tags/v}" PACKAGE_VERSION=$(grep -oP '__version__\s*=\s*"\K[^"]+' src/vocalinux/version.py) echo "Tag version: $TAG" echo "Package version: $PACKAGE_VERSION" if [[ "$PACKAGE_VERSION" != *"$TAG"* ]] && [[ "$TAG" != *"$PACKAGE_VERSION"* ]]; then echo "::error::Tag v$TAG does not match src/vocalinux/version.py ($PACKAGE_VERSION); refusing to ship the signed repo" exit 1 fi # Fail-closed: no key, no repo, no publish — never a silent skip. - name: Require the repository signing key env: XBPS_SIGNING_KEY: ${{ secrets.XBPS_SIGNING_KEY }} run: | if [ -z "$XBPS_SIGNING_KEY" ]; then echo "::error::XBPS_SIGNING_KEY is unset; refusing to publish an unsigned repository (packaging/void/README.md)" exit 1 fi - name: Install podman run: | sudo apt-get update sudo apt-get install -y --no-install-recommends podman - name: Build and sign the repository env: XBPS_SIGNING_KEY: ${{ secrets.XBPS_SIGNING_KEY }} run: | set -euo pipefail printf '%s' "$XBPS_SIGNING_KEY" > /tmp/xbps-sign-key.pem chmod 600 /tmp/xbps-sign-key.pem XBPS_SIGNING_KEY_FILE=/tmp/xbps-sign-key.pem bash packaging/void/repo.sh rm -f /tmp/xbps-sign-key.pem ls -la publish/xbps/repo publish/xbps/keys - name: Upload the signed repository uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: xbps-repo path: | publish/xbps/repo/* publish/xbps/keys/* if-no-files-found: error retention-days: 7 - name: Attach the signed repository to the release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail cd publish/xbps # Full signed repo or nothing: binary packages (*.x86_64.xbps) # with their signatures, the signed repository metadata # (*-repodata), and the client key files (FINGERPRINT plus the # fingerprint-named .plist). Each class is required below, so a # partial repo can never attach. compgen -G 'repo/*.x86_64.xbps' > /dev/null || { echo "missing packages"; exit 1; } compgen -G 'repo/*.sig2' > /dev/null || { echo "missing signatures"; exit 1; } compgen -G 'repo/*-repodata*' > /dev/null || { echo "missing repodata"; exit 1; } compgen -G 'keys/FINGERPRINT' > /dev/null || { echo "missing FINGERPRINT"; exit 1; } compgen -G 'keys/*.plist' > /dev/null || { echo "missing client key"; exit 1; } gh release upload "${{ github.ref_name }}" \ repo/*.x86_64.xbps repo/*.sig2 repo/*-repodata* keys/FINGERPRINT keys/*.plist \ --repo ${{ github.repository }} \ --clobber # GitHub Release first, Store second. A super-privileged plug can still # queue a human review and refuse edge/candidate; the .snap still # has to land on the tag for sideload. grade: stable allows candidate; do # not auto-release to stable — promote after QA via snap-promote.yml (#783). # Secret: SNAPCRAFT_STORE_CREDENTIALS (snapcraft export-login). build-snap: name: Build Snap runs-on: ubuntu-24.04 timeout-minutes: 45 permissions: contents: read steps: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Build snap id: build uses: snapcore/action-build@3bdaa03e1ba6bf59a65f84a751d943d549a54e79 # v1 - name: Verify the installed snap env: SNAP_FILE: ${{ steps.build.outputs.snap }} run: bash snap/local/smoke-test.sh "$SNAP_FILE" - name: Upload the snap uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: snap-amd64 path: ${{ steps.build.outputs.snap }} if-no-files-found: error retention-days: 7 attach-snap: name: Attach Snap needs: [build-and-release, build-snap] runs-on: ubuntu-latest permissions: contents: write steps: - name: Download the snap uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: snap-amd64 path: dist - name: Attach to release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAG: ${{ github.ref_name }} run: | set -euo pipefail ls -la dist/*.snap gh release upload "$TAG" dist/*.snap \ --repo ${{ github.repository }} \ --clobber publish-snap: name: Publish Snap to Store needs: [build-snap, build-and-release] runs-on: ubuntu-latest permissions: contents: read steps: - name: Require Snap Store credentials env: SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} run: | if [ -z "$SNAPCRAFT_STORE_CREDENTIALS" ]; then echo "SNAPCRAFT_STORE_CREDENTIALS is unset; cannot publish the snap" exit 1 fi - name: Download the snap uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: snap-amd64 path: dist - name: Locate the snap id: snapfile run: | set -euo pipefail file=$(ls dist/*.snap) echo "file=$file" >> "$GITHUB_OUTPUT" ls -la "$file" - name: Install snapcraft run: sudo snap install snapcraft --classic - name: Publish snap to edge and candidate env: SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} SNAP_FILE: ${{ steps.snapfile.outputs.file }} run: | set -euo pipefail log=/tmp/snap-upload.log set +e /snap/bin/snapcraft upload "$SNAP_FILE" --release edge,candidate >"$log" 2>&1 status=$? set -e cat "$log" if [ "$status" -eq 0 ]; then exit 0 fi if grep -E -q "will need manual review|human review required" "$log" \ && grep -F -q "allow-installation" "$log"; then echo "::warning::Snap Store queued a human review (allow-installation). The .snap is on the GitHub Release." exit 0 fi if grep -F -q "Waiting for previous upload(s)" "$log"; then echo "::error::Snap Store publish failed: an older upload is still in the review queue. Reject and remove it at https://dashboard.snapcraft.io/ (the upload page lists it), then re-run this job." exit "$status" fi echo "::error::Snap Store publish failed (exit $status) for a reason other than allow-installation review." exit "$status" deploy-website: name: Deploy Website runs-on: ubuntu-latest needs: [build-and-release, publish-pypi] permissions: contents: write steps: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup Node.js uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: '24' cache: 'npm' cache-dependency-path: './web/package-lock.json' - name: Install dependencies working-directory: ./web run: npm ci || npm install - name: Sync version from source code run: | VERSION=$(grep -oP '__version__\s*=\s*"\K[^"]+' src/vocalinux/version.py) echo "Syncing version: $VERSION" # JSON-LD uses unquoted JS keys: softwareVersion: "x.y.z" if grep -qE 'softwareVersion:\s*"' web/src/app/page.tsx; then sed -i 's/softwareVersion: "[^"]*"/softwareVersion: "'"$VERSION"'"/' web/src/app/page.tsx echo "Updated page.tsx version:" grep -oP 'softwareVersion:\s*"\K[^"]+' web/src/app/page.tsx elif grep -qE 'softwareVersion:\s*"' web/src/app/layout.tsx; then sed -i 's/softwareVersion: "[^"]*"/softwareVersion: "'"$VERSION"'"/' web/src/app/layout.tsx echo "Updated layout.tsx version:" grep -oP 'softwareVersion:\s*"\K[^"]+' web/src/app/layout.tsx else echo "::warning::No softwareVersion field found in page.tsx or layout.tsx; skipping sync" fi # Update package.json version (remove -alpha/beta/rc suffix for npm) NPM_VERSION=${VERSION%-*} NPM_VERSION=${NPM_VERSION%-beta*} NPM_VERSION=${NPM_VERSION%-rc*} sed -i 's/"version": "[^"]*"/"version": "'"$NPM_VERSION"'"/' web/package.json # Verify the changes echo "Updated package.json version:" grep -oP '"version":\s*"\K[^"]+' web/package.json - name: Build and deploy website working-directory: ./web run: | npm ci || npm install npm run deploy - name: Deploy to gh-pages uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453 # v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./web/out publish_branch: gh-pages cname: voculinux.com force_orphan: true