forked from kunchenguid/firstmate
-
Notifications
You must be signed in to change notification settings - Fork 0
368 lines (354 loc) · 14.2 KB
/
Copy pathci.yml
File metadata and controls
368 lines (354 loc) · 14.2 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
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
jobs:
lint:
name: Lint shell scripts
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Install pinned ShellCheck
run: |
set -eu
bin/fm-install-shellcheck.sh "$RUNNER_TEMP/bin"
echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH"
# Single owner of the lint definition (file set + config + version). Do not
# re-spell the shellcheck command here; keep CI and the pre-push gate on it.
- run: bin/fm-lint.sh
# Deterministic proof that portable parallel shards + portable serial + Herdr
# equal the complete tests/*.test.sh inventory with no missing or duplicates.
test-coverage:
name: Test coverage guard
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Prove complete regression partition
run: bin/fm-test-run.sh --check-coverage
# Two duration-balanced portable parallel shards of the Phase 2 proven-isolated
# set only. Composition owner: bin/fm-test-run.sh (docs/fm-test-portable-shards.md).
tests-portable-parallel-1:
name: Behavior portable parallel 1
runs-on: ubuntu-latest
# Measured shard wall is ~1 min of serial sum on proven scripts; this cap is
# a hang tripwire with margin, not the expected healthy end of the lane.
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Install pinned ShellCheck
run: |
set -eu
bin/fm-install-shellcheck.sh "$RUNNER_TEMP/bin"
echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH"
- name: Install tasks-axi
run: |
set -eu
npm install -g tasks-axi
tasks-axi --version
- name: Run portable parallel shard 1
run: |
set -eu
mkdir -p "$RUNNER_TEMP/fm-test"
bin/fm-test-run.sh --lane portable-parallel-1 \
--json "$RUNNER_TEMP/fm-test/fm-test-timing-portable-parallel-1.json"
- name: Upload shard 1 timing artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: fm-test-timing-portable-parallel-1
path: ${{ runner.temp }}/fm-test/fm-test-timing-portable-parallel-1.json
if-no-files-found: warn
tests-portable-parallel-2:
name: Behavior portable parallel 2
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Install pinned ShellCheck
run: |
set -eu
bin/fm-install-shellcheck.sh "$RUNNER_TEMP/bin"
echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH"
- name: Install tasks-axi
run: |
set -eu
npm install -g tasks-axi
tasks-axi --version
- name: Run portable parallel shard 2
run: |
set -eu
mkdir -p "$RUNNER_TEMP/fm-test"
bin/fm-test-run.sh --lane portable-parallel-2 \
--json "$RUNNER_TEMP/fm-test/fm-test-timing-portable-parallel-2.json"
- name: Upload shard 2 timing artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: fm-test-timing-portable-parallel-2
path: ${{ runner.temp }}/fm-test/fm-test-timing-portable-parallel-2.json
if-no-files-found: warn
# Required portable serial remainder: watcher, lock, AFK, tmux, daemon,
# ambiguous, and other stateful tests. Real Herdr stays in tests-herdr.
tests-portable-serial:
name: Behavior portable serial
runs-on: ubuntu-latest
# Measured serial remainder is ~13 min wall without Herdr. Cap is a hang
# tripwire above observed p99 script cost and suite wall, not the expected
# healthy end (interim 25m full-suite slack reduced after sharding).
timeout-minutes: 20
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Install pinned ShellCheck
run: |
set -eu
bin/fm-install-shellcheck.sh "$RUNNER_TEMP/bin"
echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH"
- name: Require tmux for e2e tests
run: |
set -eu
command -v tmux >/dev/null || {
echo "::error::tmux is required for real afk injection e2e coverage"
exit 1
}
tmux -V
- name: Install tasks-axi for backlog-handoff delegation
run: |
set -eu
npm install -g tasks-axi
tasks-axi --version
- name: Run portable serial remainder
run: |
set -eu
mkdir -p "$RUNNER_TEMP/fm-test"
bin/fm-test-run.sh --lane portable-serial \
--json "$RUNNER_TEMP/fm-test/fm-test-timing-portable-serial.json"
- name: Upload portable serial timing artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: fm-test-timing-portable-serial
path: ${{ runner.temp }}/fm-test/fm-test-timing-portable-serial.json
if-no-files-found: warn
# Required real-Herdr lane: pinned install, serial real-herdr-gated family,
# hard-fail on "skip: herdr not found". Starts on Linux x86_64; if a genuine
# platform invariant fails (focus, cleanup, default-session tripwire), keep
# the failure evidence and move the job to macOS rather than skip assertions.
tests-herdr:
name: Behavior tests (Herdr)
runs-on: ubuntu-latest
# Real Herdr is slower than the portable suite; this is a hang tripwire,
# not the expected healthy end of the lane (estimate 15-40 min first cut).
timeout-minutes: 40
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Require tools for real Herdr
run: |
set -eu
command -v jq >/dev/null || { echo "::error::jq is required"; exit 1; }
command -v python3 >/dev/null || { echo "::error::python3 is required"; exit 1; }
command -v curl >/dev/null || { echo "::error::curl is required"; exit 1; }
command -v tar >/dev/null || { echo "::error::tar is required"; exit 1; }
jq --version
python3 --version
- name: Install pinned Herdr
run: |
set -eu
bin/fm-install-herdr.sh "$RUNNER_TEMP/bin"
echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH"
- name: Install pinned Treehouse
run: |
set -eu
# Several real-herdr-gated E2E scripts require treehouse for spawn
# worktree acquisition (presentation, workspace-per-home, autodetect).
bin/fm-install-treehouse.sh "$RUNNER_TEMP/bin"
echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH"
- name: Assert Herdr pin and protocol floor
run: |
set -eu
command -v herdr >/dev/null || { echo "::error::herdr not on PATH after install"; exit 1; }
command -v treehouse >/dev/null || { echo "::error::treehouse not on PATH after install"; exit 1; }
herdr --version
treehouse --version
status=$(herdr status --json)
printf '%s\n' "$status"
version=$(printf '%s' "$status" | jq -r '.client.version // empty')
protocol=$(printf '%s' "$status" | jq -r '.client.protocol // empty')
[ "$version" = "0.7.4" ] || {
echo "::error::expected exact Herdr pin 0.7.4, got ${version:-<empty>}"
exit 1
}
case "$protocol" in
''|*[!0-9]*) echo "::error::could not read client protocol"; exit 1 ;;
esac
[ "$protocol" -ge 16 ] || {
echo "::error::herdr protocol $protocol is below the required floor 16"
exit 1
}
- name: Start default Herdr session for fleet-state tripwire
run: |
set -eu
# Lab provision requires exactly one running default session so the
# default-session tripwire can arm. Start it headless; never use it
# for lab work (tests still refuse default and use fm-lab-* only).
mkdir -p "$RUNNER_TEMP/fm-herdr"
nohup herdr server >"$RUNNER_TEMP/fm-herdr/default-server.log" 2>&1 &
echo $! >"$RUNNER_TEMP/fm-herdr/default-server.pid"
attempt=0
while [ "$attempt" -lt 150 ]; do
running=$(herdr status --json 2>/dev/null | jq -r '.server.running // false' || echo false)
if [ "$running" = true ]; then
herdr session list --json | jq -e '
[.sessions[]? | select(.default == true and .name == "default" and .running == true)]
| length == 1
' >/dev/null
echo "default Herdr session is running"
exit 0
fi
sleep 0.2
attempt=$((attempt + 1))
done
echo "::error::default Herdr server did not become ready"
cat "$RUNNER_TEMP/fm-herdr/default-server.log" || true
exit 1
- name: Snapshot pre-suite Herdr sessions
run: |
set -eu
mkdir -p "$RUNNER_TEMP/fm-herdr"
bin/fm-herdr-ci-cleanup.sh snapshot "$RUNNER_TEMP/fm-herdr/sessions-before.json"
- name: Run real-Herdr family (serial, required)
run: |
set -eu
mkdir -p "$RUNNER_TEMP/fm-test"
# Serial only. Fail if any script reports herdr not found. Live
# harness credential tests stay outside this family (opt-in env only).
bin/fm-test-run.sh --family real-herdr-gated \
--fail-on-gate-skip 'herdr not found' \
--json "$RUNNER_TEMP/fm-test/fm-test-timing-herdr.json"
- name: Cleanup job-owned Herdr lab sessions
if: always()
run: |
set -eu
bin/fm-herdr-ci-cleanup.sh teardown "$RUNNER_TEMP/fm-herdr/sessions-before.json" || {
echo "::warning::lab session cleanup reported a problem; see logs"
# Surface diagnostics without weakening the suite result above.
herdr session list --json || true
exit 1
}
- name: Upload Herdr timing and diagnostics
if: always()
uses: actions/upload-artifact@v4
with:
name: fm-test-timing-herdr
path: |
${{ runner.temp }}/fm-test/fm-test-timing-herdr.json
${{ runner.temp }}/fm-herdr/sessions-before.json
${{ runner.temp }}/fm-herdr/default-server.log
if-no-files-found: warn
# Aggregate per-lane timing into one summary artifact for critical-path review.
tests-timing-aggregate:
name: Behavior timing aggregate
runs-on: ubuntu-latest
needs:
- tests-portable-parallel-1
- tests-portable-parallel-2
- tests-portable-serial
- tests-herdr
if: always()
steps:
- uses: actions/checkout@v6
- name: Download lane timing artifacts
uses: actions/download-artifact@v4
with:
pattern: fm-test-timing-*
path: ${{ runner.temp }}/fm-test-aggregate
merge-multiple: true
- name: Build aggregate timing summary
run: |
set -eu
mkdir -p "$RUNNER_TEMP/fm-test"
shopt -s nullglob
inputs=("$RUNNER_TEMP"/fm-test-aggregate/fm-test-timing-*.json)
if [ "${#inputs[@]}" -eq 0 ]; then
echo "::warning::no per-lane timing JSON found to aggregate"
exit 0
fi
bin/fm-test-run.sh --aggregate-json \
"$RUNNER_TEMP/fm-test/fm-test-timing-aggregate.json" \
"${inputs[@]}"
- name: Upload aggregate timing artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: fm-test-timing-aggregate
path: ${{ runner.temp }}/fm-test/fm-test-timing-aggregate.json
if-no-files-found: warn
macos-stock-bash:
name: Stock macOS Bash snapshot compatibility
runs-on: macos-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
- name: Run snapshot consumers with stock Bash
shell: /bin/bash {0}
env:
PATH: /bin:/usr/bin:/usr/sbin:/sbin:/usr/local/bin:/opt/homebrew/bin
run: |
set -eu
case "$BASH_VERSION" in
3.2.57*) ;;
*) echo "::error::expected stock macOS Bash 3.2.57, got $BASH_VERSION"; exit 1 ;;
esac
/bin/bash --version | head -1
command -v jq >/dev/null || { echo "::error::jq is required"; exit 1; }
shell_inventory="$RUNNER_TEMP/fm-shell-inventory"
bin/fm-lint.sh --list-files > "$shell_inventory"
parse_fail=0
while IFS= read -r f; do
/bin/bash -n "$f" || { echo "::error::stock macOS Bash 3.2 failed to parse $f"; parse_fail=1; }
done < "$shell_inventory"
[ "$parse_fail" -eq 0 ] || { echo "::error::stock macOS Bash 3.2 parse sweep failed"; exit 1; }
snapshot_output=$(/bin/bash tests/fm-fleet-snapshot-view.test.sh)
printf '%s\n' "$snapshot_output"
snapshot_count=$(printf '%s\n' "$snapshot_output" | grep -c '^ok - ')
[ "$snapshot_count" -eq 15 ] || {
echo "::error::expected 15 snapshot/fleet-view tests, got $snapshot_count"
exit 1
}
bearings_output=$(/bin/bash tests/fm-bearings-snapshot.test.sh)
printf '%s\n' "$bearings_output"
bearings_count=$(printf '%s\n' "$bearings_output" | grep -c '^ok - ')
[ "$bearings_count" -eq 42 ] || {
echo "::error::expected 42 Bearings tests, got $bearings_count"
exit 1
}
invariants:
name: Repo invariants
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Symlinks must stay intact
run: |
set -eu
[ "$(readlink CLAUDE.md)" = "AGENTS.md" ] || { echo "::error::CLAUDE.md must be a symlink to AGENTS.md"; exit 1; }
[ "$(readlink .claude/skills)" = "../.agents/skills" ] || { echo "::error::.claude/skills must be a symlink to ../.agents/skills"; exit 1; }
- name: Personal fleet paths must not be tracked
run: |
set -eu
tracked=$(git ls-files -- data state config projects .no-mistakes)
if [ -n "$tracked" ]; then
echo "::error::Personal fleet paths are tracked in git:"
printf '%s\n' "$tracked"
exit 1
fi