Skip to content

✨[FEAT] Bitrise CI/CD 환경 구축#137

Open
SuHyeon00 wants to merge 17 commits intodevelopfrom
feature/bitrise-setting
Open

✨[FEAT] Bitrise CI/CD 환경 구축#137
SuHyeon00 wants to merge 17 commits intodevelopfrom
feature/bitrise-setting

Conversation

@SuHyeon00
Copy link
Contributor

@SuHyeon00 SuHyeon00 commented Feb 8, 2026

📝 작업 내용

  • Bitrise 빌드 파이프라인 워크플로우 구축
  • 앞으로 작업 후 PM님께 빌드본 공유드릴 때 에디슨 계정으로 bitrise 들어가셔서 Start Build로 빌드본 공유드릴 브랜치 선택하시고 워크플로우는 build-qa로 선택 하여 워크플로우 실행시키면 됩니다!

📸 스크린샷 (선택)

스크린샷 2026-02-08 23 43 58 스크린샷 2026-02-08 23 45 48 스크린샷 2026-02-08 23 46 59

💬 리뷰 요구사항(선택)

리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요

ex) 메서드 XXX의 이름을 더 잘 짓고 싶은데 혹시 좋은 명칭이 있을까요?


Note

Medium Risk
Introduces dynamic base URL switching via Firebase Remote Config and adjusts token refresh/caching logic, which can affect networking/auth flows if misconfigured. CI/build variant changes (new qa type, Google services/Crashlytics setup) could also impact release packaging and runtime configuration.

Overview
Adds Bitrise CI (bitrise.yml) with build-qa and build-release workflows that generate local.properties, provision google-services.json per build type, build/sign APKs, deploy artifacts, and optionally notify Discord.

Integrates Firebase (Analytics, Remote Config, Crashlytics) and adds a new qa build type plus debug/release suffixing; app startup now fetches Remote Config with retry/backoff to dynamically override the API base domain via DomainProvider + HostSelectionInterceptor.

Centralizes logging into AppLogger (Logcat + Crashlytics) and adds Crashlytics user/build context (UserContext), wiring it into app init and Google login; also tightens token refresh/error handling (suspend refresh API, explicit refresh failures, broader unauthorized detection) and makes TokenManager thread-safe with preloading + mutex.

Written by Cursor Bugbot for commit dd37659. This will update automatically on new commits. Configure here.

@SuHyeon00 SuHyeon00 self-assigned this Feb 8, 2026
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @SuHyeon00, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

이 Pull Request는 Bitrise를 활용한 CI/CD 환경을 구축하여 개발 및 배포 프로세스를 자동화하고 효율성을 높이는 데 중점을 둡니다. 또한, Firebase 서비스를 통합하여 앱의 안정성 모니터링(Crashlytics), 원격 설정 관리(Remote Config), 사용자 분석(Analytics) 기능을 강화했습니다. 이와 함께 네트워크 통신 및 토큰 관리 로직을 개선하여 앱의 견고성을 향상시키고, 로깅 시스템을 중앙화하여 디버깅 및 문제 해결을 용이하게 했습니다.

