Mirror Sync with Upstream #127
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 | |
| concurrency: | |
| group: upstream-sync | |
| cancel-in-progress: true | |
| 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: | | |
| set -euo pipefail | |
| git fetch origin ${BASE_BRANCH} --quiet | |
| echo "Checking differences excluding .github/workflows/**" | |
| if git diff --quiet upstream/${BASE_BRANCH} origin/${BASE_BRANCH} -- . ':(exclude).github/workflows/**'; 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: | | |
| set -euo pipefail | |
| git fetch origin --quiet || true | |
| # 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 | |
| # Rebase local state of sync branch onto our main to start clean | |
| git reset --hard origin/${BASE_BRANCH} | |
| # Compute and apply upstream changes excluding workflow files | |
| if git diff --quiet origin/${BASE_BRANCH} upstream/${BASE_BRANCH} -- . ':(exclude).github/workflows/**'; then | |
| echo "No non-workflow changes from upstream; nothing to apply." | |
| else | |
| git diff --binary origin/${BASE_BRANCH} upstream/${BASE_BRANCH} -- . ':(exclude).github/workflows/**' > /tmp/upstream-no-workflows.patch | |
| git apply -3 /tmp/upstream-no-workflows.patch || { | |
| echo "::error::Failed to apply upstream patch excluding workflows"; | |
| exit 1; | |
| } | |
| git add -A | |
| git commit -m "Sync with upstream/${BASE_BRANCH} (excluding .github/workflows)" || true | |
| fi | |
| # 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 | |
| # Decide if PR is needed based on content differences excluding workflow files | |
| if git diff --quiet origin/${BASE_BRANCH} origin/${SYNC_BRANCH} -- . ':(exclude).github/workflows/**'; then | |
| echo "has_changes=false" >> $GITHUB_OUTPUT | |
| echo "All upstream changes already present in main (excluding workflows); no PR needed" | |
| else | |
| echo "has_changes=true" >> $GITHUB_OUTPUT | |
| echo "PR is needed; content changes detected outside .github/workflows/**" | |
| echo "Changed files (excluding workflows):" | |
| git diff --name-only origin/${BASE_BRANCH} origin/${SYNC_BRANCH} -- . ':(exclude).github/workflows/**' | head -50 || true | |
| 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: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| 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 (excluding .github/workflows). No PR updates needed." | |
| elif [ "${{ steps.branch_diff.outputs.has_changes || 'false' }}" = "false" ]; then | |
| echo "Branches were out of sync but have identical content outside workflows. Sync branch updated." | |
| else | |
| echo "Opened/updated PR from ${SYNC_BRANCH} -> ${BASE_BRANCH}." | |
| echo "Content changes detected between branches (excluding .github/workflows)" | |
| 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 ls-remote --heads origin 'hotfix/*' | awk '{print $2}' || 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 }}" |