Skip to content
Merged
148 changes: 148 additions & 0 deletions .github/actions/ios-simulator-build/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
name: iOS Simulator Build
description: Select an installed iOS simulator runtime and build the app with xcodebuild.

inputs:
scheme:
description: Xcode scheme to build
required: true

runs:
using: composite
steps:
- name: Select iOS Simulator Runtime (installed)
id: pick_ios
shell: bash
run: |
set -euo pipefail

# macOS 메인 버전에 맞는 iOS 버전 중 최신 버전의 iPhone 선택
RESULT=$(python3 - <<'PY'
import re, subprocess, sys

xcode_ver = subprocess.check_output(["xcodebuild", "-version"], text=True).splitlines()[0].strip()
xcode_major = xcode_ver.split()[1].split('.')[0]
try:
xcode_major_num = int(xcode_major)
except ValueError:
xcode_major_num = None
if xcode_major_num is not None and xcode_major_num <= 15:
xcode_major = "26"

text = subprocess.check_output(["xcrun", "simctl", "list", "devices"], text=True)
lines = text.splitlines()

def ver_key(v):
return tuple(int(x) for x in v.split('.'))

# 1) 최신 iOS 버전(해당 mac 메이저) 찾기
latest_ver = None
for line in lines:
header = re.match(r"^-- iOS ([0-9]+(?:\.[0-9]+)*) --$", line.strip())
if not header:
continue
ver = header.group(1)
if not ver.startswith(f"{xcode_major}."):
continue
if latest_ver is None or ver_key(ver) > ver_key(latest_ver):
latest_ver = ver

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

iOS 시뮬레이터 버전을 선택하는 Python 스크립트 로직에 문제가 있어, 의도와 다르게 동작하여 시뮬레이터를 찾지 못하고 실패할 수 있습니다.

  1. xcodebuild -version으로 얻은 Xcode 메이저 버전(예: 15)과 simctl이 목록으로 보여주는 iOS SDK 버전(예: 17.2)은 직접적인 숫자 연관성이 없습니다.
  2. xcode_major를 특정 조건에서 "26"으로 하드코딩하는 부분은 iOS 버전 규칙과 맞지 않아 원하는 결과를 얻기 어렵습니다.

xcrun simctl list devices는 현재 Xcode 환경에서 사용 가능한 시뮬레이터 목록을 보여주므로, 복잡한 버전 확인 없이 목록에서 가장 최신 버전의 iOS 시뮬레이터를 선택하는 것이 더 간단하고 안정적인 방법입니다. 아래와 같이 스크립트를 수정하는 것을 제안합니다.

# 설치된 iOS 버전 중 최신 버전의 iPhone 선택
        RESULT=$(python3 - <<'PY'
        import re, subprocess, sys

        text = subprocess.check_output(["xcrun", "simctl", "list", "devices"], text=True)
        lines = text.splitlines()

        def ver_key(v):
          return tuple(int(x) for x in v.split('.'))

        # 1) 최신 iOS 버전 찾기
        latest_ver = None
        for line in lines:
          header = re.match(r"^-- iOS ([0-9]+(?:\.[0-9]+)*) --$", line.strip())
          if not header:
            continue
          ver = header.group(1)
          if latest_ver is None or ver_key(ver) > ver_key(latest_ver):
            latest_ver = ver


if latest_ver is None:
print(f"No iOS versions found for Xcode major {xcode_major}", file=sys.stderr)
sys.exit(1)

# 2) 해당 버전 섹션에서 첫 iPhone 찾고 즉시 종료
current_ver = None
for line in lines:
header = re.match(r"^-- iOS ([0-9]+(?:\.[0-9]+)*) --$", line.strip())
if header:
current_ver = header.group(1)
continue
if current_ver != latest_ver:
continue
if "(unavailable)" in line:
continue
if "iPhone" in line:
raw = line.strip()
# key:value 형태면 딕셔너리로 파싱해서 name만 사용
if "platform:" in raw and "name:" in raw and "OS:" in raw:
kv = {}
for part in raw.split(","):
if ":" not in part:
continue
k, v = part.split(":", 1)
kv[k.strip()] = v.strip()
name = kv.get("name", raw)
else:
name = raw
# UUID/상태만 제거하고 모델명 괄호는 유지
name = re.sub(r"\s+\([0-9A-Fa-f-]{36}\)\s+\(.*\)$", "", name)
print(f"{latest_ver}|{name}")
sys.exit(0)

print(f"No iPhone candidates found for iOS {latest_ver}", file=sys.stderr)
sys.exit(1)
PY
)

if [ -z "${RESULT:-}" ]; then
echo "No iPhone simulator devices detected." >&2
exit 1
fi

IFS='|' read -r IOS_VER DEVICE_NAME <<< "$RESULT"

echo "Chosen iOS runtime version (iPhone): $IOS_VER"
echo "Chosen simulator: $DEVICE_NAME"

echo "ios_version=$IOS_VER" >> "$GITHUB_OUTPUT"
echo "device_name=$DEVICE_NAME" >> "$GITHUB_OUTPUT"

- name: Build
shell: bash
run: |
set -euo pipefail
set -x
IOS_VER="${{ steps.pick_ios.outputs.ios_version }}"
DEVICE_NAME="${{ steps.pick_ios.outputs.device_name }}"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The GitHub Action uses expressions like ${{ steps.pick_ios.outputs.ios_version }} and ${{ steps.pick_ios.outputs.device_name }} directly within a bash run script. GitHub Actions expressions are substituted with their literal values before the shell script is executed. If the output of a previous step contains malicious characters (like double quotes or semicolons), it can lead to command injection. It is a security best practice to pass step outputs to the shell script using environment variables to prevent injection vulnerabilities.

SPM_DIR="$GITHUB_WORKSPACE/.spm"
mkdir -p "$SPM_DIR"

xcodebuild -version

echo "Using scheme: ${{ inputs.scheme }}"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The GitHub Action uses the expression ${{ inputs.scheme }} directly within a bash run script. If an attacker can control the scheme input, they can inject arbitrary shell commands into the CI environment. To remediate this, pass the input to the shell script using an environment variable.

Example:

- name: Build
  shell: bash
  env:
    SCHEME: ${{ inputs.scheme }}
  run: |
    xcodebuild -scheme "$SCHEME" ...


echo "Using simulator: $DEVICE_NAME (iOS ${IOS_VER})"

set -o pipefail
set +e
echo "== Resolving Swift Package dependencies =="
xcodebuild \
-scheme "${{ inputs.scheme }}" \
-configuration Debug \
-clonedSourcePackagesDirPath "$SPM_DIR" \
-resolvePackageDependencies
echo "== Starting xcodebuild build =="
xcodebuild \
-scheme "${{ inputs.scheme }}" \
-configuration Debug \
-destination "platform=iOS Simulator,OS=${IOS_VER},name=${DEVICE_NAME}" \
-clonedSourcePackagesDirPath "$SPM_DIR" \
-skipPackagePluginValidation \
-skipMacroValidation \
-showBuildTimingSummary \
build \
| tee build.log
echo "== xcodebuild finished =="
XC_STATUS=${PIPESTATUS[0]}
set -e

if [ -f build.log ]; then
echo "== error: lines =="
if grep -i "error:" build.log; then
if [ "$XC_STATUS" -eq 0 ]; then
XC_STATUS=1
fi
fi
fi

exit $XC_STATUS
140 changes: 3 additions & 137 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,144 +37,10 @@ jobs:
restore-keys: |
${{ runner.os }}-spm-


- name: Select iOS Simulator Runtime (installed)
id: pick_ios
shell: bash
run: |
set -euo pipefail

# macOS 메인 버전에 맞는 iOS 버전 중 최신 버전의 iPhone 선택
RESULT=$(python3 - <<'PY'
import re, subprocess, sys

xcode_ver = subprocess.check_output(["xcodebuild", "-version"], text=True).splitlines()[0].strip()
xcode_major = xcode_ver.split()[1].split('.')[0]
try:
xcode_major_num = int(xcode_major)
except ValueError:
xcode_major_num = None
if xcode_major_num is not None and xcode_major_num <= 15:
xcode_major = "26"

text = subprocess.check_output(["xcrun", "simctl", "list", "devices"], text=True)
lines = text.splitlines()

def ver_key(v):
return tuple(int(x) for x in v.split('.'))

# 1) 최신 iOS 버전(해당 mac 메이저) 찾기
latest_ver = None
for line in lines:
header = re.match(r"^-- iOS ([0-9]+(?:\.[0-9]+)*) --$", line.strip())
if not header:
continue
ver = header.group(1)
if not ver.startswith(f"{xcode_major}."):
continue
if latest_ver is None or ver_key(ver) > ver_key(latest_ver):
latest_ver = ver

if latest_ver is None:
print(f"No iOS versions found for Xcode major {xcode_major}", file=sys.stderr)
sys.exit(1)

# 2) 해당 버전 섹션에서 첫 iPhone 찾고 즉시 종료
current_ver = None
for line in lines:
header = re.match(r"^-- iOS ([0-9]+(?:\.[0-9]+)*) --$", line.strip())
if header:
current_ver = header.group(1)
continue
if current_ver != latest_ver:
continue
if "(unavailable)" in line:
continue
if "iPhone" in line:
raw = line.strip()
# key:value 형태면 딕셔너리로 파싱해서 name만 사용
if "platform:" in raw and "name:" in raw and "OS:" in raw:
kv = {}
for part in raw.split(","):
if ":" not in part:
continue
k, v = part.split(":", 1)
kv[k.strip()] = v.strip()
name = kv.get("name", raw)
else:
name = raw
# UUID/상태만 제거하고 모델명 괄호는 유지
name = re.sub(r"\s+\([0-9A-Fa-f-]{36}\)\s+\(.*\)$", "", name)
print(f"{latest_ver}|{name}")
sys.exit(0)

print(f"No iPhone candidates found for iOS {latest_ver}", file=sys.stderr)
sys.exit(1)
PY
)

if [ -z "${RESULT:-}" ]; then
echo "No iPhone simulator devices detected." >&2
exit 1
fi

IFS='|' read -r IOS_VER DEVICE_NAME <<< "$RESULT"

echo "Chosen iOS runtime version (iPhone): $IOS_VER"
echo "Chosen simulator: $DEVICE_NAME"

echo "ios_version=$IOS_VER" >> "$GITHUB_OUTPUT"
echo "device_name=$DEVICE_NAME" >> "$GITHUB_OUTPUT"

- name: Build
shell: bash
run: |
set -euo pipefail
set -x
IOS_VER="${{ steps.pick_ios.outputs.ios_version }}"
DEVICE_NAME="${{ steps.pick_ios.outputs.device_name }}"
SPM_DIR="$GITHUB_WORKSPACE/.spm"
mkdir -p "$SPM_DIR"

xcodebuild -version

echo "Using scheme: $SCHEME"

echo "Using simulator: $DEVICE_NAME (iOS ${IOS_VER})"

set -o pipefail
set +e
echo "== Resolving Swift Package dependencies =="
xcodebuild \
-scheme "$SCHEME" \
-configuration Debug \
-clonedSourcePackagesDirPath "$SPM_DIR" \
-resolvePackageDependencies
echo "== Starting xcodebuild build =="
xcodebuild \
-scheme "$SCHEME" \
-configuration Debug \
-destination "platform=iOS Simulator,OS=${IOS_VER},name=${DEVICE_NAME}" \
-clonedSourcePackagesDirPath "$SPM_DIR" \
-skipPackagePluginValidation \
-skipMacroValidation \
-showBuildTimingSummary \
build \
| tee build.log
echo "== xcodebuild finished =="
XC_STATUS=${PIPESTATUS[0]}
set -e

if [ -f build.log ]; then
echo "== error: lines =="
if grep -i "error:" build.log; then
if [ "$XC_STATUS" -eq 0 ]; then
XC_STATUS=1
fi
fi
fi

exit $XC_STATUS
uses: ./.github/actions/ios-simulator-build
with:
scheme: ${{ env.SCHEME }}

- name: Comment build failure on PR
if: failure() && github.event.pull_request.head.repo.fork == false
Expand Down
9 changes: 0 additions & 9 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,6 @@ jobs:
run: |
printf '%s' "$ASC_KEY_CONTENT" | base64 -D > "$ASC_KEY_PATH"

- name: Debug ASC key fingerprint
run: |
shasum -a 256 "$ASC_KEY_PATH"

- name: Debug ASC metadata fingerprint
run: |
printf '%s' "$ASC_KEY_ID" | shasum -a 256
printf '%s' "$ASC_ISSUER_ID" | shasum -a 256

- name: Release to App Store Connect
run: bundle exec fastlane release

Expand Down
46 changes: 38 additions & 8 deletions .github/workflows/testflight.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ on:
workflow_dispatch:

env:
SCHEME: DevLog
RUBY_VERSION: "3.2"
XCODE_VERSION: latest
APP_STORE_TEAM_ID: ${{ secrets.APP_STORE_TEAM_ID }}
Expand All @@ -22,7 +23,37 @@ permissions:
contents: read

jobs:
validate:
runs-on: macos-latest
timeout-minutes: 30

steps:
- uses: actions/checkout@v4

- name: Set up Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: ${{ env.XCODE_VERSION }}

- name: Cache SwiftPM
uses: actions/cache@v4
with:
path: |
~/.swiftpm
~/Library/Caches/org.swift.swiftpm
~/Library/Developer/Xcode/SourcePackages
.spm
key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }}
restore-keys: |
${{ runner.os }}-spm-

- name: Validate Debug Simulator Build
uses: ./.github/actions/ios-simulator-build
with:
scheme: ${{ env.SCHEME }}

testflight:
needs: validate
runs-on: macos-latest
timeout-minutes: 45

Expand All @@ -47,14 +78,13 @@ jobs:
run: |
printf '%s' "$ASC_KEY_CONTENT" | base64 -D > "$ASC_KEY_PATH"

- name: Debug ASC key fingerprint
run: |
shasum -a 256 "$ASC_KEY_PATH"
- name: Build for TestFlight
run: bundle exec fastlane testflight_build_only

- name: Debug ASC metadata fingerprint
run: |
printf '%s' "$ASC_KEY_ID" | shasum -a 256
printf '%s' "$ASC_ISSUER_ID" | shasum -a 256
- name: Skip TestFlight Upload for Local QA Tag
if: github.event_name == 'push' && startsWith(github.ref_name, 'qa-local-')
run: echo "Skipping TestFlight upload for local QA tag ${GITHUB_REF_NAME}"

- name: Upload to TestFlight
run: bundle exec fastlane deploy_testflight
if: github.event_name != 'push' || !startsWith(github.ref_name, 'qa-local-')
run: bundle exec fastlane upload_testflight_build
Loading