Hugo ブログのデザインと記事を別リポジトリで管理して、デプロイの干渉を断ち切る

デザインを更新したのに、次のデプロイで古いテーマが使われた。 背景: Hugoは静的サイトジェネレータで、テーマ(デザイン・レイアウト)と記事(Markdownファイル)を組み合わせてWebサイトを生成します。

2リポジトリ構成でblog-site(テーマ・インフラ)とblog-articles(記事・デプロイ)を分けていたが、「どのテーマバージョンを使うか」の問題は解決しない。

blog-site/          # テーマリポジトリ
├── themes/
├── layouts/
└── static/

blog-articles/      # 記事リポジトリ  
├── content/
├── site-bundle.lock
└── scripts/build.sh

これがsite-bundle.lockの起源だ。

なぜ2リポジトリに分けたか

分離の理由は、記事追加でテーマビルドが走る無駄と、変更範囲の独立性確保だった。しかし分離後に新たな問題が見えてきた。

「分けただけ」の問題

リポジトリが分離していても、blog-articlesは最終的にblog-siteのテーマを使ってHugoビルドを実行する必要がある。問題は「どのバージョンのテーマを使うか」の答えがなかったことだ。

最初の実装ではblog-articlesのCIがblog-siteを毎回クローンして最新のテーマを取得していた。これは「常に最新テーマが使われる」という意味では直感的だが、2つの問題がある。

  • テーマの不完全な変更途中にデプロイが走る可能性がある
  • テーマを意図的に固定したいとき(重大な変更の前に記事だけ先に出す場合など)に手段がない

実際に起きたのは「デザインを更新したが、別のタイミングでblog-articlesのデプロイが走り、古いテーマで本番が更新された」というケースだった。CIはすべて成功していたが、出力が意図したものではなかった。

site-bundle.lock

解決策としてsite-bundle.lockを導入した。ファイルの中身はシンプルだ。

SITE_BUNDLE_URL="https://gitlabce.vp25.home/tkdev/blog-site/-/archive/5636d9ac02b/blog-site-5636d9ac02b.tar.gz"
SITE_BUNDLE_SHA256="d35f0cb71d9a1618a7f8997cdcb63df7af646fa48d62af9924b1bd005e652d87"

GitLabはコミットSHAを指定したアーカイブURLを自動生成する。 : https://gitlab.com/user/repo/-/archive/abc123/repo-abc123.tar.gzのような形式で、特定コミットのスナップショットをダウンロードできます。このURLをlockファイルに書くことで、「このコミットのテーマを使う」というバージョンの固定が成立する。SHA256はバンドルの完全性検証のためだ。

blog-articlesのビルドスクリプト(scripts/build.sh)はこのファイルを読んでアーカイブをダウンロードし、SHA256を検証してからHugoビルドを実行する。

SITE_BUNDLE_URL="$(read_lock_value SITE_BUNDLE_URL)"
SITE_BUNDLE_SHA256="$(read_lock_value SITE_BUNDLE_SHA256 false || true)"

curl --fail --silent --show-error --location \
  --header "JOB-TOKEN: ${SITE_BUNDLE_JOB_TOKEN}" \
  --output "${BUNDLE_ARCHIVE}" \
  "${SITE_BUNDLE_URL}"

if [[ -n "${SITE_BUNDLE_SHA256}" ]]; then
  printf "%s  %s\n" "${SITE_BUNDLE_SHA256}" "${BUNDLE_ARCHIVE}" | sha256sum -c -
fi

lockファイルを更新しない限り、何度デプロイしても同じテーマが使われる。 ⚠️ 注意: この仕組みはテーマの緊急修正時に更新手順を忘れやすいという運用上のリスクがあります。テーマを変えたいときは明示的にlockを更新する必要がある。

何がよいか

この設計のポイントは「デザインの反映は意図的な操作」という制約が構造に入っていることだ。

lockを更新せずにテーマが変わることはない。デプロイのたびにテーマが変わる可能性がないので、記事追加のデプロイがテーマの変更を意図せず含むことがなくなった。

もう1つの利点は検証のタイミングだ。CIのtestステージにverify-site-bundle-lockジョブを置いている。lockに記載のURLにアクセスしてSHA256を検証する。lockが古いブランチを指していてURLが無効になっていれば、このジョブは失敗する。ビルドやデプロイに進む前に問題が検知できる。

# verify_site_bundle_lock.sh の核心部分
curl --fail --silent --show-error ... --output "${tmp_file}" "${SITE_BUNDLE_URL}"
printf "%s  %s\n" "${SITE_BUNDLE_SHA256}" "${tmp_file}" | sha256sum -c -

lockの更新コスト

テーマを変えたあと、lockを更新する手順は以下だ。

  1. blog-siteで変更をpushしてCIを実行
  2. 新しいコミットSHAからアーカイブURLを組み立てる
    git rev-parse HEAD
    
  3. アーカイブをダウンロードしてSHA256を計算
    sha256sum blog-site-<commit>.tar.gz
    
  4. site-bundle.lockの2行を更新してpush

手順は単純だが、この更新を忘れると旧テーマのまま記事が追加され続ける。自動化はできるが、「テーマ変更を意識的に確認して本番に反映するタイミング」として手動ステップを残した。実際に更新漏れが起きた経験から、この一手間を省かないことにした。 参考: 類似のアプローチとして、package.jsonのlock機能やDockerイメージのタグ固定などがあります。バージョン固定による安定性と更新の意図性確保は、多くのシステムで採用されているパターンです。


リポジトリを分けることで変更範囲を分離し、lockファイルでテーマバージョンを固定する。この組み合わせが「デプロイの干渉を断ち切る」設計の実体だ。分けるだけでは不十分で、「どのバージョンを使うか」という問いに答える仕組みがセットで必要だった。