- Published on
Flutter CI/CD with Codemagic and AWS CodeCommit
- Authors

- Name
- Phat Tran
The entire release process for our Flutter super app hangs on one convention: push a git tag that matches build/<env>-vX.Y.Z+N, and Codemagic takes it from there. It builds the right flavor, signs it, sends the Android build to Firebase App Distribution and the iOS build to TestFlight, then reports to Telegram.
I set this pipeline up for FE Online 2.0. In the config below the project is renamed to Acme App, but the rest is the real thing: a Flutter monorepo managed with melos, hosted on AWS CodeCommit, with three environments (UAT, STAGING, PRODUCTION).
The trigger format:
build/<env>-v{versionNumber}+{buildNumber}
# Example
build/uat-v2.5.0+120
Connecting CodeCommit
Codemagic does not treat CodeCommit as a first-class provider, so the connection goes through SSH.
- In Codemagic, click Add application, pick Other, then Connect via SSH. Paste the repo's SSH URL and select Flutter as the project type.
- Create an SSH key pair as described in Connecting repository via SSH. The public key goes into AWS IAM, the private key into Codemagic.
- In CodeCommit, under Settings → Triggers, add a webhook pointing at the Codemagic endpoint so tag pushes actually reach Codemagic. The steps are in Setting up webhooks for AWS CodeCommit.
iOS signing
Two one-time jobs at team level.
First, connect the Apple Developer Portal: Teams → General settings → Team integrations → Developer Portal → Connect, then upload an App Store Connect API key. Signing in with an Apple ID also works, but the API key is the recommended route.
Second, upload the signing assets under Teams → Code signing identities: the distribution certificate as a .p12 and the distribution provisioning profile as a .mobileprovision. Codemagic decrypts and installs both during every iOS build, so nobody touches a keychain by hand.
Secrets
Under Teams → Global variables and secrets we keep two groups. firebase_credentials holds FIREBASE_SERVICE_ACCOUNT, which the Android workflow uses to publish builds. Telegram holds TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID, and TELEGRAM_TOPIC_ID for the build notifications. Codemagic stores the values AES-encrypted and redacts them from build logs.
The codemagic.yaml
Create codemagic.yaml in the repo root. Below are the two UAT workflows, Android and iOS. The staging and production workflows are copies with the flavor-specific lines changed, so I will spare you the repetition.
workflows:
android-uat-workflow:
name: android-acme-app-uat
instance_type: mac_mini_m2
max_build_duration: 120
environment:
groups:
- firebase_credentials
- Telegram
vars:
PACKAGE_NAME: 'com.acme.app.uat'
APP_SCHEME: 'uat'
JAVA_TOOL_OPTIONS: '-Xmx5g'
flutter: 3.13.3
triggering:
events:
- tag
tag_patterns:
- pattern: 'build/uat-*'
include: true
scripts:
- name: Initialize development environment
script: |
flutter pub global activate melos
flutter pub global run melos clean
flutter pub global run melos bootstrap
sh gen.sh
- name: Configure script permissions
script: |
chmod +x ./scripts/*.sh
- name: Process version tag
script: |
./scripts/cicd_extract_tag.sh
- name: Build UAT APK
script: |
cd apps/mobile_app
if flutter build apk --release \
--flavor uat \
--build-name=$APP_BUILD_VERSION \
--build-number=$APP_BUILD_NUMBER; then
echo "BUILD_STATUS=success" >> $CM_ENV
else
echo "BUILD_STATUS=failed" >> $CM_ENV
exit 1
fi
artifacts:
- apps/mobile_app/build/**/outputs/**/*.apk
- apps/mobile_app/build/**/outputs/**/mapping.txt
- flutter_drive.log
publishing:
firebase:
firebase_service_account: $FIREBASE_SERVICE_ACCOUNT
android:
app_id: 1:123456789012:android:0a1b2c3d4e5f67890abcde # from google-services.json
groups:
- all
artifact_type: 'apk'
scripts:
- name: Send build report to Telegram
script: |
./scripts/cicd_send_sms_telegram.sh
ios-uat-workflow:
name: ios-acme-app-uat
instance_type: mac_mini_m2
max_build_duration: 120
integrations:
app_store_connect: Acme Codemagic App Manager API Key
environment:
ios_signing:
distribution_type: app_store
bundle_identifier: com.acme.app.uat
vars:
APP_ID: 1234567890 # numeric Apple ID of the app in App Store Connect
APP_SCHEME: 'uat'
groups:
- Telegram
flutter: 3.13.3
xcode: 16.2
cocoapods: default
triggering:
events:
- tag
tag_patterns:
- pattern: 'build/uat-*'
include: true
scripts:
- name: Configure Xcode code signing
script: |
xcode-project use-profiles
- name: Initialize development environment
script: |
flutter pub global activate melos
flutter pub global run melos clean
flutter pub global run melos bootstrap
sh gen.sh
- name: Configure script permissions
script: |
chmod +x ./scripts/*.sh
- name: Process version tag
script: |
./scripts/cicd_extract_tag.sh
- name: Install Flutter dependencies
script: |
cd apps/mobile_app
flutter pub get
- name: Setup iOS dependencies
script: |
cd apps/mobile_app/ios
pod deintegrate
pod install --repo-update
- name: Build UAT IPA
script: |
cd apps/mobile_app
if flutter build ipa --release \
--build-name=$APP_BUILD_VERSION \
--build-number=$APP_BUILD_NUMBER \
--export-options-plist=/Users/builder/export_options.plist \
--flavor uat; then
echo "BUILD_STATUS=success" >> $CM_ENV
else
echo "BUILD_STATUS=failed" >> $CM_ENV
exit 1
fi
artifacts:
- apps/mobile_app/build/ios/ipa/*.ipa
- $CM_BUILD_DIR/build/ios/ipa/*.ipa
- /tmp/xcodebuild_logs/*.log
- flutter_drive.log
publishing:
app_store_connect:
auth: integration
submit_to_testflight: true
scripts:
- name: Send build report to Telegram
script: |
./scripts/cicd_send_sms_telegram.sh
The two scripts doing the real work
The yaml references two shell scripts, and the tag parser is the heart of the whole convention, so here they are, trimmed to their essentials.
cicd_extract_tag.sh turns the tag into build variables. Codemagic puts the triggering tag into CM_TAG, and anything you append to the file at $CM_ENV becomes an environment variable for every later step. That is how --build-name and --build-number in the build step get their values:
#!/bin/sh
# CM_TAG holds the tag that triggered the build, e.g. build/uat-v2.5.0+120
VERSION_PART="${CM_TAG##*-v}" # 2.5.0+120
echo "APP_BUILD_VERSION=${VERSION_PART%%+*}" >> $CM_ENV # 2.5.0
echo "APP_BUILD_NUMBER=${VERSION_PART##*+}" >> $CM_ENV # 120
Push build/uat-v2.5.0+120 and the pipeline builds version 2.5.0 with build number 120. The version lives in the tag and nowhere else, so there is no version constant in the repo to forget to bump.
cicd_send_sms_telegram.sh closes the loop. It reads the BUILD_STATUS value that the build step wrote to $CM_ENV and posts the outcome to the team chat:
#!/bin/sh
if [ "$BUILD_STATUS" = "success" ]; then
TEXT="✅ $APP_SCHEME $APP_BUILD_VERSION+$APP_BUILD_NUMBER is on its way to testers"
else
TEXT="❌ $APP_SCHEME $APP_BUILD_VERSION+$APP_BUILD_NUMBER failed — check the Codemagic logs"
fi
curl -s "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
-d chat_id="$TELEGRAM_CHAT_ID" \
-d message_thread_id="$TELEGRAM_TOPIC_ID" \
-d text="$TEXT" > /dev/null
A few parts that are easy to miss
instance_type: mac_mini_m2is Apple Silicon, which builds faster. For the Android-only workflow alinux_x2instance would also do the job at a lower per-minute rate; we kept both workflows on the same instance type for simplicity.tag_patternsis the whole trigger. Only tags matchingbuild/uat-*start these workflows, which is how one repo carries three environments without the workflows stepping on each other.- The melos steps exist because this is a monorepo. A plain
flutter pub getat the root would not link the packages together. - The
BUILD_STATUSvalue written to$CM_ENVfeeds the Telegram script inpublishing.scripts, so the chat message can say whether the build actually succeeded. artifactslists what survives after the build VM is destroyed: the APK or IPA, mapping files, logs.publishing.firebasehands the APK to the tester group.publishing.scriptsruns afterwards, which is where the Telegram notification goes, and where a JIRA update or GitHub release would go too.
Day to day, the routine is short. Finish the work, push a tag like build/uat-v2.5.0+120, and go make coffee. By the time you are back, Telegram either confirms the build went out or tells you what to go fix.