플러터 앱을 만들었으니, 배포 / 출시 를 자동화 해보자. (feat. androidpublisher)
플러터 앱을 만들었고, 이제 플레이 콘솔에서 수동으로 해보니 별다른 문제없이 배포되었다.
내부테스트 까지 해봤고, 조금 정리후에는 실제 플레이 스토어에 출시 할 수 있을 듯 하다.
그전에 수동 업로드 과정을 자동화 하고자 한다.
관련 API 를 제공하는 듯 하다.

Google Play Android Developer API 가 세팅되어 있어야 한다.
관련 부분은 필요하면 따로 정리.
CI/CD 로 주로 fastlane 과 연동하는 방법을 많이 쓰는 것 같다.
난 좀 단순화(?) 시켜서 반자동 으로 처리하고자 한다.
업로드 부분은 python 코드로 다음과 같다.
{ | |
"package_name": "{your-package-name}", | |
"service_account_file": "../{your-google-play-developer-api-key.json}", | |
"aab_file": "build/app/outputs/bundle/release/app-release.aab", | |
"mapping_file": "build/app/outputs/mapping/release/mapping.txt", | |
"debug_symbols_dir": "build/app/intermediates/merged_native_libs/release/out/lib" | |
} |
import sys | |
import os | |
import json | |
import zipfile | |
from google.oauth2 import service_account | |
from googleapiclient.discovery import build | |
from googleapiclient.http import MediaFileUpload | |
import logging | |
#logging.basicConfig(level=logging.DEBUG) | |
def load_config(config_file): | |
with open(config_file, 'r') as f: | |
return json.load(f) | |
def create_zip_from_directory(zip_filename, directory): | |
with zipfile.ZipFile(zip_filename, 'w', zipfile.ZIP_DEFLATED) as zipf: | |
for root, _, files in os.walk(directory): | |
for file in files: | |
file_path = os.path.join(root, file) | |
zipf.write(file_path, os.path.relpath(file_path, directory)) | |
def upload_aab_and_symbols(config, release_notes): | |
credentials = service_account.Credentials.from_service_account_file( | |
config['service_account_file'], | |
scopes=['https://www.googleapis.com/auth/androidpublisher'] | |
) | |
service = build('androidpublisher', 'v3', credentials=credentials) | |
package_name = config['package_name'] | |
aab_file = config['aab_file'] | |
mapping_file = config['mapping_file'] | |
debug_symbols_dir = config['debug_symbols_dir'] | |
try: | |
edit_request = service.edits().insert(body={}, packageName=package_name) | |
result = edit_request.execute() | |
edit_id = result['id'] | |
# AAB 파일 업로드 | |
print(f'AAB 파일 업로드 : start') | |
aab_response = service.edits().bundles().upload( | |
editId=edit_id, | |
packageName=package_name, | |
media_body=MediaFileUpload(aab_file, mimetype='application/octet-stream') | |
).execute() | |
print(f'AAB 파일이 성공적으로 업로드되었습니다. 버전 코드: {aab_response["versionCode"]}') | |
versionCode = aab_response["versionCode"] | |
debugUploadDone = 1 | |
print(f'디버그 심볼 파일 업로드 : start') | |
# 디버그 심볼 ZIP 파일 생성 및 업로드 | |
zip_filename = 'native_debug_symbols.zip' | |
create_zip_from_directory(zip_filename, debug_symbols_dir) | |
print(f'ZIP 파일 생성됨: {zip_filename}') | |
try: | |
deobfuscation_file = service.edits().deobfuscationfiles().upload( | |
editId=edit_id, | |
packageName=package_name, | |
apkVersionCode=versionCode, | |
deobfuscationFileType='nativeCode', | |
media_body=MediaFileUpload( | |
zip_filename, | |
mimetype='application/octet-stream', | |
resumable=True, | |
chunksize=262144 | |
) | |
).execute() | |
print('디버그 심볼 ZIP 파일이 업로드되었습니다.') | |
except Exception as upload_error: | |
print(f'ZIP 파일 업로드 중 오류 발생: {upload_error}') | |
debugUploadDone = 0 # fail | |
# proguard | |
print(f'ReTrace 매핑 파일 업로드 : start') | |
try: | |
mapping_file_response = service.edits().deobfuscationfiles().upload( | |
editId=edit_id, | |
packageName=package_name, | |
apkVersionCode=versionCode, | |
deobfuscationFileType='proguard', | |
media_body=MediaFileUpload(mapping_file, mimetype='application/octet-stream') | |
).execute() | |
print('ReTrace 매핑 파일이 성공적으로 업로드되었습니다.') | |
except Exception as e: | |
print(f'ReTrace 매핑 파일 업로드 실패: 오류 내용: {str(e)}') | |
debugUploadDone = 0 # fail | |
# all process done. | |
if debugUploadDone == 1 : | |
# 릴리즈 노트 업데이트 및 트랙에 추가 | |
service.edits().tracks().update( | |
editId=edit_id, | |
track='internal', | |
packageName=package_name, | |
body={ | |
'releases': [{ | |
'versionCodes': [versionCode], | |
'status': 'draft', # draft => only / statusUnspecified / inProgress / halted / - completed(X) | |
'releaseNotes': [ | |
{ | |
'language': 'ko-KR', | |
'text': release_notes | |
} | |
] | |
}] | |
} | |
).execute() | |
# 변경사항 커밋 | |
commit_request = service.edits().commit(editId=edit_id, packageName=package_name) | |
commit_request.execute() | |
print('AAB 파일, 네이티브 디버그 기호, 그리고 ReTrace 매핑 파일이 성공적으로 Google Play Console에 업로드되었습니다.') | |
else : | |
print('\n===== Fail release upload') | |
except Exception as e: | |
print(f'오류 발생: {str(e)}') | |
finally: | |
# 임시로 생성한 ZIP 파일 삭제 | |
if os.path.exists(zip_filename): | |
os.remove(zip_filename) | |
if __name__ == "__main__": | |
if len(sys.argv) < 2: | |
print(f"사용법: python {sys.argv[0]} '릴리즈 노트' [config.json-another]") | |
sys.exit(1) | |
release_notes = sys.argv[1] | |
config_file = 'config.json' | |
if len(sys.argv) > 2 and sys.argv[2]: | |
config_file = sys.argv[2] | |
try: | |
config = load_config(config_file) | |
except Exception as e: | |
print(f'config load 오류 발생: {str(e)}') | |
upload_aab_and_symbols(config, release_notes) |
위와 같은 코드를 사용했다.
본인 프로젝트에 맞는 config.json 의 내용을 수정하고, python 환경에서 실행하면 된다.
flutter build appbundle 으로 aab 파일을 생성한 후에 업로드
프로젝트 폴더에서 실행
python3 play-release.py 'feature: auto relase code'
python 환경이 구성되어 있지 않다면.
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
brew install python
pip3 install google-auth google-auth-httplib2 google-api-python-client
python venv 를 구성하는 방법도 좋은 팁이다.(이 부분도 추후 필요시 정리)
'알아둘일' 카테고리의 다른 글
안드로이드 앱 출시는 힘겹구나! (feat. 비공개테스트) (0) | 2024.07.14 |
---|---|
플러터 앱 - 자동배포 iOS 쪽도 해보자 (feat. xcrun altool) (0) | 2024.07.02 |
플러터 - 앱로빈 광고 연동시 iOS native 광고 노출 이상 현상 (feat. bundle id) (0) | 2024.06.11 |
우분투 <=> 윈도우 - smb 연결 시 캐시가 문제가 되나? (feat. ubuntu 22.04) (0) | 2024.06.05 |
(미해결)플러터 - 안드로이드 에뮬레이터 이상 현상 (feat. emulator) (0) | 2024.05.29 |
WRITTEN BY
- 1day1
하루하루 즐거운일 하나씩, 행복한일 하나씩 만들어 가요.