Skip to content

feat: support the merge_group event for merge queue checks - #1894

Open
imichaelmoore wants to merge 6 commits into
anthropics:mainfrom
imichaelmoore:claude/project-thread-o77agd
Open

imichaelmoore wants to merge 6 commits into
anthropics:mainfrom
imichaelmoore:claude/project-thread-o77agd

Conversation

@imichaelmoore

@imichaelmoore imichaelmoore commented Oct 10, 2026 •

Copy link
Copy Markdown

Before: a workflow triggered by a merge queue (on: merge_group) fails immediately with "Unsupported event type: merge_group".

After: merge_group runs in agent mode with a prompt, like workflow_dispatch and schedule, so the action can serve as a merge queue check.

How

  • merge_group joins the automation events. It needs a prompt, and track_progress is rejected as it is for the others.
  • The merge group commit contains the queued pull requests' changes, so .claude/, .mcp.json, CLAUDE.md and the other sensitive paths are restored from the target branch before the CLI starts, the same protection pull requests get. Choosing the restore branch moved from run.ts into a tested resolveRestoreBase(). Pull request behavior is unchanged.
  • The restore now names the remote-tracking ref in full (refs/remotes/origin/<base>), so a tag or local branch called origin/<base> cannot be picked instead. This applies to pull request events too.
  • There is no special case for actors. GitHub attributes a merge_group run to whoever queued the pull request, and github-merge-queue[bot] is only the actor of the queue's later push to the target branch. GitHub's own github/docs repository relies on this in .github/workflows/restrict-merge-queue.yml. So a person passes the bot check as usual, and an app that queues pull requests needs an allowed_bots entry.
  • examples/merge-queue-check.yml writes the group's diff to a file, gives Claude only Read, Grep and Glob (--restricted --strict-mcp-config --tools), passes the read-only workflow token, and fails the job unless Claude returns block_merge: false.
  • Docs: a merge queue section in docs/custom-automations.md and a "Using this action with merge_group" section in docs/security.md.

Test plan

bun run typecheck, bun run format:check and bun test (987 pass) on this branch merged with main. actionlint passes on the example.

Ran src/entrypoints/run.ts with a simulated merge_group payload, a temp repo with a local origin, the pinned Claude Code CLI and a mock API:

  • main fails with "Unsupported event type: merge_group". This branch reaches the CLI with the target branch's config restored and the queued versions in .claude-pr/.
  • With the example's arguments the session has Read, Grep, Glob and StructuredOutput only, in both default and auto permission mode. Bash, Write, Edit, WebFetch, Skill and Agent calls are refused, reads outside the workspace are refused, and no hook or .mcp.json server starts.
  • A person as actor passes. An app is rejected without allowed_bots and passes with it.
  • With a tag or a local branch named origin/main, both the example's diff and the restore still use the target branch.
  • A shallow single-ref checkout and a target branch with a slash in its name both restore correctly. track_progress and an invalid base_ref are rejected.

Not verified: a live merge queue run, or the GitHub App token exchange for this event (the example passes github_token, which bypasses it).

claude added 2 commits October 5, 2026 01:40
Workflows triggered by a merge queue failed with "Unsupported event type:
merge_group". The event now runs in agent mode like the other automation
events.

- GitHub reports these runs as github-merge-queue[bot]; the human-actor
  check accepts that actor for merge_group without an allowed_bots entry.
- The merge group commit carries the queued pull requests' changes, so
  Claude's config paths are restored from the target branch, as on PRs.
  The choice of restore branch moves into a tested resolveRestoreBase().
- Adds docs, an example merge queue check, and a project verify skill.
The example now passes the workflow token, so a queued PR that edits the
workflow is no longer skipped by the App token exchange, and Claude stays
within the job's read-only permissions. It reviews a diff written before
the action runs, taken against the target branch so entries ahead in the
queue are covered, and it no longer allows Bash.

- security.md lists the merge queue app under Bot User Control and covers
  merge_group alongside pull_request_target and workflow_run.