Highlights

  • Bitrise CI/CD 환경 구축: Bitrise CI/CD 파이프라인을 구축하여 QA 및 릴리즈 빌드를 자동화했습니다. 이는 bitrise.yml 파일에 정의된 워크플로우를 통해 Google Services 설정, 환경 변수 주입, APK 빌드 및 서명, Bitrise 배포, Discord 알림 등의 과정을 포함합니다.
  • Firebase 통합 및 로깅 개선: Firebase Crashlytics, Remote Config, Analytics를 앱에 통합했습니다. 모든 android.util.Log 호출을 AppLogger 유틸리티 클래스로 대체하여 Crashlytics에 로그를 자동으로 기록하고 예외를 보고하도록 중앙 집중화했습니다. 또한 UserContext를 통해 사용자 식별자 및 빌드 정보를 Crashlytics에 설정합니다.
  • 동적 베이스 URL 및 토큰 관리 강화: DomainProviderHostSelectionInterceptor를 도입하여 앱의 베이스 URL을 동적으로 설정할 수 있도록 했습니다. 토큰 관리 로직은 Mutex를 사용하여 스레드 안전성을 확보하고, NoRefreshTokenException, RefreshFailedException과 같은 구체적인 예외를 정의하여 오류 처리를 개선했습니다. 또한, 토큰 재시도 핸들러가 HttpException의 401 응답 코드도 처리하도록 확장되었습니다.
  • QA 빌드 타입 추가: 새로운 qa 빌드 타입을 app/build.gradle.kts에 추가하여 QA 환경에 특화된 애플리케이션 ID 및 버전 접미사를 사용할 수 있도록 구성했습니다.
  • 사용자 모델에 ID 필드 추가: 도메인 및 데이터 계층의 UserUserEntity 모델에 id: Long? 필드를 추가하고, 로그인 및 회원가입 응답에서 이 ID를 매핑하도록 업데이트했습니다. 이는 Crashlytics 사용자 식별자 설정에 활용됩니다.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • app/build.gradle.kts
    • Google Services 및 Firebase Crashlytics Gradle 플러그인 추가
    • 새로운 'qa' 빌드 타입 정의 (applicationIdSuffix, versionNameSuffix, buildConfigField 포함)
    • Compose Options 블록 제거
    • Firebase BOM, Config, Analytics, Crashlytics 라이브러리 의존성 추가
  • app/src/main/java/com/umc/edison/EdisonApplication.kt
    • Firebase, AppLogger, DomainProvider, Coroutine 관련 import 추가
    • DomainProvider 및 UserContext 주입
    • Crashlytics 컨텍스트 초기화 및 Remote Config 초기화 로직 추가
    • Remote Config를 통해 동적으로 base_url 설정 기능 구현
  • app/src/main/java/com/umc/edison/common/logging/AppLogger.kt
    • 새 파일 추가: android.util.Log 호출을 Firebase Crashlytics 로깅과 통합하는 유틸리티 객체 구현
  • app/src/main/java/com/umc/edison/common/logging/UserContext.kt
    • 새 파일 추가: Crashlytics에 사용자 설치 ID, 계정 ID, 빌드 정보(타입, ID, 버전)를 설정하는 싱글톤 클래스 구현
  • app/src/main/java/com/umc/edison/data/model/user/UserEntity.kt
    • id: Long? 필드 추가
    • toDomain()toData() 메서드에 id 필드 매핑 로직 추가
  • app/src/main/java/com/umc/edison/data/repository/BubbleRepositoryImpl.kt
    • android.util.Log 호출을 AppLogger로 대체
  • app/src/main/java/com/umc/edison/data/repository/SyncRepositoryImpl.kt
    • android.util.Log 호출을 AppLogger로 대체
  • app/src/main/java/com/umc/edison/data/token/DefaultTokenRetryHandler.kt
    • retrofit2.HttpException import 추가
    • 토큰 재시도 로직에서 ApiException 외에 HttpException (401 Unauthorized)도 처리하도록 확장
    • 새로 정의된 NoRefreshTokenException 사용
  • app/src/main/java/com/umc/edison/data/token/TokenExceptions.kt
    • 새 파일 추가: NoRefreshTokenExceptionRefreshFailedException 정의
  • app/src/main/java/com/umc/edison/data/token/TokenManager.kt
    • 토큰 로딩 및 캐싱 작업에 kotlinx.coroutines.sync.Mutex를 사용하여 스레드 안전성 확보
    • 초기화 로직을 preloadTokens() 메서드로 분리하고 loadJob을 통해 비동기 로딩 관리
    • ensureLoaded() 메서드를 추가하여 토큰 접근 전에 로딩 완료 보장
    • 기존 println 경고 메시지를 AppLogger.w로 대체
  • app/src/main/java/com/umc/edison/domain/model/user/User.kt
    • id: Long? 필드 추가
  • app/src/main/java/com/umc/edison/presentation/base/BaseViewModel.kt
    • android.util.Log 호출을 AppLogger로 대체
  • app/src/main/java/com/umc/edison/presentation/login/GoogleLoginHelper.kt
    • Firebase Crashlytics, AppLogger, UserContext import 추가
    • UserContext 주입
    • Google 로그인 실패 시 Crashlytics에 오류 정보 기록
    • android.util.Log 호출을 AppLogger로 대체
    • Google 로그인 성공 시 UserContext를 통해 Crashlytics 사용자 ID 설정
  • app/src/main/java/com/umc/edison/presentation/model/UserModel.kt
    • toDomain() 메서드에 id = null 매핑 추가
  • app/src/main/java/com/umc/edison/remote/api/RefreshTokenApiService.kt
    • refreshToken 메서드를 suspend 함수로 변경
  • app/src/main/java/com/umc/edison/remote/config/DomainProvider.kt
    • 새 파일 추가: 앱의 베이스 도메인을 동적으로 관리하고 제공하는 싱글톤 클래스 구현
  • app/src/main/java/com/umc/edison/remote/datasources/UserRemoteDataSourceImpl.kt
    • RefreshFailedException import 추가
    • refreshAccessToken 메서드에서 응답의 isSuccess를 확인하고 실패 시 RefreshFailedException 발생
  • app/src/main/java/com/umc/edison/remote/di/NetworkModule.kt
    • HostSelectionInterceptor import 추가
    • provideOkHttpClientHostSelectionInterceptor 추가하여 동적 호스트 선택 기능 활성화
  • app/src/main/java/com/umc/edison/remote/interceptor/HostSelectionInterceptor.kt
    • 새 파일 추가: DomainProvider를 사용하여 요청의 호스트를 동적으로 변경하는 OkHttp 인터셉터 구현
  • app/src/main/java/com/umc/edison/remote/model/login/LoginResponse.kt
    • memberIdUserEntity.id에 매핑하도록 toUserEntity() 메서드 수정
  • app/src/main/java/com/umc/edison/remote/model/login/SignUpResponse.kt
    • memberIdUserEntity.id에 매핑하도록 toUserEntity() 메서드 수정
  • app/src/main/java/com/umc/edison/remote/token/AccessTokenInterceptor.kt
    • 액세스 토큰이 없을 경우 AppLogger.w를 사용하여 경고 로그 기록
  • app/src/main/java/com/umc/edison/ui/artboard/ArtLetterDetailScreen.kt
    • android.util.Log 호출을 AppLogger로 대체
  • app/src/main/java/com/umc/edison/ui/components/ImageGallery.kt
    • android.util.Log 호출을 AppLogger로 대체
  • app/src/main/java/com/umc/edison/ui/navigation/NavRoute.kt
    • BubbleEdit 경로 생성 시 bubbleId != null 조건을 !bubbleId.isNullOrEmpty()로 변경
  • bitrise.yml
    • 새 파일 추가: Bitrise CI/CD 워크플로우 정의 (build-qa, build-release)
    • Google Services JSON 파일 설정 스크립트 포함
    • 환경 변수 설정 스크립트 (BASE_URL, BRANCH_KEY 등) 포함
    • Gradle 빌드 태스크 동적 선택 로직 포함
    • APK 서명, Bitrise 배포, Discord 알림 단계 포함
  • build.gradle.kts
    • 루트 build.gradle.kts에 Google Services 및 Firebase Crashlytics 플러그인 apply false로 추가
  • gradle/libs.versions.toml
    • Hilt 라이브러리 버전 업데이트
    • Firebase BOM, Google Services, Crashlytics, Crashlytics Plugin 버전 정의 및 라이브러리/플러그인 별칭 추가
