Pipeline: GitHub

Table of Contents

Pipeline: GitHub

The entry points for this plugin’s functionality are additional global variables, available to pipeline scripts when the plugin is enabled and the prerequisites are met.

License

MIT

Prerequisites

  • Jenkins running Java 8 or higher.

  • Projects/jobs must be automatically created by the GitHub Organization folder/project type.

See: GitHub Branch Source Plugin

Credentials

Currently all operations against GitHub will be performed using the builds GitHubSCMSource credentials. These will typically be the Scan Credentials you configured in your GitHub Organization.

However you can override this in a pipeline script by calling setCredentials(String userName, String password) before any properties or methods are accessed/invoked on the pullRequest global variable.

pullRequest.setCredentials('John.Smith', 'qwerty4321')

If you plan to use this plugin to add/modify/remove comments, labels, commit statuses etc. Please ensure that the required permissions are assigned to the token supplied in the credentials (Scan Credentials or Manually supplied).

Triggers

This plugin adds the following pipeline triggers

  • issueCommentTrigger
  • pullRequestReview

issueCommentTrigger

Requirements

  • This trigger only works on Pull Requests, created by the GitHub Branch Source Plugin.
  • Currently this trigger will only allow collaborators of the repository in question to trigger builds.

Limitations

The Pull Request's job/build must have run at least once for the trigger to be registered. If an initial run never takes place then the trigger won't be registered and cannot pickup on any comments made.

This should not be an issue in practice, because a requirement of using this plugin is that your jobs are setup automatically by the GitHub Branch Source Plugin, which will trigger an initial build when it is notified of a new Pull Request.

Considerations

This trigger would be of limited usefulness for people wishing to build public GitHub/Jenkins bots, using pipeline scripts. As there is no way to ensure that a Pull Request's Jenkinsfile contains any triggers. Not to mention you would not want to trust just any Jenkinsfile from a random Pull Request/non-collaborator.

This trigger is intended to be used inside enterprise organizations:

  1. Where all branches and forks just contain a token Jenkinsfile that delegates to the real pipeline script, using shared libraries.
  2. Trust all their Pull Request authors.

Parameters

  • commentPattern (Required) - A Java style regular expression

Usage

Scripted Pipeline:

properties([
    pipelineTriggers([
        issueCommentTrigger('.*test this please.*')
    ])
])

Declarative Pipeline:

pipeline {
    triggers {
        issueCommentTrigger('.*test this please.*')
    }
}

Detecting whether a build was started by the trigger in a script:

Note that the following uses currentBuild.rawBuild and should therefore only be done in a @NonCPS context. See the workflow-cps-plugin Technical Design for more information.

def triggerCause = currentBuild.rawBuild.getCause(org.jenkinsci.plugins.pipeline.github.trigger.IssueCommentCause)

if (triggerCause) {
    echo("Build was started by ${triggerCause.userLogin}, who wrote: " +
         "\"${triggerCause.comment}\", which matches the " +
         "\"${triggerCause.triggerPattern}\" trigger pattern.")
} else {
    echo('Build was not started by a trigger')
}

Environment variables

The GitHub comment and author that triggered the build are exposed as environment variables (from version > 2.5).

  • GITHUB_COMMENT
  • GITHUB_COMMENT_AUTHOR

pullRequestReview

Parameters

  • reviewStates (Optional) - A Java array of the PR review states you wish to trigger the build with. If not specified it will trigger for any review state. Possible states are pending, approved, changes_requested, commented, dismissed

Usage

Scripted Pipeline:

properties([
    pipelineTriggers([
      pullRequestReview(reviewStates: ['approved'])
    ])
])

Declarative Pipeline:

pipeline {
    triggers {
      pullRequestReview(reviewStates: ['approved'])
    }
}

Detecting whether a build was started by the trigger in a script:

Note that the following uses currentBuild.rawBuild and should therefore only be done in a @NonCPS context. See the workflow-cps-plugin Technical Design for more information.

