-
Notifications
You must be signed in to change notification settings - Fork 4.4k
880 lines (806 loc) · 37.1 KB
/
Copy pathpr-craft-k8s-tests.yml
File metadata and controls
880 lines (806 loc) · 37.1 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
# Runs the Craft Kubernetes integration tests against a Helm-installed kind
# cluster with the real API, Celery workers, sandbox proxy, and sandbox pods.
#
# Each test file owns a module-scoped sandbox pod and the single-node kind
# cluster fits one sandbox pod at a time, so the lane shards per test file, each
# shard on its own kind cluster (dynamic `fail-fast: false` matrix).
#
# Images are built once and pushed to the shared ECR repo, then each shard
# mirrors them into a local `localhost:5001` registry that kind pulls from
# unauthenticated — sandbox pods have no `imagePullSecrets`, so the image must
# come from an unauthenticated registry. (kind pulls from the local registry
# rather than `kind load`, which writes a multi-GB tar to tmpfs /tmp.)
# https://kind.sigs.k8s.io/docs/user/local-registry/
name: Craft Kubernetes Integration Tests
concurrency:
group: Craft-K8s-Tests-${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: true
on:
schedule:
- cron: "0 7 * * *" # 07:00 UTC nightly
merge_group:
pull_request:
branches: [main]
# NOTE: Intentionally no `paths:` filter. Trigger-level `paths:` is ignored
# for `merge_group`, so we always trigger and let the `changes` job below
# decide whether the real test job runs (matches the docker-compose lane
# cadence; this lane is expensive and must not run on every PR).
push:
tags:
- "v*.*.*"
workflow_dispatch:
permissions:
contents: read
env:
S3_ENDPOINT_URL: "http://127.0.0.1:9004"
S3_FILE_STORE_BUCKET_NAME: "onyx-file-store-bucket"
S3_AWS_ACCESS_KEY_ID: "minioadmin"
S3_AWS_SECRET_ACCESS_KEY: "minioadmin"
SANDBOX_BACKEND: "kubernetes"
SANDBOX_NAMESPACE: "onyx-sandboxes"
SANDBOX_SERVICE_ACCOUNT_NAME: "sandbox"
SANDBOX_CONTAINER_IMAGE: "localhost:5001/onyx-sandbox:ci"
# Sandbox pod resource requests live in values-ci.yaml (configMap.SANDBOX_POD_*).
ONYX_SERVER_URL: "http://onyx-api-service.onyx.svc.cluster.local:8080"
# Proxy lives in release namespace `onyx`. SANDBOX_PROXY_HOST is set
# later to the proxy Service ClusterIP — the runner has no cluster DNS,
# so a Service FQDN here would fail _resolve_proxy_ip in the manager.
HELM_RELEASE_NAMESPACE: "onyx"
SANDBOX_PROXY_PORT: "8080"
SANDBOX_PROXY_CA_SECRET: "sandbox-proxy-ca"
SANDBOX_PROXY_CA_CONFIGMAP: "sandbox-proxy-ca-bundle"
BACKEND_IMAGE: "localhost:5001/onyx-backend:ci"
# Fail fast on opencode hangs (default is 900s) so a stuck turn surfaces logs
# well within the job timeout instead of being killed with zero signal.
SANDBOX_TURN_TIMEOUT_SECONDS: "120"
KIND_REGISTRY_NAME: "kind-registry"
KIND_REGISTRY_PORT: "5001"
KIND_VERSION: "v0.31.0"
KUBECTL_VERSION: "v1.35.0"
CRAFT_ASSETS_ARTIFACT: "craft-k8s-assets"
# The pytest process runs on the runner, so it reaches chart-managed
# Postgres/Redis through kubectl port-forwards.
POSTGRES_HOST: "127.0.0.1"
POSTGRES_PORT: "5432"
POSTGRES_USER: "postgres"
POSTGRES_PASSWORD: "password"
POSTGRES_DB: "postgres"
REDIS_HOST: "127.0.0.1"
REDIS_PORT: "6379"
REDIS_PASSWORD: "password"
API_SERVER_HOST: "127.0.0.1"
API_SERVER_PORT: "8080"
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
jobs:
changes:
# Gates the expensive test job. On pull_request / merge_group we use
# paths-filter; on schedule / push (tags) / workflow_dispatch the filter is
# skipped and the output defaults to `true` so the lane runs.
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
pull-requests: read
outputs:
craft_k8s: ${{ steps.filter.outputs.craft_k8s || 'true' }}
steps:
- name: Checkout code
if: github.event_name == 'pull_request' || github.event_name == 'merge_group'
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
with:
persist-credentials: false
- uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706
id: filter
if: github.event_name == 'pull_request' || github.event_name == 'merge_group'
with:
filters: |
craft_k8s:
- 'backend/Dockerfile'
- 'backend/onyx/sandbox_proxy/**'
- 'backend/onyx/server/features/build/**'
- 'backend/onyx/server/features/skill/**'
- 'backend/onyx/skills/**'
- 'backend/shared_configs/**'
# Only the sandbox/craft-specific chart bits gate this lane. Generic
# chart changes (workers, deps, version bumps) are install-tested by
# the `Helm - Lint and Test Charts` lane (`ct install` into kind), so
# they don't need the full sandbox suite here.
- 'deployment/helm/charts/onyx/templates/sandbox-*'
- 'deployment/helm/charts/onyx/templates/sandbox-proxy/**'
- 'deployment/helm/charts/onyx/templates/network-policy-sandbox-*'
- 'deployment/helm/charts/onyx/templates/craft-*'
- 'deployment/helm/charts/onyx/templates/celery-worker-scheduled-tasks*'
- 'deployment/helm/charts/onyx/charts/code-interpreter-*'
- 'deployment/helm/charts/onyx/values-ci.yaml'
- 'backend/tests/integration/tests/craft/*.py'
- 'backend/tests/integration/tests/craft/k8s/**'
- 'backend/tests/common/craft/**'
- 'backend/tests/integration/conftest.py'
- 'backend/tests/integration/common_utils/**'
- '.github/workflows/pr-craft-k8s-tests.yml'
- '.github/actions/setup-python-and-install-dependencies/**'
- '.github/actions/setup-test-license/**'
- '.github/actions/login-ecr-pullthrough-cache/**'
discover-test-files:
# One shard per craft k8s test file (module-scoped pool fixtures keep a file on one shard).
needs: changes
if: needs.changes.outputs.craft_k8s == 'true'
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
test-files: ${{ steps.set-matrix.outputs.test-files }}
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # ratchet:actions/checkout@v6
with:
persist-credentials: false
- name: Discover craft k8s test files
id: set-matrix
run: |
set -eo pipefail
# `name` = log-friendly shard id; `path` = repo-root-relative pytest target.
entries=""
for f in $(find backend/tests/integration/tests/craft/k8s \
-maxdepth 1 -name 'test_*.py' -type f | sort); do
base=$(basename "$f" .py)
shard="${base#test_}"
entries="${entries}{\"path\":\"${f}\",\"name\":\"${shard}\"},"
done
if [ -z "${entries}" ]; then
echo "::error::no craft k8s test files discovered"
exit 1
fi
echo "test-files=[${entries%,}]" >> "$GITHUB_OUTPUT"
prepare-craft-assets:
name: Prepare Craft test assets
needs: changes
if: needs.changes.outputs.craft_k8s == 'true'
runs-on:
- runs-on
- runner=2cpu-linux-x64
- spot=false
- ${{ format('run-id={0}-craft-k8s-tools', github.run_id) }}
timeout-minutes: 10
permissions:
contents: read
steps:
- uses: runs-on/action@4e5f72399b6b17f2e79c511c1b38a315a64d22dc
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # ratchet:actions/checkout@v6
with:
persist-credentials: false
- name: Download and verify kind tools
run: |
set -euo pipefail
download() {
curl --fail --location --silent --show-error \
--retry 5 --retry-delay 2 --retry-all-errors \
--output "$2" "$1"
}
cache_dir="${RUNNER_TOOL_CACHE}/kind/${KIND_VERSION}/amd64"
kind_dir="${cache_dir}/kind/bin"
kubectl_dir="${cache_dir}/kubectl/bin"
mkdir -p "${kind_dir}" "${kubectl_dir}"
kind_filename="kind-linux-amd64"
kind_url="https://github.com/kubernetes-sigs/kind/releases/download/${KIND_VERSION}"
download "${kind_url}/${kind_filename}" "${kind_dir}/${kind_filename}"
download "${kind_url}/${kind_filename}.sha256sum" "${kind_dir}/${kind_filename}.sha256sum"
(
cd "${kind_dir}"
grep "${kind_filename}" "${kind_filename}.sha256sum" | sha256sum --check -
mv "${kind_filename}" kind
rm "${kind_filename}.sha256sum"
chmod +x kind
)
kubectl_url="https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64"
download "${kubectl_url}/kubectl" "${kubectl_dir}/kubectl"
download "${kubectl_url}/kubectl.sha256" "${kubectl_dir}/kubectl.sha256"
(
cd "${kubectl_dir}"
echo "$(cat kubectl.sha256) kubectl" | sha256sum --check -
rm kubectl.sha256
chmod +x kubectl
)
- name: Download Helm chart dependencies
run: |
set -euo pipefail
retry() {
local attempt
for attempt in 1 2 3 4 5; do
if "$@"; then
return 0
fi
if [ "${attempt}" -eq 5 ]; then
return 1
fi
echo "Command failed (attempt ${attempt}/5); retrying ..."
sleep $((attempt * 5))
done
}
retry helm repo add --force-update ingress-nginx https://kubernetes.github.io/ingress-nginx
retry helm repo add --force-update opensearch https://opensearch-project.github.io/helm-charts
retry helm repo add --force-update cloudnative-pg https://cloudnative-pg.github.io/charts
retry helm repo add --force-update ot-container-kit https://ot-container-kit.github.io/helm-charts
retry helm repo add --force-update minio https://charts.min.io/
retry helm repo add --force-update code-interpreter https://onyx-dot-app.github.io/python-sandbox/
retry helm repo update
if ! retry helm dependency build --skip-refresh deployment/helm/charts/onyx; then
echo "helm dependency build failed; pulling disabled code-interpreter dependency directly"
code_interpreter_version=$(awk '
$1 == "-" && $2 == "name:" && $3 == "code-interpreter" { found = 1 }
found && $1 == "version:" { print $2; exit }
' deployment/helm/charts/onyx/Chart.yaml)
test -n "${code_interpreter_version}"
retry helm pull code-interpreter/code-interpreter \
--version "${code_interpreter_version}" \
--destination deployment/helm/charts/onyx/charts
fi
helm dependency list deployment/helm/charts/onyx
helm dependency list deployment/helm/charts/onyx \
| awk 'NR > 1 && NF && $4 != "ok" { bad = 1 } END { exit bad }'
# Tar preserves executable permissions for kind and kubectl.
- name: Package Craft test assets
run: |
set -euo pipefail
assets_dir="${RUNNER_TEMP}/craft-assets"
mkdir -p "${assets_dir}"
tar -C "${RUNNER_TOOL_CACHE}/kind/${KIND_VERSION}/amd64" \
-czf "${assets_dir}/kind-tools.tar.gz" .
tar -C deployment/helm/charts/onyx/charts \
-czf "${assets_dir}/helm-dependencies.tar.gz" .
- name: Upload Craft test assets
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: ${{ env.CRAFT_ASSETS_ARTIFACT }}
path: ${{ runner.temp }}/craft-assets
if-no-files-found: error
retention-days: 1
compression-level: 0
overwrite: true
build-images:
# Build both images in parallel (one matrix leg each) and push to the shared
# ECR repo so each test shard pulls prebuilt images instead of cold-building.
name: build-image (${{ matrix.image }})
needs: changes
if: needs.changes.outputs.craft_k8s == 'true'
strategy:
fail-fast: false
matrix:
include:
- image: sandbox
context: ./backend/onyx/server/features/build/sandbox/image
file: ./backend/onyx/server/features/build/sandbox/image/Dockerfile
# CI tests don't exercise skill runtime deps (soffice/pdftoppm/pptxgenjs),
# so skip LibreOffice et al. to keep the CI image lean. Browser stays
# on (ENABLE_BROWSER defaults true) — chromium is lighter and the
# browser integration test needs it.
extra_build_args: "ENABLE_SKILLS=false"
target: ""
- image: backend
context: ./backend
file: ./backend/Dockerfile
extra_build_args: ""
# The production image; the backend Dockerfile's default (last) stage is the
# dev variant.
target: runtime
runs-on:
- runs-on
- runner=8cpu-linux-x64
- spot=false
- volume=100gb
- ${{ format('run-id={0}-craft-k8s-build-{1}', github.run_id, matrix.image) }}
- extras=ecr-cache
timeout-minutes: 30
permissions:
contents: read
steps:
- uses: runs-on/action@4e5f72399b6b17f2e79c511c1b38a315a64d22dc
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # ratchet:actions/checkout@v6
with:
persist-credentials: false
- name: Log in to ECR pull-through cache
uses: ./.github/actions/login-ecr-pullthrough-cache
with:
ecr-registry: ${{ vars.ECR_REGISTRY }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # ratchet:docker/setup-buildx-action@v4
- name: Build and push ${{ matrix.image }} image
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a
with:
context: ${{ matrix.context }}
file: ${{ matrix.file }}
# Empty target means the Dockerfile's default (last) stage.
target: ${{ matrix.target }}
platforms: linux/amd64
build-args: |
BASE_IMAGE_REGISTRY=${{ env.BASE_IMAGE_REGISTRY }}
${{ matrix.extra_build_args }}
tags: ${{ env.RUNS_ON_ECR_CACHE }}:craft-k8s-${{ matrix.image }}-${{ github.run_id }}
push: true
# Attestations attach as ECR referrers to the image digest, which is
# stable across runs and caps out at 100 per subject.
provenance: false
sbom: false
cache-from: type=gha,scope=craft-${{ matrix.image }}
cache-to: type=gha,scope=craft-${{ matrix.image }},mode=max
craft-k8s-tests:
name: craft-k8s (${{ matrix.test-file.name }})
needs: [changes, discover-test-files, prepare-craft-assets, build-images]
if: needs.changes.outputs.craft_k8s == 'true'
# spot=false: this is a long lane (full kind cluster per shard); on-demand
# avoids mid-run spot reclamation. Matches the compose lane.
runs-on:
- runs-on
- runner=8cpu-linux-x64
- spot=false
- volume=100gb
- ${{ format('run-id={0}-craft-k8s-tests-{1}', github.run_id, matrix.test-file.name) }}
- extras=ecr-cache
timeout-minutes: 40
strategy:
# fail-fast off so one shard's failure doesn't cancel the others.
fail-fast: false
matrix:
test-file: ${{ fromJson(needs.discover-test-files.outputs.test-files) }}
# id-token: OIDC for the setup-test-license step.
permissions:
contents: read
id-token: write
env:
PYTHONPATH: ./backend
MODEL_SERVER_HOST: "disabled"
DISABLE_TELEMETRY: "true"
DISABLE_VECTOR_DB: "false"
steps:
- uses: runs-on/action@4e5f72399b6b17f2e79c511c1b38a315a64d22dc
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # ratchet:actions/checkout@v6
with:
persist-credentials: false
- name: Setup Python and Install Dependencies
uses: ./.github/actions/setup-python-and-install-dependencies
with:
requirements: |
backend/requirements/default.txt
backend/requirements/dev.txt
backend/requirements/ee.txt
- name: Download Craft test assets
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
name: ${{ env.CRAFT_ASSETS_ARTIFACT }}
path: ${{ runner.temp }}/craft-assets
- name: Install Craft test assets
run: |
set -euo pipefail
kind_cache_dir="${RUNNER_TOOL_CACHE}/kind/${KIND_VERSION}/amd64"
charts_dir="deployment/helm/charts/onyx/charts"
mkdir -p "${kind_cache_dir}" "${charts_dir}"
tar -C "${kind_cache_dir}" \
-xzf "${RUNNER_TEMP}/craft-assets/kind-tools.tar.gz"
tar -C "${charts_dir}" \
-xzf "${RUNNER_TEMP}/craft-assets/helm-dependencies.tar.gz"
- name: Log in to ECR pull-through cache
uses: ./.github/actions/login-ecr-pullthrough-cache
with:
ecr-registry: ${{ vars.ECR_REGISTRY }}
- name: Start local image registry
run: |
docker run -d --restart=always \
-p "127.0.0.1:${KIND_REGISTRY_PORT}:5000" \
--name "${KIND_REGISTRY_NAME}" \
"${BASE_IMAGE_REGISTRY:-docker.io}/library/registry:2"
# Re-push the prebuilt ECR images into this shard's local registry under the
# tags kind pulls (unauthenticated); the blob copy is cheap vs a cold build.
- name: Mirror prebuilt images into local registry
env:
ECR_CACHE: ${{ env.RUNS_ON_ECR_CACHE }}
RUN_ID: ${{ github.run_id }}
run: |
set -eo pipefail
mirror() {
local src="$1" dst="$2"
docker pull "$src"
docker tag "$src" "$dst"
docker push "$dst"
}
mirror "${ECR_CACHE}:craft-k8s-sandbox-${RUN_ID}" "${SANDBOX_CONTAINER_IMAGE}"
mirror "${ECR_CACHE}:craft-k8s-backend-${RUN_ID}" "${BACKEND_IMAGE}"
- name: Write kind config
run: |
cat > "${RUNNER_TEMP}/kind-config.yaml" <<'EOF'
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
containerdConfigPatches:
- |-
[plugins."io.containerd.grpc.v1.cri".registry]
config_path = "/etc/containerd/certs.d"
EOF
- name: Create kind cluster
uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # ratchet:helm/kind-action@v1.14.0
with:
version: ${{ env.KIND_VERSION }}
kubectl_version: ${{ env.KUBECTL_VERSION }}
cluster_name: onyx-craft-ci
node_image: kindest/node:v1.33.1
config: ${{ runner.temp }}/kind-config.yaml
- name: Connect local registry to kind network
run: |
docker network connect "kind" "${KIND_REGISTRY_NAME}" || true
REGISTRY_DIR="/etc/containerd/certs.d/localhost:${KIND_REGISTRY_PORT}"
docker exec onyx-craft-ci-control-plane mkdir -p "${REGISTRY_DIR}"
docker exec onyx-craft-ci-control-plane bash -c "cat > ${REGISTRY_DIR}/hosts.toml <<EOF
[host.\"http://${KIND_REGISTRY_NAME}:5000\"]
EOF"
# Pod spec sets nodeSelector onyx.app/workload=sandbox
# (kubernetes_sandbox_manager.py). In prod this matches a dedicated
# node pool; in kind we just label the single control-plane node.
- name: Label kind node for sandbox workload
run: kubectl label node onyx-craft-ci-control-plane onyx.app/workload=sandbox
- name: Validate Helm chart dependencies
run: |
helm dependency list deployment/helm/charts/onyx
helm dependency list deployment/helm/charts/onyx \
| awk 'NR > 1 && NF && $4 != "ok" { bad = 1 } END { exit bad }'
- name: Generate sandbox push key
run: |
python - <<'PY' >> "$GITHUB_ENV"
import base64
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives.serialization import Encoding
from cryptography.hazmat.primitives.serialization import NoEncryption
from cryptography.hazmat.primitives.serialization import PrivateFormat
key = Ed25519PrivateKey.generate()
raw = key.private_bytes(
encoding=Encoding.Raw,
format=PrivateFormat.Raw,
encryption_algorithm=NoEncryption(),
)
print(f"ONYX_SANDBOX_PUSH_PRIVATE_KEY={base64.b64encode(raw).decode('ascii')}")
PY
- name: Install Onyx chart into kind
run: |
set -eo pipefail
kubectl create namespace "${HELM_RELEASE_NAMESPACE}" \
--dry-run=client -o yaml | kubectl apply -f -
kubectl config set-context --current --namespace="${HELM_RELEASE_NAMESPACE}"
for attempt in 1 2 3; do
if helm upgrade --install onyx deployment/helm/charts/onyx \
-n "${HELM_RELEASE_NAMESPACE}" \
-f deployment/helm/charts/onyx/values-ci.yaml \
--set "api.image.repository=localhost:5001/onyx-backend" \
--set "api.image.tag=ci" \
--set "celery_shared.image.repository=localhost:5001/onyx-backend" \
--set "celery_shared.image.tag=ci" \
--set-string "auth.sandboxPushSecret.values.private_key=${ONYX_SANDBOX_PUSH_PRIVATE_KEY}" \
--timeout 10m; then
exit 0
fi
if [ "$attempt" -lt 3 ]; then
echo "helm install failed (attempt ${attempt}/3); waiting before retry ..."
kubectl -n "${HELM_RELEASE_NAMESPACE}" get pods || true
sleep 20
fi
done
helm status onyx -n "${HELM_RELEASE_NAMESPACE}" || true
kubectl get pods -A || true
exit 1
- name: Wait for chart Postgres ready
run: |
set -eo pipefail
kubectl -n "${HELM_RELEASE_NAMESPACE}" rollout status \
deployment/onyx-cloudnative-pg --timeout=180s
# Don't add a `pod -l cnpg.io/cluster` wait — it also matches the
# transient initdb pod (deleted mid-wait → NotFound).
kubectl -n "${HELM_RELEASE_NAMESPACE}" wait \
--for=condition=Ready cluster/onyx-pg \
--timeout=300s
- name: Forward chart Postgres to runner
run: |
set -eo pipefail
# setsid survives the step-boundary process-group cleanup; the loop
# survives kubectl port-forward's mid-run drops.
setsid bash -c "while true; do kubectl -n \"$HELM_RELEASE_NAMESPACE\" port-forward svc/onyx-pg-rw \"$POSTGRES_PORT\":5432; sleep 1; done" \
> "${RUNNER_TEMP}/postgres-port-forward.log" 2>&1 &
echo "$!" > "${RUNNER_TEMP}/postgres-port-forward.pid"
for _ in $(seq 1 60); do
if python -c 'import os, psycopg2; conn = psycopg2.connect(dbname=os.environ["POSTGRES_DB"], user=os.environ["POSTGRES_USER"], password=os.environ["POSTGRES_PASSWORD"], host=os.environ["POSTGRES_HOST"], port=os.environ["POSTGRES_PORT"], connect_timeout=2); conn.close()'; then
echo "chart Postgres is reachable at ${POSTGRES_HOST}:${POSTGRES_PORT}"
exit 0
fi
sleep 2
done
cat "${RUNNER_TEMP}/postgres-port-forward.log" || true
exit 1
- name: Wait for API startup migrations
run: |
kubectl -n "${HELM_RELEASE_NAMESPACE}" rollout status \
deployment/onyx-api-server --timeout=360s
kubectl -n "${HELM_RELEASE_NAMESPACE}" exec deployment/onyx-api-server -- \
alembic current --check-heads
- name: Wait for chart OpenSearch ready
run: |
kubectl -n "${HELM_RELEASE_NAMESPACE}" rollout status \
statefulset/onyx-opensearch-master --timeout=420s
- name: Wait for chart Redis ready
run: |
set -eo pipefail
kubectl -n "${HELM_RELEASE_NAMESPACE}" rollout status \
deployment/redis-operator --timeout=180s
for _ in $(seq 1 120); do
endpoints=$(kubectl -n "${HELM_RELEASE_NAMESPACE}" get endpoints onyx \
-o jsonpath='{.subsets[0].addresses[0].ip}' 2>/dev/null || true)
if [ -n "${endpoints}" ]; then
exit 0
fi
sleep 2
done
kubectl -n "${HELM_RELEASE_NAMESPACE}" get redis,pods,svc || true
exit 1
- name: Forward chart Redis to runner
run: |
set -eo pipefail
setsid bash -c "while true; do kubectl -n \"$HELM_RELEASE_NAMESPACE\" port-forward svc/onyx \"$REDIS_PORT\":6379; sleep 1; done" \
> "${RUNNER_TEMP}/redis-port-forward.log" 2>&1 &
echo "$!" > "${RUNNER_TEMP}/redis-port-forward.pid"
for _ in $(seq 1 60); do
if python -c 'import os; from redis import Redis; Redis(host=os.environ["REDIS_HOST"], port=int(os.environ["REDIS_PORT"]), password=os.environ["REDIS_PASSWORD"], socket_connect_timeout=2).ping()'; then
echo "chart Redis is reachable at ${REDIS_HOST}:${REDIS_PORT}"
exit 0
fi
sleep 2
done
cat "${RUNNER_TEMP}/redis-port-forward.log" || true
exit 1
- name: Wait for chart MinIO ready
run: |
kubectl -n "${HELM_RELEASE_NAMESPACE}" rollout status \
deployment/onyx-minio --timeout=180s
if kubectl -n "${HELM_RELEASE_NAMESPACE}" get job/onyx-minio-post-job >/dev/null 2>&1; then
kubectl -n "${HELM_RELEASE_NAMESPACE}" wait \
--for=condition=complete job/onyx-minio-post-job \
--timeout=180s
else
echo "MinIO post-install hook job already completed and was deleted by Helm"
fi
- name: Forward chart MinIO to runner
run: |
set -eo pipefail
setsid bash -c "while true; do kubectl -n \"$HELM_RELEASE_NAMESPACE\" port-forward svc/onyx-minio 9004:9000; sleep 1; done" \
> "${RUNNER_TEMP}/minio-port-forward.log" 2>&1 &
echo "$!" > "${RUNNER_TEMP}/minio-port-forward.pid"
for _ in $(seq 1 30); do
if curl -fsS "${S3_ENDPOINT_URL}/minio/health/ready"; then
echo "chart MinIO is reachable at ${S3_ENDPOINT_URL}"
exit 0
fi
sleep 1
done
cat "${RUNNER_TEMP}/minio-port-forward.log" || true
exit 1
- name: Wait for chart app runtime ready
run: |
set -eo pipefail
for deployment in \
onyx-sandbox-proxy \
onyx-api-server \
onyx-celery-beat \
onyx-celery-worker-primary \
onyx-celery-worker-light \
onyx-celery-worker-heavy \
onyx-celery-worker-scheduled-tasks; do
kubectl -n "${HELM_RELEASE_NAMESPACE}" rollout status \
"deployment/${deployment}" --timeout=360s
done
# Tier-gated EE routes (test_skill_push user-groups) need a license.
- name: Fetch dev license
uses: ./.github/actions/setup-test-license
with:
aws-oidc-role-arn: ${{ secrets.AWS_OIDC_ROLE_ARN }}
- name: Seed dev license
# The api-server can briefly restart if Postgres/cnpg flaps right after
# rollout, so re-assert readiness and retry the exec.
run: |
set -eo pipefail
for attempt in $(seq 1 5); do
kubectl -n "${HELM_RELEASE_NAMESPACE}" rollout status \
deployment/onyx-api-server --timeout=120s || true
if kubectl -n "${HELM_RELEASE_NAMESPACE}" exec deployment/onyx-api-server -- \
env ONYX_DEV_LICENSE="${ONYX_DEV_LICENSE}" python -m scripts.seed_dev_license; then
echo "seeded dev license (attempt ${attempt})"
exit 0
fi
echo "seed dev license failed (attempt ${attempt}); retrying ..."
sleep 10
done
exit 1
- name: Forward chart API to runner
run: |
set -eo pipefail
setsid bash -c "while true; do kubectl -n \"$HELM_RELEASE_NAMESPACE\" port-forward svc/onyx-api-service \"$API_SERVER_PORT\":8080; sleep 1; done" \
> "${RUNNER_TEMP}/api-port-forward.log" 2>&1 &
echo "$!" > "${RUNNER_TEMP}/api-port-forward.pid"
for _ in $(seq 1 60); do
if curl -fsS "http://${API_SERVER_HOST}:${API_SERVER_PORT}/health"; then
echo "chart API reachable at ${API_SERVER_HOST}:${API_SERVER_PORT}"
exit 0
fi
sleep 2
done
cat "${RUNNER_TEMP}/api-port-forward.log" || true
exit 1
# The test process runs on the CI runner, which has no cluster DNS.
# `socket.gethostbyname(IP)` returns the IP unchanged, so resolving
# the Service ClusterIP here lets _resolve_proxy_ip in the manager
# succeed without any DNS resolution. host_aliases on sandbox pods
# then pins `sandbox-proxy` to this same IP.
- name: Resolve proxy Service IP and export to env
run: |
PROXY_IP=$(kubectl -n "${HELM_RELEASE_NAMESPACE}" get svc \
onyx-sandbox-proxy -o jsonpath='{.spec.clusterIP}')
echo "SANDBOX_PROXY_HOST=${PROXY_IP}" >> "$GITHUB_ENV"
echo "resolved proxy ClusterIP: ${PROXY_IP}"
# Sandbox pods are torn down on failure before `kind export logs` runs,
# so kubelet GCs their opencode-serve logs. Stream them to disk live (one
# follower per pod, retrying the attach) so failures leave a trail.
- name: Start sandbox pod log capture
run: |
mkdir -p sandbox-logs
# The inner `bash -c` body is single-quoted on purpose: its $vars
# expand at runtime inside that shell, not here.
# shellcheck disable=SC2016
nohup bash -c '
while true; do
for name in $(kubectl -n onyx-sandboxes get pods \
-o jsonpath="{.items[*].metadata.name}" 2>/dev/null); do
if mkdir "sandbox-logs/.lock-${name}" 2>/dev/null; then
(
for _ in $(seq 1 120); do
kubectl -n onyx-sandboxes logs --all-containers=true \
--prefix=true -f "$name" \
>> "sandbox-logs/${name}.log" 2>&1 && break
kubectl -n onyx-sandboxes get pod "$name" \
>/dev/null 2>&1 || break
sleep 1
done
) &
fi
done
sleep 1
done
' > sandbox-logs/_watcher.log 2>&1 &
echo "$!" > "${RUNNER_TEMP}/sandbox-log-watcher.pid"
disown || true
echo "sandbox pod log capture started (pid $!)"
- name: Run Craft K8s tests (${{ matrix.test-file.name }})
shell: script -q -e -c "bash --noprofile --norc -eo pipefail {0}"
run: |
py.test \
--durations=8 \
-o junit_family=xunit2 \
-v \
--ff \
-m "not nightly" \
"${{ matrix.test-file.path }}"
- name: Collect diagnostics on failure
if: failure()
run: |
mkdir -p kind-logs
echo "::group::sandbox pod logs"
if compgen -G "sandbox-logs/*.log" > /dev/null; then
for f in sandbox-logs/*.log; do echo "--- ${f} ---"; cat "${f}" || true; done
else
echo "no sandbox pod logs were captured"
fi
echo "::endgroup::"
echo "::group::disk and docker status"
df -h || true; docker system df || true; docker images || true; ls -lah /tmp || true
echo "::endgroup::"
echo "::group::MinIO diagnostics"
kubectl -n "${HELM_RELEASE_NAMESPACE}" get pods,svc,job -l release=onyx || true
kubectl -n "${HELM_RELEASE_NAMESPACE}" logs deployment/onyx-minio --tail=500 || true
kubectl -n "${HELM_RELEASE_NAMESPACE}" logs job/onyx-minio-post-job --all-containers --tail=500 || true
python - <<'PY' || true
import os, boto3
from botocore.config import Config
s3 = boto3.client(
"s3",
endpoint_url=os.environ.get("S3_ENDPOINT_URL"),
aws_access_key_id=os.environ.get("S3_AWS_ACCESS_KEY_ID"),
aws_secret_access_key=os.environ.get("S3_AWS_SECRET_ACCESS_KEY"),
region_name=os.environ.get("AWS_REGION_NAME") or "us-east-1",
config=Config(s3={"addressing_style": "path"}),
)
print("buckets:", [b["Name"] for b in s3.list_buckets().get("Buckets", [])])
for b in s3.list_buckets().get("Buckets", []):
objs = s3.list_objects_v2(Bucket=b["Name"]).get("Contents", [])
print(b["Name"], "->", [o["Key"] for o in objs][:20])
PY
echo "::endgroup::"
echo "::group::chart resources and app logs"
helm status onyx -n "${HELM_RELEASE_NAMESPACE}" || true
for dep in onyx-api-server onyx-celery-worker-primary \
onyx-celery-worker-heavy onyx-celery-worker-scheduled-tasks \
onyx-cloudnative-pg redis-operator; do
kubectl -n "${HELM_RELEASE_NAMESPACE}" logs "deployment/${dep}" --all-containers --tail=500 || true
done
kubectl -n "${HELM_RELEASE_NAMESPACE}" logs statefulset/onyx-opensearch-master --all-containers --tail=500 || true
kubectl -n "${HELM_RELEASE_NAMESPACE}" get cluster,redis,statefulset,pods,svc,pvc || true
echo "::endgroup::"
echo "::group::port-forward logs"
for f in postgres redis minio api; do
echo "--- ${f} ---"; cat "${RUNNER_TEMP}/${f}-port-forward.log" 2>/dev/null || true
done
echo "::endgroup::"
kind export logs ./kind-logs --name onyx-craft-ci || true
kubectl get pods -A -o wide > kind-logs/pods.txt 2>&1 || true
kubectl get svc -A -o wide > kind-logs/services.txt 2>&1 || true
kubectl describe pods -n onyx-sandboxes > kind-logs/sandbox-pods-describe.txt 2>&1 || true
kubectl describe pods -n "${HELM_RELEASE_NAMESPACE}" > kind-logs/release-ns-pods-describe.txt 2>&1 || true
kubectl -n "${HELM_RELEASE_NAMESPACE}" logs -l app.kubernetes.io/component=sandbox-proxy --tail=500 > kind-logs/sandbox-proxy.log 2>&1 || true
cp "${RUNNER_TEMP}"/*-port-forward.log kind-logs/ 2>/dev/null || true
- name: Upload logs
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: craft-k8s-logs-${{ matrix.test-file.name }}
path: |
kind-logs/
sandbox-logs/
retention-days: 7
- name: Cleanup kind cluster and port-forwards
if: always()
run: |
set +e
for pid_file in \
"${RUNNER_TEMP}/postgres-port-forward.pid" \
"${RUNNER_TEMP}/redis-port-forward.pid" \
"${RUNNER_TEMP}/minio-port-forward.pid" \
"${RUNNER_TEMP}/api-port-forward.pid" \
"${RUNNER_TEMP}/sandbox-log-watcher.pid"; do
if [ -f "${pid_file}" ]; then
kill "$(cat "${pid_file}")" 2>/dev/null || true
fi
done
kind delete cluster --name onyx-craft-ci || true
docker network disconnect kind "${KIND_REGISTRY_NAME}" 2>/dev/null || true
docker rm -f "${KIND_REGISTRY_NAME}" 2>/dev/null || true
craft-k8s-tests-required:
# Single required status check for the craft-k8s suite. Always runs so
# branch protection has a stable target, and passes cleanly when `changes`
# reports no relevant paths changed (i.e. the test job was legitimately
# skipped).
runs-on: ubuntu-latest
timeout-minutes: 5
needs: [changes, discover-test-files, build-images, craft-k8s-tests]
if: ${{ always() }}
steps:
- name: Check job status
env:
CHANGES_RESULT: ${{ needs.changes.result }}
RUN_TESTS: ${{ needs.changes.outputs.craft_k8s }}
DISCOVER_RESULT: ${{ needs.discover-test-files.result }}
BUILD_RESULT: ${{ needs.build-images.result }}
TEST_RESULT: ${{ needs.craft-k8s-tests.result }}
run: |
# Fail closed if `changes` didn't succeed. Otherwise an empty
# RUN_TESTS (which is what we'd see when `changes` failed/cancelled)
# would be indistinguishable from "no relevant paths changed" and we
# would incorrectly pass the required check.
if [ "${CHANGES_RESULT}" != "success" ]; then
echo "changes job did not succeed (result: ${CHANGES_RESULT})"
exit 1
fi
if [ "${RUN_TESTS}" != "true" ]; then
echo "No relevant paths changed -- required check passes."
exit 0
fi
if [ "${DISCOVER_RESULT}" != "success" ] || [ "${BUILD_RESULT}" != "success" ]; then
echo "Setup results: discover-test-files=${DISCOVER_RESULT}, build-images=${BUILD_RESULT}"
exit 1
fi
if [ "${TEST_RESULT}" != "success" ]; then
echo "Test result: ${TEST_RESULT}"
exit 1
fi
echo "All tests passed."