Files
voculinux/.github/workflows/release.yml
T
xavierk ba6462ccf8 refactor(ci): move the xbps job out of the GitHub release workflow
The signed repository is now published by the tag-triggered Gitea workflow,
so release.yml drops the build-xbps job, its place in the checksum job's
needs list, the xbps-repo artifact pattern, and the checksum lines for
files that no longer arrive. The release notes point at the Gitea release
instead of promising attached repository files.

The policy tests now assert the new shape: release.yml carries no xbps
job, and the Gitea workflow is the one place that runs repo.sh.
2026-11-06 18:26:14 +05:30

1208 lines
50 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'
# 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<<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
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)
Not attached here. The signed xbps repository is published by the
tag-triggered Gitea workflow `.gitea/workflows/xbps-release.yml` to
the release named `latest` on the project's Gitea host. It is a
third-party repository published by this project, not upstream Void
Linux. The registration lines and the key fingerprint to pin are in
the Void Linux section of the README.
### 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]
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)'
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
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.
sha256sum -- *.whl *.tar.gz *.AppImage *.deb *.rpm *.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/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 }}'
# 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