Inspiration

On Bitbucket, the pull requests that merge themselves are the ones nobody reads: Dependabot bumps, agent-authored branches, infra tweaks. Branch permissions can require a reviewer, but they cannot say "a patch bump may merge alone, a major bump needs a human, and nobody touches the Dockerfile without one". We wanted a rule set that lives next to the code, is read from the branch being merged into (so a pull request cannot loosen the rules it is judged by), and explains every decision with a rule number instead of a red X. Build with context: the context here is the policy file, the diff, the reviewers and the linked Jira work item, all read at evaluation time.

What it does

Merge Policy Gate is a Forge custom merge check for Bitbucket Cloud. It reads MERGE-POLICY.md from the destination branch and decides pass or held, citing the rules it relied on:

  • §2.1 protected paths (CI config, Dockerfiles, the policy file) need a human approval;
  • §2.2 dependency files may change automatically only for patch or minor bumps; majors and added or removed packages wait for a human (the gate diffs package.json, requirements.txt, pyproject.toml, go.mod, Cargo.toml between destination and source commits);
  • §3.4 size limits; §3.5 drafts never merge; §3.6 defines a human approval (an Approved review by a non-author);
  • §4.1 the linked Jira work item key must be in the branch or title when required, optionally in an allowed status.

The verdict shows in three places: the merge-checks card (held: §2.1 …; §2.2 … rules=[…] policy=MERGE-POLICY.md), a pull-request sidebar card (UI Kit) with one row per rule, HELD/OK, the reason and the unblock step, and a repository settings page that shows whether the policy file was found, the parsed rules, the starter template and a Dry-run button for every open pull request. The check re-runs on pushes, reviewer status changes and merges, so an approval releases §2.1/§2.2 without a new push.

The same evaluator is exposed through rovo:mcp as four tools (evaluate-pull-request, last-verdict, explain-rule, policy-template) and a rovo:agent, so a coding agent can ask the gate before it pushes. An open-source skills/merge-policy-gate/SKILL.md tells Claude Code or any MCP client when to call which tool and how to plan a change that passes. A read-only web trigger (the verdict feed) publishes verdicts per pull request and per Jira key, which the companion Jira app Merge Gate Explainer (a separate submission) and a Jira Automation template consume.

How we built it

Forge, Node 24, UI Kit. Modules: bitbucket:mergeCheck (on-code-pushed, on-reviewer-status-changed, on-merge), bitbucket:repoPullRequestCard, bitbucket:repoSettingsMenuPage, rovo:agent, rovo:mcp, action ×4, webtrigger, resolver functions via @forge/resolver. Policy parsing and every rule live in src/policy.js with unit tests (node --test). Verdicts are stored in Forge KVS keyed by repository slug and pull request, plus an index by Jira key. Scopes: read:pullrequest:bitbucket, read:repository:bitbucket, storage:app. The app never writes to the repository and makes no calls outside Atlassian.

Challenges we ran into

  • The merge-check event carries UUIDs, the agent and MCP callers pass slugs, and the sidebar card only sees the repository UUID without a workspace scope; everything is normalised to the slug path and the card parses the slugs from its page URL.
  • @forge/resolver is not a constructor under ESM bundling (makeResolver is).
  • Bitbucket's Markdown renderer collapses a fence whose info string has a space, so the policy block is marked with a first-line # merge-policy comment and the parser accepts both spellings.
  • Required checks are Premium-only on Bitbucket; on Free/Standard the gate is advisory, so the UI says so instead of pretending.

Accomplishments that we're proud of

A live drill on a public repository (hanvigo-dev/gate-demo): PR 2 (Dockerfile + httpx major) held under §2.1/§2.2, PR 3 (520 lines) held under §3.4, PR 1 evaluated on merge and allowed, dry-runs from the settings page, and a coding agent calling the same evaluator over MCP. Everything the judge sees is one evaluator, one stored verdict, one rule numbering.

