Skip to content

feat(updater): pre-release channel opt-in toggle - #68

Merged
Nic-dorman merged 4 commits into
mainfrom
feat/updater-prerelease-channel
Apr 30, 2026
Merged

feat(updater): pre-release channel opt-in toggle#68
Nic-dorman merged 4 commits into
mainfrom
feat/updater-prerelease-channel

Conversation

@Nic-dorman

Copy link
Copy Markdown
Contributor

Summary

Adds an opt-in "Pre-release builds" toggle to Settings → Advanced so internal testers running on RCs get auto-updates between RC builds, not just at stable cuts. Off by default — zero impact on the stable-channel population.

Why

tauri-plugin-updater reads its endpoint from compile-time tauri.conf.json config, and we point that at releases/latest/download/latest.json. GitHub's /releases/latest/... redirect is defined to skip releases marked prerelease=true, so RC artefacts are invisible to the in-app updater even though release.yml correctly publishes a signed latest.json to every prerelease.

Internal testers had to download every RC manually. This PR closes that gap.

How

The plugin's Builder doesn't accept runtime endpoints, so we bypass its frontend check()/Update.downloadAndInstall() entirely and own the flow ourselves via app.updater_builder().endpoints(...) (which the per-call UpdaterBuilder does support). Plugin stays registered for permissions / target detection.

Three commits, each independently buildable:

  1. feat(settings): add pre-release channel toggle (UI only)

    • AppConfig.prerelease_channel: bool (default false), persisted via existing config.toml round-trip.
    • Settings → Advanced toggle bound to it.
    • Restart-hint banner that shows whenever the live value differs from the snapshot taken on first loadConfig. The snapshot represents the channel the running session resolved its endpoint URL from, so the hint only appears when a restart would actually change behaviour.
  2. feat(updater): channel-aware check + install commands

    • New src-tauri/src/updater_channel.rs module with UpdaterChannelState (managed) holding a cached resolved endpoint URL and the pending Update instance.
    • check_for_update_custom reads the channel flag, resolves the endpoint (stable redirect for off; latest tag including pre-releases via the GitHub releases API for on), builds an UpdaterBuilder with that endpoint, calls .check(), stashes the Update for later install. Returns UpdateMetadata { version, body, is_prerelease }.
    • install_pending_update pulls the Update from state, runs download_and_install, re-emits Started/Progress/Finished as update-download-event with the same payload shape the JS plugin emits today (frontend handler unchanged).
  3. feat(updater): switch frontend store to channel-aware Tauri commands

    • stores/updater.ts no longer imports from @tauri-apps/plugin-updater. Calls our custom commands instead. Subscribes to update-download-event for progress.
    • The markRaw(Update) workaround goes away — the Update instance now lives in Rust state, not Pinia.
    • UpdateDialog shows an amber "Pre-release" badge next to the version when meta.is_prerelease is true.

Notes

  • Restart-required is the documented UX, but the implementation is more lenient. The check command re-reads the config every call and the prerelease URL is cached for the session. So flipping the toggle and clicking Check Now does pick up the new channel. Restart is still the right hint for users because the auto-check that runs on app boot uses whatever the channel was at boot.
  • Does not change release.yml. The workflow's existing prerelease=true detection (*-rc.* / *-beta.* / *-alpha.*) is correct.
  • Plugin remains registered — needed for permissions and target/exe detection. Its compile-time endpoint config becomes effectively dead but the plugin still expects a valid value, so leaving the existing one is fine.

Test plan

  • cargo check --all-targets clean
  • cargo clippy --all-targets -- -D warnings clean
  • cargo fmt --check clean
  • npx nuxi build clean (Nuxt + Vite)
  • CI green (Linux)
  • Manual: toggle off → stable check returns expected version, no badge; existing RCs invisible.
  • Manual: toggle on → restart hint appears; after restart, check resolves the latest pre-release tag, dialog shows "Pre-release" amber badge.
  • Manual: install end-to-end on an internal RC build (download progress, restart-on-finish).
  • Manual: toggle on → toggle off → restart → next check reverts to stable.

vitest locally hits the same Windows + npm + @vue/test-utils ESM flake we've seen on every branch since #59 — pre-existing, CI on Linux is the source of truth.

Closes

Linear ticket tracking this work (internal).

@Nic-dorman
Nic-dorman marked this pull request as ready for review April 30, 2026 11:30
Nic-dorman added a commit that referenced this pull request Apr 30, 2026
)

* feat(uploads): live progress + chain auto-switch + EIP-1559 fees for external-signer

Builds on top of #65 (direct-path progress wiring) to also drive a live
progress bar through the WalletConnect / external-signer flow, plus a
small grab-bag of fixes that surfaced during end-to-end Sepolia testing.

