name: Build and Deploy Verso on: push: branches: - main workflow_dispatch: env: SITE_URL: https://test.alocoq.fr jobs: deploy: runs-on: native timeout-minutes: 240 steps: - name: Build and push Verso images with BuildKit run: | kubectl -n ci delete job verso-buildkit --ignore-not-found=true --wait=true cat <<'EOF' | kubectl apply -f - apiVersion: batch/v1 kind: Job metadata: name: verso-buildkit 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 https://git.alocoq.fr/alois/verso.git /workspace/repo # (#1) Build the base image only when it actually changes. # The base layers' only repo input is Dockerfile-base, so # we key on a content hash of that file: the base is tagged # verso-base:base- and the app builds FROM that exact # tag. If a base with this hash is already in the registry, # the heavy base build (apt, TeX Live, Quarto) is skipped. 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 # Push to the in-cluster registry (plain HTTP) to bypass # the Traefik ingress, whose read timeout was killing the # multi-GB TeX Live layer upload mid-stream. Mark the # registry http+insecure so both push and the base pull # for the app build treat it as plain HTTP. Written inside # the container so no extra k8s resources are needed. 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" # (#1) Base build, only when prepare flagged it changed. # (#2) Import/export a registry layer cache so that, when # the base does change, unchanged layers (e.g. apt) are # still reused instead of rebuilt from scratch. 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, built FROM the content-pinned base tag. # (#2) The registry cache lets yarn install be skipped when # package.json is unchanged; the web build only re-runs # when the frontend source actually changes. 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:latest,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 --timeout=14400s - name: Show build logs if: always() run: | kubectl -n ci logs job/verso-buildkit -c prepare || true kubectl -n ci logs job/verso-buildkit -c buildkit || true - name: Recreate test dependencies run: | kubectl -n test delete deployment mongo redis --ignore-not-found=true --wait=true kubectl -n test delete service mongo redis --ignore-not-found=true --wait=true cat <<'EOF' | kubectl apply -f - apiVersion: apps/v1 kind: Deployment metadata: name: mongo namespace: test spec: replicas: 1 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 emptyDir: {} --- apiVersion: v1 kind: Service metadata: name: mongo namespace: test spec: selector: app: mongo ports: - name: mongo port: 27017 targetPort: 27017 --- apiVersion: apps/v1 kind: Deployment metadata: name: redis namespace: test spec: replicas: 1 selector: matchLabels: app: redis template: metadata: labels: app: redis spec: containers: - name: redis image: redis:7 ports: - containerPort: 6379 volumeMounts: - name: redis-data mountPath: /data volumes: - name: redis-data emptyDir: {} --- apiVersion: v1 kind: Service metadata: name: redis namespace: test spec: selector: app: redis ports: - name: redis port: 6379 targetPort: 6379 EOF kubectl -n test rollout status deployment/mongo --timeout=180s kubectl -n test rollout status deployment/redis --timeout=180s sleep 5 kubectl -n test exec deploy/mongo -- mongosh --eval ' rs.initiate({ _id: "rs0", members: [{ _id: 0, host: "mongo:27017" }] }) ' kubectl -n test exec deploy/mongo -- mongosh --eval ' while (rs.status().myState !== 1) { sleep(1000) } print("Mongo replica set is PRIMARY") ' - name: Ensure Verso deployment exists run: | # Stamp the instance name with this build number, e.g. "Verso V0.83 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: test spec: replicas: 1 selector: matchLabels: app: verso template: metadata: labels: app: verso spec: containers: - name: verso # Pull via the public address: the cluster nodes' containerd # is configured for registry.alocoq.fr, not the in-cluster # service name. Both front the same registry storage, so the # image pushed via the in-cluster address resolves here too. image: registry.alocoq.fr/verso:latest 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://test.alocoq.fr # Default UI language for the instance. - name: OVERLEAF_SITE_LANGUAGE value: fr # Allow anonymous visitors to reach the site so link # sharing and public presentation links work without a # login. Per-project and per-route access checks still # apply; private presentation links still require login. - name: OVERLEAF_ALLOW_PUBLIC_ACCESS value: "true" # Also let anonymous visitors use read-AND-write share # links (edit without an account). Read-only links only # need OVERLEAF_ALLOW_PUBLIC_ACCESS above. - name: OVERLEAF_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING value: "true" # Let Quarto Python cells use a project's requirements.txt: # the compiler installs it into a cached venv. Gated to the # project owner + invited collaborators (never anonymous / # link-sharing users). - name: OVERLEAF_ENABLE_PROJECT_PYTHON_VENV value: "true" --- apiVersion: v1 kind: Service metadata: name: verso namespace: test spec: selector: app: verso ports: - name: http port: 80 targetPort: 80 EOF - name: Deploy Verso image run: | kubectl -n test set image deployment/verso \ verso=registry.alocoq.fr/verso:latest kubectl -n test rollout restart deployment/verso kubectl -n test rollout status deployment/verso --timeout=300s - name: Create admin user run: | sleep 20 kubectl -n test exec deploy/verso -- bash -lc ' cd /overleaf/services/web node modules/server-ce-scripts/scripts/create-user \ --admin \ --email=test@example.com || true ' - name: Cleanup if: always() run: | kubectl -n ci delete job verso-buildkit --ignore-not-found=true --wait=true