What we learned

Forge apps cannot hold Bitbucket and Jira scopes at once, which forced a clean two-app design with a documented read-only contract between them. Policy-as-code only works if the policy is read from the destination branch.

What's next for Merge Policy Gate

Marketplace listing (free), per-path reviewer groups, a verdict history view, and support for Bitbucket's required checks on Premium workspaces with a "why required" explanation.

Try it

Judge guide with the 10-minute path and installation steps: JUDGES.md in the repository. Demo pull requests: https://bitbucket.org/hanvigo-dev/gate-demo/pull-requests/2 (held), /3 (held §3.4), /1 (merged, pass).

Installation link (production, Bitbucket): https://developer.atlassian.com/console/install/d6c929d9-996c-4661-a715-c6dc155b06e3?signature=AYABeHOebPd9E7jo6CI047aQm7wAAAADAAdhd3Mta21zAEthcm46YXdzOmttczp1cy13ZXN0LTI6NzA5NTg3ODM1MjQzOmtleS83MDVlZDY3MC1mNTdjLTQxYjUtOWY5Yi1lM2YyZGNjMTQ2ZTcAuAECAQB4CxqLTo9l4lqXuFjNim1xJM%2Bp%2F3TVP9xHS2BrU5DRRucBnl9XWlV3hGc9ThHSmoJXhAAAAH4wfAYJKoZIhvcNAQcGoG8wbQIBADBoBgkqhkiG9w0BBwEwHgYJYIZIAWUDBAEuMBEEDBFi11JPZvdsLd5uaQIBEIA7bgA6H3UVPzlQTFPh8oiOseLt4IKm1NrCWlA0tj0TSoNuiULVBmyv%2BX1UDsNdSZEr1HVchswTeMMrl%2BIAB2F3cy1rbXMAS2Fybjphd3M6a21zOmV1LXdlc3QtMTo3MDk1ODc4MzUyNDM6a2V5LzQ2MzBjZTZiLTAwYzMtNGRlMi04NzdiLTYyN2UyMDYwZTVjYwC4AQICAHi9bVMAkme4qVQX1fQHo6ePw3H40A7cGfz7yrmSfBWMXgHNn5p7p1hGBYy1P5F0x2aHAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMb%2FjOrdSJxe6K51HQAgEQgDvNgeT3BpnUOF5IDFpsWcdBd2q730%2BLntee4Jn3gg8fHiVTNzZ683W2x9hB3cIQSDV4aVDMuG6YnyV5MQAHYXdzLWttcwBLYXJuOmF3czprbXM6dXMtZWFzdC0xOjcwOTU4NzgzNTI0MzprZXkvNmMxMjBiYTAtNGNkNS00OTg1LWI4MmUtNDBhMDQ5NTJjYzU3ALgBAgIAeDYASx0XCjkny4iLV6vz%2BKtEGSGT6kdt0QRpkn6D%2FlVEAY5Dz3Un8NQ5KaKilMq4riEAAAB%2BMHwGCSqGSIb3DQEHBqBvMG0CAQAwaAYJKoZIhvcNAQcBMB4GCWCGSAFlAwQBLjARBAyt4Ji9EPQNXgcJpnMCARCAO1Cku4V4Oqf2phiY30FLwVZ3itFzy681DJW2Wx8epC4eZFafBAj7l7he3tEigRpdzA2ilFdX2iblCsOpAgAAAAAMAAAQAAAAAAAAAAAAAAAAABIj0CCefgMRqcFJJ7WeQTf%2F%2F%2F%2F%2FAAAAAQAAAAAAAAAAAAAAAQAAADLxBQHMpnDbIPQsMRJDLTSE116oIYM2BZXnLg7y%2BTaDNJLuPBITq4SZyRsA3sO8WAxvfSFJ6wn%2FRHmg1GohcEZsxiA%3D&product=bitbucket

Share this project:

Updates

Submission history