Repository navigation
1341 lines (1228 loc) · 71.5 KB
/
Copy pathci.yml
File metadata and controls
1341 lines (1228 loc) · 71.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
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
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
name: CI
on:
push:
branches:
- ci
- main
pull_request:
types: [opened, synchronize, reopened]
permissions:
contents: read
actions: read
jobs:
lint-and-build:
name: Lint, Typecheck and Build
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
# Node 24, the root's engines floor, for every Node tool below, and npm's
# cache for the VS Code extension's install before knip.
- name: Setup Node
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: "24"
cache: npm
cache-dependency-path: vscode-extension/package-lock.json
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: "1.4.4"
- name: Install dependencies
uses: ./.github/actions/bun-install
- name: Chart version sync guard (issue #138)
# Runs inside the required "Lint, Typecheck and Build" check on
# purpose: a separate job would be advisory-only without a branch
# protection change. actions/checkout is shallow and single-ref, so
# fetch main explicitly for the base comparison (appVersion changed
# => chart version must change too and must not be a released tag).
# Strict mode makes unavailable git state a failure in CI rather than
# a skipped check.
run: |
git fetch --no-tags --depth=1 origin +refs/heads/main:refs/remotes/origin/main
bun run chart:check
env:
CHART_SYNC_STRICT: "1"
- name: Login channel showcase drift guard (#425)
# Same reasoning as the chart guard above: inside the required check so
# it actually gates. src/lib/distribution/channels.generated.ts is the
# login hero's copy of distribution/channels.yaml, generated and
# committed so the unauthenticated page parses no yaml at runtime -
# nothing else notices when the inventory moves on without it.
run: bun run channels:showcase:check
- name: Localized README drift guard (#317)
# Same reasoning as the chart guard above: inside the required check so
# it actually gates. README_zh.md and README_ja.md restate README.md's
# engine table and install commands, and nothing else notices drift.
run: bun run readme:check
- name: Security posture drift guard (control 3.3)
# Same reasoning as the two guards above: inside the required check so it actually gates.
# docs/SECURITY.md claims which controls exist and names what verifies each one; this fails
# the build when a row points at a file that is missing or never runs, when a security test
# exists that no row accounts for, or when a control claims to be implemented with nothing
# verifying it.
run: bun run security:check
- name: Check formatting (Biome)
run: bun run format
- name: Run linters (oxlint + ESLint)
run: bun run lint
# The Windows launcher (issue #114) is dependency-free Go whose pure
# logic tests run on any OS. Gate it here, not only in the release
# workflow: windows-package is a hard release gate, so a launcher
# regression discovered first at tag time would block the release.
- name: Setup Go (Windows launcher toolchain)
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: packaging/windows/launcher/go.mod
# Dependency-free module (no go.sum) - caching would only warn.
cache: false
- name: Vet and test the Windows launcher (Go)
run: |
cd packaging/windows/launcher
gofmt -l . | tee /tmp/gofmt.out
test ! -s /tmp/gofmt.out
go vet ./...
go test ./...
- name: Run TypeScript type check
run: bun run typecheck
env:
# tsconfig.json takes every TypeScript file, tests included, and that
# program peaks within tens of MB of Node's default heap of about 4 GB
# on this runner, so every step that checks it, `next build` and the
# Playwright runs that build included, raises the heap, as the payload
# build does since #1535 (docs/TOOLCHAIN.md, tests/unit/ci-node-heap.test.ts).
NODE_OPTIONS: --max-old-space-size=6144
- name: Install the VS Code extension's dependencies for knip
# knip's vscode-extension workspace reads the extension's own manifest:
# without vscode-extension/node_modules it reports c8, mocha and
# vscode-test as unlisted binaries and c8 and @vscode/test-cli as unused.
run: npm ci --ignore-scripts
working-directory: vscode-extension
- name: Run knip (unused files, exports, dependencies)
run: bun run knip
- name: Build project
run: bun run build
env:
NODE_OPTIONS: --max-old-space-size=6144
NEXT_TELEMETRY_DISABLED: 1
JWT_SECRET: test-secret-for-ci-build-only-32ch
ADMIN_EMAIL: admin@libredb.org
ADMIN_PASSWORD: test-admin
USER_EMAIL: user@libredb.org
USER_PASSWORD: test-user
- name: Build library package (tsup)
run: bun run build:lib
- name: Check package types resolution (attw)
run: bun run attw
- name: Check package entry closures (lib:closure)
run: bun run lib:closure
test:
name: Unit & Integration Tests
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
- name: Setup Node
# The suite runs under Bun, but tests that spawn `bin/studio.js` get
# whatever `node` the image happens to ship - currently 22.x, below the
# launcher's own Node 24 floor, so the launcher refuses and the test
# dies on a bare exit 1. Pin the product's baseline instead of
# inheriting the runner's default (#709 was red on exactly this).
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: "24"
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: "1.4.4"
- name: Install dependencies
uses: ./.github/actions/bun-install
- name: Set up Helm
uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5.0.1
with:
version: v4.1.3
- name: Add Bitnami repo
run: helm repo add bitnami https://charts.bitnami.com/bitnami --timeout 5m
# --skip-refresh: the `helm repo add` above already fetched the index; Helm 4's `dependency build` re-fetches it by default and takes no --timeout.
- name: Build chart dependencies
run: helm dependency build charts/libredb-studio --skip-refresh
- name: Run the test suite with coverage
# tests/run-tests.ts runs every test file in its own bun process, several
# at a time, and merges the per-file lcov reports. It is the same command
# a contributor runs (`bun run test`) with coverage turned on, which is
# the point: the gate and the documented command cannot drift apart.
run: bun run test:coverage
env:
# Optional on a contributor's machine, mandatory here: this job installs Helm and
# builds the chart dependency, so a chart test that did not run is a defect, and the
# runner refuses the run instead of listing it as not run (tests/runner/requirements.ts).
LIBREDB_REQUIRE_HELM: "1"
JWT_SECRET: test-secret-for-ci-build-only-32ch
ADMIN_EMAIL: admin@libredb.org
ADMIN_PASSWORD: test-admin
USER_EMAIL: user@libredb.org
USER_PASSWORD: test-user
# The project holds 100% line coverage (#196). Fail fast with the exact
# uncovered file:line ranges instead of letting coverage erode silently.
- name: Enforce 100% line coverage
run: bun run coverage:check
# Production runs the Node release the Dockerfile's runner stage names, and the byte transport's refusal of a
# link-local DNS answer with DB_HTTP_BLOCK_PRIVATE_HOSTS off rests on that runtime's Agent using the transport's
# own lookup, so the transport's runtime cases run once more on exactly that release, and so do the RabbitMQ AMQP
# dial's and the MQTT dial's, whose egress rests on the same runtime's lookup handling, the S3 client wire cases,
# and the Snowflake key cases, whose missing and wrong passphrase codes come from that release's OpenSSL.
# The suite above runs every one of these files on Node 24, the engines floor, so they are not run on it again.
# The release is read from the Dockerfiles when the job runs, not written here: Dependabot moves the image's Node
# and never this file, and a literal here stayed at 26.10.0 through four image bumps from #1696 on.
# Dockerfile.alpine's runner stage must name the same release, or this step fails; Dockerfile.alpine-slim is not
# read, because its runner is plain Alpine with Alpine's own nodejs package and names no Node release.
# tests/unit/ci-image-node.test.ts runs this step's shell against the Dockerfiles.
- name: Read the Node release of the Dockerfile runner stages
id: image-node
shell: bash
run: |
runner_node() {
sed -n 's/^FROM node:\([^-[:space:]]*\)-[^[:space:]]* AS runner$/\1/p' "$1"
}
debian="$(runner_node Dockerfile)"
alpine="$(runner_node Dockerfile.alpine)"
if ! [[ "$debian" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error file=Dockerfile::The runner stage must be one FROM node:<major>.<minor>.<patch>-<variant> AS runner line; read '${debian//$'\n'/ }'."
exit 1
fi
if [ "$alpine" != "$debian" ]; then
echo "::error file=Dockerfile.alpine::The runner stage runs Node '${alpine//$'\n'/ }' and Dockerfile's runs $debian; the two images must ship one Node release."
exit 1
fi
echo "version=$debian" >> "$GITHUB_OUTPUT"
- name: Setup the image's Node for the runtime cases
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: ${{ steps.image-node.outputs.version }}
- name: Run the transport runtime cases, the S3 client wire cases, the RabbitMQ and MQTT dial cases and the Snowflake key cases on the image's Node
env:
IMAGE_NODE: ${{ steps.image-node.outputs.version }}
run: |
test "$(node --version)" = "v$IMAGE_NODE"
NODE_TRANSPORT_NODES="$(command -v node)" bun tests/run-tests.ts tests/unit/db/http/node-transport-runtimes.test.ts tests/unit/db/s3/client-wire.test.ts tests/unit/db/rabbitmq/dial-runtimes.test.ts tests/unit/db/mqtt/dial-runtimes.test.ts tests/unit/db/snowflake/key-runtimes.test.ts
- name: Upload coverage artifact
# always(): keep the lcov available for debugging when coverage:check
# fails for a non-obvious reason (e.g. malformed report, merge bug).
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: coverage
path: coverage/lcov.info
# Codecov publishes the same merged lcov for PR annotations and the README
# badge. It is NOT the coverage gate: `coverage:check` above is, and it runs
# first. Two consequences are deliberate here:
# - always(), so a run that trips the gate still ships the report that
# shows which lines regressed, matching the artifact step above.
# - fail_ci_if_error stays false. This job is a required check, and a
# Codecov outage - or a fork PR, which gets no secrets and so no token -
# must not be able to block a merge. Codecov's own PR statuses are
# informational for the same reason (see codecov.yml).
- name: Upload coverage to Codecov
if: always()
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage/lcov.info
# The merged lcov is the only report worth sending; without this the
# action walks the tree and uploads the per-file intermediates that
# `merge-lcov.mjs` already folded in.
disable_search: true
fail_ci_if_error: false
# The same suite, on the two platforms the project has never measured. Linux is
# covered by the `test` job above, which runs the identical runner with coverage.
#
# A SEPARATE job rather than a matrix on `test`: GitHub appends the matrix values
# to a matrix job's name, so matrixing `test` would rename the required check
# "Unit & Integration Tests" out of existence and every pull request would wait
# forever for a check that can no longer report. This job is deliberately not a
# required check yet; promote it once it has a history of being green.
#
# It uses no secrets, so it runs on fork pull requests like any other job.
test-cross-platform:
name: Cross-platform Tests (${{ matrix.os }})
runs-on: ${{ matrix.os }}
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
os: [windows-latest, macos-latest]
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
- name: Setup Node
# Both images ship Node 22 by default, below the product's floor, and the
# tests that spawn bin/studio.js get whatever `node` is on PATH (#709).
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: "24"
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: "1.4.4"
- name: Install dependencies
uses: ./.github/actions/bun-install
- name: Set up Helm
uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5.0.1
with:
version: v4.1.3
- name: Add Bitnami repo
run: helm repo add bitnami https://charts.bitnami.com/bitnami --timeout 5m
- name: Build chart dependencies
run: helm dependency build charts/libredb-studio --skip-refresh
- name: Report the toolchain
# A red leg on a platform nobody here runs is only actionable if the run
# says what it had: a missing 7z or an older bash changes what "red" means.
shell: bash
run: |
echo "platform: $(uname -s 2>/dev/null || echo windows) $(uname -m 2>/dev/null || echo unknown)"
echo "bun: $(bun --version)"
echo "node: $(node --version)"
echo "bash: ${BASH_VERSION:-unknown}"
echo "helm: $(helm version --short)"
for tool in sh bash tar unzip 7z git; do
printf '%-9s %s\n' "$tool:" "$(command -v "$tool" || echo "not on PATH")"
done
- name: Run the test suite
# Deliberately NOT `shell: bash`. On Windows that would be Git Bash, whose
# PATH carries the whole unix toolset, and the contributor this job stands
# in for runs the command from PowerShell. Running it the way they do is
# what makes this job evidence.
run: bun run test
env:
# Optional on a contributor's machine, mandatory here: this job installs Helm and
# builds the chart dependency, so a chart test that did not run is a defect, and the
# runner refuses the run instead of listing it as not run (tests/runner/requirements.ts).
LIBREDB_REQUIRE_HELM: "1"
JWT_SECRET: test-secret-for-ci-build-only-32ch
ADMIN_EMAIL: admin@libredb.org
ADMIN_PASSWORD: test-admin
USER_EMAIL: user@libredb.org
USER_PASSWORD: test-user
# The VS Code extension's gates: typecheck, build, the unit leg with its own
# 100% line coverage gate, both integration runtimes, the e2e leg, the VSIX,
# and a macOS and Windows smoke subset. The steps live in the reusable
# workflow, which vscode-extension-release.yml calls too on a
# vscode-extension-* tag, so the gates cannot drift.
# Not a required check until the owner adds it to main's branch protection.
vscode-extension:
name: VS Code Extension
uses: ./.github/workflows/vscode-extension.yml
e2e:
name: E2E Tests
needs: [lint-and-build]
runs-on: ubuntu-latest
timeout-minutes: 30
# Runs INSIDE Playwright's own image so `playwright install` never touches
# apt. `--with-deps` used to pull ~111 MB of system libraries from
# azure.archive.ubuntu.com on every run; when that mirror degrades - as it
# did on 2026-08-18/19, down to 21.8 kB/s - the step took 86 minutes and
# twice ran into the 6-hour job default, against 12 seconds of actual
# browser download and 3 minutes of actual tests. The image ships those
# libraries prebuilt, so the apt path is removed rather than retried.
# The tag MUST track @playwright/test in package.json: a mismatch only
# costs a re-download into PLAYWRIGHT_BROWSERS_PATH (the install step below
# is the fail-safe), never a broken job, which is why it went unnoticed;
# tests/unit/ci-playwright-image.test.ts now fails on it.
container:
image: mcr.microsoft.com/playwright:v1.64.1-noble
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
# oven-sh/setup-bun is not usable inside the Playwright image: it
# extracts bun from a .zip via @actions/tool-cache, which shells out to
# `unzip` - a binary this image does not ship, and installing it would
# put apt back on the critical path, which is the whole point of moving
# here. npm resolves bun through platform tarballs instead, so it needs
# no unzip. Keep the version in step with the setup-bun pins elsewhere.
- name: Setup Bun
run: npm install -g bun@1.4.4
- name: Install dependencies
uses: ./.github/actions/bun-install
- name: Install Playwright browsers
# webkit is required alongside chromium: playwright.config.ts's
# webkit-security project (scoped to e2e/security-headers.spec.ts)
# runs under this job's unfiltered `playwright test` invocation. The
# other Playwright-installing workflows (docker-build-push.yml,
# release-artifacts.yml, and the channel-e2e job below) all drive
# scripts/channel-embedded-sample-e2e.sh, which pins
# --config=playwright.channel.config.ts - a chromium-only project
# list that never resolves the webkit-security project - so they stay
# chromium-only on purpose.
# No --with-deps: the container image already carries the system
# libraries. This step is now only a fail-safe for a tag/package
# version drift, and is a no-op when they agree.
run: bunx playwright install chromium webkit
- name: Run E2E tests
# --grep-invert: e2e/functional-smoke.spec.ts needs a Docker daemon,
# which a container job does not have. It has its own job below. Being
# explicit matters: the spec's own `test.skip(!dockerAvailable())` would
# let it vanish silently here (it did - PR #435 turned 0 skipped into 1
# skipped with CI still green), and a test that disappears without
# saying so is worse than one that is openly not run.
run: bunx playwright test --grep-invert "Functional smoke"
env:
NODE_OPTIONS: --max-old-space-size=6144
JWT_SECRET: test-secret-for-ci-build-only-32ch
ADMIN_EMAIL: admin@libredb.org
ADMIN_PASSWORD: test-admin
USER_EMAIL: user@libredb.org
USER_PASSWORD: test-user
- name: Run subpath deployment E2E
# Separate build, after the root-path servers have stopped.
run: bun run test:e2e:base-path
env:
NODE_OPTIONS: --max-old-space-size=6144
- name: Upload Playwright report
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
if: always()
with:
name: playwright-report
path: playwright-report/
# The product's reason to exist, end to end: boot the app, create a real
# PostgreSQL connection through the modal, run SQL, see rows. Split out of the
# e2e job because the spec starts its own throwaway postgres with
# `docker run -p 127.0.0.1::5432` - that binds the HOST loopback, and a
# container job has its own network namespace and cannot reach it. Keeping
# this on the bare runner is what lets the spec stay free of any CI-only
# branch, which is the whole point of a test that claims to exercise the real
# path.
functional-smoke:
name: Functional Smoke (PostgreSQL, MongoDB, RabbitMQ)
needs: [lint-and-build]
runs-on: ubuntu-latest
# Raised from 30 with the six provider live checks below. THIS NUMBER IS A BUDGET AND
# NOT A MEASUREMENT, and saying so is the point: the six steps were never run in CI, so
# nobody has timed them here. What is known is what they do - four pull a pinned image,
# `snmp` and `netconf` build one, and the readiness loops alone allow 90, 60, 60, 60, 60
# and 180 seconds before any check runs. Re-measure this from the first green run and
# bring the number down to what the job actually takes; a cap nobody has measured is a
# cap that will one day cut a run in half and read like a defect.
timeout-minutes: 60
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: "1.4.4"
- name: Install dependencies
uses: ./.github/actions/bun-install
- name: Passkey storage contract on PostgreSQL
# The same contract cases tests/unit/lib/storage/providers/sqlite-passkeys.test.ts runs on
# SQLite in `bun run test`, against a real PostgreSQL: its casts, row locks and ON CONFLICT
# behaviour under concurrency are what the multi-replica guarantees rest on, and a mocked
# pg cannot show them. Superuser, because one case deletes an account with foreign keys off.
# The host port is Docker's pick and the name is per run: a fixed loopback port inside the ephemeral range
# (32768-60999) can be held by a client socket, open or in TIME_WAIT, and Docker then refuses the bind
# with "address already in use", as 54332 did for the check below on PR #2216.
run: |
pg="passkey-contract-pg-$(openssl rand -hex 4)"
trap 'docker rm -f "$pg" > /dev/null' EXIT
docker run -d --rm --name "$pg" -e POSTGRES_PASSWORD=contract -p 127.0.0.1::5432 postgres:17
port="$(docker port "$pg" 5432/tcp | head -n 1)"
port="${port##*:}"
export LIBREDB_LIVE_POSTGRES_URL="postgresql://postgres:contract@127.0.0.1:${port}/postgres?sslmode=disable"
# The image's first start runs a temporary server on its Unix socket only, stops it and starts the real
# one, so a psql over the socket can answer from the temporary server and the contract's TCP connection
# then lands in the restart ("Connection terminated unexpectedly"). Wait on TCP, where only the real
# server listens, and stop here if it never does.
ready=""
for i in $(seq 1 60); do
docker exec "$pg" pg_isready -q -h 127.0.0.1 -p 5432 -U postgres && { ready=yes; break; }
sleep 1
done
if [ -z "$ready" ]; then
echo "PostgreSQL accepted no TCP connection within 60 s"
docker logs "$pg"
exit 1
fi
bun tests/live/passkey-store-postgres.ts
# STORAGE_POSTGRES_SCHEMA on the same server: a throwaway database and a role that owns only schema
# studio and has no CREATE on public, so one unqualified table, foreign key or index fails the step.
bun tests/live/storage-schema-postgres.ts
# Governance on the same server: a throwaway database holding a governance source built from the published
# manifest, read by the real snapshot loader as libredb_studio. Any failed case exits non-zero and fails the
# step; later governance slices add their cases to the same script.
bun tests/live/governance-live-check.ts
- name: PostgreSQL agent read-only live check
# The agent's PostgreSQL read-only profile against a real server, through the production factory
# and the resolved agent role: a write and a multi-command string must both be refused. The
# integration suite proves the same rules against a mock, which cannot see a pg driver or server
# change. The script starts and removes its own container, on a host port Docker picks.
run: bun tests/live/postgres-read-only-live.ts
- name: Install Playwright browsers
# chromium only and no --with-deps: ubuntu-latest ships Google Chrome,
# so chromium's shared libraries are already present, and this job runs
# no webkit - webkit is the browser that needs the extra packages whose
# apt download made these jobs take 86 minutes (PR #435).
run: bunx playwright install chromium
- name: Run functional smoke
run: bunx playwright test --project=chromium --grep "Functional smoke"
env:
NODE_OPTIONS: --max-old-space-size=6144
JWT_SECRET: test-secret-for-ci-build-only-32ch
ADMIN_EMAIL: admin@libredb.org
ADMIN_PASSWORD: test-admin
USER_EMAIL: user@libredb.org
USER_PASSWORD: test-user
# The RabbitMQ provider's read-only check (#1656): every provider surface against a fresh 4.3.6 broker, the
# broker's own take counters compared before and after, and one control take that must move them. It runs after
# the functional smoke on purpose: the steps run in order with no `if:`, so a red oracle cannot hide the
# PostgreSQL result. The broker serves this run alone, so the broker-wide counters are compared too. The image
# reference is database-compose.yml's `rabbitmq` service's, and tests/unit/db/rabbitmq/live-environment.test.ts
# holds the two equal. The passwords are made for this run and never leave it. The wait asks the management API
# before it runs the CLI: the image's HOME is /var/lib/rabbitmq, so a `docker exec` CLI that ran before the
# server would write the Erlang cookie there as root, and the server, running as rabbitmq, could not read it.
- name: RabbitMQ read-only check
run: |
image="rabbitmq:4.3.6-management@sha256:8dd6e3570ddaa2ef82a6c3a8950e79c1f73893adfbdddbc374fc1f1ac8a0f5dd"
export RABBITMQ_LIVE_DIGEST="${image#*@}"
export RABBITMQ_LIVE_ADMIN_PASSWORD="$(openssl rand -hex 16)"
export RABBITMQ_LIVE_FIXTURE_PASSWORD="$(openssl rand -hex 16)"
trap 'docker rm -f libredb-ci-rabbitmq > /dev/null' EXIT
docker run -d --name libredb-ci-rabbitmq --ulimit nofile=65536:65536 \
-e RABBITMQ_DEFAULT_USER=libredb-admin -e RABBITMQ_DEFAULT_PASS="$RABBITMQ_LIVE_ADMIN_PASSWORD" \
-v "$PWD/docker/rabbitmq/enabled_plugins:/etc/rabbitmq/enabled_plugins:ro" \
-p 127.0.0.1:5672:5672 -p 127.0.0.1:15672:15672 -p 127.0.0.1:15692:15692 "$image"
ready=""
end=$((SECONDS + 60))
while [ "$SECONDS" -lt "$end" ]; do
if curl -fsS -o /dev/null -u "libredb-admin:$RABBITMQ_LIVE_ADMIN_PASSWORD" http://127.0.0.1:15672/api/overview \
&& docker exec libredb-ci-rabbitmq rabbitmq-diagnostics -q check_port_connectivity > /dev/null 2>&1; then
ready=yes
break
fi
sleep 1
done
if [ -z "$ready" ]; then
echo "The RabbitMQ broker was not ready within 60 s."
docker logs --tail 50 libredb-ci-rabbitmq
exit 1
fi
bun tests/live/rabbitmq-seed.ts
bun tests/live/rabbitmq-read-only.ts --dedicated
- name: MongoDB agent read-only live check
# The agent's MongoDB read-only profile against real servers, through the production factory (#1614): the
# read user admitted, and root, a matrix of broader roles, an anonymous connection and a server without
# authorization refused at open with nothing left behind; each write the editor's envelope can send, and the
# escapes the check names, refused before they are sent, and the writes refused by the server around that
# fence; each slow read ended at its deadline, by the server and by the provider's own kill; a read past the
# byte budget refused within a fixed memory ceiling. The provider suite proves the same rules against a
# stand-in driver, which cannot see a driver or server change. The script starts and removes its own
# containers. It runs after the other checks for the RabbitMQ step's reason: the steps run in order with no
# `if:`, so a red check here cannot hide the results above it. The image is database-compose.yml's `mongodb`
# service's and the script's own default; tests/unit/db/mongodb/live-environment.test.ts holds the three
# equal.
run: bun tests/live/mongodb-agent-profile.ts
env:
LIBREDB_LIVE_MONGO_IMAGE: mongo:9.0.4@sha256:f90507905ce6e52e4a63d0ff8964fc254fa4d37ccbe56c56f4bc0186eb02edea
# THE SIX PROVIDER LIVE CHECKS BELOW WERE WRITTEN AND NEVER RUN. Each of the six
# industrial and network providers shipped with a live check and with its CI step
# already written, in `docker/<engine>/ci-step.yml`, and every one of those files
# said the same thing: the block is delivered on its own because five engines were
# landing in one round and five sessions editing this file would be a rerun of the
# merge the repository had just finished, so "the coordinating session pastes the
# block below into the job that runs the other live checks". That paste never
# happened. MEASURED 19 October 2026: `tests/live/` carried the checks, the fixtures
# and their pins were in place, and none of the nineteen workflow files mentioned
# opcua, mtconnect, modbus, sparkplug, snmp or netconf. The scripts therefore ran
# only by hand. (An earlier draft of this comment added that `docs/providers/*.md`
# claimed a CI check CI never asked for. MEASURED: five of those six documents never
# mention CI at all, so the claim was removed rather than kept because it read well.)
#
# They are pasted here, in this job, after the checks above, for the reason the
# RabbitMQ and MongoDB steps give: the steps run in order with no `if:`, so a red
# check here cannot hide a result above it.
#
# THE ORDER IS CHEAPEST FIRST, and that is a choice about how fast a red arrives
# rather than about importance: the first four pull a digest-pinned image, while
# `snmp` and `netconf` build theirs (Netopeer2 from four verified tarballs), so a
# defect the cheap fixtures can see is reported without waiting for two builds.
- name: OPC UA read-only check
run: |
set -euo pipefail
image="mcr.microsoft.com/iotedge/opc-plc:2.15.10@sha256:28bbdaa2e8781fe3b8bda1bbd2d4ebe0ad5bcb1fbb83f69c74b8a92efa7fe75c"
# The digest reaches the suite, so the image this step runs and the image the fixture documents
# cannot drift apart: `tests/unit/db/opcua/live-environment.test.ts` compares them.
export OPCUA_LIVE_DIGEST="${image#*@}"
trap 'docker rm -f libredb-ci-opcua > /dev/null' EXIT
# Loopback only. Without the host address Docker publishes on 0.0.0.0, and this server is
# started --autoaccept --unsecuretransport: it would accept any client on every interface of
# the runner. The host port is Docker's pick: a fixed one inside the ephemeral range
# (32768-60999) can be held by a client socket, open or in TIME_WAIT, and
# Docker then refuses the bind with "address already in use".
docker run -d --name libredb-ci-opcua -p 127.0.0.1::50000 "$image" \
--pn=50000 --autoaccept --unsecuretransport --ut --sph --hn --slownodes=3 --fastnodes=3
port="$(docker port libredb-ci-opcua 50000/tcp | head -n 1)"
export LIBREDB_OPCUA_LIVE_URL="opc.tcp://127.0.0.1:${port##*:}/"
# 1. The server's own statement. opc-plc prints this once the simulation is up.
logged=""
end=$((SECONDS + 90))
while [ "$SECONDS" -lt "$end" ]; do
if docker logs libredb-ci-opcua 2>&1 | grep -q "PLC simulation started"; then
logged=yes
break
fi
sleep 1
done
if [ -z "$logged" ]; then
echo "NOT MEASURED: the OPC UA fixture never printed its start line within 90 s, so nothing below ran."
docker logs --tail 50 libredb-ci-opcua
exit 1
fi
# 2. The protocol. GetEndpoints on an unsecured channel, before any session exists: it proves a
# reader can reach the server, which the log line alone does not.
answered=""
end=$((SECONDS + 60))
while [ "$SECONDS" -lt "$end" ]; do
if bun tests/live/opcua-endpoints-probe.ts "$LIBREDB_OPCUA_LIVE_URL" | grep -q "GETENDPOINTS_OK"; then
answered=yes
break
fi
sleep 2
done
if [ -z "$answered" ]; then
echo "NOT MEASURED: the fixture started but answered no GetEndpoints within 60 s, so the live check never ran."
docker logs --tail 50 libredb-ci-opcua
exit 1
fi
# Two runners. The provider reaches the network through `node-opcua-client`, and Bun and Node
# do not resolve or socket alike; a check that ran on one of them would be a claim about one.
bun tests/live/opcua-live-check.ts
# THE EXTERNALS ARE NOT A PRECAUTION, they are what the first CI run measured. Without
# them this line exits 1 with `Could not resolve` for `proper-lockfile`, for seven
# `@duckdb/node-bindings-*/duckdb.node` paths and for `../build/Release/cpufeatures.node`
# (run 39294474038, reproduced locally on bun 1.4.2). The reason is the one the Sparkplug
# step's file already names: this check reaches `createDatabaseProvider`, the factory
# imports every provider, and DuckDB's bindings for the other six platforms cannot be
# resolved on this one. Node loads what it needs at run time from node_modules for the
# platform it is on, and `cpu-features` is loaded by ssh2 inside a try/catch.
# MEASURED: the five other provider bundles in this job build with no external at all,
# so the flags are on the one line that needs them rather than on all six.
# `--outdir` AND NOT `--outfile`, and that is the second thing CI measured rather than a
# preference. With `--outfile` this line failed on the runner with
# `error: cannot write multiple output files without an output directory` (run
# 39297290735), while the same command succeeded locally on darwin-arm64 - so the
# number of outputs bun decides to emit for these externals is not the same on both
# platforms, and `--outdir` is the form that holds whatever it emits. The glob is
# needed too: `--external @duckdb/node-bindings-linux-x64` does NOT cover the import,
# which is the subpath `@duckdb/node-bindings-linux-x64/duckdb.node`. MEASURED: with the
# eight package names spelled out and no glob, the build still exits 1 on all eight.
out="$(mktemp -d)"
bun build --target=node --outdir "$out" tests/live/opcua-live-check.ts \
--external proper-lockfile --external "@duckdb/*" --external cpu-features
# `--require reflect-metadata` IS MEASURED, not defensive. node-opcua reaches the
# network through tsyringe, which refuses to load without the polyfill: the bundle
# threw `tsyringe requires a reflect polyfill` on Node and exited 1 (run 39299407452).
# Bun needs nothing, which is why the Bun run above has always passed and this line is
# the one that fell over. Reproduced and fixed under the runner's own Node v24.21.0:
# plain `node` throws, `node --require reflect-metadata` gets past module load and
# fails only on ECONNREFUSED where no fixture is listening, which is the right failure
# for a machine with no server. MEASURED the same way: the other five provider bundles
# in this job load under Node with no polyfill, because only this check reaches
# `createDatabaseProvider`. `reflect-metadata` is a transitive dependency here
# (tsyringe <- node-opcua); if a lockfile update ever drops it, this line is where
# that shows up.
node --require reflect-metadata "$out/opcua-live-check.js"
- name: MTConnect provider check
run: |
agent="mtconnect/agent:2.8.0.4@sha256:c42f90850ab70d65ae5e73b14392b3041ed51065bd7bf287658db6f7260df641"
adapter="python:3.14-slim@sha256:0741d101873c12ab927e6f8653feb8862b9bd58771177acb1b885b95141f91b4"
trap 'docker rm -f libredb-ci-mtconnect libredb-ci-mtconnect-adapter > /dev/null 2>&1;
docker network rm libredb-ci-mtconnect > /dev/null 2>&1' EXIT
docker network create libredb-ci-mtconnect > /dev/null
# The alias is the name the agent's own configuration dials, so docker/mtconnect-init/agent.cfg
# is the same file here and under compose.
docker run -d --name libredb-ci-mtconnect-adapter --network libredb-ci-mtconnect \
--network-alias mtconnect-adapter \
-v "$PWD/docker/mtconnect-init:/fixture:ro" "$adapter" python /fixture/adapter.py
docker run -d --name libredb-ci-mtconnect --network libredb-ci-mtconnect \
-v "$PWD/docker/mtconnect-init:/mtconnect/config:ro" \
-p 127.0.0.1:17322:5000 "$agent"
# TWO QUESTIONS, TWO SENTENCES. A readiness check that reports one failure for two different
# states tells whoever reads the log the wrong thing half the time. "The agent never answered"
# and "the agent answered but the device behind it never arrived" are different repairs, so the
# loop records which of the two it last saw. Neither question asks whether a port is open: a
# published port accepts a connection before the server inside has spoken, so "open" measures
# Docker rather than the fixture.
ready=""
answered=""
end=$((SECONDS + 90))
while [ "$SECONDS" -lt "$end" ]; do
current="$(docker exec libredb-ci-mtconnect sh -c \
'wget -qO- http://127.0.0.1:5000/current' 2> /dev/null || true)"
if [ -n "$current" ]; then
answered=yes
case "$current" in
*ConnectionStatus*ESTABLISHED*) ready=yes; break ;;
esac
fi
sleep 2
done
if [ -z "$ready" ]; then
if [ -z "$answered" ]; then
echo "MEASURED NOTHING: the MTConnect agent never answered /current within 90 s."
echo "The fixture did not come up; the checks below would have measured nothing."
else
echo "MEASURED NOTHING: the agent answered, but ConnectionStatus never reached ESTABLISHED"
echo "within 90 s. The agent is up and the device behind it is not, so the fixture is half"
echo "built: every data item would read UNAVAILABLE and every check would pass on nothing."
fi
echo "--- agent ---"
docker logs --tail 50 libredb-ci-mtconnect
echo "--- adapter ---"
docker logs --tail 50 libredb-ci-mtconnect-adapter
exit 1
fi
# The live check first: it is the only layer that can see a window read at connect going stale,
# and it prints its own count rather than a word, so a check that was deleted or could not run
# does not leave the same line behind as a run where every check passed.
# MTCONNECT_LIVE_REQUIRED turns a skip into a failure here. MEASURED while writing this file:
# with the published port set to a wrong one, every check skipped, the integration test still
# passed from its captures, and the step exited 0 - a step that reached no agent reported
# success. Without this flag the canary below is green, which is how it was found.
MTCONNECT_LIVE_REQUIRED=1 bun tests/live/mtconnect-live-check.ts
# And the same check under Node, which is what the contract asks for and what this file's own
# docblock describes: the provider's transport is the repository's own and both runtimes drive
# it, so a difference between them is a defect the Bun run alone cannot see. The Node run goes
# through `bun build --target=node` rather than Node's own TypeScript support, as
# `tests/live/oxia-live-check.ts` does, so the `@/` alias is resolved while bundling.
OUT="$(mktemp -d)"
bun build tests/live/mtconnect-live-check.ts --target=node --outfile "$OUT/live.mjs"
MTCONNECT_LIVE_REQUIRED=1 node "$OUT/live.mjs"
rm -rf "$OUT"
# Then the same captures the integration test replays, read against the agent that produced
# them: a capture taken from another build fails the digest case rather than drifting quietly.
bun tests/run-tests.ts tests/integration/db/mtconnect-provider.test.ts
- name: Modbus live check
run: |
image="oitc/modbus-server:2.3.0@sha256:32bfe144512e1f070cf7e7e2513cdcb774d74db142ee5dbad45b21d6d705ab97"
export MODBUS_LIVE_DIGEST="${image#*@}"
trap 'docker rm -f libredb-ci-modbus > /dev/null' EXIT
docker run -d --name libredb-ci-modbus \
-v "$PWD/docker/modbus/modbus_server.json:/app/modbus_server.json:ro" \
-p 127.0.0.1:1502:5020 "$image"
# The address the probes use AND the sentences name, from one place: the sentences said
# "on 1502" while the probe read MODBUS_PROBE, so a run against another port was
# refused with a sentence naming a port it had never asked.
export MODBUS_PROBE="${MODBUS_PROBE:-127.0.0.1:1502}"
ready=""
end=$((SECONDS + 60))
while [ "$SECONDS" -lt "$end" ]; do
# Two questions, and the second plainly implies the first. They are asked separately
# so the two failures can be told apart below, not for coverage.
if bun tests/live/modbus-ready-probe.ts --speaks && bun tests/live/modbus-ready-probe.ts --tables; then
ready=yes
break
fi
sleep 1
done
if [ -z "$ready" ]; then
# Which of the two it was, in its own sentence. Both are NOT MEASURED, because in
# neither case did anything about this repository's code get measured, but a reader
# who has to fix one of them needs to know which one.
if bun tests/live/modbus-ready-probe.ts --speaks; then
echo "NOT MEASURED: a Modbus server answered on $MODBUS_PROBE but its tables are not this fixture's, so the live check never ran."
else
echo "NOT MEASURED: nothing speaking Modbus answered on $MODBUS_PROBE within 60 s, so the live check never ran."
fi
docker logs --tail 50 libredb-ci-modbus
exit 1
fi
bun tests/live/modbus-live-check.ts
out="$(mktemp -d)"
bun build tests/live/modbus-live-check.ts --target=node --outfile "$out/live.mjs"
node "$out/live.mjs"
- name: Sparkplug B live check
run: |
image="eclipse-mosquitto:2.0.22@sha256:199ea8ef2e35ec2b1b37e59cfd1dbae538ed4dfa4a2251a121a52215a6248a21"
export SPARKPLUG_LIVE_DIGEST="${image#*@}"
trap 'docker rm -f libredb-ci-sparkplug libredb-ci-sparkplug-publisher > /dev/null 2>&1' EXIT
docker run -d --name libredb-ci-sparkplug \
-v "$PWD/docker/sparkplug/mosquitto.conf:/mosquitto/config/mosquitto.conf:ro" \
-p 127.0.0.1:1884:1883 "$image"
# First readiness check: the broker answers about itself.
listening=""
end=$((SECONDS + 60))
while [ "$SECONDS" -lt "$end" ]; do
if docker exec libredb-ci-sparkplug \
mosquitto_sub -h 127.0.0.1 -p 1883 -t '$SYS/broker/version' -C 1 -W 3 > /dev/null 2>&1; then
listening=yes
break
fi
sleep 1
done
if [ -z "$listening" ]; then
echo "The broker did not accept a session within 60 s."
docker logs --tail 50 libredb-ci-sparkplug
exit 1
fi
docker run -d --name libredb-ci-sparkplug-publisher --network host \
-v "$PWD/docker/sparkplug:/publisher:ro" \
python:3.14-slim@sha256:0741d101873c12ab927e6f8653feb8862b9bd58771177acb1b885b95141f91b4 \
python3 /publisher/publisher.py --host 127.0.0.1 --port 1884
# Second readiness check, independent of the first: a Sparkplug message actually arrives.
# The broker answered the first one before the publisher existed, which is the state this
# check exists to refuse.
publishing=""
end=$((SECONDS + 60))
while [ "$SECONDS" -lt "$end" ]; do
if docker exec libredb-ci-sparkplug \
mosquitto_sub -h 127.0.0.1 -p 1883 -t 'spBv1.0/#' -C 1 -W 5 > /dev/null 2>&1; then
publishing=yes
break
fi
sleep 1
done
if [ -z "$publishing" ]; then
echo "No Sparkplug message arrived within 60 s, so the fixture is a broker with nothing on it."
docker logs --tail 50 libredb-ci-sparkplug-publisher
exit 1
fi
# --strict, because a SKIP keeps the exit code at 0: if the digest export above were ever
# dropped, the digest item would skip, this step would stay green and nothing would have
# checked which broker the run measured. In CI an item that cannot run is a failure.
bun tests/live/sparkplug-live-check.ts --strict
bun build tests/live/sparkplug-live-check.ts --target=node --outfile "$RUNNER_TEMP/sparkplug-live.mjs"
node "$RUNNER_TEMP/sparkplug-live.mjs" --strict
- name: SNMP live check
run: |
base="sha256:a29215f6a35e51e22adffa17f89e9d2ef06214e64a2bad10d765c46aea49f11f"
# This value was EXPORTED as SNMP_LIVE_DIGEST and read by nobody: the live check speaks SNMP
# to an agent, and an agent does not report the image it was built from. An export nothing
# reads is worse than no export, because the next reader takes it for a check that runs. So
# the digest does its work here instead, before the build, and the unit test still holds this
# file equal to the Dockerfile.
if ! grep -q "$base" docker/snmp/Dockerfile; then
echo "NOT MEASURED: docker/snmp/Dockerfile does not carry $base, so this run would build another base."
exit 1
fi
# The trap, not a last line: a `docker rm` written at the end does not run when a check above
# it fails, which is exactly the run that leaves a container behind.
trap 'docker rm -f libredb-ci-snmp > /dev/null 2>&1' EXIT
docker build -t libredb-ci-snmp:test docker/snmp
docker run -d --name libredb-ci-snmp \
--health-cmd 'snmpget -v2c -c libredb-live -t 2 -r 1 127.0.0.1:161 1.3.6.1.2.1.1.5.0' \
--health-interval 3s --health-retries 10 --health-start-period 3s \
-p 127.0.0.1:1161:161/udp libredb-ci-snmp:test
# TWO READINESS CHECKS, independent on purpose, and the SECOND one crosses the published port.
# The first is Docker's own health verdict, which asks the agent from INSIDE the container. On
# its own it is not enough, and that was measured rather than reasoned: a canary that published
# the fixture on the wrong host port left the inside-the-container check perfectly happy, the
# step walked past readiness, and the live check then failed with "nothing is listening" - a
# red that reads like a provider defect for a fixture nobody could reach. So the second check
# asks the same question from the HOST side, over the mapping the live check will use, with the
# image's own snmpget (the host has no SNMP tools and should not need any).
# A UDP port that is merely open proves nothing either way: snmpd binds before it reads its
# configuration, so a port probe reports a half-configured fixture as healthy.
# THE TWO CHECKS ALSO ASK TWO DIFFERENT FACTS, and that is the second half of the same point.
# They both used to ask for sysName, which snmpd.conf sets, while every value the live check
# reads comes from the `pass` script in playpen.sh. MEASURED by breaking that script: sysName
# still answered on both paths and the playpen answered "No Such Instance", so both checks
# passed over a fixture that carries none of the data the live check is about. The host-side
# check therefore asks the playpen for a value only the script can produce; the health command
# inside the container keeps asking sysName, so the two differ in path AND in fact.
ready=""
end=$((SECONDS + 60))
while [ "$SECONDS" -lt "$end" ]; do
health="$(docker inspect -f '{{.State.Health.Status}}' libredb-ci-snmp 2>/dev/null || true)"
if [ "$health" = "healthy" ] \
&& docker run --rm --network host libredb-ci-snmp:test \
snmpget -v2c -c libredb-live -t 2 -r 1 127.0.0.1:1161 .1.3.6.1.4.1.8072.9999.9999.4.0 2>/dev/null \
| grep -q "live-test-value"; then
ready=yes
break
fi
sleep 1
done
if [ -z "$ready" ]; then
# NOT "the live check failed". Nothing was measured: the fixture never answered, so this run
# says nothing about the provider, and the sentence has to say which of the two it is.
echo "NOT MEASURED: the SNMP fixture did not answer within 60 s, so the live check never ran."
docker logs --tail 50 libredb-ci-snmp
exit 1
fi
# Both runtimes, because the provider's socket is node:dgram and the two runtimes implement it
# separately. The Node run goes through a bundle for the reason written in the check's header.
bun tests/live/snmp-live-check.ts
out="$(mktemp -d)"
bun build tests/live/snmp-live-check.ts --target=node --outfile "$out/live.mjs"
node "$out/live.mjs"
- name: NETCONF live check
run: |
# The port the probes use AND the sentences name, from one place. MEASURED elsewhere in
# this repository, on the Modbus fixture's step: that step was
# measured saying "on 1502" while its probe read another variable, so a run against a
# different port was refused with a sentence naming a port it had never asked.
export NETCONF_PROBE_PORT="${NETCONF_PROBE_PORT:-1830}"
export NETCONF_PROBE="${NETCONF_PROBE:-127.0.0.1:$NETCONF_PROBE_PORT}"
trap 'docker rm -f libredb-ci-netconf > /dev/null 2>&1' EXIT
# Built from the repository, so the tag below names this build and never a registry's.
docker build -t libredb-fixture/netconf:2.8.7 docker/netconf
docker run -d --name libredb-ci-netconf \
-e LIBREDB_NETCONF_INTERFACES=5000 \
-e LIBREDB_NETCONF_RO_PASSWORD=fixture-password \
-e LIBREDB_NETCONF_ROOT_PASSWORD=fixture-root \
-e LIBREDB_NETCONF_KEY_PASSPHRASE=fixture-passphrase \
-p "127.0.0.1:$NETCONF_PROBE_PORT:830" libredb-fixture/netconf:2.8.7
# The first question is asked inside the container, where 127.0.0.1 is the server itself
# and the answer cannot be Docker's. The second is asked from the HOST, over the published
# port, and that split is the point: a readiness check that only ever looks inside never
# tests the mapping, so a wrong `-p` passes it and every check afterwards fails with
# ECONNREFUSED, which reads as a provider defect rather than as an unreachable fixture.
# Asking from the host raises the question of whose banner answers. It can only be this
# container's: `docker run` above exits 125 when the published port is already taken, and
# the step stops there, so a foreign server never reaches this loop. MEASURED: starting a
# second fixture on a port another container held exited 125 with "failed to set up
# container networking".
configured=""
serving=""
end=$((SECONDS + 180))
while [ "$SECONDS" -lt "$end" ]; do
if docker exec libredb-ci-netconf test -f /fixture/.ready 2> /dev/null; then
configured=yes
if ssh-keyscan -T 3 -p "$NETCONF_PROBE_PORT" 127.0.0.1 2> /dev/null | grep -q 'SSH-2.0'; then
serving=yes
break
fi
fi
sleep 2
done
if [ -z "$serving" ]; then
# Both are NOT MEASURED, because in neither case did anything about this repository's
# code get measured. A reader who has to fix one of them needs to know which one.
if [ -n "$configured" ]; then
echo "NOT MEASURED: the NETCONF fixture finished configuring itself but never answered"
echo "on $NETCONF_PROBE within 180 s. Either the server is not speaking or the published"
echo "port does not reach it; this question is asked from the host for that reason. The"
echo "live check never ran."
else
echo "NOT MEASURED: the NETCONF fixture never wrote /fixture/.ready within 180 s, so it"
echo "never finished loading its accounts, its NACM rule-list and its interface list."
echo "The live check never ran, and a server reached before this point would have"
echo "refused every read."
fi
docker logs --tail 80 libredb-ci-netconf
exit 1
fi
# The deliverable has to be here to be measured. Without this the step would report
# success on a missing file: `bun` would fail with a module error, which reads as a broken
# step rather than as a deliverable that never landed.
test -f tests/live/netconf-live-check.ts || {
echo "NOT MEASURED: tests/live/netconf-live-check.ts is absent, so the fixture above was"
echo "built and started and nothing was asked of it."
exit 1
}