# Healer Man update and release runbook Use this checklist whenever game changes need to be pushed to Gitea, deployed to the TrueNAS server, and released as an Obtainium-compatible APK. The examples below use Android version `0.1.1` and `versionCode` `2`. Replace both with the next values for the release you are making. Every APK must use a larger `versionCode` than every APK previously released. ## What requires a new APK? Use the full procedure in this document when the update changes any of these: - React, TypeScript, JavaScript, gameplay, UI, shaders, or asset-loading code. - Capacitor or native Android code, permissions, or configuration. - The downloadable-content manifest format or compatibility rules. - Starter content that must work immediately after a fresh offline install. For an asset-only update, such as a compatible GLB, texture, audio file, or data file, use the shorter procedure at the end. Those updates can be published by the server and downloaded by an already-installed APK without an Obtainium release. Single-player remains offline-capable. A fresh APK includes the runtime and starter content. Additional content that a player has downloaded is retained in Android app-private storage and can be used offline after download. ## Fixed locations ```text Development checkout: F:\Projects\HealerMan Gitea repository: https://git.whoagland.com/phenom/healer-man Git push endpoint: ssh://git@192.168.1.180:30009/phenom/healer-man.git TrueNAS working checkout: /mnt/usbssds/apps/healer-man/app TrueNAS bare Gitea repository: /mnt/.ix-apps/app_mounts/gitea/data/git/repositories/phenom/healer-man.git Persistent database: /mnt/usbssds/apps/healer-man/data/game.db Published downloadable content: /mnt/usbssds/apps/healer-man/content Public game URL: https://iwanttoheal.phenomrom.com ``` The commands in the Windows sections are PowerShell commands. The commands in the TrueNAS sections are shell commands entered after signing in to TrueNAS. ## One-time release setup These items only need to be configured once on the development PC. ### 1. Confirm Git uses LAN SSH for pushes ```powershell Set-Location F:\Projects\HealerMan git remote -v ssh -T -p 30009 git@192.168.1.180 ``` The SSH test should say that Gitea authenticated you as `phenom`. The current repository is configured to fetch over HTTPS and push over LAN SSH. If the push URL ever needs to be restored, run: ```powershell git remote set-url --push origin ` ssh://git@192.168.1.180:30009/phenom/healer-man.git ``` ### 2. Install the Android build requirements Install JDK 21 and Android SDK Platform 36. Configure their actual paths, then verify them: ```powershell $env:JAVA_HOME = 'C:\path\to\jdk-21' $env:ANDROID_SDK_ROOT = "$env:LOCALAPPDATA\Android\Sdk" & "$env:JAVA_HOME\bin\java.exe" -version Test-Path "$env:ANDROID_SDK_ROOT\platforms\android-36\android.jar" ``` The last command must return `True`. The build wrapper can also use the portable JDK and Android SDK locations described in `scripts/runAndroidGradle.ps1`. ### 3. Create and protect one permanent signing key Skip this step if the permanent Healer Man release key already exists. ```powershell New-Item -ItemType Directory -Force F:\secure keytool -genkeypair -v ` -keystore F:\secure\healer-man-release.jks ` -alias healer-man ` -keyalg RSA ` -keysize 4096 ` -validity 10000 ``` Back up the keystore and its passwords somewhere secure. Do not put them in the repository. Android will not install a future update over the existing game if it is signed with a different key. ### 4. Create a Gitea access token In Gitea, create an access token for `phenom` that can create repository releases and upload attachments to `phenom/healer-man`. Store it in a password manager. Never add the token to a file in the repository or distribute it to players. Obtainium must be able to read the release without your Gitea credentials. Either make `phenom/healer-man` publicly readable or publish the APK to a separate public release-only repository. Gitea release visibility follows the repository's access rules. ### 5. Confirm the TrueNAS app has the correct data mount The persistent database must be mounted at `/app/runtime-data`. Do **not** mount anything at `/app/data`, because `/app/data/loot/schema.sql` is source data that the build needs. The complete known-good YAML is in the appendix. ## Full release: Git, server, and APK ### Step 1: Open the project and inspect every change ```powershell Set-Location F:\Projects\HealerMan git status --short --branch git diff --stat git diff ``` Check that every listed file belongs to this update. Do not use `git reset --hard` or discard unfamiliar changes. Before continuing, make sure the project contains no passwords, Gitea tokens, `.env` files, keystores, database files, or other secrets. ### Step 2: Select the Android version Choose both values before building: ```powershell $VersionName = '0.1.1' $VersionCode = 2 ``` - `VersionName` is the player-facing semantic version. - `VersionCode` is Android's integer update counter. It must always increase. - Record the last used `versionCode` in the Gitea release notes or your release records so it is never accidentally reused. Update `package.json` and `package-lock.json` to the same version: ```powershell npm version $VersionName --no-git-tag-version ``` Do not create a local Git tag with `npm version`; the release script creates the Gitea tag after the signed APK builds successfully. ### Step 3: Run automated validation ```powershell npm run content:test npm run server:test npm test npm run build ``` All four commands must succeed. Apply these additional targeted checks whenever the matching area was edited: ```powershell # Character, gear, appearance, weapon, or attachment changes: npm test -- --run src/avatar # Camera or desktop input changes: npx vitest run src/game/inputManager.test.ts src/game/inputMath.test.ts ``` Then run the game locally and verify: ```powershell npm run dev ``` - Check the roster preview and an in-dungeon character. Confirm that bodies, weapons, and equipped silhouettes render, and record screenshot evidence in `PLAYTEST_NOTES.md`. - Hold and drag the right mouse button in the 3D scene. Confirm the camera moves, releasing the button stops it, no browser capture prompt appears, and `document.pointerLockElement` remains `null`. - Exercise the gameplay, UI, and content changed by this release. - Stop the development server with `Ctrl+C` when finished. ### Step 4: Review and commit the release First check for whitespace errors and review the final file list: ```powershell git diff --check git status --short git diff --stat ``` Stage the intended release: ```powershell git add -A git status --short git diff --cached --stat git diff --cached --check ``` Do not continue if the staged list contains a keystore, APK, `.env` file, database, `content/`, `dist/`, `dist-android/`, or `.android-public/` output. Unstage any accidental file with `git restore --staged -- ` without deleting the local file. Commit only after the staged set is correct: ```powershell git commit -m "Release Healer Man $VersionName" $ReleaseCommit = git rev-parse HEAD $ReleaseCommit ``` Keep the printed commit ID so it can be compared with TrueNAS. ### Step 5: Push `main` to Gitea ```powershell git push origin main git status --short --branch git ls-remote origin refs/heads/main ``` The push must finish successfully and the remote `main` hash must equal `$ReleaseCommit`. Open the repository in Gitea and confirm the new commit is at the top of `main`. If SSH authentication fails, test it again: ```powershell ssh -T -p 30009 git@192.168.1.180 ``` Do not switch this large repository back to an HTTPS push just to work around an error; its large initial pack previously exceeded the HTTPS proxy connection. ### Step 6: Stop and back up the TrueNAS app In the TrueNAS Apps UI, stop the `healer-man` app. This prevents the running container from reading files while its checkout changes. Then sign in to a TrueNAS shell and create a point-in-time SQLite copy: ```sh sudo mkdir -p /mnt/usbssds/apps/healer-man/backups if sudo test -f /mnt/usbssds/apps/healer-man/data/game.db; then sudo cp -p \ /mnt/usbssds/apps/healer-man/data/game.db \ /mnt/usbssds/apps/healer-man/backups/game-$(date +%Y%m%d-%H%M%S).db fi df -h /mnt/usbssds/apps/healer-man ``` A TrueNAS dataset snapshot is recommended in addition to the file copy, especially before a database migration. ### Step 7: Pull the release into the TrueNAS working checkout The safe-directory commands are normally one-time setup, but running them again is harmless: ```sh sudo git config --global --add safe.directory \ /mnt/usbssds/apps/healer-man/app sudo git config --global --add safe.directory \ /mnt/.ix-apps/app_mounts/gitea/data/git/repositories/phenom/healer-man.git ``` Pull from the local Gitea bare repository: ```sh sudo git -C /mnt/usbssds/apps/healer-man/app pull --ff-only \ /mnt/.ix-apps/app_mounts/gitea/data/git/repositories/phenom/healer-man.git \ main sudo chown -R truenas_admin:truenas_admin \ /mnt/usbssds/apps/healer-man/app sudo git -C /mnt/usbssds/apps/healer-man/app rev-parse HEAD ``` The final commit ID must match `$ReleaseCommit` from the development PC. Always use `sudo` for this pull: `truenas_admin` cannot read the bare Gitea repository directly. ### Step 8: Start the TrueNAS app and watch startup Start `healer-man` in the TrueNAS Apps UI, then open its logs. A successful startup performs these actions in order: 1. Installs the locked dependencies. 2. Initializes `/app/runtime-data/game.db`. 3. Publishes the content manifest and immutable objects to `/app/content`. 4. Generates the loot catalog and builds the browser client. 5. Starts the combined web/API/content server on port `4173`. The deprecation, funding, and `npm audit` lines are warnings; they do not by themselves close the server. Continue until the logs show the server listening. If the logs report this path, the YAML is still wrong: ```text ENOENT: no such file or directory, open '/app/data/loot/schema.sql' ``` Change the database volume and `DATA_DIR` to `/app/runtime-data` using the YAML in the appendix, then redeploy the app. ### Step 9: Verify the live server From the TrueNAS shell: ```sh curl -fI http://127.0.0.1:4173/ curl -fsS http://127.0.0.1:4173/api/health curl -fsS http://127.0.0.1:4173/content/manifest.json | head -c 500 ``` From the development PC or another LAN machine: ```powershell curl.exe -fI https://iwanttoheal.phenomrom.com/ curl.exe -fsS https://iwanttoheal.phenomrom.com/api/health ``` Also open the public URL in a browser. Confirm the game loads, login works, and the changed gameplay is present. If this release includes new or changed assets, confirm the client sees the new content manifest and can retrieve one of those assets. ### Step 10: Set release secrets for this PowerShell session Set the permanent keystore path and alias: ```powershell Set-Location F:\Projects\HealerMan $env:ANDROID_KEYSTORE_FILE = 'F:\secure\healer-man-release.jks' $env:ANDROID_KEY_ALIAS = 'healer-man' ``` Read the passwords and token without displaying them or placing them in command history: ```powershell $env:ANDROID_KEYSTORE_PASSWORD = [System.Net.NetworkCredential]::new( '', (Read-Host 'Keystore password' -AsSecureString) ).Password $env:ANDROID_KEY_PASSWORD = [System.Net.NetworkCredential]::new( '', (Read-Host 'Key password' -AsSecureString) ).Password $env:GITEA_TOKEN = [System.Net.NetworkCredential]::new( '', (Read-Host 'Gitea token' -AsSecureString) ).Password ``` If Java and the Android SDK are not configured permanently, set `JAVA_HOME` and `ANDROID_SDK_ROOT` in this same shell now. ### Step 11: Build, sign, and publish the APK Confirm the version variables still contain the intended values: ```powershell $VersionName $VersionCode (Get-Content package.json -Raw | ConvertFrom-Json).version ``` The package version must match `$VersionName`. Then run: ```powershell powershell.exe -NoProfile -ExecutionPolicy Bypass ` -File scripts/publish-gitea-release.ps1 ` -VersionName $VersionName ` -VersionCode $VersionCode ` -ReleaseNotes "Healer Man $VersionName client update; Android versionCode $VersionCode." ``` The script will: 1. Build the compact Android web bundle and synchronize Capacitor. 2. Build a signed release APK. 3. Name it `healer-man-VERSION-release.apk`. 4. Create a SHA-256 checksum file. 5. Create the Gitea tag and non-draft release. 6. Upload the APK and checksum as release attachments. Do not interrupt it while Gradle or the upload is running. ### Step 12: Verify the APK and Obtainium release Check the local artifact: ```powershell $Apk = "android\app\build\outputs\apk\release\healer-man-$VersionName-release.apk" Get-Item $Apk | Select-Object FullName, Length, LastWriteTime Get-FileHash $Apk -Algorithm SHA256 ``` Open: ```text https://git.whoagland.com/phenom/healer-man/releases ``` Confirm the release has both attachments: ```text healer-man-VERSION-release.apk healer-man-VERSION-release.apk.sha256 ``` On a test Android device: 1. Refresh Healer Man in Obtainium. 2. Confirm it detects the new version. 3. Install the update over the existing app; do not uninstall first. 4. Launch once online and allow changed content to download. 5. Disconnect Wi-Fi/mobile data and confirm single-player still starts and previously downloaded content remains playable. If Obtainium needs manual source settings, use the releases page above, select the HTML source override, and filter APK links with: ```text healer-man-.*-release\.apk$ ``` ### Step 13: Clear secrets from the shell ```powershell Remove-Item Env:ANDROID_KEYSTORE_PASSWORD -ErrorAction SilentlyContinue Remove-Item Env:ANDROID_KEY_PASSWORD -ErrorAction SilentlyContinue Remove-Item Env:GITEA_TOKEN -ErrorAction SilentlyContinue ``` The keystore path and alias are not passwords, but they may also be cleared if desired. ## Short procedure for an asset-only update Use this only when no APK-owned code, native behavior, starter content, or content compatibility rules changed. Do **not** bump the Android version. Run these exact PowerShell commands: ```powershell Set-Location F:\Projects\HealerMan npm run content:test npm run build git status --short git diff --check git add -A git status --short git diff --cached --stat git diff --cached --check git commit -m "Update Healer Man assets" git push origin main git rev-parse HEAD ``` Read the staged file list before committing and stop if a test, build, or Git command fails. Next, stop `healer-man` in the TrueNAS Apps UI and run this exact block in a TrueNAS shell: ```sh sudo mkdir -p /mnt/usbssds/apps/healer-man/backups if sudo test -f /mnt/usbssds/apps/healer-man/data/game.db; then sudo cp -p \ /mnt/usbssds/apps/healer-man/data/game.db \ /mnt/usbssds/apps/healer-man/backups/game-$(date +%Y%m%d-%H%M%S).db fi sudo git -C /mnt/usbssds/apps/healer-man/app pull --ff-only \ /mnt/.ix-apps/app_mounts/gitea/data/git/repositories/phenom/healer-man.git \ main sudo chown -R truenas_admin:truenas_admin \ /mnt/usbssds/apps/healer-man/app sudo git -C /mnt/usbssds/apps/healer-man/app rev-parse HEAD ``` Start `healer-man` in the TrueNAS Apps UI. Startup runs `content:publish`, writes new immutable objects, and atomically replaces `manifest.json` after the objects exist. When the logs show the server listening, run: ```sh curl -fI http://127.0.0.1:4173/ curl -fsS http://127.0.0.1:4173/api/health curl -fsS http://127.0.0.1:4173/content/manifest.json | head -c 500 ``` Finally, launch a currently installed APK while online. Confirm it downloads the changed content, then disconnect the device and test the content offline. Changing or adding an asset changes the published manifest. Because content is addressed by its hash, an unchanged asset is reused while a changed file becomes a new immutable object. Old clients continue using their active verified manifest until the new download is complete; a partially downloaded update is not activated. ## Rollback Do not rewrite or force-push shared `main` history. If a source/server release is bad, revert it from the development PC: ```powershell Set-Location F:\Projects\HealerMan git revert git push origin main ``` Then stop the TrueNAS app, pull the new revert commit with the Step 7 command, and start the app again. Restore a database backup only if the failure changed or damaged persistent data. An already-installed Android APK cannot be rolled back in place to a lower `versionCode`. Publish a corrected APK with a new version name and a still-higher `versionCode`. ## Common failures - **TrueNAS says the bare repository is not a Git repository:** run the pull with `sudo`. The normal `truenas_admin` account cannot read Gitea's repository storage. - **`publish-content.mjs` is missing:** the TrueNAS checkout is not at the pushed commit. Compare `git rev-parse HEAD` on both systems and pull again. - **`/app/data/loot/schema.sql` is missing:** a volume is masking source `/app/data`. Apply the corrected YAML below. - **Git HTTPS push fails after uploading a large pack:** keep the configured LAN SSH push URL and test port `30009` authentication. - **APK installs as a separate app or refuses to update:** verify the app ID is still `com.phenomrom.healerman`, use the same signing key, and increase `versionCode`. - **Gitea APK upload returns an HTTP size error:** increase the Gitea/reverse proxy release-upload limit or use Gitea's direct LAN web endpoint with the script's `-GiteaBaseUrl` option. Do not assume `192.168.1.180:8080` is Gitea; verify the actual Gitea web port first. - **Release creation succeeds but attachment upload fails:** do not repeatedly create the same tag. Remove the incomplete release/tag in Gitea or upload the attachments to that release, then retry only after its state is understood. ## Appendix: known-good TrueNAS YAML The TrueNAS app name is `healer-man`: ```yaml services: healerman: image: node:24-bookworm-slim command: >- sh -lc "npm ci --include=dev && npm run db:init && npm run content:publish && npm run build && npm start" environment: CORS_ORIGINS: "https://iwanttoheal.phenomrom.com,capacitor://localhost,http://localhost,https://localhost" CONTENT_DIR: /app/content DATA_DIR: /app/runtime-data HOST: 0.0.0.0 NODE_OPTIONS: "--max-old-space-size=4096" PORT: "4173" SESSION_TTL_DAYS: "30" STATIC_DIR: /app/dist TRUST_PROXY: "true" VITE_API_BASE_URL: "https://iwanttoheal.phenomrom.com" init: true ports: - "4173:4173" restart: unless-stopped volumes: - /mnt/usbssds/apps/healer-man/app:/app - /mnt/usbssds/apps/healer-man/data:/app/runtime-data - /mnt/usbssds/apps/healer-man/content:/app/content working_dir: /app ``` The critical distinction is: ```text Source loot schema: /app/data/loot/schema.sql Persistent database: /app/runtime-data/game.db ``` Never add a volume mounted at `/app/data`.