-
Notifications
You must be signed in to change notification settings - Fork 4.4k
Expand file tree
/
Copy pathpr-craft-compose-integration.yml
More file actions
460 lines (427 loc) · 18.4 KB
/
Copy pathpr-craft-compose-integration.yml
File metadata and controls
460 lines (427 loc) · 18.4 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
# Stands up the full Craft docker-compose stack with the --include-craft overlay
# and exercises the Docker-backed Craft E2E suite: approval-gate flow, workspace
# setup, and docker-specific posture invariants the K8s lane can't catch by
# construction (image ENTRYPOINT concat, HOME after setpriv, curl httpoxy on the
# bridge).
#
# Images are built once and pushed to ECR; the lane shards per test file, each
# shard pulling the prebuilt images onto its own compose stack.
#
# Always triggers on PRs + merge_group so the `craft-compose-required` job below
# can serve as the single stable required status check in branch protection; the
# heavy test job is gated internally by the `changes` job and is a no-op
# when no relevant paths changed. Also runs nightly, on release tags, and on
# demand.
name: Craft Docker-Compose Integration
concurrency:
group: Craft-Compose-Integration-${{ github.workflow }}-${{ 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. We always trigger and let the
# `changes` job below decide whether the real test job runs.
push:
tags:
- "v*.*.*"
workflow_dispatch:
permissions:
contents: read
env:
# Pin S3_ENDPOINT_URL defensively. The runner env is usually clean, but we hit
# a leak during local smoke (host shell exported localhost:9004, compose's
# ${VAR:-default} picked it over --env-file).
S3_ENDPOINT_URL: "http://minio:9000"
SANDBOX_BACKEND: "docker"
ENABLE_CRAFT: "true"
POSTGRES_PASSWORD: "password"
POSTGRES_USER: "postgres"
IMAGE_TAG: "latest"
# Pin to a CI-only tag so the locally-built sandbox image is what compose
# passes to api_server, and what DockerSandboxManager.provision then loads
# into each sandbox container.
SANDBOX_CONTAINER_IMAGE: "onyxdotapp/sandbox:ci"
# Read by api_server on boot; Sandbox containers call back via this URL on the
# compose bridge.
ONYX_SERVER_URL: "http://api_server:8080"
jobs:
changes:
# Decides whether the heavy integration job runs. 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 everything runs.
runs-on: ubuntu-latest
timeout-minutes: 5
# paths-filter needs pull-requests:read to list PR files on private repos.
permissions:
contents: read
pull-requests: read
outputs:
craft_compose: ${{ steps.filter.outputs.craft_compose || '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_compose:
- 'backend/Dockerfile'
- 'backend/onyx/sandbox_proxy/**'
- 'backend/onyx/server/features/build/**'
- 'backend/onyx/skills/**'
- 'deployment/docker_compose/docker-compose.yml'
- 'deployment/docker_compose/docker-compose.dev.yml'
- 'deployment/docker_compose/docker-compose.craft.yml'
- 'backend/tests/integration/conftest.py'
- 'backend/tests/integration/common_utils/**'
- 'backend/tests/integration/tests/craft/*.py'
- 'backend/tests/integration/tests/craft/docker_e2e/**'
- '.github/workflows/pr-craft-compose-integration.yml'
- '.github/actions/setup-test-license/**'
- '.github/actions/setup-python-and-install-dependencies/**'
- '.github/actions/login-ecr-pullthrough-cache/**'
discover-test-files:
# One shard per craft compose test file (top-level craft/ + docker_e2e/,
# excluding the k8s/ subdir).
needs: changes
if: needs.changes.outputs.craft_compose == '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 compose test files
id: set-matrix
run: |
set -eo pipefail
# `path` is relative to backend/ (the test job's working-directory).
matrix_file="${RUNNER_TEMP}/craft-compose-test-files.jsonl"
: > "${matrix_file}"
{
find backend/tests/integration/tests/craft \
-maxdepth 1 -name 'test_*.py' -type f -print0
find backend/tests/integration/tests/craft/docker_e2e \
-maxdepth 1 -name 'test_*.py' -type f -print0
} | sort -z | while IFS= read -r -d '' f; do
base=$(basename "${f}" .py)
shard="${base#test_}"
rel="${f#backend/}"
jq -cn --arg path "${rel}" --arg name "${shard}" \
'{path: $path, name: $name}' >> "${matrix_file}"
done
if [ ! -s "${matrix_file}" ]; then
echo "::error::no craft compose test files discovered"
exit 1
fi
echo "test-files=$(jq -cs . "${matrix_file}")" >> "$GITHUB_OUTPUT"
build-images:
# Build the 2 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_compose == '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 than the
# skill bundle and keeps the CI image consistent with prod.
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-compose-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-compose-${{ 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-compose-${{ matrix.image }}
cache-to: type=gha,scope=craft-compose-${{ matrix.image }},mode=max
craft-compose-integration-test:
name: craft-compose (${{ matrix.test-file.name }})
needs: [changes, discover-test-files, build-images]
if: needs.changes.outputs.craft_compose == 'true'
permissions:
contents: read
id-token: write
runs-on:
- runs-on
- runner=4cpu-linux-x64
- spot=false
- volume=100gb
- ${{ format('run-id={0}-craft-compose-tests-{1}', github.run_id, matrix.test-file.name) }}
- extras=ecr-cache
timeout-minutes: 30
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) }}
env:
PYTHONPATH: ./backend
DISABLE_TELEMETRY: "true"
steps:
- uses: runs-on/action@4e5f72399b6b17f2e79c511c1b38a315a64d22dc
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
with:
persist-credentials: false
- name: Fetch dev license
uses: ./.github/actions/setup-test-license
with:
aws-oidc-role-arn: ${{ secrets.AWS_OIDC_ROLE_ARN }}
- name: Setup Python + deps
uses: ./.github/actions/setup-python-and-install-dependencies
with:
requirements: |
backend/requirements/default.txt
backend/requirements/dev.txt
backend/requirements/ee.txt
- name: Log in to ECR pull-through cache
uses: ./.github/actions/login-ecr-pullthrough-cache
with:
ecr-registry: ${{ vars.ECR_REGISTRY }}
# Retag the prebuilt ECR images to the local names compose expects (compose
# consumes local docker images directly, so pull+tag is enough).
- name: Pull and tag prebuilt images
env:
ECR_CACHE: ${{ env.RUNS_ON_ECR_CACHE }}
RUN_ID: ${{ github.run_id }}
run: |
set -eo pipefail
retag() {
local src="$1" dst="$2"
docker pull "$src"
docker tag "$src" "$dst"
}
retag "${ECR_CACHE}:craft-compose-sandbox-${RUN_ID}" "${SANDBOX_CONTAINER_IMAGE}"
retag "${ECR_CACHE}:craft-compose-backend-${RUN_ID}" "onyxdotapp/onyx-backend:latest"
- name: Create compose-external resources
# docker-compose.craft.yml declares both as external. In prod install.sh
# --include-craft creates them, but CI doesn't run install.sh.
run: |
docker network create onyx_craft_sandbox
docker volume create sandbox_proxy_ca
# CI skips install.sh, so these have to be added explicitly.
- name: Provide USER_AUTH_SECRET + upload caps (.env)
working-directory: deployment/docker_compose
run: |
{
echo 'USER_AUTH_SECRET=craft-compose-ci-only-dummy-secret'
echo 'BUILD_MAX_UPLOAD_FILE_SIZE_MB=2'
echo 'BUILD_MAX_UPLOAD_FILES_PER_SESSION=5'
echo 'BUILD_MAX_TOTAL_UPLOAD_SIZE_MB=4'
echo 'USER_LIBRARY_MAX_FILES_PER_UPLOAD=5'
echo 'SANDBOX_IDLE_CLEANUP_INTERVAL_SECONDS=10'
} > .env
# Raw docker compose (ods compose has no craft overlay yet). Only
# api_server/background/sandbox-proxy are named; depends_on pulls in db /
# cache / opensearch / model servers / minio. web_server + nginx omitted.
- name: Bring up the stack (yml + dev + craft)
working-directory: deployment/docker_compose
run: |
docker compose \
-f docker-compose.yml \
-f docker-compose.dev.yml \
-f docker-compose.craft.yml \
--env-file env.template \
up -d --wait --wait-timeout 180 \
api_server background sandbox-proxy
- name: Sanity check stack health
# --fail: without it, curl exits 0 on 4xx/5xx and the step would accept
# an unhealthy api_server. -w still emits on --fail.
run: |
docker ps --format 'table {{.Names}}\t{{.Status}}'
curl --fail -sS -o /dev/null -w "api_server /health -> %{http_code}\n" \
http://localhost:8080/health
- name: Seed dev license
run: |
docker exec \
-e ONYX_DEV_LICENSE="${ONYX_DEV_LICENSE}" \
onyx-api_server-1 \
python -m scripts.seed_dev_license
- name: Run integration tests
timeout-minutes: 25
shell: bash
working-directory: backend
env:
API_SERVER_HOST: "127.0.0.1"
API_SERVER_PORT: "8080"
POSTGRES_DB: "postgres"
POSTGRES_HOST: "127.0.0.1"
POSTGRES_PORT: "5432"
REDIS_HOST: "127.0.0.1"
REDIS_PORT: "6379"
S3_ENDPOINT_URL: "http://127.0.0.1:9004"
S3_AWS_ACCESS_KEY_ID: "minioadmin"
S3_AWS_SECRET_ACCESS_KEY: "minioadmin"
# onyx.* reads these at import; DockerSandboxManager needs the
# proxy/docker env. USER_AUTH_SECRET must match the stack's .env.
ENABLE_CRAFT: "true"
ENABLE_PAID_ENTERPRISE_EDITION_FEATURES: "true"
USER_AUTH_SECRET: "craft-compose-ci-only-dummy-secret"
SANDBOX_BACKEND: "docker"
SANDBOX_PROXY_HOST: "sandbox-proxy"
SANDBOX_PROXY_PORT: "8080"
SANDBOX_DOCKER_NETWORK: "onyx_craft_sandbox"
SANDBOX_DOCKER_SOCKET: "/var/run/docker.sock"
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
set -o pipefail
mkdir -p ../compose-logs
PYTHONUNBUFFERED=1 stdbuf -oL -eL py.test \
-v \
--tb=short \
-rs \
--durations=10 \
"${{ matrix.test-file.path }}" \
2>&1 | tee ../compose-logs/pytest.log
- name: Stack state + log tails on failure
if: ${{ !success() }}
run: |
echo "=== docker ps -a ==="; docker ps -a || true
echo "=== api_server tail ==="; docker logs --tail 200 onyx-api_server-1 || true
echo "=== sandbox-proxy tail ==="; docker logs --tail 200 onyx-sandbox-proxy-1 || true
echo "=== background tail ==="; docker logs --tail 100 onyx-background-1 || true
echo "=== sandbox-* tails ==="; \
for c in $(docker ps -aq --filter "name=sandbox-" | grep -v sandbox-proxy || true); do \
echo "--- $c ---"; docker logs --tail 200 "$c" || true; \
done
- name: Collect Docker logs on failure
if: ${{ !success() }}
run: |
mkdir -p compose-logs
# Compose-managed services (api_server, background, sandbox-proxy
# and their depends_on graph).
cd deployment/docker_compose
for cid in $(docker compose \
-f docker-compose.yml \
-f docker-compose.dev.yml \
-f docker-compose.craft.yml \
ps -aq); do
name=$(docker inspect --format='{{.Name}}' "$cid" | sed 's/^\///')
docker logs "$cid" > "../../compose-logs/${name}.log" 2>&1 || true
done
# Test-provisioned sandbox containers (not in any compose file --
# DockerSandboxManager creates them at runtime).
cd ../..
for cid in $(docker ps -aq --filter "name=sandbox-" \
| grep -v sandbox-proxy || true); do
name=$(docker inspect --format='{{.Name}}' "$cid" | sed 's/^\///')
docker logs "$cid" > "compose-logs/${name}.log" 2>&1 || true
done
- name: Upload logs
if: ${{ !success() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: craft-compose-logs-${{ matrix.test-file.name }}
path: compose-logs/
retention-days: 7
# -v drops the project's named volumes; the external sandbox_proxy_ca /
# onyx_craft_sandbox are removed explicitly below.
- name: Teardown
if: always()
working-directory: deployment/docker_compose
run: |
docker compose \
-f docker-compose.yml \
-f docker-compose.dev.yml \
-f docker-compose.craft.yml \
down -v || true
docker volume rm sandbox_proxy_ca || true
docker network rm onyx_craft_sandbox || true
craft-compose-integration-required:
# Single required status check for the craft-compose integration 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-compose-integration-test]
if: ${{ always() }}
steps:
- name: Check job status
env:
CHANGES_RESULT: ${{ needs.changes.result }}
RUN_TESTS: ${{ needs.changes.outputs.craft_compose }}
DISCOVER_RESULT: ${{ needs.discover-test-files.result }}
BUILD_RESULT: ${{ needs.build-images.result }}
TEST_RESULT: ${{ needs.craft-compose-integration-test.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."