Skip to content

Commit 2077340

Browse files
feat: Helm chart + image for self-hosted deployment (#22)
* feat: deploy to hub-prod (Postgres job store, chart, image) Make serge deployable to the hub-prod cluster (hub-utils namespace): - store: optional Postgres backend selected by a postgres:// DATABASE_URL so the hosted deployment keeps job state off the ephemeral pod filesystem; SQLite remains the default. Quotes the reserved "user" column and rewrites ?->%s for psycopg. Adds psycopg to the web extra. - Dockerfile: production image for reviewbot-web (uvicorn:8080 + bwrap). - CI: build/push to the internal registry and trigger infra-deployment. - chart/: Helm chart (deployment/service/ingress/InfisicalSecret), internet-facing ALB on serge.huggingface.co, secrets via Infisical. * chore: switch serge domain to serge.hf.co * refactor: drop Postgres + hub specifics, ship a generic Helm chart Pivot to self-hosted deployment: the team will `helm install` the chart on their own cluster instead of ArgoCD on hub-prod. - Revert the Postgres job store back to SQLite (no app-code change); the embedded DB now persists on a PersistentVolumeClaim in the chart. - Make the chart generic: drop the Infisical CR and the hub-specific env/prod.yaml; sensitive env comes from a pre-created Secret (existingSecret); ingress/nodeSelector/storageClass/securityContext are values-driven. Add the PVC and a hardened securityContext. - CI: drop the ArgoCD/infra-deployment dispatch; keep build-and-push. * ci: build and push the image to GHCR (ghcr.io/huggingface/serge) * chart: add env/prod.yaml for open-source cluster (internal ALB) * chart: add serge-secrets example manifest (+ gitignore the filled copy) * chart: use spec.ingressClassName instead of the deprecated ingress.class annotation * tweaks * tweaks * add --last-error --------- Co-authored-by: Tarek Ziade <tarek@ziade.org>
1 parent b843289 commit 2077340

18 files changed

Lines changed: 860 additions & 0 deletions

.github/workflows/docker.yml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
name: Create and publish a Docker image
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
# Allow building/pushing on demand (e.g. before the first deploy).
8+
workflow_dispatch:
9+
10+
concurrency:
11+
group: ${{ github.workflow }}-${{ github.ref }}
12+
cancel-in-progress: true
13+
14+
jobs:
15+
build-and-push-image:
16+
runs-on: ubuntu-latest
17+
permissions:
18+
contents: read
19+
packages: write
20+
steps:
21+
- name: Checkout repository
22+
uses: actions/checkout@v4
23+
- name: Log in to GitHub Container Registry
24+
uses: docker/login-action@v3
25+
with:
26+
registry: ghcr.io
27+
username: ${{ github.actor }}
28+
password: ${{ secrets.GITHUB_TOKEN }}
29+
- name: Extract metadata (tags, labels) for Docker
30+
id: meta
31+
uses: docker/metadata-action@v5
32+
with:
33+
images: ghcr.io/${{ github.repository }}
34+
tags: |
35+
type=raw,value=latest,enable={{is_default_branch}}
36+
type=sha,prefix=sha-
37+
- name: Set up Docker Buildx
38+
uses: docker/setup-buildx-action@v3
39+
- name: Build and push Docker image
40+
uses: docker/build-push-action@v6
41+
with:
42+
context: .
43+
file: Dockerfile
44+
push: true
45+
platforms: 'linux/amd64'
46+
tags: ${{ steps.meta.outputs.tags }}
47+
labels: ${{ steps.meta.outputs.labels }}
48+
cache-to: type=gha,mode=max
49+
cache-from: type=gha

.github/workflows/tests.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,19 @@ jobs:
3232
3333
- name: Run tests
3434
run: make test
35+
36+
helm:
37+
runs-on: ubuntu-latest
38+
39+
steps:
40+
- name: Check out repository
41+
uses: actions/checkout@v6.0.3
42+
43+
- name: Set up Helm
44+
uses: azure/setup-helm@v4
45+
46+
- name: Lint Helm chart
47+
run: helm lint deploy/helm
48+
49+
- name: Render production values
50+
run: helm template serge deploy/helm -n serge -f deploy/helm/env/prod.yaml

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,5 @@ dist/
1616
*.db
1717
.serena/
1818
.jekyll-cache/
19+
# Filled-in copy of deploy/helm/serge-secrets.example.yaml — never commit real secrets.
20+
deploy/helm/serge-secrets.yaml

Dockerfile

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Production image for the serge web app (reviewbot-web). Mirrors the EC2
2+
# host: python3.11 + bubblewrap (so HELPER_SANDBOX can stay on), the
3+
# package installed into a venv with the [web] extra (FastAPI/uvicorn),
4+
# running uvicorn on $PORT (default 8080) as an unprivileged user. The
5+
# embedded SQLite job store persists on a mounted volume (see chart/).
6+
#
7+
# The sandbox-verification image used for local bwrap testing lives at
8+
# docker/Dockerfile and is unrelated to this one.
9+
FROM python:3.11-slim
10+
11+
RUN apt-get update \
12+
&& apt-get install -y --no-install-recommends bubblewrap ca-certificates git \
13+
&& rm -rf /var/lib/apt/lists/*
14+
15+
# Unprivileged service user, mirroring ec2-user on the real host.
16+
RUN useradd --create-home --shell /bin/bash app
17+
18+
WORKDIR /opt/app
19+
COPY . /opt/app
20+
RUN python -m venv /opt/app/.venv \
21+
&& /opt/app/.venv/bin/pip install --upgrade pip \
22+
&& /opt/app/.venv/bin/pip install -e '.[web]'
23+
24+
ENV PATH="/opt/app/.venv/bin:${PATH}"
25+
ENV PORT=8080
26+
EXPOSE 8080
27+
USER app
28+
CMD ["reviewbot-web"]

deploy/README.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Deploying Serge
2+
3+
This directory packages Serge's web app (`reviewbot-web`) for Kubernetes.
4+
The Helm chart is intentionally self-contained and values-driven so a team can
5+
deploy it into its own cluster without changing application code.
6+
7+
## Contents
8+
9+
- `helm/` contains the Helm chart for the web app: Deployment, Service,
10+
ConfigMap, optional Ingress, optional ServiceAccount, and a PersistentVolumeClaim.
11+
- `helm/env/prod.yaml` contains the production values used for
12+
`serge.huggingface.tech` on the open-source EKS cluster.
13+
- `helm/serge-secrets.example.yaml` is a template for the sensitive runtime env.
14+
Copy it to `helm/serge-secrets.yaml`, fill it locally, and never commit it.
15+
- `scripts/deploy.sh` checks the current Kubernetes context, creates the namespace
16+
when needed, optionally applies a local Secret file, and runs Helm.
17+
- `scripts/logs.sh` finds the current running Serge pod and prints recent logs.
18+
19+
## Chart Behavior
20+
21+
Serge uses embedded SQLite for review/task history. The chart therefore runs a
22+
single replica with a `Recreate` rollout strategy and mounts a PVC at
23+
`persistence.mountPath` (`/var/lib/reviewbot` by default). `WEB_STORE_PATH` is
24+
set to `<mountPath>/jobs.db`, so the database survives pod restarts.
25+
26+
The container runs as a non-root user, drops Linux capabilities, uses
27+
`RuntimeDefault` seccomp, and sets `fsGroup` so the app user can write the
28+
volume. Sensitive values are loaded from a pre-created Secret via
29+
`existingSecret`; non-secret runtime config lives in `envVars`.
30+
31+
## Deploy
32+
33+
Create or update the Secret in the target namespace:
34+
35+
```bash
36+
cp deploy/helm/serge-secrets.example.yaml deploy/helm/serge-secrets.yaml
37+
$EDITOR deploy/helm/serge-secrets.yaml
38+
deploy/scripts/deploy.sh -n serge --secret-file deploy/helm/serge-secrets.yaml
39+
```
40+
41+
Deploy without applying a Secret file, assuming `serge-secrets` already exists:
42+
43+
```bash
44+
deploy/scripts/deploy.sh -n serge -f deploy/helm/env/prod.yaml
45+
```
46+
47+
Use `--context` when you want the script to refuse any other kube context:
48+
49+
```bash
50+
deploy/scripts/deploy.sh \
51+
--context infra:opensource-aws-use1-prod-54 \
52+
-n serge \
53+
-f deploy/helm/env/prod.yaml
54+
```
55+
56+
Fetch recent logs:
57+
58+
```bash
59+
deploy/scripts/logs.sh \
60+
--context infra:opensource-aws-use1-prod-54 \
61+
-n serge \
62+
--since 2h \
63+
--grep 'error|traceback|crashed|HTTPError'
64+
```
65+
66+
Print only the latest error block:
67+
68+
```bash
69+
deploy/scripts/logs.sh \
70+
--context infra:opensource-aws-use1-prod-54 \
71+
-n serge \
72+
--since 2h \
73+
--last-error
74+
```
75+
76+
## Notes
77+
78+
- The production image is published to GHCR as `ghcr.io/huggingface/serge`.
79+
- `HELPER_SANDBOX=require` needs nodes that allow unprivileged user namespaces.
80+
Set it to `auto` or `off` in `envVars` if the cluster cannot support that.
81+
- Avoid `kubectl apply` for filled Secret manifests long-term: it can store
82+
plaintext Secret values in the `last-applied-configuration` annotation. The
83+
helper strips that annotation after applying a Secret file.

deploy/helm/Chart.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
apiVersion: v2
2+
name: serge
3+
version: 0.1.0
4+
type: application
5+
description: serge — GitHub PR reviewer web app (reviewbot-web)
6+
icon: https://huggingface.co/front/assets/huggingface_logo-noborder.svg

deploy/helm/env/prod.yaml

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Values for the serge deployment on the open-source EKS cluster
2+
# (opensource-aws-use1-prod-54), namespace chosen at install time.
3+
# Deploy: deploy/scripts/deploy.sh -n serge -f deploy/helm/env/prod.yaml
4+
#
5+
# Image is public on GHCR (ghcr.io/huggingface/serge) so no imagePullSecret
6+
# is needed. The ACM cert for serge.huggingface.tech already exists in this
7+
# account (infra 05-eks-utils) and is auto-discovered by the ALB controller
8+
# via the ingress host; external-dns creates the DNS record from the ingress.
9+
10+
image:
11+
# Bump to a newer sha-<commit> (pushed by CI on merge to main) or use latest.
12+
tag: sha-33be7c5
13+
14+
replicas: 1
15+
16+
# Sensitive env: create this Secret in the target namespace beforehand
17+
# (GITHUB_APP_ID, GITHUB_PRIVATE_KEY, GITHUB_WEBHOOK_SECRET,
18+
# GITHUB_OAUTH_CLIENT_ID, GITHUB_OAUTH_CLIENT_SECRET, WEB_SESSION_SECRET,
19+
# LLM_API_KEY, ...).
20+
existingSecret: serge-secrets
21+
22+
persistence:
23+
enabled: true
24+
size: 5Gi
25+
# "" = cluster default StorageClass (EBS gp3 on this cluster).
26+
storageClass: ""
27+
28+
ingress:
29+
enabled: true
30+
host: serge.huggingface.tech
31+
path: /
32+
className: "alb"
33+
annotations:
34+
# Internal ALB: reachable from the internal network / VPN only, NOT the
35+
# public internet. (Note: inbound GitHub App webhooks cannot reach an
36+
# internal ALB — fine for the OAuth web flow, which redirects in the
37+
# user's browser.)
38+
alb.ingress.kubernetes.io/scheme: "internal"
39+
alb.ingress.kubernetes.io/target-type: "ip"
40+
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS": 443}]'
41+
alb.ingress.kubernetes.io/ssl-redirect: "443"
42+
alb.ingress.kubernetes.io/healthcheck-path: "/healthz"
43+
alb.ingress.kubernetes.io/tags: "Env=prod,Project=serge"
44+
45+
# Non-secret runtime config.
46+
envVars:
47+
PORT: "8080"
48+
LOG_LEVEL: "INFO"
49+
LLM_API_BASE: "https://router.huggingface.co/v1"
50+
LLM_MODEL: "moonshotai/Kimi-K2.6"
51+
LLM_STREAM: "1"
52+
LLM_BILL_TO: "huggingface"
53+
# Kimi can spend a substantial part of the completion budget on reasoning.
54+
# Keep enough room for long tool-heavy reviews and the final JSON review.
55+
LLM_MAX_TOKENS: "49152"
56+
TOOL_MAX_ITERATIONS: "30"
57+
TASK_LLM_MAX_TOKENS: "49152"
58+
TASK_TOOL_MAX_ITERATIONS: "30"
59+
MENTION_TRIGGER: "@askserge"
60+
REVIEW_EVENT: "COMMENT"
61+
TASK_API_ENABLED: "1"
62+
# Refuse to run PR-author subprocesses unless bubblewrap is available.
63+
# Requires the node to allow unprivileged user namespaces; if pods fail
64+
# to run reviews, switch to "auto".
65+
HELPER_SANDBOX: "require"
66+
DEV_NO_AUTH: "0"
67+
WEB_ALLOWED_ORG: "huggingface"
68+
GITHUB_OAUTH_CALLBACK_URL: "https://serge.huggingface.tech/auth/callback"
69+
WEB_GITHUB_APP_URL: "https://github.com/apps/sergereview"
70+
71+
resources:
72+
requests:
73+
cpu: 500m
74+
memory: 512Mi
75+
limits:
76+
cpu: "1"
77+
memory: 1Gi
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# serge-secrets — sensitive env injected into the pod via envFrom (the chart
2+
# references this by name through `existingSecret: serge-secrets`). The keys
3+
# MUST be the exact env var names serge reads.
4+
#
5+
# Usage: copy to serge-secrets.yaml (git-ignored), fill in the values, then:
6+
# deploy/scripts/deploy.sh -n <namespace> --secret-file deploy/helm/serge-secrets.yaml
7+
# Do NOT commit the filled-in file.
8+
apiVersion: v1
9+
kind: Secret
10+
metadata:
11+
name: serge-secrets
12+
# namespace: <namespace> # or pass -n <namespace> to kubectl apply
13+
type: Opaque
14+
stringData:
15+
# --- GitHub App (required: serge publishes reviews via the App) ---
16+
GITHUB_APP_ID: "123456"
17+
# Inline PEM. Paste the full key, indented under the block scalar.
18+
GITHUB_PRIVATE_KEY: |
19+
-----BEGIN RSA PRIVATE KEY-----
20+
REPLACE_WITH_THE_GITHUB_APP_PRIVATE_KEY
21+
-----END RSA PRIVATE KEY-----
22+
23+
# --- Web auth (required when DEV_NO_AUTH=0, which prod uses) ---
24+
GITHUB_OAUTH_CLIENT_ID: "Iv1.xxxxxxxxxxxx"
25+
GITHUB_OAUTH_CLIENT_SECRET: "replace-me"
26+
# Strong random secret: openssl rand -hex 32
27+
WEB_SESSION_SECRET: "replace-me"
28+
29+
# --- LLM (optional in web mode: per-repo keys can live in the DB; set a
30+
# default here if you want one) ---
31+
LLM_API_KEY: "hf_or_sk-..."
32+
33+
# --- Optional: only needed if you wire up inbound GitHub App webhooks.
34+
# An internal ALB can't receive them, so this is usually unused here. ---
35+
# GITHUB_WEBHOOK_SECRET: "replace-me"

deploy/helm/templates/_helpers.tpl

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{{- define "name" -}}
2+
{{- default $.Release.Name | trunc 63 | trimSuffix "-" -}}
3+
{{- end -}}
4+
5+
{{- define "app.name" -}}
6+
serge
7+
{{- end -}}
8+
9+
{{- define "labels.standard" -}}
10+
release: {{ $.Release.Name | quote }}
11+
heritage: {{ $.Release.Service | quote }}
12+
chart: "{{ include "name" . }}"
13+
app: "{{ include "app.name" . }}"
14+
{{- end -}}

deploy/helm/templates/config.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
apiVersion: v1
2+
kind: ConfigMap
3+
metadata:
4+
labels: {{ include "labels.standard" . | nindent 4 }}
5+
name: {{ include "name" . }}
6+
namespace: {{ .Release.Namespace }}
7+
data:
8+
{{- range $key, $value := $.Values.envVars }}
9+
{{ $key }}: {{ $value | quote }}
10+
{{- end }}

0 commit comments

Comments
 (0)