19 KiB
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
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
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:
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:
$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.
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
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:
$VersionName = '0.1.1'
$VersionCode = 2
VersionNameis the player-facing semantic version.VersionCodeis Android's integer update counter. It must always increase.- Record the last used
versionCodein 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:
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
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:
# 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:
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.pointerLockElementremainsnull. - Exercise the gameplay, UI, and content changed by this release.
- Stop the development server with
Ctrl+Cwhen finished.
Step 4: Review and commit the release
First check for whitespace errors and review the final file list:
git diff --check
git status --short
git diff --stat
Stage the intended release:
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 -- <path> without
deleting the local file.
Commit only after the staged set is correct:
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
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:
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:
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:
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:
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:
- Installs the locked dependencies.
- Initializes
/app/runtime-data/game.db. - Publishes the content manifest and immutable objects to
/app/content. - Generates the loot catalog and builds the browser client.
- 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:
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:
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:
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:
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:
$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:
$VersionName
$VersionCode
(Get-Content package.json -Raw | ConvertFrom-Json).version
The package version must match $VersionName. Then run:
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:
- Build the compact Android web bundle and synchronize Capacitor.
- Build a signed release APK.
- Name it
healer-man-VERSION-release.apk. - Create a SHA-256 checksum file.
- Create the Gitea tag and non-draft release.
- 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:
$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:
https://git.whoagland.com/phenom/healer-man/releases
Confirm the release has both attachments:
healer-man-VERSION-release.apk
healer-man-VERSION-release.apk.sha256
On a test Android device:
- Refresh Healer Man in Obtainium.
- Confirm it detects the new version.
- Install the update over the existing app; do not uninstall first.
- Launch once online and allow changed content to download.
- 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:
healer-man-.*-release\.apk$
Step 13: Clear secrets from the shell
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:
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:
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:
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:
Set-Location F:\Projects\HealerMan
git revert <bad-commit-id>
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 normaltruenas_adminaccount cannot read Gitea's repository storage. publish-content.mjsis missing: the TrueNAS checkout is not at the pushed commit. Comparegit rev-parse HEADon both systems and pull again./app/data/loot/schema.sqlis 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
30009authentication. - 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 increaseversionCode. - 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
-GiteaBaseUrloption. Do not assume192.168.1.180:8080is 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:
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:
Source loot schema: /app/data/loot/schema.sql
Persistent database: /app/runtime-data/game.db
Never add a volume mounted at /app/data.