Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
ea55011
feat: misc changes for quicker build and dpeloy times
HardMax71 Feb 25, 2026
d2e6b1a
fix: fixture for user in frontend/unit tests
HardMax71 Feb 25, 2026
5ff60e7
fix: deploy issue
HardMax71 Feb 25, 2026
b562d73
fix: quicker frontend/unit tests
HardMax71 Feb 25, 2026
98cdac2
fix: quicker frontend/unit tests
HardMax71 Feb 25, 2026
274bf84
fix: quicker frontend/unit tests - independent unit tests
HardMax71 Feb 26, 2026
9cf05ae
feat: pulling store refactor for specific components
HardMax71 Feb 26, 2026
56a19d8
feat: pulling store refactor for specific components
HardMax71 Feb 26, 2026
cba41e2
fix: in e2e-ready - pre-pull of python3.11 image
HardMax71 Feb 26, 2026
0efeb04
fix: timeout check for liveness of kube in e2e-ready
HardMax71 Feb 26, 2026
4285cd2
fix: test timeout fix
HardMax71 Feb 26, 2026
7caa7c5
fix: removed test completely
HardMax71 Feb 26, 2026
726143c
fix: event waiter + brought back failing test
HardMax71 Feb 26, 2026
7198b0b
fix: meddling of workers into stuff of each other -> adding kafka suffix
HardMax71 Feb 26, 2026
9d50921
fix: teardown improvement
HardMax71 Feb 26, 2026
3a28d86
Merge remote-tracking branch 'origin/feat/lower-img-sizes' into feat/…
HardMax71 Feb 26, 2026
90068f9
fix: pytest-timeout try
HardMax71 Feb 27, 2026
a476dcf
fix: pytest-timeout try
HardMax71 Feb 27, 2026
20feb21
fix: removed xx.aclose in tests
HardMax71 Feb 27, 2026
957464b
feat: deps groups in backend
HardMax71 Feb 27, 2026
162b9a3
feat: conftest better fix
HardMax71 Feb 27, 2026
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
8 changes: 5 additions & 3 deletions .github/actions/e2e-ready/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ runs:
sudo k3s kubectl config view --raw > /home/runner/.kube/config
sudo chmod 600 /home/runner/.kube/config
export KUBECONFIG=/home/runner/.kube/config
timeout 90 bash -c 'until kubectl cluster-info 2>/dev/null; do sleep 3; done'
kubectl wait --for=condition=Ready node --all --timeout=90s
Comment thread
HardMax71 marked this conversation as resolved.
kubectl create namespace integr8scode --dry-run=client -o yaml | kubectl apply -f -
sed -E 's#https://(127\.0\.0\.1|0\.0\.0\.0):6443#https://host.docker.internal:6443#g' \
/home/runner/.kube/config > backend/kubeconfig.yaml
Expand All @@ -32,9 +32,11 @@ runs:
cp backend/config.test.toml backend/config.toml
cp backend/secrets.example.toml backend/secrets.toml

- name: Pre-pull test runtime image into K3s
- name: Pre-pull test runtime images into K3s
shell: bash
run: sudo k3s crictl pull docker.io/library/python:3.11-slim
run: |
sudo k3s crictl pull docker.io/library/python:3.11-slim
sudo k3s crictl pull docker.io/library/busybox:1.36

- name: Wait for image pull and infra
shell: bash
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:
run: uv tool install mkdocs --with mkdocs-material --with mkdocs-mermaid2-plugin --with mkdocs-swagger-ui-tag

- name: Install backend dependencies
run: cd backend && uv sync --frozen
run: cd backend && uv sync --frozen --no-dev

- name: Set up config for OpenAPI generation
run: cp backend/secrets.example.toml backend/secrets.toml
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/mypy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
run: |
cd backend
uv python install 3.12
uv sync --frozen
uv sync --frozen --group lint --no-dev

- name: Run mypy
env:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ruff.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
run: |
cd backend
uv python install 3.12
uv sync --frozen
uv sync --frozen --group lint --no-dev

- name: Run ruff
run: |
Expand Down
10 changes: 6 additions & 4 deletions .github/workflows/stack-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ jobs:
run: |
cd backend
uv python install 3.12
uv sync --frozen
uv sync --frozen --group test --no-dev

- name: Set up config
run: cp backend/secrets.example.toml backend/secrets.toml
Expand Down Expand Up @@ -170,7 +170,9 @@ jobs:
# ── Backend (depends on local base image) ───────────────
- name: Build backend image
run: |
docker build -t integr8scode-backend:latest --build-context base=docker-image://integr8scode-base:latest -f ./backend/Dockerfile ./backend
docker build -t integr8scode-backend:latest \
--build-context base=docker-image://integr8scode-base:latest \
-f ./backend/Dockerfile ./backend

