├── .gitignore ├── README.md ├── build.gradle ├── images └── screen.png ├── lib └── jira-client-0.3.jar └── src └── main └── java └── org └── rundeck └── plugins └── notification └── JiraNotification.java /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Description 3 | 4 | This Notification plugin will append job status as a comment to a Jira ticket. 5 | 6 | Here's a screenshot of the notification: 7 | 8 | ![screenshot](images/screen.png) 9 | 10 | Big props to [rcarz](https://github.com/rcarz) and his fantabulous 11 | [jira-client](https://github.com/rcarz/jira-client) that made this plugin possible. 12 | 13 | 14 | ## Build / Deploy 15 | 16 | To build the project from source, run: `gradle build`. 17 | The resulting jar will be found in `build/libs`. 18 | 19 | Copy the jar to Rundeck plugins directory. For example, on an RPM installation: 20 | 21 | cp build/libs/jira-notification-1.0.0.jar /var/lib/rundeck/libext 22 | 23 | or for a launcher: 24 | 25 | cp build/libs/jira-notification-1.0.0.jar $RDECK_BASE/libext 26 | 27 | Then restart the Rundeck service. 28 | 29 | ## Configuration 30 | 31 | The Jira connection credentials are set in the project.properties file 32 | for your project. 33 | 34 | ``` 35 | project.plugin.Notification.JIRA.login=slomo 36 | project.plugin.Notification.JIRA.password=s1inky 37 | project.plugin.Notification.JIRA.url=https://myOnDemand.atlassian.net 38 | ``` 39 | 40 | ## Usage 41 | 42 | To use the plugin, configure your job to send a notification 43 | for on start, success or failure. 44 | 45 | The plugin has one input option: 46 | 47 | * issue: The JIRA issue ID. 48 | 49 | ## Example 50 | 51 | The example job below sends a notification on start. 52 | Note, the JIRA issue ID is passed as a job option: 53 | 54 | ```YAML 55 | - id: a3528977-cc67-4d50-97d4-729845c643b9 56 | name: say hi 57 | description: this is the hi saying job 58 | project: examples 59 | loglevel: INFO 60 | multipleExecutions: true 61 | sequence: 62 | keepgoing: false 63 | strategy: node-first 64 | commands: 65 | - script: |- 66 | #!/usr/bin/env python 67 | print 'this is a python script' 68 | notification: 69 | onstart: 70 | plugin: 71 | type: JIRA 72 | configuration: 73 | issue: '${option.jira_issue}' 74 | options: 75 | jira_issue: 76 | required: true 77 | description: 'the JIRA issue ' 78 | uuid: a3528977-cc67-4d50-97d4-729845c643b9 79 | ``` 80 | 81 | ## Troubleshooting 82 | 83 | Errors from JIRA communication can be found in Rundeck's service.log. 84 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'pl.allegro.tech.build.axion-release' version '1.10.0' 3 | } 4 | defaultTasks 'clean','build' 5 | apply plugin: 'java' 6 | apply plugin: 'idea' 7 | sourceCompatibility = 1.8 8 | ext.rundeckPluginVersion= '1.1' 9 | 10 | 11 | scmVersion { 12 | tag { 13 | prefix = 'v' 14 | versionSeparator = '' 15 | def origDeserialize=deserialize 16 | //apend .0 to satisfy semver if the tag version is only X.Y 17 | deserialize = { config, position, tagName -> 18 | def orig = origDeserialize(config, position, tagName) 19 | if (orig.split('\\.').length < 3) { 20 | orig += ".0" 21 | } 22 | orig 23 | } 24 | } 25 | } 26 | project.version = scmVersion.version 27 | 28 | /** 29 | * Set this to a comma-separated list of full classnames of your implemented Rundeck 30 | * plugins. 31 | */ 32 | ext.pluginClassNames='org.rundeck.plugins.notification.JiraNotification' 33 | 34 | 35 | repositories { 36 | mavenCentral() 37 | 38 | flatDir { 39 | dirs 'lib' 40 | } 41 | } 42 | 43 | configurations{ 44 | //declare custom pluginLibs configuration to include only libs for this plugin 45 | pluginLibs 46 | 47 | //declare compile to extend from pluginLibs so it inherits the dependencies 48 | compile{ 49 | extendsFrom pluginLibs 50 | } 51 | } 52 | 53 | dependencies { 54 | // add any third-party jar dependencies you wish to include in the plugin 55 | // using the `pluginLibs` configuration as shown here: 56 | 57 | pluginLibs group: 'net.rcarz', name: 'jira-client', version: '0.5' 58 | 59 | //the compile dependency won't add the rundeck-core jar to the plugin contents 60 | compile group: 'org.rundeck', name: 'rundeck-core', version: '3.0.12-20190114' 61 | } 62 | 63 | // task to copy plugin libs to output/lib dir 64 | task copyToLib(type: Copy) { 65 | into "$buildDir/output/lib" 66 | from configurations.pluginLibs 67 | } 68 | 69 | 70 | jar { 71 | from "$buildDir/output" 72 | manifest { 73 | def libList = configurations.pluginLibs.collect{'lib/'+it.name}.join(' ') 74 | attributes 'Rundeck-Plugin-Name' : 'Jira Notification' 75 | attributes 'Rundeck-Plugin-Description' : 'This Notification plugin will append job status as a comment to a Jira ticket.' 76 | attributes 'Rundeck-Plugin-Rundeck-Compatibility-Version': '3.x' 77 | attributes 'Rundeck-Plugin-Tags': 'java,notification,jira' 78 | attributes 'Rundeck-Plugin-License': 'Apache 2.0' 79 | attributes 'Rundeck-Plugin-Source-Link': 'https://github.com/rundeck-plugins/jira-notification' 80 | attributes 'Rundeck-Plugin-Target-Host-Compatibility': 'all' 81 | attributes 'Rundeck-Plugin-Author': 'Rundeck, Inc.' 82 | attributes 'Rundeck-Plugin-Classnames': pluginClassNames 83 | attributes 'Rundeck-Plugin-File-Version': project.version 84 | attributes 'Rundeck-Plugin-Version': rundeckPluginVersion, 'Rundeck-Plugin-Archive': 'true' 85 | attributes 'Rundeck-Plugin-Libs': "${libList}" 86 | } 87 | } 88 | jar.dependsOn(copyToLib) 89 | 90 | task wrapper(type: Wrapper) { 91 | gradleVersion = '1.3' 92 | } 93 | -------------------------------------------------------------------------------- /images/screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rundeck-plugins/jira-notification/d2bf229bc5ed9099795df9081ffdee4b5e5f5802/images/screen.png -------------------------------------------------------------------------------- /lib/jira-client-0.3.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rundeck-plugins/jira-notification/d2bf229bc5ed9099795df9081ffdee4b5e5f5802/lib/jira-client-0.3.jar -------------------------------------------------------------------------------- /src/main/java/org/rundeck/plugins/notification/JiraNotification.java: -------------------------------------------------------------------------------- 1 | package org.rundeck.plugins.notification; 2 | 3 | 4 | import com.dtolabs.rundeck.core.plugins.Plugin; 5 | import com.dtolabs.rundeck.core.plugins.configuration.PropertyScope; 6 | import com.dtolabs.rundeck.plugins.descriptions.PluginDescription; 7 | import com.dtolabs.rundeck.plugins.descriptions.PluginProperty; 8 | import com.dtolabs.rundeck.plugins.notification.NotificationPlugin; 9 | 10 | import net.rcarz.jiraclient.BasicCredentials; 11 | import net.rcarz.jiraclient.Issue; 12 | import net.rcarz.jiraclient.JiraClient; 13 | import net.rcarz.jiraclient.JiraException; 14 | 15 | import java.util.Date; 16 | import java.util.Map; 17 | 18 | /** 19 | * Jira Notification Plugin 20 | */ 21 | @Plugin(service = "Notification", name = "JIRA") 22 | @PluginDescription(title = "JIRA Notification", description = "Append notification messages to a Jira issue.") 23 | public class JiraNotification implements NotificationPlugin { 24 | 25 | @PluginProperty(name = "issue key", title = "issue key", description = "Jira issue ID") 26 | private String issueKey; 27 | 28 | @PluginProperty(name = "url", title = "Server URL", description = "Jira server URL", scope = PropertyScope.Project) 29 | private String serverURL; 30 | 31 | @PluginProperty(name = "login", title = "login", description = "The account login name", scope = PropertyScope.Project) 32 | private String login; 33 | 34 | @PluginProperty(name = "password", title = "password", description = "The account password", scope = PropertyScope.Project) 35 | private String password; 36 | 37 | public JiraNotification() { 38 | 39 | } 40 | 41 | @Override 42 | public boolean postNotification(String trigger, Map executionData, Map config) { 43 | if (null == login) { 44 | throw new IllegalStateException("login is required"); 45 | } 46 | if (null == password) { 47 | throw new IllegalStateException("password is required"); 48 | } 49 | if (null == serverURL) { 50 | throw new IllegalStateException("server URL is required"); 51 | } 52 | /** 53 | * Connect to JIRA using the configured credentials. 54 | */ 55 | final BasicCredentials creds = new BasicCredentials(login, password); 56 | final JiraClient jira = new JiraClient(serverURL, creds); 57 | try { 58 | /* Retrieve the issue from JIRA */ 59 | Issue issue = jira.getIssue(issueKey); 60 | 61 | /* Add a comment to the issue */ 62 | String message = generateMessage(trigger, executionData); 63 | issue.addComment(message); 64 | 65 | } catch (JiraException je) { 66 | je.printStackTrace(System.err); 67 | return false; 68 | } 69 | 70 | return true; 71 | } 72 | 73 | /** 74 | * Format the message to send using JIRA's wiki-ish https://jira.atlassian.com/secure/WikiRendererHelpAction.jspa 75 | * Content includes execution and job info, as well as, options and failed nodes. 76 | * @param trigger Job trigger event 77 | * @param executionData Job execution data 78 | * @return String formatted with job data 79 | */ 80 | private String generateMessage(String trigger, Map executionData) { 81 | Object job = executionData.get("job"); 82 | Map jobdata = (Map) job; 83 | Object groupPath = jobdata.get("group"); 84 | Object jobname = jobdata.get("name"); 85 | String jobgroup = (!isBlank(groupPath.toString()) ? groupPath + "/" : ""); 86 | String jobdesc = (String)jobdata.get("description"); 87 | String emoticon = (trigger.equals("success") ? "(/)" : "(x)"); 88 | Date date = (trigger.equals("running") ? (Date)executionData.get("dateStarted") : (Date)executionData.get("dateEnded")); 89 | 90 | StringBuilder sb = new StringBuilder(); 91 | 92 | sb.append("{panel:title=Rundeck Job Notification}\n"); 93 | sb.append("h3. ").append(emoticon).append(" [#"+executionData.get("id")); 94 | sb.append(" ").append(executionData.get("status")); 95 | sb.append(" by " + executionData.get("user")); 96 | sb.append(" at ").append(date); 97 | sb.append("|").append(executionData.get("href")).append("]\n"); 98 | 99 | sb.append("\n"); 100 | sb.append("h4. Job: [").append(jobdata.get("project")+"] \"").append(jobgroup+jobname).append("\"\n"); 101 | sb.append("_").append(jobdesc).append("_\n"); 102 | sb.append("\n"); 103 | 104 | Map context = (Map) executionData.get("context"); 105 | 106 | Map options = (Map) context.get("option"); 107 | Map secureOption = (Map) context.get("secureOption"); 108 | if (null != options && options.size()>0) { 109 | sb.append("h6. User Options\n"); 110 | for (Object o : options.entrySet()) { 111 | Map.Entry entry = (Map.Entry) o; 112 | if (secureOption == null || !secureOption.containsKey(entry.getKey())) { 113 | sb.append("* ").append(entry.getKey()).append(": ").append(entry.getValue()).append("\n"); 114 | } 115 | } 116 | } 117 | String status = (String)executionData.get("status"); 118 | if (status.equals("failed")) { 119 | Map nodestatus = (Map)executionData.get("nodestatus"); 120 | sb.append("\n"); 121 | sb.append("h6. Nodes failed [").append(nodestatus.get("failed")) 122 | .append(" out of ").append(nodestatus.get("total")).append("]\n"); 123 | sb.append("* ").append(executionData.get("failedNodeListString")).append("\n"); 124 | } 125 | sb.append("\n"); 126 | sb.append("Job execution output: ").append(executionData.get("href")).append("\n"); 127 | 128 | sb.append("{panel}"); 129 | return sb.toString(); 130 | } 131 | 132 | private boolean isBlank(String string) { 133 | return null == string || "".equals(string); 134 | } 135 | 136 | 137 | } --------------------------------------------------------------------------------