-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy path.gitlab-ci.yml
More file actions
1145 lines (1050 loc) · 43.9 KB
/
.gitlab-ci.yml
File metadata and controls
1145 lines (1050 loc) · 43.9 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
include:
- local: 'components/index_pilot/.gitlab-ci.yml'
- local: 'quality/gitlab-ci-quality.yml'
- template: Security/SAST.gitlab-ci.yml
- project: 'postgres-ai/infra'
file: '/ci/templates/approval-check.yml'
# Auto-cancel redundant pipelines when new commits are pushed
workflow:
auto_cancel:
on_new_commit: interruptible
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == "main"
- if: $CI_COMMIT_TAG
# Make all jobs interruptible by default (deploy/publish jobs override this)
default:
interruptible: true
stages:
- security
- build
- test
- validate
- publish
- release
- preview
variables:
HELM_VERSION: "3.13.0"
# Build images from current code for e2e tests
# Images are pushed to GitLab Container Registry and pulled by test jobs
# ─── Secret Scanning ───────────────────────────────────────────────────────────
gitleaks:
stage: security
image:
name: ghcr.io/gitleaks/gitleaks:v8.30.0@sha256:105ac66a57b2bb8afb61a3b8a5dcc4817773d03724a7e8a515214cfe58225556
entrypoint: [""] # Override default entrypoint so GitLab CI can run shell commands
variables:
GIT_DEPTH: "0" # Full clone required — shallow clone silently misses commit history
script:
- |
# Scope scan to new commits only — avoids re-scanning repo history on every run
if [[ "${CI_MERGE_REQUEST_DIFF_BASE_SHA:-}" =~ ^[0-9a-f]{40}$ ]]; then
# MR pipeline: scan only commits added in this MR
gitleaks detect --source . --config gitleaks.toml --verbose --redact \
--report-path gitleaks-report.json --report-format json \
--log-opts="${CI_MERGE_REQUEST_DIFF_BASE_SHA}..${CI_COMMIT_SHA}"
elif [ -n "${CI_COMMIT_BEFORE_SHA:-}" ] && \
[ "${CI_COMMIT_BEFORE_SHA}" != "0000000000000000000000000000000000000000" ]; then
# Push to existing branch: scan only new commits
gitleaks detect --source . --config gitleaks.toml --verbose --redact \
--report-path gitleaks-report.json --report-format json \
--log-opts="${CI_COMMIT_BEFORE_SHA}..${CI_COMMIT_SHA}"
else
# New branch or force push: scan commits unique to this branch only
MERGE_BASE=$(git merge-base "origin/${CI_DEFAULT_BRANCH:-main}" "$CI_COMMIT_SHA" 2>/dev/null || echo "")
if [ -n "$MERGE_BASE" ] && [ "$MERGE_BASE" != "$CI_COMMIT_SHA" ]; then
LOG_OPTS="${MERGE_BASE}..${CI_COMMIT_SHA}"
else
# Fallback: scan only HEAD commit (safe — bounded to a single commit)
LOG_OPTS="${CI_COMMIT_SHA}^!"
fi
gitleaks detect --source . --config gitleaks.toml --verbose --redact \
--report-path gitleaks-report.json --report-format json \
--log-opts="$LOG_OPTS"
fi
artifacts:
paths:
- gitleaks-report.json
when: on_failure
expire_in: 7 days
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_PIPELINE_SOURCE == "push"
allow_failure: false
# ───────────────────────────────────────────────────────────────────────────────
# ─── Gitleaks Rule + Allowlist Tests ──────────────────────────────────────────
gitleaks-rule-test:
stage: security
image:
name: ghcr.io/gitleaks/gitleaks:v8.30.0@sha256:105ac66a57b2bb8afb61a3b8a5dcc4817773d03724a7e8a515214cfe58225556
entrypoint: [""]
before_script:
- apk add --no-cache python3
script:
- |
echo "=== Test 1: Detection rules must fire on synthetic fixtures ==="
python3 -c "import re; src=open('gitleaks.toml').read(); src=re.sub(r'paths = \\[.*?\\n\\]','paths = []',src,flags=re.DOTALL); open('/tmp/gitleaks-rules-only.toml','w').write(src)"
FIXTURE_EXIT=0
gitleaks detect --source tests/fixtures/gitleaks/ --config /tmp/gitleaks-rules-only.toml \
--no-git --verbose --redact 2>&1 || FIXTURE_EXIT=$?
if [ "$FIXTURE_EXIT" -eq 0 ]; then
echo "FAIL: Rules did not fire on fixture file"
exit 1
fi
echo "PASS: Rules fired on fixtures (exit=$FIXTURE_EXIT)"
echo ""
echo "=== Test 2: Allowlist must suppress fixtures in full-repo scan ==="
gitleaks detect --source . --config gitleaks.toml --no-git --redact
echo "PASS: Full repo scan clean (allowlist working)"
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_PIPELINE_SOURCE == "push"'
allow_failure: false
# ───────────────────────────────────────────────────────────────────────────────
build:test:images:
stage: build
image: docker:27.3
services:
- name: docker:27.3-dind
command: ["--tls=false"]
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
DOCKER_API_VERSION: "1.45"
GIT_STRATEGY: fetch
PGAI_TAG: ${CI_COMMIT_REF_SLUG}
before_script:
- docker version
- echo "$CI_REGISTRY_PASSWORD" | docker login "$CI_REGISTRY" -u "$CI_REGISTRY_USER" --password-stdin
script:
- |
set -euo pipefail
BUILD_TS="$(date -u '+%Y-%m-%d %H:%M:%S UTC')"
echo "Building images with tag: $PGAI_TAG"
echo "Registry: $CI_REGISTRY_IMAGE"
# Build and push postgres-ai-configs
docker build \
--build-arg "VERSION=$PGAI_TAG" \
--build-arg "BUILD_TS=$BUILD_TS" \
-f config/Dockerfile \
-t "$CI_REGISTRY_IMAGE/postgres-ai-configs:$PGAI_TAG" \
config
docker push "$CI_REGISTRY_IMAGE/postgres-ai-configs:$PGAI_TAG"
# Build and push reporter
docker build \
--build-arg "VERSION=$PGAI_TAG" \
--build-arg "BUILD_TS=$BUILD_TS" \
-f reporter/Dockerfile \
-t "$CI_REGISTRY_IMAGE/reporter:$PGAI_TAG" \
reporter
docker push "$CI_REGISTRY_IMAGE/reporter:$PGAI_TAG"
# Build and push monitoring-flask-backend
docker build \
--build-arg "VERSION=$PGAI_TAG" \
--build-arg "BUILD_TS=$BUILD_TS" \
-f monitoring_flask_backend/Dockerfile \
-t "$CI_REGISTRY_IMAGE/monitoring-flask-backend:$PGAI_TAG" \
monitoring_flask_backend
docker push "$CI_REGISTRY_IMAGE/monitoring-flask-backend:$PGAI_TAG"
echo ""
echo "Images pushed to GitLab Container Registry:"
echo " $CI_REGISTRY_IMAGE/postgres-ai-configs:$PGAI_TAG"
echo " $CI_REGISTRY_IMAGE/reporter:$PGAI_TAG"
echo " $CI_REGISTRY_IMAGE/monitoring-flask-backend:$PGAI_TAG"
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH =~ /^feature\//'
reporter:tests:
stage: test
image: python:3.11-bullseye
variables:
GIT_STRATEGY: fetch
PIP_DISABLE_PIP_VERSION_CHECK: "1"
PIP_NO_CACHE_DIR: "1"
before_script:
- python --version
- pip install --upgrade pip
- apt-get update
- apt-get install -y --no-install-recommends postgresql postgresql-client && rm -rf /var/lib/apt/lists/*
- pip install -r reporter/requirements-dev.txt
# Start PostgreSQL for integration tests
- service postgresql start
- su - postgres -c "psql -c 'SELECT version();'" || echo "PostgreSQL started"
script:
- chown -R postgres:postgres "$CI_PROJECT_DIR"
- su - postgres -c "cd \"$CI_PROJECT_DIR\" && python -m pytest --run-integration --cov=reporter --cov-report=term --cov-report=xml:coverage/reporter-coverage.xml tests/reporter"
# Fix ownership for artifact collection
- chown -R root:root "$CI_PROJECT_DIR/coverage" || true
coverage: '/TOTAL\s+\d+\s+\d+\s+(\d+)%/'
artifacts:
when: always
paths:
- coverage/
expire_in: 7 days
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main"'
vm-auth:config:tests:
stage: test
image: python:3.11-bullseye
variables:
GIT_STRATEGY: fetch
PIP_DISABLE_PIP_VERSION_CHECK: "1"
PIP_NO_CACHE_DIR: "1"
before_script:
- pip install pytest pyyaml
# Install helm for template tests
- curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
- helm repo add grafana https://grafana.github.io/helm-charts
- helm dependency build postgres_ai_helm/
script:
- helm lint postgres_ai_helm/
- helm template test postgres_ai_helm/ --set secrets.createFromValues=true >/tmp/postgres-ai-helm-rendered.yaml
- python -m pytest tests/compliance_vectors/test_vm_auth.py tests/compliance_vectors/test_flask_resources.py -v
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main"'
terraform:aws:tests:
stage: test
image:
name: hashicorp/terraform:1.9
entrypoint: [""]
variables:
TF_IN_AUTOMATION: "true"
script:
# file("~/.ssh/test-key.pem") in terraform_data connection block is
# evaluated eagerly during config parsing even though the resource is
# overridden in tests. Create a dummy file so file() resolves; the
# content is never used for an actual SSH connection.
- mkdir -p ~/.ssh && printf 'dummy-key-for-testing\n' > ~/.ssh/test-key.pem
- cd terraform/aws
- terraform init -backend=false
- terraform validate
- terraform test -filter=tests/kms_encryption.tftest.hcl
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes:
- terraform/aws/**/*
- if: '$CI_COMMIT_BRANCH == "main"'
changes:
- terraform/aws/**/*
cli:node:smoke:
stage: test
image: node:20-alpine
variables:
GIT_STRATEGY: fetch
before_script:
- corepack enable || true
- apk add --no-cache bash curl unzip openssl
- curl -fsSL https://bun.sh/install | bash
- export BUN_INSTALL="$HOME/.bun" && export PATH="$BUN_INSTALL/bin:$PATH"
script:
- node -v && npm -v && bun --version
- cd cli && bun install && bun run build && cd ..
- node ./cli/dist/bin/postgres-ai.js --help
- node ./cli/dist/bin/postgres-ai.js mon status --help
- node ./cli/dist/bin/postgres-ai.js mon targets list --help
# Verify prepare-db --print-sql works (SQL templates are bundled correctly)
- node ./cli/dist/bin/postgres-ai.js prepare-db --print-sql 2>&1 | grep -q "CREATE" || (echo "prepare-db --print-sql failed to output SQL" && exit 1)
- npm install -g ./cli
- echo "prefix=$(npm config get prefix)" && echo "PATH=$PATH"
- command -v postgres-ai && postgres-ai --help
- command -v postgresai && postgresai --help
- rm -f .pgwatch-config
- node ./cli/dist/bin/postgres-ai.js auth login --set-key "test_key_1234567890"
- node ./cli/dist/bin/postgres-ai.js auth show-key | grep -E "\*{2,}|[0-9]{4}$"
- test -f ~/.config/postgresai/config.json
- grep -q 'test_key' ~/.config/postgresai/config.json
- node ./cli/dist/bin/postgres-ai.js auth remove-key
- if grep -q 'apiKey' ~/.config/postgresai/config.json; then echo 'key not removed' && exit 1; fi
- node ./cli/dist/bin/postgres-ai.js mon targets list | head -n 1 || true
- node ./cli/dist/bin/postgres-ai.js mon targets add 'postgresql://user:pass@host:5432/db' ci-test || true
- node ./cli/dist/bin/postgres-ai.js mon targets remove ci-test || true
# Verify production OAuth endpoint is reachable (smoke test for auth flow)
- |
echo "Testing OAuth endpoint reachability..."
# Generate random state and code_challenge for smoke test (these are throwaway values)
CI_STATE=$(openssl rand -base64 16 | tr -d '/+=')
CI_CHALLENGE=$(openssl rand -base64 32 | tr -d '/+=')
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
-H "Content-Type: application/json" \
-d "{\"client_type\":\"cli\",\"state\":\"${CI_STATE}\",\"code_challenge\":\"${CI_CHALLENGE}\",\"code_challenge_method\":\"S256\",\"redirect_uri\":\"http://localhost:0/callback\"}" \
"https://postgres.ai/api/general/rpc/oauth_init" || echo "000")
echo "OAuth init endpoint returned HTTP $HTTP_CODE"
if [ "$HTTP_CODE" != "200" ] && [ "$HTTP_CODE" != "201" ]; then
echo "WARNING: OAuth endpoint returned unexpected status (expected 200/201, got $HTTP_CODE)"
echo "This may indicate the OAuth endpoint is misconfigured or unreachable"
fi
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
cli:node:tests:
stage: test
image: node:20-bullseye
variables:
GIT_STRATEGY: fetch
NPM_CONFIG_AUDIT: "false"
NPM_CONFIG_FUND: "false"
before_script:
- corepack enable || true
- apt-get update
- apt-get install -y --no-install-recommends postgresql postgresql-client unzip && rm -rf /var/lib/apt/lists/*
# Install Bun
- curl -fsSL https://bun.sh/install | bash
- export BUN_INSTALL="$HOME/.bun" && export PATH="$BUN_INSTALL/bin:$PATH"
# initdb refuses to run as root; run CLI tests as an unprivileged user
- useradd -m -s /bin/bash pgtest || true
- chown -R pgtest:pgtest "$CI_PROJECT_DIR"
# Install Bun for pgtest user
- su - pgtest -c "curl -fsSL https://bun.sh/install | bash"
- su - pgtest -c "cd \"$CI_PROJECT_DIR/cli\" && export PATH=\"\$HOME/.bun/bin:\$PATH\" && bun install"
script:
# Use 'bun run test' (not 'bun test') to invoke the npm script which generates metrics-embedded.ts first
# Coverage is enabled via bunfig.toml and outputs text + lcov reports
- su - pgtest -c "cd \"$CI_PROJECT_DIR/cli\" && export PATH=\"\$HOME/.bun/bin:\$PATH\" && bun run test"
# Fix ownership of coverage directory for artifact collection
- chown -R root:root "$CI_PROJECT_DIR/cli/coverage" || true
# Regex matches Bun coverage table: "All files | % Funcs | % Lines |" - captures % Lines
coverage: '/All files[^|]*\|[^|]*\|\s*([\d.]+)/'
artifacts:
when: always
paths:
- cli/coverage/
expire_in: 7 days
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main"'
# Validate helm chart on merge requests and main branch
validate-helm-chart:
stage: validate
image:
name: alpine/helm:${HELM_VERSION}
entrypoint: [""]
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes:
- postgres_ai_helm/**/*
- .gitlab-ci.yml
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
changes:
- postgres_ai_helm/**/*
- .gitlab-ci.yml
script:
- echo "Starting helm chart validation"
- apk add --no-cache bash git
- bash postgres_ai_helm/test-release-logic.sh
- cd postgres_ai_helm
- echo "Validating helm chart"
- helm dependency update
- helm lint .
- helm template test-release .
- helm package .
- ls -lh postgres-ai-monitoring-*.tgz
artifacts:
paths:
- postgres_ai_helm/*.tgz
expire_in: 1 week
cli:npm:publish:
stage: publish
interruptible: false # Never cancel publish jobs
image: node:20-bullseye
variables:
GIT_STRATEGY: fetch
NPM_CONFIG_AUDIT: "false"
NPM_CONFIG_FUND: "false"
before_script:
- corepack enable || true
# Install Bun for build step
- curl -fsSL https://bun.sh/install | bash
- export BUN_INSTALL="$HOME/.bun" && export PATH="$BUN_INSTALL/bin:$PATH"
- node -v && npm -v && bun --version
script:
- |
set -euo pipefail
: "${NPM_TOKEN:?NPM_TOKEN is required to publish}"
RAW_TAG="${CI_COMMIT_TAG:-}"
if [ -z "$RAW_TAG" ]; then
echo "CI_COMMIT_TAG is empty"
exit 1
fi
# npm requires SemVer. We accept an optional leading 'v' in git tags (e.g., v0.14.0 or 0.14.0).
NPM_VERSION="${RAW_TAG#v}"
if ! printf '%s' "$NPM_VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+([-+][-0-9A-Za-z.]+)?$'; then
echo "Invalid npm version derived from git tag: RAW_TAG='$RAW_TAG' -> NPM_VERSION='$NPM_VERSION' (must be SemVer)"
exit 1
fi
# Determine npm dist-tags:
# - X.Y.Z (final release) → @latest
# - X.Y.Z-beta.N → @latest + @beta
# - X.Y.Z-rc.N → @latest + @rc
# - X.Y.Z-dev.N → @dev only
DIST_TAG=""
EXTRA_TAG=""
if printf '%s' "$NPM_VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
DIST_TAG="latest"
elif printf '%s' "$NPM_VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+-beta\.[0-9]+$'; then
DIST_TAG="latest"
EXTRA_TAG="beta"
elif printf '%s' "$NPM_VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+$'; then
DIST_TAG="latest"
EXTRA_TAG="rc"
elif printf '%s' "$NPM_VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+-dev\.[0-9]+$'; then
DIST_TAG="dev"
fi
if [ -z "$DIST_TAG" ]; then
echo "Unsupported version: $NPM_VERSION (expected X.Y.Z, X.Y.Z-beta.N, X.Y.Z-rc.N, or X.Y.Z-dev.N)"
exit 1
fi
# Configure npm auth without committing credentials
printf "//registry.npmjs.org/:_authToken=%s\n" "$NPM_TOKEN" > ~/.npmrc
if [ -n "$EXTRA_TAG" ]; then
echo "Publishing postgresai@${NPM_VERSION} to dist-tags: ${DIST_TAG}, ${EXTRA_TAG}"
else
echo "Publishing postgresai@${NPM_VERSION} to dist-tag: ${DIST_TAG}"
fi
cd cli
# Use bun for faster install and build
export BUN_INSTALL="$HOME/.bun" && export PATH="$BUN_INSTALL/bin:$PATH"
bun install
# Make the git tag the source of truth without committing version bumps.
npm version --no-git-tag-version "$NPM_VERSION"
bun run build
npm publish --dry-run --tag "$DIST_TAG" --access public
npm publish --tag "$DIST_TAG" --access public
# Add extra dist-tag if specified (e.g., @beta or @rc in addition to @latest)
if [ -n "$EXTRA_TAG" ]; then
npm dist-tag add "postgresai@${NPM_VERSION}" "$EXTRA_TAG"
fi
# Wait until the registry sees the new postgresai version. This prevents the pgai step from
# failing due to eventual consistency when verifying/pinning dependencies.
for i in $(seq 1 30); do
if npm view "postgresai@${NPM_VERSION}" version >/dev/null 2>&1; then
break
fi
echo "Waiting for npm to recognize postgresai@${NPM_VERSION} (attempt $i/30)..."
sleep 2
done
npm view "postgresai@${NPM_VERSION}" version >/dev/null
if [ -n "$EXTRA_TAG" ]; then
echo "Publishing pgai@${NPM_VERSION} (wrapper) to dist-tags: ${DIST_TAG}, ${EXTRA_TAG}"
else
echo "Publishing pgai@${NPM_VERSION} (wrapper) to dist-tag: ${DIST_TAG}"
fi
cd ../pgai
# Update version + dependency so `npx pgai@<tag>` pulls the matching postgresai version.
npm pkg set "dependencies.postgresai=$NPM_VERSION"
npm version --no-git-tag-version "$NPM_VERSION"
npm publish --dry-run --tag "$DIST_TAG" --access public
npm publish --tag "$DIST_TAG" --access public
# Add extra dist-tag if specified
if [ -n "$EXTRA_TAG" ]; then
npm dist-tag add "pgai@${NPM_VERSION}" "$EXTRA_TAG"
fi
for i in $(seq 1 30); do
if npm view "pgai@${NPM_VERSION}" version >/dev/null 2>&1; then
break
fi
echo "Waiting for npm to recognize pgai@${NPM_VERSION} (attempt $i/30)..."
sleep 2
done
# Verify both packages are visible as published versions.
npm view "postgresai@${NPM_VERSION}" version >/dev/null
npm view "pgai@${NPM_VERSION}" version >/dev/null
after_script:
- rm -f ~/.npmrc
rules:
- if: '$CI_COMMIT_TAG =~ /^helm-v/'
when: never
- if: '$CI_COMMIT_TAG'
.docker-publish-base: &docker-publish-base
stage: publish
interruptible: false # Never cancel publish jobs
image: docker:27.3
services:
- name: docker:27.3-dind
command: ["--tls=false"]
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
DOCKER_API_VERSION: "1.45"
GIT_STRATEGY: fetch
.docker-build-script: &docker-build-script |
echo "Logging into container registry..."
echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY" -u "$REGISTRY_USER" --password-stdin
echo "Enabling binfmt (QEMU) for arm64 builds..."
docker run --privileged --rm tonistiigi/binfmt --install arm64
echo "Creating/using buildx builder..."
docker buildx create --name postgres-ai-builder --use 2>/dev/null || docker buildx use postgres-ai-builder
docker buildx inspect --bootstrap
echo "Building and pushing images: $VERSION"
echo " Platforms: $PLATFORMS"
echo " BUILD_TS: $BUILD_TS"
docker buildx build \
--platform "$PLATFORMS" \
--build-arg "VERSION=$VERSION" \
--build-arg "BUILD_TS=$BUILD_TS" \
-f reporter/Dockerfile \
-t "postgresai/reporter:$VERSION" \
--push \
reporter
docker buildx build \
--platform "$PLATFORMS" \
--build-arg "VERSION=$VERSION" \
--build-arg "BUILD_TS=$BUILD_TS" \
-f monitoring_flask_backend/Dockerfile \
-t "postgresai/monitoring-flask-backend:$VERSION" \
--push \
monitoring_flask_backend
docker buildx build \
--platform "$PLATFORMS" \
--build-arg "VERSION=$VERSION" \
--build-arg "BUILD_TS=$BUILD_TS" \
-f config/Dockerfile \
-t "postgresai/postgres-ai-configs:$VERSION" \
--push \
config
echo ""
echo "Published images:"
echo " postgresai/reporter:$VERSION"
echo " postgresai/monitoring-flask-backend:$VERSION"
echo " postgresai/postgres-ai-configs:$VERSION"
docker:publish:images:
<<: *docker-publish-base
script:
- |
set -euo pipefail
REGISTRY="${DH_CI_REGISTRY:-${DOCKERHUB_REGISTRY:-docker.io}}"
REGISTRY_USER="${DH_CI_REGISTRY_USER:-${DOCKERHUB_USERNAME:-}}"
REGISTRY_PASSWORD="${DH_CI_REGISTRY_PASSWORD:-${DOCKERHUB_TOKEN:-}}"
: "${REGISTRY_USER:?DH_CI_REGISTRY_USER (or DOCKERHUB_USERNAME) is required}"
: "${REGISTRY_PASSWORD:?DH_CI_REGISTRY_PASSWORD (or DOCKERHUB_TOKEN) is required}"
RAW_TAG="${CI_COMMIT_TAG:-}"
if [ -z "$RAW_TAG" ]; then
echo "CI_COMMIT_TAG is empty"
exit 1
fi
if ! printf '%s' "$RAW_TAG" | grep -Eq '^[A-Za-z0-9][-A-Za-z0-9_.]{0,127}$'; then
echo "Invalid Docker tag: '$RAW_TAG' (must match ^[A-Za-z0-9][-A-Za-z0-9_.]{0,127}$)"
exit 1
fi
VERSION="$RAW_TAG"
BUILD_TS="$(date -u '+%Y-%m-%d %H:%M:%S UTC')"
PLATFORMS="linux/amd64,linux/arm64"
- *docker-build-script
rules:
- if: '$CI_COMMIT_TAG =~ /^helm-v/'
when: never
- if: '$CI_COMMIT_TAG'
# Release helm chart when a tag is pushed.
# NOTE: The 'helm-v' prefix is an explicit exception to the org 'vX.Y.Z' tag convention.
# It is required to distinguish helm chart releases from npm/Docker image releases so that
# the cli:npm:publish and docker:publish:images jobs are not triggered on helm chart tags.
release-helm-chart:
stage: release
image:
name: alpine/helm:${HELM_VERSION}
entrypoint: [""]
rules:
- if: '$CI_COMMIT_TAG =~ /^helm-v[0-9]+\.[0-9]+\.[0-9]+$/'
script:
- apk add --no-cache bash
- |
bash -euo pipefail << 'BASH_SCRIPT'
set -euo pipefail
# Extract version from tag
VERSION="${CI_COMMIT_TAG#helm-}"
VERSION="${VERSION#v}"
echo "Version: ${VERSION}"
cd postgres_ai_helm
# Update Chart.yaml chart version only (appVersion tracks the deployed app, not the chart)
sed "s/^version: .*/version: \"${VERSION}\"/" Chart.yaml > Chart.yaml.tmp
mv Chart.yaml.tmp Chart.yaml
# Verify the substitution succeeded
if ! grep -q "^version: \"${VERSION}\"" Chart.yaml; then
echo "ERROR: Failed to update version in Chart.yaml (sed pattern may not match)" >&2
exit 1
fi
# Build and package
helm dependency update
helm lint .
helm package .
pkg=(postgres-ai-monitoring-*.tgz)
if [ ! -f "${pkg[0]}" ]; then
echo "Error: Package file not found after helm package" >&2
exit 1
fi
PACKAGE_FILE="${pkg[0]}"
echo "Packaged: ${PACKAGE_FILE} ($(du -h "${PACKAGE_FILE}" | cut -f1))"
# Copy artifact to fixed name so release: block can reference it with a known path
# (shell variables like ${PACKAGE_FILE} are not available in the release: YAML block)
cp "${PACKAGE_FILE}" "../postgres-ai-monitoring-chart.tgz"
BASH_SCRIPT
artifacts:
paths:
- postgres-ai-monitoring-chart.tgz
expire_in: 30 days
release:
tag_name: $CI_COMMIT_TAG
name: "Helm chart $CI_COMMIT_TAG"
description: |
## PostgresAI monitoring helm chart $CI_COMMIT_TAG
### Installation
Download `postgres-ai-monitoring-chart.tgz` from this release's assets and run:
```bash
helm install postgres-ai-monitoring postgres-ai-monitoring-chart.tgz
```
### Upgrade
```bash
helm upgrade postgres-ai-monitoring postgres-ai-monitoring-chart.tgz
```
### What's included
- PGWatch for Postgres metrics collection
- VictoriaMetrics for metrics storage
- Grafana for visualization
- Node exporter for system metrics
- cAdvisor for container metrics
- Automated reporting cronjobs
### Documentation
- [Installation guide]($CI_PROJECT_URL/-/blob/main/postgres_ai_helm/INSTALLATION_GUIDE.md)
- [Helm chart README]($CI_PROJECT_URL/-/blob/main/postgres_ai_helm/README.md)
- [Release process]($CI_PROJECT_URL/-/blob/main/postgres_ai_helm/RELEASE.md)
assets:
links:
- name: "postgres-ai-monitoring-chart.tgz"
url: "$CI_PROJECT_URL/-/jobs/$CI_JOB_ID/artifacts/raw/postgres-ai-monitoring-chart.tgz"
link_type: package
cli:node:e2e:dind:
stage: test
image: node:20-alpine
services:
- name: docker:27.3-dind
command: ["--tls=false"]
needs:
- job: build:test:images
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
DOCKER_API_VERSION: "1.45"
GIT_STRATEGY: fetch
PGAI_TAG: ${CI_COMMIT_REF_SLUG}
before_script:
- corepack enable || true
- apk add --no-cache bash curl unzip docker-cli docker-compose openssl postgresql-client
# Install Bun
- curl -fsSL https://bun.sh/install | bash
- export BUN_INSTALL="$HOME/.bun" && export PATH="$BUN_INSTALL/bin:$PATH"
- node -v && npm -v && bun --version && docker version
- cd cli && bun install && bun run build && cd ..
# Pull images from GitLab Container Registry
- echo "$CI_REGISTRY_PASSWORD" | docker login "$CI_REGISTRY" -u "$CI_REGISTRY_USER" --password-stdin
- |
echo "Pulling images from GitLab Container Registry..."
docker pull "$CI_REGISTRY_IMAGE/postgres-ai-configs:$PGAI_TAG"
docker pull "$CI_REGISTRY_IMAGE/reporter:$PGAI_TAG"
docker pull "$CI_REGISTRY_IMAGE/monitoring-flask-backend:$PGAI_TAG"
echo "Images ready:"
docker images | grep "$CI_REGISTRY_IMAGE"
# Create .env file with registry, tag, and per-job credentials
- |
REPLICATOR_PASSWORD="$(od -An -N32 -tx1 /dev/urandom | tr -d ' \n')"
VM_AUTH_PASSWORD="$(dd if=/dev/urandom bs=18 count=1 2>/dev/null | base64 | tr -d '\n')"
printf "%s\n" \
"PGAI_REGISTRY=$CI_REGISTRY_IMAGE" \
"PGAI_TAG=$PGAI_TAG" \
"REPLICATOR_PASSWORD=$REPLICATOR_PASSWORD" \
"VM_AUTH_USERNAME=vmauth" \
"VM_AUTH_PASSWORD=$VM_AUTH_PASSWORD" > .env
script:
- ./tests/e2e.cli.sh
after_script:
- docker ps -a || true
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
cli:node:full:dind:
stage: test
image: node:20-alpine
services:
- name: docker:27.3-dind
command: ["--tls=false"]
needs:
- job: build:test:images
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
DOCKER_API_VERSION: "1.45"
GIT_STRATEGY: fetch
PGAI_TAG: ${CI_COMMIT_REF_SLUG}
before_script:
- corepack enable || true
- apk add --no-cache bash curl unzip git docker-cli docker-compose openssl postgresql-client
# Install Bun
- curl -fsSL https://bun.sh/install | bash
- export BUN_INSTALL="$HOME/.bun" && export PATH="$BUN_INSTALL/bin:$PATH"
- node -v && npm -v && bun --version && docker version
- cd cli && bun install && bun run build && cd ..
# Pull images from GitLab Container Registry
- echo "$CI_REGISTRY_PASSWORD" | docker login "$CI_REGISTRY" -u "$CI_REGISTRY_USER" --password-stdin
- |
echo "Pulling images from GitLab Container Registry..."
docker pull "$CI_REGISTRY_IMAGE/postgres-ai-configs:$PGAI_TAG"
docker pull "$CI_REGISTRY_IMAGE/reporter:$PGAI_TAG"
docker pull "$CI_REGISTRY_IMAGE/monitoring-flask-backend:$PGAI_TAG"
echo "Images ready:"
docker images | grep "$CI_REGISTRY_IMAGE"
# Create .env file with registry, tag, and per-job credentials
- |
REPLICATOR_PASSWORD="$(od -An -N32 -tx1 /dev/urandom | tr -d ' \n')"
VM_AUTH_PASSWORD="$(dd if=/dev/urandom bs=18 count=1 2>/dev/null | base64 | tr -d '\n')"
printf "%s\n" \
"PGAI_REGISTRY=$CI_REGISTRY_IMAGE" \
"PGAI_TAG=$PGAI_TAG" \
"REPLICATOR_PASSWORD=$REPLICATOR_PASSWORD" \
"VM_AUTH_USERNAME=vmauth" \
"VM_AUTH_PASSWORD=$VM_AUTH_PASSWORD" > .env
script:
- echo "=== Testing local-install (demo mode) ==="
- node ./cli/dist/bin/postgres-ai.js mon local-install --demo
- sleep 10
- node ./cli/dist/bin/postgres-ai.js mon status
- echo ""
- echo "=== Testing shell command ==="
- echo "SELECT 1;" | node ./cli/dist/bin/postgres-ai.js mon shell target-db || true
- echo ""
- echo "=== Testing complete workflow ==="
- node ./cli/dist/bin/postgres-ai.js mon targets add "postgresql://monitor:monitor_pass@target-db:5432/target_database" demo-test
- node ./cli/dist/bin/postgres-ai.js mon targets list
- node ./cli/dist/bin/postgres-ai.js mon targets test demo-test || true
- node ./cli/dist/bin/postgres-ai.js mon health --wait 120
- node ./cli/dist/bin/postgres-ai.js mon show-grafana-credentials
- echo ""
- echo "=== Testing VM Basic Auth ==="
# Read VM auth credentials from .env (CLI uses CWD when docker-compose.yml is present)
- |
VM_USER=$(grep '^VM_AUTH_USERNAME=' .env | cut -d= -f2)
VM_PASS=$(grep '^VM_AUTH_PASSWORD=' .env | cut -d= -f2)
echo "VM_AUTH_USERNAME=$VM_USER"
echo "VM_AUTH_PASSWORD is set: $(test -n "$VM_PASS" && echo yes || echo no)"
# In DinD, port-mapped ports are on the docker host, not localhost
VM_HOST="${DOCKER_HOST:-localhost}"
VM_HOST=$(echo "$VM_HOST" | sed 's|tcp://||;s|:.*||')
VM_URL="http://${VM_HOST}:59090"
echo "VM URL: $VM_URL"
# Test: unauthenticated API request should return 401
HTTP_NO_AUTH=$(curl -s -o /dev/null -w "%{http_code}" "$VM_URL/api/v1/query?query=up")
echo "No auth -> HTTP $HTTP_NO_AUTH"
test "$HTTP_NO_AUTH" = "401" || (echo "FAIL: expected 401 without auth" && exit 1)
# Test: authenticated API request should return 200
HTTP_WITH_AUTH=$(curl -s -o /dev/null -w "%{http_code}" -u "$VM_USER:$VM_PASS" "$VM_URL/api/v1/query?query=up")
echo "With auth -> HTTP $HTTP_WITH_AUTH"
test "$HTTP_WITH_AUTH" = "200" || (echo "FAIL: expected 200 with auth" && exit 1)
# Test: health endpoint should remain accessible without auth (for probes)
HTTP_HEALTH=$(curl -s -o /dev/null -w "%{http_code}" "$VM_URL/health")
echo "Health (no auth) -> HTTP $HTTP_HEALTH"
test "$HTTP_HEALTH" = "200" || (echo "FAIL: expected 200 for health" && exit 1)
echo "VM Basic Auth tests passed"
- echo ""
- echo "=== Cleanup ==="
- node ./cli/dist/bin/postgres-ai.js mon stop
- node ./cli/dist/bin/postgres-ai.js mon clean || true
after_script:
- docker ps -a || true
- docker logs sink-prometheus 2>&1 || true
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH =~ /^feature\//'
allow_failure: false
integration:xmin-horizon:
stage: test
image: docker:27.3
services:
- name: docker:27.3-dind
command: ["--tls=false"]
needs:
- job: build:test:images
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
DOCKER_API_VERSION: "1.45"
GIT_STRATEGY: fetch
PGAI_TAG: ${CI_COMMIT_REF_SLUG}
before_script:
- apk add --no-cache bash docker-cli docker-compose
- echo "$CI_REGISTRY_PASSWORD" | docker login "$CI_REGISTRY" -u "$CI_REGISTRY_USER" --password-stdin
- REPLICATOR_PASSWORD="$(od -An -N32 -tx1 /dev/urandom | tr -d ' \n')"
- VM_AUTH_PASSWORD="$(dd if=/dev/urandom bs=18 count=1 2>/dev/null | base64 | tr -d '\n')"
- printf "%s\n" "PGAI_REGISTRY=$CI_REGISTRY_IMAGE" "PGAI_TAG=$PGAI_TAG" "REPLICATOR_PASSWORD=$REPLICATOR_PASSWORD" "VM_AUTH_USERNAME=vmauth" "VM_AUTH_PASSWORD=$VM_AUTH_PASSWORD" > .env
- cp instances.demo.yml instances.yml
script:
- docker compose up -d target-db sink-prometheus
- |
bash <<'BASH'
set -Eeuo pipefail
IFS=$'\n\t'
NETWORK="$(docker inspect -f '{{range $name, $_ := .NetworkSettings.Networks}}{{println $name}}{{end}}' target-db | head -n1)"
test -n "$NETWORK"
target_ready=0
for i in {1..60}; do
if docker run --rm --network "$NETWORK" postgres:17 pg_isready -h target-db -U postgres; then
target_ready=1
break
fi
sleep 2
done
test "$target_ready" = "1"
monitor_ready=0
for i in {1..60}; do
if docker run --rm --network "$NETWORK" postgres:17 \
psql "postgresql://monitor:monitor_pass@target-db:5432/target_database" -c 'select 1'; then
monitor_ready=1
break
fi
sleep 2
done
test "$monitor_ready" = "1"
docker compose up -d target-standby
standby_ready=0
for i in {1..60}; do
if docker run --rm --network "$NETWORK" postgres:17 pg_isready -h target-standby -U postgres; then
standby_ready=1
break
fi
sleep 2
done
test "$standby_ready" = "1"
standby_recovery_ready=0
for i in {1..60}; do
if docker run --rm --network "$NETWORK" postgres:17 \
psql "postgresql://postgres:postgres@target-standby:5432/target_database" -tAc 'select pg_is_in_recovery()' | grep -qx t; then
standby_recovery_ready=1
break
fi
sleep 2
done
test "$standby_recovery_ready" = "1"
PROMETHEUS_USERNAME="$(awk -F= '$1 == "VM_AUTH_USERNAME" { value = substr($0, index($0, "=") + 1) } END { print value }' .env)"
PROMETHEUS_PASSWORD="$(awk -F= '$1 == "VM_AUTH_PASSWORD" { value = substr($0, index($0, "=") + 1) } END { print value }' .env)"
test -n "$PROMETHEUS_USERNAME"
test -n "$PROMETHEUS_PASSWORD"
export PROMETHEUS_USERNAME PROMETHEUS_PASSWORD
docker compose up -d pgwatch-prometheus
docker run --rm \
--network "$NETWORK" \
-v "$CI_PROJECT_DIR:/workspace" \
-w /workspace \
-e TARGET_DB_URL="postgresql://postgres:postgres@target-db:5432/target_database" \
-e STANDBY_DB_URL="postgresql://postgres:postgres@target-standby:5432/target_database" \
-e PROMETHEUS_URL="http://sink-prometheus:9090" \
-e PROMETHEUS_USERNAME \
-e PROMETHEUS_PASSWORD \
-e COLLECTION_WAIT_SECONDS="480" \
-e REQUIRE_REPLICATION_SLOT_TEST="1" \
-e REQUIRE_PREPARED_XACTS_TEST="1" \
-e REQUIRE_STANDBY_FEEDBACK_TEST="1" \
python:3.11-slim \
bash -lc './tests/xmin_horizon/run_test.sh'
BASH
after_script:
- docker ps -a || true
- docker logs target-standby 2>&1 || true
- docker logs pgwatch-prometheus 2>&1 || true
- docker logs sink-prometheus 2>&1 || true
- docker compose down -v || true
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes:
- tests/xmin_horizon/**/*
- config/pgwatch-prometheus/metrics.yml
- config/target-db/**/*
- config/grafana/dashboards/Dashboard_7_Autovacuum_and_bloat.json
- postgres_ai_helm/config/grafana/dashboards/Dashboard_7_Autovacuum_and_bloat.json
- docker-compose.yml
- .gitlab-ci.yml
integration:xmin-horizon:standby-feedback:
stage: test
image: python:3.11-slim
variables:
GIT_STRATEGY: fetch
REQUIRE_STANDBY_FEEDBACK_TEST: "1"
script:
- |
: "${XMIN_HORIZON_STANDBY_TARGET_DB_URL:?set in CI for standby-feedback xmin coverage}"
: "${XMIN_HORIZON_STANDBY_DB_URL:?set in CI for standby-feedback xmin coverage}"
: "${XMIN_HORIZON_STANDBY_PROMETHEUS_URL:?set in CI for standby-feedback xmin coverage}"
TARGET_DB_URL="$XMIN_HORIZON_STANDBY_TARGET_DB_URL" \
STANDBY_DB_URL="$XMIN_HORIZON_STANDBY_DB_URL" \
PROMETHEUS_URL="$XMIN_HORIZON_STANDBY_PROMETHEUS_URL" \
REQUIRE_STANDBY_FEEDBACK_TEST="1" \
./tests/xmin_horizon/run_test.sh
rules:
- if: '$XMIN_HORIZON_STANDBY_TARGET_DB_URL && $XMIN_HORIZON_STANDBY_DB_URL && $XMIN_HORIZON_STANDBY_PROMETHEUS_URL'
changes:
- tests/xmin_horizon/**/*
- config/pgwatch-prometheus/metrics.yml
- .gitlab-ci.yml
cli:node:integration:
stage: test
image: node:20-alpine
variables:
GIT_STRATEGY: fetch
before_script:
- corepack enable || true
- apk add --no-cache bash curl unzip
# Install Bun
- curl -fsSL https://bun.sh/install | bash
- export BUN_INSTALL="$HOME/.bun" && export PATH="$BUN_INSTALL/bin:$PATH"
- node -v && npm -v && bun --version
- cd cli && bun install && bun run build && cd ..
script:
- |
set -euo pipefail
: "${PGAI_API_KEY:?PGAI_API_KEY is required for integration tests}"
BASE_URL="${PGAI_BASE_URL:-https://v2.postgres.ai/api/general/}"
echo "Using BASE_URL=$BASE_URL"
# Placeholder: run CLI help until API-backed commands are implemented
node ./cli/dist/bin/postgres-ai.js --help
rules:
- if: '$PGAI_API_KEY'
# =============================================================================
# Preview Environment Jobs
# =============================================================================
# Variables for preview environment (set in GitLab CI/CD settings)
# - PREVIEW_SSH_PRIVATE_KEY: SSH key for deploy user on preview VM
# - PREVIEW_VM_HOST: Hostname/IP of preview VM (e.g., 178.156.234.54)
# - PREVIEW_VM_USER: Username on preview VM (default: deploy)
.preview_base:
stage: preview
image: alpine:3.19
variables:
PREVIEW_VM_USER: deploy
PREVIEW_BASE_DIR: /opt/postgres-ai-previews
before_script:
- apk add --no-cache openssh-client rsync bash coreutils
- eval $(ssh-agent -s)
# PREVIEW_SSH_PRIVATE_KEY can be either:
# - File type variable (contains path to key file)
# - Regular variable with base64-encoded key content
- |
# Pipe to ssh-add to avoid exposing key in process arguments (ps aux)
# Note: Using pipe instead of process substitution for POSIX sh compatibility
if [ -f "$PREVIEW_SSH_PRIVATE_KEY" ]; then
tr -d '\r' < "$PREVIEW_SSH_PRIVATE_KEY" | ssh-add -
else