Skip to content
Open
Show file tree
Hide file tree
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
25 changes: 25 additions & 0 deletions .github/actions/notify-slack-deploy/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ inputs:
run_url:
description: Link to the deploy run, not to the run posting this.
required: true
repo_url:
description: Repository URL, used to link the pull request. Omit to leave the title unlinked.
required: false
commit_message:
description: >
The deployed commit's message. Only its first line is used, which on a
squash merge is the pull request title. Omitted from the card if absent.
required: false
started_at:
description: When the deploy run started, ISO 8601. Omitted from the message if absent.
required: false
Expand All @@ -52,11 +60,26 @@ runs:
COMMIT_SHA: ${{ inputs.commit_sha }}
ACTOR: ${{ inputs.actor }}
RUN_URL: ${{ inputs.run_url }}
REPO_URL: ${{ inputs.repo_url }}
COMMIT_MESSAGE: ${{ inputs.commit_message }}
STARTED_AT: ${{ inputs.started_at }}
ENDED_AT: ${{ inputs.ended_at }}
run: |
set -euo pipefail

# A squash merge puts the pull request title and number in the first
# line, as "title (#123)". A direct push or a merge commit will not
# match, so the number is optional and the title falls back to the
# whole line.
subject="$(printf '%s' "${COMMIT_MESSAGE:-}" | head -1)"
pr_number="$(printf '%s' "$subject" | sed -n 's/.*(#\([0-9]\{1,\}\))$/\1/p')"
title="$subject"
pr_url=''
if [ -n "$pr_number" ]; then
title="$(printf '%s' "$subject" | sed 's/ *(#[0-9]\{1,\})$//')"
[ -n "${REPO_URL:-}" ] && pr_url="$REPO_URL/pull/$pr_number"
fi

# Best effort: a timestamp that will not parse costs the message two
# fields rather than failing the run.
epoch() { date -u -d "$1" +%s 2>/dev/null || true; }
Expand All @@ -83,6 +106,8 @@ runs:
--arg actor "$ACTOR" \
--arg app_url "$APP_URL" \
--arg run_url "$RUN_URL" \
--arg title "$title" \
--arg pr_url "$pr_url" \
--arg started_epoch "$started_epoch" \
--arg started_label "$started_label" \
--arg duration "$duration" \
Expand Down
22 changes: 20 additions & 2 deletions .github/actions/notify-slack-deploy/payload.jq
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# Slack Block Kit payload for a deploy notification.
# Run with jq -n and the --arg values listed in action.yml.

# Slack only needs these three escaped, ampersand first so the entities the
# other two produce are not re-encoded. Without it a commit subject reading
# "<!channel>" would ping the channel, and "<url|text>" would render as a
# disguised link.
def mrkdwn: gsub("&"; "&amp;") | gsub("<"; "&lt;") | gsub(">"; "&gt;");

($environment | (.[0:1] | ascii_upcase) + .[1:]) as $env_name |

# Only in_progress, success and cancelled need their own words. Everything
Expand Down Expand Up @@ -43,11 +49,22 @@ end) as $copy |
# comes from the icon set on the Slack app itself.
username: "\($service) Deploy",
text: $copy.headline,
blocks: [
blocks: (
[
{
type: "header",
text: { type: "plain_text", text: $copy.headline }
},
}
]
# What shipped, full width rather than in the field grid below, which is two
# narrow columns and would wrap a real title badly.
+ (if $title == "" then []
elif $pr_url == "" then
[{ type: "context", elements: [{ type: "mrkdwn", text: ($title | mrkdwn) }] }]
else
[{ type: "context", elements: [{ type: "mrkdwn", text: "<\($pr_url)|\($title | mrkdwn)>" }] }]
Comment on lines +63 to +65

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🔎 Supported by static analysis

🤖 get_repo_knowledge executed:

get_repo_knowledge Flagsmith/flagsmith /tmp/coderabbit-repo-knowledge/flagsmith-flagsmith-a74d157a/conventions /tmp/coderabbit-repo-knowledge/flagsmith-flagsmith-a74d157a/learnings

Length of output: 6518


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- payload.jq ---'
cat -n .github/actions/notify-slack-deploy/payload.jq
printf '%s\n' '--- action references ---'
rg -n -C 4 'notify-slack-deploy|payload\.jq|repo_url|pr_number|title' .github/actions .github/workflows

Repository: Flagsmith/flagsmith

Length of output: 18587


🌐 Web query:

site:api.slack.com/reference/block-kit/composition-objects/text-object mrkdwn 3000 characters incoming webhook invalid_blocks

💡 Result:

Slack’s documentation confirms:

  • A mrkdwn text object must contain 1–3,000 characters. Exceeding that limit can cause an incoming webhook request to fail with an invalid_blocks-style validation error. [1]
  • For a section block, the text object also has a 3,000-character maximum. [2]
  • Incoming webhooks accept Block Kit payloads and return HTTP 400 errors for malformed requests. [3]

Example fix:

const chunks = text.match(/[\s\S]{1,3000}/g) || [];

const payload = {
  blocks: chunks.map(chunk => ({
    type: "section",
    text: {
      type: "mrkdwn",
      text: chunk
    }
  }))
};

Also validate the serialized text content, escape &, <, and > where appropriate, and ensure each block is valid JSON. [4]

Sources

[1] (api.slack.com)
[2] (api.slack.com)
[3] (api.slack.com)
[4] (api.slack.com)

Citations:


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- action contract and webhook handling ---'
cat -n .github/actions/notify-slack-deploy/action.yml | sed -n '1,135p'
printf '%s\n' '--- commit-message constraints in the relevant workflows ---'
rg -n -C 5 'commit_message:|github\.event\.head_commit\.message|head_commit|commit message|subject' .github/workflows/frontend-deploy-production.yml .github/actions/notify-slack-deploy .github | head -220

Repository: Flagsmith/flagsmith

Length of output: 16886


Bound the complete context text to Slack’s 3,000-character limit.

The context mrkdwn text includes the escaped $title and, when $pr_url is set, the link syntax. $title has no length limit, and escaping can increase its length. A long commit title can therefore make Slack reject the payload and remove the notification. Limit the complete rendered value to 3,000 characters without splitting an entity, while reserving space for the pull-request URL and link delimiters. The notification step already uses continue-on-error, so deployment continues but the notification is lost.

Source: MCP tools


🔒 Security & Privacy | 🛡️ Analyzed with Security Review | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

site:api.slack.com/reference/block-kit/composition-objects/text-object verbatim explicit mrkdwn links

💡 Result:

Slack Text objects support two formats:

  • type: "plain_text" — unformatted text; supports the optional emoji field.
  • type: "mrkdwn" — Slack’s Markdown-like syntax; supports bold, italics, strikethrough, line breaks, and links. [1][2]

For mrkdwn, links can be written as:

{
  "type": "mrkdwn",
  "text": "Visit <https://example.com|our website>."
}

The optional verbatim field controls automatic parsing:

  • verbatim: false (default): Slack auto-links URLs, channel names, and certain mentions.
  • verbatim: true: Slack preserves plain text as written, while still processing Markdown and explicit parsing strings. [1]

Example:

{
  "type": "mrkdwn",
  "verbatim": true,
  "text": "Literal `#channel` and https://example.com"
}

Here, the URL and channel reference are not automatically converted; use explicit syntax such as <https://example.com|Example> when you want a link. [1][2]

Sources

[1] Slack Text object reference (api.slack.com)
[2] Slack message formatting guide (api.slack.com)

Citations:


Reachability: External
Exploitability: Moderate
CWE: CWE-116 — Improper Encoding or Escaping of Output

Disable automatic URL parsing for the title.

A commit title can contain a URL and reach this Slack notification. Set verbatim: true on both title mrkdwn objects. Slack will still process the explicit pull-request link.

Suggested fix
-       [{ type: "context", elements: [{ type: "mrkdwn", text: ($title | mrkdwn) }] }]
+       [{ type: "context", elements: [{ type: "mrkdwn", verbatim: true, text: ($title | mrkdwn) }] }]
...
-       [{ type: "context", elements: [{ type: "mrkdwn", text: "<\($pr_url)|\($title | mrkdwn)>" }] }]
+       [{ type: "context", elements: [{ type: "mrkdwn", verbatim: true, text: "<\($pr_url)|\($title | mrkdwn)>" }] }]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
[{ type: "context", elements: [{ type: "mrkdwn", text: ($title | mrkdwn) }] }]
else
[{ type: "context", elements: [{ type: "mrkdwn", text: "<\($pr_url)|\($title | mrkdwn)>" }] }]
[{ type: "context", elements: [{ type: "mrkdwn", verbatim: true, text: ($title | mrkdwn) }] }]
else
[{ type: "context", elements: [{ type: "mrkdwn", verbatim: true, text: "<\($pr_url)|\($title | mrkdwn)>" }] }]

Source: MCP tools

end)
+ [
{
type: "section",
text: { type: "mrkdwn", text: $copy.body }
Expand Down Expand Up @@ -87,4 +104,5 @@ end) as $copy |
)
}
]
)
}
4 changes: 4 additions & 0 deletions .github/workflows/frontend-deploy-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ jobs:
commit_sha: ${{ github.sha }}
actor: ${{ github.actor }}
run_url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
repo_url: ${{ github.server_url }}/${{ github.repository }}
commit_message: ${{ github.event.head_commit.message }}

run-unit-tests:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -169,6 +171,8 @@ jobs:
commit_sha: ${{ github.sha }}
actor: ${{ github.actor }}
run_url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
repo_url: ${{ github.server_url }}/${{ github.repository }}
commit_message: ${{ github.event.head_commit.message }}
started_at: ${{ steps.outcome.outputs.started_at }}
ended_at: ${{ steps.outcome.outputs.ended_at }}

Expand Down
Loading