Skip to content

fix: artwork backfill with live redraws, case skin filtering, CI recu… #667

fix: artwork backfill with live redraws, case skin filtering, CI recu…

fix: artwork backfill with live redraws, case skin filtering, CI recu… #667

name: Auto-rebase PRs on develop push
# When develop receives new commits (push OR PR merge), automatically rebase
# all open PRs targeting develop so they stay current.
#
# Triggers:
# push to develop — direct commits (e.g. manual hotfixes)
# pull_request:closed — PR was squash/merge-merged; fires reliably even when
# GitHub's push event is swallowed by rate-limiting or
# is suppressed by concurrent workflow activity
# workflow_dispatch — manual trigger for debugging
#
# /rebase comments are handled exclusively by rebase.yml (single-PR).
# This workflow only handles the "rebase everything after develop advances" case.
#
# Only rebases PRs from the same repo (not forks - we can't push to those).
# Skips draft PRs and PRs that are already up to date.
# Dispatches Claude to resolve rebase conflicts automatically.
#
# Each eligible PR is rebased in a separate matrix job (up to 8 in parallel)
# so a large backlog doesn't block for O(n) sequential time.
on:
push:
branches: [develop]
pull_request:
types: [closed]
branches: [develop]
workflow_dispatch:
permissions:
contents: write
pull-requests: write
issues: write
actions: write
concurrency:
# Only one rebase run at a time. Cancel stale runs so the latest develop
# state wins. NOTE: only push/pull_request/workflow_dispatch events land here
# now — bot comments no longer poison this group.
group: auto-rebase-develop
cancel-in-progress: true
jobs:
# ── Job 1: Discover which PRs need rebasing ────────────────────────────────
discover:
name: Find PRs to rebase
# Only run when develop actually advanced (push/merge) or explicit dispatch.
# For pull_request events, only fire when the PR was actually merged.
if: |
(github.event_name == 'push' && github.ref == 'refs/heads/develop') ||
(github.event_name == 'pull_request' && github.event.pull_request.merged == true) ||
(github.event_name == 'workflow_dispatch')
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
prs: ${{ steps.list.outputs.prs }}
count: ${{ steps.list.outputs.count }}
steps:
- name: List eligible open PRs
id: list
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
run: |
echo "Develop was pushed to / PR merged. Looking for PRs to rebase..."
# Fetch all open non-draft PRs targeting develop from the same repo (not forks)
prs=$(gh api "repos/$REPO/pulls?base=develop&state=open&per_page=100" \
--jq '[.[] | select(
.draft == false and
.head.repo.full_name == .base.repo.full_name
) | {number: .number, branch: .head.ref}]')
count=$(echo "$prs" | jq length)
echo "Found $count eligible open PR(s) targeting develop."
# Write outputs — prs must be a JSON array string for fromJSON()
echo "prs=$prs" >> "$GITHUB_OUTPUT"
echo "count=$count" >> "$GITHUB_OUTPUT"
# ── Job 2: Rebase each PR (parallel matrix) ───────────────────────────────
rebase:
name: "Rebase PR #${{ matrix.pr.number }} (${{ matrix.pr.branch }})"
needs: discover
if: needs.discover.outputs.count != '0'
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false # One conflict shouldn't abort other PR rebases
max-parallel: 8 # Cap concurrency to avoid GitHub API rate-limits
matrix:
pr: ${{ fromJSON(needs.discover.outputs.prs) }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ github.token }}
- name: Configure git identity
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Fetch develop + PR branch in one round-trip
id: fetch
run: |
branch="${{ matrix.pr.branch }}"
if git fetch origin develop "$branch" 2>/dev/null; then
echo "fetched=true" >> "$GITHUB_OUTPUT"
else
echo " Cannot fetch '$branch' — branch may be deleted, skipping"
echo "fetched=false" >> "$GITHUB_OUTPUT"
fi
- name: Check if already up to date
id: check
if: steps.fetch.outputs.fetched == 'true'
run: |
branch="${{ matrix.pr.branch }}"
if git merge-base --is-ancestor origin/develop "origin/$branch" 2>/dev/null; then
echo "Already up to date — skipping"
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
- name: Rebase (with merge fallback on conflict)
if: steps.fetch.outputs.fetched == 'true' && steps.check.outputs.skip == 'false'
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
run: |
branch="${{ matrix.pr.branch }}"
pr_num="${{ matrix.pr.number }}"
# Attempt rebase first (linear history preferred).
# If rebase fails due to conflicts, fall back to a merge commit —
# this avoids the "freshly-made PR immediately has rebase conflict"
# problem that happens when multiple agent PRs touch overlapping files.
git checkout -B "$branch" "origin/$branch"
# Use git's built-in union merge driver for CHANGELOG so that concurrent
# agent PRs each adding entries never produce a real conflict — both sets
# of lines are kept (which is always the correct resolution for changelogs).
echo "CHANGELOG.md merge=union" >> .git/info/attributes
echo "CHANGELOG*.md merge=union" >> .git/info/attributes
# Fetch PR labels once — used in both push_and_retrigger() and the conflict path.
pr_labels=$(gh pr view "$pr_num" --repo "$REPO" \
--json labels --jq '[(.labels // [])[].name] | join(",")' \
2>/dev/null || echo "")
push_and_retrigger() {
if git push --force-with-lease origin "$branch"; then
# Re-trigger CI — GITHUB_TOKEN pushes don't fire pull_request/push events.
# Pass triggered_by=rebase so agent-validation skips the 30-min smoke build:
# a clean rebase replays the same PR commits, so compilation is unchanged.
gh workflow run agent-validation.yml --repo "$REPO" --ref "$branch" \
-f branch="$branch" -f pr_number="$pr_num" -f triggered_by="rebase" 2>/dev/null || true
# NOTE: do NOT re-trigger ai-review.yml here. A clean rebase replays the
# same PR commits onto develop — the diff is identical, so re-reviewing
# wastes runners and causes an N-PR cascade on every merge to develop.
# AI review is only needed when the PR's own content changes (new commits
# from the author or a fix-cycle push), which is handled by ai-review-trigger.yml
# on pull_request:synchronize events from non-GITHUB_TOKEN pushes.
# Re-trigger IPA build if PR has build-ipa label
if echo "$pr_labels" | grep -q "build-ipa"; then
echo " PR has build-ipa label — re-dispatching build.yml"
gh workflow run build.yml --repo "$REPO" --ref "$branch" \
-f pr_number="$pr_num" 2>/dev/null || true
fi
return 0
else
echo " Push rejected (branch protection?)"
return 1
fi
}
if git rebase origin/develop; then
echo "Rebase succeeded"
push_and_retrigger \
&& echo "Pushed successfully (rebase)" \
|| { echo "Push failed"; exit 1; }
else
# Rebase failed — abort and try a plain merge instead.
# A merge commit avoids the commit-replay conflict while still
# keeping the branch current with develop (GitHub's MERGEABLE check
# uses 3-way merge, so this matches what GitHub expects).
echo "Rebase conflict — falling back to merge"
git rebase --abort 2>/dev/null || true
git checkout -B "$branch" "origin/$branch"
if git merge --no-edit -m "chore: sync with develop" origin/develop 2>/dev/null; then
echo "Merge fallback succeeded"
push_and_retrigger \
&& echo "Pushed successfully (merge fallback)" \
|| { echo "Push failed"; exit 1; }
else
# Both rebase and merge failed — genuine conflict, dispatch the owning fixer.
git merge --abort 2>/dev/null || true
echo "Both rebase and merge failed — dispatching fixer workflow"
FOLLOWUP_WORKFLOW="claude-code.yml"
FOLLOWUP_AGENT="Claude"
if echo "$pr_labels" | grep -Eq '(^|,)cursor-work(,|$)'; then
FOLLOWUP_WORKFLOW="cursor-agent.yml"
FOLLOWUP_AGENT="Cursor"
fi
if gh workflow run "$FOLLOWUP_WORKFLOW" \
--repo "$REPO" \
-f pr_number="$pr_num" \
-f action="fix_rebase_conflict" 2>/dev/null; then
gh pr comment "$pr_num" --repo "$REPO" \
--body $'🤖 **Merge conflict detected** — '"${FOLLOWUP_AGENT}"$' has been dispatched to resolve the conflicts automatically.\n\nIf conflicts cannot be resolved cleanly, the workflow will post a comment explaining what needs manual attention.' 2>/dev/null || true
else
gh pr comment "$pr_num" --repo "$REPO" \
--body $'**Auto-sync conflict**\n\nThis PR could not be automatically synced with `develop` due to merge conflicts. Please resolve manually:\n\n```bash\ngit fetch origin\ngit checkout '"${branch}"'\ngit merge origin/develop\n# resolve conflicts, then:\ngit push\n```\n\nOr comment `/rebase` here to retry.' 2>/dev/null || true
fi
exit 1
fi
fi