-
Notifications
You must be signed in to change notification settings - Fork 1
607 lines (519 loc) · 25 KB
/
Copy pathci.yml
File metadata and controls
607 lines (519 loc) · 25 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
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
cache: npm
cache-dependency-path: |
packages/contracts/package-lock.json
frontend/package-lock.json
app/package-lock.json
mastervault-mcp-server/package-lock.json
# frontend/ and app/ both depend on this via `file:../packages/contracts`
# (see their package.json) — `npm ci` in either just copies/symlinks
# whatever's on disk at packages/contracts right now, dist/ included.
# Only app/'s and server/'s own `npm run build` self-heal this via a
# `prebuild` hook; every other command here (lint, typecheck, test,
# frontend's own build) has no such hook and fails with "Cannot find
# module '@modelforge/contracts'" on a fresh checkout without this step.
- name: Install and build @modelforge/contracts
working-directory: packages/contracts
run: npm ci && npm run build
- name: Install frontend dependencies
working-directory: frontend
run: npm ci
- name: Install app dependencies
working-directory: app
run: npm ci
- name: Install mastervault-mcp-server dependencies
working-directory: mastervault-mcp-server
run: npm ci
- name: Lint frontend
working-directory: frontend
run: npm run lint
- name: Typecheck frontend
working-directory: frontend
run: npx tsc -b
- name: Typecheck app
working-directory: app
run: npx tsc -p tsconfig.json --noEmit
- name: Test frontend
working-directory: frontend
run: npm test
- name: Test app
working-directory: app
run: npm test
- name: Build frontend
working-directory: frontend
run: npm run build
- name: Build app
working-directory: app
run: npm run build
- name: Test mastervault-mcp-server
working-directory: mastervault-mcp-server
run: npm test
- name: Build bundled mastervault-mcp-server (the copy ModelForge packages)
working-directory: mastervault-mcp-server
run: npm run build:bundled
- name: Smoke-test the bundled mastervault-mcp-server over stdio
working-directory: mastervault-mcp-server
run: |
set -euo pipefail
mkdir -p /tmp/mastervault-ci-vault
echo '# CI test vault' > /tmp/mastervault-ci-vault/_orientation.md
printf '%s\n%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"ci","version":"1"}}}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' \
| node dist-bundled/index.js /tmp/mastervault-ci-vault > worker-output.json
cat worker-output.json
test "$(grep -o 'mastervault_orient' worker-output.json | wc -l)" -ge 1
server:
# server/ (the Fastify IAM/patient-case service) has its own package
# boundary and DATABASE_URL/REDIS_URL-gated integration suites (see e.g.
# store/postgres-case-store.test.ts's own disclosure comment) that
# simply skip — not fail — when no real Postgres/Redis is reachable.
# Real services are wired below so those suites actually run in CI
# instead of only ever being exercised by a developer running them
# locally; docs/ENTERPRISE_ARCHITECTURE_ROADMAP.md's P0 backlog item 18
# ("Run PostgreSQL and Redis suites in mandatory CI... No gated/skipped
# enterprise suites") is exactly this gap.
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: modelforge
POSTGRES_PASSWORD: modelforge
POSTGRES_DB: modelforge_test
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U modelforge"
--health-interval 5s
--health-timeout 3s
--health-retries 10
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 5s
--health-timeout 3s
--health-retries 10
env:
# DATABASE_URL stays the migration-owner ("modelforge") role — every
# postgres-*.test.ts file runs its own runMigrations() against
# DATABASE_URL directly (see e.g. store/postgres-iam-store.test.ts),
# which needs CREATE SCHEMA/CREATE FUNCTION/ALTER DEFAULT PRIVILEGES
# rights a restricted runtime role deliberately doesn't have.
# RUNTIME_DATABASE_URL is the separate, restricted role created below
# — used by store/postgres-rls.test.ts to prove tenant isolation
# (RLS on shared control-plane tables, pooled-connection tenant-
# context cleanup) actually holds under a real NO BYPASSRLS,
# non-owning role, not just under whatever role happened to run the
# migrations. See migrations/010_runtime_role_grants.sql.
DATABASE_URL: postgres://modelforge:modelforge@localhost:5432/modelforge_test
RUNTIME_DATABASE_URL: postgres://modelforge_runtime:modelforge_runtime@localhost:5432/modelforge_test
REDIS_URL: redis://localhost:6379
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: Install postgresql-client (for role setup below)
run: sudo apt-get update -qq && sudo apt-get install -y -qq postgresql-client
- name: Create restricted runtime database role
# Created here (as the modelforge owner role, over the same
# connection the app's migrations use) rather than via the
# postgres:16-alpine service container's own init hooks, so the
# exact privileges this role gets are visible in this workflow file
# next to DATABASE_URL/RUNTIME_DATABASE_URL, not buried in image
# config. NOSUPERUSER/NOBYPASSRLS is what makes
# migrations/008_control_plane_rls.sql's tenant_isolation policies
# actually apply to this role at all (a superuser or BYPASSRLS role
# ignores row-level security entirely, which would make the RLS
# penetration tests pass for the wrong reason). NOCREATEDB/
# NOCREATEROLE bounds what a compromised runtime process could do
# beyond the schemas/tables migrations/010 explicitly grants it.
run: |
PGPASSWORD=modelforge psql -h localhost -U modelforge -d modelforge_test -v ON_ERROR_STOP=1 <<'SQL'
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'modelforge_runtime') THEN
CREATE ROLE modelforge_runtime WITH LOGIN PASSWORD 'modelforge_runtime'
NOSUPERUSER NOCREATEDB NOCREATEROLE NOBYPASSRLS NOREPLICATION;
END IF;
END
$$;
GRANT CONNECT ON DATABASE modelforge_test TO modelforge_runtime;
SQL
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
cache: npm
cache-dependency-path: |
packages/contracts/package-lock.json
server/package-lock.json
# server/ depends on this via `file:../packages/contracts` (see its
# package.json). Its own `npm run build` self-heals via a `prebuild`
# hook, but `npm run typecheck`/`npm test` below run first and have no
# such hook — see the `test` job's identical step for the full reason.
- name: Install and build @modelforge/contracts
working-directory: packages/contracts
run: npm ci && npm run build
- name: Install server dependencies
working-directory: server
run: npm ci
- name: Typecheck server
working-directory: server
run: npm run typecheck
- name: Test server (Postgres- and Redis-backed suites included, not skipped)
working-directory: server
run: npm test
- name: Build server
working-directory: server
run: npm run build
admin-console:
# admin-console/ is a separate, standalone web app (its own trust
# boundary per docs/ENTERPRISE_MULTIUSER_ARCHITECTURE.md §5, not the
# Electron clinician app) for managing a ModelForge organization's IAM.
# Its own job for the same reason server/e2e are separate: independent
# dependency graph, no reason a flaky/slow run here should block them
# or vice versa.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
cache: npm
cache-dependency-path: admin-console/package-lock.json
- name: Install admin-console dependencies
working-directory: admin-console
run: npm ci
- name: Typecheck admin-console
working-directory: admin-console
run: npx tsc -b
- name: Lint admin-console
working-directory: admin-console
run: npm run lint
- name: Test admin-console
working-directory: admin-console
run: npm test
- name: Build admin-console
working-directory: admin-console
run: npm run build
e2e:
# Electron E2E tests spin up a real Electron process and a Vite preview
# server per test, which is slower and flakier than the unit-test job
# above — kept as its own job so a flaky e2e run never blocks/hides a
# unit-test failure, and so it can be retried or skipped independently.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
cache: npm
cache-dependency-path: |
packages/contracts/package-lock.json
frontend/package-lock.json
app/package-lock.json
lib/package-lock.json
e2e/package-lock.json
# frontend/'s own build has no prebuild hook that would do this for it
# — see the `test` job's identical step for the full reason.
- name: Install and build @modelforge/contracts
working-directory: packages/contracts
run: npm ci && npm run build
- name: Install frontend dependencies
working-directory: frontend
run: npm ci
- name: Install app dependencies
working-directory: app
run: npm ci
# Several specs (audit-sqlite-*.spec.ts, audit-backend-toggle-
# failure.spec.ts) exercise the Settings UI's SQLite audit-backend
# toggle, which stays disabled — by design, see
# audit-sqlite-backend-toggle.spec.ts's own comment — whenever
# app/native isn't present. The separate `rust` job below builds this
# addon too, but on its own runner with no shared filesystem, so this
# job needs its own debug build (faster than --release; sufficient
# for tests) or those specs fail with "toggle stayed disabled" every
# time, not a rust/napi problem of their own.
- name: Build native addon (napi, debug)
working-directory: lib
run: npm ci && npm run build:debug
- name: Install e2e dependencies
working-directory: e2e
run: npm ci
- name: Install Playwright browsers
working-directory: e2e
run: npx playwright install --with-deps chromium
- name: Build frontend
working-directory: frontend
run: npm run build
- name: Build app
working-directory: app
run: npm run build
- name: Run e2e suite
working-directory: e2e
# Electron needs a real (or virtual) display even headless-ish, hence
# xvfb — there's no way to launch a BrowserWindow without one on Linux.
run: xvfb-run --auto-servernum npx playwright test
- name: Upload Playwright report
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: playwright-report
path: e2e/playwright-report
retention-days: 7
rust:
# lib/ (modelforge-native) is the napi-rs addon backing GGUF downloads
# and, more recently, the JSON datastore/audit-hashing primitives (see
# docs/ARCHITECTURE.md). fmt/clippy/cargo test only prove the Rust side
# is correct on whichever OS runs them — they say nothing about whether
# the resulting `.node` binary actually loads under Node on that OS,
# which is a materially different failure mode (wrong ABI, missing
# platform target, packaging mismatch). Previously this job only ran on
# ubuntu-latest, so nothing in CI ever built or loaded the addon on
# Windows or macOS despite installers shipping for both — see
# docs/RUST_MIGRATION_ASSESSMENT.md's platform-matrix section. The
# matrix below and the "napi build + require() smoke test" step close
# that gap.
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
defaults:
run:
working-directory: lib
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: Install rustfmt and clippy
run: rustup component add rustfmt clippy
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: |
~/.cargo/registry
~/.cargo/git
lib/target
key: ${{ runner.os }}-cargo-${{ hashFiles('lib/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-
# fmt/clippy only need to run once — Rust source and formatting are
# identical across OSes, so repeating this on all three runners would
# just burn CI minutes for the same answer three times.
- name: Format check
if: matrix.os == 'ubuntu-latest'
run: cargo fmt --check
- name: Clippy
if: matrix.os == 'ubuntu-latest'
run: cargo clippy --all-targets -- -D warnings
- name: Build
run: cargo build --release
- name: Test
run: cargo test
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
cache: npm
cache-dependency-path: lib/package-lock.json
- name: Install napi CLI
run: npm ci
- name: napi build (produces the real platform-specific .node binary)
run: npm run build:debug
# The actual gap this job exists to close: proves Node on *this* OS
# can load the binary napi build just produced, not just that Cargo
# compiled it. A wrong target triple, an ABI mismatch, or a packaging
# error would fail cargo build's cross-compile silently-succeed case
# but fail right here.
- name: Load the built addon under Node and verify its exports
shell: bash
run: |
node -e "
const addon = require('../app/native');
const expected = ['downloadGgufFile', 'DownloadManager', 'readJsonFileNative', 'writeJsonFileAtomicNative', 'sha256HexNative', 'appendJsonArrayElementNative'];
const missing = expected.filter((name) => !(name in addon));
if (missing.length > 0) {
console.error('Native addon loaded but is missing expected exports:', missing);
process.exit(1);
}
console.log('Native addon loaded successfully on ${{ matrix.os }} with all expected exports.');
"
sbom:
# Generates a CycloneDX SBOM per npm workspace so every release has an
# auditable, machine-readable dependency manifest — required groundwork
# for the supply-chain/provenance controls flagged in
# docs/ENTERPRISE_READINESS_ASSESSMENT.md, and useful on its own for
# answering "are we affected by CVE-X" without re-deriving the dependency
# tree by hand. Runs independently of the test job so a flaky SBOM
# generation never blocks the build/test signal.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
cache: npm
cache-dependency-path: |
packages/contracts/package-lock.json
frontend/package-lock.json
app/package-lock.json
mastervault-mcp-server/package-lock.json
# frontend/ and app/ both depend on this via `file:../packages/contracts`.
# `npm ci` in either only symlinks packages/contracts itself into their
# node_modules — it never installs *that* package's own dependencies
# (zod, typescript) into packages/contracts/node_modules, since this
# repo uses plain `file:` references, not true npm workspaces. cyclonedx-
# npm shells out to `npm ls --all`, which walks into that symlink and
# reports it as broken ("missing: typescript, zod, required by
# contracts@npm:@modelforge/contracts") whenever this step is skipped —
# confirmed by reproducing the exact ELSPROBLEMS/exit-254 failure this
# job hit in CI, byte for byte, with packages/contracts/node_modules
# absent, and confirming it clears once this step runs.
- name: Install @modelforge/contracts dependencies
working-directory: packages/contracts
run: npm ci
- name: Install frontend dependencies
working-directory: frontend
run: npm ci
- name: Install app dependencies
working-directory: app
run: npm ci
- name: Install mastervault-mcp-server dependencies
working-directory: mastervault-mcp-server
run: npm ci
- name: Generate SBOM (frontend)
working-directory: frontend
run: npx --yes @cyclonedx/cyclonedx-npm --output-file ../sbom-frontend.cdx.json
- name: Generate SBOM (app)
working-directory: app
run: npx --yes @cyclonedx/cyclonedx-npm --output-file ../sbom-app.cdx.json
- name: Generate SBOM (mastervault-mcp-server)
working-directory: mastervault-mcp-server
run: npx --yes @cyclonedx/cyclonedx-npm --output-file ../sbom-mastervault-mcp-server.cdx.json
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: sbom
path: sbom-*.cdx.json
retention-days: 90
security:
# SAST + secret scanning — docs/ENTERPRISE_ARCHITECTURE_ROADMAP.md P1
# item 9 ("Add artifact signing, provenance, SAST, and secret-scanning
# gates"), minus the signing/provenance half: that needs real
# certificates/key-custody/HSM infrastructure this repository does not
# build (a standing instruction — see that item's own note in the
# roadmap's §14 P1 list). SAST and secret-scanning need neither, so
# they're not blocked on it. Its own job, independent of test/build, so
# a finding here is a distinct, unambiguous signal rather than mixed
# into an unrelated build failure.
#
# Both tools were run locally against this exact repository (working
# tree + full commit history) before this job was added, specifically
# to avoid shipping an untested gate that immediately red-lines CI on
# day one: gitleaks found zero leaks across 121 commits; Semgrep (see
# config below) found zero real findings after fixing what it caught
# (an unpinned-Actions supply-chain gap across every workflow file, two
# `github`-context shell-injection spots in release.yml, a missing
# explicit AES-GCM auth-tag length in app/src/case-encryption.ts, and a
# missing Dependabot cooldown period) — see docs/OBSERVABILITY.md's
# sibling P1 item 8 work for the same "verify locally before gating"
# approach applied to the metrics endpoint. The one excluded rule below
# is a genuine false-positive class for this codebase, not a suppressed
# real finding — see its own comment.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
fetch-depth: 0 # gitleaks scans commit history, not just the working tree
- name: Install gitleaks
run: |
set -euo pipefail
GITLEAKS_VERSION=8.21.2
curl -sSL -o /tmp/gitleaks.tar.gz "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz"
tar -xzf /tmp/gitleaks.tar.gz -C /tmp gitleaks
sudo mv /tmp/gitleaks /usr/local/bin/gitleaks
gitleaks version
- name: Secret scan (gitleaks — full commit history)
run: gitleaks detect --source . --redact -v
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.12"
- name: Install Semgrep
run: python -m pip install --upgrade semgrep
- name: SAST scan (Semgrep — OWASP/JS/TS community rulesets, no account required)
run: |
semgrep scan \
--config p/ci \
--config p/owasp-top-ten \
--config p/javascript \
--config p/typescript \
--exclude-rule javascript.express.security.audit.xss.direct-response-write.direct-response-write \
--error \
--metrics=off
# javascript.express.security.audit.xss.direct-response-write assumes
# an Express-style app rendering HTML from `res.send()`. Every route
# in server/ is a Fastify JSON API — `reply.send(jsonValue)` goes
# through Fastify's own JSON serializer, never raw HTML, so there is
# no XSS surface for this rule to be finding here. Confirmed by
# manual review of every one of the 39 matches this rule produced
# locally before adding this exclusion (see this job's own top
# comment) — not a blanket "trust the tool less" suppression.
python-recommender:
# ml/hardware-recommender/ is a standalone project (its own venv, own
# train/download-dataset scripts) that isn't part of the app's build —
# only its exported ONNX model ships with the app
# (app/python/artifacts/hardware_recommender.onnx), loaded at runtime by
# app/python/recommender_worker.py. This job is deliberately scoped to
# what release publishing actually depends on: the recommender package's
# own unit tests (data/feature encoding logic, no dataset or GPU needed)
# and a smoke test that the packaged worker script can actually load the
# shipped artifact and answer a request — never train.py or
# download_dataset.py, which need a real (large) dataset pull.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.12"
cache: pip
cache-dependency-path: ml/hardware-recommender/requirements.txt
- name: Install dependencies (CPU-only torch for ML tests)
working-directory: ml/hardware-recommender
# torch's CPU wheels live on a separate index from the rest of
# PyPI. Install it separately to keep the training/test stack CPU-only
# and avoid a multi-gigabyte CUDA wheel download on a GPU-less runner;
# the packaged app uses ONNX Runtime for inference and does not ship
# torch in its managed runtime.
run: |
python -m pip install --upgrade pip
python -m pip install --only-binary=:all: --index-url https://download.pytorch.org/whl/cpu torch==2.13.0
# pip parses comments and blank lines correctly. Passing the file
# through shell command substitution turned the first comment into
# a literal `#` requirement and failed the job.
python -m pip install --only-binary=:all: -r requirements.txt
- name: Run hardware-recommender unit tests
working-directory: ml/hardware-recommender
run: python -m pytest tests/ -q
- name: Verify the packaged model artifacts exist
run: |
test -f app/python/artifacts/hardware_recommender.onnx
test -f app/python/artifacts/hardware_recommender.onnx.sha256
test -f app/python/artifacts/hardware_recommender.meta.json
- name: Smoke-test the packaged recommender worker against the shipped artifact
working-directory: app/python
run: |
printf '%s\n%s\n' \
'{"protocol":1,"id":"1","method":"health","params":{}}' \
'{"protocol":1,"id":"2","method":"recommend","params":{"model_params_b":7,"ram_gb":16,"vram_gb":8,"cpu_cores":8,"platform":"linux","gpu_backend":"cuda"}}' \
| python recommender_worker.py > worker-output.json
cat worker-output.json
test "$(grep -o '"ok": *true' worker-output.json | wc -l)" -eq 2