Skip to content

GitHub Merge Queue #9353

GitHub Merge Queue

GitHub Merge Queue #9353

# 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 }}