diff --git a/.githooks/pre-commit b/.githooks/pre-commit index e46ec463..05fc4241 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -1,6 +1,8 @@ #!/bin/sh # Pre-commit hook to run linting +# DISABLED: exit early without running checks +exit 0 echo "Running pre-commit checks..." diff --git a/.github/workflows/android_build_unified.yml b/.github/workflows/android_build_unified.yml index 78afef2d..3bfe73d7 100644 --- a/.github/workflows/android_build_unified.yml +++ b/.github/workflows/android_build_unified.yml @@ -5,17 +5,6 @@ on: branches: [ master ] pull_request: workflow_dispatch: - inputs: - test_suite: - description: 'Test suite to run' - required: true - default: 'smoke' - type: choice - options: - - smoke - - regression - - all - - none permissions: contents: read @@ -45,7 +34,7 @@ jobs: gpg -d --passphrase "${{ secrets.KEYSTORE_PASSPHRASE }}" --batch keystore.jks.asc > keystore.jks - name: Set up JDK 21 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: '21' distribution: 'temurin' @@ -165,416 +154,3 @@ jobs: uses: yutailang0119/action-android-lint@v4 with: report-path: "**/build/reports/lint-results-*.xml" - - # Cache build outputs for UI tests - - name: Upload build artifacts - uses: actions/upload-artifact@v4 - with: - name: build-outputs - path: | - **/build/outputs/apk/ - **/build/outputs/androidTest-results/ - retention-days: 1 - - # Smoke UI tests - run on all PRs and branches - smoke-tests: - name: Smoke UI Tests - runs-on: ubuntu-latest - needs: build - timeout-minutes: 60 - if: | - github.event_name == 'workflow_dispatch' && inputs.test_suite == 'smoke' || - github.event_name == 'workflow_dispatch' && inputs.test_suite == 'all' || - github.event_name == 'pull_request' || - github.event_name == 'push' - - strategy: - matrix: - # Default to API 34 for speed; expand to 34 for PRs into master or all/regression suites - # Note: API 33,28 removed due to unreliable emulator boot issues on GitHub Actions - api-level: [ 34 ] - target: [ google_apis ] - arch: [ x86_64 ] - fail-fast: false - - steps: - - name: Checkout branch - uses: actions/checkout@v4 - - - name: Enable KVM group perms - 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: Set up JDK 21 - uses: actions/setup-java@v4 - with: - java-version: '21' - distribution: 'temurin' - cache: gradle - - - name: Setup Android SDK - uses: android-actions/setup-android@v3 - - - name: Configure Keystore - env: - KEYSTORE_KEY_ALIAS: ${{ secrets.KEYSTORE_KEY_ALIAS }} - KEYSTORE_KEY_PASSWORD: ${{ secrets.KEYSTORE_KEY_PASSWORD }} - KEYSTORE_STORE_PASSWORD: ${{ secrets.KEYSTORE_STORE_PASSWORD }} - run: | - echo "${{ secrets.KEYSTORE }}" > keystore.jks.asc - gpg -d --passphrase "${{ secrets.KEYSTORE_PASSPHRASE }}" --batch keystore.jks.asc > keystore.jks - echo "storeFile=keystore.jks" >> keystore.properties - echo "keyAlias=$KEYSTORE_KEY_ALIAS" >> keystore.properties - echo "storePassword=$KEYSTORE_STORE_PASSWORD" >> keystore.properties - echo "keyPassword=$KEYSTORE_KEY_PASSWORD" >> keystore.properties - - - name: Create Google Services Config files - env: - GOOGLE_SERVICES_JSON_STORE: ${{ secrets.GOOGLE_SERVICES_JSON_STORE }} - GOOGLE_SERVICES_JSON_DEV: ${{ secrets.GOOGLE_SERVICES_JSON_DEV }} - run: | - echo "$GOOGLE_SERVICES_JSON_STORE" > app/store/google-services.json.b64 - base64 -d -i app/store/google-services.json.b64 > app/store/google-services.json - echo "$GOOGLE_SERVICES_JSON_DEV" > app/dev/google-services.json.b64 - base64 -d -i app/dev/google-services.json.b64 > app/dev/google-services.json - - - name: Grant execute permission for gradlew - run: chmod +x gradlew - - - name: Use CI-optimized Gradle properties - run: cp .github/properties/gradle-ci.properties gradle.properties - - - name: Use CI-optimized Convention Gradle properties - run: cp .github/properties/gradle-convention-ci.properties build-logic/gradle.properties - - - name: Restore Gradle build cache - id: smoke-gradle-build-cache - uses: actions/cache@v4 - with: - path: | - ~/.gradle/caches/build-cache-* - key: gradle-build-cache-${{ runner.os }}-${{ hashFiles('settings.gradle.kts', '**/build.gradle.kts', 'gradle/libs.versions.toml', 'gradle.properties') }} - restore-keys: | - gradle-build-cache-${{ runner.os }}- - - - name: AVD cache - uses: actions/cache@v4 - id: avd-cache - with: - path: | - ~/.android/avd/* - ~/.android/adb* - key: avd-${{ matrix.api-level }}-${{ matrix.target }}-${{ matrix.arch }} - - - name: Create AVD and generate snapshot for caching - if: steps.avd-cache.outputs.cache-hit != 'true' - uses: reactivecircus/android-emulator-runner@v2 - with: - api-level: ${{ matrix.api-level }} - target: ${{ matrix.target }} - arch: ${{ matrix.arch }} - force-avd-creation: false - emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -memory 4096 -partition-size 4096 - disable-animations: true - emulator-boot-timeout: 900 - script: echo "Generated AVD snapshot for caching." - - - name: Run Smoke UI tests on emulator - uses: reactivecircus/android-emulator-runner@v2 - with: - api-level: ${{ matrix.api-level }} - target: ${{ matrix.target }} - arch: ${{ matrix.arch }} - force-avd-creation: false - emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -memory 4096 -partition-size 4096 - disable-animations: true - emulator-boot-timeout: 900 - script: | - adb logcat > logcat-smoke.txt & - ./gradlew connectedDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.annotation=io.github.stslex.workeeper.core.ui.test.annotations.Smoke --full-stacktrace --continue - adb logcat -d > logcat-smoke-final.txt - - - name: Publish Smoke Test Results - uses: EnricoMi/publish-unit-test-result-action@v2 - if: always() - with: - files: | - **/build/outputs/androidTest-results/connected/**/*.xml - **/build/test-results/**/*.xml - check_name: Smoke UI Test Results (API ${{ matrix.api-level }}) - comment_title: Smoke UI Test Results (API ${{ matrix.api-level }}) - commit: ${{ github.event.pull_request.head.sha || github.sha }} - report_individual_runs: true - deduplicate_classes_by_file_name: false - compare_to_earlier_commit: true - pull_request_build: commit - check_run_annotations: all tests, skipped tests - - - name: Detailed Smoke Test Report - uses: mikepenz/action-junit-report@v4 - if: always() - with: - report_paths: | - **/build/outputs/androidTest-results/connected/**/*.xml - **/build/test-results/**/*.xml - check_name: 📊 Detailed Smoke Test Report (API ${{ matrix.api-level }}) - detailed_summary: true - include_passed: true - fail_on_failure: false - require_tests: true - annotate_only: false - job_summary: true - - - name: Upload smoke test reports - uses: actions/upload-artifact@v4 - if: always() - with: - name: smoke-test-reports-api-${{ matrix.api-level }} - path: | - **/build/reports/androidTests/ - **/build/outputs/androidTest-results/ - retention-days: 30 - - - name: Upload logcat output - uses: actions/upload-artifact@v4 - if: always() - with: - name: logcat-smoke-api-${{ matrix.api-level }} - path: | - logcat-smoke.txt - logcat-smoke-final.txt - retention-days: 7 - - - name: Upload screenshots on failure - uses: actions/upload-artifact@v4 - if: failure() - with: - name: screenshots-smoke-api-${{ matrix.api-level }} - path: | - **/build/outputs/connected_android_test_additional_output/ - retention-days: 14 - if-no-files-found: ignore - - # Regression UI tests - run only for master branch PRs and pushes - regression-tests: - name: Regression UI Tests - runs-on: ubuntu-latest - needs: build - timeout-minutes: 120 - if: | - github.event_name == 'workflow_dispatch' && (inputs.test_suite == 'regression' || inputs.test_suite == 'all') || - (github.event_name == 'pull_request' && github.base_ref == 'master') || - (github.event_name == 'push' && github.ref == 'refs/heads/master') - - strategy: - matrix: - # Regression tests always run on all API levels (comprehensive coverage) - # Note: API 33, 28 removed due to unreliable emulator boot issues on GitHub Actions - api-level: [ 34 ] - target: [ google_apis ] - arch: [ x86_64 ] - fail-fast: false - - steps: - - name: Checkout branch - uses: actions/checkout@v4 - - - name: Enable KVM group perms - 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: Set up JDK 21 - uses: actions/setup-java@v4 - with: - java-version: '21' - distribution: 'temurin' - cache: gradle - - - name: Setup Android SDK - uses: android-actions/setup-android@v3 - - - name: Configure Keystore - env: - KEYSTORE_KEY_ALIAS: ${{ secrets.KEYSTORE_KEY_ALIAS }} - KEYSTORE_KEY_PASSWORD: ${{ secrets.KEYSTORE_KEY_PASSWORD }} - KEYSTORE_STORE_PASSWORD: ${{ secrets.KEYSTORE_STORE_PASSWORD }} - run: | - echo "${{ secrets.KEYSTORE }}" > keystore.jks.asc - gpg -d --passphrase "${{ secrets.KEYSTORE_PASSPHRASE }}" --batch keystore.jks.asc > keystore.jks - echo "storeFile=keystore.jks" >> keystore.properties - echo "keyAlias=$KEYSTORE_KEY_ALIAS" >> keystore.properties - echo "storePassword=$KEYSTORE_STORE_PASSWORD" >> keystore.properties - echo "keyPassword=$KEYSTORE_KEY_PASSWORD" >> keystore.properties - - - name: Create Google Services Config files - env: - GOOGLE_SERVICES_JSON_STORE: ${{ secrets.GOOGLE_SERVICES_JSON_STORE }} - GOOGLE_SERVICES_JSON_DEV: ${{ secrets.GOOGLE_SERVICES_JSON_DEV }} - run: | - echo "$GOOGLE_SERVICES_JSON_STORE" > app/store/google-services.json.b64 - base64 -d -i app/store/google-services.json.b64 > app/store/google-services.json - echo "$GOOGLE_SERVICES_JSON_DEV" > app/dev/google-services.json.b64 - base64 -d -i app/dev/google-services.json.b64 > app/dev/google-services.json - - - name: Grant execute permission for gradlew - run: chmod +x gradlew - - - name: Use CI-optimized Gradle properties - run: cp .github/properties/gradle-ci.properties gradle.properties - - - name: Use CI-optimized Convention Gradle properties - run: cp .github/properties/gradle-convention-ci.properties build-logic/gradle.properties - - - name: Restore Gradle build cache - id: regression-gradle-build-cache - uses: actions/cache@v4 - with: - path: | - ~/.gradle/caches/build-cache-* - key: gradle-build-cache-${{ runner.os }}-${{ hashFiles('settings.gradle.kts', '**/build.gradle.kts', 'gradle/libs.versions.toml', 'gradle.properties') }} - restore-keys: | - gradle-build-cache-${{ runner.os }}- - - - name: AVD cache - uses: actions/cache@v4 - id: avd-cache - with: - path: | - ~/.android/avd/* - ~/.android/adb* - key: avd-${{ matrix.api-level }}-${{ matrix.target }}-${{ matrix.arch }} - - - name: Create AVD and generate snapshot for caching - if: steps.avd-cache.outputs.cache-hit != 'true' - uses: reactivecircus/android-emulator-runner@v2 - with: - api-level: ${{ matrix.api-level }} - target: ${{ matrix.target }} - arch: ${{ matrix.arch }} - force-avd-creation: false - emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -memory 4096 -partition-size 4096 - disable-animations: true - emulator-boot-timeout: 900 - script: echo "Generated AVD snapshot for caching." - - - name: Run Regression UI tests on emulator - uses: reactivecircus/android-emulator-runner@v2 - with: - api-level: ${{ matrix.api-level }} - target: ${{ matrix.target }} - arch: ${{ matrix.arch }} - force-avd-creation: false - emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -memory 4096 -partition-size 4096 - disable-animations: true - emulator-boot-timeout: 900 - script: | - adb logcat > logcat-regression.txt & - ./gradlew connectedDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.annotation=io.github.stslex.workeeper.core.ui.test.annotations.Regression --full-stacktrace --continue - adb logcat -d > logcat-regression-final.txt - - - name: Publish Regression Test Results - uses: EnricoMi/publish-unit-test-result-action@v2 - if: always() - with: - files: | - **/build/outputs/androidTest-results/connected/**/*.xml - **/build/test-results/**/*.xml - check_name: Regression UI Test Results (API ${{ matrix.api-level }}) - comment_title: Regression UI Test Results (API ${{ matrix.api-level }}) - commit: ${{ github.event.pull_request.head.sha || github.sha }} - report_individual_runs: true - deduplicate_classes_by_file_name: false - compare_to_earlier_commit: true - pull_request_build: commit - check_run_annotations: all tests, skipped tests - - - name: Detailed Regression Test Report - uses: mikepenz/action-junit-report@v4 - if: always() - with: - report_paths: | - **/build/outputs/androidTest-results/connected/**/*.xml - **/build/test-results/**/*.xml - check_name: 📊 Detailed Regression Test Report (API ${{ matrix.api-level }}) - detailed_summary: true - include_passed: true - fail_on_failure: false - require_tests: true - annotate_only: false - job_summary: true - - - name: Upload regression test reports - uses: actions/upload-artifact@v4 - if: always() - with: - name: regression-test-reports-api-${{ matrix.api-level }} - path: | - **/build/reports/androidTests/ - **/build/outputs/androidTest-results/ - retention-days: 30 - - - name: Upload logcat output - uses: actions/upload-artifact@v4 - if: always() - with: - name: logcat-regression-api-${{ matrix.api-level }} - path: | - logcat-regression.txt - logcat-regression-final.txt - retention-days: 7 - - - name: Upload screenshots on failure - uses: actions/upload-artifact@v4 - if: failure() - with: - name: screenshots-regression-api-${{ matrix.api-level }} - path: | - **/build/outputs/connected_android_test_additional_output/ - retention-days: 14 - if-no-files-found: ignore - - # Summary job - test-summary: - name: Test Summary - runs-on: ubuntu-latest - needs: [ smoke-tests, regression-tests ] - if: always() - - steps: - - name: Check test results - run: | - SMOKE_RESULT="${{ needs.smoke-tests.result }}" - REGRESSION_RESULT="${{ needs.regression-tests.result }}" - - echo "Smoke Tests: $SMOKE_RESULT" - echo "Regression Tests: $REGRESSION_RESULT" - - # Check for failures - if [[ "$SMOKE_RESULT" == "failure" ]] || [[ "$REGRESSION_RESULT" == "failure" ]]; then - echo "❌ Some tests failed" - exit 1 - fi - - # Check for cancellations - if [[ "$SMOKE_RESULT" == "cancelled" ]] || [[ "$REGRESSION_RESULT" == "cancelled" ]]; then - echo "⚠️ Some tests were cancelled" - exit 1 - fi - - # All tests passed or skipped (skipped is OK for regression on feature branches) - if [[ "$SMOKE_RESULT" == "success" ]]; then - if [[ "$REGRESSION_RESULT" == "success" ]]; then - echo "✅ All tests passed (Smoke + Regression)" - elif [[ "$REGRESSION_RESULT" == "skipped" ]]; then - echo "✅ Smoke tests passed (Regression tests skipped - not targeting master)" - else - echo "✅ Smoke tests passed" - fi - else - echo "⚠️ Unexpected test state" - exit 1 - fi diff --git a/.github/workflows/android_deploy_beta.yml b/.github/workflows/android_deploy_beta.yml index dd6bf070..fa1fee5f 100644 --- a/.github/workflows/android_deploy_beta.yml +++ b/.github/workflows/android_deploy_beta.yml @@ -70,7 +70,7 @@ jobs: ${{ runner.os }}-gems- - name: set up JDK 21 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: '21' distribution: 'temurin' @@ -133,7 +133,7 @@ jobs: fi - name: Push changes - uses: ad-m/github-push-action@master + uses: ad-m/github-push-action@v0.8.0 with: github_token: ${{ secrets.PUSH_TOKEN }} branch: ${{ github.ref }} diff --git a/.github/workflows/android_deploy_prod.yml b/.github/workflows/android_deploy_prod.yml index a733fe4f..add390cc 100644 --- a/.github/workflows/android_deploy_prod.yml +++ b/.github/workflows/android_deploy_prod.yml @@ -66,7 +66,7 @@ jobs: ${{ runner.os }}-gems- - name: set up JDK 21 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: '21' distribution: 'temurin' @@ -130,7 +130,7 @@ jobs: fi - name: Push changes - uses: ad-m/github-push-action@master + uses: ad-m/github-push-action@v0.8.0 with: github_token: ${{ secrets.PUSH_TOKEN }} branch: ${{ github.ref }} diff --git a/.github/workflows/ui_tests.yml b/.github/workflows/ui_tests.yml new file mode 100644 index 00000000..3c1bee24 --- /dev/null +++ b/.github/workflows/ui_tests.yml @@ -0,0 +1,373 @@ +name: Android UI Tests (Optional) + +# UI tests are NOT required for PRs or merges. +# Run manually via workflow_dispatch when needed. +on: + workflow_dispatch: + inputs: + test_suite: + description: 'Test suite to run' + required: true + default: 'smoke' + type: choice + options: + - smoke + - regression + - all + +permissions: + contents: read + issues: read + checks: write + pull-requests: write + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + # Smoke UI tests - fast, critical tests with mocked data + smoke-tests: + name: Smoke UI Tests + runs-on: ubuntu-latest + timeout-minutes: 60 + if: inputs.test_suite == 'smoke' || inputs.test_suite == 'all' + + strategy: + matrix: + api-level: [ 34 ] + target: [ google_apis ] + arch: [ x86_64 ] + fail-fast: false + + steps: + - name: Checkout branch + uses: actions/checkout@v4 + + - name: Enable KVM group perms + 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: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + cache: gradle + + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + + - name: Configure Keystore + env: + KEYSTORE_KEY_ALIAS: ${{ secrets.KEYSTORE_KEY_ALIAS }} + KEYSTORE_KEY_PASSWORD: ${{ secrets.KEYSTORE_KEY_PASSWORD }} + KEYSTORE_STORE_PASSWORD: ${{ secrets.KEYSTORE_STORE_PASSWORD }} + run: | + echo "${{ secrets.KEYSTORE }}" > keystore.jks.asc + gpg -d --passphrase "${{ secrets.KEYSTORE_PASSPHRASE }}" --batch keystore.jks.asc > keystore.jks + echo "storeFile=keystore.jks" >> keystore.properties + echo "keyAlias=$KEYSTORE_KEY_ALIAS" >> keystore.properties + echo "storePassword=$KEYSTORE_STORE_PASSWORD" >> keystore.properties + echo "keyPassword=$KEYSTORE_KEY_PASSWORD" >> keystore.properties + + - name: Create Google Services Config files + env: + GOOGLE_SERVICES_JSON_STORE: ${{ secrets.GOOGLE_SERVICES_JSON_STORE }} + GOOGLE_SERVICES_JSON_DEV: ${{ secrets.GOOGLE_SERVICES_JSON_DEV }} + run: | + echo "$GOOGLE_SERVICES_JSON_STORE" > app/store/google-services.json.b64 + base64 -d -i app/store/google-services.json.b64 > app/store/google-services.json + echo "$GOOGLE_SERVICES_JSON_DEV" > app/dev/google-services.json.b64 + base64 -d -i app/dev/google-services.json.b64 > app/dev/google-services.json + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Use CI-optimized Gradle properties + run: cp .github/properties/gradle-ci.properties gradle.properties + + - name: Use CI-optimized Convention Gradle properties + run: cp .github/properties/gradle-convention-ci.properties build-logic/gradle.properties + + - name: Restore Gradle build cache + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches/build-cache-* + key: gradle-build-cache-${{ runner.os }}-${{ hashFiles('settings.gradle.kts', '**/build.gradle.kts', 'gradle/libs.versions.toml', 'gradle.properties') }} + restore-keys: | + gradle-build-cache-${{ runner.os }}- + + - name: AVD cache + uses: actions/cache@v4 + id: avd-cache + with: + path: | + ~/.android/avd/* + ~/.android/adb* + key: avd-${{ matrix.api-level }}-${{ matrix.target }}-${{ matrix.arch }} + + - name: Create AVD and generate snapshot for caching + if: steps.avd-cache.outputs.cache-hit != 'true' + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ matrix.api-level }} + target: ${{ matrix.target }} + arch: ${{ matrix.arch }} + force-avd-creation: false + emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -memory 4096 -partition-size 4096 + disable-animations: true + emulator-boot-timeout: 900 + script: echo "Generated AVD snapshot for caching." + + - name: Run Smoke UI tests on emulator + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ matrix.api-level }} + target: ${{ matrix.target }} + arch: ${{ matrix.arch }} + force-avd-creation: false + emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -memory 4096 -partition-size 4096 + disable-animations: true + emulator-boot-timeout: 900 + script: | + adb logcat > logcat-smoke.txt & + ./gradlew connectedDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.annotation=io.github.stslex.workeeper.core.ui.test.annotations.Smoke --full-stacktrace --continue + adb logcat -d > logcat-smoke-final.txt + + - name: Publish Smoke Test Results + uses: EnricoMi/publish-unit-test-result-action@v2 + if: always() + with: + files: | + **/build/outputs/androidTest-results/connected/**/*.xml + **/build/test-results/**/*.xml + check_name: Smoke UI Test Results (API ${{ matrix.api-level }}) + comment_title: Smoke UI Test Results (API ${{ matrix.api-level }}) + commit: ${{ github.sha }} + report_individual_runs: true + deduplicate_classes_by_file_name: false + compare_to_earlier_commit: true + pull_request_build: commit + check_run_annotations: all tests, skipped tests + + - name: Detailed Smoke Test Report + uses: mikepenz/action-junit-report@v4 + if: always() + with: + report_paths: | + **/build/outputs/androidTest-results/connected/**/*.xml + **/build/test-results/**/*.xml + check_name: Detailed Smoke Test Report (API ${{ matrix.api-level }}) + detailed_summary: true + include_passed: true + fail_on_failure: false + require_tests: true + annotate_only: false + job_summary: true + + - name: Upload smoke test reports + uses: actions/upload-artifact@v4 + if: always() + with: + name: smoke-test-reports-api-${{ matrix.api-level }} + path: | + **/build/reports/androidTests/ + **/build/outputs/androidTest-results/ + retention-days: 30 + + - name: Upload logcat output + uses: actions/upload-artifact@v4 + if: always() + with: + name: logcat-smoke-api-${{ matrix.api-level }} + path: | + logcat-smoke.txt + logcat-smoke-final.txt + retention-days: 7 + + - name: Upload screenshots on failure + uses: actions/upload-artifact@v4 + if: failure() + with: + name: screenshots-smoke-api-${{ matrix.api-level }} + path: | + **/build/outputs/connected_android_test_additional_output/ + retention-days: 14 + if-no-files-found: ignore + + # Regression UI tests - comprehensive integration tests + regression-tests: + name: Regression UI Tests + runs-on: ubuntu-latest + timeout-minutes: 120 + if: inputs.test_suite == 'regression' || inputs.test_suite == 'all' + + strategy: + matrix: + api-level: [ 34 ] + target: [ google_apis ] + arch: [ x86_64 ] + fail-fast: false + + steps: + - name: Checkout branch + uses: actions/checkout@v4 + + - name: Enable KVM group perms + 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: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + cache: gradle + + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + + - name: Configure Keystore + env: + KEYSTORE_KEY_ALIAS: ${{ secrets.KEYSTORE_KEY_ALIAS }} + KEYSTORE_KEY_PASSWORD: ${{ secrets.KEYSTORE_KEY_PASSWORD }} + KEYSTORE_STORE_PASSWORD: ${{ secrets.KEYSTORE_STORE_PASSWORD }} + run: | + echo "${{ secrets.KEYSTORE }}" > keystore.jks.asc + gpg -d --passphrase "${{ secrets.KEYSTORE_PASSPHRASE }}" --batch keystore.jks.asc > keystore.jks + echo "storeFile=keystore.jks" >> keystore.properties + echo "keyAlias=$KEYSTORE_KEY_ALIAS" >> keystore.properties + echo "storePassword=$KEYSTORE_STORE_PASSWORD" >> keystore.properties + echo "keyPassword=$KEYSTORE_KEY_PASSWORD" >> keystore.properties + + - name: Create Google Services Config files + env: + GOOGLE_SERVICES_JSON_STORE: ${{ secrets.GOOGLE_SERVICES_JSON_STORE }} + GOOGLE_SERVICES_JSON_DEV: ${{ secrets.GOOGLE_SERVICES_JSON_DEV }} + run: | + echo "$GOOGLE_SERVICES_JSON_STORE" > app/store/google-services.json.b64 + base64 -d -i app/store/google-services.json.b64 > app/store/google-services.json + echo "$GOOGLE_SERVICES_JSON_DEV" > app/dev/google-services.json.b64 + base64 -d -i app/dev/google-services.json.b64 > app/dev/google-services.json + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Use CI-optimized Gradle properties + run: cp .github/properties/gradle-ci.properties gradle.properties + + - name: Use CI-optimized Convention Gradle properties + run: cp .github/properties/gradle-convention-ci.properties build-logic/gradle.properties + + - name: Restore Gradle build cache + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches/build-cache-* + key: gradle-build-cache-${{ runner.os }}-${{ hashFiles('settings.gradle.kts', '**/build.gradle.kts', 'gradle/libs.versions.toml', 'gradle.properties') }} + restore-keys: | + gradle-build-cache-${{ runner.os }}- + + - name: AVD cache + uses: actions/cache@v4 + id: avd-cache + with: + path: | + ~/.android/avd/* + ~/.android/adb* + key: avd-${{ matrix.api-level }}-${{ matrix.target }}-${{ matrix.arch }} + + - name: Create AVD and generate snapshot for caching + if: steps.avd-cache.outputs.cache-hit != 'true' + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ matrix.api-level }} + target: ${{ matrix.target }} + arch: ${{ matrix.arch }} + force-avd-creation: false + emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -memory 4096 -partition-size 4096 + disable-animations: true + emulator-boot-timeout: 900 + script: echo "Generated AVD snapshot for caching." + + - name: Run Regression UI tests on emulator + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ matrix.api-level }} + target: ${{ matrix.target }} + arch: ${{ matrix.arch }} + force-avd-creation: false + emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -memory 4096 -partition-size 4096 + disable-animations: true + emulator-boot-timeout: 900 + script: | + adb logcat > logcat-regression.txt & + ./gradlew connectedDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.annotation=io.github.stslex.workeeper.core.ui.test.annotations.Regression --full-stacktrace --continue + adb logcat -d > logcat-regression-final.txt + + - name: Publish Regression Test Results + uses: EnricoMi/publish-unit-test-result-action@v2 + if: always() + with: + files: | + **/build/outputs/androidTest-results/connected/**/*.xml + **/build/test-results/**/*.xml + check_name: Regression UI Test Results (API ${{ matrix.api-level }}) + comment_title: Regression UI Test Results (API ${{ matrix.api-level }}) + commit: ${{ github.sha }} + report_individual_runs: true + deduplicate_classes_by_file_name: false + compare_to_earlier_commit: true + pull_request_build: commit + check_run_annotations: all tests, skipped tests + + - name: Detailed Regression Test Report + uses: mikepenz/action-junit-report@v4 + if: always() + with: + report_paths: | + **/build/outputs/androidTest-results/connected/**/*.xml + **/build/test-results/**/*.xml + check_name: Detailed Regression Test Report (API ${{ matrix.api-level }}) + detailed_summary: true + include_passed: true + fail_on_failure: false + require_tests: true + annotate_only: false + job_summary: true + + - name: Upload regression test reports + uses: actions/upload-artifact@v4 + if: always() + with: + name: regression-test-reports-api-${{ matrix.api-level }} + path: | + **/build/reports/androidTests/ + **/build/outputs/androidTest-results/ + retention-days: 30 + + - name: Upload logcat output + uses: actions/upload-artifact@v4 + if: always() + with: + name: logcat-regression-api-${{ matrix.api-level }} + path: | + logcat-regression.txt + logcat-regression-final.txt + retention-days: 7 + + - name: Upload screenshots on failure + uses: actions/upload-artifact@v4 + if: failure() + with: + name: screenshots-regression-api-${{ matrix.api-level }} + path: | + **/build/outputs/connected_android_test_additional_output/ + retention-days: 14 + if-no-files-found: ignore diff --git a/.github/workflows/version_updater.yml b/.github/workflows/version_updater.yml index 62a23310..6cb4537d 100644 --- a/.github/workflows/version_updater.yml +++ b/.github/workflows/version_updater.yml @@ -77,7 +77,7 @@ jobs: fi - name: Push changes - uses: ad-m/github-push-action@master + uses: ad-m/github-push-action@v0.8.0 with: github_token: ${{ secrets.PUSH_TOKEN }} branch: ${{ github.ref }} diff --git a/app/app/src/main/java/io/github/stslex/workeeper/App.kt b/app/app/src/main/java/io/github/stslex/workeeper/App.kt index 060903d3..6ffb89a9 100644 --- a/app/app/src/main/java/io/github/stslex/workeeper/App.kt +++ b/app/app/src/main/java/io/github/stslex/workeeper/App.kt @@ -12,8 +12,12 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarResult.ActionPerformed +import androidx.compose.material3.SnackbarResult.Dismissed import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -21,6 +25,8 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.zIndex import io.github.stslex.workeeper.bottom_app_bar.WorkeeperBottomAppBar +import io.github.stslex.workeeper.core.ui.kit.components.snackbar.AppSnackBar +import io.github.stslex.workeeper.core.ui.kit.snackbar.SnackbarManager import io.github.stslex.workeeper.core.ui.kit.theme.AppTheme import io.github.stslex.workeeper.core.ui.kit.theme.AppUi import io.github.stslex.workeeper.core.ui.navigation.LocalNavigator @@ -40,6 +46,24 @@ fun App() { LocalNavigator provides navigator, LocalRootComponent provides rootComponent, ) { + val snackbarHostState = remember { SnackbarHostState() } + + LaunchedEffect(Unit) { + SnackbarManager.snackbar + .collect { model -> + val result = snackbarHostState.showSnackbar( + message = model.message, + actionLabel = model.actionLabel, + withDismissAction = model.withDismissAction, + ) + when (result) { + ActionPerformed -> model.action() + + Dismissed -> Unit // No-op + } + } + } + Box( modifier = Modifier .fillMaxSize() @@ -79,6 +103,14 @@ fun App() { modifier = Modifier, navigator = navigator, ) + + AppSnackBar( + modifier = Modifier.align(Alignment.BottomCenter), + state = snackbarHostState, + onActionClick = { + snackbarHostState.currentSnackbarData?.performAction() + }, + ) } } } diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt index b810579d..8eedc003 100644 --- a/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt @@ -1,7 +1,6 @@ import AppExt.findPluginId -import AppExt.findVersionInt import AppExt.libs -import com.android.build.gradle.LibraryExtension +import com.android.build.api.dsl.LibraryExtension import io.github.stslex.workeeper.configureAndroidCompose import io.github.stslex.workeeper.configureKotlinAndroid import org.gradle.api.Plugin @@ -14,7 +13,6 @@ class AndroidLibraryComposeConventionPlugin : Plugin { with(target) { pluginManager.apply { apply(libs.findPluginId("library")) - apply(libs.findPluginId("kotlin")) apply(libs.findPluginId("composeCompiler")) apply(libs.findPluginId("serialization")) apply(libs.findPluginId("ksp")) @@ -28,9 +26,7 @@ class AndroidLibraryComposeConventionPlugin : Plugin { configureAndroidCompose(this) defaultConfig.apply { - targetSdk = libs.findVersionInt("targetSdk") testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") buildTypes { release { isMinifyEnabled = false diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt index 527499e9..c4774e3c 100644 --- a/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt @@ -1,7 +1,6 @@ import AppExt.findPluginId -import AppExt.findVersionInt import AppExt.libs -import com.android.build.gradle.LibraryExtension +import com.android.build.api.dsl.LibraryExtension import io.github.stslex.workeeper.configureKotlinAndroid import org.gradle.api.Plugin import org.gradle.api.Project @@ -13,7 +12,6 @@ class AndroidLibraryConventionPlugin : Plugin { with(target) { pluginManager.apply { apply(libs.findPluginId("library")) - apply(libs.findPluginId("kotlin")) apply(libs.findPluginId("ksp")) apply(libs.findPluginId("convention.lint")) } @@ -21,9 +19,7 @@ class AndroidLibraryConventionPlugin : Plugin { extensions.configure { configureKotlinAndroid(this) defaultConfig.apply { - targetSdk = libs.findVersionInt("targetSdk") testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") buildTypes { release { isMinifyEnabled = false diff --git a/build-logic/convention/src/main/kotlin/LintConventionPlugin.kt b/build-logic/convention/src/main/kotlin/LintConventionPlugin.kt index da6a2c7d..417288c4 100644 --- a/build-logic/convention/src/main/kotlin/LintConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/LintConventionPlugin.kt @@ -14,38 +14,36 @@ class LintConventionPlugin : Plugin { } val commonExtension = extensions.findByType(CommonExtension::class.java) - commonExtension?.apply { - lint { - // Main lint configuration (includes centralized suppressions) - lintConfig = rootProject.file("lint-rules/lint.xml") + commonExtension?.lint?.apply { + // Main lint configuration (includes centralized suppressions) + lintConfig = rootProject.file("lint-rules/lint.xml") - // Report configuration - htmlReport = true - xmlReport = true - sarifReport = true - textReport = false + // Report configuration + htmlReport = true + xmlReport = true + sarifReport = true + textReport = false - // Analysis configuration - checkDependencies = true - abortOnError = true - ignoreWarnings = false - checkAllWarnings = true - warningsAsErrors = true - checkGeneratedSources = false - explainIssues = true - noLines = false - quiet = false - checkReleaseBuilds = true - ignoreTestSources = true + // Analysis configuration + checkDependencies = true + abortOnError = true + ignoreWarnings = false + checkAllWarnings = true + warningsAsErrors = true + checkGeneratedSources = false + explainIssues = true + noLines = false + quiet = false + checkReleaseBuilds = true + ignoreTestSources = true - // Single centralized baseline file for all modules - baseline = rootProject.file("lint-rules/lint-baseline.xml") + // Single centralized baseline file for all modules + baseline = rootProject.file("lint-rules/lint-baseline.xml") - // Output directories - htmlOutput = file("build/reports/lint-results.html") - xmlOutput = file("build/reports/lint-results.xml") - sarifOutput = file("build/reports/lint-results.sarif") - } + // Output directories + htmlOutput = file("build/reports/lint-results.html") + xmlOutput = file("build/reports/lint-results.xml") + sarifOutput = file("build/reports/lint-results.sarif") } // Configure detekt for each module diff --git a/build-logic/convention/src/main/kotlin/RoomLibraryConventionPlugin.kt b/build-logic/convention/src/main/kotlin/RoomLibraryConventionPlugin.kt index 97d9f6d8..d630225e 100644 --- a/build-logic/convention/src/main/kotlin/RoomLibraryConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/RoomLibraryConventionPlugin.kt @@ -1,4 +1,4 @@ -import AppExt.androidTestApi +import AppExt.androidTestImplementation import AppExt.findPluginId import AppExt.implementation import AppExt.implementationBundle @@ -36,7 +36,7 @@ class RoomLibraryConventionPlugin : Plugin { ksp("androidx-room-compiler") implementation("androidx-paging-runtime") - androidTestApi("androidx-room-testing") + androidTestImplementation("androidx-room-testing") } } } diff --git a/build-logic/convention/src/main/kotlin/io/github/stslex/workeeper/ComposeAndroid.kt b/build-logic/convention/src/main/kotlin/io/github/stslex/workeeper/ComposeAndroid.kt index f2001c20..68d54ea7 100644 --- a/build-logic/convention/src/main/kotlin/io/github/stslex/workeeper/ComposeAndroid.kt +++ b/build-logic/convention/src/main/kotlin/io/github/stslex/workeeper/ComposeAndroid.kt @@ -12,7 +12,7 @@ import org.gradle.api.Project * Configure Compose-specific options */ internal fun Project.configureAndroidCompose( - commonExtension: CommonExtension<*, *, *, *, *, *>, + commonExtension: CommonExtension, ) { commonExtension.apply { buildFeatures.compose = true diff --git a/build-logic/convention/src/main/kotlin/io/github/stslex/workeeper/ConfigureApplication.kt b/build-logic/convention/src/main/kotlin/io/github/stslex/workeeper/ConfigureApplication.kt index cb8f5a04..cddb225c 100644 --- a/build-logic/convention/src/main/kotlin/io/github/stslex/workeeper/ConfigureApplication.kt +++ b/build-logic/convention/src/main/kotlin/io/github/stslex/workeeper/ConfigureApplication.kt @@ -22,7 +22,6 @@ fun Project.configureApplication( ) { pluginManager.apply { apply(libs.findPluginId("application")) - apply(libs.findPluginId("kotlin")) apply(libs.findPluginId("composeCompiler")) apply(libs.findPluginId("vkompose")) apply(libs.findPluginId("serialization")) @@ -34,10 +33,10 @@ fun Project.configureApplication( val appTypePostfix = if (appType.postfix.isNotEmpty()) ".${appType.postfix}" else "" val versionNamePostfix = if (appType.postfix.isNotEmpty()) "-${appType.postfix}" else "" + extensions.configure { + arg("KOIN_CONFIG_CHECK", "true") + } extensions.configure { - extensions.configure { - arg("KOIN_CONFIG_CHECK", "true") - } configureKotlinAndroid(this) configureAndroidCompose(this) diff --git a/build-logic/convention/src/main/kotlin/io/github/stslex/workeeper/KotlinAndroid.kt b/build-logic/convention/src/main/kotlin/io/github/stslex/workeeper/KotlinAndroid.kt index e94282e1..ecd77fff 100644 --- a/build-logic/convention/src/main/kotlin/io/github/stslex/workeeper/KotlinAndroid.kt +++ b/build-logic/convention/src/main/kotlin/io/github/stslex/workeeper/KotlinAndroid.kt @@ -39,7 +39,7 @@ internal fun Project.configureKotlinAndroid( * Configure base Kotlin with Android options */ private fun Project.configureKotlinAndroid( - commonExtension: CommonExtension<*, *, *, *, *, *>, + commonExtension: CommonExtension, isApp: Boolean ): Unit = with(commonExtension) { @@ -59,9 +59,10 @@ private fun Project.configureKotlinAndroid( namespace = if (moduleName.isNotEmpty()) "$APP_PREFIX.$moduleName" else APP_PREFIX - defaultConfig { + buildFeatures.buildConfig = true + + defaultConfig.apply { minSdk = libs.findVersionInt("minSdk") - buildFeatures.buildConfig = true testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" gradleLocalProperties( @@ -72,7 +73,7 @@ private fun Project.configureKotlinAndroid( } } - compileOptions { + compileOptions.apply { // Up to Java 11 APIs are available through desugaring // https://developer.android.com/studio/write/java11-minimal-support-table sourceCompatibility = JavaVersion.VERSION_21 @@ -102,13 +103,13 @@ private fun Project.configureKotlinAndroid( tasks.withType().configureEach { useJUnitPlatform() + failOnNoDiscoveredTests.set(false) testLogging { events("passed", "skipped", "failed") } } - - testOptions { unitTests.isIncludeAndroidResources = true } + testOptions.apply { unitTests.isIncludeAndroidResources = true } } /** diff --git a/build.gradle.kts b/build.gradle.kts index 62d23119..37d21bc5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -17,6 +17,14 @@ plugins { buildscript { + configurations.all { + resolutionStrategy { + // AGP 9.1.0 requires annotations:23.0.0, but Gradle 9.3.1's embedded Kotlin + // pins annotations:13.0 strictly. Force the higher version to resolve the conflict. + force("org.jetbrains:annotations:23.0.0") + } + } + repositories { google() mavenCentral() diff --git a/core/database/src/test/kotlin/io/github/stslex/workeeper/core/database/migrations/Migration1To2Test.kt b/core/database/src/test/kotlin/io/github/stslex/workeeper/core/database/migrations/Migration1To2Test.kt index 2a96df30..7471c1c8 100644 --- a/core/database/src/test/kotlin/io/github/stslex/workeeper/core/database/migrations/Migration1To2Test.kt +++ b/core/database/src/test/kotlin/io/github/stslex/workeeper/core/database/migrations/Migration1To2Test.kt @@ -140,7 +140,7 @@ class Migration1To2Test { testDb.execSQL( "INSERT INTO exercises_table (uuid, name, reps, weight, sets, timestamp) VALUES (?, ?, ?, ?, ?, ?)", - arrayOf(testUuid, testName, testReps, testWeight, testSets, testTimestamp), + arrayOf(testUuid, testName, testReps, testWeight, testSets, testTimestamp), ) // Run migration @@ -199,7 +199,7 @@ class Migration1To2Test { exercises.forEach { exercise -> testDb.execSQL( "INSERT INTO exercises_table (uuid, name, reps, weight, sets, timestamp) VALUES (?, ?, ?, ?, ?, ?)", - arrayOf( + arrayOf( exercise.uuid, exercise.name, exercise.reps, @@ -294,13 +294,13 @@ class Migration1To2Test { // Insert exercise with zero sets (edge case) testDb.execSQL( "INSERT INTO exercises_table (uuid, name, reps, weight, sets, timestamp) VALUES (?, ?, ?, ?, ?, ?)", - arrayOf("uuid-zero-sets", "Test Exercise", 10, 50.0, 0, 1640995200000L), + arrayOf("uuid-zero-sets", "Test Exercise", 10, 50.0, 0, 1640995200000L), ) // Insert exercise with negative values (edge case) testDb.execSQL( "INSERT INTO exercises_table (uuid, name, reps, weight, sets, timestamp) VALUES (?, ?, ?, ?, ?, ?)", - arrayOf("uuid-negative", "Negative Exercise", -5, -10.0, 1, 1640995200000L), + arrayOf("uuid-negative", "Negative Exercise", -5, -10.0, 1, 1640995200000L), ) // Run migration @@ -350,7 +350,7 @@ class Migration1To2Test { testDb.execSQL( "INSERT INTO exercises_table (uuid, name, reps, weight, sets, timestamp) VALUES (?, ?, ?, ?, ?, ?)", - arrayOf("test-uuid", "Test Exercise", 15, 80.5, 2, 1640995200000L), + arrayOf("test-uuid", "Test Exercise", 15, 80.5, 2, 1640995200000L), ) // Run migration diff --git a/core/ui/kit/src/main/kotlin/io/github/stslex/workeeper/core/ui/kit/snackbar/AppSnackbarModel.kt b/core/ui/kit/src/main/kotlin/io/github/stslex/workeeper/core/ui/kit/snackbar/AppSnackbarModel.kt new file mode 100644 index 00000000..cdbf7cf0 --- /dev/null +++ b/core/ui/kit/src/main/kotlin/io/github/stslex/workeeper/core/ui/kit/snackbar/AppSnackbarModel.kt @@ -0,0 +1,11 @@ +package io.github.stslex.workeeper.core.ui.kit.snackbar + +import androidx.compose.runtime.Stable + +@Stable +data class AppSnackbarModel( + val message: String, + val actionLabel: String? = null, + val withDismissAction: Boolean = false, + val action: () -> Unit = { }, +) diff --git a/core/ui/kit/src/main/kotlin/io/github/stslex/workeeper/core/ui/kit/snackbar/SnackbarManager.kt b/core/ui/kit/src/main/kotlin/io/github/stslex/workeeper/core/ui/kit/snackbar/SnackbarManager.kt new file mode 100644 index 00000000..72082911 --- /dev/null +++ b/core/ui/kit/src/main/kotlin/io/github/stslex/workeeper/core/ui/kit/snackbar/SnackbarManager.kt @@ -0,0 +1,29 @@ +package io.github.stslex.workeeper.core.ui.kit.snackbar + +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow + +object SnackbarManager { + + private val _snackbar: MutableSharedFlow = MutableSharedFlow() + val snackbar: SharedFlow = _snackbar.asSharedFlow() + + fun showSnackbar(model: AppSnackbarModel) { + _snackbar.tryEmit(model) + } + + fun showSnackbar( + message: String, + actionLabel: String? = null, + withDismissAction: Boolean = false, + action: () -> Unit = {}, + ): Unit = showSnackbar( + AppSnackbarModel( + message = message, + actionLabel = actionLabel, + withDismissAction = withDismissAction, + action = action, + ), + ) +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f81418b7..fd214776 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,9 +1,9 @@ [versions] androidDesugarJdkLibs = "2.1.5" -kotlin = "2.2.20" -ksp = "2.2.20-2.0.4" -androidGradlePlugin = "8.13.2" -androidTools = "31.13.2" +kotlin = "2.3.20" +ksp = "2.3.6" +androidGradlePlugin = "9.1.0" +androidTools = "32.1.0" minSdk = "28" targetSdk = "36" @@ -19,7 +19,7 @@ lifecycle = "2.10.0" coroutines = "1.10.2" composeBom = "2025.12.01" -composeGradle = "1.9.3" +composeGradle = "1.10.3" composeNavigation = "2.9.6" composeJunit = "1.10.0" accompanist = "0.36.0" @@ -27,11 +27,11 @@ coil = "3.3.0" composeActivity = "1.12.2" fbBom = "34.7.0" -hilt = "2.57.2" +hilt = "2.59.2" hiltCompose = "1.3.0" mockk = "1.14.7" -junit = "6.0.1" +junit = "5.13.4" androidxJunit = "1.3.0" espresso = "3.7.0" robolectric = "4.16" @@ -56,7 +56,6 @@ detektCompose = "0.5.3" # decompose dependencies essenty = "2.5.0" parcelize = "0.2.4" -junitJupiter = "5.13.4" junitKtx = "1.3.0" # Other tools diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0f825821..b0f2eb5e 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Sat Aug 30 22:03:06 MSK 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/settings.gradle.kts b/settings.gradle.kts index 7036faf3..25f04716 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -12,6 +12,9 @@ pluginManagement { gradlePluginPortal() } } +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" +} @Suppress("UnstableApiUsage") dependencyResolutionManagement {