Release Healer Man 0.1.3

This commit is contained in:
phenom
2026-08-17 17:12:02 -04:00
parent 7e60864e70
commit 5bb170039a
12 changed files with 1569 additions and 23 deletions
+2
View File
@@ -20,3 +20,5 @@ releases/
/content/ /content/
dist-android/ dist-android/
.android-public/ .android-public/
__pycache__/
*.pyc
+21 -10
View File
@@ -176,17 +176,28 @@ schema-compatible with Healer Man and must not be copied over the new one.
## Update workflow ## Update workflow
For a code update that includes a new APK, choose the next version first. The For a code update that includes a new APK, the visual workflow in
example below upgrades `0.1.0` to `0.1.1`; replace `2` if it is not the next `RELEASE_RUNBOOK.md` is recommended. For a manual release, choose the next
unused Android `versionCode`. Then run these commands in **Windows PowerShell** version first. The example below upgrades `0.1.2` to `0.1.3`; replace `1003` if
it is not greater than every prior Android `versionCode`. Then run these
commands in **Windows PowerShell**
on the development PC. Stop if any test or build fails: on the development PC. Stop if any test or build fails:
```powershell ```powershell
Set-Location F:\Projects\HealerMan Set-Location F:\Projects\HealerMan
$VersionName = '0.1.1' $VersionName = '0.1.3'
$VersionCode = 2 $VersionCode = 1003
npm version $VersionName --no-git-tag-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)
)
npm run content:test npm run content:test
npm run server:test npm run server:test
@@ -298,17 +309,17 @@ Gitea release/tag, and upload both attachments with:
```powershell ```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts/publish-gitea-release.ps1 ` powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts/publish-gitea-release.ps1 `
-VersionName 0.1.1 ` -VersionName 0.1.3 `
-VersionCode 2 ` -VersionCode 1003 `
-ReleaseNotes 'Healer Man client update.' -ReleaseNotes 'Healer Man client update.'
``` ```
The result contains: The result contains:
```text ```text
Tag: v0.1.1 Tag: v0.1.3
APK: healer-man-0.1.1-release.apk APK: healer-man-0.1.3-release.apk
Checksum: healer-man-0.1.1-release.apk.sha256 Checksum: healer-man-0.1.3-release.apk.sha256
``` ```
Obtainium should monitor: Obtainium should monitor:
+54 -4
View File
@@ -3,7 +3,42 @@
Use this checklist whenever game changes need to be pushed to Gitea, deployed Use this checklist whenever game changes need to be pushed to Gitea, deployed
to the TrueNAS server, and released as an Obtainium-compatible APK. to the TrueNAS server, and released as an Obtainium-compatible APK.
The examples below use Android version `0.1.1` and `versionCode` `2`. Replace ## Visual release manager
On the development PC, double-click `scripts\release-manager.cmd` or run:
```powershell
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 both with the next values for the release you are making. Every APK must use a
larger `versionCode` than every APK previously released. larger `versionCode` than every APK previously released.
@@ -150,8 +185,8 @@ other secrets.
Choose both values before building: Choose both values before building:
```powershell ```powershell
$VersionName = '0.1.1' $VersionName = '0.1.3'
$VersionCode = 2 $VersionCode = 1003
``` ```
- `VersionName` is the player-facing semantic version. - `VersionName` is the player-facing semantic version.
@@ -159,10 +194,20 @@ $VersionCode = 2
- Record the last used `versionCode` in the Gitea release notes or your release - Record the last used `versionCode` in the Gitea release notes or your release
records so it is never accidentally reused. records so it is never accidentally reused.
Update `package.json` and `package-lock.json` to the same version: Update `package.json`, `package-lock.json`, and the canonical Android release
metadata to the same version:
```powershell ```powershell
npm version $VersionName --no-git-tag-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 Do not create a local Git tag with `npm version`; the release script creates the
@@ -544,6 +589,11 @@ An already-installed Android APK cannot be rolled back in place to a lower
## Common failures ## 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 - **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 with `sudo`. The normal `truenas_admin` account cannot read Gitea's repository
storage. storage.
+3 -1
View File
@@ -3,8 +3,10 @@ import groovy.json.JsonSlurper
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
def packageJson = new JsonSlurper().parse(file('../../package.json')) def packageJson = new JsonSlurper().parse(file('../../package.json'))
def releaseVersionFile = file('../../release-version.json')
def releaseVersion = releaseVersionFile.exists() ? new JsonSlurper().parse(releaseVersionFile) : [:]
def releaseVersionName = System.getenv('ANDROID_VERSION_NAME') ?: packageJson.version def releaseVersionName = System.getenv('ANDROID_VERSION_NAME') ?: packageJson.version
def releaseVersionCode = (System.getenv('ANDROID_VERSION_CODE') ?: '1').toInteger() def releaseVersionCode = (System.getenv('ANDROID_VERSION_CODE') ?: releaseVersion.versionCode ?: '1').toInteger()
def releaseKeystoreFile = System.getenv('ANDROID_KEYSTORE_FILE') def releaseKeystoreFile = System.getenv('ANDROID_KEYSTORE_FILE')
def releaseKeystorePassword = System.getenv('ANDROID_KEYSTORE_PASSWORD') def releaseKeystorePassword = System.getenv('ANDROID_KEYSTORE_PASSWORD')
def releaseKeyAlias = System.getenv('ANDROID_KEY_ALIAS') def releaseKeyAlias = System.getenv('ANDROID_KEY_ALIAS')
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "healer-man", "name": "healer-man",
"version": "0.1.2", "version": "0.1.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "healer-man", "name": "healer-man",
"version": "0.1.2", "version": "0.1.3",
"dependencies": { "dependencies": {
"@capacitor/android": "8.4.1", "@capacitor/android": "8.4.1",
"@capacitor/core": "8.4.1", "@capacitor/core": "8.4.1",
+3 -2
View File
@@ -1,7 +1,7 @@
{ {
"name": "healer-man", "name": "healer-man",
"private": true, "private": true,
"version": "0.1.2", "version": "0.1.3",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --host 0.0.0.0", "dev": "vite --host 0.0.0.0",
@@ -90,11 +90,12 @@
"assets:ktx2:refresh-metadata": "node scripts/asset-pipeline/refresh-ktx2-metadata.mjs", "assets:ktx2:refresh-metadata": "node scripts/asset-pipeline/refresh-ktx2-metadata.mjs",
"prebuild": "npm run loot:generate && npm run assets:ktx2:audit", "prebuild": "npm run loot:generate && npm run assets:ktx2:audit",
"build": "tsc -b && vite build", "build": "tsc -b && vite build",
"build:android:web": "npm run prebuild && npm run android:prepare && tsc -b && vite build --mode android", "build:android:web": "npm run prebuild && npm run android:prepare && tsc -b && node --max-old-space-size=8192 node_modules/vite/bin/vite.js build --mode android",
"android:sync": "npm run build:android:web && cap sync android", "android:sync": "npm run build:android:web && cap sync android",
"android:apk": "npm run android:sync && powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts/runAndroidGradle.ps1 assembleDebug", "android:apk": "npm run android:sync && powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts/runAndroidGradle.ps1 assembleDebug",
"android:apk:release": "npm run android:sync && powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts/runAndroidGradle.ps1 assembleRelease", "android:apk:release": "npm run android:sync && powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts/runAndroidGradle.ps1 assembleRelease",
"android:install": "npm run android:sync && powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts/runAndroidGradle.ps1 installDebug", "android:install": "npm run android:sync && powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts/runAndroidGradle.ps1 installDebug",
"release:gui": "python scripts/release_manager.py",
"preview": "vite preview --host 0.0.0.0", "preview": "vite preview --host 0.0.0.0",
"pretest": "npm run loot:generate", "pretest": "npm run loot:generate",
"test": "vitest run", "test": "vitest run",
+4
View File
@@ -0,0 +1,4 @@
{
"versionName": "0.1.3",
"versionCode": 1003
}
+21
View File
@@ -28,13 +28,34 @@ foreach ($secret in $requiredSecrets) {
} }
$projectRoot = Split-Path -Parent $PSScriptRoot $projectRoot = Split-Path -Parent $PSScriptRoot
$packageVersion = (Get-Content -LiteralPath (Join-Path $projectRoot 'package.json') -Raw | ConvertFrom-Json).version
$releaseVersionPath = Join-Path $projectRoot 'release-version.json'
if (-not (Test-Path -LiteralPath $releaseVersionPath)) {
throw "Canonical release metadata is missing: $releaseVersionPath"
}
$releaseVersion = Get-Content -LiteralPath $releaseVersionPath -Raw | ConvertFrom-Json
if ($packageVersion -ne $VersionName -or $releaseVersion.versionName -ne $VersionName) {
throw "VersionName $VersionName does not match package.json and release-version.json."
}
if ([int]$releaseVersion.versionCode -ne $VersionCode) {
throw "VersionCode $VersionCode does not match release-version.json ($($releaseVersion.versionCode))."
}
$env:ANDROID_VERSION_NAME = $VersionName $env:ANDROID_VERSION_NAME = $VersionName
$env:ANDROID_VERSION_CODE = [string]$VersionCode $env:ANDROID_VERSION_CODE = [string]$VersionCode
Push-Location $projectRoot Push-Location $projectRoot
try { try {
$initialGitStatus = & git status --porcelain
if ($LASTEXITCODE -ne 0) { throw "Unable to inspect the Git working tree." }
if ($initialGitStatus) { throw "APK publication requires a clean Git working tree." }
& npm run android:sync & npm run android:sync
if ($LASTEXITCODE -ne 0) { throw "Android web build and Capacitor sync failed with exit code $LASTEXITCODE" } if ($LASTEXITCODE -ne 0) { throw "Android web build and Capacitor sync failed with exit code $LASTEXITCODE" }
$postSyncGitStatus = & git status --porcelain --untracked-files=no
if ($LASTEXITCODE -ne 0) { throw "Unable to inspect Android sync changes." }
if ($postSyncGitStatus) {
throw "Android sync changed tracked files. Review and commit them before publishing the APK."
}
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts/runAndroidGradle.ps1 assembleRelease & powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts/runAndroidGradle.ps1 assembleRelease
if ($LASTEXITCODE -ne 0) { throw "Android release build failed with exit code $LASTEXITCODE" } if ($LASTEXITCODE -ne 0) { throw "Android release build failed with exit code $LASTEXITCODE" }
+11
View File
@@ -0,0 +1,11 @@
@echo off
setlocal
cd /d "%~dp0.."
where py >nul 2>&1
if errorlevel 1 goto use_python
py -3 scripts\release_manager.py %*
exit /b %errorlevel%
:use_python
python scripts\release_manager.py %*
exit /b %errorlevel%
File diff suppressed because it is too large Load Diff
+49
View File
@@ -0,0 +1,49 @@
import unittest
import release_manager
class VersionTests(unittest.TestCase):
def test_bumps_each_semver_component(self):
self.assertEqual(release_manager.bump_semver("0.1.2", "patch"), "0.1.3")
self.assertEqual(release_manager.bump_semver("0.1.2", "minor"), "0.2.0")
self.assertEqual(release_manager.bump_semver("0.1.2", "major"), "1.0.0")
def test_android_code_is_monotonic(self):
self.assertEqual(release_manager.semantic_version_code("0.1.3"), 1003)
self.assertEqual(release_manager.suggest_version_code("0.1.3", 5000), 5001)
def test_rejects_non_release_semver(self):
with self.assertRaises(ValueError):
release_manager.parse_semver("1.2")
class SafetyTests(unittest.TestCase):
def test_detects_outputs_and_secrets(self):
paths = [
"src/game/example.ts",
"dist-android/assets/app.js",
"android/app/release.apk",
".env.production",
"FINE.key.ts",
"scripts/content/publish-content.mjs",
"public/data/healer-man-loot.sqlite",
]
self.assertEqual(
release_manager.find_dangerous_staged_paths(paths),
[
"dist-android/assets/app.js",
"android/app/release.apk",
".env.production",
],
)
def test_truenas_commands_pin_expected_commit(self):
block = release_manager.generate_truenas_deploy_commands("abc123")
self.assertIn("EXPECTED_COMMIT='abc123'", block)
self.assertIn("pull --ff-only", block)
self.assertIn('test "$ACTUAL_COMMIT" = "$EXPECTED_COMMIT"', block)
if __name__ == "__main__":
unittest.main()
+8 -4
View File
@@ -8,7 +8,8 @@ $ErrorActionPreference = 'Stop'
$projectRoot = Split-Path -Parent $PSScriptRoot $projectRoot = Split-Path -Parent $PSScriptRoot
$documentsRoot = Split-Path -Parent $projectRoot $documentsRoot = Split-Path -Parent $projectRoot
$wowRoot = Join-Path $documentsRoot 'wow335a' $wowRoot = Join-Path $documentsRoot 'wow335a'
$jdkRoot = Join-Path $wowRoot 'dungeon-export-work\tools\temurin-21' $projectJdkRoot = Join-Path $projectRoot '.tools\temurin-21'
$legacyJdkRoot = Join-Path $wowRoot 'dungeon-export-work\tools\temurin-21'
$portableSdkRoot = Join-Path $wowRoot '.android-sdk' $portableSdkRoot = Join-Path $wowRoot '.android-sdk'
$jdkHome = $null $jdkHome = $null
@@ -16,14 +17,17 @@ $configuredJavaHome = [Environment]::GetEnvironmentVariable('JAVA_HOME')
if ($configuredJavaHome -and (Test-Path -LiteralPath (Join-Path $configuredJavaHome 'bin\java.exe'))) { if ($configuredJavaHome -and (Test-Path -LiteralPath (Join-Path $configuredJavaHome 'bin\java.exe'))) {
$jdkHome = Get-Item -LiteralPath $configuredJavaHome $jdkHome = Get-Item -LiteralPath $configuredJavaHome
} else { } else {
$jdkHome = Get-ChildItem -LiteralPath $jdkRoot -Directory -ErrorAction SilentlyContinue | $jdkHome = @($projectJdkRoot, $legacyJdkRoot) |
Where-Object { Test-Path -LiteralPath (Join-Path $_.FullName 'bin\java.exe') } | ForEach-Object {
Get-ChildItem -LiteralPath $_ -Directory -ErrorAction SilentlyContinue |
Where-Object { Test-Path -LiteralPath (Join-Path $_.FullName 'bin\java.exe') }
} |
Sort-Object Name -Descending | Sort-Object Name -Descending |
Select-Object -First 1 Select-Object -First 1
} }
if (-not $jdkHome) { if (-not $jdkHome) {
throw "Java 21 was not found. Set JAVA_HOME or install the portable JDK under $jdkRoot" throw "Java 21 was not found. Set JAVA_HOME or install the portable JDK under $projectJdkRoot"
} }
$configuredSdkRoot = [Environment]::GetEnvironmentVariable('ANDROID_SDK_ROOT') $configuredSdkRoot = [Environment]::GetEnvironmentVariable('ANDROID_SDK_ROOT')