Sync Upstream PRs #53
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 PRs | |
| on: | |
| schedule: | |
| # Run daily at 6 AM UTC | |
| - cron: '0 6 * * *' | |
| workflow_dispatch: | |
| inputs: | |
| sync_mode: | |
| description: 'What to sync' | |
| required: true | |
| default: 'all_open_prs' | |
| type: choice | |
| options: | |
| - all_open_prs | |
| - main_branch | |
| - specific_pr | |
| pr_number: | |
| description: 'Specific PR number (only for specific_pr mode)' | |
| required: false | |
| type: string | |
| force_fmt: | |
| description: 'Force run formatter on all existing sync branches' | |
| required: false | |
| default: false | |
| type: boolean | |
| env: | |
| UPSTREAM_REPO: ionic-team/capacitor | |
| UPSTREAM_BRANCH: main | |
| jobs: | |
| # Job 1: Fetch all open PRs from upstream and create corresponding PRs | |
| sync-open-prs: | |
| if: github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sync_mode == 'all_open_prs') | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 | |
| filter: blob:none | |
| token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' | |
| - name: Install dependencies | |
| run: npm install | |
| - name: Configure Git | |
| run: | | |
| git config user.name "Capacitor+ Bot" | |
| git config user.email "bot@capgo.app" | |
| - name: Fetch and sync all open PRs from upstream | |
| env: | |
| GH_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} | |
| run: | | |
| echo "Fetching open PRs from upstream repository..." | |
| # Get all open PRs from upstream (only from forks/external contributors) | |
| OPEN_PRS=$(gh pr list \ | |
| --repo ${{ env.UPSTREAM_REPO }} \ | |
| --state open \ | |
| --json number,title,headRefName,headRepositoryOwner,author,url,isDraft \ | |
| --limit 100) | |
| echo "Found PRs:" | |
| echo "$OPEN_PRS" | jq -r '.[] | "PR #\(.number): \(.title) by @\(.author.login)"' | |
| # Filter to non-draft PRs and exclude our own PRs (riderx is the maintainer) | |
| # This prevents syncing PRs we created ourselves on the upstream repo | |
| ALL_PRS=$(echo "$OPEN_PRS" | jq '[.[] | select(.isDraft == false and .author.login != "riderx")]') | |
| echo "" | |
| echo "All open PRs (non-draft):" | |
| echo "$ALL_PRS" | jq -r '.[] | "PR #\(.number): \(.title) by @\(.author.login)"' | |
| PR_COUNT=$(echo "$ALL_PRS" | jq 'length') | |
| echo "" | |
| echo "Total PRs to sync: $PR_COUNT" | |
| if [ "$PR_COUNT" -eq 0 ]; then | |
| echo "No PRs to sync" | |
| exit 0 | |
| fi | |
| # Process each PR | |
| echo "$ALL_PRS" | jq -c '.[]' | while read -r pr; do | |
| PR_NUMBER=$(echo "$pr" | jq -r '.number') | |
| PR_TITLE=$(echo "$pr" | jq -r '.title') | |
| PR_AUTHOR=$(echo "$pr" | jq -r '.author.login') | |
| PR_URL=$(echo "$pr" | jq -r '.url') | |
| echo "" | |
| echo "==========================================" | |
| echo "Processing PR #$PR_NUMBER: $PR_TITLE" | |
| echo "Author: @$PR_AUTHOR" | |
| echo "==========================================" | |
| # Fetch the PR branch from upstream first to check for updates | |
| echo "Fetching PR #$PR_NUMBER from upstream..." | |
| git fetch https://github.com/${{ env.UPSTREAM_REPO }}.git pull/$PR_NUMBER/head:upstream-pr-$PR_NUMBER || { | |
| echo "Failed to fetch PR #$PR_NUMBER, skipping..." | |
| continue | |
| } | |
| SYNC_BRANCH="sync/upstream-pr-$PR_NUMBER" | |
| UPSTREAM_HEAD=$(git rev-parse upstream-pr-$PR_NUMBER) | |
| # Check if we already have a PR for this upstream PR | |
| EXISTING_PR=$(gh pr list \ | |
| --state open \ | |
| --search "upstream PR #$PR_NUMBER" \ | |
| --json number,headRefName \ | |
| --limit 1) | |
| if [ "$(echo "$EXISTING_PR" | jq 'length')" -gt 0 ]; then | |
| EXISTING_PR_NUM=$(echo "$EXISTING_PR" | jq -r '.[0].number') | |
| echo "PR #$EXISTING_PR_NUM for upstream #$PR_NUMBER already exists" | |
| # Check if upstream has new commits by comparing with our sync branch | |
| git fetch origin "$SYNC_BRANCH" 2>/dev/null || true | |
| if git rev-parse "origin/$SYNC_BRANCH" >/dev/null 2>&1; then | |
| # Get the upstream commit that was merged into our branch | |
| # We need to check if the upstream PR has new commits | |
| OUR_BRANCH_HEAD=$(git rev-parse "origin/$SYNC_BRANCH") | |
| # Check if force_fmt is enabled | |
| FORCE_FMT="${{ github.event.inputs.force_fmt }}" | |
| # Check if upstream commit is already in our branch | |
| if git merge-base --is-ancestor "$UPSTREAM_HEAD" "origin/$SYNC_BRANCH" 2>/dev/null; then | |
| if [ "$FORCE_FMT" = "true" ]; then | |
| echo "Force fmt enabled - will re-format even though branch is up to date..." | |
| else | |
| echo "Our sync branch is up to date with upstream PR, skipping..." | |
| git branch -D upstream-pr-$PR_NUMBER 2>/dev/null || true | |
| continue | |
| fi | |
| else | |
| echo "Upstream PR has new commits! Updating our sync branch..." | |
| # Continue to re-sync the branch | |
| fi | |
| else | |
| echo "Sync branch doesn't exist on remote, will recreate..." | |
| fi | |
| else | |
| # Also check closed PRs to avoid re-creating rejected ones | |
| CLOSED_PR=$(gh pr list \ | |
| --state closed \ | |
| --search "upstream PR #$PR_NUMBER" \ | |
| --json number \ | |
| --limit 1) | |
| if [ "$(echo "$CLOSED_PR" | jq 'length')" -gt 0 ]; then | |
| echo "PR for upstream #$PR_NUMBER was previously closed, skipping..." | |
| git branch -D upstream-pr-$PR_NUMBER 2>/dev/null || true | |
| continue | |
| fi | |
| fi | |
| # Delete the local branch if it exists | |
| git branch -D "$SYNC_BRANCH" 2>/dev/null || true | |
| # Create sync branch from plus | |
| git checkout plus | |
| git checkout -b "$SYNC_BRANCH" | |
| # Try to merge the PR | |
| echo "Merging PR #$PR_NUMBER..." | |
| if git merge upstream-pr-$PR_NUMBER --no-edit -m "chore: sync upstream PR #$PR_NUMBER from @$PR_AUTHOR"; then | |
| echo "Merge successful!" | |
| # Run formatter to ensure lint passes | |
| echo "Running formatter..." | |
| npm run fmt || echo "Formatter had some issues, continuing..." | |
| # Check if fmt made any changes and commit them | |
| if ! git diff --quiet; then | |
| echo "Formatter made changes, committing..." | |
| git add -A | |
| git commit -m "style: auto-format code after upstream sync" | |
| fi | |
| # Push the branch | |
| git push origin "$SYNC_BRANCH" --force | |
| # Create PR body | |
| PR_BODY=$(cat <<EOF | |
| ## Upstream PR Sync | |
| This PR syncs changes from an external contributor's PR on the official Capacitor repository. | |
| ### Original PR | |
| - **PR:** [#$PR_NUMBER]($PR_URL) | |
| - **Title:** $PR_TITLE | |
| - **Author:** @$PR_AUTHOR | |
| ### Automation | |
| - CI will run automatically | |
| - Claude Code will review for security/breaking changes | |
| - If approved, this PR will be auto-merged | |
| - A new release will be published automatically | |
| --- | |
| *Synced from upstream by Capacitor+ Bot* | |
| EOF | |
| ) | |
| # Create the PR | |
| gh pr create \ | |
| --base plus \ | |
| --head "$SYNC_BRANCH" \ | |
| --title "chore: sync upstream PR #$PR_NUMBER - $PR_TITLE" \ | |
| --body "$PR_BODY" \ | |
| --label "upstream-sync,automated" || { | |
| echo "Failed to create PR, it may already exist" | |
| } | |
| echo "Created PR for upstream #$PR_NUMBER" | |
| else | |
| echo "Merge conflict detected for PR #$PR_NUMBER" | |
| git merge --abort | |
| # Create an issue for manual intervention | |
| gh issue create \ | |
| --title "Merge conflict: Upstream PR #$PR_NUMBER - $PR_TITLE" \ | |
| --body "The sync of upstream PR #$PR_NUMBER from @$PR_AUTHOR encountered merge conflicts. | |
| **Original PR:** $PR_URL | |
| Please resolve the conflicts manually." \ | |
| --label "upstream-sync,needs-attention,merge-conflict" || { | |
| echo "Issue may already exist" | |
| } | |
| fi | |
| # Clean up | |
| git checkout plus | |
| git branch -D upstream-pr-$PR_NUMBER 2>/dev/null || true | |
| done | |
| echo "" | |
| echo "Sync complete!" | |
| # Job 2: Sync main branch from upstream | |
| sync-main-branch: | |
| if: github.event_name == 'workflow_dispatch' && github.event.inputs.sync_mode == 'main_branch' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 | |
| filter: blob:none | |
| token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' | |
| - name: Install dependencies | |
| run: npm install | |
| - name: Configure Git | |
| run: | | |
| git config user.name "Capacitor+ Bot" | |
| git config user.email "bot@capgo.app" | |
| - name: Sync main branch from upstream | |
| env: | |
| GH_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} | |
| run: | | |
| git remote add upstream https://github.com/${{ env.UPSTREAM_REPO }}.git || true | |
| git fetch upstream | |
| git checkout plus | |
| git fetch upstream main | |
| # Check if there are new commits from upstream | |
| UPSTREAM_COMMITS=$(git rev-list plus..upstream/main --count) | |
| if [ "$UPSTREAM_COMMITS" -eq 0 ]; then | |
| echo "No new commits from upstream main branch" | |
| exit 0 | |
| fi | |
| echo "Found $UPSTREAM_COMMITS new commits from upstream" | |
| # Generate branch name with date | |
| SYNC_BRANCH="sync/upstream-main-$(date +%Y%m%d-%H%M%S)" | |
| # Create sync branch from plus | |
| git checkout -b "$SYNC_BRANCH" plus | |
| # Merge upstream changes | |
| if git merge upstream/main --no-edit -m "chore: sync with upstream main branch"; then | |
| # Run formatter to ensure lint passes | |
| echo "Running formatter..." | |
| npm run fmt || echo "Formatter had some issues, continuing..." | |
| # Check if fmt made any changes and commit them | |
| if ! git diff --quiet; then | |
| echo "Formatter made changes, committing..." | |
| git add -A | |
| git commit -m "style: auto-format code after upstream sync" | |
| fi | |
| git push origin "$SYNC_BRANCH" | |
| PR_BODY=$(cat <<'EOF' | |
| ## Upstream Main Branch Sync | |
| This PR syncs the latest changes from the official Capacitor main branch. | |
| ### Automation | |
| - CI will run automatically | |
| - Claude Code will review for security/breaking changes | |
| - If approved, this PR will be auto-merged | |
| --- | |
| *Synced from upstream by Capacitor+ Bot* | |
| EOF | |
| ) | |
| gh pr create \ | |
| --base plus \ | |
| --head "$SYNC_BRANCH" \ | |
| --title "chore: sync with upstream main $(date +%Y-%m-%d)" \ | |
| --body "$PR_BODY" \ | |
| --label "upstream-sync,automated" | |
| else | |
| echo "Merge conflicts detected" | |
| git merge --abort | |
| gh issue create \ | |
| --title "Merge conflict: Upstream main sync $(date +%Y-%m-%d)" \ | |
| --body "The automated upstream main branch sync encountered merge conflicts. Please resolve them manually." \ | |
| --label "upstream-sync,needs-attention,merge-conflict" | |
| fi | |
| # Job 3: Sync a specific PR by number | |
| sync-specific-pr: | |
| if: github.event_name == 'workflow_dispatch' && github.event.inputs.sync_mode == 'specific_pr' && github.event.inputs.pr_number != '' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 | |
| filter: blob:none | |
| token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' | |
| - name: Install dependencies | |
| run: npm install | |
| - name: Configure Git | |
| run: | | |
| git config user.name "Capacitor+ Bot" | |
| git config user.email "bot@capgo.app" | |
| - name: Sync specific PR | |
| env: | |
| GH_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} | |
| PR_NUMBER: ${{ github.event.inputs.pr_number }} | |
| run: | | |
| echo "Syncing specific PR #$PR_NUMBER from upstream..." | |
| # Get PR info | |
| PR_INFO=$(gh pr view $PR_NUMBER --repo ${{ env.UPSTREAM_REPO }} --json title,author,url,state) | |
| PR_TITLE=$(echo "$PR_INFO" | jq -r '.title') | |
| PR_AUTHOR=$(echo "$PR_INFO" | jq -r '.author.login') | |
| PR_URL=$(echo "$PR_INFO" | jq -r '.url') | |
| PR_STATE=$(echo "$PR_INFO" | jq -r '.state') | |
| echo "PR #$PR_NUMBER: $PR_TITLE" | |
| echo "Author: @$PR_AUTHOR" | |
| echo "State: $PR_STATE" | |
| if [ "$PR_STATE" != "OPEN" ]; then | |
| echo "Warning: PR #$PR_NUMBER is not open (state: $PR_STATE)" | |
| fi | |
| # Fetch the PR | |
| git fetch https://github.com/${{ env.UPSTREAM_REPO }}.git pull/$PR_NUMBER/head:upstream-pr-$PR_NUMBER | |
| # Create branch for this PR | |
| SYNC_BRANCH="sync/upstream-pr-$PR_NUMBER" | |
| # Delete existing branch if any | |
| git branch -D "$SYNC_BRANCH" 2>/dev/null || true | |
| git push origin --delete "$SYNC_BRANCH" 2>/dev/null || true | |
| git checkout plus | |
| git checkout -b "$SYNC_BRANCH" | |
| # Merge the PR commits | |
| if git merge upstream-pr-$PR_NUMBER --no-edit -m "chore: sync upstream PR #$PR_NUMBER from @$PR_AUTHOR"; then | |
| # Run formatter to ensure lint passes | |
| echo "Running formatter..." | |
| npm run fmt || echo "Formatter had some issues, continuing..." | |
| # Check if fmt made any changes and commit them | |
| if ! git diff --quiet; then | |
| echo "Formatter made changes, committing..." | |
| git add -A | |
| git commit -m "style: auto-format code after upstream sync" | |
| fi | |
| git push origin "$SYNC_BRANCH" --force | |
| PR_BODY=$(cat <<EOF | |
| ## Upstream PR Sync | |
| This PR syncs changes from an external contributor's PR on the official Capacitor repository. | |
| ### Original PR | |
| - **PR:** [#$PR_NUMBER]($PR_URL) | |
| - **Title:** $PR_TITLE | |
| - **Author:** @$PR_AUTHOR | |
| ### Automation | |
| - CI will run automatically | |
| - Claude Code will review for security/breaking changes | |
| - If approved, this PR will be auto-merged | |
| --- | |
| *Synced from upstream by Capacitor+ Bot* | |
| EOF | |
| ) | |
| gh pr create \ | |
| --base plus \ | |
| --head "$SYNC_BRANCH" \ | |
| --title "chore: sync upstream PR #$PR_NUMBER - $PR_TITLE" \ | |
| --body "$PR_BODY" \ | |
| --label "upstream-sync,automated" | |
| echo "Successfully created PR for upstream #$PR_NUMBER" | |
| else | |
| echo "Merge conflicts detected for PR #$PR_NUMBER" | |
| git merge --abort | |
| gh issue create \ | |
| --title "Merge conflict: Upstream PR #$PR_NUMBER - $PR_TITLE" \ | |
| --body "The sync of upstream PR #$PR_NUMBER from @$PR_AUTHOR encountered merge conflicts. | |
| **Original PR:** $PR_URL | |
| Please resolve the conflicts manually." \ | |
| --label "upstream-sync,needs-attention,merge-conflict" | |
| fi |