Repository navigation
Sync Upstream #1520
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Sync Upstream | |
| on: | |
| schedule: | |
| - cron: '0 */2 * * *' # Every 2 hours | |
| workflow_dispatch: {} # Manual trigger | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| jobs: | |
| sync: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout with full history | |
| uses: actions/checkout@v6.0.2 | |
| with: | |
| fetch-depth: 0 | |
| - name: Add upstream remote and fetch | |
| run: | | |
| git remote add upstream https://github.com/TanStack/pacer.git | |
| git fetch upstream main | |
| - name: Compare versions across all packages | |
| id: compare | |
| run: | | |
| PACKAGES=( | |
| angular-pacer | |
| pacer | |
| pacer-devtools | |
| pacer-lite | |
| preact-pacer | |
| preact-pacer-devtools | |
| react-pacer | |
| react-pacer-devtools | |
| solid-pacer | |
| solid-pacer-devtools | |
| ) | |
| NEEDS_SYNC=false | |
| CHANGED="" | |
| for PKG in "${PACKAGES[@]}"; do | |
| UPSTREAM=$(git show upstream/main:packages/$PKG/package.json | jq -r '.version') | |
| OURS=$(git show origin/main:packages/$PKG/package.json | jq -r '.version') | |
| if [ "$UPSTREAM" != "$OURS" ]; then | |
| echo "$PKG: upstream v$UPSTREAM vs ours v$OURS" | |
| NEEDS_SYNC=true | |
| CHANGED="${CHANGED}- **$PKG**: v$OURS → v$UPSTREAM\n" | |
| else | |
| echo "$PKG: up to date (v$OURS)" | |
| fi | |
| done | |
| if [ "$NEEDS_SYNC" = "false" ]; then | |
| echo "All packages are up to date" | |
| fi | |
| # Use the core pacer package version for branch naming | |
| UPSTREAM_VERSION=$(git show upstream/main:packages/pacer/package.json | jq -r '.version') | |
| echo "needs_sync=$NEEDS_SYNC" >> "$GITHUB_OUTPUT" | |
| echo "upstream_version=$UPSTREAM_VERSION" >> "$GITHUB_OUTPUT" | |
| echo "date=$(git log -1 --format='%cd' --date=format:'%Y%m%d' upstream/main)" >> "$GITHUB_OUTPUT" | |
| # 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 or branch | |
| if: steps.compare.outputs.needs_sync == 'true' | |
| id: check_pr | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| # Pin gh to the fork. Without this, gh defaults to the parent | |
| # (TanStack/pacer) because this is a fork — pr list would query | |
| # the wrong repo and pr create would 403 on the upstream. | |
| GH_REPO: ${{ github.repository }} | |
| run: | | |
| VERSION=${{ steps.compare.outputs.upstream_version }} | |
| DATE=${{ steps.compare.outputs.date }} | |
| BRANCH="sync/v${VERSION}-${DATE}" | |
| # 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" | |
| echo "pr_exists=true" >> "$GITHUB_OUTPUT" | |
| else | |
| 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. 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" | |
| 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' && 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' && steps.check_pr.outputs.branch_exists == 'false' | |
| id: merge | |
| run: | | |
| VERSION=${{ steps.compare.outputs.upstream_version }} | |
| DATE=${{ steps.compare.outputs.date }} | |
| BRANCH="sync/v${VERSION}-${DATE}" | |
| # Create branch from our main (preserving fork commits) | |
| git checkout -b "$BRANCH" origin/main | |
| # Configure git for the merge | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| # Attempt the merge — allow failure so we can auto-resolve conflicts | |
| echo "Merging upstream/main into fork branch..." | |
| if git merge upstream/main -m "Merge upstream TanStack Pacer v${VERSION} into fork"; then | |
| echo "Merge completed cleanly" | |
| echo "auto_resolved=" >> "$GITHUB_OUTPUT" | |
| git push origin "$BRANCH" | |
| exit 0 | |
| fi | |
| echo "Merge has conflicts — attempting auto-resolution..." | |
| CONFLICTED=$(git diff --name-only --diff-filter=U) | |
| # ── Auto-resolution rules ───────────────────────────────────── | |
| # Each rule handles a category of files that we never customize | |
| # in the fork and can safely resolve automatically. | |
| AUTO_RESOLVED="" | |
| UNRESOLVED="" | |
| for FILE in $CONFLICTED; do | |
| case "$FILE" in | |
| # Generated docs — take upstream; autofix CI regenerates them | |
| docs/*) | |
| git checkout --theirs -- "$FILE" && git add "$FILE" | |
| AUTO_RESOLVED="${AUTO_RESOLVED} ${FILE} (took upstream — generated docs)\n" | |
| ;; | |
| # 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 — 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 | |
| # intentionally diverged here; upstream changes are not adopted | |
| # automatically and can be cherry-picked manually if desired. | |
| .github/workflows/pr.yml) | |
| git checkout --ours -- "$FILE" && git add "$FILE" | |
| AUTO_RESOLVED="${AUTO_RESOLVED} ${FILE} (kept fork version — simplified PR workflow)\n" | |
| ;; | |
| # Upstream changelogs — take upstream; fork doesn't add entries | |
| packages/*/CHANGELOG.md) | |
| git checkout --theirs -- "$FILE" && git add "$FILE" | |
| AUTO_RESOLVED="${AUTO_RESOLVED} ${FILE} (took upstream — changelog)\n" | |
| ;; | |
| # Lockfile — take upstream, then reconcile with fork's root package.json | |
| pnpm-lock.yaml) | |
| git checkout --theirs -- "$FILE" && git add "$FILE" | |
| AUTO_RESOLVED="${AUTO_RESOLVED} ${FILE} (took upstream + pnpm install to reconcile)\n" | |
| NEEDS_LOCKFILE_RECONCILE=true | |
| ;; | |
| # Example app package.json — take upstream; fork doesn't modify examples | |
| examples/*/package.json | examples/*/*/package.json) | |
| git checkout --theirs -- "$FILE" && git add "$FILE" | |
| AUTO_RESOLVED="${AUTO_RESOLVED} ${FILE} (took upstream — example dep versions)\n" | |
| ;; | |
| # Everything else requires manual review | |
| *) | |
| UNRESOLVED="${UNRESOLVED} ${FILE}\n" | |
| ;; | |
| esac | |
| done | |
| if [ -n "$AUTO_RESOLVED" ]; then | |
| echo "Auto-resolved conflicts:" | |
| echo -e "$AUTO_RESOLVED" | |
| fi | |
| # If any conflicts remain that we can't auto-resolve, abort | |
| REMAINING=$(git diff --name-only --diff-filter=U) | |
| if [ -n "$REMAINING" ]; then | |
| echo "::error::Unable to auto-resolve all conflicts. Manual intervention required:" | |
| echo -e "$UNRESOLVED" | |
| git merge --abort | |
| exit 1 | |
| fi | |
| # Reconcile lockfile if it was conflict-resolved | |
| if [ "${NEEDS_LOCKFILE_RECONCILE:-}" = "true" ]; then | |
| echo "Running pnpm install to reconcile lockfile with fork's package.json..." | |
| pnpm install --no-frozen-lockfile | |
| git add pnpm-lock.yaml | |
| fi | |
| # All conflicts resolved — complete the merge | |
| git commit --no-edit | |
| echo "auto_resolved=$(echo -e "$AUTO_RESOLVED" | base64 -w 0)" >> "$GITHUB_OUTPUT" | |
| git push origin "$BRANCH" | |
| - name: Create PR | |
| if: steps.compare.outputs.needs_sync == 'true' && steps.check_pr.outputs.pr_exists == 'false' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| GH_REPO: ${{ github.repository }} | |
| run: | | |
| VERSION=${{ steps.compare.outputs.upstream_version }} | |
| DATE=${{ steps.compare.outputs.date }} | |
| BRANCH="sync/v${VERSION}-${DATE}" | |
| CHANGED=$(echo "${{ steps.compare.outputs.changed }}" | base64 -d) | |
| AUTO_RESOLVED="" | |
| if [ -n "${{ steps.merge.outputs.auto_resolved }}" ]; then | |
| DECODED=$(echo "${{ steps.merge.outputs.auto_resolved }}" | base64 -d) | |
| AUTO_RESOLVED="### Auto-resolved conflicts | |
| \`\`\` | |
| ${DECODED} | |
| \`\`\` | |
| " | |
| fi | |
| gh pr create \ | |
| --base main \ | |
| --head "$BRANCH" \ | |
| --title "Sync upstream TanStack Pacer v${VERSION} (${DATE})" \ | |
| --body "## Upstream sync | |
| New upstream release detected. This PR merges upstream \`main\` into our fork. | |
| ### Changed packages | |
| ${CHANGED} | |
| ${AUTO_RESOLVED}### After merging | |
| The [publish-fork](${{ github.server_url }}/${{ github.repository }}/actions/workflows/publish-fork.yml) workflow will automatically publish any packages with changed versions. | |
| --- | |
| *Auto-generated by the [sync-upstream](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) workflow.*" |