Diffinery
Collaboration: Pull requests
Docs / Collaboration

Pull requests

Open, review, check, update and merge pull requests in the web UI, the CLI, the REST API and over MCP, including intent, drafts, blockers and auto-merge.

A pull request proposes merging the commits on one branch (the head) into another branch of the same repository (the base). Use it to get changes reviewed, run CI on them, and merge them under the base branch's protection rules, whether the author is a person or an AI agent.

How pull requests work

  • Same repository. The head and base are branches of the same repository. In the API, head may be written owner:branch; the part before the colon is ignored.
  • Shared numbering. Pull requests and issues share one number sequence per repository, like on GitHub. /acme/website/pull/12 and /acme/website/issues/12 redirect to each other depending on what #12 is, and /acme/website/pulls/12 redirects to /acme/website/pull/12.
  • One open pull request per branch pair. You can't open a second pull request for the same head and base while one is open.
  • The head moves with the branch. Pushing to the head branch updates the pull request (a synchronize event), re-runs its pull_request workflows, and, if the base branch's rule has Dismiss stale approvals when new commits are pushed, dismisses earlier approvals. A push by anyone other than the person or agent who turned on auto-merge turns it off.
  • Intent. Each pull request can carry an intent: a short statement of what the change is meant to do, shown above the conversation. See Intent.
  • Auto-merge. Instead of waiting to click merge, you can turn on auto-merge: Diffinery merges the pull request by itself once every rule of its base branch is met. See Auto-merge.
  • Agents. Pull requests opened by agents always need at least one human approval. See Pull requests opened by agents.

Permissions

ActionWho can do it
View pull requestsAnyone who can read the repository
Open a pull requestWrite or higher
Comment and leave a Comment reviewAnyone signed in who can read the repository
Approve or request changesWrite or higher, and not the pull request's author
Request reviewers after opening, or remove a requestThe author, or triage or higher
Edit title and description, close, reopenThe author, or triage or higher
Mark a draft ready for review, or convert it back to a draftThe author, or write or higher
Edit or delete a comment or line commentIts author, or maintain or higher
Lock and unlock the conversationTriage or higher
Comment, review or react on a locked pull requestTriage or higher
Dismiss a reviewMaintain or higher, not the pull request's author, and never an agent
Update the branch with its base (update-branch)Write or higher, subject to the head branch's protection
Merge, or add to and remove from the merge queueWrite or higher, subject to merge blockers
Turn auto-merge on or offWrite or higher; agents and workflow tokens only where the base branch allows it

A token's scope can lower your role: a read token acts with at most read, a write token with at most maintain, and only an admin token keeps admin. Every change (opening, commenting, reviewing, editing, marking ready, merging) needs a signed-in caller: without credentials the API answers 401 with authentication required, even on a public repository, and a read-scoped token gets 403 with this token needs the write scope. On an archived repository every change is refused with 403 and repository is archived (read-only). A workflow's GITHUB_TOKEN has extra limits: see Pull requests and workflow tokens. See Permissions and Tokens.

Open a pull request

From a push

When you push a new branch (other than the default branch) that has no pull request yet, git prints a link to open one:

remote: Create a pull request for 'fix-pricing-link':
remote:   https://diffinery.com/acme/website/compare/fix-pricing-link

Pushing to a branch that already has an open pull request prints Updated pull request #12: https://diffinery.com/acme/website/pull/12.

In the web UI

  1. Open the Pull requests tab and click New pull request, or go to https://diffinery.com/acme/website/compare. You can also open https://diffinery.com/acme/website/compare/main...fix-pricing-link directly, or click Compare next to a branch on the Branches page.
  2. Pick the base (the branch to merge into) and the compare branch (your changes), then click Compare. The page shows how many commits the compare branch is ahead and behind, the commits, and the diff.
  3. Under Open a pull request, fill in:
    • Title (required). With a single commit it defaults to that commit's subject; otherwise to the branch name.
    • Intent: what this change is meant to do. Optional, but recommended.
    • Description (Markdown). With a single commit it defaults to the rest of that commit's message.
    • Create as draft if the work isn't ready to merge.
  4. Click Create pull request.

The form appears only when both sides are branches, the compare branch has commits the base doesn't, and you have write access. If a pull request for the pair is already open, the page says A pull request for these branches already exists: #12 instead. When there is nothing to compare, it says There isn't anything to compare.

With the CLI

Install the CLI and sign in through your browser:

curl -fsSL https://diffinery.com/install | bash
diffinery login

From a clone with your branch checked out:

git push -u origin fix-pricing-link
diffinery pr create \
  --title "Fix the Contact sales link" \
  --intent "Point the pricing page's Contact sales link at /contact; no other behaviour changes." \
  --body "Fixes #12" \
  --reviewer diffy
FlagMeaning
-t, --title TEXTTitle (required)
-b, --body TEXTDescription (Markdown)
--body-file FILERead the description from a file, or stdin with -
-H, --head BRANCHBranch with your changes (default: the current branch)
-B, --base BRANCHBranch to merge into (default: the repository's default branch)
-d, --draftOpen as a draft
--intent TEXTWhat the change is for (agents should always set it)
-r, --reviewer LOGINRequest a review (repeat for more)
-R, --repo OWNER/NAMERepository (default: inferred from the git origin remote)
--jsonPrint the pull request as JSON

With the REST API

curl -X POST https://diffinery.com/api/v3/repos/acme/website/pulls \
  -H "Authorization: Bearer dfy_..." -H "Content-Type: application/json" \
  -d '{
    "title": "Fix the Contact sales link",
    "head": "fix-pricing-link",
    "base": "main",
    "body": "Fixes #12",
    "intent": "Point the pricing page'\''s Contact sales link at /contact; no other behaviour changes.",
    "draft": false,
    "reviewers": ["diffy"]
  }'

base defaults to the default branch. The response (201 Created) is GitHub's pull request object: id and number (both the pull request number), title, body, state, locked, user, labels (label objects), assignees, requested_reviewers (user objects), head and base (ref, sha, label, repo), draft, merged, merged_at, merged_by, merge_commit_sha, html_url and the usual _links, plus Diffinery's intent.

Over MCP

The create_pull_request tool takes owner, repo, title, head, and optionally body, base, draft, intent and reviewers. An agent typically creates a branch and commits with commit_files first. See MCP server.

Errors when opening

MessageCause
head and base must be different branchesHead and base are the same branch.
head branch 'x' does not existPush the branch first.
base branch 'x' does not existCheck the base name.
no commits between main and xThe head has nothing the base doesn't already contain.
a pull request already exists for x: #12Use the open pull request, or close it first. The response also includes number.
title is requiredGive it a title.
write access requiredOpening a pull request needs write access.
GitHub Actions is not permitted to create pull requests. ... (403)A workflow's GITHUB_TOKEN tried to open a pull request in a repository that doesn't allow it. See Pull requests and workflow tokens.

Intent

The intent is a plain statement of what the change is supposed to achieve, separate from the description. It appears at the top of the conversation in a highlighted Intent box ("what this change is meant to do"), is included in notification emails, is returned as intent by the API and by diffinery pr view, and is included in the get_pull_request MCP result. Reviewers, human or AI, can check the diff against it rather than guessing the goal from the code.

Write it as one or two sentences about the outcome and its limits, for example: "Fix the race in token refresh without changing the public API." Agents should always set it. The intent is set when you open the pull request and can't be edited afterwards.

Drafts

Open a pull request as a draft with Create as draft, diffinery pr create --draft, "draft": true in the API, or draft: true in create_pull_request. A draft:

  • shows a Draft badge and appears in lists with a draft label,
  • can be reviewed, and runs its pull_request workflows like any other pull request,
  • can't be merged or added to a merge queue: it has the blocker pull request is a draft, and admins can't bypass it.

When the work is ready, mark the draft ready for review. You can also turn an open pull request back into a draft. Either change needs you to be the pull request's author or to have write access or higher, and the pull request must be open. Through the API, the token needs the write scope.

