LoginSignup
0
0

mdBookドキュメントをCloudflare Pagesへデプロイする

Last updated at Posted at 2024-02-27

はじめに

mdBookのドキュメントをGitHub ActionsでCloudflare Pagesへデプロイしてみました。
比較的かんたんに作れたと思うので、入門用によいかもしれません。
専用のドキュメント管理サイトを持ちたい、という方にもおすすめします。

本記事は創作レベルであり、本来のmdBook機能を発揮できていない部分がありますので、参考程度にご覧ください。
例)メタ情報の一部(タイトル)が表示されない

使用ツールについて

mdBookはドキュメント生成ツールです。Markdownファイルからリッチな画面を作成してくれます。似たツールにMkDocsがあります。
所有元がrust-langということもあり、Rustを扱う方ならみたことない人はいないほどRust関連のドキュメント管理に利用されています。

Cloudflare Pagesは無料プランで、500ビルド/月まで可能であり、最大25MiBのファイルを20,000までホストできます。以前から気になっていたので使ってみました。
Limits · Cloudflare Pages docs

準備編

まずはツールを使ってみてイメージを持つことからはじめます。

mdbookを使ってみる

インストール

ローカルではcargo install mdbookでインストールしましたが、バイナリが配布されているのでcargoを使わずともインストールできます (後述の.github/workflows/reusable_build.ymlをご参照ください)。
参考:https://rust-lang.github.io/mdBook/guide/installation.html

ドキュメント生成

mdbook init foo --title foo --ignore gitとすると下記の構成でファイルが生成されます。
実際に編集するのはsrc/配下のMarkdownファイルで、ビルド資材がbook/配下に生成されるというシンプルな構成となってます。

image.png

まだ使い慣れていないですが、mdbook serveしたままSUMMARY.mdに見出しを作ると自動的にMarkdownファイルを生成してくれるのは良さそうでした。

例)SUMMARY.mdに見出しを作ったときのファイル生成

  # Summary

  - [Chapter 1](./chapter_1.md)
+ - [Chapter 2](./chapter_2.md)

image.png

Cloudflare Pagesを使ってみる

公式ドキュメントを参照しながら進めていきましょう。
Get started guide · Cloudflare Pages docs

リポジトリを準備する

リポジトリを新規作成し、適当なHtmlを用意します。

gh repo create my-project --public --clone
cd my-project
touch index.html
code index.html

Cloudflare Pagesへ手動デプロイする

Cloudflareへアカウント登録した上で、Cloudflare dashboard からPagesを新規作成します。
先ほど作成したmy-projectリポジトリを指定してビルド設定へ進めていきましょう。
今回はmdBookを使うだけなので、Framework presetはNoneを選択しました。
最後まで終わるとデプロイできると思います。

デプロイ自動化の準備をする

Use Direct Upload with continuous integration · Cloudflare Pages docs
CloudFlareからワークフローが提供されており、GitHub Actionsでやります。
https://github.com/cloudflare/pages-action

Cloudflare API Tokenを生成する

Create API token · Cloudflare Fundamentals docs
デプロイに必要なAPI Tokenを生成しましょう。
cloudflare/pages-actionワークフローはWranglerというCLIを使用してデプロイしてます。

Cloudflare dashboard からAPI Tokenを生成します。

設定例
image.png

Create an API token in the Cloudflare dashboard with the "Cloudflare Pages — Edit" permission.
https://github.com/cloudflare/pages-action

PermissionはCloudflare Pages:EditだけでOKです。
作成したAPI Tokenはすぐに下記手順でsecretへ登録しておきましょう。後から参照できないです。

GitHub secretを登録する

GitHub Actions でのシークレットの使用 - GitHub Docs
リポジトリへsecretを登録しましょう。

本編

最終的なディレクトリ構成です。

.github/workflows/deploy.ymlはCloudflare Pagesへのデプロイを行います。
.github/workflows/reusable_build.ymlgenerate-book.pyを呼び出し、ビルド資材をまとめます。
generate-book.pyはディレクトリ構成を考慮して適切にmdbook buildします。
book/src/generate-book.pyの成果物であり、ローカル検証に利用します。
foo/home/mdbook initで作成したディレクトリです。home/はトップページに表示する特別な画面と位置づけており、foo/は特定の話題に関するドキュメントとお考えください。

image.png

最終的にはビルド成果物book/をCloudflare Pagesへデプロイするイメージです。
ローカル環境では下記によりbook/のビルド結果を確認できます。

python3 generate-book.py home
python3 generate-book.py foo

generate-book.py

GitHub hosted runnerではpythonを実行できます。
pythonで書いた経緯は下記を参考にしたからです。はじめて書いたのでボロボロかもしれません。
https://github.com/rust-lang/rfcs

引数を受け取り、src/配下へ一時的にMarkdownファイルを配置します。
mdbook buildsrc/配下を元にbook/へビルド成果物を出力します。
このとき、
 home/の場合、book/配下にビルド成果物が格納されます。
 foo/の場合、book/foo/配下にビルド成果物が格納されます。

#!/usr/bin/env python3

import os
import shutil
import subprocess
import sys

# mdbookがsrc/配下をみてbuildするので、一時的に資材をsrc/配下へ配置しbuildする
# build資材はbook/配下にまとまっていく想定
def main(book_title):
    init_dir('src')
    gen_src(book_title)

    # homeはトップのためbook/直下に配置する
    if book_title == 'home':
        result = subprocess.call(['mdbook', 'build', '-d', 'book'])
    else:
        result = subprocess.call(['mdbook', 'build', '-d', f'book/{book_title}'])
    # TODO: test
    if result != 0:
        print("Error: An error occurred during the execution of mdbook build.", file=sys.stderr)
        sys.exit(1)