The three external-signer Tauri commands now spawn the same forwarder
PR #65 introduced and pass it into ant-core's new `_with_progress`
upstream methods:

  start_upload          → file_prepare_upload_with_progress
  confirm_upload        → finalize_upload_with_progress
  confirm_upload_merkle → finalize_upload_merkle_with_progress

ant-core dep is pinned to the upstream PR's branch
(`feat/external-signer-progress-events`) until that lands and a tag
ships. Swap the dep back to a tagged `ant-core-vX.Y.Z` once #68 is
merged + tagged.

External-signer's transfer_id is the opaque `quote-...` string returned
by `start_upload`, not the row id. The progress listener gains a
`_transferIdToRowId` map, populated as soon as `getUploadQuote` knows
the id (BEFORE invoking start_upload, so quote-phase events route from
the very first event). The wallet/download paths still use `String(id)`
and fall through to the numeric parse — both flows share one listener.

- `showsProgressBar` widened to render during `quoting` / `paying` too,
  not just `uploading`. The bar otherwise vanished during the wallet
  popup and the bulk of the work on the external-signer path.
- `stageDetail()` switched to per-stage percent ("Storing · 67%") instead
  of fractions ("Storing · 6/9"). Bar = global progress, label = local.
- `StatusBadge` gets `max-w-xs truncate` + a `title` tooltip so long
  failure messages don't blow the table width.

- **V2-236** chain auto-switch — `ensureActiveChain()` calls
  `switchChain` before `writeContract` so the wallet is always on the
  target chain. Without it, a wallet on Arbitrum One signing for an
  Arbitrum Sepolia tx blew up with a chain-mismatch error and no clear
  recovery in the UI.
- **V2-231** EIP-1559 fee invariant — override BOTH `maxFeePerGas` and
  `maxPriorityFeePerGas` explicitly (1 gwei / 0.1 gwei). viem's
  partial auto-estimation produced an invalid pair on Arbitrum's tiny
  base fees, surfacing as either a base-fee race or a "User rejected"
  wrapper around the underlying invariant violation. Three sites
  (payForQuotes, payForMerkleTree, approve in ensureAllowance).

End-to-end on a Sepolia-EVM devnet (`start-devnet-sepolia` from
ant-client + MetaMask Mobile via WalletConnect, funded Sepolia wallet):

- 9-chunk file, full external-signer flow: quote → MM popup → store
- Bar climbs 0→50% during quoting (per-chunk ChunkQuoted), pauses at
  50% during the MM popup, climbs 50→100% during storage.
- Tx hash confirmed on chain, `External-signer upload finalized` logged
- 21/21 vitest pass, cargo check clean against the branch ant-core dep

Path-dep to upstream branch will fail any CI that pulls a registry-only
ant-core. Hold until #68 lands, then bump the dep to the new tag and
unmark draft.

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

* feat(uploads/downloads): segmented bar-dot-bar progress + clearer column layout

Layout changes (pages/files.vue):
- Status column moved one to the left in both upload and download tables.
- File name cell becomes the wider one: filename on top, progress bar below
  (only when active). Bar gets a min-w-[8rem] so flex-1 segments don't
  collapse to invisible widths in narrow rows.
- Status cell stacks the badge on top with the stage detail line below it,
  so badge + sub-text track together visually.

Progress bar (components/ProgressLine.vue + new BarSegment.vue + PayDot.vue):
- Upload bar is now segmented [quote] · [pay-dot] · [store], conveying the
  three-phase shape of the external-signer flow at a glance. Wallet-flow
  and downloads use the same component but the dot stays solid (wallet)
  or the bar collapses to a single fill (download).
- Bar fill is now stage-driven, not status-driven. Wallet-flow JS pre-flips
  status='uploading' before any quoting starts; the old logic took that
  literally and rendered the left segment full + dot solid before any work
  was done. New logic prefers the live `stage` from ant-core's events and
  uses status only as a "we're past quoting" fallback (paying / complete).
- PayDot's empty state uses bg-autonomi-surface (same as the bar tracks)
  instead of a transparent bordered ring, for tighter visual cohesion.

Status label (pages/files.vue + components/StatusBadge.vue):
- statusLabel() now consults `stage` when the outer status disagrees with
  the inner phase. Wallet-flow uploads under stage='quoting' now show
  "Quoting" instead of "Uploading"; downloads under stage='resolving'
  show "Resolving datamap" instead of "Downloading". Badge + sub-text
  stay in sync.
- StatusBadge: non-error labels render at natural width with
  whitespace-nowrap (no truncate, no max-w), so the column auto-sizes
  tight to its widest live label. Error labels keep `max-w-xs truncate`
  so a viem stacktrace can't blow the column out.
- New badge entries for "Encrypting…" and "Resolving datamap".

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

* fix(uploads): persist gas_cost in upload_history.json so it survives reloads

