Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 165 additions & 0 deletions .github/workflows/sync-upstream.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
name: Sync with upstream

# Weekly sync of this fork with juhaku/utoipa. Produces a PR against master
# that merges upstream while preserving fork-specific commits.
#
# Token note:
# PRs opened by GITHUB_TOKEN do NOT trigger other workflows (build.yaml,
# etc.) — a security guard against recursive CI. To get CI on the sync PR,
# create a fine-grained PAT with `contents: write` + `pull-requests: write`
# on this repo and store it as the `SYNC_PAT` secret. The workflow falls
# back to GITHUB_TOKEN if SYNC_PAT is not set.

on:
schedule:
# Mondays at 06:00 UTC.
- cron: "0 6 * * 1"
workflow_dispatch:

permissions:
contents: write
pull-requests: write
issues: write

concurrency:
group: sync-upstream
cancel-in-progress: false

jobs:
sync:
# Don't activate on forks-of-this-fork.
if: github.repository == 'Devolutions/utoipa'
runs-on: ubuntu-latest
env:
UPSTREAM_REPO: juhaku/utoipa
UPSTREAM_BRANCH: master
FORK_BRANCH: master
SYNC_BRANCH: sync/upstream
steps:
- name: Checkout fork (full history)
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.SYNC_PAT || secrets.GITHUB_TOKEN }}

- name: Configure git identity
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

- name: Add upstream remote and fetch
run: |
git remote add upstream "https://github.com/${UPSTREAM_REPO}.git"
git fetch upstream "${UPSTREAM_BRANCH}"
git fetch origin "${SYNC_BRANCH}" || true

- name: Determine whether sync is needed
id: check
run: |
BEHIND=$(git rev-list --count "${FORK_BRANCH}..upstream/${UPSTREAM_BRANCH}")
AHEAD=$(git rev-list --count "upstream/${UPSTREAM_BRANCH}..${FORK_BRANCH}")
UPSTREAM_SHA=$(git rev-parse "upstream/${UPSTREAM_BRANCH}")
echo "behind=${BEHIND}" >> "$GITHUB_OUTPUT"
echo "upstream_sha=${UPSTREAM_SHA}" >> "$GITHUB_OUTPUT"
echo "Fork is ${AHEAD} ahead and ${BEHIND} behind upstream (${UPSTREAM_SHA})."

- name: Attempt merge on sync branch
if: steps.check.outputs.behind != '0'
id: merge
env:
UPSTREAM_SHA: ${{ steps.check.outputs.upstream_sha }}
run: |
git checkout -B "${SYNC_BRANCH}" "${FORK_BRANCH}"
if git merge --no-ff --no-edit \
-m "Merge upstream ${UPSTREAM_REPO}@${UPSTREAM_BRANCH} (${UPSTREAM_SHA:0:7})" \
"upstream/${UPSTREAM_BRANCH}"; then
echo "status=clean" >> "$GITHUB_OUTPUT"
else
git merge --abort || true
echo "status=conflict" >> "$GITHUB_OUTPUT"
fi

- name: Push sync branch
if: steps.merge.outputs.status == 'clean'
id: push
env:
GH_TOKEN: ${{ secrets.SYNC_PAT || secrets.GITHUB_TOKEN }}
run: |
# If an open PR exists and someone has pushed commits to its branch
# (e.g. manual conflict resolution from a previous run), don't
# clobber that work. Comment on the PR instead.
PR_NUM=$(gh pr list --head "${SYNC_BRANCH}" --base "${FORK_BRANCH}" \
--state open --json number --jq '.[0].number // empty')
if [ -n "${PR_NUM}" ] && git rev-parse --verify "origin/${SYNC_BRANCH}" >/dev/null 2>&1; then
HUMAN_AHEAD=$(git rev-list --count "HEAD..origin/${SYNC_BRANCH}")
if [ "${HUMAN_AHEAD}" -gt 0 ]; then
echo "Open PR #${PR_NUM} has ${HUMAN_AHEAD} commit(s) we don't have — skipping push."
gh pr comment "${PR_NUM}" --body "New upstream commits are available since this PR was opened. Pull \`master\` into this branch and merge \`upstream/master\` again to incorporate them."
echo "skipped=true" >> "$GITHUB_OUTPUT"
exit 0
fi
fi
git push --force-with-lease origin "${SYNC_BRANCH}"
echo "skipped=false" >> "$GITHUB_OUTPUT"

- name: Open or update sync PR
if: steps.merge.outputs.status == 'clean' && steps.push.outputs.skipped != 'true'
env:
GH_TOKEN: ${{ secrets.SYNC_PAT || secrets.GITHUB_TOKEN }}
UPSTREAM_SHA: ${{ steps.check.outputs.upstream_sha }}
run: |
TITLE="Sync with upstream ${UPSTREAM_REPO}@${UPSTREAM_SHA:0:7}"
BODY=$(cat <<EOF
Automated weekly sync with upstream.

- Upstream: \`${UPSTREAM_REPO}@${UPSTREAM_BRANCH}\`
- Upstream HEAD: \`${UPSTREAM_SHA}\`
- Strategy: \`git merge --no-ff\` (preserves fork-specific commits)
- Generated by \`.github/workflows/sync-upstream.yaml\`
EOF
)
EXISTING=$(gh pr list --head "${SYNC_BRANCH}" --base "${FORK_BRANCH}" \
--state open --json number --jq '.[0].number // empty')
if [ -z "${EXISTING}" ]; then
gh pr create --base "${FORK_BRANCH}" --head "${SYNC_BRANCH}" \
--title "${TITLE}" --body "${BODY}"
else
gh pr edit "${EXISTING}" --title "${TITLE}" --body "${BODY}"
fi
Comment thread
metal-face marked this conversation as resolved.

- name: Report merge conflict
if: steps.merge.outputs.status == 'conflict'
env:
GH_TOKEN: ${{ secrets.SYNC_PAT || secrets.GITHUB_TOKEN }}
UPSTREAM_SHA: ${{ steps.check.outputs.upstream_sha }}
run: |
TITLE="Upstream sync conflict (${UPSTREAM_SHA:0:7})"
BODY=$(cat <<EOF
The weekly upstream sync failed to merge cleanly.

- Upstream: \`${UPSTREAM_REPO}@${UPSTREAM_BRANCH}\`
- Upstream HEAD: \`${UPSTREAM_SHA}\`

To resolve manually:

\`\`\`bash
git fetch origin
git checkout -B ${SYNC_BRANCH} origin/${FORK_BRANCH}
git remote add upstream https://github.com/${UPSTREAM_REPO}.git || true
git fetch upstream ${UPSTREAM_BRANCH}
git merge upstream/${UPSTREAM_BRANCH}
# resolve conflicts, then:
git push origin ${SYNC_BRANCH}
gh pr create --base ${FORK_BRANCH} --head ${SYNC_BRANCH} \\
--title "Sync with upstream" --body "Manual conflict resolution"
\`\`\`
EOF
)
EXISTING=$(gh issue list --state open --search "Upstream sync conflict in:title" \
--json number,title \
--jq '[.[] | select(.title | startswith("Upstream sync conflict"))] | .[0].number // empty')
if [ -z "${EXISTING}" ]; then
gh issue create --title "${TITLE}" --body "${BODY}"
else
gh issue comment "${EXISTING}" --body "${BODY}"
fi
Comment thread
metal-face marked this conversation as resolved.