In-app updates on Windows
ci / build (ubuntu-22.04) (push) Canceled after 0s
ci / build (windows-2025) (push) Canceled after 0s
ci / deny (push) Canceled after 0s

The Microsoft Store doesn't update EXE installs, so the app updates itself
with Tauri's updater. It reads updater/latest.json from the Kestrove Gitea a
minute after startup and once a day. A new version shows as a banner and in
Settings > Updates, and installs only on "Update and restart". The vault locks
before the installer takes over.

The installer must be signed with the Kestrove updater key, with the signature
bound to its version (requireSignedVersion), so a compromised download server
can hold updates back but can't ship one or replay an old installer. Signing
is a release step, after Authenticode: gitea-release.sh signs, uploads the
.sig, and writes latest.json last. Plain builds need no key.

Checks reveal only the IP address and app version, and can be turned off in
Settings. endpoints is a list, so a mirror can be added. Linux packages update
through their package manager.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-30 18:19:24 +04:00
co-authored by Claude Opus 5.5
parent 70ccff34f2
commit d70dcc04b6
9 changed files with 634 additions and 22 deletions
+33 -3
View File
@@ -1,10 +1,17 @@
#!/bin/sh
# Creates (or reuses) the Gitea release for this version and uploads the desktop bundles to it.
# Creates (or reuses) the Gitea release for this version, uploads the desktop bundles to it, and writes
# updater/latest.json, which the Windows app checks for updates (src-tauri/src/updates.rs).
#
# GITEA_TOKEN=… scripts/gitea-release.sh [dir-with-files] (default ../release/desktop)
# GITEA_TOKEN=… TAURI_SIGNING_PRIVATE_KEY_PATH=… scripts/gitea-release.sh [dir-with-files] (default ../release/desktop)
#
# GITEA_TOKEN: a Gitea access token with the write:repository scope (Settings → Applications). The tag
# v<version> must already be pushed. Files: *.AppImage, *.deb, *.rpm, *-setup.exe, kestrove-nmh-*, SHA256SUMS.
#
# Updates: the Windows installer needs an updater signature (<installer>.sig) from the Kestrove updater key, whose
# public half is in src-tauri/tauri.conf.json. Sign after Authenticode signing: the signature covers the final
# file. With TAURI_SIGNING_PRIVATE_KEY_PATH set, a missing .sig is made here (the key's password is asked for, or
# read from TAURI_SIGNING_PRIVATE_KEY_PASSWORD). Optional NOTES: the text shown with the update.
# Users get the update only once updater/latest.json is committed and pushed to main (printed at the end).
set -eu
cd "$(dirname "$0")/.."
: "${GITEA_TOKEN:?set GITEA_TOKEN (Gitea → Settings → Applications → token with write:repository)}"
@@ -14,6 +21,15 @@ DIR=${1:-../release/desktop}
V=$(node -p 'require("./package.json").version')
TAG="v$V"
AUTH="Authorization: token $GITEA_TOKEN"
SETUP="$DIR/Kestrove_${V}_x64-setup.exe"
if [ -f "$SETUP" ] && [ ! -f "$SETUP.sig" ]; then
: "${TAURI_SIGNING_PRIVATE_KEY_PATH:?$SETUP.sig is missing: set TAURI_SIGNING_PRIVATE_KEY_PATH to the updater key to sign it}"
# --app-version binds the signature to this version (the app requires it), so an older signed installer can't
# be offered again under a newer version number.
npx tauri signer sign -f "$TAURI_SIGNING_PRIVATE_KEY_PATH" ${TAURI_SIGNING_PRIVATE_KEY_PASSWORD+-p "$TAURI_SIGNING_PRIVATE_KEY_PASSWORD"} \
--app-version "$V" "$SETUP"
fi
(cd "$DIR" && rm -f SHA256SUMS && shasum -a 256 -- *.AppImage *.deb *.rpm *-setup.exe kestrove-nmh-* 2>/dev/null > SHA256SUMS; cat SHA256SUMS)
@@ -22,9 +38,23 @@ if [ -z "$ID" ]; then
BODY=$(node -e 'console.log(JSON.stringify({tag_name: process.argv[1], name: "Kestrove " + process.argv[2], body: "Kestrove desktop " + process.argv[2] + " for Windows and Linux. On a Mac, use the Kestrove app from the Mac App Store instead.\n\nVerify downloads with SHA256SUMS.", draft: false, prerelease: false}))' "$TAG" "$V")
ID=$(curl -fsS -X POST -H "$AUTH" -H "Content-Type: application/json" -d "$BODY" "$API/repos/$REPO/releases" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>console.log(JSON.parse(s).id))')
fi
for f in "$DIR"/*.AppImage "$DIR"/*.deb "$DIR"/*.rpm "$DIR"/*-setup.exe "$DIR"/kestrove-nmh-* "$DIR"/SHA256SUMS; do
for f in "$DIR"/*.AppImage "$DIR"/*.deb "$DIR"/*.rpm "$DIR"/*-setup.exe "$DIR"/*-setup.exe.sig "$DIR"/kestrove-nmh-* "$DIR"/SHA256SUMS; do
[ -f "$f" ] || continue
echo "uploading $(basename "$f")"
curl -fsS -X POST -H "$AUTH" -F "attachment=@$f" "$API/repos/$REPO/releases/$ID/assets?name=$(basename "$f")" >/dev/null
done
echo "Release: ${API%/api/v1}/$REPO/releases/tag/$TAG"
# The update manifest, written only after the installer it points to is uploaded.
if [ -f "$SETUP.sig" ]; then
mkdir -p updater
node -e '
const [v, url, sigFile, notes] = process.argv.slice(1);
const signature = require("fs").readFileSync(sigFile, "utf8").trim();
const doc = { version: v, notes, pub_date: new Date().toISOString().replace(/\.\d+Z$/, "Z"),
platforms: { "windows-x86_64": { signature, url } } };
require("fs").writeFileSync("updater/latest.json", JSON.stringify(doc, null, 2) + "\n");
' "$V" "${API%/api/v1}/$REPO/releases/download/$TAG/$(basename "$SETUP")" "$SETUP.sig" "${NOTES:-Kestrove $V}"
echo "Wrote updater/latest.json. To offer the update, commit it and push to main:"
echo " git add updater/latest.json && git commit -m \"Offer $V as an update\" && git push origin main"
fi