Do you have LaTeX projects on GitHub? Ever wondered how to get GitHub to build them automatically? Hereās how I do it.
A geeky workflow for LaTeX documents
Iām a geek. More precisely, Iām a physicist. So Iām probably even geekier than your standard geek.
Now, when physicists write documents, such as papers, letters, or slides for talks, they tend to use LaTeX. Iām no exception. After all, the layout is automatic, the mathematics looks great, and I get to write code to construct my documents. Whatās there not to like?
When creating the slides for my talks, I use the beamer class. Itās become the standard these days when writing talks in LaTeX.1 Of course, because LaTeX is code, I put my work into Git and push it to an online Git service such as GitLab or GitHub. When using GitHub, I build the document using a GitHub Actions workflow. This way, the document builds in a āpristineā environment, and helps me spot issues that might not be obvious when working only on my development system.
A version lag in Ubuntuās TeXLive packages
In one recent project, I was building the slides on GitHub by installing the standard Ubuntu TeXLive LaTeX packages and then running latexmk. The problem here is that the Ubuntu LaTeX packages arenāt as up to date as one might like them to be. On GitHub, using for instance the ubuntu-22.04 Docker image,2 one has access to a TeXLive version from 2021. Thatās somewhat ⦠old. I needed a recent TeXLive distribution.
But how best to get a recent TeXLive? Starting from a stock Ubuntu image and then installing TeXLive is a no-goer, as anyone who has installed a full TeXLive distribution will know. This process can potentially take hours only to download the individual TeXLive packages. That definitely wonāt work in a GitHub-CI build: the process will time out before any document has been built. Also, getting feedback on broken builds would take forever, thus reducing the usefulness of building the project on GitHub.
Another possibility would be to use either the small or medium TeXLive distribution, installing any missing packages as necessary. This would be a lot of work trying to find out which packages are missing. Iād have to update the CI config, wait for a build to run (which involves a long installation step, so it takes a while), see if the build fails, read the log output to find the missing package, add it to the CI config, and try everything again. Future package additions to the document would also make the build fail if Iām not vigilant in keeping the CI config up to date. Not an optimal situation.
A LaTeX-specific GitHub action
Surely thereās a better way? Yes, there is. Introducing the latex-action GitHub action. Itās a
[ā¦] GitHub Action that compiles LaTeX documents to PDF using a complete TeXLive environment.
Thatās just what weāre after!
The action provides many TeXLive versions to choose from, from 2020 up to the latest version as of writing, 2025. You can also choose between Alpine or Debian Linux as the base image, giving you some flexibility about what other software you might want to install. There are other options available to handle paths, compilation settings, etc. Check out the actionās documentation for more details.
Sounds great! Letās begin!
A worked example
Using a simple example for illustration, Iāll first describe how to build a LaTeX document within a GitHub workflow using the stock Ubuntu images. Then Iāll show how to migrate to a workflow using the latex-action action.
Weāll construct a short beamer document thatās got a couple of slides in it with just enough complexity to make it a bit interesting. For those who are interested, the document introduces the basics of the discrete Fourier transform.
Building the document locally
Imagine we have a local Git repository called discrete-fourier-transform with a file containing the following LaTeX code:3
\documentclass[t,aspectratio=169]{beamer}
\usetheme{metropolis}
\title{Briefly introducing the discrete Fourier transform}
\author{Dorotea Avra}
\begin{document}
\maketitle
\begin{frame}{Background}
\begin{itemize}
\item From Wikipedia:\footnote{\url{https://en.wikipedia.org/wiki/Discrete_Fourier_transform}}
\end{itemize}
\begin{quotation}
the discrete Fourier transform is a discrete version of the Fourier
transform that converts a finite sequence of equally-spaced samples of a
function to a same-length sequence of equally spaced samples of the
discrete-time Fourier transform [\ldots].
\end{quotation}
\end{frame}
\begin{frame}{Background}
\begin{itemize}
\item If data sets contain periodic oscillations, one can analyse
them with ``spectral methods'', a.k.a. ``Fourier transform
methods''.
\item Sometimes it is easier or more useful to analyse and process
data in the frequency domain than it is in the time domain.
\item The discrete Fourier transform is a numerical method to
calculate the Fourier transform of a data set on a computer.
\end{itemize}
\end{frame}
\begin{frame}{Discrete Fourier transformation definition}
Consider a vector of $N$ evenly-spaced time series
points,\footnote{Discussion based on Chapter 5.2 Spectral Analysis from
\emph{Numerical Methods for Physics} by Alejandro L.\ Garcia, 1994.}
\begin{equation}
\mathbf{y} = [y_1, y_2, \ldots, y_N]
\end{equation}
The data are sampled every $\tau$ seconds, thus giving us a list of time
points defined by:
\begin{equation}
t_i = \tau (i - 1)
\end{equation}
where $i$ is the 1-based index of the input vector.
\end{frame}
\begin{frame}{Discrete Fourier transformation definition}
Given the vector of data points, $\mathbf{y}$, we can define its discrete
Fourier transform, $\mathbf{Y}$, as a vector:
\begin{equation}
Y_{k+1} = \sum_{j=0}^{N-1} y_{j+1} e^{-2 \pi i j k / N}
\end{equation}
where $i = \sqrt{-1}$. Note that $j$ and $k$ start at zero, whereas the
vector uses a 1-based index.
\end{frame}
\begin{frame}{Example}
Following the discrete Fourier transform discussion in
Garcia,\footnote{Chapter 5.2 Spectral Analysis from \emph{Numerical Methods for
Physics} by Alejandro L.\ Garcia, 1994.} using a sampling interval of
$\tau = 1$, $N = 50$ data points, a signal frequency of $f_s = 0.2$, and
a phase of $\phi_s = 0$ we obtain the following graph:
\centerline{
\includegraphics[width=0.5\textwidth]{dft-sine-wave.png}
}
\end{frame}
\end{document}
Assuming that you have LaTeX installed on your local machine, and that the code is saved in a file called discrete-fourier-transform.tex, you can build the document yourself by running latexmk in the discrete-fourier-transform directory. This will create a PDF file called discrete-fourier-transform.pdf.
Hereās the output I got for the title page:
That looks good. With this foundation in place, assume that weāve also created a corresponding repository on GitHub and have pushed the code upstream. If you want to follow along at home, or simply donāt want to copy-and-paste the code from above, you can clone the repository I created.
Getting GitHub to build the document for us
With a GitHub repository on hand, we can get GitHub to build our slides for us. To do that, we need to create a YAML configuration file in the location that GitHub expects to find continuous integration tasks, namely, .gitlab/workflows. Letās create the directory:
$ mkdir -p .github/workflows
Using your favourite editor, create a file called ci.yml within that directory, and fill it with these contents:
name: Build slides in CI
on:
[push, pull_request]
jobs:
build:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: sudo apt install texlive-latex-recommended texlive-latex-extra texlive-luatex latexmk
- name: Build slides
run: latexmk
In GitHub parlance, this file is called a workflow.
We denote the workflowās name by the value of the name keyword given at the top level.
The on keyword specifies which events will trigger a workflow. In the case here, weāve specified that the workflow should run on pushes to any branch or on any pull requests. Itās possible to make these settings more specific, but letās keep things simple.
Next up is the jobs keyword, which defines the jobs to run within the workflow. There can be many jobs in a workflow, but in our case, we only need one.
The keyword in the level under jobs defines a block of job-related tasks as well as the jobās name. The single job we defined in the workflow above is called build.
Weāve configured the build job to run on a host using Ubuntu 22.04 as its base Docker image via the runs-on keyword. You might wonder why I didnāt use the other available Ubuntu version (24.04), a.k.a. ubuntu-latest. Well, I had repository lookup errors when trying to install the required LaTeX packages. Hence, I reverted to the older Ubuntu version where the installation worked as expected. Stability matters. Especially when presenting worked examples.
The next keyword is called steps and defines the individual steps that the job should run.
The first step checks out our repository into our build host with the actions/checkout@4 GitHub action. The steps thereafter run shell commands via the run keyword. You can also give each step a name, as weāve done here, so that they are easier to find within the GitHub Actions tab.
Workflows appear in the Actions tab on the GitHub repository project page. Iām still a bit confused about how the naming works here. It seems that one combines actions into a workflow. However, the overarching term is GitHub Actions, under which one defines workflows. These then contain actions. It seems a bit circular to me, but I simply might not have understood the concept yet.
Anyway, the subsequent steps in this job install the relevant Ubuntu TeXLive packages and build the slides with latexmk.
To get GitHub to follow these instructions and thus build the document, we need to add it to our repository and commit that change:
$ git add .github/workflows/ci.yml
$ git commit
[main bc389e5] Build document using GitHub's CI system
Date: Sun Feb 8 21:49:21 2026 +0100
1 file changed, 17 insertions(+)
create mode 100644 .github/workflows/ci.yml
We push this change upstream to get GitHub to build our document.
$ git push
Enumerating objects: 6, done.
Counting objects: 100% (6/6), done.
Delta compression using up to 4 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (5/5), 663 bytes | 331.00 KiB/s, done.
Total 5 (delta 1), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
To github.com:paultcochrane/discrete-fourier-transform.git
315cf5a..bc389e5 main -> main
Opening the Actions tab in our GitHub discrete-fourier-transform project, we see a green tick next to the workflow run. This is a good sign: the workflow ran successfully. The workflow runās title is the commit messageās subject.
Clicking on the workflow runās title, weāre taken to a page with further details:
Within the grey box on this page, we see the name of the job we ran: build. Clicking on the smaller white box for the job, weāre taken to the details for the job:
Here we see the steps that the job took to build everything. If we click on the āBuild slidesā step, the section expands, and we see its build log. If youāre following along at home, youāll see lots of LaTeX build information. Scrolling to the end, youāll see output like this:
The important bits are at the end, in particular these lines:
Output written on discrete-fourier-transform.pdf (6 pages, 143709 bytes).
Transcript written on discrete-fourier-transform.log.
Latexmk: Log file says output to 'discrete-fourier-transform.pdf'
Latexmk: Examining 'discrete-fourier-transform.log'
=== TeX engine is 'LuaHBTeX'
Latexmk: All targets (discrete-fourier-transform.pdf) are up-to-date
where we find out that the document was written to a file called discrete-fourier-transform.pdf and that all targets are up to date. Awesome!
Archiving artefacts from the build
Ok, so the build looks good, but does the output of that build also look good? We canāt tell right now because we have no way of viewing it. GitHub built it, but deleted it as soon as the job finished.
Thatās ⦠suboptimal. We want to check the output as well. How to do that? For this task, we want to archive the build artefacts, which we can do with the actions/upload-artifact@v4 GitHub action.
To archive our GitHub-built PDF file, we append the following step to those defined in the build job in our ci.yml workflow file:
- name: Archive built PDF document
uses: actions/upload-artifact@v4
with:
name: dft-slides
path: discrete-fourier-transform.pdf
Now commit this change and push it:
$ git commit .github/workflows/ci.yml
[main d5ec2be] Archive built PDF document as artefact
Date: Mon Feb 9 14:04:58 2026 +0100
1 file changed, 6 insertions(+)
$ git push
Enumerating objects: 9, done.
Counting objects: 100% (9/9), done.
Delta compression using up to 4 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (5/5), 585 bytes | 585.00 KiB/s, done.
Total 5 (delta 2), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (2/2), completed with 2 local objects.
To github.com:paultcochrane/discrete-fourier-transform.git
bc389e5..d5ec2be main -> main
Revisiting the details page for the build job:
we see a new step appear after āBuild slidesā called āArchive built PDF documentā. Clicking on this step, we see its log output:
This part of the log output is interesting:
Artifact dft-slides.zip successfully finalized. Artifact ID 5432273684
Artifact dft-slides has been successfully uploaded! Final size is 137788 bytes. Artifact ID is 5432273684
Artifact download URL: https://github.com/paultcochrane/discrete-fourier-transform/actions/runs/21826316227/artifacts/5432273684
because it shows that the build artefact (called dft-slides.zip) was created successfully, and we see a link where we can download it.
Of course, you could click on this link to download the archived artefacts. However, a simpler way to access build artefacts is to look at the overview page for a given workflow run. There, youāll see a new section, called āArtifactsā:4
From here, you can click on the little download button on the right-hand side. This will download the .zip file that the workflow run created.
If you download that file now and open it, youāll find the document that GitHub built:
Now things are looking good!
Finding font failures
Not very surprisingly, this result looks much the same as our earlier result. I say much the same because the results are not identical. In particular, the fonts are different. It seems that some fonts have been replaced by default ones. A quick look at the āBuild slidesā log output reveals:
luaotfload | db : Reload initiated (formats: otf,ttf,ttc); reason: Font "Fira Sans Light" not found.
Package beamerthememetropolis Warning: Could not find Fira Sans fonts on
input line 95.
So, the Fira Sans font that the metropolis theme needs isnāt available. This is a secondary issue that we want to fix by using an up-to-date TeXLive distribution.
Migrating to latex-action
Ok, weāve got a document built on GitHub, and itās almost producing the PDF output we expect. Now we need to get around the issue of using an outdated TeXLive distribution. Instead of using a version from 2021, we want to use a version from at least 2025.
This is what we really came here for: to see the latex-action action in action.
To be honest, the change is very simple: remove the āInstall dependenciesā step and swap out the current āBuild slidesā step with this YAML code:
- name: Build slides
uses: xu-cheng/latex-action@v4
with:
root_file: discrete-fourier-transform.tex
texlive_version: 2025
latexmk_use_lualatex: true
The new step uses the same name as the step it replaced, but now weāre using a GitHub action via the uses keyword instead of running latexmk.
Weāre also being careful to specify a version. Theoretically, it should be possible to do without the @v4 suffix, but specifying an explicit version is good practice. This way, we ensure builds are more reproducible. Were we to leave out the version suffix, there could be future changes to latex-action that break an otherwise functioning build. Of course, updating to a new version would be a good idea once it exists, but we need to check that it works as expected before committing to it.
We configure options to the latex-action GitHub action within the with keyword block. The root_file option specifies the main file that builds the entire document. The texlive_version option specifies the TeXLive version we want to use, i.e. 2025. We also want to use LuaLaTeX when running the latexmk command, so that we get the full power of a modern LaTeX engine. Hence, we set latexmk_use_lualatex to true.
Whatās the effect of this change? Well, instead of pulling in an Ubuntu Docker image and then installing the Ubuntu TeXLive packages, we now grab a Docker image with TeXLive pre-installed and use that as our build host. This gives us a complete TeXLive environment from the get-go.
Letās see how we get on. Doing our commit and push dance:
$ git commit .github/workflows/ci.yml
[main bbe6dba] Replace Ubuntu TeXLive with latex-action TeXLive
Date: Mon Feb 9 14:55:41 2026 +0100
1 file changed, 5 insertions(+), 4 deletions(-)
$ git push
Enumerating objects: 9, done.
Counting objects: 100% (9/9), done.
Delta compression using up to 4 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (5/5), 562 bytes | 562.00 KiB/s, done.
Total 5 (delta 2), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (2/2), completed with 2 local objects.
To github.com:paultcochrane/discrete-fourier-transform.git
d5ec2be..bbe6dba main -> main
and letting GitHub do its thing, we find that everything builds successfully:
Thatās a good sign.
If we now download our archived artefacts from the workflow overview page (note that the file size is also smaller):
we see that the font issue has been corrected:
And now weāre using an up-to-date TeXLive version! Yay! š
Where to from here?
And thatās it really. Of course, there are more things you could do to extend and customise the build process.
One thing you might like to try is to add the action-gh-release GitHub action. This action automates creating releases according to Git tags. Your Git project will then host any PDF documents you deem worthy of a versioned release (and hence a Git tag) on the repositoryās project page. If you want to upload draft releases, thatās also possible. This way, you can show users current development-grade releases of your LaTeX documents.
The skyās the limit! Have fun!
Addendum: metropolis and moloch woes
One other incentive I had to use the latex-action GitHub action was a desire to replace the metropolis beamer theme with moloch.
I like metropolis. Itās clean and minimalistic, and looks rather stylish. Sadly, itās now unmaintained, and the last release to CTAN was from 2017. Itās been replaced by its fork, a project called moloch. Its internals are cleaner, it fixes some bugs in the metropolis theme, and it is less fragile to internal changes in beamer itself.
Unfortunately, moloch is only available in recent TeXLive versions (it was first released in 2024) and hence isnāt available in the Ubuntu TeXLive packages on GitHub. Hence, my wish to migrate to a GitHub action providing an up-to-date TeXLive version.
Implementing this change while maintaining the same look led down rather a deep rabbit hole. Regrettably, I wasnāt able to solve all the problems that appeared. Thus, I decided against introducing moloch as an additional change in the main part of this article.
One major blocking issue I had was with fonts. By default, metropolis uses the Fira Sans font; moloch doesnāt: it seems to use a mixture of the Computer Modern and Latin Modern fonts. Using Fira Sans with moloch is simple enough: itās a configuration option added in the document preamble. However, this also changed the font used for mathematics, and I couldnāt get that to look right. For instance, bold mathematics were shown with a serif font, whereas metropolis used a sans-serif font without problems. Using either the Fira Math or firamath-otf packages bundled with TeXLive didnāt help, because they currently only provide the regular typeface. The lighter typefaces arenāt yet bundled with TeXLive, thus Iām hoping a future TeXLive update might help this situation.
Oh well. Ya canāt win every day! Iām going to stick to metropolis for the time being. It still does the job well and works (for my purposes) with the current TeXLive version.
Once I realised that this detail wasnāt necessary for what I wanted to introduce in the article, I decided to leave it out of the main text. Iām guessing no one missed it but me! š
Does anyone remember the prosper or foiltex classes? Wow, it seems like aeons ago that I was using them. I think Iām showing my age⦠ā©
Iām aware that the
ubuntu-24.04a.k.aubuntu-latestDocker image is available. However, I got errors when fetching packages. Thus, for this article, and the sake of stability for anyone following along at home, I stuck withubuntu-22.04, where I knew that everything worked. ā©Dorotea Avra is not the name of a real person. I generated it using a random name generation site. ā©
Note that American English spells this word as āartifactā. Since I grew up with a British English-derived variant of the English language, I spell it āartefactā. Just so you know. ā©












Top comments (0)