[Flutter] Deploy Everything #1
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |