CI(GitHub Actions など)

「自分の環境では通るのに」——LaTeX 文書を誰かと共同で書いていれば、いつか必ず言うことになる台詞です。CI(継続的インテグレーション)でビルドするというのは、この台詞を機械に検証させるということで、push のたびにまっさらな TeX Live 環境がリポジトリを一から取り直し、PDF が本当に再現するかどうかを代わりに答えてくれます。共著者の TeX Live には別バージョンのパッケージが入っている、\setmainfont が指しているフォントは自分のノート PC にしかない、.bbl を commit し忘れている——どれもありふれた話で、どれも原因を作った当の機械では決して再現しません。このページでは、GitHub Actions で PDF をビルドする最小の workflow から、TeX Live を runner に持ち込む三つの方法、キャッシュ、成果物の配布、そして一番厄介な失敗——CI は緑なのに PDF が壊れている——までを順に見ていきます。

なぜ LaTeX を CI でビルドするのか

理由は一つで、手元の環境が証拠にならないからです。LaTeX の出力は文書のソースだけでは決まりません。その機械に入っている TeX Live の年度、個々のパッケージのリビジョン、システムにインストールされたフォント、TEXINPUTS のどこかに紛れ込んだ古い .sty にまで依存します。だから「手元では通った」という報告には、本当は「2024 年の私の TeX Live で、3年前に手で置いたクラスファイルがある状態では」という長い但し書きが隠れています。CI はこの但し書きを毎回はっきりさせる装置です。ジョブは空のコンテナから始まり、リポジトリに入っているものしか見えません。だから PDF ができたなら、それは「リポジトリの中身だけで PDF ができる」ことが証明されたということになります。

この考え方の最大の実例は arXiv です。arXiv は投稿された PDF をそのまま公開するのではなく、送られてきた LaTeX ソースを自前のサーバーでコンパイルし直します。しかも著者が選べる TeX Live は常に二つのバージョンだけで、それぞれ特定の日付の状態に凍結されています。世界最大の LaTeX ビルドサーバーが真っ先にやったことが「環境を固定する」だった、というのは示唆的です。自分のリポジトリで同じことをする道具が CI で、副産物として、TeX を入れていない共著者や査読者にも常に最新の PDF を渡せるようになります。ビルドの内容は .github/workflows/ 以下に置いた YAML ファイルに書きます。

GitHub Actions で PDF をビルドする最小の workflow

必要なステップは三つだけです——ソースを取ってくる、コンパイルする、PDF を保存する。次の YAML を .github/workflows/build.yml として置けば、それで動きます。actions/checkout がリポジトリを runner に展開し、xu-cheng/latex-action が TeX Live 入りのコンテナの中でコンパイルし、actions/upload-artifact が出来上がった PDF をワークフロー実行ページに貼り付けます。on: [push, pull_request] にしてあるのは、レビューに壊れた PDF が回ってくるのを防ぐためです。

terminal
# .github/workflows/build.yml
name: Build LaTeX
on: [push, pull_request]
permissions:
  contents: read
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: xu-cheng/latex-action@v4
        with:
          root_file: main.tex
      - uses: actions/upload-artifact@v7
        with:
          name: pdf
          path: main.pdf
          if-no-files-found: error

xu-cheng/latex-action で必須の入力は root_file だけです。中で実際に走るのは latexmk で、既定の引数は -pdf -file-line-error -halt-on-error -interaction=nonstopmode——つまり pdfLaTeX で、エラーが出たら即座に止まる設定が最初から入っています。-file-line-error はエラー表示を file:line: message の形に変えるもので、CI のログを追うときに効きます。エンジンを替えたいときは latexmk_use_xelatex: true または latexmk_use_lualatex: true、TeX Live の年度を固定したいときは texlive_version を指定します。ベースは既定で Alpine Linux で、os: debian に切り替えられます。追加のシステムパッケージが要るなら extra_system_packages、フォントを持ち込むなら extra_fonts を使います。

upLaTeX と dvipdfmx を組み合わせる日本語文書のように、手順そのものが標準から外れている場合は、.latexmkrc をリポジトリに置いてしまうのが一番確実です。CI もローカルも同じ設定ファイルを読むので、二か所を別々に直す必要がなくなります——latexmk 側の設定の書き方は自動ビルドのページに譲ります。もう一つ、テンプレートを長く使うつもりなら覚えておきたいのが、アクションのメジャーバージョンは動くということ。たとえば actions/checkout は v7 で挙動が変わり、pull_request_targetworkflow_run から起動されたときに fork の PR のコードを既定でチェックアウトしなくなりました。年に一度は公式 README を見る日を決めるか、依存更新の PR を受け入れる設定にしておきます。

TeX Live を runner に持ち込む三つの方法

