Skip to content

Commit d8fa84f

Browse files
committed
ci: 테스트 스킴을 위한 yml 파일 구현
1 parent 2771e99 commit d8fa84f

1 file changed

Lines changed: 303 additions & 0 deletions

File tree

.github/workflows/test.yml

Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
name: iOS Tests
2+
3+
on:
4+
pull_request:
5+
6+
env:
7+
WORKSPACE: DevLog.xcworkspace
8+
XCODE_VERSION: "26.3"
9+
MATCH_GIT_URL: ${{ secrets.MATCH_GIT_URL }}
10+
MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }}
11+
12+
permissions:
13+
contents: read
14+
issues: write
15+
pull-requests: write
16+
checks: write
17+
18+
jobs:
19+
test:
20+
runs-on: macos-latest
21+
timeout-minutes: 30
22+
strategy:
23+
fail-fast: false
24+
matrix:
25+
include:
26+
- name: core-data
27+
schemes: "DevLogDomain DevLogData"
28+
- name: app-layer
29+
schemes: "DevLogPersistence DevLogPresentation"
30+
- name: widget
31+
schemes: "DevLogWidgetCore"
32+
steps:
33+
- uses: actions/checkout@v5
34+
35+
- name: Install private config files
36+
uses: ./.github/actions/install-private-config
37+
with:
38+
git_url: ${{ env.MATCH_GIT_URL }}
39+
git_basic_authorization: ${{ env.MATCH_GIT_BASIC_AUTHORIZATION }}
40+
41+
- name: Select Xcode
42+
shell: bash
43+
run: |
44+
set -euo pipefail
45+
46+
if [ "$XCODE_VERSION" = "latest" ]; then
47+
XCODE_APP="$(find /Applications -maxdepth 1 -name 'Xcode*.app' -type d | sort -V | tail -n 1)"
48+
else
49+
XCODE_APP="/Applications/Xcode_${XCODE_VERSION}.app"
50+
if [ ! -d "$XCODE_APP" ]; then
51+
XCODE_APP="/Applications/Xcode-${XCODE_VERSION}.app"
52+
fi
53+
fi
54+
55+
if [ ! -d "${XCODE_APP:-}" ]; then
56+
echo "Requested Xcode not found for version: $XCODE_VERSION" >&2
57+
exit 1
58+
fi
59+
60+
sudo xcode-select -s "$XCODE_APP/Contents/Developer"
61+
xcodebuild -version
62+
63+
- name: Set up Tuist
64+
uses: jdx/mise-action@v4
65+
with:
66+
install: true
67+
cache: true
68+
69+
- name: Cache SwiftPM
70+
uses: actions/cache@v5
71+
with:
72+
path: |
73+
~/.swiftpm
74+
~/Library/Caches/org.swift.swiftpm
75+
~/Library/Developer/Xcode/SourcePackages
76+
.spm
77+
key: ${{ runner.os }}-spm-${{ hashFiles('.mise.toml', 'Tuist.swift', 'Workspace.swift', 'Tuist/ProjectDescriptionHelpers/*.swift', 'Application/**/Project.swift', 'Widget/**/Project.swift') }}
78+
restore-keys: |
79+
${{ runner.os }}-spm-
80+
81+
- name: Generate Xcode workspace with Tuist
82+
shell: bash
83+
run: |
84+
set -euo pipefail
85+
86+
tuist generate --no-open
87+
88+
- name: Select iOS Simulator Runtime (installed)
89+
id: pick_ios
90+
shell: bash
91+
run: |
92+
set -euo pipefail
93+
94+
RESULT=$(python3 - <<'PY'
95+
import re, subprocess, sys
96+
97+
def ver_key(version):
98+
return tuple(int(part) for part in version.split('.'))
99+
100+
text = subprocess.check_output(["xcrun", "simctl", "list", "devices"], text=True)
101+
lines = text.splitlines()
102+
current_ver = None
103+
candidates = []
104+
105+
for line in lines:
106+
header = re.match(r"^-- iOS ([0-9]+(?:\.[0-9]+)*) --$", line.strip())
107+
if header:
108+
current_ver = header.group(1)
109+
continue
110+
if current_ver is None:
111+
continue
112+
if "(unavailable)" in line:
113+
continue
114+
if "iPhone" not in line:
115+
continue
116+
117+
raw = line.strip()
118+
if "platform:" in raw and "name:" in raw and "OS:" in raw:
119+
kv = {}
120+
for part in raw.split(","):
121+
if ":" not in part:
122+
continue
123+
k, v = part.split(":", 1)
124+
kv[k.strip()] = v.strip()
125+
name = kv.get("name", raw)
126+
else:
127+
name = raw
128+
name = re.sub(r"\s+\([0-9A-Fa-f-]{36}\)\s+\(.*\)$", "", name)
129+
130+
candidates.append((current_ver, name))
131+
132+
if len(candidates) <= 0:
133+
print("No available iPhone simulators found", file=sys.stderr)
134+
sys.exit(1)
135+
136+
latest_version = max((candidate[0] for candidate in candidates), key=ver_key)
137+
latest_candidates = [
138+
candidate for candidate in candidates
139+
if candidate[0] == latest_version
140+
]
141+
chosen_version, chosen_device_name = min(
142+
latest_candidates,
143+
key=lambda candidate: candidate[1]
144+
)
145+
146+
print(f"{chosen_version}|{chosen_device_name}")
147+
sys.exit(0)
148+
PY
149+
)
150+
151+
if [ -z "${RESULT:-}" ]; then
152+
echo "No iPhone simulator devices detected." >&2
153+
exit 1
154+
fi
155+
156+
IFS='|' read -r IOS_VER DEVICE_NAME <<< "$RESULT"
157+
158+
echo "Chosen iOS runtime version (iPhone): $IOS_VER"
159+
echo "Chosen simulator: $DEVICE_NAME"
160+
161+
echo "ios_version=$IOS_VER" >> "$GITHUB_OUTPUT"
162+
echo "device_name=$DEVICE_NAME" >> "$GITHUB_OUTPUT"
163+
164+
- name: Test
165+
shell: bash
166+
env:
167+
IOS_VER: ${{ steps.pick_ios.outputs.ios_version }}
168+
DEVICE_NAME: ${{ steps.pick_ios.outputs.device_name }}
169+
TEST_SCHEMES: ${{ matrix.schemes }}
170+
TEST_GROUP: ${{ matrix.name }}
171+
run: |
172+
set -uo pipefail
173+
set -x
174+
175+
SPM_DIR="$GITHUB_WORKSPACE/.spm"
176+
RESULT_DIR="$GITHUB_WORKSPACE/test-results/$TEST_GROUP"
177+
mkdir -p "$SPM_DIR" "$RESULT_DIR"
178+
179+
xcodebuild -version
180+
181+
STATUS=0
182+
SUMMARY="$RESULT_DIR/summary.txt"
183+
: > "$SUMMARY"
184+
185+
for TEST_SCHEME in $TEST_SCHEMES; do
186+
LOG_PATH="$RESULT_DIR/${TEST_SCHEME}.log"
187+
echo "== Starting xcodebuild test: ${TEST_SCHEME} =="
188+
echo "scheme=${TEST_SCHEME}" >> "$SUMMARY"
189+
190+
set +e
191+
xcodebuild \
192+
-workspace "$WORKSPACE" \
193+
-scheme "$TEST_SCHEME" \
194+
-configuration Debug \
195+
-destination "platform=iOS Simulator,OS=${IOS_VER},name=${DEVICE_NAME}" \
196+
-clonedSourcePackagesDirPath "$SPM_DIR" \
197+
-skipPackagePluginValidation \
198+
-skipMacroValidation \
199+
-showBuildTimingSummary \
200+
test \
201+
| tee "$LOG_PATH"
202+
XC_STATUS=${PIPESTATUS[0]}
203+
set -e
204+
205+
echo "status=${XC_STATUS}" >> "$SUMMARY"
206+
echo "" >> "$SUMMARY"
207+
208+
if [ "$XC_STATUS" -ne 0 ]; then
209+
STATUS="$XC_STATUS"
210+
fi
211+
212+
echo "== Finished xcodebuild test: ${TEST_SCHEME} (${XC_STATUS}) =="
213+
done
214+
215+
exit "$STATUS"
216+
217+
- name: Upload test logs
218+
if: always()
219+
uses: actions/upload-artifact@v4
220+
with:
221+
name: ios-test-${{ matrix.name }}
222+
path: test-results/${{ matrix.name }}
223+
if-no-files-found: ignore
224+
225+
test-report:
226+
runs-on: ubuntu-latest
227+
needs: test
228+
if: always() && needs.test.result == 'failure' && github.event.pull_request.head.repo.fork == false
229+
steps:
230+
- name: Download test logs
231+
uses: actions/download-artifact@v4
232+
with:
233+
path: test-artifacts
234+
235+
- name: Comment test failure on PR
236+
uses: actions/github-script@v7
237+
with:
238+
script: |
239+
const fs = require('fs');
240+
const path = require('path');
241+
242+
const root = 'test-artifacts';
243+
const summaries = [];
244+
245+
function walk(directory) {
246+
if (!fs.existsSync(directory)) return [];
247+
const entries = fs.readdirSync(directory, { withFileTypes: true });
248+
return entries.flatMap((entry) => {
249+
const fullPath = path.join(directory, entry.name);
250+
return entry.isDirectory() ? walk(fullPath) : [fullPath];
251+
});
252+
}
253+
254+
for (const summaryPath of walk(root).filter((filePath) => path.basename(filePath) === 'summary.txt')) {
255+
const artifactName = summaryPath.split(path.sep)[1] || 'unknown';
256+
const text = fs.readFileSync(summaryPath, 'utf8');
257+
const records = text
258+
.trim()
259+
.split(/\n\n+/)
260+
.map((record) => Object.fromEntries(
261+
record
262+
.split(/\r?\n/)
263+
.filter(Boolean)
264+
.map((line) => {
265+
const index = line.indexOf('=');
266+
return index < 0 ? [line, ''] : [line.slice(0, index), line.slice(index + 1)];
267+
})
268+
));
269+
270+
for (const record of records) {
271+
if (record.status && record.status !== '0') {
272+
summaries.push({
273+
artifactName,
274+
scheme: record.scheme || 'unknown',
275+
status: record.status,
276+
});
277+
}
278+
}
279+
}
280+
281+
let body = '❌ iOS tests failed.\n\n';
282+
283+
if (summaries.length <= 0) {
284+
body += 'No failed scheme summary was found. Check the uploaded test log artifacts.';
285+
} else {
286+
body += 'Failed schemes:\n\n';
287+
body += summaries
288+
.map((summary) => `- ${summary.scheme} (${summary.artifactName}, exit ${summary.status})`)
289+
.join('\n');
290+
body += '\n\nCheck the uploaded test log artifacts for full diagnostics.';
291+
}
292+
293+
if (!context.payload.pull_request) {
294+
core.info('No PR context; skipping comment.');
295+
return;
296+
}
297+
298+
await github.rest.issues.createComment({
299+
owner: context.repo.owner,
300+
repo: context.repo.repo,
301+
issue_number: context.payload.pull_request.number,
302+
body,
303+
});

0 commit comments

Comments
 (0)