Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
349 changes: 349 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,349 @@
name: Release

on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+*'

env:
CARGO_TERM_COLOR: always

jobs:
create-release:
name: Create GitHub Release
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
release_id: ${{ steps.create_release.outputs.id }}
upload_url: ${{ steps.create_release.outputs.upload_url }}
steps:
- uses: actions/checkout@v4

- name: Verify updater pubkey is configured
run: |
if grep -q 'REPLACE_BEFORE_FIRST_RELEASE' src-tauri/tauri.conf.json; then
echo "ERROR: Updater pubkey is still a placeholder."
echo "Run 'npx tauri signer generate' and update src-tauri/tauri.conf.json"
exit 1
fi

- name: Extract version and changelog
id: meta
env:
TAG_NAME: ${{ github.ref_name }}
run: |
VERSION="${TAG_NAME#v}"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
BODY=$(sed -n "/^## \[$VERSION\]/,/^## \[/{ /^## \[$VERSION\]/d; /^## \[/d; p }" CHANGELOG.md)
if [ -z "$BODY" ]; then
BODY="Release $VERSION"
fi
{
echo "body<<EOF"
echo "$BODY"
echo "EOF"
} >> "$GITHUB_OUTPUT"

- name: Create GitHub Release
id: create_release
uses: softprops/action-gh-release@v2
with:
name: Vortex ${{ github.ref_name }}
body: ${{ steps.meta.outputs.body }}
draft: false
prerelease: ${{ contains(github.ref_name, '-') }}
token: ${{ secrets.GITHUB_TOKEN }}

build-tauri-linux:
name: Build (Linux)
needs: create-release
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4

- name: Install Linux dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
libwebkit2gtk-4.1-dev \
build-essential \
libssl-dev \
libappindicator3-dev \
librsvg2-dev

- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm

- name: Install frontend dependencies
run: npm ci

- uses: dtolnay/rust-toolchain@stable

- uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri

- name: Build Tauri app
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
run: npx tauri build

