From f9949ca48e601f140998110528f8d46475eed25a Mon Sep 17 00:00:00 2001 From: Diego Pereira Date: Thu, 30 Apr 2026 12:01:52 -0300 Subject: [PATCH 1/3] feat(webhook)!: per-instruction beneficiary event payload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aligns the fx-webhook docs with the fx-payment refactor (PR tracefinance/fx-payment#67). Each beneficiary lifecycle event now carries the affected payment instruction plus beneficiary context, instead of the full beneficiary snapshot with the entire instruction list. Schema changes: - `BeneficiaryEvent` -> `BeneficiaryPaymentInstructionEvent` - `instructions: List` -> `instruction: PaymentInstructionEvent` - adds `relationshipType` (`SELF_OWNED` | `THIRD_PARTY`) The three webhook operations (`BENEFICIARY_PAYMENT_INSTRUCTION_CREATED` / `_APPROVED` / `_REJECTED`) now reference the new schema, and the narrative descriptions are updated — consumers needing the full set of instructions on a beneficiary should call `GET /api/beneficiaries/{beneficiaryId}` on the fx-payment API. Co-Authored-By: Claude Opus 4.7 (1M context) --- ...neficiary-payment-instruction-approved.mdx | 2 +- ...neficiary-payment-instruction-rejected.mdx | 2 +- apis/fx-webhook/openapi.yml | 55 ++++++++++--------- 3 files changed, 32 insertions(+), 27 deletions(-) diff --git a/api-reference/fx-webhook/events/beneficiary/beneficiary-payment-instruction-approved.mdx b/api-reference/fx-webhook/events/beneficiary/beneficiary-payment-instruction-approved.mdx index a14249b..6fd3bff 100644 --- a/api-reference/fx-webhook/events/beneficiary/beneficiary-payment-instruction-approved.mdx +++ b/api-reference/fx-webhook/events/beneficiary/beneficiary-payment-instruction-approved.mdx @@ -1,5 +1,5 @@ --- title: "BENEFICIARY_PAYMENT_INSTRUCTION_APPROVED" -description: "Fires when a payment instruction on a beneficiary is approved; check instructions[].status to find the one that transitioned." +description: "Fires when a payment instruction on a beneficiary is approved. Carries the affected instruction plus beneficiary context." openapi: "apis/fx-webhook/openapi.yml webhook BENEFICIARY_PAYMENT_INSTRUCTION_APPROVED" --- diff --git a/api-reference/fx-webhook/events/beneficiary/beneficiary-payment-instruction-rejected.mdx b/api-reference/fx-webhook/events/beneficiary/beneficiary-payment-instruction-rejected.mdx index 147f1c0..1948e4a 100644 --- a/api-reference/fx-webhook/events/beneficiary/beneficiary-payment-instruction-rejected.mdx +++ b/api-reference/fx-webhook/events/beneficiary/beneficiary-payment-instruction-rejected.mdx @@ -1,5 +1,5 @@ --- title: "BENEFICIARY_PAYMENT_INSTRUCTION_REJECTED" -description: "Fires when a payment instruction on a beneficiary is rejected; check instructions[].status to find the one that transitioned." +description: "Fires when a payment instruction on a beneficiary is rejected. Carries the affected instruction plus beneficiary context." openapi: "apis/fx-webhook/openapi.yml webhook BENEFICIARY_PAYMENT_INSTRUCTION_REJECTED" --- diff --git a/apis/fx-webhook/openapi.yml b/apis/fx-webhook/openapi.yml index 2cdc7b8..b1fe065 100644 --- a/apis/fx-webhook/openapi.yml +++ b/apis/fx-webhook/openapi.yml @@ -1014,33 +1014,38 @@ components: - customerId - assets - atTime - BeneficiaryEvent: + BeneficiaryPaymentInstructionEvent: type: object description: > Payload delivered for beneficiary payment-instruction lifecycle events (`BENEFICIARY_PAYMENT_INSTRUCTION_CREATED`, `BENEFICIARY_PAYMENT_INSTRUCTION_APPROVED`, `BENEFICIARY_PAYMENT_INSTRUCTION_REJECTED`). - Review status is tracked **per payment instruction**, not on the - beneficiary itself. The payload always carries the full beneficiary - snapshot — to find which instruction triggered the event, inspect - `instructions[].status` (or, for `created`, the most recently added - entry). + Each event corresponds to **one** payment instruction transition. The + payload includes the affected `instruction` plus enough beneficiary + context (`id`, `customerId`, `holder`, `relationshipType`) to act on + the event without an extra lookup. To see the full set of payment + instructions attached to a beneficiary, call + `GET /api/beneficiaries/{beneficiaryId}` on the fx-payment API. properties: id: type: string format: uuid + description: ID of the beneficiary the affected instruction belongs to. readOnly: true customerId: type: string format: uuid holder: $ref: "#/components/schemas/HolderEvent" - instructions: - type: array - description: Payment instructions attached to the beneficiary, each with its own review status. - items: - $ref: "#/components/schemas/PaymentInstructionEvent" + relationshipType: + type: string + enum: + - SELF_OWNED + - THIRD_PARTY + description: Whether the beneficiary is the customer themselves (`SELF_OWNED`) or a separate party (`THIRD_PARTY`). + instruction: + $ref: "#/components/schemas/PaymentInstructionEvent" atTime: type: string format: date-time @@ -1049,7 +1054,8 @@ components: - id - customerId - holder - - instructions + - relationshipType + - instruction - atTime OperationEvent: type: object @@ -1454,8 +1460,8 @@ webhooks: the beneficiary itself is first created and when an additional instruction is later attached. The new instruction enters `PENDING_REVIEW` and transitions independently to `APPROVED` or - `REJECTED`. The payload is the full beneficiary snapshot; inspect - `instructions[]` to find the newly added one. + `REJECTED`. The payload carries the affected `instruction` plus the + beneficiary context (`id`, `customerId`, `holder`, `relationshipType`). tags: - Beneficiary requestBody: @@ -1463,7 +1469,7 @@ webhooks: content: application/json: schema: - $ref: "#/components/schemas/BeneficiaryEvent" + $ref: "#/components/schemas/BeneficiaryPaymentInstructionEvent" responses: "200": $ref: "#/components/responses/WebhookAck" @@ -1472,10 +1478,9 @@ webhooks: summary: Beneficiary payment instruction approved description: > Fires when one of the beneficiary's payment instructions is approved - and becomes usable. Review status lives on each instruction; check - `instructions[].status` to find the one that just transitioned. A - beneficiary with multiple instructions emits this event once per - approval. + and becomes usable. The payload carries the approved `instruction` + (with `status: APPROVED`) plus the beneficiary context. A beneficiary + with multiple instructions emits this event once per approval. tags: - Beneficiary requestBody: @@ -1483,7 +1488,7 @@ webhooks: content: application/json: schema: - $ref: "#/components/schemas/BeneficiaryEvent" + $ref: "#/components/schemas/BeneficiaryPaymentInstructionEvent" responses: "200": $ref: "#/components/responses/WebhookAck" @@ -1492,10 +1497,10 @@ webhooks: summary: Beneficiary payment instruction rejected description: > Fires when one of the beneficiary's payment instructions is rejected - in compliance review and cannot be used. Review status lives on each - instruction; check `instructions[].status` to find the one that just - transitioned. A beneficiary with multiple instructions emits this - event once per rejection. + in compliance review and cannot be used. The payload carries the + rejected `instruction` (with `status: REJECTED`) plus the beneficiary + context. A beneficiary with multiple instructions emits this event + once per rejection. tags: - Beneficiary requestBody: @@ -1503,7 +1508,7 @@ webhooks: content: application/json: schema: - $ref: "#/components/schemas/BeneficiaryEvent" + $ref: "#/components/schemas/BeneficiaryPaymentInstructionEvent" responses: "200": $ref: "#/components/responses/WebhookAck" From af64ecfd72ade29e9f2b6935eafa86d59447f8b6 Mon Sep 17 00:00:00 2001 From: Diego Pereira Date: Thu, 30 Apr 2026 12:05:28 -0300 Subject: [PATCH 2/3] ci: add PR Slack notification workflow Mirrors the convention already in use across fx-payment, fx-account, fx-auth, fx-customer-compliance, and fx-identity. Posts a Slack message when a PR is opened/reopened and updates the same message when the PR is closed/merged. Reads `SLACK_PR_BOT_TOKEN` and `SLACK_PR_BACKEND_CHANNEL_ID` from org/repo secrets. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/pr-slack-notify.yml | 222 ++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 .github/workflows/pr-slack-notify.yml diff --git a/.github/workflows/pr-slack-notify.yml b/.github/workflows/pr-slack-notify.yml new file mode 100644 index 0000000..172b331 --- /dev/null +++ b/.github/workflows/pr-slack-notify.yml @@ -0,0 +1,222 @@ +name: PR Slack Notification + +on: + pull_request: + types: [opened, reopened, closed] + +permissions: + pull-requests: write + +env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_PR_BOT_TOKEN }} + SLACK_CHANNEL: ${{ secrets.SLACK_PR_BACKEND_CHANNEL_ID }} + +jobs: + slack-notify: + runs-on: ubuntu-latest + steps: + - name: Extract key changes from PR body + id: extract + env: + PR_BODY: ${{ github.event.pull_request.body }} + run: | + # 1. Extract bullets from ## Key Changes section (per PR template) + changes=$(echo "$PR_BODY" | awk '/^## Key Changes/{found=1; next} /^## /{if(found) exit} found && /^- /{print}') + + # 2. Fallback: grab freeform text from ## Description + if [ -z "$changes" ]; then + changes=$(echo "$PR_BODY" | awk '/^## Description/{found=1; next} /^## /{if(found) exit} found' | sed '/^$/d' | head -5) + fi + + # 3. Last resort: use full PR body + if [ -z "$changes" ]; then + changes="$PR_BODY" + fi + + # Truncate total output to 2000 chars as safety limit + changes=$(printf '%s' "$changes" | head -c 2000) + + # Convert markdown to Slack mrkdwn format and limit to 3 bullets + changes=$(echo "$changes" | sed 's/^- /• /') + changes=$(echo "$changes" | sed 's/\*\*\([^*]*\)\*\*/\*\1\*/g') + changes=$(echo "$changes" | sed 's/`//g') + changes=$(echo "$changes" | head -3) + + delimiter=$(openssl rand -hex 8) + echo "changes<<${delimiter}" >> "$GITHUB_OUTPUT" + echo "$changes" >> "$GITHUB_OUTPUT" + echo "${delimiter}" >> "$GITHUB_OUTPUT" + + - name: Resolve action and color + id: meta + env: + EVENT_ACTION: ${{ github.event.action }} + PR_MERGED: ${{ github.event.pull_request.merged }} + PR_AUTHOR: ${{ github.event.pull_request.user.login }} + MERGED_BY: ${{ github.event.pull_request.merged_by.login }} + SENDER: ${{ github.event.sender.login }} + run: | + if [ "$EVENT_ACTION" = "opened" ] || [ "$EVENT_ACTION" = "reopened" ]; then + echo "action_text=created" >> "$GITHUB_OUTPUT" + echo "actor=$PR_AUTHOR" >> "$GITHUB_OUTPUT" + echo "color=#36a64f" >> "$GITHUB_OUTPUT" + elif [ "$PR_MERGED" = "true" ]; then + echo "action_text=merged" >> "$GITHUB_OUTPUT" + echo "actor=${MERGED_BY:-$SENDER}" >> "$GITHUB_OUTPUT" + echo "color=#8957e5" >> "$GITHUB_OUTPUT" + else + echo "action_text=closed" >> "$GITHUB_OUTPUT" + echo "actor=$SENDER" >> "$GITHUB_OUTPUT" + echo "color=#da1e28" >> "$GITHUB_OUTPUT" + fi + + # ── Find existing Slack message (used by closed/merged) ───── + + - name: Find Slack message reference + if: github.event.action == 'closed' + id: find + uses: actions/github-script@v7 + with: + script: | + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + per_page: 100, + }); + + const marker = comments.findLast(c => c.body.includes('slack-pr-notify:')); + if (!marker) { + core.warning('No Slack message reference found'); + core.setOutput('found', 'false'); + return; + } + + const match = marker.body.match(/slack-pr-notify:(\{.*?\})/); + const data = JSON.parse(match[1]); + core.setOutput('found', 'true'); + core.setOutput('ts', data.ts); + core.setOutput('channel', data.channel); + + # ── OPENED / REOPENED: Post new Slack message ────────────── + + - name: Post Slack message + if: github.event.action == 'opened' || github.event.action == 'reopened' + id: post + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_TITLE: ${{ github.event.pull_request.title }} + PR_URL: ${{ github.event.pull_request.html_url }} + ACTION_TEXT: ${{ steps.meta.outputs.action_text }} + ACTOR: ${{ steps.meta.outputs.actor }} + COLOR: ${{ steps.meta.outputs.color }} + CHANGES: ${{ steps.extract.outputs.changes }} + run: | + PAYLOAD=$(jq -n \ + --arg channel "$SLACK_CHANNEL" \ + --arg pr_url "$PR_URL" \ + --arg pr_num "$PR_NUMBER" \ + --arg action "$ACTION_TEXT" \ + --arg actor "$ACTOR" \ + --arg title "$PR_TITLE" \ + --arg color "$COLOR" \ + --arg changes "$CHANGES" \ + '{ + channel: $channel, + text: ("Pull request *#" + $pr_num + "* " + $action + " by " + $actor + ": <" + $pr_url + "|" + $title + ">"), + unfurl_links: false, + attachments: [{ + color: $color, + blocks: [{ + type: "context", + elements: [{ + type: "mrkdwn", + text: $changes + }] + }] + }] + }') + + RESPONSE=$(curl -s -X POST "https://slack.com/api/chat.postMessage" \ + -H "Authorization: Bearer ${SLACK_BOT_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "$PAYLOAD") + + OK=$(echo "$RESPONSE" | jq -r '.ok') + if [ "$OK" != "true" ]; then + echo "::error::Slack API error: $(echo "$RESPONSE" | jq -r '.error')" + exit 1 + fi + + echo "ts=$(echo "$RESPONSE" | jq -r '.ts')" >> "$GITHUB_OUTPUT" + echo "channel=$(echo "$RESPONSE" | jq -r '.channel')" >> "$GITHUB_OUTPUT" + + - name: Save Slack message reference + if: github.event.action == 'opened' || github.event.action == 'reopened' + uses: actions/github-script@v7 + env: + SLACK_TS: ${{ steps.post.outputs.ts }} + SLACK_CH: ${{ steps.post.outputs.channel }} + with: + script: | + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + body: `🔔 Slack notification sent\n` + }); + + # ── CLOSED / MERGED: Update Slack message ──────────────── + + - name: Update Slack message + if: github.event.action == 'closed' && steps.find.outputs.found == 'true' + env: + SLACK_TS: ${{ steps.find.outputs.ts }} + SLACK_CH: ${{ steps.find.outputs.channel }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_TITLE: ${{ github.event.pull_request.title }} + PR_URL: ${{ github.event.pull_request.html_url }} + ACTION_TEXT: ${{ steps.meta.outputs.action_text }} + ACTOR: ${{ steps.meta.outputs.actor }} + COLOR: ${{ steps.meta.outputs.color }} + CHANGES: ${{ steps.extract.outputs.changes }} + run: | + PAYLOAD=$(jq -n \ + --arg channel "$SLACK_CH" \ + --arg ts "$SLACK_TS" \ + --arg pr_url "$PR_URL" \ + --arg pr_num "$PR_NUMBER" \ + --arg action "$ACTION_TEXT" \ + --arg actor "$ACTOR" \ + --arg title "$PR_TITLE" \ + --arg color "$COLOR" \ + --arg changes "$CHANGES" \ + '{ + channel: $channel, + ts: $ts, + text: ("Pull request *#" + $pr_num + "* " + $action + " by " + $actor + ": <" + $pr_url + "|" + $title + ">"), + unfurl_links: false, + attachments: [{ + color: $color, + blocks: [{ + type: "context", + elements: [{ + type: "mrkdwn", + text: $changes + }] + }] + }] + }') + + RESPONSE=$(curl -s -X POST "https://slack.com/api/chat.update" \ + -H "Authorization: Bearer ${SLACK_BOT_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "$PAYLOAD") + + OK=$(echo "$RESPONSE" | jq -r '.ok') + if [ "$OK" != "true" ]; then + echo "::error::Slack API error: $(echo "$RESPONSE" | jq -r '.error')" + exit 1 + fi + + echo "✅ Slack message updated to: ${ACTION_TEXT}" From 175e85f84ac1834b7fc872e1dc409e9746be5723 Mon Sep 17 00:00:00 2001 From: Diego Pereira Date: Thu, 30 Apr 2026 16:44:38 -0300 Subject: [PATCH 3/3] ci: remove pr-slack-notify workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverts the file added in af64ecf. Out of scope for this PR — its purpose is the webhook event payload shape change in fx-webhook. --- .github/workflows/pr-slack-notify.yml | 222 -------------------------- 1 file changed, 222 deletions(-) delete mode 100644 .github/workflows/pr-slack-notify.yml diff --git a/.github/workflows/pr-slack-notify.yml b/.github/workflows/pr-slack-notify.yml deleted file mode 100644 index 172b331..0000000 --- a/.github/workflows/pr-slack-notify.yml +++ /dev/null @@ -1,222 +0,0 @@ -name: PR Slack Notification - -on: - pull_request: - types: [opened, reopened, closed] - -permissions: - pull-requests: write - -env: - SLACK_BOT_TOKEN: ${{ secrets.SLACK_PR_BOT_TOKEN }} - SLACK_CHANNEL: ${{ secrets.SLACK_PR_BACKEND_CHANNEL_ID }} - -jobs: - slack-notify: - runs-on: ubuntu-latest - steps: - - name: Extract key changes from PR body - id: extract - env: - PR_BODY: ${{ github.event.pull_request.body }} - run: | - # 1. Extract bullets from ## Key Changes section (per PR template) - changes=$(echo "$PR_BODY" | awk '/^## Key Changes/{found=1; next} /^## /{if(found) exit} found && /^- /{print}') - - # 2. Fallback: grab freeform text from ## Description - if [ -z "$changes" ]; then - changes=$(echo "$PR_BODY" | awk '/^## Description/{found=1; next} /^## /{if(found) exit} found' | sed '/^$/d' | head -5) - fi - - # 3. Last resort: use full PR body - if [ -z "$changes" ]; then - changes="$PR_BODY" - fi - - # Truncate total output to 2000 chars as safety limit - changes=$(printf '%s' "$changes" | head -c 2000) - - # Convert markdown to Slack mrkdwn format and limit to 3 bullets - changes=$(echo "$changes" | sed 's/^- /• /') - changes=$(echo "$changes" | sed 's/\*\*\([^*]*\)\*\*/\*\1\*/g') - changes=$(echo "$changes" | sed 's/`//g') - changes=$(echo "$changes" | head -3) - - delimiter=$(openssl rand -hex 8) - echo "changes<<${delimiter}" >> "$GITHUB_OUTPUT" - echo "$changes" >> "$GITHUB_OUTPUT" - echo "${delimiter}" >> "$GITHUB_OUTPUT" - - - name: Resolve action and color - id: meta - env: - EVENT_ACTION: ${{ github.event.action }} - PR_MERGED: ${{ github.event.pull_request.merged }} - PR_AUTHOR: ${{ github.event.pull_request.user.login }} - MERGED_BY: ${{ github.event.pull_request.merged_by.login }} - SENDER: ${{ github.event.sender.login }} - run: | - if [ "$EVENT_ACTION" = "opened" ] || [ "$EVENT_ACTION" = "reopened" ]; then - echo "action_text=created" >> "$GITHUB_OUTPUT" - echo "actor=$PR_AUTHOR" >> "$GITHUB_OUTPUT" - echo "color=#36a64f" >> "$GITHUB_OUTPUT" - elif [ "$PR_MERGED" = "true" ]; then - echo "action_text=merged" >> "$GITHUB_OUTPUT" - echo "actor=${MERGED_BY:-$SENDER}" >> "$GITHUB_OUTPUT" - echo "color=#8957e5" >> "$GITHUB_OUTPUT" - else - echo "action_text=closed" >> "$GITHUB_OUTPUT" - echo "actor=$SENDER" >> "$GITHUB_OUTPUT" - echo "color=#da1e28" >> "$GITHUB_OUTPUT" - fi - - # ── Find existing Slack message (used by closed/merged) ───── - - - name: Find Slack message reference - if: github.event.action == 'closed' - id: find - uses: actions/github-script@v7 - with: - script: | - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.payload.pull_request.number, - per_page: 100, - }); - - const marker = comments.findLast(c => c.body.includes('slack-pr-notify:')); - if (!marker) { - core.warning('No Slack message reference found'); - core.setOutput('found', 'false'); - return; - } - - const match = marker.body.match(/slack-pr-notify:(\{.*?\})/); - const data = JSON.parse(match[1]); - core.setOutput('found', 'true'); - core.setOutput('ts', data.ts); - core.setOutput('channel', data.channel); - - # ── OPENED / REOPENED: Post new Slack message ────────────── - - - name: Post Slack message - if: github.event.action == 'opened' || github.event.action == 'reopened' - id: post - env: - PR_NUMBER: ${{ github.event.pull_request.number }} - PR_TITLE: ${{ github.event.pull_request.title }} - PR_URL: ${{ github.event.pull_request.html_url }} - ACTION_TEXT: ${{ steps.meta.outputs.action_text }} - ACTOR: ${{ steps.meta.outputs.actor }} - COLOR: ${{ steps.meta.outputs.color }} - CHANGES: ${{ steps.extract.outputs.changes }} - run: | - PAYLOAD=$(jq -n \ - --arg channel "$SLACK_CHANNEL" \ - --arg pr_url "$PR_URL" \ - --arg pr_num "$PR_NUMBER" \ - --arg action "$ACTION_TEXT" \ - --arg actor "$ACTOR" \ - --arg title "$PR_TITLE" \ - --arg color "$COLOR" \ - --arg changes "$CHANGES" \ - '{ - channel: $channel, - text: ("Pull request *#" + $pr_num + "* " + $action + " by " + $actor + ": <" + $pr_url + "|" + $title + ">"), - unfurl_links: false, - attachments: [{ - color: $color, - blocks: [{ - type: "context", - elements: [{ - type: "mrkdwn", - text: $changes - }] - }] - }] - }') - - RESPONSE=$(curl -s -X POST "https://slack.com/api/chat.postMessage" \ - -H "Authorization: Bearer ${SLACK_BOT_TOKEN}" \ - -H "Content-Type: application/json" \ - -d "$PAYLOAD") - - OK=$(echo "$RESPONSE" | jq -r '.ok') - if [ "$OK" != "true" ]; then - echo "::error::Slack API error: $(echo "$RESPONSE" | jq -r '.error')" - exit 1 - fi - - echo "ts=$(echo "$RESPONSE" | jq -r '.ts')" >> "$GITHUB_OUTPUT" - echo "channel=$(echo "$RESPONSE" | jq -r '.channel')" >> "$GITHUB_OUTPUT" - - - name: Save Slack message reference - if: github.event.action == 'opened' || github.event.action == 'reopened' - uses: actions/github-script@v7 - env: - SLACK_TS: ${{ steps.post.outputs.ts }} - SLACK_CH: ${{ steps.post.outputs.channel }} - with: - script: | - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.payload.pull_request.number, - body: `🔔 Slack notification sent\n` - }); - - # ── CLOSED / MERGED: Update Slack message ──────────────── - - - name: Update Slack message - if: github.event.action == 'closed' && steps.find.outputs.found == 'true' - env: - SLACK_TS: ${{ steps.find.outputs.ts }} - SLACK_CH: ${{ steps.find.outputs.channel }} - PR_NUMBER: ${{ github.event.pull_request.number }} - PR_TITLE: ${{ github.event.pull_request.title }} - PR_URL: ${{ github.event.pull_request.html_url }} - ACTION_TEXT: ${{ steps.meta.outputs.action_text }} - ACTOR: ${{ steps.meta.outputs.actor }} - COLOR: ${{ steps.meta.outputs.color }} - CHANGES: ${{ steps.extract.outputs.changes }} - run: | - PAYLOAD=$(jq -n \ - --arg channel "$SLACK_CH" \ - --arg ts "$SLACK_TS" \ - --arg pr_url "$PR_URL" \ - --arg pr_num "$PR_NUMBER" \ - --arg action "$ACTION_TEXT" \ - --arg actor "$ACTOR" \ - --arg title "$PR_TITLE" \ - --arg color "$COLOR" \ - --arg changes "$CHANGES" \ - '{ - channel: $channel, - ts: $ts, - text: ("Pull request *#" + $pr_num + "* " + $action + " by " + $actor + ": <" + $pr_url + "|" + $title + ">"), - unfurl_links: false, - attachments: [{ - color: $color, - blocks: [{ - type: "context", - elements: [{ - type: "mrkdwn", - text: $changes - }] - }] - }] - }') - - RESPONSE=$(curl -s -X POST "https://slack.com/api/chat.update" \ - -H "Authorization: Bearer ${SLACK_BOT_TOKEN}" \ - -H "Content-Type: application/json" \ - -d "$PAYLOAD") - - OK=$(echo "$RESPONSE" | jq -r '.ok') - if [ "$OK" != "true" ]; then - echo "::error::Slack API error: $(echo "$RESPONSE" | jq -r '.error')" - exit 1 - fi - - echo "✅ Slack message updated to: ${ACTION_TEXT}"