Skip to content

Mirror Sync with Upstream #8

Mirror Sync with Upstream

Mirror Sync with Upstream #8

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