- The base ref test now proves validation runs after refs/heads/ is
  stripped.
- Drops the local verify skill, which is unrelated to this change.
The example wrote its diff with settings taken from the checkout, which
is the queued pull requests' own content.

- A .gitattributes entry marking a file -diff or binary reduced it to
  "Binary files differ", hiding its content from the review. Attributes
  now come from the target branch via --attr-source, and the prompt asks
  Claude to open any binary file that looks like it should be text.
- `ignore = all` in .gitmodules hid submodule pointer changes, so the
  diff passes --ignore-submodules=none.
- A committed merge-group.diff symlink redirected the write. The path is
  removed first.
- Quoting the reason on one line stopped `::` workflow commands but not
  the legacy `##[` form, which the runner matches anywhere in a line.
  Commands are now stopped with a random token while the reason prints.
- security.md listed merge_group under checkout guidance that is wrong
  for it: with no `ref:` the checkout is the merge group, not the base
  branch. It now has its own section, which says what the example relies
  on, that the hook caveat applies to merge groups too, and how to use
  the subdirectory pattern for stricter separation. The original heading
  and its anchor are restored.
- The docs no longer claim every merge_group run is attributed to the
  merge queue app, and say what happens for any other actor.
- The allowed_bots descriptions said the default allows no bots. They
  now say the merge queue app is accepted on merge_group events without
  an entry. The longer cell re-pads the inputs table in usage.md.
- The example job sets timeout-minutes so a stalled run cannot hold up
  the queue.
GitHub attributes a merge_group run to whoever queued the pull request,
not to github-merge-queue[bot]. That app is only the actor of the queue's
push to the target branch. Drop the special case for it, so a person
passes the bot check as usual and an app that queues pull requests needs
an allowed_bots entry. action.yml, docs/usage.md, constants.ts and
actor.ts are back to what main has.

Example:
- --allowedTools only pre-approves tools. Use --tools to set which tools
  the session has, --restricted to keep the file tools in the workspace
  and ignore settings files, and --strict-mcp-config to skip .mcp.json.
- Diff against refs/remotes/origin/<base>, so a tag called origin/<base>
  cannot be picked instead of the target branch.
- Block text-looking files that git reports as binary instead of reading
  them from the working tree, where restored paths hold the target
  branch's copy.
- Add notes on allowed_bots, large diffs, the timeout, and the workflow
  file being read from the merge group commit.

restoreConfigFromBase names the remote-tracking ref in full as well, for
the same reason as the example.

Docs: rewrite the merge_group sections of security.md and
custom-automations.md to match, including what the subprocess env scrub
needs on the runner and what the restore does not cover.

Tests: the merge_group actor cases set their inputs explicitly, the
restore has cases for a shadowing tag and local branch, and the event
guards have more negative cases.
…eue check

Restore:
- Fetch the base branch with both sides of the refspec spelled out, so a
  successful fetch always leaves refs/remotes/origin/<base> at the branch's
  tip, whatever the configured refspec is and even if the remote has a tag
  of the same name. --no-tags keeps the fetch from pulling tags in.
- Check out refs/remotes/origin/<base>^{commit}. git checkout tries its
  argument as a branch name first, and that form cannot be one.

Merge queue example:
- Turn includeGitInstructions off. Claude Code otherwise opens the
  conversation with git status output and recent commit subjects, which
  queued pull requests write, ahead of the prompt.
- End the diff's revisions with --, treat Git LFS pointers like binary
  files, and keep secret values out of the printed reason.
- Say that any failure of the review step fails the check, and when to make
  the check required.

Docs: leave --add-dir out of the merge group layout, mention that a queued
skill can carry hooks, and say what makes a CODEOWNERS entry binding.

Tests: the restore cases now start with no local base branch, no
remote-tracking branch and a refspec that leaves it out, and cover a branch
named after the full ref and a remote tag named after the base branch. The
actor tests pin that github-merge-queue[bot] gets no special case.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants