【T-01】GitHub Actionsを使ったMarkdown→WordPress自動投稿システムの構築
kome
🔧 TECH ARTICLE 🔧
〜 T-01 〜
GitHub Actionsを使ったMarkdown→WordPress自動投稿システムの構築
⚙️ はじめに
この記事では、GitHub ActionsとWordPress REST APIを組み合わせて、Markdownファイルを自動的にWordPressサイトに投稿するシステム「wp-story-sync」の構築方法を詳しく解説します。実際のプロジェクトで使用されているコードとワークフローを基に、実践的な実装方法を説明します。
⚙️ システムアーキテクチャ
▸ 全体構成
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ GitHub Repo │────│ GitHub Actions │────│ WordPress Site │
│ │ │ │ │ │
│ stories/ │ │ 1. Validation │ │ REST API v2 │
│ ├─ series1.md │ │ 2. Dry-run │ │ + App Password │
│ ├─ series2.md │ │ 3. Publish │ │ │
│ └─ ... │ │ │ │ │
└─────────────────┘ └──────────────────┘ └─────────────────┘
▸ コンポーネント構成
- Configuration Layer (
lib/config.py) – 設定管理 - WordPress API Layer (
lib/wp_client.py) – REST API クライアント - Caching Layer (
lib/wp_cache.py) – API レスポンスキャッシュ - Business Logic (
lib/story_processor.py) – ストーリー処理ロジック - GitHub Actions Workflows – CI/CDパイプライン
⚙️ 実装詳細
▸ 1. WordPress REST API クライアント
WordPress API との通信を担うクライアントクラスです:
# lib/wp_client.py (抜粋)
class WordPressClient:
"""WordPress REST API client with caching."""
def __init__(self, config: WordPressConfig, cache: Optional[WordPressCache] = None):
self.config = config
self.cache = cache or WordPressCache()
self.logger = logging.getLogger("wp-client")
self.session = requests.Session()
def _request(self, method: str, path: str, **kwargs) -> requests.Response:
"""Make authenticated HTTP request to WordPress API."""
if self.config.dry_run and method.upper() in ('POST', 'PUT', 'PATCH', 'DELETE'):
self.logger.info("DRY RUN: %s %s", method.upper(), path)
# Return mock response for dry run
response = requests.Response()
response.status_code = 200
response._content = b'{"id": 999, "status": "dry-run"}'
return response
url = self.config.endpoint(path)
kwargs.setdefault('auth', self.config.auth_tuple())
kwargs.setdefault('timeout', self.config.timeout)
response = self.session.request(method, url, **kwargs)
return response
主要機能:
– WordPress Application Password認証
– ドライランモード(テスト用)
– キャッシュ統合
– エラーハンドリング
– WordPress Application Password認証
– ドライランモード(テスト用)
– キャッシュ統合
– エラーハンドリング
▸ 2. ストーリー処理エンジン
Markdownファイルの処理とWordPress投稿データの生成:
# lib/story_processor.py (主要部分)
class StoryProcessor:
def process_story_file(self, file_path: Path) -> Optional[Dict[str, Any]]:
"""Process a single story markdown file and publish to WordPress."""
try:
# Parse frontmatter and content
with file_path.open('r', encoding='utf-8') as f:
post = frontmatter.load(f)
metadata = post.metadata
content = post.content
# Convert markdown to HTML
html_content = markdown.markdown(content, extensions=['extra', 'codehilite'])
# Prepare WordPress post data
post_data = self._prepare_post_data(metadata, html_content, file_path)
# Check if post exists (by slug)
slug = metadata.get('slug', '')
existing_posts = self.wp_client.search_posts_by_slug(slug)
if existing_posts:
# Update existing post
post_id = existing_posts[0]['id']
result = self.wp_client.update_post(post_id, post_data)
else:
# Create new post
result = self.wp_client.create_post(post_data)
return result
except Exception as e:
self.logger.exception("Error processing story file %s: %s", file_path, e)
return None
処理フロー:
1. Frontmatter解析
2. Markdown→HTML変換
3. 投稿データ準備
4. 既存投稿チェック
5. 作成/更新実行
1. Frontmatter解析
2. Markdown→HTML変換
3. 投稿データ準備
4. 既存投稿チェック
5. 作成/更新実行
▸ 3. GitHub Actions ワークフロー
本格的な本番環境対応のワークフロー設定:
# .github/workflows/wp-sync.yaml
name: WP Story Sync
on:
push:
branches:
- main
paths:
- 'stories/**'
- 'scripts/**'
- 'config/**'
workflow_dispatch:
inputs:
mode:
description: 'Sync mode'
required: true
default: 'dry-run'
type: choice
options:
- dry-run
- publish
jobs:
# ============================================================
# Job 1: Validate and Dry-Run
# ============================================================
validate:
name: Validate Stories
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: 'pip'
- name: Install dependencies
run: pip install -r requirements.txt
- name: Validate strict
run: python3 scripts/validate.py --strict stories/
- name: Dry-run WordPress sync
run: python3 scripts/post_story.py --dry-run --gated stories/
# ============================================================
# Job 2: Publish to WordPress (Production Only)
# ============================================================
publish:
name: Publish to WordPress
needs: validate
runs-on: ubuntu-latest
# Only run on main branch push, not PRs
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
# Requires production environment approval
environment: production
env:
WP_BASE_URL: ${{ secrets.WP_BASE_URL }}
WP_USERNAME: ${{ secrets.WP_USERNAME }}
WP_APP_PASSWORD: ${{ secrets.WP_APP_PASSWORD }}
WP_FORCE_PUBLISH: "1"
steps:
- name: Final validation
run: python3 scripts/validate.py --strict stories/
- name: Publish to WordPress
run: python3 scripts/post_story.py --mode publish --gated stories/
💡 ワークフローの特徴💡 :
– 多段階バリデーション: 厳格なチェック
– 環境分離: production環境は承認制
– 安全な自動化: PRでは投稿実行なし
– アーティファクト保存: ログと検証結果の保持
– 多段階バリデーション: 厳格なチェック
– 環境分離: production環境は承認制
– 安全な自動化: PRでは投稿実行なし
– アーティファクト保存: ログと検証結果の保持
▸ 4. Frontmatterスキーマ
統一されたMarkdownフロントマター仕様:
---
title: "記事タイトル"
slug: "unique-post-slug"
categories: ["Tech", "WordPress"]
tags: ["automation", "CI/CD"]
author: "TechWeaver"
status: "publish" # draft, publish, trash
date: "2025-01-11"
excerpt: "記事の要約"
featured_image: ""
episode_id: "T-01"
series: "Tech"
---
自動分類機能:
# 自動的にシリーズ情報を分類
if "stories/solaris" in path_str:
if "Solaris" not in categories:
categories.append("Solaris")
if not metadata.get('author'):
metadata['author'] = 'StarWeaver'
elif "stories/ragteller" in path_str:
if "RAGteller" not in categories:
categories.append("RAGteller")
⚙️ セキュリティ実装
▸ 1. 認証システム
WordPress Application Passwordを使用した安全な認証:
class WordPressConfig:
def auth_tuple(self) -> tuple[str, str]:
"""Return authentication tuple for requests."""
return (self.username, self.app_password)
def endpoint(self, path: str) -> str:
"""Build full API endpoint URL."""
return f"{self.base_url.rstrip('/')}/wp-json/wp/v2/{path.lstrip('/')}"
⚠️ セキュリティポイント⚠️ :
– Application Password(専用パスワード)
– HTTPS必須
– 環境変数での秘密情報管理
– 最小権限の原則
– Application Password(専用パスワード)
– HTTPS必須
– 環境変数での秘密情報管理
– 最小権限の原則
▸ 2. 入力値検証
厳格なバリデーション機能:
# scripts/validate.py による事前チェック
def validate_frontmatter(metadata: Dict[str, Any]) -> List[str]:
"""Validate frontmatter fields."""
errors = []
# Required fields check
for field in REQUIRED_FIELDS:
if field not in metadata:
errors.append(f"Missing required field: {field}")
# Title validation
title = metadata.get('title', '')
if len(title) > 200:
errors.append("Title too long (max 200 chars)")
# Slug validation
slug = metadata.get('slug', '')
if not re.match(r'^[a-z0-9-]+$', slug):
errors.append("Invalid slug format")
return errors
▸ 3. 公開制御
多層防御による安全な公開制御:
# GitHub環境保護ルール
environment: production
# ↓ 手動承認が必要
# ブランチ保護
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
# 内部ガード
WP_FORCE_PUBLISH: "1"
💡 パフォーマンス最適化
▸ 1. インテリジェントキャッシュ
API レスポンスのキャッシュによる高速化:
class WordPressCache:
"""Thread-safe cache for WordPress API responses."""
def __init__(self, ttl: int = 3600):
self.ttl = ttl
self._cache: Dict[str, CacheEntry] = {}
self._lock = threading.Lock()
self._stats = {"hits": 0, "misses": 0}
def get(self, key: str) -> Optional[Any]:
"""Get cached value if not expired."""
with self._lock:
entry = self._cache.get(key)
if entry and not entry.is_expired():
self._stats["hits"] += 1
return entry.value
self._stats["misses"] += 1
return None
💡 最適化効果💡 :
– カテゴリ/タグID取得の高速化
– API呼び出し回数の削減
– レスポンス時間の向上
– カテゴリ/タグID取得の高速化
– API呼び出し回数の削減
– レスポンス時間の向上
▸ 2. バッチ処理
効率的なディレクトリ処理:
def process_directory(self, stories_dir: Path) -> Dict[str, Any]:
"""Process all story files in a directory."""
markdown_files = list(stories_dir.glob('**/*.md'))
results = []
for md_file in markdown_files:
result = self.process_story_file(md_file)
results.append({
"file": str(md_file.relative_to(stories_dir)),
"status": "success" if result else "failed",
"post_id": result.get('id') if result else None
})
return {
"processed": len(markdown_files),
"successful": len([r for r in results if r["status"] == "success"]),
"results": results
}
⚙️ 運用とモニタリング
▸ 1. ログ管理
構造化ログによる運用監視:
# lib/utils.py
def setup_logging(config: LoggingConfig) -> None:
"""Setup structured logging with file output."""
# Create logs directory
config.log_dir.mkdir(exist_ok=True)
# Configure root logger
logging.basicConfig(
level=getattr(logging, config.level),
format='%(asctime)s [%(levelname)s] %(name)s: %(message)s',
handlers=[
logging.FileHandler(config.log_dir / 'wp-sync.log'),
logging.StreamHandler()
]
)
ログ項目:
– 処理ファイル名
– WordPress投稿ID
– エラー詳細
– パフォーマンス情報
– 処理ファイル名
– WordPress投稿ID
– エラー詳細
– パフォーマンス情報
▸ 2. アーティファクト管理
GitHub Actionsでの証跡管理:
- name: Upload validation artifacts
uses: actions/upload-artifact@v4
if: always()
with:
name: validation-logs
path: |
.logs/
stories/.validated
- name: Upload publish logs
uses: actions/upload-artifact@v4
if: always()
with:
name: publish-logs
path: .logs/
▸ 3. エラーハンドリング
堅牢なエラー処理機能:
class ResponseDebugger:
"""Debug and save HTTP responses."""
def save_exception(self, context: str, exception: Exception) -> None:
"""Save exception details for debugging."""
error_file = self.log_dir / f"error_{context}_{self._timestamp()}.json"
error_data = {
"timestamp": datetime.now().isoformat(),
"context": context,
"exception_type": type(exception).__name__,
"exception_message": str(exception),
"traceback": traceback.format_exc()
}
with error_file.open('w') as f:
json.dump(error_data, f, indent=2)
⚙️ 実践的な使用例
▸ 1. 新規ストーリー投稿
# 1. ストーリーファイル作成
cat > stories/tech/new-story.md << 'EOF'
---
title: "新しい技術記事"
slug: "new-tech-story"
categories: ["Tech"]
tags: ["tutorial"]
status: "publish"
---
# 新しい技術記事
本文内容...
EOF
# 2. Git操作
git add stories/tech/new-story.md
git commit -m "add: 新しい技術記事を追加"
git push origin main
# 3. 自動実行
# GitHub Actionsが自動で:
# - バリデーション実行
# - ドライラン確認
# - WordPress投稿
▸ 2. ローカル開発・テスト
# 環境設定
export WP_BASE_URL="https://your-site.com"
export WP_USERNAME="your-username"
export WP_APP_PASSWORD="xxxx xxxx xxxx xxxx"
# バリデーション実行
python scripts/validate.py --strict stories/
# ドライラン実行
python scripts/post_story.py --dry-run stories/
# 実際の投稿(要注意)
python scripts/post_story.py --mode publish stories/
▸ 3. トラブルシューティング
# デバッグモード実行
python scripts/post_story.py --debug --dry-run stories/
# ログ確認
cat .logs/wp-sync.log
# APIレスポンス確認
ls .logs/
# response_categories_20250111_143022.json
# response_posts_20250111_143025.json
⚙️ 今後の拡張可能性
▸ 1. メディア自動アップロード
# 将来実装予定
def upload_featured_image(self, image_path: Path) -> Optional[int]:
"""Upload image and return media ID."""
with image_path.open('rb') as f:
files = {'file': f}
response = self._request('POST', 'media', files=files)
return response.json().get('id')
▸ 2. マルチサイト対応
# config/sites.yaml (計画)
sites:
main:
base_url: "https://main-site.com"
username: "main-user"
staging:
base_url: "https://staging.com"
username: "stage-user"
▸ 3. Webhookトリガー
# webhookによるリアルタイム同期
@app.route('/webhook', methods=['POST'])
def handle_webhook():
"""Handle GitHub webhook for real-time sync."""
payload = request.get_json()
if payload.get('ref') == 'refs/heads/main':
trigger_sync_job()
⚙️ まとめ
この記事では、GitHub ActionsとWordPress REST APIを使った自動投稿システムの詳細な実装方法を解説しました。
💡 主要なポイント💡 :
- モジュラー設計: 保守性の高いアーキテクチャ
- セキュリティ重視: Application Password + 環境保護
- 運用考慮: ログ・モニタリング・エラーハンドリング
- パフォーマンス: キャッシュ・バッチ処理
- 拡張性: 将来機能への対応設計
このシステムにより、技術ブログやドキュメントサイトの更新を完全自動化でき、開発者は執筆に集中できる環境を構築できます。実際のプロダクションで稼働している実装例として、参考にしていただければと思います。
⚙️ 参考リンク
この記事は wp-story-sync プロジェクトの実装を基にしています。最新のコードとドキュメントは GitHub リポジトリでご確認ください。
⚙️ Tech Series – T-01 🔧
Engineering the future…