Slack alerts #2197
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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("&"; "&") | | |
| gsub("<"; "<") | | |
| gsub(">"; ">"); | |
| ($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("&"; "&") | gsub("<"; "<") | gsub(">"; ">"); | |
| { | |
| 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("&"; "&") | | |
| gsub("<"; "<") | | |
| gsub(">"; ">"); | |
| ($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" |