Mirror Sync with Upstream #8
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 | |
| 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" | |
| # Content-based sync check (robust to squash/rebase): compare trees | |
| UP_TREE=$(git rev-parse upstream/${BASE_BRANCH}^{tree}) | |
| OR_TREE=$(git rev-parse origin/${BASE_BRANCH}^{tree} || true) | |
| echo "upstream tree: $UP_TREE" | |
| echo "origin tree: $OR_TREE" | |
| if [ "$UP_TREE" = "$OR_TREE" ]; 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 | |
| # 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 | |
| # After resetting sync to upstream, decide if PR is needed based on patch-unique commits | |
| UPSTREAM_PATCH_AHEAD=$(git rev-list --left-only --cherry-pick --count upstream/${BASE_BRANCH}...origin/${BASE_BRANCH} || echo 0) | |
| echo "upstream patch-ahead of main by: $UPSTREAM_PATCH_AHEAD commits" | |
| if [ "$UPSTREAM_PATCH_AHEAD" -gt 0 ]; then | |
| echo "has_changes=true" >> $GITHUB_OUTPUT | |
| echo "PR is needed; upstream has patch-unique commits not in main" | |
| echo "Representative missing upstream commits:" | |
| git log --left-right --cherry-pick --oneline upstream/${BASE_BRANCH}...origin/${BASE_BRANCH} | sed -n 's/^</missing /p' | head -10 || true | |
| else | |
| echo "has_changes=false" >> $GITHUB_OUTPUT | |
| echo "All upstream changes already present in main (possibly via squash/rebase); no PR needed" | |
| 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. 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 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 }}" |