Skip to content

Merge pull request #458 from AppsFlyerSDK/docs-workflow-and-feature-c… #43

Merge pull request #458 from AppsFlyerSDK/docs-workflow-and-feature-c…

Merge pull request #458 from AppsFlyerSDK/docs-workflow-and-feature-c… #43

# =============================================================================
# Lint, Test & Build - PR/push gate and reusable validation workflow
# =============================================================================
#
# Purpose: Validates code quality, runs unit tests, and builds the example app
# in release mode for both Android and iOS on every PR and push to
# development/master. Also reusable from rc-release.yml and
# production-release.yml.
#
# What it does:
# 1. Lints + format-checks + runs Dart/Flutter unit tests with coverage.
# 2. Builds Android example app (release App Bundle).
# 3. Builds iOS example app (no-codesign release IPA).
# 4. Caches dependencies for faster subsequent runs.
#
# Note on debug builds: debug-mode Android APK and iOS simulator builds used
# to live here, but they duplicate exactly what ios-e2e.yml / android-e2e.yml
# do before running scenarios. Release-mode builds remain here because E2E
# never exercises them and they catch R8/proguard + archive-bundling issues
# that debug builds don't.
#
# Triggers:
# - Pull requests to development or master branches
# - Direct pushes to development or master branches
# - Manual workflow dispatch for testing
# - workflow_call (rc-release.yml, production-release.yml)
#
# =============================================================================
name: Lint, Test & Build
on:
# Trigger on pull requests targeting main branches
pull_request:
branches:
- development
- master
paths-ignore:
- '**.md'
- 'doc/**'
- 'assets/**'
- '.github/workflows/close_inactive_issues.yml'
- '.github/workflows/responseToSupportIssue*.yml'
# Trigger on direct pushes to main branches
push:
branches:
- development
- master
paths-ignore:
- '**.md'
- 'doc/**'
- 'assets/**'
# Allow manual triggering for testing
workflow_dispatch:
# Reusable from other workflows. Callers (rc-release.yml) can pass
# skip_unit=true to skip the unit/lint/format job, or skip_builds=true to
# skip the release-mode Android/iOS builds. Skipping the unit job or the
# build jobs is still treated as a pass at the ci-summary level; the
# caller's pre-publish gate decides whether 'skipped' is acceptable for
# publish.
workflow_call:
inputs:
skip_unit:
description: 'Skip the unit-tests/lint/format job (build jobs still run)'
required: false
type: boolean
default: false
skip_builds:
description: 'Skip the Android + iOS release-mode build jobs (unit job still runs)'
required: false
type: boolean
default: false
# Ensure only one CI run per PR/branch at a time
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
# ===========================================================================
# Job 1: Run Unit Tests
# ===========================================================================
# Runs all Dart/Flutter unit tests for the plugin
# Uses: Ubuntu runner (fastest and free for public repos)
# ===========================================================================
test:
name: πŸ§ͺ Run Unit Tests
runs-on: ubuntu-latest
# When called from rc-release.yml with skip_unit=true, skip this job.
# On PR/push triggers, inputs.skip_unit is unset and resolves to false.
if: ${{ inputs.skip_unit != true }}
steps:
# Step 1: Checkout the repository code
- name: Checkout repository
uses: actions/checkout@v5
# Step 2: Set up Flutter SDK
# Uses subosito/flutter-action which caches Flutter SDK automatically
- name: Setup Flutter SDK
uses: subosito/flutter-action@v2
with:
channel: 'stable' # Use latest stable Flutter
cache: true # Cache Flutter SDK for faster runs
# Step 3: Verify Flutter installation
- name: Display Flutter version
run: |
flutter --version
dart --version
# Step 4: Get plugin dependencies
# This installs all packages defined in pubspec.yaml
- name: Install plugin dependencies
run: flutter pub get
# Step 5: Analyze code for issues
# Checks for code quality issues, unused imports, etc.
# Only fails on actual errors, not info-level warnings
- name: Analyze code
run: flutter analyze --no-fatal-infos --no-fatal-warnings
# Step 6: Format check
# Ensures code follows Dart formatting standards. On failure, prints
# the offending files and the exact one-liner to fix locally, plus a
# GitHub Actions `::error::` annotation at the top of the run.
- name: Check code formatting
run: |
if output=$(dart format --output=none --set-exit-if-changed . 2>&1); then
echo "$output"
exit 0
fi
echo "$output"
changed=$(echo "$output" | awk '/^Changed / {sub(/^Changed /, ""); print}')
inline=$(echo "$changed" | tr '\n' ' ')
echo "::group::πŸ”§ Files that need formatting"
echo "$changed"
echo "::endgroup::"
echo "::error title=Code formatting check failed::Run 'dart format .' locally, commit the result, and push again. Files: $inline"
echo
echo "Fix locally with:"
echo " dart format $inline"
echo " git add $inline"
echo " git commit --amend --no-edit # or make a new commit"
echo
echo "Maintainer tip: ./scripts/install-hooks.sh installs a"
echo "pre-commit hook that runs the same check locally."
exit 1
# Step 7: Run unit tests
# Executes all tests in the test/ directory
- name: Run unit tests
run: flutter test --coverage
# Step 8: Upload coverage report (optional)
# Useful for tracking code coverage over time
- name: Upload coverage to Codecov (optional)
if: success()
uses: codecov/codecov-action@v6
with:
files: ./coverage/lcov.info
fail_ci_if_error: false # Don't fail CI if coverage upload fails
# ===========================================================================
# Job 2: Build Android Example App
# ===========================================================================
# Builds the example app for Android to ensure plugin integration works
# Uses: Ubuntu runner with Java 17
# ===========================================================================
build-android:
name: πŸ€– Build Android Example
runs-on: ubuntu-latest
needs: test
# Run when tests pass OR when skipped (skip_unit=true), AND skip_builds is
# not set. Keeps today's "test fails -> skip the build" behavior on
# PR/push, plus lets a caller workflow opt out of the release builds for
# fast iteration.
if: ${{ (needs.test.result == 'success' || needs.test.result == 'skipped') && inputs.skip_builds != true }}
steps:
# Step 1: Checkout code
- name: Checkout repository
uses: actions/checkout@v5
# Step 2: Set up Java (required for Android builds)
# Android builds require JDK 17 for modern Gradle versions
- name: Setup Java
uses: actions/setup-java@v5
with:
distribution: 'temurin' # Eclipse Temurin (formerly AdoptOpenJDK)
java-version: '17'
cache: 'gradle' # Cache Gradle dependencies
# Step 3: Set up Flutter SDK
- name: Setup Flutter SDK
uses: subosito/flutter-action@v2
with:
channel: 'stable'
cache: true
# Step 4: Display versions for debugging
- name: Display versions
run: |
flutter --version
java -version
echo "JAVA_HOME: $JAVA_HOME"
# Step 5: Get plugin dependencies
- name: Install plugin dependencies
run: flutter pub get
# Step 6: Get example app dependencies
- name: Install example app dependencies
working-directory: example
run: flutter pub get
# Step 7: Create dummy .env file for CI (not committed to repo)
- name: Create dummy .env for CI build
working-directory: example
run: |
echo "DEV_KEY=dummy_dev_key" > .env
echo "APP_ID=dummy_app_id" >> .env
# Step 8: Build Android App Bundle (release mode, no signing)
# App Bundle is the preferred format for Play Store. Catches R8/proguard
# regressions a debug APK won't. Debug APK builds were removed because
# android-e2e.yml already builds and runs `flutter build apk --debug`
# against the same source tree.
- name: Build Android App Bundle (release)
working-directory: example
run: flutter build appbundle --release
# Step 9: Upload App Bundle artifact (optional)
# Useful for manual install testing and archiving release-mode output.
- name: Upload App Bundle artifact
if: success()
uses: actions/upload-artifact@v5
with:
name: android-appbundle-release
path: example/build/app/outputs/bundle/release/app-release.aab
retention-days: 7
# ===========================================================================
# Job 3: Build iOS Example App
# ===========================================================================
# Builds the example app for iOS to ensure plugin integration works
# Uses: macOS runner (required for Xcode and iOS builds)
# Note: macOS runners consume 10x minutes on private repos
# ===========================================================================
build-ios:
name: 🍎 Build iOS Example
runs-on: macos-15 # macOS 15 (Sequoia) with Xcode 16.4 default; matches ios-e2e + rc-smoke
needs: test
# Run when tests pass OR when skipped (skip_unit=true), AND skip_builds is
# not set. Keeps today's "test fails -> skip the build" behavior on
# PR/push, plus lets a caller workflow opt out of the release builds for
# fast iteration.
if: ${{ (needs.test.result == 'success' || needs.test.result == 'skipped') && inputs.skip_builds != true }}
steps:
# Step 1: Checkout code
- name: Checkout repository
uses: actions/checkout@v5
# Step 2: Set up Flutter SDK
- name: Setup Flutter SDK
uses: subosito/flutter-action@v2
with:
channel: 'stable'
cache: true
# Step 3: Display versions
- name: Display versions
run: |
flutter --version
xcodebuild -version
pod --version
# Step 4: Get plugin dependencies
- name: Install plugin dependencies
run: flutter pub get
# Step 5: Get example app dependencies
- name: Install example app dependencies
working-directory: example
run: flutter pub get
# Step 6: Update CocoaPods repo (ensures latest pod specs)
# This can be slow, so we only update if needed
- name: Update CocoaPods repo
working-directory: example/ios
run: pod repo update
# Step 7: Install CocoaPods dependencies
# This installs native iOS dependencies including AppsFlyer SDK
- name: Install CocoaPods dependencies
working-directory: example/ios
run: pod install
# Step 8: Create dummy .env file for CI (not committed to repo)
- name: Create dummy .env for CI build
working-directory: example
run: |
echo "DEV_KEY=dummy_dev_key" > .env
echo "APP_ID=dummy_app_id" >> .env
# Step 9: Build iOS IPA without code signing (release mode)
# Validates a full release build without requiring certificates. Catches
# archive bundling and signing-config issues a simulator-debug build
# won't. The simulator-debug build that used to live here was dropped
# because ios-e2e.yml already runs `flutter build ios --simulator --debug`
# against the same source tree.
- name: Build iOS IPA (no codesign)
working-directory: example
run: flutter build ipa --release --no-codesign
# Step 10: Upload build artifacts (optional)
- name: Upload iOS build artifact
if: success()
uses: actions/upload-artifact@v5
with:
name: ios-app-unsigned
path: example/build/ios/archive/Runner.xcarchive
retention-days: 7
# ===========================================================================
# Job 4: Summary Report
# ===========================================================================
# Provides a summary of all CI jobs
# ===========================================================================
ci-summary:
name: πŸ“‹ CI Summary
runs-on: ubuntu-latest
needs: [test, build-android, build-ios]
if: always() # Run even if previous jobs fail
steps:
- name: Check CI Results
run: |
echo "==================================="
echo "Lint, Test & Build summary"
echo "==================================="
echo "Test Job: ${{ needs.test.result }}"
echo "Android Build: ${{ needs.build-android.result }}"
echo "iOS Build: ${{ needs.build-ios.result }}"
echo "==================================="
# 'skipped' is acceptable for any of the three jobs (skip_unit=true
# or skip_builds=true from a caller workflow); only 'failure' or
# 'cancelled' should fail the summary. The caller's pre-publish
# gate decides whether a skip is acceptable for publish.
test_result="${{ needs.test.result }}"
android_result="${{ needs.build-android.result }}"
ios_result="${{ needs.build-ios.result }}"
if [[ "$test_result" != "success" && "$test_result" != "skipped" ]]; then
echo "❌ Unit/lint job failed"
exit 1
fi
if [[ "$android_result" != "success" && "$android_result" != "skipped" ]]; then
echo "❌ Android release build failed"
exit 1
fi
if [[ "$ios_result" != "success" && "$ios_result" != "skipped" ]]; then
echo "❌ iOS release build failed"
exit 1
fi
echo "βœ… Lint, Test & Build passed"