Implement verified household TV updates and release tooling
This commit is contained in:
parent
152dac97c0
commit
d171aaae9e
29 changed files with 2485 additions and 99 deletions
185
docker/README.md
185
docker/README.md
|
|
@ -11,7 +11,7 @@ make test # app default/debug suite plus wolfskin-design JVM/capture suite
|
|||
make apk-debug # default debug, no secrets; MPV stub and no extra codec decoders
|
||||
```
|
||||
|
||||
APKs remain under `app/build/outputs/apk/default/{release,debug}/`. Debug builds
|
||||
APKs remain under `app/build/outputs/apk/default/{release,debug,optimized}/`. Debug builds
|
||||
are not playback-complete and must not be distributed as releases. Upstream's
|
||||
`.github/workflows` and dependency version catalog are unchanged; release builds
|
||||
use the catalog's exact Wholphin extension version (currently `0.2.4`). Do not
|
||||
|
|
@ -27,6 +27,134 @@ Upstream enables JVM unit tests only for debug variants; there is no
|
|||
`testDefaultReleaseUnitTest` task. `make test` uses the closest default-flavor
|
||||
task without changing upstream's test configuration.
|
||||
|
||||
## Household update channel
|
||||
|
||||
The household assembly remains `defaultOptimized`: R8/resource shrinking,
|
||||
non-debuggable, baseline profiles, pinned playback extensions and no debug
|
||||
fixtures. Its installation identity is `org.justinvisser.wolfskin.tv.opt`.
|
||||
The persistent external Android debug keystore is now the **household signing
|
||||
authority**, not a disposable development key. Certificate SHA-256:
|
||||
`182bac72e292b9ea4d3d4e6383f92eb2161fa28ba5afcff0337cb7d5a9d5341d`.
|
||||
Never print/copy that keystore into artifacts or regenerate it for a release.
|
||||
The household wrapper fails if it is missing; protect and escrow the original.
|
||||
|
||||
Same package + same signer + strictly higher Android versionCode allows an
|
||||
in-place upgrade retaining Jellyfin logins, databases and preferences. Never
|
||||
uninstall, clear data, change identity or replace the key as an update recovery
|
||||
step. An older ephemeral signer is a separate, owner-approved migration gate;
|
||||
this change does not prove the TV's installed signer or retained-login behavior.
|
||||
|
||||
The dedicated public distribution repository is
|
||||
`justinzeus/wolfskin-tv-updates`; its default API is
|
||||
`https://git.justinvisser.org/api/v1/repos/justinzeus/wolfskin-tv-updates/releases/latest`.
|
||||
Repository creation and each actual publication are separate owner gates.
|
||||
Neither command creates a repository. Source remains in `justinzeus/wolfskin`.
|
||||
Initialize the distribution repository's default branch (for example with a
|
||||
README) before its first draft; Forgejo needs a commit on which to create a
|
||||
distribution tag. That tag is not the source tag in the separate source repo.
|
||||
The old production package keeps its old release API and parser. Only the Opt
|
||||
package migrates the exact old default saved URL; intentional custom URLs remain.
|
||||
Installed-release notes use the household channel too.
|
||||
|
||||
### Build, inspect and reserve a code
|
||||
|
||||
```sh
|
||||
make tv-release VERSION=1.1.1 VERSION_CODE=100001 \
|
||||
TV_RELEASE_DIR=/var/tmp/wolfskin-tv-release-1.1.1
|
||||
# Prerequisites only: no build, tag, ledger mutation or publication.
|
||||
make tv-release VERSION=1.1.1 VERSION_CODE=100001 \
|
||||
TV_RELEASE_DIR=/var/tmp/wolfskin-tv-release-dry TV_RELEASE_FLAGS=--dry-run
|
||||
```
|
||||
|
||||
Required: a clean committed source checkout, the existing persistent key, native
|
||||
package credentials through the existing secret mount, no local AAR overrides,
|
||||
and an explicit code above every entry in `docker/tv-version-codes`. Known Opt
|
||||
code 65 seeds that ledger; old production code 28 and Wholphin code 62 belong
|
||||
to different packages. Proposed first household code 100001 exceeds all three.
|
||||
No household code derives from tag counts. Direct optimized Gradle tasks also
|
||||
require `-PtvVersionCode`; the packaging command additionally enforces the ledger.
|
||||
|
||||
The builder inspects final signed APKs with aapt2/apksigner and checks package,
|
||||
version/code, signer, non-debuggable flag, minimum SDK, native ABI inventory,
|
||||
fixture exclusion and both dexopt profiles. It emits four standalone APKs:
|
||||
`Wolfskin-armeabi-v7a.apk`, `Wolfskin-arm64-v8a.apk`, `Wolfskin-x86_64.apk`,
|
||||
and universal `Wolfskin.apk`; plus `wolfskin-update.json`, `RELEASE-NOTES.md`
|
||||
and `SHA256SUMS`. Hashes and sizes come from final signed bytes. Universal
|
||||
32-bit x86 is not MPV-complete and is not a supported playback ABI.
|
||||
|
||||
Manifest schema 1 contains `packageId`, `versionName`, `versionCode`,
|
||||
`publishedAt` (UTC ISO-8601), immutable `sourceCommit`, `sourceTag`,
|
||||
`signerSha256`, and `assets[]` with `abi`, `name`, `bytes`, `sha256`, `minSdk`.
|
||||
All ABI outputs share one code/signer/identity. The local source tag is
|
||||
`pwolfskin-opt-v<VERSION>`; the distribution tag is `wolfskin-opt-v<VERSION>`.
|
||||
After successful packaging the builder appends the code to the tracked ledger.
|
||||
Commit that reservation and explicitly push the source tag before distribution.
|
||||
An existing output directory is refused to preserve previous evidence.
|
||||
|
||||
Keep `private/` private: build/dependency logs, R8 mapping/effective configuration,
|
||||
APK metadata/certificate inspection and native/ZIP inventory. Publication uploads
|
||||
only the seven named public files, never that directory, credentials or keys.
|
||||
Review release notes and corresponding-source/native links before promotion.
|
||||
|
||||
### Explicit draft publication
|
||||
|
||||
```sh
|
||||
make tv-publish TV_RELEASE_DIR=/var/tmp/wolfskin-tv-release-1.1.1 \
|
||||
TV_PUBLISH_FLAGS=--dry-run
|
||||
# Only after owner approval to upload:
|
||||
make tv-publish TV_RELEASE_DIR=/var/tmp/wolfskin-tv-release-1.1.1
|
||||
# Only after owner approval to publish the verified draft (use its printed id):
|
||||
make tv-publish TV_RELEASE_DIR=/var/tmp/wolfskin-tv-release-1.1.1 \
|
||||
TV_PUBLISH_FLAGS='--draft-id <id> --promote'
|
||||
make tv-tooling-test
|
||||
```
|
||||
|
||||
The publisher reads git's credential helper into process memory, not arguments,
|
||||
logs or files. It creates a draft, uploads all public files, then downloads and
|
||||
hashes their bytes while still draft. Existing matching drafts can be resumed
|
||||
and reverified. Upload/verification failures remain draft. Promotion requires
|
||||
the explicit flag; then anonymous latest/asset hash checks must pass. A failed
|
||||
post-promotion check attempts withdrawal to draft and reports failure, not
|
||||
success. Cross-origin credential forwarding and non-HTTPS URLs are refused.
|
||||
`--test-server` permits only a loopback HTTP fake Forgejo; production defaults
|
||||
cannot be redirected to a different repository by a CLI repository argument.
|
||||
Dry-run never fetches a credential or makes publication requests.
|
||||
|
||||
### Client trust and installation
|
||||
|
||||
`UpdateController` is the process-scoped UI contract with `StateFlow` state,
|
||||
shared auto-check preference, refreshed PackageManager installed version and
|
||||
explicit check/download/cancel/install/retry/resume actions. Its capture fake
|
||||
does not perform real operations. Classic Opt update controls use that same
|
||||
controller; the old package retains its original updater.
|
||||
|
||||
The client uses a dedicated anonymous OkHttp client, validating every redirect:
|
||||
HTTPS, no URL user-info, no cookies/Jellyfin token/repository credential.
|
||||
Loopback HTTP is available only through the test transport constructor flag.
|
||||
It selects armeabi-v7a first when supported, then remaining device ABIs and
|
||||
a universal fallback, subject to minSdk. Universal fallback is limited to
|
||||
the channel's supported playback ABIs: armeabi-v7a, arm64-v8a and x86_64.
|
||||
It cannot make an unsupported processor compatible. Empty-release-list success
|
||||
is distinct from a latest-endpoint 404, authorization failure, invalid metadata
|
||||
or up-to-date code.
|
||||
|
||||
Downloads are app-private. Retry starts a new truncated partial rather than
|
||||
concatenating unvalidated ranges; cancel and process recreation discard partials.
|
||||
Length, SHA-256, APK PackageManager package/version/current signer and manifest
|
||||
signer must agree before Ready. Restoring Ready re-hashes/re-inspects the APK.
|
||||
Checksum files on the same server are not an independent trust authority:
|
||||
the installed Android signer is the installation boundary.
|
||||
|
||||
EOF never starts installation. Install requires a separate OK, per-package
|
||||
unknown-app permission and a confirmation-required PackageInstaller session.
|
||||
Returning from permission settings requires another explicit Install press.
|
||||
Denial, cancellation and missing system handlers retain the verified download.
|
||||
No broad FileProvider, public Downloads write, silent install or automatic
|
||||
uninstall is involved. PackageManager is re-read on return; a successful callback
|
||||
is not proof that the running process is the new version. Real OEM confirmation,
|
||||
process replacement and retained-login acceptance remain device-walk gates.
|
||||
|
||||
|
||||
### Full-suite test diagnostics
|
||||
|
||||
The Gradle daemon heap (`org.gradle.jvmargs` in `gradle.properties`) is separate
|
||||
|
|
@ -89,13 +217,12 @@ wait for either response. The minified SDK log tag is not stable across builds.
|
|||
|
||||
The `optimized` build type inherits `release`: R8 optimization/minification,
|
||||
resource shrinking, non-debuggable code, dependency consumer keep rules and
|
||||
dependency baseline profiles. It changes only signing (the Android debug key),
|
||||
package (`org.justinvisser.wolfskin.tv.opt`) and launcher label (`Wolfskin (Opt)`).
|
||||
The launcher icon/banner and release-compatible version name are unchanged:
|
||||
the shared upgrade/updater parser rejects extra suffixes such as `-opt`.
|
||||
`make apk` remains the clean, release-key-signed production build. Debug and
|
||||
optimized containers share the persistent debug key described below; release
|
||||
signing is unchanged.
|
||||
dependency baseline profiles. Its package (`org.justinvisser.wolfskin.tv.opt`),
|
||||
launcher label (`Wolfskin (Opt)`) and persistent signer remain unchanged. It is
|
||||
the household release assembly, with plain versionName `1.1.1` and an explicit
|
||||
monotonic versionCode; source commit identity is recorded separately.
|
||||
`make apk` remains the old `org.justinvisser.wolfskin.tv` production assembly and
|
||||
release signer. It must never publish to the household channel.
|
||||
|
||||
Build all four default-flavor outputs in one invocation through the container
|
||||
wrapper, without a release keystore. On a host whose Gradle cache already holds
|
||||
|
|
@ -103,6 +230,7 @@ the pinned native extensions and their metadata:
|
|||
|
||||
```sh
|
||||
bash docker/gradle.sh optimized --offline \
|
||||
-PtvVersion=1.1.1 -PtvVersionCode=100001 \
|
||||
-PWholphinExtensionsUsername=JustinZeus \
|
||||
-PWholphinExtensionsPassword=unused \
|
||||
:app:assembleDefaultOptimized
|
||||
|
|
@ -926,14 +1054,12 @@ Jellyfin read by the selected person's saved Jellyfin user ID, so a lost
|
|||
Conductarr operator or a renamed household profile does not hide that user's
|
||||
own library. A different saved user ID still fails closed.
|
||||
|
||||
`make test` runs `OwnedSourceAuditTest` through the design module's JVM suite.
|
||||
It scans the owned Kotlin sources in `wolfskin-design/src/main/java` and
|
||||
`app/src/wolfskin`, rejecting untokened `.dp`/`.sp` conversions and numeric Color
|
||||
constructors outside generated tokens, token font sizes below 12 sp, raw
|
||||
household library labels, and page-counter/navigation wording. Its negative
|
||||
cases plant each violation independently. Existing native capture checks also
|
||||
verify rendered font sizes and wording. `LibraryProgressiveSurfaceTest` captures
|
||||
the usable first chunk while the final read is still suspended.
|
||||
`make test` keeps the native capture checks for rendered font sizes and visible
|
||||
surface behavior, plus generated-token drift checks. Styling uses generated
|
||||
tokens rather than raw dimensions. The source-regex wording/style audit was
|
||||
removed: source spelling is not a consumer-visible behavior contract.
|
||||
`LibraryProgressiveSurfaceTest` captures the usable first chunk while the final
|
||||
read is still suspended.
|
||||
|
||||
## Toolchain and disk use
|
||||
|
||||
|
|
@ -980,21 +1106,25 @@ the checkout when building on another host, for example:
|
|||
WOLFSKIN_DEBUG_KEYSTORE="$HOME/.local/share/wolfskin-signing/debug.keystore" make apk-debug
|
||||
```
|
||||
|
||||
The wrapper creates a missing key once using the build image's `keytool`: RSA
|
||||
2048, 10,000-day validity, alias `androiddebugkey`, store/key passwords `android`,
|
||||
and subject `CN=Android Debug`. The containing directory is mode `0700`, the
|
||||
file mode `0600`. Existing keys are reused, never regenerated; generation errors
|
||||
abort the build rather than falling back to an ephemeral container key.
|
||||
For ordinary debug builds only, the wrapper creates a missing key once using
|
||||
the build image's `keytool`: RSA 2048, 10,000-day validity, alias
|
||||
`androiddebugkey`, store/key passwords `android`, subject `CN=Android Debug`.
|
||||
Household and optimized modes require the existing key and refuse generation.
|
||||
The directory is mode `0700`, the file mode `0600`; existing keys are reused.
|
||||
Generation errors abort rather than falling back to an ephemeral container key.
|
||||
The file is bind-mounted read-only at `/tmp/.android/debug.keystore`, with
|
||||
`ANDROID_USER_HOME=/tmp/.android`, for every non-release Gradle container.
|
||||
Keep and back up this file across clean builds, disposable worktrees, container
|
||||
rebuilds and Gradle cache resets. Never commit it. The standard debug passwords
|
||||
are public; this key is for local debug/optimized builds, not release signing.
|
||||
are public; the private key is nevertheless the permanent household signing
|
||||
authority. It is distinct from the old production package's release signer.
|
||||
|
||||
### Preserve household TV data
|
||||
|
||||
Install and update the household TV app only in place with `adb install -r`,
|
||||
using the same package and persistent debug signer. Its certificate SHA-256 is:
|
||||
Normal household updates use the in-app verified download and explicit Android
|
||||
confirmation, preserving the same package and persistent signer. A separately
|
||||
authorized bootstrap may use operator `adb install -r`; that is not a substitute
|
||||
for the in-app update acceptance walk. The certificate SHA-256 is:
|
||||
|
||||
```text
|
||||
182bac72e292b9ea4d3d4e6383f92eb2161fa28ba5afcff0337cb7d5a9d5341d
|
||||
|
|
@ -1012,9 +1142,10 @@ a **keep app data** choice during interactive uninstall. Choose it when
|
|||
available; older Android versions ignore the attribute. This is a retention
|
||||
prompt, not an automatic backup or a guarantee against data clearing.
|
||||
|
||||
For an in-place update, compare `dumpsys package <pkg>` before and after:
|
||||
`firstInstallTime` must remain unchanged. The guarded installer that checks
|
||||
the signer and this timestamp lives in the operator's tooling, not this repo.
|
||||
An operator can compare `dumpsys package <pkg>` before and after an in-place
|
||||
update: `firstInstallTime` must remain unchanged. The in-app service checks
|
||||
package, current signer, checksum and higher code; real-device timestamp and
|
||||
retained-login proof remain a separate operator walk, not a JVM-test claim.
|
||||
|
||||
|
||||
## Release credentials
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue