From 5f9519c9fe51d04f1737334011103d154a3bc18a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Aug 2026 22:13:14 +0000 Subject: [PATCH 1/6] Add Slack notification for new issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Posts to #gh-wallet-eng whenever an issue is opened. Fires on `opened` only, which the GitHub Slack app's `issues` subscription cannot express (it always covers opened + closed + reopened). Notifies for every new issue for now; the workflow comments document the `author_association` filter to add if the channel becomes too noisy. This repo had no Slack workflows previously, so the SLACK_WEBHOOK secret needs to resolve here — see the note in the workflow header. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_017Ka8rvrCJEKLroVskzayvo --- .github/workflows/notify-slack-new-issue.yml | 54 ++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 .github/workflows/notify-slack-new-issue.yml diff --git a/.github/workflows/notify-slack-new-issue.yml b/.github/workflows/notify-slack-new-issue.yml new file mode 100644 index 000000000..bafa6b2f0 --- /dev/null +++ b/.github/workflows/notify-slack-new-issue.yml @@ -0,0 +1,54 @@ +# Posts a Slack notification to #gh-wallet-eng whenever a new issue is opened. +# +# Fires on issue creation only (`types: [opened]`) — not on close, reopen, or +# edit. The GitHub Slack app's `/github subscribe issues` cannot be +# narrowed this way; its `issues` feature always covers opened + closed + +# reopened, and it has no author filter at all. That is why this is a workflow +# rather than a subscription. +# +# Currently notifies for EVERY new issue, including ones opened by the team. +# If the channel gets too noisy, restrict it to externally-authored issues by +# adding this to the `notify` job: +# +# if: >- +# github.event.issue.user.type != 'Bot' && +# !contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), +# github.event.issue.author_association) +# +# Note that `author_association` reports MEMBER based on Stellar org +# membership; verify it behaves as expected for teammates whose org membership +# is private before relying on it. +# +# Requires a SLACK_WEBHOOK secret. This repo had no Slack workflows before, so +# confirm the secret resolves here — it is used by several workflows in +# stellar/freighter, but if it is a repo-level secret there rather than an +# org-level one it must be added to this repo separately. + +name: Notify Slack of new issue + +on: + issues: + types: [opened] + +permissions: + contents: read + +jobs: + notify: + name: Notify Slack + runs-on: ubuntu-latest + steps: + - name: Slack Notification + uses: rtCamp/action-slack-notify@07cbdbfd6c6190970778d8f98f11d073b2932aae #v2.3.3 + env: + MSG_MINIMAL: true + SLACK_CHANNEL: gh-wallet-eng + SLACK_COLOR: "#70E1C8" + SLACK_ICON: https://github.com/stellar/freighter/blob/master/docs/static/images/logo.png?size=48 + SLACK_MESSAGE: + "<${{ github.event.issue.html_url }}|#${{ github.event.issue.number + }}: ${{ github.event.issue.title }}> — opened by ${{ + github.event.issue.user.login }}" + SLACK_TITLE: "New issue in ${{ github.repository }}" + SLACK_USERNAME: Freighter Administrative Assistant + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} From c3d25fa8178617f9879e132564f4c20a0b994773 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Aug 2026 22:36:25 +0000 Subject: [PATCH 2/6] Use dedicated webhook secret for issue notifications A Slack-app incoming webhook is bound to the channel it was installed against and ignores the channel in the payload, so SLACK_CHANNEL alone cannot redirect traffic to #gh-wallet-eng. Point this workflow at its own SLACK_WEBHOOK_GH_ISSUES secret, holding a webhook bound to #gh-wallet-eng. This repo has no other Slack workflows, so this is the only Slack secret it needs. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_017Ka8rvrCJEKLroVskzayvo --- .github/workflows/notify-slack-new-issue.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/notify-slack-new-issue.yml b/.github/workflows/notify-slack-new-issue.yml index bafa6b2f0..b23ebd01d 100644 --- a/.github/workflows/notify-slack-new-issue.yml +++ b/.github/workflows/notify-slack-new-issue.yml @@ -19,10 +19,13 @@ # membership; verify it behaves as expected for teammates whose org membership # is private before relying on it. # -# Requires a SLACK_WEBHOOK secret. This repo had no Slack workflows before, so -# confirm the secret resolves here — it is used by several workflows in -# stellar/freighter, but if it is a repo-level secret there rather than an -# org-level one it must be added to this repo separately. +# Requires a SLACK_WEBHOOK_GH_ISSUES secret on this repo, holding an incoming +# webhook bound to #gh-wallet-eng. This is deliberately separate from the +# SLACK_WEBHOOK secret used by the freighter release workflows: a Slack-app +# incoming webhook is bound to the channel it was installed against and +# ignores the channel in the payload, so posting to a different channel needs +# its own webhook. SLACK_CHANNEL below is kept for documentation and only +# takes effect on legacy webhooks. name: Notify Slack of new issue @@ -51,4 +54,4 @@ jobs: github.event.issue.user.login }}" SLACK_TITLE: "New issue in ${{ github.repository }}" SLACK_USERNAME: Freighter Administrative Assistant - SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_GH_ISSUES }} From e417c7072fbebc8bfb54789ccbb6cbae23cb401b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Aug 2026 23:09:35 +0000 Subject: [PATCH 3/6] Post via slackapi/slack-github-action with a Block Kit payload Addresses review feedback on the notification step. Issue titles are attacker-controlled, and the previous SLACK_MESSAGE put the title inside Slack's syntax unescaped. A title could terminate the link and inject an arbitrary hyperlink into the channel. Switch to slackapi/slack-github-action and put the title in a Block Kit plain_text object, which Slack does not parse as mrkdwn, so link and mention syntax renders literally. The payload is built with `jq --arg` and passed as a file, so untrusted text is never interpolated into YAML or JSON syntax. The top-level fallback text, which IS parsed as mrkdwn, escapes &, < and > instead. This also avoids introducing a supply-chain gap to this repo: rtCamp's action is a composite that runs docker://ghcr.io/rtcamp/action-slack-notify at a mutable tag, so a SHA pin would not have pinned the executed code. slackapi's action is node24, so the SHA pin covers what actually runs. Drop the unused repository token permissions while here. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_017Ka8rvrCJEKLroVskzayvo --- .github/workflows/notify-slack-new-issue.yml | 98 ++++++++++++++++---- 1 file changed, 78 insertions(+), 20 deletions(-) diff --git a/.github/workflows/notify-slack-new-issue.yml b/.github/workflows/notify-slack-new-issue.yml index b23ebd01d..19b4178b4 100644 --- a/.github/workflows/notify-slack-new-issue.yml +++ b/.github/workflows/notify-slack-new-issue.yml @@ -21,11 +21,33 @@ # # Requires a SLACK_WEBHOOK_GH_ISSUES secret on this repo, holding an incoming # webhook bound to #gh-wallet-eng. This is deliberately separate from the -# SLACK_WEBHOOK secret used by the freighter release workflows: a Slack-app -# incoming webhook is bound to the channel it was installed against and -# ignores the channel in the payload, so posting to a different channel needs -# its own webhook. SLACK_CHANNEL below is kept for documentation and only -# takes effect on legacy webhooks. +# SLACK_WEBHOOK secret used by the stellar/freighter release workflows, so that +# a workflow fed by untrusted input does not share the release webhook. +# +# ============================================================================ +# Why this uses slackapi/slack-github-action +# ============================================================================ +# Issue titles are attacker-controlled: anyone can open an issue here, and the +# title is relayed verbatim into an internal channel. Two properties matter. +# +# 1. slackapi/slack-github-action is `runs: using: node24`, so pinning it by +# commit SHA pins the code that actually executes. The rtCamp action used by +# the freighter release workflows is a composite that shells out to +# `docker://ghcr.io/rtcamp/action-slack-notify` at a MUTABLE tag, so a SHA +# pin there does not pin what really runs. +# +# 2. The title goes into a Block Kit `plain_text` object, which Slack does not +# parse as mrkdwn. A title such as `` +# therefore renders literally instead of becoming a live link. Building the +# payload with `jq --arg` and handing it over as a file means no untrusted +# text is ever interpolated into YAML or JSON syntax. +# +# The `plain_text` title is deliberately NOT entity-escaped — plain_text is not +# mrkdwn, so `<` would render as literal "<". The top-level `text` +# fallback (used for push notifications, where mrkdwn IS parsed) is escaped +# instead. Every other interpolated value is GitHub-generated and constrained: +# logins are alphanumeric plus hyphens, and the URL and number come from the +# API. name: Notify Slack of new issue @@ -33,25 +55,61 @@ on: issues: types: [opened] -permissions: - contents: read +# Nothing here needs the repository token. +permissions: {} jobs: notify: name: Notify Slack runs-on: ubuntu-latest steps: - - name: Slack Notification - uses: rtCamp/action-slack-notify@07cbdbfd6c6190970778d8f98f11d073b2932aae #v2.3.3 + - name: Build Slack payload env: - MSG_MINIMAL: true - SLACK_CHANNEL: gh-wallet-eng - SLACK_COLOR: "#70E1C8" - SLACK_ICON: https://github.com/stellar/freighter/blob/master/docs/static/images/logo.png?size=48 - SLACK_MESSAGE: - "<${{ github.event.issue.html_url }}|#${{ github.event.issue.number - }}: ${{ github.event.issue.title }}> — opened by ${{ - github.event.issue.user.login }}" - SLACK_TITLE: "New issue in ${{ github.repository }}" - SLACK_USERNAME: Freighter Administrative Assistant - SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_GH_ISSUES }} + ISSUE_TITLE: ${{ github.event.issue.title }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + ISSUE_URL: ${{ github.event.issue.html_url }} + ISSUE_AUTHOR: ${{ github.event.issue.user.login }} + REPO: ${{ github.repository }} + run: | + jq -n \ + --arg title "$ISSUE_TITLE" \ + --arg number "$ISSUE_NUMBER" \ + --arg url "$ISSUE_URL" \ + --arg author "$ISSUE_AUTHOR" \ + --arg repo "$REPO" \ + ' + # Escape Slack mrkdwn control characters, "&" first so that the + # entities introduced here are not themselves re-escaped. + def slack_escape: + gsub("&"; "&") | gsub("<"; "<") | gsub(">"; ">"); + { + text: ("New issue in " + $repo + ": #" + $number + ": " + + ($title | slack_escape)), + blocks: [ + { + type: "section", + text: { + type: "plain_text", + text: ("#" + $number + ": " + $title), + emoji: false + } + }, + { + type: "context", + elements: [ + { + type: "mrkdwn", + text: ("New issue in *" + $repo + "* — opened by " + + $author + " • <" + $url + "|View issue>") + } + ] + } + ] + }' > "$RUNNER_TEMP/slack-payload.json" + + - name: Notify Slack + uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0 + with: + webhook: ${{ secrets.SLACK_WEBHOOK_GH_ISSUES }} + webhook-type: incoming-webhook + payload-file-path: ${{ runner.temp }}/slack-payload.json From eb92cf5aba8394a117eafd453580b8aaa3ad73c3 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Aug 2026 23:24:44 +0000 Subject: [PATCH 4/6] Trim workflow comments to describe behavior only Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_017Ka8rvrCJEKLroVskzayvo --- .github/workflows/notify-slack-new-issue.yml | 59 +++----------------- 1 file changed, 8 insertions(+), 51 deletions(-) diff --git a/.github/workflows/notify-slack-new-issue.yml b/.github/workflows/notify-slack-new-issue.yml index 19b4178b4..c0098341c 100644 --- a/.github/workflows/notify-slack-new-issue.yml +++ b/.github/workflows/notify-slack-new-issue.yml @@ -1,53 +1,12 @@ -# Posts a Slack notification to #gh-wallet-eng whenever a new issue is opened. +# Posts a Slack notification to #gh-wallet-eng when an issue is opened. # -# Fires on issue creation only (`types: [opened]`) — not on close, reopen, or -# edit. The GitHub Slack app's `/github subscribe issues` cannot be -# narrowed this way; its `issues` feature always covers opened + closed + -# reopened, and it has no author filter at all. That is why this is a workflow -# rather than a subscription. +# Requires the SLACK_WEBHOOK_GH_ISSUES secret: an incoming webhook bound to +# that channel. # -# Currently notifies for EVERY new issue, including ones opened by the team. -# If the channel gets too noisy, restrict it to externally-authored issues by -# adding this to the `notify` job: -# -# if: >- -# github.event.issue.user.type != 'Bot' && -# !contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), -# github.event.issue.author_association) -# -# Note that `author_association` reports MEMBER based on Stellar org -# membership; verify it behaves as expected for teammates whose org membership -# is private before relying on it. -# -# Requires a SLACK_WEBHOOK_GH_ISSUES secret on this repo, holding an incoming -# webhook bound to #gh-wallet-eng. This is deliberately separate from the -# SLACK_WEBHOOK secret used by the stellar/freighter release workflows, so that -# a workflow fed by untrusted input does not share the release webhook. -# -# ============================================================================ -# Why this uses slackapi/slack-github-action -# ============================================================================ -# Issue titles are attacker-controlled: anyone can open an issue here, and the -# title is relayed verbatim into an internal channel. Two properties matter. -# -# 1. slackapi/slack-github-action is `runs: using: node24`, so pinning it by -# commit SHA pins the code that actually executes. The rtCamp action used by -# the freighter release workflows is a composite that shells out to -# `docker://ghcr.io/rtcamp/action-slack-notify` at a MUTABLE tag, so a SHA -# pin there does not pin what really runs. -# -# 2. The title goes into a Block Kit `plain_text` object, which Slack does not -# parse as mrkdwn. A title such as `` -# therefore renders literally instead of becoming a live link. Building the -# payload with `jq --arg` and handing it over as a file means no untrusted -# text is ever interpolated into YAML or JSON syntax. -# -# The `plain_text` title is deliberately NOT entity-escaped — plain_text is not -# mrkdwn, so `<` would render as literal "<". The top-level `text` -# fallback (used for push notifications, where mrkdwn IS parsed) is escaped -# instead. Every other interpolated value is GitHub-generated and constrained: -# logins are alphanumeric plus hyphens, and the URL and number come from the -# API. +# The issue title is untrusted input. It is rendered through a Block Kit +# plain_text object, which Slack does not parse as mrkdwn, so link and mention +# syntax in a title cannot take effect. Do not move the title into a mrkdwn +# field. The top-level text fallback is mrkdwn-parsed, so it is escaped. name: Notify Slack of new issue @@ -55,7 +14,6 @@ on: issues: types: [opened] -# Nothing here needs the repository token. permissions: {} jobs: @@ -78,8 +36,7 @@ jobs: --arg author "$ISSUE_AUTHOR" \ --arg repo "$REPO" \ ' - # Escape Slack mrkdwn control characters, "&" first so that the - # entities introduced here are not themselves re-escaped. + # "&" first, so the entities introduced here are not re-escaped. def slack_escape: gsub("&"; "&") | gsub("<"; "<") | gsub(">"; ">"); { From 1f272d61eec5d67478b8c7849c07eef66b3271d1 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 11 Aug 2026 23:16:22 +0000 Subject: [PATCH 5/6] TEMP: verify Slack webhook end to end (delete before merge) Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_017Ka8rvrCJEKLroVskzayvo --- .github/workflows/tmp-slack-test.yml | 70 ++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 .github/workflows/tmp-slack-test.yml diff --git a/.github/workflows/tmp-slack-test.yml b/.github/workflows/tmp-slack-test.yml new file mode 100644 index 000000000..401d71442 --- /dev/null +++ b/.github/workflows/tmp-slack-test.yml @@ -0,0 +1,70 @@ +# TEMPORARY — delete before merging. Verifies SLACK_WEBHOOK_GH_ISSUES end to +# end by running the same payload build and post as notify-slack-new-issue.yml +# against fixed values, since `issues` events only run workflows from the +# default branch. + +name: TEMP Slack notification test + +on: + push: + branches: [claude/slack-external-issue-notifications-d3wuc6] + +permissions: {} + +jobs: + notify: + name: Notify Slack (test) + runs-on: ubuntu-latest + steps: + - name: Build Slack payload + env: + ISSUE_TITLE: '[TEST - please ignore] escaping check & "quotes"' + ISSUE_NUMBER: "0" + ISSUE_URL: https://github.com/stellar/freighter-mobile/pull/972 + ISSUE_AUTHOR: workflow-test + REPO: ${{ github.repository }} + run: | + jq -n \ + --arg title "$ISSUE_TITLE" \ + --arg number "$ISSUE_NUMBER" \ + --arg url "$ISSUE_URL" \ + --arg author "$ISSUE_AUTHOR" \ + --arg repo "$REPO" \ + ' + # "&" first, so the entities introduced here are not re-escaped. + def slack_escape: + gsub("&"; "&") | gsub("<"; "<") | gsub(">"; ">"); + { + text: ("New issue in " + $repo + ": #" + $number + ": " + + ($title | slack_escape)), + blocks: [ + { + type: "section", + text: { + type: "plain_text", + text: ("#" + $number + ": " + $title), + emoji: false + } + }, + { + type: "context", + elements: [ + { + type: "mrkdwn", + text: ("New issue in *" + $repo + "* — opened by " + + $author + " • <" + $url + "|View issue>") + } + ] + } + ] + }' > "$RUNNER_TEMP/slack-payload.json" + + - name: Show payload + run: cat "$RUNNER_TEMP/slack-payload.json" + + - name: Notify Slack + uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0 + with: + webhook: ${{ secrets.SLACK_WEBHOOK_GH_ISSUES }} + webhook-type: incoming-webhook + payload-file-path: ${{ runner.temp }}/slack-payload.json From 9099f566853b6acbcfb19f75b54af8eac23e9372 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 11 Aug 2026 23:17:19 +0000 Subject: [PATCH 6/6] Remove temporary Slack webhook test workflow Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_017Ka8rvrCJEKLroVskzayvo --- .github/workflows/tmp-slack-test.yml | 70 ---------------------------- 1 file changed, 70 deletions(-) delete mode 100644 .github/workflows/tmp-slack-test.yml diff --git a/.github/workflows/tmp-slack-test.yml b/.github/workflows/tmp-slack-test.yml deleted file mode 100644 index 401d71442..000000000 --- a/.github/workflows/tmp-slack-test.yml +++ /dev/null @@ -1,70 +0,0 @@ -# TEMPORARY — delete before merging. Verifies SLACK_WEBHOOK_GH_ISSUES end to -# end by running the same payload build and post as notify-slack-new-issue.yml -# against fixed values, since `issues` events only run workflows from the -# default branch. - -name: TEMP Slack notification test - -on: - push: - branches: [claude/slack-external-issue-notifications-d3wuc6] - -permissions: {} - -jobs: - notify: - name: Notify Slack (test) - runs-on: ubuntu-latest - steps: - - name: Build Slack payload - env: - ISSUE_TITLE: '[TEST - please ignore] escaping check & "quotes"' - ISSUE_NUMBER: "0" - ISSUE_URL: https://github.com/stellar/freighter-mobile/pull/972 - ISSUE_AUTHOR: workflow-test - REPO: ${{ github.repository }} - run: | - jq -n \ - --arg title "$ISSUE_TITLE" \ - --arg number "$ISSUE_NUMBER" \ - --arg url "$ISSUE_URL" \ - --arg author "$ISSUE_AUTHOR" \ - --arg repo "$REPO" \ - ' - # "&" first, so the entities introduced here are not re-escaped. - def slack_escape: - gsub("&"; "&") | gsub("<"; "<") | gsub(">"; ">"); - { - text: ("New issue in " + $repo + ": #" + $number + ": " - + ($title | slack_escape)), - blocks: [ - { - type: "section", - text: { - type: "plain_text", - text: ("#" + $number + ": " + $title), - emoji: false - } - }, - { - type: "context", - elements: [ - { - type: "mrkdwn", - text: ("New issue in *" + $repo + "* — opened by " - + $author + " • <" + $url + "|View issue>") - } - ] - } - ] - }' > "$RUNNER_TEMP/slack-payload.json" - - - name: Show payload - run: cat "$RUNNER_TEMP/slack-payload.json" - - - name: Notify Slack - uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0 - with: - webhook: ${{ secrets.SLACK_WEBHOOK_GH_ISSUES }} - webhook-type: incoming-webhook - payload-file-path: ${{ runner.temp }}/slack-payload.json