LaTeX を使った文書作成の良い所は Git 及び GitHub を使ったバージョン管理が容易なことですが、ウォンテッドリーの業務で CI/CD パイプラインの改善に取り組んだ後で振り返ると、折角 GitHub を使うのであれば GitHub Actions を使って定型フローを自動化できないか試したくなります。LaTeX で文書を作成する際の定型作業と言えば PDF 等のビルドですが、ローカルで開発している際には latexmk 等を用いて自動化できるにせよ、作業漏れが怖いので GitHub 側でもビルドを行うようにしておきたいですよね。
本記事では LaTeX で書かれた文書をモノレポで管理している場合に、GitHub Actions で PDF ファイルのビルドを自動化する際のワークフローを作成します。同じ研究テーマで卒論と修論を書いている場合など、関連する LaTeX 文書を単一のリポジトリでまとめて扱いたい場面は多々あると思われますが、本記事で作成するワークフローを用いればそのようなリポジトリ構成にも対応できることでしょう。
リポジトリ構成
まず、本記事で LaTeX 文書ビルドのワークフローを構築するリポジトリですが、例えば article1 と article2 の二つの文書を管理している場合には次のディレクトリ構造を取るような構成を想定しています。
src
├─ article1
│ ├─ article1.tex
│ └─ chapter1.tex
└─ article2
├─ article2.tex
└─ chapter1.tex
asset
├─ article1
│ └─ photo1.png
└─ article2
└─ photo1.jpg
generated
├─ article1.pdf
└─ article2.pdf
src ディレクトリ配下には文章毎にディレクトリを切ってそれぞれの LaTeX ソースコードを配置し、asset ディレクトリ配下でも文章毎にディレクトリを切ってそれぞれの LaTeX 文書で使う画像等を配置する訳ですね。BibTeX が使いたい場合は bibliography ディレクトリを追加しても良いですが、説明を簡単にするために一旦省略します。
LaTeX では \include を使って複数のファイルに分割したソースコードから単一の文書を作成する事もできるのですが、その場合どのファイルを起点としてビルドを行うのか自明ではないため、起点となるファイルにはディレクトリ分割の際に用いた article1 の様な文章名を付ける規約だけ設けておきます。今回作成するワークフローではそれ以外にファイル名の制限はなく、LaTeX で扱えるものであれば何でも構いません。例えば上のディレクトリ構成図で挙げている chapter1.tex や photo1.png の様なファイルは、極端な話自分が覚えられるのであれば lorem.tex であったり ipsum.png であったりしても良い訳です。
また、LaTeX 文書をビルドして得た PDF ファイルの配置先ですが、今回作成するワークフローではトップレベルの generated ディレクトリ以下に article1.pdf といった文書名に対応する名前で追加した上で commit して、GitHub 上のリポジトリに push する事を想定しています。成果物の配置先としては artifact としてアップロードしたり、release に追加したりだとか色々考えられますが、Git のリポジトリ内でソースコードと共に管理する事を選んだ理由としては GitHub の UI 上で閲覧が楽なことが挙げられます。
差分ビルド
モノレポで LaTeX 文書を管理していても殆どのコミットでは一つの文書しか変更しませんから、高速化のために変更のあった文書だけをビルドする実装にすると良さそうです。一般に LaTeX 文書のビルド環境は巨大になりがちで、有名なディストリビューションの TeX Live をフルにインストールすると 8GB 近くにもなるぐらいですから、小さい文書しかビルドしない場合にも差分ビルドによる環境構築のコスト削減が効いてきます。
本記事では、dorny/paths-filter を使って変更されたファイルを検知した上で、ディレクトリ構造に基づいて変更のあった文書を検知し、そのリストをビルド用ジョブのマトリックスに渡す事で差分ビルドを行います。
filter-modified-docs:
runs-on: ubuntu-slim
steps:
- uses: actions/checkout@v6
- uses: dorny/paths-filter@v3
id: filter
with:
list-files: json
filters: |
changed:
- 'src/**'
- 'asset/**'
- name: List modified document directories
id: list-modified-doc-directories
run: |
echo "result=$(jq -c "$JQ_FILTER" <<< "$JQ_INPUT_JSON")" > $GITHUB_OUTPUT
env:
JQ_INPUT_JSON: ${{ steps.filter.outputs.changed_files }}
JQ_FILTER: 'map( capture("^(src|asset)/(?<a>[a-zA-Z0-9_-]+)/") | .a ) | unique | .'
outputs:
matrix: ${{ steps.list-modified-doc-directories.outputs.result }}
dorny/paths-filter というのは直近 GitHub に push されたコミットや PR で変更されたファイルを検知してくれるアクションで、例えば次のように list-files に json を指定すると、JSON で変更のあったファイルを列挙してくれます。
uses: dorny/paths-filter@v3
with:
list-files: json
{
"changed_files": [
"src/article1/article1.tex",
"asset/article1/photo1.png"
]
}
また、ビルドを行って欲しいのは LaTeX 文書に変更があった時で、それ以外の .gitignore 等別のファイルは変更があっても無視して欲しいのですが、dorny/paths-filter に filters でパスを指定すると、特定のパスのみの変更を列挙してくれます。
uses: dorny/paths-filter@v3
with:
list-files: json
filters: |
changed:
- 'src/**'
- 'asset/**'
dorny/paths-filter で変更のあったファイルの列挙は行えた訳ですが、LaTeX 文書のビルドは文書単位で行うので、変更のあったファイルがどの文書のものかを割り出してやる必要があります。これは、文書名に基づいたディレクトリ名を付ける命名規約を採用していたので、パスを見れば判別できます。
例えば src/article1/article1.tex と asset/article1/photo1.png に変更があった場合は文書 article1 のみをビルド対象とする様なマトリクスを作る必要がある訳ですが、src 以下と asset 以下のパスは文章名に基づいて切る規約を採用しているので、src/{ ディレクトリ名 }/ や asset/{ ディレクトリ名 }/ のディレクトリ名の部分を抜き出してやれば良さそうですね。
本記事では、jq を使って正規表現でパスから文書名を抜き出し、重複排除を行う事で変更のあった文書名を列挙する事にします。ここで重複排除も行うのは、ビルド用ジョブのマトリクスに渡した際に、同じ文書を複数回ビルドしないようにするためです。
run: |
echo "result=$(jq -c "$JQ_FILTER" <<< "$JQ_INPUT_JSON")" > $GITHUB_OUTPUT
env:
JQ_INPUT_JSON: ${{ steps.filter.outputs.changed_files }}
JQ_FILTER: 'map( capture("^(src|asset)/(?<a>[a-zA-Z0-9_-]+)/") | .a ) | unique | .'
LaTeX 文書のビルド
本記事では、LaTeX 文書のビルドは事前に変更のあったファイルから準備しておいたマトリクスに基づいて、xu-cheng/latex-action を実行する事で行います。
build:
runs-on: ubuntu-latest
needs: filter-modified-docs
if: ${{ needs.filter-modified-docs.outputs.matrix != '' && toJson(fromJson(needs.filter-modified-docs.outputs.matrix)) != '[]' }}
strategy:
matrix:
tex: ${{ fromJson(needs.filter-modified-docs.outputs.matrix) }}
steps:
- uses: actions/checkout@v6
- name: LaTeX compilation
uses: xu-cheng/latex-action@v4
with:
args: "-pdfdvi"
work_in_root_file_dir: true
root_file: src/${{ matrix.tex }}/${{ matrix.tex }}.tex
xu-cheng/latex-action というのは TeX Live の実行環境を用意して LaTeX のビルドツールである latexmk を使って LaTeX 文書のビルドを行ってくれるアクションです。その際ビルドの基点となるファイルを指定する必要がある訳ですが、これは文書名と同じファイル名を付ける規約を採用しておいたので簡単に定まります。
name: LaTeX compilation
uses: xu-cheng/latex-action@v4
with:
args: "-pdfdvi"
work_in_root_file_dir: true
root_file: src/${{ matrix.tex }}/${{ matrix.tex }}.tex
なお、latexmk で使う LaTeX エンジンに uplatex を使いたいだとか細かいビルドオプションを指定したい場面も出てくると思いますが、それに関してはあまり xu-cheng/latex-action は調整が効かないので .tex ファイルと同じディレクトリに .latexmk ファイルを置いて設定を書いておくと良いでしょう。
$latex = 'uplatex -halt-on-error';
$dvipdf = 'dvipdfmx %O -o %D %S'
成果物のコミット
本記事では LaTeX 文書をビルドして得られた成果物を、stefanzweifel/git-auto-commit-action を使って commit と GitHub 上のリポジトリへの push を行う事で公開します。
commit-and-push:
runs-on: ubuntu-latest
needs:
- build
- filter-modified-docs
permissions:
contents: write
steps:
- uses: actions/checkout@v6
- name: Download artifact
uses: actions/download-artifact@v7
with:
path: generated
merge-multiple: true
- name: Push the result of make command
uses: stefanzweifel/git-auto-commit-action@v7
with:
commit_message: "Build tex file on ${{ needs.filter-modified-docs.outputs.matrix }}"
成果物のコミットにあたって問題となるのは複数の文書がビルドされた際で、Git のログを綺麗にする意味でもコンフリクトを避ける意味でも複数の成果物を一回でまとめて commit と push を行うのが望ましいです。
マトリクス戦略で LaTeX 文書をビルドした後すぐに stefanzweifel/git-auto-commit-action を使ってしまうと複数回 commit と push が走ってしまうので、ビルド用のジョブとは別でジョブを作って stefanzweifel/git-auto-commit-action を実行する事にします。ジョブ間での成果物の受け渡しは actions/upload-artifact と actions/download-artifact を使って artifact 経由で行うと比較的容易です。
build:
(中略)
steps:
(中略)
- name: Upload to artifact
uses: actions/upload-artifact@v5
with:
name: ${{ matrix.tex }}
path: src/${{ matrix.tex }}/*.pdf
commit-and-push:
runs-on: ubuntu-latest
needs:
- build
- filter-modified-docs
permissions:
contents: write
steps:
- uses: actions/checkout@v6
- name: Download artifact
uses: actions/download-artifact@v6
with:
path: generated
merge-multiple: true
- name: Push the result of make command
uses: stefanzweifel/git-auto-commit-action@v7
with:
commit_message: "Build tex file on ${{ needs.filter-modified-docs.outputs.matrix }}"
まとめ
本記事では LaTeX で書かれた文書をモノレポで管理している場合に、GitHub Actions で PDF ファイルのビルドを自動化する際のワークフローを作成しました。最終的なワークフロー全文は次の通りです。
name: LaTeX compilations
on:
push:
branches:
- main
pull_request:
paths:
- 'src/**'
- 'asset/**'
jobs:
filter-modified-docs:
runs-on: ubuntu-slim
steps:
- uses: actions/checkout@v6
- uses: dorny/paths-filter@v3
id: filter
with:
list-files: json
filters: |
changed:
- 'src/**'
- 'asset/**'
- name: List modified document directories
id: list-modified-doc-directories
run: |
echo "result=$(jq -c "$JQ_FILTER" <<< "$JQ_INPUT_JSON")" > $GITHUB_OUTPUT
env:
JQ_INPUT_JSON: ${{ steps.filter.outputs.changed_files }}
JQ_FILTER: 'map( capture("^(src|asset)/(?<a>[a-zA-Z0-9_-]+)/") | .a ) | unique | .'
outputs:
matrix: ${{ steps.list-modified-doc-directories.outputs.result }}
build:
runs-on: ubuntu-latest
needs: filter-modified-docs
if: ${{ needs.filter-modified-docs.outputs.matrix != '' && toJson(fromJson(needs.filter-modified-docs.outputs.matrix)) != '[]' }}
strategy:
matrix:
tex: ${{ fromJson(needs.filter-modified-docs.outputs.matrix) }}
steps:
- uses: actions/checkout@v6
- name: LaTeX compilation
uses: xu-cheng/latex-action@v4
with:
args: "-pdfdvi"
work_in_root_file_dir: true
root_file: src/${{ matrix.tex }}/${{ matrix.tex }}.tex
- name: Upload to artifact
uses: actions/upload-artifact@v6
with:
name: ${{ matrix.tex }}
path: src/${{ matrix.tex }}/*.pdf
commit-and-push:
runs-on: ubuntu-latest
needs:
- build
- filter-modified-docs
permissions:
contents: write
steps:
- uses: actions/checkout@v6
- name: Download artifact
uses: actions/download-artifact@v7
with:
path: generated
merge-multiple: true
- name: Push the result of make command
uses: stefanzweifel/git-auto-commit-action@v7
with:
commit_message: "Build tex file on ${{ needs.filter-modified-docs.outputs.matrix }}"
学生の頃 LaTeX でレポートや論文を書いていた時は GitHub をバージョン管理とバックアップ位にしか使えていなかったのですが、ウォンテッドリーに就職して CI/CD パイプラインの最適化に取り組んでみてから振り返ってみると、色々改善できる点があったと気付かされます。流石に業務で LaTeX を扱う事はないのですが、似たような構成の CI/CD パイプラインについてはインフラ業務の一環として保守しているので、もし興味があればカジュアルにお話ししてみませんか?