Repository navigation
Releases and Auto Updates
English · Español
How a version is cut (pnpm release), what the release workflow builds (desktop installers and the Android APK), how they are signed, 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
GH-->>GH: android job (after the desktop legs): signed APK + its .sig into the same draft
S->>GH: set notes on draft and in latest.json, add the android-aarch64 entry
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.
-
The Android APK is part of the same release. It is built by the
androidjob of the same workflow into the same draft, and listed in the samelatest.jsonunderandroid-aarch64. Who writes what inlatest.jsonis deliberate; see "The Android APK" below.
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). If the run fails,esperarEjecucion(withtoleraAndroid) reads its jobs (gh run view --json jobs) and classifies them withfallosDeRelease(scripts/release-notas.mjs, pure and tested: failed, cancelled or timed-out jobs, split into the one namedandroidand the desktop ones). If onlyandroidfailed (typically theANDROID_KEY_*secrets are missing) it warns and carries on without the APK:latest.jsongets noandroid-aarch64entry, so phones do not see this version, and a loose.apkleft in the draft should be deleted before publishing. If any desktop job failed it stops as before, naming those jobs. Why: the desktop installers are the main release and should not wait for the phone; a release with a broken desktop build must never go out. -
Validate the draft: it must be a draft, its
targetCommitishmust equal the pushed commit, if theandroidjob passed it must contain the APK (Tapete_<version>_android-arm64.apk) and its.sig(otherwise theANDROID_KEY_*secrets are probably missing) and alatest.jsonasset (otherwise the signing secret is probably missing). -
Put the notes on the draft (
componer(notas)appends the fixed text) and rewritelatest.json: itsnoteswith the real notes (notasDelActualizador) and, when theandroidjob passed, the Android entry (conAndroid: keyandroid-aarch64, the download URL of the APK in this release and the contents of its.sig), 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), nombreApk(version) (the APK name; it must match the name release.yml gives the APK, because the android-aarch64 URL in latest.json is built from it) and conAndroid(json, version, repo, firma) (throws if the JSON is not that version's or the signature is empty or not base64; leaves everything already there untouched).
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; the build job is a matrix over four legs (fail-fast: false), followed by the android job:
| 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 Tapete v__VERSION__ (body: the fixed text "Instaladores de Tapete (https://tapete.zodhub.app). Sin firma de código: ver el README.", the same string as TEXTO_FIJO) 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.
The android job runs on ubuntu-24.04 and needs: build: the draft release is created by tauri-action in build, and the APK is uploaded to it, so the job has to wait for it. Steps: Java 21 (Temurin), the Android SDK (android-actions/setup-android) with platform 37, build-tools 36.0.0 and the stable NDK 28.2 installed through sdkmanager (NDK_HOME set from it), Node, Rust with the aarch64-linux-android target, pnpm install --frozen-lockfile, then:
-
The signing key. The secrets
ANDROID_KEY_ALIAS,ANDROID_KEY_PASSWORDandANDROID_KEY_BASE64(the.jksin base64) are written askeystore.propertiesinsrc-tauri/gen/android/, read byapp/build.gradle.kts(onepasswordfor both store and key). If any is missing the job fails on purpose before compiling (an unsigned APK is not installable). Without the file, a local build still works but producesapp-universal-release-unsigned.apk. A last step (if: always()) deletes the key and the properties file. -
Build:
pnpm tauri android build --apk --target aarch64 --ciwithCARGO_PROFILE_RELEASE_STRIP=symbolsset only on this step. Why: without it the Rust library weighs about 100 MB and the arm64 release APK about 102 MB; stripped symbols bring the library to about 72 MB, and the APK stores it uncompressed, so the APK shrinks by about the same 28 MB. Desktop binaries are not affected (the variable is set on this step only). Onlyaarch64is built for releases (x86_64 is for the emulator). -
Name, sign and check. There must be exactly one release APK, not
*unsigned*; it is copied toTapete_<version>_android-arm64.apk;apksigner verifymust pass;aapt dump badgingmust show aversionCodeofmajor*1000000 + minor*1000 + patch(what Tauri derives from the version; it has to grow with each release or Android refuses to update); and the updater signature is made withpnpm tauri signer sign --app-version <version>using the same updater key as the desktop installers, giving the.sig.--app-versionis needed becauserequireSignedVersionexpects the version inside the signature. -
Upload the APK and the
.sigto the draft withgh release upload --clobber. It does not touchlatest.json.
Why the job does not edit latest.json: the desktop legs of build rewrite that file in parallel (each merges its entries) and a fourth writer would overwrite or be overwritten. Instead scripts/release.mjs adds android-aarch64 at the end, once every job has finished, from the .sig it downloads.
The expected files in the draft are now 19: nine installers and bundles, seven .sig, latest.json, plus the APK and its .sig (it was 17 before Android). If a job fails because of the network, re-run only the failed jobs of that run and publish with all five jobs green and the draft complete.
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.
From 4.0.0 the visible name of the app is Tapete: productName and the window titles (tauri.conf.json, tauri.android.conf.json), the Android app_name (res/values/strings.xml), index.html, the strings of the interface that name the app, the release title and text, the APK file name, the PDF metadata, the .pbdeck filter name in file dialogs, the exported identity file name (identidad-tapete.json) and the HTTP User-Agent (tls::AGENTE, see Rust-App-Layer).
The technical identity does not change, on purpose: it is what keeps an installed copy updating over itself and finding its data.
| Kept as it was | Why |
|---|---|
identifier com.estigio.proxybuilder
|
The data folders, the Android application id and the Windows registry keys derive from it; changing it would start every user with an empty library, and Android would refuse to install the APK over the old one |
Data folders, log file proxy-builder.log, localStorage keys proxybuilder.*
|
Same data for the old and the new name |
ALPN proxy-builder/*/1, codes PB1- / PBD1-
|
A 3.x and a 4.0 copy keep playing and passing decks to each other |
.pbdeck and its manifest format; identity file type proxy-builder-identidad
|
Files written by older versions still open, and older versions still open new files |
proxybuilder:// links (still accepted; the app now shares the web links https://tapete.zodhub.app/join/…, and tapete:// is accepted since 3.6.0) |
Links already sent keep opening the app, and a 3.5 or earlier copy only understands proxybuilder://
|
Crate and package names, executable proxy-builder, packs repository proxy-builder-packs, Android Keystore alias |
Internal names nobody sees; the Keystore alias seals the identity key, and a new alias could not open it |
Because productName changes, the installers would otherwise leave two apps installed side by side (the install folder, the Windows "Uninstall" entry and the Linux package name all come from it). Each installer therefore replaces the old one:
-
.deb:bundle.linux.debdeclaresconflicts,replacesandprovides: proxy-builder, sodpkg/aptremoves the old package when the new one goes in. -
.rpm:bundle.linux.rpmdeclaresconflictsandobsoletes: proxy-builder. -
Windows (NSIS):
bundle.windows.nsis.installerHookspoints tosrc-tauri/windows/hooks.nsh. ItsNSIS_HOOK_PREINSTALLlooks up the old "Proxy Builder" entry underHKCU(Tauri's defaultcurrentUsermode; the install folder is read fromSoftware\<publisher>\Proxy Builder, the publisher coming from theidentifier) and, if it exists in a folder other than the new one, runs itsuninstall.exewith/S _?=<folder>and then deletesuninstall.exeand the folder.-
PREINSTALL, not later: the old uninstaller deletes the registry entries of the links (
proxybuilder://,tapete://); the new installer writes them afterwards, so they survive. -
/S: silent, so the "delete the app data" checkbox stays unticked. The data is shared by both names because it derives from the unchangedidentifier. Without/UPDATE, so the old shortcuts with the old name go too. -
_?=: runs the uninstaller in place so the installer can wait for it; that is whyuninstall.exeand the folder are removed by hand afterwards.
-
PREINSTALL, not later: the old uninstaller deletes the registry entries of the links (
- AppImage and macOS: nothing to declare. Inferred: the updater writes the new bundle where the old one was, so there is no second copy.
These installer rules have not yet been tried on real systems; the first 4.0.0 install on each one is the test.
- 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.
-
The Android signing key is a second, separate secret and just as irreplaceable. Android only installs an update if it is signed with the same key as the installed app; with another key the install fails (
INSTALL_FAILED_UPDATE_INCOMPATIBLE) and the only way out is uninstalling, which deletes the app's data (the library). It lives in a keystore kept by the maintainer outside the repository and in the repository secretsANDROID_KEY_ALIAS,ANDROID_KEY_PASSWORDandANDROID_KEY_BASE64. Never replace it; keep copies in two places. The APK therefore carries two signatures with different jobs: Android's (who may update it) and the updater's minisign.sig(a second barrier checked by the app before handing the file to the system). -
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 |
1. https://github.com/PoolFlamingo/tapete/releases/latest/download/latest.json 2. 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 |
bundle.android.minSdkVersion (tauri.android.conf.json) |
26 |
Two endpoints, in this order. The repository is going to be renamed for the app's new name (Tapete). The updater plugin tries the endpoints in order and moves to the next one when a URL answers with an error status, so: before the rename the new URL gives 404 and the old one answers; after the rename the new one answers directly (and the old one would still work, because GitHub redirects a renamed repository). Either way the copies installed today reach the versions published under the new name. The test la_configuracion_del_actualizador_es_valida pins both URLs in this order. PAGINA_DE_RELEASES (actualizar.rs, opened by update_open_release) keeps the old repository URL on purpose: it is a page in the browser, not a download, and GitHub's redirect covers it.
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<Paquete> } holds the found update and, once downloaded, the package: Paquete::Memoria(Vec<u8>) on desktop (kept in memory until installed) or Paquete::Apk(PathBuf) on Android (the APK written in app_cache_dir/actualizacion/: it weighs tens of MB and the system installer reads it from a file) |
estado(app) → ActualizacionEstado { soportada, actual, instalaElSistema }
|
soportada() is `!cfg!(debug_assertions) && (bundle_type().is_some() |
buscar(app, estado) → Option<ActualizacionInfo>
|
Builds the updater (with an on_before_exit hook that saves the window state), on Android sets the target android-<arch> (std::env::consts::ARCH, so android-aarch64), calls check(), stores the pending update; when already up to date on Android it deletes a previously downloaded APK. 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, cache, 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. On Android the bytes are then written to app_cache_dir/actualizacion/Tapete_<version>.apk (ruta_apk) after emptying that folder (only one APK fits) |
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 |
instalar_android (private) |
Android's instalar: looks for the downloaded APK of that version, checks the minisign signature again (update.verify) and hands it to android::instalar_apk. No restart. The package stays if the person cancels the system installer, so they can press Install again. If the system does not yet let the app install packages, the plugin opens the "install unknown apps" setting of the app and Rust answers Unsupported with motivo: instalar_permiso: the person allows it, comes back and presses Install again |
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. On Android the plugin looks up and downloads (with the signature checked) but does not install: that is the system installer's job, as above.
The only Cargo feature of the app crate. With it, the updater reads the latest.json at the URL in PB_ACTUALIZAR_URL (at run time or, failing that, the value present when compiling) instead of GitHub's, so an emulator (x86_64, hence the android-x86_64 target) can update from a file served on the development machine. 🛑 It is never turned on by default, by CI, by the workflow or by package.json: a test (la_feature_de_pruebas_no_se_activa_nunca_sola) reads Cargo.toml, ci.yml, release.yml, package.json and tauri.conf.json and fails if the feature or a default = [ appears. Without it an update URL cannot be changed in a release build.
| 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 preparation; see commands/export.rs, cards.rs, identify.rs, diseno.rs, fuentes.rs, mesa.rs, red.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 |
The same store and screens serve Android, with instalaElSistema changing the words and one step: "Install" instead of "Restart" (toast action, Settings button), and no restart, so after update_install returns the step goes back to lista (the system installer is on screen; if the person cancels it they can press Install again). The first time, Unsupported with instalar_permiso shows its own sentence ("Android needs your permission to install the update: its setting has been opened. Allow installing apps from Proxy Builder, come back and press Install again"). Installing with the game table open still waits until the table is left (see below); Rust also answers Busy there.
The flow, end to end: check (the target android-aarch64 of latest.json) → download to app_cache_dir/actualizacion/ with the minisign signature checked → Install → signature checked again → system installer (after the "install unknown apps" setting the first time) → Android verifies that the signing key and a higher versionCode match → the app is replaced and relaunched. Android requires the same signing key and a larger versionCode; the minisign signature is a second barrier. Library, lists, history and network identity survive an update (they live in the app's data folder).
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 |
| Files in the draft | 19 expected (17 before Android); release.mjs aborts if the APK or its .sig is missing |
| Android key lost or changed | Nobody can update over an installed app; reinstalling deletes the library. Keep the keystore and its password in two places |
android job failing the first time |
It had only been run locally before: SDK package names, Gradle cache or the secrets are the usual causes. Re-run only the failed job and read its log first. pnpm release itself goes on without the APK when only that job fails (step 6) |
| Android and unknown sources | The first update asks for "install unknown apps" for the app (Tapete from 4.0.0, Proxy Builder before); some manufacturers put the setting elsewhere, and Play Protect may say "app not recognised": Install anyway |
| Installing 4.0.0 over Proxy Builder | The installers remove the old package or installation (see "From Proxy Builder to Tapete"); data, identity and links stay. Check it on each system the first time |
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
- Rename-to-Tapete · Cambio de nombre a Tapete
- License · Licencia
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
- The-Card-Stamp · El sello de las cartas
- 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
- Game-Engine · Motor de la mesa
- Game-Table · Mesa de juego
- Online-Play · Juego en línea
- Friends-and-Messages · Amigos y mensajes
Mobile · Móvil
Frontend · Frontend
- Frontend-Architecture · Arquitectura del frontend
- State-Stores · Stores de estado
- Internationalization · Internacionalización
- Themes-and-Skins · Temas y skins
- Icons · Iconos
- 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
- Card-Search · Búsqueda de cartas
- Set-Browser · Explorador de sets
Reference · Referencia