Skip to content

auto-author

auto-author #86

Workflow file for this run

name: auto-author
# Daily schedule enabled after draft quality was verified across two batches
# (docs/auto-content-cron-plan.md §7). The cron only DRAFTS + opens a PR — it
# never writes to Firestore. Publishing stays a human step (merge + seed), so a
# scheduled run cannot push unreviewed content into the Google-indexed corpus.
#
# The one thing it DOES write to main directly is content/backlog.ts, flipping
# the topics it drafted `pending` → `drafted`. That is the queue pointer, not
# content, and it has to land immediately: the next run checks out main, so a
# mark that waits for review advances nothing and the drafter re-drafts the same
# head of the queue forever. (It did exactly that from 2026-07-11 to 2026-08-21
# — 42 daily PRs, all three identical topics, ~250 wasted generations.)
# `content/backlog.ts` is in no workflow's trigger paths, so that push loops
# nothing.
on:
workflow_dispatch:
inputs:
limit:
description: 'Max topics to draft this run (hard cap 5)'
default: '3'
schedule:
- cron: '17 3 * * *' # 03:17 UTC daily; scheduled runs use the default limit (3)
permissions:
contents: write
pull-requests: write
# Never let two runs race on the backlog pointer or push to main together.
concurrency: auto-author
env:
# Backpressure: unmerged drafts PRs mean nobody is publishing, so drafting
# more just burns Anthropic credit. Above this many open auto-draft PRs the
# run skips before it makes a single model call.
MAX_OPEN_DRAFT_PRS: '5'
jobs:
draft:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
# Matches CI: --ignore-scripts skips husky's `prepare`, which has no job here.
- run: npm ci --ignore-scripts
- name: Backpressure — skip if drafts PRs are piling up
id: backpressure
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
OPEN=$(gh pr list --state open --limit 200 \
--json headRefName --jq '[.[]|select(.headRefName|startswith("auto-draft/"))]|length')
echo "open drafts PRs: $OPEN (max ${MAX_OPEN_DRAFT_PRS})"
if [ "$OPEN" -ge "$MAX_OPEN_DRAFT_PRS" ]; then
echo "go=false" >> "$GITHUB_OUTPUT"
echo "::warning::auto-author skipped — $OPEN open auto-draft PRs (>= $MAX_OPEN_DRAFT_PRS). Merge or close them; drafting more would only burn API credit."
else
echo "go=true" >> "$GITHUB_OUTPUT"
fi
- name: Generate drafts
if: steps.backpressure.outputs.go == 'true'
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
# Writes content/auto-draft.en.ts (the reviewable carrier) AND advances
# content/backlog.ts. The two are committed to different places below.
run: npx tsx scripts/auto-author.ts --apply --limit=${{ github.event.inputs.limit || '3' }}
- name: Stash the backlog advance
if: steps.backpressure.outputs.go == 'true'
run: |
if git diff --quiet -- content/backlog.ts; then
echo "advanced=false" >> "$GITHUB_ENV"
else
cp content/backlog.ts "$RUNNER_TEMP/backlog.ts"
echo "advanced=true" >> "$GITHUB_ENV"
fi
git checkout -- content/backlog.ts
# CI cannot run on the PR this job opens: GitHub suppresses workflows on
# PRs created with GITHUB_TOKEN (the runs land as `action_required` and
# never execute), which is why 42 drafts PRs sat with zero checks. Running
# the same three gates here is what actually protects main — a malformed
# carrier would otherwise only fail typecheck AFTER the merge.
# Upgrade path: create the PR with a PAT/GitHub App token instead, and
# ci.yml will run on it normally.
- name: Gates (typecheck / lint / test)
if: steps.backpressure.outputs.go == 'true'
run: |
npm run typecheck
npm run lint
npm test
- name: Open PR if drafts changed
id: pr
if: steps.backpressure.outputs.go == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if git diff --quiet -- content/auto-draft.en.ts; then
echo "No new drafts produced — nothing to open."
echo "opened=false" >> "$GITHUB_OUTPUT"
exit 0
fi
BRANCH="auto-draft/$(date +%Y%m%d-%H%M%S)"
git config user.name "gptwiki-bot"
git config user.email "bot@gptwiki.net"
git checkout -b "$BRANCH"
git add content/auto-draft.en.ts
# --no-verify: skip the husky lint-staged hook rewriting generated data.
git commit --no-verify -m "auto-author: draft batch (review before seeding)"
git push -u origin "$BRANCH"
gh pr create \
--title "Auto-author drafts (en+zh) — review before seeding" \
--body "Generated by \`scripts/auto-author.ts\` — each topic as en+zh with a shared image prompt. Typecheck/lint/test ran in the drafting job (GitHub suppresses CI on \`GITHUB_TOKEN\`-authored PRs). **Review facts + sources**, then merge — \`auto-seed\` publishes en+zh + one hero image per topic automatically. Rationale + guardrails: docs/auto-content-cron-plan.md."
echo "opened=true" >> "$GITHUB_OUTPUT"
# Only after the PR exists, so a failure above leaves the topics `pending`
# to be retried tomorrow rather than silently consumed.
- name: Advance the backlog on main
if: steps.backpressure.outputs.go == 'true' && steps.pr.outputs.opened == 'true' && env.advanced == 'true'
run: |
git config user.name "gptwiki-bot"
git config user.email "bot@gptwiki.net"
git checkout main
git pull --rebase origin main
cp "$RUNNER_TEMP/backlog.ts" content/backlog.ts
if git diff --quiet -- content/backlog.ts; then
echo "backlog already advanced upstream — nothing to push."; exit 0
fi
git commit --no-verify -m "backlog: mark drafted topics [skip ci]" -- content/backlog.ts
git push origin main