{"slug": "the-pipeline-that-writes-this-blog", "title": "The pipeline that writes this blog", "summary": "Russell Jones's blog now runs a fully automated GitHub Actions workflow, Content Autopilot, that mines project activity, uses Claude Code headless in CI to curate and write Hugo posts, and publishes them without human review. The pipeline throttles based on Buffer queue capacity, enforces a strict curation gate, and verifies any code snippets against the real source before publishing.", "body_md": "Ahnii!\n\nThis post is a little unusual: it was drafted by the same pipeline it describes. This blog runs a GitHub Actions workflow, [Content Autopilot](https://github.com/jonesrussell/blog/blob/main/.github/workflows/content-autopilot.yml), that mines project activity, curates it with [Claude Code](https://claude.com/product/claude-code) running headless in CI, writes a Hugo post, and publishes it to `main`\n\nwithout a human in the loop. This post covers how the throttle, the curation gate, and the publish flow fit together.\n\nZero-touch pipelines need a rate limiter or they'll flood every channel the moment there's something to say. Instead of a fixed schedule, this one throttles on distribution capacity: Buffer's free tier caps a channel at 10 scheduled posts, so `scripts/buffer-queue-depth.mjs`\n\nchecks each channel's queue depth before anything else runs.\n\n``` js\nconst CAP = Number(process.argv[2] || 9);\n// ...\nconst near = hasNext || count >= CAP;\nif (near) atCap = true;\n```\n\nIf any channel is at or near the cap, the script exits non-zero and the workflow stops before spending a single Claude token. Quiet weeks produce less; weeks where Buffer drains fast produce more. The queue itself decides the pace.\n\nOnce there's room to publish, the workflow pulls open `stage:mined`\n\nissues from a separate tracking repo — one issue per interesting thing that happened across other projects. Not every mined item is worth a post. A lot of them are per-directory scaffolding noise.\n\nRather than hand-curate, the workflow hands the candidate list to Claude with an explicit bar to clear:\n\n```\nSTEP 1 - CURATE (be ruthless). Pick AT MOST ONE candidate that would make\na genuinely useful, self-contained post a reader would value. Reject thin\nper-directory dev-scaffolding items (e.g. \"packages/field work\"). If\nNOTHING clears that bar, write {\"status\":\"NONE\",\"reason\":\"...\"} to\nautopilot-result.json and STOP.\n```\n\nIf nothing qualifies, the run ends with no post and no distribution. The gate is \"good enough to publish,\" not \"something to publish\" — a distinction that matters once nobody is reading the draft before it goes live.\n\nThe riskiest part of an unsupervised pipeline is a model confidently inventing a function signature or a config flag that doesn't exist. The prompt closes that door explicitly:\n\n```\nSTEP 2 - VERIFY. If the post would contain code, config, or interface\nsignatures, verify each against the real source repo using:\ngh api repos/<owner>/<repo>/contents/<path> (GH_TOKEN is set). If you\ncannot verify a snippet, DO NOT invent it. Either write the post without\nit, or if the post depends on it write\n{\"status\":\"HOLD\",\"issue\":N,\"reason\":\"unverifiable code\"} to\nautopilot-result.json and STOP.\n```\n\nClaude reads the actual source file through the GitHub API before quoting it. If it can't verify something the post depends on, it holds rather than guesses. This is the same rule the project's `CLAUDE.md`\n\nalready enforces for human-written posts — the autopilot just has to follow it without anyone checking its homework afterward.\n\nOnce Claude writes the post and a companion `docs/social/<slug>.md`\n\nfile, the workflow runs `hugo --gc --minify`\n\n. If the build fails, the post never gets committed — a broken page bundle or bad frontmatter simply stops the run. Only after a clean build does the workflow commit, `git pull --rebase`\n\n, and push to `main`\n\n, which triggers the existing deploy workflow.\n\n```\n- name: Build gate\n  if: steps.result.outputs.status == 'PUBLISH'\n  run: hugo --gc --minify\n\n- name: Publish\n  if: steps.result.outputs.status == 'PUBLISH' && github.event.inputs.dry_run != 'true'\n  run: |\n    git add \"${{ steps.result.outputs.post_path }}\" \"${{ steps.result.outputs.social_path }}\" data/autopilot-ledger.json\n    git commit -m \"content: autopilot publish ${{ steps.result.outputs.slug }} (queue #${{ steps.result.outputs.issue }})\"\n    git pull --rebase origin main\n    git push origin main\n```\n\nA JSON ledger (`data/autopilot-ledger.json`\n\n) records which mined issue numbers have already been processed, so a rerun never republishes the same source.\n\nThe last step reads the same `docs/social/<slug>.md`\n\nfile and queues one post per platform through Buffer's GraphQL API:\n\n``` js\n// Parse \"## Platform\" sections out of the markdown.\nconst parsed = {};\nfor (const part of md.split(/^##[ \\t]+/m).slice(1)) {\n  const nl = part.indexOf(\"\\n\");\n  const name = (nl === -1 ? part : part.slice(0, nl)).trim().toLowerCase();\n  parsed[name] = (nl === -1 ? \"\" : part.slice(nl + 1)).trim();\n}\n```\n\nEach platform's body is queued with `createPost`\n\n, and Bluesky posts over 300 characters are rejected before they ever reach the API. The originating tracking issue is then closed, so the same idea can't get mined and drafted twice.\n\nNothing here is exotic on its own — a cron trigger, an LLM call, a build step, a git push. What makes it safe to run unsupervised is that every stage can say no: the throttle can refuse to run, the curator can refuse to pick anything, the verifier can refuse to invent code, and the build gate can refuse a broken post. Zero-touch doesn't mean zero judgment; it means the judgment has to be encoded into the pipeline instead of applied by a person reading a draft.\n\nBaamaapii", "url": "https://wpnews.pro/news/the-pipeline-that-writes-this-blog", "canonical_source": "https://dev.to/jonesrussell/the-pipeline-that-writes-this-blog-5h9j", "published_at": "2026-08-16 21:44:02+00:00", "updated_at": "2026-08-16 22:12:20.598218+00:00", "lang": "en", "topics": ["developer-tools", "ai-agents", "mlops", "generative-ai"], "entities": ["GitHub Actions", "Claude Code", "Buffer", "Hugo", "Russell Jones"], "alternates": {"html": "https://wpnews.pro/news/the-pipeline-that-writes-this-blog", "markdown": "https://wpnews.pro/news/the-pipeline-that-writes-this-blog.md", "text": "https://wpnews.pro/news/the-pipeline-that-writes-this-blog.txt", "jsonld": "https://wpnews.pro/news/the-pipeline-that-writes-this-blog.jsonld"}}