3

I use the multibranch plugin to scan new pull requests, and I need to limit my builds to approved pull requests only. I filter the approved pull requests after they are scanned, however the repository scanner can only tell whether a pull request has new commits.

I tried the the PR comment build plugin and added the Trigger build on pull request review property to the github branch source to no avail - it seems that adding this property doesn't have any effect on how the scanner processes pull requests.

Can I tell the repository scanner to trigger a build on new reviews? Is there any other way to build pull requests only after approval?

Thanks!

towel
  • 2,094
  • 1
  • 20
  • 30

1 Answers1

2

I had to accept that there was no way to make the branch scan consider non-git objects (such as github reviews).

I ended up making a pipeline that kept a list of all the yet-to-be-approved pull requests (using the github API), and once a pull request was approved it would trigger a build on it.

It feels hacky, but unfortunately that was the only way I could think of to build only on approval... Important note: this solution requires an existing multibranch job to work with. So this is what I did:

First query for the existing pull requests and their status (install the httpRequest plugin):

// Send a query to github and get the response JSON
def githubQuery(Map args = [:]) {
  def formattedQuery = args.query.replaceAll('\n', ' ').replaceAll('"', '\\\\"')
  def response = httpRequest(
    authentication: args.auth,
    httpMode: 'POST',
    requestBody: """{ "query": "${formattedQuery}" }""",
    url: "https://api.github.com/graphql"
  )
  (response.status == 200) ? readJSON(text: response.content) : [:]
}

def getPRStatus(Map args = [:]) {
  // Build the query to get all open pull requests and their status
  def query = """query {
    organization(login: "${args.organization}") {
      repositories(first: 30) {
        nodes {
          name
          pullRequests(first: 100, states: [OPEN]) {
            nodes {
              number
              state
              reviews(first: 10, states: [APPROVED]) {
                totalCount
              }
            }
          }
        }
      }
    }
  }"""
  def response = githubQuery(args + [query: query])
  def repositories = response?.data.organization.repositories.nodes
  // Organize the pull requests into approved and unapproved groups
  repositories?.collectEntries { repo ->
    // Take out draft pull requests
    def prs = repo.pullRequests.nodes.findAll { it.state != "DRAFT" }
    def repoPrs = [
      unapproved: prs.findAll { it.reviews.totalCount == 0 },
      approved: prs.findAll { it.reviews.totalCount > 0 }
    ].collectEntries { category, categoryPrs ->
      [ category, categoryPrs.collect { it.number } ]
    }
    [ repo.name, repoPrs ]
  }
}

Then compare each pull request's status to its status from the previous poll, and build only those that changed their status to approved:

def monitorRecentlyApprovedPRs(Map args = [:]) {
  def prMap = getPRStatus(args)
  // Build recently approved pull requests on each repository
  prMap.each { repoName, repoPrs ->
    // Get previously unapproved pull requests
    def previouslyUnapproved = currentBuild.previousBuild?.buildVariables?."${repoName}"?.tokenize(",").collect { it.toInteger() } ?: []
    // Build recently approved pull requests
    repoPrs.approved.intersect(previouslyUnapproved).each { prNumber ->
      build job: "/${args.multibranch}/PR-${prNumber}", wait: false
    }
    env."${repoName}" = repoPrs.unapproved.join(",")
  }
}

When calling monitorRecentlyApprovedPRs you'll have to provide these arguments:

monitorRecentlyApprovedPRs organization: "YOUR-ORGANIZATION", auth: "GITHUB-CREDENTIALS", multibranch: "PATH-TO-THE-MULTIBRANCH-JOB-IN-JENKINS"

Finally, update the multibranch's Jenkinsfile to skip unapproved PRs:

def shouldBuildPR(Map args = [:]) {
  // Get pull request info
  def query = """query {
    organization(login: "${args.organization}") {
      repository(name: "${args.repo}") {
        pullRequest(number: ${args.pr}) {
          state
          reviews(first: 10, states: [APPROVED]) {
            totalCount
          }
        }
      }
    }
  }"""
  def response = githubQuery(args + [query: query])
  def prInfo = response?.data.organization.repository.pullRequest
  def shouldBuild = (
    // Skip merged pull requests
    prInfo.state != "MERGED" &&
    // Check for draft state
    (prInfo.state != "DRAFT") &&
    // Check for approval
    (prInfo.reviews.totalCount > 0)
  )
  shouldBuild
}

To call shouldBuildPR you'll provide these arguments:

shouldBuildPR(organization: "YOUR-ORGANIZATION", repo: "PR-REPO", auth: "GITHUB-CREDENTIALS", pr: env.CHANGE_ID)

If the returned value is false you should stop the rest of the pipeline's execution. Things would have been a lot simpler if the multibranch pipeline plugin provided a PR status environment variable :)

towel
  • 2,094
  • 1
  • 20
  • 30
  • can you post the steps you took to trigger this build please? – sfgroups Jan 19 '20 at 15:08
  • 1
    I edited my answer to include the steps to control PR builds according to their status. I hope it's a least somewhat useful, as it multiples the amount of github API calls... – towel Jan 21 '20 at 11:31