① 배운 것
지금까지는 안드로이드, ios를 각각의 workflow로 빌드 및 앱 디스트리뷰션에 배포했습니다.
이제는 두개의 workflow를 하나로 합치고 플러터를 다운 받은 후 깃헙액션 캐시에 저장해서 다음에 캐시에서 플러터를 가져와서 사용할 수 있게 해봅시다
name: Android CD to Firebase App distribution [flutter build apk --release --flavor dev]
env:
GRADLE_OPTS: "-Dorg.gradle.jvmargs=-Xmx4g -Dorg.gradle.daemon=false"
GRADLE_BUILD_ACTION_CACHE_DEBUG_ENABLED: true
on:
workflow_dispatch:
push:
branches:
- develop_test
paths:
- '.github/workflows/firebase_app_distribution_dev.yml'
permissions:
contents: write
jobs:
cd-build:
runs-on: ubuntu-latest
steps:
# 디스크 공간 확보를 위한 cleanup steps
- name: Cleanup Disk Space
run: |
# 사용하지 않는 큰 패키지들 제거
sudo rm -rf /usr/share/dotnet
sudo rm -rf /opt/ghc
sudo rm -rf /usr/local/share/boost
# 패키지 캐시 정리
sudo apt-get clean
# 디스크 사용량 확인
df -h
- name: Checkout
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
distribution: 'corretto'
java-version: 17
- name: Generate key.properties
run: |
echo '${{ secrets.KEY_PROPERTIES }}' | base64 --d >> ./android/key.properties
- name: Generate local.properties
run: |
echo '${{ secrets.LOCAL_PROPERTIES }}' | base64 --d >> ./android/local.properties
- name: Generate google-services.json
run: echo '${{ secrets.GOOGLE_SERVICE_JSON }}' | base64 --d > ./android/app/google-services.json
- name: Generate keystore.jks
run: echo '${{ secrets.JKS }}' | base64 --d > ./android/app/hiing-keystore.jks
- name: Extract Version
run: |
VERSION_NAME=$(grep 'version:' pubspec.yaml | sed -e 's/version: //' -e 's/+.*//')
VERSION_CODE=$(grep 'version:' pubspec.yaml | sed -e 's/version: //' -e 's/.*+//')
echo "VERSION_NAME=$VERSION_NAME" >> $GITHUB_ENV
echo "VERSION_CODE=$VERSION_CODE" >> $GITHUB_ENV
id: extract_version
- name: setup flutter
uses: subosito/flutter-action@v1
with:
channel: 'stable'
- name: setup flutter jdk directory
run: |
flutter config --jdk-dir /opt/hostedtoolcache/Java_Corretto_jdk/17.0.13-11.1/x64
- name: Configure Git
run: |
git config --global url."https://${{ vars.USER_NAME }}:${{ secrets.TOKEN_GITHUB }}@github.com/".insteadOf "https://github.com/"
- name: install dependecies
run: flutter pub get
- name: Generate config
run: |
echo '${{ secrets.CONFIG }}' | base64 --d >> ./lib/common/config.dart
- name: Build Release APK
run: |
flutter build apk --release --flavor dev
- name: Upload Release Build to Artifacts
uses: actions/upload-artifact@v3
with:
name: debug-artifacts
path: build/app/outputs/flutter-apk/app-dev-release.apk
if-no-files-found: error
- name: Create Github Release
uses: softprops/action-gh-release@v1
with:
tag_name: "${{ env.VERSION_NAME }}(${{ env.VERSION_CODE }})"
release_name: "${{ env.VERSION_NAME }}(${{ env.VERSION_CODE }})"
generate_release_notes: true
files: |
build/app/outputs/flutter-apk/app-dev-release.apk
- name: Upload artifact to Firebase App Distribution
uses: wzieba/Firebase-Distribution-Github-Action@v1
id: upload
with:
appId: ${{secrets.FIREBASE_APP_ID_ANDROID_DEV}}
serviceCredentialsFileContent: ${{ secrets.CREDENTIAL_FILE_CONTENT }}
groups: ${{ vars.APP_DISTRIBUTION_GROUPS }}
file: build/app/outputs/flutter-apk/app-dev-release.apk
- name: notify
if: always()
run: |
STATUS="${{ job.status }}"
VERSION="${{ env.VERSION_NAME }}(${{ env.VERSION_CODE }})"
if [[ "$STATUS" == "success" ]] ; then
EMOJI="🎉"
else
EMOJI="😵💫"
fi
curl -X POST https://slack.com/api/chat.postMessage \
-H "Authorization: ${{ secrets.SLACK_TOKEN }}" \
-H "Content-Type: application/json" \
-d '{
"channel": "${{ vars.SLACK_CHANNEL_ID }}",
"text": "[App distribution] 안드로이드 배포 '"${STATUS}"''"${EMOJI}"'\n버전 👉 '"${VERSION}"'"
}'
name: "[DEV] Build and Publish iOS"
on:
workflow_dispatch:
push:
branches:
- develop_test
paths:
- '.github/workflows/ios_firebase_cd.yml'
permissions:
contents: write
jobs:
build:
runs-on: macos-latest
outputs:
version_name: ${{ env.VERSION_NAME }}
version_code: ${{ env.VERSION_CODE }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Install the Apple certificate and provisioning profile
- name: Install the Apple certificate and provisioning profile
env:
BUILD_CERTIFICATE_BASE64: ${{ secrets.APPSTORE_CERT_BASE64 }}
P12_PASSWORD: ${{ secrets.APPSTORE_CERT_PASSWORD }}
BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.MOBILEPROVISION_BASE64 }}
BUILD_PROVISION_NOTI_PROFILE_BASE64: ${{ secrets.NOTI_MOBILEPROVISION_BASE64 }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
# 변수 생성
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
PP_PATH=$RUNNER_TEMP/hiing_github_action_2.mobileprovision
PP_NOTI_PATH=$RUNNER_TEMP/hiing_github_action_noti_2.mobileprovision
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
#secrets으로 부터 인증서와 프로비저닝 프로파일 가져오기
echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode --output $CERTIFICATE_PATH
echo -n "$BUILD_PROVISION_PROFILE_BASE64" | base64 --decode --output $PP_PATH
echo -n "$BUILD_PROVISION_NOTI_PROFILE_BASE64" | base64 --decode --output $PP_NOTI_PATH
# 키체인 초기화 - 임시 키체인 생성
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
# import certificate to keychain
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
# provisioning profile 적용
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles
cp $PP_NOTI_PATH ~/Library/MobileDevice/Provisioning\ Profiles
# 디버깅
echo "Listing provisioning profiles:"
ls -l "$HOME/Library/MobileDevice/Provisioning Profiles/"
- name: Extract Version
run: |
VERSION_NAME=$(grep 'version:' pubspec.yaml | sed -e 's/version: //' -e 's/+.*//')
VERSION_CODE=$(grep 'version:' pubspec.yaml | sed -e 's/version: //' -e 's/.*+//')
echo "VERSION_NAME=$VERSION_NAME" >> $GITHUB_ENV
echo "VERSION_CODE=$VERSION_CODE" >> $GITHUB_ENV
id: extract_version
# Install flutter
- name: Flutter get
uses: subosito/flutter-action@v1
with:
channel: 'stable'
- name: Configure Git
run: |
git config --global url."https://${{ vars.USER_NAME }}:${{ secrets.TOKEN_GITHUB }}@github.com/".insteadOf "https://github.com/"
# Install your project's dependencies
- run: flutter pub get
# Make config.dart
- name: Generate config
run: |
echo '${{ secrets.CONFIG }}' | base64 --d >> ./lib/common/config.dart
# Build and sign the ipa using a single flutter command
- name: Building IPA
run: flutter build ipa --export-options-plist=ios/Runner/ExportOptions.plist --flavor dev
# Collect the file and upload as artifact
- name: collect ipa artifacts
uses: actions/upload-artifact@v4
with:
name: debug-ipa
path: build/ios/ipa/*.ipa
if-no-files-found: error
# Important! Cleanup: remove the certificate and provisioning profile from the runner!
- name: Clean up keychain and provisioning profile
if: ${{ always() }}
run: |
security delete-keychain $RUNNER_TEMP/app-signing.keychain-db
rm ~/Library/MobileDevice/Provisioning\ Profiles/hiing_github_action_2.mobileprovision
rm ~/Library/MobileDevice/Provisioning\ Profiles/hiing_github_action_noti_2.mobileprovision
- name: Create Github Release
uses: softprops/action-gh-release@v1
with:
tag_name: "${{ env.VERSION_NAME }}(${{ env.VERSION_CODE }})"
release_name: "${{ env.VERSION_NAME }}(${{ env.VERSION_CODE }})"
generate_release_notes: true
files: |
build/ios/ipa/*.ipa
# Release job, upload the ipa to Firebase App Distribution
# Container action is only supported on Linux
release:
name: Release ipa to Firebase
needs: [ build ]
runs-on: ubuntu-latest
steps:
# Retrieve ipa file from GitHub artifacts
- uses: actions/checkout@v4
- name: Get release-ipa from artifacts
uses: actions/download-artifact@v4
with:
name: debug-ipa
# Upload ipa file to Firebase app distribution.
- name: Upload artifact to Firebase App Distribution
uses: wzieba/Firebase-Distribution-Github-Action@v1
with:
appId: ${{secrets.FIREBASE_APP_ID_IOS_DEV}}
serviceCredentialsFileContent: ${{ secrets.CREDENTIAL_FILE_CONTENT }}
groups: ${{ vars.APP_DISTRIBUTION_GROUPS }}
file: hiing.ipa
notify:
name: Slack Notification
needs: [ build, release ]
if: always()
runs-on: ubuntu-latest
steps:
- name: Send Slack Notification
run: |
BUILD_STATUS="${{ needs.build.result }}"
RELEASE_STATUS="${{ needs.release.result }}"
VERSION="${{ needs.build.outputs.version_name }}(${{ needs.build.outputs.version_code }})"
# 전체 상태 확인 (하나라도 실패하면 failure)
if [[ "$BUILD_STATUS" == "success" ]] && [[ "$RELEASE_STATUS" == "success" ]]; then
FINAL_STATUS="SUCCESS"
else
FINAL_STATUS="FAILURE"
fi
if [[ "$FINAL_STATUS" == "SUCCESS" ]] ; then
EMOJI="🎉"
else
EMOJI="😵💫"
fi
curl -X POST https://slack.com/api/chat.postMessage \
-H "Authorization: ${{ secrets.SLACK_TOKEN}}" \
-H "Content-Type: application/json" \
-d '{
"channel": "${{ vars.SLACK_CHANNEL_ID }}",
"text": "[IOS distribution] IOS 배포 '"${FINAL_STATUS}"''"${EMOJI}"'\n버전 👉 '"${VERSION}"'"
}'
name: Android, IOS Build and Deploy to Firebase App Distribution
on:
workflow_dispatch:
push:
branches:
- develop_test
permissions:
contents: write
jobs:
build:
strategy:
matrix:
platform: [android, ios]
include:
- platform: android
runs-on: ubuntu-latest
flutter_path: /opt/hostedtoolcache/flutter
- platform: ios
runs-on: macos-latest
flutter_path: /Users/runner/hostedtoolcache/flutter
fail-fast: false
runs-on: ${{ matrix.runs-on }}
outputs:
version_name: ${{ env.VERSION_NAME }}
version_code: ${{ env.VERSION_CODE }}
android_build_status: ${{ matrix.platform == 'android' && job.status || '' }}
ios_build_status: ${{ matrix.platform == 'ios' && job.status || '' }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Cache Flutter dependencies
uses: actions/cache@v3
with:
path: ${{ matrix.flutter_path }}
key: ${{ runner.os }}-flutter-${{ hashFiles('**/pubspec.lock') }}
restore-keys: |
${{ runner.os }}-flutter-
- name: Extract Version
run: |
VERSION_NAME=$(grep 'version:' pubspec.yaml | sed -e 's/version: //' -e 's/+.*//')
VERSION_CODE=$(grep 'version:' pubspec.yaml | sed -e 's/version: //' -e 's/.*+//')
echo "VERSION_NAME=$VERSION_NAME" >> $GITHUB_ENV
echo "VERSION_CODE=$VERSION_CODE" >> $GITHUB_ENV
- name: Setup Android Environment
if: matrix.platform == 'android'
run: |
# Cleanup Disk Space
sudo rm -rf /usr/share/dotnet
sudo rm -rf /opt/ghc
sudo rm -rf /usr/local/share/boost
sudo apt-get clean
# Generate required files
echo '${{ secrets.KEY_PROPERTIES }}' | base64 --d >> ./android/key.properties
echo '${{ secrets.LOCAL_PROPERTIES }}' | base64 --d >> ./android/local.properties
echo '${{ secrets.GOOGLE_SERVICE_JSON }}' | base64 --d > ./android/app/google-services.json
echo '${{ secrets.JKS }}' | base64 --d > ./android/app/hiing-keystore.jks
- name: Setup JDK 17 for Android
if: matrix.platform == 'android'
uses: actions/setup-java@v3
with:
distribution: 'corretto'
java-version: 17
- name: Setup iOS Environment
if: matrix.platform == 'ios'
env:
BUILD_CERTIFICATE_BASE64: ${{ secrets.APPSTORE_CERT_BASE64 }}
P12_PASSWORD: ${{ secrets.APPSTORE_CERT_PASSWORD }}
BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.MOBILEPROVISION_BASE64 }}
BUILD_PROVISION_NOTI_PROFILE_BASE64: ${{ secrets.NOTI_MOBILEPROVISION_BASE64 }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
# 변수 생성
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
PP_PATH=$RUNNER_TEMP/hiing_github_action_2.mobileprovision
PP_NOTI_PATH=$RUNNER_TEMP/hiing_github_action_noti_2.mobileprovision
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
#secrets으로 부터 인증서와 프로비저닝 프로파일 가져오기
echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode --output $CERTIFICATE_PATH
echo -n "$BUILD_PROVISION_PROFILE_BASE64" | base64 --decode --output $PP_PATH
echo -n "$BUILD_PROVISION_NOTI_PROFILE_BASE64" | base64 --decode --output $PP_NOTI_PATH
# 키체인 초기화 - 임시 키체인 생성
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
# import certificate to keychain
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
# provisioning profile 적용
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles
cp $PP_NOTI_PATH ~/Library/MobileDevice/Provisioning\ Profiles
# 디버깅
echo "Listing provisioning profiles:"
ls -l "$HOME/Library/MobileDevice/Provisioning Profiles/"
- name: Setup Flutter
uses: subosito/flutter-action@v1
with:
channel: 'stable'
- name: Setup Flutter JDK Directory
if: matrix.platform == 'android'
run: |
flutter config --jdk-dir /opt/hostedtoolcache/Java_Corretto_jdk/17.0.13-11.1/x64
- name: Configure Git
run: |
git config --global url."https://${{ vars.USER_NAME }}:${{ secrets.TOKEN_GITHUB }}@github.com/".insteadOf "https://github.com/"
- name: Install Dependencies
run: flutter pub get
- name: Generate Config
run: |
echo '${{ secrets.CONFIG }}' | base64 --d >> ./lib/common/config.dart
- name: Build Android
if: matrix.platform == 'android'
run: flutter build apk --release --flavor dev
- name: Build iOS
if: matrix.platform == 'ios'
run: flutter build ipa --export-options-plist=ios/Runner/ExportOptions.plist --flavor dev
- name: Upload Build Artifacts
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.platform }}-artifacts
path: |
${{ matrix.platform == 'android' && 'build/app/outputs/flutter-apk/app-dev-release.apk' || 'build/ios/ipa/*.ipa' }}
if-no-files-found: error
- name: Create Github Release
uses: softprops/action-gh-release@v1
with:
tag_name: "${{ env.VERSION_NAME }}(${{ env.VERSION_CODE }})"
release_name: "${{ env.VERSION_NAME }}(${{ env.VERSION_CODE }})"
generate_release_notes: true
files: |
${{ matrix.platform == 'android' && 'build/app/outputs/flutter-apk/app-dev-release.apk' || 'build/ios/ipa/*.ipa' }}
- name: Clean up iOS keychain
if: matrix.platform == 'ios' && always()
run: |
security delete-keychain $RUNNER_TEMP/app-signing.keychain-db
rm ~/Library/MobileDevice/Provisioning\ Profiles/hiing_github_action_2.mobileprovision
rm ~/Library/MobileDevice/Provisioning\ Profiles/hiing_github_action_noti_2.mobileprovision
deploy:
needs: build
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
platform: [android, ios]
include:
- platform: android
file_path: app-dev-release.apk
- platform: ios
file_path: hiing.ipa
outputs:
android_deploy_status: ${{ matrix.platform == 'android' && job.status || '' }}
ios_deploy_status: ${{ matrix.platform == 'ios' && job.status || '' }}
steps:
- name: Check Build Status
id: check
if: |
(matrix.platform == 'android' && needs.build.outputs.android_build_status == 'success') ||
(matrix.platform == 'ios' && needs.build.outputs.ios_build_status == 'success')
run: echo "proceed=true" >> $GITHUB_OUTPUT
- name: Download Artifacts
if: steps.check.outputs.proceed == 'true'
uses: actions/download-artifact@v4
with:
name: ${{ matrix.platform }}-artifacts
- name: Upload to Firebase App Distribution
if: steps.check.outputs.proceed == 'true'
uses: wzieba/Firebase-Distribution-Github-Action@v1
with:
appId: ${{ matrix.platform == 'android' && secrets.FIREBASE_APP_ID_ANDROID_DEV || secrets.FIREBASE_APP_ID_IOS_DEV }}
serviceCredentialsFileContent: ${{ secrets.CREDENTIAL_FILE_CONTENT }}
groups: ${{ vars.APP_DISTRIBUTION_GROUPS }}
file: ${{ matrix.file_path }}
notify:
needs: [build, deploy]
if: always()
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
platform: [android, ios]
steps:
- name: Send ${{ matrix.platform }} Notification
run: |
VERSION="${{ needs.build.outputs.version_name }}(${{ needs.build.outputs.version_code }})"
BUILD_STATUS="${{ matrix.platform == 'android' && needs.build.outputs.android_build_status || needs.build.outputs.ios_build_status }}"
DEPLOY_STATUS="${{ matrix.platform == 'android' && needs.deploy.outputs.android_deploy_status || needs.deploy.outputs.ios_deploy_status }}"
if [[ "$BUILD_STATUS" == "success" ]] && [[ "$DEPLOY_STATUS" == "success" ]]; then
FINAL_STATUS="SUCCESS 🎉"
else
FINAL_STATUS="FAILURE 😵💫"
fi
PLATFORM_NAME="${{ matrix.platform == 'ios' && 'IOS' || '안드로이드' }}"
curl -X POST https://slack.com/api/chat.postMessage \
-H "Authorization: ${{ secrets.SLACK_TOKEN }}" \
-H "Content-Type: application/json" \
-d '{
"channel": "${{ vars.SLACK_CHANNEL_ID }}",
"text": "[App Distribution] '"${PLATFORM_NAME}"' 배포 '"${FINAL_STATUS}"'\n버전 👉 '"${VERSION}"'"
}'
appId: ${{ matrix.platform == 'android' && secrets.FIREBASE_APP_ID_ANDROID_DEV || secrets.FIREBASE_APP_ID_IOS_DEV }}
👇이런식으로는 쓸 수 없음

