Skip to content

๐ŸŽจ [Design] ์Šค๋ ˆ๋“œ ๋งŒ๋“ค๊ธฐ ํ™”๋ฉด ํ”ผ๊ทธ๋งˆ ์‹œ์•ˆ ๋ฐ˜์˜ (#1313) (#1324) #499

๐ŸŽจ [Design] ์Šค๋ ˆ๋“œ ๋งŒ๋“ค๊ธฐ ํ™”๋ฉด ํ”ผ๊ทธ๋งˆ ์‹œ์•ˆ ๋ฐ˜์˜ (#1313) (#1324)

๐ŸŽจ [Design] ์Šค๋ ˆ๋“œ ๋งŒ๋“ค๊ธฐ ํ™”๋ฉด ํ”ผ๊ทธ๋งˆ ์‹œ์•ˆ ๋ฐ˜์˜ (#1313) (#1324) #499

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