OpenAI APIとGitHub Actions、Zenn-CLIを使った記事自動生成システム
AIを活用してZennの記事を自動生成・投稿したいと考えたことはありませんか?この記事では、OpenAI API(Embedding/GPTモデル)とzenn-cliを組み合わせて、過去記事と関連性のある新しいトピックを自動で見つけ、Zennに"無限"に記事を投稿し続ける仕組みを解説します。PythonスクリプトとGitHub Actionsを使って、完全自動化まで実現します。
1. システム概要
このシステムでは以下の特徴があります:
- OpenAI Embedding APIを使って過去記事との類似度を計算し、重複を避けた新トピックを選定
- メモファイル(
memo.md
)によるトピック管理と方向性の指示 - Wikipedia APIとBrave Search APIを活用した情報収集
- GitHub Actionsによる定期的な自動生成・投稿
- GPT-4.1-miniによる高品質な記事生成
2. 必要なAPIキーの準備
-
OpenAI API: OpenAI公式サイトでAPIキーを取得し、
.openai_api_key
ファイルに保存 -
Brave Search API(オプション): Brave Search APIで取得し、
.brave_search_api_key
に保存
3. 必要なライブラリのインストール
pip install openai numpy pyyaml requests
npm install zenn-cli -g # Zenn CLIのインストール
4. ZennとGitHubの連携設定
4.1 Zenn側での設定
- Zennアカウント作成→ダッシュボード→Settings→GitHub連携からリポジトリを登録
- 連携するブランチ(例:main)を指定
4.2 Zenn CLIでローカル環境構築
npx zenn init
4.3 GitHub Actionsの設定
以下のワークフローファイルを .github/workflows/auto-generate-article.yml
として作成します:
name: Auto Generate Zenn Article
permissions:
contents: write
on:
schedule:
- cron: '0 0 * * *' # 毎日0時(日本時間9時)実行
workflow_dispatch: # 手動実行も可能
jobs:
generate:
runs-on: ubuntu-latest
defaults:
run:
shell: bash
working-directory: ${{ github.workspace }}
steps:
# 1. リポジトリのチェックアウト
- name: Checkout with PAT only
uses: actions/checkout@v4
with:
token: ${{ secrets.PAT_TOKEN }}
persist-credentials: false
fetch-depth: 0
# 2. リモートURLの設定
- name: Configure remote with PAT
run: |
git remote set-url origin https://${{ secrets.PAT_TOKEN }}@github.com/${{ github.repository }}.git
# 3. Python環境セットアップ
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: pip install openai numpy pyyaml requests
# 4. Node.js環境のセットアップ
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
# 5. APIキー設定と記事生成
- name: Generate Article with API Keys
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
BRAVE_SEARCH_API_KEY: ${{ secrets.BRAVE_SEARCH_API_KEY }}
run: |
echo "$OPENAI_API_KEY" > .openai_api_key
echo "$BRAVE_SEARCH_API_KEY" > .brave_search_api_key
python generate_zenn_article_embedding.py
# 6. 生成結果をコミット&プッシュ
- name: Commit and Push
run: |
git config user.name "${{ github.actor }}"
git config user.email "${{ github.actor }}@users.noreply.github.com"
git add articles/*.md
git commit -m 'auto: generate article' || echo "No changes to commit"
git push origin HEAD:${{ github.ref_name }}
GitHub Secretsに以下を設定します:
-
OPENAI_API_KEY
: OpenAI APIキー -
BRAVE_SEARCH_API_KEY
: Brave Search APIキー(オプション) -
PAT_TOKEN
: GitHubのPersonal Access Token(repo:contentsスコープ)
5. トピック管理とメモ機能
memo.md
ファイルを使って、記事生成の方向性やトピック候補を管理できます。以下の形式で記述します:
# 記事の方向性と要望
- 初心者向けのチュートリアル形式で
- Pythonコード例を必ず含める
- セキュリティの観点も言及する
# トピック一覧
## Webフレームワーク
- FastAPIの実践的な使い方
- Django RESTフレームワークによるAPI開発
- Flask vs FastAPIの詳細比較
## クラウド技術
- AWS Lambdaでサーバーレスアプリケーション構築
- GCPのCloud Runを使ったコンテナデプロイ
システムは以下の優先順位でトピックを選択します:
- コマンドラインで指定されたトピック
- memo.mdに記載された未使用トピック(使用頻度の低いトピックを優先)
- AIによる新トピックの提案
6. 記事生成のコア機能
6.1 Embeddingによる類似度計算
ここではgenerate_zenn_article_embedding.py
のコア部分をより詳細に解説します:
import openai
import os
import glob
import re
import yaml
import unicodedata
import random
import string
import numpy as np
import time
from datetime import datetime
import requests
from urllib.parse import quote
from collections import Counter
# APIキーの設定
API_KEY_PATH = ".openai_api_key"
try:
with open(API_KEY_PATH, "r", encoding="utf-8") as f:
OPENAI_API_KEY = f.read().strip()
if not OPENAI_API_KEY:
raise ValueError()
except Exception:
raise RuntimeError(f"OpenAI APIキーが {API_KEY_PATH} に見つかりません。")
client = openai.OpenAI(api_key=OPENAI_API_KEY)
# Brave Search APIキー設定
BRAVE_KEY_PATH = ".brave_search_api_key"
BRAVE_SEARCH_API_KEY = ""
if os.path.exists(BRAVE_KEY_PATH):
with open(BRAVE_KEY_PATH, "r", encoding="utf-8") as f:
BRAVE_SEARCH_API_KEY = f.read().strip()
if not BRAVE_SEARCH_API_KEY:
BRAVE_SEARCH_API_KEY = os.getenv("BRAVE_SEARCH_API_KEY", "").strip()
if not BRAVE_SEARCH_API_KEY:
print(f"Warning: Brave Search APIキーが未設定です。Web検索をスキップします。")
BRAVE_SEARCH_API_HOST = os.getenv("BRAVE_SEARCH_API_HOST", "api.search.brave.com")
ARTICLES_DIR = "articles"
MEMO_PATH = "memo.md"
# レート制限対策のリトライ関数
def api_call_with_retry(func, max_retries=5):
retries = 0
while retries < max_retries:
try:
return func()
except openai.RateLimitError:
sleep_time = 2 ** retries # 指数関数的に待機時間増加
print(f"レート制限により{sleep_time}秒待機")
time.sleep(sleep_time)
retries += 1
raise Exception(f"{max_retries}回の再試行後もAPIエラーが続きました")
# slug生成ロジック
def slugify(value):
value = unicodedata.normalize('NFKC', value)
value = value.lower()
value = re.sub(r'[^a-z0-9\-_]', '', value)
if len(value) < 12:
pad = ''.join(random.choices(string.ascii_lowercase + string.digits, k=12-len(value)))
value = (value + '-' + pad)[:12]
return value[:50]
# コサイン類似度計算
def cosine_similarity(a, b):
a = np.array(a)
b = np.array(b)
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
# Embedding取得
def get_embedding(text):
return api_call_with_retry(
lambda: client.embeddings.create(
input=text,
model="text-embedding-3-small"
).data[0].embedding
)
# 過去記事のembedding取得
def process_existing_articles():
md_files = glob.glob(os.path.join(ARTICLES_DIR, '*.md'))
article_embeddings = []
article_summaries = []
article_topics = []
all_topics = []
for file in md_files:
with open(file, 'r', encoding='utf-8') as f:
content = f.read()
# frontmatterを除外し、本文の冒頭500文字程度をembedding対象に
body = re.split(r'^---.*?---\s*', content, flags=re.DOTALL|re.MULTILINE)
summary = body[-1].strip()[:500]
article_summaries.append(summary)
article_embeddings.append(get_embedding(summary))
# 記事からトピックを抽出
topics = extract_topics_from_article(file)
article_topics.append(topics)
all_topics.extend(topics)
# トピックの出現頻度計算
topic_counter = Counter(all_topics)
total_articles = len(md_files)
print("現在のトピック分布:")
for topic, count in topic_counter.most_common(10):
topic_ratio = count / total_articles
print(f"- {topic}: {topic_ratio:.2f} ({count}/{total_articles})")
return article_embeddings, article_summaries, article_topics, all_topics
# メモからトピック取得
def get_memo_topics():
if not os.path.exists(MEMO_PATH):
return [], {}, []
with open(MEMO_PATH, 'r', encoding='utf-8') as f:
memo_content = f.read()
# メイントピックとサブトピックの抽出
memo_main_topics, memo_subtopics = extract_topics_from_memo(memo_content)
article_directions = extract_direction_from_memo(memo_content)
print("\nメモファイルから取得したメイントピック:")
for topic in memo_main_topics:
print(f"- {topic} (サブトピック: {len(memo_subtopics.get(topic, []))}個)")
return memo_main_topics, memo_subtopics, article_directions
6.2 外部情報の取得
記事生成の品質向上のため、WikipediaとBrave Searchから情報を取得します:
# Wikipediaから概要情報取得
def fetch_wikipedia_summary(topic, sentences=3):
try:
# Wikipedia REST APIを利用
url = f"https://ja.wikipedia.org/api/rest_v1/page/summary/{quote(topic)}"
resp = requests.get(url, timeout=10)
if resp.status_code == 200:
# 概要テキストを取得
return resp.json().get('extract', '')
except Exception as e:
print(f"Wikipedia APIエラー: {e}")
return ''
# Brave Search APIからWeb検索結果取得
def fetch_web_search_snippets(query, num_results=5):
if not BRAVE_SEARCH_API_KEY:
print("Brave Search APIキーが設定されていないためスキップします")
return ""
# エンドポイントとヘッダー設定
url = f"https://{BRAVE_SEARCH_API_HOST}/res/v1/web/search"
headers = {
"x-subscription-token": BRAVE_SEARCH_API_KEY,
"Authorization": f"Bearer {BRAVE_SEARCH_API_KEY}"
}
params = {"q": query, "source": "web", "size": num_results}
try:
# 検索クエリ実行
resp = requests.get(url, headers=headers, params=params, timeout=15)
if resp.status_code != 200:
print(f"Brave Search APIエラー: {resp.status_code}, {resp.text}")
return ""
# 結果処理
data = resp.json()
web_results = data.get("web", {}).get("results", [])
snippets = []
for item in web_results[:num_results]:
desc = item.get("description", "")
link = item.get("url", "")
snippets.append(f"- {desc} ({link})")
return "\n".join(snippets)
except Exception as e:
print(f"Brave Search API呼び出しエラー: {e}")
return ""
# 新トピックの探索と選定
def select_new_topic(article_summaries, article_embeddings, article_topics, memo_centroid=None):
# ChatGPTで新トピック候補を複数生成
prompt_topic = (
"以下は過去のZenn記事の内容要約です。これらと重複せず、" \
"多様な分野(モバイル、組み込み、データベース、セキュリティ、" \
"機械学習、フロントエンドなど)から新しい技術トピックを5つ日本語で提案してください。"
"\n---\n" + '\n'.join(article_summaries) + "\n---\n"
"提案形式: 1行につき1トピック名だけを出力してください。説明や前置きは不要です。"
)
resp = api_call_with_retry(lambda: client.chat.completions.create(
model="gpt-4.1-mini",
messages=[
{"role": "system", "content": "あなたは優秀なテック編集者です。"},
{"role": "user", "content": prompt_topic}
],
max_tokens=128,
temperature=0.9,
))
topic_candidates = [line.strip() for line in resp.choices[0].message.content.split('\n') if line.strip()]
# 各トピック候補のembeddingと既存記事との類似度を計算
best_topic = None
best_score = -1.0
best_avg_sim = None
for topic in topic_candidates:
# トピックの重複チェック
topic_lower = topic.lower()
is_duplicate = any(any(topic_lower in t.lower() for t in article_topic_list)
for article_topic_list in article_topics)
# 重複していればスキップ
if is_duplicate:
print(f"トピック '{topic}' は既存記事と重複するためスキップ")
continue
# embedding取得と類似度計算
topic_emb = get_embedding(topic)
sims = [cosine_similarity(topic_emb, art_emb) for art_emb in article_embeddings]
avg_sim = np.mean(sims)
# memoとの類似度ボーナスを反映
memo_bonus = cosine_similarity(topic_emb, memo_centroid) if memo_centroid is not None else 0
combined_score = memo_bonus * 0.7 + (1 - avg_sim) * 0.3 # メモ類似度高、他記事との類似度低いほど高スコア
# 最適な類似度範囲で選択
if 0.2 < avg_sim < 0.7 and combined_score > best_score:
best_score = combined_score
best_topic = topic
best_avg_sim = avg_sim
# 該当がなければ、一番重複しない(最小類似度)トピックを選択
if best_topic is None:
min_sim = 1.0
for topic in topic_candidates:
topic_emb = get_embedding(topic)
sims = [cosine_similarity(topic_emb, art_emb) for art_emb in article_embeddings]
avg_sim = np.mean(sims)
if avg_sim < min_sim:
min_sim = avg_sim
best_topic = topic
best_avg_sim = avg_sim
if best_topic:
print(f"AIがembedding類似度で選んだ新しいトピック: {best_topic} (平均類似度: {best_avg_sim:.2f})")
else:
best_topic = topic_candidates[0] if topic_candidates else "AI技術の最新動向"
return best_topic
6.3 記事生成プロセス
トピックが決定したら、GPT-4.1-miniを使用して記事を生成します。ここではメインの記事生成処理を紹介します:
# 記事生成関数
def generate_article(theme, article_directions=None):
# 方向性テキストの準備
direction_text = ""
if article_directions:
direction_text = "記事の方向性と要望:\n" + "\n".join(article_directions) + "\n\n"
# 補足情報をWikipediaとWeb検索から取得
print(f"トピック '{theme}' に関する情報を収集中...")
wiki_sum = fetch_wikipedia_summary(theme)
web_search_snippets = fetch_web_search_snippets(theme)
# 記事生成プロンプトの構築
prompt = f"""{theme}
この記事はChatGPTを利用して生成されたものであることを説明の最初に明記してください。
前提情報:
Wikipedia概要:
{wiki_sum}
Web検索結果:
{web_search_snippets}
{direction_text}以下の構成に従って記事を書いてください:
- 1. 導入:テーマの概要や重要性(最低300文字で背景と動機を詳述)
- 2. 背景・基礎知識:必要な前提知識や歴史(用語定義や図解提案を含む)
- 3. 本論:技術的な詳細や仕組み、手順(コードフローやアーキテクチャ図も言及)
- 4. 具体例・コード例:実行可能なフルコードを含め、手順も詳しく説明
- 5. 応用・発展:より高度な使い方や関連分野への応用例を示す
- 6. まとめ・今後の展望:実装上の注意点や次のステップを提示
- 7. Tips & Best Practices:実践的なコツを5つ挙げて詳細解説
- ※さらに、各セクションの主要ポイントを箇条書きで整理し、参考リンクを3つ以上示してください。
記事本文の前に、以下の形式で Zenn 用 frontmatter を YAML 形式で出力してください:
---
title: "記事タイトル"
emoji: "絵文字"
type: "tech"
topics:
- トピック1
- トピック2
- トピック3
published: True
---
トピックには {theme} と関連技術を含めてください。"""
print(f"記事生成開始: {theme}")
# API呼び出し(レート制限寛時自動リトライ付き)
try:
response = api_call_with_retry(lambda: client.chat.completions.create(
model="gpt-4.1-mini", # 高品質なモデルを利用
messages=[
{"role": "system", "content": "あなたは技術記事執筆のプロフェッショナルです。深い知識で技術詳細と実装方法を解説できます。"},
{"role": "user", "content": prompt}
],
max_tokens=2048, # 長めの記事生成
temperature=0.7, # 創造性と一貫性のバランスを取る
))
content = response.choices[0].message.content
print("記事生成完了")
# frontmatterと本文をパースする
fm_match = re.search(r'^---\s*\n(.*?)\n---\s*\n(.*)', content, re.DOTALL | re.MULTILINE)
# 正しい形式で生成されたかチェック
if fm_match:
# frontmatter部分と本文部分を取得
fm_yaml = fm_match.group(1)
body = fm_match.group(2)
# YAMLとしてパース
try:
fm = yaml.safe_load(fm_yaml)
# 必要なフィールドがあるか確認
title = fm.get('title', f"{theme}について")
emoji = fm.get('emoji', '💻')
topics = fm.get('topics', [theme, 'IT技術', 'プログラミング'])
if isinstance(topics, str):
topics = [topics] # 文字列の場合はリストに変換
except Exception as e:
print(f"YAMLパースエラー: {e}")
# エラー時はデフォルト値を設定
title = f"{theme}について"
emoji = '💻'
topics = [theme, 'IT技術', 'プログラミング']
else:
# frontmatterがない場合はデフォルト値で作成
title = f"{theme}について"
emoji = '💻'
topics = [theme, 'IT技術', 'プログラミング']
body = content
# ファイル名用のslug生成
now = datetime.now().strftime("%Y%m%d-%H%M%S")
slug_base = slugify(title)
slug = f"{slug_base}-{now}"[:50]
# 最終的なfrontmatterの生成
updated_frontmatter = yaml.safe_dump({
'title': title,
'emoji': emoji,
'type': 'tech',
'topics': topics,
'published': True
}, allow_unicode=True, sort_keys=False)
final_content = f"---\n{updated_frontmatter}---\n\n{body}"
# 記事ファイルを保存
output_path = f"articles/{slug}.md"
with open(output_path, "w", encoding="utf-8") as f:
f.write(final_content)
print(f"記事を保存しました: {output_path}")
return output_path
except Exception as e:
print(f"記事生成中にエラーが発生しました: {e}")
return None
6.4 メイン処理フロー
最後に、メイン処理部分でどのように全体を組み合わせているかを見てみましょう:
def main():
# コマンドライン引数のパース
parser = argparse.ArgumentParser()
parser.add_argument('--topic', '-t', help='手動で指定するトピック', default=None)
args = parser.parse_args()
# 既存記事からデータ取得
article_embeddings, article_summaries, article_topics, all_topics = process_existing_articles()
# メモファイルからトピック取得
memo_main_topics, memo_subtopics, article_directions = get_memo_topics()
# トピック選定ロジック
if args.topic: # 手動指定トピックが優先
theme = args.topic
print(f"手動指定トピック: {theme}")
elif memo_main_topics and memo_subtopics: # 次にメモファイルから優先トピックを選択
# メモファイルから最適なトピックを選択するロジック
# ...コード省略...
else: # AIによる新トピックの探索
theme = select_new_topic(article_summaries, article_embeddings, article_topics)
print(f"AI選定トピック: {theme}")
# 記事生成実行
output_path = generate_article(theme, article_directions)
if output_path:
print(f"成功: 記事 '{theme}' を {output_path} に生成しました。")
return 0
else:
print("エラー: 記事生成に失敗しました。")
return 1
if __name__ == "__main__":
sys.exit(main())
7. 運用上の注意点
7.1 API利用コスト
- OpenAI APIの利用には料金が発生します(embedding、GPT利用ともに)
- 記事生成頻度によって月額コストが変動するため、予算管理が必要です
7.2 セキュリティ考慮事項
- API KeyはGitHub Secretsで安全に管理
- GitHubのPATは最小権限原則に従い設定する
- 生成されたコンテンツは公開前に確認することをお勧めします
7.3 記事品質の向上
-
memo.md
に詳細な方向性指示を記載することで品質向上 - 生成されたコンテンツに対する人間の編集・レビューを併用する
- 定期的にプロンプトをブラッシュアップする
7.4 チーム開発への応用
このシステムは個人利用だけでなく、チームでの技術ブログ運用にも適しています:
- 共有リポジトリとGitHub Issuesでトピック管理
- PRレビューで記事の質チェック
- チームメンバーの専門知識をmemo.mdに反映
8. 運用コストと実践上の注意点
8.1 API利用コストの目安
自動記事生成システムの運用コストは主にOpenAI APIの利用量によって決まります:
- Embedding API: 1,000トークンあたり約$0.0001(text-embedding-3-small)
- GPT-4.1-mini: 1,000トークンあたり入力約$0.0015、出力約$0.0045
毎日1記事生成する場合、月に約$5〜15程度の費用目安となります。
8.2 トラブルシューティング
実運用でよくある問題と対策:
-
API制限エラー: 指数バックオフによるリトライロジックの実装
def api_call_with_retry(func, max_retries=5): retries = 0 while retries < max_retries: try: return func() except openai.RateLimitError: sleep_time = 2 ** retries # 指数関数的に待機時間増加 print(f"レート制限により{sleep_time}秒待機") time.sleep(sleep_time) retries += 1
-
Push権限エラー(403):
-
permissions: contents: write
がワークフロー先頭に記載されているか確認
-
-
初回Cron未実行:
- Cronトリガーは初回に動作しないため、workflow_dispatchで手動起動してください
-
.openai_api_keyの管理:
-
.gitignore
に登録し、Actions内でのみ一時生成することで安全に運用
-
-
生成内容の品質: プロンプトのチューニングと定期的な見直し
-
GitHubのトークン失効: PAT有効期限の管理と更新通知の設定
8.3 実際の生成記事例と品質
以下は、実際に生成された記事の一部です:
---
title: "RustとWebAssemblyで作る高速ブラウザゲーム開発入門"
emoji: "🎮"
type: "tech"
topics: ["Rust", "WebAssembly", "ゲーム開発", "JavaScript", "Web"]
published: true
---
この記事はChatGPTを利用して生成されました。
## はじめに
ブラウザ上で動作するゲーム開発において、パフォーマンスは常に重要な要素です。...
生成された記事は基本的に投稿可能な品質ですが、以下の点で人間による確認・修正が役立ちます:
- コードの正確性と最新性の確認
- 事実情報の検証(特に急速に変化する技術分野)
- 専門的なニュアンスの調整
8.4 便利なカスタマイズテクニック
システムをより効果的に使うためのカスタマイズヒント:
-
記事生成頻度の調整:cron式を変更して適切な間隔(週2回など)に設定
-
メモファイルの作成例:
# 記事の方向性と要望 - 初心者にわかりやすく、ステップバイステップで説明する - 実用的なコード例を必ず含める - 簡単なデモプロジェクトも欲しい # トピック一覧 ## フロントエンド - ReactとTypeScriptで作るコンポーネントライブラリ - Next.jsを使った静的サイト生成
-
プロンプトチューニング:
generate_zenn_article_embedding.py
のプロンプト部分を編集して自分好みのスタイルに調整
9. まとめと発展方向
この仕組みを用いることで、Zennへの継続的な記事投稿を自動化できます。このアプローチにより:
- 過去記事との重複を避けつつ、既存コンテンツと関連性のあるテーマを継続的に発掘
- 記事生成から投稿までのプロセスを完全自動化し、人的コストやヒューマンエラーを削減
- ワークフローの再現性・拡張性も高く、他サービスへの応用や高度な運用も容易
さらに発展させる方向性としては:
- 画像生成APIとの連携で図解も自動生成
- 記事の統計分析と自動レポート生成
- SNS投稿の自動化連携
- 既存記事の更新プロセスの自動化
ぜひこのシステムをカスタマイズして、あなただけの自動記事生成・投稿パイプラインを構築してみてください!
GitHub Repository: 完全なコードはzenn-testリポジトリで参照できます。
手動実行も可能で、初回の起動確認やデバッグに便利です。
name: Auto Generate Zenn Article
permissions:
contents: write # Actionsからリポジトリへの書き込みを許可
on:
schedule:
- cron: '0 0 * * *' # 毎日0時(日本時間)に実行
workflow_dispatch: # 手動トリガー(初回実行やテストに利用)
jobs:
generate:
runs-on: ubuntu-latest
steps:
- name: リポジトリのチェックアウト
uses: actions/checkout@v4
- name: Python環境のセットアップ
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: 依存パッケージのインストール
run: pip install openai numpy pyyaml
- name: Node.js環境のセットアップ(zenn-cli用)
uses: actions/setup-node@v4
with:
node-version: '20'
- name: 記事自動生成
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
cd $GITHUB_WORKSPACE
echo "${OPENAI_API_KEY}" > .openai_api_key
python generate_zenn_article_embedding.py
- name: コミット&プッシュ
run: |
git config --global user.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
git add articles/*.md
git commit -m 'auto: generate article [skip ci]' || echo "No changes to commit"
git push
ステップ解説
-
permissions: contents: write
ワークフローの先頭に記述し、Actionsからリポジトリへの書き込み権限を付与します。 -
トリガー設定(on)
-
schedule
: cronで毎日0時に自動実行 -
workflow_dispatch
: 手動起動(初回検証やデバッグ用)
-
-
環境構築
- Python 3.11(actions/setup-python)
- 必要パッケージ(openai, numpy, pyyaml)
- Node.js(zenn-cli用、不要なら省略可)
-
記事生成
- Secretsに登録した
OPENAI_API_KEY
を.openai_api_key
に書き出し -
generate_zenn_article_embedding.py
を実行し、articles/
配下にMarkdown記事を生成
- Secretsに登録した
-
コミット&プッシュ
-
github-actions[bot]
ユーザーで自動コミット&push - 変更がない場合もエラーを抑制(
|| echo "No changes to commit"
)
-
トラブルシューティング
-
APIキーが空
-
.openai_api_key
のサイズが0バイトになっていないか確認 - リポジトリの「Settings > Secrets and variables > Actions」で
OPENAI_API_KEY
を再設定
-
-
Push権限エラー(403)
-
permissions: contents: write
がワークフロー先頭に記載されているか確認
-
-
初回Cron未実行
- Cronトリガーは初回に動作しないため、workflow_dispatchで手動起動してください
-
.openai_api_keyの管理
-
.gitignore
に登録し、Actions内でのみ一時生成することで安全に運用
-
7. まとめ
本記事で紹介した仕組みを活用することで、OpenAI APIとembeddingを用いた「関連性の高い新規トピックの自動探索」と、zenn-cli+GitHub Actionsによる「自動記事生成・投稿フロー」を構築できます。
このアプローチにより、
- 過去記事との重複を避けつつ、既存コンテンツと関連性のあるテーマを継続的に発掘
- 記事生成から投稿までのプロセスを完全自動化し、人的コストやヒューマンエラーを削減
- ワークフローの再現性・拡張性も高く、他サービスへの応用や高度な運用も容易
AI活用による技術情報発信の自動化・効率化に、ぜひ本記事の手法を役立ててみてください。
Discussion