fix: artwork backfill with live redraws, case skin filtering, CI recu… #667
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: 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 |