Journal

How a save becomes a deploy

Following one click of the Save button through the Git Data API, a webhook, a tarball, and an atomic pointer swap.

You click Save in the editor. A few seconds later, every replica is serving the new page. Here’s everything that happens in between.

1. Validate before committing

The form carries the base SHA it was loaded from. Before anything touches GitHub, trunkcms runs a validation build: the same load-and-render pass as a real build, against an overlay of the current snapshot plus your pending changes. Nothing is written to disk. If your front matter has a bad date, or a settings change selects a theme that doesn’t exist, you find out here, not in production.

2. One atomic commit

Writes go through the Git Data API rather than the Contents API, so one save can include the post, its staged images, and anything else, all together:

  1. POST /git/blobs for each binary file
  2. POST /git/trees with the base commit’s tree
  3. POST /git/commits with the base SHA as parent
  4. PATCH /git/refs/heads/main with force: false

The last step is the concurrency check. If someone else’s commit landed first, the ref update isn’t a fast-forward and fails. If their commit touched different files, trunkcms rebuilds the tree on the new head and retries once. Otherwise you get a conflict screen instead of a silent overwrite.

The commit is made with the App’s installation token, with you as the git author. GitHub links your noreply address to your account, so history shows your avatar.

3. Everyone hears about it

The replica that made the commit syncs to it immediately, so you see your change right away. GitHub fires a push webhook at whichever replica it reaches. Every other replica notices on its next poll, a conditional request that returns 304 Not Modified when nothing changed and doesn’t count against the rate limit.

4. Rebuild and swap

Each instance streams the tarball for the new SHA to disk and renders the whole site: posts, pages, tags, authors, feeds, sitemap. Output goes into a content-addressed store, so pages that didn’t change aren’t written again.

Then one atomic.Pointer swap, and the new site is live. Requests already in progress keep the previous build’s files, which aren’t cleaned up until the build after.

If the build fails, there’s no swap. The last good site keeps serving and the error shows up on the admin dashboard.

The admin UI is just one convenient way to make commits.

A git push from your laptop goes through steps 3 and 4 the same way.