Repository: electron/electron
Pull request: ci: reapply patches if PR base branch updates them

#### Description of Change

<!--
Thank you for your Pull Request. Please provide a description above and review
the requirements below.

Contributors guide: https://github.com/electron/electron/blob/main/CONTRIBUTING.md
-->

We're bit from time to time by merging multiple PRs which have patches changes that conflict with each other once they're merged. This has bit us three times in the past week or so, and historically bites us multiple times a year, if not more often:
* #49399
* #49498
* #49513

In an ideal world we'd have a merge queue that ran CI fully before merging a given PR, but given our long CI times, this isn't tenable.

This PR attempts to solve the particular issue of patch conflicts by adding a new `apply-patches` job which will run for any PR which touches `patches/**` or `DEPS` and rebases onto the base branch before running `./src/electron/.github/actions/checkout`, which will do a sync and apply the patches in the PR. The second part of the puzzle is the new `rerun-apply-patches.yml` workflow which runs whenever `patches/**` or `DEPS` is touched on one of our releases branches and triggers a re-run of the `apply-patches` job on any PR targeting that base branch. Since `apply-patches` always rebases onto the latest base branch, this should catch any patch conflicts.

There's still a small window where this check won't save us if two PRs are merged simultaneously since the GHA workflow won't kick off fast enough, but we have our existing mechanisms for catching that (the "Audit CI on Branches" workflow) so this can only improve the situation, even if it's not perfect.

I had some help from Claude writing `rerun-apply-patches.yml`, but it's difficult to test E2E so I think we'll just do a fast follow-up if needed.

#### Checklist
<!-- Remove items that do not apply. For completed items, change [ ] to [x]. -->

- [x] PR description included

#### Release Notes

Notes: none <!-- Please add a one-line description for app developers to read in the release notes, or 'none' if no notes relevant to app developers. Examples and help on special cases: https://github.com/electron/clerk/blob/main/README.md#examples -->


Changed files:
filename: .github/workflows/build.yml
patch: @@ -57,6 +57,7 @@ jobs:
         src: ${{ steps.filter.outputs.src }}
         build-image-sha: ${{ steps.set-output.outputs.build-image-sha }}
         docs-only: ${{ steps.set-output.outputs.docs-only }}
+        has-patches: ${{ steps.filter.outputs.patches }}
     steps:
     - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
       with:
@@ -73,6 +74,9 @@ jobs:
             - CODE_OF_CONDUCT.md
           src:
             - '!docs/**'
+          patches:
+            - DEPS
+            - 'patches/**'
     - name: Set Outputs for Build Image SHA & Docs Only
       id: set-output
       run: |
@@ -105,6 +109,41 @@ jobs:
       container: '{"image":"ghcr.io/electron/build:${{ needs.checkout-linux.outputs.build-image-sha }}","options":"--user root","volumes":["/mnt/cross-instance-cache:/mnt/cross-instance-cache"]}'
     secrets: inherit
 
+  # Apply Patches Job
+  apply-patches:
+    needs: setup
+    if: ${{ needs.setup.outputs.has-patches == 'true' }}
+    runs-on: electron-arc-centralus-linux-amd64-32core
+    permissions:
+      contents: read
+    container:
+      image: ghcr.io/electron/build:${{ needs.setup.outputs.build-image-sha }}
+      options: --user root
+      volumes:
+        - /mnt/cross-instance-cache:/mnt/cross-instance-cache
+        - /var/run/sas:/var/run/sas
+    env:
+      CHROMIUM_GIT_COOKIE: ${{ secrets.CHROMIUM_GIT_COOKIE }}
+      GCLIENT_EXTRA_ARGS: '--custom-var=checkout_arm=True --custom-var=checkout_arm64=True'
+    steps:
+    - name: Checkout Electron
+      uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
+      with:
+        path: src/electron
+        fetch-depth: 0
+        ref: ${{ github.event.pull_request.head.sha }}
+    - name: Rebase onto Base Branch
+      working-directory: src/electron
+      run: |
+        git config user.email "electron@github.com"
+        git config user.name "Electron Bot"
+        git fetch origin ${{ github.event.pull_request.base.ref }}
+        git rebase origin/${{ github.event.pull_request.base.ref }}
+    - name: Checkout & Sync & Save
+      uses: ./src/electron/.github/actions/checkout
+      with:
+        target-platform: linux
+
   # Checkout Jobs
   checkout-macos:
     needs: setup

filename: .github/workflows/rerun-apply-patches.yml
patch: @@ -0,0 +1,69 @@
+name: Rerun PR Apply Patches
+
+on:
+  push:
+    branches:
+      - main
+      - '[1-9][0-9]-x-y'
+    paths:
+      - 'DEPS'
+      - 'patches/**'
+
+permissions: {}
+
+jobs:
+  rerun-apply-patches:
+    runs-on: ubuntu-latest
+    permissions:
+      contents: read
+      actions: write
+      pull-requests: read
+    steps:
+      - name: Find PRs and Rerun Apply Patches
+        env:
+          GH_REPO: ${{ github.repository }}
+          GH_TOKEN: ${{ github.token }}
+        run: |
+          BRANCH="${GITHUB_REF#refs/heads/}"
+
+          # Find all open PRs targeting this branch
+          PRS=$(gh pr list --base "$BRANCH" --state open --limit 250 --json number)
+
+          echo "$PRS" | jq -c '.[]' | while read -r pr; do
+            PR_NUMBER=$(echo "$pr" | jq -r '.number')
+            echo "Processing PR #${PR_NUMBER}"
+
+            # Find the apply-patches job check for this PR
+            CHECK=$(gh pr checks "$PR_NUMBER" --json link,name,state,workflow --jq '[.[] | select(.workflow == "Build" and .name == "apply-patches")] | first')
+
+            if [ -z "$CHECK" ] || [ "$CHECK" = "null" ]; then
+              echo "  No apply-patches job found for PR #${PR_NUMBER}"
+              continue
+            fi
+
+            STATE=$(echo "$CHECK" | jq -r '.state')
+            if [ "$STATE" = "SKIPPED" ]; then
+              echo "  apply-patches job was skipped for PR #${PR_NUMBER} (no patches)"
+              continue
+            fi
+
+            LINK=$(echo "$CHECK" | jq -r '.link')
+
+            # Extract the run ID from the link (format: .../runs/RUN_ID/job/JOB_ID)
+            RUN_ID=$(echo "$LINK" | grep -oE 'runs/[0-9]+' | cut -d'/' -f2)
+
+            if [ -z "$RUN_ID" ]; then
+              echo "  Could not extract run ID from link: ${LINK}"
+              continue
+            fi
+
+            # Get the job database ID for the apply-patches job
+            JOB_ID=$(gh run view "$RUN_ID" --json jobs --jq '.jobs[] | select(.name == "apply-patches") | .databaseId')
+
+            if [ -z "$JOB_ID" ]; then
+              echo "  Could not find apply-patches job ID for run: ${RUN_ID}"
+              continue
+            fi
+
+            gh run rerun "$RUN_ID" --job "$JOB_ID"
+          done