CI #61
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: [ main, "2.1.0" ] | |
| pull_request: | |
| workflow_dispatch: # manual: full legacy suite on demand | |
| schedule: | |
| - cron: '0 3 * * *' # nightly: full legacy suite | |
| concurrency: | |
| group: ci-${{ github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| env: | |
| GRADLE_OPTS: "-Dorg.gradle.jvmargs=-Xmx4g" | |
| jobs: | |
| # --------------------------------------------------------------------------- | |
| # Baseline: multiplatform compile + the full JVM unit suite. KSAFE_KEYVAULT_IT | |
| # is unset here, so the JVM suite runs against the software fallback (no real | |
| # Keychain/keyring access — see ksafe/build.gradle.kts). | |
| # --------------------------------------------------------------------------- | |
| build: | |
| name: Build + fast JVM tests | |
| 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: Compile all :ksafe targets | |
| run: ./gradlew :ksafe:compileKotlinJvm :ksafe:compileKotlinJs :ksafe:compileKotlinWasmJs :ksafe:compileKotlinMetadata --stacktrace | |
| # Only the fast, feature-relevant JVM unit test. The full :ksafe:jvmTest | |
| # suite is run by the separate (non-blocking) jvm-full-suite job — it | |
| # contains pre-existing stress tests that livelock on a 2-vCPU runner | |
| # (≈10s locally, never-completing in CI), unrelated to this change. | |
| - name: Key vault unit test (software fallback) | |
| run: ./gradlew :ksafe:jvmTest --tests "eu.anifantakis.lib.ksafe.JvmKeyVaultMigrationTest" --stacktrace | |
| - name: Upload test report | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: jvm-fast-test-report | |
| path: ksafe/build/reports/tests/jvmTest | |
| if-no-files-found: ignore | |
| # --------------------------------------------------------------------------- | |
| # Full :ksafe:jvmTest suite — now runs on push/PR. The livelock was the | |
| # concurrency-stress tests enqueuing tens of thousands of fire-and-forget | |
| # putDirects that a 2-vCPU runner's single DataStore writer can't drain in | |
| # time. `-PksafeStressScale=0.05` shrinks those magnitudes to thousands of | |
| # ops (still exercises the same cache/dirty-keys races) so the suite is | |
| # drainable; full intensity remains the local default. | |
| # --------------------------------------------------------------------------- | |
| jvm-full-suite: | |
| name: Full JVM suite | |
| 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 | |
| # -PksafeTestLog kept ON: scaling the stress magnitude alone did NOT | |
| # stop the 2-vCPU hang, so a specific test deadlocks (not a throughput | |
| # drain). The per-test "STARTED/PASSED" events make the job log name the | |
| # culprit (last STARTED with no result) on timeout. | |
| - name: Full JVM unit suite (software fallback, scaled stress) | |
| run: ./gradlew :ksafe:jvmTest -PksafeStressScale=0.05 -PksafeTestLog --stacktrace | |
| - name: Upload test report | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: jvm-full-test-report | |
| path: ksafe/build/reports/tests/jvmTest | |
| if-no-files-found: ignore | |
| # --------------------------------------------------------------------------- | |
| # :ksafe-compose and :ksafe-biometrics — jvm + both browser targets. Their | |
| # native (macos/iosSim) tests are folded into the `apple` job below. Android | |
| # is intentionally manual/on-device only (see note near the bottom). | |
| # --------------------------------------------------------------------------- | |
| modules: | |
| name: Compose + Biometrics (jvm + js + wasmJs) | |
| 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: Install Chrome | |
| id: setup-chrome | |
| uses: browser-actions/setup-chrome@v1 | |
| - name: Module tests | |
| env: | |
| CHROME_BIN: ${{ steps.setup-chrome.outputs.chrome-path }} | |
| run: > | |
| ./gradlew | |
| :ksafe-compose:jvmTest :ksafe-compose:jsBrowserTest :ksafe-compose:wasmJsBrowserTest | |
| :ksafe-biometrics:jvmTest :ksafe-biometrics:jsBrowserTest :ksafe-biometrics:wasmJsBrowserTest | |
| --continue --stacktrace | |
| - name: Upload test report | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: modules-test-report | |
| path: | | |
| ksafe-compose/build/reports/tests | |
| ksafe-biometrics/build/reports/tests | |
| if-no-files-found: ignore | |
| # --------------------------------------------------------------------------- | |
| # Web: real WebCrypto + IndexedDB path under headless Chrome | |
| # (Kotlin/JS + Kotlin/WASM). Exercises WebKeyStoreIntegrationTest. | |
| # --------------------------------------------------------------------------- | |
| web: | |
| name: Web browser tests (js + wasmJs) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 40 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-java@v4 | |
| with: | |
| distribution: temurin | |
| java-version: '17' | |
| - uses: gradle/actions/setup-gradle@v4 | |
| - name: Install Chrome | |
| id: setup-chrome | |
| uses: browser-actions/setup-chrome@v1 | |
| - name: Browser tests + Kotlin/JS truncation guard | |
| env: | |
| CHROME_BIN: ${{ steps.setup-chrome.outputs.chrome-path }} | |
| # verifyWebTestParity dependsOn jsBrowserTest + wasmJsBrowserTest and | |
| # fails if Kotlin/JS silently registered fewer tests than wasmJs. | |
| # --continue so both browser suites run/report before the guard checks. | |
| run: ./gradlew :ksafe:verifyWebTestParity --continue --stacktrace | |
| - name: Upload test report | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: web-test-report | |
| path: ksafe/build/reports/tests | |
| if-no-files-found: ignore | |
| # --------------------------------------------------------------------------- | |
| # Linux Secret Service (libsecret) — real gnome-keyring under a dbus session. | |
| # --------------------------------------------------------------------------- | |
| keyvault-linux: | |
| name: Key vault IT — Linux Secret Service | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| env: | |
| KSAFE_KEYVAULT_IT: "1" | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-java@v4 | |
| with: | |
| distribution: temurin | |
| java-version: '17' | |
| - uses: gradle/actions/setup-gradle@v4 | |
| - name: Install gnome-keyring + libsecret | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y gnome-keyring libsecret-1-0 libsecret-tools dbus-x11 python3-secretstorage | |
| - name: Key vault integration test (libsecret) | |
| shell: bash | |
| run: | | |
| dbus-run-session -- bash -lc ' | |
| set -e | |
| # --login CREATES the login keyring (then unlocks it and aliases it | |
| # as the default collection). --unlock only unlocks a pre-existing | |
| # one, which is why earlier attempts failed: there was no | |
| # collection/login object, so default-collection resolution tried | |
| # to CreateCollection and hit the absent headless prompter. | |
| eval "$(echo -n \"\" | gnome-keyring-daemon --daemonize --login)" | |
| eval "$(echo -n \"\" | gnome-keyring-daemon --start --components=secrets)" | |
| sleep 2 | |
| # Sanity: default collection now resolves without a prompt. | |
| python3 -c "import secretstorage; b=secretstorage.dbus_init(); c=secretstorage.get_default_collection(b); print(\"default collection\", c.collection_path, \"locked\", c.is_locked())" | |
| ./gradlew :ksafe:jvmTest --tests "eu.anifantakis.lib.ksafe.JvmKeyVaultIntegrationTest" --tests "eu.anifantakis.lib.ksafe.Jvm200To210FixtureTest" --tests "eu.anifantakis.lib.ksafe.JvmKeyVaultMigrationTest" --no-daemon --no-configuration-cache --stacktrace | |
| ' | |
| - name: Upload test report | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: keyvault-linux-report | |
| path: ksafe/build/reports/tests/jvmTest | |
| if-no-files-found: ignore | |
| # --------------------------------------------------------------------------- | |
| # Windows DPAPI — available for the runner user out of the box. | |
| # --------------------------------------------------------------------------- | |
| keyvault-windows: | |
| name: Key vault IT — Windows DPAPI | |
| runs-on: windows-latest | |
| timeout-minutes: 30 | |
| defaults: | |
| run: | |
| shell: bash | |
| env: | |
| KSAFE_KEYVAULT_IT: "1" | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-java@v4 | |
| with: | |
| distribution: temurin | |
| java-version: '17' | |
| - uses: gradle/actions/setup-gradle@v4 | |
| - name: Key vault integration test (DPAPI) | |
| run: ./gradlew :ksafe:jvmTest --tests 'eu.anifantakis.lib.ksafe.JvmKeyVaultIntegrationTest' --tests 'eu.anifantakis.lib.ksafe.Jvm200To210FixtureTest' --tests 'eu.anifantakis.lib.ksafe.JvmKeyVaultMigrationTest' --no-daemon --no-configuration-cache --stacktrace | |
| - name: Upload test report | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: keyvault-windows-report | |
| path: ksafe/build/reports/tests/jvmTest | |
| if-no-files-found: ignore | |
| # --------------------------------------------------------------------------- | |
| # macOS Keychain — a dedicated, unlocked keychain (not the login keychain) so | |
| # there are no GUI access prompts and no developer-keychain pollution. | |
| # --------------------------------------------------------------------------- | |
| keyvault-macos: | |
| name: Key vault IT — macOS Keychain | |
| runs-on: macos-latest | |
| timeout-minutes: 30 | |
| env: | |
| KSAFE_KEYVAULT_IT: "1" | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-java@v4 | |
| with: | |
| distribution: temurin | |
| java-version: '17' | |
| - uses: gradle/actions/setup-gradle@v4 | |
| - name: Create + unlock a dedicated keychain | |
| run: | | |
| security create-keychain -p "" build.keychain | |
| security default-keychain -s build.keychain | |
| security unlock-keychain -p "" build.keychain | |
| # No auto-lock (timeout / sleep) for the duration of the job. | |
| security set-keychain-settings build.keychain | |
| - name: Key vault integration test (Keychain) | |
| run: ./gradlew :ksafe:jvmTest --tests 'eu.anifantakis.lib.ksafe.JvmKeyVaultIntegrationTest' --tests 'eu.anifantakis.lib.ksafe.Jvm200To210FixtureTest' --tests 'eu.anifantakis.lib.ksafe.JvmKeyVaultMigrationTest' --no-daemon --no-configuration-cache --stacktrace | |
| - name: Upload test report | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: keyvault-macos-report | |
| path: ksafe/build/reports/tests/jvmTest | |
| if-no-files-found: ignore | |
| # --------------------------------------------------------------------------- | |
| # Apple native (Kotlin/Native): macosArm64 + iosSimulatorArm64. Covers the | |
| # CryptoKit/appleMain path and the full common suite incl. the #31 | |
| # regression. (Real iOS-device & real-Keychain coverage needs a signed app | |
| # bundle / device farm — not possible on hosted CI; the simulator + native | |
| # macOS runner is the standard proxy.) | |
| # --------------------------------------------------------------------------- | |
| apple: | |
| name: Apple native (macosArm64 + iosSimulatorArm64) | |
| runs-on: macos-latest | |
| timeout-minutes: 45 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-java@v4 | |
| with: | |
| distribution: temurin | |
| java-version: '17' | |
| - uses: gradle/actions/setup-gradle@v4 | |
| - name: Native Apple tests (all 3 modules) | |
| run: > | |
| ./gradlew | |
| :ksafe:macosArm64Test :ksafe:iosSimulatorArm64Test | |
| :ksafe-compose:macosArm64Test :ksafe-compose:iosSimulatorArm64Test | |
| :ksafe-biometrics:macosArm64Test :ksafe-biometrics:iosSimulatorArm64Test | |
| --continue --stacktrace | |
| - name: Upload test report | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: apple-native-report | |
| path: ksafe/build/reports/tests | |
| if-no-files-found: ignore | |
| # --------------------------------------------------------------------------- | |
| # Android instrumented on a REAL device via Firebase Test Lab. | |
| # | |
| # There is deliberately no hosted-emulator job: GitHub's Android emulator | |
| # uses a software-emulated Keymaster under nested virtualization that is so | |
| # slow it stalls early Keystore-heavy tests (froze at test #3 regardless of | |
| # scope) AND would only validate the *emulated* Keystore, not the real | |
| # hardware-backed one that is KSafe's actual Android value. FTL runs the | |
| # full connectedAndroidDeviceTest suite on a real device's real TEE/Keystore | |
| # (verified equivalent locally: Galaxy S24 Ultra, 123/123 incl. #31). | |
| # | |
| # SETUP (repo owner, one-time — I cannot do these; they are credentials): | |
| # 1. A Firebase/GCP project with Test Lab + Cloud Testing + Tool Results | |
| # APIs enabled, billing (Blaze) on (physical devices need it; small | |
| # free quota exists). | |
| # 2. A service account with roles: roles/cloudtestservice.testAdmin, | |
| # roles/firebase.testLabAdmin, roles/serviceusage.serviceUsageConsumer. | |
| # Download its JSON key. | |
| # 3. GitHub repo secret GCP_SA_KEY = the service-account JSON. | |
| # GitHub repo variable GCP_PROJECT_ID = the GCP project id. | |
| # Until those exist this job NO-OPS (skips, stays green) so it never | |
| # reddens the pipeline. It is continue-on-error until the first real run is | |
| # validated (the self-instrumented-library APK arg / FTL device codename | |
| # are the likely one-line tweaks) — then flip continue-on-error to false to | |
| # make it a hard gate. Skipped on pull_request to limit FTL spend. | |
| # --------------------------------------------------------------------------- | |
| android-firebase: | |
| name: Android instrumented (Firebase Test Lab, real device) | |
| if: ${{ github.event_name != 'pull_request' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| continue-on-error: true | |
| env: | |
| FIREBASE_SA_KEY: ${{ secrets.GCP_SA_KEY }} | |
| FIREBASE_PROJECT: ${{ vars.GCP_PROJECT_ID }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Check Firebase Test Lab credentials | |
| id: ftl | |
| run: | | |
| if [ -n "$FIREBASE_SA_KEY" ] && [ -n "$FIREBASE_PROJECT" ]; then | |
| echo "enabled=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "enabled=false" >> "$GITHUB_OUTPUT" | |
| echo "::notice::Firebase Test Lab not configured (set GCP_SA_KEY secret + GCP_PROJECT_ID variable). Skipping real-device Android tests." | |
| fi | |
| - uses: actions/setup-java@v4 | |
| if: steps.ftl.outputs.enabled == 'true' | |
| with: | |
| distribution: temurin | |
| java-version: '17' | |
| - uses: gradle/actions/setup-gradle@v4 | |
| if: steps.ftl.outputs.enabled == 'true' | |
| - name: Assemble instrumentation test APK | |
| if: steps.ftl.outputs.enabled == 'true' | |
| run: ./gradlew :ksafe:assembleAndroidDeviceTest --stacktrace | |
| - name: Authenticate to Google Cloud | |
| if: steps.ftl.outputs.enabled == 'true' | |
| uses: google-github-actions/auth@v2 | |
| with: | |
| credentials_json: ${{ secrets.GCP_SA_KEY }} | |
| - name: Set up gcloud | |
| if: steps.ftl.outputs.enabled == 'true' | |
| uses: google-github-actions/setup-gcloud@v2 | |
| - name: Run full suite on Firebase Test Lab (real device, real Keystore) | |
| if: steps.ftl.outputs.enabled == 'true' | |
| run: | | |
| set -euo pipefail | |
| TEST_APK=$(find ksafe/build/outputs/apk -name '*.apk' -type f | head -1) | |
| [ -n "$TEST_APK" ] || { echo "No androidTest APK found under ksafe/build/outputs/apk"; exit 1; } | |
| echo "Test APK: $TEST_APK" | |
| # Self-instrumented library: the AGP KMP plugin emits a single | |
| # androidTest APK (ksafe/build/outputs/apk/androidTest/) that is | |
| # both the app under test and the test. Real device => full suite | |
| # (no notClass exclusion; the emulator-only stalls don't apply). | |
| # --use-orchestrator matches the build's clearPackageData config. | |
| # This exact invocation was validated on Firebase Test Lab: | |
| # Pixel 8 / API 34, 123/123 passed (~2 min on device). Adjust with: | |
| # gcloud firebase test android models list --filter="form=PHYSICAL" | |
| # --results-bucket: FTL's *default* results bucket is a | |
| # Firebase-managed bucket NOT governed by this project's IAM, so a | |
| # custom CI service account can never get storage.objects.create on | |
| # it (403 on upload, regardless of project-level storage roles). We | |
| # therefore write to a dedicated bucket we own in this project; the | |
| # SA has roles/storage.objectAdmin scoped to exactly that bucket. | |
| # Setup: gcloud storage buckets create gs://ksafe-ci-ftl-results | |
| # --project=ksafe-ci --location=US --uniform-bucket-level-access | |
| # gcloud storage buckets add-iam-policy-binding | |
| # gs://ksafe-ci-ftl-results | |
| # --member=serviceAccount:<the GCP_SA_KEY account> | |
| # --role=roles/storage.objectAdmin | |
| gcloud firebase test android run \ | |
| --project "$FIREBASE_PROJECT" \ | |
| --results-bucket=ksafe-ci-ftl-results \ | |
| --type instrumentation \ | |
| --app "$TEST_APK" \ | |
| --test "$TEST_APK" \ | |
| --device model=shiba,version=34,locale=en,orientation=portrait \ | |
| --use-orchestrator \ | |
| --timeout 20m \ | |
| --num-flaky-test-attempts 2 |