承認は「生成前」ではなく「固定された生成物の後」に置く
まず生成 workflow が投稿候補を PR または Issue に保存する。人間は URL、表記、画像、文字数を確認し、承認後にその候補の immutable な ID と本文ハッシュを記録する。投稿 workflow は任意の文章や記事パスを受け取らず、承認済み ID だけを入力にする。さらに GitHub Environment の required reviewers を投稿 job に設定すると、実行直前にも承認を求められる。
# generate-post.yml(抜粋)
on: { workflow_dispatch: { inputs: { article_path: { required: true, type: string } } } }
jobs:
generate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: node tools/sns/generate-candidate.mjs "$ARTICLE_PATH"
env: { ARTICLE_PATH: ${{ inputs.article_path }} }
# 候補本文・source commit・sha256を PR/Issue または署名済みartifact に保存する。
# post-approved.yml(抜粋)
on: { workflow_dispatch: { inputs: { candidate_id: { required: true, type: string } } } }
jobs:
post:
environment: x-production # repository 側で required reviewers を設定
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Post the reviewed, immutable candidate
env:
CANDIDATE_ID: ${{ inputs.candidate_id }}
X_ACCESS_TOKEN: ${{ secrets.X_ACCESS_TOKEN }}
run: node tools/sns/post-approved.mjs "$CANDIDATE_ID"
post-approved.mjs は候補レコードを読み、approved 状態、本文ハッシュ、許可された保存先、未投稿状態を検証してから送信する。投稿前に候補を排他的にclaimし、API応答のpost IDを保存します。ただしX側の投稿とローカル記録は同一トランザクションにできません。応答喪失や保存前クラッシュでは結果が不明になるため、自動で再投稿せず確認待ちへ戻します。単に保存を原子的にするだけで二重投稿を防げるという説明は訂正します。
frontmatter とパスを安全に扱う
frontmatter を正規表現で読むと、引用符、複数行、tags 配列、コロンで壊れる。yaml などの YAML パーサーを package.json と lockfile に固定して使い、必要なフィールドの型を検証する。記事パスは解決後の絶対パスが許可ディレクトリ配下で、拡張子が .md または .mdx であることを確認する。workflow input を shell コマンドへ直接展開しない。
import path from 'node:path';
import { realpathSync } from 'node:fs';
const postsRoot = realpathSync(path.resolve(process.cwd(), 'content/posts')) + path.sep;
const requested = process.env.ARTICLE_PATH ?? '';
const article = realpathSync(path.resolve(postsRoot, requested)); // symlink経由の逸脱も確認
if (!article.startsWith(postsRoot) || !['.md', '.mdx'].includes(path.extname(article))) {
throw new Error('article path is outside the approved directory');
}
文字数は投稿直前に weighted length を検証する
日本語を単純に2文字として切り詰める実装は、URL 短縮や絵文字などを正しく扱えない。X の公式仕様に沿った weighted character 計算を採用し、title、tags、custom text、短縮後 URL を結合した最終本文で投稿直前に検証する。上限を超えた候補は自動投稿せず、切り詰めた案を再レビューに戻す。計算ライブラリと URL 変換の仕様は更新されるため、テスト入力と依存版を lockfile に残す。
料金と運用記録
GitHub Actions の実行料金と X API の投稿・読み取り料金は別である。「無料」と断定せず、確認日、利用プラン、想定リクエスト数、超過時の挙動を運用文書に記録する。この記事では現行単価を固定掲載しない。変更があり得るため、導入判断時に公式の料金表で確認する。