def init_dir(dir):
    if os.path.exists(dir):
        # Clear out src to remove stale links in case you switch branches.
        shutil.rmtree(dir)
    os.mkdir(dir)

# src/配下のファイルを生成
def gen_src(input_dir):
    # src/配下のMarkdownファイル
    entries = [e for e in os.scandir(f'{input_dir}/src') if e.name.endswith('.md')]
    for entry in entries:
        symlink(f'../{entry.path}', f'src/{entry.name}')
    # src/images/配下の画像ファイル
    if os.path.exists(f'{input_dir}/src/images'):
      entries = [e for e in os.scandir(f'{input_dir}/src/images') if e.name.endswith(('.png', '.svg'))]
      if len(entries) > 0:
        init_dir('src/images')
        for entry in entries:
            symlink(f'../../{entry.path}', f'src/images/{entry.name}')

def symlink(src, dst):
    if not os.path.exists(dst):
        os.symlink(src, dst)

if __name__ == '__main__':
    # 第一引数としてbook_titleを取得できなければ終了する
    book_title = sys.argv[1] if len(sys.argv) > 1 else ""
    if book_title == '':
        print("Error: No argument provided. Please specify the book title.", file=sys.stderr)
        sys.exit(1)
    main(book_title)

.github/workflows/reusable_build.yml

ドキュメントを増やす場合に記述量を減らすため、reusable-workflowとしています(参考:ワークフローの再利用 - GitHub Docs

mdBookをインストールします。
該当ディレクトリのsrc/配下の全ファイルを基にしたハッシュをキーとして、キャッシュを確認します。
なおキャッシュは7日で消えるらしい(参考:依存関係をキャッシュしてワークフローのスピードを上げる - GitHub Docs)ので、更新頻度によっては意味がないかもしれません。
キャッシュがない場合は、上述のgenerate-book.pyでビルドしキャッシュを保存します。
最後にArtifactをアップロードします。upload-artifactはnameが重複した場合に競合エラーとなるので名前をつけてあげます。

name: Reusable Build Workflow

on:
  workflow_call:
    inputs:
      dir-name:
        description: 'Directory name used in executing mdbook build.'
        required: true
        type: string

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Install mdbook
        run: |
          mkdir mdbook
          # for updates see: https://github.com/rust-lang/mdBook/tags
          curl -Lf https://github.com/rust-lang/mdBook/releases/download/v0.4.37/mdbook-v0.4.37-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=./mdbook
          echo `pwd`/mdbook >> $GITHUB_PATH
      # To avoid useless build
      - name: Check Cache ${{ inputs.dir-name }}
        # can't set dynamic id. To avoid conflict, each reusable flow should be runned synchronously...
        id:  cache_check
        uses: actions/cache/restore@v4
        with:
          path: ./book
          key: ${{ runner.os }}-${{hashfiles(format('./{0}/src/**', inputs.dir-name))}}
      - name: Generate Book ${{ inputs.dir-name }}
        if : steps.cache_check.outputs.cache-hit != 'true'
        run: |
          ./generate-book.py ${{ inputs.dir-name }}
      - name: Save Cache ${{ inputs.dir-name }}
        if : steps.cache_check.outputs.cache-hit != 'true'
        uses: actions/cache/save@v4
        with:
          path: ./book
          key: ${{ runner.os }}-${{hashfiles(format('./{0}/src/**', inputs.dir-name))}}
      - name: Upload Artifact
        uses: actions/upload-artifact@v4
        with:
          # name isn't allowed duplicated.
          name: ${{format('artifact_{0}', inputs.dir-name)}}
          path: ./book

ちなみにワークフロー上で保存したキャッシュの確認は下記で行えました。

gh extension install actions/gh-actions-cache
gh actions-cache list --limit 10

参考:

.github/workflows/deploy.yml

上述の.github/workflows/reusable_build.ymlをビルド対象毎に呼び出します。
依存関係をつけて直列にしている理由は、.github/workflows/reusable_build.ymlCheck Cache ${{ inputs.dir-name }} stepのidに区別がつかないためです。job.idやstep.idには動的な値を埋め込むことができませんでした。(別の方法を模索中です)
download-artifactの際、取得したartifactをマージしたものをbook/配下へ展開しています (参考:https://github.com/actions/download-artifact/blob/main/docs/MIGRATION.md#multiple-uploads-to-the-same-named-artifact)。
book/配下をCoudflare Pagesへデプロイします。

name: Deploy
on:
  push:
    branches:
      - main

jobs:
  build_base:
    uses: ./.github/workflows/reusable_build.yml
    with:
      dir-name: home
  build_foo:
    needs: build_base
    uses: ./.github/workflows/reusable_build.yml
    with:
      dir-name: foo

  deploy:
    needs: [build_base, build_foo]
    runs-on: ubuntu-latest
    permissions:
      contents: read
      deployments: write
    steps:
      # https://github.com/actions/download-artifact/blob/main/docs/MIGRATION.md#multiple-uploads-to-the-same-named-artifact
      - name: Download Artifact
        uses: actions/download-artifact@v4
        with:
          path: ./book
          pattern: artifact_*
          merge-multiple: true
      - name: Deploy to Cloudflare Pages
        uses: cloudflare/pages-action@v1
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          projectName: my-project
          directory: book
          branch: main

動作確認

mainブランチをpushしてActionsが成功すればOKです。
Actions結果画面にデフォルトでdeploy summaryが表示されます。

image.png

Preview URLへアクセスするとhome/の内容がトップページとして表示されます。
foo/配下は/fooへアクセスすると確認できるので、リンクを作っておきました。

image.png

おわりに

はじめて触れた内容が多く、ツッコミどころ満載な気がします。
Cloudflareに手を出せたので、無料枠の範囲で色々手を出してみたいです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0