TECH · HOMELAB · HOMELAB

Chillarin Blog 構成紹介 - 5.運用効率アップを目的とした自動化 -

GitHub Actions + self-hosted runner による Hugo 自動デプロイと、OpenAI API を使った X 半自動投稿の設計思想・フロー解説。

tech 2026-01-10 47 min read by ちらりん
cover · 1024×1024

本章の内容

前回(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 は「公開物を配るだけ」にでき、デプロイは「生成物を公開ディレクトリへ反映するだけ」に単純化できます。


全体像(自動デプロイ)

この章のゴールを、まず図にします。

flowchart TB Dev["Author (GitHub UI / CLI)"] --> Push["Push to main"] Push --> GA["GitHub Actions (deploy.yml)"] GA --> Runner["self-hosted runner (systemd)"] Runner --> Build["hugo --minify (to RUNNER_TEMP)"] Build --> Verify["Validate output (index.html etc)"] Verify --> Rsync["rsync to /opt/chirarin-blog-public"] Rsync --> Nginx["nginx serves /opt/chirarin-blog-public"] Nginx --> User["User Browser"]

ポイントは 「ビルド → 検証 → 反映」 を必ず分けることです。 公開ディレクトリを直接いじると “全消し事故” が起きます。だからこそ、ガードレール込みで設計します。


旧方式(deploy-old.yml)をやめた理由

以前は、GitHub-hosted runner(ubuntu-latest)から SSH で自宅サーバに入って rsync する方式でも動きます。 ただし、この方式は次の “地雷” を抱えやすいです。

  • SSH鍵やホスト名など、Secrets が増える
  • ネットワークや鍵周りのトラブルで詰まりやすい
  • rsync --delete を “サーバ直撃” しやすく、事故りやすい

そこで、実行場所そのものを自宅サーバに寄せる(self-hosted runner) 方式に切り替えます。 これで 「ビルドも反映も同一ホスト」 になり、仕組みが一気に単純になります。


deploy.yml(self-hosted)の設計意図

deploy.yml は “壊れにくさ” を優先しています。大事な設計ポイントは3つです。

  1. ビルド先を公開ディレクトリにしない(一時領域に出す)
  2. 成果物が無いなら反映を止める(検証で落ちる)
  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 が常駐し、ジョブを実行する

よく使う確認コマンド

bash
# 状態(起動してるか)
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つだけです。

  1. GitHub Actions のログ(Web UI)
  2. サーバの runner ログ(journalctl)

よくある原因トップ3

  • Hugo ビルド失敗(テーマ/テンプレ編集時のエラー、Front Matterミスなど)
  • 権限問題(/opt/chirarin-blog-public に書けない、root混入など)
  • runner 自体の問題(落ちてる、更新で service ファイル変えて daemon-reload 未実施など)

まずは「成果物があるか」を見るのが早いです。

bash
ls -la /opt/chirarin-blog-public | head
test -f /opt/chirarin-blog-public/index.html && echo "DEPLOY OK"

Xへの半自動投稿(Issue承認フロー)

“自動で投稿” は便利ですが、事故ると取り返しがつきません。 そこでこのブログでは Issue を介した承認フローにしています。

目的

記事を main に push したら、

  1. 自動で 投稿ドラフトIssue を作る
  2. 人間が本文を確認して、OKなら approve ラベル
  3. Actions が X に投稿し、published ラベルで二重投稿を防止

という流れにします。

主要ファイル(3点セット)

  • .github/workflows/social-draft.yml content/posts/** の変更をトリガに、ドラフトIssueを作成(必要な env を渡す)
  • scripts/social/create_draft_issue.py 変更された記事を検出し、Front Matter から URL を生成し、OpenAI APIで要約 → Xドラフト文を自動生成
  • .github/workflows/social-publish.yml Issue に approve ラベルが付いた時だけ投稿して、published を付与(2重投稿防止の最終ガード)

publish 側は、Issue 本文の <!-- X_START -->...<!-- X_END --> / <!-- URL_START -->...<!-- URL_END --> を抽出して動く前提です。 なので Issue本文のマーカーは削除しないのがルールです。

📦 social-draft.ymlcreate_draft_issue.pysocial-publish.yml のコード全量は 有料コンテンツ: 自宅サーバ構築シリーズ に含まれています。


AI要約(OpenAI API)で「ドラフト」を賢くする

この仕組みの “キモ” は、記事本文から X 用の短文を自動生成するところです。 ただし完全自動投稿は危険なので、生成結果はまず Issue に落として人間が承認します(ここが安全装置)。

どうやって要約しているか(実装の流れ)

create_draft_issue.py の中で、以下をやっています。

  1. push 差分から content/posts/** の更新ファイルを拾う(複数コミットでもOK)
  2. 記事の Front Matter を読み、日付/slug から投稿URLを組み立てる
  3. 記事本文を “要約用に軽量化” する
    • コードブロック(``` / ~~~)を除去
    • 画像だけの行や Hugo shortcode の画像行を除去
    • 最大 3500 文字で打ち切り(トークン節約)
  4. OpenAI API(Responses API)を叩いて X 草案を生成
    • エンドポイントは https://api.openai.com/v1/responses(OpenAI Responses API)
    • OPENAI_MODEL 未指定なら スクリプト既定値をデフォルト(利用時に最新の公開モデルIDに合わせて OPENAI_MODEL 環境変数で上書き推奨)
    • “先頭行は必ず【新記事】” などのルールを developer message で固定
    • URL は 生成させず、後段でスクリプトが付与する(誤生成防止)
  5. 生成結果を 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 認証情報サイトURLOpenAI 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 承認フローにして、必ず人間が最後に見る(approvepublished が要)

次回(6章)は、チンチラライブ映像の “ギミック” 編です。 stream.html / status.json / sidebar 条件分岐で、運用に強いライブ枠を作っていきます。



自宅サーバ運用の全体像は 自宅サーバ運用の完全ガイド — Proxmox + Cloudflare Tunnel + Docker で個人ブログを公開し続ける にまとめています。Proxmox クラスタ・公開経路・Docker 本番化・障害対応までを 1 ページで通読できる Pillar ガイドです。

· · ·

コメント