Ubisam-MobileGit/tasks/016-release-hardening.md
choiminyeong f6ac20c8ed TASK-063: task records say what is actually open
No task waits on a Codex review; the redesign tasks are confirmed on the
product owner's Z Fold7; TASK-016's leftovers are on hold; no biometric
reauthentication.

Agent: main (opus)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:25:49 +09:00

8.5 KiB

TASK-016: Release hardening

Status: partially implemented — the two open items (R8 minification, on-device regression of every authenticated screen) are on hold by the human product owner's decision (2026-09-29) Owner: Claude (human product owner directed Claude to own all subsequent tasks, 2026-08-26) Reviewer: Codex

Outcome

The parts of release hardening that can be verified without an authenticated Gitea session are done: the one previously-recorded P3 dispatcher issue is fixed, the whole repository (not just this task's diff) was scanned for hardcoded secrets, the manifest/backup/token-storage security posture was reviewed, and the release build variant itself was confirmed to compile. The parts that require being signed in — regression-testing every screen built this session, and any decision to enable R8/ProGuard minification — are explicitly left open, not silently skipped.

Scope

  • Included:
    • Fix the P3 finding recorded during the TASK-004/005/006 review (docs/development-log.md, 2026-08-25): CodeBrowserViewModel ran JSON parsing, base64 decoding, directory sort/dedupe, and the TASK-006A content classifier on viewModelScope's Main dispatcher.
    • Full-repository secret/credential scan (not scoped to one task's diff).
    • Manifest, token-storage, and backup-rule security review.
    • Confirm the release build variant itself compiles (assembleRelease), independent of any minification decision.
    • Pre-authentication on-device smoke test using a local emulator: cold install, launch, sign-in screen renders and is localized, tapping "Gitea로 로그인" correctly hands off to the system browser toward the OAuth authorize flow, no crash.
  • Excluded (explicitly blocked, not attempted):
    • Enabling isMinifyEnabled/R8 shrinking for the release build type. See "Known limitations."
    • Any device regression of a screen that requires being signed in (repository list, favorites, organizations, code browser, history, work items, notifications, the navigation back stack, logout) — see "Known limitations."
    • Configuring a real release signing config / keystore — this involves a secret-management decision (where the keystore lives, whether to use Play App Signing) that belongs to the human product owner, not something to invent here.
    • Bumping versionCode/versionName — left at 1/0.1.0; not yet meaningful until an actual release is cut.

Writable paths

  • app/src/main/java/com/ubisam/ubgitea/feature/codebrowser/CodeBrowserViewModel.kt
  • app/src/test/java/com/ubisam/ubgitea/feature/codebrowser/CodeBrowserViewModelTest.kt
  • tasks/016-release-hardening.md

Acceptance criteria

  • The recorded P3 dispatcher issue in CodeBrowserViewModel is fixed (heavy work moved off the Main dispatcher), with the dispatcher injectable for deterministic tests, matching the existing pattern in AndroidKeystoreTokenVault.
  • A full-repository secret/credential scan finds no hardcoded secrets (only the pre-existing test that asserts client_secret is absent from the OAuth URI).
  • Manifest/backup/token-storage security posture reviewed: allowBackup="false", data_extraction_rules.xml excludes all sharedpref data from both cloud backup and device transfer, AndroidKeystoreTokenVault uses AES/GCM via the Android Keystore with MODE_PRIVATE prefs, no Log.*/println calls exist anywhere in app/src/main, cleartext HTTP traffic is blocked by Android's own default (no usesCleartextTraffic override, minSdk 28).
  • ./gradlew assembleRelease succeeds (confirms the release build type itself is not broken), independent of the minification decision below.
  • Pre-authentication screen verified on a real emulator (see Evidence).
  • Blocked — R8/ProGuard minification decision: left disabled (isMinifyEnabled = false, unchanged). Enabling it is a real correctness risk specific to this codebase — every data/*Api.kt class uses Gson with TypeToken<List<T>> generic reflection, which R8 can silently break (wrong/missing keep rules erase generic signatures or rename @SerializedName-less fields) with no compile-time signal, and the only way to catch that is exercising the JSON-parsing-heavy authenticated screens on a minified build — which requires the login this session cannot complete (see below). Left as an open decision for whoever has real device + login access.
  • Blocked — On-device regression of every authenticated screen built this session (repository list/paging/favorites/organizations, code browser, history, work items, notifications, navigation back-stack restoration, logout). Structurally cannot be completed by Claude in this environment: completing Gitea OAuth login requires entering the human product owner's password, which is a universally prohibited action regardless of whose credentials are offered — no amount of additional emulator/tooling work changes that. Confirmed with the human product owner (2026-08-26): proceed with everything else, leave this explicitly open for the user or Codex (whichever has real login access) to complete.

Required checks

./gradlew assembleDebug
./gradlew testDebugUnitTest
./gradlew lintDebug
./gradlew assembleRelease

Evidence

  • Branch claude/task-015-notifications (continued in the same worktree/branch as TASK-015, per the standing "proceed to the next task without stopping" direction; no separate branch was cut since this is a small, tightly-scoped follow-on).
  • CodeBrowserViewModel.kt: loadDirectory's tree-listing/dedupe/sort and loadFile's fetch+classify are now wrapped in withContext(defaultDispatcher), with defaultDispatcher: CoroutineDispatcher = Dispatchers.Default as a constructor parameter (production code takes the default; CodeBrowserViewModelTest injects the same StandardTestDispatcher already used for Dispatchers.Main, so advanceUntilIdle() still deterministically drives the moved work instead of racing a real background thread pool).
  • Full-repository secret scan: git grep -inE "client_secret[[:space:]]*[:=]...|password[[:space:]]*[:=]...|BEGIN (RSA|EC|OPENSSH|PGP) PRIVATE KEY|AKIA[0-9A-Z]{16}|xox[baprs]-..." across the whole tree — zero matches.
  • git grep -inE "clientSecret|client_secret" — only the pre-existing test assertion that it's absent from the authorization URI, and prior dev-log/task-file mentions of that same test; no leaked value.
  • git grep -inE "Log\.(d|v|i|w|e)\(|println\(|System\.out" -- 'app/src/main/**/*.kt' — zero matches; nothing to strip for release.
  • Reviewed AndroidManifest.xml: allowBackup="false", App Link intent filter has android:autoVerify="true" scoped to the exact OAuth callback host+path, no usesCleartextTraffic override (blocked by default at minSdk 28).
  • Reviewed data_extraction_rules.xml: both <cloud-backup> and <device-transfer> exclude domain="sharedpref" path="." — the encrypted token vault's prefs file cannot leave the device via Android 12+ backup/transfer, on top of allowBackup="false" already blocking the legacy path.
  • Reviewed AndroidKeystoreTokenVault.kt: AES/GCM key generated inside the Android Keystore (PURPOSE_ENCRYPT/DECRYPT, setRandomizedEncryptionRequired(true)), tokens stored only as ciphertext+IV in MODE_PRIVATE SharedPreferences, a decrypt failure clears the store rather than surfacing a stale/corrupt token. No changes needed.
  • ./gradlew assembleRelease: BUILD SUCCESSFUL (unsigned release APK; lintVitalRelease passed).
  • On-device smoke test: booted the WhatToEat_API_36 AVD (Android emulator already present in this environment; confirmed with the human product owner before using it), installed the debug build, launched cold — no crash, no AndroidRuntime/FATAL in logcat. Screenshot confirms the sign-in screen renders with correct Korean text ("사내 Gitea에 로그인해 접근 가능한 저장소를 확인하세요" / "Gitea로 로그인"). Tapping the login button correctly launches the system browser (Chrome) rather than crashing or no-opping, confirming the browserRequests → startActivity(ACTION_VIEW) handoff in MainActivity works on a real Android runtime, not just in unit tests.
  • ./gradlew assembleDebug testDebugUnitTest lintDebug --rerun-tasks (after the dispatcher fix): BUILD SUCCESSFUL, 182 unit tests total across 30 suites, 0 failures/errors, 0 lint errors.
  • Emulator was shut down (adb emu kill) after the smoke test; no device is left running.

Review findings

None yet — no Codex review pending (Codex retired, 2026-09-29), with the two explicitly blocked acceptance criteria above flagged for whoever picks this up next.