Skip to content

Instantly share code, notes, and snippets.

@logickoder
Last active November 19, 2024 13:56
Show Gist options
  • Save logickoder/67978325e23d0df326db2d263e6349c0 to your computer and use it in GitHub Desktop.
Save logickoder/67978325e23d0df326db2d263e6349c0 to your computer and use it in GitHub Desktop.
My Flutter CI/CD Workflow
# From .github/workflows/build-production.yml
name: Build and upload android app to Google Play Store
on:
push:
branches:
- main
paths:
- 'android/**'
- 'lib/**'
- 'pubspec.yaml'
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r ./.github/scripts/requirements.txt
- name: set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: 'gradle'
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
channel: "stable"
- run: flutter --version
- name: Install dependencies
run: flutter pub get
- name: Run build runner
run: flutter pub run build_runner build --delete-conflicting-outputs
- name: Build production aab
run: |
python -u .github/scripts/build_android_production.py
- name: Upload aab to Google Drive
uses: logickoder/g-drive-upload@main
with:
credentials: ${{ secrets.GCP_CREDENTIAL }}
filename: app-production.aab
folderId: ${{ secrets.DRIVE_FOLDER_ID }}
overwrite: "true"
mimeType: "application/vnd.android.package-archive"
- name: Deploy aab to Play Store
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.GCP_CREDENTIAL }}
packageName: 'dev.logickoder.app'
releaseFiles: app-production.aab
mappingFile: build/app/outputs/mapping/release/mapping.txt
debugSymbols: build/app/intermediates/merged_native_libs/release/out/lib
inAppUpdatePriority: 5
track: production
status: completed
# From .github/workflows/build-staging.yml
name: Build and upload staging app to Google Drive
on:
push:
branches:
- staging
paths:
- 'android/**'
- 'lib/**'
- 'pubspec.yaml'
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r ./.github/scripts/requirements.txt
- name: set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: 'gradle'
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
channel: "stable"
- run: flutter --version
- name: Install dependencies
run: flutter pub get
- name: Run build runner
run: flutter pub run build_runner build --delete-conflicting-outputs
- name: Build staging apk
run: |
python -u .github/scripts/build_android_staging.py
- name: Upload staging apk to Google Drive
uses: logickoder/g-drive-upload@main
with:
credentials: ${{ secrets.GCP_CREDENTIAL }}
filename: ${{ env.FILE_NAME }}
folderId: ${{ secrets.DRIVE_FOLDER_ID }}
overwrite: "true"
mimeType: "application/vnd.android.package-archive"
# From .github/scripts/build_android.py
import os
import subprocess
from dotenv import load_dotenv
def build(extension=None, environment=None, version_name=None, version_code=None):
'''Builds the android app based on the extension (apk or aab), environment (production or staging) and the version information'''
load_dotenv()
workspace = os.getenv('GITHUB_WORKSPACE')
os.chdir(workspace)
is_actions = os.getenv('GITHUB_ACTIONS') == 'true'
extension = extension or os.getenv('EXTENSION') or 'apk'
environment = environment or os.getenv('ENVIRONMENT') or 'staging'
# Determine the build params from the extension and environment
config_file = "config.staging.json" if environment == "staging" else "config.json"
build_format = "apk" if extension == "apk" else "appbundle"
app_name = "app-staging" if environment == "staging" else "app-production"
version_name = version_name or os.getenv('VERSION_NAME') or "1.0.0"
version_code = version_code or os.getenv('VERSION_CODE') or "1"
# print all the calculated values
print(
f"Build extension: {extension}, Environment: {environment}, Config file: {config_file}, Build format: {build_format}, App name: {app_name}, Version name: {version_name}, Version code: {version_code}")
# Construct the build command
build_command = [
"flutter", "build", build_format,
f"--dart-define-from-file={config_file}",
f"--build-name={version_name}",
f"--build-number={version_code}"
]
if not is_actions:
build_command.insert(0, "/c")
build_command.insert(0, "cmd")
subprocess.run(build_command, check=True)
# Determine the output file paths
if extension == "apk":
file_path = f"build/app/outputs/flutter-apk/app-release.apk"
else:
file_path = f"build/app/outputs/bundle/release/app-release.aab"
# Set the FILE_NAME and move the file
os.rename(file_path, f"{workspace}/{app_name}.{extension}")
with open(os.getenv('GITHUB_ENV') or f"{workspace}\\.github\\env", "a") as env_file:
env_file.write(f"FILE_NAME={app_name}.{extension}\n")
if __name__ == '__main__':
build()
# From .github/scripts/build_android_production.py
from build_android import build
from get_version import get_version
if __name__ == '__main__':
version_name, version_code = get_version()
build(extension='aab', environment='production', version_name=version_name, version_code=version_code)
# From .github/scripts/build_android_staging.py
from build_android import build
from get_version import get_version
if __name__ == '__main__':
version_name, version_code = get_version()
build(extension='apk', environment='staging', version_name=version_name, version_code=version_code)
#!/bin/sh
# From ios/ci_scripts/ci_post_clone.sh
# Runner
# Make sure to setup XCode Cloud and link your repo to trigger the build
# Created by logickoder on 17/09/2024 with help from Dirisu Jesse
#
# Fail this script if any subcommand fails.
#set -e
# The default execution directory of this script is the ci_scripts directory.
cd $CI_PRIMARY_REPOSITORY_PATH # change working directory to the root of your cloned repo.
# Install Flutter using git.
git clone https://github.com/flutter/flutter.git --depth 1 -b stable $HOME/flutter
export PATH="$PATH:$HOME/flutter/bin"
# Install FlutterFire CLI
echo "Installing FlutterFire CLI..."
dart pub global activate flutterfire_cli
export PATH="$PATH":"$HOME/.pub-cache/bin"
# Install Flutter artifacts for iOS (--ios) platform.
flutter precache --ios
# Install Flutter dependencies.
flutter pub get
# Generate files
flutter packages pub run build_runner build --delete-conflicting-outputs
# Install CocoaPods using Homebrew.
HOMEBREW_NO_AUTO_UPDATE=1 # disable homebrew's automatic updates.
brew install cocoapods
# Install CocoaPods dependencies.
pod install # run `pod install` in the `ios` directory.
# Check flutter installation health
flutter doctor -v
# Build iOS app
# xcodebuild -project Runner.xcodeproj -archivePath ../../build/ios/archive/Runner.xcarchive
if [ "$ENVIRONMENT" = "production" ]; then
flutter build ios --release --dart-define-from-file=config.json
else
flutter build ios --release --dart-define-from-file=config.staging.json
fi
# Close out successfully
exit 0
# From .github/scripts/get_version.py
import os
import yaml
from dotenv import load_dotenv
def get_version():
'''Fetches the current app version from pubspec.yaml or 1.0.0+1 if it can't retrieve it'''
load_dotenv()
os.chdir(os.getenv('GITHUB_WORKSPACE'))
with open("pubspec.yaml", 'r') as stream:
try:
pubspec = yaml.safe_load(stream)
version = pubspec.get('version', '1.0.0+1')
version_name, version_code = version.split('+')
return version_name, version_code
except yaml.YAMLError as exc:
print(f"::error ::{exc}")
return "1.0.0", "1"
if __name__ == '__main__':
get_version()
# From .github/scripts/requirements.txt
python-dotenv==1.0.1
PyYAML==6.0.2
@logickoder
Copy link
Author

My complete pipeline for building and pushing my flutter app to play and app store.

You can tweak to your usecase.

iOS is run with only the ci_post_clone.sh, you have to link the repo to Xcode Cloud for it to work.
Android is run with the other files.

You can notice at the top of each file a comment #From ${path}, path represents where the files should be placed relative to the root of your flutter project

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment