Skip to content

Commit

Permalink
Merge pull request #50 from cloudogu/feature/scmm_pr_handling
Browse files Browse the repository at this point in the history
Feature/scmm pr handling
  • Loading branch information
marekzan authored Feb 8, 2021
2 parents 75086d0 + fa78679 commit ddd8f19
Show file tree
Hide file tree
Showing 7 changed files with 689 additions and 2 deletions.
10 changes: 10 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# top-most EditorConfig file
root = true

[*]
indent_style = space
# Unix-style newlines with a newline ending every file
end_of_line = lf
charset = utf-8
insert_final_newline = true
indent_size = 4
104 changes: 103 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,12 @@ Jenkins Pipeline Shared library, that contains additional features for Git, Mave
- [SonarCloud](#sonarcloud)
- [Pull Requests in SonarQube](#pull-requests-in-sonarqube)
- [Changelog](#changelog)
- [changelogFileName](#changelogFileName)
- [changelogFileName](#changelogfilename)
- [GitHub](#github)
- [GitFlow](#gitflow)
- [SCM-Manager](#scm-manager)
- [Pull Requests](#pull-requests)
- [HttpClient](#httpclient)
- [Steps](#steps)
- [mailIfStatusChanged](#mailifstatuschanged)
- [isPullRequest](#ispullrequest)
Expand Down Expand Up @@ -898,6 +901,105 @@ stage('Gitflow') {
* `gitflow.finishRelease(releaseVersion)` - Finishes a git release by merging into develop and master.
* Use the `releaseVersion` (String) as the name of the new git release.

# SCM-Manager

Provides the functionality to handle pull requests on a SCMManager repository.

You need to pass `usernamePassword` (i.e. a String containing the ID that refers to the
[Jenkins credentials](https://jenkins.io/doc/book/using/using-credentials/)) to `SCMManager` during construction.
These are then used for handling the pull requests.

```groovy
SCMManager scmm = new SCMManager(this, 'ourCredentials')
```

Set the repository url through the `repositoryUrl` property like so:

```groovy
SCMManager scmm = new SCMManager(this, 'https://hostname/scm', 'ourCredentials')
```

## Pull Requests

Each method requires a `repository` parameter, a String containing namespace and name, e.g. `cloudogu/ces-build-lib`.

* `scmm.searchPullRequestIdByTitle(repository, title)` - Returns a pull request ID by title, or empty, if not present.
* Use the `repository` (String) as the GitOps repository
* Use the `title` (String) as the title of the pull request in question.
* This methods requires the `readJSON()` step from the
[Pipeline Utility Steps plugin](https://plugins.jenkins.io/pipeline-utility-steps/).
* `scmm.createPullRequest(repository, source, target, title, description)` - Creates a pull request, or empty, if not present.
* Use the `repository` (String) as the GitOps repository
* Use the `source` (String) as the source branch of the pull request.
* Use the `target` (String) as the target branch of the pull request.
* Use the `title` (String) as the title of the pull request.
* Use the `description` (String) as the description of the pull request.
* `scmm.updateDescription(repository, pullRequestId, title, description)` - Updates the description of a pull request.
* Use the `repository` (String) as the GitOps repository
* Use the `pullRequestId` (String) as the ID of the pull request.
* Use the `title` (String) as the title of the pull request.
* Use the `description` (String) as the description of the pull request.
* `scmm.createOrUpdatePullRequest(repository, source, target, title, description)` - Creates a pull request if no PR is found or updates the existing one.
* Use the `repository` (String) as the GitOps repository
* Use the `source` (String) as the source branch of the pull request.
* Use the `target` (String) as the target branch of the pull request.
* Use the `title` (String) as the title of the pull request.
* Use the `description` (String) as the description of the pull request.
* `scmm.addComment(repository, pullRequestId, comment)` - Adds a comment to a pull request.
* Use the `repository` (String) as the GitOps repository
* Use the `pullRequestId` (String) as the ID of the pull request.
* Use the `comment` (String) as the comment to add to the pull request.

Example:

```groovy
def scmm = new SCMManager(this, 'https://your.ecosystem.com/scm', scmManagerCredentials)
def pullRequestId = scmm.createPullRequest('cloudogu/ces-build-lib', 'feature/abc', 'develop', 'My title', 'My description')
pullRequestId = scmm.searchPullRequestIdByTitle('cloudogu/ces-build-lib', 'My title')
scmm.updatePullRequest('cloudogu/ces-build-lib', pullRequestId, 'My new title', 'My new description')
scmm.addComment('cloudogu/ces-build-lib', pullRequestId, 'A comment')
```

# HttpClient

`HttpClient` provides a simple `curl` frontend for groovy.

* Not surprisingly, it requires `curl` on the jenkins agents.
* If you need to authenticate, you can create a `HttpClient` with optional credentials ID (`usernamePassword` credentials)
* `HttpClient` provides `get()`, `put()` and `post()` methods
* All methods have the same signature, e.g.
`http.get(url, contentType = '', data = '')`
* `url` (String)
* optional `contentType` (String) - set as acceptHeader in the request
* optional `data` (Object) - sent in the body of the request
* If successful, all methods return the same data structure a map of
* `httpCode` - as string containing the http status code
* `headers` - a map containing the response headers, e.g. `[ location: 'http://url' ]`
* `body` - an optional string containing the body of the response
* In case of an error (Connection refused, Could not resolve host, etc.) an exception is thrown which fails the build
right away. If you don't want the build to fail, wrap the call in a `try`/`catch` block.

Example:

```groovy
HttpClient http = new HttpClient(scriptMock, 'myCredentialID')
// Simplest example
echo http.get('http://url')
// POSTing data
def dataJson = JsonOutput.toJson([
comment: comment
])
def response = http.post('http://url/comments"', 'application/json', dataJson)
if (response.status == '201' && response.content-type == 'application/json') {
def json = readJSON text: response.body
echo json.count
}
```

# Steps

## mailIfStatusChanged
Expand Down
90 changes: 90 additions & 0 deletions src/com/cloudogu/ces/cesbuildlib/HttpClient.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package com.cloudogu.ces.cesbuildlib

/**
* An HTTP client that calls curl on the shell.
*
* Returns a map of
* * httpCode (String)
* * headers (Map)
* * body (String)
*/
class HttpClient implements Serializable {
private script
private credentials
private Sh sh

HttpClient(script, credentials = '') {
this.script = script
this.credentials = credentials
this.sh = new Sh(script)
}

Map get(String url, String contentType = '', def data = '') {
return httpRequest('GET', url, contentType, data)
}

Map put(String url, String contentType = '', def data = '') {
return httpRequest('PUT', url, contentType, data)
}

Map post(String url, String contentType = '', def data = '') {
return httpRequest('POST', url, contentType, data)
}

protected String executeWithCredentials(Closure closure) {
if (credentials) {
script.withCredentials([script.usernamePassword(credentialsId: credentials,
passwordVariable: 'CURL_PASSWORD', usernameVariable: 'CURL_USER')]) {
closure.call(true)
}
} else {
closure.call(false)
}
}

protected String getCurlAuthParam() {
"-u ${script.env.CURL_USER}:${script.env.CURL_PASSWORD}"
}

protected Map httpRequest(String httpMethod, String url, String contentType, def data) {
String httpResponse
def rawHeaders
def body

executeWithCredentials {

String curlCommand =
"curl -i -X ${httpMethod} " +
(credentials ? "${getCurlAuthParam()} " : '') +
(contentType ? "-H 'Content-Type: ${contentType}' " : '') +
(data ? "-d '${data.toString()}' " : '') +
"${url}"

// Command must be run inside this closure, otherwise the credentials will not be masked (using '*') in the console
httpResponse = sh.returnStdOut curlCommand
}

String[] responseLines = httpResponse.split("\n")

// e.g. HTTP/2 301
String httpCode = responseLines[0].split(" ")[1]
def separatingLine = responseLines.findIndexOf { it.trim().isEmpty() }

if (separatingLine > 0) {
rawHeaders = responseLines[1..(separatingLine -1)]
body = responseLines[separatingLine+1..-1].join('\n')
} else {
// No body returned
rawHeaders = responseLines[1..-1]
body = ''
}

def headers = [:]
for(String line: rawHeaders) {
// e.g. cache-control: no-cache
def splitLine = line.split(':', 2);
headers[splitLine[0].trim()] = splitLine[1].trim()
}
return [ httpCode: httpCode, headers: headers, body: body]
}
}
140 changes: 140 additions & 0 deletions src/com/cloudogu/ces/cesbuildlib/SCMManager.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package com.cloudogu.ces.cesbuildlib

import groovy.json.JsonOutput

class SCMManager implements Serializable {

private script
protected HttpClient http
protected String baseUrl

SCMManager(script, String baseUrl, String credentials) {
this.script = script
this.baseUrl = baseUrl
this.http = new HttpClient(script, credentials)
}

String searchPullRequestIdByTitle(String repository, String title) {
def pullRequest
for (Map pr : getPullRequests(repository)) {
if (pr.title == title) {
pullRequest = pr
}
}

if (pullRequest) {
return pullRequest.id.toString()
} else {
return ''
}
}

String createPullRequest(String repository, String source, String target, String title, String description) {
def dataJson = JsonOutput.toJson([
title : title,
description: description,
source : source,
target : target
])
def httpResponse = http.post(pullRequestEndpoint(repository), 'application/vnd.scmm-pullRequest+json;v=2', dataJson)

script.echo "Creating pull request yields httpCode: ${httpResponse.httpCode}"
if (httpResponse.httpCode != "201") {
script.echo 'WARNING: Http status code indicates, that pull request was not created'
return ''
}

// example: "location: https://some/pr/42" - extract id
return httpResponse.headers.location.split("/")[-1]
}

boolean updatePullRequest(String repository, String pullRequestId, String title, String description) {
// In order to update the description put in also the title. Otherwise the title is overwritten with an empty string.
def dataJson = JsonOutput.toJson([
title : title,
description: description
])

def httpResponse = http.put("${pullRequestEndpoint(repository)}/${pullRequestId}", 'application/vnd.scmm-pullRequest+json;v=2', dataJson)

script.echo "Pull request update yields http_code: ${httpResponse.httpCode}"
if (httpResponse.httpCode != "204") {
script.echo 'WARNING: Http status code indicates, that the pull request was not updated'
return false
}
return true
}

String createOrUpdatePullRequest(String repository, String source, String target, String title, String description) {

def pullRequestId = searchPullRequestIdByTitle(repository, title)

if(pullRequestId.isEmpty()) {
return createPullRequest(repository, source, target, title, description)
} else {
if(updatePullRequest(repository, pullRequestId, title, description)) {
return pullRequestId
} else {
return ''
}
}
}

boolean addComment(String repository, String pullRequestId, String comment) {
def dataJson = JsonOutput.toJson([
comment: comment
])
def httpResponse = http.post("${pullRequestEndpoint(repository)}/${pullRequestId}/comments", 'application/json', dataJson)

script.echo "Adding comment yields http_code: ${httpResponse.httpCode}"
if (httpResponse.httpCode != "201") {
script.echo 'WARNING: Http status code indicates, that the comment was not added'
return false
}
return true
}

protected String pullRequestEndpoint(String repository) {
"${this.baseUrl}/api/v2/pull-requests/${repository}"
}

/**
* @return SCM-Manager's representation of PRs. Basically a list of PR objects.
* properties (as of SCM-Manager 2.12.0)
* * id
* * author
* * id
* * displayName
* * mail
* * source - the source branch
* * target - the target branch
* * title
* * description (branch)
* * creationDate: (e.g. "2020-10-09T15:08:11.459Z")
* * lastModified"
* * status, e.g. "OPEN"
* * reviewer (list)
* * tasks
* * todo (number)
* * done (number
* * tasks sourceRevision
* * targetRevision
* * targetRevision
* * markedAsReviewed (list)
* * emergencyMerged
* * ignoredMergeObstacles
*/
protected List getPullRequests(String repository) {
def httpResponse = http.get(pullRequestEndpoint(repository), 'application/vnd.scmm-pullRequestCollection+json;v=2')

script.echo "Getting all pull requests yields httpCode: ${httpResponse.httpCode}"
if (httpResponse.httpCode != "200") {
script.echo 'WARNING: Http status code indicates, that the pull requests could not be retrieved'
return []
}

def prsAsJson = script.readJSON text: httpResponse.body
return prsAsJson._embedded.pullRequests
}

}
Loading

0 comments on commit ddd8f19

Please sign in to comment.