def triggerCause = currentBuild.rawBuild.getCause(org.jenkinsci.plugins.pipeline.github.trigger.PullRequestReviewCause)

if (triggerCause) {
    echo("Build was started by ${triggerCause.userLogin}, who reviewed the PR: " +
         "\"${triggerCause.state}\", which matches one of " +
         "\"${triggerCause.reviewStates}\" trigger pattern.")
} else {
    echo('Build was not started by a trigger')
}

Environment variables

The GitHub review comment and author that triggered the build are exposed as environment variables (from version > 2.8).

  • GITHUB_REVIEW_COMMENT
  • GITHUB_REVIEW_AUTHOR
  • GITHUB_REVIEW_STATE

Global Variables

repository

Coming soon!

pullRequest

Usage

Before you can use the pullRequest global variable you must ensure you are actually in a Pull Request build job. The best way to do this is to check for the existence of the CHANGE_ID environment variable.

Scripted Pipeline:

node {
    stage('Build') {
        try {
            echo 'Hello World'
        } catch (err) {
            // CHANGE_ID is set only for pull requests, so it is safe to access the pullRequest global variable
            if (env.CHANGE_ID) {
                pullRequest.addLabel('Build Failed')
            }
            throw err
        }
    }
}

Declarative Pipeline:

pipeline {
    agent any
    stages {
        stage('Build') {
            steps {
                echo 'Hello World'
            }
        }
    }
    post {
        failure {
            script {
                // CHANGE_ID is set only for pull requests, so it is safe to access the pullRequest global variable
                if (env.CHANGE_ID) {
                    pullRequest.addLabel('Build Failed')
                }
            }
        }
    }
}

Properties

Name Type Setter Description
id Integer false
state String true Valid values open or closed
number Integer false
url String false
patchUrl String false
diffUrl String false
issueUrl String false
title String true
body String true
locked Boolean true Accepts true, false or 'true', 'false'
milestone Milestone true Setter accepts int or Milestone class.
head String false Revision (SHA) of the head commit of this pull request
headRef String false Name of the branch this pull request is created for
base String true Name of the base branch in the current repository this pull request targets
files Iterable<CommitFile> false
assignees Iterable<String> true Accepts a List<String>
commits Iterable<Commit> false
comments Iterable<IssueComment> false
reviewComments Iterable<ReviewComment> false
labels Iterable<String> true Accepts a List<String>
statuses Iterable<CommitStatus> false
requestedReviewers Iterable<String> false
reviews Iterable<Review> false
updatedAt Date false
createdAt Date false
createdBy String false
closedAt Date false
closedBy String false
mergedAt Date false
mergedBy String false
commitCount Integer false
commentCount Integer false
additions Integer false
deletions Integer false
changedFiles Integer false
merged Boolean false
mergeable Boolean false
mergeCommitSha String false
mergeableState String false
maintainerCanModify Boolean true Accepts true, false or 'true', 'false'
draft Boolean false

Methods

Merge

String merge([String commitTitle, String commitMessage, String sha, String mergeMethod])

Returns the merge's SHA/commit id.

Commit Status

CommitStatus createStatus(String status [, String context, String description, String targetUrl])

Labels

void addLabels(List labels)

void removeLabel(String label)

Assignees

void addAssignees(List assignees)

void removeAssignees(List assignees)

Reviews

void review(String event)

void review(String event, String body)

void review(String commitId, String event, String body)

Review Comments

ReviewComment reviewComment(String commitId, String path, int position, String body)

ReviewComment editReviewComment(long commentId, String body)

ReviewComment replyToReviewComment(long commentId, String body)

void deleteReviewComment(long commentId)

Pull Request Comments (Issue Comments)

IssueComment comment(String body)

IssueComment editComment(long commentId, String body)

void deleteComment(long commentId)

Requested Reviewers

Iterable getRequestedReviewers<>()

