Skip to content

Commit a7fcd2d

Browse files
committed
fix(docs-sync): emit review_items_file output, fix auto_push fast path, tighten gates
1 parent ae5fc2c commit a7fcd2d

2 files changed

Lines changed: 66 additions & 31 deletions

File tree

.github/workflows/showcase_docs-sync.yml

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -59,16 +59,13 @@ jobs:
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
62+
# The sync script writes review-items.txt and emits
63+
# review_items_file=<abs-path> directly to GITHUB_OUTPUT.
64+
# Verify the file actually exists so downstream steps don't
65+
# silently read an empty path.
66+
if [ ! -f review-items.txt ]; then
6567
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)"
6768
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"
7269
else
7370
echo "Sync failed with exit code $EXIT_CODE"
7471
exit 1
@@ -119,12 +116,29 @@ jobs:
119116
# GitHub's PR search syntax has no `head:` qualifier, so that
120117
# query always returns empty. Use jq on the full list instead
121118
# to filter by headRefName prefix client-side.
122-
EXISTING_PR=$(gh pr list \
123-
--state open \
124-
--base "$SHOWCASE_BRANCH" \
125-
--json number,url,headRefName \
126-
--jq '[.[] | select(.headRefName | startswith("docs-sync/needs-review/"))] | .[0].url' \
127-
2>/dev/null || echo "")
119+
# Fail the step on gh API failure rather than swallowing it —
120+
# `|| echo ""` would silently treat a transient 5xx as "no
121+
# existing PR" and open a duplicate. Retry once with backoff
122+
# to absorb blips; if both attempts fail, exit non-zero.
123+
gh_pr_list_existing() {
124+
gh pr list \
125+
--state open \
126+
--base "$SHOWCASE_BRANCH" \
127+
--json number,url,headRefName \
128+
--jq '[.[] | select(.headRefName | startswith("docs-sync/needs-review/"))] | .[0].url'
129+
}
130+
if ! EXISTING_PR=$(gh_pr_list_existing 2>gh-err.txt); then
131+
echo "::warning::gh pr list failed on first attempt — retrying after 5s"
132+
cat gh-err.txt || true
133+
sleep 5
134+
if ! EXISTING_PR=$(gh_pr_list_existing 2>gh-err.txt); then
135+
echo "::error::gh pr list failed twice — aborting to avoid duplicate PR creation"
136+
cat gh-err.txt || true
137+
rm -f gh-err.txt
138+
exit 1
139+
fi
140+
fi
141+
rm -f gh-err.txt
128142
if [ -n "$EXISTING_PR" ]; then
129143
echo "Open needs-review PR already exists: $EXISTING_PR"
130144
echo "Skipping new PR — Slack alert will point at the existing one."
@@ -180,6 +194,15 @@ jobs:
180194
# Explicit signal: no PR opened, and this is the intended outcome
181195
# (not an error path). Downstream alerts key on this.
182196
echo "pr_opened=false" >> "$GITHUB_OUTPUT"
197+
# Set needs_review explicitly so downstream `needs_review != 'true'`
198+
# gates (notify-auto-sync) don't misfire on this deliberate
199+
# no-op. Value depends on why we got here: push_and_pr path =
200+
# review still pending; auto_push path = no review needed.
201+
if [ "$ACTION" = "push_and_pr" ]; then
202+
echo "needs_review=true" >> "$GITHUB_OUTPUT"
203+
else
204+
echo "needs_review=false" >> "$GITHUB_OUTPUT"
205+
fi
183206
exit 0
184207
fi
185208

showcase/scripts/sync-docs-from-main.ts

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -617,15 +617,17 @@ function main(): SyncResult {
617617
}
618618
}
619619

620-
// Update sync marker only when NOTHING had local modifications (no
621-
// merge conflicts AND no auto-merged-with-local-mods files). If any file
622-
// had local mods — even an auto-merged one — keep the old sha so the
623-
// next run still has the correct base context for 3-way merging until
624-
// a human has reviewed/merged the needs-review PR.
620+
// Update sync marker whenever all files were successfully resolved:
621+
// clean transforms, clean auto-merges, or no changes at all. A successful
622+
// 3-way auto-merge means the file IS resolved — its content on disk
623+
// reflects the merged state, so the next run should treat HEAD as the
624+
// new base. Only outstanding merge conflicts (upstream-wins written to
625+
// PR branch only) or deletions (not auto-applied) keep the old sha so
626+
// future runs still flag them until a human merges the needs-review PR.
625627
if (
626628
!dryRun &&
627-
result.needsReview.length === 0 &&
628629
result.mergeConflict.length === 0 &&
630+
result.deleted.length === 0 &&
629631
(result.copied.length > 0 ||
630632
result.transformed.length > 0 ||
631633
result.autoMerged.length > 0)
@@ -678,14 +680,16 @@ const hasAutoApplied =
678680
result.copied.length > 0 ||
679681
result.transformed.length > 0 ||
680682
result.autoMerged.length > 0;
681-
// Any file that had showcase-local modifications (needsReview) must route
682-
// through push_and_pr so a human can review — even if the 3-way merge was
683-
// clean (autoMerged). Silent auto-merging files with local mods would
684-
// clobber intentional showcase divergence.
683+
// Only files that still need human attention force push_and_pr:
684+
// - mergeConflict: 3-way merge failed, upstream-wins content staged to PR
685+
// branch, human must reconcile
686+
// - deleted: files gone on main, not auto-deleted in showcase, human decides
687+
// Auto-merged files (clean 3-way merge) are considered RESOLVED — they go
688+
// through the auto_push fast path and the marker advances. `needsReview`
689+
// is the superset tracker (local-mods detected pre-merge) and is NOT a
690+
// gating condition on its own.
685691
const hasReviewItems =
686-
result.mergeConflict.length > 0 ||
687-
result.needsReview.length > 0 ||
688-
result.deleted.length > 0;
692+
result.mergeConflict.length > 0 || result.deleted.length > 0;
689693

690694
if (!hasAutoApplied && !hasReviewItems) {
691695
process.exit(2);
@@ -720,10 +724,18 @@ if (!hasAutoApplied && !hasReviewItems) {
720724
// ROOT makes the contract explicit: whoever invokes the script picks
721725
// where these artifacts land, and the workflow invokes from repo root.
722726
const artifactDir = process.cwd();
723-
fs.writeFileSync(
724-
path.join(artifactDir, "review-items.txt"),
725-
reviewLines.join("\n") + "\n",
726-
);
727+
const reviewItemsPath = path.join(artifactDir, "review-items.txt");
728+
fs.writeFileSync(reviewItemsPath, reviewLines.join("\n") + "\n");
729+
// Emit the absolute path to GITHUB_OUTPUT so the workflow's PR-body and
730+
// Slack payload steps can read the file without hardcoding a location.
731+
// (Downstream steps use `steps.sync.outputs.review_items_file`.) Skipped
732+
// outside CI — process.env.GITHUB_OUTPUT is unset for local runs.
733+
if (process.env.GITHUB_OUTPUT) {
734+
fs.appendFileSync(
735+
process.env.GITHUB_OUTPUT,
736+
`review_items_file=${reviewItemsPath}\n`,
737+
);
738+
}
727739
// Persist the conflict manifest so the workflow can apply upstream-wins
728740
// content to the PR branch (NOT the current worktree — see
729741
// conflictManifest comment above).

0 commit comments

Comments
 (0)