Skip to content

Commit ca61413

Browse files
authored
Simple merge queue (#22992)
1 parent 33c5ce2 commit ca61413

3 files changed

Lines changed: 140 additions & 16 deletions

File tree

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
#!/usr/bin/env node
2+
3+
import { getOctokit } from '@actions/github'
4+
const token = process.env.GITHUB_TOKEN
5+
const github = getOctokit(token)
6+
7+
// Mergeable status documentation here:
8+
// https://docs.github.com/en/graphql/reference/enums#mergestatestatus
9+
// https://docs.github.com/en/graphql/reference/enums#mergeablestate
10+
11+
/*
12+
This script gets a list of automerge-enabled PRs and sorts them
13+
by priority. The PRs with the skip-to-front-of-merge-queue label
14+
are prioritized first. The rest of the PRs are sorted by the date
15+
they were updated. This is basically a FIFO queue, while allowing
16+
writers the ability to skip the line when high-priority ships are
17+
needed but a freeze isn't necessary.
18+
*/
19+
20+
main()
21+
22+
async function main() {
23+
// Get a list of open PRs and order them from oldest to newest
24+
const query = `query ($first: Int, $after: String, $firstLabels: Int) {
25+
organization(login: "github") {
26+
repository(name: "docs-internal") {
27+
pullRequests(first: $first, after: $after, states: OPEN, orderBy: {field: UPDATED_AT, direction: ASC}) {
28+
edges{
29+
node {
30+
number
31+
url
32+
updatedAt
33+
mergeable
34+
mergeStateStatus
35+
autoMergeRequest {
36+
enabledBy {
37+
login
38+
}
39+
enabledAt
40+
}
41+
labels (first:$firstLabels){
42+
nodes {
43+
name
44+
}
45+
}
46+
}
47+
}
48+
pageInfo {
49+
hasNextPage
50+
endCursor
51+
}
52+
}
53+
}
54+
}
55+
}`
56+
57+
const queryVariables = {
58+
first: 100,
59+
after: null, // when pagination in null it will get first page
60+
firstLabels: 100,
61+
headers: {
62+
// required for the mergeStateStatus enum
63+
accept: 'application/vnd.github.merge-info-preview+json',
64+
},
65+
}
66+
let hasNextPage = true
67+
const autoMergeEnabledPRs = []
68+
69+
// we need to get all the paginated results in the case that
70+
// there are more than 100 PRs
71+
while (hasNextPage) {
72+
const graph = await github.graphql(query, queryVariables)
73+
const dataRoot = graph.organization.repository.pullRequests
74+
const pullRequests = dataRoot.edges
75+
// update pagination variables
76+
hasNextPage = dataRoot.pageInfo.hasNextPage
77+
// the endCursor is the start cursor for the next page
78+
queryVariables.after = dataRoot.pageInfo.endCursor
79+
80+
const filteredPrs = pullRequests
81+
// this simplifies the format received from the graphql query to
82+
// remove the unnecessary nested objects
83+
.map((pr) => {
84+
// make the labels object just an array of the label names
85+
const labelArray = pr.node.labels.nodes.map((label) => label.name)
86+
pr.node.labels = labelArray
87+
// return the pr object and ✂️ the node property
88+
return pr.node
89+
})
90+
.filter((pr) => pr.autoMergeRequest !== null)
91+
.filter((pr) => pr.mergeable === 'MERGEABLE')
92+
// filter out prs that don't have a calculated mergeable state yet
93+
.filter((pr) => pr.mergeStateStatus !== 'UNKNOWN')
94+
// filter out prs that still need a review, have merge conflicts,
95+
// or have failing ci tests
96+
.filter((pr) => pr.mergeStateStatus !== 'BLOCKED')
97+
// **NOTE**: In the future we may want to send slack message to initiators
98+
// of PRs with the following merge states because these can happen after
99+
// a PR is green and the automerge is enabled
100+
.filter((pr) => pr.mergeStateStatus !== 'DIRTY')
101+
.filter((pr) => pr.mergeStateStatus !== 'UNSTABLE')
102+
103+
autoMergeEnabledPRs.push(...filteredPrs)
104+
}
105+
106+
// Get the list of prs with the skip label so they can
107+
// be put at the beginning of the list
108+
const prioritizedPrList = autoMergeEnabledPRs
109+
.filter((pr) => pr.labels.includes('skip-to-front-of-merge-queue'))
110+
.concat(autoMergeEnabledPRs.filter((pr) => !pr.labels.includes('skip-to-front-of-merge-queue')))
111+
112+
const nextInQueue = prioritizedPrList.shift()
113+
// Update the branch for the next PR in the merge queue
114+
github.rest.pulls.updateBranch({
115+
owner: 'github',
116+
repo: 'docs-internal',
117+
pull_number: parseInt(nextInQueue.number),
118+
})
119+
120+
console.log(`⏱ Total PRs in the merge queue: ${prioritizedPrList.length + 1}`)
121+
console.log(`🚂 Updated branch for PR #${JSON.stringify(nextInQueue, null, 2)}`)
122+
console.log(`🚏 Next up in the queue: `)
123+
console.log(JSON.stringify(prioritizedPrList, null, 2))
124+
}

.github/allowed-actions.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ export default [
1717
'cschleiden/actions-linter@caffd707beda4fc6083926a3dff48444bc7c24aa', // uses github-actions-parser v0.23.0
1818
'dawidd6/action-delete-branch@47743101a121ad657031e6704086271ca81b1911', // v3.0.2
1919
'dawidd6/action-download-artifact@af92a8455a59214b7b932932f2662fdefbd78126', // v2.15.0
20-
'docker://chinthakagodawita/autoupdate-action:v1',
2120
'dorny/paths-filter@eb75a1edc117d3756a18ef89958ee59f9500ba58',
2221
'trilom/file-changes-action@a6ca26c14274c33b15e6499323aac178af06ad4b', // v1.2.4
2322
'github/codeql-action/analyze@v1',
Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
name: Autoupdate branch
22

3-
# **What it does**: Any pull requests with automerge will get main branch updates.
4-
# **Why we have it**: So we don't have to watch pull requests and click update branch 100x.
3+
# **What it does**: The next pull request in the merge queue will get its
4+
# branch updated with main. Only updating one branch ensures that pull requests
5+
# in the queue are merged sequentially.
6+
# **Why we have it**: So we don't have to watch pull requests and click
7+
# update branch 1000x.
58
# **Who does it impact**: Our health.
6-
79
#
8-
# This workflow checks all open PRs targeting `main` as their base branch and
9-
# will attempt to update them if they have automerge.
10-
# It is triggered when a `push` event occurs ON the `main` branch (e.g. a PR
11-
# was merged or a force-push was done).
10+
# The merge queue consists of any pull requests with automerge enabled and
11+
# are mergeable. There is a label that can be used to skip to the front of
12+
# the queue (`skip-to-front-of-merge-queue`).
1213
#
13-
# It should work on all PRs created from source branches within the repo itself
14-
# but is unlikely to work for PRs created from forked repos.
14+
# This workflow is triggered when a `push` event occurs ON the `main` branch
15+
# (e.g. a PR was merged or a force-push was done).
1516
#
16-
# It is still worthwhile to leave it enabled for the `docs` open source repo as
17-
# it should at least be running on `repo-sync` branch PRs.
17+
# This workflow runs on all PRs created from source branches within the
18+
# public and private docs repos but is won't work for PRs created from
19+
# forked repos.
1820
#
1921

2022
on:
@@ -28,8 +30,7 @@ jobs:
2830
name: autoupdate
2931
runs-on: ubuntu-latest
3032
steps:
31-
- uses: docker://chinthakagodawita/autoupdate-action:v1
33+
- name: Update next PR in queue
3234
env:
33-
GITHUB_TOKEN: ${{ secrets.OCTOMERGER_PAT_WITH_REPO_AND_WORKFLOW_SCOPE }}
34-
PR_FILTER: auto_merge
35-
MERGE_MSG: 'Autoupdate branch'
35+
GITHUB_TOKEN: ${{ secrets.DOCUBOT_REPO_PAT }}
36+
run: node .github/actions-scripts/update-merge-queue-branch.js

0 commit comments

Comments
 (0)