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
143 changes: 143 additions & 0 deletions .github/workflows/appstore.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
name: iOS App Store

on:
workflow_dispatch:
inputs:
upload_to_app_store_connect:
description: Upload to App Store Connect
required: true
default: "false"
type: choice
options:
- "false"
- "true"

env:
RUBY_VERSION: "3.2"
XCODE_VERSION: latest
APP_STORE_TEAM_ID: ${{ secrets.APP_STORE_TEAM_ID }}
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
ASC_KEY_PATH: fastlane/AuthKey.p8
SPACESHIP_CONNECT_API_IN_HOUSE: "false"
MATCH_GIT_URL: ${{ secrets.MATCH_GIT_URL }}
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }}

permissions:
contents: read

jobs:
appstore:
runs-on: macos-latest
timeout-minutes: 45

steps:
- name: Checkout
uses: actions/checkout@v5

- name: Install private config files
uses: ./.github/actions/install-private-config
with:
git_url: ${{ env.MATCH_GIT_URL }}
git_basic_authorization: ${{ env.MATCH_GIT_BASIC_AUTHORIZATION }}

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
ruby-version: ${{ env.RUBY_VERSION }}

- name: Select Xcode
shell: bash
run: |
set -euo pipefail

if [ "$XCODE_VERSION" = "latest" ]; then
XCODE_APP="$(find /Applications -maxdepth 1 -name 'Xcode*.app' -type d | sort -V | tail -n 1)"
else
XCODE_APP="/Applications/Xcode_${XCODE_VERSION}.app"
if [ ! -d "$XCODE_APP" ]; then
XCODE_APP="/Applications/Xcode-${XCODE_VERSION}.app"
fi
fi

if [ ! -d "${XCODE_APP:-}" ]; then
echo "Requested Xcode not found for version: $XCODE_VERSION" >&2
exit 1
fi

sudo xcode-select -s "$XCODE_APP/Contents/Developer"
xcodebuild -version

- name: Set up Tuist
uses: jdx/mise-action@v4
with:
install: true
cache: true

- name: Generate Xcode workspace with Tuist
shell: bash
run: |
set -euo pipefail

tuist generate --no-open

- name: Write App Store Connect API key
env:
ASC_KEY_CONTENT: ${{ secrets.ASC_KEY_CONTENT }}
run: |
printf '%s' "$ASC_KEY_CONTENT" | base64 -D > "$ASC_KEY_PATH"

- name: Build for App Store
run: bundle exec fastlane appstore_build_only

- name: Upload dSYM to Firebase Crashlytics
shell: bash
run: |
set -euo pipefail

google_service_info_path="Application/DevLogApp/Sources/Resource/GoogleService-Info.plist"
dsym_zip_path="$(find fastlane/appstore_build -maxdepth 1 -name '*.dSYM.zip' -print -quit)"
upload_symbols_path="$(find "$HOME/Library/Developer/Xcode/DerivedData" "$HOME/Library/Developer/Xcode/SourcePackages" "$GITHUB_WORKSPACE" -path '*/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/upload-symbols' -type f -print -quit 2>/dev/null || true)"

if [ ! -f "$google_service_info_path" ]; then
echo "Missing GoogleService-Info.plist at $google_service_info_path" >&2
exit 1
fi

if [ -z "$dsym_zip_path" ]; then
echo "Missing dSYM zip in fastlane/appstore_build" >&2
exit 1
fi

if [ -z "$upload_symbols_path" ]; then
echo "Missing Firebase Crashlytics upload-symbols script" >&2
exit 1
fi

"$upload_symbols_path" -gsp "$google_service_info_path" -p ios "$dsym_zip_path"

