Compare commits
2 Commits
53d18beb03
...
a7f252e656
| Author | SHA1 | Date |
|---|---|---|
|
|
a7f252e656 | |
|
|
a63bf350b1 |
|
|
@ -1,9 +1,15 @@
|
||||||
# Android CI — JVM unit tests + static repo scans + secret scan.
|
# Android CI — JVM unit tests + Android Lint + R8 release-build gate + static
|
||||||
|
# repo scans + secret scan.
|
||||||
#
|
#
|
||||||
# Secrets are intentionally ABSENT here: the debug build compiles with an empty
|
# Real Firebase/RC secrets are intentionally ABSENT: the debug build compiles with
|
||||||
# APP_CHECK_DEBUG_TOKEN and a placeholder RC_API_KEY (see app/build.gradle.kts
|
# an empty APP_CHECK_DEBUG_TOKEN and a placeholder RC_API_KEY (see
|
||||||
# `secret()` resolution). CI-built APKs therefore cannot pass App Check once
|
# app/build.gradle.kts `secret()` resolution). CI-built APKs therefore cannot pass
|
||||||
# enforcement is on — expected; CI only runs JVM tests.
|
# App Check once enforcement is on — expected.
|
||||||
|
#
|
||||||
|
# The `release-build` job DOES exercise the RELEASE variant (minify+shrink+sign)
|
||||||
|
# using a THROWAWAY keystore + a dummy RC_API_KEY, purely to satisfy the release
|
||||||
|
# guards; that AAB is never distributed. A green release-build proves the R8
|
||||||
|
# toolchain, not that reflectively-loaded classes survive shrinking at runtime.
|
||||||
#
|
#
|
||||||
# app/google-services.json is gitignored (live Firebase config), but the
|
# app/google-services.json is gitignored (live Firebase config), but the
|
||||||
# google-services Gradle plugin hard-fails without one, so CI copies the dummy
|
# google-services Gradle plugin hard-fails without one, so CI copies the dummy
|
||||||
|
|
@ -45,6 +51,85 @@ jobs:
|
||||||
name: unit-test-reports
|
name: unit-test-reports
|
||||||
path: app/build/reports/tests/
|
path: app/build/reports/tests/
|
||||||
|
|
||||||
|
android-lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 20
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: temurin
|
||||||
|
java-version: "17"
|
||||||
|
|
||||||
|
- uses: gradle/actions/setup-gradle@v4
|
||||||
|
|
||||||
|
- name: Provide stub google-services.json (real one is gitignored)
|
||||||
|
run: |
|
||||||
|
if [ ! -f app/google-services.json ]; then
|
||||||
|
cp .github/ci/google-services.json app/google-services.json
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fails on error-severity issues (NewApi, correctness). This gate caught two
|
||||||
|
# LocalDate.ofInstant NewApi crashes (API 34 call, minSdk 26) on 2026-07-11
|
||||||
|
# that emulator QA missed because the fixture emulators run API 34+.
|
||||||
|
- name: Android Lint (debug)
|
||||||
|
run: ./gradlew :app:lintDebug
|
||||||
|
|
||||||
|
- name: Upload lint report on failure
|
||||||
|
if: failure()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: lint-report
|
||||||
|
path: app/build/reports/lint-results-debug.html
|
||||||
|
|
||||||
|
release-build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: temurin
|
||||||
|
java-version: "17"
|
||||||
|
|
||||||
|
- uses: gradle/actions/setup-gradle@v4
|
||||||
|
|
||||||
|
- name: Provide stub google-services.json (real one is gitignored)
|
||||||
|
run: |
|
||||||
|
if [ ! -f app/google-services.json ]; then
|
||||||
|
cp .github/ci/google-services.json app/google-services.json
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Throwaway upload key — only exists to satisfy the release signing guard.
|
||||||
|
# This AAB is never distributed.
|
||||||
|
- name: Generate throwaway CI signing keystore
|
||||||
|
run: |
|
||||||
|
keytool -genkeypair -v \
|
||||||
|
-keystore "$RUNNER_TEMP/ci-release.jks" \
|
||||||
|
-alias ci -keyalg RSA -keysize 2048 -validity 30 \
|
||||||
|
-storepass ci_ci_ci -keypass ci_ci_ci \
|
||||||
|
-dname "CN=CI, O=CI, C=US"
|
||||||
|
|
||||||
|
- name: Build minified release bundle (R8 gate)
|
||||||
|
env:
|
||||||
|
RC_API_KEY: ci_dummy_rc_key_not_real
|
||||||
|
RELEASE_STORE_FILE: ${{ runner.temp }}/ci-release.jks
|
||||||
|
RELEASE_STORE_PASSWORD: ci_ci_ci
|
||||||
|
RELEASE_KEY_ALIAS: ci
|
||||||
|
RELEASE_KEY_PASSWORD: ci_ci_ci
|
||||||
|
# Skip the Crashlytics mapping upload — it needs real Firebase creds the
|
||||||
|
# stub google-services.json does not have.
|
||||||
|
run: ./gradlew :app:bundleRelease -x uploadCrashlyticsMappingFileRelease
|
||||||
|
|
||||||
|
- name: Upload R8 mapping on failure
|
||||||
|
if: failure()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: r8-mapping
|
||||||
|
path: app/build/outputs/mapping/release/
|
||||||
|
|
||||||
repo-scans:
|
repo-scans:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
|
|
|
||||||
|
|
@ -288,6 +288,9 @@ private fun rememberMediaImageLoader(): ImageLoader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// False positive: the producer DOES assign `value` unconditionally below. The
|
||||||
|
// ProduceStateDoesNotAssignValue check misfires when the RHS is a suspend call.
|
||||||
|
@Suppress("ProduceStateDoesNotAssignValue")
|
||||||
@Composable
|
@Composable
|
||||||
private fun EncryptedChatImage(
|
private fun EncryptedChatImage(
|
||||||
mediaUrl: String,
|
mediaUrl: String,
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,8 @@ class YourProgressViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun dueFollowUpDay(createdAt: Long, outcomes: List<Outcome>): OutcomeDay? {
|
private fun dueFollowUpDay(createdAt: Long, outcomes: List<Outcome>): OutcomeDay? {
|
||||||
val paired = LocalDate.ofInstant(Instant.ofEpochMilli(createdAt), ZoneId.systemDefault())
|
// LocalDate.ofInstant is API 34+; atZone().toLocalDate() is API 26 (our minSdk) — same result.
|
||||||
|
val paired = Instant.ofEpochMilli(createdAt).atZone(ZoneId.systemDefault()).toLocalDate()
|
||||||
val today = LocalDate.now()
|
val today = LocalDate.now()
|
||||||
val ageDays = java.time.temporal.ChronoUnit.DAYS.between(paired, today)
|
val ageDays = java.time.temporal.ChronoUnit.DAYS.between(paired, today)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -186,6 +186,9 @@ private fun DiscussionMessageBubble(
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Downloads the encrypted image bytes, decrypts them on-device, and renders the photo. */
|
/** Downloads the encrypted image bytes, decrypts them on-device, and renders the photo. */
|
||||||
|
// False positive: the producer DOES assign `value` unconditionally below. The
|
||||||
|
// ProduceStateDoesNotAssignValue check misfires when the RHS is a suspend call.
|
||||||
|
@Suppress("ProduceStateDoesNotAssignValue")
|
||||||
@Composable
|
@Composable
|
||||||
private fun EncryptedChatImage(
|
private fun EncryptedChatImage(
|
||||||
mediaUrl: String,
|
mediaUrl: String,
|
||||||
|
|
|
||||||
|
|
@ -220,7 +220,8 @@ class SettingsViewModel @Inject constructor(
|
||||||
fun consumeOutcomeError() = _uiState.update { it.copy(outcomeError = null) }
|
fun consumeOutcomeError() = _uiState.update { it.copy(outcomeError = null) }
|
||||||
|
|
||||||
private fun dueFollowUpDay(createdAt: Long, outcomes: List<Outcome>): OutcomeDay? {
|
private fun dueFollowUpDay(createdAt: Long, outcomes: List<Outcome>): OutcomeDay? {
|
||||||
val paired = LocalDate.ofInstant(Instant.ofEpochMilli(createdAt), ZoneId.systemDefault())
|
// LocalDate.ofInstant is API 34+; atZone().toLocalDate() is API 26 (our minSdk) — same result.
|
||||||
|
val paired = Instant.ofEpochMilli(createdAt).atZone(ZoneId.systemDefault()).toLocalDate()
|
||||||
val today = LocalDate.now()
|
val today = LocalDate.now()
|
||||||
val ageDays = java.time.temporal.ChronoUnit.DAYS.between(paired, today)
|
val ageDays = java.time.temporal.ChronoUnit.DAYS.between(paired, today)
|
||||||
val due = listOf(
|
val due = listOf(
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue