@@ -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