5959 elif [ $EXIT_CODE -eq 3 ]; then
6060 echo "Has review items + clean transforms"
6161 echo "action=push_and_pr" >> "$GITHUB_OUTPUT"
62+ if [ -f review-items.txt ]; then
63+ REVIEW_ITEMS=$(cat review-items.txt)
64+ else
65+ echo "::warning::review-items.txt not found despite exit code 3 — sync script may have a bug"
66+ REVIEW_ITEMS="(review-items.txt not found — check sync script output)"
67+ fi
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"
6272 else
6373 echo "Sync failed with exit code $EXIT_CODE"
6474 exit 1
@@ -91,8 +101,11 @@ jobs:
91101 CHANGED=$(git diff --cached --name-only | wc -l | tr -d ' ')
92102
93103 if [ "$CHANGED" = "0" ]; then
94- echo "Nothing to commit"
104+ echo "Nothing to commit — no PR will be opened (deliberate) "
95105 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"
96109 exit 0
97110 fi
98111
@@ -111,44 +124,163 @@ jobs:
111124 echo "Created PR: $PR_URL"
112125 echo "files_changed=${CHANGED}" >> "$GITHUB_OUTPUT"
113126 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"
114131
115132 gh pr merge "$PR_URL" --merge
116133
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+
117201 - name : Notify Slack (auto-sync)
202+ id : notify-auto-sync
118203 if : always() && steps.push.outcome == 'success' && steps.push.outputs.files_changed != '0'
119204 uses : slackapi/slack-github-action@v2.1.0
120205 with :
121206 webhook : ${{ secrets.SLACK_WEBHOOK_OSS_ALERTS }}
122207 webhook-type : incoming-webhook
123- payload : |
124- { "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>" }
208+ payload-file-path : slack-payloads/auto-sync.json
125209
126210 - name : Notify Slack (merge failed)
127- if : failure() && steps.push.outputs.pr_url != ''
211+ id : notify-merge-failed
212+ if : failure() && steps.push.outputs.pr_opened == 'true'
128213 uses : slackapi/slack-github-action@v2.1.0
129214 with :
130215 webhook : ${{ secrets.SLACK_WEBHOOK_OSS_ALERTS }}
131216 webhook-type : incoming-webhook
132- payload : |
133- { "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>" }
217+ payload-file-path : slack-payloads/merge-failed.json
134218
135219 # Review items = files the sync script flagged but did NOT write to disk
136- # (local modifications, files deleted on main). No file changes to propose —
137- # just alert Slack so a human can manually reconcile.
138- - name : Notify Slack (review needed)
139- if : always() && steps.sync.outputs.action == 'push_and_pr'
220+ # (local modifications, files deleted on main). A PR was opened for the
221+ # clean-transform portion — link it so reviewers can click through.
222+ # Gated on pr_opened == 'true' so it only fires when we actually have a
223+ # PR URL (not on any error path that leaves pr_url empty).
224+ - name : Notify Slack (review needed, with PR)
225+ id : notify-review-with-pr
226+ if : always() && steps.sync.outputs.action == 'push_and_pr' && steps.push.outputs.pr_opened == 'true'
140227 uses : slackapi/slack-github-action@v2.1.0
141228 with :
142229 webhook : ${{ secrets.SLACK_WEBHOOK_OSS_ALERTS }}
143230 webhook-type : incoming-webhook
144- payload : |
145- { "text": ":warning: *Docs sync*: files needing manual review — see workflow run for details\n<https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}|View run>" }
231+ payload-file-path : slack-payloads/review-with-pr.json
232+
233+ # Review items but the clean-transform portion was empty so no PR was
234+ # opened. Gated on pr_opened == 'false' (deliberate no-PR path) — not
235+ # empty pr_url, which would also match error paths.
236+ - name : Notify Slack (review needed, no PR)
237+ id : notify-review-no-pr
238+ if : always() && steps.sync.outputs.action == 'push_and_pr' && steps.push.outputs.pr_opened == 'false'
239+ uses : slackapi/slack-github-action@v2.1.0
240+ with :
241+ webhook : ${{ secrets.SLACK_WEBHOOK_OSS_ALERTS }}
242+ webhook-type : incoming-webhook
243+ payload-file-path : slack-payloads/review-no-pr.json
146244
147245 - name : Notify Slack (failure)
148- if : failure() && steps.push.outputs.pr_url == ''
246+ id : notify-failure
247+ if : failure() && steps.push.outputs.pr_opened != 'true'
149248 uses : slackapi/slack-github-action@v2.1.0
150249 with :
151250 webhook : ${{ secrets.SLACK_WEBHOOK_OSS_ALERTS }}
152251 webhook-type : incoming-webhook
153- payload : |
154- { "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>" }
252+ payload-file-path : slack-payloads/failure.json
253+
254+ # Unconditional fallback: if any notify-* step above failed (webhook 5xx,
255+ # rate limit, malformed payload), fire a plain-text alert so we never
256+ # silently lose a review-needed or failure notification. Uses curl
257+ # directly so it doesn't share failure modes with the slackapi action.
258+ # Payload is inlined (not read from slack-payloads/) so this fallback
259+ # has no dependency on the Build Slack payloads step succeeding — if
260+ # that step broke (jq missing, mkdir failed, etc), every notify-* step
261+ # would fail AND the fallback could not read its file. RUN_URL is
262+ # constructed from GitHub-controlled env vars only (no user input), so
263+ # direct string interpolation into the JSON literal is safe — the
264+ # values cannot contain " or \.
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+ RUN_URL="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
283+ curl -sS -X POST \
284+ -H "Content-Type: application/json" \
285+ --data "{\"text\": \":rotating_light: *Docs sync*: review/alert machinery failed — check Actions UI ${RUN_URL}\"}" \
286+ "$SLACK_WEBHOOK" || echo "::warning::fallback Slack post also failed"
0 commit comments