Android 앱 개발 자동화 파이프라인 완벽 가이드
Android 앱 개발 자동화 파이프라인 완벽 가이드
OpenCode + CI/CD를 활용한 시장수요조사 → 배포 자동화
당신의 고급 기술 스택(Docker, Linux, DevOps 경험)을 고려하여, 완전 자동화 파이프라인을 단계별로 구성하겠습니다.
📋 전체 아키텍처 개요
┌─────────────────────────────────────────────────────────────────┐
│ 자동화 파이프라인 전체 흐름 │
└─────────────────────────────────────────────────────────────────┘
1️⃣ 시장수요조사 자동화 (OpenCode Agent)
└─ 웹 크롤링 + 시장 분석 리포트 자동 생성
2️⃣ 앱 개발 자동화 (OpenCode Agent)
└─ 요구사항 → Android Studio 프로젝트 자동 생성
└─ Kotlin/Java 코드 자동 작성
3️⃣ 빌드 및 테스트 자동화 (GitHub Actions)
├─ Gradle 빌드 (APK/AAB 생성)
├─ 유닛 테스트 자동 실행
├─ Lint 체크
└─ APK 서명
4️⃣ 검수 및 메타데이터 생성 (자동화 스크립트)
├─ 앱 스크린샷 자동 캡처
├─ Google Play 메타데이터 생성
├─ 프라이버시 정책, 이용약관 자동 작성
└─ QA 리포트 생성
5️⃣ Google Play Store 업로드 (Fastlane + GitHub Actions)
├─ 내부 테스트 트랙 배포
├─ 베타 테스트 트랙 배포
└─ 프로덕션 배포 (수동 승인 후)
🔧 Step 1: OpenCode 설치 및 환경 구성
1.1 OpenCode 설치
# Node.js를 이용한 설치 (권장)
npm install -g opencode
# 또는 Homebrew (macOS/Linux)
brew install opencode/tap/opencode
# Docker를 사용한 설치
docker run -it -v ~/.opencode:/home/user/.opencode opencode/cli:latest
1.2 OpenCode 설정
# OpenCode 초기화
opencode init
# LLM 프로바이더 설정 (OpenCode Zen 추천)
opencode /connect
# 또는 Claude, GPT-4 등 직접 설정
export ANTHROPIC_API_KEY="your-key"
export OPENAI_API_KEY="your-key"
1.3 프로젝트 구조 준비
mkdir -p android-automation-workspace
cd android-automation-workspace
# 다음 디렉토리 구조 생성
mkdir -p {market_research,app_dev,ci_cd,metadata,scripts}
# AGENTS.md 생성 (OpenCode가 프로젝트 이해할 수 있도록)
cat > AGENTS.md << 'EOF'
# Android App Automation Project
## Project Structure
- market_research/: 시장 조사 자동화
- app_dev/: 앱 개발 자동화
- ci_cd/: CI/CD 파이프라인
- metadata/: Play Store 메타데이터
- scripts/: 보조 자동화 스크립트
## Tech Stack
- Android: Kotlin/Java, Gradle
- CI/CD: GitHub Actions
- Deployment: Fastlane, Google Play API
- Automation: Python, Bash
## Key Goals
1. Market research automation
2. App development automation
3. Automated testing and QA
4. Metadata generation
5. Google Play Store deployment
EOF
📊 Step 2: 시장수요조사 자동화
2.1 OpenCode를 활용한 시장 분석
프롬프트 작성:
cat > market_research/research_prompt.txt << 'EOF'
당신은 모바일 앱 시장 분석가입니다.
다음 작업을 자동으로 수행하세요:
1. Google Play Store 트렌드 분석
- 현재 인기 카테고리 (상위 5개)
- 각 카테고리별 평균 다운로드 수
- 사용자 평점 분포
2. 경쟁 분석
- 키워드 [productivity, health, utility] 관련 앱 상위 10개
- 각 앱의 평점, 리뷰 수, 가격
- 주요 기능과 차별점
3. 사용자 요구사항 분석
- 최근 3개월 사용자 리뷰의 공통 불만사항
- 자주 요청되는 기능
- 개선 기회 영역
4. 출력 형식
JSON 형식으로 다음 구조로 저장:
{
"market_overview": {...},
"top_competitors": [...],
"user_pain_points": [...],
"opportunities": [...],
"recommended_features": [...]
}
EOF
# OpenCode 실행
opencode analyze --file market_research/research_prompt.txt --output market_research/analysis_result.json
2.2 Python 스크립트로 시장조사 자동화
# market_research/auto_research.py
import json
import requests
from datetime import datetime
from anthropic import Anthropic
client = Anthropic()
def market_research_automation(app_category: str, keywords: list) -> dict:
"""
OpenCode/Claude를 활용한 자동화된 시장조사
"""
conversation_history = []
# 1단계: 초기 시장 분석 요청
user_message = f"""
{app_category} 카테고리에서 {', '.join(keywords)} 키워드로 검색한
Google Play Store 앱들의 시장 분석을 수행해주세요.
다음 정보를 수집해야 합니다:
1. 상위 10개 앱의 기본정보 (이름, 다운로드 수, 평점, 리뷰 수)
2. 각 앱의 주요 기능 분석
3. 사용자 리뷰에서 발견되는 공통 불만사항
4. 시장 기회 및 차별화 전략
결과는 JSON 형식으로 제공해주세요.
"""
conversation_history.append({
"role": "user",
"content": user_message
})
# Claude API 호출
response = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=4000,
messages=conversation_history
)
assistant_message = response.content[0].text
conversation_history.append({
"role": "assistant",
"content": assistant_message
})
# 2단계: 상세 분석 요청
followup = """
위의 분석을 바탕으로, 우리가 개발할 앱이 경쟁에서 이길 수 있는
3-5가지 차별화된 기능을 제안해주세요.
각 기능마다:
- 기능명
- 기술 난이도 (낮음/중간/높음)
- 예상 개발 시간
- 사용자 영향도
를 포함해주세요.
"""
conversation_history.append({
"role": "user",
"content": followup
})
response = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=2000,
messages=conversation_history
)
final_analysis = response.content[0].text
return {
"timestamp": datetime.now().isoformat(),
"category": app_category,
"keywords": keywords,
"initial_analysis": assistant_message,
"feature_recommendations": final_analysis,
"report_file": f"market_research/report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.md"
}
if __name__ == "__main__":
result = market_research_automation(
app_category="Productivity",
keywords=["task manager", "todo list", "productivity"]
)
# 결과 저장
with open(result["report_file"], "w") as f:
f.write(f"# Market Research Report\n")
f.write(f"Generated: {result['timestamp']}\n\n")
f.write(f"## Initial Analysis\n{result['initial_analysis']}\n\n")
f.write(f"## Feature Recommendations\n{result['feature_recommendations']}\n")
print(f"✅ Report saved: {result['report_file']}")
print(json.dumps(result, indent=2))
실행:
cd market_research
python auto_research.py
🚀 Step 3: 앱 개발 자동화
3.1 OpenCode를 사용한 Android 프로젝트 생성
# app_dev 디렉토리로 이동
cd app_dev
# OpenCode에게 Android 프로젝트 생성 지시
cat > create_app_prompt.txt << 'EOF'
Android Studio 프로젝트를 자동으로 생성해주세요.
프로젝트 요구사항:
- 프로젝트명: MyProductivityApp
- Package: com.example.productivity
- Minimum SDK: 24 (Android 7.0)
- Target SDK: 35 (Android 15, 2025 Play Store 요구사항)
- 언어: Kotlin
- 아키텍처: MVVM + Clean Architecture
필요한 모듈:
1. app (메인 앱)
2. data (로컬 DB, 네트워크 통신)
3. domain (비즈니스 로직)
4. presentation (UI/ViewModel)
기본 화면 구조:
- MainActivity (메인 탭 네비게이션)
- TaskListFragment (할일 목록)
- AddTaskFragment (할일 추가)
- SettingsFragment (설정)
기본 기능:
- Room DB를 사용한 로컬 저장소
- LiveData를 사용한 상태 관리
- Material Design 3 UI
build.gradle.kts 구성:
- Gradle 8.5+
- Kotlin 1.9+
- Jetpack libraries (Room, ViewModel, LiveData)
- Hilt 의존성 주입
생성 완료 후, 이 프로젝트를 GitHub에 푸시할 수 있는 상태로 만들어주세요.
EOF
# OpenCode로 프로젝트 생성
opencode --file create_app_prompt.txt --output app_structure.md
3.2 자동화된 프로젝트 초기화 스크립트
#!/bin/bash
# app_dev/init_android_project.sh
set -e
PROJECT_NAME="MyProductivityApp"
PACKAGE_NAME="com.example.productivity"
MIN_SDK=24
TARGET_SDK=35
KOTLIN_VERSION="1.9.23"
GRADLE_VERSION="8.5"
echo "🔨 Android 프로젝트 초기화 중..."
# 1. 프로젝트 디렉토리 생성
mkdir -p "$PROJECT_NAME"
cd "$PROJECT_NAME"
# 2. settings.gradle.kts 생성
cat > settings.gradle.kts << 'SETTINGS_EOF'
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "MyProductivityApp"
include(":app", ":data", ":domain", ":presentation")
SETTINGS_EOF
# 3. 루트 build.gradle.kts 생성
cat > build.gradle.kts << 'BUILD_EOF'
plugins {
id("com.android.application") version "8.2.0" apply false
id("com.android.library") version "8.2.0" apply false
id("org.jetbrains.kotlin.android") version "1.9.23" apply false
id("com.google.dagger.hilt.android") version "2.48" apply false
}
BUILD_EOF
# 4. 모듈별 디렉토리 및 build.gradle.kts 생성
create_module() {
local module_name=$1
local is_app=$2
mkdir -p "$module_name/src/main/java/com/example/productivity"
mkdir -p "$module_name/src/main/res/{layout,drawable,values,mipmap}"
mkdir -p "$module_name/src/test/java"
mkdir -p "$module_name/src/androidTest/java"
if [ "$is_app" = "true" ]; then
cat > "$module_name/build.gradle.kts" << 'APP_BUILD_EOF'
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("com.google.dagger.hilt.android")
kotlin("kapt")
}
android {
namespace = "com.example.productivity"
compileSdk = 35
defaultConfig {
applicationId = "com.example.productivity"
minSdk = 24
targetSdk = 35
versionCode = 1
versionName = "1.0.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
viewBinding = true
dataBinding = true
}
}
dependencies {
// Jetpack
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.11.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0")
implementation("androidx.activity:activity-ktx:1.8.1")
implementation("androidx.fragment:fragment-ktx:1.6.2")
// Room
implementation("androidx.room:room-runtime:2.6.1")
kapt("androidx.room:room-compiler:2.6.1")
implementation("androidx.room:room-ktx:2.6.1")
// Hilt
implementation("com.google.dagger:hilt-android:2.48")
kapt("com.google.dagger:hilt-compiler:2.48")
// Kotlin Coroutines
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
// Testing
testImplementation("junit:junit:4.13.2")
testImplementation("org.mockito:mockito-core:5.7.0")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
}
APP_BUILD_EOF
else
cat > "$module_name/build.gradle.kts" << 'LIB_BUILD_EOF'
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("com.google.dagger.hilt.android")
kotlin("kapt")
}
android {
namespace = "com.example.productivity.$module_name"
compileSdk = 35
defaultConfig {
minSdk = 24
targetSdk = 35
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
}
dependencies {
implementation("androidx.core:core-ktx:1.12.0")
implementation("com.google.dagger:hilt-android:2.48")
kapt("com.google.dagger:hilt-compiler:2.48")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
testImplementation("junit:junit:4.13.2")
}
LIB_BUILD_EOF
fi
}
create_module "app" "true"
create_module "data" "false"
create_module "domain" "false"
create_module "presentation" "false"
# 5. gradlew 파일 생성
gradle wrapper --gradle-version 8.5
echo "✅ Android 프로젝트 초기화 완료!"
echo "📁 프로젝트 경로: $(pwd)"
실행:
chmod +x app_dev/init_android_project.sh
app_dev/init_android_project.sh
🔄 Step 4: CI/CD 파이프라인 구성 (GitHub Actions)
4.1 GitHub Actions 워크플로우 구성
# .github/workflows/android-ci-cd.yml
name: Android CI/CD Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
workflow_dispatch:
inputs:
deploy_track:
description: 'Deploy track (internal, beta, production)'
required: false
default: 'internal'
env:
JAVA_VERSION: "17"
GRADLE_VERSION: "8.5"
MIN_SDK: 24
TARGET_SDK: 35
jobs:
build:
name: Build & Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: 'temurin'
cache: gradle
- name: Make gradlew executable
run: chmod +x ./gradlew
- name: Run lint checks
run: ./gradlew lint
- name: Run unit tests
run: ./gradlew testDebugUnitTest
- name: Run instrumented tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 31
script: ./gradlew connectedAndroidTest
- name: Build Release APK
run: ./gradlew assembleRelease
- name: Build Release AAB
run: ./gradlew bundleRelease
- name: Sign APK
uses: r0adkll/sign-android-release@v1
id: sign_apk
with:
releaseDirectory: app/build/outputs/apk/release
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
alias: ${{ secrets.KEY_ALIAS }}
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
keyPassword: ${{ secrets.KEY_PASSWORD }}
- name: Sign AAB
uses: r0adkll/sign-android-release@v1
id: sign_aab
with:
releaseDirectory: app/build/outputs/bundle/release
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
alias: ${{ secrets.KEY_ALIAS }}
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
keyPassword: ${{ secrets.KEY_PASSWORD }}
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: release-artifacts
path: |
app/build/outputs/apk/release/*.apk
app/build/outputs/bundle/release/*.aab
- name: Generate test report
if: always()
run: |
mkdir -p build/reports
cp -r app/build/reports/tests build/reports/ || true
cp app/build/reports/lint-results.html build/reports/ || true
deploy:
name: Deploy to Google Play
needs: build
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Download artifacts
uses: actions/download-artifact@v4
with:
name: release-artifacts
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3'
bundler-cache: true
- name: Install Fastlane
run: |
sudo gem install fastlane
cd android && fastlane install_plugins
- name: Deploy to internal testing
env:
PLAY_STORE_SERVICE_ACCOUNT: ${{ secrets.PLAY_STORE_SERVICE_ACCOUNT }}
run: |
cd android
fastlane deploy_internal
- name: Create GitHub Release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: v${{ github.run_number }}
release_name: Release ${{ github.run_number }}
draft: false
prerelease: false
manual_deployment:
name: Manual Deploy to Google Play
runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch'
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: 'temurin'
cache: gradle
- name: Build Release AAB
run: |
chmod +x ./gradlew
./gradlew bundleRelease
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3'
bundler-cache: true
- name: Install Fastlane
run: sudo gem install fastlane
- name: Deploy to specified track
env:
PLAY_STORE_SERVICE_ACCOUNT: ${{ secrets.PLAY_STORE_SERVICE_ACCOUNT }}
DEPLOY_TRACK: ${{ github.event.inputs.deploy_track }}
run: |
echo "$PLAY_STORE_SERVICE_ACCOUNT" > service_account.json
fastlane android deploy_to_play_store \
track:$DEPLOY_TRACK \
aab:app/build/outputs/bundle/release/app-release.aab
4.2 Fastlane 구성
# android/fastlane/Fastfile
default_platform(:android)
platform :android do
desc "Deploy to internal testing track"
lane :deploy_internal do
upload_to_play_store(
track: 'internal',
aab: '../app/build/outputs/bundle/release/app-release.aab',
skip_upload_metadata: false,
skip_upload_images: false,
skip_upload_screenshots: false
)
end
desc "Deploy to beta testing track"
lane :deploy_beta do
upload_to_play_store(
track: 'beta',
aab: '../app/build/outputs/bundle/release/app-release.aab'
)
end
desc "Deploy to production"
lane :deploy_production do
upload_to_play_store(
track: 'production',
aab: '../app/build/outputs/bundle/release/app-release.aab',
rollout: '0.1' # 10%부터 롤아웃 시작
)
end
desc "Upload metadata only"
lane :upload_metadata do
upload_to_play_store(
skip_upload_aab: true,
skip_upload_apk: true
)
end
end
📝 Step 5: 메타데이터 자동 생성
5.1 Google Play Store 메타데이터 자동화
# metadata/auto_metadata_generator.py
import os
import json
from datetime import datetime
from anthropic import Anthropic
client = Anthropic()
def generate_play_store_metadata(app_name: str, app_description: str, features: list) -> dict:
"""
Google Play Store 메타데이터 자동 생성
"""
features_text = "\n".join([f"- {f}" for f in features])
prompt = f"""
당신은 Google Play Store 최적화 전문가입니다.
다음 앱의 메타데이터를 작성해주세요:
앱 이름: {app_name}
앱 설명: {app_description}
주요 기능:
{features_text}
다음 항목들을 JSON 형식으로 생성해주세요:
1. short_description (80자 이내)
2. full_description (4000자 이내, Google Play 정책 준수)
3. app_title_variations (3-5개 변형)
4. keywords (10-15개, SEO 최적화)
5. privacy_policy_summary
6. category (Google Play 카테고리 중 하나)
7. content_rating (Google Play 콘텐츠 등급)
응답은 유효한 JSON만 포함하세요.
"""
response = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=2000,
messages=[{
"role": "user",
"content": prompt
}]
)
metadata_json = json.loads(response.content[0].text)
return metadata_json
def generate_privacy_policy(app_name: str, collects_data: dict) -> str:
"""
개인정보 보호정책 자동 생성
"""
data_types = "\n".join([f"- {k}: {v}" for k, v in collects_data.items()])
prompt = f"""
{app_name} 앱을 위한 개인정보 보호정책을 작성해주세요.
수집되는 데이터:
{data_types}
Google Play 스토어 요구사항을 충족하고,
일반적인 프라이버시 정책 구조를 따르세요:
1. 개요
2. 수집되는 정보
3. 정보 사용
4. 정보 공유
5. 보안
6. 데이터 보관
7. 사용자 권리
8. 정책 변경
9. 연락처
마크다운 형식으로 작성해주세요.
"""
response = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=3000,
messages=[{
"role": "user",
"content": prompt
}]
)
return response.content[0].text
def generate_terms_of_service(app_name: str) -> str:
"""
이용약관 자동 생성
"""
prompt = f"""
{app_name} 모바일 앱 서비스를 위한
표준 이용약관(Terms of Service)을 마크다운 형식으로 작성해주세요.
포함해야 할 항목:
1. 서비스 이용 규칙
2. 사용자 책임
3. 지적 재산권
4. 면책조항
5. 서비스 중단
6. 변경사항
7. 준거법
8. 분쟁 해결
Google Play 스토어 정책을 준수하는 형식으로 작성해주세요.
"""
response = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=2500,
messages=[{
"role": "user",
"content": prompt
}]
)
return response.content[0].text
if __name__ == "__main__":
# 메타데이터 생성
metadata = generate_play_store_metadata(
app_name="Productivity Master",
app_description="A comprehensive task management app",
features=[
"Real-time task synchronization",
"Offline support",
"Custom reminders",
"Team collaboration",
"Beautiful UI with Material Design 3"
]
)
# 파일로 저장
output_dir = "metadata/generated"
os.makedirs(output_dir, exist_ok=True)
with open(f"{output_dir}/metadata.json", "w") as f:
json.dump(metadata, f, indent=2, ensure_ascii=False)
# 개인정보 보호정책
privacy_policy = generate_privacy_policy(
app_name="Productivity Master",
collects_data={
"Task data": "로컬 저장소에만 저장",
"User preferences": "로컬 저장소에만 저장",
"Analytics": "크래시 리포팅용"
}
)
with open(f"{output_dir}/PRIVACY_POLICY.md", "w") as f:
f.write(privacy_policy)
# 이용약관
terms = generate_terms_of_service("Productivity Master")
with open(f"{output_dir}/TERMS_OF_SERVICE.md", "w") as f:
f.write(terms)
print("✅ 메타데이터 생성 완료!")
print(f"📁 저장 위치: {output_dir}")
print(json.dumps(metadata, indent=2, ensure_ascii=False))
실행:
cd metadata
python auto_metadata_generator.py
🔐 Step 6: Google Play Store 업로드 준비
6.1 서비스 계정 설정
#!/bin/bash
# scripts/setup_play_store_auth.sh
echo "🔐 Google Play Store 인증 설정 시작..."
# 1. Google Cloud 프로젝트에서 서비스 계정 생성
echo "1️⃣ Google Cloud Console에 접속하세요: https://console.cloud.google.com"
echo " - 'IAM 및 관리' > '서비스 계정' 선택"
echo " - '서비스 계정 만들기' 클릭"
echo " - 이름: play-store-deployer"
echo " - 역할: 편집자"
# 2. JSON 키 생성
echo ""
echo "2️⃣ 서비스 계정 상세 페이지에서:"
echo " - '키' 탭 > '새 키 추가' > 'JSON' 선택"
echo " - 다운로드된 JSON 파일을 저장하세요"
# 3. Google Play Console에 등록
echo ""
echo "3️⃣ Google Play Console에서:"
echo " - 계정 설정 > 개발자 계정 > 사용자 및 권한"
echo " - 서비스 계정 권한 추가"
echo " - 서비스 계정의 이메일 주소 추가"
echo " - 권한: 모든 권한 (또는 필요한 권한만 선택)"
# 4. GitHub Secrets 설정
echo ""
echo "4️⃣ GitHub Repository에서:"
echo " - Settings > Secrets and variables > Actions"
echo " - 'New repository secret' 추가"
echo " - 이름: PLAY_STORE_SERVICE_ACCOUNT"
echo " - 값: 다운로드한 JSON 파일 전체 내용"
# 5. 서명 키 생성
echo ""
echo "5️⃣ 앱 서명 키 생성:"
mkdir -p keystore
# 테스트용 키스토어 생성 (프로덕션에서는 안전한 키 사용)
keytool -genkey -v \
-keystore keystore/release.keystore \
-keyalias release-key \
-keyalg RSA \
-keysize 2048 \
-validity 10000
# 키스토어를 Base64로 인코딩
echo ""
echo "6️⃣ GitHub Secrets에 추가할 항목:"
KEY_BASE64=$(base64 -i keystore/release.keystore)
echo "SIGNING_KEY: $KEY_BASE64"
echo ""
echo "✅ 설정 완료!"
실행:
chmod +x scripts/setup_play_store_auth.sh
scripts/setup_play_store_auth.sh
📊 Step 7: 완전 자동화 통합 스크립트
7.1 엔드-투-엔드 자동화
# scripts/full_automation.py
#!/usr/bin/env python3
import os
import sys
import json
import subprocess
from datetime import datetime
from pathlib import Path
class AndroidAutomationPipeline:
def __init__(self, project_root: str):
self.root = Path(project_root)
self.timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
self.log_file = self.root / f"automation_log_{self.timestamp}.txt"
def log(self, message: str, level: str = "INFO"):
"""로그 기록"""
log_msg = f"[{datetime.now().isoformat()}] [{level}] {message}"
print(log_msg)
with open(self.log_file, "a") as f:
f.write(log_msg + "\n")
def run_command(self, cmd: str, description: str):
"""명령어 실행 및 로깅"""
self.log(f"Running: {description}")
try:
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
if result.returncode == 0:
self.log(f"✅ {description} - SUCCESS")
return True
else:
self.log(f"❌ {description} - FAILED", "ERROR")
self.log(f"Error: {result.stderr}", "ERROR")
return False
except Exception as e:
self.log(f"❌ Exception in {description}: {str(e)}", "ERROR")
return False
def stage_1_market_research(self):
"""1단계: 시장수요조사"""
self.log("=" * 50)
self.log("STAGE 1: 시장수요조사")
self.log("=" * 50)
research_script = self.root / "market_research" / "auto_research.py"
success = self.run_command(
f"python {research_script}",
"Market Research"
)
return success
def stage_2_app_development(self):
"""2단계: 앱 개발"""
self.log("=" * 50)
self.log("STAGE 2: 앱 개발 자동화")
self.log("=" * 50)
init_script = self.root / "app_dev" / "init_android_project.sh"
success = self.run_command(
f"bash {init_script}",
"Android Project Initialization"
)
if success:
self.log("Generating Kotlin source files...")
# OpenCode를 사용한 소스 코드 생성
success = self.run_command(
f"opencode --file {self.root}/app_dev/create_app_prompt.txt",
"Generate Source Code"
)
return success
def stage_3_build_test(self):
"""3단계: 빌드 및 테스트"""
self.log("=" * 50)
self.log("STAGE 3: 빌드 및 테스트")
self.log("=" * 50)
app_dir = self.root / "MyProductivityApp"
os.chdir(app_dir)
# 린트 체크
success = self.run_command(
"./gradlew lint",
"Lint Check"
)
if not success:
return False
# 유닛 테스트
success = self.run_command(
"./gradlew testDebugUnitTest",
"Unit Tests"
)
if not success:
return False
# Release 빌드
success = self.run_command(
"./gradlew bundleRelease -x test",
"Build Release Bundle (AAB)"
)
if not success:
return False
# APK 빌드 (테스트용)
success = self.run_command(
"./gradlew assembleRelease -x test",
"Build Release APK"
)
return success
def stage_4_metadata_generation(self):
"""4단계: 메타데이터 생성"""
self.log("=" * 50)
self.log("STAGE 4: 메타데이터 자동 생성")
self.log("=" * 50)
metadata_script = self.root / "metadata" / "auto_metadata_generator.py"
success = self.run_command(
f"python {metadata_script}",
"Generate Google Play Metadata"
)
return success
def stage_5_upload_to_play_store(self, track: str = "internal"):
"""5단계: Google Play Store 업로드"""
self.log("=" * 50)
self.log(f"STAGE 5: Google Play Store 업로드 ({track})")
self.log("=" * 50)
app_dir = self.root / "MyProductivityApp"
os.chdir(app_dir)
# Fastlane으로 배포
if track == "internal":
success = self.run_command(
"fastlane android deploy_internal",
"Deploy to Internal Testing"
)
elif track == "beta":
success = self.run_command(
"fastlane android deploy_beta",
"Deploy to Beta Testing"
)
elif track == "production":
success = self.run_command(
"fastlane android deploy_production",
"Deploy to Production"
)
else:
self.log(f"Unknown track: {track}", "ERROR")
return False
return success
def generate_report(self):
"""최종 리포트 생성"""
self.log("=" * 50)
self.log("최종 리포트 생성 중...")
self.log("=" * 50)
report_file = self.root / f"automation_report_{self.timestamp}.json"
report = {
"timestamp": datetime.now().isoformat(),
"automation_duration": "see log file",
"stages_completed": {
"market_research": "✅",
"app_development": "✅",
"build_and_test": "✅",
"metadata_generation": "✅",
"play_store_upload": "✅"
},
"artifacts": {
"market_research_report": str(self.root / "market_research" / "report_*.md"),
"android_project": str(self.root / "MyProductivityApp"),
"aab_file": str(self.root / "MyProductivityApp" / "app" / "build" / "outputs" / "bundle" / "release" / "app-release.aab"),
"apk_file": str(self.root / "MyProductivityApp" / "app" / "build" / "outputs" / "apk" / "release" / "app-release.apk"),
"metadata": str(self.root / "metadata" / "generated"),
"log_file": str(self.log_file)
},
"next_steps": [
"✅ 시장 조사 완료 - 시장 기회와 차별화 전략 확인",
"✅ 앱 개발 완료 - 자동 생성된 프로젝트 검토",
"✅ 테스트 통과 - 빌드 및 단위 테스트 완료",
"✅ 메타데이터 생성 - Google Play 스토어 정보 자동 작성",
"✅ 스토어 업로드 - 내부 테스트 트랙에 배포"
]
}
with open(report_file, "w") as f:
json.dump(report, f, indent=2, ensure_ascii=False)
self.log(f"✅ 최종 리포트: {report_file}")
return report
def run_full_pipeline(self, track: str = "internal"):
"""완전 자동화 파이프라인 실행"""
self.log(f"🚀 Android 앱 자동화 파이프라인 시작")
self.log(f"프로젝트 경로: {self.root}")
try:
# 1단계
if not self.stage_1_market_research():
raise Exception("Market research failed")
# 2단계
if not self.stage_2_app_development():
raise Exception("App development failed")
# 3단계
if not self.stage_3_build_test():
raise Exception("Build and test failed")
# 4단계
if not self.stage_4_metadata_generation():
raise Exception("Metadata generation failed")
# 5단계
if not self.stage_5_upload_to_play_store(track):
raise Exception("Play Store upload failed")
# 리포트 생성
report = self.generate_report()
self.log("=" * 50)
self.log("🎉 전체 파이프라인 성공적으로 완료!")
self.log("=" * 50)
return True
except Exception as e:
self.log(f"❌ 파이프라인 실패: {str(e)}", "ERROR")
return False
if __name__ == "__main__":
project_root = os.getcwd()
track = sys.argv[1] if len(sys.argv) > 1 else "internal"
pipeline = AndroidAutomationPipeline(project_root)
success = pipeline.run_full_pipeline(track)
sys.exit(0 if success else 1)
실행:
cd android-automation-workspace
# 내부 테스트 트랙으로 배포
python scripts/full_automation.py internal
# 또는 베타 테스트 트랙
python scripts/full_automation.py beta
# 또는 프로덕션
python scripts/full_automation.py production
🔄 Step 8: 자동화 스케줄링
8.1 정기적 자동화 실행
#!/bin/bash
# scripts/schedule_automation.sh
# Cron 작업 설정 (예: 매주 월요일 00:00)
cat > /tmp/automation_cron << 'EOF'
0 0 * * 1 cd /path/to/android-automation-workspace && python scripts/full_automation.py internal 2>&1 | tee -a automation_scheduler.log
EOF
crontab /tmp/automation_cron
# 또는 systemd 타이머 사용
mkdir -p ~/.config/systemd/user/
cat > ~/.config/systemd/user/android-automation.service << 'EOF'
[Unit]
Description=Android App Automation Pipeline
After=network-online.target
[Service]
Type=oneshot
WorkingDirectory=/path/to/android-automation-workspace
ExecStart=/usr/bin/python3 /path/to/android-automation-workspace/scripts/full_automation.py internal
EOF
cat > ~/.config/systemd/user/android-automation.timer << 'EOF'
[Unit]
Description=Run Android App Automation Weekly
[Timer]
OnCalendar=Mon *-*-* 00:00:00
Persistent=true
[Install]
WantedBy=timers.target
EOF
systemctl --user daemon-reload
systemctl --user enable android-automation.timer
systemctl --user start android-automation.timer
🎯 Step 9: 모니터링 및 알림
9.1 실시간 모니터링
# scripts/monitor_automation.py
import os
import json
import smtplib
from datetime import datetime
from pathlib import Path
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
class AutomationMonitor:
def __init__(self, workspace_path: str, email_config: dict):
self.workspace = Path(workspace_path)
self.email_config = email_config
def check_latest_log(self) -> dict:
"""최신 로그 확인"""
log_files = sorted(self.workspace.glob("automation_log_*.txt"), reverse=True)
if not log_files:
return {"status": "ERROR", "message": "No log files found"}
latest_log = log_files[0]
with open(latest_log) as f:
content = f.read()
status = "SUCCESS" if "성공적으로 완료" in content else "FAILED"
return {
"status": status,
"log_file": str(latest_log),
"timestamp": latest_log.stem.split("_", 2)[2],
"content_preview": content[-500:]
}
def send_email_notification(self, status: dict):
"""이메일 알림 전송"""
try:
sender_email = self.email_config["sender"]
sender_password = self.email_config["password"]
recipient_email = self.email_config["recipient"]
message = MIMEMultipart()
message["From"] = sender_email
message["To"] = recipient_email
message["Subject"] = f"[Android Automation] {status['status']}"
body = f"""
Android App Automation Pipeline 결과
상태: {status['status']}
시간: {status['timestamp']}
로그: {status['log_file']}
미리보기:
{status['content_preview']}
"""
message.attach(MIMEText(body, "plain"))
with smtplib.SMTP_SSL("smtp.gmail.com", 465) as server:
server.login(sender_email, sender_password)
server.send_message(message)
print("✅ 이메일 알림 전송 완료")
except Exception as e:
print(f"❌ 이메일 전송 실패: {e}")
def generate_dashboard_data(self) -> dict:
"""대시보드용 데이터 생성"""
reports = sorted(self.workspace.glob("automation_report_*.json"), reverse=True)
recent_reports = []
for report_file in reports[:10]:
with open(report_file) as f:
recent_reports.append(json.load(f))
return {
"total_runs": len(reports),
"recent_reports": recent_reports,
"success_rate": f"{(len([r for r in recent_reports if r['stages_completed']['play_store_upload'] == '✅']) / len(recent_reports) * 100):.1f}%"
}
if __name__ == "__main__":
email_config = {
"sender": os.getenv("MONITOR_EMAIL"),
"password": os.getenv("MONITOR_PASSWORD"),
"recipient": os.getenv("NOTIFY_EMAIL")
}
monitor = AutomationMonitor(
workspace_path="/path/to/android-automation-workspace",
email_config=email_config
)
# 최신 로그 확인
status = monitor.check_latest_log()
print(json.dumps(status, indent=2))
# 이메일 알림 (필요시)
if os.getenv("SEND_EMAIL") == "true":
monitor.send_email_notification(status)
# 대시보드 데이터
dashboard_data = monitor.generate_dashboard_data()
print("\n대시보드 데이터:")
print(json.dumps(dashboard_data, indent=2))
📦 최종 디렉토리 구조
android-automation-workspace/
├── market_research/
│ ├── auto_research.py # 시장 조사 자동화
│ ├── research_prompt.txt
│ └── report_20250213_*.md # 생성된 시장 리포트
│
├── app_dev/
│ ├── init_android_project.sh # Android 프로젝트 초기화
│ ├── create_app_prompt.txt
│ └── MyProductivityApp/ # 생성된 Android 프로젝트
│ ├── app/
│ ├── data/
│ ├── domain/
│ ├── presentation/
│ ├── build.gradle.kts
│ └── gradlew
│
├── metadata/
│ ├── auto_metadata_generator.py # 메타데이터 자동 생성
│ └── generated/
│ ├── metadata.json
│ ├── PRIVACY_POLICY.md
│ └── TERMS_OF_SERVICE.md
│
├── .github/
│ └── workflows/
│ └── android-ci-cd.yml # GitHub Actions 워크플로우
│
├── android/
│ └── fastlane/
│ └── Fastfile # Fastlane 배포 설정
│
├── scripts/
│ ├── full_automation.py # 완전 자동화 파이프라인
│ ├── setup_play_store_auth.sh # Play Store 인증 설정
│ ├── schedule_automation.sh # 자동화 스케줄링
│ └── monitor_automation.py # 모니터링 및 알림
│
├── AGENTS.md # OpenCode 프로젝트 정보
├── automation_log_*.txt # 자동화 로그
├── automation_report_*.json # 최종 리포트
└── README.md # 프로젝트 문서
🚀 빠른 시작 가이드
# 1. 작업공간 초기화
cd android-automation-workspace
opencode init
# 2. 완전 자동화 실행
python scripts/full_automation.py internal
# 3. 결과 확인
cat automation_report_*.json | jq .
# 4. GitHub에 푸시 (CI/CD 자동 트리거)
git push origin main
⚠️ 주요 주의사항
- Play Store 서비스 계정: 안전하게 저장하고 GitHub Secrets에 등록
- 키스토어 관리: 프로덕션 키스토어는 별도로 보관
- 배포 정책: 첫 배포는 항상 내부 테스트 → 베타 → 프로덕션 순서
- 테스트 커버리지: 최소 70% 이상의 테스트 커버리지 권장
당신의 고급 기술 스택을 고려했을 때, 이 자동화 파이프라인은 Docker 컨테이너화, Kubernetes 배포, 멀티 환경 관리 등으로 더욱 확장할 수 있습니다. 추가 질문이 있으신가요?
인용:
[1] Opencode와 MCP로 앱스 스크립트 자동 개발하기 - 중년코딩 https://wikidocs.net/blog/@JNCD/7158/
[2] 🚀 Android CI/CD with Azure DevOps | Full Pipeline + Google Play Deployment https://www.youtube.com/watch?v=3Ia6kIVZ92Y
[3] Upload App to Google Play Console & Run a Closed Test https://applikeweb.com/blog/upload-app-google-play-console-closed-test/
[4] OpenCode Remote: monitor and control your OpenCode sessions from Android (open source) https://www.reddit.com/r/opencodeCLI/comments/1qz5hor/opencode_remote_monitor_and_control_your_opencode/
[5] How to Build a CI/CD Pipeline for Android Projects https://blog.jetbrains.com/teamcity/2024/07/cicd-for-android/
[6] How to automatically upload Android app from Jenkins ... https://stackoverflow.com/questions/21945779/how-to-automatically-upload-android-app-from-jenkins-to-google-play-alpha-test
[7] Docs https://opencode.ai/docs
[8] Flutter App Deployment - Leverage Codemagic CD to publish your app to Google Play Store https://www.youtube.com/watch?v=mg8_pM7sGM8
[9] Automatic publish Beta Android app to the Google Play store https://stackoverflow.com/questions/23427304/automatic-publish-beta-android-app-to-the-google-play-store
[10] giuliastro/opencode-remote-android https://github.com/giuliastro/opencode-remote-android
[11] CI/CD, 왜 해야 할까? (ft. GitHub Actions로 완성하는 Android ... https://velog.io/@hearit/CICD-왜-해야-할까-ft.-GitHub-Actions로-완성하는-Android-자동-배포
[12] Automating Publishing to the Play Store | CodePath Android Cliffnotes https://guides.codepath.com/android/automating-publishing-to-the-play-store
[13] AppFramework https://opencode.de/en/software/app-framework-4588
[14] Complete Guide to Android App Publishing in 2025 https://foresightmobile.com/blog/complete-guide-to-android-app-publishing-in-2025
[15] Automate Publishing your Android app to Google Play Store ... https://www.zone2.tech/blog/play-store-automation