Skip to content

CI

CI #61

Workflow file for this run

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