void createReviewRequests(List reviewers)

void createTeamReviewRequests(List teams)

Iterable getRequestedTeamReviewers<>()

void deleteReviewRequests(List reviewers)

void deleteTeamReviewRequests(List teams)

Delete Branch

void deleteBranch()

Misc

void setCredentials(String userName, String password)

Auxiliary Classes

CommitStatus

Properties

Name Type Setter Description
id String false
url String false
state String false One of pending, success, failure or error
context String false
description String false
targetUrl String false
createdAt Date false
updatedAt Date false
creator String false

Methods

None.

Commit

Properties

Name Type Setter Description
sha String false
url String false
author String false
committer String false
parents Iterable<String> false List of parent commit SHA's
message String false
commentCount Integer false
comments Iterable<ReviewComment> false
additions Integer false
deletions Integer false
totalChanges Integer false
files Iterable<CommitFile> false List of files added, removed and or modified in this commit
statuses Iterable<CommitStatus> false List of statuses associated with this commit

Methods

Commit Status

CommitStatus createStatus(String status [, String context, String description, String targetUrl])

Review Comment

ReviewComment comment(String body [, String path, Integer position])

CommitFile

Properties

Name Type Setter Description
sha String false
filename String false
status String false
patch String false
additions Integer false
deletions Integer false
changes Integer false
rawUrl String false
blobUrl String false

Methods

None.

IssueComment

Properties

Name Type Setter Description
id Integer false
url String false
user String false
body String true
createdAt Date false
updatedAt Date false

Methods

void delete()

ReviewComment

Properties

Name Type Setter Description
id Long false
url String false
user String false
createdAt Date false
updatedAt Date false
commitId String false
originalCommitId Long false
body String true
path String false
line Integer false This will always return null as the GitHub APIs no longer return line, use position instead.
position Integer false
originalPosition Integer false
diffHunk String false
pullRequestUrl String false
pullRequestReviewId Long false
inReplyToId Long false

Methods

void delete()

Review

Properties

Name Type Setter Description
id long false
user String false
body String false
commitId String false
state String One of APPROVED, PENDING, CHANGES_REQUESTED, DISMISSED, COMMENTED

Methods

None.

Milestone

Properties

Name Type Setter Description
number Integer false
createdAt Date false
dueOn Date false
updatedAt Date false
closedAt Date false
closedIssues Integer false
openIssues Integer false
description String false
state String false
title String false
url String false
creator String false

Methods

None.

Examples

Pull Requests

Updating a Pull Request's title and body

pullRequest.title = 'Updated title'
pullRequest.body = pullRequest.body + '\nEdited by Pipeline'

Closing a Pull Request

pullRequest.status = 'closed'

Creating a Commit Status against the head of the Pull Request

pullRequest.createStatus(status: 'success',
                         context: 'continuous-integration/jenkins/pr-merge/tests',
                         description: 'All tests are passing',
                         targetUrl: "${env.JOB_URL}/testResults")

Locking and unlocking a Pull Request's conversation

if (pullRequest.locked) {
    pullRequest.locked = false
}

Merging a Pull Request

if (pullRequest.mergeable) {
    pullRequest.merge('merge commit message here')
}
// or
if (pullRequest.mergeable) {
    pullRequest.merge(commitTitle: 'Make it so..', commitMessage: 'TO BOLDLY GO WHERE NO MAN HAS GONE BEFORE...', mergeMethod: 'squash')
}

Adding a label

pullRequest.addLabel('Build Passing')

Removing a label

pullRequest.removeLabel('Build Passing')

Replacing all labels

pullRequest.labels = ['Bug', 'Feature']

Adding an assignee

pullRequest.addAssignee('Spock')

Removing an assignee

pullRequest.removeAssignee('McCoy')

Replacing all assignees

pullRequest.assignees = ['Data', 'Scotty']

Listing all added/modified/removed files

