Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 161 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
quality-checks:
name: Quality Checks
runs-on: ubuntu-latest

defaults:
run:
working-directory: ./code-interpreter

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Install uv
uses: astral-sh/setup-uv@v4
with:
version: "latest"

- name: Install dependencies
run: uv sync --locked

- name: Run mypy type checking
run: uv run mypy .

- name: Run ruff linting
run: uv run ruff check .

- name: Run ruff formatting check
run: uv run ruff format --check .

integration-tests:
name: Integration Tests
runs-on: ubuntu-latest

defaults:
run:
working-directory: ./code-interpreter

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Install uv
uses: astral-sh/setup-uv@v4
with:
version: "latest"

- name: Install dependencies
run: uv sync --locked

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Pull executor Docker image
run: docker pull onyxdotapp/python-executor-sci:latest

- name: Run integration tests
run: uv run pytest tests/integration_tests -v --tb=short -x

- name: Show Docker container logs on failure
if: failure()
run: |
echo "=== Docker containers ==="
docker ps -a
echo "=== Docker logs for all containers ==="
for container in $(docker ps -aq); do
echo "--- Logs for container $container ---"
docker logs $container || true
done

e2e-tests:
name: E2E Tests
runs-on: ubuntu-latest

defaults:
run:
working-directory: ./code-interpreter

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Install uv
uses: astral-sh/setup-uv@v4
with:
version: "latest"

- name: Install dependencies
run: uv sync --locked

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Build Docker image
run: docker build -t code-interpreter:test -f Dockerfile .
working-directory: ./code-interpreter

- name: Run Docker container for E2E tests
run: |
docker run -d --name code-interpreter-test \
-p 8000:8000 \
-e HOST=0.0.0.0 \
-e PORT=8000 \
-v /var/run/docker.sock:/var/run/docker.sock \
--user root \
code-interpreter:test

# Wait for service to be ready
echo "Waiting for service to start..."
for i in {1..30}; do
if curl -sf http://localhost:8000/health > /dev/null 2>&1; then
echo "Service is ready!"
curl -s http://localhost:8000/health
exit 0
fi
echo "Attempt $i/30: Service not ready yet..."
sleep 2
done

echo "ERROR: Service failed to start within 60 seconds"
echo "Container logs:"
docker logs code-interpreter-test
exit 1

- name: Run E2E tests
run: uv run pytest tests/e2e -q
env:
CODE_INTERPRETER_URL: http://localhost:8000

- name: Show container logs on failure
if: failure()
run: docker logs code-interpreter-test

- name: Stop and remove container
if: always()
run: |
docker stop code-interpreter-test || true
docker rm code-interpreter-test || true
86 changes: 86 additions & 0 deletions .github/workflows/docker-build-push.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
name: Build and Push Docker Images

on:
push:
tags:
- 'v*'
workflow_dispatch:

env:
DOCKERHUB_USERNAME: onyxdotapp

jobs:
build-executor:
name: Build and Push Executor Image
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ env.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.DOCKERHUB_USERNAME }}/python-executor-sci
tags: |
type=semver,pattern={{version}}
type=raw,value=latest

- name: Build and push executor image
uses: docker/build-push-action@v5
with:
context: ./executor
file: ./executor/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

build-code-interpreter:
name: Build and Push Code Interpreter Image
runs-on: ubuntu-latest
needs: build-executor

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ env.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.DOCKERHUB_USERNAME }}/code-interpreter
tags: |
type=semver,pattern={{version}}
type=raw,value=latest

- name: Build and push code-interpreter image
uses: docker/build-push-action@v5
with:
context: ./code-interpreter
file: ./code-interpreter/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
2 changes: 2 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ repos:
hooks:
- id: ruff
args: ["--fix", "--exit-non-zero-on-fix"]
- id: ruff-format
types_or: [ python, pyi ]

# We would like to have a mypy pre-commit hook, but due to the fact that
# pre-commit runs in it's own isolated environment, we would need to install
Expand Down
4 changes: 1 addition & 3 deletions code-interpreter/app/app_configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@
KUBERNETES_EXECUTOR_IMAGE = (
os.environ.get("KUBERNETES_EXECUTOR_IMAGE") or "onyxdotapp/python-executor-sci"
)
KUBERNETES_EXECUTOR_SERVICE_ACCOUNT = (
os.environ.get("KUBERNETES_EXECUTOR_SERVICE_ACCOUNT") or ""
)
KUBERNETES_EXECUTOR_SERVICE_ACCOUNT = os.environ.get("KUBERNETES_EXECUTOR_SERVICE_ACCOUNT") or ""

# Execution limits
MAX_EXEC_TIMEOUT_MS = int(os.environ.get("MAX_EXEC_TIMEOUT_MS") or 60_000)
Expand Down
5 changes: 2 additions & 3 deletions code-interpreter/app/services/executor_docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

logger = logging.getLogger(__name__)


class DockerExecutor(BaseExecutor):
def __init__(self) -> None:
self.docker_binary = self._resolve_docker_binary()
Expand Down Expand Up @@ -151,9 +152,7 @@ def _extract_workspace_snapshot(self, container_name: str) -> tuple[WorkspaceEnt
if file_obj:
content = file_obj.read()
entries.append(
WorkspaceEntry(
path=clean_path, kind="file", content=content
)
WorkspaceEntry(path=clean_path, kind="file", content=content)
)

return tuple(entries)
Expand Down
2 changes: 1 addition & 1 deletion code-interpreter/app/services/executor_kubernetes.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from pathlib import Path
from typing import Any

from kubernetes import client, config, stream # type: ignore
from kubernetes.client import ( # type: ignore[import-untyped]
V1Container,
V1ObjectMeta,
Expand All @@ -30,7 +31,6 @@
WorkspaceEntry,
wrap_last_line_interactive,
)
from kubernetes import client, config, stream # type: ignore[import-untyped, attr-defined]

logger = logging.getLogger(__name__)

Expand Down
5 changes: 2 additions & 3 deletions code-interpreter/app/services/file_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,9 +150,8 @@ def cleanup_expired_files(self, max_age_sec: int) -> int:
meta_dict = json.loads(metadata_path.read_text(encoding="utf-8"))
metadata = FileMetadata(**meta_dict)

if (
current_time - metadata.upload_time > max_age_sec
and self.delete_file(metadata.file_id)
if current_time - metadata.upload_time > max_age_sec and self.delete_file(
metadata.file_id
):
deleted_count += 1
except (json.JSONDecodeError, TypeError, FileNotFoundError):
Expand Down
3 changes: 3 additions & 0 deletions code-interpreter/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ target-version = "py311"
select = ["E", "F", "I", "UP", "B", "SIM", "ANN", "S"]
ignore = ["S603", "S607"]

[tool.ruff.lint.isort]
known-third-party = ["kubernetes"]

[tool.ruff.lint.per-file-ignores]
"tests/**" = ["S101"]

Expand Down
4 changes: 1 addition & 3 deletions code-interpreter/tests/e2e/test_basic_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,7 @@ def test_execute_edits_passed_file() -> None:

# Upload the file to be edited
initial_content = "Hello World\nThis is line 2\nThis is line 3"
upload_files = {
"file": ("input.txt", initial_content.encode("utf-8"), "text/plain")
}
upload_files = {"file": ("input.txt", initial_content.encode("utf-8"), "text/plain")}

try:
upload_response = client.post("/v1/files", files=upload_files)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def test_numpy_pandas_matplotlib_stack() -> None:
json={
"code": code,
"stdin": None,
"timeout_ms": 2000,
"timeout_ms": 5000,
},
)

Expand Down