From 19ca2b63e7122e8c6f48ac01b3aeaaf0a09d98e1 Mon Sep 17 00:00:00 2001 From: Andreas Ronneseth Date: Sat, 3 Jan 2026 13:56:21 -0700 Subject: [PATCH 1/5] fix(deploy): remove channel field from webhook payload Remove the channel field from the Slack webhook payload as incoming webhooks post to the channel configured in the webhook URL. The SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK is correct for webhook URLs. DEVEX-693 --- .github/workflows/slack-deploy-notification.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/slack-deploy-notification.yaml b/.github/workflows/slack-deploy-notification.yaml index b86f269..e2c5409 100644 --- a/.github/workflows/slack-deploy-notification.yaml +++ b/.github/workflows/slack-deploy-notification.yaml @@ -36,7 +36,6 @@ jobs: with: payload: | { - "channel": "#release", "text": "${{ github.event.repository.name }} deployed", "blocks": [ { From c785c2aebd1521790ea5c34bc6bcddb688423a8e Mon Sep 17 00:00:00 2001 From: Andreas Ronneseth Date: Sat, 3 Jan 2026 14:00:32 -0700 Subject: [PATCH 2/5] fix(deploy): conditionally show APM link in Slack notification Make the APM link conditional - only show it when apm_url is explicitly provided. This fixes issues where workflows that don't provide apm_url were getting broken or inappropriate links. DEVEX-693 --- .github/workflows/slack-deploy-notification.yaml | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/workflows/slack-deploy-notification.yaml b/.github/workflows/slack-deploy-notification.yaml index e2c5409..06f7303 100644 --- a/.github/workflows/slack-deploy-notification.yaml +++ b/.github/workflows/slack-deploy-notification.yaml @@ -19,7 +19,7 @@ on: description: "url to service APM" required: false type: string - default: "https://app.datadoghq.com/services?env=production" + default: "" secrets: slack_webhook_url: required: true @@ -28,6 +28,18 @@ jobs: notify: runs-on: ubuntu-latest steps: + - name: Build message text + id: build-message + run: | + BASE_TEXT="*${{ github.event.repository.name }} deployed by ${{ github.triggering_actor }}*" + if [ -n "${{ inputs.apm_url }}" ]; then + MESSAGE_TEXT="${BASE_TEXT}\n\tView the <${{ inputs.apm_url }}|Service APM>" + else + MESSAGE_TEXT="${BASE_TEXT}" + fi + echo "message_text<> $GITHUB_OUTPUT + echo "$MESSAGE_TEXT" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT - name: Send deployment notification uses: slackapi/slack-github-action@v1.24.0 env: @@ -42,7 +54,7 @@ jobs: "type": "section", "text": { "type": "mrkdwn", - "text": "*${{ github.event.repository.name }} deployed by ${{ github.triggering_actor }}*\n\tView the <${{ inputs.apm_url }}|Service APM>" + "text": "${{ steps.build-message.outputs.message_text }}" }, "accessory": { "type": "button", From 40c789ec67821910ec92dbf5e331a81f4e63616b Mon Sep 17 00:00:00 2001 From: Andreas Ronneseth Date: Sat, 3 Jan 2026 14:04:10 -0700 Subject: [PATCH 3/5] fix(deploy): replace slack action with direct curl to fix CI errors Replace slackapi/slack-github-action with direct curl command to avoid action parsing errors. This matches the working curl example and should resolve the 'channel: command not found' CI errors. DEVEX-693 --- .../workflows/slack-deploy-notification.yaml | 67 ++++++++++--------- 1 file changed, 36 insertions(+), 31 deletions(-) diff --git a/.github/workflows/slack-deploy-notification.yaml b/.github/workflows/slack-deploy-notification.yaml index 06f7303..d82cffb 100644 --- a/.github/workflows/slack-deploy-notification.yaml +++ b/.github/workflows/slack-deploy-notification.yaml @@ -28,42 +28,47 @@ jobs: notify: runs-on: ubuntu-latest steps: - - name: Build message text - id: build-message + - name: Build and send Slack notification + env: + SLACK_WEBHOOK_URL: ${{ secrets.slack_webhook_url }} run: | BASE_TEXT="*${{ github.event.repository.name }} deployed by ${{ github.triggering_actor }}*" - if [ -n "${{ inputs.apm_url }}" ]; then - MESSAGE_TEXT="${BASE_TEXT}\n\tView the <${{ inputs.apm_url }}|Service APM>" + if [ -n "${{ inputs.apm_url }}" ] && [ "${{ inputs.apm_url }}" != "" ]; then + MESSAGE_TEXT="${BASE_TEXT}\\n\\tView the <${{ inputs.apm_url }}|Service APM>" else MESSAGE_TEXT="${BASE_TEXT}" fi - echo "message_text<> $GITHUB_OUTPUT - echo "$MESSAGE_TEXT" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - - name: Send deployment notification - uses: slackapi/slack-github-action@v1.24.0 - env: - SLACK_WEBHOOK_URL: ${{ secrets.slack_webhook_url }} - SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK - with: - payload: | - { - "text": "${{ github.event.repository.name }} deployed", - "blocks": [ - { - "type": "section", + + # Escape the message text for JSON + ESCAPED_MESSAGE=$(echo "$MESSAGE_TEXT" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g') + REPO_NAME="${{ github.event.repository.name }}" + IMAGE_TAG="${{ inputs.image_tag }}" + RELEASE_URL="${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ inputs.image_tag }}" + + PAYLOAD=$(cat < Date: Sat, 3 Jan 2026 14:26:07 -0700 Subject: [PATCH 4/5] revert: restore slack-deploy-notification to original state Revert all DEVEX-693 changes and restore the workflow to its original state before DEVEX-693 work began. The webhook URL in GitHub secrets should be updated to point to the #release channel instead. DEVEX-693 --- .../workflows/slack-deploy-notification.yaml | 63 +++++++------------ 1 file changed, 23 insertions(+), 40 deletions(-) diff --git a/.github/workflows/slack-deploy-notification.yaml b/.github/workflows/slack-deploy-notification.yaml index d82cffb..c8d4db1 100644 --- a/.github/workflows/slack-deploy-notification.yaml +++ b/.github/workflows/slack-deploy-notification.yaml @@ -19,7 +19,7 @@ on: description: "url to service APM" required: false type: string - default: "" + default: "https://app.datadoghq.com/services?env=production" secrets: slack_webhook_url: required: true @@ -28,47 +28,30 @@ jobs: notify: runs-on: ubuntu-latest steps: - - name: Build and send Slack notification + - name: Send deployment notification + uses: slackapi/slack-github-action@v1.24.0 env: SLACK_WEBHOOK_URL: ${{ secrets.slack_webhook_url }} - run: | - BASE_TEXT="*${{ github.event.repository.name }} deployed by ${{ github.triggering_actor }}*" - if [ -n "${{ inputs.apm_url }}" ] && [ "${{ inputs.apm_url }}" != "" ]; then - MESSAGE_TEXT="${BASE_TEXT}\\n\\tView the <${{ inputs.apm_url }}|Service APM>" - else - MESSAGE_TEXT="${BASE_TEXT}" - fi - - # Escape the message text for JSON - ESCAPED_MESSAGE=$(echo "$MESSAGE_TEXT" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g') - REPO_NAME="${{ github.event.repository.name }}" - IMAGE_TAG="${{ inputs.image_tag }}" - RELEASE_URL="${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ inputs.image_tag }}" - - PAYLOAD=$(cat <\n\tView the <${{ inputs.apm_url }}|Service APM>" }, - "url": "${RELEASE_URL}" + "accessory": { + "type": "button", + "text": { + "type": "plain_text", + "text": "${{ inputs.image_tag }}" + }, + "url": "${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ inputs.image_tag }}" + } } - } - ] - } - JSON - ) - - curl -X POST -H 'Content-type: application/json' \ - --data "$PAYLOAD" \ - "$SLACK_WEBHOOK_URL" + ] + } From 3373da2a391ab2495ad3188fd037e978d25cce7d Mon Sep 17 00:00:00 2001 From: Andreas Ronneseth Date: Sat, 3 Jan 2026 14:32:55 -0700 Subject: [PATCH 5/5] security: fix command injection risk in JIRA ticket check workflow Fix potential security issue where JIRA ticket description content could be interpreted as shell commands: 1. Only request 'status' field from JIRA API instead of full ticket (avoids processing description) 2. Use temporary file to safely store curl response instead of direct variable assignment 3. Use printf instead of echo when processing user-controlled content (PR body, commit messages) 4. Use HTTP status codes for error checking instead of parsing JSON error messages 5. Properly quote all variables and use jq with error handling This prevents ticket descriptions containing shell-like text from being executed as commands. DEVEX-693 --- .github/workflows/jira-ticket-check.yaml | 65 +++++++++++++++--------- 1 file changed, 40 insertions(+), 25 deletions(-) diff --git a/.github/workflows/jira-ticket-check.yaml b/.github/workflows/jira-ticket-check.yaml index 5319428..7b01cd2 100644 --- a/.github/workflows/jira-ticket-check.yaml +++ b/.github/workflows/jira-ticket-check.yaml @@ -120,14 +120,15 @@ jobs: id: pr-description-check if: steps.pr-title-check.outputs.ticket == '' run: | - PR_BODY="${{ github.event.pull_request.body }}" + # Use printf instead of echo to safely handle special characters + PR_BODY=$(printf '%s' "${{ github.event.pull_request.body }}") JIRA_PROJECT_PATTERN="${{ inputs.jira-project-pattern }}" if [ -z "$JIRA_PROJECT_PATTERN" ]; then JIRA_PROJECT_PATTERN="[A-Za-z]+" else # Make pattern case-insensitive by adding both cases if it's a simple pattern # For complex patterns, rely on grep -i flag - JIRA_PROJECT_PATTERN=$(echo "$JIRA_PROJECT_PATTERN" | sed 's/\[A-Z\]\+/[A-Za-z]+/g' | sed 's/\[A-Z\]/[A-Za-z]/g') + JIRA_PROJECT_PATTERN=$(printf '%s' "$JIRA_PROJECT_PATTERN" | sed 's/\[A-Z\]\+/[A-Za-z]+/g' | sed 's/\[A-Z\]/[A-Za-z]/g') fi JIRA_PATTERN="${JIRA_PROJECT_PATTERN}-[0-9]+" @@ -137,11 +138,11 @@ jobs: exit 0 fi - # Case-insensitive search - if echo "$PR_BODY" | grep -qiE "$JIRA_PATTERN"; then - TICKET=$(echo "$PR_BODY" | grep -oiE "$JIRA_PATTERN" | head -1) + # Case-insensitive search - use printf to safely pass content to grep + if printf '%s' "$PR_BODY" | grep -qiE "$JIRA_PATTERN"; then + TICKET=$(printf '%s' "$PR_BODY" | grep -oiE "$JIRA_PATTERN" | head -1) # Normalize to uppercase for Jira API (Jira ticket keys are uppercase) - TICKET=$(echo "$TICKET" | tr '[:lower:]' '[:upper:]') + TICKET=$(printf '%s' "$TICKET" | tr '[:lower:]' '[:upper:]') echo "ticket=$TICKET" >> $GITHUB_OUTPUT echo "found_in=pr_description" >> $GITHUB_OUTPUT echo "✅ Found Jira ticket in PR description: $TICKET" @@ -179,11 +180,11 @@ jobs: # Get all commit messages between base and head COMMIT_MESSAGES=$(git log --pretty=format:"%s" ${BASE_SHA}..${HEAD_SHA} 2>/dev/null || echo "") - # Case-insensitive search - if [ -n "$COMMIT_MESSAGES" ] && echo "$COMMIT_MESSAGES" | grep -qiE "$JIRA_PATTERN"; then - TICKET=$(echo "$COMMIT_MESSAGES" | grep -oiE "$JIRA_PATTERN" | head -1) + # Case-insensitive search - use printf to safely pass content to grep + if [ -n "$COMMIT_MESSAGES" ] && printf '%s' "$COMMIT_MESSAGES" | grep -qiE "$JIRA_PATTERN"; then + TICKET=$(printf '%s' "$COMMIT_MESSAGES" | grep -oiE "$JIRA_PATTERN" | head -1) # Normalize to uppercase for Jira API (Jira ticket keys are uppercase) - TICKET=$(echo "$TICKET" | tr '[:lower:]' '[:upper:]') + TICKET=$(printf '%s' "$TICKET" | tr '[:lower:]' '[:upper:]') echo "ticket=$TICKET" >> $GITHUB_OUTPUT echo "found_in=commits" >> $GITHUB_OUTPUT echo "✅ Found Jira ticket in commit messages: $TICKET" @@ -294,53 +295,67 @@ jobs: echo "🔍 Verifying ticket $TICKET exists in Jira..." - # Construct URL and validate it - API_URL="https://${JIRA_INSTANCE}/rest/api/3/issue/${TICKET}" + # Construct URL and validate it - only request fields we need (status) to avoid processing ticket description + API_URL="https://${JIRA_INSTANCE}/rest/api/3/issue/${TICKET}?fields=status" # Make API request to check if ticket exists # Force HTTP/1.1 to avoid HTTP/2 issues that can cause "Failed sending HTTP request" - BODY=$(curl -s \ + # Use a temporary file to safely store the response + TEMP_FILE=$(mktemp) + HTTP_CODE=$(curl -s \ --http1.1 \ -X GET \ -H "Authorization: Basic ${AUTH_STRING}" \ -H "Accept: application/json" \ --max-time 30 \ - "${API_URL}" 2>&1) + -w "%{http_code}" \ + -o "${TEMP_FILE}" \ + "${API_URL}" 2>&1 | tail -1) + + # Read response body from temp file + BODY=$(cat "${TEMP_FILE}" 2>/dev/null || echo "") + rm -f "${TEMP_FILE}" - # Check if response indicates ticket doesn't exist (404 error in JSON) - if echo "$BODY" | grep -qiE '"errorMessages"|"statusCode":\s*404'; then + # Check HTTP status code first + if [ "$HTTP_CODE" = "404" ]; then echo "❌ Ticket $TICKET does not exist in Jira" echo "exists=false" >> $GITHUB_OUTPUT exit 1 fi - # Check if response indicates authentication failure (401/403 in JSON) - if echo "$BODY" | grep -qiE '"statusCode":\s*(401|403)'; then + if [ "$HTTP_CODE" = "401" ] || [ "$HTTP_CODE" = "403" ]; then echo "⚠️ Authentication failed or insufficient permissions to verify ticket" echo "⚠️ Continuing without verification..." echo "exists=false" >> $GITHUB_OUTPUT exit 0 fi + # Check if response indicates ticket doesn't exist (error in JSON) + if echo "$BODY" | grep -qiE '"errorMessages"'; then + echo "❌ Ticket $TICKET does not exist in Jira" + echo "exists=false" >> $GITHUB_OUTPUT + exit 1 + fi + # If we get here, assume the ticket exists and try to extract status echo "✅ Ticket $TICKET exists in Jira" echo "exists=true" >> $GITHUB_OUTPUT - # Extract status from Jira API response (status is nested: fields.status.name) - # Try using jq first (if available), then fall back to grep/sed + # Extract status from Jira API response using jq (safest method) + # Use --raw-output to get plain text, and handle errors properly if command -v jq >/dev/null 2>&1; then - STATUS=$(echo "$BODY" | jq -r '.fields.status.name // empty' 2>/dev/null || echo "") + STATUS=$(printf '%s' "$BODY" | jq -r '.fields.status.name // empty' 2>/dev/null || echo "") fi + # Fallback: extract status using grep/sed pattern matching (only if jq fails) if [ -z "$STATUS" ]; then - # Fallback: extract status using grep/sed pattern matching - # Look for "status" object followed by "name" field - STATUS=$(echo "$BODY" | grep -o '"status"[^}]*"name":"[^"]*' | sed 's/.*"name":"\([^"]*\)".*/\1/' | head -1 || echo "") + # Use printf instead of echo to avoid interpretation of escape sequences + STATUS=$(printf '%s' "$BODY" | grep -o '"status"[^}]*"name":"[^"]*' | sed 's/.*"name":"\([^"]*\)".*/\1/' | head -1 || echo "") fi if [ -z "$STATUS" ]; then # Alternative: look for any "name" field that appears after "status" in the JSON - STATUS=$(echo "$BODY" | sed -n 's/.*"status"[^}]*"name":"\([^"]*\)".*/\1/p' | head -1 || echo "") + STATUS=$(printf '%s' "$BODY" | sed -n 's/.*"status"[^}]*"name":"\([^"]*\)".*/\1/p' | head -1 || echo "") fi # Normalize status to lowercase for comparison