AIエージェントの公開事故を防ぐ|draft・approved・publishedを分ける実装
draft: falseは、Markdown では 1 行です。しかし、この 1 行を朝稿の生成処理に触らせると、文章を作る権限と公開する権限が同じになります。
hibanas-net では、生成した記事を content branch へ置き、draft: trueのまま PR #24 でレビューしました。マージと公開は別の指示として扱います。GitHub に存在しない environment や production 承認は前提にしません。
前回から今回への差分
前回のGit管理ブログへ安全に書き込むcronでは、clean 確認、fast-forward 同期、品質検査、push までを扱いました。
| 前回完了時 | 今回完了後 |
|---|---|
| 検証済みの記事をGitへ保存します | 保存先をcontent branchに限定します |
| 生成処理の成功を確認します | draft: trueとPRのレビューを別に確認します |
| pushで処理が終わります | 公開対象の1ファイルだけを別操作でdraft: falseへ変えます |
Astro の Content Collections でdraftを扱えている場合、「実際の朝稿ブランチを確認する」まで飛ばせます。
なぜapprovedをfrontmatterへ足さないのか
記事ファイルが持つ状態は、公開対象かどうかです。hibanas-net ではdraftの boolean で表します。
レビューの承認は、特定の commit 差分に対する記録です。これは GitHub の PR に残ります。frontmatter へapproved: trueを足すと、誰が、どの commit を、いつ確認したのかが 1 行へ潰れます。
そのため、状態を次の場所へ分けます。
| 状態 | 記録する場所 | 変更する担当 |
|---|---|---|
| draft | 記事のdraft: true | 朝稿の生成担当です |
| approved | PRのレビュー履歴 | レビュー担当です |
| published | 対象記事のdraft: falseとmainのcommit | 明示された公開担当です |
用語表
| 用語 | hibanas-netでの意味 |
|---|---|
| content branch | 朝稿の記事とOGをmainから分けて置くbranchです |
| draft | Astroの公開対象から外す記事状態です |
| approved | PR上で対象差分のレビューが終わった状態です |
| published | 指定記事がdraft: falseになり、mainへ入った状態です |
| quality gates | frontmatter、文体、型、buildを確認する5コマンドです |
| fail closed | 状態を確認できない場合に公開せず止める動きです |
実際の流れ
手順画面の生成画像は使いません。hibanas-net の処理をテキスト図にすると、次の順序です。
朝稿の生成
├─ mainをfast-forwardで同期します
├─ content branchを作ります
├─ 記事をdraft: trueで保存します
├─ OGを文字なし16:9で保存します
└─ 品質ゲートを5つ通します
│
▼
PR #24
├─ 記事とOGだけをレビューします
├─ 指摘を同じbranchで直します
└─ draft: trueのままmainへ入れます
│
▼
明示された公開操作
├─ 対象記事を1本に固定します
├─ draft: trueをdraft: falseへ1回だけ変えます
├─ 品質ゲートを再実行します
└─ mainへ反映します
OpenCode の成功、CI の成功、PR の承認だけでは公開しません。公開操作には、レビュー済みの対象記事を明示した指示が必要です。
実際の朝稿ブランチを確認する
PR #24 の branch と状態は、GitHub CLI で確認できます。hibanas-net で実行したコマンドです。
~/.local/bin/gh pr view 24 \
--repo dicekanbe/hibanas-net \
--json number,state,headRefName,mergedAt \
--jq '{number,state,headRefName,mergedAt}'
実行結果です。
{"headRefName":"content/2026-08-18-ai-agent-draft-approval-publish-gates","mergedAt":"2026-08-17T23:42:59Z","number":24,"state":"MERGED"}
この PR で追加した記事は、まさにsrc/content/posts/ai-agent-draft-approval-publish-gates.mdです。OG はpublic/images/posts/ai-agent-draft-approval-publish-gates.webpです。抽象的なファイル名へ置き換えません。
書き始める前にmainから分ける
PR #24 で使った branch 名を含むコマンドは次の形です。
git status --porcelain=v1
git switch main
git pull --ff-only
git switch -c content/2026-08-18-ai-agent-draft-approval-publish-gates
最初のgit statusが何か返した場合は止めます。自動で stash、reset、clean を実行しません。main の同期に失敗した場合も記事を書きません。
branch を作った後は、現在地を確認します。
git branch --show-current
PR #24 で使った branch 名は次のとおりです。
content/2026-08-18-ai-agent-draft-approval-publish-gates
対象を記事とOGの2ファイルに絞る
朝稿で変更してよいパスを実名で固定します。
article=src/content/posts/ai-agent-draft-approval-publish-gates.md
image=public/images/posts/ai-agent-draft-approval-publish-gates.webp
test "$(grep -c '^draft: true$' "$article")" -eq 1
git status --porcelain=v1 | while IFS= read -r line; do
path=${line#???}
case "$path" in
"$article"|"$image") ;;
*) printf 'unexpected path: %s\n' "$path" >&2; exit 1 ;;
esac
done
grep -cが 1 でなければ失敗します。対象外のパスが 1 つでも出た場合も、終了コード 1 で止まります。成功時に仮のメッセージは表示しません。何も表示せず終了コード 0 になります。
既存記事の公開状態が紛れ込んでいないかも調べます。
if git diff --unified=0 -- \
'src/content/**/*.md' \
'src/content/**/*.mdx' |
grep -Eq '^-draft: true$|^\+draft: false$'; then
printf 'refuse: publication change detected\n' >&2
exit 1
fi
.mdと.mdxは別々の pathspec にします。PR #24 のレビューでは、'src/content/**/*.{md,mdx}'が期待どおり展開されない点を指摘され、修正しました。
5つの品質ゲートを実行する
hibanas-net で使うコマンドは、repository にある実ファイルへ合わせています。
node scripts/audit-frontmatter.mjs
node scripts/check-humanization.mjs
node_modules/.bin/textlint "src/content/**/*.{md,mdx}"
node_modules/.bin/astro check
node_modules/.bin/astro build
5 つすべてが終了コード 0 になるまで push しません。astro buildでは、公開記事の route が生成されます。draft: trueの記事は公開対象へ入りません。
この章の実行結果は、記事を改稿した現在の branch でも確認します。結果の要点は末尾の PR で再現できます。
PR #24でレビューへ渡した内容
対象 2 ファイルだけを stage します。PR #24 の commit に記録されたメッセージも実名のまま使います。
git add \
src/content/posts/ai-agent-draft-approval-publish-gates.md \
public/images/posts/ai-agent-draft-approval-publish-gates.webp
git commit -m "content: AIエージェントの公開ゲート下書きを追加"
git push -u origin HEAD
PR の URL は次のとおりです。
https://github.com/dicekanbe/hibanas-net/pull/24
レビューでは、Git pathspec の誤りと、ガイドラインに合わない自己言及を修正しました。OG は 1200×675 ピクセルで、記事タイトルを焼き込んでいません。修正後もdraft: trueを維持しました。
レビュー自体にも品質バーを持たせる
PR #26 では、OpenCode 記事内の YAML を main の workflow へ同期しました。PR #27 では、.github/workflows/opencode-pr-review.ymlの prompt へ hibanas-net #15 の必須項目を加えました。
~/.local/bin/gh pr view 26 --repo dicekanbe/hibanas-net --json number,state,title --jq '{number,state,title}'
~/.local/bin/gh pr view 27 --repo dicekanbe/hibanas-net --json number,state,title --jq '{number,state,title}'
実行結果です。
{"number":26,"state":"MERGED","title":"content: OpenCode workflow記事を実装に同期"}
{"number":27,"state":"MERGED","title":"ci: OpenCodeの記事レビュー基準を強化"}
PR #27 後の prompt は、です・ます調、連載の差分、Why before How、用語表、テキスト構成図、実行可能なコマンド、正直な出力、飛ばせる章、エラー表、一次情報の出典表を required として扱います。生成した手順画面と、文字入り OG も禁止します。
それでも OpenCode は公開担当ではありません。prompt にもA successful OpenCode review is not permission to publish the article.と明記しています。
公開操作は1ファイルだけを変える
公開の指示を受けた後も、対象を名前で固定します。別の下書きをまとめて公開しません。
python3 - <<'PY'
from pathlib import Path
path = Path("src/content/posts/ai-agent-draft-approval-publish-gates.md")
text = path.read_text()
old = "draft: true"
if text.count(old) != 1:
raise SystemExit("draft state is not unique")
path.write_text(text.replace(old, "draft: false"))
PY
git diff --check
git diff -- src/content/posts/ai-agent-draft-approval-publish-gates.md
期待する差分は、指定ファイルのdraft1 行だけです。title、description、date、image、本文が同時に変わった場合は公開操作を止めます。品質ゲート 5 つを再実行してから main へ反映します。
よくあるエラー
| 症状 | 原因 | 対応 |
|---|---|---|
| 朝稿がmainを直接変更します | 生成と公開の境界がありません | cleanなmainからcontent/*を作ります |
draft: trueの削除を見逃します | +draft: falseだけを探しています | -draft: trueも同じdiff検査へ入れます |
| Markdownのpathspecが効きません | brace展開を引用符内へ入れています | .mdと.mdxを別々に指定します |
| 関係ない記事がcommitされます | git add .を使っています | 記事とOGの実パスだけをstageします |
| OpenCodeがLGTMなら公開されます | レビュー成功を公開指示として扱っています | 対象記事を明示した公開操作を別にします |
| 公開時に本文まで変わります | draft変更と改稿を混ぜています | 1行以外の差分があれば止めます |
このrepositoryで採用していないもの
GitHub environment のproduction承認は、この手順へ入れていません。2026 年 8 月 18 日に GitHub API で dicekanbe/hibanas-net の environment 一覧を確認したところ、登録名は返りませんでした。
main の branch protection も、API は404 Not Foundを返しました。規則がない場合と権限不足を API だけでは区別できません。そのため、存在を前提にした設定例や期待値は載せません。
hibanas-net で実際に確認できる境界は、content branch、PR、draft、5 つの品質ゲートです。この 4 つで止め、存在しない仕組みを完成形として補いません。
出典
| 一次情報 | 確認した内容 |
|---|---|
| hibanas-net PR #24 | 朝稿のbranch、レビュー指摘、記事とOGの差分 |
| hibanas-net PR #26 | OpenCode記事と本番workflowの同期 |
| hibanas-net PR #27 | #15品質バーを含むレビューprompt |
| Astro Docs: Content collections | schemaとentryの扱い |
| GitHub Docs: Pull requests | commit差分に対するレビュー履歴 |
| hibanas-net docs/guidelines.md | です・ます調、連載ハンズオン、画像の基準 |
