Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions .github/workflows/android-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ permissions:
env:
# PR CI는 compile/build 체크만 하므로, fork PR에서는 dummy 값으로도 통과 가능하게 둔다.
KAKAO_NATIVE_APP_KEY_DEV: ${{ secrets.KAKAO_NATIVE_APP_KEY_DEV || 'dummy-ci-kakao-key' }}
KAKAO_NATIVE_APP_KEY_STAGING: ${{ secrets.KAKAO_NATIVE_APP_KEY_STAGING || 'dummy-ci-kakao-key' }}
KAKAO_NATIVE_APP_KEY_PROD: ${{ secrets.KAKAO_NATIVE_APP_KEY_PROD || 'dummy-ci-kakao-key' }}

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
Expand Down Expand Up @@ -74,5 +76,21 @@ jobs:
- name: Grant execute permission for gradlew
run: chmod +x ./gradlew

- name: Build (assembleDebug, only compile check)
run: ./gradlew :app:assembleDebug --no-daemon --stacktrace
- name: Build dev debug
run: ./gradlew :app:assembleDevDebug --no-daemon --stacktrace

- name: Build staging debug
if: github.event_name == 'push' && github.ref == 'refs/heads/develop'
run: ./gradlew :app:assembleStagingDebug --no-daemon --stacktrace

- name: Build staging release
if: github.event_name == 'push' && github.ref == 'refs/heads/develop'
run: ./gradlew :app:assembleStagingRelease --no-daemon --stacktrace
Comment thread
coderabbitai[bot] marked this conversation as resolved.

- name: Build prod debug
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
run: ./gradlew :app:assembleProdDebug --no-daemon --stacktrace

- name: Build prod release
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
run: ./gradlew :app:assembleProdRelease --no-daemon --stacktrace
107 changes: 94 additions & 13 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import java.util.Properties
import org.jetbrains.kotlin.gradle.dsl.JvmTarget

plugins {
alias(libs.plugins.android.application)
Expand All @@ -19,18 +19,62 @@ val localProperties = Properties().apply {
}
}

