6565 echo "::warning::review-items.txt not found despite exit code 3 — sync script may have a bug"
6666 REVIEW_ITEMS="(review-items.txt not found — check sync script output)"
6767 fi
68- # JSON-escape review items for embedding in Slack payload
69- REVIEW_ITEMS_JSON=$(echo "$REVIEW_ITEMS" | jq -Rs '.' | sed 's/^"//;s/"$//')
70- echo "review_items_json=${REVIEW_ITEMS_JSON}" >> "$GITHUB_OUTPUT"
68+ # Persist review items to a file for downstream jq-based payload building
69+ # (avoids unsafe string interpolation of untrusted filenames into JSON).
70+ printf '%s' "$REVIEW_ITEMS" > review-items-raw.txt
71+ echo "review_items_file=review-items-raw.txt" >> "$GITHUB_OUTPUT"
7172 else
7273 echo "Sync failed with exit code $EXIT_CODE"
7374 exit 1
@@ -100,8 +101,11 @@ jobs:
100101 CHANGED=$(git diff --cached --name-only | wc -l | tr -d ' ')
101102
102103 if [ "$CHANGED" = "0" ]; then
103- echo "Nothing to commit"
104+ echo "Nothing to commit — no PR will be opened (deliberate) "
104105 echo "files_changed=0" >> "$GITHUB_OUTPUT"
106+ # Explicit signal: no PR opened, and this is the intended outcome
107+ # (not an error path). Downstream alerts key on this.
108+ echo "pr_opened=false" >> "$GITHUB_OUTPUT"
105109 exit 0
106110 fi
107111
@@ -120,54 +124,162 @@ jobs:
120124 echo "Created PR: $PR_URL"
121125 echo "files_changed=${CHANGED}" >> "$GITHUB_OUTPUT"
122126 echo "pr_url=${PR_URL}" >> "$GITHUB_OUTPUT"
127+ # Only set pr_opened=true after the PR URL is captured. Any earlier
128+ # failure (push, gh pr create) will leave this unset so downstream
129+ # alerts fall through to failure() instead of a falsely-benign path.
130+ echo "pr_opened=true" >> "$GITHUB_OUTPUT"
123131
124132 gh pr merge "$PR_URL" --merge
125133
134+ # Build all Slack payloads via jq into tmpfiles. This guarantees any
135+ # review-item filename containing ", \, or control chars is safely
136+ # JSON-escaped (never string-interpolated into a JSON literal).
137+ - name : Build Slack payloads
138+ id : payloads
139+ if : always()
140+ env :
141+ RUN_URL : https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
142+ PR_URL : ${{ steps.push.outputs.pr_url }}
143+ FILES_CHANGED : ${{ steps.push.outputs.files_changed }}
144+ REVIEW_ITEMS_FILE : ${{ steps.sync.outputs.review_items_file }}
145+ SYNC_OUTCOME : ${{ steps.sync.outcome }}
146+ BOT_TOKEN_OUTCOME : ${{ steps.bot-token.outcome }}
147+ PUSH_OUTCOME : ${{ steps.push.outcome }}
148+ run : |
149+ set -euo pipefail
150+ mkdir -p slack-payloads
151+
152+ # auto-sync success (PR merged)
153+ jq -n \
154+ --arg pr_url "${PR_URL:-}" \
155+ --arg files "${FILES_CHANGED:-?}" \
156+ --arg run_url "$RUN_URL" \
157+ '{text: (":arrows_counterclockwise: *Docs sync*: auto-merged " + $files + " file(s)\n" + $pr_url + "\n<" + $run_url + "|View run>")}' \
158+ > slack-payloads/auto-sync.json
159+
160+ # merge failed (PR exists but gh pr merge failed)
161+ jq -n \
162+ --arg pr_url "${PR_URL:-}" \
163+ --arg run_url "$RUN_URL" \
164+ '{text: (":warning: *Docs sync*: PR created but auto-merge FAILED — needs manual merge\n" + $pr_url + "\n<" + $run_url + "|View run>")}' \
165+ > slack-payloads/merge-failed.json
166+
167+ # review-needed payloads: read items from file, let jq handle escaping
168+ REVIEW_ITEMS=""
169+ if [ -n "${REVIEW_ITEMS_FILE:-}" ] && [ -f "$REVIEW_ITEMS_FILE" ]; then
170+ REVIEW_ITEMS=$(cat "$REVIEW_ITEMS_FILE")
171+ fi
172+
173+ jq -n \
174+ --arg items "$REVIEW_ITEMS" \
175+ --arg pr_url "${PR_URL:-}" \
176+ --arg run_url "$RUN_URL" \
177+ '{text: (":warning: *Docs sync*: files needing manual review\n```" + $items + "```\nReview: " + $pr_url + "\n<" + $run_url + "|View run>")}' \
178+ > slack-payloads/review-with-pr.json
179+
180+ jq -n \
181+ --arg items "$REVIEW_ITEMS" \
182+ --arg run_url "$RUN_URL" \
183+ '{text: (":warning: *Docs sync*: files needing manual review (no auto-PR opened)\n```" + $items + "```\n<" + $run_url + "|View run>")}' \
184+ > slack-payloads/review-no-pr.json
185+
186+ # failure alert
187+ FAILED_STEP="unknown"
188+ if [ "${SYNC_OUTCOME:-}" = "failure" ]; then
189+ FAILED_STEP="sync-docs script"
190+ elif [ "${BOT_TOKEN_OUTCOME:-}" = "failure" ]; then
191+ FAILED_STEP="bot token generation (check DEVOPS_BOT_PRIVATE_KEY secret)"
192+ elif [ "${PUSH_OUTCOME:-}" = "failure" ]; then
193+ FAILED_STEP="push/PR creation"
194+ fi
195+ jq -n \
196+ --arg failed_step "$FAILED_STEP" \
197+ --arg run_url "$RUN_URL" \
198+ '{text: (":x: *Docs sync*: workflow failed\n*Failed step:* " + $failed_step + " | <" + $run_url + "|View run>")}' \
199+ > slack-payloads/failure.json
200+
201+ # plain-text fallback used only when notify-* steps themselves fail
202+ # (webhook 5xx, rate limit, etc). Keeps us from silently losing alerts.
203+ jq -n \
204+ --arg run_url "$RUN_URL" \
205+ '{text: (":rotating_light: *Docs sync*: review/alert machinery failed — check Actions UI\n<" + $run_url + "|View run>")}' \
206+ > slack-payloads/notify-fallback.json
207+
126208 - name : Notify Slack (auto-sync)
209+ id : notify-auto-sync
127210 if : always() && steps.push.outcome == 'success' && steps.push.outputs.files_changed != '0'
128211 uses : slackapi/slack-github-action@v2.1.0
129212 with :
130213 webhook : ${{ secrets.SLACK_WEBHOOK_OSS_ALERTS }}
131214 webhook-type : incoming-webhook
132- payload : |
133- { "text": ":arrows_counterclockwise: *Docs sync*: auto-merged ${{ steps.push.outputs.files_changed || '?' }} file(s)\n${{ steps.push.outputs.pr_url || '' }}\n<https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}|View run>" }
215+ payload-file-path : slack-payloads/auto-sync.json
134216
135217 - name : Notify Slack (merge failed)
136- if : failure() && steps.push.outputs.pr_url != ''
218+ id : notify-merge-failed
219+ if : failure() && steps.push.outputs.pr_opened == 'true'
137220 uses : slackapi/slack-github-action@v2.1.0
138221 with :
139222 webhook : ${{ secrets.SLACK_WEBHOOK_OSS_ALERTS }}
140223 webhook-type : incoming-webhook
141- payload : |
142- { "text": ":warning: *Docs sync*: PR created but auto-merge FAILED — needs manual merge\n${{ steps.push.outputs.pr_url }}\n<https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}|View run>" }
224+ payload-file-path : slack-payloads/merge-failed.json
143225
144226 # Review items = files the sync script flagged but did NOT write to disk
145- # (local modifications, files deleted on main). A PR may have been opened
146- # for the clean-transform portion — link it so reviewers can click through.
147- - name : Notify Slack (review needed)
148- if : always() && steps.sync.outputs.action == 'push_and_pr' && steps.push.outputs.pr_url != ''
227+ # (local modifications, files deleted on main). A PR was opened for the
228+ # clean-transform portion — link it so reviewers can click through.
229+ # Gated on pr_opened == 'true' so it only fires when we actually have a
230+ # PR URL (not on any error path that leaves pr_url empty).
231+ - name : Notify Slack (review needed, with PR)
232+ id : notify-review-with-pr
233+ if : always() && steps.sync.outputs.action == 'push_and_pr' && steps.push.outputs.pr_opened == 'true'
149234 uses : slackapi/slack-github-action@v2.1.0
150235 with :
151236 webhook : ${{ secrets.SLACK_WEBHOOK_OSS_ALERTS }}
152237 webhook-type : incoming-webhook
153- payload : |
154- { "text": ":warning: *Docs sync*: files needing manual review\n```${{ steps.sync.outputs.review_items_json }}```\nReview: ${{ steps.push.outputs.pr_url }}\n<https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}|View run>" }
238+ payload-file-path : slack-payloads/review-with-pr.json
155239
156- # Fallback: review items but no PR was opened (clean-transform portion was empty).
240+ # Review items but the clean-transform portion was empty so no PR was
241+ # opened. Gated on pr_opened == 'false' (deliberate no-PR path) — not
242+ # empty pr_url, which would also match error paths.
157243 - name : Notify Slack (review needed, no PR)
158- if : always() && steps.sync.outputs.action == 'push_and_pr' && steps.push.outputs.pr_url == ''
244+ id : notify-review-no-pr
245+ if : always() && steps.sync.outputs.action == 'push_and_pr' && steps.push.outputs.pr_opened == 'false'
159246 uses : slackapi/slack-github-action@v2.1.0
160247 with :
161248 webhook : ${{ secrets.SLACK_WEBHOOK_OSS_ALERTS }}
162249 webhook-type : incoming-webhook
163- payload : |
164- { "text": ":warning: *Docs sync*: files needing manual review (no auto-PR opened)\n```${{ steps.sync.outputs.review_items_json }}```\n<https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}|View run>" }
250+ payload-file-path : slack-payloads/review-no-pr.json
165251
166252 - name : Notify Slack (failure)
167- if : failure() && steps.push.outputs.pr_url == ''
253+ id : notify-failure
254+ if : failure() && steps.push.outputs.pr_opened != 'true'
168255 uses : slackapi/slack-github-action@v2.1.0
169256 with :
170257 webhook : ${{ secrets.SLACK_WEBHOOK_OSS_ALERTS }}
171258 webhook-type : incoming-webhook
172- payload : |
173- { "text": ":x: *Docs sync*: workflow failed\n*Failed step:* ${{ steps.sync.outcome == 'failure' && 'sync-docs script' || steps.bot-token.outcome == 'failure' && 'bot token generation (check DEVOPS_BOT_PRIVATE_KEY secret)' || steps.push.outcome == 'failure' && 'push/PR creation' || 'unknown' }} | <https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}|View run>" }
259+ payload-file-path : slack-payloads/failure.json
260+
261+ # Unconditional fallback: if any notify-* step above failed (webhook 5xx,
262+ # rate limit, malformed payload), fire a plain-text alert so we never
263+ # silently lose a review-needed or failure notification. Uses curl
264+ # directly so it doesn't share failure modes with the slackapi action.
265+ - name : Notify Slack (alert machinery failed)
266+ if : >-
267+ always() && (
268+ steps.notify-auto-sync.outcome == 'failure' ||
269+ steps.notify-merge-failed.outcome == 'failure' ||
270+ steps.notify-review-with-pr.outcome == 'failure' ||
271+ steps.notify-review-no-pr.outcome == 'failure' ||
272+ steps.notify-failure.outcome == 'failure'
273+ )
274+ env :
275+ SLACK_WEBHOOK : ${{ secrets.SLACK_WEBHOOK_OSS_ALERTS }}
276+ run : |
277+ set -eu
278+ if [ -z "${SLACK_WEBHOOK:-}" ]; then
279+ echo "::warning::SLACK_WEBHOOK_OSS_ALERTS not set — cannot post fallback alert"
280+ exit 0
281+ fi
282+ curl -sS -X POST \
283+ -H "Content-Type: application/json" \
284+ --data @slack-payloads/notify-fallback.json \
285+ "$SLACK_WEBHOOK" || echo "::warning::fallback Slack post also failed"
0 commit comments