Files
voculinux/.github/workflows/release.yml
T
SergeyandGitHub 0ba029b015 chore(release): make a release verifiable, and pin what builds it (epic #701, phase 5) (#759)
* chore(release): build once, and publish sha256sums and provenance (epic #701, phase 5)

* chore(ci): pin every third-party action by commit sha (epic #701, phase 5)

* test(ci): anchor the dependabot guard on the ecosystem it must cover
2026-08-31 14:32:20 -07:00

419 lines
16 KiB
YAML

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@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
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@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
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@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2
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@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- 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@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
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@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
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@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
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@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: python-dist
path: dist
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
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@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- 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@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- name: Setup Node.js
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
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