사실 위의 코드는 잘 작동하는지 아직 확인을 못해봤습니다 ㅠㅠ
이유는 깃헙액션이 제공하는 기본 무료 리밋을 다 썼기 때문입니다 ㅠㅠ
한달에 2천 minutes를 제공하는데, 제가 삽질을 너무 많이해버려서 다 써버렸습니다 ㅠㅠ
그리고 리눅스는 실제 1분에 1분이 차감되는데, mac os는 실제 1분에 10분이 차감되다고 합니다 ㅠㅠ
ios 배포에 삽질을 매우 많이 했기 때문에 무료 제공되는 시간을 다 써버리고말았습니다 ㅠ
그래서 12월에 다시 돌려보고 문제가 있으면 포스팅을 고치도록 하겠습니다.
② 회고 (restropective)
③ 개선을 위한 방법
깃헙에서 제공하는 runner말고 로컬이나 클라우드를 runner로 지정할 수도 있던데
그걸 하다가 삽질을 또해서 ios 키체인을 다 날려버리고 인증서를 다 다시 설정했다 ㅠㅠ
self host runner를 설정하려면 또 엄청난 삽질을 해봐야할것 같은데
일이 다시 바빠지기도했고, 테스트 할때 말고 실제로는 2천분을 다 쓸일이 없을 것 같기 때문에
일단 다음달에 다시 츄라이 해보기로하고 self host runner 설정하는건 나~중에 기회가 될때 해보기로 함
문제가없엇다고합니다~