๐จ [Design] ์ค๋ ๋ ๋ง๋ค๊ธฐ ํ๋ฉด ํผ๊ทธ๋ง ์์ ๋ฐ์ (#1313) (#1324) #499
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # | |
| # 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 |