Skip to content

feat(update): Stable/Preview update channels with opt-in toggle - #199

Merged
debpalash merged 1 commit into
mainfrom
feat/update-channels
May 31, 2026
Merged

feat(update): Stable/Preview update channels with opt-in toggle#199
debpalash merged 1 commit into
mainfrom
feat/update-channels

Conversation

@debpalash

@debpalash debpalash commented May 31, 2026

Copy link
Copy Markdown
Owner

Completes the auto-update plan's follow-up: an opt-in Stable / Preview update channel toggle (Settings → About → Update channel), plus the release-pipeline path that feeds Preview.

What & why

Channel Tracks Default
Stable tagged vX.Y.Z (releases/latest/.../latest.json) ✅ every install + launch
Preview latest main build, a rolling preview prerelease (releases/download/preview/latest.json); falls back to stable if a stable release is ahead opt-in

Switching is instant — the channel is read per update check, no restart. Local-first preserved: same signed minisign key on both manifests, no accounts, no telemetry, no extra network calls.

The Rust requirement (discovered, not in the original offer)

I verified against tauri-plugin-updater@2.10.1 source: the plugin reads endpoints only from tauri.conf.json, and neither the JS check() nor the registration Builder exposes a runtime endpoint setter. The only runtime-endpoint API is UpdaterExt::endpoints. So check+install moved into two Rust commands (check_update/install_update) that mirror the plugin's own check/download_and_install — the Stable path behaves identically to the JS flow from #198; only which manifest is consulted changes.

  • Rustconfig.rs (update_channel, default stable, validated) + updater_channel.rs (channel endpoints, check_update, install_update emitting update://progress).
  • Frontendutils/updateChannel.js (+test), utils/updater.js rerouted through the Rust commands via the same store contract (UpdateBadge/App.jsx untouched), Settings About toggle + channel-aware endpoint/diagnostics, i18n (en + zh-CN).

Release pipeline — additive & guarded

release.yml gains a workflow_dispatch publish_preview input that publishes/updates a rolling preview prerelease with its own signed latest.json. The v* tag-push stable path evaluates to its exact prior values (tagName/releaseName/draft/prerelease) and is never touched. Preview builds are manual — no scheduled CI spend, nothing auto-published. To cut a preview: Actions → Desktop Release → Run workflow → publish_preview = true.

Maintainer note: the Preview channel only does anything once you run that workflow once to create the preview release. Until then the preview endpoint 404s harmlessly and Preview users get stable. See docs/update-channels.md.

Verification

cargo check ✓ (compiles clean, local), tsc ✓, vitest 162/162 (+updateChannel 3), bun run build ✓, CJK guard ✓ (zh-CN is the translation layer), release.yml YAML parses (stable path unchanged).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Users can now select between Stable and Preview update channels in Settings → About
    • Preview channel delivers rolling pre-release builds
    • Update checking and installation workflows now respect the selected channel
    • English and Chinese localization added for channel settings

Adds a user-selectable updater release channel (Settings -> About -> Update
channel). Stable (default, every install + launch) tracks tagged vX.Y.Z
releases; Preview tracks the latest main build via a rolling "preview"
prerelease, falling back to stable if a stable release is ahead.

Why Rust: tauri-plugin-updater reads its endpoints from tauri.conf.json and
neither the JS check() nor the plugin's registration Builder can change them at
runtime (verified against the 2.10.1 source). The only runtime-endpoint API is
UpdaterExt::endpoints, so check+install move into two Rust commands that mirror
the plugin's own check/download_and_install -- the Stable path behaves
identically to the JS flow it replaces; only which manifest is consulted
changes. Switching is instant (channel is read per check), no restart.

backend (Rust):
- config.rs: update_channel field (default "stable", VALID_CHANNELS) +
  get/set_update_channel commands.
- updater_channel.rs: channel_endpoints() (preview -> [preview, stable]) +
  check_update / install_update commands; install emits update://progress.

frontend:
- utils/updateChannel.js (+test): single source of truth, normalizeChannel.
- utils/updater.js: routes the badge flow (#198) through the Rust commands via
  the same store contract -- UpdateBadge/App.jsx unchanged.
- Settings About: Stable/Preview segmented toggle, channel-aware endpoint row +
  diagnostics; Check-for-updates honors the live channel.
- i18n en + zh-CN.

release.yml: additive, workflow_dispatch-guarded preview publish to a rolling
"preview" prerelease. The v* tag-push stable path evaluates to its exact prior
values (verified) and is never affected. Preview builds are manual -- no
scheduled CI spend, nothing auto-published.

docs/update-channels.md.

Verified: cargo check (compiles clean), tsc, vitest 162/162, build, CJK guard,
release.yml YAML parses.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Implements a dual update-channel system allowing users to select between stable (latest tagged release) and preview (rolling main branch) channels. Changes span Rust Tauri backend for endpoint routing, JavaScript frontend Settings UI with channel selection and persistence, refactored update flow using Rust commands, localization strings, release workflow updates, and user documentation.

Changes

Dual Update Channels System

Layer / File(s) Summary
Channel definition and persistence
frontend/src-tauri/src/config.rs, frontend/src/utils/updateChannel.js, frontend/src/utils/updateChannel.test.ts
VALID_CHANNELS constant and AppConfig.update_channel field with serde defaults define the channel model. Tauri commands get_update_channel and set_update_channel persist and retrieve the selection. JavaScript utility updateChannel.js provides shared normalizeChannel() with comprehensive tests.
Rust updater backend with endpoint selection
frontend/src-tauri/src/updater_channel.rs, frontend/src-tauri/src/lib.rs
Manifest URLs for stable and preview channels with channel_endpoints() helper selects endpoints based on channel string and falls back to stable for unknown values. check_update and install_update Tauri commands build channel-specific updaters. UpdateMeta and ProgressPayload structures serialize update metadata and download progress. Handlers are wired into lib.rs invoke registry.
Settings UI for channel selection
frontend/src/pages/Settings.jsx
updateChannel state initialized from persisted config. changeChannel callback normalizes, updates state, and persists via set_update_channel, showing toast feedback. Segmented control UI allows stable/preview toggling with conditional preview hint and dynamic endpoint display.
Diagnostics and update flow integration
frontend/src/pages/Settings.jsx
Diagnostics output includes current channel and corresponding endpoint URL. checkForUpdates refactored to call check_update and install_update Tauri commands with channel parameter, dialog-based user confirmation, and progress-based relaunch. Dependency lists updated to include updateChannel.
JavaScript updater utility refactoring
frontend/src/utils/updater.js
currentChannel() helper fetches persisted channel with normalization fallback. checkForUpdate() calls Rust check_update command and maps result into store state. installUpdate() calls Rust install_update with progress event subscription tracking downloaded bytes capped at 99%, then relaunches via process plugin. Error handling uses fallback message format.
Localization for channel UI
frontend/src/i18n/locales/en.json, frontend/src/i18n/locales/zh-CN.json
English and Chinese i18n strings added under about section: update_channel label, channel_stable and channel_preview names, templated channel_set confirmation, and channel_preview_hint describing preview behavior.
Release workflow and user documentation
.github/workflows/release.yml, docs/update-channels.md
Workflow comments expanded to document preview release path. New publish_preview boolean input controls whether workflow_dispatch targets preview prerelease. tauri-apps/tauri-action conditionally publishes to preview tag and "Preview" release when enabled. docs/update-channels.md explains channel selection path, update semantics, manifest URLs, signature expectations, and maintainer workflow for rolling preview builds.

Sequence Diagram

sequenceDiagram
  participant User
  participant Settings as Settings UI
  participant TauriBackend as Tauri Backend
  participant Updater as Update Service
  participant ReleaseServer as Release Server
  
  User->>Settings: select channel (stable/preview)
  Settings->>TauriBackend: set_update_channel(channel)
  TauriBackend->>TauriBackend: persist to config
  
  User->>Settings: check for updates
  Settings->>TauriBackend: check_update(channel)
  TauriBackend->>TauriBackend: select endpoint by channel
  TauriBackend->>Updater: build with channel endpoint
  Updater->>ReleaseServer: fetch manifest
  ReleaseServer-->>Updater: update metadata or none
  Updater-->>TauriBackend: update info or none
  TauriBackend-->>Settings: UpdateMeta or None
  
  alt update available
    Settings->>User: prompt install?
    User->>Settings: confirm
    Settings->>TauriBackend: install_update(channel)
    TauriBackend->>Updater: download with channel endpoint
    Updater->>ReleaseServer: download release
    ReleaseServer-->>Updater: bytes + progress
    Updater->>Updater: emit update://progress events
    TauriBackend->>TauriBackend: track progress
    TauriBackend->>Settings: progress updates
    Settings->>User: show progress bar
    Updater->>TauriBackend: install complete
    TauriBackend-->>Settings: success
    Settings->>User: relaunch app
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • debpalash/OmniVoice-Studio#198: Concurrent refactoring of the auto-update flow including checkForUpdate and installUpdate utilities that this PR extends with channel-aware Rust commands.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding stable/preview update channels with a user-facing toggle in settings.
Description check ✅ Passed The description covers key changes, release pipeline details, Rust implementation rationale, and verification steps, though the template's Testing and Checklist sections remain uncompleted.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/update-channels

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps

greptile-apps Bot commented May 31, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds an opt-in Stable / Preview update-channel toggle (Settings → About) backed by two new Rust commands (check_update / install_update) that select endpoints at runtime via UpdaterExt, working around the fact that tauri-plugin-updater reads endpoints only from tauri.conf.json. The release pipeline gains a publish_preview workflow-dispatch input to publish a rolling preview prerelease.

  • Rust side (config.rs + updater_channel.rs): channel is persisted in AppConfig, validated against VALID_CHANNELS, and read per-check; both Rust commands mirror the plugin's own check/download_and_install flow with progress events on update://progress.
  • Frontend (updater.js + Settings.jsx): JS update helpers are rerouted through the new Rust commands; a Segmented toggle is added to the About section, and diagnostics copy now includes the active channel and its endpoint URL.
  • CI (.github/workflows/release.yml): all tauri-action fields use ternary expressions that evaluate to their prior values on the v* tag-push stable path; only a manual publish_preview=true dispatch creates/updates the rolling preview prerelease.

Confidence Score: 3/5

The stable update path and Settings toggle UI are solid, but the badge-install flow has a real channel-consistency defect that can leave users stuck.

The background-check path reads and stores the channel at launch time but does not persist it alongside the update availability in the store. installUpdate then re-reads the channel freshly from config at install time. A user who switches from Preview to Stable in Settings after seeing the badge will trigger install_update on the Stable channel, which either installs a different update or returns 'No update available' while the badge still displays.

frontend/src/utils/updater.js (badge-flow channel consistency) and frontend/src-tauri/src/updater_channel.rs (double check() round-trip in the install command).

Important Files Changed

Filename Overview
frontend/src/utils/updater.js Rerouted update check/install through Rust commands; channel mismatch between launch check and badge install can leave UI in a broken state.
frontend/src-tauri/src/updater_channel.rs New Rust module for channel-aware updater; install_update re-runs check() creating a double round-trip and TOCTOU; silent URL parse drop is low-risk but fragile.
frontend/src-tauri/src/config.rs Adds update_channel field with stable default, validation against VALID_CHANNELS, and get/set Tauri commands — looks correct.
frontend/src/pages/Settings.jsx Adds channel toggle UI and wires check/install to Rust commands; double check_update+install_update round-trip is the main concern.
.github/workflows/release.yml Adds publish_preview boolean input and conditional expressions for tagName/releaseName/draft/prerelease; stable tag-push path evaluates to prior values correctly.
frontend/src/utils/updateChannel.js Tiny utility exporting channel list and normalizeChannel(); clean and well-tested.
frontend/src/utils/updateChannel.test.ts Three focused unit tests covering happy path and clamping behavior; no issues.

Sequence Diagram

sequenceDiagram
    participant App as App.jsx (launch)
    participant Badge as UpdateBadge
    participant Settings as Settings.jsx
    participant JS as updater.js
    participant Rust as updater_channel.rs
    participant GH as GitHub Releases

    Note over App,GH: Background check on launch (badge flow)
    App->>JS: checkForUpdate(store)
    JS->>Rust: "invoke('check_update', {channel})"
    Rust->>GH: GET manifest (stable or preview)
    GH-->>Rust: latest.json
    Rust-->>JS: "UpdateMeta {version, notes}"
    JS->>App: store.setUpdateAvailable(version, notes)
    App->>Badge: renders badge

    Note over Badge,GH: User clicks badge → install
    Badge->>JS: installUpdate(store)
    JS->>Rust: "invoke('install_update', {channel*})"
    Note right of JS: channel* re-read from config
    Rust->>GH: check() again (2nd request)
    Rust->>GH: download binary
    GH-->>Rust: binary chunks
    Rust-->>JS: update://progress events
    Rust-->>JS: Ok(())
    JS->>App: relaunch()
Loading

Fix All in Claude Code

Reviews (1): Last reviewed commit: "feat(update): Stable/Preview update chan..." | Re-trigger Greptile

Comment on lines +58 to +64
const channel = await currentChannel();
store.setUpdateProgress(0);
await update.downloadAndInstall((ev) => {
if (ev.event === 'Started') total = ev.data?.contentLength || 0;
else if (ev.event === 'Progress') {
got += ev.data?.chunkLength || 0;
if (total > 0) store.setUpdateProgress(Math.min(99, (got / total) * 100));
} else if (ev.event === 'Finished') {
store.setUpdateReady();
}
unlisten = await listen('update://progress', (ev) => {
const { downloaded = 0, total = 0 } = ev?.payload || {};
if (total > 0) store.setUpdateProgress(Math.min(99, (downloaded / total) * 100));
});
await invoke('install_update', { channel });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Channel mismatch between background check and badge install

checkForUpdate (called at launch) reads the channel once and records the found version in the store, but installUpdate calls currentChannel() freshly at install time. If the user changes the channel in Settings between seeing the badge and clicking install, installUpdate runs against the new channel — which may have no update at all. The result is setUpdateError("No update available") while the badge still shows "update available", leaving the UI in a broken state. The fix is to store the channel alongside the version in the store when setUpdateAvailable is called, and pass it through to installUpdate.

Fix in Claude Code

Comment on lines +75 to +86
pub async fn install_update(app: AppHandle, channel: String) -> Result<(), String> {
let updater = app
.updater_builder()
.endpoints(channel_endpoints(&channel))
.map_err(|e| format!("updater endpoints: {e}"))?
.build()
.map_err(|e| format!("updater build: {e}"))?;
let update = updater
.check()
.await
.map_err(|e| e.to_string())?
.ok_or_else(|| "No update available".to_string())?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 install_update re-runs check() — double network round-trip and TOCTOU in the Settings confirm flow

Settings.jsx::checkForUpdates calls check_update (network request #1), surfaces the version to the user in a native ask() dialog, then calls install_update which internally calls check() again (network request #2). In the window between the two checks a new release could land, causing the user to confirm "install 0.3.1" but actually install 0.3.2 — or, if the manifest briefly 404s, they'd get toast.error('Update check failed: No update available') after already clicking "yes". A more robust design would have install_update accept and re-use the pre-checked update object, or the Rust side could cache the update between the two calls.

Fix in Claude Code

} else {
vec![STABLE_MANIFEST]
};
raw.iter().filter_map(|u| u.parse().ok()).collect()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Silent URL-parse failures in channel_endpoints will produce an empty endpoint list with no diagnostic. Since the URLs are compile-time constants this is currently safe, but if either constant is ever malformed the updater will silently do nothing instead of surfacing an error. Consider using an explicit expect to make the failure loud.

Suggested change
raw.iter().filter_map(|u| u.parse().ok()).collect()
raw.iter()
.map(|u| u.parse().expect("BUG: hardcoded manifest URL is malformed"))
.collect()

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.github/workflows/release.yml (1)

85-88: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Pin action to a major version tag.

This action uses @latest instead of a pinned version. As per coding guidelines, GitHub Actions should be pinned to at least a major version tag to prevent unexpected behavior changes.

📌 Suggested fix

Check the action's releases and pin to the appropriate major version:

-        uses: awalsh128/cache-apt-pkgs-action@latest
+        uses: awalsh128/cache-apt-pkgs-action@v1
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 85 - 88, The workflow pins the
GitHub Action to `@latest` (uses: awalsh128/cache-apt-pkgs-action@latest), which
can introduce breaking changes; update the uses entry to a specific major
version tag (e.g., change awalsh128/cache-apt-pkgs-action@latest to the action's
current major release tag such as `@v1` or `@v1.x`) by consulting the action's
releases and replacing the `@latest` reference with that major-version tag so the
step (uses: awalsh128/cache-apt-pkgs-action@...) is stable and follows pinning
guidelines.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/src-tauri/src/updater_channel.rs`:
- Around line 21-31: The current channel_endpoints function returns
[PREVIEW_MANIFEST, STABLE_MANIFEST] which relies on tauri-plugin-updater's
sequential endpoint lookup (it stops at the first reachable manifest) and
therefore does not implement the intended "use preview but fall back to newer
stable" behavior; change the approach so the app explicitly fetches and compares
manifests from PREVIEW_MANIFEST and STABLE_MANIFEST (e.g., implement a new
helper like fetch_and_select_manifest or fetch_manifests_and_pick_newest), parse
the version fields from both manifests, choose the manifest with the
highest/newest version, and then pass only that chosen URL to
tauri-plugin-updater (or configure the updater with a single selected endpoint)
instead of returning both URLs from channel_endpoints; ensure channel_endpoints
or its callers use PREVIEW_MANIFEST and STABLE_MANIFEST only as sources to
compare rather than letting the plugin pick the first reachable one.
- Around line 40-44: The ProgressPayload currently serializes total: Option<u64>
which becomes null and breaks the JS consumer; update the emit path to always
send a concrete number (e.g. 0) instead of None — either change the
ProgressPayload field to total: u64 or, if keeping Option<u64>, ensure wherever
ProgressPayload is constructed (the chunk/progress callback that provides total)
you use total.unwrap_or(0) so the serialized payload never contains null;
reference ProgressPayload, its downloaded and total fields, and the chunk
callback/download_and_install emit code when making the change.

In `@frontend/src/pages/Settings.jsx`:
- Around line 1173-1175: Replace the hardcoded toast strings in Settings.jsx
with i18n keys: call the translation function t() for the downloading message
(pass update.version as the interpolation variable) when creating the loading
toast (tid) and use t('about.installed_relaunching') for the success toast after
invoke('install_update', { channel }) completes; also add the two keys
"about.downloading_version" with a "{{version}}…" placeholder and
"about.installed_relaunching" to the locales (e.g., locales/en.json) so the
t(...) lookups resolve.

---

Outside diff comments:
In @.github/workflows/release.yml:
- Around line 85-88: The workflow pins the GitHub Action to `@latest` (uses:
awalsh128/cache-apt-pkgs-action@latest), which can introduce breaking changes;
update the uses entry to a specific major version tag (e.g., change
awalsh128/cache-apt-pkgs-action@latest to the action's current major release tag
such as `@v1` or `@v1.x`) by consulting the action's releases and replacing the
`@latest` reference with that major-version tag so the step (uses:
awalsh128/cache-apt-pkgs-action@...) is stable and follows pinning guidelines.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 348112d3-de24-4a71-baf8-16fe68e8edcf

📥 Commits

Reviewing files that changed from the base of the PR and between 588177c and 7953629.

📒 Files selected for processing (11)
  • .github/workflows/release.yml
  • docs/update-channels.md
  • frontend/src-tauri/src/config.rs
  • frontend/src-tauri/src/lib.rs
  • frontend/src-tauri/src/updater_channel.rs
  • frontend/src/i18n/locales/en.json
  • frontend/src/i18n/locales/zh-CN.json
  • frontend/src/pages/Settings.jsx
  • frontend/src/utils/updateChannel.js
  • frontend/src/utils/updateChannel.test.ts
  • frontend/src/utils/updater.js

Comment on lines +21 to +31
/// Endpoints for a channel. Preview tries the rolling `preview` manifest first,
/// then falls back to stable so a preview user still receives a newer *stable*
/// release if one is ahead of the latest preview. Any unknown channel → stable.
fn channel_endpoints(channel: &str) -> Vec<tauri::Url> {
let raw = if channel == "preview" {
vec![PREVIEW_MANIFEST, STABLE_MANIFEST]
} else {
vec![STABLE_MANIFEST]
};
raw.iter().filter_map(|u| u.parse().ok()).collect()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

❓ Verification inconclusive

🌐 Web query:

tauri-plugin-updater v2 multiple endpoints behavior: does it try endpoints in order and use the first reachable manifest, or compare versions across all endpoints?

💡 Result:

In Tauri v2, when multiple endpoints are provided in the tauri-plugin-updater configuration, the updater attempts to reach them in the order they are defined [1][2]. It does not compare versions across all endpoints [2][3]. Specifically, the behavior is as follows: - Sequential Attempt: The updater tries to request the update manifest from the endpoints in the sequence provided in the array [1][2]. - Fallback Mechanism: If an endpoint returns a non-2XX status code (indicating it is not reachable or did not return a valid update manifest), the plugin will proceed to attempt the next URL in the list [2][3]. - Termination: The process stops and successfully proceeds as soon as it retrieves a valid update manifest from an endpoint [2]. It does not aggregate results or perform a "best version" comparison across multiple successful responses. This design is intended to provide a fallback mechanism for high availability, allowing developers to specify alternative mirrors or backup servers if the primary endpoint is unavailable [1].

Citations:


Fix updater fallback logic (major): tauri-plugin-updater with multiple endpoints tries them sequentially and stops at the first reachable/valid update manifest; it does not compare versions across endpoints. So [PREVIEW_MANIFEST, STABLE_MANIFEST] only falls back to stable when preview fails, not when stable is newer than preview—contradicting the documented “preview falls back to newer stable if one is ahead” requirement.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src-tauri/src/updater_channel.rs` around lines 21 - 31, The current
channel_endpoints function returns [PREVIEW_MANIFEST, STABLE_MANIFEST] which
relies on tauri-plugin-updater's sequential endpoint lookup (it stops at the
first reachable manifest) and therefore does not implement the intended "use
preview but fall back to newer stable" behavior; change the approach so the app
explicitly fetches and compares manifests from PREVIEW_MANIFEST and
STABLE_MANIFEST (e.g., implement a new helper like fetch_and_select_manifest or
fetch_manifests_and_pick_newest), parse the version fields from both manifests,
choose the manifest with the highest/newest version, and then pass only that
chosen URL to tauri-plugin-updater (or configure the updater with a single
selected endpoint) instead of returning both URLs from channel_endpoints; ensure
channel_endpoints or its callers use PREVIEW_MANIFEST and STABLE_MANIFEST only
as sources to compare rather than letting the plugin pick the first reachable
one.

Comment on lines +40 to +44
#[derive(Serialize, Clone)]
struct ProgressPayload {
downloaded: usize,
total: Option<u64>,
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

total: None serializes to null and stalls the JS progress bar.

When the server omits content-length, total is Nonenull in the emitted payload. The consumer in updater.js destructures const { downloaded = 0, total = 0 } = ..., but a JS default only applies to undefined, not null, so total stays null, total > 0 is false, and the progress bar never advances (jumps from 0% straight to ready). Consider emitting a concrete fallback (or fixing the guard on the JS side).

🛠️ One option: coerce missing total at emit
-struct ProgressPayload {
-    downloaded: usize,
-    total: Option<u64>,
-}
+struct ProgressPayload {
+    downloaded: usize,
+    total: u64,
+}
             move |chunk, total| {
                 downloaded += chunk;
                 let _ = app_for_chunk
-                    .emit("update://progress", ProgressPayload { downloaded, total });
+                    .emit("update://progress", ProgressPayload { downloaded, total: total.unwrap_or(0) });
             },

(Alternatively, handle null explicitly in updater.js.)

Confirm the chunk callback signature emits Option<u64> for total in the version in use:

#!/bin/bash
rg -nP 'download_and_install' -g '*.rs' -A3
fd -t f Cargo.toml -x rg -nP 'tauri-plugin-updater' {}

Also applies to: 88-100

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src-tauri/src/updater_channel.rs` around lines 40 - 44, The
ProgressPayload currently serializes total: Option<u64> which becomes null and
breaks the JS consumer; update the emit path to always send a concrete number
(e.g. 0) instead of None — either change the ProgressPayload field to total: u64
or, if keeping Option<u64>, ensure wherever ProgressPayload is constructed (the
chunk/progress callback that provides total) you use total.unwrap_or(0) so the
serialized payload never contains null; reference ProgressPayload, its
downloaded and total fields, and the chunk callback/download_and_install emit
code when making the change.

Comment on lines +1173 to +1175
const tid = toast.loading(`Downloading ${update.version}…`);
await invoke('install_update', { channel });
toast.success('Installed — relaunching.', { id: tid });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Hardcoded English strings violate i18n requirement.

Lines 1173 and 1175 contain user-facing text that should use the translation layer. As per coding guidelines, "All user-facing text in the UI must go through the i18n translation layer using t('...') keys in locales/*.json files."

🌐 Proposed fix to use i18n
-      const tid = toast.loading(`Downloading ${update.version}…`);
+      const tid = toast.loading(t('about.downloading_version', { version: update.version }));
       await invoke('install_update', { channel });
-      toast.success('Installed — relaunching.', { id: tid });
+      toast.success(t('about.installed_relaunching'), { id: tid });

Add to en.json:

"about.downloading_version": "Downloading {{version}}…",
"about.installed_relaunching": "Installed — relaunching."
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const tid = toast.loading(`Downloading ${update.version}…`);
await invoke('install_update', { channel });
toast.success('Installed — relaunching.', { id: tid });
const tid = toast.loading(t('about.downloading_version', { version: update.version }));
await invoke('install_update', { channel });
toast.success(t('about.installed_relaunching'), { id: tid });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/Settings.jsx` around lines 1173 - 1175, Replace the
hardcoded toast strings in Settings.jsx with i18n keys: call the translation
function t() for the downloading message (pass update.version as the
interpolation variable) when creating the loading toast (tid) and use
t('about.installed_relaunching') for the success toast after
invoke('install_update', { channel }) completes; also add the two keys
"about.downloading_version" with a "{{version}}…" placeholder and
"about.installed_relaunching" to the locales (e.g., locales/en.json) so the
t(...) lookups resolve.

@debpalash
debpalash merged commit 672f106 into main May 31, 2026
15 checks passed
@debpalash
debpalash deleted the feat/update-channels branch May 31, 2026 02:19
debpalash added a commit that referenced this pull request May 31, 2026
…202)

PR #200 added 18 new locale files, but they predated #199 (auto-update badge +
Stable/Preview channel toggle), so they were missing the `update.*` namespace
(6 keys) and `about.channel_*` (5 keys) — those strings fell back to English in
ar/de/es/fr/hi/id/it/ja/ko/nl/pl/pt/ru/sv/th/tr/uk/vi/zh-TW.

Backfill all 11 keys in every one of those languages so the updater UI is fully
localized. en.json / zh-CN.json already had them and are untouched. Placeholders
({{version}}, {{pct}}, {{channel}}) preserved verbatim; files re-emitted in the
exact format scripts/translate_all.py writes (ensure_ascii=False, indent=2) so
the diff is additions only (+13 lines/file, 0 deletions).

Also fix scripts/translate_all.py: LOCALES_DIR was hardcoded to a contributor's
absolute path (/Users/.../orca/...) — make it repo-relative so the generator
actually runs for anyone.

Verified: all 21 locales valid JSON + key-complete, placeholders intact;
tsc clean; vitest 162/162; build OK; CJK guard passes (locales are the
allowlisted translation layer).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant