Build Lotus Chat Desktop / prepare (push) Successful in 8s
Build Lotus Chat Desktop / build-linux (push) Successful in 30m37s
Build Lotus Chat Desktop / build-arch (push) Successful in 11s
Build Lotus Chat Desktop / build-windows (push) Successful in 32m6s
Build Lotus Chat Desktop / update-manifest (push) Successful in 8s
The web deploy serves a config.json kept on the server (not in git), which is where the Giphy key lives. The desktop app bundles the repo's config.json, whose gifApiKey is empty, so on desktop the GIF button never appeared even with Settings → GIF Picker switched on. scripts/sync-web-config.mjs copies an allow-list of deployment-only keys (just gifApiKey) from https://chat.lotusguild.org/config.json into cinny/config.json before the Windows and Linux builds. The key is public already (served to every web visitor; Giphy keys are client keys). A fetch failure is a warning, not a build failure. Tested locally: sets the key, is a no-op on a second run, and skips cleanly on a 404. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
397 lines
18 KiB
YAML
397 lines
18 KiB
YAML
name: Build Lotus Chat Desktop
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
# Docs and the catch-up workflow itself don't need a ~30-min Tauri build.
|
|
paths-ignore:
|
|
- '**.md'
|
|
- '.gitea/workflows/catch-up.yml'
|
|
workflow_dispatch:
|
|
|
|
env:
|
|
GITEA_URL: https://code.lotusguild.org
|
|
REPO: LotusGuild/cinny-desktop
|
|
|
|
# Continuous "latest" builds track lotus HEAD, so only the newest submodule bump
|
|
# needs to build. Cancel a superseded in-flight release: rapid lotus pushes would
|
|
# otherwise queue several ~30-min Tauri builds back-to-back, and build-linux runs
|
|
# on ubuntu-latest — the same runner pool as cinny's web CI — so the queue also
|
|
# starves web CI/deploys. update-manifest only publishes release.json on full
|
|
# success, so a cancelled run leaves the last good manifest untouched.
|
|
concurrency:
|
|
group: desktop-release-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
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
|
|
|
|
# Deployment-only config (e.g. the Giphy key) lives on the web server,
|
|
# not in git; copy it in so the desktop GIF picker works.
|
|
- name: Sync web config
|
|
shell: powershell
|
|
run: node scripts/sync-web-config.mjs
|
|
|
|
- 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: Sync web config
|
|
run: node scripts/sync-web-config.mjs
|
|
|
|
- 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"
|
|
|
|
build-arch:
|
|
# Needs build-linux's .deb to already be on the release (this job
|
|
# downloads it from the fixed "latest" asset URL, same as update-manifest
|
|
# does for signatures) rather than recompiling from source.
|
|
needs: [prepare, build-linux]
|
|
runs-on: ubuntu-latest
|
|
container:
|
|
image: archlinux:base-devel
|
|
steps:
|
|
# actions/checkout@v4 is a Node.js action and archlinux:base-devel has
|
|
# no Node runtime — it fails with "node: executable file not found".
|
|
# This job only needs one file, so skip checkout and fetch it directly
|
|
# from Gitea's raw endpoint instead.
|
|
- name: Install build tools
|
|
run: pacman -Sy --noconfirm --needed curl python
|
|
|
|
- name: Fetch PKGBUILD
|
|
run: |
|
|
mkdir -p packaging/pacman
|
|
curl -sf "$GITEA_URL/LotusGuild/cinny-desktop/raw/branch/main/packaging/pacman/PKGBUILD" \
|
|
-o packaging/pacman/PKGBUILD
|
|
|
|
- name: Set package version
|
|
env:
|
|
VERSION: ${{ needs.prepare.outputs.version }}
|
|
run: sed -i "s/^pkgver=.*/pkgver=$VERSION/" packaging/pacman/PKGBUILD
|
|
|
|
- name: Build .pkg.tar.zst
|
|
run: |
|
|
# makepkg refuses to run as root. --nodeps: the package() step just
|
|
# extracts the .deb payload, so the runtime `depends` (webkit2gtk,
|
|
# gstreamer, ...) don't need to be installed on the build container.
|
|
useradd -m builder
|
|
chown -R builder:builder packaging/pacman
|
|
su builder -c 'cd packaging/pacman && makepkg -f --noconfirm --nodeps'
|
|
|
|
- name: Upload to release
|
|
env:
|
|
TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
|
RELEASE_ID: ${{ needs.prepare.outputs.release_id }}
|
|
run: |
|
|
PKG=$(ls packaging/pacman/*.pkg.tar.zst)
|
|
NAME="LotusChat-x86_64.pkg.tar.zst"
|
|
|
|
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
|
|
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 @"$PKG"
|
|
|
|
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
|