Build Overleaf Full Image #161
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Build our own "full" Overleaf image with TeX Live scheme-full | |
| # Daily scheduled build that fetches the latest version from upstream | |
| # and creates an optimized image with minimal layers | |
| # | |
| # Tags: latest, yyyy-mm-dd | |
| name: Build Overleaf Full Image | |
| on: | |
| schedule: | |
| # Daily at 04:00 UTC (after mirroring workflow) | |
| - cron: '0 4 * * *' | |
| push: | |
| branches: | |
| - main | |
| paths: | |
| - '.github/workflows/build-overleaf-full.yml' | |
| - '.github/scripts/mirror_images.py' | |
| - '.github/scripts/check_platform_support.py' | |
| workflow_dispatch: | |
| inputs: | |
| force_rebuild: | |
| description: 'Force rebuild even if image exists for today' | |
| required: false | |
| default: 'false' | |
| type: boolean | |
| base_version: | |
| description: 'Override base image version (leave empty for latest)' | |
| required: false | |
| default: '' | |
| type: string | |
| env: | |
| REGISTRY: ghcr.io | |
| IMAGE_NAME: btreemap/overleaf | |
| permissions: | |
| contents: read | |
| packages: write | |
| jobs: | |
| detect-version: | |
| name: Detect Latest Version | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.detect.outputs.version }} | |
| build_date: ${{ steps.detect.outputs.build_date }} | |
| should_build: ${{ steps.check.outputs.should_build }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Detect latest upstream version | |
| id: detect | |
| run: | | |
| BUILD_DATE=$(date -u '+%Y-%m-%d') | |
| echo "build_date=${BUILD_DATE}" >> "$GITHUB_OUTPUT" | |
| OVERRIDE_VERSION="${{ inputs.base_version }}" | |
| if [[ -n "$OVERRIDE_VERSION" ]]; then | |
| echo "version=${OVERRIDE_VERSION}" >> "$GITHUB_OUTPUT" | |
| echo "Using override version: ${OVERRIDE_VERSION}" | |
| else | |
| VERSION=$(python3 .github/scripts/mirror_images.py latest --source sharelatex/sharelatex) | |
| echo "version=${VERSION}" >> "$GITHUB_OUTPUT" | |
| echo "Detected version: ${VERSION}" | |
| fi | |
| echo "Build date: ${BUILD_DATE}" | |
| - name: Check if today's image exists | |
| id: check | |
| run: | | |
| BUILD_DATE="${{ steps.detect.outputs.build_date }}" | |
| FORCE_REBUILD="${{ inputs.force_rebuild || 'false' }}" | |
| IMAGE="ghcr.io/${{ env.IMAGE_NAME }}" | |
| if [[ "$FORCE_REBUILD" == "true" ]]; then | |
| echo "Force rebuild requested" | |
| echo "should_build=true" >> "$GITHUB_OUTPUT" | |
| elif docker buildx imagetools inspect "${IMAGE}:${BUILD_DATE}" > /dev/null 2>&1; then | |
| echo "Image for ${BUILD_DATE} already exists, skipping build" | |
| echo "should_build=false" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "Image for ${BUILD_DATE} not found, will build" | |
| echo "should_build=true" >> "$GITHUB_OUTPUT" | |
| fi | |
| build: | |
| name: Build Full Image (${{ matrix.arch }}) | |
| needs: detect-version | |
| if: needs.detect-version.outputs.should_build == 'true' | |
| runs-on: ${{ matrix.runner }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - platform: linux/amd64 | |
| runner: ubuntu-latest | |
| arch: amd64 | |
| optional: false | |
| - platform: linux/arm64 | |
| runner: ubuntu-24.04-arm | |
| arch: arm64 | |
| optional: true | |
| steps: | |
| - name: Checkout repository for scripts | |
| uses: actions/checkout@v6 | |
| - name: Generate optimized Dockerfile | |
| run: | | |
| cat > Dockerfile << 'DOCKERFILE' | |
| # Optimized Overleaf Full Image | |
| # Built from upstream sharelatex/sharelatex with full TeX Live and extras | |
| # Commands structured to balance caching and regular apt upgrades | |
| ARG BASE_VERSION=latest | |
| FROM sharelatex/sharelatex:${BASE_VERSION} | |
| SHELL ["/bin/bash", "-c"] | |
| # TeX Live setup and configuration | |
| RUN set -eux; \ | |
| # Update tlmgr | |
| wget -q "https://mirror.ctan.org/systems/texlive/tlnet/update-tlmgr-latest.sh" -O /tmp/update-tlmgr.sh; \ | |
| sh /tmp/update-tlmgr.sh; \ | |
| tlmgr --version; \ | |
| # Update texlive-scripts for ctex support | |
| tlmgr update texlive-scripts; \ | |
| # Update all packages | |
| tlmgr update --all; \ | |
| # Install scheme-full | |
| tlmgr install scheme-full; \ | |
| # Recreate symlinks | |
| tlmgr path add; \ | |
| # Enable shell-escape by default | |
| TEXLIVE_FOLDER=$(find /usr/local/texlive/ -maxdepth 1 -type d -name '20*' | head -1); \ | |
| if [ -n "$TEXLIVE_FOLDER" ]; then \ | |
| echo "% enable shell-escape by default" >> "$TEXLIVE_FOLDER/texmf.cnf"; \ | |
| echo "shell_escape = t" >> "$TEXLIVE_FOLDER/texmf.cnf"; \ | |
| fi; \ | |
| # Clean up TeX-related caches and temp files | |
| rm -rf /tmp/*; \ | |
| rm -rf /usr/local/texlive/*/tlpkg/backups/*; \ | |
| rm -rf /root/.cache/* | |
| # Daily-refreshing OS packages and extra tools | |
| # APT_REFRESH is set per-build (daily) in the workflow to bust only this layer's cache. | |
| ARG APT_REFRESH=0 | |
| RUN set -eux; \ | |
| echo "APT_REFRESH=${APT_REFRESH}"; \ | |
| apt-get update; \ | |
| apt-get upgrade -y; \ | |
| apt-get install -y --no-install-recommends inkscape lilypond; \ | |
| apt-get clean; \ | |
| rm -rf /var/lib/apt/lists/* | |
| DOCKERFILE | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Check upstream platform availability | |
| id: platform | |
| run: | | |
| set -euo pipefail | |
| IMAGE="docker.io/sharelatex/sharelatex:${{ needs.detect-version.outputs.version }}" | |
| export PLATFORM="${{ matrix.platform }}" | |
| if docker buildx imagetools inspect --format '{{json .}}' "$IMAGE" > /tmp/inspect.json 2> /tmp/inspect.err; then | |
| SUPPORTED=$(python3 .github/scripts/check_platform_support.py \ | |
| --platform "${PLATFORM}" \ | |
| --inspect-json /tmp/inspect.json) | |
| else | |
| echo "Unable to inspect ${IMAGE}; treating ${PLATFORM} as unsupported." | |
| if [ -s /tmp/inspect.err ]; then | |
| echo "Inspect error output:" | |
| cat /tmp/inspect.err | |
| fi | |
| SUPPORTED="false" | |
| fi | |
| if [[ "$SUPPORTED" == "true" ]]; then | |
| echo "Upstream image supports ${PLATFORM}" | |
| echo "should_build=true" >> "$GITHUB_OUTPUT" | |
| elif [[ "${{ matrix.optional }}" == "true" ]]; then | |
| echo "Upstream image does not provide ${PLATFORM}; skipping build." | |
| echo "should_build=false" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "Upstream image does not provide ${PLATFORM}." >&2 | |
| exit 1 | |
| fi | |
| - name: Login to GHCR | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Generate Docker metadata | |
| id: meta | |
| uses: docker/metadata-action@v5 | |
| with: | |
| images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | |
| labels: | | |
| org.opencontainers.image.title=Overleaf Full | |
| org.opencontainers.image.description=Overleaf with full TeX Live scheme-full, inkscape, and lilypond | |
| org.opencontainers.image.vendor=BTreeMap | |
| org.opencontainers.image.version=${{ needs.detect-version.outputs.version }} | |
| - name: Build and push by digest | |
| id: build | |
| uses: docker/build-push-action@v7 | |
| if: steps.platform.outputs.should_build == 'true' | |
| with: | |
| context: . | |
| file: ./Dockerfile | |
| platforms: ${{ matrix.platform }} | |
| push: true | |
| labels: ${{ steps.meta.outputs.labels }} | |
| build-args: | | |
| BASE_VERSION=${{ needs.detect-version.outputs.version }} | |
| APT_REFRESH=${{ needs.detect-version.outputs.build_date }} | |
| outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true | |
| cache-from: type=gha,scope=overleaf-full-${{ matrix.arch }} | |
| cache-to: type=gha,mode=max,scope=overleaf-full-${{ matrix.arch }} | |
| - name: Export digest | |
| if: steps.platform.outputs.should_build == 'true' | |
| run: | | |
| mkdir -p /tmp/digests | |
| digest="${{ steps.build.outputs.digest }}" | |
| touch "/tmp/digests/${digest#sha256:}" | |
| - name: Upload digest | |
| uses: actions/upload-artifact@v7 | |
| if: steps.platform.outputs.should_build == 'true' | |
| with: | |
| name: digests-full-${{ matrix.arch }} | |
| path: /tmp/digests/* | |
| if-no-files-found: error | |
| retention-days: 1 | |
| merge: | |
| name: Create Multi-Arch Manifest | |
| needs: [detect-version, build] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Download digests | |
| uses: actions/download-artifact@v8 | |
| continue-on-error: true | |
| with: | |
| path: /tmp/digests | |
| pattern: digests-full-* | |
| merge-multiple: true | |
| - name: Check for digest files | |
| id: digests | |
| run: | | |
| mkdir -p /tmp/digests | |
| digest_probe=$(find /tmp/digests -maxdepth 1 -type f -print -quit) | |
| if [ -n "$digest_probe" ]; then | |
| digests=(/tmp/digests/*) | |
| echo "Found ${#digests[@]} digest(s)." | |
| echo "has_digests=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "No digests found; skipping manifest creation." | |
| echo "has_digests=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| if: steps.digests.outputs.has_digests == 'true' | |
| - name: Login to GHCR | |
| uses: docker/login-action@v3 | |
| if: steps.digests.outputs.has_digests == 'true' | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Generate Docker metadata | |
| id: meta | |
| uses: docker/metadata-action@v5 | |
| if: steps.digests.outputs.has_digests == 'true' | |
| with: | |
| images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | |
| tags: | | |
| # Latest tag | |
| type=raw,value=latest | |
| # Date-based tag | |
| type=raw,value=${{ needs.detect-version.outputs.build_date }} | |
| # Version-based tag | |
| type=raw,value=${{ needs.detect-version.outputs.version }}-full | |
| - name: Create manifest list and push | |
| if: steps.digests.outputs.has_digests == 'true' | |
| working-directory: /tmp/digests | |
| run: | | |
| TAGS=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") | |
| DIGESTS=$(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *) | |
| # shellcheck disable=SC2086 | |
| docker buildx imagetools create $TAGS $DIGESTS | |
| - name: Inspect image | |
| if: steps.digests.outputs.has_digests == 'true' | |
| run: | | |
| docker buildx imagetools inspect "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" |