Markdown to PDF in CI: introducing glu-action
glu-action is a GitHub Action that runs glu inside your workflow. One step in a job renders a PDF with embedded fonts, hyphenation across languages, and optional PDF/UA tagging. No TeX distribution, no container image.
- uses: actions/checkout@v6
- uses: boxesandglue/glu-action@v1
with:
input: docs/manual.md
- uses: actions/upload-artifact@v7
with:
name: manual
path: docs/manual.pdfThat is the whole action. It downloads the matching glu release binary, prepends it to $GITHUB_PATH, and runs glu on your input. No sudo, no Docker layer, no cache warm-up.
Why another Markdown-to-PDF action?
The Marketplace already has several Markdown-to-PDF actions. What they do not cover is the more interesting part.
Pandoc-based actions are versatile but stop at “produce a PDF”. Accessibility tagging, deterministic output, and multi-pass aux-file convergence (for tables of contents, cross-references, page numbers that resolve correctly) usually require either a LaTeX backend (slow, large) or extra tooling around the action.
LaTeX-based actions like Tectonic support those features. Build times are then measured in minutes per document, with a toolchain cache of several hundred megabytes.
glu-action sits between the two. It produces tagged, accessible PDFs with cross-references and convergent aux passes, in the same time budget as a plain Markdown-to-HTML step.
Two specifics
PDF/UA from the frontmatter
Accessibility is a compliance topic now. The European Accessibility Act (Directive (EU) 2019/882) applies from 28 June 2025 and covers PDFs that are delivered as part of services such as banking, e-commerce, e-books, and passenger transport. The Web Accessibility Directive (Directive (EU) 2016/2102) has applied to public-sector websites and documents since 2018, with mobile apps added in 2021. Both directives reference the harmonised European standard EN 301 549, which in turn requires PDF/UA-1 (ISO 14289-1) for PDF content. Procurement specs and supplier audits across the EU therefore reference PDF/UA, often without naming the standard in the contract itself.
With glu, you opt in from the Markdown frontmatter:
---
title: Annual report 2026
lang: en
format: PDF/UA
---That single declaration enables the full tagging pipeline: a structure tree with semantic roles for headings, lists, tables, and figures; /Lang on the document and on per-paragraph language switches; /Alt text on images; XMP metadata with pdfuaid:part 1; and /StructParent wiring for imported PDF page assets (Form XObjects). The CI step does not change. The frontmatter does the work.
Reproducible builds
Identical input should produce a byte-identical PDF. glu accepts --source-date-epoch (the Reproducible Builds convention) and clamps all timestamps in the output to that value:
- uses: boxesandglue/glu-action@v1
with:
input: docs/manual.md
args: --source-date-epoch ${{ github.event.head_commit.timestamp }}The PDF hash becomes a function of the content only. Useful for caching, audit trails, PR-diffing, and for detecting whether a document actually changed, without false positives from embedded timestamps or UUIDs.
What you can do with it
Documentation in CI
The obvious use. Render your README or docs/ directory on every push to main and attach the PDF as a release asset. Pin a version: so the rendering stays stable across glu updates. A “docs must build” check on pull requests catches broken Markdown before merge.
Multi-language docs work as a job matrix, with one job per language, each consuming a different Markdown source with the corresponding lang: frontmatter. RTL scripts (Arabic, Hebrew) are auto-detected from content; no extra configuration needed.
Reports and status documents
Combine the action with Lua scripts that read JSON files, query an API, or pull data from a database, and you get a templated report pipeline. glu’s aux-convergence loop handles content-dependent layout (tables of contents, “page X of Y” footers, cross-references) without a separate post-processing step.
Quarterly reports, weekly status emails, auto-generated changelogs from gh api output: each becomes a reproducible workflow step.
Accessible publications
Beyond format: PDF/UA, pair the action with a validator like veraPDF as a second step:
- uses: boxesandglue/glu-action@v1
with:
input: report.md
output: dist/report.pdf
- run: verapdf --flavour ua1 dist/report.pdfThe build fails if the PDF does not pass UA-1 validation. For organisations facing accessibility audits this is the difference between testing once and testing on every change.
PR previews
Render the PDF on every pull request and post a comment with the artifact-download link, optionally also a first-page screenshot via pdftoppm. Reviewers see the actual output instead of a preview that might diverge from production.
Because builds are deterministic, you can also diff the PR’s PDF against the one from main and show only the meaningful changes.
Composing with other toolchains
The action doesn’t need to own the whole pipeline. A few patterns that work well:
- Static site + PDF download: Hugo, Astro, or Jekyll generates HTML for the website; glu-action renders the same Markdown source as a downloadable PDF.
- ETL → report: a data-pipeline step writes JSON, a Lua-template step renders Markdown with embedded glu Lua blocks, the action produces the final PDF.
- Release announcements: a tag push triggers the action, the resulting PDF is emailed to a mailing list as a formatted attachment.
Action inputs
| Input | Purpose |
|---|---|
input |
The Markdown, HTML, or Lua file to render |
output |
Where the PDF is written (defaults to <input>.pdf) |
css |
Additional stylesheet, passed to glu as --css |
version |
Pin a specific glu release for reproducibility |
args |
Catch-all for everything else (--max-passes, --source-date-epoch, --safe, …) |
The action is a thin wrapper around glu. It handles platform detection, release download, and $GITHUB_PATH plumbing. Everything that controls how the PDF is rendered (fonts, page size, PDF/UA mode, document language, callbacks for page decorations) is configured in the Markdown frontmatter or a companion Lua file, checked in alongside the content.
The output_path output makes it easy to chain:
- id: render
uses: boxesandglue/glu-action@v1
with:
input: docs/manual.md
- uses: actions/upload-artifact@v7
with:
name: manual
path: ${{ steps.render.outputs.output_path }}What the action is not
Some limits:
- Not a replacement for a Markdown editor. Authoring stays local. The development loop is
glu --watch foo.md. The action is for the publish step. - Not a full LaTeX workflow replacement. If you depend on BibTeX, microtype, or specific math typography from the LaTeX ecosystem, glu does not replace that. glu covers the documents that do not need LaTeX’s full breadth, with embedded fonts, structured PDF output, and significantly shorter build times.
- No Windows runner yet. glu itself ships a Windows binary, but the action’s runner matrix does not cover Windows. Linux (amd64, arm64) and macOS (amd64, arm64) are supported. If you need Windows, open an issue.
Getting started
Repository: boxesandglue/glu-action. The minimal workflow is the snippet at the top of this post. From there, add format: PDF/UA to the Markdown frontmatter, pin a version: once a release works for you, and pass args: --source-date-epoch ... for reproducible output.
For missing flags, missing platforms, or integration patterns that should be documented, please open an issue against the action or against glu itself.