[Android] Github Action 를 이용하여 CD 를 적용 하는 방법

이지훈·2024년 5월 21일
1
post-thumbnail
post-custom-banner

서두

그동안 프로젝트에서 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
  1. 우선 해당 step 들은 main 브랜치로 PR 을 올릴 때, 실행된다.
            -   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
  1. Github Secrets 에 등록해둔 앱의 release 빌드를 위한 keystore 파일 및, 프로퍼티 텍스트 파일 들을 생성한다.

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
  1. GA 혹은 Firebase Crashlytics, 등 Firebase 를 프로젝트에서 사용할 경우, 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
  1. main branch 에 올린 PR 의 title 에서 version 정보를 추출한다.
    ex) PR Title 이 release v1.0.0 이라면 v1.0.0 이 version name 으로 추출됨
            -   name: Build Release APK
                run: |
                    ./gradlew :app:assembleRelease
  1. relase 빌드 (release apk 생성)
            -   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
  1. release.apk 를 Github Action 의 아티팩트로 저장하여, 나중에 다운로드 하거나 참조할 수 있게 함.(release note 내용에 포함됨)
           -   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
  1. 추출한 version name 과 release apk 를 통해 github release note 생성
            -   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
  1. release apk 를 Firebase App distribution 에 등록

문제 발생 1

app:validateSigningRelease task 중 다음과 같은 에러가 발생하였다.

Keystore file '***' not found for signing config 'release'.

keystore 파일을 등록해줬는데도 불구하고, keystore 파일을 찾을 수 없다는 에러이다.

문제 해결 1

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

와 같은 에러가 발생할 수 있으니 주의하자.

문제 발생 2

위의 문제를 해결하여, 이제 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 를 등록할 권한이 설정을 해놓았는데 불구하고, 권한이 없다는 것이었다.

문제 해결 2

우선 선행 조건으로 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) 을 업데이트 하여 해결하였다.

문제 발생 3

ERROR: Failed to authenticate, have you run firebase login?

what? 내가 firebase 로그인을 안했다고?

문제 해결 3

[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 설정 및 에러 해결기

profile
실력은 고통의 총합이다. Android Developer
post-custom-banner

3개의 댓글

comment-user-thumbnail
2024년 6월 20일

지훈님 팬이에오

1개의 답글
comment-user-thumbnail
2024년 11월 5일

지훈님 팬이에오 222 정말 많은 도움되었습니다. 감사합니다!!

답글 달기