From fd5f69a147965701a699b96accebd75ca22fcbc5 Mon Sep 17 00:00:00 2001 From: Meanski Date: Mon, 20 Jul 2026 23:50:13 +1200 Subject: [PATCH] Add Discord - Send Notification step template --- gulpfile.babel.js | 2 + step-templates/discord-send-notification.json | 135 ++++++++++++++++++ step-templates/logos/discord.png | Bin 0 -> 7979 bytes 3 files changed, 137 insertions(+) create mode 100644 step-templates/discord-send-notification.json create mode 100644 step-templates/logos/discord.png diff --git a/gulpfile.babel.js b/gulpfile.babel.js index 05c116f2d..2e7ee861d 100644 --- a/gulpfile.babel.js +++ b/gulpfile.babel.js @@ -192,6 +192,8 @@ function humanize(categoryId) { return "Convex"; case "cyberark": return "CyberArk"; + case "discord": + return "Discord"; case "dll": return "dll"; case "dlm": diff --git a/step-templates/discord-send-notification.json b/step-templates/discord-send-notification.json new file mode 100644 index 000000000..4e7dc0742 --- /dev/null +++ b/step-templates/discord-send-notification.json @@ -0,0 +1,135 @@ +{ + "Id": "5adb6692-04b1-4475-800b-fbf0652696ed", + "Name": "Discord - Send Notification", + "Description": "Posts a message and/or rich embed to a [Discord](https://discord.com) channel via a [webhook](https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks). No Discord application or bot is required \u2014 create a webhook in the channel's settings (Integrations \u2192 Webhooks) and paste its URL into this step.\n\nThe embed can automatically colour itself green or red based on the deployment outcome (set Color to `auto` and use this step with a \"run always\" condition), include Project/Release/Environment fields, and link back to the deployment in Octopus.\n\nMentions (`@everyone`, roles, users) in message content are suppressed by default to prevent accidental pings from interpolated release notes.\n\nSets the output variable `MessageId` for use in later steps.\n\nRequires `curl` and either `jq` or `python3` on the worker.", + "ActionType": "Octopus.Script", + "Version": 1, + "CommunityActionTemplateId": null, + "Packages": [], + "Properties": { + "Octopus.Action.Script.ScriptSource": "Inline", + "Octopus.Action.Script.Syntax": "Bash", + "Octopus.Action.Script.ScriptBody": "#!/bin/bash\n\n# Discord - Send Notification\n# Posts a message and/or rich embed to a Discord channel via a webhook.\n# API reference: https://discord.com/developers/docs/resources/webhook#execute-webhook\n\n# Read step parameters\nWebhookUrl=$(get_octopusvariable \"DiscordNotify.WebhookUrl\")\nContent=$(get_octopusvariable \"DiscordNotify.Content\")\nUsername=$(get_octopusvariable \"DiscordNotify.Username\")\nAvatarUrl=$(get_octopusvariable \"DiscordNotify.AvatarUrl\")\nIncludeEmbed=$(get_octopusvariable \"DiscordNotify.IncludeEmbed\")\nEmbedTitle=$(get_octopusvariable \"DiscordNotify.EmbedTitle\")\nEmbedDescription=$(get_octopusvariable \"DiscordNotify.EmbedDescription\")\nEmbedColor=$(get_octopusvariable \"DiscordNotify.Color\")\nIncludeFields=$(get_octopusvariable \"DiscordNotify.IncludeDeploymentFields\")\nSuppressMentions=$(get_octopusvariable \"DiscordNotify.SuppressMentions\")\nThreadId=$(get_octopusvariable \"DiscordNotify.ThreadId\")\n\n# System variables used for the auto colour, detail fields, and deployment link\nSysProjectName=$(get_octopusvariable \"Octopus.Project.Name\")\nSysReleaseNumber=$(get_octopusvariable \"Octopus.Release.Number\")\nSysEnvironmentName=$(get_octopusvariable \"Octopus.Environment.Name\")\nSysDeploymentError=$(get_octopusvariable \"Octopus.Deployment.Error\")\nSysServerUri=$(get_octopusvariable \"Octopus.Web.ServerUri\")\nSysDeploymentLink=$(get_octopusvariable \"Octopus.Web.DeploymentLink\")\n\n# Validate required tooling\n\nif ! command -v curl >/dev/null 2>&1; then\n echo \"ERROR: curl is required on the worker to run this step.\" >&2\n exit 1\nfi\n\nif command -v jq >/dev/null 2>&1; then\n JSON_TOOL=\"jq\"\nelif command -v python3 >/dev/null 2>&1; then\n JSON_TOOL=\"python3\"\nelse\n echo \"ERROR: jq or python3 is required on the worker to build and parse JSON.\" >&2\n exit 1\nfi\n\n# Prints the JSON-encoded (quoted) form of $1\njson_escape() {\n if [[ \"$JSON_TOOL\" == \"jq\" ]]; then\n jq -cn --arg v \"$1\" '$v'\n else\n python3 -c 'import json,sys; print(json.dumps(sys.argv[1]))' \"$1\"\n fi\n}\n\n# Reads JSON on stdin and prints the value at dotted path $1 (empty if missing or null)\njson_get() {\n if [[ \"$JSON_TOOL\" == \"jq\" ]]; then\n jq -r \".$1 // empty\" 2>/dev/null\n else\n python3 -c '\nimport json, sys\ntry:\n data = json.load(sys.stdin)\nexcept Exception:\n sys.exit(0)\ncur = data\nfor part in sys.argv[1].split(\".\"):\n if isinstance(cur, dict) and part in cur:\n cur = cur[part]\n else:\n sys.exit(0)\nif cur is None:\n sys.exit(0)\nprint(cur if not isinstance(cur, (dict, list)) else json.dumps(cur))\n' \"$1\"\n fi\n}\n\n# Truncates $1 to at most $2 characters, appending an ellipsis when cut\ntruncate_to() {\n local s=\"$1\" max=\"$2\"\n if (( ${#s} > max )); then\n echo \"${s:0:$((max - 1))}\u2026\"\n else\n echo \"$s\"\n fi\n}\n\n# Validate required parameters\n\nif [[ -z \"$WebhookUrl\" ]]; then\n echo \"ERROR: WebhookUrl is required.\" >&2\n exit 1\nfi\n\nif [[ ! \"$WebhookUrl\" =~ ^https://(ptb\\.|canary\\.)?(discord|discordapp)\\.com/api/webhooks/ ]]; then\n echo \"ERROR: WebhookUrl does not look like a Discord webhook URL (expected https://discord.com/api/webhooks/...).\" >&2\n exit 1\nfi\n\nif [[ -z \"$Content\" && \"$IncludeEmbed\" != \"True\" ]]; then\n echo \"ERROR: Nothing to send. Provide Content, or enable Include Embed.\" >&2\n exit 1\nfi\n\n# Build the payload\n\nPARTS=()\n\nif [[ -n \"$Content\" ]]; then\n Content=$(truncate_to \"$Content\" 2000)\n PARTS+=(\"\\\"content\\\":$(json_escape \"$Content\")\")\nfi\n\n[[ -n \"$Username\" ]] && PARTS+=(\"\\\"username\\\":$(json_escape \"$Username\")\")\n[[ -n \"$AvatarUrl\" ]] && PARTS+=(\"\\\"avatar_url\\\":$(json_escape \"$AvatarUrl\")\")\n\nif [[ \"$SuppressMentions\" == \"True\" ]]; then\n PARTS+=(\"\\\"allowed_mentions\\\":{\\\"parse\\\":[]}\")\nfi\n\nif [[ \"$IncludeEmbed\" == \"True\" ]]; then\n EPARTS=()\n\n if [[ -n \"$EmbedTitle\" ]]; then\n EmbedTitle=$(truncate_to \"$EmbedTitle\" 256)\n EPARTS+=(\"\\\"title\\\":$(json_escape \"$EmbedTitle\")\")\n fi\n\n if [[ -n \"$EmbedDescription\" ]]; then\n EmbedDescription=$(truncate_to \"$EmbedDescription\" 4096)\n EPARTS+=(\"\\\"description\\\":$(json_escape \"$EmbedDescription\")\")\n fi\n\n # Link the embed title back to the deployment when running in a deployment context\n if [[ -n \"$SysServerUri\" && -n \"$SysDeploymentLink\" ]]; then\n EPARTS+=(\"\\\"url\\\":$(json_escape \"${SysServerUri}${SysDeploymentLink}\")\")\n fi\n\n # Resolve the embed colour (Discord expects a decimal integer)\n case \"$(echo \"$EmbedColor\" | tr '[:upper:]' '[:lower:]')\" in\n auto)\n # Green unless the deployment has an error\n if [[ -n \"$SysDeploymentError\" ]]; then COLOR=15548997; else COLOR=5763719; fi\n ;;\n success) COLOR=5763719 ;;\n failure) COLOR=15548997 ;;\n warning) COLOR=16705372 ;;\n info|\"\") COLOR=5793266 ;;\n *)\n if [[ \"$EmbedColor\" =~ ^[0-9]+$ ]]; then\n COLOR=\"$EmbedColor\"\n else\n echo \"WARNING: Unrecognised Color '$EmbedColor'; using info blue.\" >&2\n COLOR=5793266\n fi\n ;;\n esac\n EPARTS+=(\"\\\"color\\\":$COLOR\")\n\n EPARTS+=(\"\\\"timestamp\\\":\\\"$(date -u '+%Y-%m-%dT%H:%M:%SZ')\\\"\")\n\n if [[ \"$IncludeFields\" == \"True\" ]]; then\n FIELDS=()\n [[ -n \"$SysProjectName\" ]] && FIELDS+=(\"{\\\"name\\\":\\\"Project\\\",\\\"value\\\":$(json_escape \"$SysProjectName\"),\\\"inline\\\":true}\")\n [[ -n \"$SysReleaseNumber\" ]] && FIELDS+=(\"{\\\"name\\\":\\\"Release\\\",\\\"value\\\":$(json_escape \"$SysReleaseNumber\"),\\\"inline\\\":true}\")\n [[ -n \"$SysEnvironmentName\" ]] && FIELDS+=(\"{\\\"name\\\":\\\"Environment\\\",\\\"value\\\":$(json_escape \"$SysEnvironmentName\"),\\\"inline\\\":true}\")\n if (( ${#FIELDS[@]} > 0 )); then\n EPARTS+=(\"\\\"fields\\\":[$(IFS=,; echo \"${FIELDS[*]}\")]\")\n fi\n fi\n\n PARTS+=(\"\\\"embeds\\\":[{$(IFS=,; echo \"${EPARTS[*]}\")}]\")\nfi\n\nPAYLOAD=\"{$(IFS=,; echo \"${PARTS[*]}\")}\"\n\n# Build the URL. wait=true makes Discord return the created message so the\n# message id can be captured as an output variable.\n\nPOST_URL=\"${WebhookUrl}?wait=true\"\nif [[ -n \"$ThreadId\" ]]; then\n POST_URL=\"${POST_URL}&thread_id=${ThreadId}\"\nfi\n\n# Send, retrying on rate limiting (HTTP 429)\n\necho \"Sending Discord notification...\"\n[[ -n \"$Content\" ]] && echo \" Content : $(truncate_to \"$Content\" 60)\"\n[[ \"$IncludeEmbed\" == \"True\" && -n \"$EmbedTitle\" ]] && echo \" Embed : $EmbedTitle\"\n[[ -n \"$ThreadId\" ]] && echo \" Thread : $ThreadId\"\necho \"\"\n\nMAX_ATTEMPTS=3\nATTEMPT=1\n\nwhile true; do\n RAW=$(curl --silent --show-error --max-time 60 --write-out $'\\n%{http_code}' \\\n --request POST \"$POST_URL\" \\\n --header \"Content-Type: application/json\" \\\n --data \"$PAYLOAD\")\n\n CURL_EXIT=$?\n\n if [[ $CURL_EXIT -ne 0 ]]; then\n echo \"ERROR: curl request failed (exit code $CURL_EXIT).\" >&2\n exit 1\n fi\n\n HTTP_STATUS=$(echo \"$RAW\" | tail -n 1)\n RESPONSE=$(echo \"$RAW\" | sed '$d')\n\n if [[ \"$HTTP_STATUS\" == \"429\" ]]; then\n if (( ATTEMPT >= MAX_ATTEMPTS )); then\n echo \"ERROR: Discord rate limited the webhook and retries were exhausted.\" >&2\n exit 1\n fi\n RETRY_AFTER=$(echo \"$RESPONSE\" | json_get \"retry_after\")\n # retry_after can be fractional seconds; round up in plain bash\n WHOLE=\"${RETRY_AFTER%%.*}\"\n if [[ \"$WHOLE\" =~ ^[0-9]+$ ]]; then\n WAIT_SECONDS=$((WHOLE + 1))\n else\n WAIT_SECONDS=2\n fi\n echo \"Rate limited (429); retrying in ${WAIT_SECONDS}s (attempt $((ATTEMPT + 1))/${MAX_ATTEMPTS})...\"\n sleep \"$WAIT_SECONDS\"\n ATTEMPT=$((ATTEMPT + 1))\n continue\n fi\n\n break\ndone\n\nif [[ \"$HTTP_STATUS\" -ge 400 ]]; then\n ERROR_MSG=$(echo \"$RESPONSE\" | json_get \"message\")\n ERROR_CODE=$(echo \"$RESPONSE\" | json_get \"code\")\n echo \"ERROR: Discord API returned an error (HTTP $HTTP_STATUS).\" >&2\n [[ -n \"$ERROR_CODE\" ]] && echo \" Code : $ERROR_CODE\" >&2\n [[ -n \"$ERROR_MSG\" ]] && echo \" Message: $ERROR_MSG\" >&2\n exit 1\nfi\n\nMESSAGE_ID=$(echo \"$RESPONSE\" | json_get \"id\")\n\necho \"Notification sent successfully.\"\n[[ -n \"$MESSAGE_ID\" ]] && echo \" Message ID: $MESSAGE_ID\"\n\nset_octopusvariable \"MessageId\" \"$MESSAGE_ID\"" + }, + "Parameters": [ + { + "Id": "6f5e5a78-6498-4102-ba10-357f5c1bd3fe", + "Name": "DiscordNotify.WebhookUrl", + "Label": "Webhook URL", + "HelpText": "The Discord webhook URL (Channel Settings \u2192 Integrations \u2192 Webhooks). Consider binding this to a sensitive project variable (e.g. `#{Discord.WebhookUrl}`) so it can be scoped per environment and rotated in one place.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "Sensitive" + } + }, + { + "Id": "198f797b-fd12-4a25-b203-c6d3bf49b428", + "Name": "DiscordNotify.Content", + "Label": "Message Content", + "HelpText": "Plain message text (supports Discord markdown). Optional when Include Embed is enabled. Maximum 2000 characters \u2014 longer values are truncated.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "6469aad0-ac4e-4a5d-a115-b1940bc6642a", + "Name": "DiscordNotify.Username", + "Label": "Username Override", + "HelpText": "Overrides the webhook's configured display name. Leave blank to use the name configured on the webhook.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "b3609271-4cd3-4f63-96e7-a6726dd65828", + "Name": "DiscordNotify.AvatarUrl", + "Label": "Avatar URL Override", + "HelpText": "Overrides the webhook's configured avatar image. Leave blank to use the avatar configured on the webhook.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "bc4073e3-43de-470f-9d21-20f25b9ede5c", + "Name": "DiscordNotify.IncludeEmbed", + "Label": "Include Embed", + "HelpText": "If enabled, sends a rich embed with the title, description, colour, timestamp, a link to the deployment, and optional deployment detail fields.", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "4b3767ac-56f1-4f93-8245-12bf07d09ed0", + "Name": "DiscordNotify.EmbedTitle", + "Label": "Embed Title", + "HelpText": "Title of the embed. Maximum 256 characters \u2014 longer values are truncated.", + "DefaultValue": "#{Octopus.Project.Name} release #{Octopus.Release.Number} deployed to #{Octopus.Environment.Name}", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + }, + { + "Id": "61aa08a7-c638-49ce-b800-9ceb67ce8bac", + "Name": "DiscordNotify.EmbedDescription", + "Label": "Embed Description", + "HelpText": "Body text of the embed (supports Discord markdown). Maximum 4096 characters \u2014 longer values are truncated.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "MultiLineText" + } + }, + { + "Id": "644b2cad-e521-4a1b-a1ab-287278f4d84e", + "Name": "DiscordNotify.Color", + "Label": "Color", + "HelpText": "Colour of the embed's side bar. `auto` picks green or red based on whether the deployment has an error \u2014 useful with a \"run always\" step condition. A raw decimal colour value is also accepted.", + "DefaultValue": "auto", + "DisplaySettings": { + "Octopus.ControlType": "Select", + "Octopus.SelectOptions": "auto|Auto (green on success, red on failure)\nsuccess|Green\nfailure|Red\nwarning|Yellow\ninfo|Blue" + } + }, + { + "Id": "0bc69655-4c3d-43a9-af89-c084d1fed3e5", + "Name": "DiscordNotify.IncludeDeploymentFields", + "Label": "Include Deployment Fields", + "HelpText": "If enabled, adds Project, Release, and Environment fields to the embed (fields without a value, e.g. Release during a runbook run, are omitted).", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "da6f5ef7-5c60-44c3-98fb-f727f6e11bdd", + "Name": "DiscordNotify.SuppressMentions", + "Label": "Suppress Mentions", + "HelpText": "If enabled, `@everyone`, role, and user mentions in the message content will not ping anyone. Disable only if you intentionally want the notification to ping.", + "DefaultValue": "True", + "DisplaySettings": { + "Octopus.ControlType": "Checkbox" + } + }, + { + "Id": "06f05ca7-2f1c-4aa9-86ae-4eba79084040", + "Name": "DiscordNotify.ThreadId", + "Label": "Thread ID", + "HelpText": "Optional ID of a thread (or forum post) in the webhook's channel to post into. Leave blank to post to the channel itself.", + "DefaultValue": "", + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + } + } + ], + "StepPackageId": "Octopus.Script", + "$Meta": { + "ExportedAt": "2026-07-20T11:46:55.000Z", + "OctopusVersion": "2026.3.6158", + "Type": "ActionTemplate" + }, + "LastModifiedBy": "Meanski", + "Category": "discord" +} diff --git a/step-templates/logos/discord.png b/step-templates/logos/discord.png new file mode 100644 index 0000000000000000000000000000000000000000..21435a27f998c558f805b6a3675a1638210a395b GIT binary patch literal 7979 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 VjBl-3ng8i4YpLm}epInT{vTC8ng;*? literal 0 HcmV?d00001