Automate PDF Generation in CI/CD with GitHub Actions & FormaTeX

Automate LaTeX PDF generation in CI/CD with FormaTeX and GitHub Actions. Say goodbye to large TeX distributions and streamline your workflow today!

Automated PDF Generation in CI/CD: GitHub Actions + FormaTeX LaTeX API

By Elhassane Mehdioui


If you have ever tried to compile a LaTeX document inside a CI/CD pipeline, you already know the pain. Installing TeX Live takes several minutes, bloats your runner cache with more than 500 megabytes of fonts, packages, and binaries, and still manages to produce subtle, hard-to-debug compilation errors that only appear on the server. There is a better way. The FormaTeX API offloads every aspect of LaTeX compilation to a managed service, leaving your pipeline lean and your team focused on content rather than infrastructure.

In this guide we will walk through a complete, production-ready setup: synchronous and asynchronous compilation, GitHub Actions integration using the official forma-tex/github-action@v1 action, an equivalent GitLab CI example, webhook-based notifications, and proper secret management — all with working YAML you can drop straight into your repository.


Why TeX Live on CI Is a Problem Worth Solving

The standard advice for compiling LaTeX in CI is to install TeX Live at the start of the job. In practice this means:

  • Download time. A minimal TeX Live installation is around 200 MB; a full installation exceeds 500 MB. On a cold runner that translates to two to five minutes of pure setup time before your first pdflatex command runs.
  • Cache invalidation. Package updates invalidate cached layers unpredictably, so teams end up either pinning an old snapshot (and missing security patches) or accepting repeated long downloads.
  • Engine complexity. Modern documents often require lualatex, xelatex, or latexmk with multiple passes. Configuring this correctly across different CI environments is error-prone.
  • Debug surface area. Font-related failures, missing .sty files, and locale issues behave differently on Ubuntu 22.04, macOS runners, and Windows Server — yet all of these are possible runner OS choices on GitHub Actions.

FormaTeX eliminates all of the above. You send a .tex source (or a zip archive containing your full project) over HTTPS, and the API returns a compiled PDF. The infrastructure, the engines, the packages, and the debugging are their problem, not yours.


FormaTeX API Overview

The FormaTeX REST API exposes three main endpoints relevant to CI use cases.

EndpointMethodPurpose
/api/v1/compilePOSTSynchronous compilation — waits and returns PDF directly
/api/v1/compile/asyncPOSTAsynchronous compilation — returns a job ID immediately
/api/v1/jobs/:idGETPoll job status
/api/v1/jobs/:id/pdfGETDownload the compiled PDF once the job is complete

Every successful response includes three informational headers that are useful for observability and billing audits:

  • X-Compile-Duration-Ms — wall-clock time in milliseconds for the compilation engine.
  • X-Engine-Used — the LaTeX engine that processed the document (e.g., pdflatex, xelatex, lualatex).
  • X-Compilations-Used — the number of engine passes executed (relevant when latexmk runs multiple passes for cross-references).

Authentication

All requests require a bearer token in the Authorization header:

Authorization: Bearer YOUR_API_KEY

Pricing is straightforward: FormaTeX is free for open-source projects. Teams with private repositories or higher volume requirements can upgrade to the Pro plan.


Synchronous Compilation with curl

Before integrating into CI it is worth understanding the raw API call. For a single-file document:

curl -X POST https://api.formatex.io/api/v1/compile \
  -H "Authorization: Bearer $FORMATEX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"source": "\\documentclass{article}\\begin{document}Hello, FormaTeX!\\end{document}"}' \
  --output document.pdf

The response body is the raw PDF binary. The response headers contain X-Compile-Duration-Ms, X-Engine-Used, and X-Compilations-Used for your logs.


Asynchronous Compilation for Large Documents

For documents that take more than a few seconds — think long reports with many figures, or documents requiring multiple bibtex passes — use the async endpoint to avoid HTTP timeouts.

Step 1: Submit the job