# ── Utility images (GHA-cached, independent of base) ────────────
- name: Build cert-generator image
Expand Down Expand Up @@ -248,11 +250,11 @@ jobs:
timeout-minutes: 15
run: |
docker compose exec -T backend \
uv run pytest tests/e2e -v -rs \
sh -c 'uv sync --group test --no-dev --frozen --no-install-project && pytest tests/e2e -v -rs \
--durations=0 \
--cov=app \
--cov-report=xml:coverage-e2e.xml \
--cov-report=term
--cov-report=term'

- name: Copy coverage
if: always()
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/vulture.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
run: |
cd backend
uv python install 3.12
uv sync --frozen
uv sync --frozen --group lint --no-dev

- name: Run vulture
run: |
Expand Down
15 changes: 15 additions & 0 deletions backend/.dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,18 @@ htmlcov/
# Local dev files
*.log
.DS_Store

# Tests and docs (not needed in production image)
tests/
docs/
*.md

# Secrets and test/override configs (not needed in image)
secrets.toml
secrets.*.toml
config.test.toml
config.*.toml
!config.toml

# Dead code detection whitelist
vulture_whitelist.py
38 changes: 32 additions & 6 deletions backend/Dockerfile.base
Original file line number Diff line number Diff line change
@@ -1,26 +1,52 @@
# Shared base image for all backend services
# Contains: Python, system deps, uv, and all Python dependencies
FROM python:3.12-slim
# Multi-stage build: gcc + dev headers only in builder, not in final image

FROM python:3.12-slim AS builder

WORKDIR /app

