Compare commits
2 Commits
v0.1.6
...
54e691a4c9
| Author | SHA1 | Date | |
|---|---|---|---|
| 54e691a4c9 | |||
| 52862db8ad |
@@ -129,8 +129,23 @@ jobs:
|
|||||||
# The Tauri updater fetches a manifest describing the newest version, its
|
# The Tauri updater fetches a manifest describing the newest version, its
|
||||||
# notes, and per-target {signature, url}. The URL points at the MIRROR
|
# notes, and per-target {signature, url}. The URL points at the MIRROR
|
||||||
# repo (mca/public_releases) — that's the unauthenticated endpoint field
|
# repo (mca/public_releases) — that's the unauthenticated endpoint field
|
||||||
# appliances actually reach; see the workflow header for why. Adjust the
|
# appliances actually reach; see the workflow header for why.
|
||||||
# platform keys you actually ship.
|
#
|
||||||
|
# ONE ENTRY PER INSTALLER TYPE — this is what made every in-app update
|
||||||
|
# v0.1.0→v0.1.6 fail. tauri-plugin-updater looks up
|
||||||
|
# `{os}-{arch}-{installer}` FIRST (linux-x86_64-deb / -rpm / -appimage,
|
||||||
|
# from the running app's detected bundle type) and only then the bare
|
||||||
|
# `linux-x86_64`. The booths run the .deb, and the manifest used to
|
||||||
|
# carry ONLY `linux-x86_64` → the AppImage. So a .deb install found the
|
||||||
|
# "update", downloaded the AppImage, verified its signature fine, then
|
||||||
|
# handed the bytes to install_deb(), which checks they're a .deb
|
||||||
|
# (infer::archive::is_deb) and bails with InvalidUpdaterFormat — after
|
||||||
|
# the download, before any relaunch, with the error swallowed client-
|
||||||
|
# side until v0.1.6. Now each installer gets its own signed asset; the
|
||||||
|
# bare key stays for an AppImage install. .deb/.rpm updates run
|
||||||
|
# `pkexec dpkg -i` / `rpm -U`, so the operator sees a polkit password
|
||||||
|
# prompt — intended: updating a root-installed package IS an admin
|
||||||
|
# action on this box (see wiki/decisions/desktop-shell-tauri.md).
|
||||||
env:
|
env:
|
||||||
SERVER_URL: ${{ github.server_url }}
|
SERVER_URL: ${{ github.server_url }}
|
||||||
MIRROR_REPO: mca/public_releases
|
MIRROR_REPO: mca/public_releases
|
||||||
@@ -138,22 +153,41 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
set -e
|
set -e
|
||||||
VERSION="${TAG#v}"
|
VERSION="${TAG#v}"
|
||||||
APPIMAGE=$(cd dist && ls *.AppImage | head -1)
|
ASSET_BASE="${SERVER_URL}/${MIRROR_REPO}/releases/download/desktop-latest"
|
||||||
SIG=$(cat "dist/${APPIMAGE}.sig")
|
cat > /tmp/latest.js <<'JS'
|
||||||
ASSET_URL="${SERVER_URL}/${MIRROR_REPO}/releases/download/desktop-latest/${APPIMAGE}"
|
const fs = require("fs");
|
||||||
cat > dist/latest.json <<JSON
|
const [version, tag, base] = process.argv.slice(2);
|
||||||
{
|
const files = fs.readdirSync("dist");
|
||||||
"version": "${VERSION}",
|
const pick = (ext) => files.find((f) => f.endsWith(ext));
|
||||||
"notes": "Parking System ${TAG}",
|
const entry = (f) => ({
|
||||||
"pub_date": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
signature: fs.readFileSync(`dist/${f}.sig`, "utf8").trim(),
|
||||||
"platforms": {
|
url: `${base}/${f}`,
|
||||||
"linux-x86_64": {
|
});
|
||||||
"signature": "${SIG}",
|
const deb = pick(".deb"), rpm = pick(".rpm"), appimage = pick(".AppImage");
|
||||||
"url": "${ASSET_URL}"
|
if (!deb || !appimage) {
|
||||||
}
|
console.error(`missing bundle in dist/: deb=${deb} appimage=${appimage}`);
|
||||||
}
|
process.exit(1);
|
||||||
}
|
}
|
||||||
JSON
|
const platforms = {
|
||||||
|
"linux-x86_64-deb": entry(deb),
|
||||||
|
...(rpm ? { "linux-x86_64-rpm": entry(rpm) } : {}),
|
||||||
|
"linux-x86_64": entry(appimage),
|
||||||
|
};
|
||||||
|
fs.writeFileSync(
|
||||||
|
"dist/latest.json",
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
version,
|
||||||
|
notes: `Parking System ${tag}`,
|
||||||
|
pub_date: new Date().toISOString().replace(/\.\d+Z$/, "Z"),
|
||||||
|
platforms,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
) + "\n",
|
||||||
|
);
|
||||||
|
JS
|
||||||
|
node /tmp/latest.js "${VERSION}" "${TAG}" "${ASSET_BASE}"
|
||||||
echo "latest.json:"; cat dist/latest.json
|
echo "latest.json:"; cat dist/latest.json
|
||||||
|
|
||||||
- name: Create release + upload assets (Gitea API)
|
- name: Create release + upload assets (Gitea API)
|
||||||
|
|||||||
@@ -44,6 +44,14 @@ see that workflow's header and `wiki/decisions/desktop-shell-tauri.md`). The upd
|
|||||||
signing pubkey live in `tauri.conf.json`; the private signing key is held outside the repo, never
|
signing pubkey live in `tauri.conf.json`; the private signing key is held outside the repo, never
|
||||||
committed.
|
committed.
|
||||||
|
|
||||||
|
**The manifest carries one entry per installer type** (`linux-x86_64-deb`, `linux-x86_64-rpm`,
|
||||||
|
and bare `linux-x86_64` for AppImage). The updater picks the entry matching how the running app
|
||||||
|
was installed — a `.deb` install will only ever accept a signed `.deb`. Booths run the `.deb`,
|
||||||
|
so an in-app update ends in a **polkit password prompt** (`pkexec dpkg -i`): that is expected,
|
||||||
|
and it is the right gate — the package lives in `/usr/bin`, root-owned, and the operator is not
|
||||||
|
supposed to be able to replace it silently. Cancel the prompt and the app keeps running the old
|
||||||
|
version; the failure is logged to the server's Logs viewer.
|
||||||
|
|
||||||
## Release gate — run the REAL bundle locally before tagging
|
## Release gate — run the REAL bundle locally before tagging
|
||||||
|
|
||||||
`tauri dev` loads the SPA from `http://localhost:5173`, a plain http origin. The shipped bundle
|
`tauri dev` loads the SPA from `http://localhost:5173`, a plain http origin. The shipped bundle
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ REGISTRY=git.infra.msai.al/mca/parking_solution
|
|||||||
# Staging booth: pinned immutable stage-<sha>. After each promotion (merge dev → stage, CI builds
|
# Staging booth: pinned immutable stage-<sha>. After each promotion (merge dev → stage, CI builds
|
||||||
# :stage-<sha>), bump this to the new sha and re-sync/deploy from Core. The moving `:stage` tag
|
# :stage-<sha>), bump this to the new sha and re-sync/deploy from Core. The moving `:stage` tag
|
||||||
# exists as the pointer; we deploy the sha, not the mover.
|
# exists as the pointer; we deploy the sha, not the mover.
|
||||||
TAG=stage-5c6a21e
|
TAG=stage-8fa66c9
|
||||||
COOKIE_SECURE=0
|
COOKIE_SECURE=0
|
||||||
VISION_ENABLED=1
|
VISION_ENABLED=1
|
||||||
# Desktop app WS handshake: Origin is tauri://localhost (set explicitly by
|
# Desktop app WS handshake: Origin is tauri://localhost (set explicitly by
|
||||||
@@ -82,7 +82,7 @@ REGISTRY=git.infra.msai.al/mca/parking_solution
|
|||||||
# Staging booth: pinned immutable stage-<sha>. After each promotion (merge dev → stage, CI builds
|
# Staging booth: pinned immutable stage-<sha>. After each promotion (merge dev → stage, CI builds
|
||||||
# :stage-<sha>), bump this to the new sha and re-sync/deploy from Core. The moving `:stage` tag
|
# :stage-<sha>), bump this to the new sha and re-sync/deploy from Core. The moving `:stage` tag
|
||||||
# exists as the pointer; we deploy the sha, not the mover.
|
# exists as the pointer; we deploy the sha, not the mover.
|
||||||
TAG=stage-5c6a21e
|
TAG=stage-8fa66c9
|
||||||
COOKIE_SECURE=0
|
COOKIE_SECURE=0
|
||||||
VISION_ENABLED=1
|
VISION_ENABLED=1
|
||||||
# Desktop app WS handshake: Origin is tauri://localhost (set explicitly by
|
# Desktop app WS handshake: Origin is tauri://localhost (set explicitly by
|
||||||
|
|||||||
@@ -399,7 +399,8 @@ a fresh handshake every 10 s (use-live-feed's capped backoff), i.e. every connec
|
|||||||
it as `Authorization: Bearer` (fastify-jwt would accept it) — that puts the session token in JS,
|
it as `Authorization: Bearer` (fastify-jwt would accept it) — that puts the session token in JS,
|
||||||
which HttpOnly exists to prevent; the ticket keeps it out. Verified locally with an 11-case
|
which HttpOnly exists to prevent; the ticket keeps it out. Verified locally with an 11-case
|
||||||
handshake script: ticket/no-cookie → 101 + hello; reused/bogus/absent → 401; ticket + bad Origin
|
handshake script: ticket/no-cookie → 101 + hello; reused/bogus/absent → 401; ticket + bad Origin
|
||||||
→ 403; cookie path unchanged.
|
→ 403; cookie path unchanged. **Field-verified 2026-09-04:** v0.1.6 on the park-2 booth against
|
||||||
|
image `stage-8fa66c9` shows **LIVE** — the first desktop build to do so.
|
||||||
- **Desktop client logs had never reached `app_logs`.** `logger.ts`'s flush read the CSRF token
|
- **Desktop client logs had never reached `app_logs`.** `logger.ts`'s flush read the CSRF token
|
||||||
from `document.cookie` (null on desktop — the same jar split as above), so every
|
from `document.cookie` (null on desktop — the same jar split as above), so every
|
||||||
`POST /api/logs` from the desktop 403'd under `requireAuth`→`assertCsrf`, and the flush drops
|
`POST /api/logs` from the desktop 403'd under `requireAuth`→`assertCsrf`, and the flush drops
|
||||||
@@ -420,3 +421,37 @@ a fresh handshake every 10 s (use-live-feed's capped backoff), i.e. every connec
|
|||||||
cookie-jar split *cannot* reproduce in dev mode. The pre-tag gate is now: build the bundle
|
cookie-jar split *cannot* reproduce in dev mode. The pre-tag gate is now: build the bundle
|
||||||
locally, run the AppImage against a local server, log in, confirm **LIVE**, do one mutation,
|
locally, run the AppImage against a local server, log in, confirm **LIVE**, do one mutation,
|
||||||
and confirm a desktop-sourced row appears in the Logs viewer (`apps/desktop/README.md`).
|
and confirm a desktop-sourced row appears in the Logs viewer (`apps/desktop/README.md`).
|
||||||
|
|
||||||
|
### In-app update never worked: the manifest only described the AppImage, the booths run the .deb (2026-09-04)
|
||||||
|
|
||||||
|
Every self-update attempt from v0.1.0 through v0.1.6 ended the same way — prompt, download
|
||||||
|
traffic, then nothing, and the same prompt again next launch. The version-sync (v0.1.2) and
|
||||||
|
error-logging fixes were real but not the cause. **Root cause:** `tauri-plugin-updater` resolves
|
||||||
|
the download target as `{os}-{arch}-{installer}` **first** (`linux-x86_64-deb` here — the
|
||||||
|
bundler stamps `__TAURI_BUNDLE_TYPE_VAR_DEB` into the `.deb`'s binary, verified with `strings`
|
||||||
|
on a local build), then falls back to bare `{os}-{arch}`. `release.yml`'s `latest.json` carried
|
||||||
|
**only** `linux-x86_64`, pointing at the **AppImage**. So a `.deb` install found the update,
|
||||||
|
downloaded the AppImage, verified its signature (which was correct — for the AppImage), then
|
||||||
|
handed the bytes to `install_deb()`, whose first line checks `infer::archive::is_deb(bytes)` and
|
||||||
|
returns `InvalidUpdaterFormat`. Before v0.1.6 that error never reached the server (the desktop
|
||||||
|
log channel was itself broken — see the previous section), so it looked like a silent no-op.
|
||||||
|
Sources: `tauri-plugin-updater-2.10.1/src/updater.rs` (`get_urls`, `install_inner`,
|
||||||
|
`install_deb`), `tauri-utils/src/platform.rs` (`bundle_type`).
|
||||||
|
|
||||||
|
- **Fix:** `latest.json` now carries one signed entry per installer — `linux-x86_64-deb`,
|
||||||
|
`linux-x86_64-rpm` (when built), and bare `linux-x86_64` for the AppImage — assembled by a
|
||||||
|
small Node script in the workflow (the `.sig` files for `.deb`/`.rpm` were already being
|
||||||
|
produced and uploaded, just never referenced).
|
||||||
|
- **What a booth update now looks like:** prompt → download → **polkit password dialog**
|
||||||
|
(`pkexec dpkg -i`) → relaunch into the new version. The prompt is deliberate, not a wart: the
|
||||||
|
package is root-owned in `/usr/bin`, and under the [[threat-model]] the operator must not be
|
||||||
|
able to replace the app silently; whoever brings the box online for an update is the admin.
|
||||||
|
Cancelling the dialog leaves the old version running and logs
|
||||||
|
`desktop_update_install_failed` to `app_logs`.
|
||||||
|
- **Rejected:** switching booths to the AppImage so updates need no privilege. It would work
|
||||||
|
(the updater rewrites the AppImage in place), but the binary would then be operator-writable,
|
||||||
|
it needs FUSE on the appliance image, and launcher/autostart integration becomes manual —
|
||||||
|
three regressions to avoid one password prompt.
|
||||||
|
- **Judgment note for the retrospective:** three fixes were shipped against this symptom
|
||||||
|
without reading the updater's install path once. The whole chain is ~60 lines of vendored
|
||||||
|
Rust in `~/.cargo/registry`; it names the exact failure (`InvalidUpdaterFormat`).
|
||||||
|
|||||||
+13
@@ -2860,3 +2860,16 @@ uses the unauthenticated /health (extended with app: "parking-system") instead o
|
|||||||
plugin does set Origin itself, the http-scope "quirk" is URLPattern default-port semantics) and
|
plugin does set Origin itself, the http-scope "quirk" is URLPattern default-port semantics) and
|
||||||
added a local-AppImage pre-tag gate to the desktop README, since tauri dev cannot reproduce any
|
added a local-AppImage pre-tag gate to the desktop README, since tauri dev cannot reproduce any
|
||||||
of these origin-dependent bugs. Full detail on [[desktop-shell-tauri]].
|
of these origin-dependent bugs. Full detail on [[desktop-shell-tauri]].
|
||||||
|
|
||||||
|
## [2026-09-04] fix | Desktop in-app update never worked: latest.json described only the AppImage, booths run the .deb
|
||||||
|
|
||||||
|
tauri-plugin-updater looks up `{os}-{arch}-{installer}` first (linux-x86_64-deb — the bundler
|
||||||
|
stamps the installer type into the binary; verified with strings on a local .deb) and only then
|
||||||
|
bare linux-x86_64. release.yml's latest.json carried only the bare key → the AppImage, so every
|
||||||
|
.deb install downloaded the AppImage, passed signature verification, then failed install_deb()'s
|
||||||
|
is_deb check with InvalidUpdaterFormat — invisible until v0.1.6 fixed the desktop log channel.
|
||||||
|
This, not version drift or swallowed errors, is why v0.1.0→…→v0.1.6 never self-updated.
|
||||||
|
latest.json now has one signed entry per installer (deb, rpm, AppImage); a .deb update ends in a
|
||||||
|
polkit password prompt (pkexec dpkg -i), which is the intended admin gate on a root-installed
|
||||||
|
package. README + [[desktop-shell-tauri]] updated. First real test: tag v0.1.7 and accept the
|
||||||
|
prompt on the v0.1.6 booth.
|
||||||
|
|||||||
Reference in New Issue
Block a user