Skip to content

Commit e8940a3

Browse files
committed
ci: add automated test workflow with issue reporting
1 parent 24ac06e commit e8940a3

1 file changed

Lines changed: 178 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
name: Sensor CI
2+
3+
on:
4+
push:
5+
branches: [main, master]
6+
pull_request:
7+
schedule:
8+
# Every Monday at 02:00 UTC
9+
- cron: '0 2 * * 1'
10+
11+
jobs:
12+
test:
13+
runs-on: macos-15
14+
15+
steps:
16+
- name: Checkout
17+
uses: actions/checkout@v4
18+
19+
- name: Show Xcode and simulator info
20+
run: |
21+
xcodebuild -version
22+
swift --version
23+
xcrun simctl list runtimes
24+
xcrun simctl list devices available
25+
26+
- name: Resolve package dependencies
27+
run: swift package resolve
28+
29+
- name: Detect scheme and simulator
30+
run: |
31+
SCHEME=$(swift package dump-package | python3 -c "import json,sys; print(json.load(sys.stdin)['name'])")
32+
echo "SCHEME=$SCHEME" >> "$GITHUB_ENV"
33+
34+
# Find the latest available iPhone simulator name (name-based, more robust than UDID)
35+
SIM_NAME=$(xcrun simctl list devices available --json | python3 -c "
36+
import json, sys
37+
devs = json.load(sys.stdin)['devices']
38+
iphones = [
39+
(rt, d['name'])
40+
for rt, dl in devs.items()
41+
for d in dl
42+
if 'iOS' in rt and 'iPhone' in d['name'] and d.get('isAvailable', True)
43+
]
44+
iphones.sort(reverse=True)
45+
print(iphones[0][1] if iphones else 'iPhone 16')
46+
")
47+
echo "SIM_NAME=$SIM_NAME" >> "$GITHUB_ENV"
48+
echo "Detected scheme: $SCHEME"
49+
echo "Detected simulator: $SIM_NAME"
50+
51+
- name: Build
52+
id: build
53+
run: |
54+
xcodebuild build \
55+
-scheme "$SCHEME" \
56+
-destination "platform=iOS Simulator,name=$SIM_NAME" \
57+
-sdk iphonesimulator \
58+
2>&1 | tee build_output.txt; exit "${PIPESTATUS[0]}"
59+
continue-on-error: true
60+
61+
- name: Test
62+
id: test
63+
if: steps.build.outcome == 'success'
64+
run: |
65+
xcodebuild test \
66+
-scheme "$SCHEME" \
67+
-destination "platform=iOS Simulator,name=$SIM_NAME" \
68+
-sdk iphonesimulator \
69+
-configuration Debug \
70+
-enableCodeCoverage YES \
71+
2>&1 | tee test_output.txt; exit "${PIPESTATUS[0]}"
72+
continue-on-error: true
73+
74+
- name: Create or update issue on failure
75+
if: steps.build.outcome == 'failure' || steps.test.outcome == 'failure'
76+
uses: actions/github-script@v7
77+
with:
78+
github-token: ${{ secrets.GITHUB_TOKEN }}
79+
script: |
80+
const fs = require('fs');
81+
const failed = '${{ steps.build.outcome }}' === 'failure' ? 'build' : 'test';
82+
const logFile = failed === 'build' ? 'build_output.txt' : 'test_output.txt';
83+
84+
let raw = '';
85+
try { raw = fs.readFileSync(logFile, 'utf8'); } catch (_) {}
86+
87+
const errors = raw.split('\n')
88+
.filter(l => /error:|FAILED|\*\* BUILD FAILED/.test(l))
89+
.slice(0, 60)
90+
.join('\n');
91+
92+
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
93+
const title = `[CI] ${failed} failure detected`;
94+
const body = [
95+
`## :x: Automated CI ${failed} failure`,
96+
'',
97+
`| | |`,
98+
`|---|---|`,
99+
`| **Workflow run** | [View logs](${runUrl}) |`,
100+
`| **Trigger** | \`${context.eventName}\` |`,
101+
`| **Ref** | \`${context.ref}\` |`,
102+
`| **Commit** | \`${context.sha.slice(0,7)}\` |`,
103+
'',
104+
`## Error summary`,
105+
'```',
106+
errors || '(no error lines captured — see full logs above)',
107+
'```',
108+
].join('\n');
109+
110+
// Ensure ci-failure label exists
111+
try {
112+
await github.rest.issues.createLabel({
113+
owner: context.repo.owner,
114+
repo: context.repo.repo,
115+
name: 'ci-failure',
116+
color: 'd73a4a',
117+
description: 'Automated CI test failure',
118+
});
119+
} catch (_) { /* label already exists */ }
120+
121+
const { data: issues } = await github.rest.issues.listForRepo({
122+
owner: context.repo.owner,
123+
repo: context.repo.repo,
124+
state: 'open',
125+
labels: 'ci-failure',
126+
});
127+
128+
if (issues.length > 0) {
129+
await github.rest.issues.createComment({
130+
owner: context.repo.owner,
131+
repo: context.repo.repo,
132+
issue_number: issues[0].number,
133+
body: `### :warning: New failure (${new Date().toISOString().split('T')[0]})\n\n${body}`,
134+
});
135+
} else {
136+
await github.rest.issues.create({
137+
owner: context.repo.owner,
138+
repo: context.repo.repo,
139+
title,
140+
body,
141+
labels: ['ci-failure'],
142+
});
143+
}
144+
145+
- name: Close ci-failure issue on success
146+
if: steps.build.outcome == 'success' && steps.test.outcome == 'success'
147+
uses: actions/github-script@v7
148+
with:
149+
github-token: ${{ secrets.GITHUB_TOKEN }}
150+
script: |
151+
const { data: issues } = await github.rest.issues.listForRepo({
152+
owner: context.repo.owner,
153+
repo: context.repo.repo,
154+
state: 'open',
155+
labels: 'ci-failure',
156+
});
157+
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
158+
for (const issue of issues) {
159+
await github.rest.issues.createComment({
160+
owner: context.repo.owner,
161+
repo: context.repo.repo,
162+
issue_number: issue.number,
163+
body: `:white_check_mark: All tests are passing again. See [run ${context.runId}](${runUrl}).`,
164+
});
165+
await github.rest.issues.update({
166+
owner: context.repo.owner,
167+
repo: context.repo.repo,
168+
issue_number: issue.number,
169+
state: 'closed',
170+
});
171+
}
172+
173+
- name: Fail job if build or tests failed
174+
if: steps.build.outcome == 'failure' || steps.test.outcome == 'failure'
175+
run: |
176+
echo "Build outcome: ${{ steps.build.outcome }}"
177+
echo "Test outcome: ${{ steps.test.outcome }}"
178+
exit 1

0 commit comments

Comments
 (0)