Merge ios-completion: device builds unblocked, access token on both clients, P2 wave
Closes the six remediation items from the 2026-07-29 iOS completion audit plus the whole P2 wave and Android access-token parity. The audit's headline was that the client was code-complete but stuck at the device door: no DEVELOPMENT_TEAM, no entitlements, so it had never run on real hardware once, and it had fallen two months behind the server (Android had the git panel, iOS had none) while neither native client could connect at all once WEBTERM_TOKEN was set. Package tests 310 -> 452, app bundle 296 -> 550 (iPhone and iPad, zero known issues), integration 10 -> 32, Android 687 -> 691. ClientTLS went 55.76% -> 89.49% and is now actually in the coverage gate, which it never was. Device build now succeeds on the free personal team. src/ and public/ are untouched — the git-panel endpoints already existed server-side; iOS simply never consumed them. # Conflicts: # android/.gitignore # android/README.md # android/api-client/src/main/kotlin/wang/yaojia/webterm/api/routes/Endpoints.kt # android/app/src/main/java/wang/yaojia/webterm/screens/PairingScreen.kt # android/app/src/main/java/wang/yaojia/webterm/viewmodels/PairingViewModel.kt # android/app/src/main/java/wang/yaojia/webterm/wiring/AppEnvironment.kt # android/transport-okhttp/src/main/kotlin/wang/yaojia/webterm/transport/OkHttpClientFactory.kt # docs/PROGRESS_LOG.md
This commit is contained in:
184
.github/workflows/android.yml
vendored
Normal file
184
.github/workflows/android.yml
vendored
Normal file
@@ -0,0 +1,184 @@
|
||||
# Android CI (F2). Until this file existed the Android client had ZERO CI — 10 modules,
|
||||
# 691 JVM tests and the whole `WEBTERM_TOKEN` secret-handling wave (Tink AEAD under an
|
||||
# AndroidKeystore master key, the POST /auth probe, cookie stamping on every route and on
|
||||
# the WS upgrade) were gated by nothing at all. Modelled on ios.yml: same `on:` shape
|
||||
# (push + pull_request, path-filtered), same "one job per verification layer" structure,
|
||||
# and — like ios.yml — deliberately NO `concurrency:` group, so the two workflows behave
|
||||
# the same way on rapid pushes.
|
||||
#
|
||||
# ── HONESTY NOTE (read before trusting a green badge) ────────────────────────────────────
|
||||
# This repository's only remote is a self-hosted Gitea instance, so GitHub Actions has
|
||||
# NEVER run here and this file has never been executed by the platform. What IS verified:
|
||||
# every command below was run locally on macOS against the same Gradle build
|
||||
# (`./gradlew test :app:assembleDebug koverVerify koverLog` + the androidTest compile),
|
||||
# and the YAML parses. What is NOT verified: the runner-side pieces — action resolution,
|
||||
# the preinstalled Android SDK layout on `ubuntu-latest`, and the emulator leg.
|
||||
#
|
||||
# ── Why no `src/**` path trigger (ios.yml HAS one) ───────────────────────────────────────
|
||||
# ios.yml watches `src/**` because it owns `integration-tests`, a Swift layer that boots the
|
||||
# real Node server and would catch client↔server protocol drift. Android has no equivalent
|
||||
# leg (android/README.md, "DEFERRED — end-to-end access token": nothing here starts a server
|
||||
# with WEBTERM_TOKEN set and completes a cookie-gated WS upgrade). Triggering on `src/**`
|
||||
# would therefore burn runner minutes without checking a single contract. When an Android
|
||||
# server-contract leg is added, add `src/**` here at the same time.
|
||||
name: android
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- "android/**"
|
||||
- ".github/workflows/android.yml"
|
||||
pull_request:
|
||||
paths:
|
||||
- "android/**"
|
||||
- ".github/workflows/android.yml"
|
||||
# Manual trigger for the instrumented (device) leg below, which is dispatch-only.
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
# Layer 1 — everything that runs without a device. This is exactly the green gate
|
||||
# android/README.md documents ("./gradlew test :app:assembleDebug koverVerify"), plus a
|
||||
# compile-only pass over the instrumented sources so they cannot silently rot.
|
||||
build-and-test:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
defaults:
|
||||
run:
|
||||
working-directory: android
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# jvmToolchain(17) is declared by every module; Gradle resolves it from the
|
||||
# installed JDKs, so 17 must actually be present (the runner image's default may
|
||||
# be a different major).
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "17"
|
||||
cache: gradle
|
||||
|
||||
# :app/:terminal-view/:host-registry/:client-tls-android compile against SDK 36 with
|
||||
# build-tools 36.0.0 (android/README.md "Android SDK setup"). The runner image ships an
|
||||
# Android SDK, but which platforms it holds drifts image to image — so install what the
|
||||
# build actually needs instead of assuming, and FAIL LOUDLY if there is no sdkmanager.
|
||||
- name: Install the Android SDK packages this build compiles against
|
||||
run: |
|
||||
# `set -eu` WITHOUT pipefail on purpose: `yes | sdkmanager --licenses` kills `yes`
|
||||
# with SIGPIPE (141) as soon as the prompts stop, and pipefail would report that
|
||||
# normal ending as a failed step.
|
||||
set -eu
|
||||
sdkmanager_bin="$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager"
|
||||
if [ ! -x "$sdkmanager_bin" ]; then
|
||||
sdkmanager_bin="$(ls -1 "$ANDROID_HOME"/cmdline-tools/*/bin/sdkmanager 2>/dev/null | tail -n 1 || true)"
|
||||
fi
|
||||
if [ ! -x "$sdkmanager_bin" ]; then
|
||||
echo "::error title=No sdkmanager::none found under $ANDROID_HOME/cmdline-tools — the Android SDK is not usable on this runner" >&2
|
||||
exit 1
|
||||
fi
|
||||
yes | "$sdkmanager_bin" --licenses > /dev/null
|
||||
"$sdkmanager_bin" "platforms;android-36" "build-tools;36.0.0" "platform-tools"
|
||||
|
||||
# 10 modules of JVM unit tests (JUnit 5 + coroutines-test + Turbine + MockK). Includes
|
||||
# the Android-library modules' testDebugUnitTest and :app's — no device needed.
|
||||
- name: JVM unit tests (all modules)
|
||||
run: ./gradlew test
|
||||
|
||||
# The APK build is its own step: a Kotlin/KSP/Hilt/Compose breakage that unit tests do
|
||||
# not reach (resources, manifest merge, DI graph assembly) must not hide behind them.
|
||||
- name: Debug APK builds
|
||||
run: ./gradlew :app:assembleDebug
|
||||
|
||||
# Coverage floor: >=80% line coverage on the four modules that apply the Kover plugin
|
||||
# (:wire-protocol, :session-core, :api-client, :client-tls). koverLog prints the actual
|
||||
# percentages into the run log so a near-miss is visible before it becomes a failure.
|
||||
# The Android-framework modules are NOT gated (no Kover on them) — a known gap.
|
||||
- name: Coverage gate (Kover, >= 80% on the four gated modules)
|
||||
run: ./gradlew koverVerify koverLog
|
||||
|
||||
# The instrumented sources cannot RUN here, but they must keep COMPILING. Three modules
|
||||
# carry them, 119 @Test in total: `:app` (67 — the device-blocker suite: key routing,
|
||||
# resize, alternate-screen scroll, autofill, driven through a custom HiltTestRunner),
|
||||
# `:host-registry` (31 — incl. TinkAccessTokenStoreTest, the only assertion anywhere that
|
||||
# the access token is ciphertext at rest) and `:client-tls-android` (21 — AndroidKeyStore
|
||||
# import). A silent compile break would delete all of that without anyone noticing.
|
||||
- name: Instrumented test sources still compile
|
||||
run: ./gradlew :app:assembleDebugAndroidTest :host-registry:assembleDebugAndroidTest :client-tls-android:assembleDebugAndroidTest
|
||||
|
||||
- name: Upload test + coverage reports
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: android-reports
|
||||
path: |
|
||||
android/*/build/reports/tests/**
|
||||
android/*/build/test-results/**
|
||||
android/*/build/reports/kover/**
|
||||
retention-days: 7
|
||||
if-no-files-found: ignore
|
||||
|
||||
# Layer 2 — instrumented (device) tests. DISPATCH-ONLY, on purpose.
|
||||
#
|
||||
# What is at stake: `TinkAccessTokenStoreTest` is the ONLY test anywhere that proves the
|
||||
# frozen at-rest properties of the access token (ios-completion §1.1) — that a fresh store
|
||||
# instance decrypts it after a cold start, and that the bytes on disk are ciphertext rather
|
||||
# than the token in the clear. Tink's AEAD + the AndroidKeystore-wrapped master key have no
|
||||
# JVM double, so this can only run on a device or emulator.
|
||||
#
|
||||
# The suite itself is NOT unproven: the android-blocker-fixes session ran `:app`'s 67
|
||||
# instrumented tests green on real hardware. What has never been proven is this leg — the
|
||||
# CI *emulator* path. Those are different claims and only the second one gates this `if:`.
|
||||
#
|
||||
# Why it is not on push: this workflow has never executed on any runner (see the honesty
|
||||
# note at the top), and an emulator leg is the single most fragile thing in Android CI —
|
||||
# it needs KVM, a system image download, and an AVD boot. Wiring an unvalidated, ~15-minute,
|
||||
# KVM-dependent job into every push would either block the repo on a leg nobody has seen
|
||||
# pass, or push people to make it non-blocking — and a leg that cannot fail is a false green
|
||||
# (the same trap ios.yml's `ios17-floor-tests` comment calls out).
|
||||
#
|
||||
# THEREFORE: run it by hand from the Actions tab (workflow_dispatch) whenever the token
|
||||
# store, the DataStore stores, the AndroidKeyStore importer, or the terminal view/input
|
||||
# path change. Once it has been seen green on a real runner, promote it by deleting the
|
||||
# `if:` below — the suite has already earned it on hardware.
|
||||
#
|
||||
# MANUAL EQUIVALENT (the path that has actually been walked — DEVICE_QA_CHECKLIST.md):
|
||||
# with a device/emulator attached,
|
||||
# cd android && ANDROID_HOME=<sdk> ./gradlew :app:connectedDebugAndroidTest \
|
||||
# :host-registry:connectedDebugAndroidTest \
|
||||
# :client-tls-android:connectedDebugAndroidTest
|
||||
# (StrongBox falls back to software keys on an emulator, so hardware-backed key claims still
|
||||
# need real hardware.)
|
||||
instrumented-tests:
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "17"
|
||||
cache: gradle
|
||||
- name: Enable KVM for the emulator
|
||||
run: |
|
||||
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \
|
||||
| sudo tee /etc/udev/rules.d/99-kvm4all.rules
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger --name-match=kvm
|
||||
# API 29 == the app's minSdk: the deployment floor is where at-rest crypto behaviour is
|
||||
# most likely to differ (mirrors ios.yml's iOS 17 floor leg).
|
||||
- name: connectedDebugAndroidTest on an API 29 emulator
|
||||
uses: reactivecircus/android-emulator-runner@v2
|
||||
with:
|
||||
api-level: 29
|
||||
target: google_apis
|
||||
arch: x86_64
|
||||
working-directory: android
|
||||
script: ./gradlew :app:connectedDebugAndroidTest :host-registry:connectedDebugAndroidTest :client-tls-android:connectedDebugAndroidTest
|
||||
- name: Upload instrumented reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: android-instrumented-reports
|
||||
path: android/*/build/reports/androidTests/**
|
||||
retention-days: 7
|
||||
if-no-files-found: ignore
|
||||
Reference in New Issue
Block a user