AIエージェントの公開事故を防ぐ|draft・approved・publishedを分ける実装

AIエージェントの公開事故を防ぐ|draft・approved・publishedを分ける実装

draft: falseは、Markdown ではたった 1 行です。この 1 行を朝稿の生成処理に触らせると、文章を作る権限と公開する権限の区別が消えます。生成の失敗が、そのまま公開事故になります。

hibanas-net では、生成した記事をdraft: trueのまま content branch へ置き、PR #24 でレビューしました。マージと公開も別の操作です。対象記事を明示した指示があったときだけ、draft: falseへ変えます。

前回から今回への差分

前回のGit管理ブログへ安全に書き込むcronでは、clean 確認、fast-forward 同期、品質検査、push までを組み立てました。今回は、その push の先にあるレビューと公開の境界を扱います。

前回完了時今回完了後
検証済みの記事をGitへ保存します保存先をcontent branchに限定します
生成処理の成功を確認しますdraft: trueとPRのレビューを別々に確認します
pushで処理が終わります公開対象の1ファイルだけを別操作でdraft: falseへ変えます

すでに Astro の Content Collections でdraftを運用している場合は、「実際の朝稿ブランチを確認する」まで飛ばせます。

なぜapprovedをfrontmatterへ足さないのか

記事ファイルが持つ状態は、公開対象かどうかの 1 つだけです。hibanas-net ではdraftの boolean で表します。

レビューの承認は性質が違います。承認は特定の commit 差分に対する記録で、GitHub の PR に残ります。frontmatter へapproved: trueを足すと、誰がどの commit をいつ確認したのかが 1 行へ潰れます。承認後に記事を直せば、その 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から分ける

朝稿は、clean な main から専用 branch を切るところから始まります。PR #24 で実行したコマンドです。

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が 1 行でも返したら止めます。自動で stash、reset、clean は実行しません。main の同期に失敗した場合も記事を書きません。

branch を作ったら、現在地を確認します。

git branch --show-current

期待する出力です。

content/2026-08-18-ai-agent-draft-approval-publish-gates

対象を記事とOGの2ファイルに絞る

朝稿で変更してよいのは、記事と 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}'が期待どおり展開されないと指摘されたためです。brace 展開は、引用符の中では働きません。

5つの品質ゲートを実行する

コマンドは、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の記事は公開対象へ入りません。

PR #24でレビューへ渡した内容

対象 2 ファイルだけを stage します。commit メッセージも、PR #24 に記録された実物です。

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

レビューで直したのは 2 点です。Git pathspec の誤りと、ガイドラインに合わない自己言及です。OG は 1200×675 ピクセルで、記事タイトルは焼き込んでいません。修正後もdraft: trueを維持しました。

レビュー自体にも品質バーを持たせる

レビュー役の OpenCode workflow そのものにも手を入れました。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 が required として扱うのは、です・ます調、連載の差分、Why before How、用語表、テキスト構成図、実行可能なコマンド、正直な出力、飛ばせる章、エラー表、一次情報の出典表です。生成した手順画面と、文字入り 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、本文のどれかが同時に変わっていたら止めます。draft の変更と改稿を混ぜないためです。その後、品質ゲート 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行以外の差分があれば止めます

存在しない仕組みを前提にしない

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 つです。存在しない仕組みで図を完成させず、確認できたこの 4 つで fail closed に倒します。

出典

一次情報確認した内容
Astro Docs: Content collectionsschemaとentryの扱い
GitHub Docs: Pull requestscommit差分に対するレビュー履歴
この記事をシェア