Unverified Commit 12c1f025 authored by Wolfgang Walther's avatar Wolfgang Walther Committed by GitHub
Browse files

ci/github-script/merge: improve merge operation and error messages (#458412)

parents 91d7dd8a 1e6124a5
Loading
Loading
Loading
Loading
+43 −0
Original line number Diff line number Diff line
@@ -96,6 +96,47 @@ module.exports = async ({ github, context, core, dry }) => {
    return maintainerMaps[branch]
  }

  // Caching the list of team members saves API requests when running the bot on the schedule and
  // processing many PRs at once.
  const members = {}
  function getTeamMembers(team_slug) {
    if (context.eventName === 'pull_request') {
      // We have no chance of getting a token in the pull_request context with the right
      // permissions to access the members endpoint below. Thus, we're pretending to have
      // no members. This is OK; because this is only for the Test workflow, not for
      // real use.
      return []
    }

    if (!members[team_slug]) {
      members[team_slug] = github.paginate(github.rest.teams.listMembersInOrg, {
        org: context.repo.owner,
        team_slug,
        per_page: 100,
      })
    }

    return members[team_slug]
  }

  // Caching users saves API requests when running the bot on the schedule and processing
  // many PRs at once. It also helps to encapsulate the special logic we need, because
  // actions/github doesn't support that endpoint fully, yet.
  const users = {}
  function getUser(id) {
    if (!users[id]) {
      users[id] = github
        .request({
          method: 'GET',
          url: '/user/{id}',
          id,
        })
        .then((resp) => resp.data)
    }

    return users[id]
  }

  async function handlePullRequest({ item, stats, events }) {
    const log = (k, v) => core.info(`PR #${item.number} - ${k}: ${v}`)

@@ -121,6 +162,8 @@ module.exports = async ({ github, context, core, dry }) => {
      pull_request,
      events,
      maintainers,
      getTeamMembers,
      getUser,
    })

    // When the same change has already been merged to the target branch, a PR will still be
+47 −58
Original line number Diff line number Diff line
@@ -80,6 +80,7 @@ function runChecklist({

  return {
    checklist,
    eligible,
    result,
  }
}
@@ -109,10 +110,6 @@ async function handleMergeComment({ github, body, node_id, reaction }) {
  )
}

// Caching the list of team members saves API requests when running the bot on the schedule and
// processing many PRs at once.
const members = {}

async function handleMerge({
  github,
  context,
@@ -122,31 +119,14 @@ async function handleMerge({
  pull_request,
  events,
  maintainers,
  getTeamMembers,
  getUser,
}) {
  const pull_number = pull_request.number

  function getTeamMembers(team_slug) {
    if (context.eventName === 'pull_request') {
      // We have no chance of getting a token in the pull_request context with the right
      // permissions to access the members endpoint below. Thus, we're pretending to have
      // no members. This is OK; because this is only for the Test workflow, not for
      // real use.
      return new Set()
    }

    if (!members[team_slug]) {
      members[team_slug] = github
        .paginate(github.rest.teams.listMembersInOrg, {
          org: context.repo.owner,
          team_slug,
          per_page: 100,
        })
        .then((members) => new Set(members.map(({ id }) => id)))
    }

    return members[team_slug]
  }
  const committers = await getTeamMembers('nixpkgs-committers')
  const committers = new Set(
    (await getTeamMembers('nixpkgs-committers')).map(({ id }) => id),
  )

  const files = await github.paginate(github.rest.pulls.listFiles, {
    ...context.repo,
@@ -168,6 +148,7 @@ async function handleMerge({
      // Ignore comments where the user has been deleted already.
      user &&
      // Ignore comments which had already been responded to by the bot.
      (dry ||
        !events.some(
          ({ event, body }) =>
            ['commented'].includes(event) &&
@@ -175,7 +156,7 @@ async function handleMerge({
            // We'll just assume that nobody creates comments with this marker on purpose.
            // Additionally checking the author is quite annoying for local debugging.
            body.match(new RegExp(`^<!-- comment: ${node_id} -->$`, 'm')),
      ),
        )),
  )

  async function merge() {
@@ -184,49 +165,45 @@ async function handleMerge({
      return 'Merge completed (dry)'
    }

    // Using GraphQL's enablePullRequestAutoMerge mutation instead of the REST
    // /merge endpoint, because the latter doesn't work with Merge Queues.
    // This mutation works both with and without Merge Queues.
    // It doesn't work when there are no required status checks for the target branch.
    // All development branches have these enabled, so this is a non-issue.
    // Using GraphQL mutations instead of the REST /merge endpoint, because the latter
    // doesn't work with Merge Queues. We now have merge queues enabled on all development
    // branches, so we don't need a fallback for regular merges.
    try {
      await github.graphql(
      const resp = await github.graphql(
        `mutation($node_id: ID!, $sha: GitObjectID) {
          enablePullRequestAutoMerge(input: {
          enqueuePullRequest(input: {
            expectedHeadOid: $sha,
            pullRequestId: $node_id
          })
          { clientMutationId }
          {
            clientMutationId,
            mergeQueueEntry { mergeQueue { url } }
          }
        }`,
        { node_id: pull_request.node_id, sha: pull_request.head.sha },
      )
      return 'Enabled Auto Merge'
      return `[Queued](${resp.enqueuePullRequest.mergeQueueEntry.mergeQueue.url}) for merge`
    } catch (e) {
      log('Auto Merge failed', e.response.errors[0].message)
      log('Enqueing failed', e.response.errors[0].message)
    }

    // TODO: Observe whether the below is true and whether manual enqueue is actually needed.
    // Auto-merge doesn't work if the target branch has already run all CI, in which
    // case the PR must be enqueued explicitly.
    // We now have merge queues enabled on all development branches, thus don't need a
    // fallback after this.
    // If required status checks are not satisfied, yet, the above will fail. In this case
    // we can enable auto-merge. We could also only use auto-merge, but this often gets
    // stuck for no apparent reason.
    try {
      const resp = await github.graphql(
      await github.graphql(
        `mutation($node_id: ID!, $sha: GitObjectID) {
          enqueuePullRequest(input: {
          enablePullRequestAutoMerge(input: {
            expectedHeadOid: $sha,
            pullRequestId: $node_id
          })
          {
            clientMutationId,
            mergeQueueEntry { mergeQueue { url } }
          }
          { clientMutationId }
        }`,
        { node_id: pull_request.node_id, sha: pull_request.head.sha },
      )
      return `[Queued](${resp.enqueuePullRequest.mergeQueueEntry.mergeQueue.url}) for merge`
      return 'Enabled Auto Merge'
    } catch (e) {
      log('Enqueing failed', e.response.errors[0].message)
      log('Auto Merge failed', e.response.errors[0].message)
      throw new Error(e.response.errors[0].message)
    }
  }
@@ -265,7 +242,7 @@ async function handleMerge({
      }
    }

    const { result, checklist } = runChecklist({
    const { result, eligible, checklist } = runChecklist({
      committers,
      events,
      files,
@@ -295,6 +272,18 @@ async function handleMerge({
      '',
    ]

    if (eligible.size > 0 && !eligible.has(comment.user.id)) {
      const users = await Promise.all(
        Array.from(eligible, async (id) => (await getUser(id)).login),
      )
      body.push(
        '> [!TIP]',
        '> Maintainers eligible to merge are:',
        ...users.map((login) => `> - ${login}`),
        '',
      )
    }

    if (result) {
      await react('ROCKET')
      try {