Skip to content

Sync Forks from Upstream #70

Sync Forks from Upstream

Sync Forks from Upstream #70

Workflow file for this run

name: Sync Forks from Upstream
# Keeps your forks of gramps-project/{gramps,addons-source} in sync with
# upstream by opening (or updating) a "sync from upstream" PR for each
# matrix cell. Runs nightly and on demand.
#
# Why this opens a PR instead of fast-forwarding directly:
# * The forks carry a "PRFirst" branch ruleset (no bypass) on master
# and maintenance/gramps60, so direct push is rejected — the previous
# `gh repo sync` implementation could never have worked even with a
# valid token.
# * The forks frequently diverge from upstream because we cherry-pick
# CI/CD work onto the fork's mainline. `gh repo sync` rejects any
# divergence outright; a merge commit reconciles cleanly.
#
# Requirements: a fine-grained PAT in `secrets.FORK_SYNC_TOKEN` with
# Contents:write + Pull-requests:write on the FORK repos.
#
# Fork owner is its OWN config knob — do NOT derive it from
# github.repository_owner. The forks are not necessarily under the testbed's
# owner: this testbed lives under the `Ralphovi` org, while the forks are
# personal repos under the `eduralph` user. So the owner comes from
# `vars.FORK_OWNER` (default `eduralph`). A prior version derived it from
# github.repository_owner, which pointed the sync at the non-existent
# `Ralphovi/gramps` after the testbed moved to the org. If the forks are ever
# moved into the testbed's org, set the FORK_OWNER repo variable to match.
# The PAT scope is configured out-of-band and must cover FORK_OWNER's repos.
on:
schedule:
- cron: '0 4 * * *' # 04:00 UTC daily
workflow_dispatch:
jobs:
sync:
runs-on: ubuntu-24.04
# Cap a wedged git clone/fetch/merge/push or a hung gh call. The sync
# is a handful of git+gh operations per matrix cell; 15min is ample.
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
repo:
- name: gramps
owner: ${{ vars.FORK_OWNER || 'eduralph' }}
- name: addons-source
owner: ${{ vars.FORK_OWNER || 'eduralph' }}
branch:
- master
- maintenance/gramps60
- maintenance/gramps61
steps:
- name: Verify FORK_SYNC_TOKEN is configured
env:
TOKEN: ${{ secrets.FORK_SYNC_TOKEN }}
run: |
if [ -z "$TOKEN" ]; then
cat >&2 <<'EOF'
✗ FORK_SYNC_TOKEN repository secret is not set on this repo.
Without it, the sync workflow cannot push to the forks. To fix:
1. Create a fine-grained PAT at
https://github.com/settings/personal-access-tokens/new
scoped to the fork owner's (vars.FORK_OWNER, default
eduralph) gramps + addons-source forks with
Contents: read & write
Pull requests: read & write
2. Add it as a repo secret on this testbed:
gh secret set FORK_SYNC_TOKEN -R <this-repo>
EOF
exit 1
fi
- name: Sync ${{ matrix.repo.owner }}/${{ matrix.repo.name }}@${{ matrix.branch }}
env:
GH_TOKEN: ${{ secrets.FORK_SYNC_TOKEN }}
FORK: ${{ matrix.repo.owner }}/${{ matrix.repo.name }}
BRANCH: ${{ matrix.branch }}
UPSTREAM_OWNER: gramps-project
run: |
set -euo pipefail
UPSTREAM="$UPSTREAM_OWNER/${FORK#*/}"
SYNC_BRANCH="sync/upstream-${BRANCH//\//-}-auto"
# Clone the fork with the PAT and add upstream as a second remote.
git clone "https://x-access-token:${GH_TOKEN}@github.com/${FORK}.git" fork
cd fork
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git remote add upstream "https://github.com/${UPSTREAM}.git"
git fetch --quiet origin "$BRANCH"
git fetch --quiet upstream "$BRANCH"
# Always anchor the checkout to origin/<branch> explicitly.
# `git checkout <branch>` errors with "matched multiple remote
# tracking branches" when both origin and upstream have the
# branch and there's no local branch yet (i.e. for every branch
# except the fork's default that `git clone` checked out).
git checkout -B "$BRANCH" "origin/$BRANCH"
BEHIND="$(git rev-list --count "HEAD..upstream/${BRANCH}")"
AHEAD="$(git rev-list --count "upstream/${BRANCH}..HEAD")"
echo "Fork is ${AHEAD} ahead and ${BEHIND} behind upstream/${BRANCH}."
if [ "$BEHIND" -eq 0 ]; then
echo "✓ Fork is already up to date with upstream — nothing to sync."
# Close a stale sync PR if one exists.
if gh pr view "$SYNC_BRANCH" --repo "$FORK" --json state \
--jq '.state' 2>/dev/null | grep -q OPEN; then
gh pr close "$SYNC_BRANCH" --repo "$FORK" \
--comment "Closing automatically: upstream merged or rebased; nothing to sync."
fi
exit 0
fi
# Start the sync branch fresh from the fork's current head and
# merge upstream into it. Force-push so a previously-open sync PR
# (if any) updates with the new merge result rather than diverging.
git checkout -B "$SYNC_BRANCH" "$BRANCH"
if git merge --no-edit --no-ff "upstream/${BRANCH}" \
-m "Sync ${FORK#*/}@${BRANCH} with upstream/${UPSTREAM_OWNER} ($(date -u +%Y-%m-%d))"; then
echo "✓ Clean merge."
# The sync branch is workflow-owned (never collaborated on),
# and we rebuild it from BRANCH each run, so the local SHA
# differs from any previous run's remote SHA even when the
# merge result is identical. Force-push is correct here.
git push --force origin "$SYNC_BRANCH"
else
CONFLICTS="$(git diff --name-only --diff-filter=U)"
git merge --abort
cat >&2 <<EOF
✗ Merge conflict syncing ${FORK}@${BRANCH} from upstream:
$CONFLICTS
Resolve manually (see agent-work/scripts/sync-fork-manual.md or the previous
manual sync PRs for the pattern).
EOF
exit 1
fi
# Open the PR if it isn't already open (the force-push updated
# it if it was). The base branch is the fork's protected branch;
# PRFirst requires the merge to land via PR.
if gh pr view "$SYNC_BRANCH" --repo "$FORK" --json state \
--jq '.state' 2>/dev/null | grep -q OPEN; then
echo "✓ Existing PR updated via force-push."
gh pr view "$SYNC_BRANCH" --repo "$FORK" --json url --jq '.url'
else
gh pr create --repo "$FORK" \
--base "$BRANCH" \
--head "$SYNC_BRANCH" \
--title "Sync ${FORK#*/}@${BRANCH} with upstream ($(date -u +%Y-%m-%d))" \
--body "Automated nightly sync from \`${UPSTREAM}@${BRANCH}\`. Generated by .github/workflows/upstream-sync.yml on the testbed."
fi