diff --git a/.github/workflows/release-server-container.yml b/.github/workflows/release-server-container.yml new file mode 100644 index 00000000..9afb3bd5 --- /dev/null +++ b/.github/workflows/release-server-container.yml @@ -0,0 +1,164 @@ +name: Release Server Container + +on: + workflow_run: + workflows: ["Release to PyPI"] + types: [completed] + branches: [main] + workflow_dispatch: + inputs: + version: + description: 'Graphiti core version to build (e.g., 0.22.1)' + required: false + +env: + REGISTRY: docker.io + IMAGE_NAME: zepai/graphiti + +jobs: + build-and-push: + runs-on: depot-ubuntu-24.04-small + if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }} + permissions: + contents: write + id-token: write + environment: + name: release + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.event.workflow_run.head_sha || github.ref }} + + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install uv + uses: astral-sh/setup-uv@v3 + with: + version: "latest" + + - name: Extract version + id: version + run: | + if [ "${{ github.event_name }}" == "workflow_dispatch" ] && [ -n "${{ github.event.inputs.version }}" ]; then + VERSION="${{ github.event.inputs.version }}" + echo "Using manual input version: $VERSION" + else + # When triggered by workflow_run, get the tag that triggered the PyPI release + # The PyPI workflow is triggered by tags matching v*.*.* + VERSION=$(git tag --points-at HEAD | grep '^v[0-9]' | head -1 | sed 's/^v//') + + if [ -z "$VERSION" ]; then + # Fallback: check pyproject.toml version + VERSION=$(uv run python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])") + echo "Version from pyproject.toml: $VERSION" + else + echo "Version from git tag: $VERSION" + fi + + if [ -z "$VERSION" ]; then + echo "Could not determine version" + exit 1 + fi + fi + + # Validate it's a stable release - catch all Python pre-release patterns + # Matches: pre, rc, alpha, beta, a1, b2, dev0, etc. + if [[ $VERSION =~ (pre|rc|alpha|beta|a[0-9]+|b[0-9]+|\.dev[0-9]*) ]]; then + echo "Skipping pre-release version: $VERSION" + echo "skip=true" >> $GITHUB_OUTPUT + exit 0 + fi + + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "skip=false" >> $GITHUB_OUTPUT + + - name: Wait for PyPI availability + if: steps.version.outputs.skip != 'true' + run: | + VERSION="${{ steps.version.outputs.version }}" + echo "Checking PyPI for graphiti-core version $VERSION..." + + MAX_ATTEMPTS=10 + SLEEP_TIME=30 + + for i in $(seq 1 $MAX_ATTEMPTS); do + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "https://pypi.org/pypi/graphiti-core/$VERSION/json") + + if [ "$HTTP_CODE" == "200" ]; then + echo "✓ graphiti-core $VERSION is available on PyPI" + exit 0 + fi + + echo "Attempt $i/$MAX_ATTEMPTS: graphiti-core $VERSION not yet available (HTTP $HTTP_CODE)" + + if [ $i -lt $MAX_ATTEMPTS ]; then + echo "Waiting ${SLEEP_TIME}s before retry..." + sleep $SLEEP_TIME + fi + done + + echo "ERROR: graphiti-core $VERSION not available on PyPI after $MAX_ATTEMPTS attempts" + exit 1 + + - name: Log in to Docker Hub + if: steps.version.outputs.skip != 'true' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set up Depot CLI + if: steps.version.outputs.skip != 'true' + uses: depot/setup-action@v1 + + - name: Extract metadata + if: steps.version.outputs.skip != 'true' + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=raw,value=${{ steps.version.outputs.version }} + type=raw,value=latest + labels: | + org.opencontainers.image.title=Graphiti FastAPI Server + org.opencontainers.image.description=FastAPI server for Graphiti temporal knowledge graphs + org.opencontainers.image.version=${{ steps.version.outputs.version }} + io.graphiti.core.version=${{ steps.version.outputs.version }} + + - name: Build and push Docker image + if: steps.version.outputs.skip != 'true' + uses: depot/build-push-action@v1 + with: + project: v9jv1mlpwc + context: . + file: ./Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: | + GRAPHITI_VERSION=${{ steps.version.outputs.version }} + BUILD_DATE=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }} + VCS_REF=${{ github.sha }} + + - name: Summary + if: steps.version.outputs.skip != 'true' + run: | + echo "## 🚀 Server Container Released" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- **Version**: ${{ steps.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY + echo "- **Image**: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" >> $GITHUB_STEP_SUMMARY + echo "- **Tags**: ${{ steps.version.outputs.version }}, latest" >> $GITHUB_STEP_SUMMARY + echo "- **Platforms**: linux/amd64, linux/arm64" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Pull the image:" >> $GITHUB_STEP_SUMMARY + echo '```bash' >> $GITHUB_STEP_SUMMARY + echo "docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY diff --git a/Dockerfile b/Dockerfile index 6e11a253..b07e53a0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,40 +1,22 @@ # syntax=docker/dockerfile:1.9 -FROM python:3.12-slim as builder - -WORKDIR /app - -# Install system dependencies for building -RUN apt-get update && apt-get install -y --no-install-recommends \ - gcc \ - curl \ - ca-certificates \ - && rm -rf /var/lib/apt/lists/* - -# Install uv using the installer script -ADD https://astral.sh/uv/install.sh /uv-installer.sh -RUN sh /uv-installer.sh && rm /uv-installer.sh -ENV PATH="/root/.local/bin:$PATH" - -# Configure uv for optimal Docker usage -ENV UV_COMPILE_BYTECODE=1 \ - UV_LINK_MODE=copy \ - UV_PYTHON_DOWNLOADS=never - -# Copy and build main graphiti-core project -COPY ./pyproject.toml ./README.md ./ -COPY ./graphiti_core ./graphiti_core - -# Build graphiti-core wheel -RUN --mount=type=cache,target=/root/.cache/uv \ - uv build - -# Install the built wheel to make it available for server -RUN --mount=type=cache,target=/root/.cache/uv \ - pip install dist/*.whl - -# Runtime stage - build the server here FROM python:3.12-slim +# Inherit build arguments for labels +ARG GRAPHITI_VERSION +ARG BUILD_DATE +ARG VCS_REF + +# OCI image annotations +LABEL org.opencontainers.image.title="Graphiti FastAPI Server" +LABEL org.opencontainers.image.description="FastAPI server for Graphiti temporal knowledge graphs" +LABEL org.opencontainers.image.version="${GRAPHITI_VERSION}" +LABEL org.opencontainers.image.created="${BUILD_DATE}" +LABEL org.opencontainers.image.revision="${VCS_REF}" +LABEL org.opencontainers.image.vendor="Zep AI" +LABEL org.opencontainers.image.source="https://github.com/getzep/graphiti" +LABEL org.opencontainers.image.documentation="https://github.com/getzep/graphiti/tree/main/server" +LABEL io.graphiti.core.version="${GRAPHITI_VERSION}" + # Install uv using the installer script RUN apt-get update && apt-get install -y --no-install-recommends \ curl \ @@ -53,28 +35,29 @@ ENV UV_COMPILE_BYTECODE=1 \ # Create non-root user RUN groupadd -r app && useradd -r -d /app -g app app -# Copy graphiti-core wheel from builder -COPY --from=builder /app/dist/*.whl /tmp/ - -# Install graphiti-core wheel first -RUN --mount=type=cache,target=/root/.cache/uv \ - uv pip install --system /tmp/*.whl - -# Set up the server application +# Set up the server application first WORKDIR /app COPY ./server/pyproject.toml ./server/README.md ./server/uv.lock ./ COPY ./server/graph_service ./graph_service -# Install server dependencies and application -RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --frozen --no-dev - -# Install falkordb if requested +# Install server dependencies (without graphiti-core from lockfile) +# Then install graphiti-core from PyPI at the desired version +# This prevents the stale lockfile from pinning an old graphiti-core version ARG INSTALL_FALKORDB=false RUN --mount=type=cache,target=/root/.cache/uv \ - if [ "$INSTALL_FALKORDB" = "true" ]; then \ - WHEEL=$(ls /tmp/*.whl | head -n 1); \ - uv pip install "$WHEEL[falkordb]"; \ + uv sync --frozen --no-dev && \ + if [ -n "$GRAPHITI_VERSION" ]; then \ + if [ "$INSTALL_FALKORDB" = "true" ]; then \ + uv pip install --system --upgrade "graphiti-core[falkordb]==$GRAPHITI_VERSION"; \ + else \ + uv pip install --system --upgrade "graphiti-core==$GRAPHITI_VERSION"; \ + fi; \ + else \ + if [ "$INSTALL_FALKORDB" = "true" ]; then \ + uv pip install --system --upgrade "graphiti-core[falkordb]"; \ + else \ + uv pip install --system --upgrade graphiti-core; \ + fi; \ fi # Change ownership to app user diff --git a/server/README.md b/server/README.md index 034c4493..626d2929 100644 --- a/server/README.md +++ b/server/README.md @@ -2,6 +2,26 @@ Graph service is a fast api server implementing the [graphiti](https://github.com/getzep/graphiti) package. +## Container Releases + +The FastAPI server container is automatically built and published to Docker Hub when a new `graphiti-core` version is released to PyPI. + +**Image:** `zepai/graphiti` + +**Available tags:** +- `latest` - Latest stable release +- `0.22.1` - Specific version (matches graphiti-core version) + +**Platforms:** linux/amd64, linux/arm64 + +The automated release workflow: +1. Triggers when `graphiti-core` PyPI release completes +2. Waits for PyPI package availability +3. Builds multi-platform Docker image +4. Tags with version number and `latest` +5. Pushes to Docker Hub + +Only stable releases are built automatically (pre-release versions are skipped). ## Running Instructions