-
Notifications
You must be signed in to change notification settings - Fork 0
352 lines (324 loc) · 14.4 KB
/
Copy pathci.yml
File metadata and controls
352 lines (324 loc) · 14.4 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
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
test:
name: Tests (Python ${{ matrix.python-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# The stack claims Python 3.8+ support — exercise the low and high ends.
python-version: ["3.9", "3.11", "3.13"]
include:
- python-version: "3.8"
continue-on-error: true # 3.8 EOL oct 2024 — kept for backward-compat claim
steps:
- uses: actions/checkout@v7
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python-version }}
- name: Run test suite (stdlib unittest — zero dependencies)
run: |
python -m unittest discover -s tools/ai_docs/tests -p "test_*.py" -v
python -m unittest discover -s scripts/tests -p "test_*.py" -v
conventions:
name: Conventions declared == conventions enforced
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: "3.11"
- name: AGENTS.md and conventions.json must agree
# Without this gate, `>25 blocking` complexity silently shipped as 20
# and `>200 blocking` function size shipped as nothing at all.
run: python scripts/validate_conventions.py
- name: AGENTS.md must describe the repository it ships with
# Its scope table claimed 11 files / ~22 000 tokens for ten weeks while
# the repo grew to 189 files / ~231 000 — and told every agent to read
# the whole thing in one pass.
run: python scripts/measure_scope.py
loc-gate:
name: LOC budget gate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: "22"
- name: Enforce the stack's own LOC rule on its own sources
# Uses the same run_gate.js the PreToolUse hook uses, reading the same
# conventions.json — one implementation, so CI and the hook can never
# disagree about what the limit is.
run: |
set -e
for dir in tools skills hooks scripts; do
echo "--- $dir"
(cd "$dir" && node ../hooks/pretool-loc-gate/run_gate.js --all)
done
vault-v4-protocol:
name: Vault v4 protocol, sync and hooks
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: "3.11"
- uses: actions/setup-node@v7
with:
node-version: "22"
- name: v4 protocol — discovery, slug, markers, validator, maintenance
run: python -m unittest scripts.tests.test_vault_protocol -v
- name: v4 installer — six harness, idempotent, --check, --dry-run
run: python -m unittest scripts.tests.test_install_agents_v4 -v
- name: v4 sync — maintenance lock, green/red validator, --no-validator-check
run: python -m unittest scripts.tests.test_vault_sync_v4 -v
- name: v4 hooks — no-op, real error, v4 layout, concurrent ends
shell: bash
run: python -m unittest discover -s hooks/tests -p "test_*.py" -v
installers:
name: Installers (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: "3.11"
- name: Global installer — dry-run into a throwaway HOME
shell: bash
run: |
python scripts/install_agents.py --home "$HOME/stack-ci/fakehome" --dry-run
- name: Global installer — real install, then idempotent re-run
shell: bash
run: |
set -e
mkdir -p "$HOME/stack-ci"
python scripts/install_agents.py --home "$HOME/stack-ci/fakehome"
echo "--- second run must report 0 change(s)"
python scripts/install_agents.py --home "$HOME/stack-ci/fakehome" \
| tee "$HOME/stack-ci/second.txt"
grep -q "0 change(s), 0 issue(s)" "$HOME/stack-ci/second.txt"
- name: Global installer — --check must pass on a fresh install
shell: bash
run: python scripts/install_agents.py --home "$HOME/stack-ci/fakehome" --check
- name: Per-project installer — into a throwaway git repo
shell: bash
run: |
set -e
mkdir -p "$HOME/stack-ci/proj/src"
cd "$HOME/stack-ci/proj"
git init -q .
git config user.email ci@example.com
git config user.name CI
echo "print('hi')" > src/main.py
git add -A && git commit -qm init
cd "$GITHUB_WORKSPACE"
python install.py --project-root "$HOME/stack-ci/proj" --skip-gstack
test -f "$HOME/stack-ci/proj/AGENTS.md"
test -f "$HOME/stack-ci/proj/conventions.json"
test -d "$HOME/stack-ci/proj/.claude/skills/verify-ai-docs"
test -d "$HOME/stack-ci/proj/.agents/skills/verify-ai-docs"
- name: A re-install prunes files removed upstream
shell: bash
run: |
set -e
# A plain copy leaves a file deleted upstream sitting in every
# project that installed it earlier — the drift this stack exists
# to prevent.
touch "$HOME/stack-ci/proj/.claude/skills/verify-ai-docs/OBSOLETE.md"
python install.py --project-root "$HOME/stack-ci/proj" --skip-gstack
if [ -f "$HOME/stack-ci/proj/.claude/skills/verify-ai-docs/OBSOLETE.md" ]; then
echo "a file removed upstream survived a re-install"; exit 1
fi
- name: v4 governance block — six harness get vault block; user content survives
# Build a throwaway v4 vault, point the installer at it via
# --vault/--project-slug, and verify each harness file has
# both the method block and the vault governance block, with
# user content outside the markers preserved across a
# re-install.
shell: bash
run: |
set -e
# Build a minimal v4 vault on disk (no validator call; we
# are testing the installer, not the validator).
FAKEVAULT="$RUNNER_TEMP/fakev4"
mkdir -p "$FAKEVAULT/_system/schemas" "$FAKEVAULT/_system/tooling" "$FAKEVAULT/projects/demo"
echo '# vault' > "$FAKEVAULT/AGENTS.md"
echo '# system' > "$FAKEVAULT/_system/AGENTS.md"
cat > "$FAKEVAULT/_system/schemas/projects.json" <<'JSON'
{"schema_version": 4, "projects": [{"slug": "demo", "title": "Demo", "prefix": "DEMO", "source": "Demo", "status": "active"}]}
JSON
echo 'print("OK")' > "$FAKEVAULT/_system/tooling/vault.py"
echo '' > "$FAKEVAULT/_system/tooling/vaultlib.py"
python scripts/install_agents.py \
--home "$HOME/stack-ci/fakehome" \
--vault "$FAKEVAULT" --project-slug demo
# Six harness targets, all must have both blocks.
for f in \
"$HOME/stack-ci/fakehome/.claude/CLAUDE.md" \
"$HOME/stack-ci/fakehome/.codex/AGENTS.md" \
"$HOME/stack-ci/fakehome/.config/opencode/AGENTS.md" \
"$HOME/stack-ci/fakehome/.cursor/rules/ai-native-dev-stack.mdc" \
"$HOME/stack-ci/fakehome/.gemini/GEMINI.md" \
"$HOME/stack-ci/fakehome/.mavis/agents/mavis/agent.md"; do
grep -q "Shared engineering method" "$f"
grep -q "Vault governance (v4)" "$f"
done
# Re-install with a user note appended must preserve it.
echo "" >> "$HOME/stack-ci/fakehome/.claude/CLAUDE.md"
echo "# USER NOTE" >> "$HOME/stack-ci/fakehome/.claude/CLAUDE.md"
python scripts/install_agents.py \
--home "$HOME/stack-ci/fakehome" \
--vault "$FAKEVAULT" --project-slug demo \
| grep -q "0 change(s)"
grep -q "USER NOTE" "$HOME/stack-ci/fakehome/.claude/CLAUDE.md"
# --check must report 0 issues.
python scripts/install_agents.py \
--home "$HOME/stack-ci/fakehome" \
--vault "$FAKEVAULT" --project-slug demo --check \
| grep -q "0 issue(s)"
vault-sync:
name: Vault sync (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: "3.11"
- name: Build a throwaway vault and bare remote
shell: bash
run: |
set -e
mkdir -p "$RUNNER_TEMP/remote" "$RUNNER_TEMP/vault"
git init -q --bare "$RUNNER_TEMP/remote"
cd "$RUNNER_TEMP/vault"
git init -q -b master .
git config user.email ci@example.com
git config user.name CI
echo "# INDEX" > INDEX.md
git add -A && git commit -qm init
git remote add origin "$RUNNER_TEMP/remote"
git push -q -u origin master
git remote set-head origin -a
- name: Clean vault reports "already in sync", and is idempotent
shell: bash
run: |
set -e
python scripts/vault_sync.py --vault "$RUNNER_TEMP/vault" | tee /tmp/a.txt
grep -q "already in sync" /tmp/a.txt
python scripts/vault_sync.py --vault "$RUNNER_TEMP/vault" | grep -q "already in sync"
- name: A new note is committed and the push is verified
shell: bash
run: |
set -e
echo "note" > "$RUNNER_TEMP/vault/note.md"
python scripts/vault_sync.py --vault "$RUNNER_TEMP/vault" | grep -q "pushed to the remote"
python scripts/vault_sync.py --vault "$RUNNER_TEMP/vault" | grep -q "already in sync"
- name: A real credential blocks the push; a documented example does not
shell: bash
run: |
set -e
echo 'AKIAQ7B3CDEFGHIJKLMN' > "$RUNNER_TEMP/vault/real.md"
if python scripts/vault_sync.py --vault "$RUNNER_TEMP/vault"; then
echo "expected the credential guard to block"; exit 1
fi
test -z "$(git -C "$RUNNER_TEMP/vault" diff --cached --name-only)"
rm "$RUNNER_TEMP/vault/real.md"
echo 'AKIAIOSFODNN7EXAMPLE is the AWS docs key' > "$RUNNER_TEMP/vault/doc.md"
python scripts/vault_sync.py --vault "$RUNNER_TEMP/vault" | grep -q "pushed to the remote"
- name: A side branch is refused
shell: bash
run: |
set -e
git -C "$RUNNER_TEMP/vault" checkout -q -b side
if python scripts/vault_sync.py --vault "$RUNNER_TEMP/vault"; then
echo "expected the branch guard to block"; exit 1
fi
git -C "$RUNNER_TEMP/vault" checkout -q master
- name: A failed sync does not mark the day as done
shell: bash
run: |
set -e
# Force a failure: put the vault back on a side branch.
git -C "$RUNNER_TEMP/vault" checkout -q -b side2
export OBSIDIAN_SYNC_STATE="$RUNNER_TEMP/sentinel.txt"
rm -f "$OBSIDIAN_SYNC_STATE"
if python scripts/vault_sync_once_daily.py --vault "$RUNNER_TEMP/vault"; then
echo "expected once-daily to propagate the failure"; exit 1
fi
if [ -f "$OBSIDIAN_SYNC_STATE" ]; then
echo "sentinel written despite a failed sync — retries suppressed"; exit 1
fi
git -C "$RUNNER_TEMP/vault" checkout -q master
python scripts/vault_sync_once_daily.py --vault "$RUNNER_TEMP/vault"
test -f "$OBSIDIAN_SYNC_STATE"
- name: The lock is never left in the working tree
shell: bash
run: |
set -e
test -z "$(git -C "$RUNNER_TEMP/vault" ls-files | grep -i lock || true)"
test -z "$(git -C "$RUNNER_TEMP/vault" status --porcelain)"
hooks:
name: Hooks run on every OS
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: "22"
- name: LOC gate — pass, warn and block tiers
shell: bash
run: |
set -e
GATE=hooks/pretool-loc-gate/run_gate.js
# Node is the only guaranteed runtime in this job — build the
# fixtures with it rather than assuming `python` is on PATH.
node -e "require('fs').writeFileSync('big.py','x = 1\n'.repeat(1600))"
node -e "require('fs').writeFileSync('mid.py','x = 1\n'.repeat(900))"
# A source file under the limit passes. Use a .py: conventions.json
# is JSON, which the gate deliberately skips (see the .md case below).
node "$GATE" install.py | grep -q '"locGate":"pass"'
# A warning must carry a `reason` the agent can read, not just metadata.
node "$GATE" mid.py | grep -q '"reason"'
# A blocking file must abort and exit non-zero.
if node "$GATE" big.py > gate-out.json; then echo "expected exit 1"; exit 1; fi
grep -q '_abort' gate-out.json
# The rule is about source structure: a long CHANGELOG or dataset must
# not block an edit, and the skip must be visible rather than silent.
node -e "require('fs').writeFileSync('big.md','line\n'.repeat(2000))"
node "$GATE" big.md > md-out.json
grep -q '"locGate":"skipped"' md-out.json
grep -q 'scan_extensions' md-out.json
- name: Memory hooks — must report failure, never fake success
shell: bash
run: |
set -e
# With no vault reachable, the hooks must say so rather than
# reporting an empty-but-successful load (the old silent failure).
OBSIDIAN_API_KEY=dummy OBSIDIAN_API_TIMEOUT_MS=800 \
node hooks/session-start-memory/run.js | grep -q '"loaded":false'
OBSIDIAN_API_KEY=dummy OBSIDIAN_API_TIMEOUT_MS=800 \
node hooks/session-end-save/run.js | grep -q 'sessionSaveError'
# With no key at all, both must be clean no-ops.
node hooks/session-start-memory/run.js | grep -q 'OBSIDIAN_API_KEY not set'
node hooks/session-end-save/run.js | grep -q 'OBSIDIAN_API_KEY not set'