選択肢は三つです——アクションに任せる、runner に自分でインストールする、TeX Live 入りの Docker イメージをジョブのコンテナにする。どれを選ぶかは「環境をどこまで自分で決めたいか」と「毎回どれだけ待てるか」で決まります。というのも、TeX Live は巨大だからです。手元の TeX Live 2024 をフルインストールで測ると、ドキュメントとソース込みで 8.7 GB ありました。Island of TeX が配布している texlive/texlive イメージは、ドキュメントとソースを削ぎ落としたうえで Docker Hub 上の圧縮サイズが約 2.5 GB です。この数字が、以下の判断のほとんどを決めています。

方法TeX Live の入手向いている場面
xu-cheng/latex-actionアクション自身が TeX Live 入りの Docker イメージを引いてくるとにかく最短で動かしたい。設定は root_file だけで済ませたい
TeX-Live/setup-texlive-actionrunner 上に tlmgr でインストールし、TEXDIR をキャッシュする必要なパッケージだけ入れたい。Linux 以外の runner でも回したい
texlive/texliveジョブの container: に指定する Island of TeX の Docker イメージコンテナの中身を自分で決めたい。日付付きタグで環境を凍結したい

texlive/texlive は Docker Hub と registry.gitlab.com/islandoftex/images/texlive の両方で配られていて、既定のタグは full スキーム——ただしドキュメントとソースを外したもの——です。必要なら -doc-src-doc-src の各フレーバーがありますが、そのぶん確実に重くなります。latest は毎週作り直されるので、締切のある提出物では TL2022-2022-06-05 のような日付入りスナップショットのタグに固定するのが安全です。過去の年度をそのまま再現したいときは TL2018-historic のような historic タグが用意されています。ジョブの container: にイメージ名を書けば、あとのステップはすべてその中で走ります。

terminal
# pin the image; latest is rebuilt weekly
jobs:
  build:
    runs-on: ubuntu-latest
    container: texlive/texlive:latest
    steps:
      - uses: actions/checkout@v7
      - run: latexmk -pdf -halt-on-error -interaction=nonstopmode main.tex
      - uses: actions/upload-artifact@v7
        with:
          name: pdf
          path: main.pdf
          if-no-files-found: error

TeX Live のインストールをキャッシュして待ち時間を削る

TeX-Live/setup-texlive-action を使うなら、キャッシュは既定で有効です。このアクションは cache 入力が true を既定値としていて、内部で @actions/cache を呼び、TEXDIR をまるごと保存します。保存はジョブが終わったあとの後処理で行われるため、実行中に生成されたフォントキャッシュのようなものまで一緒に持ち越されます。だから二回目以降のビルドは、tlmgr がミラーから落とし直す時間をまるまる飛ばせます。切りたいときは cache: false です。なお、このアクションのリポジトリは以前 teatimeguest/setup-texlive-action にありましたが、いまは TeX-Live 組織へ移っています——古い記事の YAML をそのまま貼ると解決しません。

terminal
      - uses: TeX-Live/setup-texlive-action@v4
        with:
          version: 2025
          packages: |
            scheme-basic
            latexmk
            biber
            biblatex
      - run: latexmk -pdf -halt-on-error -interaction=nonstopmode main.tex

packagesscheme-basic を置き、そこから必要なものを足していくのがこのアクションの流儀です。8 GB の全部を持ってくるのではなく、biberbiblatexsiunitx のように実際に \usepackage しているものだけを列挙します。リストが長くなってきたら package-file.github/tl_packages**/DEPENDS.txt のようなファイルを指定して外に出せます。version は年度を指定でき、これが arXiv のやっていることの小さな再現になります——「latest でビルドできる」ではなく「2025 年の TeX Live でビルドできる」と言えるようになるわけです。Docker イメージを使う場合、actions/cache でイメージ自体をキャッシュしようとするのは筋が悪く、代わりにタグを固定してレジストリのプル任せにするほうが素直です。

できた PDF を配る——artifact に上げるか、Release に添付するか

査読者に「まず TeX Live を入れてください」と言わずに済ませるための仕組みが、この二つです。actions/upload-artifact を使うと PDF がワークフロー実行ページに貼り付き、リポジトリを見られる人なら誰でもダウンロードできます。保持期間はリポジトリの設定で決まり、上限は 90 日です。ここで一つ、地味に効く設定があります——archive の既定値は true で、成果物は zip に固められてから上がるため、受け取った側は main.pdf ではなく zip をダウンロードすることになります。archive: false にすると単一ファイルをそのまま上げられるので、共著者に渡すときの手間が一段減ります。

