Files
healer-man/RELEASE_RUNBOOK.md
T
2026-08-17 17:12:02 -04:00

21 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.

Visual release manager

On the development PC, double-click scripts\release-manager.cmd or run:

npm run release:gui

The release manager suggests the next semantic patch version and a strictly increasing Android versionCode, runs the complete validation suite, and stages the release. It then shows the exact staged paths and diff summary for approval before it commits, pushes main, builds the signed APK, and publishes the Gitea release. Signing passwords and the Gitea token are passed to the child process in memory rather than on its command line.

The local release key is F:\secure\healer-man-release.jks. When the companion healer-man-release-password.dpapi file is present, blank signing-password fields are filled from that Windows user-encrypted credential in memory. The encrypted credential is usable only by the Windows account that created it; back up the keystore and preserve its password separately for disaster recovery. The Remember Gitea token on this PC option stores the token the same way at F:\secure\healer-man-gitea-token.dpapi, loads it on later launches, and keeps it outside the repository. Use Forget saved token to remove it.

After the push, the GUI's Deploy tab contains the backup and fast-forward pull block for the TrueNAS shell, pinned to the new commit. Stop the app in the TrueNAS Apps UI before running it. Start the app afterward, watch its startup log, and use the Verify tab for the health checks.

The canonical Android version is stored in release-version.json. The initial 1002 code is intentionally above the historical codes in this runbook; the GUI will suggest 1003 for version 0.1.3. If APK publication fails after the Git push, fix the release-side problem and use Publish Current APK Only so the source version is not bumped a second time.

The examples below use Android version 0.1.3 and versionCode 1003. 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.3'
$VersionCode = 1003
  • 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, package-lock.json, and the canonical Android release metadata to the same version:

npm version $VersionName --no-git-tag-version
$ReleaseVersion = [ordered]@{
  versionName = $VersionName
  versionCode = $VersionCode
} | ConvertTo-Json
[IO.File]::WriteAllText(
  (Join-Path $PWD 'release-version.json'),
  "$ReleaseVersion`n",
  [Text.UTF8Encoding]::new($false)
)

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.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:

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:

  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:

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:

  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:

$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:

  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:

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

  • Android Vite build reports JavaScript heap out of memory: the checked-in build:android:web command gives Vite an 8 GB Node heap. Make sure the current package.json contains that command and close other memory-heavy applications before retrying. A failed GUI preparation restores the version files when no release commit was created, so retry the same suggested version.
  • 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:

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.