- name: Upload .deb to release
uses: softprops/action-gh-release@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
files: src-tauri/target/release/bundle/deb/*.deb

- name: Upload .rpm to release
uses: softprops/action-gh-release@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
files: src-tauri/target/release/bundle/rpm/*.rpm

- name: Upload updater bundle to release
uses: softprops/action-gh-release@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
files: |
src-tauri/target/release/bundle/appimage/*.AppImage
src-tauri/target/release/bundle/appimage/*.AppImage.sig

build-tauri-macos:
name: Build (macOS)
needs: create-release
runs-on: macos-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm

- name: Install frontend dependencies
run: npm ci

- uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-apple-darwin,x86_64-apple-darwin

- uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri

- name: Import code signing certificate
env:
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
MACOS_KEYCHAIN_PASSWD: ${{ secrets.MACOS_KEYCHAIN_PASSWD }}
MACOS_CERTIFICATE_PASSWD: ${{ secrets.MACOS_CERTIFICATE_PASSWD }}
run: |
echo "$MACOS_CERTIFICATE" | base64 --decode > certificate.p12
security create-keychain -p "$MACOS_KEYCHAIN_PASSWD" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$MACOS_KEYCHAIN_PASSWD" build.keychain
security import certificate.p12 \
-k build.keychain \
-P "$MACOS_CERTIFICATE_PASSWD" \
-T /usr/bin/codesign
security set-key-partition-list \
-S apple-tool:,apple: \
-s -k "$MACOS_KEYCHAIN_PASSWD" build.keychain
rm certificate.p12

- name: Build Tauri app
env:
APPLE_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
run: npx tauri build --target universal-apple-darwin

- name: Notarize .dmg
env:
Comment on lines +132 to +171
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 macOS builds only one architecture; latest.json references both

The Rust toolchain is configured with targets: aarch64-apple-darwin, but npx tauri build is called without --target, so it builds only for the runner's native architecture. On macos-latest (Apple Silicon), only an aarch64 DMG is produced. The update-updater job then emits a latest.json that includes both darwin-x86_64 and darwin-aarch64 entries, but the x86_64 URL will 404 and its signature will be empty.

To produce both artifacts in a single job, use the universal target:

- name: Build Tauri app (universal)
  env:
    APPLE_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
    APPLE_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWD }}
    APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
  run: npx tauri build --target universal-apple-darwin

Then update the latest.json generation to reference a single universal URL, or split into two separate jobs (one on macos-13 for x86_64, one on macos-latest for aarch64).

Fix in Claude Code

APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
DMG=$(find src-tauri/target/universal-apple-darwin/release/bundle/dmg -name '*.dmg' | head -1)
xcrun notarytool submit "$DMG" \
--apple-id "$APPLE_ID" \
--password "$APPLE_ID_PASSWORD" \
--team-id "$APPLE_TEAM_ID" \
--wait
xcrun stapler staple "$DMG"

- name: Upload .dmg to release
uses: softprops/action-gh-release@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
files: src-tauri/target/universal-apple-darwin/release/bundle/dmg/*.dmg

- name: Upload updater bundle to release
uses: softprops/action-gh-release@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
files: |
src-tauri/target/universal-apple-darwin/release/bundle/macos/*.app.tar.gz
src-tauri/target/universal-apple-darwin/release/bundle/macos/*.app.tar.gz.sig

build-tauri-windows:
name: Build (Windows)
needs: create-release
runs-on: windows-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm

- name: Install frontend dependencies
run: npm ci

- uses: dtolnay/rust-toolchain@stable

- uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri

- name: Import signing certificate
env:
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
run: |
$cert = [System.Convert]::FromBase64String("$env:WINDOWS_CERTIFICATE")
[System.IO.File]::WriteAllBytes("certificate.p12", $cert)

- name: Build Tauri app
env:
WINDOWS_CERTIFICATE_FILE: certificate.p12
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWD }}
WINDOWS_SIGNING_THUMBPRINT: ${{ secrets.WINDOWS_SIGNING_THUMBPRINT }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
run: npx tauri build

- name: Clean up certificate
if: always()
run: Remove-Item -Path certificate.p12 -ErrorAction SilentlyContinue

- name: Upload .msi to release
uses: softprops/action-gh-release@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
files: src-tauri/target/release/bundle/msi/*.msi

- name: Upload updater bundle to release
uses: softprops/action-gh-release@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
files: |
src-tauri/target/release/bundle/nsis/*.exe
src-tauri/target/release/bundle/nsis/*.exe.sig

publish-flatpak:
name: Publish Flatpak
needs: create-release
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4

- name: Install Flatpak tools
run: |
sudo apt-get update
sudo apt-get install -y flatpak flatpak-builder
flatpak remote-add --if-not-exists --user flathub https://dl.flathub.org/repo/flathub.flatpakrepo

- name: Build Flatpak bundle
run: |
mkdir -p _repo
flatpak-builder --force-clean --repo=_repo --user --install-deps-from=flathub \
_flatpak_build contrib/flatpak/org.vortex.Vortex.yml
flatpak build-bundle _repo vortex.flatpak org.vortex.Vortex

- name: Upload .flatpak to release
uses: softprops/action-gh-release@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
files: vortex.flatpak
Comment on lines +257 to +281
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 npm ci --offline inside Flatpak sandbox will always fail

The Flatpak manifest runs npm ci --offline, but the module definition contains no pre-generated offline npm sources (produced by flatpak-node-generator). Inside the Flatpak build sandbox there is no network access and no pre-populated npm cache, so npm ci --offline exits with an error — the publish-flatpak job will fail on every tag push.

The comment in the manifest acknowledges this ("For Flathub submission, use flatpak-node-generator…"), but the CI workflow doesn't guard against it. Options:

  1. Run flatpak-builder with --share=network (acceptable for non-Flathub CI builds) and drop the --offline flag from the manifest's build command.
  2. Pre-generate the sources with flatpak-node-generator package-lock.json -o contrib/flatpak/node-sources.json and add a flatpak-builder-tools source entry referencing it.

Fix in Claude Code


update-updater:
name: Update updater manifest
needs: [build-tauri-linux, build-tauri-macos, build-tauri-windows]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4

- name: Download signature files from release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG_NAME: ${{ github.ref_name }}
run: |
mkdir -p sigs
gh release download "$TAG_NAME" --pattern '*.sig' --dir sigs || true

- name: Generate latest.json
env:
TAG_NAME: ${{ github.ref_name }}
run: |
VERSION="${TAG_NAME#v}"
PUB_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
NOTES=$(sed -n "/^## \[$VERSION\]/,/^## \[/{ /^## \[$VERSION\]/d; /^## \[/d; p }" CHANGELOG.md | head -20)
REPO="https://github.com/mpiton/vortex"

# Read signatures from .sig files (each build job uploads specific .sig files)
LINUX_SIG=$(cat sigs/*.AppImage.sig 2>/dev/null || echo "")
MACOS_SIG=$(cat sigs/*.app.tar.gz.sig 2>/dev/null || echo "")
WINDOWS_SIG=$(cat sigs/*.exe.sig 2>/dev/null || echo "")

# Validate signatures — fail if any are missing
for sig_name in LINUX_SIG MACOS_SIG WINDOWS_SIG; do
eval "val=\$$sig_name"
if [ -z "$val" ]; then
echo "ERROR: Missing signature: $sig_name"
exit 1
fi
done

cat > latest.json <<EOF
{
"version": "$VERSION",
"notes": $(echo "$NOTES" | python3 -c "import sys, json; print(json.dumps(sys.stdin.read().strip()))"),
"pub_date": "$PUB_DATE",
"platforms": {
"linux-x86_64": {
"url": "$REPO/releases/download/$TAG_NAME/vortex_${VERSION}_amd64.AppImage",
"signature": "$LINUX_SIG"
},
"darwin-universal": {
"url": "$REPO/releases/download/$TAG_NAME/Vortex.app.tar.gz",
"signature": "$MACOS_SIG"
},
"windows-x86_64": {
"url": "$REPO/releases/download/$TAG_NAME/Vortex_${VERSION}_x64-setup.exe",
"signature": "$WINDOWS_SIG"
}
}
}
EOF

- name: Upload latest.json to release
uses: softprops/action-gh-release@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
files: latest.json
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `VortexArchiveExtractor` composite: format routing, recursive extraction with configurable depth
- `ExtractArchiveCommand` CQRS handler with `spawn_blocking` for CPU-bound extraction
- `ListArchiveContentsQuery` handler for archive preview without extraction
- i18n & advanced theming (Task 27)
- `react-i18next` + `i18next` + `i18next-browser-languagedetector` installed for internationalisation
- `src/i18n/i18n.ts`: i18next instance initialized with `LanguageDetector` (localStorage → navigator fallback)
- `src/i18n/locales/en.json` and `src/i18n/locales/fr.json`: complete English and French translations covering navigation, all settings sections, downloads search, and media grabber dialogs
- `src/hooks/useLanguage.ts`: `useLanguage()` hook for language switching — calls `i18n.changeLanguage()` and persists `locale` to backend config via `settingsStore.updateConfig()`
- `src/hooks/useAppEffects.ts`: `useAppEffects()` hook applying DOM side-effects on config changes — toggles `compact-mode` class on `<body>` and sets `--color-accent` CSS variable on `:root`
- All hardcoded UI strings replaced with `t('key')` calls: navigation labels (Sidebar), settings tabs and all 6 settings sections, downloads search bar, media grabber dialog
- `src/types/layout.ts`: `RouteConfig.label` renamed to `labelKey` (i18n translation key), Sidebar uses `t(route.labelKey)`
- `src/App.tsx`: `import './i18n/i18n'` added as first import to ensure i18n is initialized before rendering
- `src/layouts/AppLayout.tsx`: loads `settings_get` on mount, feeds result to `settingsStore` for `useAppEffects` to pick up initial compact mode and accent color
- `src/index.css`: `body.compact-mode` selector with reduced font size, line height, and spacing overrides
- Accent color runtime: changing accent color preset updates `--color-accent` CSS variable immediately without reload
- `UpdateConfigCommand` locale validation: rejects locales not in `["en", "fr", "de", "es", "ja", "zh"]`
- `src/test-setup.ts`: global `react-i18next` mock returning English translation values via key lookup so all existing tests continue passing
- 17 new frontend tests: `useLanguage` (4), `useAppEffects` (5), translation key parity en↔fr (8)
- 2 new Rust tests: `test_handle_update_config_rejects_invalid_locale`, `test_handle_update_config_accepts_valid_locale`
- Release & distribution pipeline (Task 28)
- `.github/workflows/release.yml`: triggered on `v*.*.*` tags, 6 jobs
- `create-release`: extracts changelog body from CHANGELOG.md, creates GitHub Release
- `build-tauri-linux`: builds .deb and .rpm, uploads to release
- `build-tauri-macos`: builds .dmg with code signing + notarization via xcrun notarytool, uploads to release
- `build-tauri-windows`: builds .msi with certificate import, uploads to release
- `publish-flatpak`: builds Flatpak bundle from manifest, uploads to release
- `update-updater`: generates `latest.json` updater manifest and uploads to release
- Tauri in-app updater configured in `tauri.conf.json` (plugins.updater, endpoint → GitHub Releases)
- `tauri-plugin-updater` added to Cargo.toml dependencies
- `contrib/vortex.service` — systemd user unit for headless/autostart scenarios
- `contrib/vortex.desktop` — Freedesktop .desktop entry (MimeType magnet + uri-list)
- `contrib/flatpak/org.vortex.Vortex.yml` — Flatpak manifest (runtime 23.08, Rust + Node 22 SDK)
- `contrib/icons/README.md` — icon generation instructions via `npx tauri icon`
- `contrib/winget/Vortex.yaml` — Winget manifest template (TODO placeholders for future submission)
- `contrib/homebrew/vortex.rb` — Homebrew cask template (TODO placeholders for future submission)
Loading
Loading