그동안 프로젝트에서 develop branch에 PR을 올릴 때, 주로 Github Action 을 이용하여 CI(Continuous Integration) 만을 적용하였는데(code style check, run build success 확인을 위함), 배포의 자동화를 구축해보기 위하여 CD(Continuous Delivery/Deployment) 도 적용 해보도록 하겠다.
배포의 자동화는 main 또는 release 브랜치에 merge 될 경우 Google PlayStore 에 자동 배포까지를 포함하는게 맞으나, 이번 글에서 다루는 범위는 main branch에 PR 을 올릴 경우, release apk 를 생성하고, github release note 를 생성하고, release.apk 를 firebase app distribution에 올려 테스터들에게 테스트를 할 수 있도록 제공하는 기능까지이다. 다음 글에서 PlayStore에 배포까지 다뤄보도록 하겠다.
그동안 앱을 PlayStore 에 배포하는 과정은 다음과 같았다.
1. 팀원분들과 개발을 열심히 한다.(feature branch 개발 -> develop PR -> CI 및 코드리뷰 -> develop merge)
2. MVP 개발이 완료되면, signingConfigs 설정 후 release apk 를 생성한다.(높은 확률로 첫 release 빌드에선 proguard 관련한 문제가 발생하기에 문제 해결)
4. 생성된 release apk 를 다른 팀원에게 전달하여, QA 를 진행 (마감이 급박한 상황일 경우엔 우선 PlayStore에 출시 하고, QA 반영 결과를 업데이트 버전에 반영)
5. QA 에서 발견된 문제들을 해결하고, develop -> main PR -> merge 및 Github release note 생성(app version update)
6. release aab 파일을 PlayStore 에 등록 및 출시 노트 작성 -> 새 버전 출시
4번 에서 apk 의 용량이 클 경우, slack 이나 discord 에 직접 올릴 수 없는 문제가 발생하여, google drive 에 apk 를 올리고, 권한 설정 변경 후 url 을 팀원에게 공유하는 과정이 포함될 수 있다.
여기서 2 ~ 5번 과정을 Github Action CD 를 통해 자동화 해보도록 하겠다.
내가 기존에 사용했던 android-ci.yml 파일과 레퍼런스들을 참고하여 작성한 android-ci.yml 은 다음과 같다.
name: Android CD
env:
GRADLE_OPTS: "-Dorg.gradle.jvmargs=-Xmx4g -Dorg.gradle.daemon=false"
GRADLE_BUILD_ACTION_CACHE_DEBUG_ENABLED: true
on:
pull_request:
branches:
- main
jobs:
cd-build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
distribution: 'corretto'
java-version: 17
- name: Generate unifest.jks
run: echo '${{ secrets.UNIFEST_KEYSTORE }}' | base64 --d > ./app/unifest.jks
- name: Generate secrets.properties
run: |
echo '${{ secrets.SECRETS_PROPERTIES }}' >> ./secrets.properties
- name: Generate local.properties
run: |
echo '${{ secrets.LOCAL_PROPERTIES }}' >> ./local.properties
- name: Generate keystore.properties
run: |
echo '${{ secrets.KEYSTORE_PROPERTIES }}' >> ./keystore.properties
- name: Generate google-services.json
run: echo '${{ secrets.GOOGLE_SERVICES }}' | base64 --d > ./app/google-services.json
- name: Extract Version Name
run: echo "##[set-output name=version;]v$(echo '${{ github.event.pull_request.title }}' | grep -oP 'release v\K[0-9]+\.[0-9]+\.[0-9]+')"
id: extract_version
- name: Build Release APK
run: |
./gradlew :app:assembleRelease
- name: Upload Release Build to Artifacts
uses: actions/upload-artifact@v3
with:
name: release-artifacts
path: app/build/outputs/apk/release/
if-no-files-found: error
- name: Create Github Release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ steps.extract_version.outputs.version }}
release_name: ${{ steps.extract_version.outputs.version }}
generate_release_notes: true
files: |
app/build/outputs/apk/release/app-release.apk
- name: Upload artifact to Firebase App Distribution
uses: wzieba/Firebase-Distribution-Github-Action@v1
with:
appId: ${{secrets.FIREBASE_APP_ID}}
serviceCredentialsFileContent: ${{ secrets.CREDENTIAL_FILE_CONTENT }}
groups: testers
file: app/build/outputs/apk/release/app-release.apk
주요 step 들에 대한 간략한 설명을 하면,
on:
pull_request:
branches:
- main
- name: Generate unifest.jks
run: echo '${{ secrets.UNIFEST_KEYSTORE }}' | base64 --d > ./app/unifest.jks
- name: Generate secrets.properties
run: |
echo '${{ secrets.SECRETS_PROPERTIES }}' >> ./secrets.properties
- name: Generate local.properties
run: |
echo '${{ secrets.LOCAL_PROPERTIES }}' >> ./local.properties
- name: Generate keystore.properties
run: |
echo '${{ secrets.KEYSTORE_PROPERTIES }}' >> ./keystore.properties
text 파일 뿐만 아니라, keystore 또는 json 파일을 Github Secrets 에 등록하는 방법등은 아래의 블로그를 참고하면 좋을 것 같다.
Github) Github actions에서 Secrets로 환경변수 관리하기
- name: Generate google-services.json
run: echo '${{ secrets.GOOGLE_SERVICES }}' | base64 --d > ./app/google-services.json
- name: Extract Version Name
run: echo "##[set-output name=version;]v$(echo '${{ github.event.pull_request.title }}' | grep -oP 'release v\K[0-9]+\.[0-9]+\.[0-9]+')"
id: extract_version
- name: Build Release APK
run: |
./gradlew :app:assembleRelease
- name: Upload Release Build to Artifacts
uses: actions/upload-artifact@v3
with:
name: release-artifacts
path: app/build/outputs/apk/release/
if-no-files-found: error
- name: Create Github Release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ steps.extract_version.outputs.version }}
release_name: ${{ steps.extract_version.outputs.version }}
generate_release_notes: true
files: |
app/build/outputs/apk/release/app-release.apk
- name: Upload artifact to Firebase App Distribution
uses: wzieba/Firebase-Distribution-Github-Action@v1
with:
appId: ${{secrets.FIREBASE_APP_ID}}
serviceCredentialsFileContent: ${{ secrets.CREDENTIAL_FILE_CONTENT }}
groups: testers
file: app/build/outputs/apk/release/app-release.apk
app:validateSigningRelease task 중 다음과 같은 에러가 발생하였다.
Keystore file '***' not found for signing config 'release'.
keystore 파일을 등록해줬는데도 불구하고, keystore 파일을 찾을 수 없다는 에러이다.
keystore 파일이 저장된 위치를 가리키는 keystore.properties 내에 STORE_FILE 값을 절대 경로에서 상대 경로로 변경하여 해결하였다..!
그동안 release.apk 를 로컬 환경에서 생성하였기 때문에, 문제가 되지 않았던 부분인데, remote repository 에선 로컬의 절대 경로
/Users/{USER_NAME}/AndroidStudioProjects/{REPOSITORY_NAME}/app/{KEYSTORE_FILE_NAME}.jks
을 알 수 없기 때문에 이를 식별할 수 없다.
참고로 jks 는 Java Keystore 의 약자이다. 따라서
Keystore 파일을 찾을 수 없다 == .jks 파일을 찾을 수 없다.
따라서 Github Secrets 의 저장된 KEYSTORE_PROPERTIES 내에 STORE_FILE 을 상대 경로의 형식으로 수정하여 해결하였다.
상대 경로를 잘 못 지정할 경우
ex) {REPOSITORY_NAME}/app/app/{KEYSTORE_FILE_NAME}.jks not found
와 같은 에러가 발생할 수 있으니 주의하자.
위의 문제를 해결하여, 이제 release APK 가 정상적으로 생성되고, relaese note 도 정상적으로 생성되는 것을 확인할 수 있었다.
그런데 firebase app distribution 에 release apk 를 등록하는 단계에서 다음과 같은 에러가 발생하는 것을 확인 할 수 있었다.
Error: failed to upload release. HTTP Error: 403, The caller does not have permission
403 이면 권한 관련인데, 이미 firebase app distribution 에 apk 를 등록할 권한이 설정을 해놓았는데 불구하고, 권한이 없다는 것이었다.
우선 선행 조건으로 firebase app distribution 관련 설정을 해줘야 하는데, 일련의 과정은 해당 블로그를 참고하여 진행하였다.
다시 돌아가서,
Error: failed to upload release. HTTP Error: 403, The caller does not have permission #95
위의 이슈 링크 내에
o-ifeanyi 님의 comment 를 참고하여 해결할 수 있었는데, firebase 앱 배포라는 키워드만 보고 Firebase 앱 배포 관리자 이 아닌, Firebase 앱 배포 SDK 서비스 에이전트를 선택한 것이 문제였다..! 휴먼에러
다시 권한을 Firebase 앱 배포 관리자로 변경하고, CREDENTIAL_FILE_CONTENT(.json) 을 업데이트 하여 해결하였다.
ERROR: Failed to authenticate, have you run firebase login?
what? 내가 firebase 로그인을 안했다고?
[Flutter] 우당탕탕 Firebase 설정 및 에러 해결기
역시 같은 문제를 겪었던 분이 존재하였고, 블로그의 설명대로 android studio termial 에 해당 명령어를 입력해주었다.
firebase login
그러면 이후 질문에 대한 Y/N 을 선택하게 되고(뭘 고르든 로그인 됨)
위와 같이 Firebase CLI Login 에 성공했다는 문구를 확인할 수 있다.
Firebase CLI 는 Command Line Interface 의 약자로 이를 사용하면 로컬 환경에서 Firebase 프로젝트를 설정 및 다양한 작업을 수행할 수 있다고 한다.
모든 문제 해결!
github release note 가 정상적으로 생성된 것을 확인할 수 있었고, (매번 직접 만드는게 생각보다 귀찮았는데 행--복)
Firebase App Distribution 에도 apk 파일이 올라간 것을 확인 할 수 있었다.
이제는 팀원들에게 apk 를 공유할 때, 직접 release apk 를 추출해서, google drive 와 같은 공유 툴을 통해 공유 할 필요 없다.
팀원을 firebase 콘솔에서 테스터로 한번만 등록 해주면, firebase 에서 apk 를 다운받을 수 있다.
물론 firebase 에 apk 를 직접 등록할 필요 없다. main 에 PR 이 올라가서 cd 가 성공적으로 완료되면 자동으로 올라가기 때문이다!.
다음 글에서는 PlayStore 에 앱을 자동 배포하는 방법과, app version 을 자동으로 올려주는 방법등을 알아보도록 하겠다.
CD 가 적용된 프로젝트는 하단 링크에서 확인할 수 있습니다.
https://github.com/Project-Unifest/unifest-android
참고)
[Android] Github Release 자동화 (Github Actions)
[Android] Firebase 배포 자동화 (Github Actions)
Android CD 적용기
Github) Github actions에서 Secrets로 환경변수 관리하기
https://www.browserling.com/tools/json-to-base64
Github Actions - 환경변수로 Keystore 저장하여 사용하기
[Flutter] 우당탕탕 Firebase 설정 및 에러 해결기
지훈님 팬이에오