見出し画像

GitHub Actionsでpandocのコンテナを使ってmarkdownをepubやPDFにする

久しぶりにこのブログでDevOps系の話題を書こうと思います。

今までは、技術書執筆は Google DocsかRe:Viewを使っていました。

■ReViewで本や卒論を書いてGitHub ActionsでPDFやePubを生成するテンプレート「kaitas/ReBook」

Re:Viewは Markdown風でもありTeX風でもあるので、卒論や技術書典のような書き物をPDF化するのは便利です。ePubもKindle Desktop Publishingに対応したePub3が出力できます。技術同人誌だけでなく商業出版も含めGitHub Actionsでビルドできる環境を作ってしまったので、ここ数年はこれで進めてきたのです。

ところがLLM時代になって、MarkdownやVisual Studio Code+Copilotによる執筆がより一般的になったのと、TeXタイプセットといっても数式はほとんどない「普通に美しいPDFが出せればいい」という需要があり、さらに商業出版ではepubでサポートしてほしい。コンテンツ面では特にHTMLでよし。共通に使うのはハイパーリンクや画像タグ、あとは強調構文ぐらいしか使わないので平打ちテキストやmarkdown原稿をわざわざRe:Viewで書き直す積極的な理由がほとんどなくなってしまった。ソースコードブロックも、スタイルで定義できれば、そのためにRe:Viewを使う理由にはならない。

というわけで、あとはGitHub ActionsでのビルドだけがPros/Consで残りました。これができれば何も悩むことはない。Re:View化の手間が無くなる(LLMで変換すればいいという説もあるが!)。ともかくここは意を決してpandocによるMarkdown中心とした技術書執筆ワークフローの構築をやってみたいと思います。

AIサービスを使った DevOpsへの活用

Claude 3.5 Sonnet Artifactsによる GitHub Actionsの設定

Claude 3.5 Sonnet Artifactsはとても便利です。コーディングの勉強にもなるし読みやすいので、ChatGPTや Gemini + AI Studio とはまた異なる魅力があります。コーディングだけでなく、やらせてみると GitHub Actionsの設定のようなノウハウを含めた手順書構築にも便利であることがわかりましたので、今回は「GitHub Actionsの知識があやふやな私」+「Claude」という構成でどこまでできるか試してみます。

GitHub Actions化する前に:Pandocでの環境構築

この作業に入る前に、いったんWindowsのローカル環境で以下を探求しています。

要求仕様としては以下の通りです

  • pandocコマンドで複数のmarkdownファイルを纏めてPDFやePubにしたい

  • いざというときは(GitHub Actionsに頼らず)ローカルビルドしたい

  • 今後一切、Re:Viewフレームワークにに頼らない

  • TeXを使ってPDFを生成するのは許可。LiveTex2024をフルインストールするのもやっておく。

  • コンテンツ一式もmarkdownで管理する

  • 画像についてはローカルリポジトリに置くが、リモート可なら大変ありがたい

TexLive2024のインストール、久しぶりにやりましたが数時間かかるのね。
8.54GB。これは下手な画像生成AIモデルより大きい。

大事なのは日本語環境と日本語フォント。IPAexにIPAexフォントがインストールされている。
C:\texlive\2024\texmf-dist\fonts\truetype\public\ipaex
ありがたいです。

その他、ローカルで pandocを使って思いのままにビルドできるようになるまで、LLMに壁打ちしてもらう。これはChatGPTでもできる。
完成したpandoc起動コマンド。

epub3をビルドする例。

pandoc -f markdown -t epub3 rinri.md title.txt -o sample.epub --css stylesheet.css --toc --toc-depth=2 --epub-cover-image=cover.png

同様に、複数のmarkdownファイルを入力に lualatex経由でPDF化する例。