- name: Upload App Store build log
if: always()
uses: actions/upload-artifact@v6
with:
name: appstore-build-log
path: ~/Library/Logs/gym/*.log
if-no-files-found: ignore

- name: Upload App Store dSYM
if: always()
uses: actions/upload-artifact@v6
with:
name: appstore-dsym
path: fastlane/appstore_build/*.dSYM.zip
if-no-files-found: warn

- name: Skip App Store Upload
if: inputs.upload_to_app_store_connect != 'true'
run: echo "Skipping App Store upload"

- name: Upload to App Store Connect
if: inputs.upload_to_app_store_connect == 'true'
run: bundle exec fastlane upload_appstore_build
23 changes: 5 additions & 18 deletions .github/workflows/testflight.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,14 @@ name: iOS TestFlight
on:
workflow_dispatch:
inputs:
upload_to_app_store_connect:
description: Upload to App Store Connect
upload_to_testflight:
description: Upload to TestFlight
required: true
default: "false"
type: choice
options:
- "false"
- "true"
pull_request:
types:
- closed
branches:
- develop

env:
SCHEME: DevLogApp
Expand All @@ -35,19 +30,11 @@ permissions:

jobs:
testflight:
if: github.event_name == 'workflow_dispatch' || (github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'develop' && (contains(github.event.pull_request.labels.*.name, 'qa') || contains(github.event.pull_request.labels.*.name, 'qa-local')))
runs-on: macos-latest
timeout-minutes: 45

steps:
- name: Checkout merge commit
if: github.event_name == 'pull_request'
uses: actions/checkout@v5
with:
ref: ${{ github.event.pull_request.merge_commit_sha }}

- name: Checkout current ref
if: github.event_name == 'workflow_dispatch'
- name: Checkout
uses: actions/checkout@v5

- name: Install private config files
Expand Down Expand Up @@ -159,9 +146,9 @@ jobs:
if-no-files-found: warn

- name: Skip TestFlight Upload
if: (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'qa-local')) || (github.event_name == 'workflow_dispatch' && inputs.upload_to_app_store_connect != 'true')
if: inputs.upload_to_testflight != 'true'
run: echo "Skipping TestFlight upload"

- name: Upload to TestFlight
if: (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'qa') && !contains(github.event.pull_request.labels.*.name, 'qa-local')) || (github.event_name == 'workflow_dispatch' && inputs.upload_to_app_store_connect == 'true')
if: inputs.upload_to_testflight == 'true'
run: bundle exec fastlane upload_testflight_build
8 changes: 8 additions & 0 deletions Application/DevLogApp/Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,19 @@ let project = Project(
debug: [
"APS_ENVIRONMENT": "development",
"DEBUG_INFORMATION_FORMAT": "dwarf",
"FIRESTORE_DATABASE_ID": "staging",
"INFOPLIST_KEY_FirebaseCrashlyticsCollectionEnabled": "NO",
],
staging: [
"APS_ENVIRONMENT": "production",
"DEBUG_INFORMATION_FORMAT": "dwarf-with-dsym",
"FIRESTORE_DATABASE_ID": "staging",
"INFOPLIST_KEY_FirebaseCrashlyticsCollectionEnabled": "YES",
],
release: [
"APS_ENVIRONMENT": "production",
"DEBUG_INFORMATION_FORMAT": "dwarf-with-dsym",
"FIRESTORE_DATABASE_ID": "prod",
"INFOPLIST_KEY_FirebaseCrashlyticsCollectionEnabled": "YES",
]
)
Expand Down
2 changes: 2 additions & 0 deletions Application/DevLogApp/Sources/Resource/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
<false/>
<key>FirebaseAutomaticScreenReportingEnabled</key>
<false/>
<key>FIRESTORE_DATABASE_ID</key>
<string>$(FIRESTORE_DATABASE_ID)</string>
<key>GIDClientID</key>
<string>$(CLIENT_ID)</string>
<key>GITHUB_CLIENT_ID</key>
Expand Down
24 changes: 21 additions & 3 deletions Application/DevLogApp/Tests/Support/LocalFirebaseRESTSupport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ final class LocalFirebaseRESTSupport {
func fetchPushNotificationIDs(userId: String) async throws -> [String] {
let googleServiceInfo = try loadGoogleServiceInfo()
let url = firestoreBaseURL.appending(
path: "v1/projects/\(googleServiceInfo.projectId)/databases/(default)/documents/users/" +
path: "v1/projects/\(googleServiceInfo.projectId)/databases/\(databaseID())/documents/users/" +
"\(userId)/notifications",
directoryHint: .notDirectory
)
Expand Down Expand Up @@ -181,7 +181,8 @@ final class LocalFirebaseRESTSupport {
func fetchWebPageURLs(userId: String) async throws -> [String] {
let googleServiceInfo = try loadGoogleServiceInfo()
let url = firestoreBaseURL.appending(
path: "v1/projects/\(googleServiceInfo.projectId)/databases/(default)/documents/users/\(userId)/webPages",
path: "v1/projects/\(googleServiceInfo.projectId)/databases/\(databaseID())" +
"/documents/users/\(userId)/webPages",
directoryHint: .notDirectory
)
let (data, response) = try await URLSession.shared.data(from: url)
Expand Down Expand Up @@ -301,7 +302,8 @@ private extension LocalFirebaseRESTSupport {
let encodedPath = encode(documentPath)
var request = URLRequest(
url: firestoreBaseURL.appending(
path: "v1/projects/\(googleServiceInfo.projectId)/databases/(default)/documents/\(encodedPath)",
path: "v1/projects/\(googleServiceInfo.projectId)/databases/\(databaseID())" +
"/documents/\(encodedPath)",
directoryHint: .notDirectory
)
)
Expand Down Expand Up @@ -336,6 +338,22 @@ private extension LocalFirebaseRESTSupport {
}.joined(separator: "/")
}

func databaseID() -> String {
let environmentValue = ProcessInfo.processInfo.environment["FIRESTORE_DATABASE_ID"]?
.trimmingCharacters(in: .whitespacesAndNewlines)
if let environmentValue, !environmentValue.isEmpty {
return environmentValue
}

let bundleValue = Bundle.main.object(forInfoDictionaryKey: "FIRESTORE_DATABASE_ID") as? String
let databaseID = bundleValue?.trimmingCharacters(in: .whitespacesAndNewlines)
guard let databaseID, !databaseID.isEmpty, !databaseID.hasPrefix("$(") else {
return "staging"
}

return databaseID
}

func stringValue(_ value: String) -> [String: Any] {
["stringValue": value]
}
Expand Down
45 changes: 45 additions & 0 deletions Application/DevLogInfra/Sources/Common/FirebaseConfiguration.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//
// FirebaseConfiguration.swift
// DevLogInfra
//
// Created by opfic on 6/26/26.
//

import FirebaseFirestore
import FirebaseFunctions
import Foundation

enum FirebaseConfiguration {
private enum InfoKey {
static let databaseID = "FIRESTORE_DATABASE_ID"
}

static let defaultDatabaseID = "staging"

static var databaseID: String {
let environmentValue = ProcessInfo.processInfo.environment[InfoKey.databaseID]?
.trimmingCharacters(in: .whitespacesAndNewlines)
if let environmentValue, !environmentValue.isEmpty {
return environmentValue
}

guard let rawValue = Bundle.main.object(forInfoDictionaryKey: InfoKey.databaseID) as? String else {
return defaultDatabaseID
}
Comment thread
opficdev marked this conversation as resolved.

let databaseID = rawValue.trimmingCharacters(in: .whitespacesAndNewlines)
if databaseID.isEmpty || databaseID.hasPrefix("$(") {
return defaultDatabaseID
}

return databaseID
}
Comment thread
opficdev marked this conversation as resolved.

static var firestore: Firestore {
Firestore.firestore(database: databaseID)
}

static var functions: Functions {
Functions.functions(region: "asia-northeast3")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
//

import FirebaseFunctions
import DevLogData

extension Functions {
func httpsCallable(_ name: some RawRepresentable<String>) -> HTTPSCallable {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ final class AuthServiceImpl: AuthService {
}
}

private let store = Firestore.firestore()
private let store = FirebaseConfiguration.firestore
private let messaging = Messaging.messaging()
private let logger = Logger(category: "AuthServiceImpl")
private let subject = CurrentValueSubject<Bool, Never>(Auth.auth().currentUser != nil)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ final class PushNotificationServiceImpl: PushNotificationService {
case undoPushNotificationDeletion
}

private let store = Firestore.firestore()
private let functions = Functions.functions(region: "asia-northeast3")
private let store = FirebaseConfiguration.firestore
private let functions = FirebaseConfiguration.functions
private let logger = Logger(category: "PushNotificationServiceImpl")

/// 푸시 알림 On/Off 설정
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ final class AppleAuthenticationServiceImpl: AuthenticationService {

private var appleSignInDelegate: AppleSignInDelegate?
private var appleSignInContinuation: CheckedContinuation<ASAuthorization, Error>?
private let store = Firestore.firestore()
private let functions = Functions.functions(region: "asia-northeast3")
private let store = FirebaseConfiguration.firestore
private let functions = FirebaseConfiguration.functions
private let messaging = Messaging.messaging()
private var user: User? { Auth.auth().currentUser }
private let providerID = AuthProviderID.apple
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ final class GithubAuthenticationServiceImpl: NSObject, AuthenticationService {
static let acceptHeader = "application/vnd.github.v3+json"
}

private let store = Firestore.firestore()
private let functions = Functions.functions(region: "asia-northeast3")
private let store = FirebaseConfiguration.firestore
private let functions = FirebaseConfiguration.functions
private let messaging = Messaging.messaging()
private var user: User? { Auth.auth().currentUser }
private let providerID = AuthProviderID.gitHub
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ final class GoogleAuthenticationServiceImpl: AuthenticationService {
}
}

private let store = Firestore.firestore()
private let store = FirebaseConfiguration.firestore
private let messaging = Messaging.messaging()
private var user: User? { Auth.auth().currentUser }
private let provider = TopViewControllerProvider()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ final class TodoCategoryServiceImpl: TodoCategoryService {
case user
}

private let store = Firestore.firestore()
private let store = FirebaseConfiguration.firestore
private let logger = Logger(category: "TodoCategoryServiceImpl")

func fetchCategoryPreferences() async throws -> [TodoCategoryPreferenceResponse] {
Expand Down
Loading
Loading