// 로컬은 local.properties를 우선 사용하고, CI는 env로 폴백하되 둘 다 없으면 즉시 실패한다.
val kakaoNativeAppKey = localProperties
.getProperty("kakao.native.app.key.dev")
.orEmpty()
.ifBlank { System.getenv("KAKAO_NATIVE_APP_KEY_DEV").orEmpty() }
.ifBlank {
fun requestedTasksMentionFlavor(flavorName: String): Boolean {
val flavorVariantPattern = Regex(".*$flavorName(debug|release|$).*", RegexOption.IGNORE_CASE)

return gradle.startParameter.taskNames.any { taskName ->
flavorVariantPattern.matches(taskName.substringAfterLast(":"))
}
}

fun requestedTasksIncludeAggregateBuild(): Boolean {
val flavors = listOf("dev", "staging", "prod")
val aggregateTaskNames = setOf("assemble", "build", "check", "test", "bundle", "install")
val aggregateVariantTaskPattern = Regex(
pattern = "(assemble|bundle|install)(debug|release)?|" +
"test(debug|release)?(unittest)?|" +
"connected(debug|release)?androidtest",
option = RegexOption.IGNORE_CASE
)

return gradle.startParameter.taskNames.any { taskName ->
val simpleTaskName = taskName.substringAfterLast(":")
val hasFlavor = flavors.any { flavor ->
Regex(".*$flavor(debug|release|$).*", RegexOption.IGNORE_CASE).matches(simpleTaskName)
}

!hasFlavor &&
(
simpleTaskName.lowercase() in aggregateTaskNames ||
aggregateVariantTaskPattern.matches(simpleTaskName)
)
}
}

fun shouldRequireKakaoNativeAppKey(flavorName: String): Boolean {
return requestedTasksMentionFlavor(flavorName) || requestedTasksIncludeAggregateBuild()
}

fun kakaoNativeAppKey(
flavorName: String,
propertyName: String,
environmentName: String
): String {
val key = localProperties
.getProperty(propertyName)
.orEmpty()
.ifBlank { System.getenv(environmentName).orEmpty() }

if (key.isBlank() && shouldRequireKakaoNativeAppKey(flavorName)) {
error(
"Missing Kakao native app key. Set 'kakao.native.app.key.dev' in local.properties " +
"or KAKAO_NATIVE_APP_KEY_DEV in the environment."
"Missing Kakao native app key for $flavorName. " +
"Set '$propertyName' in local.properties or $environmentName in the environment."
)
}

return key
}

android {
namespace = "com.lyrics.feelin"
compileSdk = 36
Expand All @@ -43,12 +87,49 @@ android {
versionName = "1.0.0"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

flavorDimensions += "environment"

productFlavors {
create("dev") {
dimension = "environment"
applicationId = "com.lyrics.feelin.dev"
val kakaoKey = kakaoNativeAppKey(
flavorName = name,
propertyName = "kakao.native.app.key.dev",
environmentName = "KAKAO_NATIVE_APP_KEY_DEV"
)
buildConfigField("String", "BASE_URL", "\"http://dev.feelinapp.com/\"")
buildConfigField("String", "KAKAO_NATIVE_APP_KEY", "\"$kakaoKey\"")
manifestPlaceholders["kakaoNativeAppKey"] = kakaoKey
}

// Application 클래스에서 사용할 BuildConfig 생성
buildConfigField("String", "KAKAO_NATIVE_APP_KEY", "\"$kakaoNativeAppKey\"")
create("staging") {
dimension = "environment"
applicationId = "com.lyrics.feelin.qa"
val kakaoKey = kakaoNativeAppKey(
flavorName = name,
propertyName = "kakao.native.app.key.staging",
environmentName = "KAKAO_NATIVE_APP_KEY_STAGING"
)
buildConfigField("String", "BASE_URL", "\"http://dev.feelinapp.com/\"")
buildConfigField("String", "KAKAO_NATIVE_APP_KEY", "\"$kakaoKey\"")
manifestPlaceholders["kakaoNativeAppKey"] = kakaoKey
}

// AndroidManifest.xml에서 사용할 placeholder
manifestPlaceholders["kakaoNativeAppKey"] = kakaoNativeAppKey
// MARK(@이대근): prod용 사이닝 키를 추후 생성 필요 2026.06.11.
create("prod") {
dimension = "environment"
val kakaoKey = kakaoNativeAppKey(
flavorName = name,
propertyName = "kakao.native.app.key.prod",
environmentName = "KAKAO_NATIVE_APP_KEY_PROD"
)
buildConfigField("String", "BASE_URL", "\"http://api.feelinapp.com/\"")
Comment thread
coderabbitai[bot] marked this conversation as resolved.
buildConfigField("String", "KAKAO_NATIVE_APP_KEY", "\"$kakaoKey\"")
manifestPlaceholders["kakaoNativeAppKey"] = kakaoKey
}
}

buildTypes {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
package com.lyrics.feelin

import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4

import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.*
import org.junit.Test
import org.junit.runner.RunWith

import org.junit.Assert.*

/**
* Instrumented test, which will execute on an Android device.
*
Expand All @@ -19,6 +17,6 @@ class ExampleInstrumentedTest {
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.lyrics.feelin", appContext.packageName)
assertEquals(BuildConfig.APPLICATION_ID, appContext.packageName)
}
}
}
3 changes: 3 additions & 0 deletions app/src/dev/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<resources>
<string name="app_name">[DEV] Feelin</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ object NetworkModule {
@Singleton
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl("http://dev.feelinapp.com/") // TODO(@이대근): productFlavor 변수화 필요 2025.10.05.
.baseUrl(BuildConfig.BASE_URL)
.addConverterFactory(
Json.asConverterFactory(
"application/json; charset=UTF-8".toMediaType()
Expand Down
3 changes: 3 additions & 0 deletions app/src/prod/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<resources>
<string name="app_name">Feelin</string>
</resources>
3 changes: 3 additions & 0 deletions app/src/staging/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<resources>
<string name="app_name">[QA] Feelin</string>
</resources>
49 changes: 49 additions & 0 deletions app/src/test/java/com/lyrics/feelin/FlavorBuildConfigTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.lyrics.feelin

import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test

class FlavorBuildConfigTest {

@Test
fun flavorBuildConfig_matchesExpectedEnvironment() {
val expected = when (BuildConfig.FLAVOR) {
DEV_FLAVOR -> ExpectedFlavorConfig(
applicationId = "com.lyrics.feelin.dev",
baseUrl = "http://dev.feelinapp.com/"
)
STAGING_FLAVOR -> ExpectedFlavorConfig(
applicationId = "com.lyrics.feelin.qa",
baseUrl = "http://dev.feelinapp.com/"
)
PROD_FLAVOR -> ExpectedFlavorConfig(
applicationId = "com.lyrics.feelin",
baseUrl = "http://api.feelinapp.com/"
)
else -> error("Unexpected flavor: ${BuildConfig.FLAVOR}")
}

assertEquals(expected.applicationId, BuildConfig.APPLICATION_ID)
assertEquals(expected.baseUrl, BuildConfig.BASE_URL)
}

@Test
fun kakaoNativeAppKey_isInjectedForRequestedVariant() {
assertTrue(
"Kakao native app key must be injected for ${BuildConfig.FLAVOR}",
BuildConfig.KAKAO_NATIVE_APP_KEY.isNotBlank()
)
}

private data class ExpectedFlavorConfig(
val applicationId: String,
val baseUrl: String
)

private companion object {
const val DEV_FLAVOR = "dev"
const val STAGING_FLAVOR = "staging"
const val PROD_FLAVOR = "prod"
}
}
4 changes: 4 additions & 0 deletions local.properties.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
sdk.dir=YOUR_ANDROID_SDK_DIR_WILL_BE_AUTOMATICALLY_CREATED
kakao.native.app.key.dev=KAKAO_DEV_NATIVE_APP_KEY_HERE
kakao.native.app.key.staging=KAKAO_QA_NATIVE_APP_KEY_HERE
kakao.native.app.key.prod=KAKAO_PROD_NATIVE_APP_KEY_HERE
Loading