pandoc sample.md sample2.md -o sample.pdf `
 --toc `
 --toc-depth=2 `
 --pdf-engine=lualatex `
 -V documentclass=ltjsarticle `
 -V classoption="12pt,a4paper" `
 -V geometry:margin=1in `
 --epub-cover-image=cover.png `
 --include-in-header=header.tex `
 --metadata-file=title.txt

・toc:目次
・pdf-engine:これはLiveTexをインストールすれば色々片付く
・-V documentclass=ltjsarticle:jsarticleでもいけるけど、最近のトレンドはもっと新しい「LuaLaTeX」を使った環境。
・include-in-header=header.tex:ePubにおけるCSSを担当するのが「header.tex」で、これをどう書くかで問題発生したり解決できるので、ここはLLMに助けてもらうといい。

ローカルでのビルドで一通りの疑問がなくなったので、GitHub化を進めます。

ここから先は、出来上がりベースで紹介していきます。

ふりかえり:GitHub ActionsをClaudeとやってみてわかったこと

  • 初見で出してきたyamlは動くが、いろんな不具合がある

  • ビルドやインストールをゼロからやる愚鈍なActionが初版

  • そのままだとビルドに15分以上かかる

  • DockerとBuildXを使ってキャッシュ化、ghcr.ioの活用

  • GitHub Actionsの設定

  • 多くはGitHub Actionsのエラーログを食わせれば解決する

  • Actionsを運用してないとわからない勘所はけっこうある

  • 特にセキュリティアップデートがあるので最新のActionを作っていく、追従させるところが大事。初見はcheckout@v2を提案してきた

  • ディスクの節約は実用性を考えて調整必要

  • キャッシュとDocker Imageのサイズバランス、ビルド速度の調整はめちゃ大変で、だいたいイテレーションとしては18回転ぐらいかかった。

  • とはいえGitHub ActionsをClaudeが書けるということを知っているか知っていないかでAI活用の開発体験としては大変な違い。

ePubとPDFを同時に4分でビルドできるのはなかなかではないでしょうか!

Claudeと実装編。

上の流れを、pandocの起動コマンドを完成させたあとに探求していきます。まずは最適化されたワークフローを作り、とりあえずのビルドができるようになります。ただこれでは15分以上かかるので実用的ではありませんし、Actionsの処理時間は課金対象です。

とはいえ月間で、3000分は使えるので1日100分を超えなければ…というところです。GitHubの設定で見れます。

https://github.com/settings/billing/summary

編集業務で複数のワーカーが使うとなるとピーク時は1日あたり数十回ビルドすることもあるので、実用性やUXも含めて1回のプッシュでの処理時間は10分を切りたいところです。
ちなみにMacOSやWindows版のコアもあります。

GitHub Actions の課金について

Docker Buildxを使った高速化

Docker Buildxを設定し、ビルドキャッシュを使用する方針にします。
GitHub Container Registryにログインし、ビルドしたイメージをプッシュします。初回以降のステップでは、ビルドしたイメージをプルして使用します。これにより2回目以降の実行では、Dockerイメージのビルド時間が大幅に短縮されます。
ビルド済みのイメージを再利用するため、全体の実行時間が短くなります。

注意点:GitHub Container Registryを使用するには、リポジトリの設定で適切な権限を設定する必要があります。

Claudeが生成した初期の build.yaml

name: Build PDF and EPUB

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
    - uses: actions/checkout@v3

    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v2

    - name: Cache Docker layers
      uses: actions/cache@v3
      with:
        path: /tmp/.buildx-cache
        key: ${{ runner.os }}-buildx-${{ github.sha }}
        restore-keys: |
          ${{ runner.os }}-buildx-

    - name: Login to GitHub Container Registry
      uses: docker/login-action@v2
      with:
        registry: ghcr.io
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}

    - name: Build and push Docker image
      uses: docker/build-push-action@v4
      with:
        context: .
        push: true
        tags: ghcr.io/${{ github.repository_owner }}/texlive-pandoc:latest
        cache-from: type=local,src=/tmp/.buildx-cache
        cache-to: type=local,dest=/tmp/.buildx-cache-new

    - name: Move cache
      run: |
        rm -rf /tmp/.buildx-cache
        mv /tmp/.buildx-cache-new /tmp/.buildx-cache

    - name: Prepare content files list
      run: echo $(cat contents.md | grep '^-' | sed 's/^- //') > content_files.txt

    - name: Build PDF
      run: |
        docker run --rm -v ${{ github.workspace }}:/workspace ghcr.io/${{ github.repository_owner }}/texlive-pandoc:latest \
        pandoc $(cat content_files.txt) -o output.pdf \
        --toc \
        --toc-depth=2 \
        --pdf-engine=lualatex \
        -V documentclass=ltjsarticle \
        -V classoption="12pt,a4paper" \
        -V geometry:margin=1in \
        --epub-cover-image=cover.png \
        --include-in-header=header.tex \
        --metadata-file=title.txt

    - name: Build EPUB
      run: |
        docker run --rm -v ${{ github.workspace }}:/workspace ghcr.io/${{ github.repository_owner }}/texlive-pandoc:latest \
        pandoc -f markdown -t epub3 $(cat content_files.txt) title.txt -o output.epub \
        --css stylesheet.css \
        --toc \
        --toc-depth=2 \
        --epub-cover-image=cover.png

    - name: Upload PDF artifact
      uses: actions/upload-artifact@v3
      with:
        name: output-pdf
        path: output.pdf

    - name: Upload EPUB artifact
      uses: actions/upload-artifact@v3
      with:
        name: output-epub
        path: output.epub

permissions セクションを追加して、パッケージへの書き込み権限を明示的に付与しています。GitHub Container Registry へのログインで github.actor を使用しています。

github.actor の設定

リポジトリの設定で以下の点を確認してください。

・"Settings" -> "Actions" -> "General" で、"Workflow permissions" を "Read and write permissions" に設定します。
・"Settings" -> "Actions" -> "General" で、"Allow GitHub Actions to create and approve pull requests" にチェックを入れます。

初回ビルドに成功

やったぜ

この更新されたワークフローでの主な変更点は以下の通りです:

  1. actions/checkout@v4 に更新

  2. docker/setup-buildx-action@v3 に更新

  3. actions/cache@v4 に更新

  4. docker/login-action@v3 に更新

  5. docker/build-push-action@v5 に更新

  6. actions/upload-artifact@v4 に更新(両方のアップロードステップ)

とはいえビルド時間は…15分以上かかる

しかもディスクが足りない

Annotations
1 warning
build
You are running out of disk space. The runner will stop working when the machine runs out of disk space. Free space left: 83 MB

この警告は非常に重要です。GitHub Actionsのランナーのディスク容量が不足しており、わずか83MBしか残っていないことを示しています。これは、大きなDockerイメージをビルドしているために発生している可能性が高いです。

この問題に対処するために、以下の方法を試すことができます:

  1. ディスク使用量の削減:

    • 不要なファイルやキャッシュを削除する

    • Dockerイメージのサイズを縮小する

  2. ワークフローの最適化:

    • 必要最小限のパッケージのみをインストールする

    • マルチステージビルドを使用してイメージサイズを縮小する

  3. ランナーのクリーンアップ:

    • ワークフロー実行中に定期的にディスクをクリーンアップする

最終的に完成した Dockerfileと build.yml

以下に、これらの対策を適用した更新版のワークフローとDockerfileの例を示します。

最終的に完成したDockerfile

FROM ubuntu:22.04

ENV DEBIAN_FRONTEND=noninteractive
ENV TZ=Asia/Tokyo

RUN apt-get update && apt-get install -y --no-install-recommends \
    wget \
    perl \
    texlive-full \
    pandoc \
    fonts-ipafont \
    fonts-ipaexfont \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /workspace

# パスが正しく設定されているか確認
RUN which pandoc && pandoc --version && tex --version

# luatexbase.styが存在することを確認
RUN find /usr/share/texlive -name luatexbase.sty

# TeX Liveのバージョンを確認(tlmgrの代わりに)
RUN tex --version

# インストールされているTeXパッケージを確認(tlmgrの代わりに)
RUN apt list --installed | grep texlive

完成した build.yml

完成したGitHub Actionsワークフロー(.github/workflows/build.yml)

name: Build PDF and EPUB

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
    - uses: actions/checkout@v4

    - name: Check initial disk space
      run: df -h

    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v3

    - name: Login to GitHub Container Registry
      uses: docker/login-action@v3
      with:
        registry: ghcr.io
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}

    - name: Check disk space
      run: |
          echo "Disk space before build:"
          df -h
          echo "Docker image and container sizes before build:"
          docker system df -v

    - name: Build and push Docker image
      uses: docker/build-push-action@v5
      with:
        context: .
        push: true
        tags: ghcr.io/${{ github.repository_owner }}/texlive-pandoc:latest
        cache-from: type=gha
        cache-to: type=gha,mode=max

    - name: Check disk space after Docker build
      run: |
        echo "Disk space after build:"
        df -h
        echo "Docker image and container sizes after build:"
        docker system df -v
        echo "Size of the built image:"
        docker image ls ghcr.io/${{ github.repository_owner }}/texlive-pandoc:latest --format "{{.Size}}"

    - name: Check Docker image
      run: |
        docker run --rm ghcr.io/${{ github.repository_owner }}/texlive-pandoc:latest which pandoc
        docker run --rm ghcr.io/${{ github.repository_owner }}/texlive-pandoc:latest pandoc --version
        docker run --rm ghcr.io/${{ github.repository_owner }}/texlive-pandoc:latest tex --version
        docker run --rm ghcr.io/${{ github.repository_owner }}/texlive-pandoc:latest find /usr/share/texlive -name luatexbase.sty
        docker run --rm ghcr.io/${{ github.repository_owner }}/texlive-pandoc:latest apt list --installed | grep texlive

    - name: Prepare content files list
      run: |
        echo $(cat contents.md | grep '^-' | sed 's/^- //') > content_files.txt
        echo "Content of content_files.txt:"
        cat content_files.txt

    - name: Build PDF
      run: |
        docker run --rm -v ${{ github.workspace }}:/workspace ghcr.io/${{ github.repository_owner }}/texlive-pandoc:latest \
        bash -c "cd /workspace && \
        pandoc \$(cat content_files.txt) -o output.pdf \
        --toc --toc-depth=2 --pdf-engine=lualatex \
        -V documentclass=ltjsarticle -V classoption='12pt,a4paper' \
        -V geometry:margin=1in --epub-cover-image=cover.png \
        --include-in-header=header.tex --metadata-file=title.txt"

    - name: Build EPUB
      run: |
        docker run --rm -v ${{ github.workspace }}:/workspace ghcr.io/${{ github.repository_owner }}/texlive-pandoc:latest \
        bash -c "cd /workspace && \
        pandoc -f markdown -t epub3 \$(cat content_files.txt) \
        -o output.epub \
        --css stylesheet.css --toc --toc-depth=2 --epub-cover-image=cover.png \
        --metadata-file=title.txt"

    - name: Upload PDF artifact
      uses: actions/upload-artifact@v4
      with:
        name: output-pdf
        path: output.pdf

    - name: Upload EPUB artifact
      uses: actions/upload-artifact@v4
      with:
        name: output-epub
        path: output.epub

    - name: Check final disk space
      run: df -h

    - name: Clean up
      if: always()
      run: |
        docker system prune -af
        docker volume prune -f

主な変更点:

  1. Dockerfileを最適化し、マルチステージビルドを使用してイメージサイズを縮小しました。

  2. ワークフローの開始時に不要なファイルを削除して、ディスク空間を確保します。

  3. Docker BuildxのキャッシュにGitHub Actionsのキャッシュを使用します。

  4. ワークフロー終了時にDockerのクリーンアップを実行します。

・LiveTexのインストールは色々試してみたけど、結局フルインストールしたほうがシンプルに解決する。
・apt-getでインストールする

もっと削れるかもしれないけど。

そもそも9.3GBぐらいしかあいてない。

仕上がりはこんな感じ

ePubです。Kindle Desktop Publishing対応。

PDFです。


気になる本誌「AICUマガジン Vol.3」はこちら!
(これはRe:Viewでビルドしてました)


この記事が気に入ったらサポートをしてみませんか?