Mirror Sync with Upstream #5
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
| # Template for any upstream repository - update the upstream URL as needed | |
| # | |
| # Key Features: | |
| # - Creates/updates ONE evergreen PR (bot/upstream-sync -> main) | |
| # - Syncs daily or on demand; force-resets sync branch to upstream/main | |
| # - Respects branch protections (no direct pushes to main) | |
| # - Protects hotfix/* branches (we never touch them) | |
| # - Includes error handling and status reporting | |
| name: Mirror Sync with Upstream | |
| on: | |
| schedule: | |
| - cron: '0 6 * * *' # Daily at 6 AM UTC | |
| workflow_dispatch: # Allow manual trigger | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| jobs: | |
| sync: | |
| runs-on: ubuntu-latest | |
| env: | |
| SYNC_BRANCH: bot/upstream-sync | |
| BASE_BRANCH: main | |
| UPSTREAM_REPO: kubernetes-sigs/cluster-api-provider-openstack | |
| steps: | |
| - name: Checkout mirror repository | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Configure Git | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| - name: Add upstream remote & fetch | |
| run: | | |
| git remote add upstream https://github.com/${UPSTREAM_REPO}.git || true | |
| if ! git fetch upstream ${BASE_BRANCH}; then | |
| echo "::error::Failed to fetch from upstream repository" | |
| exit 1 | |
| fi | |
| echo "Fetched upstream/${BASE_BRANCH}" | |
| - name: Determine if main is already in sync | |
| id: diff | |
| run: | | |
| git fetch origin ${BASE_BRANCH} --quiet | |
| UP_SHA=$(git rev-parse upstream/${BASE_BRANCH}) | |
| OR_SHA=$(git rev-parse origin/${BASE_BRANCH} || true) | |
| echo "upstream/main: $UP_SHA" | |
| echo "origin/main : $OR_SHA" | |
| if [ "$UP_SHA" = "$OR_SHA" ]; then | |
| echo "in_sync=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "in_sync=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Update sync branch to upstream/main | |
| if: steps.diff.outputs.in_sync == 'false' | |
| run: | | |
| # Create or switch to the evergreen sync branch | |
| if git show-ref --verify --quiet refs/remotes/origin/${SYNC_BRANCH}; then | |
| # Sync branch exists on remote - check if we have it locally | |
| if git show-ref --verify --quiet refs/heads/${SYNC_BRANCH}; then | |
| git switch ${SYNC_BRANCH} | |
| else | |
| git switch -c ${SYNC_BRANCH} --track origin/${SYNC_BRANCH} | |
| fi | |
| else | |
| # Sync branch doesn't exist - create from main | |
| git switch -c ${SYNC_BRANCH} origin/${BASE_BRANCH} | |
| fi | |
| # Force reset sync branch to upstream/main | |
| git reset --hard upstream/${BASE_BRANCH} | |
| # Push the updated sync branch | |
| git push -f origin ${SYNC_BRANCH} | |
| - name: Check if sync branch differs from main | |
| if: steps.diff.outputs.in_sync == 'false' | |
| id: branch_diff | |
| run: | | |
| # Fetch latest state to ensure we have current refs | |
| git fetch origin ${BASE_BRANCH} ${SYNC_BRANCH} --quiet | |
| # Check if branches are actually different (tree-wise) | |
| MAIN_TREE=$(git rev-parse origin/${BASE_BRANCH}^{tree}) | |
| SYNC_TREE=$(git rev-parse origin/${SYNC_BRANCH}^{tree}) | |
| echo "main tree: $MAIN_TREE" | |
| echo "sync tree: $SYNC_TREE" | |
| if [ "$MAIN_TREE" = "$SYNC_TREE" ]; then | |
| echo "has_changes=false" >> $GITHUB_OUTPUT | |
| echo "Branches have identical content (same tree), no PR needed" | |
| else | |
| echo "has_changes=true" >> $GITHUB_OUTPUT | |
| echo "Branches have different content, PR is needed" | |
| # Show what's different (for debugging) | |
| echo "Files changed between branches:" | |
| git diff --name-only origin/${BASE_BRANCH} origin/${SYNC_BRANCH} | head -10 | |
| fi | |
| - name: Create or update PR to main | |
| if: steps.diff.outputs.in_sync == 'false' && steps.branch_diff.outputs.has_changes == 'true' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const {owner, repo} = context.repo; | |
| const headRef = `${owner}:${process.env.SYNC_BRANCH}`; | |
| const base = process.env.BASE_BRANCH; | |
| // Find existing open PR for this branch | |
| const prs = await github.rest.pulls.list({ | |
| owner, repo, state: 'open', head: headRef, base | |
| }); | |
| let pr; | |
| if (prs.data.length) { | |
| pr = prs.data[0]; | |
| core.info(`Found existing PR #${pr.number}`); | |
| } else { | |
| pr = (await github.rest.pulls.create({ | |
| owner, repo, | |
| title: 'Sync with upstream/main', | |
| head: process.env.SYNC_BRANCH, | |
| base, | |
| body: 'Automated sync from upstream. This PR is continuously updated.' | |
| })).data; | |
| core.info(`Created PR #${pr.number}`); | |
| } | |
| // Try to enable auto-merge (requires repo setting + permissions) | |
| try { | |
| await github.graphql( | |
| `mutation($id:ID!){ | |
| enablePullRequestAutoMerge(input:{pullRequestId:$id, mergeMethod:MERGE}) { clientMutationId } | |
| }`, | |
| { id: pr.node_id } | |
| ); | |
| core.info('Auto-merge enabled.'); | |
| } catch (e) { | |
| core.warning('Auto-merge not enabled (this is OK): ' + e.message); | |
| } | |
| - name: Clean up stale upstream tracking refs | |
| run: | | |
| git remote prune upstream | |
| echo "Pruned stale upstream refs" | |
| - name: Report sync status | |
| run: | | |
| echo "" | |
| echo "=== Mirror Sync Report ===" | |
| if [ "${{ steps.diff.outputs.in_sync }}" = "true" ]; then | |
| echo "Already in sync with upstream/main. No PR updates needed." | |
| elif [ "${{ steps.branch_diff.outputs.has_changes || 'false' }}" = "false" ]; then | |
| echo "Branches were out of sync but have identical content. Sync branch updated." | |
| else | |
| echo "Opened/updated PR from ${SYNC_BRANCH} -> ${BASE_BRANCH}." | |
| echo "Content changes detected between branches" | |
| fi | |
| echo "" | |
| echo "Latest commits on upstream/main:" | |
| git log upstream/${BASE_BRANCH} --oneline -5 || true | |
| echo "" | |
| echo "Hotfix branches (preserved, untouched by this workflow):" | |
| git for-each-ref --format='%(refname:short)' refs/heads/hotfix/ || echo " No hotfix/* branches found" | |
| echo "==========================" | |
| - name: Notify on failure | |
| if: failure() | |
| run: | | |
| echo "::error::🚨 Mirror sync failed! Manual intervention required." | |
| echo "::error::Repository: ${{ github.repository }}" | |
| echo "::error::Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" |