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' 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@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ env.PYTHON_VERSION }} cache: 'pip' - name: Install build dependencies run: | python -m pip install --upgrade pip pip install build wheel setuptools - 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 # Archive timestamps are what made two builds of one tag differ. Pinned to # the tagged commit, re-running this workflow reproduces the bytes we # published, so a checksum stays true for the life of the release. - name: Pin the build timestamp to the tagged commit run: | echo "SOURCE_DATE_EPOCH=$(git log -1 --format=%ct "$GITHUB_SHA")" >> "$GITHUB_ENV" # The only `python -m build` in this workflow. PyPI, the aarch64 AppImage # and the checksums all consume the artifact below, so the wheel on PyPI # is the wheel on the release. It used to be built three times, on three # runners, and nothing guaranteed the three agreed. - name: Build package run: | python -m build echo "Built packages:" ls -la dist/ - name: Upload the built distributions uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: python-dist path: | dist/*.whl dist/*.tar.gz if-no-files-found: error retention-days: 7 # glslc is built from the pinned shaderc commit inside the container - # Ubuntu 22.04 packages none - and depends on nothing but that commit. - name: Cache AppImage build tools uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/.cache/vocalinux-appimage key: appimage-tools-${{ runner.arch }}-${{ hashFiles('packaging/appimage/tool_checksums.txt') }} - name: Build AppImage (x86_64) env: VOCALINUX_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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: appimage-x86_64 path: dist/*.AppImage if-no-files-found: error retention-days: 7 - 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 vocalinux ./install.sh ``` ### From PyPI ```bash # Install the Linux system packages first: # GTK/AppIndicator, PortAudio, and xdotool/wtype are required. python3 -m venv ~/.local/share/vocalinux-pypi/venv --system-site-packages source ~/.local/share/vocalinux-pypi/venv/bin/activate pip install vocalinux vocalinux ``` 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 vocalinux ``` ### 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 Vocalinux-${{ steps.get_version.outputs.VERSION }}-x86_64.AppImage ./Vocalinux-${{ steps.get_version.outputs.VERSION }}-x86_64.AppImage ``` 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). ### Snap Tagged releases also attach `vocalinux_${{ steps.get_version.outputs.VERSION }}_amd64.snap` and try the Snap Store (`edge` and `candidate`). `stable` is a manual promote. Super-privileged plugs such as `uinput` can hold the Store revision in human review; the GitHub `.snap` is the sideload in that case: ```bash sudo snap install --dangerous ./vocalinux_${{ steps.get_version.outputs.VERSION }}_amd64.snap sudo snap connect vocalinux:uinput ``` `--dangerous` is required because this file is not a Store revision. It will not refresh from the Store. amd64 only. ### 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 Vocalinux-${{ 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@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: python-dist path: dist # glslc is built from the pinned shaderc commit inside the container - # Ubuntu 22.04 packages none - and depends on nothing but that commit. - name: Cache AppImage build tools uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/.cache/vocalinux-appimage key: appimage-tools-${{ runner.arch }}-${{ hashFiles('packaging/appimage/tool_checksums.txt') }} - name: Build AppImage (aarch64) env: VOCALINUX_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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 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 # 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 # 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@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-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@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, both Flatpak bundles, 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-flatpak-amd64, build-flatpak-arm64, attach-flatpak, attach-snap] runs-on: ubuntu-latest permissions: contents: write id-token: write attestations: write steps: - name: Collect every artifact this run published uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: merge-multiple: true path: dist - 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. sha256sum -- *.whl *.tar.gz *.AppImage *.flatpak *.snap | tee 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/vocalinux/ 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@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: python-dist path: dist - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ env.PYTHON_VERSION }} - name: Verify package run: | python -m pip install --upgrade pip pip install twine twine check dist/* - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1 with: skip-existing: true publish-aur: name: Publish to AUR needs: build-and-release 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: 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/vocalinux/PKGBUILD sed -i "s/^pkgver=.*/pkgver=${TAG//-/}/; s/^_tag=.*/_tag=${TAG}/; s/^pkgrel=.*/pkgrel=1/" "$PKGBUILD" grep -E '^(pkgver|_tag|pkgrel)=' "$PKGBUILD" - name: Publish AUR package if: steps.aur.outputs.configured == 'true' uses: KSXGitHub/github-actions-deploy-aur@084b0d9b15415bf9cdb65d44dad1efe37a354050 # v4.2.0 with: pkgname: vocalinux pkgbuild: packaging/aur/vocalinux/PKGBUILD updpkgsums: 'true' commit_username: ${{ secrets.AUR_USERNAME }} commit_email: ${{ secrets.AUR_EMAIL }} ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} commit_message: 'Release ${{ github.ref_name }}' # GitHub Release first, Store second. uinput is super-privileged, so the # Store can 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. # Secret: SNAPCRAFT_STORE_CREDENTIALS (snapcraft export-login). build-snap: name: Build Snap runs-on: ubuntu-latest 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: Upload the snap uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 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@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 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 --clobber publish-snap: name: Publish Snap to Store needs: build-snap 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@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 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 -F -q "will need manual review" "$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 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@820762786026740c76f36085b0efc47a31fe5020 # v7.0.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: vocalinux.com force_orphan: true