159 lines
5.3 KiB
JavaScript
159 lines
5.3 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { getOctokit } from '@actions/github'
|
|
const token = process.env.GITHUB_TOKEN
|
|
const github = getOctokit(token)
|
|
|
|
// Mergeable status documentation here:
|
|
// https://docs.github.com/en/graphql/reference/enums#mergestatestatus
|
|
// https://docs.github.com/en/graphql/reference/enums#mergeablestate
|
|
|
|
/*
|
|
This script gets a list of automerge-enabled PRs and sorts them
|
|
by priority. The PRs with the skip-to-front-of-merge-queue label
|
|
are prioritized first. The rest of the PRs are sorted by the date
|
|
they were updated. This is basically a FIFO queue, while allowing
|
|
writers the ability to skip the line when high-priority ships are
|
|
needed but a freeze isn't necessary.
|
|
*/
|
|
|
|
const DRY_RUN = Boolean(JSON.parse(process.env.DRY_RUN || 'false'))
|
|
|
|
main()
|
|
|
|
async function main() {
|
|
const [org, repo] = process.env.GITHUB_REPOSITORY.split('/')
|
|
if (!org || !repo) {
|
|
throw new Error('GITHUB_REPOSITORY environment variable not set')
|
|
}
|
|
// Get a list of open PRs and order them from oldest to newest
|
|
const query = `query ($first: Int, $after: String, $firstLabels: Int, $repo: String!, $org: String!) {
|
|
organization(login: $org) {
|
|
repository(name: $repo) {
|
|
pullRequests(first: $first, after: $after, states: OPEN, orderBy: {field: UPDATED_AT, direction: ASC}) {
|
|
edges{
|
|
node {
|
|
number
|
|
url
|
|
updatedAt
|
|
mergeable
|
|
mergeStateStatus
|
|
autoMergeRequest {
|
|
enabledBy {
|
|
login
|
|
}
|
|
enabledAt
|
|
}
|
|
labels (first:$firstLabels){
|
|
nodes {
|
|
name
|
|
}
|
|
}
|
|
commits(last: 1) {
|
|
nodes {
|
|
commit {
|
|
statusCheckRollup {
|
|
state
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
pageInfo {
|
|
hasNextPage
|
|
endCursor
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}`
|
|
|
|
const queryVariables = {
|
|
repo,
|
|
org,
|
|
first: 100,
|
|
after: null, // when pagination in null it will get first page
|
|
firstLabels: 100,
|
|
headers: {
|
|
// required for the mergeStateStatus enum
|
|
accept: 'application/vnd.github.merge-info-preview+json',
|
|
},
|
|
}
|
|
let hasNextPage = true
|
|
const autoMergeEnabledPRs = []
|
|
|
|
// we need to get all the paginated results in the case that
|
|
// there are more than 100 PRs
|
|
while (hasNextPage) {
|
|
const graph = await github.graphql(query, queryVariables)
|
|
const dataRoot = graph.organization.repository.pullRequests
|
|
const pullRequests = dataRoot.edges
|
|
// update pagination variables
|
|
hasNextPage = dataRoot.pageInfo.hasNextPage
|
|
// the endCursor is the start cursor for the next page
|
|
queryVariables.after = dataRoot.pageInfo.endCursor
|
|
|
|
const filteredPrs = pullRequests
|
|
// this simplifies the format received from the graphql query to
|
|
// remove the unnecessary nested objects
|
|
.map((pr) => {
|
|
// make the labels object just an array of the label names
|
|
const labelArray = pr.node.labels.nodes.map((label) => label.name)
|
|
pr.node.labels = labelArray
|
|
// return the pr object and ✂️ the node property
|
|
return pr.node
|
|
})
|
|
.filter((pr) => pr.autoMergeRequest !== null)
|
|
.filter((pr) => pr.mergeable === 'MERGEABLE')
|
|
// filter out prs that don't have a calculated mergeable state yet
|
|
.filter((pr) => pr.mergeStateStatus !== 'UNKNOWN')
|
|
// filter out prs that still need a review, have merge conflicts,
|
|
// or have failing ci tests
|
|
.filter((pr) => pr.mergeStateStatus !== 'BLOCKED')
|
|
// **NOTE**: In the future we may want to send slack message to initiators
|
|
// of PRs with the following merge states because these can happen after
|
|
// a PR is green and the automerge is enabled
|
|
.filter((pr) => pr.mergeStateStatus !== 'DIRTY')
|
|
.filter((pr) => pr.mergeStateStatus !== 'UNSTABLE')
|
|
.filter((pr) => {
|
|
const nodes = pr.commits.nodes
|
|
if (!nodes || !nodes.length) {
|
|
// If it has no commits, why is it even here? Anyway, skip it.
|
|
return false
|
|
}
|
|
return nodes[0].commit.statusCheckRollup.state !== 'FAILURE'
|
|
})
|
|
|
|
autoMergeEnabledPRs.push(...filteredPrs)
|
|
}
|
|
|
|
// Get the list of prs with the skip label so they can
|
|
// be put at the beginning of the list
|
|
const prioritizedPrList = autoMergeEnabledPRs.sort(
|
|
(a, b) =>
|
|
Number(b.labels.includes('skip-to-front-of-merge-queue')) -
|
|
Number(a.labels.includes('skip-to-front-of-merge-queue'))
|
|
)
|
|
|
|
if (prioritizedPrList.length) {
|
|
const nextInQueue = prioritizedPrList.shift()
|
|
// Update the branch for the next PR in the merge queue
|
|
if (DRY_RUN) {
|
|
console.log('DRY RUN! But *would* update on next-in-queue')
|
|
} else {
|
|
github.rest.pulls.updateBranch({
|
|
owner: org,
|
|
repo,
|
|
pull_number: nextInQueue.number,
|
|
})
|
|
}
|
|
console.log(`⏱ Total PRs in the merge queue: ${prioritizedPrList.length + 1}`)
|
|
console.log(`🚂 Updated branch for PR #${JSON.stringify(nextInQueue, null, 2)}`)
|
|
}
|
|
|
|
prioritizedPrList.length
|
|
? console.log(`🚏 Next up in the queue: \n ${JSON.stringify(prioritizedPrList, null, 2)}`)
|
|
: console.log(`⚡ The merge queue is empty`)
|
|
}
|