Upstream Per-PR Sync #131
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |