A PR label can carry intent. needs-review asks for a look; release-ready tells another group it can move forward. Getting that signal to the right people shouldn't need a second manual message.
I built pr-label-slack-notify, a small GitHub Action that posts to Slack when a configured label is added to a pull request. It uses incoming webhooks (no bot token), supports templates and mentions, and deduplicates with a hidden comment marker. v1.0.0 is MIT licensed.
Try it with one channel
Create an incoming webhook in Slack, save its URL as the repository secret SLACK_WEBHOOK, and add this to .github/workflows/notify-pr-label.yml on your default branch:
name: Notify Slack on PR label
on:
pull_request_target:
types: [labeled]
permissions:
contents: read
pull-requests: write
concurrency:
group: >-
slack-label-${{ github.repository }}-
${{ github.event.pull_request.number }}-
${{ github.event.label.name }}
cancel-in-progress: false
jobs:
notify:
runs-on: ubuntu-latest
steps:
# Do not check out or execute PR code in this privileged job.
- uses: tomron/pr-label-slack-notify@v1
with:
slack_webhook: ${{ secrets.SLACK_WEBHOOK }}
labels: '["release-ready", "needs-review"]'
dedup: "true"
dry_run: "true"
message_template: >-
{author_mention}'s PR <{url}|#{pr}: {title}>
was labeled *{label}* by {labeler_mention}.
mention_map: '{"octocat":"U0123456789"}'
It starts with dry_run: "true", which logs the rendered payload without calling Slack. Once the message looks right, switch it to "false". Label names are exact and case-sensitive. Pin @v1.0.0 or a full commit SHA for supply-chain safety; @v1 follows compatible releases.
One label, several channels
A modern incoming webhook is tied to one channel, so notifying several channels means several webhooks. Map each label to one URL or a list, and store the JSON as a single secret, SLACK_WEBHOOK_MAP:
{
"release-ready": [
"<incoming webhook URL for release channel>",
"<incoming webhook URL for engineering channel>"
],
"needs-review": "<incoming webhook URL for review channel>"
}
Then replace slack_webhook in the step above with webhook_map: ${{ secrets.SLACK_WEBHOOK_MAP }}. Map entries win over the optional slack_webhook fallback, duplicate URLs are called once, and all selected destinations are validated before anything is sent.
Author and labeler are different people
The template separates the PR author ({author}, {author_mention}) from whoever added the label ({labeler}, {labeler_mention}). mention_map maps GitHub usernames to Slack member IDs; unmapped users stay plain usernames. Event text is escaped, so a PR title can't smuggle in a channel-wide mention.
When a webhook fails
The other destinations still run. Deduplication is per PR, label and webhook, so retrying a partial failure doesn't re-post to channels that already succeeded.
Before posting, the action writes a pending marker, and flips it to sent once Slack replies ok. A definite 4xx rejection removes the marker so a fixed config can retry. A timeout or server error is ambiguous, since Slack may have accepted the message, so the marker stays and the action asks for review instead of posting again. Check the channel, and delete the pending marker only if you need another attempt.
This is duplicate reduction, not exactly-once delivery. Keep the per-PR, per-label concurrency group, because checking and creating comments isn't atomic. Repository writers can also edit or remove the markers.
Limits
The example uses pull_request_target so fork PRs can access secrets, which makes it a privileged workflow. There is deliberately no checkout step; don't add a PR-head checkout or build to that job. Choose the Slack audience carefully for private repositories. Only label-added events are handled, and there is no threading, since incoming webhooks don't return a message timestamp.
The README covers all inputs, outputs and recovery steps.