Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Flutter/Dart related
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
build/
*.lock.json
.metadata

# Android related
android/.gradle/
android/app/build/
android/gradle/
android/.idea/
android/local.properties
android/key.properties
android/app/release/
android/app/debug/
android/captures/
android/*.iml
*.jks
*.keystore
*.p12
google-services.json

# iOS related
ios/Pods/
ios/.symlinks/
ios/Flutter/Flutter.framework
ios/Flutter/Flutter.podspec
ios/.generated/
ios/Runner.xcworkspace/
ios/Runner.xcodeproj/xcuserdata/
ios/Runner.xcodeproj/project.xcworkspace/xcuserdata/
*.pbxuser
*.mode1v3
*.mode2v3
*.perspectivev3
*.xcuserstate
xcuserdata/
DerivedData/
*.mobileprovision
*.cer
*.p12
GoogleService-Info.plist

# IDE related
.idea/
.vscode/
*.swp
*.swo
*~
.DS_Store
Thumbs.db
desktop.ini

# Git
.git/
.gitignore
.gitattributes
.github/

# CI/CD
.gitlab-ci.yml
.circleci/
.travis.yml
azure-pipelines.yml

# Testing and coverage
coverage/
test/
integration_test/
.test_coverage/
*.lcov
*.info

# Documentation
README.md
README*.md
LICENSE
CHANGELOG.md
docs/
CONTRIBUTING.md

# Docker files
Dockerfile
Dockerfile.*
.dockerignore
docker-compose.yml
docker-compose.*.yml

# Local configurations
.env
.env.*
.env.local
.env.*.local
*.log
*.log.*

# Temporary files
*.tmp
*.temp
*.bak
*.swp
*~

# Firebase
firebase.json
.firebaserc
firestore.rules
firestore.indexes.json
storage.rules
218 changes: 218 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
name: Flutter CI/CD Pipeline

on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
workflow_dispatch:

env:
FLUTTER_VERSION: '3.38.4'
ANDROID_SDK_VERSION: '35'

jobs:
# ==========================================
# Job 1: Code Quality & Analysis
# ==========================================
analyze:
name: Code Analysis
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
channel: 'stable'
cache: true

- name: Create fake firebase_options.dart
run: |
mkdir -p lib
cat <<EOF > lib/firebase_options.dart
// ignore_for_file: type=lint
import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/foundation.dart';
class DefaultFirebaseOptions {
static FirebaseOptions get currentPlatform {
if (kIsWeb) throw UnsupportedError('Web not configured');
switch (defaultTargetPlatform) {
case TargetPlatform.android: return android;
case TargetPlatform.iOS: return ios;
default: throw UnsupportedError('Platform not configured');
}
}
static const FirebaseOptions android = FirebaseOptions(apiKey: 'fake', appId: 'fake', messagingSenderId: 'fake', projectId: 'fake', storageBucket: 'fake');
static const FirebaseOptions ios = FirebaseOptions(apiKey: 'fake', appId: 'fake', messagingSenderId: 'fake', projectId: 'fake', storageBucket: 'fake', iosBundleId: 'fake');
}
EOF

- name: Get dependencies
run: flutter pub get

- name: Run build_runner
run: dart run build_runner build --delete-conflicting-outputs

- name: Format generated code
run: dart format .

- name: Verify formatting
run: dart format --set-exit-if-changed lib/ test/
continue-on-error: false

- name: Analyze code
run: flutter analyze --fatal-infos

- name: Run tests
run: flutter test --coverage || echo "⚠️ No tests found"

- name: Upload coverage to artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: code-coverage
path: coverage/lcov.info
if-no-files-found: ignore
retention-days: 7

# ==========================================
# Job 2: Android Build (Docker)
# ==========================================
build-android:
name: Build Android APK
runs-on: ubuntu-latest
needs: analyze
timeout-minutes: 60

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Create mock configuration files
run: |
mkdir -p lib
# firebase_options.dart
echo "import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/foundation.dart'; class DefaultFirebaseOptions { static FirebaseOptions get currentPlatform { if (kIsWeb) throw UnsupportedError('Web not configured'); switch (defaultTargetPlatform) { case TargetPlatform.android: return android; case TargetPlatform.iOS: return ios; default: throw UnsupportedError('Platform not configured'); } } static const FirebaseOptions android = FirebaseOptions(apiKey: 'fake', appId: 'fake', messagingSenderId: 'fake', projectId: 'fake', storageBucket: 'fake'); static const FirebaseOptions ios = FirebaseOptions(apiKey: 'fake', appId: 'fake', messagingSenderId: 'fake', projectId: 'fake', storageBucket: 'fake', iosBundleId: 'fake'); }" > lib/firebase_options.dart
# google-services.json
echo '{"project_info":{"project_number":"123","project_id":"fake","storage_bucket":"fake"},"client":[{"client_info":{"mobilesdk_app_id":"1:123:android:123","android_client_info":{"package_name":"com.flutter.starter.template.flutter_starter_template"}},"api_key":[{"current_key":"fake"}]}]}' > android/app/google-services.json

- name: Build Docker image
id: build
uses: docker/build-push-action@v5
with:
context: .
push: false
load: true
tags: flutter-build:latest
build-args: |
BUILD_DATE=${{ github.event.repository.updated_at }}
VCS_REF=${{ github.sha }}
FLUTTER_VERSION=${{ env.FLUTTER_VERSION }}
ANDROID_SDK_VERSION=${{ env.ANDROID_SDK_VERSION }}
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Extract APK
run: |
docker create --name temp flutter-build:latest
mkdir -p apk-output
docker cp temp:/artifacts/. apk-output/
docker rm temp
ls -lh apk-output/

- name: Upload Android APK artifacts
uses: actions/upload-artifact@v4
with:
name: android-apk-${{ github.sha }}
path: apk-output/*.apk
retention-days: 30
compression-level: 0

- name: Generate Summary
run: |
echo "## πŸ€– Android APK Build Results" >> $GITHUB_STEP_SUMMARY
echo "| Architecture | File | Size |" >> $GITHUB_STEP_SUMMARY
echo "|--------------|----------|------|" >> $GITHUB_STEP_SUMMARY
for apk in apk-output/*.apk; do
name=$(basename "$apk")
size=$(du -h "$apk" | cut -f1)
arch="Universal"
if [[ "$name" == *"arm64"* ]]; then arch="ARM64"; fi
if [[ "$name" == *"armeabi"* ]]; then arch="ARMv7"; fi
if [[ "$name" == *"x86_64"* ]]; then arch="x86_64"; fi
echo "| $arch | \`$name\` | $size |" >> $GITHUB_STEP_SUMMARY
done

# ==========================================
# Job 3: iOS Build (macOS runner)
# ==========================================
build-ios:
name: Build iOS IPA
runs-on: macos-latest
needs: analyze
timeout-minutes: 45

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
channel: 'stable'
cache: true

- name: Create mock configuration files
run: |
mkdir -p lib ios/Runner
# Create firebase_options.dart
echo "import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/foundation.dart'; class DefaultFirebaseOptions { static FirebaseOptions get currentPlatform { if (kIsWeb) throw UnsupportedError('Web not configured'); switch (defaultTargetPlatform) { case TargetPlatform.android: return android; case TargetPlatform.iOS: return ios; default: throw UnsupportedError('Platform not configured'); } } static const FirebaseOptions android = FirebaseOptions(apiKey: 'fake', appId: 'fake', messagingSenderId: 'fake', projectId: 'fake', storageBucket: 'fake'); static const FirebaseOptions ios = FirebaseOptions(apiKey: 'fake', appId: 'fake', messagingSenderId: 'fake', projectId: 'fake', storageBucket: 'fake', iosBundleId: 'fake'); }" > lib/firebase_options.dart
# Create GoogleService-Info.plist
echo '<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict><key>API_KEY</key><string>fake</string><key>GCM_SENDER_ID</key><string>123</string><key>PLIST_VERSION</key><string>1</string><key>BUNDLE_ID</key><string>com.flutter.starter.template.flutterStarterTemplate</string><key>PROJECT_ID</key><string>fake</string><key>STORAGE_BUCKET</key><string>fake</string><key>IS_ADS_ENABLED</key><false/><key>IS_ANALYTICS_ENABLED</key><false/><key>IS_APPINVITE_ENABLED</key><true/><key>IS_GCM_ENABLED</key><true/><key>IS_SIGNIN_ENABLED</key><true/><key>GOOGLE_APP_ID</key><string>1:123:ios:123</string></dict></plist>' > ios/Runner/GoogleService-Info.plist

- name: Get dependencies
run: flutter pub get

- name: Install Pods
run: |
cd ios
rm -rf Pods Podfile.lock
pod install --repo-update

- name: Build iOS app
run: |
flutter build ios --release --no-codesign --build-number=${{ github.run_number }}

- name: Create IPA
run: |
mkdir -p Payload
cp -R build/ios/iphoneos/Runner.app Payload/
IPA_NAME="flutter-starter-ios-${{ github.run_number }}-unsigned.ipa"
zip -r "$IPA_NAME" Payload
rm -rf Payload

- name: Upload iOS IPA artifact
uses: actions/upload-artifact@v4
with:
name: ios-ipa-${{ github.sha }}
path: "*.ipa"
retention-days: 30
compression-level: 0

- name: Generate Summary
run: |
echo "## 🍎 iOS IPA Build Results" >> $GITHUB_STEP_SUMMARY
echo "| File | Size |" >> $GITHUB_STEP_SUMMARY
echo "|------|------|" >> $GITHUB_STEP_SUMMARY
for ipa in *.ipa; do
size=$(du -h "$ipa" | cut -f1)
echo "| \`$ipa\` | $size |" >> $GITHUB_STEP_SUMMARY
done
echo "⚠️ Unsigned IPA for testing only." >> $GITHUB_STEP_SUMMARY
15 changes: 14 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,22 @@ linked_*.ds
unlinked.ds
unlinked_spec.ds

# Build Runner generated
*.g.dart
*.mocks.dart

# Firebase related
firebase_options.dart

# Docker related
*.img
*.tar
*.tar.gz

# APK related
*.apk
apk-output/

# Symbolication related
app.*.symbols

Expand Down Expand Up @@ -130,4 +143,4 @@ ios/Runner/GoogleService-Info.plist
android/app/google-services.json

# Generated localization
lib/core/localization/generated/
lib/core/localization/generated/
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added DI-independent fallback error handler inside runZonedGuarded to ensure reliable error reporting even if DI or Firebase are not ready.
- Added unified analytics architecture with `Firebase Analytics` integration, debug logging, and automatic screen view tracking via `AnalyticsRouteObserver`.
- Added `AppLoggerMixin` for convenient logger access in Bloc/Cubit
- **CI/CD**: Complete automated build pipeline with GitHub Actions
- Multi-stage Dockerfile with Flutter 3.38.4 and Android SDK 35
- Support for split APK by ABI (armeabi-v7a, arm64-v8a, x86_64)
- `.dockerignore` for optimized build context
- GitHub Actions workflow: code analysis, Android (Docker + DockerHub), iOS (macOS runner)
- Automated quality checks, artifact uploads, and build summaries

### Changed

Expand Down
Loading