From 0f34cff389780948bcc0429f656ebbd04472385d Mon Sep 17 00:00:00 2001 From: Namnarak Date: Fri, 13 Nov 2026 15:44:10 +0700 Subject: [PATCH 1/2] fix(updater): resolve AppImage update feed from GitHub Releases Packaged AppImage builds always reported "Update feed is not configured" because the feed required LINEY_UPDATE_FEED_URL and build.publish was null, so no latest-linux.yml was ever generated. Default to the baked-in github provider (Cytech-Team/Liney) with a pure resolver (electron/updater-config.mjs + tests), keep LINEY_UPDATE_FEED_URL as a generic-feed override, stop mapping autoUpdater.channel to preview/stable (electron-builder never emits preview-linux.yml), and upload *.yml/*.blockmap as release assets. --- .github/workflows/release.yml | 7 +++- docs/UPDATE_FEED.md | 61 +++++++++++++++++++++++++++ electron/main.mjs | 34 ++++++++++----- electron/updater-config.mjs | 60 +++++++++++++++++++++++++++ electron/updater-config.test.ts | 71 ++++++++++++++++++++++++++++++++ package.json | 6 ++- scripts/verify-linux-packages.sh | 9 ++++ 7 files changed, 236 insertions(+), 12 deletions(-) create mode 100644 docs/UPDATE_FEED.md create mode 100644 electron/updater-config.mjs create mode 100644 electron/updater-config.test.ts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6d43996..5ad88d7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -43,6 +43,8 @@ jobs: release-electron/*.AppImage release-electron/*.deb release-electron/*.rpm + release-electron/*.yml + release-electron/*.blockmap if-no-files-found: error retention-days: 14 @@ -137,7 +139,8 @@ jobs: shell: bash run: | set -euo pipefail - ls -lh release-electron/*.AppImage release-electron/*.deb release-electron/*.rpm release-electron/*.flatpak + ls -lh release-electron/*.AppImage release-electron/*.deb release-electron/*.rpm release-electron/*.flatpak release-electron/*.yml + ls -lh release-electron/*.blockmap || echo "No blockmap (differential update disabled)" - name: Publish GitHub Release assets uses: softprops/action-gh-release@v2 with: @@ -147,3 +150,5 @@ jobs: release-electron/*.deb release-electron/*.rpm release-electron/*.flatpak + release-electron/*.yml + release-electron/*.blockmap diff --git a/docs/UPDATE_FEED.md b/docs/UPDATE_FEED.md new file mode 100644 index 0000000..8836404 --- /dev/null +++ b/docs/UPDATE_FEED.md @@ -0,0 +1,61 @@ +# Liney Electron Update Feed + +AppImage auto-update uses `electron-updater` with the GitHub Releases feed +baked into the packaged app. No manual `LINEY_UPDATE_FEED_URL` is needed for +normal installs. + +## How it works + +- `package.json → build.publish` points at `Cytech-Team/Liney` (`provider: github`). + `electron-builder` writes `release-electron/latest-linux.yml` (+ `.blockmap`) + alongside the AppImage. The `.yml` carries `version`, file `url`, `sha512` + and signature metadata. The updater verifies these before downloading. +- `electron/main.mjs` resolves the feed via `electron/updater-config.mjs`: + - dev (`!app.isPackaged`) → `unconfigured`, reason "disabled in development". + - packaged non-AppImage (deb/rpm/flatpak, no `$APPIMAGE`) → `unconfigured`, + reason "requires AppImage". Those formats update via apt/dnf/flatpak. + - packaged AppImage, no env override → `configured: true`, provider `github`. + - packaged AppImage + `LINEY_UPDATE_FEED_URL` → `configured: true`, + provider `generic`, URL `${feed}/${channel}/` expecting `latest-linux.yml`. +- `autoUpdater.channel` is left at the default (`latest`) on purpose so the + updater looks for `latest-linux.yml`. Setting it to `preview`/`stable` would + make it look for `preview-linux.yml`, which `electron-builder` never emits. + +## Preview vs Stable + +- Display channel (`preview`/`stable`) comes from `LINEY_UPDATE_CHANNEL` + (default `preview`). It only affects the About UI and `allowPrerelease`. +- `allowPrerelease = channel === "preview"`. + - Preview builds see prereleases + stable releases (newest first from feed). + - Stable builds (`LINEY_UPDATE_CHANNEL=stable`) see stable releases only + (`/releases/latest`). +- Channels stay separate via GitHub stable releases vs prereleases, matching + the About footnote. No extra hosting is required. + +## CI requirements + +- `release.yml` uploads `*.yml` + `*.blockmap` with the AppImage to the + GitHub Release. Without `latest-linux.yml` in the release assets, + `checkForUpdates()` reports `ERR_UPDATER_CHANNEL_FILE_NOT_FOUND`. +- `scripts/verify-linux-packages.sh` asserts `latest-linux.yml` exists, + contains `version: $VERSION` and a `sha512` entry. +- Renderer (`src/App.tsx` → About → Software Updates) already handles + `idle/checking/available/downloading/downloaded/current/unconfigured/error` + via `liney:updater:event`. No UI change needed. + +## Manual test + +```sh +# Source build (expect unconfigured, dev reason) +LINEY_DATA_DIR=$(mktemp -d) npm run electron:smoke + +# Packaged AppImage without network (expect checking → current/error, +# never "Update feed is not configured") +./release-electron/Liney-Electron-Preview-0.1.4-x86_64.AppImage --appimage-extract-and-run + +# Self-hosted generic feed override +LINEY_UPDATE_FEED_URL=https://updates.example.com \ +LINEY_UPDATE_CHANNEL=preview \ +./release-electron/Liney-Electron-Preview-0.1.4-x86_64.AppImage --appimage-extract-and-run +# Serve preview/latest-linux.yml + AppImage under https://updates.example.com/preview/ +``` diff --git a/electron/main.mjs b/electron/main.mjs index 88b6af8..519a261 100644 --- a/electron/main.mjs +++ b/electron/main.mjs @@ -13,6 +13,7 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; import { execFileSync } from "node:child_process"; import { getEditActionCalls, hasNativeEditShortcuts, installNativeEditMenu, installNativeEditShortcuts, resetEditActionCalls } from "./edit-menu.mjs"; +import { resolveUpdaterConfig } from "./updater-config.mjs"; protocol.registerSchemesAsPrivileged([ {scheme:"lineyapp",privileges:{standard:true,secure:true,supportFetchAPI:true,stream:true}} @@ -67,8 +68,13 @@ let quitting = false; let tray = null; let closeToTray = false; let toneTimer = null; -const updateChannel = process.env.LINEY_UPDATE_CHANNEL === "stable" ? "stable" : "preview"; -const updateFeedBase = String(process.env.LINEY_UPDATE_FEED_URL || "").trim().replace(/\/+$/, ""); +const updaterConfig = resolveUpdaterConfig({ + isPackaged: app.isPackaged, + appImagePath: process.env.APPIMAGE, + feedUrlEnv: process.env.LINEY_UPDATE_FEED_URL, + updateChannelEnv: process.env.LINEY_UPDATE_CHANNEL, +}); +const updateChannel = updaterConfig.displayChannel; let updateState = { status: "idle", channel: updateChannel, version: app.getVersion(), availableVersion: null, releaseNotes: "", percent: 0, error: null }; let updateConfigured = false; function publishUpdateState(patch) { @@ -76,12 +82,20 @@ function publishUpdateState(patch) { broadcast("liney:desktop:event", { name: "liney:updater:event", payload: updateState }); } function configureUpdater() { - if (!app.isPackaged || !updateFeedBase || process.env.APPIMAGE === undefined) return false; + if (!updaterConfig.configured) return false; autoUpdater.autoDownload = false; autoUpdater.autoInstallOnAppQuit = false; - autoUpdater.allowPrerelease = updateChannel === "preview"; - autoUpdater.channel = updateChannel; - autoUpdater.setFeedURL({ provider: "generic", url: `${updateFeedBase}/${updateChannel}/` }); + autoUpdater.allowPrerelease = updaterConfig.allowPrerelease; + // Keep autoUpdater.channel as default (latest) so both GitHub and generic + // feeds resolve `latest-linux.yml`. Setting it to "preview"/"stable" would + // make electron-updater look for `preview-linux.yml` which electron-builder + // never generates. Preview/Stable separation comes from GitHub stable vs + // prerelease tags (allowPrerelease) or from the generic URL subfolder. + if (updaterConfig.provider === "generic" && updaterConfig.url) { + autoUpdater.setFeedURL({ provider: "generic", url: updaterConfig.url }); + } + // Default GitHub provider needs no setFeedURL: electron-builder bakes + // `app-update.yml` (owner Cytech-Team, repo Liney) into the packaged app. autoUpdater.on("checking-for-update", () => publishUpdateState({ status: "checking", error: null })); autoUpdater.on("update-available", info => { const releaseNotes = typeof info.releaseNotes === "string" ? info.releaseNotes : Array.isArray(info.releaseNotes) ? info.releaseNotes.map(note => note.note || "").join("\n") : ""; @@ -92,16 +106,16 @@ function configureUpdater() { toast.show(); } }); - autoUpdater.on("update-not-available", info => publishUpdateState({ status: "current", availableVersion: info.version, releaseNotes: "", percent: 0 })); + autoUpdater.on("update-not-available", () => publishUpdateState({ status: "current", availableVersion: null, releaseNotes: "", percent: 0, error: null })); autoUpdater.on("download-progress", progress => publishUpdateState({ status: "downloading", percent: Math.max(0, Math.min(100, Math.round(progress.percent || 0))) })); - autoUpdater.on("update-downloaded", info => publishUpdateState({ status: "downloaded", availableVersion: info.version, percent: 100 })); - autoUpdater.on("error", error => publishUpdateState({ status: "error", error: String(error?.message || error).slice(0, 500) })); + autoUpdater.on("update-downloaded", info => publishUpdateState({ status: "downloaded", availableVersion: info.version, percent: 100, error: null })); + autoUpdater.on("error", error => publishUpdateState({ status: "error", availableVersion: null, releaseNotes: "", error: String(error?.message || error).slice(0, 500) })); updateConfigured = true; return true; } async function checkForUpdates() { if (!updateConfigured) { - const reason = !app.isPackaged ? "Updates are disabled in development builds" : process.env.APPIMAGE === undefined ? "Automatic updates require the AppImage package" : "Update feed is not configured"; + const reason = updaterConfig.reason || "Update feed is not configured"; publishUpdateState({ status: "unconfigured", error: reason }); return updateState; } diff --git a/electron/updater-config.mjs b/electron/updater-config.mjs new file mode 100644 index 0000000..ccf72f3 --- /dev/null +++ b/electron/updater-config.mjs @@ -0,0 +1,60 @@ +/** + * Pure update-feed resolution for Liney Electron. + * + * Renderer shows "Update feed is not configured" whenever the main process + * reports `configured: false`. Packaged AppImage builds must resolve to the + * baked-in GitHub Releases feed so auto-update works without manual env vars. + * System packages (deb/rpm/flatpak) update via their package manager. + */ + +const DEFAULT_OWNER = "Cytech-Team"; +const DEFAULT_REPO = "Liney"; + +export function resolveUpdaterConfig({ isPackaged, appImagePath, feedUrlEnv, updateChannelEnv }) { + const displayChannel = updateChannelEnv === "stable" ? "stable" : "preview"; + const allowPrerelease = displayChannel === "preview"; + + if (!isPackaged) { + return { + configured: false, + displayChannel, + allowPrerelease, + autoUpdaterChannel: null, + reason: "Updates are disabled in development builds", + }; + } + + if (appImagePath === undefined || appImagePath === null || appImagePath === "") { + return { + configured: false, + displayChannel, + allowPrerelease, + autoUpdaterChannel: null, + reason: "Automatic updates require the AppImage package", + }; + } + + const feedBase = String(feedUrlEnv || "").trim().replace(/\/+$/, ""); + if (feedBase) { + return { + configured: true, + provider: "generic", + url: `${feedBase}/${displayChannel}/`, + displayChannel, + allowPrerelease, + autoUpdaterChannel: null, + reason: null, + }; + } + + return { + configured: true, + provider: "github", + owner: DEFAULT_OWNER, + repo: DEFAULT_REPO, + displayChannel, + allowPrerelease, + autoUpdaterChannel: null, + reason: null, + }; +} diff --git a/electron/updater-config.test.ts b/electron/updater-config.test.ts new file mode 100644 index 0000000..f72183f --- /dev/null +++ b/electron/updater-config.test.ts @@ -0,0 +1,71 @@ +import { describe, expect, test } from "bun:test"; +import { resolveUpdaterConfig } from "./updater-config.mjs"; + +describe("resolveUpdaterConfig", () => { + test("disables updates in development builds", () => { + const result = resolveUpdaterConfig({ + isPackaged: false, + appImagePath: undefined, + feedUrlEnv: "", + updateChannelEnv: "", + }); + expect(result.configured).toBe(false); + expect(result.reason).toMatch(/development/i); + }); + + test("requires AppImage for packaged system packages", () => { + const result = resolveUpdaterConfig({ + isPackaged: true, + appImagePath: undefined, + feedUrlEnv: "https://updates.example.com", + updateChannelEnv: "preview", + }); + expect(result.configured).toBe(false); + expect(result.reason).toMatch(/AppImage/i); + }); + + test("uses baked-in GitHub feed for packaged AppImage without custom env", () => { + const result = resolveUpdaterConfig({ + isPackaged: true, + appImagePath: "/tmp/Liney.AppImage", + feedUrlEnv: "", + updateChannelEnv: "", + }); + expect(result.configured).toBe(true); + expect(result.provider).toBe("github"); + expect(result.owner).toBe("Cytech-Team"); + expect(result.repo).toBe("Liney"); + expect(result.displayChannel).toBe("preview"); + expect(result.allowPrerelease).toBe(true); + // Must use default latest-linux.yml so GitHub prerelease filtering works. + // Setting channel to "preview"/"stable" would look for preview-linux.yml + // which electron-builder never generates. + expect(result.autoUpdaterChannel).toBeNull(); + }); + + test("stable channel disables prerelease", () => { + const result = resolveUpdaterConfig({ + isPackaged: true, + appImagePath: "/tmp/Liney.AppImage", + feedUrlEnv: "", + updateChannelEnv: "stable", + }); + expect(result.configured).toBe(true); + expect(result.displayChannel).toBe("stable"); + expect(result.allowPrerelease).toBe(false); + expect(result.autoUpdaterChannel).toBeNull(); + }); + + test("custom generic feed keeps channel in URL and default yml name", () => { + const result = resolveUpdaterConfig({ + isPackaged: true, + appImagePath: "/tmp/Liney.AppImage", + feedUrlEnv: "https://updates.example.com///", + updateChannelEnv: "preview", + }); + expect(result.configured).toBe(true); + expect(result.provider).toBe("generic"); + expect(result.url).toBe("https://updates.example.com/preview/"); + expect(result.autoUpdaterChannel).toBeNull(); + }); +}); diff --git a/package.json b/package.json index 329a1fe..620fbbb 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,11 @@ "rpm": { "artifactName": "Liney-Electron-Preview-${version}-${arch}.rpm" }, - "publish": null, + "publish": { + "provider": "github", + "owner": "Cytech-Team", + "repo": "Liney" + }, "asarUnpack": [ "electron/dist/engine.mjs", "electron/native/**", diff --git a/scripts/verify-linux-packages.sh b/scripts/verify-linux-packages.sh index 2a3c2d1..7d47cf3 100755 --- a/scripts/verify-linux-packages.sh +++ b/scripts/verify-linux-packages.sh @@ -22,6 +22,15 @@ done PACKAGE_ROOT="$(mktemp -d)" trap 'rm -rf "$PACKAGE_ROOT"' EXIT +# Update feed metadata for AppImage auto-update (GitHub Releases). +# electron-builder writes latest-linux.yml alongside the AppImage when +# build.publish points at Cytech-Team/Liney. The updater verifies SHA-512 +# and signature from this file before downloading. +YML="$OUTPUT_DIR/latest-linux.yml" +test -s "$YML" +grep -Fq "version: $VERSION" "$YML" +grep -Fq "sha512" "$YML" + # Validate package metadata without relying on a host RPM database. [[ "$(dpkg-deb --field "$DEB" Package)" == "liney" ]] [[ "$(dpkg-deb --field "$DEB" Version)" == "$VERSION" ]] From 50c2c34a301d269be980f7c2fda2f90f72e4c038 Mon Sep 17 00:00:00 2001 From: Namnarak Date: Fri, 13 Nov 2026 15:44:10 +0700 Subject: [PATCH 2/2] fix(ui): report up-to-date and stop when no update is available update-not-available kept availableVersion set so About showed a stale "new version" subtitle; error paths also leaked stale fields. Clear version/notes/error on current, clear stale fields on error, and gate release-notes/error blocks by status so the current state reads latest-and-done. --- src/App.tsx | 6 +++--- src/lib/i18n.tsx | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index ca6e4e0..1715ad7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6336,12 +6336,12 @@ function AboutSettings() { return

Liney Electron Preview

Unofficial LINE client สำหรับ Linux · React + Electron · LINEJS Worker · SQLite history

Version {update.version} · {update.channel.toUpperCase()} channel
-
{statusText[update.status] || t("สถานะการอัปเดต")}{update.availableVersion ? t("เวอร์ชันใหม่ {{version}}", { version: update.availableVersion }) : t("ตรวจสอบเวอร์ชันใหม่จาก Cytech Team")}
+
{statusText[update.status] || t("สถานะการอัปเดต")}{update.status === "current" ? t("ไม่พบเวอร์ชันใหม่") : update.availableVersion ? t("เวอร์ชันใหม่ {{version}}", { version: update.availableVersion }) : t("ตรวจสอบเวอร์ชันใหม่จาก Cytech Team")}
{update.status === "available" && update.availableVersion !== deferredVersion &&
{t("พร้อมอัปเดต")}{t("ดาวน์โหลดได้โดยไม่ปิดแอป และจะติดตั้งเมื่อยืนยัน")}
} {update.status === "downloading" &&
Download progress{update.percent ?? 0}%
} {update.status === "downloaded" &&
{t("พร้อมติดตั้ง")}{t("ไฟล์อัปเดตผ่านการตรวจสอบความถูกต้องแล้ว")}
} - {update.releaseNotes &&
Release notes

{update.releaseNotes}

} - {update.error &&

{update.error}

} + {(update.status === "available" || update.status === "downloaded") && update.releaseNotes &&
Release notes

{update.releaseNotes}

} + {(update.status === "error" || update.status === "unconfigured") && update.error &&

{update.error}

}

{t("ช่องทาง Preview และ Stable แยกจากกัน · ตรวจสอบลายเซ็นและ SHA-512 ตาม metadata ของแพ็กเกจ · ระบบอัปเดตอัตโนมัติรองรับ AppImage เท่านั้น")}

; diff --git a/src/lib/i18n.tsx b/src/lib/i18n.tsx index a913dd8..6e38ae8 100644 --- a/src/lib/i18n.tsx +++ b/src/lib/i18n.tsx @@ -325,6 +325,7 @@ const ENGLISH_TRANSLATIONS: Record = { "กำลังดาวน์โหลด {{percent}}%": "Downloading {{percent}}%", "ดาวน์โหลดและตรวจสอบไฟล์แล้ว": "Downloaded and verified", "ใช้เวอร์ชันล่าสุดแล้ว": "You're up to date", + "ไม่พบเวอร์ชันใหม่": "No new version found", "ยังไม่ได้ตั้งค่า Update Feed": "Update feed isn't configured", "ตรวจสอบการอัปเดตไม่สำเร็จ": "Couldn't check for updates", "สถานะการอัปเดต": "Update status",