From 967c6ee68bca74e426e769ffe8ba9e7f168fe65a Mon Sep 17 00:00:00 2001 From: BenjaminLangenakenSF Date: Mon, 17 Aug 2026 16:09:19 +0200 Subject: [PATCH 1/6] fix: pass GitHub context values via env to prevent PR titles from breaking the Slack changelog workflow --- .github/workflows/slack_changelog.yml | 47 +++++++++++++++------------ 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/.github/workflows/slack_changelog.yml b/.github/workflows/slack_changelog.yml index d4b1578..917227f 100644 --- a/.github/workflows/slack_changelog.yml +++ b/.github/workflows/slack_changelog.yml @@ -21,14 +21,13 @@ jobs: - name: Get PR Details id: pr-details + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_TITLE: ${{ github.event.pull_request.title }} + PR_AUTHOR: ${{ github.event.pull_request.user.login }} + PR_URL: ${{ github.event.pull_request.html_url }} + BRANCH_NAME: ${{ github.event.pull_request.head.ref }} run: | - # Extract PR information - PR_NUMBER="${{ github.event.pull_request.number }}" - PR_TITLE="${{ github.event.pull_request.title }}" - PR_AUTHOR="${{ github.event.pull_request.user.login }}" - PR_URL="${{ github.event.pull_request.html_url }}" - BRANCH_NAME="${{ github.event.pull_request.head.ref }}" - # Get changed files from the merge commit echo "Getting files from merge commit..." git diff --name-only HEAD~1 HEAD > changed_files.txt @@ -59,7 +58,7 @@ jobs: fi # Set outputs - echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT + echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT echo "pr_title=$PR_TITLE" >> $GITHUB_OUTPUT echo "pr_author=$PR_AUTHOR" >> $GITHUB_OUTPUT echo "pr_url=$PR_URL" >> $GITHUB_OUTPUT @@ -74,34 +73,42 @@ jobs: - name: Generate Changelog Message id: changelog + env: + PR_TITLE: ${{ steps.pr-details.outputs.pr_title }} + PR_AUTHOR: ${{ steps.pr-details.outputs.pr_author }} + PR_URL: ${{ steps.pr-details.outputs.pr_url }} + TEMPLATE_FOLDERS: ${{ steps.pr-details.outputs.template_folders }} + TOTAL_TEMPLATES: ${{ steps.pr-details.outputs.total_templates }} run: | # Create a formatted changelog message TIMESTAMP=$(TZ='Europe/Brussels' date +"%d/%m/%Y %H:%M") # Build the complete message (using clean Slack formatting) MESSAGE=" - 📝 ${{ steps.pr-details.outputs.pr_title }} - 👤 ${{ steps.pr-details.outputs.pr_author }} - 🔗 ${{ steps.pr-details.outputs.pr_url }} + 📝 $PR_TITLE + 👤 $PR_AUTHOR + 🔗 $PR_URL ⏰ $TIMESTAMP Templates: - ${{ steps.pr-details.outputs.template_folders }}" + $TEMPLATE_FOLDERS" # If more than 10 templates, add a note - if [ ${{ steps.pr-details.outputs.total_templates }} -gt 10 ]; then + if [ "$TOTAL_TEMPLATES" -gt 10 ]; then MESSAGE="$MESSAGE - _...and $((${{ steps.pr-details.outputs.total_templates }} - 10)) more templates_" + _...and $((TOTAL_TEMPLATES - 10)) more templates_" fi - # Set output (escape for JSON) - # Escape quotes and newlines for JSON - MESSAGE_ESCAPED=$(echo "$MESSAGE" | sed 's/"/\\"/g' | sed ':a;N;$!ba;s/\n/\\n/g') + # Set output (escape for JSON: backslashes first, then quotes, then newlines) + MESSAGE_ESCAPED=$(echo "$MESSAGE" | sed 's/\\/\\\\/g; s/"/\\"/g' | sed ':a;N;$!ba;s/\n/\\n/g') echo "message=$MESSAGE_ESCAPED" >> $GITHUB_OUTPUT - name: Post to Slack + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + SLACK_MESSAGE: ${{ steps.changelog.outputs.message }} run: | # Send changelog message as text field for Workflow Builder - curl -X POST "${{ secrets.SLACK_WEBHOOK_URL }}" \ - -H "Content-Type: application/json" \ - -d "{\"text\":\"${{ steps.changelog.outputs.message }}\"}" \ No newline at end of file + curl -X POST "$SLACK_WEBHOOK_URL" \ + -H "Content-Type: ion/json" \ + -d "{\"text\":\"$SLACK_MESSAGE\"}" From 98713ef9c2c998a7a310c5db6ac1893d1725f75d Mon Sep 17 00:00:00 2001 From: BenjaminLangenakenSF Date: Mon, 17 Aug 2026 16:11:50 +0200 Subject: [PATCH 2/6] fix: repair two lines mangled during terminal paste --- .github/workflows/slack_changelog.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/slack_changelog.yml b/.github/workflows/slack_changelog.yml index 917227f..f8f0b32 100644 --- a/.github/workflows/slack_changelog.yml +++ b/.github/workflows/slack_changelog.yml @@ -58,7 +58,7 @@ jobs: fi # Set outputs - echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT + echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT echo "pr_title=$PR_TITLE" >> $GITHUB_OUTPUT echo "pr_author=$PR_AUTHOR" >> $GITHUB_OUTPUT echo "pr_url=$PR_URL" >> $GITHUB_OUTPUT @@ -110,5 +110,5 @@ jobs: run: | # Send changelog message as text field for Workflow Builder curl -X POST "$SLACK_WEBHOOK_URL" \ - -H "Content-Type: ion/json" \ + -H "Content-Type: application/json" \ -d "{\"text\":\"$SLACK_MESSAGE\"}" From 0f7c909155c9c121ebcdf5bd18c920d2a96694e5 Mon Sep 17 00:00:00 2001 From: BenjaminLangenakenSF Date: Tue, 25 Aug 2026 08:56:55 +0200 Subject: [PATCH 3/6] fix: address review feedback on slack changelog workflow - count template folders before truncating the list to 10, so the "...and N more templates" note is reachable again - serialize the Slack text with `jq -Rs` instead of a sed chain, so tabs, carriage returns and other control characters can no longer produce an invalid JSON payload - fail the job on a 4xx/5xx from the webhook (`curl --fail --show-error`) - quote every `$GITHUB_OUTPUT` redirection target Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/slack_changelog.yml | 41 ++++++++++++++------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/.github/workflows/slack_changelog.yml b/.github/workflows/slack_changelog.yml index f8f0b32..f2499eb 100644 --- a/.github/workflows/slack_changelog.yml +++ b/.github/workflows/slack_changelog.yml @@ -35,7 +35,7 @@ jobs: cat changed_files.txt # Extract unique template folders - TEMPLATE_FOLDERS=$(cat changed_files.txt | \ + ALL_TEMPLATE_FOLDERS=$(cat changed_files.txt | \ grep -E "(reconciliation_texts|shared_parts|export_files|account_templates)/" | \ sed 's|/[^/]*$||' | \ sed 's|/text_parts||' | \ @@ -45,31 +45,30 @@ jobs: sed 's|^reconciliation_texts/||' | \ sed 's|^shared_parts/||' | \ sed 's|^export_files/||' | \ - sed 's|^account_templates/||' | \ - head -10 | \ - sed 's/^/• /') + sed 's|^account_templates/||') - # Count template folders - if [ -z "$TEMPLATE_FOLDERS" ]; then + # Count the full list first, then truncate it to 10 for display + if [ -z "$ALL_TEMPLATE_FOLDERS" ]; then TOTAL_TEMPLATES=0 TEMPLATE_FOLDERS="No template changes" else - TOTAL_TEMPLATES=$(echo "$TEMPLATE_FOLDERS" | wc -l) + TOTAL_TEMPLATES=$(echo "$ALL_TEMPLATE_FOLDERS" | wc -l | tr -d ' ') + TEMPLATE_FOLDERS=$(echo "$ALL_TEMPLATE_FOLDERS" | head -10 | sed 's/^/• /') fi # Set outputs - echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT - echo "pr_title=$PR_TITLE" >> $GITHUB_OUTPUT - echo "pr_author=$PR_AUTHOR" >> $GITHUB_OUTPUT - echo "pr_url=$PR_URL" >> $GITHUB_OUTPUT - echo "total_templates=$TOTAL_TEMPLATES" >> $GITHUB_OUTPUT + echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT" + echo "pr_title=$PR_TITLE" >> "$GITHUB_OUTPUT" + echo "pr_author=$PR_AUTHOR" >> "$GITHUB_OUTPUT" + echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT" + echo "total_templates=$TOTAL_TEMPLATES" >> "$GITHUB_OUTPUT" # Handle multiline output for template folders { echo 'template_folders<> $GITHUB_OUTPUT + } >> "$GITHUB_OUTPUT" - name: Generate Changelog Message id: changelog @@ -99,16 +98,20 @@ jobs: _...and $((TOTAL_TEMPLATES - 10)) more templates_" fi - # Set output (escape for JSON: backslashes first, then quotes, then newlines) - MESSAGE_ESCAPED=$(echo "$MESSAGE" | sed 's/\\/\\\\/g; s/"/\\"/g' | sed ':a;N;$!ba;s/\n/\\n/g') - echo "message=$MESSAGE_ESCAPED" >> $GITHUB_OUTPUT + # Set output as a complete JSON string (quotes included), so the Slack + # payload never has to re-escape it. jq -Rs handles backslashes, quotes, + # newlines, tabs and other control characters, and emits a single line. + MESSAGE_JSON=$(printf '%s' "$MESSAGE" | jq -Rs .) + echo "message=$MESSAGE_JSON" >> "$GITHUB_OUTPUT" - name: Post to Slack env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} SLACK_MESSAGE: ${{ steps.changelog.outputs.message }} run: | - # Send changelog message as text field for Workflow Builder - curl -X POST "$SLACK_WEBHOOK_URL" \ + # Send changelog message as text field for Workflow Builder. + # SLACK_MESSAGE is already a JSON string (jq -Rs), so it is not re-quoted. + # --fail --show-error makes a 4xx/5xx from the webhook fail the job. + curl --fail --show-error --silent -X POST "$SLACK_WEBHOOK_URL" \ -H "Content-Type: application/json" \ - -d "{\"text\":\"$SLACK_MESSAGE\"}" + -d "{\"text\":$SLACK_MESSAGE}" From 037bd592bfc008d7e1bb6434b7a21d90c9962d57 Mon Sep 17 00:00:00 2001 From: BenjaminLangenakenSF Date: Tue, 25 Aug 2026 15:52:01 +0200 Subject: [PATCH 4/6] fix: harden slack changelog checkout, curl retries and step outputs - checkout the base repo (github.repository) instead of the PR head repo: the merge commit only exists in the base repo, and this job carries an inherited SLACK_WEBHOOK_URL; add persist-credentials: false - curl: --fail-with-body so Slack's rejection reason survives, plus --retry/--connect-timeout/--max-time so a transient blip or hung POST does not fail a pull_request:closed run nobody can re-trigger - read PR title/author/url straight from the github context in the step that uses them; drop the unused pr_number output and BRANCH_NAME Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/slack_changelog.yml | 33 +++++++++++++-------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/.github/workflows/slack_changelog.yml b/.github/workflows/slack_changelog.yml index f2499eb..1a53c74 100644 --- a/.github/workflows/slack_changelog.yml +++ b/.github/workflows/slack_changelog.yml @@ -16,17 +16,14 @@ jobs: uses: actions/checkout@v6 with: fetch-depth: 0 - repository: ${{ github.event.pull_request.head.repo.full_name }} + # The merge commit only exists in the base repo, and this job holds an + # inherited SLACK_WEBHOOK_URL — never check out the PR author's repo. + repository: ${{ github.repository }} ref: ${{ github.event.pull_request.merge_commit_sha }} + persist-credentials: false - name: Get PR Details id: pr-details - env: - PR_NUMBER: ${{ github.event.pull_request.number }} - PR_TITLE: ${{ github.event.pull_request.title }} - PR_AUTHOR: ${{ github.event.pull_request.user.login }} - PR_URL: ${{ github.event.pull_request.html_url }} - BRANCH_NAME: ${{ github.event.pull_request.head.ref }} run: | # Get changed files from the merge commit echo "Getting files from merge commit..." @@ -56,11 +53,8 @@ jobs: TEMPLATE_FOLDERS=$(echo "$ALL_TEMPLATE_FOLDERS" | head -10 | sed 's/^/• /') fi - # Set outputs - echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT" - echo "pr_title=$PR_TITLE" >> "$GITHUB_OUTPUT" - echo "pr_author=$PR_AUTHOR" >> "$GITHUB_OUTPUT" - echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT" + # Set outputs (PR title/author/url are read straight from the github + # context by the next step, so they never pass through this channel) echo "total_templates=$TOTAL_TEMPLATES" >> "$GITHUB_OUTPUT" # Handle multiline output for template folders @@ -73,9 +67,9 @@ jobs: - name: Generate Changelog Message id: changelog env: - PR_TITLE: ${{ steps.pr-details.outputs.pr_title }} - PR_AUTHOR: ${{ steps.pr-details.outputs.pr_author }} - PR_URL: ${{ steps.pr-details.outputs.pr_url }} + PR_TITLE: ${{ github.event.pull_request.title }} + PR_AUTHOR: ${{ github.event.pull_request.user.login }} + PR_URL: ${{ github.event.pull_request.html_url }} TEMPLATE_FOLDERS: ${{ steps.pr-details.outputs.template_folders }} TOTAL_TEMPLATES: ${{ steps.pr-details.outputs.total_templates }} run: | @@ -111,7 +105,12 @@ jobs: run: | # Send changelog message as text field for Workflow Builder. # SLACK_MESSAGE is already a JSON string (jq -Rs), so it is not re-quoted. - # --fail --show-error makes a 4xx/5xx from the webhook fail the job. - curl --fail --show-error --silent -X POST "$SLACK_WEBHOOK_URL" \ + # --fail-with-body fails the job on 4xx/5xx but still prints Slack's + # reason (invalid_payload, no_text, ...). The retry/timeout flags keep + # a transient blip or a hung POST from failing a run nobody can + # re-trigger: this job runs on `pull_request: closed`. + curl --fail-with-body --show-error --silent -X POST "$SLACK_WEBHOOK_URL" \ + --retry 3 --retry-all-errors --retry-delay 5 \ + --connect-timeout 10 --max-time 30 \ -H "Content-Type: application/json" \ -d "{\"text\":$SLACK_MESSAGE}" From 178cdd989ddc906ce736eb944991a5a539120c26 Mon Sep 17 00:00:00 2001 From: BenjaminLangenakenSF Date: Tue, 25 Aug 2026 17:25:23 +0200 Subject: [PATCH 5/6] fix: narrow slack curl retries and escape mrkdwn in changelog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop --retry-all-errors so a permanent rejection (400 invalid_payload -> exit 22) fails on the first attempt instead of resending a doomed POST. Plain --retry still covers 408, 429, 5xx and connection/timeout failures, and --retry-max-time bounds the total since --max-time is per attempt. This also shrinks the duplicate-message window on this non-idempotent POST. Escape &, < and > in the PR title and author before building MESSAGE. The webhook `text` field is parsed as mrkdwn, so a contributor-controlled title could otherwise broadcast with or render a disguised link from a trusted bot. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/slack_changelog.yml | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/.github/workflows/slack_changelog.yml b/.github/workflows/slack_changelog.yml index 1a53c74..3644def 100644 --- a/.github/workflows/slack_changelog.yml +++ b/.github/workflows/slack_changelog.yml @@ -76,6 +76,17 @@ jobs: # Create a formatted changelog message TIMESTAMP=$(TZ='Europe/Brussels' date +"%d/%m/%Y %H:%M") + # Slack parses the webhook `text` field as mrkdwn, so escape the three + # characters that carry meaning there in the contributor-controlled + # values. Without this a title can broadcast with or render + # a disguised link from a trusted bot. Order matters: + # & first, or the entities below get double-escaped. + escape_mrkdwn() { + printf '%s' "$1" | sed -e 's/&/\&/g' -e 's//\>/g' + } + PR_TITLE=$(escape_mrkdwn "$PR_TITLE") + PR_AUTHOR=$(escape_mrkdwn "$PR_AUTHOR") + # Build the complete message (using clean Slack formatting) MESSAGE=" 📝 $PR_TITLE @@ -109,8 +120,12 @@ jobs: # reason (invalid_payload, no_text, ...). The retry/timeout flags keep # a transient blip or a hung POST from failing a run nobody can # re-trigger: this job runs on `pull_request: closed`. + # Plain --retry covers 408, 429, 5xx and connection/timeout failures; + # --retry-all-errors is deliberately absent so a permanent rejection + # (400 invalid_payload -> exit 22) fails at once instead of resending a + # doomed POST. --max-time is per attempt, --retry-max-time caps the total. curl --fail-with-body --show-error --silent -X POST "$SLACK_WEBHOOK_URL" \ - --retry 3 --retry-all-errors --retry-delay 5 \ + --retry 3 --retry-delay 5 --retry-max-time 60 \ --connect-timeout 10 --max-time 30 \ -H "Content-Type: application/json" \ -d "{\"text\":$SLACK_MESSAGE}" From 1a72d48fc9ec02e96a61d26a008da81bcdc39660 Mon Sep 17 00:00:00 2001 From: BenjaminLangenakenSF Date: Thu, 27 Aug 2026 09:46:37 +0200 Subject: [PATCH 6/6] fix: keep docs-only merges from aborting the changelog step grep exits 1 when a merge touches no template paths. Under the default `bash -e` shell the pipeline's status is the last sed's, so this is currently harmless, but adding `shell: bash` (which brings pipefail) would kill the assignment before the empty-list branch runs and the Slack post would silently never happen. `|| true` removes the footgun. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/slack_changelog.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/slack_changelog.yml b/.github/workflows/slack_changelog.yml index 3644def..1eb4cf0 100644 --- a/.github/workflows/slack_changelog.yml +++ b/.github/workflows/slack_changelog.yml @@ -31,7 +31,9 @@ jobs: echo "Files changed:" cat changed_files.txt - # Extract unique template folders + # Extract unique template folders. The trailing `|| true` keeps a + # docs-only merge (no template paths, so grep exits 1) from killing + # the step if this ever runs under `shell: bash`, which adds pipefail. ALL_TEMPLATE_FOLDERS=$(cat changed_files.txt | \ grep -E "(reconciliation_texts|shared_parts|export_files|account_templates)/" | \ sed 's|/[^/]*$||' | \ @@ -42,7 +44,7 @@ jobs: sed 's|^reconciliation_texts/||' | \ sed 's|^shared_parts/||' | \ sed 's|^export_files/||' | \ - sed 's|^account_templates/||') + sed 's|^account_templates/||' || true) # Count the full list first, then truncate it to 10 for display if [ -z "$ALL_TEMPLATE_FOLDERS" ]; then