diff --git a/.github/actions/ios-simulator-build/action.yml b/.github/actions/ios-simulator-build/action.yml deleted file mode 100644 index 1e7743e4..00000000 --- a/.github/actions/ios-simulator-build/action.yml +++ /dev/null @@ -1,139 +0,0 @@ -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 - - # 설치된 iPhone 시뮬레이터 중 최신 iOS 버전 선택 - RESULT=$(python3 - <<'PY' - import re, subprocess, sys - - def ver_key(version): - return tuple(int(part) for part in version.split('.')) - - text = subprocess.check_output(["xcrun", "simctl", "list", "devices"], text=True) - lines = text.splitlines() - current_ver = None - candidates = [] - - 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 is None: - continue - if "(unavailable)" in line: - continue - if "iPhone" not in line: - continue - - raw = line.strip() - 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 - name = re.sub(r"\s+\([0-9A-Fa-f-]{36}\)\s+\(.*\)$", "", name) - - candidates.append((current_ver, name)) - - if len(candidates) <= 0: - print("No available iPhone simulators found", file=sys.stderr) - sys.exit(1) - - latest_version = max((candidate[0] for candidate in candidates), key=ver_key) - latest_candidates = [ - candidate for candidate in candidates - if candidate[0] == latest_version - ] - chosen_version, chosen_device_name = min( - latest_candidates, - key=lambda candidate: candidate[1] - ) - - print(f"{chosen_version}|{chosen_device_name}") - sys.exit(0) - 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 - env: - SCHEME: ${{ inputs.scheme }} - IOS_VER: ${{ steps.pick_ios.outputs.ios_version }} - DEVICE_NAME: ${{ steps.pick_ios.outputs.device_name }} - run: | - set -euo pipefail - set -x - 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 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5c08a789..3e200b82 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,7 +21,7 @@ jobs: outputs: has_qa_tag: ${{ steps.detect.outputs.has_qa_tag }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 fetch-tags: true @@ -49,7 +49,7 @@ jobs: runs-on: macos-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install private config files uses: ./.github/actions/install-private-config @@ -57,13 +57,30 @@ jobs: git_url: ${{ env.MATCH_GIT_URL }} git_basic_authorization: ${{ env.MATCH_GIT_BASIC_AUTHORIZATION }} - - name: Set up Xcode - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: ${{ env.XCODE_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: Cache SwiftPM - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | ~/.swiftpm @@ -74,10 +91,131 @@ jobs: restore-keys: | ${{ runner.os }}-spm- + - name: Select iOS Simulator Runtime (installed) + id: pick_ios + shell: bash + run: | + set -euo pipefail + + RESULT=$(python3 - <<'PY' + import re, subprocess, sys + + def ver_key(version): + return tuple(int(part) for part in version.split('.')) + + text = subprocess.check_output(["xcrun", "simctl", "list", "devices"], text=True) + lines = text.splitlines() + current_ver = None + candidates = [] + + 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 is None: + continue + if "(unavailable)" in line: + continue + if "iPhone" not in line: + continue + + raw = line.strip() + 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 + name = re.sub(r"\s+\([0-9A-Fa-f-]{36}\)\s+\(.*\)$", "", name) + + candidates.append((current_ver, name)) + + if len(candidates) <= 0: + print("No available iPhone simulators found", file=sys.stderr) + sys.exit(1) + + latest_version = max((candidate[0] for candidate in candidates), key=ver_key) + latest_candidates = [ + candidate for candidate in candidates + if candidate[0] == latest_version + ] + chosen_version, chosen_device_name = min( + latest_candidates, + key=lambda candidate: candidate[1] + ) + + print(f"{chosen_version}|{chosen_device_name}") + sys.exit(0) + 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 - uses: ./.github/actions/ios-simulator-build - with: - scheme: ${{ env.SCHEME }} + shell: bash + env: + IOS_VER: ${{ steps.pick_ios.outputs.ios_version }} + DEVICE_NAME: ${{ steps.pick_ios.outputs.device_name }} + run: | + set -euo pipefail + set -x + 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 - name: Comment build failure on PR if: failure() && github.event.pull_request.head.repo.fork == false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d89dd23e..ed0ee662 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,7 +30,7 @@ jobs: steps: - name: Checkout merge commit - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: ref: ${{ github.event.pull_request.merge_commit_sha }} @@ -46,10 +46,27 @@ jobs: bundler-cache: true ruby-version: ${{ env.RUBY_VERSION }} - - name: Set up Xcode - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: ${{ env.XCODE_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: Write App Store Connect API key env: diff --git a/.github/workflows/testflight.yml b/.github/workflows/testflight.yml index 0c6da6ae..a51cf6fe 100644 --- a/.github/workflows/testflight.yml +++ b/.github/workflows/testflight.yml @@ -23,49 +23,13 @@ permissions: contents: read jobs: - validate: - runs-on: macos-latest - timeout-minutes: 30 - - steps: - - uses: actions/checkout@v4 - - - 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 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 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Install private config files uses: ./.github/actions/install-private-config @@ -79,10 +43,27 @@ jobs: bundler-cache: true ruby-version: ${{ env.RUBY_VERSION }} - - name: Set up Xcode - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: ${{ env.XCODE_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: Write App Store Connect API key env: diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 9b78ed6c..1cbaa65c 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -1,7 +1,7 @@ XCODE_PROJ = "DevLog.xcodeproj" APP_IDENTIFIER = "opfic.DevLog" TARGET_NAME = "DevLog" -TESTFLIGHT_BUILD_OUTPUT_DIRECTORY = "fastlane/testflight_build" +TESTFLIGHT_BUILD_OUTPUT_DIRECTORY = File.expand_path("testflight_build", __dir__) TESTFLIGHT_IPA_OUTPUT_PATH = File.join(TESTFLIGHT_BUILD_OUTPUT_DIRECTORY, "#{TARGET_NAME}.ipa") default_platform(:ios) @@ -119,7 +119,9 @@ platform :ios do lane :upload_testflight_build do api_key = asc_api_key + # lane_context는 같은 fastlane 실행 내에서만 유지되므로, 별도 CI step에서는 고정 ipa 경로를 사용한다. ipa_output_path = lane_context[SharedValues::IPA_OUTPUT_PATH].to_s + ipa_output_path = TESTFLIGHT_IPA_OUTPUT_PATH if ipa_output_path.empty? UI.user_error!("Missing built ipa at #{ipa_output_path}") if !File.exist?(ipa_output_path)