Skip to main content
Version: Next

0033: Merge approval is a Slack reaction from a named approver, read by polling; any rejection wins

  • Status: Accepted (backfilled 2026-10-06: records a decision already built)
  • Date: 2026-10-06
  • Related: 0011 (Orion lands work), 0015 (Orion is the gate under a merge ref)

Context​

Orion lands its own work (0011). Some teams want a person to approve a merge first. The approval has to be easy to give from a phone, impossible for Orion to give itself, and readable by a process that has no public endpoint: Orion is a CLI, often on a laptop.

Decision​

Orion posts an approval request in the project's Slack channel, mentioning the approvers, and reads the answer back on each watch pass (internal/collect/approval.go). The rules:

  • Who counts: only people listed in slack.merge_approvers. Empty means nobody, never everybody: being in a channel is not authority.
  • What counts: a reaction on the request message (approve: white_check_mark, heavy_check_mark, +1, shipit, rocket; reject: x, no_entry, no_entry_sign, -1, hand), or a reply in its thread with an approving or rejecting word. Both are accepted because a phone user reacts and a laptop user types.
  • The bot's own reactions are excluded, so Orion cannot approve its own request.
  • A rejection beats every approval.
  • Polled, not pushed. Each pass reads the message; there is no listener.

When it applies: with batch integration, a green batch waits for approval only when slack.merge_approvers names people; otherwise it lands. Without batch integration, slack.require_approval turns the gate on for each ticket's pull request; with it off, Orion reports that checks pass and waits for a person to merge on GitHub.

The same reaction is how a recommendation becomes a decision (internal/decide): one approval mechanism, not two that drift apart.

Consequences​

  • Approving is one tap in Slack; nothing new to install or secure.
  • An approval arrives on the next pass, not instantly.
  • Reading reactions needs the reactions:read scope, and thread replies channels:history or groups:history.

Alternatives rejected​

  • A Slack app with a listener (socket mode). A long-running process with tokens to protect, a lot of attack surface for a single-user supervisor.
  • Approval on GitHub. Fine for people at a desk, but it does not reach the phone, and it duplicates what branch protection already does.
  • Anyone in the channel may approve. Channel membership is not authority.