From e8e50c26cdbfdf8b0d5aff8aa10b3d6c2af19ca4 Mon Sep 17 00:00:00 2001 From: Angel Yanev Date: Thu, 23 Jul 2026 09:45:37 +0300 Subject: [PATCH] Add slack bot integration for release build status --- .github/actions/common/slack.sh | 60 +++++++++++++-- .github/actions/slack-notification/action.yml | 74 ++++++++++++++++--- .github/workflows/release_build_and_test.yml | 3 +- .github/workflows/release_publish.yml | 7 +- 4 files changed, 123 insertions(+), 21 deletions(-) diff --git a/.github/actions/common/slack.sh b/.github/actions/common/slack.sh index 8db737c..a0afbda 100644 --- a/.github/actions/common/slack.sh +++ b/.github/actions/common/slack.sh @@ -1,11 +1,58 @@ #!/bin/bash -# Slack message builders for release notifications: a success message with the -# install command, or a failure message with the reason. +# Slack message builders and bot-token sender for release notifications. + +# Post a Slack message via chat.postMessage. Reads the JSON payload from stdin. +# Usage: slack_send_with_token "$SLACK_BOT_TOKEN" < message.json +slack_send_with_token() { + local token="$1" + local curl_stderr; curl_stderr=$(mktemp) + if ! curl --fail-with-body -sS -X POST https://api.slack.com/api/chat.postMessage \ + -H "Authorization: Bearer $token" \ + -H "Content-Type: application/json; charset=utf-8" \ + -d @- 2>"$curl_stderr"; then + echo "curl to chat.postMessage failed:" >&2 + cat "$curl_stderr" >&2 + rm -f "$curl_stderr" + return 1 + fi + rm -f "$curl_stderr" + return 0 +} + +# Parse a chat.postMessage response (read from stdin). Writes slack_ts and +# slack_url to $GITHUB_OUTPUT when set. Non-zero exit on API error. +# Usage: slack_handle_message_result "$channel_id" "$sent_payload" < response.json +slack_handle_message_result() { + local channel_id="$1" + local message="$2" + local response; response=$(cat) + + if echo "$response" | jq -e '.ok == true' >/dev/null; then + local slack_ts slack_channel ts_for_url slack_url + slack_ts=$(echo "$response" | jq -r '.ts') + slack_channel=$(echo "$response" | jq -r '.channel') + ts_for_url=$(echo "$slack_ts" | tr -d '.') + slack_url="https://redis.slack.com/archives/${slack_channel}/p${ts_for_url}" + if [ -n "${GITHUB_OUTPUT:-}" ]; then + echo "slack_ts=$slack_ts" >> "$GITHUB_OUTPUT" + echo "slack_url=$slack_url" >> "$GITHUB_OUTPUT" + fi + echo "✅ Slack message sent: $slack_url" + return 0 + fi + + local error; error=$(echo "$response" | jq -r '.error // "unknown"') + echo "❌ Failed to send Slack message: $error" >&2 + echo "$response" | jq '.' >&2 + echo "Message content: $message" >&2 + return 1 +} slack_format_success_message() { - # $1 release_tag $2 base_url (…/redis-cli) $3 env $4 footer - jq -n --arg tag "$1" --arg base "$2" --arg env "$3" --arg footer "$4" ' + # $1 channel $2 release_tag $3 base_url (…/redis-cli) $4 env $5 footer + jq -n --arg channel "$1" --arg tag "$2" --arg base "$3" --arg env "$4" --arg footer "$5" ' { + "channel": $channel, "icon_emoji": ":redis-circle:", "text": (":redis-circle: redis-cli " + $tag + " published (" + $env + ")"), "blocks": [ @@ -20,9 +67,10 @@ slack_format_success_message() { } slack_format_failure_message() { - # $1 release_tag $2 message $3 env $4 footer - jq -n --arg tag "$1" --arg msg "$2" --arg env "$3" --arg footer "$4" ' + # $1 channel $2 release_tag $3 message $4 env $5 footer + jq -n --arg channel "$1" --arg tag "$2" --arg msg "$3" --arg env "$4" --arg footer "$5" ' { + "channel": $channel, "icon_emoji": ":redis-circle:", "text": (":x: redis-cli " + $tag + " release failed (" + $env + ")"), "blocks": [ diff --git a/.github/actions/slack-notification/action.yml b/.github/actions/slack-notification/action.yml index 0e72983..cba178f 100644 --- a/.github/actions/slack-notification/action.yml +++ b/.github/actions/slack-notification/action.yml @@ -1,10 +1,14 @@ name: "Slack notification" -description: "Send a redis-cli release Slack notification (no-op if the webhook secret is unset)" +description: "Send a redis-cli release Slack notification via a Slack App bot token (no-op if the bot token or channel is unset)" inputs: slack_func: description: "slack_format_success_message | slack_format_failure_message" required: true + slack_channel_id: + description: "Slack channel ID to post to (if empty, this action does nothing)" + required: false + default: "" release_tag: description: "Release tag" required: true @@ -19,27 +23,73 @@ inputs: description: "Failure message" required: false default: "" - SLACK_WEB_HOOK_URL: - description: "Slack webhook URL (if empty, this action does nothing)" + SLACK_BOT_TOKEN: + description: "Slack App bot token (if empty, this action does nothing)" required: false default: "" +outputs: + slack_ts: + description: "Slack message timestamp" + value: ${{ steps.send-success.outputs.slack_ts || steps.send-failure.outputs.slack_ts }} + slack_url: + description: "Permalink to the Slack message" + value: ${{ steps.send-success.outputs.slack_url || steps.send-failure.outputs.slack_url }} + runs: using: "composite" steps: - - name: Send Slack notification - if: inputs.SLACK_WEB_HOOK_URL != '' + - name: Send success Slack notification + id: send-success + if: inputs.slack_func == 'slack_format_success_message' && inputs.SLACK_BOT_TOKEN != '' && inputs.slack_channel_id != '' + shell: bash + env: + SLACK_BOT_TOKEN: ${{ inputs.SLACK_BOT_TOKEN }} + run: | + set -e + . "${{ github.action_path }}/../common/slack.sh" + workflow_url="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + footer="Repo: ${{ github.repository }} | Commit: \`${{ github.sha }}\` | <$workflow_url|workflow run>" + + msgfile="$(mktemp)" + slack_format_success_message \ + "${{ inputs.slack_channel_id }}" \ + "${{ inputs.release_tag }}" \ + "${{ inputs.base_url }}" \ + "${{ inputs.env }}" \ + "$footer" > "$msgfile" + + result="$(mktemp)" + if ! slack_send_with_token "$SLACK_BOT_TOKEN" < "$msgfile" > "$result"; then + echo "Failed to send message:"; cat "$msgfile"; exit 1 + fi + slack_handle_message_result "${{ inputs.slack_channel_id }}" "$(cat "$msgfile")" < "$result" + + - name: Send failure Slack notification + id: send-failure + if: inputs.slack_func == 'slack_format_failure_message' && inputs.SLACK_BOT_TOKEN != '' && inputs.slack_channel_id != '' shell: bash + env: + SLACK_BOT_TOKEN: ${{ inputs.SLACK_BOT_TOKEN }} run: | + set -e . "${{ github.action_path }}/../common/slack.sh" workflow_url="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" footer="Repo: ${{ github.repository }} | Commit: \`${{ github.sha }}\` | <$workflow_url|workflow run>" - if [ "${{ inputs.slack_func }}" = "slack_format_success_message" ]; then - payload="$(slack_format_success_message "${{ inputs.release_tag }}" "${{ inputs.base_url }}" "${{ inputs.env }}" "$footer")" - else - msg="${{ inputs.message }}" - [ -n "$msg" ] || msg=":x: redis-cli release failed" - payload="$(slack_format_failure_message "${{ inputs.release_tag }}" "$msg" "${{ inputs.env }}" "$footer")" + msg="${{ inputs.message }}" + [ -n "$msg" ] || msg=":x: redis-cli release failed" + + msgfile="$(mktemp)" + slack_format_failure_message \ + "${{ inputs.slack_channel_id }}" \ + "${{ inputs.release_tag }}" \ + "$msg" \ + "${{ inputs.env }}" \ + "$footer" > "$msgfile" + + result="$(mktemp)" + if ! slack_send_with_token "$SLACK_BOT_TOKEN" < "$msgfile" > "$result"; then + echo "Failed to send message:"; cat "$msgfile"; exit 1 fi - printf '%s' "$payload" | curl -s --fail-with-body -d @- "${{ inputs.SLACK_WEB_HOOK_URL }}" + slack_handle_message_result "${{ inputs.slack_channel_id }}" "$(cat "$msgfile")" < "$result" diff --git a/.github/workflows/release_build_and_test.yml b/.github/workflows/release_build_and_test.yml index 4b0cd5b..fe16b8c 100644 --- a/.github/workflows/release_build_and_test.yml +++ b/.github/workflows/release_build_and_test.yml @@ -103,4 +103,5 @@ jobs: release_tag: ${{ needs.prepare-release.outputs.release_tag }} env: ${{ inputs.release_type == 'public' && 'production' || 'staging' }} message: ":x: redis-cli build failed" - SLACK_WEB_HOOK_URL: ${{ secrets.SLACK_WEB_HOOK_URL }} + slack_channel_id: ${{ vars.SLACK_CHANNEL_ID }} + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} diff --git a/.github/workflows/release_publish.yml b/.github/workflows/release_publish.yml index aa168c9..7e45a84 100644 --- a/.github/workflows/release_publish.yml +++ b/.github/workflows/release_publish.yml @@ -13,7 +13,9 @@ name: Release Publish # CLI_S3_IAM_ARN[_STAGING] IAM role to assume (public / internal) # CLI_S3_BUCKET[_STAGING] target bucket (public / internal) # CLI_S3_REGION[_STAGING] bucket region (public / internal) -# SLACK_WEB_HOOK_URL optional; release notifications +# SLACK_BOT_TOKEN optional; Slack App bot token for notifications +# Optional repository variable: +# SLACK_CHANNEL_ID channel to post release notifications to on: workflow_dispatch: @@ -180,4 +182,5 @@ jobs: env: ${{ inputs.release_type == 'public' && 'production' || 'staging' }} base_url: ${{ inputs.release_type == 'public' && 'https://packages.redis.io/redis-cli' || format('https://{0}.s3.{1}.amazonaws.com/redis-cli', secrets.CLI_S3_BUCKET_STAGING, secrets.CLI_S3_REGION_STAGING) }} message: ":x: redis-cli publish failed" - SLACK_WEB_HOOK_URL: ${{ secrets.SLACK_WEB_HOOK_URL }} + slack_channel_id: ${{ vars.SLACK_CHANNEL_ID }} + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}