Opt-in via new 'dependabot-auto-approve' input (mode 'dev-patch-minor'). When enabled, Dependabot PRs are evaluated on pull_request_target opened/reopened and on check_suite completed for dependabot/ branches, and approved + labeled + auto-merged only when ALL gates hold: 1. Author is the real dependabot[bot] app (login AND user id 49699333) 2. Same-repo head on a dependabot/ branch (never forks) 3. Every commit signature-verified and dependabot-authored 4. Changed files a subset of 'dependabot-allowed-files' (exact paths) 5. Every updated dependency direct:development with semver-patch/minor update per Dependabot's commit metadata (missing metadata = no approve) 6. CI green on head SHA: all check runs success/neutral/skipped with at least one non-skipped check, combined status success if any exist 7. PR open, non-draft, conflict-free, not on hold Every gate fails closed, logging the reason to the action log only. Approvals use a distinct marker (slash-commands-dependabot-auto) that push invalidation now dismisses alongside /lgtm approvals; after a human push, gate 3 fails permanently so only /lgtm can accept the PR. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ygNtiNcAaqTRnQjHCDkVS
19 KiB
slash-commands
Reusable composite action that implements Prow-style /lgtm, /hold, and
/stageblog slash commands on pull requests, gated by team membership and
CODEOWNERS. Optionally auto-approves narrowly-scoped Dependabot PRs
(dependabot-auto-approve).
What it does
Listens for PR comments and push events, then:
/lgtm— core maintainers only (always-allow-teams). Adds anacceptedlabel, submits an APPROVE review on behalf of the commenter, and enables auto-merge. The PR merges automatically once all required checks pass. CODEOWNERS does not grant/lgtmauthorization. Optionally removes staging labels (remove-labels-on-accept) and moves the PR to an Accepted status in a configured org Project (project-number)./lgtm cancel— removes theacceptedlabel, dismisses the approval, and disables auto-merge./lgtm force— same as/lgtm, but only members offorce-allow-teams(default: core + lead maintainers) may use it, and it bypasses the self-approval guard. Intended as an escape hatch for maintainers who need to approve their own PRs. The review body notes the approval came via/lgtm force./hold— adds ado-not-merge/holdlabel. Core maintainers or CODEOWNERS of the PR's changed files may use this./hold cancelor/unhold— removes the hold label./stageblog— if the PR touches any blog paths (defaultblog/**), dispatches a caller-definedworkflow_dispatchworkflow passing the PR number and the PR's head SHA at the time of the comment. Intended for letting maintainers manually stage blog previews for fork PRs (where the normalpull_request-triggered preview workflow can't access secrets). Disabled unlessstageblog-workflowis set.- New commits pushed — removes the
acceptedlabel, dismisses the approval, disables auto-merge, and posts a brief comment asking for re-approval. Exception: if theacceptedlabel was most recently applied manually by a member ofalways-allow-teamsorforce-allow-teams(rather than by the bot via/lgtm), it is treated as a sticky governance decision and left in place — nothing is removed or dismissed.
Commands are parsed from the first line of the comment (case-insensitive,
must match ^/cmd\b). Unauthorized attempts, or a PR author trying to /lgtm
their own PR, receive a 👎 reaction with no further noise. Successful commands
receive a 👍 reaction.
Dependabot auto-approve (opt-in)
When dependabot-auto-approve: dev-patch-minor is set, the action evaluates
Dependabot PRs on pull_request_target (opened/reopened) and on every
check_suite completion for a dependabot/ branch, and — when the PR passes
all of the gates below — adds the accepted label, submits a bot APPROVE
review (marker <!-- slash-commands-dependabot-auto -->), and enables
auto-merge, exactly like /lgtm. Any gate failure exits without approving
(fail closed) and logs the reason to the action log only:
- Real Dependabot — PR author login is
dependabot[bot]and the user id is49699333(the Dependabot app's stable id). A lookalike account can never match. - Same-repo head — the head branch lives in the base repo (never a
fork) and starts with
dependabot/. - Trusted commits — every commit on the PR has a verified signature
and is authored by
dependabot[bot]. - File allow-list — the changed files are a subset of
dependabot-allowed-files(exact paths; defaultpackage.json,package-lock.json). - Dependency scope — Dependabot's structured commit-message metadata
(
updated-dependencies:block) must show every updated dependency asdependency-type: direct:developmentwithupdate-type: version-update:semver-patchor…semver-minor. Missing or unparseable metadata never approves. (The metadata is trustworthy at this point because gate 3 proved the commits are dependabot-authored and signed.) - CI green — every check run on the head SHA is completed with
conclusion
success/neutral/skipped, at least one non-skipped check exists, and the combined commit status (if any statuses exist) issuccess. Pending checks exit silently — the nextcheck_suitecompletion re-evaluates. This gate lives in the action so approval itself is withheld until CI passes, even when branch protection has no required status checks. - PR state — open, not a draft, no merge conflicts, and not carrying the hold label.
Push invalidation applies to these approvals too: any push to the PR (human
or Dependabot rebase) dismisses the auto-approval, removes the label, and
disables auto-merge. If Dependabot pushed (e.g. a rebase), the PR is
re-evaluated when its checks complete and can be re-approved. If a human
pushed, gate 3 fails permanently for that PR — every subsequent evaluation
sees a non-dependabot commit — so the PR can only proceed via a maintainer's
/lgtm. That one-way trapdoor is intentional.
Enabling the mode requires two extra caller-workflow triggers — see Caller workflow requirements.
Merge gate
The merge gate is GitHub's native required-reviews rule plus
auto-merge. Configure branch protection to require at least one approving
review; /lgtm satisfies that via the App's approval and turns on auto-merge.
The PR merges as soon as CI passes. Manually adding the accepted label in
the GitHub UI has no effect — the gate is the review, not the label.
Important
If your branch protection enables Require review from Code Owners, the App's approval will not satisfy it — GitHub Apps cannot be members of org teams, so they cannot be code owners via a team entry. Auto-merge also does not inherit the bypass privileges of the App that enabled it. To make
/lgtmwork under this rule, add the GitHub App to the ruleset's bypass list (Settings → Rules → Rulesets → your ruleset → Bypass list → add the App with mode Always). The action detects this case and falls back to a direct merge, which does use the App's bypass.
Prerequisites
- A GitHub App installed on the caller repo with the permissions listed below. The App's identity is what approves PRs and enables auto-merge.
- Allow auto-merge enabled in the caller repo (Settings → General → Pull Requests → Allow auto-merge).
- The
acceptedanddo-not-merge/holdlabels must already exist in the caller repo (the action does not auto-create them). Suggested colors:accepted→#0e8a16,do-not-merge/hold→#d93f0b. - A CODEOWNERS file at
.github/CODEOWNERS(or pass a different path viacodeowners-path) if you want CODEOWNERS to grant/hold//stageblog. Not required for/lgtm.
Required secrets
The action runs as a GitHub App so its approvals satisfy branch protection's required-reviews rule without depending on the "Allow GitHub Actions to create and approve pull requests" repo setting.
| Repo secret name | Value |
|---|---|
MCP_COMMANDER_APP_ID |
The App's numeric ID (shown on the App's General settings page) |
MCP_COMMANDER_APP_KEY |
The App's private key — full contents of the downloaded .pem file, including the -----BEGIN/END----- lines |
Creating the GitHub App
Org Settings → Developer settings → GitHub Apps → New GitHub App. Configure:
| Section | Setting |
|---|---|
| Webhook | Disable (uncheck "Active") — the App is just an identity |
| Repository permissions → Pull requests | Read & write |
| Repository permissions → Contents | Read-only |
| Repository permissions → Actions | Read & write (only if /stageblog is enabled) |
| Organization permissions → Members | Read-only |
| Where can this GitHub App be installed? | Only on this account |
After creating:
- Install — left sidebar → Install App → pick the caller repo(s)
- App ID — copy from the top of General settings → save as
MCP_COMMANDER_APP_IDsecret - Private key — scroll to bottom → Generate a private key →
copy
.pemcontents → save asMCP_COMMANDER_APP_KEYsecret
Why an App and not a PAT? App installation tokens are short-lived (auto-minted per workflow run via
actions/create-github-app-token), are not tied to any individual's account, and their approvals always count toward required reviews. The automaticGITHUB_TOKENlacksread:organd its approvals are gated by a separate repo setting that may be disabled org-wide.
Caller workflow requirements
| Triggers | issue_comment (types: created) + pull_request_target (types: synchronize). With dependabot-auto-approve enabled, also pull_request_target types opened, reopened + check_suite (types: completed) — gate the check_suite job on startsWith(github.event.check_suite.head_branch, 'dependabot/') to avoid a run per suite repo-wide. |
| Permissions | contents: read is sufficient — the App token handles all writes. |
| Fork-PR safety | No special guard needed — issue_comment and pull_request_target both run in the base repo's context with the base workflow definition, so fork authors cannot modify the logic. CODEOWNERS is also fetched from the PR's base ref, never the head. |
| No checkout | The action calls GitHub API only. Do not actions/checkout PR code. |
Usage
name: Slash Commands
on:
issue_comment:
types: [created]
pull_request_target:
types: [synchronize]
permissions:
contents: read
jobs:
handle:
if: >-
(github.event_name == 'issue_comment' && github.event.issue.pull_request) ||
github.event_name == 'pull_request_target'
runs-on: ubuntu-latest
steps:
- uses: actions/create-github-app-token@v2
id: app-token
with:
app-id: ${{ secrets.MCP_COMMANDER_APP_ID }}
private-key: ${{ secrets.MCP_COMMANDER_APP_KEY }}
- uses: modelcontextprotocol/actions/slash-commands@main
with:
github-token: ${{ steps.app-token.outputs.token }}
# auto-merge-method: squash # default; also: merge, rebase
# stageblog-workflow: stage-blog.yml # uncomment to enable /stageblog
# dependabot-auto-approve: dev-patch-minor # uncomment to enable — also add
# pull_request_target types [opened, reopened] and check_suite [completed]
# triggers above (see Caller workflow requirements)
Branch protection settings for this flow: enable Require a pull request before merging → Require approvals (1), and Allow auto-merge in repo Settings. The App's APPROVE review satisfies the requirement and auto-merge handles the rest.
Enabling /stageblog — companion workflow
When stageblog-workflow is set, you also need a separate workflow in the
caller repo that receives the dispatch. Example stage-blog.yml:
name: Stage Blog (fork PRs)
on:
workflow_dispatch:
inputs:
pr_number:
required: true
type: string
head_sha:
required: true
type: string
permissions:
contents: read
pull-requests: write
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
ref: ${{ inputs.head_sha }} # pinned — do NOT re-fetch the PR head
# ... your blog build steps → output to blog/public ...
- uses: modelcontextprotocol/actions/cloudflare-pages-preview/deploy@main
with:
directory: blog/public
project-name: mcp-blog-preview
api-token: ${{ secrets.CF_PAGES_PREVIEW_API_TOKEN }}
account-id: ${{ secrets.CF_PAGES_PREVIEW_ACCOUNT_ID }}
branch: pr-${{ inputs.pr_number }}
pr-number: ${{ inputs.pr_number }}
commit-sha: ${{ inputs.head_sha }}
comment-title: "Blog Preview (staged via /stageblog)"
comment-marker: "<!-- stage-blog-comment -->"
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
github-token |
✅ | — | GitHub App installation token used for everything: team-membership checks, labels, comments, reactions, APPROVE review, auto-merge, CODEOWNERS fetch, workflow dispatch. Must have Organization Members:read + Repository Pull requests:write + Contents:read (+ Actions:write for /stageblog). Default GITHUB_TOKEN will not work. |
approved-label |
accepted |
Label added by /lgtm to mark the PR as accepted |
|
hold-label |
do-not-merge/hold |
Label added by /hold |
|
always-allow-teams |
core-maintainers |
Comma-separated team slugs (in the repo's org) whose members can /lgtm any PR. Only these teams can /lgtm — CODEOWNERS does not grant it. Members may also /hold//stageblog any PR. |
|
force-allow-teams |
core-maintainers,lead-maintainers |
Comma-separated team slugs whose members may use /lgtm force (bypasses self-approval guard). CODEOWNERS does not grant this — only team membership. |
|
codeowners-path |
.github/CODEOWNERS |
Path to CODEOWNERS, fetched from the PR's base ref (never head — tamper-proof). Grants /hold and /stageblog only, not /lgtm. |
|
invalidate-on-push |
'true' |
Remove accepted label + dismiss approval + disable auto-merge + comment when new commits are pushed. Skipped if the label was most recently applied manually by an always-allow-teams or force-allow-teams member (not the bot via /lgtm). Set 'false' to keep approval across pushes unconditionally. |
|
submit-review |
'true' |
Submit an APPROVE review alongside the label on /lgtm. Set 'false' to use label only. |
|
enable-auto-merge |
'true' |
Enable auto-merge after /lgtm (disabled on /lgtm cancel and push invalidation). Requires repo setting "Allow auto-merge". |
|
auto-merge-method |
squash |
Merge method for auto-merge: squash, merge, or rebase |
|
remove-labels-on-accept |
(empty) | Comma-separated label names to remove when /lgtm accepts (e.g. in-review,draft,proposal). Missing labels are ignored. |
|
project-number |
(empty) | Org-level Project (V2) number to update on accept. Empty = disabled. Adds the PR to the project if not already present. Requires the App to have Organization Projects: write. | |
project-status-field |
Status |
Name of the single-select field to set on accept. | |
project-accepted-option |
Accepted |
Name of the option to set in the status field on accept. | |
project-gate-label |
(empty) | If set, only update the project status when the PR has this label (e.g. SEP). Empty = update for every accepted PR. Label removal via remove-labels-on-accept is not gated — it always runs. |
|
stageblog-workflow |
(empty) | Workflow file name (e.g. stage-blog.yml) to dispatch when /stageblog is invoked. Empty = command disabled. The workflow must accept pr_number and head_sha string inputs. |
|
stageblog-paths |
blog/** |
Comma-separated CODEOWNERS-style glob patterns. /stageblog is refused if no changed file matches. |
|
dependabot-auto-approve |
(empty) | Dependabot auto-approve policy. Empty = disabled. dev-patch-minor = approve + label + auto-merge Dependabot PRs that pass all gates (see Dependabot auto-approve). Any other value disables the mode (fail closed). Requires the extra caller triggers listed above. |
|
dependabot-allowed-files |
package.json,package-lock.json |
Comma-separated exact file paths a Dependabot PR may touch to qualify for auto-approve. Any other changed file blocks auto-approval. |
Outputs
| Output | Description |
|---|---|
result |
One of: lgtm-added, lgtm-forced, lgtm-removed, hold-added, hold-removed, invalidated, invalidate-skipped, unauthorized, force-unauthorized, self-lgtm-blocked, stageblog-dispatched, stageblog-not-blog, stageblog-disabled, dependabot-approved, dependabot-skipped, noop |
actor |
Login of the commenter (empty for non-comment triggers) |
CODEOWNERS pattern support
The inline CODEOWNERS parser handles the patterns currently used in MCP repos:
| Pattern | Supported | Meaning |
|---|---|---|
/path/ |
✅ | Anchored directory prefix (matches anything under path/) |
/path/to/file.ext |
✅ | Exact anchored path |
*.ext |
✅ | Any file with extension .ext at any depth |
/path/**/*.ext |
✅ | Glob at any depth under /path/ |
**/file |
✅ | File named file at any depth |
@user / @org/team |
✅ | Owner: individual user or team (team org must match repo org) |
!negation |
❌ | Not supported (not in use in MCP repos) |
Escaped spaces (\ ) |
❌ | Not supported |
Last-match-wins per file, consistent with GitHub's native CODEOWNERS semantics.
Security notes
- Comment body injection — command parsing is a fixed regex on the first line in JS; comment text is never interpolated into a shell.
- CODEOWNERS tampering — fetched from
pr.base.ref, never the PR head. An attacker cannot add themselves as CODEOWNER in the PR under review. - Workflow modification —
issue_commentandpull_request_targetrun from the default-branch workflow definition. The action does not check out PR code. - Self-approval — explicitly blocked before the auth check. The
/lgtm forceescape hatch bypasses this, but is gated strictly toforce-allow-teamsmembership (CODEOWNERS does not grant it) and the review body records thatforcewas used. - Manual label bypass — the merge gate is the App's APPROVE review, which
is only submitted by the
/lgtmhandler after the team-membership check passes. Adding theacceptedlabel via the GitHub UI is cosmetic. /stageblogSHA pinning — the dispatch passespr.head.shacaptured at the moment the maintainer comments. The companion workflow must check outinputs.head_sha(notrefs/pull/N/headorpr.head.ref) so a fork author cannot push new commits between the maintainer's review and the build.- Review provenance — the review body always names the real approver ("on behalf of @login"), even though GitHub shows the review as App-authored.
- Dependabot auto-approve — off by default; every gate fails closed. The
author check pins the Dependabot app's numeric user id, not just the login;
fork heads are rejected outright; commit trust requires verified signatures
on dependabot-authored commits; the file allow-list is exact-path; missing
update metadata never approves; and approval waits for green CI inside the
action, so it holds even without required status checks in branch
protection. A human push onto a dependabot branch permanently disqualifies
the PR from auto-approval (gate 3) — only
/lgtmcan accept it after that.