Skip to content

fix: 구글 로그인 크래시를 부르던 -ObjC 링커 플래그 누락 보강 (#1340) #514

fix: 구글 로그인 크래시를 부르던 -ObjC 링커 플래그 누락 보강 (#1340)

fix: 구글 로그인 크래시를 부르던 -ObjC 링커 플래그 누락 보강 (#1340) #514

Workflow file for this run

#
# tuist-ci.yml
# umc-product-iOS
#
# Created by euijjang97 on 7/5/26.
#
name: Tuist CI
# 저장소의 단일 빌드/테스트 게이트 (이슈 #1128).
# 레거시 AppProduct 는 v2.2.0 에 동결되어 더 이상 변경되지 않으므로, 그것만 감시하던
# ios.yml 은 폐기했다. 다시 필요해지면 태그 v2.2.0 에서 복원한다.
# 빌드 진입점은 로컬과 동일하게 UMCApp/Makefile 을 사용한다(CLAUDE.md 규약).
on:
push:
branches: [ "develop" ]
pull_request:
branches: [ "develop" ]
permissions:
contents: read
concurrency:
group: tuist-ci-${{ github.ref }}
cancel-in-progress: true
jobs:
build-test:
name: Build & Test (UMCApp + watchOS / Tuist)
runs-on: macos-26
defaults:
run:
working-directory: UMCApp
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Select Xcode 26.4+
run: |
XCODE_PATH=$(ls -d /Applications/Xcode_26.4*.app 2>/dev/null | sort -V | tail -n 1)
if [ -z "$XCODE_PATH" ]; then
echo "❌ Xcode 26.4+ not found on runner. Available:"
ls -d /Applications/Xcode_*.app 2>/dev/null || true
exit 1
fi
echo "Selecting $XCODE_PATH"
sudo xcode-select -s "$XCODE_PATH/Contents/Developer"
xcodebuild -version
- name: Create Secrets.xcconfig placeholder
run: |
# 신규 클론에는 git-untracked Secrets.xcconfig 가 없다.
# Shared.xcconfig 의 `#include?` 는 optional 이라 없어도 빌드되지만,
# 로컬 개발과 동일한 상태를 만들기 위해 템플릿(플레이스홀더 값)을 복사한다.
if [ ! -f Secrets/Secrets.xcconfig ]; then
cp Secrets/Secrets.xcconfig.template Secrets/Secrets.xcconfig
fi
- name: Restore GoogleService-Info.plist (optional secret)
env:
GOOGLE_SERVICE_INFO_PLIST_BASE64: ${{ secrets.GOOGLE_SERVICE_INFO_PLIST_BASE64 }}
run: |
# RemoteConfigService/configureFirebaseIfNeeded()는 plist가 없어도 항상
# fail-open으로 동작하므로(#946), 시크릿이 없는 환경(fork PR 등)에서는
# 조용히 스킵한다 — 이 경우 CI는 실제 Firebase 없이 빌드/테스트만 검증한다.
if [ -z "$GOOGLE_SERVICE_INFO_PLIST_BASE64" ]; then
echo "GOOGLE_SERVICE_INFO_PLIST_BASE64 not set. Skipping GoogleService-Info.plist restore."
exit 0
fi
PLIST_DIR="UMCApp/Resources"
PLIST_PATH="${PLIST_DIR}/GoogleService-Info.plist"
mkdir -p "$PLIST_DIR"
echo "Restoring GoogleService-Info.plist from GOOGLE_SERVICE_INFO_PLIST_BASE64..."
if ! printf "%s" "$GOOGLE_SERVICE_INFO_PLIST_BASE64" | base64 --decode > "$PLIST_PATH"; then
echo "ERROR: Failed to decode GOOGLE_SERVICE_INFO_PLIST_BASE64"
exit 1
fi
# 레거시 AppProduct/ci_scripts/ci_post_clone.sh(114행~)의 검증 로직과 동일한
# 수준으로 복원 결과를 확인한다 (plist 헤더 → 유효성 → 필수 키 존재).
if ! grep -q "<plist" "$PLIST_PATH"; then
echo "ERROR: Restored GoogleService-Info.plist does not look like a plist file."
exit 1
fi
if ! plutil -lint "$PLIST_PATH" >/dev/null 2>&1; then
echo "ERROR: GoogleService-Info.plist is not a valid plist file."
exit 1
fi
if ! grep -q "<key>GOOGLE_APP_ID</key>" "$PLIST_PATH"; then
echo "ERROR: GOOGLE_APP_ID missing in GoogleService-Info.plist."
exit 1
fi
echo "GoogleService-Info.plist restored successfully at: $PLIST_PATH"
- name: Install mise & Tuist (from mise.toml)
uses: jdx/mise-action@v2
with:
working_directory: UMCApp
install: true
cache: true
- name: Cache SwiftPM / Tuist artifacts
uses: actions/cache@v4
with:
path: |
~/.cache/tuist
~/Library/Caches/org.swift.swiftpm
UMCApp/Tuist/.build
# 캐시된 SwiftPM `.build` 에는 체크아웃 절대경로(`/Users/runner/work/{레포명}/{레포명}/...`)가
# 박혀 있다. 레포 이름이 바뀌면 그 경로가 어긋나 tuist generate 가 xcframework 를 못 찾으므로,
# 키와 restore-keys 양쪽에 레포명을 넣어 rename 시 자동으로 무효화되게 한다 (이슈 #1296).
key: ${{ runner.os }}-tuist-${{ github.event.repository.name }}-${{ hashFiles('UMCApp/Tuist/Package.resolved', 'UMCApp/Tuist/Package.swift', 'UMCApp/mise.toml') }}
restore-keys: |
${{ runner.os }}-tuist-${{ github.event.repository.name }}-
- name: Install dependencies (tuist install)
run: make install
- name: Generate project (tuist generate --no-open)
run: make generate
- name: Build & Test (make test)
run: make test
# 아래 watchOS 스텝들을 별도 잡으로 빼지 않는 이유(이슈 #1216):
# 시크릿 복원 · mise/Tuist 설치 · SPM 캐시 · tuist generate 를 통째로 한 번 더
# 돌려야 해서 러너 시간이 두 배가 된다. 같은 잡에 이어 붙이면 생성된 워크스페이스를
# 그대로 재사용한다.
- name: Check watchOS simulator runtime
run: |
# generic/platform=watchOS Simulator 빌드는 런타임 설치 없이도 통과하는 경우가
# 많다. 그래서 여기서 실패시키지 않고 경고만 남긴다 — 러너 이미지에서 런타임이
# 빠졌을 때 뒤 스텝이 깨지면 이 로그가 원인을 바로 가리킨다.
if xcrun simctl list runtimes | grep -qi watchos; then
xcrun simctl list runtimes | grep -i watchos
else
echo "::warning::watchOS simulator runtime not found. Attempting download..."
xcodebuild -downloadPlatform watchOS || \
echo "::warning::watchOS runtime download failed. Continuing with generic destination."
fi
- name: Build watchOS app (make build-watch)
run: make build-watch
# UMCApp 스킴의 test action 에는 UMCAppTests 만 들어 있어서 위 `make test` 로는
# Core 모듈 테스트가 돌지 않는다. 워치 회귀를 잡으려면 스킴을 명시해야 한다.
# destination 은 Makefile 기본값(iOS Simulator)을 그대로 쓴다 — CoreWatchConnectivity
# 는 iOS/watchOS 멀티플랫폼 타겟이라 iOS 시뮬레이터에서 로직 테스트가 돈다.
- name: Test CoreWatchConnectivity
run: make test SCHEME=CoreWatchConnectivity
notify:
name: Notify Discord
needs: build-test
if: ${{ always() }}
runs-on: ubuntu-latest
permissions:
contents: read
# 실패한 잡/스텝과 그 로그를 조회하려면 워크플로 기본 `contents: read` 만으로는 부족하다.
actions: read
env:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
DISCORD_MENTION_ROLE_ID: ${{ secrets.DISCORD_MENTION_ROLE_ID }}
BUILD_RESULT: ${{ needs.build-test.result }}
REPOSITORY: ${{ github.repository }}
REF_NAME: ${{ github.ref_name }}
ACTOR: ${{ github.actor }}
COMMIT_SHA: ${{ github.sha }}
WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
PR_URL: ${{ github.event.pull_request.html_url }}
steps:
# 알림만 보고 원인을 알 수 있게 실패한 스텝 이름과 로그의 error: 라인을 미리 받아 둔다 (이슈 #1304).
# 조회가 깨져도 알림 자체는 나가야 하므로 전부 fail-open — 없으면 아래 스텝이 해당 필드를 생략한다.
- name: Collect failure details
if: ${{ needs.build-test.result == 'failure' }}
env:
GH_TOKEN: ${{ github.token }}
run: |
gh api "repos/${REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/jobs?per_page=100" \
> jobs.json || echo '{}' > jobs.json
JOB_ID=$(jq -r '[.jobs[]? | select(.conclusion == "failure")][0].id // empty' jobs.json)
if [ -n "$JOB_ID" ]; then
gh api "repos/${REPOSITORY}/actions/jobs/${JOB_ID}/logs" > job.log || true
fi
- name: Send build result to Discord
run: |
if [ -z "${DISCORD_WEBHOOK_URL:-}" ]; then
echo "DISCORD_WEBHOOK_URL is not configured. Skipping Discord notification."
exit 0
fi
python3 - << 'PY' > payload.json
import json
import os
import re
def failed_step():
"""실패한 첫 스텝을 `잡 › 스텝` 과 그 잡 로그 링크로 돌려준다."""
try:
with open("jobs.json", encoding="utf-8") as file:
jobs = json.load(file)["jobs"]
except (OSError, ValueError, KeyError):
return None
for job in jobs:
for step in job.get("steps", []):
if step.get("conclusion") == "failure":
return f"[{job['name']} › {step['name']}]({job['html_url']})"
return None
def error_excerpt():
"""로그에서 `error:` 라인만 추려 임베드 필드(1024자) 안에 들어가게 자른다.
Xcode 컴파일 에러와 XCTest 실패 모두 `error:` 를 포함하므로 이 한 줄짜리
필터로 대부분의 원인이 잡힌다. 더 정교한 파싱이 필요해지면 그때 늘린다.
"""
try:
with open("job.log", encoding="utf-8", errors="replace") as file:
lines = file.readlines()
except OSError:
return None
seen = []
for line in lines:
if "error:" not in line:
continue
# GitHub 이 붙이는 타임스탬프 접두사와 ANSI 컬러 코드를 벗겨 낸다.
text = re.sub(r"^\S+Z\s+", "", line.rstrip())
text = re.sub(r"\x1b\[[0-9;]*m", "", text)
if text and text not in seen:
seen.append(text)
if not seen:
return None
excerpt = "\n".join(seen[-5:])[:960]
return f"```\n{excerpt}\n```"
build_result = os.environ["BUILD_RESULT"]
short_sha = os.environ["COMMIT_SHA"][:7]
pr_url = os.environ.get("PR_URL") or "N/A"
mention_role_id = os.environ.get("DISCORD_MENTION_ROLE_ID", "").strip()
if build_result == "success":
status_text = "SUCCESS ✅"
color = 5763719 # green
mention = ""
elif build_result == "cancelled":
# concurrency.cancel-in-progress 로 앞선 런이 잘린 경우 — 실패가 아니다.
status_text = "CANCELLED ⏹️"
color = 9807270 # grey
mention = ""
else:
status_text = "FAILED ❌"
color = 15548997 # red
# 채널 전체를 깨우는 @here 폴백은 쓰지 않는다. 역할 ID 가 설정된 경우에만 멘션.
mention = f"<@&{mention_role_id}>" if mention_role_id else ""
fields = [
{"name": "Build Status", "value": status_text, "inline": True},
{"name": "Branch", "value": os.environ["REF_NAME"], "inline": True},
{"name": "Commit", "value": f"`{short_sha}` by {os.environ['ACTOR']}", "inline": True},
]
if build_result == "failure":
step = failed_step()
if step:
fields.append({"name": "Failed Step", "value": step, "inline": False})
excerpt = error_excerpt()
if excerpt:
fields.append({"name": "Error Log", "value": excerpt, "inline": False})
fields.append({"name": "PR", "value": pr_url, "inline": False})
payload = {
"content": mention,
"embeds": [
{
"title": "UMCApp (Tuist) CI Build Result",
"url": os.environ["WORKFLOW_URL"],
"color": color,
"fields": fields,
"footer": {"text": os.environ["REPOSITORY"]},
}
],
}
print(json.dumps(payload))
PY
curl -sS -X POST "$DISCORD_WEBHOOK_URL" \
-H "Content-Type: application/json" \
--data @payload.json