Aventuras/docs/development/release.md
Pento 267d15f2b4
Lore management: a session ledger, a shared chapter-read budget, and a duplicate consolidation window (#437)
- **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.
2026-08-12 15:32:41 +02:00

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.js against staged src-tauri/migrations/*.sql files to reject CRLF line endings.
  • pre-push: runs npm run lint and npm run check (type-checking).

Continuous Integration

GitHub Actions workflows in .github/workflows/:

  • lint-and-typecheck.yml - runs build, lint, and check on every pull request targeting master, develop, or dev.
  • release.yml - triggered by pushing a stable version tag (vX.Y.Z). Builds signed desktop binaries for Linux, Windows, macOS (Intel + Apple Silicon) via tauri-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 as release.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:

  1. Creates a release/vX.Y.Z branch.
  2. Bumps the version in package.json, package-lock.json, src-tauri/tauri.conf.json, Cargo.toml, and Cargo.lock.
  3. Commits, tags vX.Y.Z, and pushes the branch + tag together.
  4. Fast-forwards the branch it was run from onto the bump and pushes it, so the version on master is 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