Email me
How the Nightly Pipeline Works
← Automating Garage Dreams

How it works

How the Nightly Pipeline Works

One script selects, drafts, checks, reviews, builds and either publishes an article or leaves an explanation

The pipeline is deliberately built as one end-to-end operation. A successful draft does not mean a successful run. The article still has to pass the linting and review stages, survive a real Astro build and be committed and pushed to the repository before it counts.

At the other end, a rejected draft still needs to leave a useful record explaining what happened. I didn’t want the process to simply disappear for the night and leave me wondering whether it had failed, skipped an article or never run at all.

The complete path

The rebuilt site is completely static and built with Astro. There is no CMS or database sitting between the automation and the content files. One script controls the nightly article process from start to finish:

clean git tree?           abort if not, because a human is probably mid-edit
pick a section            400+ words, not boilerplate and not used before
draft it locally          Gemma 4 E4B running on the Raspberry Pi
lint it                   length, NZ spelling, numbers, hype and copied text
review it                 Claude and Codex, separately and in parallel
combine the verdicts      both have to pass, with one revision round allowed
build the site            Astro build is the real correctness gate
commit and push           the article and run log go into the same commit
deploy                    Cloudflare Pages builds the updated site

The clean-tree check is there to protect human work. If I am halfway through changing something, I don’t want the automated process writing files, committing my unfinished work or trying to clean it up.

The build catches a completely different type of problem. An article can be perfectly readable but still contain invalid frontmatter, a broken import or some markup that Astro cannot render. Running the real build means the pipeline checks the article in the same site it is about to join.

If the build fails, the article does not get committed or published.

Neither reviewer outranks the other

Claude and Codex review the article separately, and neither reviewer outranks the other. Both have to pass the article before it can be published. Either one can ask for changes, and either one can reject it outright.

The code that combines their verdicts is fairly simple:

export function combine(a: Verdict, b: Verdict, revisionsUsed = 0, maxRevisions = 1): Outcome {
    if (a.verdict === 'reject' || b.verdict === 'reject') return 'skip';
    if (a.verdict === 'pass' && b.verdict === 'pass') return 'publish';
    return revisionsUsed < maxRevisions ? 'revise' : 'skip';
}

If either reviewer returns reject, the article is skipped. If one or both ask for a revision, the local model gets one chance to adjust the article using their feedback. The revised version is then linted and sent back to both reviewers.

If they still do not both pass it, the run stops and the article is skipped for the night. Publishing nothing is a perfectly valid result. I would rather have the pipeline skip an article than gradually relax the checks until something gets through.

Local drafting, remote review

The initial drafting runs locally on the Raspberry Pi and costs nothing apart from the electricity needed to run it. The local model does not need an internet connection and there is no metered model API involved in writing the article.

The reviews run through the Claude and Codex CLIs using subscriptions I already pay for, rather than metered API keys. This means an unattended job cannot quietly run up a massive API bill overnight.

Once an article passes both reviewers and the Astro build, the pipeline commits the article and its run record together and pushes them to the repository. Cloudflare Pages then detects the push, builds the site and publishes the new version.

If an article is skipped, the pipeline commits and pushes the run record without an article. This means I can still see which source section it selected, what the reviewers said and why nothing was published.

The custom domain is still parked at the registrar for now, so the rebuilt site is running from its pages.dev address. However, the full path from the Raspberry Pi to the publicly deployed site is now working.