-
-
Notifications
You must be signed in to change notification settings - Fork 43
190 lines (167 loc) · 8.34 KB
/
Copy pathgh-merge-queue.yml
File metadata and controls
190 lines (167 loc) · 8.34 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# 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 }}