Skip to content

Mirror Sync with Upstream #127

Mirror Sync with Upstream

Mirror Sync with Upstream #127

# 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 }}"