From 9cda91dfd7fa5d34ac01b8d930cfca7a02b0bc1a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 28 May 2026 23:04:57 +0000 Subject: [PATCH 1/3] ci(sync-upstream): guard merge/push on existing remote branch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sync branch name is derived from the upstream commit date, so every run computes the same name while upstream sits on one release. The only idempotency guard checked for an *open* PR; a branch pushed by a prior run that never landed a PR (or whose PR was closed) slipped through. The job then re-created the branch from origin/main and force-free-pushed a diverged history, which git rejects as non-fast-forward — wedging the workflow on every 2-hour cron tick. Detect the existing remote branch and skip the merge/push when present, while still letting the Create PR step (re)open a PR against it. This both fixes the non-fast-forward failure and recovers orphaned branches. --- .github/workflows/sync-upstream.yml | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/.github/workflows/sync-upstream.yml b/.github/workflows/sync-upstream.yml index 41a03c905..6a0360022 100644 --- a/.github/workflows/sync-upstream.yml +++ b/.github/workflows/sync-upstream.yml @@ -68,7 +68,7 @@ jobs: # Store changed list for PR body (base64 to preserve newlines) echo "changed=$(echo -e "$CHANGED" | base64 -w 0)" >> "$GITHUB_OUTPUT" - - name: Check for existing PR + - name: Check for existing PR or branch if: steps.compare.outputs.needs_sync == 'true' id: check_pr env: @@ -90,12 +90,27 @@ jobs: echo "pr_exists=false" >> "$GITHUB_OUTPUT" fi + # The sync branch can outlive the run that created it: a prior run may + # have pushed it but failed before opening a PR (or the PR was later + # closed). Because the branch name is derived from the upstream commit + # date, every subsequent run computes the same name, re-creates the + # branch from origin/main, and produces a diverged history that git + # rejects on push as non-fast-forward — wedging the workflow until the + # branch is removed by hand. Detect the existing remote branch so we + # skip the merge/push and just (re)open the PR against what's there. + if git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null 2>&1; then + echo "Branch $BRANCH already exists on remote" + echo "branch_exists=true" >> "$GITHUB_OUTPUT" + else + echo "branch_exists=false" >> "$GITHUB_OUTPUT" + fi + - name: Setup Tools - if: steps.compare.outputs.needs_sync == 'true' && steps.check_pr.outputs.pr_exists == 'false' + if: steps.compare.outputs.needs_sync == 'true' && steps.check_pr.outputs.pr_exists == 'false' && steps.check_pr.outputs.branch_exists == 'false' uses: tanstack/config/.github/setup@main - name: Merge upstream into fork branch - if: steps.compare.outputs.needs_sync == 'true' && steps.check_pr.outputs.pr_exists == 'false' + if: steps.compare.outputs.needs_sync == 'true' && steps.check_pr.outputs.pr_exists == 'false' && steps.check_pr.outputs.branch_exists == 'false' id: merge run: | VERSION=${{ steps.compare.outputs.upstream_version }} From 43d9ec36a26d501a78ab1b4077a14725306a4367 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 28 May 2026 23:30:11 +0000 Subject: [PATCH 2/3] ci(sync-upstream): keep zizmor.yml and CODEOWNERS deleted on sync The fork removed .github/workflows/zizmor.yml and .github/CODEOWNERS, both of which still exist upstream. An upstream edit to either surfaces as a modify/delete conflict (same as release.yml), which previously fell through to the manual-review path and aborted the merge. Resolve them back to 'deleted' automatically so the fork's removals are preserved. --- .github/workflows/sync-upstream.yml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/sync-upstream.yml b/.github/workflows/sync-upstream.yml index 6a0360022..ae388b490 100644 --- a/.github/workflows/sync-upstream.yml +++ b/.github/workflows/sync-upstream.yml @@ -149,10 +149,15 @@ jobs: git checkout --theirs -- "$FILE" && git add "$FILE" AUTO_RESOLVED="${AUTO_RESOLVED} ${FILE} (took upstream — generated docs)\n" ;; - # CI workflows deleted in fork (e.g. release.yml) — keep deleted - .github/workflows/release.yml) + # Files the fork intentionally removed — keep deleted. Upstream + # still ships these, so an upstream edit surfaces as a + # modify/delete conflict that we resolve back to "deleted": + # - release.yml: fork publishes via publish-fork.yml + # - zizmor.yml: fork does not run the zizmor security scan + # - CODEOWNERS: fork does not use upstream's code owners + .github/workflows/release.yml | .github/workflows/zizmor.yml | .github/CODEOWNERS) git rm -f "$FILE" 2>/dev/null || true - AUTO_RESOLVED="${AUTO_RESOLVED} ${FILE} (kept deleted — fork uses publish-fork.yml)\n" + AUTO_RESOLVED="${AUTO_RESOLVED} ${FILE} (kept deleted — removed in fork)\n" ;; # PR workflow — keep fork's simplified version (no NX Cloud, no # nx-set-shas, no preview job, uses test:ci). The fork has From 6bfb536ea018ab1a93b1ce2a1688ff506ac4c2bf Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 28 May 2026 23:37:09 +0000 Subject: [PATCH 3/3] ci(sync-upstream): treat closed sync PRs as existing A closed (not merged) sync PR means the version was deliberately rejected. The prior open-only check let it through, but gh pr create refuses a second PR for a head that already has one, so the recovery path still wedged. Match PRs in any state so a closed PR skips the sync. --- .github/workflows/sync-upstream.yml | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/.github/workflows/sync-upstream.yml b/.github/workflows/sync-upstream.yml index ae388b490..f792a2aa7 100644 --- a/.github/workflows/sync-upstream.yml +++ b/.github/workflows/sync-upstream.yml @@ -81,7 +81,12 @@ jobs: VERSION=${{ steps.compare.outputs.upstream_version }} DATE=${{ steps.compare.outputs.date }} BRANCH="sync/v${VERSION}-${DATE}" - EXISTING=$(gh pr list --head "$BRANCH" --json number --jq 'length') + # Match PRs in ANY state. A closed (not merged) sync PR means this + # version was deliberately rejected — re-creating it would also fail + # at `gh pr create`, which refuses a second PR for a head that already + # has one. Treating closed as "exists" skips the sync entirely until + # the branch is removed by hand. + EXISTING=$(gh pr list --head "$BRANCH" --state all --json number --jq 'length') if [ "$EXISTING" -gt 0 ]; then echo "PR already exists for $BRANCH" @@ -91,13 +96,14 @@ jobs: fi # The sync branch can outlive the run that created it: a prior run may - # have pushed it but failed before opening a PR (or the PR was later - # closed). Because the branch name is derived from the upstream commit - # date, every subsequent run computes the same name, re-creates the - # branch from origin/main, and produces a diverged history that git - # rejects on push as non-fast-forward — wedging the workflow until the - # branch is removed by hand. Detect the existing remote branch so we - # skip the merge/push and just (re)open the PR against what's there. + # have pushed it but failed before opening a PR. Because the branch + # name is derived from the upstream commit date, every subsequent run + # computes the same name, re-creates the branch from origin/main, and + # produces a diverged history that git rejects on push as + # non-fast-forward — wedging the workflow until the branch is removed + # by hand. Detect the existing remote branch so we skip the merge/push + # and just open the PR against what's there. (A branch whose PR was + # closed is already handled above via pr_exists.) if git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null 2>&1; then echo "Branch $BRANCH already exists on remote" echo "branch_exists=true" >> "$GITHUB_OUTPUT"