diff --git a/.github/actions-scripts/update-merge-queue-branch.js b/.github/actions-scripts/update-merge-queue-branch.js new file mode 100644 index 0000000000..3de0ba555f --- /dev/null +++ b/.github/actions-scripts/update-merge-queue-branch.js @@ -0,0 +1,124 @@ +#!/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. +*/ + +main() + +async function main() { + // Get a list of open PRs and order them from oldest to newest + const query = `query ($first: Int, $after: String, $firstLabels: Int) { + organization(login: "github") { + repository(name: "docs-internal") { + 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 + } + } + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + }` + + const queryVariables = { + 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') + + 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 + .filter((pr) => pr.labels.includes('skip-to-front-of-merge-queue')) + .concat(autoMergeEnabledPRs.filter((pr) => !pr.labels.includes('skip-to-front-of-merge-queue'))) + + const nextInQueue = prioritizedPrList.shift() + // Update the branch for the next PR in the merge queue + github.rest.pulls.updateBranch({ + owner: 'github', + repo: 'docs-internal', + pull_number: parseInt(nextInQueue.number), + }) + + console.log(`⏱ Total PRs in the merge queue: ${prioritizedPrList.length + 1}`) + console.log(`🚂 Updated branch for PR #${JSON.stringify(nextInQueue, null, 2)}`) + console.log(`🚏 Next up in the queue: `) + console.log(JSON.stringify(prioritizedPrList, null, 2)) +} diff --git a/.github/allowed-actions.js b/.github/allowed-actions.js index 210021ab13..bb0d4a7904 100644 --- a/.github/allowed-actions.js +++ b/.github/allowed-actions.js @@ -17,7 +17,6 @@ export default [ 'cschleiden/actions-linter@caffd707beda4fc6083926a3dff48444bc7c24aa', // uses github-actions-parser v0.23.0 'dawidd6/action-delete-branch@47743101a121ad657031e6704086271ca81b1911', // v3.0.2 'dawidd6/action-download-artifact@af92a8455a59214b7b932932f2662fdefbd78126', // v2.15.0 - 'docker://chinthakagodawita/autoupdate-action:v1', 'dorny/paths-filter@eb75a1edc117d3756a18ef89958ee59f9500ba58', 'trilom/file-changes-action@a6ca26c14274c33b15e6499323aac178af06ad4b', // v1.2.4 'github/codeql-action/analyze@v1', diff --git a/.github/workflows/autoupdate-branch.yml b/.github/workflows/autoupdate-branch.yml index 4fdfb0a2fc..ebed40bb5d 100644 --- a/.github/workflows/autoupdate-branch.yml +++ b/.github/workflows/autoupdate-branch.yml @@ -1,20 +1,22 @@ name: Autoupdate branch -# **What it does**: Any pull requests with automerge will get main branch updates. -# **Why we have it**: So we don't have to watch pull requests and click update branch 100x. +# **What it does**: The next pull request in the merge queue will get its +# branch updated with main. Only updating one branch ensures that pull requests +# in the queue are merged sequentially. +# **Why we have it**: So we don't have to watch pull requests and click +# update branch 1000x. # **Who does it impact**: Our health. - # -# This workflow checks all open PRs targeting `main` as their base branch and -# will attempt to update them if they have automerge. -# It is triggered when a `push` event occurs ON the `main` branch (e.g. a PR -# was merged or a force-push was done). +# The merge queue consists of any pull requests with automerge enabled and +# are mergeable. There is a label that can be used to skip to the front of +# the queue (`skip-to-front-of-merge-queue`). # -# It should work on all PRs created from source branches within the repo itself -# but is unlikely to work for PRs created from forked repos. +# This workflow is triggered when a `push` event occurs ON the `main` branch +# (e.g. a PR was merged or a force-push was done). # -# It is still worthwhile to leave it enabled for the `docs` open source repo as -# it should at least be running on `repo-sync` branch PRs. +# This workflow runs on all PRs created from source branches within the +# public and private docs repos but is won't work for PRs created from +# forked repos. # on: @@ -28,8 +30,7 @@ jobs: name: autoupdate runs-on: ubuntu-latest steps: - - uses: docker://chinthakagodawita/autoupdate-action:v1 + - name: Update next PR in queue env: - GITHUB_TOKEN: ${{ secrets.OCTOMERGER_PAT_WITH_REPO_AND_WORKFLOW_SCOPE }} - PR_FILTER: auto_merge - MERGE_MSG: 'Autoupdate branch' + GITHUB_TOKEN: ${{ secrets.DOCUBOT_REPO_PAT }} + run: node .github/actions-scripts/update-merge-queue-branch.js