Skip to content

[Flutter] Deploy Everything #7

[Flutter] Deploy Everything

[Flutter] Deploy Everything #7

name: "[Flutter] Deploy Everything"
on:
pull_request:
types: [closed]
branches:
- main
paths:
- "android/**"
- "assets/**"
- "ios/**"
- "lib/**"
- "web/**"
- "test/**"
- "pubspec.yaml"
- "analysis_options.yaml"
workflow_dispatch:
inputs:
android:
description: "Deploy Android?"
required: true
type: boolean
default: true
ios:
description: "Deploy iOS?"
required: true
type: boolean
default: true
macos:
description: "Deploy macOS?"
required: true
type: boolean
default: true
cloudflare:
description: "Deploy to Cloudflare Pages?"
required: true
type: boolean
default: true
# github_pages:
# description: "Deploy to GitHub Pages?"
# required: true
# type: boolean
# default: true
# firebase_hosting:
# description: "Deploy to Firebase Hosting?"
# required: true
# type: boolean
# default: true
patch:
description: "Make it an instant patch release?"
required: true
type: boolean
default: false
skip:
description: "Increment build number instead?"
required: true
type: boolean
default: false
version:
description: "Custom version (format: 1.1.1 or 1.1.1+1):"
required: false
type: string
default: ""
permissions:
contents: read
pull-requests: write
concurrency:
group: "deploy"
cancel-in-progress: false
jobs:
version:
permissions:
contents: write
name: Create version number
environment:
name: app-store
if: ${{ github.event.pull_request.merged == true || (github.event_name == 'workflow_dispatch' && github.event.inputs.skip != 'true') }}
# Works only on ubuntu
runs-on: ubuntu-latest
outputs:
build_number: ${{ steps.version.outputs.build_number }}
full_version: ${{ steps.version.outputs.full_version }}
version: ${{ steps.version.outputs.version }}
app_id: ${{ steps.extract_app_id.outputs.app_id }}
firebase_id: ${{ steps.firebase_id.outputs.firebase_id }}
tag: ${{ steps.tag.outputs.tag }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get version from branch name or merge commit
if: ${{ github.event.inputs.version == '' && github.event.inputs.patch != 'true' }}
run: |
version=""
if [[ "${GITHUB_REF#refs/heads/}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Branch name matches the pattern *.*.* - stopping"
echo "${GITHUB_REF#refs/heads/}+${{ github.run_number }}" > version.txt
exit 0
fi
echo "Branch name doesn't match the pattern *.*.* - continuing"
# Initialize a variable to keep track of the number of merge commits checked
count=0
# Loop until a matching merge commit is found
while [[ ! -n $version ]]; do
# Get the latest merge commit message starting from the most recent one and moving backwards
commit_message=$(git log --merges -1 --skip=$count --pretty=%B)
# Check if the commit message contains a version number in the format *.*.*
if [[ $commit_message =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then
# Extract the version number
echo "Found a version number in the commit message"
version=$(echo "$commit_message" | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1)
break
fi
# Increment the counter to check the next merge commit in the next iteration
count=$((count + 1))
# Optional: Add a condition to exit the loop if too many commits have been checked
if [ $count -gt 100 ]; then
echo "Latest merge isn't from a branch matching *.*.* - Either repo rules are set wrong or somebody bypassed protections or it was ran from a different branch than main or version branch"
exit 1
fi
done
echo "$version+${{ github.run_number }}"
echo "$version+${{ github.run_number }}" > version.txt
- name: Get version from yaml
if: ${{ github.event.inputs.version == '' && github.event.inputs.patch == 'true' }}
run: |
sed -n 's/^version: \(.*\)$/\1/p' pubspec.yaml > version.txt
- name: Get version from input (and build number if not provided)
if: ${{ github.event.inputs.version != '' }}
run: echo "${{ github.event.inputs.version }}" | sed 's/^[^+]*$/&+${{ github.run_number }}/' > version.txt
- name: Update version in YAML
run: |
sed -i "s/^version: .*/version: $(tr -d '\n' < version.txt)/" pubspec.yaml
- name: Upload version
uses: actions/upload-artifact@v4
with:
name: version
path: version.txt
- name: Set version as variables
id: version
run: |
# Extract version, build_number, and full_version
version=$(sed 's/\+.*//' version.txt | tr -d '\n')
build_number=$(sed 's/.*+//' version.txt | tr -d '\n')
full_version=$(cat version.txt | tr -d '\n')
# Log to the console
echo "version: $version"
echo "build_number: $build_number"
echo "full_version: $full_version"
# Output to $GITHUB_OUTPUT
{
echo "version=$version"
echo "build_number=$build_number"
echo "full_version=$full_version"
} >> "$GITHUB_OUTPUT"
- name: Upload pubspec.yaml
uses: actions/upload-artifact@v4
with:
name: pubspec
path: pubspec.yaml
- name: Get applicationId
id: extract_app_id
run: |
application_id=$(grep -oP 'applicationId\s*=\s*"\K[^"]+' android/app/build.gradle)
echo "app_id=$application_id"
echo "app_id=$application_id" >> "$GITHUB_OUTPUT"
- name: Get Firebase app id
id: firebase_id
run: |
firebase_id=$(grep -A 2 'static const FirebaseOptions android' lib/firebase_options.dart | grep 'appId' | awk -F"'" '{print $2}')
echo "firebase_id=$firebase_id"
echo "firebase_id=$firebase_id" >> "$GITHUB_OUTPUT"
- name: Create version tag
id: tag
run: |
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git config --global user.name "github-actions[bot]"
git add pubspec.yaml
git commit -m "Update app version to $(cat version.txt)" || true
VERSION_PREFIX="$(cat version.txt)"
# Get the highest existing patch tag, if any
if [ "${{ github.event.inputs.patch }}" == "true" ]; then
HIGHEST_TAG=$(git tag -l "${VERSION_PREFIX}-patch*" | sort -V | tail -n 1)
# Check if a tag was found
if [ -z "$HIGHEST_TAG" ]; then
# No tags found, create the first patch tag
NEW_TAG="${VERSION_PREFIX}-patch1"
else
# Extract the patch number and increment it
LAST_PATCH_NUMBER=$(echo "$HIGHEST_TAG" | sed 's/.*patch\([0-9]*\).*/\1/')
NEW_PATCH_NUMBER=$((LAST_PATCH_NUMBER + 1))
NEW_TAG="${VERSION_PREFIX}-patch${NEW_PATCH_NUMBER}"
fi
else
NEW_TAG="${VERSION_PREFIX}"
fi
git tag $NEW_TAG # Create the tag
git push -u origin $NEW_TAG # Push the tag
echo "tag=$NEW_TAG" >> "$GITHUB_OUTPUT"
- name: Run iOS Deployment
if: ${{ github.event.pull_request.merged == true || github.event.inputs.ios == 'true' }}
run: |
LABEL=""
if [[ "${{ github.event.inputs.patch }}" == "true" ]]; then
LABEL="\"labels\": [\"patch\"],"
fi
PAYLOAD=$(cat <<EOF
{
"appId": "${{ vars.CODEMAGIC_APP_ID }}",
"workflowId": "deploy_ios",
"tag": "${{ steps.tag.outputs.tag }}",
$LABEL
"environment": {
"variables": {
"PATCH": "${{ github.event.inputs.patch }}",
"FLUTTER_VERSION": "${{ vars.FLUTTER_VERSION }}"
},
"softwareVersions": {
"flutter": "${{ vars.FLUTTER_VERSION }}"
}
}
}
EOF
)
curl -H "Content-Type: application/json" \
-H "x-auth-token: ${{ secrets.CODEMAGIC_TOKEN }}" \
--data "$PAYLOAD" \
-X POST https://api.codemagic.io/builds
- name: Run macOS Deployment
if: ${{ (github.event.pull_request.merged == true || github.event.inputs.macos == 'true') && github.event.inputs.patch != 'true' }}
run: |
PAYLOAD=$(cat <<EOF
{
"appId": "${{ vars.CODEMAGIC_APP_ID }}",
"workflowId": "deploy_macos",
"tag": "${{ steps.tag.outputs.tag }}",
"environment": {
"softwareVersions": {
"flutter": "${{ vars.FLUTTER_VERSION }}"
}
}
}
EOF
)
curl -H "Content-Type: application/json" \
-H "x-auth-token: ${{ secrets.CODEMAGIC_TOKEN }}" \
--data "$PAYLOAD" \
-X POST https://api.codemagic.io/builds
build_web:
name: "[Web] Build Web"
if: ${{ github.event.pull_request.merged == true || github.event.inputs.cloudflare == 'true' || github.event.inputs.github_pages == 'true' || github.event.inputs.firebase_hosting == 'true' }}
needs: version
# Works on ubuntu and macOS. Should propably work on windows as well
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Get pubspec
uses: actions/download-artifact@v4
with:
name: pubspec
- uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: ${{ vars.FLUTTER_VERSION }}
cache: true
- name: Build web
run: flutter build web --release --source-maps
- name: Sentry symbols upload
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
run: |
dart run sentry_dart_plugin
- name: Copy index.html to 404.html
run: cp build/web/index.html build/web/404.html
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
include-hidden-files: true
name: web-build
path: build/web
deploy_cloudflare:
name: "[Web] Deploy to Cloudflare Pages"
if: ${{ github.event.pull_request.merged == true || github.event.inputs.cloudflare == 'true' }}
needs: build_web
# Works on ubuntu and macOS. Should propably work on windows as well
runs-on: ubuntu-latest
environment:
name: cloudflare-pages
url: ${{ vars.CLOUDFLARE_PAGES_URL }}
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup git
run: |
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git config --global user.name "github-actions[bot]"
git fetch origin dist-cloudflare-pages:dist-cloudflare-pages || true
git checkout -b dist-cloudflare-pages || true
git checkout dist-cloudflare-pages || true
find . -maxdepth 1 ! -name ".git" ! -name "." -exec rm -rf {} +
- name: Download build artifact
uses: actions/download-artifact@v4
with:
name: web-build
- name: Deploy to Cloudflare Pages
run: |
# Push the new branch to the repository
git add .
git commit -m "Built web"
git push origin dist-cloudflare-pages
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Wait for deployment
run: |
sleep 30
- name: Purge cache
run: |
curl -X POST "https://api.cloudflare.com/client/v4/zones/${{ vars.CLOUDFLARE_ZONE_ID }}/purge_cache" \
-H "Authorization: Bearer ${{ secrets.CLOUDFLARE_PURGE_KEY }}" \
-H "Content-Type: application/json" \
--data '{"purge_everything": true}'
deploy_firebase:
name: "[Web] Deploy to Firebase Hosting"
runs-on: ubuntu-latest
if: false #${{ github.event.pull_request.merged == true || github.event.inputs.firebase_hosting == 'true' }}
environment:
name: firebase-hosting
url: ${{ vars.FIREBASE_HOSTING_URL }}
needs: build_web
permissions:
contents: read
checks: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Get build artifact
uses: actions/download-artifact@v4
with:
name: web-build
path: build/web
- name: Deploy to Firebase Hosting
uses: FirebaseExtended/action-hosting-deploy@v0
with:
repoToken: ${{ secrets.GITHUB_TOKEN }}
firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_APP_ELEVATE_CORE }}
channelId: live
projectId: app-elevate-core
deploy_github_pages:
name: "[Web] Deploy to GitHub Pages"
if: false # ${{ github.event.pull_request.merged == true || github.event.inputs.github_pages == 'true' }}
permissions:
contents: read
pages: write
id-token: write
needs: build_web
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Get build artifact
uses: actions/download-artifact@v4
with:
name: web-build
path: build/web
- name: Setup Pages
uses: actions/configure-pages@v5
- name: Get custom domain from GitHub API
id: get_custom_domain
run: |
CUSTOM_DOMAIN=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
https://api.github.com/repos/${{ github.repository }}/pages | jq -r .cname)
if [ "$CUSTOM_DOMAIN" != "null" ]; then
echo "CUSTOM_DOMAIN=$CUSTOM_DOMAIN"
echo "CUSTOM_DOMAIN=$CUSTOM_DOMAIN" >> $GITHUB_ENV
fi
- name: Rewrite base href
run: |
if [ ! -n "$CUSTOM_DOMAIN" ]; then
sed -i "s|<base href=\"/\">|<base href=\"/$(basename $GITHUB_REPOSITORY)/\">|" build/web/index.html
fi
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
# Upload entire repository
path: "./build/web"
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
deploy_android:
name: "[Android] Deploy to Google Play Store"
environment:
name: google-play
url: https://play.google.com/store/apps/details?id=${{ needs.version.outputs.app_id }}
needs: version
if: ${{ github.event.pull_request.merged == true || github.event.inputs.android == 'true' }}
# Works on ubuntu and macOS.
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Get pubspec
uses: actions/download-artifact@v4
with:
name: pubspec
- name: Download Android keystore
id: android_keystore
uses: timheuer/base64-to-file@v1.2.4
with:
fileName: upload-keystore.jks
encodedString: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
- name: Create key.properties
run: |
echo "storeFile=${{ steps.android_keystore.outputs.filePath }}" > android/key.properties
echo "storePassword=${{ secrets.ANDROID_STORE_PASSWORD }}" >> android/key.properties
echo "keyPassword=${{ secrets.ANDROID_KEY_PASSWORD }}" >> android/key.properties
echo "keyAlias=${{ secrets.ANDROID_KEY_ALIAS }}" >> android/key.properties
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: "zulu"
java-version: "17.x"
cache: "gradle"
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: ${{ vars.FLUTTER_VERSION }}
cache: true
- name: 🐦 Setup Shorebird
uses: shorebirdtech/setup-shorebird@v1
- name: 🚀 Use Shorebird
if: ${{ github.event.inputs.patch != 'true' }}
env:
SHOREBIRD_TOKEN: ${{ secrets.SHOREBIRD_TOKEN }}
run: shorebird release android --flutter-version=${{ vars.FLUTTER_VERSION }} -- --split-debug-info="symbolsAndroid" --obfuscate
- name: 🚀 Use Shorebird
if: ${{ github.event.inputs.patch == 'true' }}
env:
SHOREBIRD_TOKEN: ${{ secrets.SHOREBIRD_TOKEN }}
run: shorebird patch android --release-version=${{ needs.version.outputs.full_version }} -- --split-debug-info="symbolsAndroid" --obfuscate
- name: Sentry symbols upload
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
run: |
dart run sentry_dart_plugin
- name: Symbols extraction
run: |
PATH_TO_NATIVE_SYMBOLS="build/app/intermediates/merged_native_libs/release/mergeReleaseNativeLibs/out/lib"
CURRENT_EXECUTION_PATH=$(pwd)
cd $PATH_TO_NATIVE_SYMBOLS
zip -r symbols.zip .
cd $CURRENT_EXECUTION_PATH
mv $PATH_TO_NATIVE_SYMBOLS/symbols.zip build/symbols.zip
- uses: vacxe/google-play-cli-kt@master
if: ${{ github.event.inputs.patch != 'true' }}
env:
JAVA_HOME: ""
with:
service-account-json: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT }}
package-name: ${{needs.version.outputs.app_id}}
custom-script: "sh scripts/ci/deploy_google_play.sh build/app/outputs/bundle/release/app-release.aab ${{ needs.version.outputs.build_number }} internal draft build/app/outputs/mapping/release/mapping.txt build/symbols.zip"
- name: Release Artifact Upload
uses: actions/upload-artifact@v4
with:
name: android-release
path: build/app/outputs/bundle/release/app-release.aab
- name: Debug symbols Artifact Upload
uses: actions/upload-artifact@v4
with:
name: symbolsAndroid
path: symbolsAndroid
symbols_android:
name: "[Android] Upload Crashlytics Symbols"
# Doesn't work on ubuntu, Windows works sometimes but it's super slow. macOS is the best option even with the 10x
runs-on: macos-latest
needs:
- version
- deploy_android
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Get Android debug symbols from artifacts
uses: actions/download-artifact@v4
with:
name: symbolsAndroid
path: symbolsAndroid
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install Firebase CLI
run: npm install -g firebase-tools
- name: Upload Crashlytics Symbols
run: |
firebase crashlytics:symbols:upload --non-interactive --app=${{needs.version.outputs.firebase_id}} symbolsAndroid