JOB_RESPONSE=$(curl -s -X POST https://api.formatex.io/api/v1/compile/async \
  -H "Authorization: Bearer $FORMATEX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"source": "..."}')

JOB_ID=$(echo "$JOB_RESPONSE" | jq -r '.id')

Step 2: Poll until complete

while true; do
  STATUS=$(curl -s \
    -H "Authorization: Bearer $FORMATEX_API_KEY" \
    "https://api.formatex.io/api/v1/jobs/$JOB_ID" | jq -r '.status')

  if [ "$STATUS" = "completed" ]; then
    break
  elif [ "$STATUS" = "failed" ]; then
    echo "Compilation failed" && exit 1
  fi

  sleep 3
done

Step 3: Download the PDF

curl -s \
  -H "Authorization: Bearer $FORMATEX_API_KEY" \
  "https://api.formatex.io/api/v1/jobs/$JOB_ID/pdf" \
  --output output.pdf

GitHub Actions Integration

The official forma-tex/github-action@v1 action wraps all of the above into a single step. Here is a complete workflow that compiles a document on every push to main and uploads the resulting PDF as a build artifact.

# .github/workflows/compile-pdf.yml
name: Compile LaTeX PDF

on:
  push:
    branches:
      - main
  pull_request:
    paths:
      - '**.tex'
      - '**.bib'

jobs:
  compile:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Compile LaTeX document
        uses: forma-tex/github-action@v1
        with:
          api-key: ${{ secrets.FORMATEX_API_KEY }}
          input-file: docs/report.tex
          output-file: dist/report.pdf

      - name: Upload compiled PDF
        uses: actions/upload-artifact@v4
        with:
          name: compiled-pdf
          path: dist/report.pdf
          retention-days: 30

      - name: Attach PDF to release
        if: startsWith(github.ref, 'refs/tags/')
        uses: softprops/action-gh-release@v2
        with:
          files: dist/report.pdf

Storing Your API Key as a Secret

Never hard-code API keys in YAML files. In GitHub:

  1. Go to your repository Settings > Secrets and variables > Actions.
  2. Click New repository secret.
  3. Name it FORMATEX_API_KEY and paste your key as the value.

The ${{ secrets.FORMATEX_API_KEY }} syntax in the workflow above will resolve at runtime without ever exposing the value in logs.


GitLab CI Integration

If your team uses GitLab, the same workflow translates cleanly to .gitlab-ci.yml. Store your key under Settings > CI/CD > Variables with the name FORMATEX_API_KEY and the Masked flag enabled.

# .gitlab-ci.yml
stages:
  - compile

compile-pdf:
  stage: compile
  image: alpine:3.19
  before_script:
    - apk add --no-cache curl jq

  script:
    - |
      echo "Submitting compilation job..."
      JOB_RESPONSE=$(curl -s -X POST https://api.formatex.io/api/v1/compile/async \
        -H "Authorization: Bearer $FORMATEX_API_KEY" \
        -H "Content-Type: application/json" \
        --data-binary @docs/report.tex)

      JOB_ID=$(echo "$JOB_RESPONSE" | jq -r '.id')
      echo "Job ID: $JOB_ID"

      echo "Polling for completion..."
      for i in $(seq 1 30); do
        RESPONSE=$(curl -s \
          -H "Authorization: Bearer $FORMATEX_API_KEY" \
          "https://api.formatex.io/api/v1/jobs/$JOB_ID")
        STATUS=$(echo "$RESPONSE" | jq -r '.status')

        echo "Attempt $i: status=$STATUS"

        if [ "$STATUS" = "completed" ]; then
          break
        elif [ "$STATUS" = "failed" ]; then
          echo "Compilation failed:"
          echo "$RESPONSE" | jq '.error'
          exit 1
        fi

        sleep 5
      done

      echo "Downloading PDF..."
      curl -s \
        -H "Authorization: Bearer $FORMATEX_API_KEY" \
        "https://api.formatex.io/api/v1/jobs/$JOB_ID/pdf" \
        --output dist/report.pdf

      echo "Done. File size: $(wc -c < dist/report.pdf) bytes"

  artifacts:
    paths:
      - dist/report.pdf
    expire_in: 1 week

  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
      changes:
        - "**/*.tex"
        - "**/*.bib"

Webhook Notifications

For long-running compilations — or when you want to trigger downstream workflows the moment a PDF is ready — FormaTeX supports webhook callbacks. You register a URL when submitting the async job, and FormaTeX will POST to it when compilation finishes.

Two events are emitted:

  • compilation.success — the PDF is ready. The payload includes the job ID, engine used, duration, and a signed download URL.
  • compilation.failed — compilation encountered an error. The payload includes the job ID and the LaTeX error log.

Submitting a job with a webhook:

curl -X POST https://api.formatex.io/api/v1/compile/async \
  -H "Authorization: Bearer $FORMATEX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "source": "...",
    "webhook": {
      "url": "https://your-app.example.com/webhooks/formatex",
      "events": ["compilation.success", "compilation.failed"]
    }
  }'

