Skip to content

Commit a17afa8

Browse files
committed
Add workflow to sync branches from upstream
Introduce a GitHub Actions workflow (sync-branches-from-upstream.yml) that keeps fork branches in sync with their upstream parent. The job checks if the repo is a fork (using gh api + jq) and exits for upstream repos. If a fork, it checks out the repo, adds the upstream remote, fetches refs, and iterates origin branches (excluding HEAD). For each branch it skips branches missing in upstream, branches already up-to-date, or branches where the fork is ahead; otherwise it force-with-lease pushes upstream/<branch> to origin refs/heads/<branch>. The workflow runs on pushes to main/dev/dev-1.x and on a 5-minute schedule, uses write contents permission, has concurrency to cancel in-progress runs, and fails the job if any branch push fails.
1 parent d586baa commit a17afa8

File tree

1 file changed

+129
-0
lines changed

1 file changed

+129
-0
lines changed
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# This workflow syncs every branch that belongs to the current project
2+
# from the upstream repository. It can be triggered manually or runs automatically
3+
# every 5 minutes.
4+
#
5+
# Step 1: The GitHub API is queried to determine whether the current repository
6+
# is a fork. If it is the upstream (not a fork), the job exits immediately.
7+
# Step 2: Each branch present in the fork is synced from the upstream parent.
8+
# Branches that exist only in the fork (not in upstream) are skipped.
9+
# If the fork's branch is ahead of upstream (i.e. has newer commits),
10+
# the sync is also skipped to avoid overwriting fork-only changes.
11+
# Step 3: The schedule runs every 5 minutes.
12+
13+
name: Sync Branches from Upstream
14+
15+
on:
16+
push:
17+
branches: [ 'main' , 'dev' , 'dev-1.x' ]
18+
schedule:
19+
- cron: '*/5 * * * *'
20+
21+
concurrency:
22+
group: sync-branches-from-upstream
23+
cancel-in-progress: true
24+
25+
jobs:
26+
sync:
27+
name: Sync branches from upstream
28+
runs-on: ubuntu-latest
29+
permissions:
30+
contents: write
31+
steps:
32+
- name: Check if this repository is a fork
33+
id: fork_check
34+
env:
35+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
36+
run: |
37+
repo_info=$(gh api "repos/${{ github.repository }}")
38+
is_fork=$(echo "$repo_info" | jq -r '.fork')
39+
if [ "$is_fork" = "true" ]; then
40+
upstream_repo=$(echo "$repo_info" | jq -r '.parent.full_name // empty')
41+
if [ -z "$upstream_repo" ]; then
42+
echo "::error::Repository is marked as a fork but parent information is missing."
43+
exit 1
44+
fi
45+
echo "This repository is a fork of '${upstream_repo}'."
46+
echo "is_fork=true" >> "$GITHUB_OUTPUT"
47+
echo "upstream_repo=${upstream_repo}" >> "$GITHUB_OUTPUT"
48+
else
49+
echo "This repository is not a fork (it is the upstream). Nothing to sync."
50+
echo "is_fork=false" >> "$GITHUB_OUTPUT"
51+
fi
52+
53+
- name: Checkout Repository
54+
if: steps.fork_check.outputs.is_fork == 'true'
55+
uses: actions/checkout@v5
56+
with:
57+
fetch-depth: 0
58+
token: ${{ secrets.GITHUB_TOKEN }}
59+
60+
- name: Sync branches from upstream
61+
if: steps.fork_check.outputs.is_fork == 'true'
62+
env:
63+
GH_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
64+
run: |
65+
git config user.name "github-actions[bot]"
66+
git config user.email "github-actions[bot]@users.noreply.github.com"
67+
68+
upstream="${{ steps.fork_check.outputs.upstream_repo }}"
69+
echo "Upstream repository: $upstream"
70+
git remote add upstream "https://github.com/${upstream}.git"
71+
72+
echo "Fetching all refs from upstream..."
73+
git fetch upstream
74+
75+
echo "Fetching all refs from origin..."
76+
git fetch origin
77+
78+
# Collect all branches that exist in this project (origin), excluding HEAD pointer.
79+
origin_branches=$(git branch -r | grep '^ origin/' | grep -v '^ origin/HEAD' | sed 's|^ origin/||')
80+
81+
if [ -z "$origin_branches" ]; then
82+
echo "No branches found in the repository. Nothing to sync."
83+
exit 0
84+
fi
85+
86+
synced=0
87+
skipped=0
88+
failed=0
89+
90+
for branch in $origin_branches; do
91+
echo "---"
92+
# Only sync branches that also exist in upstream.
93+
if ! git ls-remote --exit-code upstream "refs/heads/${branch}" > /dev/null 2>&1; then
94+
echo "Branch '${branch}' does not exist in upstream – skipping."
95+
skipped=$((skipped + 1))
96+
continue
97+
fi
98+
99+
# If the origin and upstream branches point to the same commit, skip.
100+
origin_sha=$(git rev-parse "origin/${branch}")
101+
upstream_sha=$(git rev-parse "upstream/${branch}")
102+
if [ "$origin_sha" = "$upstream_sha" ]; then
103+
echo "Branch '${branch}' is already up to date."
104+
skipped=$((skipped + 1))
105+
continue
106+
fi
107+
108+
# If the fork branch is ahead of upstream (upstream is an ancestor of origin), skip.
109+
if git merge-base --is-ancestor "$upstream_sha" "$origin_sha"; then
110+
echo "Branch '${branch}' in fork is ahead of upstream – skipping."
111+
skipped=$((skipped + 1))
112+
continue
113+
fi
114+
115+
echo "Syncing branch '${branch}' from upstream..."
116+
if git push origin "upstream/${branch}:refs/heads/${branch}" --force-with-lease; then
117+
echo "Branch '${branch}' synced successfully."
118+
synced=$((synced + 1))
119+
else
120+
echo "::warning::Failed to push branch '${branch}'."
121+
failed=$((failed + 1))
122+
fi
123+
done
124+
125+
echo "==="
126+
echo "Sync complete: synced=${synced}, skipped=${skipped}, failed=${failed}"
127+
if [ "$failed" -gt 0 ]; then
128+
exit 1
129+
fi

0 commit comments

Comments
 (0)