Repository navigation
Releases and Auto Updates
English · Español
How a version is cut (pnpm release), what the release workflow builds, how installers are signed for the in-app updater, and how the installed app finds, downloads and installs a newer version. The updater lives in Rust (src-tauri/src/actualizar.rs, commands/actualizar.rs); the frontend only drives it and shows progress.
sequenceDiagram
participant M as Maintainer
participant S as scripts/release.mjs
participant GH as GitHub (git, Actions, Releases)
participant A as Installed app
M->>S: pnpm release patch
S->>GH: bump 4 files, commit "Version X", push main
GH-->>S: ci.yml green for that commit
S->>GH: dispatch release.yml
GH-->>GH: build + sign installers, upload to DRAFT release vX with latest.json
S->>GH: set notes on draft and in latest.json
M->>S: confirm publish
S->>GH: gh release edit --draft=false --latest (creates tag vX)
A->>GH: GET releases/latest/download/latest.json
A->>A: compare versions, download package, verify minisign signature
A->>A: install and restart
Key decisions and why:
-
Releases are cut by
scripts/release.mjsand thereleaseworkflow is manual-only. A tag trigger rebuilt everything when the draft was published (publishing creates the tag), so the workflow runs only viaworkflow_dispatch. -
Drafts do not count for the updater. The app reads
releases/latest/download/latest.json; GitHub's "latest" ignores drafts, so installed apps only see a version once the draft is published. - No OS code signing (SmartScreen and Gatekeeper will warn; see User Guide). The signature that matters here is the updater's minisign signature.
scripts/release.mjs (run as pnpm release <patch|minor|major|X.Y.Z> [options], needs git and an authenticated gh). Options (parsed by leerArgs in scripts/release-notas.mjs):
| Option | Effect |
|---|---|
--dry |
Run the checks and print what would happen; problems become warnings instead of aborting |
--publicar |
Publish the draft without asking |
--notas "text" |
Release notes inline |
--notas-fichero PATH |
Release notes from a Markdown file |
--sin-notas |
Only the fixed text |
Only one of the three notes options may be given. With none, the script writes a template to tmp/release-notas-v<version>.md (the template is a comment containing the commit list since the last tag) and opens $VISUAL, $EDITOR, nano/vi, or notepad on Windows; empty notes abort before anything is touched.
Steps, in order:
-
Checks:
gh auth status; the four version locations agree; the new version differs and isX.Y.Z; branch ismain; working tree clean; no commits behind or ahead oforigin/main; tagvX.Y.Zand a GitHub release of that tag do not exist. - Notes (as above).
-
Bump the version in four places:
package.json,src-tauri/tauri.conf.json,src-tauri/Cargo.toml,src-tauri/Cargo.lock(theproxy-builderpackage entry).versionActual()refuses to start if they disagree. - Commit
Version X.Y.Zand pushmain. -
Wait for the CI run of that commit (
ci.yml) and fail if it fails (gh run watch --exit-status, output streamed so the terminal does not look hung). -
Dispatch
release.ymlonmainand wait (roughly ten minutes). -
Validate the draft: it must be a draft, its
targetCommitishmust equal the pushed commit, and it must contain alatest.jsonasset (otherwise the signing secret is probably missing). -
Put the notes on the draft (
componer(notas)appends the fixed text) and rewritelatest.json'snoteswith the real notes (notasDelActualizador), then re-upload it with--clobber. The workflow's ownlatest.jsoncarries the fixed text, which means nothing inside the app. -
Ask to publish (
s/si/y/yes), or publish directly with--publicar. Publishing:gh release edit vX --draft=false --latest, which creates the tag. If you decline, the script prints the command to publish later.
Helpers in scripts/release-notas.mjs (unit-tested by scripts/__tests__/release-notas.test.mjs, which needs neither git nor gh): leerArgs, limpiarNotas (strips <!-- --> comments and collapses blank lines), plantilla, componer (notes plus TEXTO_FIJO, the text release.yml also sets), notasDelActualizador (throws if the JSON is not for the expected version or has no platforms).
Resuming a cut release: Inferred: because every step after the push is a separate gh operation, an interrupted release can be finished by hand from the draft (gh release edit, uploading a corrected latest.json); the script has no resume flag. The lockfile must be in the same commit as the packageManager change, per the repository's release practice.
Release notes are written in English ("What's changed since X").
.github/workflows/release.yml; matrix over four legs (fail-fast: false):
| Leg | Produces |
|---|---|
ubuntu-22.04 |
.deb, .rpm, AppImage (Tauri bundle targets all) |
windows-latest |
NSIS and MSI installers |
macos-latest with --target aarch64-apple-darwin
|
Apple Silicon .app bundle (and DMG) |
macos-latest with --target x86_64-apple-darwin
|
Intel .app bundle (and DMG) |
tauri-apps/tauri-action@v0 builds each leg and uploads to a draft release named Proxy Builder v__VERSION__ with tag v__VERSION__. Environment: GITHUB_TOKEN, TAURI_SIGNING_PRIVATE_KEY, TAURI_SIGNING_PRIVATE_KEY_PASSWORD (the key has no password; the secret may be empty). Without the private key secret tauri build fails, because bundle.createUpdaterArtifacts is true.
With createUpdaterArtifacts, each installer gets a minisign .sig file, and tauri-action merges one entry per installer into latest.json, with keys like linux-x86_64-deb, windows-x86_64-nsis, darwin-aarch64-app (named in the workflow comment). The updater picks the entry that matches the installer type the running binary came from.
- A private minisign key signs every bundle at build time. It lives only on the maintainer's machine and in the repository secret
TAURI_SIGNING_PRIVATE_KEY. - The matching public key is embedded in
src-tauri/tauri.conf.jsonatplugins.updater.pubkey(base64 of the minisign public key file). - Consequence: if the private key is lost, installed versions will reject every future update, because they only trust that public key. Treat it as irreplaceable and back it up.
-
requireSignedVersion: truemakes the signature embed the version (written bytauri build). An attacker therefore cannot announce a newer version while serving an older signed package. - The Rust test
la_configuracion_del_actualizador_es_valida(src-tauri/src/actualizar.rs) pins these settings, see Testing and CI.
From src-tauri/tauri.conf.json:
| Key | Value |
|---|---|
bundle.createUpdaterArtifacts |
true |
plugins.updater.endpoints |
https://github.com/PoolFlamingo/proxy-builder/releases/latest/download/latest.json |
plugins.updater.pubkey |
minisign public key (base64) |
plugins.updater.requireSignedVersion |
true |
plugins.updater.windows.installMode |
passive |
The plugin is registered in src-tauri/src/lib.rs (tauri_plugin_updater::Builder::new().build()) and the Actualizador state is .managed there. It is not exposed to the webview: src-tauri/capabilities/default.json has no updater permission, so the page cannot call the plugin, only the four app commands. This follows the rule that all network work is done by Rust (Architecture).
| Item | Role |
|---|---|
Actualizador |
Managed state: Mutex<Option<Pendiente>>, where Pendiente { update: Update, paquete: Option<Vec<u8>> } holds the found update and, once downloaded, the package bytes |
estado(app) → ActualizacionEstado { soportada, actual }
|
soportada() is !cfg!(debug_assertions) && tauri::utils::platform::bundle_type().is_some(): false in dev builds and for binaries that do not come from an installer |
buscar(app, estado) → Option<ActualizacionInfo>
|
Builds the updater (with an on_before_exit hook that saves the window state), calls check(), stores the pending update. Returns ErrorCode::Unsupported if unsupported. ActualizacionInfo { version, actual, notas, fecha, pide_permiso }; pide_permiso is true when the bundle type is Deb or Rpm
|
bajar(estado, version, al_avanzar) |
Downloads the package for that exact version (Actualizador::update(version) returns NotFound if a later check replaced it) while reporting (hecho, total); the plugin verifies the signature during download |
instalar(app, estado, version) |
Takes the package, runs update.install inside spawn_blocking (a .deb/.rpm install waits for the pkexec password), restores the pending update on failure so the user can retry (e.g. cancelled password), and on success calls app.request_restart() so exit events run and the window-state plugin saves the window |
error(e) |
Maps plugin errors: network errors to Network; ReleaseNotFound/TargetNotFound/TargetsNotFound to NotFound (no latest.json on the latest release, or no package for this installer type); AuthenticationFailed to Cancelled; Io to Io; the rest to Internal
|
Install method per installer type: the module documents that it downloads the package of the installer the app was installed with (.deb, .rpm, AppImage, NSIS, MSI or .app) and verifies its signature. .deb/.rpm are installed through pkexec (system password prompt; pide_permiso), and on Windows the installer closes the app itself and relaunches it (hence the on_before_exit window-state save). Inferred: AppImage and macOS .app are replaced in place by tauri-plugin-updater; the exact mechanics live inside that plugin, not in this repository.
| Command | Behaviour |
|---|---|
update_status |
Returns ActualizacionEstado
|
update_check |
Returns Option<ActualizacionInfo>
|
update_download(version) |
Emits update://progress with ActualizacionProgressEvent { hecho, total } (total is 0 when the server does not say) |
update_install(version) |
Busy guards, both before doing anything. First the game: if AppState::hay_partida() (a session in AppState.mesa, even if the user is on another tab) it returns Busy with params.motivo = "partida", which src/lib/errores.ts (MOTIVOS.Busy.partida) shows as «Hay una partida abierta: termínala o sal de la mesa para actualizar.». Then AppState::ocupado(): true while any TrabajoEnMarcha guard from AppState::trabajo() is alive (export, resolving a list, OCR batch, design batch, downloads, game and room preparation; see commands/export.rs, cards.rs, identify.rs, diseno.rs, fuentes.rs, mesa.rs, red.rs, salas.rs) or a game is open; that Busy has no motivo and shows the general «Hay un trabajo en marcha; espera a que termine.». Test con_partida_la_app_esta_ocupada. Why: restarting in the middle of a PDF export would lose it, and in the middle of a game would close it under the player (it is saved, but the session ends) |
The four commands are registered in src-tauri/src/lib.rs and typed in src/lib/ipc.ts (update_status, update_check, update_download, update_install; event update://progress). ActualizacionInfo, ActualizacionEstado and ActualizacionProgressEvent are exported to src/bindings/ by ts-rs.
stateDiagram-v2
[*] --> inactiva
inactiva --> buscando: buscar()
buscando --> alDia: no update
buscando --> disponible: found
buscando --> inactiva: error (no info)
disponible --> bajando: bajar()
bajando --> lista: package downloaded
bajando --> disponible: error
lista --> instalando: instalar()
instalando --> lista: error (retry)
instalando --> [*]: app restarts
(alDia is the step "al-dia".)
State: soportada: boolean | null, actual, paso ("inactiva" | "buscando" | "al-dia" | "disponible" | "bajando" | "lista" | "instalando"), info, progreso (0-100 or null), error (a raw AppError, translated by mensaje).
| Action | Details |
|---|---|
iniciar() |
Asks update_status once and caches; any failure means unsupported |
buscar() |
No-op while busy (buscando, bajando, instalando); if already lista, returns the info without searching again (a downloaded package is kept until restart); otherwise calls update_check
|
bajar() |
Only from disponible. Subscribes to update://progress before calling update_download, converts to a percentage, and unsubscribes in finally
|
instalar() |
Only from lista. Calls guardarAhora() (flush the deck autosave) then update_install. With the table open it only sets alSalirDeLaMesa and returns (see "Updating with a game open" below). If it returns after update_install, it failed: the step returns to lista and the error is thrown. On success the app restarts and this never returns |
| Export | Behaviour |
|---|---|
useActualizacionAutomatica() |
Mounted once in src/App.tsx. If the actualizarSola preference is on (default true, in ui-store), waits 8 s after start (PRIMERA_MS, so the app is fully up), then runs a round (iniciar, buscar, bajar) and repeats every 6 h (CADA_MS). No network or no release means silence |
useAvisosActualizacion() |
Mounted once in App. Subscribes to the store and shows a toast with id actualizacion: a loading toast with the percentage while downloading, a persistent success toast with a "Reiniciar" action when ready (text differs when a password will be asked), and an error toast if the download fails. Works for automatic and manual downloads |
BotonActualizar() |
Top-bar button visible only when a version is lista or instalando
|
PuntoActualizacion() |
Dot on the Settings button: amber and pulsing while downloading, accent colour when ready |
useAvisoAjustes() |
Tooltip text for the Settings button |
Closing the window while an update is bajando or instalando asks for confirmation (ConfirmarCierre, reason "An update is being downloaded or installed."; an update that is only available or already lista does not count). In the other direction, the close guard does not block updating: "Restart and update" saves the deck itself and restarts from Rust with request_restart, which does not go through the window close request. Inferred: from Tauri's behaviour; not exercised end to end. See Settings and Shell.
The install does not refuse and give up: it waits for the player to leave the table and then runs by itself.
-
instalar()does not callupdate_installwhile the table is open (useMesa.activa): it setsalSalirDeLaMesaand returns, with no error and no restart.useAvisosActualizaciontoasts «Se instalará al salir de la mesa.» andBotonActualizarstays disabled, with that same text as its tooltip. The same happens when the screen did not know about the game and Rust refuses withBusymotivo: partida: the store takes that refusal as «wait» (esperaALaPartida), not as a failure. - When the table closes (
useMesa.activagoes from true to false:salir()infeatures/mesa/conexion.tsaftermesa_salir, both «Dejarla para luego» and «Terminarla», or the host closing the game for a guest), the same hook callsinstalar()again: it saves the deck, installs and restarts. If that fails it is toasted like any other install error. - Checking and downloading are not blocked: with auto-update on, the new version is downloaded during the game and only the install waits.
- The Rust guard stays as a safety net (
update_installstill returnsBusywithmotivo: partida, testcon_partida_la_app_esta_ocupada): the app never restarts under a game, even if the screen asked at the wrong time. - A replay opened from the history also lives in
AppState.mesa, so it also holds the install back. Watching an online game as a spectator lives inAppState.mirada, whichhay_partida()does not look at, so it does not. - Inferred: a game that has just ended but is still on the table keeps the install waiting, because the table only closes when the player leaves it.
Actualizaciones (card in the Settings dialog): "Buscar ahora" (manual check; if actualizarSola is on it also downloads), the auto-update switch, a status line, "Descargar la X" when disponible, "Reiniciar y actualizar" when lista, a progress bar, the translated error, and the release notes in a ScrollArea. If soportada === false it explains that updates exist only in the installed app. ReiniciarParaActualizar repeats the restart button in the dialog header. Tested by src/stores/__tests__/actualizacion-store.test.ts.
| Situation | What to know |
|---|---|
| First updater-enabled version | Versions before 2.4.0 have no updater; users install 2.4.0 by hand once |
latest.json missing from the draft |
Secrets TAURI_SIGNING_PRIVATE_KEY absent or the build failed; release.mjs aborts at step 7 |
| Draft not published | Installed apps see nothing (GitHub "latest" ignores drafts) |
| Update on Linux packages |
.deb/.rpm need pkexec and show the password prompt on restart |
| Updating during an export or with a game open |
update_install returns Busy (with a game, motivo: partida and its own message); with the table open the screen does not even ask: the install waits and runs when the player leaves the table (see "Updating with a game open" above, Game Table) |
| Dev builds |
soportada is false; Settings says updates are available only in the installed app |
| Changing the key or endpoint | Breaks all installed copies; the guard test fails first |
| Four version files | Keep them in sync; only release.mjs should change them |
Lockfile and packageManager
|
Commit the lockfile with the packageManager it was generated by, or the release CI install (--frozen-lockfile) fails |
| Re-running failed jobs | Transient network failures in matrix legs can be re-run in GitHub Actions; the draft collects whatever legs have uploaded |
Use and build · Uso y desarrollo
- User-Guide · Guía de usuario
- Development-Setup · Preparación del entorno de desarrollo
- Testing-and-CI · Tests y CI
- Releases-and-Auto-Updates · Versiones y actualizaciones automáticas
- Conventions · Convenciones
Architecture · Arquitectura
- Architecture · Arquitectura
- IPC-and-Error-Handling · IPC y manejo de errores
- Rust-App-Layer · Capa de aplicación Rust
Data · Datos
- Deck-Model · Modelo del mazo
- Storage-and-Blobs · Almacenamiento y blobs
- Deck-Package-Format · Formato del paquete de mazo
- Deck-Library · Biblioteca de mazos
Getting cards · Conseguir cartas
- Decklist-Parsing · Parseo de la lista del mazo
- Scryfall-Integration · Integración con Scryfall
- External-Sources · Fuentes externas
- Card-Identification-and-OCR · Identificación de cartas y OCR
Layout, images, PDF · Maquetación, imagen y PDF
- Layout-Engine · Motor de maquetación
- Imaging-and-PDF · Imagen y PDF
- Export · Exportación
- Live-Preview · Vista previa en vivo
- Statistics · Estadísticas
Card designer · Diseñador de cartas
- Designer-Overview · Visión general del diseñador
- Designer-Engine · Motor del diseñador
- Designer-Composition · Composición del diseñador
- Frame-Packs · Packs de marcos
- Designer-App-Layer · Capa de aplicación del diseñador
- Designer-UI · Interfaz del diseñador
- Card-Editor-Dialog · Diálogo Editar carta
Game table · Mesa de juego
Frontend · Frontend
- Frontend-Architecture · Arquitectura del frontend
- State-Stores · Stores de estado
- Internationalization · Internacionalización
- Themes-and-Skins · Temas y skins
- Settings-and-Shell · Ajustes y shell
- Tutorials · Tutoriales
- Decklist-Panel · Panel de la lista del mazo
- Add-Card-and-Variants · Añadir carta y variantes
- Set-Browser · Explorador de sets
Reference · Referencia