Example webhook payload for compilation.success:

{
  "event": "compilation.success",
  "job_id": "job_abc123",
  "engine": "pdflatex",
  "duration_ms": 1842,
  "compilations_used": 2,
  "pdf_url": "https://api.formatex.io/api/v1/jobs/job_abc123/pdf"
}

In a GitHub Actions context, webhooks are most useful when combined with a repository dispatch event — your webhook receiver calls the GitHub API to trigger a downstream workflow (for example, publishing the PDF to a documentation site) the instant compilation succeeds.


Observability: Using the Response Headers

Every response from the FormaTeX API includes three headers that you should log in CI for cost auditing and performance monitoring:

RESPONSE_HEADERS=$(curl -sI \
  -H "Authorization: Bearer $FORMATEX_API_KEY" \
  "https://api.formatex.io/api/v1/jobs/$JOB_ID/pdf" \
  --output dist/report.pdf)

echo "$RESPONSE_HEADERS" | grep -E 'X-Compile-Duration-Ms|X-Engine-Used|X-Compilations-Used'

Sample output:

X-Compile-Duration-Ms: 2341
X-Engine-Used: xelatex
X-Compilations-Used: 3

Feeding these values into your CI summary (GitHub Actions supports step summaries via $GITHUB_STEP_SUMMARY) gives your team a running history of compilation performance without any external monitoring setup.


Pricing: Free for Open Source, Pro for Teams

FormaTeX uses a straightforward pricing model:

  • Free tier — unlimited compilations for public, open-source repositories. Ideal for academic projects, open-source documentation, and personal portfolios.
  • Pro plan — designed for private repositories, commercial projects, and teams that need higher concurrency, priority queue access, and dedicated support.

There are no per-seat charges on the free tier, and no credit card is required to get started. This makes FormaTeX a practical default for any open-source project that currently ships PDF documentation.


Summary

Replacing a 500 MB TeX Live installation with a single API call is one of the highest-leverage CI optimisations available to teams that ship LaTeX documents. With FormaTeX you get:

  • Sub-minute setup time on any CI runner, regardless of operating system.
  • A stable, versioned compilation environment that never breaks due to a package update.
  • Async compilation and webhook support for complex, multi-pass documents.
  • Structured response headers for observability and billing audits.
  • A free tier that covers the majority of open-source use cases with no friction.

The forma-tex/github-action@v1 action makes the GitHub integration a single YAML stanza. The GitLab pattern shown above requires only curl and jq, which are available on any minimal Alpine image. Either way, your pipeline stays lean, your PDFs stay consistent, and your team stops debugging font installations on a remote server at 2 AM.


Elhassane Mehdioui is a software engineer focused on developer tooling, automation pipelines, and document infrastructure.

Need a robust API or back-end?

Scalable back-end systems with Laravel, Node.js or Spring Boot.

REST APIs, authentication, role management, and clean architecture — built to last.

HassanOSSYS-00
SECURE CHANNEL · ACTIVE
INIT://