Mark a draft ready for review

  • Web UI: on the Conversation tab, the merge box of a draft says This pull request is still a draft and can't be merged yet. next to a Ready for review button. Click it; you see Pull request marked ready for review. The button is shown to the author and to people with write access, and not on archived repositories.
  • CLI: diffinery pr ready 12 prints ✓ Marked #12 ready for review. With --json it prints the pull request instead.
  • REST API: PATCH /repos/{owner}/{repo}/pulls/{number} with {"draft": false}. GitHub offers this only through GraphQL; on Diffinery draft is a field of the REST update.
curl -X PATCH https://diffinery.com/api/v3/repos/acme/website/pulls/12 \
  -H "Authorization: Bearer dfy_..." -H "Content-Type: application/json" \
  -d '{"draft": false}'

The response is the pull request with "draft": false. There is no MCP tool for this yet: agents use the REST API or the CLI.

Convert a pull request to a draft

  • CLI: diffinery pr ready 12 --undo prints ✓ Converted #12 to a draft.
  • REST API: PATCH /repos/{owner}/{repo}/pulls/{number} with {"draft": true}.

The web UI has no button for this.

What changing the draft state does

  • The pull request's timeline gets a ready_for_review or convert_to_draft event (GitHub's timeline names), and webhooks get a pull_request event with the action ready_for_review or converted_to_draft (GitHub's webhook names). See Webhook events.
  • pull_request and pull_request_target workflows whose types include ready_for_review or converted_to_draft run. The default types don't include them. See Events.
  • Converting a pull request to a draft turns off auto-merge, with the comment Auto-merge was turned off: the pull request was converted to a draft. Marking it ready again doesn't turn auto-merge back on.
  • Asking for the state the pull request already has changes nothing and sends no event.
  • PATCH can change draft together with title, body and state in one request.
ErrorCause
only the author or someone with write access can change draft status (403)Ask the author or a maintainer.
the pull request is not open (422)Reopen it first.
not a pull request (404)The number belongs to an issue.
this token needs the write scope (403)Use a token with the write or admin scope.

The pull request page

The page at https://diffinery.com/acme/website/pull/12 has four tabs. It updates live as comments, reviews, pushes and checks arrive.

TabShows
ConversationThe description, the intent, comments, reviews with their line comments, the merge box, Review changes, the comment form, and a sidebar with Reviewers, Labels, Milestone, Assignees, Notifications and Participants
CommitsThe commits on the head branch that aren't on the base (up to 250)
ChecksCheck runs and commit statuses reported on the head commit, and the workflow runs for this pull request
Files changedThe diff from the merge base to the head (up to 300 files), where you can comment on lines

The header says wants to merge into main from fix-pricing-link (or merged into once merged). Agent authors and reviewers are marked with an agent badge and on behalf of their owner.

Labels, assignees, milestones, comments, mentions, editing the title and description, and subscribing work exactly as on issues. See Issues.

To put a pull request in a milestone, someone with triage access or higher uses Set milestone in the sidebar, or sends {"milestone": 1} (or null) with PATCH /repos/{owner}/{repo}/issues/{number} using the pull request's number. Pull request objects from the REST API carry the same milestone object as issues. Milestones themselves are created and edited with the REST API; see Milestones.

Reviews

A review is one of three kinds:

KindWeb UI labelAPI and MCP eventEffect
ApproveApproveAPPROVECounts toward required approvals (people only, not agents or workflow tokens)
Request changesRequest changesREQUEST_CHANGESBlocks merging until that reviewer approves
CommentCommentCOMMENTFeedback only; needs a body or line comments

Rules:

  • You can't approve or request changes on your own pull request; you can still leave a Comment review.
  • Approving and requesting changes need write access. Anyone signed in who can read the repository can leave a Comment review.
  • Only open pull requests can be reviewed.
  • For each reviewer, their latest Approve or Request changes review is the one that counts. A later Comment review doesn't change it. To withdraw a change request, submit an Approve review, or ask a maintainer to dismiss it.
  • Approvals from agents and from a workflow's GITHUB_TOKEN are shown but never count toward required approvals. An agent's (or a workflow token's) Request changes review does block merging.
  • A workflow's GITHUB_TOKEN can approve or request changes only when the repository allows it; see Pull requests and workflow tokens.
  • If the base branch's rule has Dismiss stale approvals when new commits are pushed, pushing to the head marks earlier approvals as dismissed (shown as reviewed (dismissed)), and only approvals of the current head commit count.
  • Submitting any review removes you from the pull request's requested reviewers.

Reviews are submitted immediately. There are no pending (draft) reviews.

Review in the web UI

  1. On the Conversation tab, expand Review changes.
  2. Write a Review summary (Markdown).
  3. Pick the Review type: Comment, Approve or Request changes. Approve and Request changes are disabled on your own pull request or without write access.
  4. Click Submit review.

Review with the CLI

diffinery pr review 12 --approve
diffinery pr review 12 --approve --body "Looks good."
diffinery pr review 12 --request-changes --body "Please add a test for the redirect."
diffinery pr review 12 --comment --body "Nit: the link text could be clearer."

Exactly one of -a, --approve, -r, --request-changes or -c, --comment is required. --body (or --body-file) is required for request changes and comment.

Review with the REST API

curl -X POST https://diffinery.com/api/v3/repos/acme/website/pulls/12/reviews \
  -H "Authorization: Bearer dfy_..." -H "Content-Type: application/json" \
  -d '{
    "event": "REQUEST_CHANGES",
    "body": "Please add a test.",
    "comments": [{ "path": "src/pricing.html", "line": 42, "side": "RIGHT", "body": "This should be /contact." }]
  }'

event defaults to COMMENT. List reviews with GET /repos/{owner}/{repo}/pulls/{number}/reviews, or read one with GET /repos/{owner}/{repo}/pulls/{number}/reviews/{review_id}. Each has a numeric id (as on GitHub), state (APPROVED, CHANGES_REQUESTED, COMMENTED or DISMISSED), body, commit_id, submitted_at, user and html_url.

Review over MCP

review_pull_request takes owner, repo, number, event, and optionally body and comments ([{ path, line, side?, body }]).

Dismiss a review

Dismissing a review takes it out of the merge decision: a dismissed approval stops counting toward required approvals, and a dismissed change request stops blocking. The review stays visible with the state DISMISSED, and the reviewer can review again. This is the same as GitHub's dismissal, with stricter rules about who may do it, because dismissing can unblock a merge:

  • You need the maintain or admin role on the repository (a token with the write scope caps you at maintain, which is enough).
  • You can't dismiss reviews on your own pull request.
  • Agents can never dismiss reviews.
  • Only APPROVED and CHANGES_REQUESTED reviews can be dismissed.

Dismiss through the REST API with a message saying why:

curl -X PUT https://diffinery.com/api/v3/repos/acme/website/pulls/12/reviews/81/dismissals \
  -H "Authorization: Bearer dfy_..." -H "Content-Type: application/json" \
  -d '{"message": "The requested test was added in 3f2a9c1."}'

81 is the review's id from the reviews list. The response is the review with "state": "DISMISSED", and the pull request's timeline gets a review_dismissed event with the message. There is no dismiss button in the web UI, CLI command or MCP tool yet.

ErrorCause
message is required (422)Send a non-empty message.
maintain access is required to dismiss reviews (403)Your role, or your token's scope, is below maintain.
agents can't dismiss reviews (403)Dismiss as a person.
you can't dismiss reviews on your own pull request (403)Ask another maintainer.
only approving or change-requesting reviews can be dismissed (422)The review is a Comment review or already dismissed.
review not found (404)Check the review id and the pull request number.

Line comments

Line comments attach to a file and line of the diff.

  • Web UI: on the Files changed tab, click a line, type in Leave a comment on this line and click Add comment. Each line comment is posted at once as a Comment review.
  • API and MCP: include comments in a review: path (the file), line (the line number), side (RIGHT for the head version, the default, or LEFT for the base version) and body.

Line comments appear on the Files changed tab next to their line and in the conversation under their review. A comment made on an older head commit is marked outdated. List them with GET /repos/{owner}/{repo}/pulls/{number}/comments (fields include a numeric id, pull_request_review_id, body, path, line, side, commit_id, in_reply_to_id, user, created_at, reactions and Diffinery's resolved).

Single line comments and replies with the REST API

You can also post one line comment, or reply to one, without writing a whole review. Each is stored as a one-comment Comment review, as on GitHub.

# A new line comment
curl -X POST https://diffinery.com/api/v3/repos/acme/website/pulls/12/comments \
  -H "Authorization: Bearer dfy_..." -H "Content-Type: application/json" \
  -d '{"body": "This should be /contact.", "path": "src/pricing.html", "line": 42, "side": "RIGHT"}'

# A reply to line comment 57: it lands on the same file, line and side
curl -X POST https://diffinery.com/api/v3/repos/acme/website/pulls/12/comments/57/replies \
  -H "Authorization: Bearer dfy_..." -H "Content-Type: application/json" \
  -d '{"body": "Done in the latest push."}'

POST .../pulls/{number}/comments with in_reply_to (the parent comment's id) and body is the same as the replies endpoint. position is accepted as another name for line. A new comment needs path and line (otherwise path and line are required), and every comment needs a body (body is required). The same rules as reviews apply: the pull request must be open, and a locked pull request accepts comments only from triage or higher.

Other line comment endpoints:

Method and pathPurpose
GET /repos/{owner}/{repo}/pulls/commentsLine comments across all pull requests in the repository. Takes since, sort (created or updated), direction (asc, the default, or desc), per_page and page.
GET /repos/{owner}/{repo}/pulls/comments/{id}One line comment
PATCH /repos/{owner}/{repo}/pulls/comments/{id}Change its body (its author, or maintain or higher)
DELETE /repos/{owner}/{repo}/pulls/comments/{id}Delete it (its author, or maintain or higher)

Comment ids are numbers, as on GitHub.

Requested reviewers

Ask specific people or agents to review:

  • when opening: reviewers in the API and MCP, or diffinery pr create --reviewer LOGIN,
  • later, through the API (the author, or triage or higher):
curl -X POST https://diffinery.com/api/v3/repos/acme/website/pulls/12/requested_reviewers \
  -H "Authorization: Bearer dfy_..." -H "Content-Type: application/json" \
  -d '{"reviewers": ["diffy", "review-bot"]}'

Requested reviewers are notified and appear in the sidebar's Reviewers list as pending until they submit a review. Logins aren't checked. Requesting a review doesn't make it required; required approvals come from the branch protection rule. team_reviewers is refused with team reviewers are not supported; request people by login.

Read the current requests with GET /repos/{owner}/{repo}/pulls/{number}/requested_reviewers ({"users": [...], "teams": []}), and remove one (the author, or triage or higher):

curl -X DELETE https://diffinery.com/api/v3/repos/acme/website/pulls/12/requested_reviewers \
  -H "Authorization: Bearer dfy_..." -H "Content-Type: application/json" \
  -d '{"reviewers": ["review-bot"]}'

Requests and removals show in the pull request's events as review_requested and review_request_removed.

Checks

Checks are reported on the pull request's head commit, as on GitHub. Each workflow job reports a check named <workflow name> / <job name> (for example CI / test). Other systems can report check runs or commit statuses (write access). Commit statuses show with the checks and count as checks for branch protection. See Checks and statuses for both APIs. A check run, for example:

curl -X POST https://diffinery.com/api/v3/repos/acme/website/check-runs \
  -H "Authorization: Bearer dfy_..." -H "Content-Type: application/json" \
  -d '{"name": "lighthouse", "head_sha": "HEAD_SHA", "status": "completed", "conclusion": "success", "details_url": "https://ci.example.com/run/1", "output": {"title": "Score 98", "summary": "All budgets met."}}'

Posting again with the same name on the same commit (from the same account) updates the check. Read check runs with GET /repos/{owner}/{repo}/commits/{ref}/check-runs and statuses with GET /repos/{owner}/{repo}/commits/{ref}/status. A check whose reporter gave a details_url (or a status target_url) links there from the pull request.

Where checks show up:

  • The merge box lists every check with its state: All checks have passed, N failing checks, or Some checks haven't completed yet.
  • The Checks tab shows each check and the pull request's workflow runs.
  • diffinery pr checks 12 prints them and exits 0 when all passed, 1 when any failed, and 8 when some are still pending, so scripts can wait on it. Like gh pr checks --watch, diffinery pr checks 12 --watch polls (every 10 seconds; -i SECONDS to change it) until no check is pending and then exits 0 or 1. On a terminal it prints the checks each time they change; when piped, it prints them once at the end (--json prints the final list).
  • GET /repos/{owner}/{repo}/pulls/12 includes checks (name and state).

A check counts as passing when its conclusion is success, skipped or neutral (a status: when its state is success). Which checks must pass is set by Required status checks in branch protection, and not every report counts toward them: checks reported by agents never do, and a check that Diffinery Actions reports is decided by Actions alone. See Branch protection.

Pull request workflows

Workflows that trigger on pull_request behave as on GitHub:

  • They check out the test merge commit, refs/pull/<n>/merge: the head merged into the current base. GITHUB_REF is refs/pull/<n>/merge and GITHUB_SHA is the test merge commit. Their checks are reported on the head commit.
  • The default activity types are opened, synchronize and reopened. Add closed to types to run when a pull request closes; github.event.pull_request.merged tells you whether it merged. Add edited to run when the title or description changes, and ready_for_review or converted_to_draft to run when the draft state changes.
  • While the pull request conflicts with its base, there is no test merge commit and pull_request workflows don't run.
  • pull_request_target workflows run the base branch's own workflow file on the base commit, with the repository's secrets, so a pull request can't change what runs. They run even while the pull request conflicts.

Example:

name: CI
on:
  pull_request:
    branches: [main]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm test

See Events for every trigger and filter.

Update a branch with its base

When the base branch has moved on, you can merge it into the pull request's head branch on the server, like GitHub's Update branch. This is available through the REST API (write access); the web UI, CLI and MCP have no button or command for it yet.

curl -X PUT https://diffinery.com/api/v3/repos/acme/website/pulls/12/update-branch \
  -H "Authorization: Bearer dfy_..." -H "Content-Type: application/json" \
  -d '{"expected_head_sha": "3f2a9c1e..."}'

expected_head_sha is optional: when given, the update only happens if the head is still at that commit. Diffinery creates a merge commit on the head branch with the parents (head, base), the message Merge branch 'main' into fix-pricing-link, and you as author and committer (<login>@users.noreply.diffinery.com). The response is 202 with {"message": "Updating pull request branch.", "url": "..."}, and the branch has already moved when it arrives.

The update is a push to the head branch: the head branch's protection rules and agent rules apply, the pull request gets a synchronize event, its pull_request workflows run again, and stale approvals are dismissed if the base's rule says so. A commit created by an agent carries the diffinery-agent header.

ErrorCause
There are no new commits on the base branch. (422)The head already contains the base.
merge conflict between base and head (422)Resolve the conflict locally (see Conflicts).
expected head sha didn't match current head ref. (422)Someone pushed to the head. Fetch it and try again.
head branch moved; try again (409)The head moved while the merge was being made.
pull request is closed (422)Only open pull requests can be updated.
base branch main no longer exists (422)The base branch was deleted.
write access is required to update the branch (403)You need write access.

If the base requires linear history, don't use update-branch: it adds a merge commit to the head. Rebase locally instead.

Conflicts

When the head and base change the same lines, the merge box says This branch has conflicts that must be resolved, lists the conflicting files, and shows the blocker merge conflicts in N file(s). Diffinery has no web conflict editor, and update-branch refuses to merge a conflicting base. Resolve the conflict locally and push:

git fetch origin
git switch fix-pricing-link
git merge origin/main        # or: git rebase origin/main, then git push --force-with-lease
# fix the conflicts, then
git add -A && git commit
git push

The push updates the pull request, rebuilds its test merge commit, and runs its pull_request workflows again.

Merge methods

MethodWeb UI labelResult
mergeCreate a merge commitA merge commit with two parents (base, head). Subject Merge pull request #12 from fix-pricing-link, body the pull request title. Author and committer: the person merging.
squashSquash and mergeOne commit with a single parent (base). Subject <title> (#12), body a * <subject> line per commit. Author: the pull request's author; committer: the person merging.
rebaseRebase and merge (fast-forward)No new commit: the base fast-forwards to the head. Only works when the head already contains the base; otherwise it fails with rebase merge requires the branch to be up to date with its base (fast-forward only for now).

Commits Diffinery writes use the email <login>@users.noreply.diffinery.com. Through the API (and diffinery pr merge --subject/--body) you can set the commit title and message for merge and squash.

Default method when you don't pick one:

WhereBase branch without linear historyBase branch with Require linear history
REST API (merge_method left out)mergesquash
Web UI merge formCreate a merge commitSquash and merge; Create a merge commit isn't offered
CLI (diffinery pr merge without --merge, --squash or --rebase)mergemerge, which is refused: pass --squash or --rebase
MCP merge_pull_request (no method)mergemerge, which is refused: pass method

On a branch whose rule has Require linear history, a merge with the merge method is refused with 405 and main requires linear history; use squash or rebase (the merge queue refuses it with 422 and the same message), so choose squash or rebase there. Only an explicit admin bypass with bypass_rules can create a merge commit on such a branch, and that is recorded in the audit log.

In a merge queue, rebase replays each commit onto the queue instead of fast-forwarding.

Merge a pull request

Merging needs write access and a pull request with no merge blockers. If the base branch uses a merge queue, merging adds the pull request to the queue instead; see Merge when ready and the merge queue. If the pull request isn't ready yet, turn on auto-merge and Diffinery merges it once it is.

Merge in the web UI

  1. On the Conversation tab, check the merge box: This branch has no conflicts with the base branch means it is ready; otherwise it lists the blockers.
  2. Pick the Merge method: Create a merge commit, Squash and merge or Rebase and merge (fast-forward). On a base branch that requires linear history, the form starts on Squash and merge and doesn't list Create a merge commit.
  3. Leave Delete fix-pricing-link after merge ticked to delete the head branch, or untick it.
  4. Click Merge pull request.

The timeline then shows diffy merged commit abc1234 into main.

Merge with the CLI

diffinery pr merge 12 --squash --delete-branch
diffinery pr merge 12 --merge --subject "Merge the pricing fix" --body "Fixes #12"
diffinery pr merge 12 --rebase --match-head-commit 3f2a9c1e...
FlagMeaning
-m, --mergeCreate a merge commit (default)
-s, --squashSquash into one commit
-r, --rebaseFast-forward the base to the head
-d, --delete-branchDelete the head branch after merging (with --auto, once it lands). A merge never deletes the default branch or a protected branch.
-t, --subject TEXTCommit title
-b, --body TEXT, --body-file FILECommit message body
--match-head-commit SHAOnly merge if the head is still at this commit
--adminMerge now, bypassing unmet branch rules and any merge queue (repository admins; audited). Sends bypass_rules and bypass_merge_queue. See Admin bypass.
--autoTurn on auto-merge instead of merging now: Diffinery merges (or adds it to the merge queue) once every requirement is met. Sends PUT /repos/{owner}/{repo}/pulls/{number}/auto-merge with the method flag, --delete-branch, --subject, --body and --match-head-commit you pass. Can't be combined with --admin.
--disable-autoTurn auto-merge off (DELETE /repos/{owner}/{repo}/pulls/{number}/auto-merge). If the pull request is already in the merge queue, it is also removed from it, so Diffinery no longer lands it by itself.

Like gh pr merge --auto, diffinery pr merge 12 --auto --squash prints ✓ Auto-merge is on for #12 (squash): it merges once every requirement is met, or ✓ Merged #12 / ✓ Added #12 to the merge queue when the pull request was ready already. Without a method flag the server picks the merge queue's method, squash where linear history is required, and a merge commit otherwise. diffinery pr merge 12 --disable-auto prints ✓ Turned off auto-merge for #12, followed by and removed it from the merge queue when it was queued.

Merge with the REST API

curl -X PUT https://diffinery.com/api/v3/repos/acme/website/pulls/12/merge \
  -H "Authorization: Bearer dfy_..." -H "Content-Type: application/json" \
  -d '{"merge_method": "squash", "delete_branch": true, "sha": "HEAD_SHA"}'
FieldMeaning
merge_methodmerge, squash or rebase
commit_title, commit_messageOverride the commit title and body (merge and squash)
shaOnly merge if the head is still at this commit; otherwise 409 head branch was modified; review and try again
delete_branchDelete the head branch after merging (default false)
bypass_rulesAdmins only: merge even though the pull request has unmet branch rules, or create a merge commit on a branch that requires linear history. It doesn't skip a merge queue. See Admin bypass.
bypass_merge_queueAdmins only: merge directly on a branch that uses a merge queue. It skips only the queue: a pull request with unmet branch rules also needs bypass_rules.

The response is {"merged": true, "sha": "...", "method": "squash", "message": "Pull Request successfully merged"}.

Check mergeability

GET /repos/{owner}/{repo}/pulls/{number} reports, like GitHub:

FieldMeaning
mergeabletrue when the head merges into the base without conflicts, false when it conflicts, null once the pull request is closed. It doesn't consider approvals or checks.
mergeable_stateclean (no blockers), blocked (blockers other than conflicts), dirty (conflicts), draft, or unknown (merged, closed, or the base branch is gone)
rebaseabletrue when the head has no conflicts and already contains the latest base commit, so a rebase (fast-forward) merge works. null once closed.

Diffinery adds merge_blockers (the reasons it can't merge yet), approvals, required_approvals, checks (name and state of every check run and status on the head) and merge_queue.

GET /repos/{owner}/{repo}/pulls/{number}/merge depends on the client. With a GitHub media type (Accept: application/vnd.github+json, as Octokit and gh send), it answers like GitHub: 204 if the pull request is merged and 404 if not. Other clients get Diffinery's mergeability report: mergeable, state (clean, blocked, conflicting, draft, merged, closed or unknown), conflicts, approvals, requiredApprovals, changesRequested, checks, blockers, mergeBase and mergeQueue.

Merge over MCP

merge_pull_request takes owner, repo, number, and optionally method, expected_head_sha and delete_branch. On a branch with a merge queue it queues the pull request instead. Agents can't merge into the default branch or a protected branch themselves; they use enable_auto_merge there (see Auto-merge).

After the merge

  • The pull request is closed with the reason merged, and merged_by and merge_commit_sha are set.
  • The base branch moves as if pushed to: push workflows run and webhooks fire.
  • pull_request workflows whose types include closed run, with github.event.pull_request.merged set to true.
  • Open issues referenced with a closing keyword (Fixes #12, closes #3, resolves #7) close. See Issues.
  • If you asked for it, the head branch is deleted, unless it is the default branch or has moved since the merge.

Admin bypass

A repository admin can merge a pull request whose branch rules aren't met, but only by asking for it explicitly. Without the request, an admin's merge is refused like anyone else's. A bypass is possible when all of these hold:

  • you have the admin role on the repository (through a token, that needs the admin scope: a write token caps you at maintain);
  • you are a person: agents can never bypass;
  • the base branch's rule doesn't have Do not allow administrators to bypass these rules;
  • the pull request has no merge conflicts and isn't a draft.

Each bypass flag covers only what it names:

FlagSkipsDoesn't skip
bypass_rulesMissing approvals, change requests, required checks that failed or haven't run, a branch that is behind its base under linear history, and the refusal of merge commits under linear historyA merge queue
bypass_merge_queueThe merge queue: the pull request merges directlyAny unmet branch rule (approvals, checks, linear history)

To merge past both a merge queue and unmet rules, send both flags. Conflicts, a draft state and the agent rules are never bypassed.

How to ask for it:

  • Web UI: when you are an admin and the pull request has blockers but no conflicts and isn't a draft, the button reads Merge without waiting for requirements (bypass) and asks Merge without meeting the branch requirements? before merging. It sends bypass_rules. The button isn't shown when the base branch's rule has Do not allow administrators to bypass these rules. On a branch with a merge queue, the merge box instead offers Merge now (bypass the queue), which sends only bypass_merge_queue; see Merge queue.
  • CLI: diffinery pr merge 12 --squash --admin. The CLI sends both bypass_rules and bypass_merge_queue.
  • REST API: add "bypass_rules": true to PUT /repos/{owner}/{repo}/pulls/{number}/merge:
curl -X PUT https://diffinery.com/api/v3/repos/acme/website/pulls/12/merge \
  -H "Authorization: Bearer dfy_..." -H "Content-Type: application/json" \
  -d '{"merge_method": "squash", "bypass_rules": true}'

When you could bypass but didn't ask, the 405 message ends with (as an admin you can bypass with bypass_rules), for example pull request is not mergeable: needs 1 more approving review(s) (as an admin you can bypass with bypass_rules). MCP's merge_pull_request has no bypass option.

A merge that bypassed anything is recorded in the audit log as pull_request.merge_bypassed, with the pull request as the target and the base branch and the bypassed blockers in the detail, in addition to the usual pull.merge entry. blockers lists the unmet rules skipped with bypass_rules, linear history required when a merge commit was created on a linear-history branch, and merge queue when the queue was skipped with bypass_merge_queue.

Merge blockers

A pull request can be merged when it has no blockers. The merge box, diffinery pr view, merge_blockers in the API and mergeability.blockers in MCP list them with these exact messages:

BlockerMeaning and fix
already mergedThe pull request is merged.
pull request is closedReopen it first.
base branch main no longer existsThe base branch was deleted. Recreate it, or open a new pull request against another base.
pull request is a draftDrafts can't be merged. See Drafts.
merge conflicts in N file(s)Resolve the conflicts and push. See Conflicts.
needs N more approving review(s)Get more approvals from people with write access. Agent approvals don't count.
changes requested by diffyThat reviewer must approve before merging.
required check 'CI / test' has not runNo counted check run or status with that exact name was reported on the head commit. Check the workflow's trigger and the check name. Reports from agents don't count, and neither do reports from accounts other than the one bound in the rule's requiredCheckSources.
required check 'CI / test' is queued (or is in_progress)Wait for it to finish. A pending commit status shows as is in_progress.
required check 'CI / test' failure (or another conclusion such as cancelled, timed_out or action_required)The check didn't pass. Fix the code or re-run the workflow.
branch is behind its base; update it (linear history required)The base requires linear history and the head doesn't contain the latest base. Rebase onto the base and push.
write access is required to mergeYou need write access.
agents may not merge into this branch; a human must mergeAn agent tried to merge into the default branch or a branch with a protection rule that lacks Allow agents to merge pull requests. This also applies to a workflow's GITHUB_TOKEN in a run whose code came from an agent. Where the rule allows it, the agent can turn on auto-merge instead.
refusing to allow a GitHub App to create or update workflow `.github/workflows/ci.yml` without `workflows` permissionA workflow's GITHUB_TOKEN tried to merge a pull request that adds, changes or deletes a workflow file. A person must merge it.

Required approvals come from the branch protection rule's Required approvals (0 to 10). Pull requests opened by agents need at least one even without a rule.

When the base branch uses a merge queue, merging directly fails with main requires a merge queue: add the pull request to the queue (405), with blockers: ["main merges through a merge queue"]. For an admin who could bypass, the message adds (or, as an admin, bypass the queue with bypass_merge_queue).

Merge blockers apply to admins too, unless they ask for an admin bypass.

When Diffinery lands a pull request for an agent or a workflow token, through auto-merge or the merge queue, the agent blocker is replaced by the rules in Auto-merge for agents and workflow tokens, and these blockers can appear: needs N more approving review(s) of its latest commit, the pull request changes workflow file `<path>`; a person must merge it, and the refusals agents may not have pull requests merged into main automatically ... and main has no required status checks: ....

Auto-merge

Auto-merge lets you approve the outcome now and have Diffinery do the merge later: you turn it on for a pull request that isn't ready yet (required checks still running, an approval still missing, the branch behind its base), and Diffinery merges it by itself as soon as every rule of its base branch is met. It works like GitHub's auto-merge, and it is also how agents land changes on protected branches without ever merging there themselves.

To have it on for every pull request without turning it on each time, see Merge on green automatically: a repository's Settings → Merge on green picks the checks to wait for, and Settings → Pull requests (or an agent's Merge its pull requests on green) turns auto-merge on as each pull request opens. diffinery pr create --auto and "auto_merge": true on POST /repos/{owner}/{repo}/pulls turn it on for one pull request.

How auto-merge works

  • It checks right away. When you turn it on, Diffinery looks at the pull request at once. If nothing blocks it, it merges immediately (the API answers "state": "merged"). Otherwise auto-merge stays on and waits.
  • It looks again when something changes. Diffinery checks the pull request again when a check run completes or a commit status other than pending is reported on its head commit, when a review is submitted or dismissed, when its head branch or its base branch is pushed to, and when a branch protection rule is saved or deleted.
  • It merges only when nothing blocks. The pull request must have no merge blockers: every required check passed on the head commit, enough approvals from people, no changes requested, no conflicts, and an up-to-date branch where the base requires linear history. Auto-merge never bypasses anything, even when an admin turned it on. Checks that aren't required aren't waited for, so on a branch without required checks a pull request with enough approvals merges at once, even while CI is still running.
  • Into a merge queue, it queues. When the base branch has Require a merge queue, auto-merge adds the pull request to the merge queue once it is ready, with the auto-merge method, and then turns itself off: the queue takes over and merges it when its merge group passes. The API answers "state": "queued" when that happens right away.
  • It merges as the one who turned it on. The merge is made by the person, agent or workflow token that turned auto-merge on, with their access at the time of the merge, and never with more than they had when they turned it on. They appear as merged_by, and the commit is written as if they had merged by hand (see Merge methods). A merge or squash commit made for an agent carries the diffinery-agent header.
  • It keeps the head branch. Auto-merge doesn't delete the head branch after merging. Delete it yourself afterwards.
  • Turning it on again changes it. Calling it again while auto-merge is on replaces the merge method and commit text.

Closing the pull request, or merging it some other way, ends auto-merge quietly. Reopening a closed pull request doesn't turn it back on.

Who can turn on auto-merge

  • You need write access to the repository, and through the API a token with the write (or admin) scope. On an archived repository it is refused with repository is archived (read-only).
  • The pull request must be open, not a draft, not merged and not already in the merge queue.
  • People can turn it on for any base branch.
  • Agents and workflow tokens (GITHUB_TOKEN) can turn it on for a base branch that is neither the default branch nor protected by a rule. On the default branch or a protected branch, the branch's rule must have Allow agents to land pull requests through auto-merge (allowAgentAutoMerge) and at least one required status check; an agent may also use it where the rule has Allow agents to merge pull requests (agentsMayMerge). See Auto-merge for agents and workflow tokens.
  • A workflow's GITHUB_TOKEN needs contents: write in the job's permissions:, as for merging.

Anyone with write access can turn it off, whoever turned it on.

Auto-merge method

Auto-merge uses one of the three merge methods. When you don't pick one:

Base branchDefault auto-merge method
Uses a merge queueThe queue's method
Requires linear history (no merge queue)squash
Anything elsemerge

On a branch that requires linear history, merge is refused with main requires linear history; use squash or rebase (422). Outside a merge queue, rebase is a fast-forward: it only works once the head already contains the latest base commit, so prefer squash unless you keep the branch rebased yourself. Through the API and MCP you can also set the commit title and message for merge and squash; the web form doesn't offer them.

Turn on auto-merge in the web UI

  1. Open the pull request's Conversation tab. The merge box offers auto-merge when you have write access, the pull request can't merge yet, has no conflicts and isn't a draft, and auto-merge isn't already on.
  2. Under the merge button, next to Merge by itself once every requirement is met. (on a branch with a merge queue, Join the merge queue by itself once every requirement is met.), pick the Auto-merge method: Create a merge commit, Squash and merge or Rebase and merge. Create a merge commit isn't listed on a branch that requires linear history, and on a merge queue branch the queue's method is preselected.
  3. Click Enable auto-merge.

You see Auto-merge is on: it merges once every requirement is met., or Pull request merged. or Added to the merge queue. when the pull request turned out to be ready. While it waits, the merge box shows Auto-merge is on, who turned it on, and what it will do, for example Auto-merge is on · turned on by diffy · squash and merge once every requirement is met (or joins the merge queue (squash and merge) once every requirement is met). To turn it off, click Disable auto-merge in that row; you see Auto-merge is off.

The button isn't offered while the pull request can already merge: merge it directly instead.

Turn on auto-merge with the REST API

GitHub offers auto-merge only through GraphQL (enablePullRequestAutoMerge and disablePullRequestAutoMerge); Diffinery has REST endpoints for it:

curl -X PUT https://diffinery.com/api/v3/repos/acme/website/pulls/12/auto-merge \
  -H "Authorization: Bearer dfy_..." -H "Content-Type: application/json" \
  -d '{"merge_method": "squash", "commit_title": "Fix the Contact sales link (#12)"}'
FieldMeaning
merge_methodmerge, squash or rebase. Default: see Auto-merge method.
commit_title, commit_messageTitle and body of the merge or squash commit. Default: the same as a merge by hand.

All fields are optional; an empty body works. The response (200) says what happened:

{
  "state": "enabled",
  "auto_merge": {
    "enabled_by": { "login": "diffy", "type": "User" },
    "merge_method": "squash",
    "commit_title": "Fix the Contact sales link (#12)",
    "commit_message": null
  },
  "merge_commit_sha": null
}
stateMeaning
enabledAuto-merge is on and waiting. auto_merge shows its settings (enabled_by is a full user object).
mergedThe pull request was ready and merged right away. auto_merge is null and merge_commit_sha is the new commit.
queuedThe base branch uses a merge queue and the pull request was ready, so it joined the queue. auto_merge is null.

While auto-merge is on, the pull request object (GET /repos/{owner}/{repo}/pulls/{number} and the list) carries the same auto_merge object, as on GitHub; otherwise auto_merge is null.

Turn it off with:

curl -X DELETE https://diffinery.com/api/v3/repos/acme/website/pulls/12/auto-merge \
  -H "Authorization: Bearer dfy_..."

It returns 204, also when auto-merge was already off. Both endpoints need write access and a write token.

Turn on auto-merge with the CLI

diffinery pr merge --auto turns auto-merge on, with the same flags as a merge, and --disable-auto turns it off:

diffinery pr merge 12 --auto --squash --delete-branch
diffinery pr merge 12 --disable-auto

diffinery pr view 12 lists the merge blockers auto-merge is waiting on.

Turn on auto-merge over MCP

enable_auto_merge takes owner, repo, number, and optionally merge_method, commit_title, commit_message, delete_branch and expected_head_sha. It returns state (enabled, queued or merged), autoMerge (method, commitTitle, commitMessage, enabledBy and enabledAt, or null) and, once merged, mergeCommit. disable_auto_merge takes owner, repo and number and returns {"auto_merge": null}. get_pull_request shows the current setting as issue.pull.autoMerge. Both tools need a write token. See MCP server.

When auto-merge turns itself off

Auto-merge turns itself off when the pull request can never go through as set up, and posts a comment on the pull request saying why, as diffinery-auto-merge[bot]: Auto-merge was turned off: <reason>. The comment notifies the pull request's participants like any other comment.

Reason in the commentWhat happened and what to do
diffy pushed new commits to the branchSomeone other than the one who turned auto-merge on pushed to the head branch, as on GitHub. Review the new commits and turn it on again. Pushes by the same person or agent keep it on.
the pull request was converted to a draftMark it ready for review and turn auto-merge on again.
diffy no longer has write accessThe account that turned it on lost write access to the repository. Someone with write access turns it on again.
agents may not have pull requests merged into main automatically unless its branch protection allows it ... or main has no required status checks: allowAgentAutoMerge needs at least one ...An agent's or workflow token's auto-merge, and the branch rule no longer allows it. See Auto-merge for agents and workflow tokens.
main requires linear history, and auto-merge was set to create a merge commitThe rule started requiring linear history. Turn auto-merge on again with squash or rebase.
rebase merge requires the branch to be up to date with its base (fast-forward only for now)A rebase auto-merge found the head behind the base. Rebase the branch and push, or use squash.
Another merge error, or the merge failedThe merge was attempted and refused for a reason that won't fix itself. Read the message, fix the cause, and turn auto-merge on again.

When the head or base branch moves in the middle of the merge, auto-merge stays on and tries again on the next change. Whoever's action caused the turn-off (the pusher, or the person who converted the pull request to a draft) is recorded as turning it off; otherwise it is diffinery-auto-merge[bot].

Auto-merge for agents and workflow tokens

Agents never merge into the default branch or a protected branch themselves (agents may not merge into this branch; a human must merge). Auto-merge is how they land work there anyway, with CI and people still in control: the agent turns on auto-merge, and Diffinery merges once the branch's rules are met. A repository admin decides per branch rule whether this is allowed; see Agents landing changes for the full rules. In short:

  • The rule needs Allow agents to land pull requests through auto-merge (allowAgentAutoMerge) and at least one required status check. The same applies to agents adding a pull request to a merge queue on such a branch.
  • Approvals are required as for any merge (the rule's Required approvals, and at least one from a person for a pull request an agent opened), but an approval counts only for the commit it approved: if the agent pushes again, a person approves again. The blocker then reads needs 1 more approving review(s) of its latest commit. An admin can waive the approval on green required checks with Let agents' auto-merges land without an approving review (agentAutoMergeWithoutApproval); see Waiving approvals for agents. Requested changes always block.
  • A pull request that adds, changes or deletes a workflow file can't land this way; a person must merge it.
  • A workflow's GITHUB_TOKEN is held to the same rule, even when the run's code didn't come from an agent.
  • On branches that are neither the default branch nor protected, agents and workflow tokens can use auto-merge without any rule.
  • An admin can also name agents in Auto-merge pull requests from these agents (autoMergeAgents) on the rule. Diffinery then turns auto-merge on by itself, as the agent, for each pull request those agents open, mark ready or push to, so neither the agent nor a person has to. Every point above still applies. See Auto-merging agents' pull requests.

A typical agent flow over MCP:

  1. commit_files with create_branch_from to put the change on a new branch.
  2. create_pull_request with an intent.
  3. enable_auto_merge with merge_method: "squash" (not needed where the rule names the agent in autoMergeAgents: get_pull_request then already shows issue.pull.autoMerge with byRule). If the answer is isError: true with agents may not have pull requests merged into main automatically ..., the branch doesn't allow it: ask a person to merge, or an admin to set allowAgentAutoMerge.
  4. Follow CI with list_workflow_runs, and fix failures with more commits. The agent's own pushes keep auto-merge on.
  5. get_pull_request shows issue.pull.autoMerge while it waits, and issue.pull.merged once Diffinery merged it.

Over the REST API the same flow uses PUT /repos/{owner}/{repo}/pulls/{number}/auto-merge with the agent's token.

Auto-merge events and audit log

  • Timeline: turning auto-merge on or off adds an auto_merge_enabled or auto_merge_disabled event to the pull request (GitHub's names), returned by GET /repos/{owner}/{repo}/issues/{number}/events and the timeline. When a branch rule's autoMergeAgents turned it on, the event's actor is diffinery-auto-merge[bot].
  • Webhooks: a pull_request webhook with the action auto_merge_enabled or auto_merge_disabled is sent. The sender is who turned it on or off; when auto-merge turned itself off, it is the pusher, the person who converted the pull request to a draft, or diffinery-auto-merge[bot]. See Webhook events.
  • Workflows and email: these actions don't start pull_request workflows and send no notification emails. The merge itself does everything a merge by hand does (see After the merge).
  • Audit log: each step is recorded with the pull request (acme/website#12) as the target:
ActionWhenDetail
pull_request.auto_merge_enabledAuto-merge is turned on (or changed)base, method, viaAgentRule (true when it relies on allowAgentAutoMerge); with byRule (the branch rule's pattern) and agent when a rule's autoMergeAgents turned it on, by diffinery-auto-merge[bot]
pull_request.auto_merge_disabledAuto-merge is turned off, by someone or by itselfenabledBy, and reason when it turned itself off
pull_request.auto_mergedAuto-merge merged the pull request; a pull.merge entry is written toobase, method, sha
pull_request.auto_merge_enqueuedAuto-merge added the pull request to the merge queuebase, state

See Audit log.

Auto-merge errors

MessageCause and fix
pull request is a draft; mark it ready for review first (422)Mark it ready, then turn on auto-merge.
pull request is closed (422) or pull request is already merged (422)Only open pull requests can auto-merge.
not a pull request (404)The number belongs to an issue.
pull request is already in the merge queue (422)It is queued already; the queue merges it.
merge_method must be merge, squash or rebase (422)Fix merge_method.
main requires linear history; use squash or rebase (422)Pick squash or rebase.
write access required (403)You need write access to the repository.
this token needs the write scope (403)Use a write or admin token.
agents may not have pull requests merged into main automatically unless its branch protection allows it (allowAgentAutoMerge, set by a repository admin); a person must merge (403)The base branch's rule doesn't allow agents (or workflow tokens) to land changes. A person merges, or an admin turns on allowAgentAutoMerge. For a workflow token the message says workflow tokens instead of agents.
main has no required status checks: allowAgentAutoMerge needs at least one, so that CI gates what agents land (422)Add a required status check to the rule.
the pull request changes workflow file `.github/workflows/ci.yml`; CI can't gate a change to itself, so a person must merge it (422)An agent or workflow token relies on allowAgentAutoMerge for a pull request that changes a workflow. A person must merge it.
Resource not accessible by integration (...) (403)A workflow's GITHUB_TOKEN lacks contents: write.
Auto-merge is on but nothing happensA blocker remains. Check the merge box, diffinery pr view 12 or merge_blockers in the API: usually a required check that hasn't run (check the check name), a missing approval, or a change request.
Auto-merge merged before CI finishedOnly required checks are waited for. Add the checks to Required status checks in the branch's rule.

Pull requests opened by agents

When the author is an agent:

  • the pull request needs at least one approval from a person, even on a branch without protection rules, and approvals from agents never count;
  • the agent's owner can approve it, because the owner is a different account from the agent;
  • agents can't merge into the default branch or any protected branch unless its rule has Allow agents to merge pull requests, and even then the human approval is still required;
  • where the rule has Allow agents to land pull requests through auto-merge and a required check, the agent can turn on auto-merge and Diffinery merges once the rules are met; an admin can also waive the human approval for those landings with Let agents' auto-merges land without an approving review;
  • the author is shown with an agent badge and on behalf of its owner, and the API returns "type": "Bot" for it.

Pull requests and workflow tokens

A workflow job's GITHUB_TOKEN can work with pull requests, within its permissions: and these limits, as on GitHub:

  • Permissions. Opening, reviewing and editing pull requests (the /pulls endpoints) need pull-requests: write; conversation comments, labels and assignees (the /issues/{number}/... endpoints) accept issues: write or pull-requests: write. Merging needs contents: write. A request outside the token's permissions gets 403 with Resource not accessible by integration (the workflow's GITHUB_TOKEN needs permissions: pull-requests: write).
  • Opening and approving is off by default. A job token can't open a pull request, approve one or request changes unless a repository admin allows it. Otherwise the request fails with 403 and GitHub Actions is not permitted to create pull requests. (or ... to approve pull requests.), followed by An admin can allow it in the repository's Actions settings ("Allow GitHub Actions to create and approve pull requests"). Comment reviews are always allowed.
  • Its approvals never count. Even where approving is allowed, an approval from a job token doesn't count toward required approvals: only people's approvals do. Its Request changes review still blocks merging.
  • No admin bypass. A job token acts with at most the write role, so branch protection always applies to its merges.
  • No workflow file changes. A job token can't merge a pull request that adds, changes or deletes files under .github/workflows/ or .diffinery/workflows/ (see Merge blockers).
  • Auto-merge. A job token can turn on auto-merge with contents: write. On the default branch or a protected branch it is held to the agent rule (Allow agents to land pull requests through auto-merge and a required check), even when the run's code didn't come from an agent. The later merge keeps the token's permissions from when it was turned on.
  • Code from agents. In a run whose code came from an agent (an agent opened the pull request or pushed its branch last, or started the run, and re-runs of such a run and workflow_run runs that follow it), the job token acts as an agent: it can't merge into the default branch or a protected branch unless the rule has Allow agents to merge pull requests.
  • No new runs. Pull requests a job token opens, pushes or merges don't start pull_request workflows, so workflows can't trigger each other in a loop.

An admin allows workflows to open and approve pull requests on the repository's Settings → Actions tab (check Allow GitHub Actions to create and approve pull requests and click Save), or with the REST API:

curl -X PUT https://diffinery.com/api/v3/repos/acme/website/actions/permissions/workflow \
  -H "Authorization: Bearer dfy_..." -H "Content-Type: application/json" \
  -d '{"can_approve_pull_request_reviews": true}'

GET on the same path returns default_workflow_permissions (read or write) and can_approve_pull_request_reviews (false by default). Changing it needs a person with the admin role (a token with the admin scope), and each change is recorded in the audit log as repo.actions_workflow_settings. See Workflow syntax for permissions:.

Merge when ready and the merge queue

If the base branch's protection rule has Require a merge queue, the merge box shows Merge when ready instead of Merge pull request. Clicking it (or diffinery pr merge 12, PUT /repos/{owner}/{repo}/pulls/{number}/merge-queue, or the merge_pull_request MCP tool) adds the pull request to the queue. Diffinery tests it together with the pull requests ahead of it on a merge group branch and merges it once the required checks pass there. The merge box then shows Queued to merge, its position, and a Remove from queue button.

The pull request must already have no blockers to join the queue. Pushing to its branch removes it from the queue. To have it join the queue by itself once it has none, turn on auto-merge. See Merge queue for the full behaviour.

Lock the conversation

Locking a pull request limits comments, reviews, line comments and reactions to people with triage access or higher. Lock and unlock with the issue endpoints (a pull request is an issue):

curl -X PUT https://diffinery.com/api/v3/repos/acme/website/issues/12/lock \
  -H "Authorization: Bearer dfy_..." -H "Content-Type: application/json" \
  -d '{"lock_reason": "resolved"}'
curl -X DELETE https://diffinery.com/api/v3/repos/acme/website/issues/12/lock -H "Authorization: Bearer dfy_..."

Both need triage access and return 204. lock_reason is optional and one of off-topic, too heated, resolved or spam. The pull request then reports "locked": true and active_lock_reason. Others get 403 with conversation is locked. Merging is not affected. The web UI doesn't show a lock control or badge yet. See Issues.

Reactions

Add emoji reactions to a pull request, a conversation comment or a line comment through the REST API, the same way as on issues: POST /repos/{owner}/{repo}/issues/{number}/reactions, POST /repos/{owner}/{repo}/issues/comments/{id}/reactions or POST /repos/{owner}/{repo}/pulls/comments/{id}/reactions with {"content": "+1"}. See Issues for the allowed values and rules.

Events and timeline

Changes to a pull request are recorded as events: labeled, unlabeled, assigned, unassigned, renamed, closed, reopened, locked, unlocked, review_requested, review_request_removed, review_dismissed, ready_for_review, convert_to_draft, auto_merge_enabled, auto_merge_disabled and merged, with GitHub's names. (The pull_request webhook action for converting to a draft is converted_to_draft, as on GitHub.) Read them with GET /repos/{owner}/{repo}/issues/{number}/events, or read events, comments and reviews in time order with GET /repos/{owner}/{repo}/issues/{number}/timeline. See Issues.

Close and reopen

  • Web UI: Close pull request at the bottom of the conversation (posting any comment you typed), and Reopen on a closed one.
  • REST API: PATCH /repos/{owner}/{repo}/pulls/{number} with {"state": "closed"} or {"state": "open"}. The same endpoint changes title, body and draft.
  • MCP: update_issue with state.

Changing the title or description (in the web UI, with PATCH, or with update_issue) sends a pull_request webhook event with the action edited and runs pull_request workflows whose types include edited. Saving the same text again sends nothing.

Closing runs pull_request workflows whose types include closed, and removes the pull request from a merge queue. Reopening runs pull_request workflows with the reopened type. A merged pull request can't be reopened: a merged pull request cannot be reopened. Only the author or someone with triage or higher can close or reopen.

Pull request refs

Like GitHub, every pull request has two read-only refs:

RefPoints at
refs/pull/<n>/headThe pull request's head commit
refs/pull/<n>/mergeThe test merge commit: parents [base, head], authored by Diffinery <noreply@diffinery.com>. Removed while the pull request conflicts.

They are refreshed when the pull request is opened, pushed to, reopened or closed. Fetch them with git:

git fetch origin refs/pull/12/head:pr-12
git fetch origin +refs/pull/12/merge:refs/remotes/origin/pr/12/merge

They can't be pushed: refs/pull/ is managed by Diffinery for pull requests. They are also hidden from pushes, so git push --mirror leaves them alone.

Find pull requests

The Pull requests tab lists pull requests with the same filters as the issue list: open or closed, search, label and author. See Issues.

diffinery pr list --state merged --author diffy --limit 20
diffinery pr list --head fix-pricing-link
diffinery pr view 12 --comments
diffinery pr diff 12 --name-only

pr list --state accepts open (default), closed, merged or all, and pr list --head BRANCH lists only pull requests from that branch.

As with gh, the pull request number is optional for pr view, pr checks, pr diff, pr comment, pr review, pr ready and pr merge: without it, the CLI uses the open pull request whose head is your current git branch, in the repository of your origin remote (or -R OWNER/NAME). With no current branch (a detached HEAD) or no open pull request for it, the command fails and asks for a number. The list_pull_requests and get_pull_request MCP tools do the same (get_pull_request with include_diff: true adds the diff).

The REST API is GET /repos/{owner}/{repo}/pulls. Closed results include merged ones. Results come back as a page, with a Link header for the next pages and x-total-count with the total.

ParameterMeaning
stateopen (default), closed or all
headHead branch, as branch or owner:branch (the owner part is ignored)
baseBase branch
sortcreated (default), updated, or popularity (by number of comments)
directiondesc (default) or asc
labelsComma-separated label names; a pull request must have all of them
assigneeA login, none (no assignees) or * (any assignee)
creatorAuthor login
sinceOnly pull requests updated at or after this ISO 8601 time
qText to find in titles and descriptions
per_page, pagePaging: up to 100 per page, default 30

To search across repositories with qualifiers, use GET /search/issues with is:pr, for example q=repo:acme/website is:pr is:open base:main. Supported qualifiers are listed in Issues.

To find the pull requests a commit belongs to, use GET /repos/{owner}/{repo}/commits/{sha}/pulls. It returns pull requests whose head commit or merge commit is that commit, and open pull requests (among the 50 most recently opened) whose head branch contains it. An unknown commit returns 422 with No commit found for SHA: <sha>.

diffinery pr comment 12 --body "..." adds a conversation comment.

Differences from GitHub

  • Pull requests are between branches of the same repository; there are no forks.
  • The intent can't be edited after opening.
  • Draft state changes through PATCH /pulls/{number} with draft (GitHub offers this only in GraphQL).
  • An admin's merge past unmet branch rules must be asked for with bypass_rules, and skipping a merge queue with bypass_merge_queue; without them an admin is refused like anyone else. A merge commit is refused on a branch that requires linear history unless bypass_rules is sent.
  • Reviews are submitted immediately; there are no pending reviews, suggested changes, or thread resolution yet.
  • rebase outside the merge queue is a fast-forward only.
  • update-branch exists in the REST API only (merge method only, no rebase); there is no Update branch button and no web conflict editor.
  • Dismissing a review needs maintain access, and the pull request's author and agents can't do it.
  • Pull requests opened by agents always need a human approval, and agent approvals never count.
  • GET /pulls/{number}/merge answers GitHub clients with 204 or 404, and other clients with a mergeability report.
  • mergeable_state never reports behind, unstable or has_hooks.
  • Requested reviewers are accounts only (no teams).
  • Locking, reactions, review dismissal and repository-wide comment lists have no web UI yet.
  • Auto-merge is available in every repository, with no repository setting to allow it first, and has REST endpoints (PUT and DELETE /pulls/{number}/auto-merge) where GitHub uses GraphQL. It waits only for required checks, never deletes the head branch, and its auto_merge_enabled and auto_merge_disabled actions don't start pull_request workflows. The CLI has no --auto flag.
  • Agents land changes on protected branches through auto-merge only where a branch rule allows it (allowAgentAutoMerge), which has no GitHub equivalent.
  • See also Merge queue.

Troubleshooting

MessageCause and fix
pull request is not mergeable: ...The merge has blockers, listed after the colon. See Merge blockers. If it ends with (as an admin you can bypass with bypass_rules), you can merge anyway with an admin bypass.
main requires linear history; use squash or rebase (405)You asked for a merge commit on a branch that requires linear history. Use squash or rebase (diffinery pr merge 12 --squash).
only the author or someone with write access can change draft status (403)Only the author or someone with write access can mark a draft ready or convert it back.
GitHub Actions is not permitted to approve pull requests. ... (403)A workflow's GITHUB_TOKEN tried to approve or request changes. An admin can allow it; see Pull requests and workflow tokens.
Resource not accessible by integration (...) (403)A workflow's GITHUB_TOKEN lacks the permission named in the message. Add it to the job's permissions:.
repository is archived (read-only) (403)The repository is archived. An admin must unarchive it first.
authentication required (401)You sent no credentials. Every change needs a signed-in caller, even on a public repository.
this token needs the write scope (403)Your token has the read scope. Create one with write (or admin).
head branch was modified; review and try again (409)Someone pushed after you loaded the page or read the head SHA. Review the new commits and merge again.
base branch moved during merge; try again (409)The base changed while merging. Retry.
merge conflicts (405)The head conflicts with the base. Resolve and push.
rebase merge requires the branch to be up to date with its base (fast-forward only for now)Rebase the head onto the base and push, or use merge or squash.
you cannot approve or request changes on your own pull requestAsk someone else to review; you can leave a Comment review.
write access is required to approve or request changesApproving needs write access.
a comment review needs a body or line commentsAdd a review body or at least one line comment.
pull request is closed (when reviewing)Only open pull requests can be reviewed.
a merged pull request cannot be reopenedOpen a new pull request instead.
conversation is locked (403)The pull request is locked. Only triage or higher can comment, review or react.
path and line are requiredA new line comment needs path and line (or in_reply_to for a reply).
team reviewers are not supported; request people by loginRequest reviewers by login in reviewers.
main requires a merge queue: add the pull request to the queueUse Merge when ready, diffinery pr merge, or the merge queue API.
Auto-merge was turned off: ... (a comment)Auto-merge can't go through as set up. See When auto-merge turns itself off.
agents may not have pull requests merged into main automatically ...An agent or workflow token turned on auto-merge for a branch that doesn't allow it. See Auto-merge errors.
No pull_request runs appearThe pull request conflicts with its base (no test merge commit), or the workflow's on.pull_request filters exclude it.