By default, every save in Emet is a commit to the branch you’re editing, and if that branch is the one your site deploys from, the change is live as soon as the build finishes. For a personal blog that’s fine. For a site with more than one editor, or one where someone needs to sign off before things go public, it’s a bit terrifying.
The publication workflow adds a step. You edit on a working branch, and when you’re ready, Emet opens a pull request to a production branch and lets you merge it once the checks pass. Your deploy pipeline watches production and nothing goes live until that merge.
Why pull requests?#
Because GitHub already has everything a review step needs: diffs, comments, approvals, required checks, branch protection. Rather than build a parallel approval system inside Emet, the workflow drives GitHub’s, and surfaces enough of it in the CMS that editors don’t need to open GitHub themselves. Anyone who does prefer GitHub’s UI can use it; the PR is a normal PR.
Configuration#
Set production_branch at the top level of admin/site.yaml:
production_branch: main
working_branch: staging
allowed_branches: [staging, main]
publish:
merge_method: squash| Key | What it does | Default |
|---|---|---|
production_branch |
The branch your site deploys from. Setting this turns the workflow on. | unset |
working_branch |
The branch editors land on after sign-in. | the repository’s default branch |
allowed_branches |
Branches offered in the branch switcher. | all branches |
publish.merge_method |
squash, merge or rebase. |
squash |
The setup wizard writes all of these if you tick the review workflow option.
The workflow is only active when the branch you’re editing differs from production_branch. Switch to the production branch and the Sync with Production button disappears, and saves go straight to production again.
Submitting changes#
Edits are committed to the working branch as you save. The header button Sync with Production shows how many commits the working branch is ahead of production, and spins while a save is on its way to GitHub.
Click it and you get the list of changed files, each expandable to show its diff, plus optional title and description fields for the PR. The title defaults to “Content updates”. Submit for Review first pushes anything the sync service still has queued, then opens the pull request.
There’s only ever one open PR from the working branch to production. Further saves on the working branch add to it, and the button changes to show the PR’s state instead of a count: Ready when it can be merged, Pending while checks run or reviews are outstanding, Changes if a reviewer has requested changes, or Review if someone else opened the PR and it’s waiting on you.
Merging#
With a PR open, the modal shows:
- A status banner: ready to go live, waiting for branch protection requirements, merge conflicts, failed checks, or branch behind base. Conflicts and a stale branch have to be sorted out on GitHub; everything else is visible here.
- The PR title, description and the changed files with their diffs. Review threads appear inline at the lines they refer to; you can start a thread on any line, reply, and resolve or unresolve. Threads refresh every 15 seconds while the modal is open.
- Branch protection rules, required reviews, status checks and GitHub Actions workflow runs, each with a link to the run on GitHub.
- Deployments, if your host reports them to GitHub. When exactly one has a preview URL, a View Staging Site button appears next to the merge button.
- General PR comments, and a box to add one (
Ctrl+Enterto post).
If you opened the PR, there’s a reviewer picker for requesting reviews from collaborators. If someone else did, you get Approve, Request Changes and Comment buttons instead, which submit a proper GitHub review.
Go Live merges the PR with the configured method. It’s disabled until GitHub says the PR is mergeable, so required checks and approvals are enforced even for people who’d rather click through them.
After the merge#
Once the merge goes through, Emet force-updates the working branch to match production and clears its local cache, so your next round of edits starts from exactly what’s live. Note that this overwrites the working branch: anything committed to it outside the PR (which shouldn’t happen if everyone’s using Emet, but might if someone pushed by hand) will be gone.
If the branch reset fails, the modal stays open and says so. Your changes are live either way; it’s just that Emet will keep showing already-published edits as unpublished until the branch is reset. There’s a Retry Reset button, or you can reset the branch on GitHub.
Comments in the editor#
When the entry you have open has review threads on the current PR, a Comments tab appears in the editor’s right panel. Threads on body lines link to the line, and in Markdown mode there’s a gutter marker with the comment count that jumps there. Threads on frontmatter lines are labelled Front matter, file-level comments File, and threads whose lines have since changed Outdated. Resolved threads are grouped separately.
Replying and resolving happen in the publish modal; the tab links straight to it.
Reviewing entries#
The pull request above reviews the whole branch. If your team signs off on pages one at a time, turn on the per-entry workflow with review.enabled: true in cms.yaml. Each entry then moves through four states:
| State | Meaning |
|---|---|
| Draft | Nobody has asked for a review. The default. |
| Ready for review | The writer marked it ready. A reviewer can be assigned, and if a pull request is open the reviewer is also requested on GitHub, which gets them an email. |
| Changes requested | A reviewer sent it back with a note. The writer edits and marks it ready again. |
| Approved | A reviewer approved it. Emet remembers a hash of the content at that moment; edit the entry afterwards and it shows as Changed since approval until it’s reviewed again. |
The Review tab in the editor’s right panel is where this happens. It shows the state, the writer and reviewer, who has touched the file (from its commit history and the review log), the entry’s diff against the production branch, a comment thread anchored to headings in the body, and the history of actions. The rule is that you can’t review your own work: whoever marked the entry ready can withdraw it but not approve it or request changes on it, and can’t be assigned as its reviewer. Review actions commit the entry, so save your edits first.
Everything is kept in a review block in the entry’s frontmatter (the key is configurable), so it lives in the repository with the content, is hidden from the frontmatter form, and is left out of conflict diffs. When two people change the review state of the same entry at the same time, Emet keeps whichever action was more recent rather than raising a conflict.
The sidebar’s section picker gains a Review board: entries grouped by state, with a Needs you group at the top for entries assigned to you or, if you’re an admin, waiting without a reviewer, plus anything sent back to you with changes requested. The board sees entries Emet has listed; Index all reads the rest.
In the publish dialog, Go Live stays disabled while any changed entry isn’t approved or was edited after its approval. Repository admins (or the logins in review.admins) can override with a reason, which is written into each affected entry’s review history in one [skip ci] commit. The override covers the content as it was at that moment: the gate stays open for those entries, even after closing the dialog or reloading, until one of them is edited again. Publishing itself is unchanged: the whole branch goes live at once.
Polling#
Emet doesn’t get webhooks, so it polls. While the publish modal is open with a PR, it checks merge readiness every 2.5 seconds while GitHub is still working out mergeability, every 4 seconds while checks or deployments are running, and every 30 seconds once things have settled. With the modal closed it checks the comparison every minute and the PR itself every 5 to 60 seconds depending on activity, so the header button stays roughly current without hammering the API.
In practice#
- Edit on the working branch. Save as normal.
- Click Sync with Production and Submit for Review.
- Wait for checks, or for a colleague to approve.
- Click Go Live.
- Your site deploys from production. The working branch is reset to match.