# Install OS security patches + system dependencies needed by any service
RUN apt-get update && apt-get upgrade -y \
# Install build-time dependencies (gcc + dev headers for C extensions)
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
gcc \
curl \
libsnappy-dev \
liblzma-dev \
&& rm -rf /var/lib/apt/lists/*

# Install uv (using Docker Hub mirror - ghcr.io has rate limiting issues)
COPY --from=astral/uv:latest /uv /uvx /bin/

# Pre-compile bytecode for faster startup; copy mode avoids symlink issues with cache mounts
ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy

# Copy dependency files
COPY pyproject.toml uv.lock ./

# Install Python dependencies (production only)
RUN uv sync --locked --no-dev --no-install-project
# Install Python dependencies with BuildKit cache mount for faster rebuilds
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked --no-dev --no-install-project

FROM python:3.12-slim

WORKDIR /app

# Install only runtime dependencies (shared libs, no -dev headers, no gcc)
RUN apt-get update && apt-get upgrade -y \
&& apt-get install -y --no-install-recommends \
curl \
libsnappy1v5 \
liblzma5 \
&& rm -rf /var/lib/apt/lists/*

# Copy uv from builder (avoids second Docker Hub pull, guarantees version consistency)
COPY --from=builder /bin/uv /bin/uvx /bin/

# Copy pre-built virtual environment from builder stage
COPY --from=builder /app/.venv /app/.venv

# Copy dependency files (needed for uv to recognize the project)
COPY pyproject.toml uv.lock ./

# Set paths: PYTHONPATH for imports, PATH for venv binaries (no uv run needed at runtime)
ENV PYTHONPATH=/app
Expand Down
28 changes: 0 additions & 28 deletions backend/Dockerfile.test

This file was deleted.

30 changes: 25 additions & 5 deletions backend/app/api/routes/admin/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from dishka.integrations.fastapi import DishkaRoute
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query
from fastapi.responses import StreamingResponse
from sse_starlette.sse import EventSourceResponse

from app.api.dependencies import admin_user
from app.domain.enums import EventType, ExportFormat
Expand All @@ -24,6 +25,16 @@
)
from app.schemas_pydantic.common import ErrorResponse
from app.services.admin import AdminEventsService
from app.services.sse import SSEService


class _SSEResponse(EventSourceResponse):
"""Workaround: sse-starlette sets media_type only in __init__, not as a
class attribute. FastAPI reads the class attribute for OpenAPI generation,
so without this subclass every SSE endpoint shows application/json."""

media_type = "text/event-stream"


router = APIRouter(
prefix="/admin/events", tags=["admin-events"], route_class=DishkaRoute, dependencies=[Depends(admin_user)]
Expand Down Expand Up @@ -133,16 +144,25 @@ async def replay_events(

@router.get(
"/replay/{session_id}/status",
responses={404: {"model": ErrorResponse, "description": "Replay session not found"}},
response_class=_SSEResponse,
responses={
200: {"model": EventReplayStatusResponse},
404: {"model": ErrorResponse, "description": "Replay session not found"},
},
)
async def get_replay_status(session_id: str, service: FromDishka[AdminEventsService]) -> EventReplayStatusResponse:
"""Get the status and progress of a replay session."""
status = await service.get_replay_status(session_id)
async def stream_replay_status(
session_id: str,
service: FromDishka[AdminEventsService],
sse_service: FromDishka[SSEService],
) -> EventSourceResponse:
"""Stream the status and progress of a replay session via SSE."""
status = await service.get_replay_sse_status(session_id)

if not status:
raise HTTPException(status_code=404, detail="Replay session not found")

return EventReplayStatusResponse.model_validate(status)
stream = await sse_service.create_replay_stream(status)
return EventSourceResponse(stream, ping=15)


@router.delete("/{event_id}", responses={404: {"model": ErrorResponse, "description": "Event not found"}})
Expand Down
3 changes: 2 additions & 1 deletion backend/app/api/routes/saga.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,12 @@ async def list_sagas(
user: Annotated[User, Depends(current_user)],
saga_service: FromDishka[SagaService],
state: Annotated[SagaState | None, Query(description="Filter by saga state")] = None,
execution_id: Annotated[str | None, Query(description="Filter by execution ID")] = None,
limit: Annotated[int, Query(ge=1, le=1000)] = 100,
skip: Annotated[int, Query(ge=0)] = 0,
) -> SagaListResponse:
"""List sagas accessible by the current user."""
result = await saga_service.list_user_sagas(user, state, limit, skip)
result = await saga_service.list_user_sagas(user, state, execution_id=execution_id, limit=limit, skip=skip)
return SagaListResponse.model_validate(result)


Expand Down
1 change: 1 addition & 0 deletions backend/app/core/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ def create_event_replay_container(settings: Settings) -> AsyncContainer:
RepositoryProvider(),
MessagingProvider(),
DLQProvider(),
SSEProvider(),
EventReplayProvider(),
context={Settings: settings},
)
Expand Down
3 changes: 3 additions & 0 deletions backend/app/core/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ def get_broker(
logger=logger,
client_id=f"integr8scode-{settings.SERVICE_NAME}",
request_timeout_ms=settings.KAFKA_REQUEST_TIMEOUT_MS,
graceful_timeout=settings.KAFKA_GRACEFUL_TIMEOUT,
middlewares=(KafkaTelemetryMiddleware(),),
)
logger.info("Kafka broker created")
Expand Down Expand Up @@ -725,10 +726,12 @@ def get_event_replay_service(
kafka_producer: UnifiedProducer,
replay_metrics: ReplayMetrics,
logger: structlog.stdlib.BoundLogger,
sse_bus: SSERedisBus,
) -> EventReplayService:
return EventReplayService(
repository=replay_repository,
producer=kafka_producer,
replay_metrics=replay_metrics,
logger=logger,
sse_bus=sse_bus,
)
25 changes: 25 additions & 0 deletions backend/app/db/repositories/admin/admin_events_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@
ResourceUsageDomain,
UserEventCount,
)
from app.domain.replay import ReplayError as DomainReplayError
from app.domain.replay import ReplayFilter
from app.domain.sse import DomainReplaySSEPayload


class AdminEventsRepository:
Expand Down Expand Up @@ -233,6 +235,29 @@ async def update_replay_session(self, session_id: str, updates: ReplaySessionUpd
async def get_replay_session_doc(self, session_id: str) -> ReplaySessionDocument | None:
return await ReplaySessionDocument.find_one(ReplaySessionDocument.session_id == session_id)

async def get_replay_session_sse_status(self, session_id: str) -> DomainReplaySSEPayload | None:
doc = await self.get_replay_session_doc(session_id)
if not doc:
return None
return DomainReplaySSEPayload(
session_id=doc.session_id,
status=doc.status,
total_events=doc.total_events,
replayed_events=doc.replayed_events,
failed_events=doc.failed_events,
skipped_events=doc.skipped_events,
replay_id=doc.replay_id,
created_at=doc.created_at,
started_at=doc.started_at,
completed_at=doc.completed_at,
errors=[
e if isinstance(e, DomainReplayError)
else DomainReplayError(**e) if isinstance(e, dict)
else DomainReplayError(**dataclasses.asdict(e))
for e in doc.errors
],
)

async def get_execution_results_for_filter(
self, replay_filter: ReplayFilter,
) -> list[ExecutionResultSummary]:
Expand Down
4 changes: 3 additions & 1 deletion backend/app/domain/sse/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from .models import (
DomainNotificationSSEPayload,
DomainReplaySSEPayload,
SSEExecutionEventData,
)

__all__ = [
"SSEExecutionEventData",
"DomainNotificationSSEPayload",
"DomainReplaySSEPayload",
"SSEExecutionEventData",
]
19 changes: 19 additions & 0 deletions backend/app/domain/sse/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
NotificationChannel,
NotificationSeverity,
NotificationStatus,
ReplayStatus,
SSEControlEvent,
)
from app.domain.execution.models import ExecutionResultDomain
from app.domain.replay import ReplayError


@dataclass
Expand Down Expand Up @@ -45,3 +47,20 @@ class DomainNotificationSSEPayload:
tags: list[str] = field(default_factory=list)
channel: NotificationChannel = NotificationChannel.IN_APP
read_at: datetime | None = None


@dataclass
class DomainReplaySSEPayload:
"""Replay session status payload for Redis transport and SSE wire."""

session_id: str
status: ReplayStatus
total_events: int
replayed_events: int
failed_events: int
skipped_events: int
replay_id: str
created_at: datetime
started_at: datetime | None = None
completed_at: datetime | None = None
errors: list[ReplayError] = field(default_factory=list)
Loading
Loading