-
Notifications
You must be signed in to change notification settings - Fork 0
463 lines (411 loc) · 15.2 KB
/
Copy pathci.yml
File metadata and controls
463 lines (411 loc) · 15.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
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
name: CI
on:
push:
branches: [main, master, "release/**"]
tags:
- "v*"
pull_request:
branches: [main, master]
permissions:
contents: read
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
backend:
name: Backend (Laravel)
runs-on: ubuntu-latest
# 45 (was 35): pcov coverage instrumentation adds overhead to the Pest run.
timeout-minutes: 45
services:
postgres:
image: pgvector/pgvector:pg16
env:
POSTGRES_DB: parthenon_test
POSTGRES_USER: parthenon
POSTGRES_PASSWORD: secret
ports:
- 5432:5432
options: >-
--health-cmd="pg_isready -U parthenon"
--health-interval=10s
--health-timeout=5s
--health-retries=5
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd="redis-cli ping"
--health-interval=10s
--health-timeout=5s
--health-retries=5
steps:
- uses: actions/checkout@v6
- name: Install PostGIS into postgres service container
run: |
# Phase 19 (D-04): the gis schema migration MUST fail loudly when
# PostGIS is missing, so CI installs postgresql-16-postgis-3 on top
# of pgvector/pgvector:pg16 (mirrors docker/postgres/Dockerfile).
PG_CID=$(docker ps --filter "ancestor=pgvector/pgvector:pg16" --format '{{.ID}}' | head -n1)
if [ -z "$PG_CID" ]; then
echo "Could not find pgvector postgres service container" >&2
docker ps
exit 1
fi
docker exec "$PG_CID" bash -c "apt-get update -qq && apt-get install -y --no-install-recommends postgresql-16-postgis-3"
- name: Prepare Postgres schemas and extensions
env:
PGPASSWORD: secret
PGDATABASE: parthenon_test
PGUSER: parthenon
POSTGRES_CREATE_TEST_DATABASE: "1"
POSTGRES_ENABLE_POSTGIS: "1"
run: bash scripts/ci/prepare-postgres.sh
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: "8.4"
extensions: pdo_pgsql, pgsql, redis, bcmath, intl, mbstring, pcntl, zip
coverage: pcov
- name: Install Composer dependencies
working-directory: backend
run: composer install --prefer-dist --no-interaction
- name: Copy env
working-directory: backend
run: cp .env.example .env && php artisan key:generate
- name: Run Pint (code style)
working-directory: backend
run: ./vendor/bin/pint --test
- name: Run PHPStan (static analysis)
working-directory: backend
run: ./vendor/bin/phpstan analyse
- name: Run Pest (tests)
working-directory: backend
env:
DB_CONNECTION: pgsql
DB_HOST: 127.0.0.1
DB_PORT: 5432
DB_DATABASE: parthenon_test
DB_USERNAME: parthenon
DB_PASSWORD: secret
# phpunit.xml forces DB_CONNECTION=pgsql_testing, which reads DB_TEST_*
# with fallback to DB_*. Its DB_TEST_PORT default is 5480 (host PG17
# dev convention), so without these explicit overrides the test
# connection ignores the CI postgres service and tries port 5480.
DB_TEST_HOST: 127.0.0.1
DB_TEST_PORT: 5432
DB_TEST_DATABASE: parthenon_testing
DB_TEST_USERNAME: parthenon
DB_TEST_PASSWORD: secret
REDIS_HOST: 127.0.0.1
CACHE_STORE: array
SESSION_DRIVER: array
QUEUE_CONNECTION: sync
run: |
php artisan migrate --force
set +e
./vendor/bin/pest \
--coverage-clover=coverage.xml \
--do-not-fail-on-skipped \
--do-not-fail-on-warning \
--do-not-fail-on-phpunit-warning \
--do-not-fail-on-deprecation \
--do-not-fail-on-phpunit-deprecation \
--do-not-fail-on-notice \
--do-not-fail-on-incomplete 2>&1 | tee pest.log
pest_status=${PIPESTATUS[0]}
set -e
if [ "$pest_status" -ne 0 ]; then
summary=$(grep -E 'Tests:[[:space:]]' pest.log | tail -1 || true)
if echo "$summary" | grep -Eq '[1-9][0-9]* (failed|errored|risky)'; then
exit "$pest_status"
fi
if grep -Eq 'Code coverage below expected' pest.log; then
exit "$pest_status"
fi
if echo "$summary" | grep -q 'passed'; then
echo "Pest exited $pest_status with non-failure issue statuses only; continuing."
exit 0
fi
exit "$pest_status"
fi
- name: Enforce coverage floor
working-directory: backend
run: |
# Robust, explicit floor: parse the clover report's project-level
# aggregate metrics rather than relying on Pest's exit code / log text.
# Floor ratchets up toward 80% as coverage grows; measured 33.59% on
# 2026-06-20 (25693/76492 statements). NEVER lower this value.
php -r '
$floor = 32.0;
$f = "coverage.xml";
if (!is_file($f)) { fwrite(STDERR, "coverage.xml not found — coverage was not collected\n"); exit(1); }
$xml = simplexml_load_file($f);
$nodes = $xml->xpath("/coverage/project/metrics");
if (!$nodes) { fwrite(STDERR, "no project metrics in clover report\n"); exit(1); }
$m = end($nodes);
$stmts = (int) $m["statements"];
$covered = (int) $m["coveredstatements"];
$pct = $stmts > 0 ? ($covered / $stmts) * 100.0 : 0.0;
printf("Line coverage: %.2f%% (%d/%d statements); floor %.1f%%\n", $pct, $covered, $stmts, $floor);
if ($pct + 1e-9 < $floor) {
fwrite(STDERR, sprintf("::error::Coverage %.2f%% is below the %.1f%% floor\n", $pct, $floor));
exit(1);
}
'
frontend:
name: Frontend (React)
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- uses: actions/checkout@v6
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: 22
cache: npm
cache-dependency-path: frontend/package-lock.json
- name: Install dependencies
working-directory: frontend
run: npm ci --legacy-peer-deps
- name: Type check
working-directory: frontend
run: npx tsc --noEmit
- name: Lint
working-directory: frontend
run: npm run lint
- name: Test
working-directory: frontend
run: npx vitest run
- name: Build
working-directory: frontend
run: npm run build
ai:
name: AI Service (Python)
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- uses: actions/checkout@v6
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: "3.12"
cache: pip
cache-dependency-path: ai/requirements*.txt
- name: Install dependencies
working-directory: ai
run: |
pip install torch --index-url https://download.pytorch.org/whl/cpu
pip install -r requirements.txt
pip install -r requirements-dev.txt
- name: Type check (mypy)
working-directory: ai
run: mypy app/
- name: Test (pytest)
working-directory: ai
run: pytest
docs-build:
name: Documentation (Docusaurus)
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v6
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: "22"
cache: npm
cache-dependency-path: docs/site/package-lock.json
- name: Install dependencies
working-directory: docs/site
run: npm ci --legacy-peer-deps
- name: Build
working-directory: docs/site
run: npm run build
docs-pdf:
name: Documentation PDF Export
# Only runs on release branches (release/*, v*.*.*)
if: startsWith(github.ref, 'refs/heads/release/') || startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- uses: actions/checkout@v6
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: "22"
cache: npm
cache-dependency-path: docs/site/package-lock.json
- name: Install dependencies
working-directory: docs/site
run: npm ci --legacy-peer-deps
- name: Build documentation
working-directory: docs/site
run: npm run build
- name: Install Chromium
run: npx puppeteer browsers install chrome
- name: Generate PDF from built docs
working-directory: docs/site
run: |
node -e "
const puppeteer = require('puppeteer');
const path = require('path');
const fs = require('fs');
const { execSync } = require('child_process');
// Serve the built site locally
const { createServer } = require('http');
const handler = require('serve-handler');
const server = createServer((req, res) => handler(req, res, { public: 'build' }));
server.listen(3001, async () => {
const browser = await puppeteer.launch({ args: ['--no-sandbox'] });
const page = await browser.newPage();
await page.goto('http://localhost:3001/', { waitUntil: 'networkidle0' });
await page.pdf({ path: 'parthenon-user-manual.pdf', format: 'A4', printBackground: true });
await browser.close();
server.close();
console.log('PDF generated: parthenon-user-manual.pdf');
});
"
- name: Upload PDF artifact
uses: actions/upload-artifact@v7
with:
name: parthenon-user-manual-pdf
path: docs/site/parthenon-user-manual.pdf
retention-days: 90
# docs-screenshots:
# name: Documentation Screenshots (Playwright)
# # Manual trigger or release branches only — requires a running Parthenon instance
# # seeded with Eunomia synthetic data.
# #
# # To run locally:
# # cd docs/screenshots && npm ci && npx playwright test
# #
# # Environment variables required:
# # PARTHENON_URL — base URL of a running Parthenon instance
# # PARTHENON_EMAIL / PARTHENON_PASSWORD — credentials for screenshot account
# if: false # Enable when screenshot infrastructure is ready
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v6
# - uses: actions/setup-node@v6
# with: { node-version: "22" }
# - run: npm ci
# working-directory: docs/screenshots
# - run: npx playwright install --with-deps chromium
# working-directory: docs/screenshots
# - run: npx playwright test
# working-directory: docs/screenshots
# env:
# PARTHENON_URL: ${{ secrets.PARTHENON_URL }}
# PARTHENON_EMAIL: ${{ secrets.PARTHENON_EMAIL }}
# PARTHENON_PASSWORD: ${{ secrets.PARTHENON_PASSWORD }}
# - uses: actions/upload-artifact@v7
# with:
# name: docs-screenshots
# path: docs/screenshots/screenshots/
openapi-export:
name: OpenAPI Spec (Scribe)
runs-on: ubuntu-latest
timeout-minutes: 30
services:
postgres:
image: pgvector/pgvector:pg16
env:
POSTGRES_DB: parthenon_test
POSTGRES_USER: parthenon
POSTGRES_PASSWORD: secret
ports:
- 5432:5432
options: >-
--health-cmd="pg_isready -U parthenon"
--health-interval=10s
--health-timeout=5s
--health-retries=5
steps:
- uses: actions/checkout@v6
- name: Prepare Postgres schemas
env:
PGPASSWORD: secret
PGDATABASE: parthenon_test
PGUSER: parthenon
run: bash scripts/ci/prepare-postgres.sh
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: "8.4"
extensions: pdo_pgsql, pgsql, redis, bcmath, intl, mbstring, pcntl, zip
coverage: none
- name: Install Composer dependencies
working-directory: backend
run: composer install --prefer-dist --no-interaction
- name: Copy env
working-directory: backend
run: cp .env.example .env && php artisan key:generate
- name: Export OpenAPI spec with Scribe
working-directory: backend
env:
DB_CONNECTION: pgsql
DB_HOST: 127.0.0.1
DB_PORT: 5432
DB_DATABASE: parthenon_test
DB_USERNAME: parthenon
DB_PASSWORD: secret
DB_SEARCH_PATH: app,public
run: |
php artisan scribe:generate --no-interaction
test -s public/docs/openapi.yaml
php -r '
require "vendor/autoload.php";
$spec = Symfony\Component\Yaml\Yaml::parseFile("public/docs/openapi.yaml");
if (! is_array($spec)) {
fwrite(STDERR, "OpenAPI spec is not a map.\n");
exit(1);
}
$paths = $spec["paths"] ?? [];
if (! is_array($paths) || count($paths) === 0) {
fwrite(STDERR, "OpenAPI spec has no paths.\n");
exit(1);
}
file_put_contents("api.json", json_encode($spec, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . PHP_EOL);
fwrite(STDOUT, "OpenAPI spec exported with " . count($paths) . " paths.\n");
'
- name: Validate OpenAPI spec
working-directory: backend
run: |
python3 -m json.tool api.json >/dev/null
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: "22"
cache: npm
cache-dependency-path: frontend/package-lock.json
- name: Install frontend dependencies
working-directory: frontend
run: npm ci --legacy-peer-deps
- name: Generate TypeScript types
working-directory: frontend
run: npm run generate:api-types
- name: Detect OpenAPI artifact drift
run: |
# Fail if regenerated OpenAPI artifacts differ from what's committed.
# Contributors must run ./deploy.sh --openapi whenever backend
# routes, requests, or responses change.
if ! git diff --exit-code -- backend/api.json frontend/src/types/api.generated.ts; then
echo ""
echo "::error::OpenAPI artifact drift detected."
echo ""
echo "The committed OpenAPI spec or TypeScript types do not match what would be generated from the current backend routes."
echo ""
echo "To fix locally:"
echo " ./deploy.sh --openapi"
echo " git add frontend/src/types/api.generated.ts backend/api.json"
echo " git commit -m 'chore: regenerate OpenAPI types'"
echo ""
echo "Unified diff of the drift is shown above."
exit 1
fi
echo "OpenAPI types are in sync with the backend spec."
- name: Verify TypeScript types compile
working-directory: frontend
run: npx tsc --noEmit