Building a slash-command bot for pull requests Jump to heading
Some actions on a pull request are needed occasionally and do not deserve a button in every workflow: re-run the flaky integration suite, rebase on the latest main, deploy a preview, backport to a release branch. Teams end up either doing them by hand from a terminal or adding special labels whose meaning nobody remembers. A slash-command bot lets maintainers type /retest or /backport release/2.4 in a pull request comment and have automation do it. It is a few dozen lines of workflow — but because it acts on comments anyone can write, it is also an easy way to hand privileges to strangers if permissions and inputs are not checked carefully. This page builds a safe one, within pull request automation and bots.
When to use this approach Jump to heading
- Maintainers regularly perform the same few manual actions on pull requests.
- You want those actions available to people without terminal access to the repository.
- You need an audit trail of who triggered what, visible on the pull request.
- Manual workflows already exist and need a friendlier front end, such as manual dispatch workflows with inputs.
Step 1 — Listen to comments and parse the command Jump to heading
Pull request comments arrive as issue_comment events. Filter to comments on pull requests that start with a slash, and extract the command and its arguments.
# .github/workflows/slash-commands.yml
on:
issue_comment:
types: [created]
permissions: { contents: read, pull-requests: write, actions: write }
jobs:
command:
if: github.event.issue.pull_request && startsWith(github.event.comment.body, '/')
runs-on: ubuntu-latest
steps:
- id: parse
env: { BODY: "${{ github.event.comment.body }}" }
run: |
first=$(printf '%s\n' "$BODY" | head -1 | tr -s ' ')
cmd=$(printf '%s' "$first" | cut -d' ' -f1 | tr -d '/')
arg=$(printf '%s' "$first" | cut -s -d' ' -f2)
echo "cmd=$cmd" >> "$GITHUB_OUTPUT"; echo "arg=$arg" >> "$GITHUB_OUTPUT" The comment body is untrusted text. It is read through an environment variable, never interpolated into the script, for the same reason as manual-dispatch inputs.
Step 2 — Check that the commenter is allowed Jump to heading
Anyone who can comment can type a command, including outside contributors on public repositories. Check the commenter’s permission on the repository before doing anything.
- id: perm
run: |
level=$(gh api "repos/$GITHUB_REPOSITORY/collaborators/$ACTOR/permission" --jq .permission)
case "$level" in admin|maintain|write) echo "ok=true" >> "$GITHUB_OUTPUT" ;;
*) echo "ok=false" >> "$GITHUB_OUTPUT" ;; esac
env: { GH_TOKEN: "${{ github.token }}", ACTOR: "${{ github.event.comment.user.login }}" }
- if: steps.perm.outputs.ok != 'true'
run: gh pr comment "$PR" --body "@$ACTOR slash commands are available to maintainers only."
&& exit 1
env: { GH_TOKEN: "${{ github.token }}", ACTOR: "${{ github.event.comment.user.login }}", PR: "${{ github.event.issue.number }}" } Step 3 — Dispatch only allow-listed commands Jump to heading
Map each command to a fixed action. Validate arguments against strict patterns or lists. Unknown commands get a helpful reply rather than silence.
- if: steps.perm.outputs.ok == 'true'
env:
GH_TOKEN: ${{ github.token }}
CMD: ${{ steps.parse.outputs.cmd }}
ARG: ${{ steps.parse.outputs.arg }}
PR: ${{ github.event.issue.number }}
run: |
head=$(gh pr view "$PR" --json headRefName --jq .headRefName)
gh api -X POST "repos/$GITHUB_REPOSITORY/issues/comments/${{ github.event.comment.id }}/reactions" -f content=eyes >/dev/null
case "$CMD" in
retest)
gh run list --branch "$head" --limit 1 --json databaseId --jq '.[0].databaseId' |
xargs -I{} gh run rerun {} --failed ;;
backport)
printf '%s' "$ARG" | grep -Eq '^release/[0-9]+\.[0-9]+$' || { gh pr comment "$PR" --body "Usage: /backport release/X.Y"; exit 1; }
gh pr edit "$PR" --add-label "backport $ARG" ;;
deploy-preview)
gh workflow run preview.yml -f pr="$PR" ;;
*)
gh pr comment "$PR" --body "Unknown command \`/$CMD\`. Available: /retest, /backport release/X.Y, /deploy-preview"; exit 1 ;;
esac The backport command adds a label that drives the workflow from automating backport pull requests with labels, so the bot composes with automation you already have.
Step 4 — Never run the pull request’s code with the bot’s privileges Jump to heading
issue_comment workflows run from the default branch with the repository’s token and secrets — like pull_request_target. A /test command that checks out the pull request’s head and runs its build would execute an outsider’s code with those privileges. Commands that need to run pull request code should trigger an unprivileged workflow instead, or require that the commenter has reviewed and approved the exact commit.
# deploy-preview: pin the exact commit a maintainer reviewed
- run: |
sha=$(gh pr view "$PR" --json headRefOid --jq .headRefOid)
gh workflow run preview.yml -f pr="$PR" -f sha="$sha" Step 5 — Report the result on the pull request Jump to heading
People need to see that the command worked. React to the comment when received, then post or update a result comment with a link to the run.
run_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
gh pr comment "$PR" --body "✅ \`/$CMD\` done by @$ACTOR — [run]($run_url)" Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Why not use an existing slash-command action? Jump to heading
Several exist and handle parsing well. The permission check and the rule about never running pull request code still need to be right in your configuration, so understand the pattern even if you use a library.
Can commands be restricted to specific people, not just write access? Jump to heading
Yes — check team membership instead of repository permission, for example gh api orgs/$ORG/teams/release-managers/memberships/$ACTOR.
How do we stop a command running twice on edited comments? Jump to heading
Listen only to created, not edited, comment events. If someone edits a comment to add a command, they can post a new comment instead.
Related Jump to heading
- Pull Request Automation & Bots — the parent topic.
- Running CI for Fork Pull Requests Safely — the privilege model behind step 4.
- Preview Environments per Pull Request — what /deploy-preview usually triggers.
- Limiting Workflow Permissions per Job — keeping the bot’s token minimal.