Chillarin Blog 構成紹介 - 5.運用効率アップを目的とした自動化 -
GitHub Actions + self-hosted runner による Hugo 自動デプロイと、OpenAI API を使った X 半自動投稿の設計思想・フロー解説。
本章の内容
前回(4章)は Docker Compose(nginx + cloudflared + remark42)で"本番の器"を固める話でした。 この章では、その上で 「記事を書く → push → 勝手に公開が更新される」 を実現する “運用自動化” をまとめます。
このブログの運用は、乱暴に言うと次の2本立てです。
- 自動デプロイ:GitHub Actions + self-hosted runner で Hugo をビルドし、
/opt/chirarin-blog-public/に安全に反映 - Xへの半自動投稿:記事pushをトリガに「投稿ドラフトIssue」を自動生成し、
approveラベルで承認したら投稿
補足:GitHub Actions と self-hosted runner は何者?
ここ、意外と「名前だけ知ってる」人が多いので、ざっくりでも整理しておきます。
GitHub Actions とは
GitHub Actions は、GitHub に組み込まれている 自動化(CI/CD)基盤です。 「push されたらビルドする」「PRが出たらテストする」「タグを打ったらリリースする」みたいな処理を、GitHub 側がイベントとして受け取り、決められた手順を実行してくれます。
- 設定は
.github/workflows/*.ymlに書く on: push/on: pull_requestなどで"何が起きたら動くか"を決める- 中身は
jobs:→steps:で「何を順番にやるか」を書く
このブログで言うと、deploy.yml が「pushされたら Hugo ビルド → 検証 → rsync で反映」という手順書になっています。
runner とは(処理を実際に動かす"実行役")
GitHub Actions は「自動化の司令塔」で、実際にコマンドを実行する箱が runner です。
workflow 側で runs-on: を書くのは「どの箱で動かすか」を指定しています。
runner には大きく2種類あります。
- GitHub-hosted runner:GitHub が用意してくれる使い捨ての実行環境(例:
ubuntu-latest) - self-hosted runner:自分のマシンにインストールして使う実行環境(自宅サーバなど)
このブログは後者(self-hosted)です。
self-hosted runner が"自宅サーバ運用"で強い理由
自宅サーバで動くサービスは、結局「最後は自宅サーバに反映する」必要があります。 GitHub-hosted runner だと、だいたい SSH / 鍵 / ネットワーク越しの転送が絡み、運用が複雑になりがちです。
一方 self-hosted runner だと、
- ビルドも反映も同じホストで完結する(ネットワーク越しのSSH転送が減る)
- 依存(hugo/rsync等)も 自分の環境に合わせて固定できる
- “公開ディレクトリ全消し事故” みたいな事故を ガードレール設計で抑えやすい
というメリットがあります。
注意:self-hosted runner は「自分のマシンでGitHubのワークフローを実行する」=強い権限を持ち得ます。 だからこそ 対象リポジトリを絞る/常用ユーザ権限を絞る/ログ監視する など"運用の常識"が効いてきます。
自動化で守りたい「固定点」
本番運用は「どこがソースで、どこが公開物か」が曖昧になると壊れます。 Chillarin Blog は “固定点” を先に決めています。
- Hugo のソース(Git管理):
/opt/chirarin-blog-site - Hugo の生成物(公開ディレクトリ):
/opt/chirarin-blog-public - 本番の器(docker-compose / .env / データ):
/opt/chirarin-blog-infra - 自動デプロイの実行役(runner):
/opt/actions-runner(systemd で常駐)
この分離のおかげで、nginx は「公開物を配るだけ」にでき、デプロイは「生成物を公開ディレクトリへ反映するだけ」に単純化できます。
全体像(自動デプロイ)
この章のゴールを、まず図にします。
ポイントは 「ビルド → 検証 → 反映」 を必ず分けることです。 公開ディレクトリを直接いじると “全消し事故” が起きます。だからこそ、ガードレール込みで設計します。
旧方式(deploy-old.yml)をやめた理由
以前は、GitHub-hosted runner(ubuntu-latest)から SSH で自宅サーバに入って rsync する方式でも動きます。
ただし、この方式は次の “地雷” を抱えやすいです。
- SSH鍵やホスト名など、Secrets が増える
- ネットワークや鍵周りのトラブルで詰まりやすい
rsync --deleteを “サーバ直撃” しやすく、事故りやすい
そこで、実行場所そのものを自宅サーバに寄せる(self-hosted runner) 方式に切り替えます。 これで 「ビルドも反映も同一ホスト」 になり、仕組みが一気に単純になります。
deploy.yml(self-hosted)の設計意図
deploy.yml は “壊れにくさ” を優先しています。大事な設計ポイントは3つです。
- ビルド先を公開ディレクトリにしない(一時領域に出す)
- 成果物が無いなら反映を止める(検証で落ちる)
- rsync は安全寄りのオプションで反映する(中間状態を減らす)
📦
deploy.ymlの全量(ワークフロー定義・rsyncオプション・concurrency設定を含む)は 有料コンテンツ: 自宅サーバ構築シリーズ に含まれています。
1) Build to temp dir(公開dir全消し事故の保険)
RUNNER_TEMP 配下に出力して、公開ディレクトリとは切り離します。
「公開ディレクトリを rm -rf してから hugo」みたいな動線は、失敗した瞬間に"空っぽの本番"になります。 だから “まず別場所に完成品を作る” が鉄則。
2) Validate build output(ここが一番重要)
次に、最低限の成果物が存在することを検証してから先へ進みます。
これで「Hugoが何かで失敗して生成物が無い」ケースを 反映前に検知できます。
3) Safe rsync(壊れにくい反映)
反映は rsync ですが、ここでも “中間状態” を減らします。
--delete-delay:削除を最後にまとめる(途中で空になりにくい)--delay-updates:更新も最後にまとめる(途中で半端になりにくい)
さらに concurrency を入れて、連続pushでも 最後の1回だけが勝つ ようにしています。
補足:self-hosted runner はどうやって GitHub とつながる?
self-hosted runner は「GitHub に繋がるためにサーバ側で常駐するエージェント」です。
- GitHub 側で「このリポジトリ用の runner」として登録する
- runner は常駐して待機し、GitHub からジョブが割り当てられたら実行する
- 基本は runner 側から GitHub に outbound で繋ぎ続ける 形(待ち受けサーバを立てる発想ではない)
なので、自宅運用でも「何かを公開するために runner 用のポートを開ける」みたいな設計にはしません。 (公開は Cloudflare Tunnel、CI/CD実行は runner の常駐、で役割が分かれます)
self-hosted runner は “常駐サービス” にする
self-hosted runner は、1回起動して終わりではなく、常駐してジョブを待ち受ける 仕組みです。 このブログでは systemd のサービスとして有効化してあります。
- サービス名(例):
actions.runner.<owner>-<repo>.<host>.service - 実体:
/opt/actions-runner/のrunsvc.shが常駐し、ジョブを実行する
よく使う確認コマンド
# 状態(起動してるか)
sudo systemctl status actions.runner.<owner>-<repo>.<host>.service
# 直近ログ(成功/失敗の確認)
sudo journalctl -u actions.runner.<owner>-<repo>.<host>.service -n 200 --no-pager日常運用の"型"を固定する(事故防止)
運用は “人間の手順” が揺れると事故が起きます。 なので、やることは2系統に分けて固定します。
1) 記事投稿(GitHubメイン)
推奨は GitHub UI からの投稿です。
content/posts/YYYY-MM-DD-.../index.mdを作成・編集- 画像は同ディレクトリに置く(Page Bundle)
mainに commit- 自動で deploy が走って反映
2) レイアウト/設定変更(サーバCLIメイン)
layouts/ や static/、hugo.toml はサーバ側で編集する方が安全です(差分確認しやすい)。
その代わり、必ず “正本=GitHub” を守ります。
失敗した時の切り分け(最短ルート)
“どこで落ちたか” を切り分ける場所は、基本この2つだけです。
- GitHub Actions のログ(Web UI)
- サーバの runner ログ(journalctl)
よくある原因トップ3
- Hugo ビルド失敗(テーマ/テンプレ編集時のエラー、Front Matterミスなど)
- 権限問題(
/opt/chirarin-blog-publicに書けない、root混入など) - runner 自体の問題(落ちてる、更新で service ファイル変えて daemon-reload 未実施など)
まずは「成果物があるか」を見るのが早いです。
ls -la /opt/chirarin-blog-public | head
test -f /opt/chirarin-blog-public/index.html && echo "DEPLOY OK"Xへの半自動投稿(Issue承認フロー)
“自動で投稿” は便利ですが、事故ると取り返しがつきません。 そこでこのブログでは Issue を介した承認フローにしています。
目的
記事を main に push したら、
- 自動で 投稿ドラフトIssue を作る
- 人間が本文を確認して、OKなら
approveラベル - Actions が X に投稿し、
publishedラベルで二重投稿を防止
という流れにします。
主要ファイル(3点セット)
.github/workflows/social-draft.ymlcontent/posts/**の変更をトリガに、ドラフトIssueを作成(必要な env を渡す)scripts/social/create_draft_issue.py変更された記事を検出し、Front Matter から URL を生成し、OpenAI APIで要約 → Xドラフト文を自動生成.github/workflows/social-publish.ymlIssue にapproveラベルが付いた時だけ投稿して、publishedを付与(2重投稿防止の最終ガード)
publish 側は、Issue 本文の
<!-- X_START -->...<!-- X_END -->/<!-- URL_START -->...<!-- URL_END -->を抽出して動く前提です。 なので Issue本文のマーカーは削除しないのがルールです。
📦
social-draft.yml、create_draft_issue.py、social-publish.ymlのコード全量は 有料コンテンツ: 自宅サーバ構築シリーズ に含まれています。
AI要約(OpenAI API)で「ドラフト」を賢くする
この仕組みの “キモ” は、記事本文から X 用の短文を自動生成するところです。 ただし完全自動投稿は危険なので、生成結果はまず Issue に落として人間が承認します(ここが安全装置)。
どうやって要約しているか(実装の流れ)
create_draft_issue.py の中で、以下をやっています。
- push 差分から
content/posts/**の更新ファイルを拾う(複数コミットでもOK) - 記事の Front Matter を読み、日付/slug から投稿URLを組み立てる
- 記事本文を “要約用に軽量化” する
- コードブロック(``` / ~~~)を除去
- 画像だけの行や Hugo shortcode の画像行を除去
- 最大 3500 文字で打ち切り(トークン節約)
- OpenAI API(Responses API)を叩いて X 草案を生成
- エンドポイントは
https://api.openai.com/v1/responses(OpenAI Responses API) OPENAI_MODEL未指定なら スクリプト既定値をデフォルト(利用時に最新の公開モデルIDに合わせてOPENAI_MODEL環境変数で上書き推奨)- “先頭行は必ず【新記事】” などのルールを developer message で固定
- URL は 生成させず、後段でスクリプトが付与する(誤生成防止)
- エンドポイントは
- 生成結果を Issue に書き出す
- 生成に成功:
AI要約テキスト + URL - OpenAIキー無し/エラー時:
【新記事】タイトル + URLのフォールバック
- 生成に成功:
OpenAI API のリクエスト構造(ざっくり)
model:OPENAI_MODEL(スクリプト既定値。利用時に有効なモデルIDを環境変数で設定すること)input: developer / user の2メッセージ- developer:文字数、先頭行フォーマット、Markdown禁止、捏造禁止などのルール
- user:タイトル、カテゴリ、タグ、URL(参考として渡すが出力に含めない)、抜粋本文
レスポンスは output -> content(type=output_text) からテキストを拾い、X_MAX_CHARS を超えたら切り詰めます。
📦 OpenAI API リクエストの具体的な構造(JSONペイロード・developer/userメッセージの全文)は 有料コンテンツ: 自宅サーバ構築シリーズ に含まれています。
必要な GitHub 設定(Secrets / Variables)
この仕組みを動かすには、GitHub リポジトリに Secrets と Variables の設定が必要です。 大きく分けると X API 認証情報、サイトURL、OpenAI API キー(任意) の3カテゴリです。
📦 GitHub Secrets / Variables の一覧と設定手順は 有料コンテンツ: 自宅サーバ構築シリーズ に含まれています。
OpenAI は “任意” にしておくと壊れにくいです。 キーが無い場合でもフォールバック文で Issue を作れるようにしておけば、運用が止まりません。
プロンプト設計の意図(Developer/User の役割分担)
この仕組みは「AIに自由に全部作らせる」ではなく、事故りやすい部分(URL・文字数・形式・事実性)をワークフロー側で強制しつつ、AIには “要約と文章の整形” だけを担当させる思想です。
📦 プロンプトの全文(developer message / user message の具体的な記述)は 有料コンテンツ: 自宅サーバ構築シリーズ に含まれています。
1) なぜ「URL を本文に出力させない」のか
- X投稿は 本文(テキスト) と URL を “別管理” したい
- 本文はAI生成(もしくは人が修正)
- URLはHugoの front matter(date/slug)から機械的に生成し、常に正しいURLを付ける
- AIがURLを勝手に混ぜると、文字数計算が崩れたり、誤URL/余計な記号混入が起きやすい → そのため「AI出力は本文だけ」に固定し、URLはワークフローで最後に付ける
2) なぜ本文の入力を 3500文字で打ち切るのか
- モデルに記事全文を渡すと、トークン/時間/コストが増えるだけで、要約品質が必ずしも上がらない
- 先頭〜中盤(見出し+要点)が入る程度に制限し、安定して速く回すための上限
- さらに、コードブロックや画像だけの行は落として「要約に不要なノイズ」を減らす
3) なぜ「Markdown禁止」なのか
- XはMarkdownをそのまま"装飾"として解釈しない(見た目が崩れる・記号が無駄に文字数を食う)
- 記法(
```や[]())が混ざると、リンクっぽく見えて紛らわしい → 投稿は プレーンテキスト に統一
4) なぜ 1行目を固定フォーマットにしているのか
- タイムラインで「新記事だ」と瞬時に分かるように、タイトル行を固定する
- 以降の2〜3行は短文で要点だけ(長文になって読まれないのを防ぐ)
5) なぜ絵文字/記号を控えめにしているのか
- 記号多めは “それっぽいけど中身が薄い” になりがち
- ブランド/トーンを揃えつつ、読みやすさ優先
6) なぜ Issue の本文に START/END マーカーを入れているのか
- 人がIssue上で編集しても、ワークフロー側が 投稿に必要なブロックだけを安全に抽出できるようにするため
- 見出しや文言の揺れよりも、マーカーの方が壊れにくい(後方互換もしやすい)
まとめ
この章の結論はシンプルです。
- デプロイは self-hosted runner に寄せると、ネットワーク/鍵/Secrets が減って壊れにくい
- ビルドは一時領域 → 成果物検証 → rsync の順にして、公開ディレクトリ全消し事故を防ぐ
- X投稿は Issue 承認フローにして、必ず人間が最後に見る(
approveとpublishedが要)
次回(6章)は、チンチラライブ映像の “ギミック” 編です。
stream.html / status.json / sidebar 条件分岐で、運用に強いライブ枠を作っていきます。
自宅サーバ運用の全体像は 自宅サーバ運用の完全ガイド — Proxmox + Cloudflare Tunnel + Docker で個人ブログを公開し続ける にまとめています。Proxmox クラスタ・公開経路・Docker 本番化・障害対応までを 1 ページで通読できる Pillar ガイドです。
コメント