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朝稿の生成担当です
approvedPRのレビュー履歴レビュー担当です
published対象記事のdraft: falseとmainのcommit明示された公開担当です

用語表

用語hibanas-netでの意味
content branch朝稿の記事とOGをmainから分けて置くbranchです
draftAstroの公開対象から外す記事状態です
approvedPR上で対象差分のレビューが終わった状態です
published指定記事がdraft: falseになり、mainへ入った状態です
quality gatesfrontmatter、文体、型、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 #26OpenCode記事と本番workflowの同期
hibanas-net PR #27#15品質バーを含むレビューprompt
Astro Docs: Content collectionsschemaとentryの扱い
GitHub Docs: Pull requestscommit差分に対するレビュー履歴
hibanas-net docs/guidelines.mdです・ます調、連載ハンズオン、画像の基準
この記事をシェア