Skip to content

Commit a1e5933

Browse files
graycreateclaude
andcommitted
ci: consolidate workflows into single ci.yml
Merge `code-quality.yml`, `ios-build-test.yml`, and `pr-validation.yml` into a unified `ci.yml` workflow. All 6 jobs run in parallel with unified trigger conditions and concurrency control. Jobs: - lint: SwiftLint checks - swiftformat: SwiftFormat code style checks - pr-size: PR size labeling - check-commits: Conventional commit validation - build-and-test: Build and run tests - code-coverage: Test coverage reporting This reduces workflow file count from 3 to 1 for easier maintenance. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 0799947 commit a1e5933

File tree

4 files changed

+323
-312
lines changed

4 files changed

+323
-312
lines changed

.github/workflows/ci.yml

Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
name: CI
2+
3+
on:
4+
pull_request:
5+
branches: [main]
6+
types: [opened, synchronize, reopened]
7+
push:
8+
branches: [main, develop]
9+
workflow_dispatch:
10+
11+
concurrency:
12+
group: ci-${{ github.event.pull_request.number || github.ref }}
13+
cancel-in-progress: true
14+
15+
permissions:
16+
contents: read
17+
pull-requests: write
18+
issues: write
19+
20+
env:
21+
DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer
22+
23+
jobs:
24+
# ============================================
25+
# Quick validation checks (run first)
26+
# ============================================
27+
28+
lint:
29+
name: SwiftLint
30+
runs-on: ubuntu-latest
31+
32+
steps:
33+
- name: Checkout repository
34+
uses: actions/checkout@v4
35+
with:
36+
fetch-depth: 0
37+
38+
- name: SwiftLint
39+
id: swiftlint
40+
uses: norio-nomura/[email protected]
41+
with:
42+
args: --strict
43+
continue-on-error: true
44+
45+
- name: Comment PR with SwiftLint results
46+
uses: actions/github-script@v7
47+
if: github.event_name == 'pull_request'
48+
with:
49+
script: |
50+
const output = '${{ steps.swiftlint.outputs.stdout }}';
51+
if (output) {
52+
github.rest.issues.createComment({
53+
issue_number: context.issue.number,
54+
owner: context.repo.owner,
55+
repo: context.repo.repo,
56+
body: '## SwiftLint Results\n\n```\n' + output + '\n```'
57+
});
58+
}
59+
60+
swiftformat:
61+
name: SwiftFormat Check
62+
runs-on: macos-26
63+
64+
steps:
65+
- name: Checkout repository
66+
uses: actions/checkout@v4
67+
68+
- name: Install SwiftFormat
69+
run: brew install swiftformat
70+
71+
- name: Check code formatting
72+
run: |
73+
swiftformat --version
74+
swiftformat . --lint --verbose
75+
continue-on-error: true
76+
77+
- name: Generate format diff
78+
if: failure()
79+
run: |
80+
swiftformat . --dryrun > format-diff.txt
81+
cat format-diff.txt
82+
83+
- name: Upload format diff
84+
if: failure()
85+
uses: actions/upload-artifact@v4
86+
with:
87+
name: format-diff
88+
path: format-diff.txt
89+
90+
pr-size:
91+
name: PR Size Check
92+
runs-on: ubuntu-latest
93+
if: github.event_name == 'pull_request'
94+
95+
steps:
96+
- name: Checkout repository
97+
uses: actions/checkout@v4
98+
with:
99+
fetch-depth: 0
100+
101+
- name: Check PR size
102+
uses: actions/github-script@v7
103+
with:
104+
script: |
105+
const pr = context.payload.pull_request;
106+
const additions = pr.additions;
107+
const deletions = pr.deletions;
108+
const total = additions + deletions;
109+
110+
let label = '';
111+
if (total < 10) label = 'size/XS';
112+
else if (total < 50) label = 'size/S';
113+
else if (total < 200) label = 'size/M';
114+
else if (total < 500) label = 'size/L';
115+
else if (total < 1000) label = 'size/XL';
116+
else label = 'size/XXL';
117+
118+
// Remove all size labels
119+
const labels = await github.rest.issues.listLabelsOnIssue({
120+
owner: context.repo.owner,
121+
repo: context.repo.repo,
122+
issue_number: pr.number
123+
});
124+
125+
for (const label of labels.data) {
126+
if (label.name.startsWith('size/')) {
127+
await github.rest.issues.removeLabel({
128+
owner: context.repo.owner,
129+
repo: context.repo.repo,
130+
issue_number: pr.number,
131+
name: label.name
132+
});
133+
}
134+
}
135+
136+
// Add new size label
137+
await github.rest.issues.addLabels({
138+
owner: context.repo.owner,
139+
repo: context.repo.repo,
140+
issue_number: pr.number,
141+
labels: [label]
142+
});
143+
144+
check-commits:
145+
name: Check Commit Messages
146+
runs-on: ubuntu-latest
147+
if: github.event_name == 'pull_request'
148+
149+
steps:
150+
- name: Checkout repository
151+
uses: actions/checkout@v4
152+
with:
153+
fetch-depth: 0
154+
155+
- name: Check commits
156+
uses: actions/github-script@v7
157+
with:
158+
script: |
159+
const commits = await github.rest.pulls.listCommits({
160+
owner: context.repo.owner,
161+
repo: context.repo.repo,
162+
pull_number: context.issue.number
163+
});
164+
165+
const conventionalCommitRegex = /^(feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert)(\(.+\))?: .+/;
166+
const invalidCommits = [];
167+
168+
for (const commit of commits.data) {
169+
const message = commit.commit.message.split('\n')[0];
170+
if (!conventionalCommitRegex.test(message)) {
171+
invalidCommits.push(`- ${commit.sha.substring(0, 7)}: ${message}`);
172+
}
173+
}
174+
175+
if (invalidCommits.length > 0) {
176+
core.warning(`Found ${invalidCommits.length} commits without conventional format:\n${invalidCommits.join('\n')}`);
177+
}
178+
179+
# ============================================
180+
# Build and Test
181+
# ============================================
182+
183+
build-and-test:
184+
name: Build and Test
185+
runs-on: macos-26
186+
187+
steps:
188+
- name: Checkout repository
189+
uses: actions/checkout@v4
190+
with:
191+
submodules: recursive
192+
193+
- name: Select Xcode version
194+
run: sudo xcode-select -s /Applications/Xcode_26.0.1.app/Contents/Developer
195+
196+
- name: Show Xcode version
197+
run: xcodebuild -version
198+
199+
- name: Install xcpretty
200+
run: gem install xcpretty
201+
202+
- name: Cache SPM packages
203+
uses: actions/cache@v4
204+
with:
205+
path: ~/Library/Developer/Xcode/DerivedData/**/SourcePackages
206+
key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }}
207+
restore-keys: |
208+
${{ runner.os }}-spm-
209+
210+
- name: Resolve Swift packages
211+
run: |
212+
xcodebuild -resolvePackageDependencies \
213+
-project V2er.xcodeproj \
214+
-scheme V2er
215+
216+
- name: Build for testing
217+
run: |
218+
set -o pipefail && xcodebuild build-for-testing \
219+
-project V2er.xcodeproj \
220+
-scheme V2er \
221+
-sdk iphonesimulator \
222+
-destination 'platform=iOS Simulator,name=iPhone 17' \
223+
ONLY_ACTIVE_ARCH=YES \
224+
CODE_SIGN_IDENTITY="" \
225+
CODE_SIGNING_REQUIRED=NO | xcpretty --color
226+
227+
- name: Run tests
228+
run: |
229+
set -o pipefail && xcodebuild test-without-building \
230+
-project V2er.xcodeproj \
231+
-scheme V2er \
232+
-sdk iphonesimulator \
233+
-destination 'platform=iOS Simulator,name=iPhone 17' \
234+
ONLY_ACTIVE_ARCH=YES \
235+
CODE_SIGN_IDENTITY="" \
236+
CODE_SIGNING_REQUIRED=NO | xcpretty --color --test
237+
238+
- name: Upload test results
239+
uses: actions/upload-artifact@v4
240+
if: failure()
241+
with:
242+
name: test-results
243+
path: |
244+
~/Library/Logs/DiagnosticReports/
245+
~/Library/Developer/Xcode/DerivedData/**/Logs/Test/
246+
247+
# ============================================
248+
# Code Coverage
249+
# ============================================
250+
251+
code-coverage:
252+
name: Code Coverage
253+
runs-on: macos-26
254+
255+
steps:
256+
- name: Checkout repository
257+
uses: actions/checkout@v4
258+
with:
259+
submodules: recursive
260+
261+
- name: Select Xcode version
262+
run: sudo xcode-select -s /Applications/Xcode_26.0.1.app/Contents/Developer
263+
264+
- name: Install xcpretty
265+
run: gem install xcpretty
266+
267+
- name: Build and test with coverage
268+
run: |
269+
xcodebuild test \
270+
-project V2er.xcodeproj \
271+
-scheme V2er \
272+
-sdk iphonesimulator \
273+
-destination 'platform=iOS Simulator,name=iPhone 17' \
274+
-enableCodeCoverage YES \
275+
-derivedDataPath build/DerivedData \
276+
CODE_SIGN_IDENTITY="" \
277+
CODE_SIGNING_REQUIRED=NO | xcpretty
278+
279+
- name: Generate coverage report
280+
run: |
281+
cd build/DerivedData
282+
# Find the xcresult bundle
283+
RESULT_BUNDLE=$(find . -name '*.xcresult' -type d | head -n 1)
284+
285+
if [ -z "$RESULT_BUNDLE" ]; then
286+
echo "No test results found, setting coverage to 0%"
287+
echo "coverage=0.00" >> $GITHUB_ENV
288+
else
289+
xcrun xccov view --report --json "$RESULT_BUNDLE" > coverage.json || echo '{}' > coverage.json
290+
291+
# Extract coverage percentage with fallback
292+
COVERAGE=$(cat coverage.json | jq -r '.lineCoverage // 0' | awk '{printf "%.2f", $1 * 100}')
293+
echo "Code coverage: ${COVERAGE}%"
294+
echo "coverage=${COVERAGE}" >> $GITHUB_ENV
295+
fi
296+
297+
- name: Create coverage badge
298+
if: env.GIST_SECRET != ''
299+
uses: schneegans/[email protected]
300+
with:
301+
auth: ${{ secrets.GIST_SECRET }}
302+
gistID: ${{ secrets.GIST_ID }}
303+
filename: v2er-ios-coverage.json
304+
label: Coverage
305+
message: ${{ env.coverage }}%
306+
color: ${{ env.coverage > 80 && 'success' || env.coverage > 60 && 'yellow' || 'critical' }}
307+
env:
308+
GIST_SECRET: ${{ secrets.GIST_SECRET }}
309+
310+
- name: Comment PR with coverage
311+
if: github.event_name == 'pull_request'
312+
uses: actions/github-script@v7
313+
with:
314+
script: |
315+
const coverage = parseFloat('${{ env.coverage }}');
316+
const emoji = coverage > 80 ? '✅' : coverage > 60 ? '⚠️' : '❌';
317+
318+
github.rest.issues.createComment({
319+
issue_number: context.issue.number,
320+
owner: context.repo.owner,
321+
repo: context.repo.repo,
322+
body: `## Code Coverage Report ${emoji}\n\nCurrent coverage: **${coverage}%**`
323+
});

0 commit comments

Comments
 (0)