🔧 TECH ARTICLE 🔧
〜 T-01 〜
🔧 T-01 | GitHub Actions × WordPress:Markdown自動投稿システムの構築ガイド
〜書いてpushするだけで公開まで全自動〜
⚙️ はじめに
ミク

ミク
ハルさん!毎回WordPress管理画面を開いて記事を貼り付けるのが面倒で…。Markdownで書いたやつをそのまま自動で公開できたらいいのに。
ハル

ハル
それ、GitHub Actionsで全部自動化できるよ。Markdownをpushするだけで、バリデーション→HTML変換→WordPress投稿まで一気に走る。
ミク

ミク
え、管理画面すら開かなくていいんですか?
ハル

ハル
そう。エディタとGitだけ使えればいい。今日はその仕組み『wp-story-sync』を一緒に見ていこう。

⚙️ システムアーキテクチャ
本システムは、高い保守性と安全性を確保するためにコンポーネントを分離したモジュール設計を採用しています。
全体構成
リポジトリの stories/ ディレクトリにMarkdownファイルが追加・更新されると、GitHub Actionsがトリガーされ、検証を経てWordPressへ配信されます。
┌─────────────────────────┐
│  GitHub リポジトリ        │
│                         │
│  stories/               │
│  ├─ aquaria/            │  (水の惑星シリーズ)
│  ├─ solaris/            │  (音楽宇宙シリーズ)
│  ├─ ragteller/          │  (火星コロニーシリーズ)
│  └─ tech/               │  (技術記事)
└────────┬────────────────┘
         │ git push
         ▼
┌─────────────────────────┐
│  GitHub Actions          │
│                         │
│  Job 1: Validate        │  (バリデーション + Dry-run)
│  Job 2: Publish         │  (本番投稿 / mainのみ)
└────────┬────────────────┘
         │ REST API
         ▼
┌─────────────────────────┐
│  WordPress サイト         │
│                         │
│  カテゴリ・タグ自動設定    │
│  新規作成 or 更新判定     │
│  公開 / 予約投稿          │
└─────────────────────────┘
ミク

ミク
Job 1とJob 2に分かれてるんですね。なんでわざわざ2つにするんですか?」
ハル

ハル
Job 1はバリデーションとDry-run。全ブランチで動く。Job 2は本番投稿で、mainブランチのpushだけ実行される。PRや開発ブランチでは絶対に本番に投稿されない仕組みになってるんだ。
コンポーネントの役割
レイヤー 役割 主なファイル
Config 設定情報・環境変数の管理 lib/config.py
WP Client REST APIを介した通信・認証 lib/wp_client.py
Cache カテゴリ・タグ情報のキャッシュ lib/wp_cache.py
Processor Markdown解析・HTML変換・ロジック実行 lib/story_processor.py
Validation 6層バリデーション(スキーマ・日付・衝突・類似度・HTML・コンテンツ) lib/validation.py
Workflow CI/CDパイプラインの定義 .github/workflows/wp-sync.yaml

💡 ⚙️ 実装のポイント
安全なAPIクライアント設計
WordPressClient クラスでは、誤った投稿を防ぐための Dry-run(テスト実行)モード を標準搭載しています。
# lib/wp_client.py (設計パターン抜粋)
def _request(self, method: str, path: str, **kwargs) -> requests.Response:
    # Dry-run時は書き込み系メソッドをモックに差し替え
    if self.config.dry_run and method.upper() in ('POST', 'PUT', 'PATCH', 'DELETE'):
        self.logger.info("DRY RUN: %s %s", method.upper(), path)
        return self._create_mock_response()

    # 実際のAPIリクエスト
    url = self.config.endpoint(path)
    return self.session.request(method, url, auth=self.config.auth_tuple(), **kwargs)
ミク

ミク
Dry-runって何をしてるんですか?」
ハル

ハル
本番に投稿はしないけど、投稿に必要な処理は全部やるテストモードだよ。frontmatterが読めるか、HTML変換が成功するか、slugが衝突しないか、全部本番と同じ流れで確認できる。本番でだけ壊れる問題を事前に潰せる。
ストーリー処理エンジン
💡 frontmatter ライブラリを使用してメタデータを解析し、MarkdownをHTMLに変換します。既存記事がある場合は slug をキーに自動で「更新」へ分岐させるのがポイントです。
処理フロー:

1. Frontmatter解析
   story.md → メタデータ (title, slug, categories, tags, status)
                + 本文 (Markdown)

2. Markdown → HTML変換
   **太字** → <strong>太字</strong>
   ```code``` → <pre><code>code</code></pre>

3. 同期判定
   slug "my-article" でWordPressを検索
     │
     ├─ 見つかった → 既存記事を「更新」(PUT)
     │
     └─ 見つからない → 新規記事を「作成」(POST)

⚙️ Markdownファイルの書き方
Markdownファイルの先頭に「フロントマター」としてメタデータを定義します。
---
title: "記事のタイトル"
slug: "url-friendly-name"
categories: ["Tech"]
tags: ["WordPress", "自動化"]
status: "publish"
date: "2025-01-11"
excerpt: "記事の要約(100文字程度)"
---