Activity
  • 이 Pull Request는 Bitrise CI/CD 환경을 처음으로 구축하고 Firebase 통합을 진행하는 초기 설정 작업입니다.
  • 작성자는 PM이 Bitrise에 접속하여 'Start Build'를 통해 브랜치를 선택하고 'build-qa' 워크플로우를 실행하여 빌드본을 공유할 수 있도록 안내했습니다.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This PR successfully establishes the Bitrise CI/CD environment and integrates Firebase for remote configuration and crash reporting, including the introduction of a qa build type and dynamic base URL switching via Firebase Remote Config. It also refactors to a centralized AppLogger and makes TokenManager thread-safe. However, significant security concerns have been identified. The AppLogger performs insecure logging, potentially sending PII and authentication tokens to Firebase Crashlytics across all build types. Furthermore, the dynamic base URL override via Firebase Remote Config is unvalidated, which could lead to sensitive API request interception and user access token theft if the Remote Config source is compromised. Proper log filtering and domain validation are crucial to mitigate these risks. While the overall code quality is high, general improvements are also suggested for code duplication and potential threading issues.

Comment on lines +13 to +17
fun setDomain(domain: String) {
if (domain.isNotBlank() && domain != current) {
current = domain
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The application allows overriding the API base URL via Firebase Remote Config without validation. This URL is used by HostSelectionInterceptor to redirect all API requests. Since AccessTokenInterceptor adds the user's access token to these requests, a compromised Remote Config could be used to redirect traffic to a malicious server and steal user credentials.

It is highly recommended to validate the domain string against an allowlist of trusted domains before updating the current domain.

Comment on lines 36 to 52
override fun clearCachedTokens() {
cachedAccessToken = null
cachedRefreshToken = null
runBlocking {
mutex.withLock {
cachedAccessToken = null
cachedRefreshToken = null
}
}
}

override fun setCachedTokens(accessToken: String, refreshToken: String?) {
cachedAccessToken = accessToken
cachedRefreshToken = refreshToken
runBlocking {
mutex.withLock {
cachedAccessToken = accessToken
cachedRefreshToken = refreshToken
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

clearCachedTokenssetCachedTokens 함수에서 runBlocking을 사용하고 있습니다. 이 함수들이 메인 스레드에서 호출될 경우 UI 스레드를 차단하여 ANR(Application Not Responding)을 유발할 수 있습니다. AccessTokenProvider 인터페이스의 제약으로 인해 suspend 키워드를 사용하기 어렵다면, 이 함수들이 백그라운드 스레드에서만 호출되도록 특별히 주의해야 합니다. 가능하다면 suspend 함수로 리팩토링하는 것을 장기적으로 고려해보시는 것이 좋습니다.

Comment on lines +15 to +18
fun d(tag: String, message: String) {
if (isDebug) Log.d(tag, message)
Firebase.crashlytics.log("$PREFIX_DEBUG$tag: $message")
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The AppLogger.d implementation sends all log messages to Firebase Crashlytics regardless of the build type. While Log.d is conditionally called based on isDebug, Firebase.crashlytics.log is always executed. This can lead to sensitive information being sent to a third-party service even in release builds.

Specifically, BaseViewModel.kt (line 42) logs the entire dataResource.data object on success, which often contains PII such as user email addresses (see User and UserEntity models). Additionally, GoogleLoginHelper.kt (lines 91 and 104) logs Google ID tokens in debug builds, which are then transmitted to Crashlytics via this function.

applicationIdSuffix = ".qa"
versionNameSuffix = "-qa"
matchingFallbacks += listOf("debug")
buildConfigField("String", "BASE_URL", "\"${localProperties["base_url"] ?: ""}\"")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

qa 빌드 유형에서 buildConfigField를 다시 정의하고 있습니다. initWith(getByName("debug"))를 통해 debug 빌드 유형의 설정을 이미 상속받았기 때문에 이 부분은 중복됩니다. 코드의 간결성과 유지보수성을 위해 이 라인을 제거하는 것이 좋습니다.


if (attempt < maxAttempts - 1) {
delay(backoff)
backoff = (backoff * 2).coerceAtMost(8_000L)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

8_000L이라는 매직 넘버가 사용되었습니다. 이 값을 클래스 수준의 상수로 추출하면 코드의 가독성과 유지보수성이 향상됩니다. 예를 들어, 클래스 상단에 private const val MAX_BACKOFF_MS = 8_000L와 같이 정의하고, 이 라인에서는 backoff = (backoff * 2).coerceAtMost(MAX_BACKOFF_MS)와 같이 사용하는 것을 권장합니다.

Comment on lines +27 to +33
if [ -z "$BUILD_TYPE" ]; then
if [ "$BITRISE_GIT_BRANCH" = "main" ] || [ "$BITRISE_GIT_BRANCH" = "master" ]; then
BUILD_TYPE="release"
else
BUILD_TYPE="qa"
fi
fi
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

BUILD_TYPE을 결정하는 로직이 중복되어 있습니다. 이 스크립트("Setup google-services.json files")에서 BUILD_TYPE을 결정하지만, 바로 다음 스크립트("Set environment variables")에서도 동일한 목적으로 BUILD_TYPE을 결정하고 envman을 통해 환경 변수로 설정하고 있습니다. 후자의 값이 빌드 전반에 사용되므로, 이 블록은 불필요한 중복입니다. 코드의 명확성을 위해 이 if [ -z "$BUILD_TYPE" ] 블록을 제거하는 것을 권장합니다.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

This PR is being reviewed by Cursor Bugbot

Details

You are on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle.

To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" }
firebase-config-ktx = { module = "com.google.firebase:firebase-config-ktx" }
firebase-analytics-ktx = { module = "com.google.firebase:firebase-analytics-ktx" }
firebase-crashlytics-ktx = { module = "com.google.firebase:firebase-crashlytics-ktx", version.ref = "crashlytics" }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Firebase Crashlytics version bypasses BOM management

Medium Severity

The firebase-crashlytics-ktx library has an explicit version (18.6.2) specified via version.ref = "crashlytics", while firebase-config-ktx and firebase-analytics-ktx rely on the BOM for version management. When using Firebase BOM with platform(), libraries with explicit versions override the BOM-managed versions. This inconsistency means Crashlytics will use version 18.6.2 while other Firebase libraries use whatever BOM 33.1.0 specifies, potentially causing runtime compatibility issues between Firebase components.

Fix in Cursor Fix in Web

mutex.withLock {
cachedAccessToken = prefDataSource.get(ACCESS_TOKEN_KEY, "").ifEmpty { null }
cachedRefreshToken = prefDataSource.get(REFRESH_TOKEN_KEY, "").ifEmpty { null }
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mutex held during I/O causes potential main thread blocking

Medium Severity

The preloadTokens() function holds the mutex while performing I/O via prefDataSource.get() (suspend calls). Meanwhile, clearCachedTokens() and setCachedTokens() use runBlocking { mutex.withLock {...} }. If either of these non-suspend interface methods is called from the main thread while preloadTokens() is holding the mutex during I/O, the main thread will block until the I/O completes, potentially causing ANR. The I/O operations ideally would be performed outside the mutex lock, with the lock only protecting the in-memory cache updates.

Additional Locations (1)

Fix in Cursor Fix in Web

.connectTimeout(TIME_OUT.toLong(), TimeUnit.SECONDS)
.readTimeout(TIME_OUT.toLong(), TimeUnit.SECONDS)
.writeTimeout(TIME_OUT.toLong(), TimeUnit.SECONDS)
.addInterceptor(hostSelectionInterceptor)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refresh token API ignores dynamic domain configuration

High Severity

The new HostSelectionInterceptor is added to the main OkHttpClient (used by MainRetrofit) but not to RefreshRetrofit. This means main API calls respect the dynamic domain from DomainProvider (set via Firebase Remote Config), while token refresh calls always go to the static BuildConfig.BASE_URL. If the domain is updated at runtime, subsequent token refresh attempts will fail because they hit the wrong server, causing authentication failures and potential user logouts.

Additional Locations (1)

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Comments