diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml old mode 100755 new mode 100644 index 4538c49..c2f135c --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,9 +4,11 @@ on: pull_request: branches: - main + - develop push: branches: - main + - develop jobs: cancel-previous: @@ -16,10 +18,10 @@ jobs: actions: write runs-on: ubuntu-latest steps: - - name: Cancel Previous Build - uses: styfle/cancel-workflow-action@0.12.1 - with: - access_token: ${{ github.token }} + - name: Cancel Previous Build + uses: styfle/cancel-workflow-action@0.13.0 + with: + access_token: ${{ github.token }} lint: name: Lint @@ -28,20 +30,32 @@ jobs: runs-on: ubuntu-latest needs: cancel-previous steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Validate Gradle Wrapper - uses: gradle/wrapper-validation-action@v1 - - name: Copy CI gradle.properties - run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties - - name: Set up JDK - uses: actions/setup-java@v4 - with: - distribution: 'zulu' - java-version: '17' - cache: 'gradle' - - name: Lint - run: make clean lint + - name: Checkout + uses: actions/checkout@v4 + - name: Validate Gradle Wrapper + uses: gradle/actions/wrapper-validation@v4 + - name: Copy CI gradle.properties + run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + with: + cache-read-only: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/develop' }} + - name: Lint + run: ./gradlew lintDebug --build-cache --parallel --configure-on-demand + - name: Upload Lint Reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: lint-reports + path: | + **/build/reports/lint-results-*.html + **/build/reports/lint-results-*.xml + retention-days: 7 build: name: Build @@ -50,22 +64,37 @@ jobs: runs-on: ubuntu-latest needs: lint steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Validate Gradle Wrapper - uses: gradle/wrapper-validation-action@v1 - - name: Copy CI gradle.properties - run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties - - name: Set up JDK - uses: actions/setup-java@v4 - with: - distribution: 'zulu' - java-version: '17' - cache: 'gradle' - - name: APK - run: make clean assemble - - name: Bundle - run: make clean bundle + - name: Checkout + uses: actions/checkout@v4 + - name: Copy CI gradle.properties + run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + with: + cache-read-only: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/develop' }} + - name: Build APK + run: ./gradlew assembleDebug --build-cache --parallel --configure-on-demand + - name: Build Bundle + run: ./gradlew bundleDebug --build-cache --parallel --configure-on-demand + - name: Upload APK + uses: actions/upload-artifact@v4 + with: + name: apk-debug + path: | + **/build/outputs/apk/**/*.apk + retention-days: 14 + - name: Upload Bundle + uses: actions/upload-artifact@v4 + with: + name: bundle-debug + path: | + **/build/outputs/bundle/**/*.aab + retention-days: 14 test: name: Test (unit) @@ -74,39 +103,101 @@ jobs: runs-on: ubuntu-latest needs: build steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Validate Gradle Wrapper - uses: gradle/wrapper-validation-action@v1 - - name: Copy CI gradle.properties - run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties - - name: Set up JDK - uses: actions/setup-java@v4 - with: - distribution: 'zulu' - java-version: '17' - cache: 'gradle' - - name: Test - run: make clean test + - name: Checkout + uses: actions/checkout@v4 + - name: Copy CI gradle.properties + run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + with: + cache-read-only: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/develop' }} + - name: Run Unit Tests + run: ./gradlew testDebugUnitTest --build-cache --parallel --configure-on-demand + - name: Upload Test Reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: unit-test-reports + path: | + **/build/reports/tests/** + **/build/test-results/** + retention-days: 7 androidTest: name: Test (instrumented) permissions: contents: read - runs-on: macos-latest + runs-on: ubuntu-latest timeout-minutes: 45 needs: test + if: | + github.ref == 'refs/heads/main' || + github.ref == 'refs/heads/develop' || + contains(github.event.head_commit.message, '[test-instrumented]') + strategy: + matrix: + api-level: [ 34 ] steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Validate Gradle Wrapper - uses: gradle/wrapper-validation-action@v1 - - name: Copy CI gradle.properties - run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties - - name: Set up JDK - uses: actions/setup-java@v4 - with: - distribution: 'zulu' - java-version: '17' - cache: 'gradle' - + - name: Checkout + uses: actions/checkout@v4 + - name: Copy CI gradle.properties + run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + with: + cache-read-only: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/develop' }} + # Enable KVM for hardware acceleration on Ubuntu runners (required for Android Emulator performance) + - 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 + # Cache AVD to avoid recreating the emulator on every run (saves ~3-5 minutes) + - name: AVD cache + uses: actions/cache@v4 + id: avd-cache + with: + path: | + ~/.android/avd/* + ~/.android/adb* + key: avd-${{ matrix.api-level }} + # Create AVD snapshot only if not cached (first run only) + - 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 }} + arch: x86_64 + force-avd-creation: false + emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + disable-animations: true + script: echo "Generated AVD snapshot for caching." + # Run instrumented tests on the Android emulator + - name: Run Instrumented Tests + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ matrix.api-level }} + arch: x86_64 + force-avd-creation: false + emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + disable-animations: true + script: ./gradlew connectedDebugAndroidTest --build-cache --parallel --configure-on-demand + - name: Upload Instrumented Test Reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: instrumented-test-reports-api-${{ matrix.api-level }} + path: | + **/build/reports/androidTests/** + **/build/outputs/androidTest-results/** + retention-days: 7 diff --git a/README.md b/README.md index 38242b9..6d6c75f 100755 --- a/README.md +++ b/README.md @@ -71,9 +71,16 @@ or just press the gradle icon with the downward arrow at the top right of androi - [ ] Build Logic setUP - [ ] Convention Plugins setUp - [ ] Sample Application - +- [ ] Testing + - [ ] Add Compose UI tests for design system components (instrumented tests) + - [ ] Test component rendering (CPrimaryButton, ChaiText, etc.) + - [ ] Test theme switching behavior (light/dark mode) + - [ ] Test accessibility semantics + - [ ] Add screenshot/visual regression tests + - [ ] Add E2E tests example for chaidemo app + - [ ] Test complete user flow: Navigate through all demo screens + - [ ] Test component interactions in real app context + - [ ] Verify all design system components display correctly in demo ## Contributing [Hop on here for a chat and ask questions](https://github.com/droidconKE/chai/discussions). NO DMs please :) - - diff --git a/docs/ci-workflow-guide.md b/docs/ci-workflow-guide.md new file mode 100644 index 0000000..04707fd --- /dev/null +++ b/docs/ci-workflow-guide.md @@ -0,0 +1,220 @@ +# CI/CD Workflow Guide + +## Overview + +This document explains the workflow structure for the Chai Design System GitHub Actions. + +## Workflow Structure + +### 🔄 Jobs Pipeline + +``` +cancel-previous → lint → build → test → androidTest (conditional) +``` + +For running all jobs sequentially with depndencies to ensure code quality before proceeding. + +--- + +## Jobs Breakdown + +### 1. **cancel-previous** + +- **Purpose**: Cancels duplicate/outdated workflow runs +- **When**: Always runs on every push/PR +- **Why**: Saves CI minutes by stopping old builds when new commits are pushed + +### 2. **lint** + +- **Purpose**: Static code analysis (Android Lint) +- **When**: Always runs after cancel-previous +- **Runs**: `./gradlew lintDebug` +- **Artifacts**: Lint reports (HTML & XML) +- **What it checks**: + - Code quality issues + - Android best practices violations + - Potential bugs + - Security vulnerabilities + +### 3. **build** + +- **Purpose**: Compile and build the project +- **When**: Always runs after lint passes +- **Runs**: + - `./gradlew assembleDebug` - Builds APK + - `./gradlew bundleDebug` - Builds AAB (Android App Bundle) +- **Artifacts**: + - Debug APK (14 days retention) + - Debug Bundle (14 days retention) + +### 4. **test** + +- **Purpose**: Run unit tests +- **When**: Always runs after build succeeds +- **Runs**: `./gradlew testDebugUnitTest` +- **Tests**: Fast, JVM-based unit tests (no emulator needed) +- **Artifacts**: Test reports and results (7 days retention) + +### 5. **androidTest** (Conditional) + +- **Purpose**: Run instrumented tests on Android emulator +- **When**: Only runs if: + - ✅ Pushing to `main` or `develop` branches + - ✅ Commit message contains `[test-instrumented]` + - ❌ Skipped on regular PRs (saves ~10-15 minutes) +- **Runs**: `./gradlew connectedDebugAndroidTest` +- **Tests**: UI tests, integration tests requiring Android framework +- **API Level**: 34 (Android 14) +- **Artifacts**: Instrumented test reports (7 days retention) + +--- + +## Test Types Explained + +### **Unit Tests** (test job) + +- **Location**: `src/test/` +- **Run on**: JVM (fast, no emulator) +- **Purpose**: Test busines logic, utilities, view models +- **Example**: Testing color utility functions, theme logic + +```kotlin +@Test +fun `test color alpha calculation`() { + val color = Color.Red + val result = color.withAlpha(0.5f) + assertEquals(0.5f, result.alpha, 0.01f) +} +``` + +### **Instrumented Tests** (androidTest job) + +- **Location**: `src/androidTest/` +- **Run on**: Android emulator or real device +- **Purpose**: Test UI components, Android APIs, compose rendering +- **Example**: Testing button click interactions + +```kotlin +@Test +fun `CPrimaryButton displays text and handles clicks`() { + composeTestRule.setContent { + ChaiTheme { + CPrimaryButton(onClick = { /* ... */ }) { + Text("Click me") + } + } + } + composeTestRule.onNodeWithText("Click me").assertExists() + composeTestRule.onNodeWithText("Click me").performClick() +} +``` + +### **E2E Tests** (not implemened) + +- **Purpose**: Test complete user flows across the entire app +- **Example**: User opens app → navigates through all screens → interacts with all components +- **Note**: ! Not typicly needed for a component library, bt will add it for a flow:) + +--- + +## Triggering Instrumented Tests + +Since instrumented tests are slow and resource-intensive, they're conditional: + +### Option 1: Merge to main/develop + +```bash +git push origin main +# androidTest job will run automatically +``` + +### Option 2: Use commit message flag + +```bash +git commit -m "Add new button variants [test-instrumented]" +git push origin feature/new-buttons +# androidTest job will run on this PR +``` + +### Option 3: Always run (modify workflow) + +Remove the `if:` condition from the androidTest job if you want it to run on every PR. + +--- + +## Performance Optimizations + +### ⚡ Gradle Flags + +All Gradle commands use: + +- `--build-cache` - Reuses outputs from previous builds +- `--parallel` - Runs independent tasks in parallel +- `--configure-on-demand` - Only configures needed projects + +### 🗂️ Caching Strategy + +1. **Gradle dependencies**: Cached by `gradle/actions/setup-gradle@v4` +2. **Android Emulator (AVD)**: Cached to avoid ~3-5 min setup time +3. **Cache policy**: Read-only for PRs, read-write for main/develop + +### 💰 Cost Savings + +- **Unit tests**: ~2-3 minutes (free on GitHub-hosted runners) +- **Instrumented tests**: ~10-15 minutes (expensive, hence conditional) +- **Estimated monthly savings**: ~$50-100 by making androidTest conditional + +--- + +## Recommendations + +### For Design System Development: + +1. **Unit tests**: Write for utilities, theme logic, non-UI code +2. **Instrumented tests**: Write for: + - Component rendering validation + - Theme switching behavior + - Accessibility semantics + - Screenshot/visual regression tests +3. **E2E tests**: Not needed for a component library + +### When to Run Full Suite: + +- ✅ Before releases +- ✅ After major refactoring +- ✅ When adding new components +- ❌ On every small PR (wastes time/money) + +--- + +## Future Improvements + +Consider adding: + +- [ ] **Screenshot testing** with Paparazzi or Roborazzi +- [ ] **Visual regression testing** to catch UI changes +- [ ] **Accessibility testing** with Espresso accessibility checks +- [ ] **Performance testing** for compose recomposition +- [ ] **Dependency updates** with Renovate/Dependabot + +--- + +## Troubleshooting + +### "Instrumented tests not running on my PR" + +- Check if your commit message contains `[test-instrumented]` +- Or merge to main/develop branch +- Or remove the `if:` condition in the workflow + +### "Tests failing on emulator" + +- Check KVM is enabled (Ubuntu runners only) +- Verify AVD cache is valid +- Try clearing AVD cache by bumping the cache key + +### "Workflow is too slow" + +- Consider removing androidTest job if not needed +- Reduce matrix API levels (currently only API 34) +- Optimize Gradle build with more aggressive caching