Skip to content

Slack alerts

Slack alerts #2199

# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
name: Slack alerts
on:
issues:
types: [opened, reopened]
issue_comment:
types: [created]
discussion:
types: [created]
discussion_comment:
types: [created]
workflow_run:
workflows: ["Community CI", "PR Metadata", "TensorRT-Model-Connect Internal CI Bridge"]
types: [completed]
# Also catch reviews, DCO updates, and GitHub's asynchronous mergeability calculation.
schedule:
- cron: "7,22,37,52 * * * *"
workflow_dispatch:
inputs:
dry_run:
description: Preview eligible merge-ready alerts without posting or recording delivery
type: boolean
default: true
# Workflow completions and scheduled scans are trusted, default-branch events.
# Readiness jobs consume only GitHub metadata and never download artifacts,
# check out a pull-request head, or execute contributor-controlled code.
# General pull-request comments arrive through issue_comment. Maintainer
# mentions and Internal CI requests receive a higher-priority Slack heading.
# Review and diff comment events are intentionally excluded because
# fork-triggered merge-ref workflows do not receive Actions secrets.
# These trusted events read GitHub metadata only. They never check out or
# execute contributor-controlled code.
permissions: {}
jobs:
notify-ready-pr:
if: >-
github.repository == 'NVIDIA/TensorRT-Model-Connect' &&
github.event.workflow_run.name == 'Community CI' &&
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-24.04
timeout-minutes: 7
permissions:
actions: read
checks: read
contents: read
pull-requests: read
steps:
- name: Post ready external PR alert
env:
GH_TOKEN: ${{ github.token }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_COMMUNITY_ACTIVITY_WEBHOOK_URL }}
REPOSITORY: ${{ github.repository }}
WORKFLOW_RUN_NAME: ${{ github.event.workflow_run.display_title }}
run: |
set -euo pipefail
if [ -z "$SLACK_WEBHOOK_URL" ]; then
echo "::error::Missing SLACK_COMMUNITY_ACTIVITY_WEBHOOK_URL repository or organization secret."
exit 1
fi
run_name_pattern='^PR #([1-9][0-9]*) · community CI · head ([0-9a-f]{40}) · merge ([0-9a-f]{40})$'
if [[ ! "$WORKFLOW_RUN_NAME" =~ $run_name_pattern ]]; then
echo "::error::Unexpected Community CI run name: $WORKFLOW_RUN_NAME"
exit 1
fi
pr_number="${BASH_REMATCH[1]}"
HEAD_SHA="${BASH_REMATCH[2]}"
MERGE_SHA="${BASH_REMATCH[3]}"
tested_merge_json="$(
gh api "repos/$REPOSITORY/git/commits/$MERGE_SHA" \
2>/dev/null || true
)"
if [ -z "$tested_merge_json" ]; then
echo "::notice::The tested merge revision $MERGE_SHA is unavailable; skipping the readiness alert."
exit 0
fi
tested_merge_sha="$(jq -r '.sha // empty' <<<"$tested_merge_json")"
tested_parent_count="$(jq -r '.parents | length' <<<"$tested_merge_json")"
tested_base_sha="$(jq -r '.parents[0].sha // empty' <<<"$tested_merge_json")"
tested_head_sha="$(jq -r '.parents[1].sha // empty' <<<"$tested_merge_json")"
tested_tree_sha="$(jq -r '.tree.sha // empty' <<<"$tested_merge_json")"
if [ "$tested_merge_sha" != "$MERGE_SHA" ] \
|| [ "$tested_parent_count" != "2" ] \
|| [ "$tested_head_sha" != "$HEAD_SHA" ] \
|| ! [[ "$tested_base_sha" =~ ^[0-9a-f]{40}$ ]] \
|| ! [[ "$tested_tree_sha" =~ ^[0-9a-f]{40}$ ]]; then
echo "::error::The Community CI run does not identify a valid pull-request merge revision."
exit 1
fi
merge_revision_matches_tested() {
local current_merge_sha="$1"
[[ "$current_merge_sha" =~ ^[0-9a-f]{40}$ ]] || return 1
local current_merge_json
current_merge_json="$(
gh api "repos/$REPOSITORY/git/commits/$current_merge_sha" \
2>/dev/null
)" || return 1
local current_parent_count
local current_base_sha
local current_head_sha
local current_tree_sha
current_parent_count="$(jq -r '.parents | length' <<<"$current_merge_json")"
current_base_sha="$(jq -r '.parents[0].sha // empty' <<<"$current_merge_json")"
current_head_sha="$(jq -r '.parents[1].sha // empty' <<<"$current_merge_json")"
current_tree_sha="$(jq -r '.tree.sha // empty' <<<"$current_merge_json")"
[ "$current_parent_count" = "2" ] \
&& [ "$current_base_sha" = "$tested_base_sha" ] \
&& [ "$current_head_sha" = "$HEAD_SHA" ] \
&& [ "$current_tree_sha" = "$tested_tree_sha" ]
}
pr_json="$(gh api "repos/$REPOSITORY/pulls/$pr_number")"
current_head_sha="$(jq -r ".head.sha" <<<"$pr_json")"
if [ "$current_head_sha" != "$HEAD_SHA" ]; then
echo "::notice::PR #$pr_number advanced beyond $HEAD_SHA; skipping the stale readiness alert."
exit 0
fi
current_merge_sha="$(jq -r ".merge_commit_sha // empty" <<<"$pr_json")"
if ! merge_revision_matches_tested "$current_merge_sha"; then
echo "::notice::PR #$pr_number has a newer merge revision; skipping the stale readiness alert."
exit 0
fi
if [ "$(jq -r ".state" <<<"$pr_json")" != "open" ]; then
echo "::notice::PR #$pr_number is no longer open; skipping the readiness alert."
exit 0
fi
if [ "$(jq -r ".draft" <<<"$pr_json")" = "true" ]; then
echo "::notice::PR #$pr_number is still a draft; skipping the readiness alert."
exit 0
fi
if [ "$(jq -r ".base.ref" <<<"$pr_json")" != "main" ]; then
echo "::notice::PR #$pr_number does not target main; skipping the readiness alert."
exit 0
fi
author_association="$(jq -r ".author_association" <<<"$pr_json")"
case "$author_association" in
OWNER|MEMBER|COLLABORATOR)
echo "::notice::PR #$pr_number is authored by a repository collaborator; skipping the community alert."
exit 0
;;
esac
required_checks=(
"Community CPU / Required"
"PR Metadata / Required"
"DCO"
)
checks_ready=false
for attempt in {1..30}; do
pr_json="$(gh api "repos/$REPOSITORY/pulls/$pr_number")"
current_head_sha="$(jq -r ".head.sha" <<<"$pr_json")"
if [ "$current_head_sha" != "$HEAD_SHA" ]; then
echo "::notice::PR #$pr_number advanced while checking readiness; skipping the stale alert."
exit 0
fi
current_merge_sha="$(jq -r ".merge_commit_sha // empty" <<<"$pr_json")"
if ! merge_revision_matches_tested "$current_merge_sha"; then
echo "::notice::PR #$pr_number merge advanced while checking readiness; skipping the stale alert."
exit 0
fi
checks_json="$(gh api -H "Accept: application/vnd.github+json" "repos/$REPOSITORY/commits/$HEAD_SHA/check-runs?per_page=100")"
checks_ready=true
for required_check in "${required_checks[@]}"; do
conclusion="$(
jq -r --arg name "$required_check" \
'[.check_runs[] | select(.name == $name)]
| sort_by(.started_at) | last | .conclusion // ""' \
<<<"$checks_json"
)"
if [ "$conclusion" != "success" ]; then
checks_ready=false
echo "Waiting for $required_check on $HEAD_SHA (latest conclusion: ${conclusion:-missing})."
fi
done
if [ "$checks_ready" = "true" ]; then
break
fi
if [ "$attempt" -lt 30 ]; then
sleep 10
fi
done
if [ "$checks_ready" != "true" ]; then
echo "::notice::PR #$pr_number did not satisfy all alert gates; no Slack notification was sent."
exit 0
fi
item_author="$(jq -r ".user.login" <<<"$pr_json")"
item_title="$(jq -r ".title" <<<"$pr_json")"
item_url="$(jq -r ".html_url" <<<"$pr_json")"
head_display="${HEAD_SHA:0:12}"
payload="$(jq -cn \
--arg author "$item_author" \
--arg association "$author_association" \
--arg number "$pr_number" \
--arg title "$item_title" \
--arg url "$item_url" \
--arg head "$head_display" \
'def slack_escape:
gsub("&"; "&amp;") |
gsub("<"; "&lt;") |
gsub(">"; "&gt;");
($title | slack_escape) as $safe_title |
{
text: ("✅ 🔀 External PR ready for maintainer\n" +
"Pull request #" + $number + " · required checks passed\n" +
$author + " (" + $association + ")"),
blocks: [
{
type: "header",
text: {
type: "plain_text",
text: "✅ 🔀 External PR ready for maintainer",
emoji: true
}
},
{
type: "section",
fields: [
{type: "mrkdwn", text: "*Event:*\nPull request · required checks passed"},
{type: "mrkdwn", text: ("*Author:*\n" + $author + " (" + $association + ")")}
]
},
{
type: "section",
fields: [
{type: "mrkdwn", text: "*Checks:*\nCommunity CI · DCO · PR Metadata"},
{type: "mrkdwn", text: ("*Head:*\n" + $head)}
]
},
{
type: "section",
text: {
type: "mrkdwn",
text: ("*Pull request #" + $number + "*\n" +
"<" + $url + "|" + $safe_title + ">")
}
}
]
}')"
curl --fail-with-body --silent --show-error \
--request POST \
--header 'Content-Type: application/json' \
--data "$payload" \
"$SLACK_WEBHOOK_URL"
notify-merge-ready-pr:
if: >-
github.repository == 'NVIDIA/TensorRT-Model-Connect' &&
(github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success')
runs-on: ubuntu-24.04
timeout-minutes: 10
concurrency:
group: slack-merge-ready-alerts
cancel-in-progress: false
permissions:
checks: read
pull-requests: read
statuses: write
steps:
- name: Notify once per merge-ready PR head
env:
GH_TOKEN: ${{ github.token }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_COMMUNITY_ACTIVITY_WEBHOOK_URL }}
REPOSITORY: ${{ github.repository }}
DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run || false }}
run: |
set -euo pipefail
if [ "$DRY_RUN" != "true" ] && [ -z "$SLACK_WEBHOOK_URL" ]; then
echo "::error::Missing SLACK_COMMUNITY_ACTIVITY_WEBHOOK_URL."
exit 1
fi
merge_ready() {
jq -e '.state == "open" and .merged == false and .draft == false
and .base.ref == "main" and .mergeable == true
and .mergeable_state == "clean"
and .author_association != "OWNER"
and .author_association != "MEMBER"
and .author_association != "COLLABORATOR"' >/dev/null <<<"$1"
}
checks_ready() {
statuses="$(gh api --paginate "repos/$REPOSITORY/commits/$head/statuses?per_page=100" | jq -s 'add')" || return 1
jq -e --arg context "TRTMC Internal CI / Automated premerge gate" \
'[.[] | select(.context == $context)] | max_by(.id) | .state == "success"' \
>/dev/null <<<"$statuses" || return 1
local checks
checks="$(gh api --paginate "repos/$REPOSITORY/commits/$head/check-runs?per_page=100" | jq -s '[.[].check_runs[]]')" || return 1
local name
for name in "Community CPU / Required" "PR Metadata / Required" "DCO"; do
jq -e --arg name "$name" \
'[.[] | select(.name == $name)] | max_by(.id)
| .status == "completed" and .conclusion == "success"' \
>/dev/null <<<"$checks" || return 1
done
}
numbers="$(gh api --paginate "repos/$REPOSITORY/pulls?state=open&base=main&per_page=100" \
--jq '.[] | select(.draft == false) | .number')"
while IFS= read -r number; do
[[ "$number" =~ ^[1-9][0-9]*$ ]] || continue
pull="$(gh api "repos/$REPOSITORY/pulls/$number")"
merge_ready "$pull" || continue
head="$(jq -r '.head.sha' <<<"$pull")"
base="$(jq -r '.base.sha' <<<"$pull")"
[[ "$head" =~ ^[0-9a-f]{40}$ ]] || continue
marker="Slack / Merge-ready alert #$number"
checks_ready || continue
if jq -e --arg context "$marker" \
'[.[] | select(.context == $context)] | max_by(.id) | .state == "success"' \
>/dev/null <<<"$statuses"; then
echo "PR #$number at $head was already notified."
continue
fi
# Recheck the live PR and gates immediately before sending.
checks_ready || continue
pull="$(gh api "repos/$REPOSITORY/pulls/$number")"
merge_ready "$pull" || continue
jq -e --arg head "$head" --arg base "$base" \
'.head.sha == $head and .base.sha == $base' >/dev/null <<<"$pull" || continue
payload="$(jq -cn --argjson pull "$pull" \
'def slack_escape:
gsub("&"; "&amp;") | gsub("<"; "&lt;") | gsub(">"; "&gt;");
{
text: ("✅ PASS · PR #" + ($pull.number | tostring) + " ready to merge: " + $pull.html_url),
blocks: [
{type: "header", text: {type: "plain_text", text: "✅ PASS · PR ready to merge", emoji: true}},
{type: "section", text: {type: "mrkdwn", text:
("<" + $pull.html_url + "|#" + ($pull.number | tostring) + " " + ($pull.title | slack_escape) + ">")}},
{type: "section", fields: [
{type: "mrkdwn", text: ("*Author:*\n" + $pull.user.login)},
{type: "mrkdwn", text: ("*Head:*\n" + $pull.head.sha[0:12])},
{type: "mrkdwn", text: "*Checks:*\nInternal CI · Community CPU · PR Metadata · DCO"},
{type: "mrkdwn", text: "*Mergeability:*\nReady according to GitHub"}
]}
]
}')"
if [ "$DRY_RUN" = "true" ]; then
printf '%s\n' "$payload"
continue
fi
curl --fail-with-body --silent --show-error --connect-timeout 10 --max-time 30 \
--request POST --header 'Content-Type: application/json' \
--data "$payload" "$SLACK_WEBHOOK_URL"
# This non-required status records successful delivery, not a CI verdict.
gh api --silent --method POST "repos/$REPOSITORY/statuses/$head" \
-f state=success -f context="$marker" \
-f description="Merge-ready Slack notification delivered" \
-f target_url="$(jq -r '.html_url' <<<"$pull")"
done <<<"$numbers"
notify-activity:
if: >-
github.repository == 'NVIDIA/TensorRT-Model-Connect' &&
github.event.sender.type != 'Bot' &&
(
(
github.event_name == 'issues' &&
github.event.issue.author_association != 'OWNER' &&
github.event.issue.author_association != 'MEMBER' &&
github.event.issue.author_association != 'COLLABORATOR'
) ||
(
github.event_name == 'issue_comment' &&
github.event.comment.author_association != 'OWNER' &&
github.event.comment.author_association != 'MEMBER' &&
github.event.comment.author_association != 'COLLABORATOR'
) ||
(
github.event_name == 'discussion' &&
github.event.discussion.author_association != 'OWNER' &&
github.event.discussion.author_association != 'MEMBER' &&
github.event.discussion.author_association != 'COLLABORATOR'
) ||
(
github.event_name == 'discussion_comment' &&
github.event.comment.author_association != 'OWNER' &&
github.event.comment.author_association != 'MEMBER' &&
github.event.comment.author_association != 'COLLABORATOR'
)
)
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions: {}
steps:
- name: Post Slack alert
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_COMMUNITY_ACTIVITY_WEBHOOK_URL }}
EVENT_NAME: ${{ github.event_name }}
EVENT_ACTION: ${{ github.event.action }}
ITEM_AUTHOR: ${{ github.event.comment.user.login || github.event.issue.user.login || github.event.discussion.user.login }}
ITEM_AUTHOR_ASSOCIATION: ${{ github.event.comment.author_association || github.event.issue.author_association || github.event.discussion.author_association }}
ITEM_NUMBER: ${{ github.event.issue.number || github.event.discussion.number }}
ITEM_TITLE: ${{ github.event.issue.title || github.event.discussion.title }}
ITEM_URL: ${{ github.event.comment.html_url || github.event.issue.html_url || github.event.discussion.html_url }}
COMMENT_BODY: ${{ github.event.comment.body || '' }}
IS_PULL_REQUEST: ${{ github.event.issue.pull_request != null }}
IS_COMMENT: ${{ github.event_name == 'issue_comment' || github.event_name == 'discussion_comment' }}
run: |
set -euo pipefail
if [ -z "$SLACK_WEBHOOK_URL" ]; then
echo "::error::Missing SLACK_COMMUNITY_ACTIVITY_WEBHOOK_URL repository or organization secret."
exit 1
fi
case "$EVENT_NAME" in
issue_comment)
if [ "$IS_PULL_REQUEST" = "true" ]; then
item_kind="Pull request"
item_icon="🔀"
item_heading="External pull request activity"
else
item_kind="Issue"
item_icon="🎫"
item_heading="External issue activity"
fi
;;
issues)
item_kind="Issue"
item_icon="🎫"
item_heading="External issue activity"
;;
discussion|discussion_comment)
item_kind="Discussion"
item_icon="💬"
item_heading="External discussion activity"
;;
*) echo "::error::Unsupported community event: $EVENT_NAME"; exit 1 ;;
esac
alert_heading="$item_heading"
alert_emoji="$item_icon"
comment_lower="$(printf '%s' "$COMMENT_BODY" | tr '[:upper:]' '[:lower:]')"
if [ "$IS_COMMENT" = "true" ] &&
{
[[ "$comment_lower" == *"@yifeif-nv"* ]] ||
[[ "$comment_lower" == *"@chaofengw-nv"* ]] ||
[[ "$comment_lower" == *"/request-internal-ci"* ]] ||
[[ "$comment_lower" == *"internal ci"* ]] ||
[[ "$comment_lower" == *"internal-ci"* ]] ||
[[ "$comment_lower" == *"trigger ci"* ]]
}; then
alert_heading="External maintainer request"
alert_emoji="🚨 $item_icon"
fi
payload="$(jq -cn \
--arg heading "$alert_heading" \
--arg emoji "$alert_emoji" \
--arg kind "$item_kind" \
--arg action "$EVENT_ACTION" \
--arg author "$ITEM_AUTHOR" \
--arg association "$ITEM_AUTHOR_ASSOCIATION" \
--arg number "$ITEM_NUMBER" \
--arg title "$ITEM_TITLE" \
--arg url "$ITEM_URL" \
'def slack_escape:
gsub("&"; "&amp;") |
gsub("<"; "&lt;") |
gsub(">"; "&gt;");
($title | slack_escape) as $safe_title |
{
text: ($emoji + " " + $heading + "\n" +
$kind + " #" + $number + " · " + $action + "\n" +
$author + " (" + $association + ")"),
blocks: [
{
type: "header",
text: {
type: "plain_text",
text: ($emoji + " " + $heading),
emoji: true
}
},
{
type: "section",
fields: [
{type: "mrkdwn", text: ("*Event:*\n" + $kind + " · " + $action)},
{type: "mrkdwn", text: ("*Author:*\n" + $author + " (" + $association + ")")}
]
},
{
type: "section",
text: {
type: "mrkdwn",
text: ("*" + $kind + " #" + $number + "*\n" +
"<" + $url + "|" + $safe_title + ">")
}
}
]
}')"
curl --fail-with-body --silent --show-error \
--request POST \
--header 'Content-Type: application/json' \
--data "$payload" \
"$SLACK_WEBHOOK_URL"