- **New Features** - Added duplicate-entity review, merge previews, and “keep separate” decisions for characters, locations, items, and lore entries. - Added manual lore-management controls, progress summaries, and safeguards against concurrent runs. - Added shared chapter-query limits and caching. - **Bug Fixes** - Prevented manual chapter creation during active background work. - Improved name matching, duplicate handling, and lore-field cleanup. - Migrated context settings while preserving existing preferences. - **Documentation** - Streamlined the product overview, setup instructions, development guidance, and technology information.
8.6 KiB
Release, CI and Updates
How a build reaches a user, and why publishing the draft is the step that ships it.
Git Hooks
Managed by lefthook (lefthook.yml):
- pre-commit: runs
scripts/check_migrations.jsagainst stagedsrc-tauri/migrations/*.sqlfiles to reject CRLF line endings. - pre-push: runs
npm run lintandnpm run check(type-checking).
Continuous Integration
GitHub Actions workflows in .github/workflows/:
lint-and-typecheck.yml- runsbuild,lint, andcheckon every pull request targetingmaster,develop, ordev.release.yml- triggered by pushing a stable version tag (vX.Y.Z). Builds signed desktop binaries for Linux, Windows, macOS (Intel + Apple Silicon) viatauri-apps/tauri-action, plus a signed Android APK, and publishes them as a draft GitHub release with auto-updater metadata.ci.yml("Pre-release") - triggered by pushing a pre-release tag (vX.Y.Z-pre.N). Same build matrix asrelease.yml, but publishes a non-draft pre-release without updater metadata.
Both release workflows expect TAURI_SIGNING_PRIVATE_KEY(_PASSWORD) and the ANDROID_KEYSTORE_* /
ANDROID_KEY_* secrets to be configured on the repository.
The Updater
src/lib/services/updater.ts answers one question on two platforms that share no machinery
for it. UpdateInfo.canInstallInApp is the flag that tells them apart, and the dialog
(src/lib/components/updater/UpdateDialog.svelte) branches on it rather than on the platform.
Desktop uses @tauri-apps/plugin-updater: it fetches the latest.json named by the
updater.endpoints entry in tauri.conf.json, verifies its signature against the pubkey
there, and installs the new build itself.
Android has no updater at all, and this is not a configuration problem.
tauri-plugin-updater declares Android support level none, and its updater_os() has
branches for linux/macos/windows only — on Android target_os is "android", so check()
returns UnsupportedOs before a single request is sent. There is no install path either: an
APK is installed by the system package installer, not by the app it replaces. The Android
path therefore calls the GitHub Releases API directly, compares the tag against getVersion()
using src/lib/utils/version.ts, and opens the .apk asset in the browser. String comparison
is not adequate for that — '0.10.0' > '0.9.0' is false lexically — which is why the
comparison is a tested module of its own.
Two things must stay in step, or the platforms will offer different versions to their users:
the RELEASE_REPO constant in updater.ts and the updater.endpoints URL in
tauri.conf.json.
A draft release is invisible to the updater. release.yml publishes with
releaseDraft: true, and both paths resolve /releases/latest, which GitHub defines as the
latest published, non-pre-release release. Until the draft is published by hand, the
desktop endpoint 404s and the API returns the previous release — so the last step of every
release is publishing the draft on GitHub. Nothing reaches users before that.
The desktop check surfaces that state honestly rather than as a generic failure: a 404 becomes
the no-release kind ("it may still be a draft"), distinct from network and unsupported.
The release notes users read are the GitHub release body, on both platforms. They are not
taken from latest.json, whose notes field is written by tauri-action at build time from
the fixed releaseBody string in release.yml — which is a placeholder, not a changelog, and
cannot be otherwise, since the notes are written after the build. releaseNotesFor therefore
fetches the release from the API and uses its body, falling back to latest.json if the call
fails; the update installs either way. Two consequences:
- Editing a published release's text on GitHub changes what every client shows, with no rebuild and no new version.
- The notes must be written before the draft is published, because publishing is what makes the release visible to the check. A release published with the placeholder still in it will show that placeholder.
The fetched notes are used only when the release tag matches the version being offered — notes belonging to a different release are worse than none.
A .deb install is deliberately not updated in place. The plugin would attempt it —
install_deb writes the package to a temp dir and runs dpkg -i through pkexec, falling
back to zenity/kdialog and finally to a terminal sudo that a windowed app has no terminal
for — but that chain has too many ways to end half-finished for something the user starts
with one click, and the package manager is the thing that owns that install anyway. So the
check reports canInstallInApp: false with manualInstallReason: 'deb-package' and the
dialog opens the releases page instead.
An unpackaged build never installs either, and this one is a safety guard. On Linux the
plugin's extract_path is the running executable, so in tauri dev "Download and install"
moves the dev binary into a TempDir, writes the release AppImage over it, then drops the
TempDir — deleting the backup — and reports success. The developer is left with a 100 MB
AppImage where their build was. getBundleType() returns null for a build the bundler never
touched, which is exactly that case, so it is routed to the browser with
manualInstallReason: 'unpackaged'.
Note that on macOS bundle_type() falls back to App rather than None, so this guard does
not fire there.
.rpm currently still installs in place, through the same privileged-helper chain.
One more limit: the Android check is unauthenticated, so it shares GitHub's per-IP rate limit. A 403 is reported as a network-kind error.
Building Release Binaries
Cutting a New Release
npm run release -- <patch|minor|major|prerelease|x.y.z> [--dry-run] [--no-merge-back]
(wraps scripts/release.js) automates version bumps:
- Creates a
release/vX.Y.Zbranch. - Bumps the version in
package.json,package-lock.json,src-tauri/tauri.conf.json,Cargo.toml, andCargo.lock. - Commits, tags
vX.Y.Z, and pushes the branch + tag together. - Fast-forwards the branch it was run from onto the bump and pushes it, so the version on
masteris the version released. Skip with--no-merge-back.
Note the --: without it npm consumes the flags before the script sees them.
Every precondition — a clean tree, a version that moves forward, and a tag/branch that does not
already exist locally or on the remote — is checked before anything is written, and a failure
after that point deletes the branch and tag it created and returns to the original branch. Use
--dry-run to run the checks and stop.
Only X.Y.Z and X.Y.Z-pre.N are accepted. Other pre-release spellings are valid semver but match
neither workflow trigger, so they would tag and build nothing.
Pushing a stable tag (vX.Y.Z) triggers release.yml; pushing a pre-release tag (vX.Y.Z-pre.N, via the
prerelease bump type) triggers ci.yml. See Continuous Integration.
The script does not finish the release. release.yml publishes a draft, and a draft is
invisible to /releases/latest — which is where both the desktop updater and the Android check
look. Publishing the draft on GitHub is the step that actually ships it; until then no existing
install will see the new version. See The Updater.
scripts/version.js holds the version arithmetic and scripts/version.test.js covers it
(vitest.config.ts includes scripts/**/*.test.js for this).
Building Desktop
npx tauri build
Building Android
IMPORTANT: The Android project scaffold (src-tauri/gen/android/) is tracked in git.
Do NOT run npx tauri android init as it will overwrite customizations.
# One-time: detect/export ANDROID_HOME and NDK_HOME
source scripts/android-setup.sh
# Dev build + deploy to device/emulator
npx tauri android dev
# Release build (unsigned APK)
npx tauri android build
# Or: quick local debug APK build (auto-detects SDK/NDK/JDK)
./compileApk.sh
The unsigned release APK will be at:
src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release-unsigned.apk
Signing APK
# Create keystore (first time only)
keytool -genkey -v -keystore release.keystore -alias myalias -keyalg RSA -keysize 2048 -validity 10000
# Align APK
zipalign -v 4 app-universal-release-unsigned.apk app-aligned.apk
# Sign APK
apksigner sign --ks release.keystore --ks-key-alias myalias --out app-release.apk app-aligned.apk