Reviewing entries

The publication workflow reviews a whole branch at once: one pull request, one approval, everything goes live together. That’s fine until your team wants to sign off on pages one at a time – legal reads the pricing page, the editor reads the blog post, and nobody wants to approve a diff full of other people’s work.

Entry review handles that case. Each entry carries its own review state, a reviewer reads and approves it in Emet, and the publish dialog won’t let anything go live until every changed entry has been approved.

Turning it on#

It’s off by default. Add a review block to admin/cms.yaml:

review:
  enabled: true

The other keys (which frontmatter key to use, whether to gate publishing, who can override) are on the cms.yaml page. With review on, every entry gets a Review tab in the editor’s right panel, entry cards show a review badge, and the sidebar’s section picker gains a Review board.

Note that the setup wizard’s Enable review workflow option is a different thing: it sets up the branch-and-pull-request flow on the Publication page. You can use either one, or both together.

The states#

State Meaning
Draft Nobody has asked for a review yet. Every entry starts here.
Ready for review The writer has marked it ready, and it’s waiting on a reviewer.
Changes requested A reviewer sent it back with a note.
Approved A reviewer approved it.
Changed since approval It was approved, then edited. It needs another look.

The last one isn’t stored anywhere. When an entry is approved, Emet records a hash of its content, and any later edit to the frontmatter or body makes the hash stop matching. The entry then shows as Changed since approval on its card, on the board and in the publish dialog until someone reviews it again.

The Review tab#

The Review tab is where all of this happens. From top to bottom it shows:

  • The current state, the writer and the assigned reviewer.
  • The action buttons for that state (see below).
  • A reviewer picker. On GitHub-hosted sites it lists the repository’s collaborators, minus the writer; elsewhere it’s a box for a GitHub login and an Assign button.
  • Everyone who has touched the file, from its commit history and the review log.
  • Show changes since <production branch>, the entry’s diff against production, on GitHub-hosted sites with a production branch configured.
  • A comment thread. Each comment can be anchored to one of the entry’s headings or to the Whole entry, and can be resolved and reopened.
  • The history of review actions, newest first.

Which buttons you see depends on the state:

State Buttons
Draft, Changes requested, Changed since approval Ready for review
Ready for review, Approved, Changed since approval Withdraw (the writer only)
Ready for review, Changes requested Approve
Ready for review, Approved Request changes, which needs a note in the “Note for the writer” box first

The rule underneath is that you can’t review your own work. Whoever clicked Ready for review becomes the writer: they can withdraw the entry, but Approve and Request changes are disabled for them, and they’re left out of the reviewer picker.

Every review action is a commit, so the buttons are disabled while the entry has unsaved edits (“Save your changes first: review actions commit the entry.”). Review-only commits are titled chore(review): <action> <title> and carry your skip-CI marker, so marking something ready doesn’t rebuild the site.

Getting the reviewer’s attention#

If a reviewer is already assigned when the writer clicks Ready for review, and the working branch has an open pull request, Emet also requests that person’s review on the pull request. That’s what gets them a GitHub notification email. Assigning a reviewer later doesn’t send the request, so assign first and mark ready second if you want the email to go out.

Where the state lives#

The state is a block in the entry’s own frontmatter, under review unless you’ve changed review.field:

review:
  status: ready
  writer: clara
  reviewer: leo
  events: [...]
  comments: [...]

Keeping it in the file means it’s versioned with the content, survives branch resets and needs no database. Hugo ignores the key unless a template reads it. Emet hides it from the frontmatter form and leaves it out of conflict diffs, and keeps the 20 most recent events and 50 most recent comments so the block doesn’t grow forever.

When two people change the review state of the same entry at the same moment (one approves while the other comments, say), Emet doesn’t raise a conflict. The more recent action decides the state, and the comments and events from both sides are kept.

The review board#

Pick Review board from the sidebar’s section picker for a cross-section view of everything under review. Entries are grouped in this order: Ready for review, Changes requested, Changed since approval, Approved and Draft. Only entries that have a review block appear, so an entry nobody has ever marked ready isn’t listed.

Above those groups, a Needs you group collects what’s waiting on you:

  • entries Ready for review with you as the reviewer;
  • if you’re a review admin, entries Ready for review with no reviewer, unless you wrote them;
  • entries of yours that came back with Changes requested.

The board works from the same index as global search, so it only knows about entries Emet has already listed. The line at the top says how many that is, and Index all reads the rest.

The publish gate#

With a production branch configured, the publish dialog gains a Content review panel listing every changed entry with its state. If any of them isn’t approved, it says why (Not submitted for review, Waiting for a reviewer, Reviewer requested changes or Edited after approval) and Go Live stays disabled.

Only Markdown files inside your configured sections are checked. Deleted entries, data files, images and config changes don’t need approval.

Sometimes something has to go out anyway. A review admin gets a “Reason for publishing without approval” box and a Record override and unlock Go Live button. The reason is written into each blocked entry’s review history in a single commit, chore(review): publish override by <login> -- <reason>, with the skip-CI marker. The override covers the content as it was at that moment, so the gate stays open for those entries after you close the dialog or reload, until one of them is edited again.

Review admins are the logins listed in review.admins, or the repository’s admins when that list isn’t set. Note that the list replaces repository admins rather than adding to them. Everyone else sees “Ask a repository admin to override, or get the entries approved”, which is slightly misleading when you’ve set review.admins: it’s the people on that list they need to ask.

Publishing itself doesn’t change. Go Live still merges the whole branch, so the gate is about making sure every page in it has been read, not about publishing pages one at a time.

Without GitHub#

Review works on local and desktop sites too, since the state is just frontmatter. There’s no sign-in there, though: everyone is dev-user (or desktop-user in the desktop app), so the you-can’t-review-your-own-work rule can’t tell people apart. There’s no publish dialog either, and so no gate. In practice it’s a GitHub feature.