for (commitFile in pullRequest.files) {
    echo "SHA: ${commitFile.sha} File Name: ${commitFile.filename} Status: ${commitFile.status}"
}

Adding a review

pullRequest.review('APPROVE')
// or
pullRequest.review('REQUEST_CHANGES', 'Change is the essential process of all existence.')
// or
def commitId = 'SHA of the commit containing the change/file you wish to review' // if unspecified, defaults to the most recent commit
def event = 'REQUEST_CHANGES' // valid review events: APPROVE, REQUEST_CHANGES, COMMENT
def body = 'Change is the essential process of all existence.' // body is required when event equals REQUEST_CHANGES or COMMENT
pullRequest.review(commitId, event, body)

Adding a comment

def comment = pullRequest.comment('This PR is highly illogical..')

Editing a comment

pullRequest.editComment(comment.id, 'Live long and prosper.')
// or
comment.body = 'Live long and prosper.'

Deleting a comment

pullRequest.deleteComment(commentId)
// or
comment.delete()

Adding a review comment

def commitId = 'SHA of the commit containing the change/file you wish to review';
def path = 'src/main/java/Main.java'
def lineNumber = 5
def body = 'The review comment'
def comment = pullRequest.reviewComment(commitId, path, lineNumber, body)

Editing a review comment

pullRequest.editReviewComment(comment.id, 'Live long and prosper.')
// or
comment.body = 'Live long and prosper.'

Deleting a review comment

pullRequest.deleteReviewComment(comment.id)
// or
comment.delete()

Replying to a review comment

pullRequest.replyToReviewComment(comment.id, 'Khaaannnn!')
// or
comment.createReply('Khaaannnn!')

Listing a Pull Request's commits

for (commit in pullRequest.commits) {
   echo "SHA: ${commit.sha}, Committer: ${commit.committer}, Commit Message: ${commit.message}"
}

Listing a Pull Request's comments

for (comment in pullRequest.comments) {
  echo "Author: ${comment.user}, Comment: ${comment.body}"
}

Listing a Pull Request's review comments

for (reviewComment in pullRequest.reviewComments) {
  echo "File: ${reviewComment.path}, Position: ${reviewComment.position}, Author: ${reviewComment.user}, Comment: ${reviewComment.body}"
}

Listing a commit's statuses

for (commit in pullRequest.commits) {
  for (status  in commit.statuses) {
     echo "Commit: ${commit.sha}, State: ${status.state}, Context: ${status.context}, URL: ${status.targetUrl}"
  }
}

Creating a Commit Status against arbitrary commits

for (commit in pullRequest.commits) {
  commit.createStatus(status: 'pending')
}

Listing a Pull Request's current statuses

for (status in pullRequest.statuses) {
  echo "Commit: ${pullRequest.head}, State: ${status.state}, Context: ${status.context}, URL: ${status.targetUrl}"
}

Listing a Pull Request's requested reviewers

for (requestedReviewer in pullRequest.requestedReviewers) {
  echo "${requestedReviewer} was requested to review this Pull Request"
}

Listing a Pull Request's requested team reviewers

for (requestedTeamReviewer in pullRequest.requestedTeamReviewers) {
  echo "${requestedTeamReviewer} was requested to review this Pull Request"
}

Listing a Pull Request's reviews

for (review in pullRequest.reviews) {
  echo "${review.user} has a review in ${review.state} state for Pull Request. Review body: ${review.body}"
}

Requesting reviewers

pullRequest.createReviewRequests(['Spock', 'McCoy'])

Deleting requested reviewers

pullRequest.deleteReviewRequests(['McCoy'])

Requesting team reviewers

pullRequest.createTeamReviewRequests(['justice-league'])

Deleting requested team reviewers

pullRequest.deleteTeamReviewRequests(['justice-league'])

Deleting a branch of the pull request after Merging the pull request

pullRequest.merge(pullRequest.title)
pullRequest.deleteBranch()