公開版として配りたいなら Release のほうが向いています。artifact は保持期間が切れれば消えますが、Release に添付した PDF は消えず、恒久的な URL が付き、リポジトリのトップから辿れます。運用としては「バージョンタグを push したら Release を作る」という形が定石で、GitHub がホストする Ubuntu の runner イメージには GitHub CLI(gh)が最初から入っているので、追加のアクションを増やさずに一行で書けます。ただし Release の作成は書き込み操作なので、permissions:contents: write に上げ、GH_TOKEN を渡す必要があります。ビルド用の workflow は contents: read のままにしておき、リリース用は別ファイルに分けるのが安全です。

terminal
# .github/workflows/release.yml
name: Release PDF
on:
  push:
    tags: ["v*"]
permissions:
  contents: write
jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: xu-cheng/latex-action@v4
        with:
          root_file: main.tex
      - run: gh release create "$GITHUB_REF_NAME" main.pdf
        env:
          GH_TOKEN: ${{ github.token }}
  • if-no-files-found: error を必ず付けます。既定は warn なので、path: を書き間違えても workflow は緑のまま終わります。
  • archive: false にすると PDF が zip でなくそのまま上がります(単一ファイルのときだけ使えます)。
  • ビルド設定(.latexmkrc など)はリポジトリに同梱します。ローカルと CI が同じ手順を踏むようになり、差分の原因が一つ減ります。
  • 提出物では TeX Live のバージョンを固定します。texlive_versionsetup-texlive-actionversion、または日付入りのイメージタグのいずれかで指定できます。
  • permissions: は既定を contents: read にしておき、Release を作るジョブだけ contents: write に上げます。

CI が落ちるとき、そして緑なのに PDF が壊れているとき

まずログを開き、行頭が ! で始まる行を探します。LaTeX のエラーは必ずその形で出るので、何百行の中からでもすぐ見つかります。よくある症状は五つか六つに収まり、しかもどれも原因がほぼ一対一で決まります。

ログに出る文字列起きていること手当て
! LaTeX Error: File `...sty' not found.CI 側の TeX Live にそのパッケージが入っていないpackages に足す、extra_system_packages を使う、または full スキームのイメージに切り替える
! Undefined control sequence.文書側のタイプミス、または命令を提供するパッケージが未読み込みその行の綴りと \usepackage を確認する。ローカルでも同じエラーが出るはず
! Package fontspec Error: The font "..." cannot be found.コンテナにそのフォントが存在しない。ローカルでは OS が持っていたフォントをリポジトリに入れて extra_fonts で渡すか、TeX Live 同梱のフォントに替える
LaTeX Warning: There were undefined references.警告どまり。終了ステータスは 0 で、PDF には ?? が残るlatexmk に任せて必要な回数だけ回す。落としたいならログを grep して失敗させる
No files were found with the provided pathupload-artifactpath: と実際の出力名が食い違っている出力先を確認する。if-no-files-found: error にしていないと緑のまま終わる
! Emergency stop.TeX が対話プロンプトに落ちたが、CI には応答する端末が無い-interaction=nonstopmode を付ける。latex-action は既定で付けている

ここで -interaction=nonstopmode について、よくある誤解を一つ解いておきます。このオプションがエラーを握りつぶすわけではありません。未定義の命令を含む文書を pdflatex -interaction=nonstopmode に通すと、終了ステータスはきちんと 1 になります。ただし、その一方で PDF は出力されてしまいます——エラーの箇所を飛ばして最後まで走り切るからです。だからこのオプションが実際にやっているのは「止まって人間に聞かない」ことだけで、成否の情報は失われません。同じ文書を latexmk -pdf に通すと、こちらは非ゼロで終了したうえで PDF も残しません。CI ではその挙動のほうが望ましく、-halt-on-error を併用すれば最初のエラーで打ち切れます。

本当に静かに壊れるのは、エラーではなく警告のほうです。\ref\cite の参照先が解決しなかった場合、LaTeX が出すのは LaTeX Warning: There were undefined references. という警告だけで、終了ステータスは 0。CI は緑になり、artifact には ?? の並んだ PDF が上がります。しかもここに if-no-files-found の既定値 warn が重なると、path: を書き間違えた workflow が、何も残さないまま最後まで緑で終わるという状況すら成立します。緑を信用するには、少なくとも if-no-files-found: error を書き、latexmk に必要な回数のコンパイルを任せておく必要があります。

最後に、「手元では通るのに CI だけ落ちる」という残りのケース。原因はほぼ、リポジトリに入っていないものに依存していることです。手元にしかない画像や生成済みの .bbl.gitignore に引っかかって commit されていない生成ファイル、そしてよくあるのがファイル名の大文字小文字です。macOS や Windows の既定のファイルシステムは大文字小文字を区別しないので、\includegraphics{Figure1}figure1.pdf は手元では一致してしまいますが、runner の Linux では別のファイル名です。CI が落ちたら、まず「これはリポジトリの中にあるか」を確認してください。裏を返せば、この確認を毎 push 自動でやってくれるところに CI の価値があります。