Skip to content

Commit 143b3eb

Browse files
committed
fix: address CR findings — explicit pr_opened output + safe JSON + Slack fallback
Fixes 3 HIGH findings from R1 review on CopilotKit#3988: 1. pr_url empty was used as a proxy for "no PR opened because clean-transform was empty", but it's also empty on every error path (bot-token failure, gh pr create failure, push failure). Replace with an explicit pr_opened=true/ false output from the push step — true only after the PR URL is captured, false only on the deliberate CHANGED=0 path. Error paths leave it unset so alerts fall through to the failure() handler. 2. review_items_json was string-interpolated raw into a JSON payload inside triple-backticks. Any filename containing ", \\, or a control character would break the payload. Moved to a jq-based payload-file-path pattern: a dedicated Build Slack payloads step writes each payload to disk with jq --arg, so all values are safely JSON-escaped regardless of content. Slack steps consume the tmpfiles via payload-file-path. 3. If a notify-* step itself fails (webhook 5xx, rate limit, malformed JSON), the review-needed alert was silently lost — the existing failure() alert was gated on pr_url == '' and would not fire. Added an unconditional fallback step that posts a plain-text "alert machinery failed" message via curl when any notify-* step's outcome is failure, so we never lose a review-needed or failure notification.
1 parent 8a1b6fc commit 143b3eb

1 file changed

Lines changed: 134 additions & 22 deletions

File tree

.github/workflows/showcase_docs-sync.yml

Lines changed: 134 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,10 @@ jobs:
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

Comments
 (0)