Skip to content

Commit 157cf7d

Browse files
committed
fix(docs-sync): harden shell injection, add needs-review Slack, fix silent re-resolution + PR collision
1 parent 5bb7c19 commit 157cf7d

2 files changed

Lines changed: 227 additions & 56 deletions

File tree

.github/workflows/showcase_docs-sync.yml

Lines changed: 115 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,50 @@ jobs:
9797
ACTION: ${{ steps.sync.outputs.action }}
9898
REVIEW_ITEMS_FILE: ${{ steps.sync.outputs.review_items_file }}
9999
run: |
100+
# Trap-based cleanup so temp files are removed even on signal kill.
101+
CLEANUP_FILES=()
102+
cleanup() {
103+
for f in "${CLEANUP_FILES[@]}"; do
104+
[ -n "$f" ] && [ -e "$f" ] && rm -f "$f" || true
105+
done
106+
}
107+
trap cleanup EXIT INT TERM
108+
100109
git config user.name "copilotkit-devops-bot[bot]"
101110
git config user.email "copilotkit-devops-bot[bot]@users.noreply.github.com"
102111
103112
SHORT_SHA=$(git rev-parse --short origin/main)
104113
if [ "$ACTION" = "push_and_pr" ]; then
114+
# Dedupe against open needs-review PRs. If one already exists,
115+
# skip PR creation and let the Slack alert point at the existing
116+
# one (simpler than re-pushing to its branch).
117+
EXISTING_PR=$(gh pr list \
118+
--state open \
119+
--base "$SHOWCASE_BRANCH" \
120+
--search "head:docs-sync/needs-review/ in:title [NEEDS REVIEW]" \
121+
--json number,url,headRefName \
122+
--jq '.[0].url' 2>/dev/null || echo "")
123+
# Fallback: gh pr list with a plain head-prefix filter (--search
124+
# may not honor head: prefix perfectly across gh versions).
125+
if [ -z "$EXISTING_PR" ]; then
126+
EXISTING_PR=$(gh pr list \
127+
--state open \
128+
--base "$SHOWCASE_BRANCH" \
129+
--json number,url,headRefName \
130+
--jq '[.[] | select(.headRefName | startswith("docs-sync/needs-review/"))] | .[0].url' \
131+
2>/dev/null || echo "")
132+
fi
133+
if [ -n "$EXISTING_PR" ]; then
134+
echo "Open needs-review PR already exists: $EXISTING_PR"
135+
echo "Skipping new PR — Slack alert will point at the existing one."
136+
echo "files_changed=0" >> "$GITHUB_OUTPUT"
137+
echo "pr_url=${EXISTING_PR}" >> "$GITHUB_OUTPUT"
138+
echo "pr_opened=false" >> "$GITHUB_OUTPUT"
139+
echo "needs_review=true" >> "$GITHUB_OUTPUT"
140+
echo "existing_pr=true" >> "$GITHUB_OUTPUT"
141+
exit 0
142+
fi
143+
105144
BRANCH="docs-sync/needs-review/${SHORT_SHA}-$(date +%s)"
106145
COMMIT_SUBJECT="chore: docs sync from main — needs review ($(date +%Y-%m-%d))"
107146
else
@@ -110,6 +149,26 @@ jobs:
110149
fi
111150
112151
git checkout -b "$BRANCH"
152+
153+
# For push_and_pr: apply the conflict manifest now (upstream-wins
154+
# content for conflicted files, written ONLY to the PR branch so
155+
# the main branch worktree stays as-is and future sync runs still
156+
# flag those files for review until this PR is merged).
157+
if [ "$ACTION" = "push_and_pr" ] && [ -f conflict-manifest.json ]; then
158+
CLEANUP_FILES+=("conflict-manifest.json")
159+
node -e '
160+
const fs = require("fs");
161+
const path = require("path");
162+
const manifest = JSON.parse(fs.readFileSync("conflict-manifest.json", "utf-8"));
163+
for (const entry of manifest) {
164+
const target = path.resolve(entry.showcasePath);
165+
fs.mkdirSync(path.dirname(target), { recursive: true });
166+
fs.writeFileSync(target, entry.content);
167+
console.log("Applied upstream-wins to PR branch: " + entry.showcasePath);
168+
}
169+
'
170+
fi
171+
113172
git add showcase/shell/src/content/ showcase/shell/.docs-sync-sha
114173
CHANGED=$(git diff --cached --name-only | wc -l | tr -d ' ')
115174
@@ -135,32 +194,37 @@ jobs:
135194
REVIEW_ITEMS_CONTENT=$(cat "$REVIEW_ITEMS_FILE")
136195
fi
137196
PR_BODY_FILE=$(mktemp)
197+
CLEANUP_FILES+=("$PR_BODY_FILE")
138198
{
139-
echo ":warning: **Docs sync — MANUAL REVIEW REQUIRED**"
140-
echo ""
141-
echo "This PR was auto-opened because the docs-sync script detected"
142-
echo "showcase-local modifications overlapping with upstream changes."
143-
echo ""
144-
echo "The script attempted a best-effort 3-way merge:"
145-
echo ""
146-
echo "- Where \`git merge-file\` produced a clean merge, the merged content was written."
147-
echo "- Where \`git merge-file\` produced conflict markers, **upstream content was written as-is** and showcase-local modifications were overridden. **Manual review required.**"
148-
echo ""
149-
echo "### Review items"
150-
echo ""
151-
echo '```'
152-
echo "$REVIEW_ITEMS_CONTENT"
153-
echo '```'
154-
echo ""
155-
echo "### Source"
156-
echo ""
157-
echo "- Upstream ref: [\`${SHORT_SHA}\`](https://github.com/${{ github.repository }}/commit/${SHORT_SHA})"
158-
echo "- Workflow run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
159-
echo ""
160-
echo "**Review before merging.** Auto-merge is intentionally disabled"
161-
echo "for \`needs-review\` PRs — confirm the upstream-wins sections"
162-
echo "preserve any intentional showcase-local divergence you want to"
163-
echo "keep, then merge manually."
199+
printf '%s\n' ':warning: **Docs sync — MANUAL REVIEW REQUIRED**'
200+
printf '\n'
201+
printf '%s\n' 'This PR was auto-opened because the docs-sync script detected'
202+
printf '%s\n' 'showcase-local modifications overlapping with upstream changes.'
203+
printf '\n'
204+
printf '%s\n' 'The script attempted a best-effort 3-way merge:'
205+
printf '\n'
206+
printf '%s\n' '- Where `git merge-file` produced a clean merge, the merged content was written.'
207+
printf '%s\n' '- Where `git merge-file` produced conflict markers, **upstream content was written as-is** and showcase-local modifications were overridden. **Manual review required.**'
208+
printf '\n'
209+
printf '%s\n' '### Review items'
210+
printf '\n'
211+
printf '%s\n' '```'
212+
# printf '%s\n' avoids running command substitution / backticks
213+
# embedded in review-items content (cat "$REVIEW_ITEMS_FILE"
214+
# would also work; printf is equivalent here since we already
215+
# captured the content).
216+
printf '%s\n' "$REVIEW_ITEMS_CONTENT"
217+
printf '%s\n' '```'
218+
printf '\n'
219+
printf '%s\n' '### Source'
220+
printf '\n'
221+
printf '%s\n' "- Upstream ref: [\`${SHORT_SHA}\`](https://github.com/${{ github.repository }}/commit/${SHORT_SHA})"
222+
printf '%s\n' "- Workflow run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
223+
printf '\n'
224+
printf '%s\n' '**Review before merging.** Auto-merge is intentionally disabled'
225+
printf '%s\n' 'for `needs-review` PRs — confirm the upstream-wins sections'
226+
printf '%s\n' 'preserve any intentional showcase-local divergence you want to'
227+
printf '%s\n' 'keep, then merge manually.'
164228
} > "$PR_BODY_FILE"
165229
166230
PR_URL=$(gh pr create \
@@ -239,6 +303,16 @@ jobs:
239303
'{text: (":warning: *Docs sync*: auto-opened *NEEDS REVIEW* PR (best-effort 3-way merge; upstream-wins where conflicts) — human must review + merge\n```" + $items + "```\nReview: " + $pr_url + "\n<" + $run_url + "|View run>")}' \
240304
> slack-payloads/review-with-pr.json
241305
306+
# Collision path: new review items flagged but an existing open
307+
# needs-review PR already covers the territory; we did NOT open a
308+
# new PR. Point reviewers at the existing one.
309+
jq -n \
310+
--arg items "$REVIEW_ITEMS" \
311+
--arg pr_url "${PR_URL:-}" \
312+
--arg run_url "$RUN_URL" \
313+
'{text: (":warning: *Docs sync*: new review items detected, but an open *NEEDS REVIEW* PR already exists — skipped new PR creation to avoid collision. Please resolve the existing PR.\n```" + $items + "```\nExisting PR: " + $pr_url + "\n<" + $run_url + "|View run>")}' \
314+
> slack-payloads/review-existing-pr.json
315+
242316
# Fallback path: review items flagged but no PR opened (e.g. 3-way
243317
# merge produced bit-for-bit identical content to what's already on
244318
# disk so nothing to commit). Keep an alert so it's visible.
@@ -288,7 +362,7 @@ jobs:
288362
# PR URL (not on any error path that leaves pr_url empty).
289363
- name: Notify Slack (review needed, with PR)
290364
id: notify-review-with-pr
291-
if: always() && steps.sync.outputs.action == 'push_and_pr' && steps.push.outputs.pr_opened == 'true'
365+
if: always() && steps.push.outputs.needs_review == 'true' && steps.push.outputs.pr_opened == 'true'
292366
uses: slackapi/slack-github-action@v2.1.0
293367
with:
294368
webhook: ${{ secrets.SLACK_WEBHOOK_OSS_ALERTS }}
@@ -297,16 +371,28 @@ jobs:
297371

298372
# Review items but the clean-transform portion was empty so no PR was
299373
# opened. Gated on pr_opened == 'false' (deliberate no-PR path) — not
300-
# empty pr_url, which would also match error paths.
374+
# empty pr_url, which would also match error paths. Explicitly excludes
375+
# the existing-PR collision path, which gets its own step below.
301376
- name: Notify Slack (review needed, no PR)
302377
id: notify-review-no-pr
303-
if: always() && steps.sync.outputs.action == 'push_and_pr' && steps.push.outputs.pr_opened == 'false'
378+
if: always() && steps.sync.outputs.action == 'push_and_pr' && steps.push.outputs.pr_opened == 'false' && steps.push.outputs.existing_pr != 'true'
304379
uses: slackapi/slack-github-action@v2.1.0
305380
with:
306381
webhook: ${{ secrets.SLACK_WEBHOOK_OSS_ALERTS }}
307382
webhook-type: incoming-webhook
308383
payload-file-path: slack-payloads/review-no-pr.json
309384

385+
# Collision path: new review items flagged but an existing open
386+
# needs-review PR already exists — point reviewers at it.
387+
- name: Notify Slack (review needed, existing PR)
388+
id: notify-review-existing-pr
389+
if: always() && steps.push.outputs.existing_pr == 'true'
390+
uses: slackapi/slack-github-action@v2.1.0
391+
with:
392+
webhook: ${{ secrets.SLACK_WEBHOOK_OSS_ALERTS }}
393+
webhook-type: incoming-webhook
394+
payload-file-path: slack-payloads/review-existing-pr.json
395+
310396
- name: Notify Slack (failure)
311397
id: notify-failure
312398
if: failure() && steps.push.outputs.pr_opened != 'true'
@@ -334,6 +420,7 @@ jobs:
334420
steps.notify-merge-failed.outcome == 'failure' ||
335421
steps.notify-review-with-pr.outcome == 'failure' ||
336422
steps.notify-review-no-pr.outcome == 'failure' ||
423+
steps.notify-review-existing-pr.outcome == 'failure' ||
337424
steps.notify-failure.outcome == 'failure'
338425
)
339426
env:

0 commit comments

Comments
 (0)