Files
Verso/.gitea/workflows/deploy-verso-prod.yml
T
claude c249d6a6e9
Build and Deploy Verso / deploy (push) Successful in 1m19s
Build and Deploy Verso (prod) / deploy (push) Successful in 1m9s
Prod: load SMTP env via envFrom secretRef (flat, paste-proof)
Replace the six nested secretKeyRef env entries with a single
'envFrom: - secretRef: { name: verso-smtp, optional: true }' in both the
standalone app manifest and the prod workflow. Avoids the deep nesting that
tripped strict server-side decoding, and is simpler to edit. The secret's keys
must now be named exactly like the env vars (OVERLEAF_EMAIL_*).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 12:40:07 +00:00

384 lines
15 KiB
YAML

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"
# (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