GitHub Merge Queue #9359
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
| # Requires repo secret: PERSONAL_ACCESS_TOKEN with permissions: | |
| # Pull Requests: read and write | |
| # Workflow: read and write | |
| # This effectively creates a merge queue out of all PRs marked as auto-merge, but merge queues are | |
| # a GitHub feature that are only available to organizations or those on Enterprise Cloud right now. | |
| # https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/managing-a-merge-queue | |
| name: GitHub Merge Queue | |
| on: | |
| # A PR merge has occurred, or a PR is no longer eligible for the queue; move on to the next PR | |
| # Tradeoff: `pull_request_target` would allow this workflow to work with forked repositories, | |
| # but it's easy to get security wrong with `pull_request_target`. This workflow references | |
| # a personal access token, so we should avoid `pull_request_target`. | |
| pull_request: | |
| types: | |
| - closed | |
| - converted_to_draft | |
| - auto_merge_disabled | |
| # Jiggle the handle every hour just in case | |
| schedule: | |
| - cron: '0 * * * *' | |
| workflow_dispatch: | |
| concurrency: | |
| group: ${{ github.workflow }} | |
| cancel-in-progress: false # queue | |
| env: | |
| renovate_rebase_label: 'renovate/rebase' | |
| jobs: | |
| # Update the oldest auto-merge PR that's out-of-date with the base branch | |
| update-pr-branch: | |
| permissions: {} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| # Full history so the local merge has a valid merge base | |
| fetch-depth: 0 | |
| # Token needed to trigger Actions on push | |
| token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} | |
| - run: | | |
| set -x | |
| pr_search="sort:created-asc" | |
| # Wait for the merge state status to be updated | |
| while true; do | |
| pr_list="$(gh pr list --search "${pr_search}" --json number,mergeable --jq '.[] | [.number, .mergeable] | @csv')" | |
| # No PRs found, so none can be in an 'UNKNOWN' state | |
| [[ "${pr_list}" == "" ]] && break | |
| # At least one PR is not in an 'UNKNOWN' state | |
| echo "${pr_list}" | grep -v "UNKNOWN" && break | |
| echo "$(echo "${pr_list}" | wc -l | awk '{print $1}') PRs in an 'UNKNOWN' mergeable state, waiting ..." | |
| sleep 5 | |
| done | |
| echo "PRs found:" | |
| gh pr list --search "${pr_search}" --json number,title,mergeable,mergeStateStatus,autoMergeRequest | jq | |
| # Eligible: auto-merge, non-draft PRs that are either mergeable+BEHIND, or CONFLICTING | |
| eligible_prs="$(gh pr list --search "${pr_search}" --json number,isDraft,mergeable,mergeStateStatus,autoMergeRequest,headRefName,baseRefName,headRepository --jq ' | |
| .[] | |
| | select(.isDraft==false and .autoMergeRequest!=null) | |
| | select((.mergeable=="MERGEABLE" and .mergeStateStatus=="BEHIND") or .mergeable=="CONFLICTING") | |
| | [.number, .mergeable, .headRefName, .baseRefName, .headRepository.nameWithOwner] | @tsv')" | |
| if [[ "${eligible_prs}" == "" ]]; then | |
| echo "No eligible PRs found" | |
| exit 0 | |
| fi | |
| echo "$(echo "${eligible_prs}" | wc -l | awk '{print $1}') eligible PRs found" | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| while IFS=$'\t' read -r number mergeable head_ref base_ref head_repo; do | |
| [[ -z "${number}" ]] && continue | |
| if [[ "${mergeable}" == "MERGEABLE" ]]; then | |
| echo "Updating PR #${number}" | |
| gh pr update-branch "${number}" && break | |
| continue | |
| fi | |
| # CONFLICTING: attempt a local merge of the base branch into the PR branch. | |
| # The head branch may live in a fork, so fetch/push its own repository (which equals | |
| # `origin` for same-repo PRs). The base branch always lives in `origin`. | |
| head_remote="https://github.com/${head_repo}.git" | |
| echo "Attempting to resolve conflicts in PR #${number} (${head_repo}:${head_ref} <- ${base_ref})" | |
| git fetch origin "${base_ref}" | |
| git fetch "${head_remote}" "${head_ref}" | |
| git checkout -B "pr-${number}" FETCH_HEAD | |
| if git merge --no-edit "origin/${base_ref}"; then | |
| echo "PR #${number} merged cleanly, pushing" | |
| git push "${head_remote}" "HEAD:${head_ref}" && break | |
| continue | |
| fi | |
| conflicted="$(git diff --name-only --diff-filter=U)" | |
| all_prebuilds=true | |
| while IFS= read -r file; do | |
| [[ -z "${file}" ]] && continue | |
| case "${file}" in | |
| packages/*/addon-*/prebuilds/*/*.node) ;; | |
| *) all_prebuilds=false ;; | |
| esac | |
| done <<< "${conflicted}" | |
| if [[ "${all_prebuilds}" != "true" ]]; then | |
| echo "PR #${number} has conflicts outside of prebuilt binaries, skipping" | |
| git merge --abort | |
| continue | |
| fi | |
| echo "Resolving prebuilt-binary conflicts in PR #${number} by keeping the PR's own versions" | |
| while IFS= read -r file; do | |
| [[ -z "${file}" ]] && continue | |
| git checkout --ours -- "${file}" | |
| git add -- "${file}" | |
| done <<< "${conflicted}" | |
| git commit --no-edit | |
| git push "${head_remote}" "HEAD:${head_ref}" && break | |
| done <<< "${eligible_prs}" | |
| env: | |
| # GitHub won't run workflows off of code commits+pushes from the `${{github.token}}` token | |
| GH_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} | |
| GH_REPO: ${{ github.repository }} | |
| # Add a label to the oldest Renovate auto-merge PR that has conflicts, such that Renovate rebases it | |
| renovate-label-adder: | |
| permissions: | |
| pull-requests: write # gh pr edit | |
| issues: write # gh pr edit | |
| runs-on: ubuntu-latest | |
| steps: | |
| - run: | | |
| author="app/renovate" | |
| pr_search="author:${author} sort:created-asc -label:${{ env.renovate_rebase_label }}" | |
| # Wait for the merge state status to be updated | |
| while true; do | |
| pr_list="$(gh pr list --search "${pr_search}" --json number,mergeable --jq '.[] | [.number, .mergeable] | @csv')" | |
| # No PRs found, so none can be in an 'UNKNOWN' state | |
| [[ "${pr_list}" == "" ]] && break | |
| # At least one PR is not in an 'UNKNOWN' state | |
| echo "${pr_list}" | grep -v "UNKNOWN" && break | |
| echo "$(echo "${pr_list}" | wc -l | awk '{print $1}') PRs in an 'UNKNOWN' mergeable state, waiting ..." | |
| sleep 5 | |
| done | |
| echo "Renovate PRs found:" | |
| gh pr list --search "${pr_search}" --json number,title,mergeable,mergeStateStatus,autoMergeRequest | jq | |
| eligible_prs="$(gh pr list --search "${pr_search}" --json number,isDraft,mergeable,autoMergeRequest --jq '.[] | select(.isDraft==false and .mergeable=="CONFLICTING" and .autoMergeRequest!=null) | .number')" | |
| if [[ "${eligible_prs}" == "" ]]; then | |
| echo "No eligible PRs found" | |
| exit 0 | |
| fi | |
| echo "$(echo "${eligible_prs}" | wc -l | awk '{print $1}') eligible PRs found: $(echo "${eligible_prs}" | awk '{print $1}' | awk 'ORS=", "' | sed 's/, *$//')" | |
| while read -r number; do | |
| echo "Adding label '${{ env.renovate_rebase_label }}' to PR #${number}" | |
| gh pr edit "${number}" --add-label "${{ env.renovate_rebase_label }}" && break | |
| done <<< "${eligible_prs}" | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| GH_REPO: ${{ github.repository }} | |
| renovate-label-remover: | |
| if: ${{ github.event_name == 'pull_request' }} | |
| permissions: | |
| pull-requests: write # gh pr edit | |
| issues: write # gh pr edit | |
| runs-on: ubuntu-latest | |
| steps: | |
| - run: | | |
| pr_number="${{ github.event.pull_request.number }}" | |
| label="${{ env.renovate_rebase_label }}" | |
| if gh pr view "${pr_number}" --json labels --jq '.labels[].name' | grep -qFx "${label}"; then | |
| echo "Removing label '${label}' from PR #${pr_number}" | |
| gh pr edit "${pr_number}" --remove-label "${label}" | |
| else | |
| echo "PR #${pr_number} does not have label '${label}', skipping" | |
| fi | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| GH_REPO: ${{ github.repository }} |