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@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2 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. ### 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. ```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 # 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. publish-checksums: name: Checksums and Provenance needs: [build-and-release, build-appimage-arm64] 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 | 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 }}' 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