d8ce7f9dc1
Build and Deploy Verso / deploy (push) Has been cancelled
The LaTeX svg package converts .svg files to PDF at compile time by shelling out to Inkscape (requires --shell-escape). Without Inkscape in the image and the flag enabled, compilation fails with "Did you run the export with Inkscape?". - Dockerfile-base: add inkscape to the apt install block - settings.js: expose OVERLEAF_LATEX_SHELL_ESCAPE env var → clsi.latexShellEscape - LatexRunner.js: pass -shell-escape to latexmk when the setting is on - deploy-verso-prod.yml: set OVERLEAF_LATEX_SHELL_ESCAPE=true (trusted-user instance) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
386 lines
15 KiB
YAML
386 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"
|
|
- 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
|