Skip to content

Upstream Per-PR Sync #124

Upstream Per-PR Sync

Upstream Per-PR Sync #124

Workflow file for this run

name: Upstream Per-PR Sync
on:
schedule:
- cron: '0 6 * * 1-5' # Weekdays at 6 AM UTC
workflow_dispatch: # Manual trigger
env:
FORK_REPO: OpenAds-Auction/OpenAdsServer
STATE_VAR: UPSTREAM_SYNC_LAST_SHA
jobs:
sync:
runs-on: ubuntu-22.04
steps:
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.SYNC_APP_ID }}
private-key: ${{ secrets.SYNC_APP_PRIVATE_KEY }}
owner: OpenAds-Auction
repositories: OpenAdsServer
- name: Checkout fork
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}
- name: Configure git
run: |
git config user.name "openads-sync-bot"
git config user.email "openads-sync@thetradedesk.com"
- name: Fetch upstream (prebid/prebid-server master)
run: |
git remote add upstream https://github.com/prebid/prebid-server.git
git fetch upstream master --no-tags
- name: Cherry-pick each upstream PR
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
shell: bash
run: |
# Don't use set -e — we handle errors per-commit instead of aborting
set -uo pipefail
# Read last-synced SHA from repo variable (or fall back to merge-base)
LAST_SHA=$(gh variable get "$STATE_VAR" --repo "$FORK_REPO" 2>/dev/null || true)
if [ -z "$LAST_SHA" ]; then
LAST_SHA=$(git merge-base HEAD upstream/master)
echo "No state variable found — using merge-base: $LAST_SHA"
else
echo "Resuming from variable $STATE_VAR: $LAST_SHA"
fi
UPSTREAM_HEAD=$(git rev-parse upstream/master)
if [ "$LAST_SHA" = "$UPSTREAM_HEAD" ]; then
echo "✅ No new upstream commits."
exit 0
fi
# List merge/squash commits (first-parent gives us one commit per merged PR)
COMMITS=$(git rev-list --first-parent --reverse "${LAST_SHA}..${UPSTREAM_HEAD}")
COMMIT_COUNT=$(echo "$COMMITS" | wc -w)
echo "Found $COMMIT_COUNT new upstream commits"
LATEST_GOOD_SHA="$LAST_SHA"
CREATED=0
SKIPPED=0
FAILED=0
for SHA in $COMMITS; do
MSG=$(git log -1 --format='%s' "$SHA")
echo ""
echo "━━━ Processing: ${SHA:0:8} — $MSG ━━━"
# Extract upstream PR number from commit message: "... (#1234)"
PR_NUM=$(echo "$MSG" | grep -oP '#\K\d+' | tail -1 || true)
if [ -z "$PR_NUM" ]; then
echo " ⚠ Could not extract PR number, using commit SHA as identifier"
PR_NUM="commit-${SHA:0:8}"
fi
BRANCH="sync/upstream-pr-${PR_NUM}"
# Check if upstream PR is labeled as adapter-only — skip entirely if so
if [[ "$PR_NUM" != commit-* ]]; then
UPSTREAM_LABELS=$(gh api "repos/prebid/prebid-server/pulls/${PR_NUM}" --jq '[.labels[].name] | join(",")' 2>/dev/null || true)
if echo ",$UPSTREAM_LABELS," | grep -qi ",adapter,"; then
echo " ⏭ Upstream PR has adapter label ($UPSTREAM_LABELS) — skipping"
LATEST_GOOD_SHA="$SHA"
SKIPPED=$((SKIPPED + 1))
continue
fi
fi
# Skip if any PR (open, merged, or closed) already exists for this branch
EXISTING=$(gh pr list --repo "$FORK_REPO" --head "$BRANCH" --state all --json number --jq 'length')
if [ "$EXISTING" -gt 0 ]; then
echo " ⏭ PR already exists for $BRANCH, skipping"
LATEST_GOOD_SHA="$SHA"
SKIPPED=$((SKIPPED + 1))
continue
fi
# Always start from a clean main
git checkout main
git branch -D "$BRANCH" 2>/dev/null || true
git checkout -b "$BRANCH"
# Per-commit state
HAS_CONFLICT=false
AUTO_RESOLVED=false
STRUCTURAL_CONFLICT=false
# Attempt cherry-pick
CHERRY_PICK_RESULT=0
git cherry-pick "$SHA" --no-edit 2>/dev/null || CHERRY_PICK_RESULT=$?
if [ "$CHERRY_PICK_RESULT" -ne 0 ]; then
# Check if this is an empty cherry-pick (e.g., reverting a change
# that was never applied, or a change already present on main)
if git diff --cached --quiet 2>/dev/null && git diff --quiet 2>/dev/null; then
echo " ⏭ Empty cherry-pick (no diff against main) — skipping"
git cherry-pick --abort 2>/dev/null || true
git checkout main
LATEST_GOOD_SHA="$SHA"
SKIPPED=$((SKIPPED + 1))
continue
fi
# Abort the failed cherry-pick, retry with -X theirs
git cherry-pick --abort
THEIRS_RESULT=0
git cherry-pick "$SHA" --no-edit -X theirs 2>/dev/null || THEIRS_RESULT=$?
if [ "$THEIRS_RESULT" -ne 0 ]; then
# True structural conflict — -X theirs couldn't resolve it
HAS_CONFLICT=true
STRUCTURAL_CONFLICT=true
echo " ⚠ Structural conflict — -X theirs failed, creating draft PR"
git add -A
git commit --no-edit -m "Upstream PR #${PR_NUM}: ${MSG} (STRUCTURAL CONFLICT)" || true
else
# -X theirs resolved it cleanly
AUTO_RESOLVED=true
echo " ✅ Conflict auto-resolved with -X theirs"
fi
fi
# Check if there are actually any commits to push
# (handles edge case where cherry-pick produced nothing)
if [ "$(git rev-parse HEAD)" = "$(git rev-parse main)" ]; then
echo " ⏭ No changes vs main after cherry-pick — skipping"
git checkout main
LATEST_GOOD_SHA="$SHA"
SKIPPED=$((SKIPPED + 1))
continue
fi
# Skip if the only changes are to .github/workflows/ (upstream CI, not relevant to fork)
CHANGED_FILES=$(git diff --name-only main)
NON_WORKFLOW_FILES=$(echo "$CHANGED_FILES" | grep -v '^\.github/workflows/' || true)
if [ -z "$NON_WORKFLOW_FILES" ]; then
echo " ⏭ Only workflow files changed — skipping"
git checkout main
LATEST_GOOD_SHA="$SHA"
SKIPPED=$((SKIPPED + 1))
continue
fi
# Check if the only changes are adapter-only file paths
PROBABLY_ADAPTER_ONLY=false
RELEVANT_FILES=$(echo "$NON_WORKFLOW_FILES" \
| grep -v '^adapters/' \
| grep -v '^static/bidder-params/' \
| grep -v '^static/bidder-info/' \
| grep -v '^exchange/adapter_builders\.go$' \
| grep -v '^exchange/adapter_util\.go$' \
| grep -v '^openrtb_ext/bidders\.go$' \
| grep -v '^openrtb_ext/bidders_validate_test\.go$' \
| grep -v '^openrtb_ext/imp_[^/]*\.go$' \
| grep -v '^macros/macros\.go$' \
| grep -v '^config/bidderinfo\.go$' \
|| true)
if [ -z "$RELEVANT_FILES" ]; then
echo " 🏷 Only adapter files changed — will label probably-adapter-only"
PROBABLY_ADAPTER_ONLY=true
fi
git push origin "$BRANCH" --force
# Build PR body
PR_TITLE="Upstream PR #${PR_NUM}: ${MSG}"
PR_BODY="Cherry-picked from upstream [\`${SHA:0:8}\`](https://github.com/prebid/prebid-server/commit/${SHA})"
if [[ "$PR_NUM" != commit-* ]]; then
PR_BODY="${PR_BODY}
Original PR: https://github.com/prebid/prebid-server/pull/${PR_NUM}"
fi
if [ "$AUTO_RESOLVED" = true ]; then
PR_BODY="${PR_BODY}
> ⚠️ **Auto-resolved**: This cherry-pick had conflicts that were resolved by accepting the upstream version (\`-X theirs\`). Review carefully for downstream-specific changes that may have been overwritten."
fi
# Create PR — draft only for structural conflicts
DRAFT_FLAG=""
if [ "$STRUCTURAL_CONFLICT" = true ]; then
DRAFT_FLAG="--draft"
fi
PR_URL=$(gh pr create \
--repo "$FORK_REPO" \
--title "$PR_TITLE" \
--body "$PR_BODY" \
--head "$BRANCH" \
--base main \
$DRAFT_FLAG 2>&1) || {
echo " ❌ Failed to create PR: $PR_URL"
git checkout main
LATEST_GOOD_SHA="$SHA"
FAILED=$((FAILED + 1))
continue
}
echo " ✅ Created PR: $PR_URL"
# Add labels (non-fatal)
gh pr edit "$PR_URL" --repo "$FORK_REPO" --add-label "upstream-sync" || echo " ⚠ Could not add upstream-sync label"
if [ "$AUTO_RESOLVED" = true ]; then
gh pr edit "$PR_URL" --repo "$FORK_REPO" --add-label "auto-resolved-theirs" || echo " ⚠ Could not add auto-resolved-theirs label"
fi
if [ "$STRUCTURAL_CONFLICT" = true ]; then
gh pr edit "$PR_URL" --repo "$FORK_REPO" --add-label "structural-conflict" || echo " ⚠ Could not add structural-conflict label"
fi
if [ "$PROBABLY_ADAPTER_ONLY" = true ]; then
gh pr edit "$PR_URL" --repo "$FORK_REPO" --add-label "probably-adapter-only" || echo " ⚠ Could not add probably-adapter-only label"
fi
LATEST_GOOD_SHA="$SHA"
CREATED=$((CREATED + 1))
git checkout main
done
echo ""
echo "━━━ Summary ━━━"
echo " Created: $CREATED"
echo " Skipped: $SKIPPED"
echo " Failed: $FAILED"
# Save state as a repo variable (no git push needed)
if [ "$LATEST_GOOD_SHA" != "$LAST_SHA" ]; then
gh variable set "$STATE_VAR" --repo "$FORK_REPO" --body "$LATEST_GOOD_SHA"
echo "Updated $STATE_VAR → ${LATEST_GOOD_SHA:0:8}"
fi