Permalink
Browse files

support for sending and updating issues in gitlab

  • Loading branch information...
ssadedin committed May 20, 2018
1 parent 8c77cd9 commit c895006f7045d7f499615952b5f23db14f34381b
@@ -52,8 +52,7 @@ dependencies {
compile 'org.apache.xmlgraphics:batik-awt-util:1.7@jar'
compile group: 'org.fusesource.jansi', name: 'jansi', version: '1.16'
compile group: 'org.apache.activemq', name: 'activemq-client', version: '5.14.5'
compile group: 'org.gitlab4j', name: 'gitlab4j-api', version: '4.7.17' // note 4.8 not compatible with jdk1.7
compile ('com.hazelcast:hazelcast-all:2.1.2') { transitive = false }
testCompile group: 'junit', name: 'junit', version: '4.8.2'
@@ -0,0 +1,63 @@
[comment]: <> ( vim: ts=20 expandtab tw=100 spell nocindent nosmartindent filetype=Markdown)
# Gitlab Integration
Bpipe supports simple integration with Gitlab, which enables you to create
issues and add notes to them as part of your Bpipe pipeline.
## Configuration
Gitlab integration is accomplished through the existing framework for sending
notifications. To configure it, you need to set up a notification channel
of type 'gitlab' in your `bpipe.config` file for your pipeline. There
are three important settings:
- the URL of your Gitlab server
- the project within which you want to integrate
- an authentication token, generated from your settings page in Gitlab
Note that all integration is done within the context of a particular project.
If you want to communicate with more than one project, they should be configured
as separate notification channels.
Here is an example configuration:
```
notifications {
gitlab {
url='http://git.server.com'
project='test_project'
token="your-secret-token"
events=''
}
}
```
## Usage in Pipeline Scripts
Interaction with Gitlab within pipeline scripts is accomplished using
the `send` command, in the form:
```
send issue(... details...) to gitlab
```
The `gitlab` in the above expression is the name of the Gitlab notification
channel, which is `gitlab` by default, but can be a custom name if you
set it up that way in the notification configuration.
To create an issue you must at least specify the title. The other parameters
are optional and include label, assignee and description. A full example is
as follows:
```
send issue(
title: 'Hello there from Bpipe',
description: 'This issue was created by bpipe.\n\n- super awesome',
assignee: 'joe.bloggs',
label: 'testlabel'
) to gitlab
```
If you run this once, it will create the issue. If you run it again before closing
the issue, it will add a note to the issue instead.
@@ -134,7 +134,19 @@ specifying a Map as an argument to a function. The equivalent is:
) to analysis_finished_queue
```
**Send an Issue to Gitlab**
Note that you need to set up the Project, Gitlab URL and authentication token
in the `bpipe.config` file (see Gitlab Guide).
```
send issue(
title: 'Hello there from bpipe',
description: 'This issue was created by bpipe.\n\n- super awesome',
assignee: 'joe.bloggs',
label: 'testlabel'
) to gitlab
```
@@ -0,0 +1,129 @@
/*
* Copyright (c) Murdoch Childrens Research Institute and Contributers
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice, this
* list of conditions and the following disclaimer in the documentation and/or
* other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package bpipe
import java.util.Map
import org.gitlab4j.api.GitLabApi
import org.gitlab4j.api.IssuesApi
import org.gitlab4j.api.NotesApi
import groovy.json.JsonSlurper
import groovy.text.Template
import groovy.util.logging.Log
import org.gitlab4j.api.models.*
@Log
class GitlabNotificationChannel implements NotificationChannel {
ConfigObject cfg
GitLabApi gitlab
Project project
String baseURL
public GitlabNotificationChannel(ConfigObject cfg) {
this.cfg = cfg
log.info "Connecting to gitlab at $cfg.url using token"
gitlab = new GitLabApi(cfg.url, cfg.token)
log.info "Notification channel $cfg.name connected to gitlab successfully"
project = gitlab.projectApi.getProjects(cfg.project).find { Project p -> p.name == cfg.project }
log.info "Found project $project.name from Gitlab at $cfg.url"
this.baseURL = "$cfg.url/api/v4"
}
@Override
public void notify(PipelineEvent event, String subject, Template template, Map<String, Object> model) {
Map issueDetails = model['send.content']
if(this.updateExistingIssue(issueDetails))
return
Map params = [
title: issueDetails.title,
]
if('description' in issueDetails)
params.description = issueDetails.description
if('assignee' in issueDetails) {
User u = gitlab.userApi.findUsers(issueDetails.assignee)[0]
params.assignee_ids = u.id
}
else
if('assignee' in cfg) {
User u = gitlab.userApi.findUsers(issueDetails.assignee)[0]
params.assignee_ids = u.id
}
if('label' in issueDetails)
params.labels=issueDetails.label
Utils.sendURL(params,'POST', "$baseURL/projects/$project.id/issues", ["PRIVATE-TOKEN": cfg.token])
}
boolean updateExistingIssue(Map issueDetails) {
IssuesApi issuesApi = gitlab.issuesApi
// Search the project issues for one matching the given title
Integer issueId = null
try {
String searchResultJSON = Utils.sendURL('GET', "$baseURL/projects/$project.id/issues", ["PRIVATE-TOKEN": cfg.token],
scope: 'all',
state: 'opened',
search: issueDetails.title)
List<Map> searchResult = new JsonSlurper().parseText(searchResultJSON)
if(searchResult.isEmpty()) {
return false
}
issueId = searchResult[0].iid
log.info "Found issue $issueId corresonding to title $issueDetails.title in project $project.id"
NotesApi notesApi = gitlab.notesApi
notesApi.createIssueNote(project.id, issueId , issueDetails.description)
return true
}
catch(PipelineError e) {
log.info "No issue found corresonding to title $issueDetails.title in project $project.id: treating as new issue"
return false
}
}
@Override
public String getDefaultTemplate(String contentType) {
return null;
}
}
@@ -175,12 +175,15 @@ class NotificationManager {
}
}
File templateFile = ReportGenerator.resolveTemplateFile(templateName)
if(!templateFile.exists()) {
def msg = "WARNING: unable to send notification: template $templateName mapped to file $templateFile.absolutePath, but this file does not exist!"
log.error msg
println msg
return
File templateFile
if(templateName != null) {
templateFile = ReportGenerator.resolveTemplateFile(templateName)
if(!templateFile.exists()) {
def msg = "WARNING: unable to send notification: template $templateName mapped to file $templateFile.absolutePath, but this file does not exist!"
log.error msg
println msg
return
}
}
if(detail.checks) {
@@ -192,7 +195,7 @@ class NotificationManager {
String contentType = 'text/plain'
// Default content type to HTML when extension is HTML
if(templateName.endsWith('.html'))
if(templateName?.endsWith('.html'))
contentType = 'text/html'
// In case the content type is explicitly specified for the message
@@ -215,16 +218,19 @@ class NotificationManager {
pipeline : Pipeline.rootPipeline
]
log.info "Generating template from file $templateFile.absolutePath"
def template = engine.createTemplate(templateFile.getText())
sendTimestamps[category] = System.currentTimeMillis()
def template
if(templateFile != null) {
log.info "Generating template from file $templateFile.absolutePath"
template = engine.createTemplate(templateFile.getText())
}
sendTimestamps[category] = System.currentTimeMillis()
try {
channel.notify(evt, desc, template, detail)
}
catch(Throwable t) {
log.warning("Failed to send notification via channel "+ channel + " with using template file $templateFile.absolutePath,configuration " + cfg + ": " + t)
log.warning("Failed to send notification via channel "+ channel + " with using template file $templateFile?.absolutePath,configuration " + cfg + ": " + t)
log.log(Level.SEVERE, "Failed to send notification to channel $channel using template $templateFile, coniguration $cfg", t)
System.err.println "MSG: unable to send notification to channel $channel due to $t"
}
@@ -1974,6 +1974,14 @@ class PipelineContext {
*/
Object fromImpl(Object exts, Closure body) {
// If from is invoked in the form from('a','b',option:'someValue')
// then we get the first argument as a map of options
Map options = [:]
if((exts instanceof List) && (exts[0] instanceof Map)) {
options = exts[0]
exts = exts.tail()
}
log.info "From clause searching for inputs matching spec $exts"
if(!exts || exts.every { it == null })
@@ -2404,6 +2412,10 @@ class PipelineContext {
new Sender(this).html(c)
}
Sender issue(Map details) {
new Sender(this).issue(details)
}
Sender text(Closure c) {
new Sender(this).text(c)
}
@@ -84,6 +84,14 @@ class Sender {
return this
}
Sender issue(Map details) {
this.content = details
this.contentType = "application/json"
this.defaultSubject = details.title
return this
}
/**
* Support for sending HTML through a communication channel -
* the message is provided by building HTML with a MarkupBuilder.
@@ -252,7 +260,7 @@ class Sender {
log.info "Sending to $details.url with content type $contentType"
try {
connectAndSend(contentIn, url)
Utils.connectAndSend(contentIn, url, ['Content-Type':this.contentType])
}
finally {
if(contentIn.respondsTo('close'))
@@ -276,32 +284,6 @@ class Sender {
return contentIn
}
void connectAndSend(def contentIn, String url) {
new URL(url).openConnection().with {
doOutput = true
useCaches = false
setRequestProperty('Content-Type',this.contentType)
requestMethod = 'POST'
connect()
outputStream.withWriter { writer ->
writer << contentIn
}
log.info "Sent to URL $details.url"
int code = getResponseCode()
log.info("Received response code $code from server")
if(code >= 400) {
String output = errorStream.text
throw new PipelineError("Send to $details.url failed with error $code. Response contains: ${output.take(80)}")
}
if(log.isLoggable(Level.FINE))
log.fine content.text
}
}
/**
* Simplified form : 'send "hello" to gtalk'
*
Oops, something went wrong.

0 comments on commit c895006

Please sign in to comment.