Previously the runtime FileEntry tracked gas_cost (rendered as a small
"+ X gas" line under cost in the table) but the persisted UploadHistoryEntry
shape didn't carry it. On app reload / Files-page refresh, every completed
upload reloaded with cost intact but gas_cost dropped — the second line
silently vanished.

Adds optional gas_cost?: string | null on both the TS UploadHistoryEntry
and the Rust struct (with #[serde(default)] for backwards compat with
existing history files). loadHistory() and persistHistory() round-trip
the field. Legacy entries written before this change remain valid and
just render without the gas line, same as before.

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

* feat(uploads/downloads): per-row Remove + Retry actions, move Clear History

V2-233: each settled (failed / complete / downloaded) row now reveals a
hover affordance on the right — `✕` for remove. Wires to the existing
`removeEntry()` store action.

V2-234: failed upload rows also show a `↻ Retry` button. Reuses
`entry.path` so the user doesn't re-pick the file from disk; the new
`retryUpload()` store action clears stale per-attempt fields (error,
progress, stage*, address, datamap, costs, gas), flips status to
`queued_for_upload`, and lets the page scheduler re-run
`startRealUpload` (fresh quote + fresh wagmi sign).

V2-232: bulk "Clear history" moves out of the Files page header and
into Settings as a confirmation-gated action. The per-row × covers the
common case of trimming one entry; bulk wipe is one click further away
to prevent accidental loss. The new Settings card shows the count of
settled entries that would be removed and is disabled when there's
nothing to clear.

Companion rename surfaced earlier in the session: external-signer's
`paying` status now renders as "Awaiting approval" instead of "Paying"
— more accurate while wagmi is awaiting the wallet signature, and
unambiguous since the wallet flow never enters this state.

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

* style(rust): satisfy cargo fmt on autonomi_ops.rs

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Nic-dorman and others added 4 commits April 30, 2026 12:40
Adds a Settings -> Advanced -> "Pre-release builds" toggle that persists
to AppConfig.prerelease_channel. Off by default. Internal-tester
convenience: when on, the next launch resolves the latest release
including pre-releases (rc / beta) for auto-update.

UI only at this stage -- the updater plugin still uses the static stable
endpoint from tauri.conf.json. The endpoint-selection plumbing comes in
follow-up commits. The toggle shows a "Restart the app to apply this
change" hint banner whenever the live value differs from the boot
snapshot, since the resolved endpoint URL is captured once at app init.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds two Tauri commands that bypass the plugin's compile-time static
endpoint and let the running app pick its updater endpoint based on the
persisted prerelease_channel flag:

  check_for_update_custom   - reads AppConfig, resolves endpoint URL
                              (stable redirect or prerelease tag URL via
                              the GitHub releases API), builds an
                              UpdaterBuilder with that endpoint, calls
                              check, stashes the resulting Update in
                              UpdaterChannelState for the install step.

  install_pending_update    - pulls the Update from state, runs
                              download_and_install with progress events
                              re-emitted as update-download-event with the
                              same Started/Progress/Finished shape the JS
                              plugin emits today.

The frontend store still uses the old plugin's check() in this commit; the
switch happens in the next one. UpdateMetadata carries an is_prerelease
flag so the dialog can show a "Pre-release" badge to testers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the @tauri-apps/plugin-updater check()/Update.downloadAndInstall()
calls with our own check_for_update_custom + install_pending_update Tauri
commands so the running app picks its endpoint based on the persisted
prerelease_channel flag instead of the static tauri.conf.json endpoint.

Behaviour parity with the previous flow:

  - Started/Progress events drive downloadTotal/downloadedBytes the same
    way; the Rust side re-emits them as update-download-event with the
    same payload shape.
  - Error mapping (humaniseUpdateError) is preserved -- the Rust command
    surfaces tauri-plugin-updater's error strings verbatim, so the same
    regexes match.
  - markRaw() workaround for Update is no longer needed -- the Update
    instance now lives in Rust app state instead of a Pinia field.

UpdateDialog gets a "Pre-release" badge next to the version when the
offered release is a pre-release, so testers running on the rc channel
can tell at a glance which channel surfaced the update.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an inline button beside the "Restart the app to apply this change."
hint that calls tauri-plugin-process's relaunch() to self-restart. The
plugin is already registered and the process:default permission is
granted in capabilities/default.json, so no infrastructure changes needed.

Saves testers from having to close + reopen the app manually to apply a
channel toggle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Nic-dorman
Nic-dorman force-pushed the feat/updater-prerelease-channel branch from 63b02fc to a1c6e72 Compare April 30, 2026 11:41
@Nic-dorman
Nic-dorman merged commit fcfaaa0 into main Apr 30, 2026
4 checks passed
@Nic-dorman
Nic-dorman deleted the feat/updater-prerelease-channel branch April 30, 2026 11:47
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