A GitHub Action that monitors Slack channels for open pull requests and automatically marks them merged, closed, or approved — keeping your PR review channel clean and up to date.
This action is designed around a pull request review workflow that uses a dedicated Slack channel. The following practices make the most of what it provides:
-
Create a Dedicated Slack Channel: Establish a separate channel specifically for pull requests, such as
#pr-reviewsor#pull-requests. This keeps review requests visible and avoids cluttering general development channels. -
Post Pull Request Links: Developers post links to their pull requests in the dedicated channel when they are ready for review. This signals that feedback is welcome and gives the bot a message to react to.
[!NOTE] It is recommended that each pull request is posted in its own message. Messages containing multiple pull requests are supported, but there is no way to indicate the status of them individually via reactions.
-
Use Reactions for Review Status: These reactions indicate that a pull request has already received attention, so other reviewers can prioritize accordingly.
- Approval: Add an "approved" reaction (e.g., ✅) once you have approved the pull request.
- Changes Requested: Add a "changes requested" reaction (e.g., 🔄) to indicate modifications are needed before the pull request can be approved.
-
Use Reactions for Pull Request Closure: These reactions tell the bot (and your teammates) that a pull request no longer needs review.
- Merged: Add a "merged" reaction (e.g., 🚀) when a pull request is merged. The bot can also add this automatically.
- Closed: Add a "closed" reaction (e.g., ❌) when a pull request is closed without merging. The bot can also add this automatically.
Get up and running in about 5 minutes.
-
Slack bot token with the following scopes:
channels:history— read public channel historygroups:history— read private channel history (for channels the app is invited to)chat:write— post messagesreactions:write— add emoji reactions to messages
-
GitHub token with access to the repos whose PRs you want to monitor:
- Fine-grained:
pull_requests:read - Classic:
repo
[!NOTE] This action uses two separate tokens for different purposes:
- The workflow's default
GITHUB_TOKEN(granted via thecontents: writejob permission) is used to commit the state file back to the workflow repo. - The
github-tokeninput reads PR statuses from the repos being monitored — these are typically different repos, so the defaultGITHUB_TOKENwon't work here.
- Fine-grained:
-
If using Persistent PR Tracking: a repository where the GitHub Actions actor has push access. See that section for the full workflow setup, or opt out by setting
trackUnresolved: false.
Add .github/pr_channel_slackbot_config.json to your repo:
{
"channels": {
"my-prs": {
"channelId": "C123456"
}
}
}Replace C123456 with your channel's ID. To find it: right-click the channel in Slack → Copy link — the ID is the last segment of the URL (e.g. https://mycompany.slack.com/archives/C123456).
Add .github/workflows/pr_channel_slackbot.yml:
name: PR Channel Slackbot
on:
workflow_dispatch:
schedule:
# At 12:00 and 17:00 (UTC) every weekday
- cron: '0 12,17 * * 1-5'
jobs:
pr-channel-slackbot:
runs-on: ubuntu-latest
permissions:
contents: write # Allows the default GITHUB_TOKEN to commit the state file
steps:
- name: Checkout Repository
uses: actions/checkout@v6 # Required to read the config file and write the state file
with:
# Use token with write access to bypass PR checks for state file commits
token: ${{ secrets.TOKEN_WITH_WRITE_ACCESS }}
- name: Configure git user
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: PR Channel Slackbot
uses: TheSench/pr-channel-slackbot@v2
with:
slack-token: ${{ secrets.SLACK_TOKEN }}
github-token: ${{ secrets.PR_BOT_GITHUB_TOKEN }}
config-file: '.github/pr_channel_slackbot_config.json'
state-file: '.github/pr-channel-state.json'
- name: Commit state file
run: |
git add .github/pr-channel-state.json
if ! git diff --cached --quiet; then
git commit -m "chore: update PR channel state [skip ci]"
git push
fiIn your repo, go to Settings → Secrets and variables → Actions and add:
| Secret | Value |
|---|---|
SLACK_TOKEN |
Your Slack bot token |
PR_BOT_GITHUB_TOKEN |
Your GitHub token with access to the monitored repos |
That's it. On the next scheduled run (or via Actions → PR Channel Slackbot → Run workflow), the bot will scan your Slack channel and post a digest of open pull requests.
For each configured Slack channel, the action runs these steps:
-
Read Chat History: Scans channel messages (excluding thread replies) to find messages containing GitHub pull request links.
-
Check Pull Request Status: For each pull request link found:
- If the message already has a
closedormergedreaction, it is skipped. - The action queries GitHub for the pull request's status. If it is merged or closed, the corresponding reaction is added to the Slack message (and to its entry in the previous digest thread, if one exists). The action then moves on.
- If the pull request is still open, the action checks its review status. If changes have been requested, the
changesRequestedreaction is added. Otherwise, if there are approvals, theapprovedreaction is added.
- If the message already has a
-
Create a New Digest Thread: After processing all messages, the action posts a new thread in the channel summarizing open pull requests. If a previous digest thread exists, a reply is added to it linking to the new one. This step (and the next two) can be skipped with
skip-digest: true— see Reaction-Only Mode. -
Add Digest Entries: One reply per open pull request message is added to the new thread.
-
Copy Reactions (optional): If
enableReactionCopyingis enabled for the channel, reactions from the original message are copied to the digest thread entry.
When a message contains links to more than one pull request, the action uses the aggregate status to decide how to react:
- All closed without merge → message is treated as "closed"
- All closed, at least one merged → message is treated as "merged"
- All approved, none with changes requested → message is treated as "approved"
This action uses two tokens for different purposes:
| Token | Purpose | Where configured |
|---|---|---|
Default GITHUB_TOKEN |
Commits the state file back to the workflow repo | permissions: contents: write on the job |
github-token input |
Reads PR statuses from the repos being monitored | secrets.PR_BOT_GITHUB_TOKEN |
A valid Slack API bot token is required to read and post messages. This can be a token for an existing Slack app, or you can create a new one. All messages are posted as the app tied to the provided token.
To read and post in private channels, the app must be invited to those channels in Slack.
Required bot token scopes:
channels:history— read public channel historygroups:history— read private channel historychat:write— post messagesreactions:write— add emoji reactions
The github-token input is used to query pull request status and review information from GitHub. It must have access to every repository whose PRs appear in your monitored channels.
Warning
The default GITHUB_TOKEN will not work here. It is scoped to the repository running the workflow, not the repositories being monitored. Use a dedicated token (stored as PR_BOT_GITHUB_TOKEN or similar) with access to the target repos.
If the token cannot access a repository, automatic reactions are disabled for that repo's PRs, and the bot falls back to using only manually-added merged/closed reactions to determine status.
Required permissions:
- Fine-grained access tokens:
pull_requests:read - Classic access tokens:
repo
| Input | Required | Default | Description |
|---|---|---|---|
slack-token |
yes | Slack API bot token | |
github-token |
yes | GitHub API token for reading PR statuses from monitored repos | |
config-file |
yes | Path to the JSON configuration file | |
skip-digest |
no | false |
When true, skips posting the open PR digest thread to Slack. Closed/merged PR reactions still fire normally. |
state-file |
no | ./pr-channel-state.json |
Path to the state file used for persistent unresolved PR tracking. Only written when at least one channel has trackUnresolved: true. Requires contents: write permission on the workflow. |
The configuration file (e.g., .github/pr_channel_slackbot_config.json) has two sections: reactions and channels.
Example:
{
"reactions": {
"merged": [
"merged"
],
"closed": [
"pr-closed",
"closed"
],
"changesRequested": [
"changes-requested"
],
"approved": [
"approved"
]
},
"channels": {
"project-foo-prs": {
"channelId": "C123456",
"limit": 100,
"maxPages": 2,
"trackUnresolved": true
},
"project-bar-prs": {
"channelId": "C654321",
"enableReactionCopying": true
},
"project-baz-prs": {
"channelId": "C987654",
"limit": 50,
"disabled": true
}
}
}Each reaction type accepts a list of emoji names. When checking existing reactions, all names in the list are recognized. When the bot adds a reaction, it uses the first name in the list.
| Key | Behavior |
|---|---|
merged |
Messages with this reaction are skipped. Added automatically when a PR is merged. |
closed |
Messages with this reaction are skipped. Added automatically when a PR is closed without merging. |
changesRequested |
Does not affect message processing. Added when a PR has open change requests. |
approved |
Does not affect message processing. Added when a PR has approvals and no change requests. |
The channels section is a map of human-readable names to channel configurations. The key names are for your reference only and do not affect processing. Use names that match your actual Slack channel names for clarity.
Each channel configuration supports:
| Field | Required | Default | Description |
|---|---|---|---|
channelId |
yes | The Slack channel ID. Right-click the channel → Copy link — the ID is the last segment of the URL. | |
limit |
no | 50 |
Maximum messages to fetch per page of Slack history. |
maxPages |
no | 1 |
Maximum pages of history to fetch. Increase for high-volume channels where PRs may scroll off a single page. When trackUnresolved is enabled, pagination stops early once the previous digest thread is found. |
trackUnresolved |
no | true |
Persists unresolved PR message timestamps between runs so long-running PRs are never dropped from the digest. Requires state-file input and contents: write workflow permission. |
allowBotMessages |
no | true |
When true, messages posted by bots are eligible for PR tracking. Set to false to skip bot messages. |
disabled |
no | false |
Set to true to temporarily disable a channel without removing it from the config. |
enableReactionCopying |
no | false |
When true, reactions from the original message are copied to the digest thread entry. |
By default (trackUnresolved: true), the bot persists the list of unresolved PR message timestamps and the last digest thread timestamp to a JSON state file after each run. On subsequent runs, it fetches any previously-tracked messages that fall outside the current pagination window — ensuring long-running PRs are never dropped from the digest.
The state file is automatically committed and pushed back to the repository after each run (skipped if nothing changed).
Requirements:
- Set
contents: writeon the job permissions - Include
actions/checkout@v4before the bot step - Configure git user identity before the bot step
- Commit and push the state file after the bot step
- Provide the
state-fileinput (defaults to./pr-channel-state.json)
permissions:
contents: writeTo opt out, set trackUnresolved: false on each channel in your config:
{
"channels": {
"my-prs": {
"channelId": "C123456",
"trackUnresolved": false
}
}
}When disabled, you can also remove the contents: write permission and the state-file input from your workflow.
Use skip-digest: true when you want the bot to mark merged/closed PRs with reactions without posting a new digest thread every time it runs.
A common pattern is to post a full digest on a regular schedule and run reaction-only cleanup more frequently:
name: PR Channel Slackbot
on:
workflow_dispatch:
inputs:
skip-digest:
description: 'Skip posting the open PR digest'
type: boolean
default: true
schedule:
# Run every hour on weekdays
- cron: '0 * * * 1-5'
jobs:
pr-channel-slackbot:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout Repository
uses: actions/checkout@v6
with:
token: ${{ secrets.TOKEN_WITH_WRITE_ACCESS }}
- name: Configure git user
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
# When run via CRON, create digests at 8AM and 1PM ET
- name: Determine skip-digest
id: opts
run: |
if [[ "${{ github.event_name }}" == "schedule" ]]; then
HOUR=$(TZ="America/New_York" date +%-H)
if [[ "$HOUR" == "8" ]] || [[ "$HOUR" == "13" ]]; then
echo "skip-digest=false" >> "$GITHUB_OUTPUT"
else
echo "skip-digest=true" >> "$GITHUB_OUTPUT"
fi
else
echo "skip-digest=${{ inputs.skip-digest }}" >> "$GITHUB_OUTPUT"
fi
- name: Open PRs
uses: TheSench/pr-channel-slackbot@v2
with:
slack-token: ${{ secrets.SLACK_TOKEN }}
github-token: ${{ secrets.PR_BOT_GITHUB_TOKEN }}
config-file: '.github/pr_channel_slackbot_config.json'
state-file: '.github/pr-channel-state.json'
skip-digest: ${{ steps.opts.outputs.skip-digest }}
- name: Commit state file
run: |
git add .github/pr-channel-state.json
if ! git diff --cached --quiet; then
git commit -m "chore: update PR channel state [skip ci]"
git push
fiThe disableReactionCopying field has been replaced by enableReactionCopying with inverted semantics. Reaction copying is now opt-in (disabled by default).
- Had
"disableReactionCopying": true→ replace with"enableReactionCopying": false(or simply omit it — no behavior change; reactions still not copied) - Had
"disableReactionCopying": falseor omitted → add"enableReactionCopying": trueif you want reactions copied (this was previously the default; now opt-in)
Previously this field didn't exist and unresolved tracking was always off. In v2 it is on by default.
- Requires adding
contents: writepermission to your workflow job, agit configstep to set user identity, a commit-and-push step after the bot runs, AND the workflow actor must be allowed to commit directly to the branch. - To restore the old behavior, explicitly set
"trackUnresolved": falseon each channel.
Previously bot messages were always blocked. In v2 they are included by default.
- To restore the old behavior, explicitly set
"allowBotMessages": falseon each channel.
This project is licensed under the MIT License.
