Merge pull request #554 from tryigit/bolt-rust-memory-optimizations-1… #1370
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: Build | |
| on: | |
| push: | |
| branches: [ "master" ] | |
| pull_request: | |
| branches: [ "master" ] | |
| workflow_dispatch: | |
| permissions: | |
| contents: write | |
| jobs: | |
| # ============================================================ | |
| # Phase 1: Safety & Quality Gate (runs first, gates everything) | |
| # ============================================================ | |
| safety-check: | |
| name: "🛡️ Safety & Quality Gate" | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: "🔒 SELinux Policy — Check semicolons" | |
| run: | | |
| echo "=== Checking sepolicy.rule for missing semicolons ===" | |
| ERRORS=0 | |
| LINE_NUM=0 | |
| while IFS= read -r line; do | |
| LINE_NUM=$((LINE_NUM + 1)) | |
| trimmed=$(echo "$line" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//') | |
| [ -z "$trimmed" ] && continue | |
| [[ "$trimmed" == \#* ]] && continue | |
| [[ "$trimmed" == type\ * ]] && continue | |
| if echo "$trimmed" | grep -qE '^(allow|dontaudit|neverallow|auditallow) '; then | |
| statement=$(echo "$trimmed" | sed 's/#.*//' | sed 's/[[:space:]]*$//') | |
| if [[ "$statement" != *";" ]]; then | |
| echo "::error file=module/template/sepolicy.rule,line=$LINE_NUM::Missing semicolon: $trimmed" | |
| ERRORS=$((ERRORS + 1)) | |
| fi | |
| fi | |
| done < module/template/sepolicy.rule | |
| if [ "$ERRORS" -gt 0 ]; then | |
| echo "::error::$ERRORS sepolicy statement(s) missing semicolons! This WILL break policy compilation on device." | |
| exit 1 | |
| fi | |
| echo "✅ All sepolicy statements properly terminated" | |
| - name: "🔑 Security — Check for insecure PRNG" | |
| run: | | |
| echo "=== Scanning for java.util.Random in production code ===" | |
| ERRORS=0 | |
| FILES=$(grep -rl "java\.util\.Random" --include="*.kt" --include="*.java" --exclude-dir="test" --exclude-dir="androidTest" service/ module/ 2>/dev/null || true) | |
| for f in $FILES; do | |
| LINES=$(grep -n "java\.util\.Random" "$f" | grep -iv "test" || true) | |
| if [ -n "$LINES" ]; then | |
| while IFS= read -r match; do | |
| LINE_NUM=$(echo "$match" | cut -d: -f1) | |
| echo "::error file=$f,line=$LINE_NUM::Insecure java.util.Random in production code. Use java.security.SecureRandom." | |
| ERRORS=$((ERRORS + 1)) | |
| done <<< "$LINES" | |
| fi | |
| done | |
| if [ "$ERRORS" -gt 0 ]; then | |
| exit 1 | |
| fi | |
| echo "✅ No insecure PRNG usage" | |
| - name: "🦀 Security — Check for unwrap/expect in Rust FFI" | |
| run: | | |
| echo "=== Scanning for unwrap()/expect() near FFI boundary ===" | |
| for f in $(find rust/ -name "*.rs" ! -path "*/test*"); do | |
| if grep -q 'extern "C"' "$f" 2>/dev/null; then | |
| MATCHES=$(awk ' | |
| /^#\[cfg\(test\)\]/ { in_test=1 } | |
| /^mod tests/ { in_test=1 } | |
| in_test && /^}$/ { in_test=0 } | |
| !in_test && /\.(unwrap|expect)\(/ { print NR": "$0 } | |
| ' "$f" 2>/dev/null || true) | |
| if [ -n "$MATCHES" ]; then | |
| while IFS= read -r match; do | |
| LINE_NUM=$(echo "$match" | cut -d: -f1) | |
| echo "::warning file=$f,line=$LINE_NUM::unwrap()/expect() found near FFI boundary" | |
| done <<< "$MATCHES" | |
| fi | |
| fi | |
| done | |
| echo "✅ FFI panic safety check complete" | |
| - name: "⚡ Performance — Check binder FD caching" | |
| run: | | |
| if grep -q 'if (is_binder)' module/src/main/cpp/binder_interceptor.cpp 2>/dev/null; then | |
| if ! grep -q 'g_binder_fds\[fd\] = is_binder' module/src/main/cpp/binder_interceptor.cpp 2>/dev/null; then | |
| echo "::warning file=module/src/main/cpp/binder_interceptor.cpp::is_binder_fd may not cache negative results" | |
| fi | |
| fi | |
| echo "✅ Performance check complete" | |
| - name: "📱 Real-World — Validate Magisk/KernelSU module structure" | |
| run: | | |
| ERRORS=0 | |
| for f in module.prop customize.sh service.sh post-fs-data.sh sepolicy.rule; do | |
| if [ ! -f "module/template/$f" ]; then | |
| echo "::error::Missing required module file: module/template/$f" | |
| ERRORS=$((ERRORS + 1)) | |
| fi | |
| done | |
| if [ -f "module/template/module.prop" ]; then | |
| for field in id name version versionCode author description; do | |
| if ! grep -q "^${field}=" "module/template/module.prop"; then | |
| echo "::error file=module/template/module.prop::Missing required field: $field" | |
| ERRORS=$((ERRORS + 1)) | |
| fi | |
| done | |
| fi | |
| if [ "$ERRORS" -gt 0 ]; then exit 1; fi | |
| echo "✅ Module structure valid" | |
| - name: "📱 Real-World — Validate shell scripts" | |
| run: | | |
| ERRORS=0 | |
| for script in module/template/action.sh module/template/customize.sh module/template/service.sh module/template/post-fs-data.sh; do | |
| if [ -f "$script" ]; then | |
| if ! bash -n "$script" 2>/dev/null; then | |
| echo "::error file=$script::Shell syntax error" | |
| ERRORS=$((ERRORS + 1)) | |
| fi | |
| fi | |
| done | |
| if [ "$ERRORS" -gt 0 ]; then exit 1; fi | |
| echo "✅ Shell scripts valid" | |
| - name: "📱 Real-World — Validate Rust static libraries" | |
| run: | | |
| ERRORS=0 | |
| for abi in arm64-v8a armeabi-v7a x86 x86_64; do | |
| LIB="module/src/main/cpp/external/rust_libs/$abi/libcleverestricky_cbor_cose.a" | |
| if [ ! -f "$LIB" ]; then | |
| echo "::warning::Missing Rust library for $abi" | |
| else | |
| SIZE=$(stat -c%s "$LIB" 2>/dev/null || stat -f%z "$LIB" 2>/dev/null) | |
| if [ "$SIZE" -lt 1000 ]; then | |
| echo "::error file=$LIB::Rust library suspiciously small ($SIZE bytes)" | |
| ERRORS=$((ERRORS + 1)) | |
| fi | |
| fi | |
| done | |
| if [ "$ERRORS" -gt 0 ]; then exit 1; fi | |
| echo "✅ Rust libraries valid" | |
| - name: "🔗 Real-World — Check Binder transaction code consistency" | |
| run: | | |
| CPP_PROP_CODE=$(grep -oP 'REGISTER_PROPERTY_SERVICE\s*=\s*\K\d+' module/src/main/cpp/binder_interceptor.cpp 2>/dev/null || echo "not_found") | |
| KT_PROP_CODE=$(grep -oP 'REGISTER_PROPERTY_SERVICE_TRANSACTION_CODE\s*=\s*\K\d+' service/src/main/java/cleveres/tricky/cleverestech/binder/BinderInterceptor.kt 2>/dev/null || echo "not_found") | |
| if [ "$CPP_PROP_CODE" != "not_found" ] && [ "$KT_PROP_CODE" != "not_found" ]; then | |
| if [ "$CPP_PROP_CODE" != "$KT_PROP_CODE" ]; then | |
| echo "::error::Binder transaction code mismatch! C++=$CPP_PROP_CODE vs Kotlin=$KT_PROP_CODE" | |
| exit 1 | |
| fi | |
| fi | |
| echo "✅ Binder codes consistent" | |
| - name: "🔄 Daemon — Check crash retry logic in service.sh" | |
| run: | | |
| ERRORS=0 | |
| SERVICE_SH="module/template/service.sh" | |
| # 1. Verify retry counter exists | |
| if ! grep -q 'FAIL_COUNT' "$SERVICE_SH"; then | |
| echo "::error file=$SERVICE_SH::Missing FAIL_COUNT retry counter — daemon will crash permanently on first non-zero exit" | |
| ERRORS=$((ERRORS + 1)) | |
| fi | |
| # 2. Verify max retries limit exists | |
| if ! grep -q 'MAX_FAILS' "$SERVICE_SH"; then | |
| echo "::error file=$SERVICE_SH::Missing MAX_FAILS limit — daemon has no retry cap" | |
| ERRORS=$((ERRORS + 1)) | |
| fi | |
| # 3. Verify sleep between retries to prevent rapid crash loops | |
| if ! grep -q 'sleep' "$SERVICE_SH"; then | |
| echo "::error file=$SERVICE_SH::Missing sleep between retries — daemon may enter rapid crash loop" | |
| ERRORS=$((ERRORS + 1)) | |
| fi | |
| # 4. Verify retry counter resets on clean exit | |
| if ! grep -q 'FAIL_COUNT=0' "$SERVICE_SH"; then | |
| echo "::error file=$SERVICE_SH::Retry counter never resets — daemon will eventually die even after successful runs" | |
| ERRORS=$((ERRORS + 1)) | |
| fi | |
| # 5. service.sh must not give up with a hard non-zero exit after MAX_FAILS | |
| if grep -q 'Max retries reached, giving up' "$SERVICE_SH"; then | |
| echo "::error file=$SERVICE_SH::service.sh still gives up after MAX_FAILS — this can trigger repeated boot-time failure loops" | |
| ERRORS=$((ERRORS + 1)) | |
| fi | |
| # 6. Verify daemon script exists and is a valid shell script | |
| DAEMON_SCRIPT="module/template/daemon" | |
| if [ ! -f "$DAEMON_SCRIPT" ]; then | |
| echo "::error::Missing daemon script: $DAEMON_SCRIPT" | |
| ERRORS=$((ERRORS + 1)) | |
| else | |
| # Verify it launches the Java service with app_process | |
| if ! grep -q 'app_process' "$DAEMON_SCRIPT"; then | |
| echo "::error file=$DAEMON_SCRIPT::Daemon script does not use app_process to launch Java service" | |
| ERRORS=$((ERRORS + 1)) | |
| fi | |
| # Verify exec is used (prevents zombie processes) | |
| if ! grep -q '^exec ' "$DAEMON_SCRIPT"; then | |
| echo "::error file=$DAEMON_SCRIPT::Daemon script should use exec to replace shell process" | |
| ERRORS=$((ERRORS + 1)) | |
| fi | |
| fi | |
| if [ "$ERRORS" -gt 0 ]; then exit 1; fi | |
| echo "✅ Daemon crash resilience checks passed" | |
| - name: "💀 Daemon — Check for unguarded exitProcess calls" | |
| run: | | |
| ERRORS=0 | |
| # Scan for exitProcess(1) calls that could cause permanent daemon death | |
| # exitProcess(0) is fine (clean restart), exitProcess(1) is dangerous | |
| FILES=$(grep -rl 'exitProcess(1)' --include="*.kt" service/ 2>/dev/null || true) | |
| EXIT1_COUNT=0 | |
| for f in $FILES; do | |
| LINES=$(grep -c 'exitProcess(1)' "$f" 2>/dev/null || echo "0") | |
| EXIT1_COUNT=$((EXIT1_COUNT + LINES)) | |
| done | |
| # Threshold: more than 3 exitProcess(1) calls indicates excessive hard exits | |
| # that increase the risk of daemon crash loops. Each call is a potential | |
| # permanent daemon death point if retry logic is insufficient. | |
| if [ "$EXIT1_COUNT" -gt 3 ]; then | |
| echo "::warning::Found $EXIT1_COUNT exitProcess(1) calls in service code. Each one can crash the daemon permanently if service.sh lacks retry logic." | |
| fi | |
| # Verify Main.kt doesn't have unguarded exitProcess | |
| if grep -q 'exitProcess' service/src/main/java/cleveres/tricky/cleverestech/Main.kt 2>/dev/null; then | |
| echo "::warning file=service/src/main/java/cleveres/tricky/cleverestech/Main.kt::Main.kt contains exitProcess — ensure daemon restart handles this" | |
| fi | |
| echo "✅ Daemon exit analysis complete (found $EXIT1_COUNT hard exit points)" | |
| - name: "🔄 Interceptor — Check retry counter in DRM/Telephony interceptors" | |
| run: | | |
| ERRORS=0 | |
| # DrmInterceptor: triedCount must be incremented when service not found | |
| # This prevents infinite retry loops that waste CPU | |
| DRM_FILE="service/src/main/java/cleveres/tricky/cleverestech/DrmInterceptor.kt" | |
| if [ -f "$DRM_FILE" ]; then | |
| # Count triedCount increments — must be at least 2 (service-not-found + injection-failed) | |
| INC_COUNT=$(grep -c 'triedCount += 1' "$DRM_FILE" 2>/dev/null || echo "0") | |
| if [ "$INC_COUNT" -lt 2 ]; then | |
| echo "::error file=$DRM_FILE::DrmInterceptor has only $INC_COUNT triedCount increments — needs at least 2 (service-not-found + injection-failed paths)" | |
| ERRORS=$((ERRORS + 1)) | |
| fi | |
| # Must have retry limit | |
| if ! grep -q 'triedCount >= 3' "$DRM_FILE" 2>/dev/null; then | |
| echo "::error file=$DRM_FILE::DrmInterceptor missing retry limit (triedCount >= 3)" | |
| ERRORS=$((ERRORS + 1)) | |
| fi | |
| fi | |
| TEL_FILE="service/src/main/java/cleveres/tricky/cleverestech/TelephonyInterceptor.kt" | |
| if [ -f "$TEL_FILE" ]; then | |
| if ! grep -q 'triedCount >= 3' "$TEL_FILE" 2>/dev/null; then | |
| echo "::error file=$TEL_FILE::TelephonyInterceptor missing retry limit" | |
| ERRORS=$((ERRORS + 1)) | |
| fi | |
| fi | |
| if [ "$ERRORS" -gt 0 ]; then exit 1; fi | |
| echo "✅ Interceptor retry logic valid" | |
| - name: "📝 SecureFile — Check write integrity" | |
| run: | | |
| ERRORS=0 | |
| SF_FILE="service/src/main/java/cleveres/tricky/cleverestech/util/SecureFile.kt" | |
| if [ -f "$SF_FILE" ]; then | |
| # Must detect partial writes (not silently succeed) | |
| if ! grep -q 'Incomplete write\|Partial write' "$SF_FILE" 2>/dev/null; then | |
| echo "::error file=$SF_FILE::SecureFile.writeBytes does not detect partial writes — silent data corruption risk" | |
| ERRORS=$((ERRORS + 1)) | |
| fi | |
| # Must use atomic rename | |
| if ! grep -q 'Os.rename' "$SF_FILE" 2>/dev/null; then | |
| echo "::error file=$SF_FILE::SecureFile missing Os.rename — non-atomic writes can corrupt config" | |
| ERRORS=$((ERRORS + 1)) | |
| fi | |
| # Must fsync before rename | |
| if ! grep -q 'Os.fsync' "$SF_FILE" 2>/dev/null; then | |
| echo "::error file=$SF_FILE::SecureFile missing Os.fsync — crash during write can lose data" | |
| ERRORS=$((ERRORS + 1)) | |
| fi | |
| fi | |
| if [ "$ERRORS" -gt 0 ]; then exit 1; fi | |
| echo "✅ SecureFile write integrity checks passed" | |
| - name: "🔢 Injection — Check PID bounds validation" | |
| run: | | |
| ERRORS=0 | |
| INJECT_FILE="module/src/main/cpp/inject/main.cpp" | |
| if [ -f "$INJECT_FILE" ]; then | |
| # Must check for INT_MAX to prevent integer overflow | |
| if ! grep -q 'INT_MAX' "$INJECT_FILE" 2>/dev/null; then | |
| echo "::error file=$INJECT_FILE::PID validation missing INT_MAX check — potential injection into wrong process" | |
| ERRORS=$((ERRORS + 1)) | |
| fi | |
| # Must include climits for INT_MAX | |
| if ! grep -q '<climits>' "$INJECT_FILE" 2>/dev/null; then | |
| echo "::error file=$INJECT_FILE::Missing #include <climits> for INT_MAX" | |
| ERRORS=$((ERRORS + 1)) | |
| fi | |
| fi | |
| if [ "$ERRORS" -gt 0 ]; then exit 1; fi | |
| echo "✅ Injection PID validation checks passed" | |
| - name: "🛡️ Security — Check for shell injection patterns" | |
| run: | | |
| # Scan for dangerous sh -c with string interpolation in production Kotlin code | |
| # Pattern: exec("cmd $variable") or arrayOf("sh", "-c", "cmd $var") | |
| # Safe pattern: Runtime.getRuntime().exec(arrayOf("cmd", arg1, arg2)) | |
| WARNINGS=0 | |
| for f in $(find service/src/main/java -name "*.kt" 2>/dev/null); do | |
| # Find lines with exec() that contain dollar-sign interpolation | |
| # Skip lines that are purely comments (start with //) | |
| while IFS= read -r match; do | |
| LINE_NUM=$(echo "$match" | cut -d: -f1) | |
| LINE_CONTENT=$(echo "$match" | cut -d: -f2-) | |
| # Skip pure comment lines | |
| TRIMMED=$(echo "$LINE_CONTENT" | sed 's/^[[:space:]]*//') | |
| if [[ "$TRIMMED" != //* ]]; then | |
| echo "::warning file=$f,line=$LINE_NUM::Potential shell injection: exec() with string interpolation. Use array-based exec instead." | |
| WARNINGS=$((WARNINGS + 1)) | |
| fi | |
| done < <(grep -n 'exec(".*\$' "$f" 2>/dev/null || true) | |
| done | |
| echo "✅ Shell injection pattern scan complete (found $WARNINGS warnings)" | |
| - name: "🔌 Process — Check for stream leaks in Runtime.exec" | |
| run: | | |
| # Files using Runtime.exec() in retry loops must drain process streams | |
| # to prevent FD exhaustion. This check warns when exec is used without | |
| # any stream handling in the same file. | |
| WARNINGS=0 | |
| for f in $(find service/src/main/java -name "*.kt" 2>/dev/null); do | |
| EXEC_COUNT=$(grep -c 'Runtime.getRuntime().exec' "$f" 2>/dev/null || echo "0") | |
| if [ "$EXEC_COUNT" -gt 0 ]; then | |
| STREAM_COUNT=$(grep -c 'inputStream\.\|errorStream\.\|ProcessBuilder' "$f" 2>/dev/null || echo "0") | |
| if [ "$STREAM_COUNT" -eq 0 ]; then | |
| echo "::warning file=$f::$EXEC_COUNT Runtime.exec() call(s) without any stream handling — potential FD leak" | |
| WARNINGS=$((WARNINGS + 1)) | |
| fi | |
| fi | |
| done | |
| echo "✅ Process stream leak scan complete (found $WARNINGS warnings)" | |
| # ============================================================ | |
| # Phase 2: Rust Tests (after safety check) | |
| # ============================================================ | |
| rust-test: | |
| needs: safety-check | |
| runs-on: ubuntu-latest | |
| env: | |
| RUSTFLAGS: "-D warnings" | |
| defaults: | |
| run: | |
| working-directory: rust/cbor-cose | |
| steps: | |
| - name: Check out | |
| uses: actions/checkout@v6 | |
| - name: Install Rust toolchain | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Cache cargo registry and build | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| rust/cbor-cose/target | |
| key: ${{ runner.os }}-cargo-${{ hashFiles('rust/cbor-cose/Cargo.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-cargo- | |
| - name: Run tests | |
| run: cargo test --verbose | |
| - name: Run tests (release mode) | |
| run: cargo test --release --verbose | |
| - name: Check formatting | |
| run: cargo fmt -- --check | |
| - name: Run clippy | |
| run: cargo clippy -- -D warnings | |
| # ============================================================ | |
| # Phase 3: Instrumentation Tests (after safety check) | |
| # ============================================================ | |
| instrumentation-test: | |
| needs: safety-check | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| api-level: [33] | |
| name: instrumentation-test (API ${{ matrix.api-level }}) | |
| steps: | |
| - name: Enable KVM | |
| 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 | |
| - name: Check out | |
| uses: actions/checkout@v6 | |
| with: | |
| submodules: "recursive" | |
| fetch-depth: 0 | |
| - name: Setup Java | |
| uses: actions/setup-java@v5 | |
| with: | |
| distribution: temurin | |
| java-version: 21 | |
| - name: Setup Gradle | |
| uses: gradle/actions/setup-gradle@v6 | |
| with: | |
| gradle-home-cache-cleanup: true | |
| - name: Setup Android SDK | |
| uses: android-actions/setup-android@v4 | |
| with: | |
| packages: '' | |
| - name: Download KeyAttestation APK | |
| run: wget -q https://github.com/vvb2060/KeyAttestation/releases/download/v1.8.4/KeyAttestation-v1.8.4.apk -O KeyAttestation.apk | |
| - name: Build Module | |
| run: ./gradlew :module:zipDebug | |
| - name: Run E2E Integration Tests (Magisk) | |
| uses: reactivecircus/android-emulator-runner@v2 | |
| with: | |
| api-level: ${{ matrix.api-level }} | |
| target: google_apis_playstore | |
| arch: x86_64 | |
| force-avd-creation: false | |
| emulator-options: -no-window -gpu auto -no-snapshot -noaudio -no-boot-anim -camera-back none | |
| disable-animations: true | |
| script: | | |
| echo "Installing Magisk via rootAVD..." | |
| git clone https://github.com/newbit1/rootAVD.git | |
| rm -rf rootAVD/.git | |
| (cd rootAVD && ./rootAVD.sh system-images/android-33/google_apis_playstore/x86_64/ramdisk.img) | |
| adb wait-for-device | |
| echo "Pushing and Installing CleveresTricky Module..." | |
| MODULE_ZIP="$(ls module/release/*-debug.zip 2>/dev/null | head -n 1)" | |
| if [ -z "$MODULE_ZIP" ]; then | |
| echo "Error: debug module zip not found" | |
| exit 1 | |
| fi | |
| adb push "$MODULE_ZIP" /data/local/tmp/module.zip | |
| if ! adb shell "su -c 'magisk --install-module /data/local/tmp/module.zip'"; then | |
| echo "Installation via su failed, attempting direct magisk command..." | |
| adb shell "magisk --install-module /data/local/tmp/module.zip" | |
| fi | |
| adb reboot | |
| adb wait-for-device | |
| echo "Downloading Test APKs..." | |
| wget -q https://github.com/vvb2060/KeyAttestation/releases/download/v1.8.4/KeyAttestation-v1.8.4.apk -O KeyAttestation.apk | |
| wget -q https://github.com/herzhenr/spic-android/releases/download/v1.4.0/spic-v1.4.0.apk -O spic.apk | |
| echo "Installing Test APKs..." | |
| adb install -r KeyAttestation.apk | |
| adb install -r spic.apk | |
| echo "Starting Test Apps..." | |
| adb logcat -c | |
| adb shell am start -n io.github.vvb2060.keyattestation/.home.HomeActivity | |
| adb shell am start -n com.henrikherzig.playintegritychecker/.MainActivity | |
| echo "Waiting for logs to generate..." | |
| sleep 15 | |
| adb logcat -d > logcat.txt | |
| echo "Verifying CleveresTricky daemon..." | |
| if ! grep -qi "CleveresTricky daemon" logcat.txt; then | |
| echo "Error: CleveresTricky daemon not found in logcat!" | |
| cat logcat.txt | |
| exit 1 | |
| fi | |
| echo "Verifying Adaptive Binder Interceptor..." | |
| if ! grep -qi "Adaptive Binder Interceptor" logcat.txt; then | |
| echo "Error: Adaptive Binder Interceptor not found in logcat!" | |
| cat logcat.txt | |
| exit 1 | |
| fi | |
| echo "Checking Play Integrity Results..." | |
| if grep -qi "MEETS_DEVICE_INTEGRITY" logcat.txt; then | |
| echo "Play Integrity: MEETS_DEVICE_INTEGRITY" | |
| else | |
| echo "Emulator detected, hardware keystore missing - skipping strict Integrity Check" | |
| fi | |
| echo "E2E Integration Test Passed!" | |
| emulator-boot-timeout: 1200 | |
| env: | |
| ANDROID_EMULATOR_WAIT_TIME_BEFORE_KILL: 60 | |
| # ============================================================ | |
| # Phase 4: Build & Release (after Rust tests pass) | |
| # ============================================================ | |
| build: | |
| needs: rust-test | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Check out | |
| uses: actions/checkout@v6 | |
| with: | |
| submodules: "recursive" | |
| fetch-depth: 0 | |
| - name: Setup Java | |
| uses: actions/setup-java@v5 | |
| with: | |
| distribution: temurin | |
| java-version: 21 | |
| - name: Setup Gradle | |
| uses: gradle/actions/setup-gradle@v6 | |
| with: | |
| gradle-home-cache-cleanup: true | |
| - name: Setup Android SDK | |
| uses: android-actions/setup-android@v4 | |
| with: | |
| packages: '' | |
| - name: Run Module Tests | |
| run: ./gradlew :module:testDebugUnitTest | |
| - name: Run Unit Tests | |
| run: ./gradlew testDebugUnitTest | |
| - name: Build with Gradle | |
| run: | | |
| ./gradlew zipRelease | |
| ./gradlew zipDebug | |
| ./gradlew :encryptor-app:assembleRelease | |
| - name: Prepare artifact | |
| if: success() | |
| id: prepareArtifact | |
| run: | | |
| releasePath=$(ls module/release/*-release.zip) | |
| releaseName=$(basename "$releasePath" .zip) | |
| echo "releaseName=$releaseName" >> $GITHUB_OUTPUT | |
| debugPath=$(ls module/release/*-debug.zip) | |
| debugName=$(basename "$debugPath" .zip) | |
| echo "debugName=$debugName" >> $GITHUB_OUTPUT | |
| unzip "$releasePath" -d module-release | |
| unzip "$debugPath" -d module-debug | |
| - name: Upload release | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: ${{ steps.prepareArtifact.outputs.releaseName }} | |
| path: "./module-release/*" | |
| - name: Upload debug | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: ${{ steps.prepareArtifact.outputs.debugName }} | |
| path: "./module-debug/*" | |
| - name: Upload release mappings | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: release-mappings | |
| path: "./service/build/outputs/mapping/release" | |
| - name: Upload Encryptor App | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: EncryptorApp | |
| path: "./encryptor-app/build/outputs/apk/release/*.apk" | |
| - name: Check for Release | |
| if: success() && github.event_name == 'push' && github.ref == 'refs/heads/master' | |
| id: check_release | |
| run: | | |
| VERSION=$(grep 'val verName by extra' build.gradle.kts | cut -d'"' -f2 | sed 's/^[vV]//') | |
| echo "VERSION=$VERSION" >> $GITHUB_ENV | |
| if git ls-remote --exit-code --tags origin "refs/tags/V$VERSION" >/dev/null 2>&1; then | |
| echo "Tag V$VERSION already exists." | |
| echo "create_release=false" >> $GITHUB_OUTPUT | |
| else | |
| echo "Tag V$VERSION does not exist." | |
| echo "create_release=true" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Create Tag | |
| if: steps.check_release.outputs.create_release == 'true' | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git tag "V${{ env.VERSION }}" | |
| git push origin "V${{ env.VERSION }}" | |
| - name: Generate Changelog | |
| if: steps.check_release.outputs.create_release == 'true' | |
| id: changelog | |
| run: | | |
| PREV_TAG=$(git describe --tags --abbrev=0 "V${{ env.VERSION }}^" 2>/dev/null || echo "") | |
| if [ -z "$PREV_TAG" ]; then | |
| CHANGELOG=$(git log --pretty=format:"* %s (%h)" "V${{ env.VERSION }}") | |
| else | |
| CHANGELOG=$(git log --pretty=format:"* %s (%h)" "$PREV_TAG..V${{ env.VERSION }}") | |
| fi | |
| echo "changelog<<EOF" >> $GITHUB_OUTPUT | |
| echo "$CHANGELOG" >> $GITHUB_OUTPUT | |
| echo "EOF" >> $GITHUB_OUTPUT | |
| - name: Create Release | |
| if: steps.check_release.outputs.create_release == 'true' | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: "V${{ env.VERSION }}" | |
| name: "Release V${{ env.VERSION }}" | |
| body: ${{ steps.changelog.outputs.changelog }} | |
| files: | | |
| module/release/*-release.zip | |
| module/release/*-debug.zip | |
| encryptor-app/build/outputs/apk/release/*.apk |