f883781c1f
From the deep-audit wave (reviewer-verified: capability identifiers valid, no removed-crate references, GDI free ordering correct): - Removed 8 never-registered plugins (clipboard-manager, fs, shell, http, process, os, dialog, global-shortcut) from Cargo.toml AND their capability grants (shell:allow-execute, unscoped fs writes, http:default, …) — verified the web never invokes any of them. A latent RCE-class surface is gone. - on_new_window: only http/https/mailto reach the OS opener (file:///custom schemes previously bypassed the opener capability scope entirely). - set_badge_count: freed hdc + hdc_screen on all three GDI error paths (leaked per badge update in a long-running tray app). - 8s reveal failsafe gated by an AtomicBool: no longer re-shows a window the user closed to tray; page-load reveal now fires once only (logout reloads don't re-surface a tray-hidden window); recovery for a missed page-load event preserved. - toast.rs: store pruned on Activated too + capped at 20 (was unbounded). - Startup no longer panics when the bundled icon is missing (tray skipped gracefully); msSmartScreenProtection no longer disabled (throttling disables kept); rust-version corrected to 1.77.2. - release.yml update-manifest: fails on empty signatures (was: could publish a manifest that traps Windows users in a failed-update loop); partial- failure window documented. Deleted the stale upstream tauri.yml workflow. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
317 lines
14 KiB
YAML
317 lines
14 KiB
YAML
name: Build Lotus Chat Desktop
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
workflow_dispatch:
|
|
|
|
env:
|
|
GITEA_URL: https://code.lotusguild.org
|
|
REPO: LotusGuild/cinny-desktop
|
|
|
|
jobs:
|
|
prepare:
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
version: ${{ steps.ver.outputs.version }}
|
|
release_id: ${{ steps.release.outputs.release_id }}
|
|
steps:
|
|
- name: Compute version
|
|
id: ver
|
|
run: echo "version=4.12.${{ github.run_number }}" >> $GITHUB_OUTPUT
|
|
|
|
- name: Create or update release
|
|
id: release
|
|
env:
|
|
TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
|
# NOTE (partial-failure window): this renames the `latest` release's
|
|
# name/body to the new version up front, before the platform builds run.
|
|
# If a build later fails, the release keeps the OLD binary assets but the
|
|
# NEW name. That's cosmetic: the auto-updater reads release.json, which is
|
|
# only regenerated by the update-manifest job — and that job `needs:` BOTH
|
|
# build-windows and build-linux, so a failed/skipped build prevents any
|
|
# manifest (and therefore updater) change. Clients keep the last good
|
|
# release.json until a fully successful run replaces it.
|
|
run: |
|
|
VERSION="4.12.${{ github.run_number }}"
|
|
EXISTING=$(curl -sf "$GITEA_URL/api/v1/repos/$REPO/releases/tags/latest" \
|
|
-H "Authorization: token $TOKEN" 2>/dev/null || true)
|
|
RELEASE_ID=$(echo "$EXISTING" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null || true)
|
|
if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "None" ] && [ "$RELEASE_ID" != "" ]; then
|
|
curl -sf -X PATCH "$GITEA_URL/api/v1/repos/$REPO/releases/$RELEASE_ID" \
|
|
-H "Authorization: token $TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"name\":\"Lotus Chat $VERSION\",\"body\":\"Built from ${{ github.sha }}\"}" > /dev/null
|
|
else
|
|
RELEASE_ID=$(curl -sf -X POST "$GITEA_URL/api/v1/repos/$REPO/releases" \
|
|
-H "Authorization: token $TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"tag_name\":\"latest\",\"name\":\"Lotus Chat $VERSION\",\"prerelease\":true,\"body\":\"Built from ${{ github.sha }}\"}" \
|
|
| python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
|
|
fi
|
|
echo "release_id=$RELEASE_ID" >> $GITHUB_OUTPUT
|
|
|
|
build-windows:
|
|
needs: prepare
|
|
runs-on: windows
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version-file: .node-version
|
|
|
|
- name: Checkout submodules (shallow)
|
|
shell: powershell
|
|
run: git submodule update --init --depth=1
|
|
|
|
- name: Patch version
|
|
shell: powershell
|
|
run: |
|
|
$ver = '${{ needs.prepare.outputs.version }}'
|
|
node -e "const fs=require('fs');const d=JSON.parse(fs.readFileSync('src-tauri/tauri.conf.json','utf8'));d.version='$ver';fs.writeFileSync('src-tauri/tauri.conf.json',JSON.stringify(d,null,2),'utf8');"
|
|
|
|
- uses: Swatinem/rust-cache@v2
|
|
with:
|
|
workspaces: src-tauri
|
|
|
|
- name: Install frontend deps
|
|
shell: powershell
|
|
run: cd cinny; npm ci
|
|
|
|
- name: Install Tauri deps
|
|
shell: powershell
|
|
run: npm ci
|
|
|
|
- name: Build
|
|
shell: powershell
|
|
env:
|
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ''
|
|
NODE_OPTIONS: '--max_old_space_size=4096'
|
|
# Sparse registry avoids a full git clone of the crates.io index —
|
|
# eliminates the curl SSL handshake failures seen on Windows runners.
|
|
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
|
|
# Disable HTTP/2 multiplexing — ALPN negotiation can reset on Windows Schannel.
|
|
CARGO_HTTP_MULTIPLEXING: 'false'
|
|
# Retry transient network errors before failing.
|
|
CARGO_NET_RETRY: '5'
|
|
run: |
|
|
# USERPROFILE is set by Windows directly; more reliable than C:\Users\$USERNAME
|
|
$env:PATH = "$env:USERPROFILE\.cargo\bin;$env:PATH"
|
|
# Also add the actual toolchain bin to bypass rustup shim execution issues
|
|
$toolchain = Get-ChildItem "$env:USERPROFILE\.rustup\toolchains" -Directory -ErrorAction SilentlyContinue |
|
|
Where-Object { $_.Name -match 'stable' } | Select-Object -First 1
|
|
if ($toolchain) { $env:PATH = "$($toolchain.FullName)\bin;$env:PATH" }
|
|
Write-Host "cargo: $((Get-Command cargo -ErrorAction SilentlyContinue).Source)"
|
|
cargo --version
|
|
npm run tauri -- build --bundles nsis
|
|
|
|
- name: Upload to release
|
|
shell: powershell
|
|
env:
|
|
TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
|
RELEASE_ID: ${{ needs.prepare.outputs.release_id }}
|
|
VERSION: ${{ needs.prepare.outputs.version }}
|
|
run: |
|
|
$releaseId = $env:RELEASE_ID
|
|
$VERSION = $env:VERSION
|
|
Write-Host "Version: $VERSION Release: $releaseId"
|
|
|
|
$nsis = "src-tauri\target\release\bundle\nsis"
|
|
$files = @(
|
|
"$nsis\Lotus Chat_${VERSION}_x64-setup.exe",
|
|
"$nsis\Lotus Chat_${VERSION}_x64-setup.nsis.zip",
|
|
"$nsis\Lotus Chat_${VERSION}_x64-setup.nsis.zip.sig"
|
|
)
|
|
$names = @("LotusChat-x86_64-setup.exe", "LotusChat-x86_64-setup.nsis.zip", "LotusChat-x86_64-setup.nsis.zip.sig")
|
|
for ($i = 0; $i -lt $files.Length; $i++) {
|
|
$existing = (Invoke-RestMethod -Uri "$env:GITEA_URL/api/v1/repos/$env:REPO/releases/$releaseId/assets" `
|
|
-Headers @{ Authorization = "token $env:TOKEN" }) | Where-Object { $_.name -eq $names[$i] }
|
|
if ($existing) {
|
|
Invoke-RestMethod -Uri "$env:GITEA_URL/api/v1/repos/$env:REPO/releases/$releaseId/assets/$($existing.id)" `
|
|
-Method Delete -Headers @{ Authorization = "token $env:TOKEN" }
|
|
}
|
|
$bytes = [System.IO.File]::ReadAllBytes($files[$i])
|
|
Invoke-RestMethod -Uri "$env:GITEA_URL/api/v1/repos/$env:REPO/releases/$releaseId/assets?name=$($names[$i])" `
|
|
-Method Post `
|
|
-Headers @{ Authorization = "token $env:TOKEN"; "Content-Type" = "application/octet-stream" } `
|
|
-Body $bytes
|
|
}
|
|
|
|
build-linux:
|
|
needs: prepare
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
submodules: true
|
|
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version-file: .node-version
|
|
|
|
- name: Install system deps
|
|
run: |
|
|
apt-get update
|
|
apt-get install -y \
|
|
curl wget file gcc imagemagick \
|
|
libwebkit2gtk-4.1-dev \
|
|
libssl-dev \
|
|
libxdo-dev \
|
|
libayatana-appindicator3-dev \
|
|
librsvg2-dev \
|
|
patchelf \
|
|
xdg-utils \
|
|
squashfs-tools
|
|
|
|
- name: Ensure icons are RGBA PNG
|
|
run: |
|
|
for f in src-tauri/icons/*.png; do
|
|
info=$(identify -verbose "$f" 2>/dev/null | grep "Type:" | head -1)
|
|
echo "$f: $info"
|
|
convert "$f" -type TrueColorAlpha PNG32:"$f"
|
|
done
|
|
|
|
- name: Set up Rust toolchain
|
|
run: |
|
|
source "$HOME/.cargo/env" 2>/dev/null || true
|
|
if command -v cargo >/dev/null 2>&1; then
|
|
echo "Using existing Rust: $(cargo --version)"
|
|
else
|
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path --default-toolchain stable --profile minimal
|
|
source "$HOME/.cargo/env"
|
|
fi
|
|
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
|
|
|
- uses: Swatinem/rust-cache@v2
|
|
with:
|
|
workspaces: src-tauri
|
|
|
|
- name: Patch version
|
|
run: python3 -c "import json; d=json.load(open('src-tauri/tauri.conf.json')); d['version']='${{ needs.prepare.outputs.version }}'; open('src-tauri/tauri.conf.json','w').write(json.dumps(d,indent=2))"
|
|
|
|
- name: Install frontend deps
|
|
run: cd cinny && npm ci
|
|
|
|
- name: Install Tauri deps
|
|
run: npm ci
|
|
|
|
- name: Stage AppRun and linuxdeploy for AppImage bundler
|
|
run: |
|
|
set -e
|
|
mkdir -p ~/.cache/tauri
|
|
cp tools/AppRun-x86_64 ~/.cache/tauri/AppRun-x86_64
|
|
chmod +x ~/.cache/tauri/AppRun-x86_64
|
|
|
|
wget -q \
|
|
"https://github.com/tauri-apps/binary-releases/releases/download/linuxdeploy/linuxdeploy-x86_64.AppImage" \
|
|
-O /tmp/linuxdeploy.AppImage
|
|
chmod +x /tmp/linuxdeploy.AppImage
|
|
|
|
rm -rf /root/linuxdeploy-root
|
|
(cd /root && /tmp/linuxdeploy.AppImage --appimage-extract)
|
|
mv /root/squashfs-root /root/linuxdeploy-root
|
|
echo "Extracted linuxdeploy:"
|
|
ls /root/linuxdeploy-root/
|
|
ls /root/linuxdeploy-root/usr/bin/ 2>/dev/null || echo "no usr/bin"
|
|
|
|
# Pre-stage plugin scripts next to linuxdeploy so it finds them via /proc/self/exe lookup
|
|
wget -q "https://raw.githubusercontent.com/tauri-apps/linuxdeploy-plugin-gtk/master/linuxdeploy-plugin-gtk.sh" \
|
|
-O /root/linuxdeploy-root/usr/bin/linuxdeploy-plugin-gtk.sh
|
|
wget -q "https://raw.githubusercontent.com/tauri-apps/linuxdeploy-plugin-gstreamer/master/linuxdeploy-plugin-gstreamer.sh" \
|
|
-O /root/linuxdeploy-root/usr/bin/linuxdeploy-plugin-gstreamer.sh
|
|
chmod +x /root/linuxdeploy-root/usr/bin/linuxdeploy-plugin-gtk.sh \
|
|
/root/linuxdeploy-root/usr/bin/linuxdeploy-plugin-gstreamer.sh
|
|
|
|
gcc -o ~/.cache/tauri/linuxdeploy-x86_64.AppImage tools/ld_wrapper.c
|
|
chmod +x ~/.cache/tauri/linuxdeploy-x86_64.AppImage
|
|
|
|
- name: Build
|
|
env:
|
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ''
|
|
NODE_OPTIONS: '--max_old_space_size=4096'
|
|
RUST_LOG: tauri_bundler=debug
|
|
run: npm run tauri -- build --bundles appimage,deb
|
|
|
|
- name: Show linuxdeploy wrapper log
|
|
if: always()
|
|
run: cat /tmp/ld-wrapper.log 2>/dev/null || echo "no wrapper log found"
|
|
|
|
- name: Upload to release
|
|
env:
|
|
TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
|
RELEASE_ID: ${{ needs.prepare.outputs.release_id }}
|
|
VERSION: ${{ needs.prepare.outputs.version }}
|
|
run: |
|
|
APPIMAGE_DIR="src-tauri/target/release/bundle/appimage"
|
|
DEB_DIR="src-tauri/target/release/bundle/deb"
|
|
|
|
upload() {
|
|
local name="$1" path="$2"
|
|
local existing_id
|
|
existing_id=$(curl -sf "$GITEA_URL/api/v1/repos/$REPO/releases/$RELEASE_ID/assets" \
|
|
-H "Authorization: token $TOKEN" \
|
|
| python3 -c "import sys,json; assets=json.load(sys.stdin); print(next((str(a['id']) for a in assets if a['name']=='$name'), ''))" 2>/dev/null || true)
|
|
if [ -n "$existing_id" ]; then
|
|
curl -sf -X DELETE "$GITEA_URL/api/v1/repos/$REPO/releases/$RELEASE_ID/assets/$existing_id" \
|
|
-H "Authorization: token $TOKEN" || true
|
|
fi
|
|
echo "Uploading $name"
|
|
curl -sf -X POST \
|
|
"$GITEA_URL/api/v1/repos/$REPO/releases/$RELEASE_ID/assets?name=$name" \
|
|
-H "Authorization: token $TOKEN" \
|
|
-H "Content-Type: application/octet-stream" \
|
|
--data-binary @"$path"
|
|
}
|
|
|
|
upload "LotusChat-x86_64.AppImage" "$APPIMAGE_DIR/Lotus Chat_${VERSION}_amd64.AppImage"
|
|
upload "LotusChat-x86_64.AppImage.tar.gz" "$APPIMAGE_DIR/Lotus Chat_${VERSION}_amd64.AppImage.tar.gz"
|
|
upload "LotusChat-x86_64.AppImage.tar.gz.sig" "$APPIMAGE_DIR/Lotus Chat_${VERSION}_amd64.AppImage.tar.gz.sig"
|
|
upload "LotusChat-x86_64.deb" "$DEB_DIR/Lotus Chat_${VERSION}_amd64.deb"
|
|
|
|
update-manifest:
|
|
needs: [prepare, build-windows, build-linux]
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Generate and upload release.json
|
|
env:
|
|
TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
|
RELEASE_ID: ${{ needs.prepare.outputs.release_id }}
|
|
VERSION: ${{ needs.prepare.outputs.version }}
|
|
run: |
|
|
BASE="$GITEA_URL/LotusGuild/cinny-desktop/releases/download/latest"
|
|
|
|
WIN_SIG=$(curl -sf "$BASE/LotusChat-x86_64-setup.nsis.zip.sig")
|
|
LIN_SIG=$(curl -sf "$BASE/LotusChat-x86_64.AppImage.tar.gz.sig")
|
|
|
|
# Never publish a manifest with a missing/empty signature: the updater
|
|
# would reject (or worse, accept an unsigned) artifact. Fail the job so
|
|
# the previous good release.json stays in place.
|
|
[ -n "$WIN_SIG" ] || { echo "ERROR: empty Windows signature" >&2; exit 1; }
|
|
[ -n "$LIN_SIG" ] || { echo "ERROR: empty Linux signature" >&2; exit 1; }
|
|
|
|
DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
|
|
python3 -c "import json,sys; v,d,wu,ws,lu,ls=sys.argv[1:]; print(json.dumps({'version':v,'notes':'Latest Lotus Chat release','pub_date':d,'platforms':{'windows-x86_64':{'url':wu,'signature':ws},'linux-x86_64':{'url':lu,'signature':ls}}},indent=2))" \
|
|
"$VERSION" "$DATE" \
|
|
"$BASE/LotusChat-x86_64-setup.nsis.zip" "$WIN_SIG" \
|
|
"$BASE/LotusChat-x86_64.AppImage.tar.gz" "$LIN_SIG" \
|
|
> release.json
|
|
|
|
cat release.json
|
|
|
|
OLD=$(curl -sf "$GITEA_URL/api/v1/repos/$REPO/releases/$RELEASE_ID/assets" \
|
|
-H "Authorization: token $TOKEN" \
|
|
| python3 -c "import sys,json; print(next((str(a['id']) for a in json.load(sys.stdin) if a['name']=='release.json'), ''))" 2>/dev/null || true)
|
|
[ -n "$OLD" ] && curl -sf -X DELETE \
|
|
"$GITEA_URL/api/v1/repos/$REPO/releases/$RELEASE_ID/assets/$OLD" \
|
|
-H "Authorization: token $TOKEN" || true
|
|
|
|
curl -sf -X POST \
|
|
"$GITEA_URL/api/v1/repos/$REPO/releases/$RELEASE_ID/assets?name=release.json" \
|
|
-H "Authorization: token $TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
--data-binary @release.json
|