# ここから本文

本文をMarkdown記法で自由に書けます。
**太字**や`コード`も使えます。
フィールド 必須 説明
title 記事タイトル "はじめてのGitHub Actions"
slug URL用の短い名前(英数字とハイフン) "first-github-actions"
categories カテゴリ(配列) ["Tech"]
status 公開状態 "publish" or "draft"
date 公開日 "2025-01-11"
excerpt 要約文 "GitHub Actionsの入門記事"
tags タグ(配列、任意) ["入門", "CI/CD"]
ミク

ミク
slugって何に使うんですか?」
ハル

ハル
WordPressのURL末尾に使う識別子だよ。https://example.com/first-github-actions/ みたいな感じ。既存記事のslugと同じにすると更新扱いになる。だから一度公開した記事のslugは変えちゃダメ。

⚙️ GitHub Actions ワークフロー
本番環境への安全なデプロイを実現するため、ワークフローを 「検証」「公開」 の2ジョブに分離しています。
# .github/workflows/wp-sync.yaml (構成概要)
jobs:
  validate:
    name: Validate and Dry-Run
    # 全ブランチで実行。バリデーションとドライランを行う

  publish:
    name: Publish to WordPress
    needs: validate
    if: github.ref == 'refs/heads/main'  # mainブランチのみ
    environment: production              # GitHub環境保護ルールを適用
    env:
      WP_APP_PASSWORD: ${{ secrets.WP_APP_PASSWORD }}
🔐 セキュリティ上の工夫
  • Application Password: ログイン用パスワードではなく、WordPress専用の認証トークンを使用
  • Secrets管理: パスワードやURLはコードに含めず、GitHub Secretsで厳重に管理
  • 環境保護ルール: 本番投稿前にGitHub上で「手動承認」を必須にすることが可能
  • ブランチ保護: mainブランチへのpushのみ本番投稿を実行
🛡️ 多層防御による公開制御
記事が誤って公開されないよう、3つのゲートを設けています。
ゲート1: ブランチ保護
  └─ mainブランチへのpushのみ投稿を実行

ゲート2: 環境保護
  └─ 本番環境への投稿には手動承認が必要

ゲート3: バリデーション
  └─ 6層の検証をパスした記事のみ投稿可能
     (スキーマ / 日付 / slug衝突 / 類似度 / HTML / コンテンツ)
ミク

ミク
3つもゲートがあれば、うっかり公開事故はほぼ起きないですね。」
ハル

ハル
そう。でも一番大事なのは『自動化は止められるようにしておくこと』。何かあれば Kill Switch(緊急停止)でpublishを全停止できる構成にしてある。

⚙️ パフォーマンスと運用監視
キャッシュ戦略
カテゴリIDやタグ情報の取得を高速化するため、スレッドセーフなキャッシュ機構を導入しています。これにより、大量のファイルを処理する際のAPI呼び出し回数を劇的に削減しています。
1回目: カテゴリ「Tech」のID → API呼び出し → 結果をキャッシュ
2回目: カテゴリ「Tech」のID → キャッシュから即座に取得(API不要)
ログと証跡管理
GitHub Actionsのアーティファクト機能を利用し、実行時のログやバリデーション結果を保存します。
  • 📊 validation-logs: 構文チェック・バリデーションの結果
  • 📊 publish-logs: WordPressから返却された投稿IDなどの詳細
  • 処理時間やエラーの詳細も自動記録

⚙️ よくあるトラブルと対処法
トラブル 原因 対処法
❌ バリデーションエラー フロントマターの記述ミス エラーメッセージに従って修正
❌ 投稿が反映されない Dry-runモードのまま 本番モードに切り替え
❌ 403エラー 認証情報の期限切れ アプリケーションパスワードを再発行
❌ 記事が重複 slugの重複 ユニークなslugに変更
ミク

ミク
403エラーが出たとき焦りそう…。」
ハル

ハル
WordPressの管理画面でApplicationPasswordを再発行して、GitHub Secretsを更新するだけ。5分で直る。慌てずにエラーログを読めば原因はすぐわかるよ。

⚙️ まとめ
このシステムを導入することで、開発者は 「記事を書くこと」 だけに集中でき、WordPressの管理画面を開く手間から解放されます。
  1. 🚀 モジュラー設計 → 機能追加や修正が容易
  2. 🛡️ 多層防御 → バリデーションとDry-runによるミス防止
  3. 自動化パイプライン → 書いてpushするだけで公開
  4. 拡張性 → 画像アップロードやマルチサイト対応も視野に
  5. 📊 可観測性 → ログとアーティファクトで状況把握
ミク

ミク
これがあれば、ライティングに集中できますね!」
ハル

ハル
『書くこと』に集中し、『公開すること』は自動化する。それがwp-story-syncの目指す世界。まず手元でDry-runを一回やってみるといいよ。

参考リンク: WordPress REST API Documentation / GitHub Actions Documentation / Markdown記法ガイド
⚙️ Tech Series – T-01 🔧
Engineering the future…