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

178 lines
8.6 KiB
Markdown

# 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](https://github.com/evilmartians/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](#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](#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
```bash
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.
```bash
# 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:
```text
src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release-unsigned.apk
```
### Signing APK
```bash
# 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
```