name: Build and Deploy Verso (prod) # Production deploy. Triggered only by pushes to the `prod` branch — keep `main` # for day-to-day work and fast-forward `prod` when a build is stable. # # Differences from the test deploy (deploy-verso.yml): # - Runs in the `verso` namespace (test runs in `test`). # - Mongo / Redis / app data live on PersistentVolumeClaims and are applied # idempotently: this workflow NEVER deletes them, so data survives deploys. # - The replica set is initialised only once. # - Builds/pushes a distinct image tag (verso:stable) so prod and test never # clobber each other's image. # - SMTP comes from the `verso-smtp` Secret (create it with kubectl); email is # optional so the app still boots before the secret exists. # - Public self-registration stays off (CE default): friends-only, admin # creates accounts / sends invites. # # Out of band (do once): create the PVCs (server-ce/k8s/verso-prod-pvcs.yaml, # with your storageClass), the `verso-smtp` Secret, and a verso.alocoq.fr # Ingress (see server-ce/k8s/verso-prod-ingress.example.yaml) + DNS. on: push: branches: - prod workflow_dispatch: env: SITE_URL: https://verso.alocoq.fr jobs: deploy: runs-on: native timeout-minutes: 240 steps: - name: Build and push Verso prod image with BuildKit run: | kubectl -n ci delete job verso-buildkit-prod --ignore-not-found=true --wait=true cat <<'EOF' | kubectl apply -f - apiVersion: batch/v1 kind: Job metadata: name: verso-buildkit-prod namespace: ci spec: backoffLimit: 0 template: spec: restartPolicy: Never initContainers: - name: prepare image: alpine/git:latest command: ["sh", "-c"] args: - | set -eux REG=registry.git.svc.cluster.local:5000 git clone --depth 1 --branch prod https://git.alocoq.fr/alois/verso.git /workspace/repo # Build the base image only when Dockerfile-base changes # (content-hash tag); otherwise reuse the cached base. BTAG=$(sha256sum /workspace/repo/server-ce/Dockerfile-base | cut -c1-16) printf '%s' "$BTAG" > /workspace/base_tag if wget -qO- "http://$REG/v2/verso-base/tags/list" 2>/dev/null | grep -q "\"base-$BTAG\""; then echo "Base image base-$BTAG already present - skipping base build" else touch /workspace/build-base echo "Base image base-$BTAG not found - base will be built" fi volumeMounts: - name: workspace mountPath: /workspace containers: - name: buildkit image: moby/buildkit:latest securityContext: privileged: true command: ["sh", "-c"] args: - | set -eux REG=registry.git.svc.cluster.local:5000 mkdir -p /etc/buildkit printf '[registry."%s"]\n http = true\n insecure = true\n' "$REG" > /etc/buildkit/buildkitd.toml BTAG=$(cat /workspace/base_tag) BASE_REF="$REG/verso-base:base-$BTAG" if [ -f /workspace/build-base ]; then buildctl-daemonless.sh build \ --frontend=dockerfile.v0 \ --local context=/workspace/repo \ --local dockerfile=/workspace/repo/server-ce \ --opt filename=Dockerfile-base \ --import-cache type=registry,ref=$REG/verso-cache:base \ --export-cache type=registry,ref=$REG/verso-cache:base,mode=max \ --output type=image,name=$BASE_REF,push=true,registry.insecure=true else echo "Reusing existing base image $BASE_REF" fi # App image → verso:stable (prod tag). buildctl-daemonless.sh build \ --frontend=dockerfile.v0 \ --local context=/workspace/repo \ --local dockerfile=/workspace/repo/server-ce \ --opt filename=Dockerfile \ --opt build-arg:OVERLEAF_BASE_TAG=$BASE_REF \ --import-cache type=registry,ref=$REG/verso-cache:app \ --export-cache type=registry,ref=$REG/verso-cache:app,mode=max \ --output type=image,name=$REG/verso:stable,push=true,registry.insecure=true volumeMounts: - name: workspace mountPath: /workspace volumes: - name: workspace emptyDir: {} EOF - name: Wait for build run: | kubectl -n ci wait --for=condition=complete job/verso-buildkit-prod --timeout=14400s - name: Show build logs if: always() run: | kubectl -n ci logs job/verso-buildkit-prod -c prepare || true kubectl -n ci logs job/verso-buildkit-prod -c buildkit || true - name: Ensure data services (Mongo + Redis, never deleted) run: | # Mongo/Redis. Applied idempotently — this step must never delete # these, so project data survives every deploy. The namespace and the # PVCs (server-ce/k8s/verso-prod-pvcs.yaml) are provisioned out of # band, so the runner only needs namespaced rights in `verso` (like # `test`). This step assumes the namespace and the # mongo-data / redis-data / verso-data PVCs already exist. cat <<'EOF' | kubectl apply -f - apiVersion: apps/v1 kind: Deployment metadata: name: mongo namespace: verso spec: replicas: 1 strategy: type: Recreate selector: matchLabels: app: mongo template: metadata: labels: app: mongo spec: containers: - name: mongo image: mongo:8 command: ["mongod", "--replSet", "rs0", "--bind_ip_all"] ports: - containerPort: 27017 volumeMounts: - name: mongo-data mountPath: /data/db volumes: - name: mongo-data persistentVolumeClaim: claimName: mongo-data --- apiVersion: v1 kind: Service metadata: name: mongo namespace: verso spec: selector: app: mongo ports: - name: mongo port: 27017 targetPort: 27017 --- apiVersion: apps/v1 kind: Deployment metadata: name: redis namespace: verso spec: replicas: 1 strategy: type: Recreate selector: matchLabels: app: redis template: metadata: labels: app: redis spec: containers: - name: redis image: redis:7 # AOF persistence so a restart doesn't drop in-flight edits # before they're flushed to Mongo. command: ["redis-server", "--appendonly", "yes"] ports: - containerPort: 6379 volumeMounts: - name: redis-data mountPath: /data volumes: - name: redis-data persistentVolumeClaim: claimName: redis-data --- apiVersion: v1 kind: Service metadata: name: redis namespace: verso spec: selector: app: redis ports: - name: redis port: 6379 targetPort: 6379 EOF kubectl -n verso rollout status deployment/mongo --timeout=300s kubectl -n verso rollout status deployment/redis --timeout=300s - name: Initialise Mongo replica set (only if not already initialised) run: | kubectl -n verso exec deploy/mongo -- mongosh --quiet --eval ' try { rs.status() print("replica set already initialised") } catch (e) { if (e.codeName === "NotYetInitialized" || /no replset config/i.test(e.message)) { rs.initiate({ _id: "rs0", members: [{ _id: 0, host: "mongo:27017" }] }) print("replica set initiated") } else { throw e } } ' kubectl -n verso exec deploy/mongo -- mongosh --quiet --eval ' while (rs.status().myState !== 1) { sleep(1000) } print("Mongo replica set is PRIMARY") ' - name: Ensure Verso deployment + service run: | # Stamp the instance name with this build number, e.g. "Verso V0.12 Alpha". NAV_TITLE="Verso V0.${GITHUB_RUN_NUMBER:-${GITEA_RUN_NUMBER:-0}} Alpha" cat <<'EOF' | sed "s|__NAV_TITLE__|${NAV_TITLE}|g" | kubectl apply -f - apiVersion: apps/v1 kind: Deployment metadata: name: verso namespace: verso spec: replicas: 1 # RWO data volume → can't run two pods at once; recreate on update. strategy: type: Recreate selector: matchLabels: app: verso template: metadata: labels: app: verso spec: securityContext: # App runs as www-data (uid/gid 33); make the data volume # group-writable by it. fsGroup: 33 initContainers: - name: init-data-perms image: busybox:latest command: ["sh", "-c"] args: - | set -eux mkdir -p /data/template_files /data/user_files \ /data/compiles /data/cache /data/output /data/published chown -R 33:33 /data volumeMounts: - name: verso-data mountPath: /data containers: - name: verso image: registry.alocoq.fr/verso:stable # :stable is a fixed tag, so force a pull on every rollout to # pick up the freshly built image. imagePullPolicy: Always ports: - containerPort: 80 env: - name: OVERLEAF_MONGO_URL value: mongodb://mongo:27017/sharelatex?replicaSet=rs0 - name: OVERLEAF_REDIS_HOST value: redis - name: REDIS_HOST value: redis - name: OVERLEAF_APP_NAME value: Verso - name: OVERLEAF_NAV_TITLE value: "__NAV_TITLE__" - name: OVERLEAF_SITE_URL value: https://verso.alocoq.fr - name: OVERLEAF_SITE_LANGUAGE value: fr # Allow anonymous visitors so public published-presentation # links and read-only share links work without login. - name: OVERLEAF_ALLOW_PUBLIC_ACCESS value: "true" # NB: anonymous read-AND-write sharing is intentionally NOT # enabled (compiles are unsandboxed → only trusted accounts # may trigger them). Public self-registration is also off # (CE default): admin creates accounts / sends invites. - name: OVERLEAF_ENABLE_PROJECT_PYTHON_VENV value: "true" - name: OVERLEAF_LATEX_SHELL_ESCAPE value: "true" # (SMTP email vars are loaded below via envFrom.) # SMTP for password-reset / invite emails. All # OVERLEAF_EMAIL_* vars come from the optional 'verso-smtp' # Secret (its keys must be named exactly like those env # vars). Optional, so the app boots before the secret exists. envFrom: - secretRef: name: verso-smtp optional: true volumeMounts: - name: verso-data mountPath: /var/lib/overleaf/data volumes: - name: verso-data persistentVolumeClaim: claimName: verso-data --- apiVersion: v1 kind: Service metadata: name: verso namespace: verso spec: selector: app: verso ports: - name: http port: 80 targetPort: 80 EOF - name: Deploy Verso image run: | kubectl -n verso set image deployment/verso \ verso=registry.alocoq.fr/verso:stable kubectl -n verso rollout restart deployment/verso kubectl -n verso rollout status deployment/verso --timeout=600s - name: Create initial admin (only if no users exist) run: | COUNT=$(kubectl -n verso exec deploy/mongo -- mongosh sharelatex --quiet --eval 'db.users.countDocuments()' | tr -d '[:space:]') if [ "$COUNT" = "0" ]; then echo "No users yet — creating the initial admin account" kubectl -n verso exec deploy/verso -- bash -lc ' cd /overleaf/services/web node modules/server-ce-scripts/scripts/create-user \ --admin \ --email=alois.coquillard@gmail.com ' else echo "Users already exist ($COUNT) — skipping admin creation" fi