-
Notifications
You must be signed in to change notification settings - Fork 2
193 lines (177 loc) · 7.5 KB
/
sim-hid-sentinel.yml
File metadata and controls
193 lines (177 loc) · 7.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
name: SimulatorKit HID Sentinel
# Detects BC breaks in the private Apple frameworks that sim-hid-bridge
# depends on (SimulatorKit.framework, CoreSimulator.framework). Runs on a
# daily cron so a silent Apple symbol change is surfaced before users hit
# it in production. Tracked in #493.
#
# Failure modes surfaced by tests/ci/sim-hid-sentinel.test.ts:
# exit 78 SIMULATORKIT_MISSING — SimulatorKit.framework moved or gone
# exit 78 CORESIMULATOR_MISSING — CoreSimulator.framework moved or gone
# exit 78 HID_CLIENT_FAILED — SimDeviceLegacyHIDClient class gone
# exit 78 HID_FUNCTIONS_MISSING — IndigoHIDMessage* symbols gone
# diag indigoSymbols false/missing — IndigoHIDMessageToCreate/RemovePointerService gone (#590 Phase 1)
#
# When the job fails we open (or update) a tracking issue so the on-call
# engineer sees it on github.com without needing Slack access.
on:
schedule:
# Daily at 06:00 UTC — off-peak for most contributors.
- cron: '0 6 * * *'
workflow_dispatch:
push:
paths:
# Re-run on changes that can break the private-framework contract.
- 'src/native/sim-hid-bridge.swift'
- 'tests/ci/sim-hid-sentinel.test.ts'
- '.github/workflows/sim-hid-sentinel.yml'
permissions:
contents: read
issues: write
jobs:
sentinel:
# Run across the macOS runners GitHub hosts so a regression that is
# version-specific (new Xcode, new macOS) is pinpointed by row.
strategy:
fail-fast: false
matrix:
runner:
# Each runner ships a different Xcode/macOS combination so we detect
# version-specific private-framework regressions:
# macos-14 → macOS 14 (Sonoma) / Xcode 15–16
# macos-15 → macOS 15 (Sequoia) / Xcode 16–17
# macos-latest → latest GA (currently macOS 15)
- macos-latest
- macos-15
- macos-14
runs-on: ${{ matrix.runner }}
name: sentinel (${{ matrix.runner }})
steps:
- uses: actions/checkout@v4
- name: Print runner info
run: |
sw_vers
xcodebuild -version
echo "Xcode select: $(xcode-select -p)"
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build (emits dist/sim-hid-bridge)
run: npm run build
- name: Run sentinel tests
run: |
# jest.config.js testPathIgnorePatterns ignores `/tests/ci/` for
# `npm test`; override with only `/node_modules/` here so the
# scheduled sentinel actually executes instead of silently
# reporting "No tests found, exiting with code 0".
#
# Capture --json output so the alert step can surface the failing
# probe name + stderr instead of a generic "sentinel failed".
set -o pipefail
npx jest --config jest.config.js tests/ci/sim-hid-sentinel.test.ts \
--runTestsByPath \
--testPathIgnorePatterns=/node_modules/ \
--json --outputFile=sim-hid-sentinel-report.json \
2>&1 | tee sim-hid-sentinel.log
env:
# The sentinel probes the bridge directly — it does not need the
# focus-stealing fallback, and accidentally enabling it would mask
# real SimulatorKit regressions behind AppleScript taps.
OPENSAFARI_ALLOW_FOCUS_INPUT: ''
OPENSAFARI_HEADLESS_ONLY: '1'
- name: Upload sentinel artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: sim-hid-sentinel-${{ matrix.runner }}
path: |
sim-hid-sentinel.log
sim-hid-sentinel-report.json
if-no-files-found: warn
- name: Open / update tracking issue on failure
if: failure() && github.event_name == 'schedule'
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const fs = require('fs');
const runner = process.env.RUNNER_LABEL;
// Extract failing probes from the jest JSON report so the on-call
// engineer sees the exact failing assertion and error in the
// created issue, not just a "sentinel failed" placeholder.
let failing = [];
try {
const raw = fs.readFileSync('sim-hid-sentinel-report.json', 'utf8');
const report = JSON.parse(raw);
for (const suite of report.testResults ?? []) {
for (const tc of suite.assertionResults ?? []) {
if (tc.status === 'failed') {
failing.push({
name: [...(tc.ancestorTitles ?? []), tc.title].join(' › '),
messages: (tc.failureMessages ?? []).map(m =>
m.split('\n').slice(0, 8).join('\n'),
),
});
}
}
}
} catch (err) {
core.warning(`Could not parse sim-hid-sentinel-report.json: ${err.message}`);
}
const title = `[sentinel] SimulatorKit HID BC-break suspected (${runner})`;
const failureBlock = failing.length
? failing.map(f =>
`### ${f.name}\n\n\`\`\`\n${f.messages.join('\n---\n')}\n\`\`\``,
).join('\n\n')
: '_Could not parse jest report — see attached `sim-hid-sentinel.log` artifact._';
const body = [
'`sim-hid-sentinel` failed on the scheduled run.',
'',
`- Runner: \`${runner}\``,
`- Workflow run: ${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`,
'',
'## Failing probes',
'',
failureBlock,
'',
'## Triage checklist',
'',
'Apple may have moved or removed a private framework symbol that',
'`sim-hid-bridge` depends on. Verify:',
'',
'- `ls /Library/Developer/PrivateFrameworks/SimulatorKit.framework`',
'- `nm -g /Library/Developer/PrivateFrameworks/SimulatorKit.framework/SimulatorKit | grep Indigo`',
'- Xcode release notes for the current major for private-framework layout changes.',
'',
'See `docs/private-apis.md` for the BC-break response playbook.',
].join('\n');
// Reuse an existing open issue with the same title if one is
// already on the board; otherwise open a new one.
const { data: existing } = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
labels: 'sentinel',
per_page: 50,
});
const match = existing.find((i) => i.title === title);
if (match) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: match.number,
body,
});
} else {
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title,
body,
labels: ['sentinel', 'bug'],
});
}
env:
RUNNER_LABEL: ${{ matrix.runner }}