Documentation
Guides

Gated merges on your own server

Smeldr can list the Decisions in force for the files a pull request touches, put the change in front of the right approver, and, in an Area that requires approval, report a status that GitHub turns into a hold until that approver answers. The approver can be a person or an AI agent. Smeldr does not read your code against the text of the Decisions: the approver does. This page sets it up on a self-hosted Smeldr server using environment variables only. You write no Go.

Who blocks what. GitHub enforces the block. Smeldr only reports a verdict, as a commit status named smeldr/check. If you do not make that status required in GitHub's branch protection (step 6), a pull request can still be merged whatever Smeldr says.

What you need

  • A Smeldr server built from example/server of core v1.176.0 or later, reachable from GitHub at a public https address. That address is your BASE_URL. (v1.175.0 has the adapter, but only v1.176.0 creates the tables that deliver its status updates. On v1.175.0 a Check's status never changes after the first one.)
  • These features turned on: ENABLE_TOKENS, ENABLE_GOVERNANCE, ENABLE_RELATIONS, ENABLE_WEBHOOKS, and ENABLE_GOVERNANCE_RECORDS (or ENABLE_ORCHESTRATION).
  • An admin token for the server, to run the MCP or CLI steps below.

Do the steps in this order. The server refuses to start with ENABLE_GITHUB set until the token in step 2 exists and holds its grant.

1. Create the GitHub App

In GitHub, under your organization's or your own settings, create a new GitHub App:

  • Webhook URL: https://<your BASE_URL host>/github/webhook
  • Webhook secret: a long random string. You will set it as GITHUB_WEBHOOK_SECRET.
  • Repository permissions:
    • Pull requests: read and write (Smeldr's comment on a pull request needs write)
    • Commit statuses: read and write
    • Issues: read and write (Smeldr posts one comment per pull request and keeps it up to date)
    • Metadata: read
  • Subscribe to events: Pull request.

If you widen the App's permissions later, each installation must accept the change in its settings before it applies; until then GitHub refuses the call (403).

Then generate a private key for the App (GitHub downloads a .pem file), note the App ID, and install the App on the repositories you want checked.

2. Create the adapter's token

The adapter acts inside your server as a token of its own, so every check it makes is recorded under that token's name. Create it with class job, and grant it editor:

create_token  name: github-adapter, role: editor, actor_class: job, expires_in_days: 365, reason: "GitHub adapter for gated merges"
grant_role    token_id: <the token_id create_token returned>, role: editor

With the CLI: smeldr-cli token create github-adapter editor 365 --class job --reason "GitHub adapter for gated merges", then smeldr-cli grant <token_id> editor. When the token expires the adapter stops, so note the date and replace it in time.

Keep the token_id. Discard the token itself: the adapter never sends it anywhere, it only acts in process as that token's identity. The editor grant is broader than the adapter needs, which is acceptable because nothing outside the server can use that identity once the plaintext is gone.

3. Set the environment variables

VariableValue
ENABLE_GITHUB1
GITHUB_APP_IDthe App ID
GITHUB_APP_PRIVATE_KEY_FILEthe path to the .pem file (or put the key itself in GITHUB_APP_PRIVATE_KEY, with \n for each line break)
GITHUB_WEBHOOK_SECRETthe webhook secret from step 1
GITHUB_ADAPTER_TOKEN_IDthe token_id from step 2

Optional:

  • GITHUB_NAMES_SHOWN: a comma-separated list of owner/name repositories whose comments may show Area names and Decision titles. By default a comment shows addresses and states only, because anyone who can read the pull request can read the comment. Turn it on only for a repository whose readers may all see those names.
  • GITHUB_CHECK_URL, GITHUB_ITEM_URL: link formats, for example https://example.com/items/%s, if you have a page that shows a Check or an item.

Restart the server. On the first start it registers its own core webhook for check changes at BASE_URL/github/core-events; you copy no secret for it. If a value is missing or wrong, the server does not start and its error names the variable. It never prints the value.

4. Say which files need approval

Over MCP, as an admin:

  1. Create an Area for the part of the codebase that needs approval, with the approval mode you want:
    • comment-only: the pull request gets a comment and nothing else.
    • requires-approval: the merge waits until an approver approves.
    • audit-after-merge: the merge is not held; a merge without approval opens an audit item in Smeldr.

create_area name: Billing, approval_mode: requires-approval 2. Place the Area in a Domain. An Area is always part of exactly one Domain. Use an existing Domain, or create one, then relate the Area to it with the raw IDs of both:

create_domain name: Payments assert_relation source_type: Area, source_id: <the Area's ID>, target_type: Domain, target_id: <the Domain's ID>, relation_kind: belongs_to_domain 3. Give someone the right to approve for it. The approver can be a person or an AI agent:

grant_role token_id: <the approver's token_id>, role: area-approver, scope_anchor_id: <the Area's ID>

With the CLI: smeldr-cli grant <token_id> area-approver --anchor <the Area's ID>.

An approver token of class agent or job can hold area-approver only when a human token that itself holds approve-change over that Area grants it. 4. Say which files belong to it. Creating a coverage rule needs approve-change over the Area, so the token that creates it must hold area-approver on that Area too. If that is you, grant it to yourself in step 3 first.

create_coverage_rule area: <the Area's address>, repository: owner/name, pattern: internal/billing/**

The comment on a pull request lists the Decisions in force that are placed in the Area, its Domain or the whole organization. On a fresh instance with no Decisions placed there, it lists none. That is normal, not an error.

5. Open a pull request

When a pull request touches covered files, Smeldr posts a comment naming the Check (for example CHK5) and sets the smeldr/check status:

  • pending while it waits for approval,
  • success when nothing blocks the merge, or when nothing it touches is covered,
  • failure when an approver said no.

6. Make the check required

In the repository's branch protection rule (or ruleset) for your default branch, require the status check smeldr/check. This is the step that actually blocks a merge: GitHub enforces it, Smeldr only reports.

7. Approve or decline

The approver answers for their Area:

answer_change_check  check: CHK5, area: <the Area's address>, approve: true
answer_change_check  check: CHK5, area: <the Area's address>, approve: false, reason: "needs the migration first"

With the CLI: smeldr-cli check answer CHK5 <area> --approve, or smeldr-cli check answer CHK5 <area> --not-approved --reason "...". From smeldr-cli v0.33.1 the flags may come before or after the Check and the Area; on v0.32.0 they must come first. The status and the comment on the pull request update within seconds.

When Smeldr cannot be reached

If GitHub cannot reach your server, the pull request gets no smeldr/check status. Because the check is required, the merge keeps waiting. Once the server is back, redeliver the webhook from the GitHub App's "Advanced" page, or push a new commit. Smeldr's check is idempotent: a redelivered event finds the same Check.