はじめに
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/配下に生成されるというシンプルな構成となってます。
まだ使い慣れていないですが、mdbook serve
したままSUMMARY.md
に見出しを作ると自動的にMarkdownファイルを生成してくれるのは良さそうでした。
例)SUMMARY.md
に見出しを作ったときのファイル生成
# Summary
- [Chapter 1](./chapter_1.md)
+ - [Chapter 2](./chapter_2.md)
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を生成します。
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を登録しましょう。
-
CLOUDFLARE_API_TOKEN
- API Token
-
CLOUDFLARE_ACCOUNT_ID
- Account ID
本編
最終的なディレクトリ構成です。
.github/workflows/deploy.yml
はCloudflare Pagesへのデプロイを行います。
.github/workflows/reusable_build.yml
はgenerate-book.py
を呼び出し、ビルド資材をまとめます。
generate-book.py
はディレクトリ構成を考慮して適切にmdbook build
します。
book/
とsrc/
はgenerate-book.py
の成果物であり、ローカル検証に利用します。
foo/
とhome/
はmdbook init
で作成したディレクトリです。home/
はトップページに表示する特別な画面と位置づけており、foo/
は特定の話題に関するドキュメントとお考えください。
最終的にはビルド成果物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 build
でsrc/
配下を元に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.yml
のCheck 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が表示されます。
Preview URLへアクセスするとhome/
の内容がトップページとして表示されます。
foo/
配下は/foo
へアクセスすると確認できるので、リンクを作っておきました。
おわりに
はじめて触れた内容が多く、ツッコミどころ満載な気がします。
Cloudflareに手を出せたので、無料枠の範囲で色々手を出してみたいです。