Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 130 additions & 51 deletions .github/workflows/pr-comment-slack-notify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ on:
pull_request:
types: [opened, reopened]
issue_comment:
types: [created]
types: [created, edited]
pull_request_review_comment:
types: [created]
types: [created, edited]

jobs:
notify-slack:
Expand All @@ -22,6 +22,8 @@ jobs:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
SLACK_CHANNEL_ID: ${{ secrets.SLACK_CHANNEL_ID }}
EVENT_NAME: ${{ github.event_name }}
EVENT_ACTION: ${{ github.event.action }}
COMMENT_ID: ${{ github.event.comment.id }}
COMMENT_BODY: ${{ github.event.comment.body }}
COMMENT_URL: ${{ github.event.comment.html_url }}
COMMENTER: ${{ github.event.comment.user.login }}
Expand Down Expand Up @@ -118,11 +120,19 @@ jobs:

# Clean comment body:
# 1. Remove HTML comments (<!-- ... -->)
# 2. Remove GitHub admonitions (> [!TIP], > [!NOTE], etc. and their continuation lines)
# 3. Trim leading/trailing whitespace
# 2. Remove <details>...</details> blocks (CodeRabbit analysis chains)
# 3. Remove GitHub admonitions (> [!TIP], > [!NOTE], etc. and their continuation lines)
# 4. Remove --- separators and everything after them
# 5. Remove remaining HTML tags (<sub>, <summary>, etc.)
# 6. Remove lines that are only whitespace or markdown artifacts
# 7. Trim leading/trailing whitespace
CLEANED_BODY=$(echo "$COMMENT_BODY" | perl -0777 -pe '
s/<!--.*?-->//gs;
s/<details>.*?<\/details>//gs;
s/^>\s*\[!(TIP|NOTE|WARNING|CAUTION|IMPORTANT)\]\s*\n(^>.*\n)*//gm;
s/\n---\s*\n.*//s;
s/<[^>]+>//g;
s/^\s*\n//gm;
s/^\s+|\s+$//g;
')

Expand All @@ -133,83 +143,152 @@ jobs:
TRUNCATED_BODY="$CLEANED_BODY"
fi

if [ -n "$THREAD_TS" ]; then
# Thread reply — just show the comment text, no header
# Build the blocks for the comment
COMMENT_BLOCKS=$(jq -n --arg comment_body "$TRUNCATED_BODY" '[
{
"type": "rich_text",
"elements": [
{
"type": "rich_text_quote",
"elements": [
{
"type": "text",
"text": $comment_body
}
]
}
]
}
]')

# Build metadata to tag this Slack message with the GitHub comment ID
METADATA=$(jq -n --arg comment_id "$COMMENT_ID" '{
"event_type": "github_comment",
"event_payload": { "comment_id": $comment_id }
}')

if [ "$EVENT_ACTION" = "edited" ] && [ -n "$THREAD_TS" ]; then
# Edit — find the existing Slack message for this GitHub comment and update it
REPLIES=$(curl -s "https://slack.com/api/conversations.replies?channel=$SLACK_CHANNEL_ID&ts=$THREAD_TS&include_all_metadata=true&limit=200" \
-H "Authorization: Bearer $SLACK_BOT_TOKEN")

SLACK_MSG_TS=$(echo "$REPLIES" | jq -r --arg cid "$COMMENT_ID" '
.messages[]
| select(.metadata.event_payload.comment_id == $cid)
| .ts' | head -1)
Comment on lines +170 to +178
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Slack conversations.replies API pagination next_cursor limit

💡 Result:

conversations.replies uses cursor-based pagination:

  • First request: pass channel, ts, and optionally limit (page size). Example: limit=200 (Slack recommends ≤200). The method’s general maximum is documented as 1000, but see the non‑Marketplace note below. [1][2]
  • Next pages: read response_metadata.next_cursor from the response, then call again with cursor=<next_cursor> (and the same limit). Repeat until next_cursor is missing/null/empty string (""). [1][3]
  • Don’t stop just because you got fewer than limit items—Slack may return fewer (even 0) while still providing a next_cursor. Always key off next_cursor. [2][3]
  • Encoding: cursor strings often end with =; when sending as a URL query parameter, encode it (e.g., =%3D). [3]

Important (rate-limit-driven limit change): For apps commercially distributed outside the Slack Marketplace, Slack reduced the default and maximum limit for conversations.replies to 15 and rate-limited the method to 1 request/min for certain apps/installations (see Slack’s changelog and method docs for applicability dates). [1][4]

Minimal pagination loop (pseudo):

  1. cursor = null
  2. call conversations.replies(channel, ts, limit, cursor?)
  3. cursor = response.response_metadata.next_cursor
  4. if cursor is empty → done; else repeat. [1][3]

Sources:
[1] Slack method docs: conversations.replies https://api.slack.com/methods/conversations.replies
[2] Slack Conversations API pagination notes https://api.slack.com/conversations-api
[3] Slack pagination guide https://docs.slack.dev/apis/web-api/pagination/
[4] Slack changelog (rate limit + limit=15 for non‑Marketplace) https://api.slack.com/changelog/2025-05-terms-rate-limit-update-and-faq


Paginate replies to avoid missing edited messages in long threads.
conversations.replies is capped at 200 (or 15 for non-Marketplace apps); if a thread exceeds that, the target message won't be found and edits will post a duplicate reply instead of updating in place. Implement cursor-based pagination using response_metadata.next_cursor.

One detail: URL-encode the cursor when passing it as a query parameter (e.g., =%3D), since cursor strings often contain special characters.

🧭 Suggested pagination pattern
- REPLIES=$(curl -s "https://slack.com/api/conversations.replies?channel=$SLACK_CHANNEL_ID&ts=$THREAD_TS&include_all_metadata=true&limit=200" \
-   -H "Authorization: Bearer $SLACK_BOT_TOKEN")
-
- SLACK_MSG_TS=$(echo "$REPLIES" | jq -r --arg cid "$COMMENT_ID" '
-   .messages[]
-   | select(.metadata.event_payload.comment_id == $cid)
-   | .ts' | head -1)
+ SLACK_MSG_TS=""
+ CURSOR=""
+ while :; do
+   REPLIES=$(curl -s "https://slack.com/api/conversations.replies?channel=$SLACK_CHANNEL_ID&ts=$THREAD_TS&include_all_metadata=true&limit=200&cursor=$CURSOR" \
+     -H "Authorization: Bearer $SLACK_BOT_TOKEN")
+
+   SLACK_MSG_TS=$(echo "$REPLIES" | jq -r --arg cid "$COMMENT_ID" '
+     .messages[]
+     | select(.metadata.event_payload.comment_id == $cid)
+     | .ts' | head -1)
+
+   if [ -n "$SLACK_MSG_TS" ] && [ "$SLACK_MSG_TS" != "null" ]; then
+     break
+   fi
+
+   CURSOR=$(echo "$REPLIES" | jq -r '.response_metadata.next_cursor // empty')
+   [ -z "$CURSOR" ] && break
+ done

Note: Consider URL-encoding the cursor parameter if special characters cause issues.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/pr-comment-slack-notify.yml around lines 162 - 170, The
current conversations.replies call that sets REPLIES and extracts SLACK_MSG_TS
can miss the edited message in long threads due to the 200-item cap; update the
logic that runs when THREAD_TS is set (the edited branch) to perform
cursor-based pagination: repeatedly call conversations.replies with the
response_metadata.next_cursor until either the message matching COMMENT_ID is
found or no cursor remains, merging/fetching each page into REPLIES or scanning
per-page for the matching .metadata.event_payload.comment_id to set
SLACK_MSG_TS; ensure the cursor query parameter is URL-encoded when appended to
the API URL and stop the loop once SLACK_MSG_TS is determined or next_cursor is
empty to avoid infinite loops.


if [ -n "$SLACK_MSG_TS" ] && [ "$SLACK_MSG_TS" != "null" ]; then
# Found the message — update it
UPDATE_PAYLOAD=$(jq -n \
--arg channel "$SLACK_CHANNEL_ID" \
--arg ts "$SLACK_MSG_TS" \
--arg comment_body "$TRUNCATED_BODY" \
--argjson blocks "$COMMENT_BLOCKS" \
--argjson metadata "$METADATA" \
'{
"channel": $channel,
"ts": $ts,
"text": $comment_body,
"blocks": $blocks,
"metadata": $metadata
}')

curl -s -X POST "https://slack.com/api/chat.update" \
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
-H "Content-Type: application/json" \
-d "$UPDATE_PAYLOAD"
else
# Could not find the original message — post as new reply
PAYLOAD=$(jq -n \
--arg channel "$SLACK_CHANNEL_ID" \
--arg commenter "$COMMENTER" \
--arg comment_body "$TRUNCATED_BODY" \
--arg avatar "$COMMENTER_AVATAR" \
--arg thread_ts "$THREAD_TS" \
--argjson blocks "$COMMENT_BLOCKS" \
--argjson metadata "$METADATA" \
'{
"channel": $channel,
"username": $commenter,
"icon_url": $avatar,
"thread_ts": $thread_ts,
"text": $comment_body,
"unfurl_links": false,
"blocks": $blocks,
"metadata": $metadata
}')

curl -s -X POST "https://slack.com/api/chat.postMessage" \
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
-H "Content-Type: application/json" \
-d "$PAYLOAD"
fi
elif [ -n "$THREAD_TS" ]; then
# New comment with existing thread — reply in thread
PAYLOAD=$(jq -n \
--arg channel "$SLACK_CHANNEL_ID" \
--arg commenter "$COMMENTER" \
--arg comment_body "$TRUNCATED_BODY" \
--arg avatar "$COMMENTER_AVATAR" \
--arg thread_ts "$THREAD_TS" \
--argjson blocks "$COMMENT_BLOCKS" \
--argjson metadata "$METADATA" \
'{
"channel": $channel,
"username": $commenter,
"icon_url": $avatar,
"thread_ts": $thread_ts,
"text": $comment_body,
"unfurl_links": false,
"blocks": [
{
"type": "rich_text",
"elements": [
{
"type": "rich_text_quote",
"elements": [
{
"type": "text",
"text": $comment_body
}
]
}
]
}
]
"blocks": $blocks,
"metadata": $metadata
}')

curl -s -X POST "https://slack.com/api/chat.postMessage" \
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
-H "Content-Type: application/json" \
-d "$PAYLOAD"
else
# No thread found — post top-level with full context
HEADER_BLOCK=$(jq -n \
--arg repo_short "$REPO_SHORT" \
--arg pr_url "$PR_URL" \
--arg pr_number "$PR_NUMBER" \
--arg pr_title "$PR_TITLE" \
--arg commenter "$COMMENTER" \
'{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ($commenter + " commented on [" + $repo_short + "#" + $pr_number + "] <" + $pr_url + "|" + $pr_title + ">")
}
}')

ALL_BLOCKS=$(echo "$COMMENT_BLOCKS" | jq --argjson header "$HEADER_BLOCK" '[$header] + .')

PAYLOAD=$(jq -n \
--arg channel "$SLACK_CHANNEL_ID" \
--arg repo_short "$REPO_SHORT" \
--arg pr_url "$PR_URL" \
--arg pr_number "$PR_NUMBER" \
--arg pr_title "$PR_TITLE" \
--arg commenter "$COMMENTER" \
--arg comment_body "$TRUNCATED_BODY" \
--arg avatar "$COMMENTER_AVATAR" \
--argjson blocks "$ALL_BLOCKS" \
--argjson metadata "$METADATA" \
'{
"channel": $channel,
"username": $commenter,
"icon_url": $avatar,
"text": ($commenter + " commented on [" + $repo_short + "#" + $pr_number + "] <" + $pr_url + "|" + $pr_title + ">"),
"unfurl_links": false,
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ($commenter + " commented on [" + $repo_short + "#" + $pr_number + "] <" + $pr_url + "|" + $pr_title + ">")
}
},
{
"type": "rich_text",
"elements": [
{
"type": "rich_text_quote",
"elements": [
{
"type": "text",
"text": $comment_body
}
]
}
]
}
]
"blocks": $blocks,
"metadata": $metadata
}')
fi

curl -s -X POST "https://slack.com/api/chat.postMessage" \
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
-H "Content-Type: application/json" \
-d "$PAYLOAD"
curl -s -X POST "https://slack.com/api/chat.postMessage" \
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
-H "Content-Type: application/json" \
-d "$PAYLOAD"
fi
fi