├── .gitignore ├── .travis.yml ├── README.md ├── images ├── gocd-slack-notifier-demo-with-changes.png └── gocd-slack-notifier-demo.png ├── pom.xml ├── release.sh └── src ├── main ├── java │ └── in │ │ └── ashwanthkumar │ │ └── gocd │ │ ├── slack │ │ ├── GoEnvironment.java │ │ ├── GoNotificationMessage.java │ │ ├── GoNotificationPlugin.java │ │ ├── PipelineListener.java │ │ ├── SlackPipelineListener.java │ │ ├── base │ │ │ └── AbstractNotificationPlugin.java │ │ ├── jsonapi │ │ │ ├── BuildCause.java │ │ │ ├── History.java │ │ │ ├── HttpConnectionUtil.java │ │ │ ├── Job.java │ │ │ ├── Material.java │ │ │ ├── MaterialRevision.java │ │ │ ├── Modification.java │ │ │ ├── Pipeline.java │ │ │ ├── Server.java │ │ │ ├── ServerFactory.java │ │ │ └── Stage.java │ │ └── ruleset │ │ │ ├── PipelineRule.java │ │ │ ├── PipelineStatus.java │ │ │ ├── Rules.java │ │ │ └── RulesReader.java │ │ └── teams │ │ ├── CardHttpContent.java │ │ ├── MessageCardSchema.java │ │ ├── TeamsCard.java │ │ ├── TeamsPipelineListener.java │ │ └── TeamsWebhook.java └── resources │ ├── plugin.xml │ ├── reference.conf │ └── views │ └── config.template.html └── test ├── java └── in │ └── ashwanthkumar │ └── gocd │ ├── slack │ ├── GoNotificationMessageTest.java │ ├── GoNotificationMessage_FixStageTest.java │ ├── GoNotificationPluginTest.java │ ├── Status.java │ ├── jsonapi │ │ └── ServerTest.java │ ├── ruleset │ │ ├── PipelineRuleTest.java │ │ ├── RulesReaderTest.java │ │ └── RulesTest.java │ └── util │ │ └── TestUtils.java │ └── teams │ └── TeamsTest.java └── resources └── configs ├── default-pipeline-rule.conf ├── go_notify.conf ├── pipeline-rule-1.conf ├── pipeline-rule-2.conf ├── test-config-1.conf ├── test-config-invalid.conf ├── test-config-minimal-with-env-variables.conf ├── test-config-minimal-with-pipeline.conf ├── test-config-minimal.conf ├── test-config-teams.conf └── test-config-with-proxy.conf /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled source # 2 | ################### 3 | *.com 4 | *.class 5 | *.dll 6 | *.exe 7 | *.o 8 | *.so 9 | 10 | # Packages # 11 | ############ 12 | # it's better to unpack these files and commit the raw source 13 | # git has its own built in compression methods 14 | *.7z 15 | *.dmg 16 | *.iso 17 | *.rar 18 | *.tar 19 | *.zip 20 | 21 | # Logs and databases # 22 | ###################### 23 | *.log 24 | *.sql 25 | *.sqlite 26 | 27 | # OS generated files # 28 | ###################### 29 | .DS_Store 30 | .DS_Store? 31 | ._* 32 | .Spotlight-V100 33 | .Trashes 34 | ehthumbs.db 35 | Thumbs.db 36 | 37 | # Eclipse # 38 | ########## 39 | .classpath 40 | .project 41 | .settings/ 42 | 43 | # IntelliJ # 44 | ########### 45 | .idea/ 46 | out/ 47 | *.ipr 48 | *.iws 49 | *.iml 50 | 51 | # App # 52 | ###### 53 | sample.db 54 | target/ 55 | dist/ 56 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | sudo: false 3 | jdk: 4 | - openjdk11 5 | cache: 6 | directories: 7 | - $HOME/.m2/ 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/ashwanthkumar/gocd-slack-build-notifier.svg?branch=master)](https://travis-ci.org/ashwanthkumar/gocd-slack-build-notifier) 2 | # gocd-slack-build-notifier 3 | Slack based GoCD build notifier 4 | 5 | ## Setup 6 | Download jar from [releases](https://github.com/ashwanthkumar/gocd-slack-build-notifier/releases) & place it in /plugins/external & restart Go Server. 7 | 8 | ## Configuration 9 | All configurations are in [HOCON](https://github.com/typesafehub/config) format. Plugin searches for the configuration file in the following order 10 | 11 | 1. File defined by the environment variable `GO_NOTIFY_CONF`. 12 | 2. `go_notify.conf` at the user's home directory. Typically it's the `go` user's home directory (`/var/go`). 13 | 3. `go_notify.conf` present at the `CRUISE_SERVER_DIR` environment variable location. 14 | 15 | You can find the details on where / how to setup environment variables for GoCD at the [documentation](https://docs.gocd.org/current/installation/install/server/linux.html#location-of-gocd-server-files). 16 | 17 | Minimalistic configuration would be something like 18 | ```hocon 19 | gocd.slack { 20 | login = "someuser" 21 | password = "somepassword" 22 | api-token = "a-valid-token-from-gocd-server" 23 | server-host = "http://localhost:8153/" 24 | api-server-host = "http://localhost:8153/" 25 | webhookUrl = "https://hooks.slack.com/services/...." 26 | 27 | # optional fields 28 | channel = "#build" 29 | slackDisplayName = "gocd-slack-bot" 30 | slackUserIconURL = "http://example.com/slack-bot.png" 31 | display-console-log-links = true 32 | displayMaterialChanges = true 33 | process-all-rules = true 34 | proxy { 35 | hostname = "localhost" 36 | port = "5555" 37 | type = "socks" # acceptable values are http / socks 38 | } 39 | } 40 | ``` 41 | - `login` - Login for a Go user who is authorized to access the REST API. 42 | - `password` - Password for the user specified above. You might want to create a less privileged user for this plugin. 43 | - `api-token` - Valid GoCD access token. Available starting from v19.2.0 (https://api.gocd.org/current/#bearer-token-authentication). If both login/password and api-token are present, api-token takes precedence. 44 | - `server-host` - FQDN of the Go Server. All links on the slack channel will be relative to this host. 45 | - `api-server-host` - This is an optional attribute. Set this field to localhost so server will use this endpoint to get `PipelineHistory` and `PipelineInstance` 46 | - `webhookUrl` - Slack Webhook URL 47 | - `channel` - Override the default channel where we should send the notifications in slack. You can also give a value starting with `@` to send it to any specific user. 48 | - `display-console-log-links` - Display console log links in the notification. Defaults to true, set to false if you want to hide. 49 | - `displayMaterialChanges` - Display material changes in the notification (git revisions for example). Defaults to true, set to false if you want to hide. 50 | - `process-all-rules` - If true, all matching rules are applied instead of just the first. 51 | - `truncate-changes` - If true, displays only the latest 5 changes for all the materials. (Default: true) 52 | - `proxy` - Specify proxy related settings for the plugin. 53 | - `proxy.hostname` - Proxy Host 54 | - `proxy.port` - Proxy Port 55 | - `proxy.type` - `socks` or `http` are the only accepted values. 56 | 57 | ### Teams Configuration 58 | 59 | To send notifications to Microsoft Teams instead of Slack you need to configure the `listener` setting as shown in the example below. 60 | The other difference is that the channel setting is not used, 61 | instead with Teams you create an incoming webhook for each channel you want to send messages to. 62 | 63 | ```hocon 64 | gocd.slack { 65 | # Tell the plugin you are using Microsoft Teams instead of Slack. 66 | listener = "in.ashwanthkumar.gocd.teams.TeamsPipelineListener" 67 | 68 | # Determines the Team and Channel to send notifications to unless overridden by a pipeline rule. 69 | webhookUrl = "https://xxx.webhook.office.com/webhookb2/xxx/IncomingWebhook/xxx/xxx" 70 | 71 | # The channel setting is not used, only the webhookUrl. 72 | 73 | pipelines = [{ 74 | # The channel setting is ignored. 75 | 76 | # Optionally override the default webhook to send notifications to a different channel. 77 | webhookUrl = "https://example.com" 78 | 79 | # The rest of the configuration functions the same. 80 | }, 81 | } 82 | ``` 83 | 84 | ## Pipeline Rules 85 | By default the plugin pushes a note about all failed stages across all pipelines to Slack. You have fine grain control over this operation. 86 | ```hocon 87 | gocd.slack { 88 | server-host = "http://localhost:8153/" 89 | webhookUrl = "https://hooks.slack.com/services/...." 90 | 91 | pipelines = [{ 92 | name = "gocd-slack-build" 93 | stage = "build" 94 | group = ".*" 95 | label = ".*" 96 | state = "failed|passed" 97 | channel = "#oss-build-group" 98 | owners = ["ashwanthkumar"] 99 | webhookUrl = "https://hooks.slack.com/services/another-team-hook-id..." 100 | }, 101 | { 102 | name = ".*" 103 | stage = ".*" 104 | state = "failed" 105 | }] 106 | } 107 | ``` 108 | `gocd.slack.pipelines` contains all the rules for the go-server. It is a list of rules (see below for what the parameters mean) for various pipelines. The plugin will pick the first matching pipeline rule from the pipelines collection above, so your most specific rule should be first, with the most generic rule at the bottom. Alternatively, set the `process-all-rules` option to `true` and all matching rules will be applied. 109 | - `name` - Regex to match the pipeline name 110 | - `stage` - Regex to match the stage name 111 | - `group` - Regex to match the pipeline group name 112 | - `label` - Regex to match the pipeline label name 113 | - `state` - State of the pipeline at which we should send a notification. You can provide multiple values separated by pipe (`|`) symbol. Valid values are passed, failed, cancelled, building, fixed, broken or all. 114 | - `channel` - (Optional) channel where we should send the slack notification. This setting for a rule overrides the global setting 115 | - `owners` - (Optional) list of slack user handles who must be tagged in the message upon notifications 116 | - `webhookUrl` - (Optional) Use this webhook url instead of the global one. Useful if you're using multiple slack teams. 117 | 118 | ## Configuring the plugin for GoCD on Kubernetes using Helm 119 | 120 | ### Creating a Kubernetes secret to store the config file 121 | 122 | - Create a file that has the config values, for example `go_notify.conf` 123 | - Then create a Kubernetes secret using this file in the proper namespace 124 | 125 | ``` 126 | kubectl create secret generic slack-config \ 127 | --from-file=go_notify.conf=go_notify.conf \ 128 | --namespace=gocd 129 | ``` 130 | 131 | 132 | ### Adding the plugin 133 | - In order to add this plugin, you have to use a local values.yaml file that will override the default [values.yaml](https://github.com/helm/charts/blob/master/stable/gocd/values.yaml) present in the official GoCD helm chart repo. 134 | - Add the .jar file link from the releases section to the `env.extraEnvVars` section as a new environment variable. 135 | - The environment variable name must have `GOCD_PLUGIN_INSTALL` prefixed to it. 136 | - Example 137 | 138 | ``` 139 | env: 140 | extraEnvVars: 141 | - name: GOCD_PLUGIN_INSTALL_slack-notification-plugin 142 | value: https://github.com/ashwanthkumar/gocd-slack-build-notifier/releases/download/v1.3.1/gocd-slack-notifier-1.3.1.jar 143 | - name: GO_NOTIFY_CONF 144 | value: /tmp/slack/go_notify.conf 145 | ``` 146 | - Make sure to add the link of the release you want to use. 147 | - If you want to specify a custom path for the `go_notify.conf` file you can use the `GO_NOTIFY_CONF` environment variable as given above. 148 | 149 | 150 | ### Mounting the config file 151 | 152 | - Mount the previously secret to a path by adding the following configuration to the local values.yaml 153 | 154 | ``` 155 | persistence: 156 | extraVolumes: 157 | - name: slack-config 158 | secret: 159 | secretName: slack-config 160 | defaultMode: 0744 161 | 162 | extraVolumeMounts: 163 | - name: slack-config 164 | mountPath: /tmp/slack 165 | readOnly: true 166 | ``` 167 | - If you want to use a custom config location by specifying `GO_NOTIFY_CONF`, then you can use the above `mountPath`. If not, change the `mountPath` to `/var/go` as it is the default `go` user's home directory. 168 | - Then applying the local values.yaml that has these values added to it will result in a new Go Server pod being created that has the plugin installed and running. 169 | 170 | 171 | ## Screenshots 172 | 173 | 174 | 175 | ## License 176 | [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) 177 | -------------------------------------------------------------------------------- /images/gocd-slack-notifier-demo-with-changes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashwanthkumar/gocd-slack-build-notifier/2f738b3bf518016abf7544f0ed92e0c354bf5deb/images/gocd-slack-notifier-demo-with-changes.png -------------------------------------------------------------------------------- /images/gocd-slack-notifier-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashwanthkumar/gocd-slack-build-notifier/2f738b3bf518016abf7544f0ed92e0c354bf5deb/images/gocd-slack-notifier-demo.png -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | in.ashwanthkumar 8 | gocd-slack-notifier 9 | 2.2.0-beta 10 | jar 11 | 12 | 13 | UTF-8 14 | 20.1.0 15 | 16 | 17 | 18 | 19 | cd.go.plugin 20 | go-plugin-api 21 | ${go.version} 22 | provided 23 | 24 | 25 | 26 | in.ashwanthkumar 27 | slack-java-webhook 28 | 0.0.7 29 | 30 | 31 | 32 | in.ashwanthkumar 33 | my-java-utils 34 | 0.0.9 35 | 36 | 37 | 38 | com.google.code.gson 39 | gson 40 | 2.3.1 41 | 42 | 43 | 44 | com.typesafe 45 | config 46 | 1.2.1 47 | 48 | 49 | 50 | javax.xml.bind 51 | jaxb-api 52 | 2.3.0 53 | 54 | 55 | 56 | 57 | commons-io 58 | commons-io 59 | 2.7 60 | 61 | 62 | 63 | 64 | junit 65 | junit 66 | 4.13.1 67 | test 68 | 69 | 70 | org.hamcrest 71 | hamcrest-core 72 | 1.3 73 | test 74 | 75 | 76 | org.mockito 77 | mockito-core 78 | 1.8.5 79 | test 80 | 81 | 82 | 83 | 84 | 85 | oss-sonatype 86 | oss-sonatype 87 | https://oss.sonatype.org/content/repositories/releases 88 | 89 | 90 | 91 | 92 | 93 | 94 | org.apache.maven.plugins 95 | maven-compiler-plugin 96 | 2.3.2 97 | 98 | 11 99 | 11 100 | 101 | 102 | 103 | org.apache.maven.plugins 104 | maven-dependency-plugin 105 | 2.4 106 | 107 | 108 | copy-dependencies 109 | compile 110 | 111 | copy-dependencies 112 | 113 | 114 | ${project.build.outputDirectory}/lib 115 | runtime 116 | false 117 | false 118 | true 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -xe 2 | rm -rf dist/ 3 | mkdir dist 4 | 5 | mvn clean package 6 | cp target/gocd-slack-notifier*.jar dist/ 7 | -------------------------------------------------------------------------------- /src/main/java/in/ashwanthkumar/gocd/slack/GoEnvironment.java: -------------------------------------------------------------------------------- 1 | package in.ashwanthkumar.gocd.slack; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | /** 7 | * Wrapper around System.getenv, where in we can plug in custom values for unit testing 8 | */ 9 | public class GoEnvironment { 10 | private Map env = new HashMap(); 11 | 12 | public String getenv(String name) { 13 | if(env.containsKey(name)) return env.get(name); 14 | else return System.getenv(name); 15 | } 16 | 17 | /* default */ GoEnvironment setEnv(String name, String value) { 18 | env.put(name, value); 19 | return this; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/in/ashwanthkumar/gocd/slack/GoNotificationMessage.java: -------------------------------------------------------------------------------- 1 | package in.ashwanthkumar.gocd.slack; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | import com.thoughtworks.go.plugin.api.logging.Logger; 5 | import in.ashwanthkumar.gocd.slack.jsonapi.*; 6 | import in.ashwanthkumar.gocd.slack.ruleset.Rules; 7 | import in.ashwanthkumar.utils.lang.StringUtils; 8 | 9 | import java.io.IOException; 10 | import java.net.URI; 11 | import java.net.URISyntaxException; 12 | import java.util.List; 13 | 14 | public class GoNotificationMessage { 15 | private Logger LOG = Logger.getLoggerFor(GoNotificationMessage.class); 16 | 17 | private final ServerFactory serverFactory; 18 | 19 | public GoNotificationMessage() { 20 | serverFactory = new ServerFactory(); 21 | } 22 | 23 | GoNotificationMessage(ServerFactory serverFactory, PipelineInfo pipeline) { 24 | this.serverFactory = serverFactory; 25 | this.pipeline = pipeline; 26 | } 27 | 28 | /** 29 | * Raised when we can't find information about our build in the array 30 | * returned by the server. 31 | */ 32 | static public class BuildDetailsNotFoundException extends Exception { 33 | public BuildDetailsNotFoundException(String pipelineName, 34 | int pipelineCounter) 35 | { 36 | super(String.format("could not find details for %s/%d", 37 | pipelineName, pipelineCounter)); 38 | } 39 | } 40 | 41 | static class StageInfo { 42 | @SerializedName("name") 43 | String name; 44 | 45 | @SerializedName("counter") 46 | String counter; 47 | 48 | @SerializedName("state") 49 | String state; 50 | 51 | @SerializedName("result") 52 | String result; 53 | 54 | @SerializedName("create-time") 55 | String createTime; 56 | 57 | @SerializedName("last-transition-time") 58 | String lastTransitionTime; 59 | } 60 | 61 | static class PipelineInfo { 62 | @SerializedName("name") 63 | String name; 64 | 65 | @SerializedName("counter") 66 | String counter; 67 | 68 | @SerializedName("group") 69 | String group; 70 | 71 | @SerializedName("label") 72 | String label; 73 | 74 | @SerializedName("stage") 75 | StageInfo stage; 76 | 77 | @Override 78 | public String toString() { 79 | return name + "/" + counter + "/" + stage.name + "/" + stage.result; 80 | } 81 | } 82 | 83 | @SerializedName("pipeline") 84 | private PipelineInfo pipeline; 85 | 86 | // Internal cache of pipeline history data from GoCD's JSON API. 87 | private History mRecentPipelineHistory; 88 | 89 | public String goServerUrl(String host) throws URISyntaxException { 90 | return new URI(String.format("%s/go/pipelines/%s/%s/%s/%s", host, pipeline.name, pipeline.counter, pipeline.stage.name, pipeline.stage.counter)).normalize().toASCIIString(); 91 | } 92 | 93 | public String fullyQualifiedJobName() { 94 | return pipeline.name + "/" + pipeline.counter + "/" + pipeline.stage.name + "/" + pipeline.stage.counter; 95 | } 96 | 97 | public String getPipelineName() { 98 | return pipeline.name; 99 | } 100 | 101 | public String getPipelineCounter() { 102 | return pipeline.counter; 103 | } 104 | 105 | public String getPipelineLabel() { 106 | return pipeline.label; 107 | } 108 | 109 | public String getStageName() { 110 | return pipeline.stage.name; 111 | } 112 | 113 | public String getStageCounter() { 114 | return pipeline.stage.counter; 115 | } 116 | 117 | public String getStageState() { 118 | return pipeline.stage.state; 119 | } 120 | 121 | public String getStageResult() { 122 | return pipeline.stage.result; 123 | } 124 | 125 | public String getCreateTime() { 126 | return pipeline.stage.createTime; 127 | } 128 | 129 | public String getLastTransitionTime() { 130 | return pipeline.stage.lastTransitionTime; 131 | } 132 | 133 | public String getPipelineGroup() { 134 | return pipeline.group; 135 | } 136 | 137 | /** 138 | * Fetch the full history of this pipeline from the server. We can't 139 | * get specify a specific version, unfortunately. 140 | */ 141 | public History fetchRecentPipelineHistory(Rules rules) 142 | throws URISyntaxException, IOException 143 | { 144 | if (mRecentPipelineHistory == null) { 145 | Server server = serverFactory.getServer(rules); 146 | mRecentPipelineHistory = server.getPipelineHistory(pipeline.name); 147 | } 148 | return mRecentPipelineHistory; 149 | } 150 | 151 | public Pipeline fetchDetailsForBuild(Rules rules, int counter) 152 | throws URISyntaxException, IOException, BuildDetailsNotFoundException 153 | { 154 | History history = fetchRecentPipelineHistory(rules); 155 | if (history != null) { 156 | Pipeline[] pipelines = history.pipelines; 157 | // Search through the builds in our recent history, and hope that 158 | // we can find the build we want. 159 | for (int i = 0, size = pipelines.length; i < size; i++) { 160 | Pipeline build = pipelines[i]; 161 | if (build.counter == counter) 162 | return build; 163 | } 164 | } 165 | throw new BuildDetailsNotFoundException(getPipelineName(), counter); 166 | } 167 | 168 | public void tryToFixStageResult(Rules rules) 169 | { 170 | String currentStatus = pipeline.stage.state.toUpperCase(); 171 | String currentResult = pipeline.stage.result.toUpperCase(); 172 | if (currentStatus.equals("BUILDING") && currentResult.equals("UNKNOWN")) { 173 | pipeline.stage.result = "Building"; 174 | return; 175 | } 176 | // We only need to double-check certain messages; the rest are 177 | // trusty-worthy. 178 | if (!currentResult.equals("PASSED") && !currentResult.equals("FAILED")) 179 | return; 180 | 181 | // Fetch our history. If we can't get it, just give up; this is a 182 | // low-priority tweak. 183 | History history = null; 184 | try { 185 | history = fetchRecentPipelineHistory(rules); 186 | } catch(Exception e) { 187 | LOG.warn(String.format("Error getting pipeline history: " + 188 | e.getMessage())); 189 | return; 190 | } 191 | 192 | // Figure out whether the previous run of this stage passed or failed. 193 | Stage previous = history.previousRun(Integer.parseInt(pipeline.counter), 194 | pipeline.stage.name, 195 | Integer.parseInt(pipeline.stage.counter)); 196 | if (previous == null || StringUtils.isEmpty(previous.result)) { 197 | LOG.info("Couldn't find any previous run of " + 198 | pipeline.name + "(" + pipeline.label + ")" + "/" + pipeline.counter + "/" + 199 | pipeline.stage.name + "/" + pipeline.stage.counter); 200 | return; 201 | } 202 | String previousResult = previous.result.toUpperCase(); 203 | 204 | // Fix up our build status. This is slightly asymmetrical, because 205 | // we want to be quicker to praise than to blame. Also, I _think_ 206 | // that the typical representation of stageResult is initial caps 207 | // only, but our callers should all be using toUpperCase on it in 208 | // any event. 209 | //LOG.info("current: "+currentResult + ", previous: "+previousResult); 210 | if (currentResult.equals("PASSED") && !previousResult.equals("PASSED")) 211 | pipeline.stage.result = "Fixed"; 212 | else if (currentResult.equals("FAILED") && 213 | previousResult.equals("PASSED")) 214 | pipeline.stage.result = "Broken"; 215 | } 216 | 217 | public Pipeline fetchDetails(Rules rules) 218 | throws URISyntaxException, IOException, BuildDetailsNotFoundException 219 | { 220 | return fetchDetailsForBuild(rules, Integer.parseInt(getPipelineCounter())); 221 | } 222 | 223 | public List fetchChanges(Rules rules) 224 | throws URISyntaxException, IOException 225 | { 226 | Server server = serverFactory.getServer(rules); 227 | Pipeline pipelineInstance = 228 | server.getPipelineInstance(pipeline.name, Integer.parseInt(pipeline.counter)); 229 | return pipelineInstance.rootChanges(server); 230 | } 231 | 232 | public Stage pickCurrentStage(Stage[] stages) { 233 | for (Stage stage : stages) { 234 | if (getStageName().equals(stage.name)) { 235 | return stage; 236 | } 237 | } 238 | throw new IllegalArgumentException("The list of stages from the pipeline (" 239 | + getPipelineName() 240 | + ") doesn't have the active stage (" 241 | + getStageName() 242 | + ") for which we got the notification."); 243 | } 244 | 245 | } 246 | -------------------------------------------------------------------------------- /src/main/java/in/ashwanthkumar/gocd/slack/GoNotificationPlugin.java: -------------------------------------------------------------------------------- 1 | package in.ashwanthkumar.gocd.slack; 2 | 3 | import com.google.gson.GsonBuilder; 4 | import com.thoughtworks.go.plugin.api.GoApplicationAccessor; 5 | import com.thoughtworks.go.plugin.api.GoPlugin; 6 | import com.thoughtworks.go.plugin.api.GoPluginIdentifier; 7 | import com.thoughtworks.go.plugin.api.annotation.Extension; 8 | import com.thoughtworks.go.plugin.api.logging.Logger; 9 | import com.thoughtworks.go.plugin.api.request.GoPluginApiRequest; 10 | import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; 11 | import in.ashwanthkumar.gocd.slack.base.AbstractNotificationPlugin; 12 | import in.ashwanthkumar.gocd.slack.ruleset.Rules; 13 | import in.ashwanthkumar.gocd.slack.ruleset.RulesReader; 14 | import in.ashwanthkumar.utils.lang.StringUtils; 15 | import org.apache.commons.io.IOUtils; 16 | 17 | import java.io.File; 18 | import java.io.IOException; 19 | import java.util.*; 20 | import java.util.concurrent.locks.ReentrantReadWriteLock; 21 | 22 | import static java.util.Arrays.asList; 23 | 24 | @Extension 25 | public class GoNotificationPlugin extends AbstractNotificationPlugin implements GoPlugin { 26 | public static final String CRUISE_SERVER_DIR = "CRUISE_SERVER_DIR"; 27 | private static Logger LOGGER = Logger.getLoggerFor(GoNotificationPlugin.class); 28 | private static final long CONFIG_REFRESH_INTERVAL = 10 * 1000; // 10 seconds 29 | 30 | public static final String EXTENSION_TYPE = "notification"; 31 | private static final List goSupportedVersions = asList("1.0"); 32 | 33 | public static final String REQUEST_NOTIFICATIONS_INTERESTED_IN = "notifications-interested-in"; 34 | public static final String REQUEST_STAGE_STATUS = "stage-status"; 35 | public static final String REQUEST_GET_CONFIGURATION = "go.plugin-settings.get-configuration"; 36 | public static final String REQUEST_GET_VIEW = "go.plugin-settings.get-view"; 37 | public static final String REQUEST_VALIDATE_CONFIGURATION = "go.plugin-settings.validate-configuration"; 38 | 39 | public static final int SUCCESS_RESPONSE_CODE = 200; 40 | public static final int INTERNAL_ERROR_RESPONSE_CODE = 500; 41 | 42 | public static final String GO_NOTIFY_CONF = "GO_NOTIFY_CONF"; 43 | public static final String CONFIG_FILE_NAME = "go_notify.conf"; 44 | public static final String HOME_PLUGIN_CONFIG_PATH = System.getProperty("user.home") + File.separator + CONFIG_FILE_NAME; 45 | 46 | private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); 47 | private GoEnvironment environment = new GoEnvironment(); 48 | private Rules rules; 49 | 50 | private final Timer timer = new Timer(); 51 | private long configLastModified = 0L; 52 | private File pluginConfig; 53 | 54 | public GoNotificationPlugin() { 55 | pluginConfig = findGoNotifyConfigPath(); 56 | timer.scheduleAtFixedRate(new TimerTask() { 57 | @Override 58 | public void run() { 59 | if (pluginConfig.lastModified() != configLastModified) { 60 | if (configLastModified == 0L) { 61 | LOGGER.info("Loading configuration file"); 62 | } else { 63 | LOGGER.info("Reloading configuration file since some modifications were found"); 64 | } 65 | try { 66 | lock.writeLock().lock(); 67 | rules = RulesReader.read(pluginConfig); 68 | } catch (Exception e) { 69 | LOGGER.error(e.getMessage(), e); 70 | } finally { 71 | lock.writeLock().unlock(); 72 | } 73 | configLastModified = pluginConfig.lastModified(); 74 | } 75 | } 76 | }, 0, CONFIG_REFRESH_INTERVAL); 77 | } 78 | 79 | // used for tests 80 | GoNotificationPlugin(GoEnvironment environment) { 81 | this.environment = environment; 82 | } 83 | 84 | public void initializeGoApplicationAccessor(GoApplicationAccessor goApplicationAccessor) { 85 | // ignore 86 | } 87 | 88 | public GoPluginApiResponse handle(GoPluginApiRequest goPluginApiRequest) { 89 | String requestName = goPluginApiRequest.requestName(); 90 | if (requestName.equals(REQUEST_NOTIFICATIONS_INTERESTED_IN)) { 91 | return handleNotificationsInterestedIn(); 92 | } else if (requestName.equals(REQUEST_STAGE_STATUS)) { 93 | return handleStageNotification(goPluginApiRequest); 94 | } else if (requestName.equals(REQUEST_GET_VIEW)) { 95 | return handleRequestGetView(); 96 | } else if (requestName.equals(REQUEST_VALIDATE_CONFIGURATION)) { 97 | return handleValidateConfig(goPluginApiRequest.requestBody()); 98 | } else if (requestName.equals(REQUEST_GET_CONFIGURATION)) { 99 | return handleRequestGetConfiguration(); 100 | } 101 | return null; 102 | } 103 | 104 | private GoPluginApiResponse handleValidateConfig(String requestBody) { 105 | List response = Arrays.asList(); 106 | return renderJSON(SUCCESS_RESPONSE_CODE, response); 107 | } 108 | 109 | 110 | public GoPluginIdentifier pluginIdentifier() { 111 | return new GoPluginIdentifier(EXTENSION_TYPE, goSupportedVersions); 112 | } 113 | 114 | 115 | private GoPluginApiResponse handleRequestGetView() { 116 | Map response = new HashMap(); 117 | 118 | try { 119 | String template = IOUtils.toString(getClass().getResourceAsStream("/views/config.template.html"), "UTF-8"); 120 | response.put("template", template); 121 | } catch (IOException e) { 122 | response.put("error", "Can't load view template"); 123 | return renderJSON(INTERNAL_ERROR_RESPONSE_CODE, response); 124 | } 125 | 126 | 127 | return renderJSON(SUCCESS_RESPONSE_CODE, response); 128 | } 129 | 130 | private GoPluginApiResponse handleRequestGetConfiguration() { 131 | Map response = new HashMap(); 132 | response.put("server-url-external", configField("External GoCD Server URL", "", "1", true, false)); 133 | response.put("pipelineConfig", configField("Pipeline Notification Rules", "", "2", true, false)); 134 | return renderJSON(SUCCESS_RESPONSE_CODE, response); 135 | } 136 | 137 | private GoPluginApiResponse handleNotificationsInterestedIn() { 138 | Map response = new HashMap(); 139 | response.put("notifications", Arrays.asList(REQUEST_STAGE_STATUS)); 140 | return renderJSON(SUCCESS_RESPONSE_CODE, response); 141 | } 142 | 143 | private GoPluginApiResponse handleStageNotification(GoPluginApiRequest goPluginApiRequest) { 144 | GoNotificationMessage message = parseNotificationMessage(goPluginApiRequest); 145 | int responseCode = SUCCESS_RESPONSE_CODE; 146 | 147 | Map response = new HashMap(); 148 | List messages = new ArrayList(); 149 | try { 150 | response.put("status", "success"); 151 | LOGGER.info(message.fullyQualifiedJobName() + " has " + message.getStageState() + "/" + message.getStageResult()); 152 | lock.readLock().lock(); 153 | rules.getPipelineListener().notify(message); 154 | } catch (Exception e) { 155 | LOGGER.info(message.fullyQualifiedJobName() + " failed with error", e); 156 | responseCode = INTERNAL_ERROR_RESPONSE_CODE; 157 | response.put("status", "failure"); 158 | if (!isEmpty(e.getMessage())) { 159 | messages.add(e.getMessage()); 160 | } 161 | } finally { 162 | lock.readLock().unlock(); 163 | } 164 | 165 | if (!messages.isEmpty()) { 166 | response.put("messages", messages); 167 | } 168 | return renderJSON(responseCode, response); 169 | } 170 | 171 | private boolean isEmpty(String str) { 172 | return str == null || str.trim().isEmpty(); 173 | } 174 | 175 | private GoNotificationMessage parseNotificationMessage(GoPluginApiRequest goPluginApiRequest) { 176 | return new GsonBuilder().create().fromJson(goPluginApiRequest.requestBody(), GoNotificationMessage.class); 177 | } 178 | 179 | private File findGoNotifyConfigPath() { 180 | // case 1: Look for an environment variable by GO_NOTIFY_CONF and if a file identified by the value exist 181 | String goNotifyConfPath = environment.getenv(GO_NOTIFY_CONF); 182 | if (StringUtils.isNotEmpty(goNotifyConfPath)) { 183 | File pluginConfig = new File(goNotifyConfPath); 184 | if (pluginConfig.exists()) { 185 | LOGGER.info(String.format("Configuration file found using GO_NOTIFY_CONF at %s", pluginConfig.getAbsolutePath())); 186 | return pluginConfig; 187 | } 188 | } 189 | // case 2: Look for a file called go_notify.conf in the home folder 190 | File pluginConfig = new File(HOME_PLUGIN_CONFIG_PATH); 191 | if (pluginConfig.exists()) { 192 | LOGGER.info(String.format("Configuration file found at Home Dir as %s", pluginConfig.getAbsolutePath())); 193 | return pluginConfig; 194 | } 195 | // case 3: Look for a file - go_notify.conf in the current working directory of the server 196 | String goServerDir = environment.getenv(CRUISE_SERVER_DIR); 197 | pluginConfig = new File(goServerDir + File.separator + CONFIG_FILE_NAME); 198 | if (pluginConfig.exists()) { 199 | LOGGER.info(String.format("Configuration file found using CRUISE_SERVER_DIR at %s", pluginConfig.getAbsolutePath())); 200 | return pluginConfig; 201 | } 202 | 203 | throw new RuntimeException("Unable to find go_notify.conf. Please make sure you've set it up right."); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/main/java/in/ashwanthkumar/gocd/slack/PipelineListener.java: -------------------------------------------------------------------------------- 1 | package in.ashwanthkumar.gocd.slack; 2 | 3 | import com.thoughtworks.go.plugin.api.logging.Logger; 4 | import in.ashwanthkumar.gocd.slack.ruleset.PipelineRule; 5 | import in.ashwanthkumar.gocd.slack.ruleset.PipelineStatus; 6 | import in.ashwanthkumar.gocd.slack.ruleset.Rules; 7 | import in.ashwanthkumar.utils.lang.option.Option; 8 | 9 | import java.util.List; 10 | 11 | abstract public class PipelineListener { 12 | private static final Logger LOG = Logger.getLoggerFor(PipelineListener.class); 13 | protected Rules rules; 14 | 15 | public PipelineListener(Rules rules) { 16 | this.rules = rules; 17 | } 18 | 19 | public void notify(GoNotificationMessage message) throws Exception { 20 | message.tryToFixStageResult(rules); 21 | LOG.debug(String.format("Finding rules with state %s", message.getStageResult())); 22 | List foundRules = rules.find(message.getPipelineName(), message.getStageName(), message.getPipelineGroup(), message.getPipelineLabel(), message.getStageResult()); 23 | if (foundRules.size() > 0) { 24 | for (PipelineRule pipelineRule : foundRules) { 25 | LOG.debug(String.format("Matching rule is %s", pipelineRule)); 26 | handlePipelineStatus(pipelineRule, PipelineStatus.valueOf(message.getStageResult().toUpperCase()), message); 27 | if (! rules.getProcessAllRules()) { 28 | break; 29 | } 30 | } 31 | } else { 32 | LOG.warn(String.format("Couldn't find any matching rule for %s/%s with status=%s", message.getPipelineName(), message.getStageName(), message.getStageResult())); 33 | } 34 | } 35 | 36 | protected void handlePipelineStatus(PipelineRule rule, PipelineStatus status, GoNotificationMessage message) throws Exception { 37 | status.handle(this, rule, message); 38 | } 39 | 40 | /** 41 | * Invoked when pipeline is BUILDING 42 | * 43 | * @param rule 44 | * @param message 45 | * @throws Exception 46 | */ 47 | public abstract void onBuilding(PipelineRule rule, GoNotificationMessage message) throws Exception; 48 | 49 | /** 50 | * Invoked when pipeline PASSED 51 | * 52 | * @param message 53 | * @throws Exception 54 | */ 55 | public abstract void onPassed(PipelineRule rule, GoNotificationMessage message) throws Exception; 56 | 57 | /** 58 | * Invoked when pipeline FAILED 59 | * 60 | * @param message 61 | * @throws Exception 62 | */ 63 | public abstract void onFailed(PipelineRule rule, GoNotificationMessage message) throws Exception; 64 | 65 | /** 66 | * Invoked when pipeline is BROKEN 67 | * 68 | * Note - This currently is not implemented 69 | * 70 | * @param message 71 | * @throws Exception 72 | */ 73 | public abstract void onBroken(PipelineRule rule, GoNotificationMessage message) throws Exception; 74 | 75 | /** 76 | * Invoked when pipeline is FIXED 77 | * 78 | * Note - This currently is not implemented 79 | * 80 | * @param message 81 | * @throws Exception 82 | */ 83 | public abstract void onFixed(PipelineRule rule, GoNotificationMessage message) throws Exception; 84 | 85 | /** 86 | * Invoked when pipeline is CANCELLED 87 | * 88 | * @param message 89 | * @throws Exception 90 | */ 91 | public abstract void onCancelled(PipelineRule rule, GoNotificationMessage message) throws Exception; 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/in/ashwanthkumar/gocd/slack/SlackPipelineListener.java: -------------------------------------------------------------------------------- 1 | package in.ashwanthkumar.gocd.slack; 2 | 3 | import com.thoughtworks.go.plugin.api.logging.Logger; 4 | import in.ashwanthkumar.gocd.slack.jsonapi.MaterialRevision; 5 | import in.ashwanthkumar.gocd.slack.jsonapi.Modification; 6 | import in.ashwanthkumar.gocd.slack.jsonapi.Pipeline; 7 | import in.ashwanthkumar.gocd.slack.jsonapi.Stage; 8 | import in.ashwanthkumar.gocd.slack.ruleset.PipelineRule; 9 | import in.ashwanthkumar.gocd.slack.ruleset.PipelineStatus; 10 | import in.ashwanthkumar.gocd.slack.ruleset.Rules; 11 | import in.ashwanthkumar.slack.webhook.Slack; 12 | import in.ashwanthkumar.slack.webhook.SlackAttachment; 13 | import in.ashwanthkumar.utils.collections.Lists; 14 | import in.ashwanthkumar.utils.func.Function; 15 | import in.ashwanthkumar.utils.lang.StringUtils; 16 | 17 | import java.net.URI; 18 | import java.net.URISyntaxException; 19 | import java.util.ArrayList; 20 | import java.util.List; 21 | 22 | import static in.ashwanthkumar.utils.lang.StringUtils.startsWith; 23 | 24 | public class SlackPipelineListener extends PipelineListener { 25 | public static final int DEFAULT_MAX_CHANGES_PER_MATERIAL_IN_SLACK = 5; 26 | private static final Logger LOG = Logger.getLoggerFor(SlackPipelineListener.class); 27 | 28 | private final Slack slack; 29 | 30 | public SlackPipelineListener(Rules rules) { 31 | super(rules); 32 | slack = new Slack(rules.getWebHookUrl(), rules.getProxy()); 33 | updateSlackChannel(rules.getSlackChannel()); 34 | 35 | slack.displayName(rules.getSlackDisplayName()) 36 | .icon(rules.getSlackUserIcon()); 37 | } 38 | 39 | @Override 40 | public void onBuilding(PipelineRule rule, GoNotificationMessage message) throws Exception { 41 | updateSlackChannel(rule.getChannel()); 42 | updateWebhookUrl(rule.getWebhookUrl()); 43 | slack.push(slackAttachment(rule, message, PipelineStatus.BUILDING)); 44 | } 45 | 46 | @Override 47 | public void onPassed(PipelineRule rule, GoNotificationMessage message) throws Exception { 48 | updateSlackChannel(rule.getChannel()); 49 | updateWebhookUrl(rule.getWebhookUrl()); 50 | slack.push(slackAttachment(rule, message, PipelineStatus.PASSED).color("good")); 51 | } 52 | 53 | @Override 54 | public void onFailed(PipelineRule rule, GoNotificationMessage message) throws Exception { 55 | updateSlackChannel(rule.getChannel()); 56 | updateWebhookUrl(rule.getWebhookUrl()); 57 | slack.push(slackAttachment(rule, message, PipelineStatus.FAILED).color("danger")); 58 | } 59 | 60 | @Override 61 | public void onBroken(PipelineRule rule, GoNotificationMessage message) throws Exception { 62 | updateSlackChannel(rule.getChannel()); 63 | updateWebhookUrl(rule.getWebhookUrl()); 64 | slack.push(slackAttachment(rule, message, PipelineStatus.BROKEN).color("danger")); 65 | } 66 | 67 | @Override 68 | public void onFixed(PipelineRule rule, GoNotificationMessage message) throws Exception { 69 | updateSlackChannel(rule.getChannel()); 70 | updateWebhookUrl(rule.getWebhookUrl()); 71 | slack.push(slackAttachment(rule, message, PipelineStatus.FIXED).color("good")); 72 | } 73 | 74 | @Override 75 | public void onCancelled(PipelineRule rule, GoNotificationMessage message) throws Exception { 76 | updateSlackChannel(rule.getChannel()); 77 | updateWebhookUrl(rule.getWebhookUrl()); 78 | slack.push(slackAttachment(rule, message, PipelineStatus.CANCELLED).color("warning")); 79 | } 80 | 81 | private SlackAttachment slackAttachment(PipelineRule rule, GoNotificationMessage message, PipelineStatus pipelineStatus) throws URISyntaxException { 82 | String title = String.format("Stage [%s] %s %s", message.fullyQualifiedJobName(), pipelineStatus.verb(), pipelineStatus).replaceAll("\\s+", " "); 83 | SlackAttachment buildAttachment = new SlackAttachment("") 84 | .fallback(title) 85 | .title(title, message.goServerUrl(rules.getGoServerHost())); 86 | 87 | List consoleLogLinks = new ArrayList(); 88 | // Describe the build. 89 | try { 90 | Pipeline details = message.fetchDetails(rules); 91 | Stage stage = message.pickCurrentStage(details.stages); 92 | buildAttachment.addField(new SlackAttachment.Field("Triggered by", stage.approvedBy, true)); 93 | if (details.buildCause.triggerForced) { 94 | buildAttachment.addField(new SlackAttachment.Field("Reason", "Manual Trigger", true)); 95 | } else { 96 | buildAttachment.addField(new SlackAttachment.Field("Reason", details.buildCause.triggerMessage, true)); 97 | } 98 | buildAttachment.addField(new SlackAttachment.Field("Label", details.label, true)); 99 | if (rules.getDisplayConsoleLogLinks()) { 100 | consoleLogLinks = createConsoleLogLinks(rules.getGoServerHost(), details, stage, pipelineStatus); 101 | } 102 | } catch (Exception e) { 103 | buildAttachment.text("(Couldn't fetch build details; see server log.) "); 104 | LOG.warn("Couldn't fetch build details", e); 105 | } 106 | buildAttachment.addField(new SlackAttachment.Field("Status", pipelineStatus.name(), true)); 107 | 108 | // Describe the root changes that made up this build. 109 | if (rules.getDisplayMaterialChanges()) { 110 | try { 111 | List changes = message.fetchChanges(rules); 112 | for (MaterialRevision change : changes) { 113 | StringBuilder sb = new StringBuilder(); 114 | boolean isTruncated = false; 115 | if (rules.isTruncateChanges() && change.modifications.size() > DEFAULT_MAX_CHANGES_PER_MATERIAL_IN_SLACK) { 116 | change.modifications = Lists.take(change.modifications, DEFAULT_MAX_CHANGES_PER_MATERIAL_IN_SLACK); 117 | isTruncated = true; 118 | } 119 | for (Modification mod : change.modifications) { 120 | String url = change.modificationUrl(mod); 121 | if (url != null) { 122 | sb.append("<").append(url).append("|").append(mod.revision).append(">"); 123 | sb.append(": "); 124 | } else if (mod.revision != null) { 125 | sb.append(mod.revision); 126 | sb.append(": "); 127 | } 128 | String comment = mod.summarizeComment(); 129 | if (comment != null) { 130 | sb.append(comment); 131 | } 132 | if (mod.userName != null) { 133 | sb.append(" - "); 134 | sb.append(mod.userName); 135 | } 136 | sb.append("\n"); 137 | } 138 | String fieldNamePrefix = (isTruncated) ? String.format("Latest %d", DEFAULT_MAX_CHANGES_PER_MATERIAL_IN_SLACK) : "All"; 139 | String fieldName = String.format("%s changes for %s", fieldNamePrefix, change.material.description); 140 | buildAttachment.addField(new SlackAttachment.Field(fieldName, sb.toString(), false)); 141 | } 142 | } catch (Exception e) { 143 | buildAttachment.addField(new SlackAttachment.Field("Changes", "(Couldn't fetch changes; see server log.)", true)); 144 | LOG.warn("Couldn't fetch changes", e); 145 | } 146 | } 147 | 148 | if (!consoleLogLinks.isEmpty()) { 149 | String logLinks = Lists.mkString(consoleLogLinks, "", "", "\n"); 150 | buildAttachment.addField(new SlackAttachment.Field("Console Logs", logLinks, true)); 151 | } 152 | 153 | if (!rule.getOwners().isEmpty()) { 154 | List slackOwners = Lists.map(rule.getOwners(), new Function() { 155 | @Override 156 | public String apply(String input) { 157 | return String.format("<@%s>", input); 158 | } 159 | }); 160 | buildAttachment.addField(new SlackAttachment.Field("Owners", Lists.mkString(slackOwners, ","), true)); 161 | } 162 | LOG.info("Pushing " + title + " notification to Slack"); 163 | return buildAttachment; 164 | } 165 | 166 | private List createConsoleLogLinks(String host, Pipeline pipeline, Stage stage, PipelineStatus pipelineStatus) throws URISyntaxException { 167 | List consoleLinks = new ArrayList(); 168 | for (String job : stage.jobNames()) { 169 | URI link; 170 | // We should be linking to Console Tab when the status is building, 171 | // while all others will be the console.log artifact. 172 | if (pipelineStatus == PipelineStatus.BUILDING) { 173 | link = new URI(String.format("%s/go/tab/build/detail/%s/%d/%s/%d/%s#tab-console", host, pipeline.name, pipeline.counter, stage.name, stage.counter, job)); 174 | } else { 175 | link = new URI(String.format("%s/go/files/%s/%d/%s/%d/%s/cruise-output/console.log", host, pipeline.name, pipeline.counter, stage.name, stage.counter, job)); 176 | } 177 | // TODO - May be it's only useful to show the failed job logs instead of all jobs? 178 | consoleLinks.add("<" + link.normalize().toASCIIString() + "| View " + job + " logs>"); 179 | } 180 | return consoleLinks; 181 | } 182 | 183 | private void updateSlackChannel(String slackChannel) { 184 | LOG.debug(String.format("Updating target slack channel to %s", slackChannel)); 185 | // by default post it to where ever the hook is configured to do so 186 | if (startsWith(slackChannel, "#")) { 187 | slack.sendToChannel(slackChannel.substring(1)); 188 | } else if (startsWith(slackChannel, "@")) { 189 | slack.sendToUser(slackChannel.substring(1)); 190 | } 191 | } 192 | 193 | private void updateWebhookUrl(String webbookUrl) { 194 | LOG.debug(String.format("Updating target webhookUrl to %s", webbookUrl)); 195 | // by default pick the global webhookUrl 196 | if (StringUtils.isNotEmpty(webbookUrl)) { 197 | slack.setWebhookUrl(webbookUrl); 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/main/java/in/ashwanthkumar/gocd/slack/base/AbstractNotificationPlugin.java: -------------------------------------------------------------------------------- 1 | package in.ashwanthkumar.gocd.slack.base; 2 | 3 | import com.google.gson.GsonBuilder; 4 | import com.thoughtworks.go.plugin.api.response.DefaultGoPluginApiResponse; 5 | import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; 6 | 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | 10 | abstract public class AbstractNotificationPlugin { 11 | 12 | /** 13 | * Create a configuration field for the plugin. 14 | * 15 | * @param displayName Name of the configuration 16 | * @param defaultValue Default value if none provided 17 | * @param displayOrder Order in which it should be displayed 18 | * @param required If the field is mandatory. 19 | * @param secure If the data in the field should be stored encrypted. 20 | * @return 21 | */ 22 | protected Map configField(String displayName, String defaultValue, String displayOrder, boolean required, boolean secure) { 23 | Map serverUrlParams = new HashMap<>(); 24 | serverUrlParams.put("display-name", displayName); 25 | serverUrlParams.put("display-value", defaultValue); 26 | serverUrlParams.put("display-order", displayOrder); 27 | serverUrlParams.put("required", required); 28 | serverUrlParams.put("secure", secure); 29 | return serverUrlParams; 30 | } 31 | 32 | protected GoPluginApiResponse renderJSON(final int responseCode, final Object response) { 33 | final String json = response == null ? null : new GsonBuilder().disableHtmlEscaping().create().toJson(response); 34 | DefaultGoPluginApiResponse pluginApiResponse = new DefaultGoPluginApiResponse(responseCode); 35 | pluginApiResponse.setResponseBody(json); 36 | return pluginApiResponse; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/in/ashwanthkumar/gocd/slack/jsonapi/BuildCause.java: -------------------------------------------------------------------------------- 1 | package in.ashwanthkumar.gocd.slack.jsonapi; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | public class BuildCause { 6 | @SerializedName("approver") 7 | public String approver; 8 | 9 | @SerializedName("trigger_forced") 10 | public boolean triggerForced; 11 | 12 | @SerializedName("trigger_message") 13 | public String triggerMessage; 14 | 15 | @SerializedName("material_revisions") 16 | public MaterialRevision[] materialRevisions; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/in/ashwanthkumar/gocd/slack/jsonapi/History.java: -------------------------------------------------------------------------------- 1 | package in.ashwanthkumar.gocd.slack.jsonapi; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | import com.thoughtworks.go.plugin.api.logging.Logger; 5 | 6 | public class History { 7 | private Logger LOG = Logger.getLoggerFor(History.class); 8 | 9 | @SerializedName("pipelines") 10 | public Pipeline[] pipelines; 11 | 12 | /** 13 | * Find the most recent run of the specified stage _before_ this one. 14 | */ 15 | public Stage previousRun(int pipelineCounter, String stageName, int stageCounter) { 16 | LOG.debug(String.format("Looking for stage before %d/%s/%d", 17 | pipelineCounter, stageName, stageCounter)); 18 | 19 | // Note that pipelines and stages are stored in reverse 20 | // chronological order. 21 | for (int i = 0; i < pipelines.length; i++) { 22 | Pipeline pipeline = pipelines[i]; 23 | for (int j = 0; j < pipeline.stages.length; j++) { 24 | Stage stage = pipeline.stages[j]; 25 | LOG.debug(String.format("Checking %d/%s/%d", 26 | pipeline.counter, stage.name, stage.counter)); 27 | 28 | if (stage.name.equals(stageName)) { 29 | 30 | // Same pipeline run, earlier instance of stage. 31 | if (pipeline.counter == pipelineCounter && 32 | stage.counter < stageCounter) 33 | return stage; 34 | 35 | // Previous pipeline run. 36 | if (pipeline.counter < pipelineCounter) 37 | return stage; 38 | } 39 | } 40 | } 41 | // Not found. 42 | return null; 43 | } 44 | 45 | @Override 46 | public String toString() { 47 | if (pipelines != null && pipelines.length > 0) { 48 | if (pipelines.length > 1) { 49 | return pipelines[0].toString() + "..."; 50 | } else { 51 | return pipelines[0].toString(); 52 | } 53 | } else { 54 | return "No history"; 55 | } 56 | } 57 | } 58 | 59 | -------------------------------------------------------------------------------- /src/main/java/in/ashwanthkumar/gocd/slack/jsonapi/HttpConnectionUtil.java: -------------------------------------------------------------------------------- 1 | package in.ashwanthkumar.gocd.slack.jsonapi; 2 | 3 | import com.google.gson.GsonBuilder; 4 | import com.google.gson.JsonElement; 5 | import com.google.gson.JsonParser; 6 | 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | import java.io.InputStreamReader; 10 | import java.net.HttpURLConnection; 11 | import java.net.URL; 12 | 13 | public class HttpConnectionUtil { 14 | 15 | HttpURLConnection getConnection(URL url) throws IOException { 16 | return (HttpURLConnection) url.openConnection(); 17 | } 18 | 19 | JsonElement responseToJson(Object content) { 20 | JsonParser parser = new JsonParser(); 21 | return parser.parse(new InputStreamReader((InputStream) content)); 22 | } 23 | 24 | public T convertResponse(JsonElement json, Class type) { 25 | return new GsonBuilder().create().fromJson(json, type); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/in/ashwanthkumar/gocd/slack/jsonapi/Job.java: -------------------------------------------------------------------------------- 1 | package in.ashwanthkumar.gocd.slack.jsonapi; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | public class Job { 6 | @SerializedName("name") 7 | public String name; 8 | 9 | @SerializedName("result") 10 | public String result; 11 | 12 | @SerializedName("state") 13 | public String state; 14 | 15 | @SerializedName("id") 16 | private int id; 17 | 18 | @SerializedName("scheduled_date") 19 | private long scheduledDate; 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/in/ashwanthkumar/gocd/slack/jsonapi/Material.java: -------------------------------------------------------------------------------- 1 | package in.ashwanthkumar.gocd.slack.jsonapi; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | public class Material { 6 | @SerializedName("id") 7 | public int id; 8 | 9 | // Format: "Pipeline", etc. 10 | @SerializedName("type") 11 | public String type; 12 | 13 | // Format: "zoo" or "git@github.com:foo/bar.git, Branch: master" 14 | @SerializedName("description") 15 | public String description; 16 | 17 | //"fingerprint": "d22ec438c20be7f700e2aca7f4f416eef11e5ec2bbcf201c6f03f02ed8b2a6e0", 18 | 19 | public boolean isPipeline() { 20 | return type.equals("Pipeline"); 21 | } 22 | 23 | // Override hashCode and equals with implementations generated by 24 | // Eclipse so we can compare MaterialRevision objects. 25 | 26 | @Override 27 | public int hashCode() { 28 | final int prime = 31; 29 | int result = 1; 30 | result = prime * result + id; 31 | return result; 32 | } 33 | 34 | @Override 35 | public boolean equals(Object obj) { 36 | if (this == obj) 37 | return true; 38 | if (obj == null) 39 | return false; 40 | if (getClass() != obj.getClass()) 41 | return false; 42 | Material other = (Material) obj; 43 | if (id != other.id) 44 | return false; 45 | return true; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/in/ashwanthkumar/gocd/slack/jsonapi/MaterialRevision.java: -------------------------------------------------------------------------------- 1 | package in.ashwanthkumar.gocd.slack.jsonapi; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | import com.thoughtworks.go.plugin.api.logging.Logger; 5 | 6 | import java.io.IOException; 7 | import java.net.MalformedURLException; 8 | import java.util.List; 9 | import java.util.regex.Matcher; 10 | import java.util.regex.Pattern; 11 | 12 | public class MaterialRevision { 13 | static private final Pattern PIPELINE_REVISION_PATTERN = 14 | Pattern.compile("^([^/]+)/(\\d+)/.*"); 15 | static private final Pattern GITHUB_MATERIAL_PATTERN = 16 | Pattern.compile("^URL: git@github\\.com:(.+)\\.git,.*"); 17 | 18 | private Logger LOG = Logger.getLoggerFor(MaterialRevision.class); 19 | 20 | @SerializedName("changed") 21 | public boolean changed; 22 | 23 | @SerializedName("material") 24 | public Material material; 25 | 26 | @SerializedName("modifications") 27 | public List modifications; 28 | 29 | /** 30 | * Is this revision a pipeline, or something else (generally a commit 31 | * to a version control system)? 32 | */ 33 | public boolean isPipeline() { 34 | return material.isPipeline(); 35 | } 36 | 37 | /** 38 | * Return a URL pointing to more information about one of our 39 | * modifications, if we can figure out how to generate one. It's an 40 | * error to call us with a modification that isn't part of this 41 | * MaterialRevision. (This is implemented this way because 42 | * Modification objects are deserialized without any back-pointers to 43 | * the containing MaterialRevision.) 44 | */ 45 | public String modificationUrl(Modification modification) { 46 | if (!material.type.equals("Git") || material.description == null 47 | || modification.revision == null) { 48 | LOG.info(String.format("Can't build URL for modification (%s)/(%s)/(%s)", 49 | material.type, material.description, 50 | modification.revision)); 51 | return null; 52 | } 53 | 54 | // Parse descriptions like: 55 | // "URL: git@github.com:faradayio/marius.git, Branch: master" 56 | Matcher matcher = GITHUB_MATERIAL_PATTERN.matcher(material.description); 57 | if (!matcher.matches()) { 58 | LOG.info("Can't build URL for non-GitHub repo: " + material.description); 59 | return null; 60 | } 61 | String org_and_repo = matcher.group(1); 62 | 63 | // Shorten our commit ID. 64 | String commit = modification.revision; 65 | if (commit.length() > 6) 66 | commit = commit.substring(0, 6); 67 | 68 | return "https://github.com/" + org_and_repo + "/commit/" + commit; 69 | } 70 | 71 | 72 | /** 73 | * Collect all changed MaterialRevision objects, walking changed 74 | * "Pipeline" objects recursively instead of including them directly. 75 | */ 76 | void addChangesRecursively(Server server, List outChanges) 77 | throws MalformedURLException, IOException { 78 | // Give up now if this material hasn't changed. 79 | if (!changed) { 80 | return; 81 | } 82 | 83 | if (!isPipeline()) { 84 | // Add this change if somebody hasn't added it already (which 85 | // can happen in complex pipelines). 86 | if (!outChanges.contains(this)) 87 | outChanges.add(this); 88 | } else { 89 | // Recursively walk pipeline. We're not entirely sure what it 90 | // would mean to have multiple associated modifications with 91 | // isPipeline is true, so we walk all of them just to be on the 92 | // safe side. 93 | for (Modification m : modifications) { 94 | // Parse out the pipeline info. 95 | Matcher matcher = PIPELINE_REVISION_PATTERN.matcher(m.revision); 96 | if (matcher.matches()) { 97 | String pipelineName = matcher.group(1); 98 | int pipelineCounter = Integer.parseInt(matcher.group(2)); 99 | 100 | // Fetch the pipeline and walk it recursively. 101 | Pipeline pipeline = 102 | server.getPipelineInstance(pipelineName, pipelineCounter); 103 | pipeline.addChangesRecursively(server, outChanges); 104 | } else { 105 | LOG.error("Error matching pipeline revision: " + m.revision); 106 | } 107 | } 108 | } 109 | } 110 | 111 | // Override hashCode and equals with implementations generated by 112 | // Eclipse so we can compare MaterialRevision objects using (for 113 | // example) list.contains(mr). 114 | 115 | @Override 116 | public int hashCode() { 117 | final int prime = 31; 118 | int result = 1; 119 | result = prime * result + (changed ? 1231 : 1237); 120 | result = prime * result + ((material == null) ? 0 : material.hashCode()); 121 | result = prime * result + modifications.hashCode(); 122 | return result; 123 | } 124 | 125 | @Override 126 | public boolean equals(Object obj) { 127 | if (this == obj) 128 | return true; 129 | if (obj == null) 130 | return false; 131 | if (getClass() != obj.getClass()) 132 | return false; 133 | MaterialRevision other = (MaterialRevision) obj; 134 | if (changed != other.changed) 135 | return false; 136 | if (material == null) { 137 | if (other.material != null) 138 | return false; 139 | } else if (!material.equals(other.material)) 140 | return false; 141 | if (!modifications.equals(other.modifications)) 142 | return false; 143 | return true; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/main/java/in/ashwanthkumar/gocd/slack/jsonapi/Modification.java: -------------------------------------------------------------------------------- 1 | package in.ashwanthkumar.gocd.slack.jsonapi; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | public class Modification { 6 | @SerializedName("id") 7 | public int id; 8 | 9 | // Format: "cucumber/102/BuildAndPublish/1" for pipelines, and 10 | // "2d110a724f3e716f801b6e87d420d7f0c32a208f" for git commits. 11 | @SerializedName("revision") 12 | public String revision; 13 | 14 | @SerializedName("comment") 15 | public String comment; 16 | 17 | @SerializedName("user_name") 18 | public String userName; 19 | 20 | //"modified_time": 1436365681065, 21 | //"email_address": null 22 | 23 | /** 24 | * Return a shortened form of the comment, or null if we have no comment. 25 | * Designed to reduce git commit messages to just their summary line. 26 | */ 27 | public String summarizeComment() { 28 | if (comment == null) 29 | return null; 30 | 31 | String[] lines = comment.split("\\r?\\n"); 32 | return lines[0]; 33 | } 34 | 35 | // Override hashCode and equals with implementations generated by 36 | // Eclipse so we can compare MaterialRevision objects. 37 | 38 | @Override 39 | public int hashCode() { 40 | final int prime = 31; 41 | int result = 1; 42 | result = prime * result + id; 43 | return result; 44 | } 45 | 46 | @Override 47 | public boolean equals(Object obj) { 48 | if (this == obj) 49 | return true; 50 | if (obj == null) 51 | return false; 52 | if (getClass() != obj.getClass()) 53 | return false; 54 | Modification other = (Modification) obj; 55 | if (id != other.id) 56 | return false; 57 | return true; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/in/ashwanthkumar/gocd/slack/jsonapi/Pipeline.java: -------------------------------------------------------------------------------- 1 | package in.ashwanthkumar.gocd.slack.jsonapi; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | import java.io.IOException; 6 | import java.net.MalformedURLException; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | public class Pipeline { 11 | @SerializedName("id") 12 | public int id; 13 | 14 | @SerializedName("name") 15 | public String name; 16 | 17 | @SerializedName("counter") 18 | public int counter; 19 | 20 | @SerializedName("preparing_to_schedule") 21 | public boolean preparingToSchedule; 22 | 23 | @SerializedName("can_run") 24 | public boolean canRun; 25 | 26 | @SerializedName("build_cause") 27 | public BuildCause buildCause; 28 | 29 | @SerializedName("stages") 30 | public Stage[] stages; 31 | 32 | @SerializedName("label") 33 | public String label; 34 | 35 | // "comment" 36 | // "natural_order" 37 | // "stages" 38 | 39 | /** 40 | * Collect all changed MaterialRevision objects, walking changed 41 | * "Pipeline" objects recursively instead of including them directly. 42 | */ 43 | public List rootChanges(Server server) 44 | throws MalformedURLException, IOException { 45 | List result = new ArrayList(); 46 | addChangesRecursively(server, result); 47 | return result; 48 | } 49 | 50 | void addChangesRecursively(Server server, List outChanges) 51 | throws MalformedURLException, IOException { 52 | for (MaterialRevision mr : buildCause.materialRevisions) { 53 | mr.addChangesRecursively(server, outChanges); 54 | } 55 | } 56 | 57 | @Override 58 | public String toString() { 59 | if (stages != null && stages.length > 0) { 60 | return name + "/" + counter + "/" + stages[0].name + "/" + stages[0].result; 61 | } else { 62 | return name + "/" + counter; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/in/ashwanthkumar/gocd/slack/jsonapi/Server.java: -------------------------------------------------------------------------------- 1 | package in.ashwanthkumar.gocd.slack.jsonapi; 2 | 3 | import com.google.gson.JsonElement; 4 | import com.thoughtworks.go.plugin.api.logging.Logger; 5 | import in.ashwanthkumar.gocd.slack.ruleset.Rules; 6 | 7 | import javax.xml.bind.DatatypeConverter; 8 | import java.io.IOException; 9 | import java.net.HttpURLConnection; 10 | import java.net.MalformedURLException; 11 | import java.net.URISyntaxException; 12 | import java.net.URL; 13 | 14 | import static in.ashwanthkumar.utils.lang.StringUtils.isNotEmpty; 15 | 16 | /** 17 | * Actual methods for contacting the remote server. 18 | */ 19 | public class Server { 20 | private Logger LOG = Logger.getLoggerFor(Server.class); 21 | 22 | // Contains authentication credentials, etc. 23 | private Rules mRules; 24 | private HttpConnectionUtil httpConnectionUtil; 25 | 26 | /** 27 | * Construct a new server object, using credentials from Rules. 28 | */ 29 | public Server(Rules rules) { 30 | mRules = rules; 31 | httpConnectionUtil = new HttpConnectionUtil(); 32 | } 33 | 34 | Server(Rules mRules, HttpConnectionUtil httpConnectionUtil) { 35 | this.mRules = mRules; 36 | this.httpConnectionUtil = httpConnectionUtil; 37 | } 38 | 39 | JsonElement getUrl(URL url) 40 | throws IOException { 41 | URL normalizedUrl; 42 | try { 43 | normalizedUrl = url.toURI().normalize().toURL(); 44 | } catch (URISyntaxException e) { 45 | throw new RuntimeException(e); 46 | } 47 | LOG.info("Fetching " + normalizedUrl.toString()); 48 | 49 | HttpURLConnection request = httpConnectionUtil.getConnection(normalizedUrl); 50 | // @since 20.1.0 51 | request.setRequestProperty("Accept", "application/vnd.go.cd.v1+json"); 52 | request.setRequestProperty("User-Agent", "plugin/slack.notifier"); 53 | 54 | // Add in our HTTP authorization credentials if we have them. 55 | // Favor the API Token over username/password 56 | String authHeader = null; 57 | if (isNotEmpty(mRules.getGoAPIToken())) { 58 | authHeader = "Bearer " + mRules.getGoAPIToken(); 59 | } else if (isNotEmpty(mRules.getGoLogin()) && isNotEmpty(mRules.getGoPassword())) { 60 | String userpass = mRules.getGoLogin() + ":" + mRules.getGoPassword(); 61 | authHeader = "Basic " + DatatypeConverter.printBase64Binary(userpass.getBytes()); 62 | } 63 | if (authHeader != null) { 64 | request.setRequestProperty("Authorization", authHeader); 65 | } 66 | 67 | request.connect(); 68 | 69 | return httpConnectionUtil.responseToJson(request.getContent()); 70 | } 71 | 72 | /** 73 | * Get the recent history of a pipeline. 74 | */ 75 | public History getPipelineHistory(String pipelineName) 76 | throws MalformedURLException, IOException { 77 | URL url = new URL(String.format("%s/go/api/pipelines/%s/history", 78 | mRules.getGoAPIServerHost(), pipelineName)); 79 | JsonElement json = getUrl(url); 80 | return httpConnectionUtil.convertResponse(json, History.class); 81 | } 82 | 83 | /** 84 | * Get a specific instance of a pipeline. 85 | */ 86 | public Pipeline getPipelineInstance(String pipelineName, int pipelineCounter) 87 | throws MalformedURLException, IOException { 88 | URL url = new URL(String.format("%s/go/api/pipelines/%s/%d", 89 | mRules.getGoAPIServerHost(), pipelineName, pipelineCounter)); 90 | JsonElement json = getUrl(url); 91 | return httpConnectionUtil.convertResponse(json, Pipeline.class); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/in/ashwanthkumar/gocd/slack/jsonapi/ServerFactory.java: -------------------------------------------------------------------------------- 1 | package in.ashwanthkumar.gocd.slack.jsonapi; 2 | 3 | import in.ashwanthkumar.gocd.slack.ruleset.Rules; 4 | 5 | public class ServerFactory { 6 | 7 | public Server getServer(Rules rules) { 8 | return new Server(rules); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/in/ashwanthkumar/gocd/slack/jsonapi/Stage.java: -------------------------------------------------------------------------------- 1 | package in.ashwanthkumar.gocd.slack.jsonapi; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | import in.ashwanthkumar.utils.collections.Lists; 5 | import in.ashwanthkumar.utils.func.Function; 6 | 7 | import java.util.List; 8 | 9 | public class Stage { 10 | @SerializedName("id") 11 | public int id; 12 | 13 | @SerializedName("name") 14 | public String name; 15 | 16 | @SerializedName("counter") 17 | public int counter; 18 | 19 | @SerializedName("result") 20 | public String result; 21 | 22 | @SerializedName("approved_by") 23 | public String approvedBy; 24 | 25 | @SerializedName("jobs") 26 | public Job[] jobs; 27 | 28 | // "approval_type" 29 | // "can_run" 30 | // "operate_permission" 31 | // "rerun_of_counter" 32 | // "scheduled" 33 | 34 | public List jobNames() { 35 | return Lists.map(Lists.of(jobs), new Function() { 36 | @Override 37 | public String apply(Job input) { 38 | return input.name; 39 | } 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/in/ashwanthkumar/gocd/slack/ruleset/PipelineRule.java: -------------------------------------------------------------------------------- 1 | package in.ashwanthkumar.gocd.slack.ruleset; 2 | 3 | import com.typesafe.config.Config; 4 | import in.ashwanthkumar.utils.collections.Iterables; 5 | import in.ashwanthkumar.utils.collections.Lists; 6 | import in.ashwanthkumar.utils.func.Predicate; 7 | import in.ashwanthkumar.utils.lang.StringUtils; 8 | 9 | import java.util.*; 10 | 11 | import static in.ashwanthkumar.utils.lang.StringUtils.isEmpty; 12 | 13 | public class PipelineRule { 14 | private String nameRegex; 15 | private String stageRegex; 16 | private String groupRegex; 17 | private String labelRegex; 18 | private String channel; 19 | private String webhookUrl; 20 | private Set owners = new HashSet<>(); 21 | private Set status = new HashSet<>(); 22 | 23 | public PipelineRule() { 24 | } 25 | 26 | public PipelineRule(PipelineRule copy) { 27 | this.nameRegex = copy.nameRegex; 28 | this.stageRegex = copy.stageRegex; 29 | this.groupRegex = copy.groupRegex; 30 | this.labelRegex = copy.labelRegex; 31 | this.channel = copy.channel; 32 | this.status = copy.status; 33 | this.owners = copy.owners; 34 | this.webhookUrl = copy.webhookUrl; 35 | } 36 | 37 | public PipelineRule(String nameRegex, String stageRegex) { 38 | this.nameRegex = nameRegex; 39 | this.stageRegex = stageRegex; 40 | } 41 | 42 | public String getNameRegex() { 43 | return nameRegex; 44 | } 45 | 46 | public PipelineRule setNameRegex(String nameRegex) { 47 | this.nameRegex = nameRegex; 48 | return this; 49 | } 50 | 51 | public String getGroupRegex() { 52 | return groupRegex; 53 | } 54 | 55 | public PipelineRule setGroupRegex(String groupRegex) { 56 | this.groupRegex = groupRegex; 57 | return this; 58 | } 59 | 60 | public String getStageRegex() { 61 | return stageRegex; 62 | } 63 | 64 | public PipelineRule setStageRegex(String stageRegex) { 65 | this.stageRegex = stageRegex; 66 | return this; 67 | } 68 | 69 | public String getLabelRegex() { 70 | return labelRegex; 71 | } 72 | 73 | public PipelineRule setLabelRegex(String labelRegex) { 74 | this.labelRegex = labelRegex; 75 | return this; 76 | } 77 | 78 | public String getChannel() { 79 | return channel; 80 | } 81 | 82 | public PipelineRule setChannel(String channel) { 83 | this.channel = channel; 84 | return this; 85 | } 86 | 87 | public Set getStatus() { 88 | return status; 89 | } 90 | 91 | public PipelineRule setStatus(Set status) { 92 | this.status = status; 93 | return this; 94 | } 95 | 96 | public Set getOwners() { 97 | return owners; 98 | } 99 | 100 | public PipelineRule setOwners(Set owners) { 101 | this.owners = owners; 102 | return this; 103 | } 104 | 105 | public String getWebhookUrl() { 106 | return webhookUrl; 107 | } 108 | 109 | public PipelineRule setWebhookUrl(String webhookUrl) { 110 | this.webhookUrl = webhookUrl; 111 | return this; 112 | } 113 | 114 | public boolean matches(String pipeline, String stage, String group, String label, final String pipelineState) { 115 | return pipeline.matches(nameRegex) 116 | && stage.matches(stageRegex) 117 | && matchesGroup(group) 118 | && Iterables.exists(status, hasStateMatching(pipelineState)) 119 | && label.matches(labelRegex); 120 | } 121 | 122 | private boolean matchesGroup(String group) { 123 | return StringUtils.isEmpty(groupRegex) || group.matches(groupRegex); 124 | } 125 | 126 | private Predicate hasStateMatching(final String pipelineState) { 127 | return new Predicate() { 128 | @Override 129 | public Boolean apply(PipelineStatus input) { 130 | return input.matches(pipelineState); 131 | } 132 | }; 133 | } 134 | 135 | @Override 136 | public boolean equals(Object o) { 137 | if (this == o) return true; 138 | if (o == null || getClass() != o.getClass()) return false; 139 | 140 | PipelineRule that = (PipelineRule) o; 141 | 142 | if (channel != null ? !channel.equals(that.channel) : that.channel != null) return false; 143 | if (nameRegex != null ? !nameRegex.equals(that.nameRegex) : that.nameRegex != null) return false; 144 | if (groupRegex != null ? !groupRegex.equals(that.groupRegex) : that.groupRegex != null) return false; 145 | if (labelRegex != null ? !labelRegex.equals(that.labelRegex) : that.labelRegex != null) return false; 146 | if (stageRegex != null ? !stageRegex.equals(that.stageRegex) : that.stageRegex != null) return false; 147 | if (status != null ? !status.equals(that.status) : that.status != null) return false; 148 | if (owners != null ? !owners.equals(that.owners) : that.owners != null) return false; 149 | if (webhookUrl != null ? !webhookUrl.equals(that.webhookUrl) : that.webhookUrl != null) return false; 150 | 151 | return true; 152 | } 153 | 154 | @Override 155 | public int hashCode() { 156 | int result = nameRegex != null ? nameRegex.hashCode() : 0; 157 | result = 31 * result + (groupRegex != null ? groupRegex.hashCode() : 0); 158 | result = 31 * result + (stageRegex != null ? stageRegex.hashCode() : 0); 159 | result = 31 * result + (labelRegex != null ? labelRegex.hashCode() : 0); 160 | result = 31 * result + (channel != null ? channel.hashCode() : 0); 161 | result = 31 * result + (status != null ? status.hashCode() : 0); 162 | result = 31 * result + (owners != null ? owners.hashCode() : 0); 163 | result = 31 * result + (webhookUrl != null ? webhookUrl.hashCode() : 0); 164 | return result; 165 | } 166 | 167 | @Override 168 | public String toString() { 169 | return "PipelineRule{" + 170 | "nameRegex='" + nameRegex + '\'' + 171 | ", groupRegex='" + groupRegex + '\'' + 172 | ", stageRegex='" + stageRegex + '\'' + 173 | ", labelRegex='" + labelRegex + '\'' + 174 | ", channel='" + channel + '\'' + 175 | ", status=" + status + 176 | ", owners=" + owners + 177 | ", webhookUrl=" + webhookUrl + 178 | '}'; 179 | } 180 | 181 | public static PipelineRule fromConfig(Config config) { 182 | PipelineRule pipelineRule = new PipelineRule(); 183 | pipelineRule.setNameRegex(config.getString("name")); 184 | if (config.hasPath("group")) { 185 | pipelineRule.setGroupRegex(config.getString("group")); 186 | } 187 | if (config.hasPath("stage")) { 188 | pipelineRule.setStageRegex(config.getString("stage")); 189 | } 190 | if (config.hasPath("label")) { 191 | pipelineRule.setLabelRegex(config.getString("label")); 192 | } 193 | if (config.hasPath("state")) { 194 | String stateT = config.getString("state"); 195 | String[] states = stateT.split("\\|"); 196 | Set status = new HashSet(); 197 | for (String state : states) { 198 | status.add(PipelineStatus.valueOf(state.toUpperCase())); 199 | } 200 | pipelineRule.setStatus(status); 201 | } 202 | if (config.hasPath("channel")) { 203 | pipelineRule.setChannel(config.getString("channel")); 204 | } 205 | if (config.hasPath("webhookUrl")) { 206 | pipelineRule.setWebhookUrl(config.getString("webhookUrl")); 207 | } 208 | if (config.hasPath("owners")) { 209 | List nonEmptyOwners = Lists.filter(config.getStringList("owners"), new Predicate() { 210 | @Override 211 | public Boolean apply(String input) { 212 | return StringUtils.isNotEmpty(input); 213 | } 214 | }); 215 | pipelineRule.getOwners().addAll(nonEmptyOwners); 216 | } 217 | 218 | return pipelineRule; 219 | } 220 | 221 | public static PipelineRule fromConfig(Config config, String channel) { 222 | PipelineRule pipelineRule = fromConfig(config); 223 | if (StringUtils.isEmpty(pipelineRule.getChannel())) { 224 | pipelineRule.setChannel(channel); 225 | } 226 | return pipelineRule; 227 | } 228 | 229 | public static PipelineRule merge(PipelineRule pipelineRule, PipelineRule defaultRule) { 230 | PipelineRule ruleToReturn = new PipelineRule(pipelineRule); 231 | if (isEmpty(pipelineRule.getNameRegex())) { 232 | ruleToReturn.setNameRegex(defaultRule.getNameRegex()); 233 | } 234 | 235 | if (isEmpty(pipelineRule.getGroupRegex())) { 236 | ruleToReturn.setGroupRegex(defaultRule.getGroupRegex()); 237 | } 238 | 239 | if (isEmpty(pipelineRule.getStageRegex())) { 240 | ruleToReturn.setStageRegex(defaultRule.getStageRegex()); 241 | } 242 | 243 | if (isEmpty(pipelineRule.getLabelRegex())) { 244 | ruleToReturn.setLabelRegex(defaultRule.getLabelRegex()); 245 | } 246 | 247 | if (isEmpty(pipelineRule.getChannel())) { 248 | ruleToReturn.setChannel(defaultRule.getChannel()); 249 | } 250 | 251 | if (isEmpty(pipelineRule.getWebhookUrl())) { 252 | ruleToReturn.setWebhookUrl(defaultRule.getWebhookUrl()); 253 | } 254 | 255 | if (pipelineRule.getStatus().isEmpty()) { 256 | ruleToReturn.setStatus(defaultRule.getStatus()); 257 | } else { 258 | ruleToReturn.getStatus().addAll(pipelineRule.getStatus()); 259 | } 260 | 261 | if (pipelineRule.getOwners().isEmpty()) { 262 | ruleToReturn.setOwners(defaultRule.getOwners()); 263 | } else { 264 | ruleToReturn.getOwners().addAll(pipelineRule.getOwners()); 265 | } 266 | 267 | return ruleToReturn; 268 | } 269 | 270 | } 271 | -------------------------------------------------------------------------------- /src/main/java/in/ashwanthkumar/gocd/slack/ruleset/PipelineStatus.java: -------------------------------------------------------------------------------- 1 | package in.ashwanthkumar.gocd.slack.ruleset; 2 | 3 | import in.ashwanthkumar.gocd.slack.GoNotificationMessage; 4 | import in.ashwanthkumar.gocd.slack.PipelineListener; 5 | 6 | public enum PipelineStatus { 7 | /** 8 | * Status of the pipeline while being built. 9 | */ 10 | BUILDING { 11 | @Override 12 | public void handle(PipelineListener listener, PipelineRule rule, GoNotificationMessage message) throws Exception { 13 | listener.onBuilding(rule, message); 14 | } 15 | }, 16 | /** 17 | * The pipeline has passed earlier and also now. 18 | */ 19 | PASSED { 20 | @Override 21 | public void handle(PipelineListener listener, PipelineRule rule, GoNotificationMessage message) throws Exception { 22 | listener.onPassed(rule, message); 23 | } 24 | }, 25 | /** 26 | * Pipeline has failed for the first time 27 | */ 28 | FAILED { 29 | @Override 30 | public void handle(PipelineListener listener, PipelineRule rule, GoNotificationMessage message) throws Exception { 31 | listener.onFailed(rule, message); 32 | } 33 | }, 34 | /** 35 | * Current and previous run of the pipeline failed hences broken 36 | */ 37 | BROKEN { 38 | @Override 39 | public void handle(PipelineListener listener, PipelineRule rule, GoNotificationMessage message) throws Exception { 40 | listener.onBroken(rule, message); 41 | } 42 | }, 43 | /** 44 | * Previous run has failed but now it succeeded 45 | */ 46 | FIXED { 47 | @Override 48 | public void handle(PipelineListener listener, PipelineRule rule, GoNotificationMessage message) throws Exception { 49 | listener.onFixed(rule, message); 50 | } 51 | }, 52 | /** 53 | * Pipeline is an unknown state (often temporary?) 54 | */ 55 | UNKNOWN { 56 | @Override 57 | public void handle(PipelineListener listener, PipelineRule rule, GoNotificationMessage message) throws Exception { 58 | /* 59 | * No-op - We never report this status. 60 | */ 61 | } 62 | }, 63 | /** 64 | * Pipeline has been cancelled. 65 | */ 66 | CANCELLED { 67 | @Override 68 | public void handle(PipelineListener listener, PipelineRule rule, GoNotificationMessage message) throws Exception { 69 | listener.onCancelled(rule, message); 70 | } 71 | }, 72 | /** 73 | * Pretty obvious ah? 74 | */ 75 | ALL { 76 | @Override 77 | public void handle(PipelineListener listener, PipelineRule rule, GoNotificationMessage message) throws Exception { 78 | /* 79 | * No-op - Since we use this flag only to denote handle all states but not the actual state itself. 80 | */ 81 | } 82 | }; 83 | 84 | public String verb() { 85 | switch (this) { 86 | case BROKEN: 87 | case FIXED: 88 | case BUILDING: 89 | return "is"; 90 | case FAILED: 91 | case PASSED: 92 | return "has"; 93 | case CANCELLED: 94 | return "was"; 95 | default: 96 | return ""; 97 | } 98 | } 99 | 100 | public boolean matches(String state) { 101 | return this == ALL || this == PipelineStatus.valueOf(state.toUpperCase()); 102 | } 103 | 104 | public abstract void handle(PipelineListener listener, PipelineRule rule, GoNotificationMessage message) throws Exception; 105 | } 106 | -------------------------------------------------------------------------------- /src/main/java/in/ashwanthkumar/gocd/slack/ruleset/Rules.java: -------------------------------------------------------------------------------- 1 | package in.ashwanthkumar.gocd.slack.ruleset; 2 | 3 | import com.thoughtworks.go.plugin.api.logging.Logger; 4 | import com.typesafe.config.Config; 5 | import in.ashwanthkumar.gocd.slack.PipelineListener; 6 | import in.ashwanthkumar.utils.collections.Lists; 7 | import in.ashwanthkumar.utils.func.Function; 8 | import in.ashwanthkumar.utils.func.Predicate; 9 | import in.ashwanthkumar.utils.lang.StringUtils; 10 | import in.ashwanthkumar.utils.lang.option.Option; 11 | 12 | import java.net.InetSocketAddress; 13 | import java.net.Proxy; 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | 17 | import static in.ashwanthkumar.gocd.slack.ruleset.PipelineRule.merge; 18 | 19 | public class Rules { 20 | 21 | private static Logger LOGGER = Logger.getLoggerFor(Rules.class); 22 | 23 | private boolean enabled; 24 | private String webHookUrl; 25 | private String slackChannel; 26 | private String slackDisplayName; 27 | private String slackUserIconURL; 28 | private String goServerHost; 29 | private String goAPIServerHost; 30 | private String goLogin; 31 | private String goPassword; 32 | private String goAPIToken; 33 | private boolean displayConsoleLogLinks; 34 | private boolean displayMaterialChanges; 35 | private boolean processAllRules; 36 | private boolean truncateChanges; 37 | 38 | private Proxy proxy; 39 | 40 | private List pipelineRules = new ArrayList(); 41 | private PipelineListener pipelineListener; 42 | 43 | public boolean isEnabled() { 44 | return enabled; 45 | } 46 | 47 | public Rules setEnabled(boolean enabled) { 48 | this.enabled = enabled; 49 | return this; 50 | } 51 | 52 | public String getWebHookUrl() { 53 | return webHookUrl; 54 | } 55 | 56 | public Rules setWebHookUrl(String webHookUrl) { 57 | this.webHookUrl = webHookUrl; 58 | return this; 59 | } 60 | 61 | public String getSlackChannel() { 62 | return slackChannel; 63 | } 64 | 65 | public Rules setSlackChannel(String slackChannel) { 66 | this.slackChannel = slackChannel; 67 | return this; 68 | } 69 | 70 | public String getSlackDisplayName() { 71 | return slackDisplayName; 72 | } 73 | 74 | private Rules setSlackDisplayName(String displayName) { 75 | this.slackDisplayName = displayName; 76 | return this; 77 | } 78 | 79 | public String getSlackUserIcon() { 80 | return slackUserIconURL; 81 | } 82 | 83 | private Rules setSlackUserIcon(String iconURL) { 84 | this.slackUserIconURL = iconURL; 85 | return this; 86 | } 87 | 88 | public List getPipelineRules() { 89 | return pipelineRules; 90 | } 91 | 92 | public Rules setPipelineRules(List pipelineRules) { 93 | this.pipelineRules = pipelineRules; 94 | return this; 95 | } 96 | 97 | public String getGoServerHost() { 98 | return goServerHost; 99 | } 100 | 101 | public Rules setGoServerHost(String goServerHost) { 102 | this.goServerHost = goServerHost; 103 | return this; 104 | } 105 | 106 | 107 | public String getGoAPIServerHost() { 108 | if (StringUtils.isNotEmpty(goAPIServerHost)) { 109 | return goAPIServerHost; 110 | } 111 | return getGoServerHost(); 112 | } 113 | 114 | public Rules setGoAPIServerHost(String goAPIServerHost) { 115 | this.goAPIServerHost = goAPIServerHost; 116 | return this; 117 | } 118 | 119 | public String getGoLogin() { 120 | return goLogin; 121 | } 122 | 123 | public Rules setGoLogin(String goLogin) { 124 | this.goLogin = goLogin; 125 | return this; 126 | } 127 | 128 | public String getGoPassword() { 129 | return goPassword; 130 | } 131 | 132 | public Rules setGoPassword(String goPassword) { 133 | this.goPassword = goPassword; 134 | return this; 135 | } 136 | 137 | public String getGoAPIToken() { 138 | return goAPIToken; 139 | } 140 | 141 | public Rules setGoAPIToken(String goAPIToken) { 142 | this.goAPIToken = goAPIToken; 143 | return this; 144 | } 145 | 146 | public boolean getDisplayConsoleLogLinks() { 147 | return displayConsoleLogLinks; 148 | } 149 | 150 | public Rules setDisplayConsoleLogLinks(boolean displayConsoleLogLinks) { 151 | this.displayConsoleLogLinks = displayConsoleLogLinks; 152 | return this; 153 | } 154 | 155 | public boolean getDisplayMaterialChanges() { 156 | return displayMaterialChanges; 157 | } 158 | 159 | public Rules setDisplayMaterialChanges(boolean displayMaterialChanges) { 160 | this.displayMaterialChanges = displayMaterialChanges; 161 | return this; 162 | } 163 | 164 | public boolean getProcessAllRules() { 165 | return processAllRules; 166 | } 167 | 168 | public Rules setProcessAllRules(boolean processAllRules) { 169 | this.processAllRules = processAllRules; 170 | return this; 171 | } 172 | 173 | public boolean isTruncateChanges() { 174 | return truncateChanges; 175 | } 176 | 177 | public Rules setTruncateChanges(boolean truncateChanges) { 178 | this.truncateChanges = truncateChanges; 179 | return this; 180 | } 181 | 182 | public Proxy getProxy() { 183 | return proxy; 184 | } 185 | 186 | public Rules setProxy(Proxy proxy) { 187 | this.proxy = proxy; 188 | return this; 189 | } 190 | 191 | public PipelineListener getPipelineListener() { 192 | return pipelineListener; 193 | } 194 | 195 | public List find(final String pipeline, final String stage, final String group, final String label, final String pipelineStatus) { 196 | Predicate predicate = new Predicate() { 197 | public Boolean apply(PipelineRule input) { 198 | return input.matches(pipeline, stage, group, label, pipelineStatus); 199 | } 200 | }; 201 | 202 | if(processAllRules) { 203 | return Lists.filter(pipelineRules, predicate); 204 | } else { 205 | List found = new ArrayList(); 206 | Option match = Lists.find(pipelineRules, predicate); 207 | if(match.isDefined()) { 208 | found.add(match.get()); 209 | } 210 | return found; 211 | } 212 | } 213 | 214 | public static Rules fromConfig(Config config) { 215 | boolean isEnabled = config.getBoolean("enabled"); 216 | 217 | String webhookUrl = config.getString("webhookUrl"); 218 | String channel = null; 219 | if (config.hasPath("channel")) { 220 | channel = config.getString("channel"); 221 | } 222 | 223 | String displayName = "gocd-slack-bot"; 224 | if (config.hasPath("slackDisplayName")) { 225 | displayName = config.getString("slackDisplayName"); 226 | } 227 | 228 | String iconURL = "https://raw.githubusercontent.com/ashwanthkumar/assets/c597777ee749c89fec7ce21304d727724a65be7d/images/gocd-logo.png"; 229 | if (config.hasPath("slackUserIconURL")) { 230 | iconURL = config.getString("slackUserIconURL"); 231 | } 232 | 233 | String serverHost = config.getString("server-host"); 234 | String apiServerHost = null; 235 | if (config.hasPath("api-server-host")) { 236 | apiServerHost = config.getString("api-server-host"); 237 | } 238 | String login = null; 239 | if (config.hasPath("login")) { 240 | login = config.getString("login"); 241 | } 242 | String password = null; 243 | if (config.hasPath("password")) { 244 | password = config.getString("password"); 245 | } 246 | 247 | String apiToken = null; 248 | if (config.hasPath("api-token")) { 249 | apiToken = config.getString("api-token"); 250 | } 251 | 252 | boolean displayConsoleLogLinks = true; 253 | if (config.hasPath("display-console-log-links")) { 254 | displayConsoleLogLinks = config.getBoolean("display-console-log-links"); 255 | } 256 | 257 | // TODO - Next major release - change this to - separated config 258 | boolean displayMaterialChanges = true; 259 | if (config.hasPath("displayMaterialChanges")) { 260 | displayMaterialChanges = config.getBoolean("displayMaterialChanges"); 261 | } 262 | 263 | boolean processAllRules = false; 264 | if (config.hasPath("process-all-rules")) { 265 | processAllRules = config.getBoolean("process-all-rules"); 266 | } 267 | 268 | boolean truncateChanges = true; 269 | if(config.hasPath("truncate-changes")) { 270 | truncateChanges = config.getBoolean("truncate-changes"); 271 | } 272 | 273 | Proxy proxy = null; 274 | if (config.hasPath("proxy")) { 275 | Config proxyConfig = config.getConfig("proxy"); 276 | if (proxyConfig.hasPath("hostname") && proxyConfig.hasPath("port") && proxyConfig.hasPath("type")) { 277 | String hostname = proxyConfig.getString("hostname"); 278 | int port = proxyConfig.getInt("port"); 279 | String type = proxyConfig.getString("type").toUpperCase(); 280 | Proxy.Type proxyType = Proxy.Type.valueOf(type); 281 | proxy = new Proxy(proxyType, new InetSocketAddress(hostname, port)); 282 | } 283 | } 284 | 285 | final PipelineRule defaultRule = PipelineRule.fromConfig(config.getConfig("default"), channel); 286 | 287 | List pipelineRules = Lists.map((List) config.getConfigList("pipelines"), new Function() { 288 | public PipelineRule apply(Config input) { 289 | return merge(PipelineRule.fromConfig(input), defaultRule); 290 | } 291 | }); 292 | 293 | Rules rules = new Rules() 294 | .setEnabled(isEnabled) 295 | .setWebHookUrl(webhookUrl) 296 | .setSlackChannel(channel) 297 | .setSlackDisplayName(displayName) 298 | .setSlackUserIcon(iconURL) 299 | .setPipelineRules(pipelineRules) 300 | .setGoServerHost(serverHost) 301 | .setGoAPIServerHost(apiServerHost) 302 | .setGoLogin(login) 303 | .setGoPassword(password) 304 | .setGoAPIToken(apiToken) 305 | .setDisplayConsoleLogLinks(displayConsoleLogLinks) 306 | .setDisplayMaterialChanges(displayMaterialChanges) 307 | .setProcessAllRules(processAllRules) 308 | .setTruncateChanges(truncateChanges) 309 | .setProxy(proxy); 310 | try { 311 | rules.pipelineListener = Class.forName(config.getString("listener")).asSubclass(PipelineListener.class).getConstructor(Rules.class).newInstance(rules); 312 | } catch (Exception e) { 313 | LOGGER.error("Exception while initializing pipeline listener", e); 314 | throw new RuntimeException(e); 315 | } 316 | 317 | return rules; 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /src/main/java/in/ashwanthkumar/gocd/slack/ruleset/RulesReader.java: -------------------------------------------------------------------------------- 1 | package in.ashwanthkumar.gocd.slack.ruleset; 2 | 3 | import com.thoughtworks.go.plugin.api.logging.Logger; 4 | import com.typesafe.config.Config; 5 | import com.typesafe.config.ConfigFactory; 6 | 7 | import java.io.File; 8 | 9 | public class RulesReader { 10 | private Logger LOG = Logger.getLoggerFor(RulesReader.class); 11 | 12 | public static Rules read() { 13 | return new RulesReader().load(); 14 | } 15 | 16 | public static Rules read(File file) { 17 | return new RulesReader().load(file); 18 | } 19 | 20 | public static Rules read(String file) { 21 | return new RulesReader().load(ConfigFactory.parseResources(file)); 22 | } 23 | 24 | protected Rules load(Config config) { 25 | Config envThenSystem = ConfigFactory.systemEnvironment().withFallback(ConfigFactory.systemProperties()); 26 | Config configWithFallback = config.withFallback(ConfigFactory.load(getClass().getClassLoader())).resolveWith(envThenSystem); 27 | return Rules.fromConfig(configWithFallback.getConfig("gocd.slack")); 28 | } 29 | 30 | public Rules load() { 31 | return load(ConfigFactory.load()); 32 | } 33 | 34 | public Rules load(File file) { 35 | return load(ConfigFactory.parseFile(file)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/in/ashwanthkumar/gocd/teams/CardHttpContent.java: -------------------------------------------------------------------------------- 1 | package in.ashwanthkumar.gocd.teams; 2 | 3 | import com.google.api.client.http.AbstractHttpContent; 4 | import com.google.api.client.json.Json; 5 | 6 | import java.io.IOException; 7 | import java.io.OutputStream; 8 | import java.io.OutputStreamWriter; 9 | 10 | /** 11 | * Serialize a {@link TeamsCard} for the Google HTTP Client. 12 | */ 13 | public class CardHttpContent extends AbstractHttpContent { 14 | private final TeamsCard card; 15 | 16 | protected CardHttpContent(TeamsCard card) { 17 | super(Json.MEDIA_TYPE); 18 | this.card = card; 19 | } 20 | 21 | @Override 22 | public void writeTo(OutputStream out) throws IOException { 23 | try (var osw = new OutputStreamWriter(out)) { 24 | osw.write(card.toString()); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/in/ashwanthkumar/gocd/teams/MessageCardSchema.java: -------------------------------------------------------------------------------- 1 | package in.ashwanthkumar.gocd.teams; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | import in.ashwanthkumar.gocd.slack.ruleset.PipelineStatus; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | /** 10 | * These objects create the MessageCard JSON sent to Teams using {@link com.google.gson.Gson}. 11 | * More details: 12 | * https://docs.microsoft.com/en-us/outlook/actionable-messages/message-card-reference 13 | */ 14 | public class MessageCardSchema { 15 | @SerializedName("@type") 16 | String type = "MessageCard"; 17 | String themeColor = Color.NONE.getHexCode(); 18 | String title = ""; 19 | /** 20 | * Not sure what this does, but a summary or text field is required. 21 | */ 22 | String summary = "GoCD build update"; 23 | List sections = new ArrayList<>(); 24 | List potentialAction = new ArrayList<>(); 25 | 26 | public enum Color { 27 | NONE(""), 28 | RED("990000"), 29 | GREEN("009900"); 30 | 31 | private final String hexCode; 32 | 33 | Color(String hexCode) { 34 | this.hexCode = hexCode; 35 | } 36 | 37 | public static Color findColor(PipelineStatus status) { 38 | switch (status) { 39 | case PASSED: 40 | case FIXED: 41 | return Color.GREEN; 42 | case FAILED: 43 | case BROKEN: 44 | return Color.RED; 45 | default: 46 | return Color.NONE; 47 | } 48 | } 49 | 50 | public String getHexCode() { 51 | return this.hexCode; 52 | } 53 | } 54 | 55 | public static class Fact { 56 | String name = ""; 57 | String value = ""; 58 | 59 | public Fact(String name, String value) { 60 | this.name = name; 61 | this.value = value; 62 | } 63 | } 64 | 65 | public static class FactSection { 66 | List facts = new ArrayList<>(); 67 | } 68 | 69 | public static class OpenUriAction { 70 | @SerializedName("@type") 71 | String type = "OpenUri"; 72 | String name = ""; 73 | List targets = new ArrayList<>(); 74 | 75 | public OpenUriAction(String name, String uri) { 76 | this.name = name; 77 | this.targets.add(new MessageCardSchema.Target(uri)); 78 | } 79 | } 80 | 81 | public static class Target { 82 | String os = "default"; 83 | String uri = ""; 84 | 85 | public Target(String uri) { 86 | this.uri = uri; 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/in/ashwanthkumar/gocd/teams/TeamsCard.java: -------------------------------------------------------------------------------- 1 | package in.ashwanthkumar.gocd.teams; 2 | 3 | import com.google.gson.Gson; 4 | 5 | /** 6 | * Populate the values of a Message Card for Teams. 7 | */ 8 | public class TeamsCard { 9 | private final MessageCardSchema.FactSection factSection = new MessageCardSchema.FactSection(); 10 | private final MessageCardSchema schema = new MessageCardSchema(); 11 | 12 | public TeamsCard() { 13 | this.schema.sections.add(this.factSection); 14 | } 15 | 16 | public void setTitle(String title) { 17 | this.schema.title = title; 18 | } 19 | 20 | public void addFact(String name, String value) { 21 | this.factSection.facts.add(new MessageCardSchema.Fact(name, value)); 22 | } 23 | 24 | @Override 25 | public String toString() { 26 | return new Gson().toJson(schema); 27 | } 28 | 29 | public void setColor(MessageCardSchema.Color color) { 30 | this.schema.themeColor = color.getHexCode(); 31 | } 32 | 33 | public void addLinkAction(String name, String uri) { 34 | this.schema.potentialAction.add(new MessageCardSchema.OpenUriAction(name, uri)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/in/ashwanthkumar/gocd/teams/TeamsPipelineListener.java: -------------------------------------------------------------------------------- 1 | package in.ashwanthkumar.gocd.teams; 2 | 3 | import com.thoughtworks.go.plugin.api.logging.Logger; 4 | import in.ashwanthkumar.gocd.slack.GoNotificationMessage; 5 | import in.ashwanthkumar.gocd.slack.PipelineListener; 6 | import in.ashwanthkumar.gocd.slack.jsonapi.Pipeline; 7 | import in.ashwanthkumar.gocd.slack.jsonapi.Stage; 8 | import in.ashwanthkumar.gocd.slack.ruleset.PipelineRule; 9 | import in.ashwanthkumar.gocd.slack.ruleset.PipelineStatus; 10 | import in.ashwanthkumar.gocd.slack.ruleset.Rules; 11 | 12 | import java.io.IOException; 13 | import java.net.URISyntaxException; 14 | 15 | /** 16 | * To enable this for Teams support add the following line to your config: 17 | * listener = "in.ashwanthkumar.gocd.teams.TeamsPipelineListener" 18 | * 19 | * @see in.ashwanthkumar.gocd.slack.SlackPipelineListener 20 | */ 21 | public class TeamsPipelineListener extends PipelineListener { 22 | private static final Logger LOG = Logger.getLoggerFor(TeamsPipelineListener.class); 23 | private final TeamsWebhook teams; 24 | 25 | public TeamsPipelineListener(Rules rules) { 26 | super(rules); 27 | teams = new TeamsWebhook(rules.getProxy()); 28 | } 29 | 30 | private String getWebhook(PipelineRule rule) { 31 | final String ruleWebhook = rule.getWebhookUrl(); 32 | if (ruleWebhook != null && !ruleWebhook.isEmpty()) { 33 | return ruleWebhook; 34 | } else { 35 | return this.rules.getWebHookUrl(); 36 | } 37 | } 38 | 39 | private void sendMessage(PipelineRule rule, GoNotificationMessage message, PipelineStatus status) 40 | throws URISyntaxException, IOException { 41 | final TeamsCard card = new TeamsCard(); 42 | card.setColor(MessageCardSchema.Color.findColor(status)); 43 | card.addLinkAction("Details", message.goServerUrl(rules.getGoServerHost())); 44 | card.setTitle(String.format("Stage [%s] %s %s", 45 | message.fullyQualifiedJobName(), 46 | status.verb(), 47 | status) 48 | .replaceAll("\\s+", " ")); 49 | 50 | try { 51 | Pipeline details = message.fetchDetails(rules); 52 | Stage stage = message.pickCurrentStage(details.stages); 53 | card.addFact("Triggered by", stage.approvedBy); 54 | card.addFact("Reason", details.buildCause.triggerMessage); 55 | card.addFact("Label", details.label); 56 | } catch (Exception ex) { 57 | card.addFact("Internal Error", "Problem with build details; see server log"); 58 | LOG.warn("Problem with build details", ex); 59 | } 60 | 61 | teams.send(getWebhook(rule), card); 62 | } 63 | 64 | 65 | @Override 66 | public void onBuilding(PipelineRule rule, GoNotificationMessage message) throws Exception { 67 | sendMessage(rule, message, PipelineStatus.BUILDING); 68 | } 69 | 70 | @Override 71 | public void onPassed(PipelineRule rule, GoNotificationMessage message) throws Exception { 72 | sendMessage(rule, message, PipelineStatus.PASSED); 73 | } 74 | 75 | @Override 76 | public void onFailed(PipelineRule rule, GoNotificationMessage message) throws Exception { 77 | sendMessage(rule, message, PipelineStatus.FAILED); 78 | } 79 | 80 | @Override 81 | public void onBroken(PipelineRule rule, GoNotificationMessage message) throws Exception { 82 | sendMessage(rule, message, PipelineStatus.BROKEN); 83 | } 84 | 85 | @Override 86 | public void onFixed(PipelineRule rule, GoNotificationMessage message) throws Exception { 87 | sendMessage(rule, message, PipelineStatus.FIXED); 88 | } 89 | 90 | @Override 91 | public void onCancelled(PipelineRule rule, GoNotificationMessage message) throws Exception { 92 | sendMessage(rule, message, PipelineStatus.CANCELLED); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/in/ashwanthkumar/gocd/teams/TeamsWebhook.java: -------------------------------------------------------------------------------- 1 | package in.ashwanthkumar.gocd.teams; 2 | 3 | import com.google.api.client.http.GenericUrl; 4 | import com.google.api.client.http.HttpRequestFactory; 5 | import com.google.api.client.http.javanet.NetHttpTransport; 6 | import com.thoughtworks.go.plugin.api.logging.Logger; 7 | 8 | import java.io.IOException; 9 | import java.net.Proxy; 10 | import java.util.Objects; 11 | 12 | /** 13 | * Sends post requests to a Teams channels incoming webhook. 14 | * Uses Google HTTP Client. 15 | */ 16 | public class TeamsWebhook { 17 | private static final Logger LOG = Logger.getLoggerFor(TeamsWebhook.class); 18 | private final HttpRequestFactory requestFactory; 19 | 20 | public TeamsWebhook(Proxy proxy) { 21 | requestFactory = new NetHttpTransport.Builder() 22 | .setProxy(proxy) 23 | .build() 24 | .createRequestFactory(); 25 | } 26 | 27 | public void send(String webhookUrl, TeamsCard card) throws IOException { 28 | Objects.requireNonNull(webhookUrl); 29 | Objects.requireNonNull(card); 30 | LOG.debug("Using webhook: " + webhookUrl); 31 | LOG.debug("Sending Card: " + card); 32 | requestFactory.buildPostRequest( 33 | new GenericUrl(webhookUrl), 34 | new CardHttpContent(card) 35 | ) 36 | .execute(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/resources/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Slack Notification Plugin 5 | 2.1.0-beta 6 | 20.1.0 7 | Plugin to send build notifications to slack 8 | 9 | Ashwanth Kumar 10 | https://github.com/ashwanthkumar/gocd-slack-build-notifier 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/main/resources/reference.conf: -------------------------------------------------------------------------------- 1 | gocd.slack { 2 | # feature flag for notification plugin, turning this false will not post anything to Slack 3 | # quite useful while testing / debugging 4 | enabled = true 5 | 6 | # Enter full FQDN of your GoCD instance. We'll be sending links on your slack channel using this as the base uri. 7 | #server-host = "http://go.cd/" # Mandatory Field 8 | 9 | # If you have security enabled, you'll need to provide a username and 10 | # password for your GoCD server to get more detailed logging. 11 | #login = "admin" 12 | #password = "tiger" 13 | 14 | # Global default channel for all pipelines, these can be overriden at a pipeline level as well 15 | #channel = "@ashwanthkumar" # Defaults to the webhook configured channel 16 | 17 | # Setup up an incoming webhook in your slack team on https://my.slack.com/services/new/incoming-webhook/ 18 | #webhookUrl: "" # Mandatory field 19 | 20 | # If you don't want to see the console log links in the notification (for size concerns). 21 | # Defaults to true. 22 | #display-console-log-links = true 23 | 24 | # If you don't want to see the revision changes in the notification (for size or confidentiality concerns) 25 | # defaults to true 26 | #displayMaterialChanges = true 27 | 28 | # TODO - Implementation is not yet pluggable 29 | listener = "in.ashwanthkumar.gocd.slack.SlackPipelineListener" 30 | 31 | # Default settings for pipelines 32 | default { 33 | name = ".*" 34 | stage = ".*" 35 | group = ".*" 36 | label = ".*" 37 | # you can provide multiple values by separating them with | (pipe) symbol - failed|broken 38 | state = "broken|failed|fixed|cancelled" # accepted values - failed / broken / fixed / passed / cancelled / all 39 | #channel = "gocd" # Mandatory field 40 | } 41 | 42 | # Example settings would be like 43 | # pipelines = [{ 44 | # nameRegex = "upc14" 45 | # channel = "#" 46 | # state = "failed|broken" 47 | # }] 48 | pipelines = [{ 49 | name = ".*" 50 | stage = ".*" 51 | state = "broken|failed|fixed|cancelled" 52 | }] 53 | } 54 | 55 | -------------------------------------------------------------------------------- /src/main/resources/views/config.template.html: -------------------------------------------------------------------------------- 1 |
2 |

Slack Notifier Plugin

3 |

Note - Values here are for demonstration purpose only

4 |
5 | 6 |
7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 23 | 29 | 33 | 37 | 38 | 39 | 40 |
 

Pipeline | Stage | Job

Channel

Owners

21 | 22 | 24 | 28 | 30 | 32 | 34 | 36 |
41 | 42 | Add 44 | {{ GOINPUTNAME[pipelineConfig].$error.server }} 45 |
46 |
47 | 48 | 119 | 124 | -------------------------------------------------------------------------------- /src/test/java/in/ashwanthkumar/gocd/slack/GoNotificationMessageTest.java: -------------------------------------------------------------------------------- 1 | package in.ashwanthkumar.gocd.slack; 2 | 3 | import in.ashwanthkumar.gocd.slack.jsonapi.*; 4 | import in.ashwanthkumar.gocd.slack.ruleset.Rules; 5 | import in.ashwanthkumar.gocd.slack.util.TestUtils; 6 | import in.ashwanthkumar.utils.collections.Lists; 7 | import org.junit.Test; 8 | 9 | import java.util.List; 10 | 11 | import static org.hamcrest.CoreMatchers.is; 12 | import static org.junit.Assert.assertThat; 13 | import static org.mockito.Mockito.mock; 14 | import static org.mockito.Mockito.when; 15 | 16 | public class GoNotificationMessageTest { 17 | 18 | private static final String PIPELINE_NAME = "pipeline"; 19 | 20 | @Test 21 | public void shouldFetchPipelineDetails() throws Exception { 22 | Server server = mock(Server.class); 23 | 24 | History pipelineHistory = new History(); 25 | pipelineHistory.pipelines = new Pipeline[]{ 26 | pipeline(PIPELINE_NAME, 8), 27 | pipeline(PIPELINE_NAME, 9), 28 | pipeline(PIPELINE_NAME, 10), 29 | pipeline(PIPELINE_NAME, 11), 30 | pipeline(PIPELINE_NAME, 12) 31 | }; 32 | when(server.getPipelineHistory(PIPELINE_NAME)).thenReturn(pipelineHistory); 33 | 34 | GoNotificationMessage message = new GoNotificationMessage( 35 | TestUtils.createMockServerFactory(server), 36 | info(PIPELINE_NAME, 10) 37 | ); 38 | 39 | Pipeline result = message.fetchDetails(new Rules()); 40 | 41 | assertThat(result.name, is(PIPELINE_NAME)); 42 | assertThat(result.counter, is(10)); 43 | } 44 | 45 | @Test(expected = GoNotificationMessage.BuildDetailsNotFoundException.class) 46 | public void shouldFetchPipelineDetailsNotFound() throws Exception { 47 | Server server = mock(Server.class); 48 | 49 | History pipelineHistory = new History(); 50 | pipelineHistory.pipelines = new Pipeline[]{ 51 | pipeline(PIPELINE_NAME, 8), 52 | pipeline(PIPELINE_NAME, 9) 53 | }; 54 | when(server.getPipelineHistory(PIPELINE_NAME)).thenReturn(pipelineHistory); 55 | 56 | GoNotificationMessage message = new GoNotificationMessage( 57 | TestUtils.createMockServerFactory(server), 58 | info(PIPELINE_NAME, 10) 59 | ); 60 | 61 | message.fetchDetails(new Rules()); 62 | } 63 | 64 | @Test(expected = GoNotificationMessage.BuildDetailsNotFoundException.class) 65 | public void shouldFetchPipelineDetailsNothingFound() throws Exception { 66 | Server server = mock(Server.class); 67 | 68 | History pipelineHistory = new History(); 69 | pipelineHistory.pipelines = new Pipeline[]{ 70 | pipeline("something-different", 10) 71 | }; 72 | when(server.getPipelineHistory("something-different")).thenReturn(pipelineHistory); 73 | 74 | GoNotificationMessage message = new GoNotificationMessage( 75 | TestUtils.createMockServerFactory(server), 76 | info(PIPELINE_NAME, 10) 77 | ); 78 | 79 | message.fetchDetails(new Rules()); 80 | } 81 | 82 | @Test 83 | public void shouldFetchChanges() throws Exception { 84 | Server server = mock(Server.class); 85 | 86 | Pipeline pipeline1 = new Pipeline(); 87 | { 88 | pipeline1.buildCause = new BuildCause(); 89 | 90 | MaterialRevision leafRevision = new MaterialRevision(); 91 | leafRevision.material = new Material(); 92 | leafRevision.material.type = "Something"; 93 | leafRevision.material.id = 1338; 94 | leafRevision.changed = true; 95 | 96 | MaterialRevision pipelineRevision = new MaterialRevision(); 97 | pipelineRevision.material = new Material(); 98 | pipelineRevision.material.type = "Pipeline"; 99 | pipelineRevision.changed = true; 100 | 101 | Modification modification = new Modification(); 102 | modification.revision = "pipeline2/11/foo"; 103 | 104 | pipelineRevision.modifications = Lists.of(modification); 105 | pipeline1.buildCause.materialRevisions = new MaterialRevision[]{ 106 | leafRevision, pipelineRevision 107 | }; 108 | } 109 | Pipeline pipeline2 = new Pipeline(); 110 | { 111 | pipeline2.buildCause = new BuildCause(); 112 | 113 | MaterialRevision leafRevision = new MaterialRevision(); 114 | leafRevision.material = new Material(); 115 | leafRevision.material.type = "Something other"; 116 | leafRevision.material.id = 1337; 117 | leafRevision.changed = true; 118 | 119 | pipeline2.buildCause.materialRevisions = new MaterialRevision[]{ 120 | leafRevision 121 | }; 122 | } 123 | when(server.getPipelineInstance("pipeline1", 10)).thenReturn(pipeline1); 124 | when(server.getPipelineInstance("pipeline2", 11)).thenReturn(pipeline2); 125 | 126 | GoNotificationMessage message = new GoNotificationMessage( 127 | TestUtils.createMockServerFactory(server), 128 | info("pipeline1", 10) 129 | ); 130 | 131 | List revisions = message.fetchChanges(new Rules()); 132 | 133 | assertThat(revisions.size(), is(2)); 134 | } 135 | 136 | private static Pipeline pipeline(String name, int counter) { 137 | Pipeline pipeline = new Pipeline(); 138 | pipeline.name = name; 139 | pipeline.counter = counter; 140 | return pipeline; 141 | } 142 | 143 | private static GoNotificationMessage.PipelineInfo info(String name, int counter) { 144 | GoNotificationMessage.PipelineInfo pipeline = new GoNotificationMessage.PipelineInfo(); 145 | pipeline.counter = Integer.toString(counter); 146 | pipeline.name = name; 147 | return pipeline; 148 | } 149 | 150 | } 151 | -------------------------------------------------------------------------------- /src/test/java/in/ashwanthkumar/gocd/slack/GoNotificationMessage_FixStageTest.java: -------------------------------------------------------------------------------- 1 | package in.ashwanthkumar.gocd.slack; 2 | 3 | import in.ashwanthkumar.gocd.slack.jsonapi.*; 4 | import in.ashwanthkumar.gocd.slack.ruleset.Rules; 5 | import in.ashwanthkumar.gocd.slack.util.TestUtils; 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | import org.junit.runners.Parameterized; 9 | 10 | import java.io.IOException; 11 | import java.util.*; 12 | 13 | import static org.hamcrest.CoreMatchers.is; 14 | import static org.junit.Assert.assertThat; 15 | import static org.mockito.Mockito.mock; 16 | import static org.mockito.Mockito.when; 17 | 18 | @RunWith(Parameterized.class) 19 | public class GoNotificationMessage_FixStageTest { 20 | 21 | public static final String PIPELINE_NAME = "PL"; 22 | public static final String STAGE_NAME = "STG"; 23 | 24 | private History pipelineHistory; 25 | private GoNotificationMessage.PipelineInfo pipeline; 26 | private String expectedStatus; 27 | 28 | public GoNotificationMessage_FixStageTest(History pipelineHistory, GoNotificationMessage.PipelineInfo pipeline, String expectedStatus) { 29 | this.pipelineHistory = pipelineHistory; 30 | this.pipeline = pipeline; 31 | this.expectedStatus = expectedStatus; 32 | } 33 | 34 | @Parameterized.Parameters(name = "{index}: Pipeline <{0}> to <{1}> should return status {2}") 35 | public static Collection data() { 36 | return Arrays.asList(new Object[][] { 37 | // One history pipeline, same pipeline run 38 | { 39 | givenHistory(pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(1), Status.Failed))), 40 | whenPipelineFinished(pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(2), Status.Building))), 41 | thenExpectStatus(Status.Building) 42 | }, { 43 | givenHistory(pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(1), Status.Failed))), 44 | whenPipelineFinished(pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(2), Status.Passed))), 45 | thenExpectStatus(Status.Fixed) 46 | }, { 47 | givenHistory(pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(1), Status.Failed))), 48 | whenPipelineFinished(pipeline(PIPELINE_NAME, counter(2), stage(STAGE_NAME, counter(2), Status.Passed))), 49 | thenExpectStatus(Status.Fixed) 50 | }, { 51 | givenHistory(pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(1), Status.Passed))), 52 | whenPipelineFinished(pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(2), Status.Failed))), 53 | thenExpectStatus(Status.Broken) 54 | }, { 55 | givenHistory(pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(1), Status.Passed))), 56 | whenPipelineFinished(pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(2), Status.Passed))), 57 | thenExpectStatus(Status.Passed) 58 | }, { 59 | givenHistory(pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(1), Status.Failed))), 60 | whenPipelineFinished(pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(2), Status.Failed))), 61 | thenExpectStatus(Status.Failed) 62 | }, { 63 | givenHistory(pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(1), Status.Cancelled))), 64 | whenPipelineFinished(pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(2), Status.Passed))), 65 | thenExpectStatus(Status.Fixed) 66 | }, { 67 | givenHistory(pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(1), Status.Cancelled))), 68 | whenPipelineFinished(pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(2), Status.Failed))), 69 | thenExpectStatus(Status.Failed) 70 | }, { 71 | givenHistory(pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(1), Status.Cancelled))), 72 | whenPipelineFinished(pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(2), Status.Cancelled))), 73 | thenExpectStatus(Status.Cancelled) 74 | }, 75 | 76 | // Multiple stages 77 | { 78 | givenHistory(pipeline(PIPELINE_NAME, counter(1), 79 | stage("other-stage-name-1", counter(1), Status.Failed), 80 | stage(STAGE_NAME, counter(1), Status.Failed), 81 | stage("other-stage-name-2", counter(1), Status.Failed))), 82 | whenPipelineFinished(pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(4), Status.Passed))), 83 | thenExpectStatus(Status.Fixed) 84 | }, { 85 | givenHistory(pipeline(PIPELINE_NAME, counter(1), 86 | stage("other-stage-name-1", counter(1), Status.Passed), 87 | stage(STAGE_NAME, counter(1), Status.Failed), 88 | stage("other-stage-name-2", counter(1), Status.Passed))), 89 | whenPipelineFinished(pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(4), Status.Passed))), 90 | thenExpectStatus(Status.Fixed) 91 | }, 92 | 93 | // One history pipeline, next pipeline run 94 | { 95 | givenHistory(pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(1), Status.Passed))), 96 | whenPipelineFinished(pipeline(PIPELINE_NAME, counter(2), stage(STAGE_NAME, counter(1), Status.Failed))), 97 | thenExpectStatus(Status.Broken) 98 | }, { 99 | givenHistory(pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(1), Status.Passed))), 100 | whenPipelineFinished(pipeline(PIPELINE_NAME, counter(2), stage(STAGE_NAME, counter(1), Status.Passed))), 101 | thenExpectStatus(Status.Passed) 102 | }, { 103 | givenHistory(pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(1), Status.Failed))), 104 | whenPipelineFinished(pipeline(PIPELINE_NAME, counter(2), stage(STAGE_NAME, counter(1), Status.Passed))), 105 | thenExpectStatus(Status.Fixed) 106 | }, { 107 | givenHistory(pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(1), Status.Failed))), 108 | whenPipelineFinished(pipeline(PIPELINE_NAME, counter(2), stage(STAGE_NAME, counter(1), Status.Failed))), 109 | thenExpectStatus(Status.Failed) 110 | }, { 111 | givenHistory(pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(1), Status.Failed))), 112 | whenPipelineFinished(pipeline(PIPELINE_NAME, counter(2), stage(STAGE_NAME, counter(1), Status.Cancelled))), 113 | thenExpectStatus(Status.Cancelled) 114 | }, { 115 | givenHistory(pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(1), Status.Passed))), 116 | whenPipelineFinished(pipeline(PIPELINE_NAME, counter(2), stage(STAGE_NAME, counter(1), Status.Cancelled))), 117 | thenExpectStatus(Status.Cancelled) 118 | }, 119 | // No history 120 | { 121 | givenHistory(noPipelines()), 122 | whenPipelineFinished(pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(1), Status.Passed))), 123 | thenExpectStatus(Status.Passed) 124 | }, { 125 | givenHistory(noPipelines()), 126 | whenPipelineFinished(pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(1), Status.Failed))), 127 | thenExpectStatus(Status.Failed) 128 | }, { 129 | givenHistory(noPipelines()), 130 | whenPipelineFinished(pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(1), Status.Cancelled))), 131 | thenExpectStatus(Status.Cancelled) 132 | }, { 133 | givenHistory(noPipelines()), 134 | whenPipelineFinished(pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(1), Status.Building))), 135 | thenExpectStatus(Status.Building) 136 | }, 137 | // Longer history, next pipeline run 138 | { 139 | givenHistory(pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(1), Status.Failed)), 140 | pipeline(PIPELINE_NAME, counter(2), stage(STAGE_NAME, counter(1), Status.Failed)), 141 | pipeline(PIPELINE_NAME, counter(3), stage(STAGE_NAME, counter(1), Status.Failed))), 142 | whenPipelineFinished(pipeline(PIPELINE_NAME, counter(4), stage(STAGE_NAME, counter(1), Status.Passed))), 143 | thenExpectStatus(Status.Fixed) 144 | }, { 145 | givenHistory( 146 | pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(1), Status.Passed)), 147 | pipeline(PIPELINE_NAME, counter(2), stage(STAGE_NAME, counter(1), Status.Passed)), 148 | pipeline(PIPELINE_NAME, counter(3), stage(STAGE_NAME, counter(1), Status.Failed)) 149 | ), 150 | whenPipelineFinished(pipeline(PIPELINE_NAME, counter(4), stage(STAGE_NAME, counter(1), Status.Passed))), 151 | thenExpectStatus(Status.Fixed) 152 | }, { 153 | givenHistory( 154 | pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(1), Status.Passed)), 155 | pipeline(PIPELINE_NAME, counter(2), stage(STAGE_NAME, counter(1), Status.Passed)), 156 | pipeline(PIPELINE_NAME, counter(3), stage(STAGE_NAME, counter(1), Status.Failed)), 157 | pipeline(PIPELINE_NAME, counter(3), stage(STAGE_NAME, counter(2), Status.Failed)) 158 | ), 159 | whenPipelineFinished(pipeline(PIPELINE_NAME, counter(4), stage(STAGE_NAME, counter(1), Status.Passed))), 160 | thenExpectStatus(Status.Fixed) 161 | }, { 162 | givenHistory( 163 | pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(1), Status.Passed)), 164 | pipeline(PIPELINE_NAME, counter(2), stage(STAGE_NAME, counter(1), Status.Passed)), 165 | pipeline(PIPELINE_NAME, counter(3), stage(STAGE_NAME, counter(1), Status.Passed)) 166 | ), 167 | whenPipelineFinished(pipeline(PIPELINE_NAME, counter(4), stage(STAGE_NAME, counter(1), Status.Failed))), 168 | thenExpectStatus(Status.Broken) 169 | }, { 170 | givenHistory( 171 | pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(1), Status.Passed)), 172 | pipeline(PIPELINE_NAME, counter(2), stage(STAGE_NAME, counter(1), Status.Passed)), 173 | pipeline(PIPELINE_NAME, counter(3), stage(STAGE_NAME, counter(1), Status.Failed)), 174 | pipeline(PIPELINE_NAME, counter(3), stage(STAGE_NAME, counter(2), Status.Passed)) 175 | ), 176 | whenPipelineFinished(pipeline(PIPELINE_NAME, counter(4), stage(STAGE_NAME, counter(1), Status.Failed))), 177 | thenExpectStatus(Status.Broken) 178 | }, { 179 | givenHistory( 180 | pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(1), Status.Passed)), 181 | pipeline(PIPELINE_NAME, counter(2), stage(STAGE_NAME, counter(1), Status.Passed)), 182 | pipeline(PIPELINE_NAME, counter(3), stage(STAGE_NAME, counter(1), Status.Passed)), 183 | pipeline(PIPELINE_NAME, counter(3), stage(STAGE_NAME, counter(2), Status.Failed)) 184 | ), 185 | whenPipelineFinished(pipeline(PIPELINE_NAME, counter(4), stage(STAGE_NAME, counter(1), Status.Failed))), 186 | thenExpectStatus(Status.Failed) 187 | }, { 188 | givenHistory( 189 | pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(1), Status.Passed)), 190 | pipeline(PIPELINE_NAME, counter(2), stage(STAGE_NAME, counter(1), Status.Passed)), 191 | pipeline(PIPELINE_NAME, counter(3), stage(STAGE_NAME, counter(1), Status.Passed)), 192 | pipeline(PIPELINE_NAME, counter(3), stage(STAGE_NAME, counter(2), Status.Passed)) 193 | ), 194 | whenPipelineFinished(pipeline(PIPELINE_NAME, counter(4), stage(STAGE_NAME, counter(1), Status.Failed))), 195 | thenExpectStatus(Status.Broken) 196 | }, { 197 | givenHistory( 198 | pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(1), Status.Passed)), 199 | pipeline(PIPELINE_NAME, counter(2), stage(STAGE_NAME, counter(1), Status.Passed)), 200 | pipeline(PIPELINE_NAME, counter(3), stage(STAGE_NAME, counter(1), Status.Passed)), 201 | pipeline(PIPELINE_NAME, counter(3), stage(STAGE_NAME, counter(2), Status.Passed)) 202 | ), 203 | whenPipelineFinished(pipeline(PIPELINE_NAME, counter(4), stage(STAGE_NAME, counter(1), Status.Cancelled))), 204 | thenExpectStatus(Status.Cancelled) 205 | }, { 206 | givenHistory( 207 | pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(1), Status.Passed)), 208 | pipeline(PIPELINE_NAME, counter(2), stage(STAGE_NAME, counter(1), Status.Passed)), 209 | pipeline(PIPELINE_NAME, counter(3), stage(STAGE_NAME, counter(1), Status.Passed)), 210 | pipeline(PIPELINE_NAME, counter(3), stage(STAGE_NAME, counter(2), Status.Failed)) 211 | ), 212 | whenPipelineFinished(pipeline(PIPELINE_NAME, counter(4), stage(STAGE_NAME, counter(1), Status.Cancelled))), 213 | thenExpectStatus(Status.Cancelled) 214 | }, { 215 | givenHistory( 216 | pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(1), Status.Passed)), 217 | pipeline(PIPELINE_NAME, counter(2), stage(STAGE_NAME, counter(1), Status.Passed)), 218 | pipeline(PIPELINE_NAME, counter(3), stage(STAGE_NAME, counter(1), Status.Failed)), 219 | pipeline(PIPELINE_NAME, counter(3), stage(STAGE_NAME, counter(2), Status.Passed)) 220 | ), 221 | whenPipelineFinished(pipeline(PIPELINE_NAME, counter(4), stage(STAGE_NAME, counter(1), Status.Passed))), 222 | thenExpectStatus(Status.Passed) 223 | }, { 224 | givenHistory( 225 | pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(1), Status.Failed)), 226 | pipeline(PIPELINE_NAME, counter(2), stage(STAGE_NAME, counter(1), Status.Failed)), 227 | pipeline(PIPELINE_NAME, counter(3), stage(STAGE_NAME, counter(1), Status.Passed)), 228 | pipeline(PIPELINE_NAME, counter(3), stage(STAGE_NAME, counter(2), Status.Failed)) 229 | ), 230 | whenPipelineFinished(pipeline(PIPELINE_NAME, counter(4), stage(STAGE_NAME, counter(1), Status.Failed))), 231 | thenExpectStatus(Status.Failed) 232 | }, 233 | // Longer history, same pipeline as the last in history 234 | { 235 | givenHistory( 236 | pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(1), Status.Passed)), 237 | pipeline(PIPELINE_NAME, counter(2), stage(STAGE_NAME, counter(1), Status.Passed)), 238 | pipeline(PIPELINE_NAME, counter(3), stage(STAGE_NAME, counter(1), Status.Passed)), 239 | pipeline(PIPELINE_NAME, counter(3), stage(STAGE_NAME, counter(2), Status.Passed)) 240 | ), 241 | whenPipelineFinished(pipeline(PIPELINE_NAME, counter(3), stage(STAGE_NAME, counter(3), Status.Failed))), 242 | thenExpectStatus(Status.Broken) 243 | }, { 244 | givenHistory( 245 | pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(1), Status.Passed)), 246 | pipeline(PIPELINE_NAME, counter(2), stage(STAGE_NAME, counter(1), Status.Passed)), 247 | pipeline(PIPELINE_NAME, counter(3), stage(STAGE_NAME, counter(1), Status.Passed)), 248 | pipeline(PIPELINE_NAME, counter(3), stage(STAGE_NAME, counter(2), Status.Failed)) 249 | ), 250 | whenPipelineFinished(pipeline(PIPELINE_NAME, counter(3), stage(STAGE_NAME, counter(3), Status.Passed))), 251 | thenExpectStatus(Status.Fixed) 252 | }, { 253 | givenHistory( 254 | pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(1), Status.Passed)), 255 | pipeline(PIPELINE_NAME, counter(2), stage(STAGE_NAME, counter(1), Status.Passed)), 256 | pipeline(PIPELINE_NAME, counter(3), stage(STAGE_NAME, counter(1), Status.Passed)), 257 | pipeline(PIPELINE_NAME, counter(3), stage(STAGE_NAME, counter(2), Status.Failed)) 258 | ), 259 | whenPipelineFinished(pipeline(PIPELINE_NAME, counter(3), stage(STAGE_NAME, counter(3), Status.Failed))), 260 | thenExpectStatus(Status.Failed) 261 | }, { 262 | givenHistory( 263 | pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(1), Status.Passed)), 264 | pipeline(PIPELINE_NAME, counter(2), stage(STAGE_NAME, counter(1), Status.Passed)), 265 | pipeline(PIPELINE_NAME, counter(3), stage(STAGE_NAME, counter(1), Status.Passed)), 266 | pipeline(PIPELINE_NAME, counter(3), stage(STAGE_NAME, counter(2), Status.Passed)) 267 | ), 268 | whenPipelineFinished(pipeline(PIPELINE_NAME, counter(3), stage(STAGE_NAME, counter(3), Status.Passed))), 269 | thenExpectStatus(Status.Passed) 270 | }, { 271 | givenHistory( 272 | pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(1), Status.Passed)), 273 | pipeline(PIPELINE_NAME, counter(2), stage(STAGE_NAME, counter(1), Status.Passed)), 274 | pipeline(PIPELINE_NAME, counter(3), stage(STAGE_NAME, counter(1), Status.Passed)), 275 | pipeline(PIPELINE_NAME, counter(3), stage(STAGE_NAME, counter(2), Status.Failed)) 276 | ), 277 | whenPipelineFinished(pipeline(PIPELINE_NAME, counter(3), stage(STAGE_NAME, counter(3), Status.Cancelled))), 278 | thenExpectStatus(Status.Cancelled) 279 | }, { 280 | givenHistory( 281 | pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(1), Status.Passed)), 282 | pipeline(PIPELINE_NAME, counter(2), stage(STAGE_NAME, counter(1), Status.Passed)), 283 | pipeline(PIPELINE_NAME, counter(3), stage(STAGE_NAME, counter(1), Status.Passed)), 284 | pipeline(PIPELINE_NAME, counter(3), stage(STAGE_NAME, counter(2), Status.Cancelled)) 285 | ), 286 | whenPipelineFinished(pipeline(PIPELINE_NAME, counter(3), stage(STAGE_NAME, counter(3), Status.Failed))), 287 | thenExpectStatus(Status.Failed) 288 | }, { 289 | givenHistory( 290 | pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(1), Status.Passed)), 291 | pipeline(PIPELINE_NAME, counter(2), stage(STAGE_NAME, counter(1), Status.Passed)), 292 | pipeline(PIPELINE_NAME, counter(3), stage(STAGE_NAME, counter(1), Status.Passed)), 293 | pipeline(PIPELINE_NAME, counter(3), stage(STAGE_NAME, counter(2), Status.Cancelled)) 294 | ), 295 | whenPipelineFinished(pipeline(PIPELINE_NAME, counter(3), stage(STAGE_NAME, counter(3), Status.Passed))), 296 | thenExpectStatus(Status.Fixed) 297 | }, { 298 | givenHistory( 299 | pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(1), Status.Passed)), 300 | pipeline(PIPELINE_NAME, counter(2), stage(STAGE_NAME, counter(1), Status.Passed)), 301 | pipeline(PIPELINE_NAME, counter(3), stage(STAGE_NAME, counter(1), Status.Failed)), 302 | pipeline(PIPELINE_NAME, counter(3), stage(STAGE_NAME, counter(2), Status.Passed)) 303 | ), 304 | whenPipelineFinished(pipeline(PIPELINE_NAME, counter(3), stage(STAGE_NAME, counter(3), Status.Passed))), 305 | thenExpectStatus(Status.Passed) 306 | }, { 307 | givenHistory( 308 | pipeline(PIPELINE_NAME, counter(1), stage(STAGE_NAME, counter(1), Status.Failed)), 309 | pipeline(PIPELINE_NAME, counter(2), stage(STAGE_NAME, counter(1), Status.Failed)), 310 | pipeline(PIPELINE_NAME, counter(3), stage(STAGE_NAME, counter(1), Status.Passed)), 311 | pipeline(PIPELINE_NAME, counter(3), stage(STAGE_NAME, counter(2), Status.Failed)) 312 | ), 313 | whenPipelineFinished(pipeline(PIPELINE_NAME, counter(3), stage(STAGE_NAME, counter(3), Status.Failed))), 314 | thenExpectStatus(Status.Failed) 315 | } 316 | 317 | }); 318 | } 319 | 320 | @Test 321 | public void shouldResolveCorrectStageStatus() throws IOException { 322 | Server server = mock(Server.class); 323 | when(server.getPipelineHistory(PIPELINE_NAME)).thenReturn(pipelineHistory); 324 | 325 | GoNotificationMessage message = new GoNotificationMessage( 326 | TestUtils.createMockServerFactory(server), 327 | pipeline 328 | ); 329 | 330 | message.tryToFixStageResult(new Rules()); 331 | 332 | assertThat(message.getStageResult(), is(expectedStatus)); 333 | } 334 | 335 | /** 336 | * @param pipelines Pipelines in chronological order, oldest one first. 337 | * @return History object 338 | */ 339 | private static History givenHistory(Pipeline... pipelines) { 340 | History history = new History(); 341 | List helperList = Arrays.asList(pipelines); 342 | Collections.reverse(helperList); 343 | history.pipelines = helperList.toArray(new Pipeline[pipelines.length]); 344 | return history; 345 | } 346 | 347 | private static Pipeline pipeline(String name, int counter, Stage... stages) { 348 | Pipeline pipeline = new Pipeline(); 349 | pipeline.name = name; 350 | pipeline.counter = counter; 351 | pipeline.stages = stages; 352 | 353 | return pipeline; 354 | } 355 | 356 | private static Pipeline[] noPipelines() { 357 | return new Pipeline[0]; 358 | } 359 | 360 | private static Stage stage(String name, int counter, Status status) { 361 | Stage stage = new Stage(); 362 | stage.name = name; 363 | stage.counter = counter; 364 | stage.result = status.getStatus(); 365 | return stage; 366 | } 367 | 368 | private static GoNotificationMessage.PipelineInfo whenPipelineFinished(Pipeline pipeline) { 369 | GoNotificationMessage.PipelineInfo info = new GoNotificationMessage.PipelineInfo(); 370 | info.name = pipeline.name; 371 | info.counter = Integer.toString(pipeline.counter); 372 | info.stage = new GoNotificationMessage.StageInfo(); 373 | 374 | Stage stage = pipeline.stages[0]; 375 | info.stage.counter = Integer.toString(stage.counter); 376 | info.stage.name = stage.name; 377 | info.stage.state = Status.valueOf(stage.result).getStatus(); 378 | info.stage.result = Status.valueOf(stage.result).getResult(); 379 | return info; 380 | } 381 | 382 | private static String thenExpectStatus(Status status) { 383 | return status.getStatus(); 384 | } 385 | 386 | private static int counter(int value) { 387 | return value; 388 | } 389 | 390 | } -------------------------------------------------------------------------------- /src/test/java/in/ashwanthkumar/gocd/slack/GoNotificationPluginTest.java: -------------------------------------------------------------------------------- 1 | package in.ashwanthkumar.gocd.slack; 2 | 3 | 4 | import com.thoughtworks.go.plugin.api.request.GoPluginApiRequest; 5 | import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; 6 | import in.ashwanthkumar.gocd.slack.util.TestUtils; 7 | import org.junit.Ignore; 8 | import org.junit.Test; 9 | 10 | import java.io.File; 11 | 12 | import static in.ashwanthkumar.gocd.slack.GoNotificationPlugin.*; 13 | import static org.hamcrest.CoreMatchers.containsString; 14 | import static org.hamcrest.CoreMatchers.equalTo; 15 | import static org.hamcrest.CoreMatchers.is; 16 | import static org.hamcrest.core.IsNull.notNullValue; 17 | import static org.junit.Assert.assertThat; 18 | import static org.mockito.Mockito.mock; 19 | import static org.mockito.Mockito.when; 20 | 21 | public class GoNotificationPluginTest { 22 | 23 | public static final String USER_HOME = "user.home"; 24 | 25 | public static final String NOTIFICATION_INTEREST_RESPONSE = "{\"notifications\":[\"stage-status\"]}"; 26 | public static final String GET_CONFIGURATION_RESPONSE = "{\"pipelineConfig\":{\"display-name\":\"Pipeline Notification Rules\",\"secure\":false,\"display-order\":\"2\",\"required\":true,\"display-value\":\"\"},\"server-url-external\":{\"display-name\":\"External GoCD Server URL\",\"secure\":false,\"display-order\":\"1\",\"required\":true,\"display-value\":\"\"}}"; 27 | private static final String GET_CONFIG_VALIDATION_RESPONSE = "[]"; 28 | 29 | @Test 30 | public void canHandleConfigValidationRequest() { 31 | GoNotificationPlugin plugin = createGoNotificationPluginFromConfigAtHomeDir(); 32 | 33 | GoPluginApiRequest request = mock(GoPluginApiRequest.class); 34 | when(request.requestName()).thenReturn(REQUEST_VALIDATE_CONFIGURATION); 35 | when(request.requestBody()).thenReturn("{\"plugin-settings\":" + 36 | "{\"external_server_url\":{\"value\":\"bob\"}}}"); 37 | 38 | GoPluginApiResponse rv = plugin.handle(request); 39 | 40 | assertThat(rv, is(notNullValue())); 41 | assertThat(rv.responseBody(), equalTo(GET_CONFIG_VALIDATION_RESPONSE)); 42 | } 43 | 44 | @Test 45 | @Ignore 46 | public void canHandleConfigurationRequest() { 47 | GoNotificationPlugin plugin = createGoNotificationPluginFromConfigAtHomeDir(); 48 | 49 | GoPluginApiRequest request = mock(GoPluginApiRequest.class); 50 | when(request.requestName()).thenReturn(REQUEST_GET_CONFIGURATION); 51 | 52 | GoPluginApiResponse rv = plugin.handle(request); 53 | 54 | assertThat(rv, is(notNullValue())); 55 | assertThat(rv.responseBody(), equalTo(GET_CONFIGURATION_RESPONSE)); 56 | } 57 | 58 | @Test 59 | public void canHandleGetViewRequest() { 60 | GoNotificationPlugin plugin = createGoNotificationPluginFromConfigAtHomeDir(); 61 | 62 | GoPluginApiRequest request = mock(GoPluginApiRequest.class); 63 | when(request.requestName()).thenReturn(REQUEST_GET_VIEW); 64 | 65 | GoPluginApiResponse rv = plugin.handle(request); 66 | 67 | assertThat(rv, is(notNullValue())); 68 | assertThat(rv.responseBody(), containsString("
url = ArgumentCaptor.forClass(URL.class); 30 | verify(httpConnectionUtil).getConnection( 31 | url.capture() 32 | ); 33 | assertThat(url.getValue().toString(), is("https://example.org/go/api/pipelines/pipeline-test/history")); 34 | } 35 | 36 | @Test 37 | public void testGetPipelineHistoryEvenWhenGoServerHostHasTrailingSlash() throws Exception { 38 | HttpConnectionUtil httpConnectionUtil = mockConnection(); 39 | 40 | Rules rules = new Rules(); 41 | rules.setGoServerHost("https://example.org/"); 42 | Server server = new Server(rules, httpConnectionUtil); 43 | 44 | server.getPipelineHistory("pipeline-test"); 45 | 46 | ArgumentCaptor url = ArgumentCaptor.forClass(URL.class); 47 | verify(httpConnectionUtil).getConnection( 48 | url.capture() 49 | ); 50 | assertThat(url.getValue().toString(), is("https://example.org/go/api/pipelines/pipeline-test/history")); 51 | } 52 | 53 | @Test 54 | public void testGetPipelineInstance() throws Exception { 55 | HttpConnectionUtil httpConnectionUtil = mockConnection(); 56 | 57 | Rules rules = new Rules(); 58 | rules.setGoServerHost("https://example.org"); 59 | Server server = new Server(rules, httpConnectionUtil); 60 | 61 | server.getPipelineInstance("pipeline-test", 42); 62 | 63 | ArgumentCaptor url = ArgumentCaptor.forClass(URL.class); 64 | verify(httpConnectionUtil).getConnection( 65 | url.capture() 66 | ); 67 | assertThat(url.getValue().toString(), is("https://example.org/go/api/pipelines/pipeline-test/42")); 68 | } 69 | 70 | @Test 71 | public void shouldConnectWithAPIToken() throws IOException { 72 | HttpConnectionUtil httpConnectionUtil = mockConnection(); 73 | Rules rules = new Rules(); 74 | Server server = new Server(rules, httpConnectionUtil); 75 | rules.setGoAPIToken("a-valid-token-from-gocd-server"); 76 | 77 | HttpURLConnection conn = mock(HttpURLConnection.class); 78 | when(httpConnectionUtil.getConnection(any(URL.class))).thenReturn(conn); 79 | when(conn.getContent()).thenReturn(new Object()); 80 | 81 | server.getUrl(new URL("http://exmaple.org/")); 82 | 83 | verify(conn).setRequestProperty(eq("User-Agent"), anyString()); 84 | verify(conn).setRequestProperty("Authorization", "Bearer a-valid-token-from-gocd-server"); 85 | } 86 | 87 | @Test 88 | public void shouldConnectWithUserPassCredentials() throws IOException { 89 | HttpConnectionUtil httpConnectionUtil = mockConnection(); 90 | Rules rules = new Rules(); 91 | Server server = new Server(rules, httpConnectionUtil); 92 | rules.setGoLogin("login"); 93 | rules.setGoPassword("pass"); 94 | 95 | HttpURLConnection conn = mock(HttpURLConnection.class); 96 | when(httpConnectionUtil.getConnection(any(URL.class))).thenReturn(conn); 97 | when(conn.getContent()).thenReturn(new Object()); 98 | 99 | server.getUrl(new URL("http://exmaple.org/")); 100 | 101 | verify(conn).setRequestProperty(eq("User-Agent"), anyString()); 102 | verify(conn).setRequestProperty("Authorization", "Basic bG9naW46cGFzcw=="); 103 | } 104 | 105 | @Test 106 | public void shouldConnectWithAPITokenFavoringOverUserPassCredential() throws IOException { 107 | HttpConnectionUtil httpConnectionUtil = mockConnection(); 108 | Rules rules = new Rules(); 109 | Server server = new Server(rules, httpConnectionUtil); 110 | rules.setGoAPIToken("a-valid-token-from-gocd-server"); 111 | rules.setGoLogin("login"); 112 | rules.setGoPassword("pass"); 113 | 114 | HttpURLConnection conn = mock(HttpURLConnection.class); 115 | when(httpConnectionUtil.getConnection(any(URL.class))).thenReturn(conn); 116 | when(conn.getContent()).thenReturn(new Object()); 117 | 118 | server.getUrl(new URL("http://exmaple.org/")); 119 | 120 | verify(conn).setRequestProperty(eq("User-Agent"), anyString()); 121 | verify(conn).setRequestProperty("Authorization", "Bearer a-valid-token-from-gocd-server"); 122 | } 123 | 124 | @Test 125 | public void shouldNotSetAuthorizationHeaderWithoutCredentials() throws IOException { 126 | HttpConnectionUtil httpConnectionUtil = mockConnection(); 127 | Rules rules = new Rules(); 128 | Server server = new Server(rules, httpConnectionUtil); 129 | 130 | HttpURLConnection conn = mock(HttpURLConnection.class); 131 | when(httpConnectionUtil.getConnection(any(URL.class))).thenReturn(conn); 132 | when(conn.getContent()).thenReturn(new Object()); 133 | 134 | server.getUrl(new URL("http://exmaple.org/")); 135 | 136 | verify(conn).setRequestProperty(eq("User-Agent"), anyString()); 137 | verify(conn, never()).setRequestProperty(eq("Authorization"), anyString()); 138 | } 139 | 140 | @Test 141 | public void shouldNotSetAuthorizationHeaderWithEmptyPassword() throws IOException { 142 | HttpConnectionUtil httpConnectionUtil = mockConnection(); 143 | Rules rules = new Rules(); 144 | rules.setGoLogin("login"); 145 | rules.setGoPassword(null); 146 | Server server = new Server(rules, httpConnectionUtil); 147 | 148 | HttpURLConnection conn = mock(HttpURLConnection.class); 149 | when(httpConnectionUtil.getConnection(any(URL.class))).thenReturn(conn); 150 | when(conn.getContent()).thenReturn(new Object()); 151 | 152 | server.getUrl(new URL("http://exmaple.org/")); 153 | 154 | verify(conn).setRequestProperty(eq("User-Agent"), anyString()); 155 | verify(conn, never()).setRequestProperty(eq("Authorization"), anyString()); 156 | } 157 | 158 | @Test 159 | public void shouldNotSetAuthorizationHeaderWithEmptyLoginName() throws IOException { 160 | HttpConnectionUtil httpConnectionUtil = mockConnection(); 161 | Rules rules = new Rules(); 162 | rules.setGoLogin(null); 163 | rules.setGoPassword("pass"); 164 | Server server = new Server(rules, httpConnectionUtil); 165 | 166 | HttpURLConnection conn = mock(HttpURLConnection.class); 167 | when(httpConnectionUtil.getConnection(any(URL.class))).thenReturn(conn); 168 | when(conn.getContent()).thenReturn(new Object()); 169 | 170 | server.getUrl(new URL("http://exmaple.org/")); 171 | 172 | verify(conn).setRequestProperty(eq("User-Agent"), anyString()); 173 | verify(conn, never()).setRequestProperty(eq("Authorization"), anyString()); 174 | } 175 | 176 | @Test 177 | public void shouldNotSetAuthorizationHeaderWithEmptyPasswordCredentials() throws IOException { 178 | HttpConnectionUtil httpConnectionUtil = mockConnection(); 179 | Rules rules = new Rules(); 180 | rules.setGoLogin(""); 181 | rules.setGoPassword(""); 182 | Server server = new Server(rules, httpConnectionUtil); 183 | 184 | HttpURLConnection conn = mock(HttpURLConnection.class); 185 | when(httpConnectionUtil.getConnection(any(URL.class))).thenReturn(conn); 186 | when(conn.getContent()).thenReturn(new Object()); 187 | 188 | server.getUrl(new URL("http://exmaple.org/")); 189 | 190 | verify(conn).setRequestProperty(eq("User-Agent"), anyString()); 191 | verify(conn, never()).setRequestProperty(eq("Authorization"), anyString()); 192 | } 193 | 194 | private HttpConnectionUtil mockConnection() throws IOException { 195 | HttpConnectionUtil httpConnectionUtil = mock(HttpConnectionUtil.class); 196 | 197 | HttpURLConnection conn = mock(HttpURLConnection.class); 198 | when(httpConnectionUtil.getConnection(any(URL.class))).thenReturn(conn); 199 | when(conn.getContent()).thenReturn(new Object()); 200 | 201 | return httpConnectionUtil; 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/test/java/in/ashwanthkumar/gocd/slack/ruleset/PipelineRuleTest.java: -------------------------------------------------------------------------------- 1 | package in.ashwanthkumar.gocd.slack.ruleset; 2 | 3 | import com.typesafe.config.Config; 4 | import com.typesafe.config.ConfigFactory; 5 | import in.ashwanthkumar.utils.collections.Sets; 6 | import org.junit.Test; 7 | 8 | import static in.ashwanthkumar.gocd.slack.ruleset.PipelineStatus.FAILED; 9 | import static in.ashwanthkumar.gocd.slack.ruleset.PipelineStatus.PASSED; 10 | import static junit.framework.Assert.assertFalse; 11 | import static junit.framework.Assert.assertTrue; 12 | import static org.hamcrest.core.Is.is; 13 | import static org.hamcrest.core.IsCollectionContaining.hasItem; 14 | import static org.junit.Assert.assertThat; 15 | 16 | public class PipelineRuleTest { 17 | @Test 18 | public void shouldGenerateRuleFromConfig() { 19 | Config config = ConfigFactory.parseResources("configs/pipeline-rule-1.conf").getConfig("pipeline"); 20 | PipelineRule build = PipelineRule.fromConfig(config); 21 | assertThat(build.getNameRegex(), is(".*")); 22 | assertThat(build.getStageRegex(), is(".*")); 23 | assertThat(build.getGroupRegex(), is(".*")); 24 | assertThat(build.getStatus(), hasItem(FAILED)); 25 | assertThat(build.getChannel(), is("#gocd")); 26 | assertThat(build.getWebhookUrl(), is("https://hooks.slack.com/services/")); 27 | assertThat(build.getOwners(), is(Sets.of("ashwanthkumar", "gobot"))); 28 | } 29 | 30 | @Test 31 | public void shouldSetValuesFromDefaultsWhenPropertiesAreNotDefined() { 32 | Config defaultConf = ConfigFactory.parseResources("configs/default-pipeline-rule.conf").getConfig("pipeline"); 33 | PipelineRule defaultRule = PipelineRule.fromConfig(defaultConf); 34 | 35 | Config config = ConfigFactory.parseResources("configs/pipeline-rule-2.conf").getConfig("pipeline"); 36 | PipelineRule build = PipelineRule.fromConfig(config); 37 | 38 | PipelineRule mergedRule = PipelineRule.merge(build, defaultRule); 39 | assertThat(mergedRule.getNameRegex(), is("gocd-slack-build-notifier")); 40 | assertThat(mergedRule.getGroupRegex(), is("ci")); 41 | assertThat(mergedRule.getStageRegex(), is("build")); 42 | assertThat(mergedRule.getStatus(), hasItem(FAILED)); 43 | assertThat(mergedRule.getChannel(), is("#gocd")); 44 | assertThat(mergedRule.getOwners(), is(Sets.of("ashwanthkumar", "gobot"))); 45 | } 46 | 47 | @Test 48 | public void shouldMatchThePipelineAndStageAgainstRegex() { 49 | PipelineRule pipelineRule = new PipelineRule("gocd-.*", ".*").setGroupRegex("ci").setLabelRegex(".*").setStatus(Sets.of(FAILED, PASSED)); 50 | assertTrue(pipelineRule.matches("gocd-slack-build-notifier", "build", "ci", ".*", "failed")); 51 | assertTrue(pipelineRule.matches("gocd-slack-build-notifier", "package", "ci", ".*", "passed")); 52 | assertTrue(pipelineRule.matches("gocd-slack-build-notifier", "publish", "ci", ".*", "passed")); 53 | 54 | assertFalse(pipelineRule.matches("gocd", "publish", "ci", ".*", "failed")); 55 | } 56 | 57 | 58 | } -------------------------------------------------------------------------------- /src/test/java/in/ashwanthkumar/gocd/slack/ruleset/RulesReaderTest.java: -------------------------------------------------------------------------------- 1 | package in.ashwanthkumar.gocd.slack.ruleset; 2 | 3 | import in.ashwanthkumar.utils.collections.Sets; 4 | import org.junit.Test; 5 | 6 | import java.net.InetSocketAddress; 7 | import java.net.Proxy; 8 | 9 | import static org.hamcrest.CoreMatchers.*; 10 | import static org.junit.Assert.assertThat; 11 | 12 | public class RulesReaderTest { 13 | 14 | @Test 15 | public void shouldReadTestConfig() { 16 | Rules rules = RulesReader.read("configs/test-config-1.conf"); 17 | assertThat(rules.isEnabled(), is(true)); 18 | assertThat(rules.getSlackChannel(), is("#gocd")); 19 | assertThat(rules.getGoServerHost(), is("http://localhost:8080/")); 20 | assertThat(rules.getPipelineRules().size(), is(2)); 21 | assertThat(rules.getPipelineRules().size(), is(2)); 22 | assertThat(rules.getDisplayConsoleLogLinks(), is(false)); 23 | assertThat(rules.getDisplayMaterialChanges(), is(false)); 24 | 25 | PipelineRule pipelineRule1 = new PipelineRule() 26 | .setNameRegex("gocd-slack-build-notifier") 27 | .setStageRegex(".*") 28 | .setGroupRegex(".*") 29 | .setLabelRegex(".*") 30 | .setChannel("#gocd") 31 | .setStatus(Sets.of(PipelineStatus.FAILED)); 32 | assertThat(rules.getPipelineRules(), hasItem(pipelineRule1)); 33 | 34 | PipelineRule pipelineRule2 = new PipelineRule() 35 | .setNameRegex("my-java-utils") 36 | .setStageRegex("build") 37 | .setGroupRegex("ci") 38 | .setLabelRegex(".*") 39 | .setChannel("#gocd-build") 40 | .setStatus(Sets.of(PipelineStatus.FAILED)); 41 | assertThat(rules.getPipelineRules(), hasItem(pipelineRule2)); 42 | 43 | assertThat(rules.getPipelineListener(), notNullValue()); 44 | } 45 | 46 | @Test 47 | public void shouldReadMinimalConfig() { 48 | Rules rules = RulesReader.read("configs/test-config-minimal.conf"); 49 | 50 | assertThat(rules.isEnabled(), is(true)); 51 | 52 | assertThat(rules.getGoLogin(), is("someuser")); 53 | assertThat(rules.getGoPassword(), is("somepassword")); 54 | assertThat(rules.getGoAPIToken(), is("a-valid-token-from-gocd-server")); 55 | assertThat(rules.getGoServerHost(), is("http://localhost:8153/")); 56 | assertThat(rules.getWebHookUrl(), is("https://hooks.slack.com/services/")); 57 | 58 | assertThat(rules.getSlackChannel(), is("#build")); 59 | assertThat(rules.getSlackDisplayName(), is("gocd-slack-bot")); 60 | assertThat(rules.getSlackUserIcon(), is("http://example.com/slack-bot.png")); 61 | 62 | // Default rules 63 | assertThat(rules.getPipelineRules().size(), is(1)); 64 | assertThat(rules.getDisplayConsoleLogLinks(), is(true)); 65 | assertThat(rules.getDisplayMaterialChanges(), is(true)); 66 | 67 | PipelineRule pipelineRule = new PipelineRule() 68 | .setNameRegex(".*") 69 | .setStageRegex(".*") 70 | .setGroupRegex(".*") 71 | .setLabelRegex(".*") 72 | .setChannel("#build") 73 | .setStatus(Sets.of(PipelineStatus.CANCELLED, PipelineStatus.BROKEN, PipelineStatus.FAILED, PipelineStatus.FIXED)); 74 | assertThat(rules.getPipelineRules(), hasItem(pipelineRule)); 75 | 76 | assertThat(rules.getPipelineListener(), notNullValue()); 77 | } 78 | 79 | @Test 80 | public void shouldReadMinimalConfigWithPipeline() { 81 | Rules rules = RulesReader.read("configs/test-config-minimal-with-pipeline.conf"); 82 | assertThat(rules.isEnabled(), is(true)); 83 | assertThat(rules.getSlackChannel(), nullValue()); 84 | assertThat(rules.getGoServerHost(), is("https://go-instance:8153/")); 85 | assertThat(rules.getWebHookUrl(), is("https://hooks.slack.com/services/")); 86 | assertThat(rules.getPipelineRules().size(), is(1)); 87 | 88 | PipelineRule pipelineRule = new PipelineRule() 89 | .setNameRegex(".*") 90 | .setStageRegex(".*") 91 | .setGroupRegex(".*") 92 | .setLabelRegex(".*") 93 | .setChannel("#foo") 94 | .setStatus(Sets.of(PipelineStatus.FAILED)) 95 | .setWebhookUrl("https://hooks.slack.com/services/for-pipeline"); 96 | assertThat(rules.getPipelineRules(), hasItem(pipelineRule)); 97 | 98 | assertThat(rules.getPipelineListener(), notNullValue()); 99 | } 100 | 101 | @Test 102 | public void shouldReadMinimalConfigWithPipelineAndEnvironmentVariables() { 103 | Rules rules = RulesReader.read("configs/test-config-minimal-with-env-variables.conf"); 104 | assertThat(rules.isEnabled(), is(true)); 105 | assertThat(rules.getSlackChannel(), nullValue()); 106 | assertThat(rules.getGoServerHost(), is("https://go-instance:8153/")); 107 | assertThat(rules.getWebHookUrl(), is("https://hooks.slack.com/services/")); 108 | assertThat(rules.getPipelineRules().size(), is(1)); 109 | assertThat(rules.getGoLogin(), is(System.getenv("HOME"))); 110 | 111 | PipelineRule pipelineRule = new PipelineRule() 112 | .setNameRegex(".*") 113 | .setStageRegex(".*") 114 | .setGroupRegex(".*") 115 | .setLabelRegex(".*") 116 | .setChannel("#foo") 117 | .setStatus(Sets.of(PipelineStatus.FAILED)); 118 | assertThat(rules.getPipelineRules(), hasItem(pipelineRule)); 119 | 120 | assertThat(rules.getPipelineListener(), notNullValue()); 121 | assertThat(rules.getProxy(), nullValue()); 122 | } 123 | 124 | @Test(expected = RuntimeException.class) 125 | public void shouldThrowExceptionIfConfigInvalid() { 126 | RulesReader.read("test-config-invalid.conf"); 127 | } 128 | 129 | @Test 130 | public void shouldReadProxyConfig() { 131 | Rules rules = RulesReader.read("configs/test-config-with-proxy.conf"); 132 | assertThat(rules.isEnabled(), is(true)); 133 | Proxy expectedProxy = new Proxy(Proxy.Type.SOCKS, new InetSocketAddress("localhost", 5555)); 134 | assertThat(rules.getProxy(), is(expectedProxy)); 135 | } 136 | 137 | 138 | } 139 | -------------------------------------------------------------------------------- /src/test/java/in/ashwanthkumar/gocd/slack/ruleset/RulesTest.java: -------------------------------------------------------------------------------- 1 | package in.ashwanthkumar.gocd.slack.ruleset; 2 | 3 | import in.ashwanthkumar.gocd.slack.Status; 4 | import org.junit.Test; 5 | 6 | import java.util.Arrays; 7 | import java.util.List; 8 | import java.util.HashSet; 9 | import java.util.Set; 10 | 11 | import static org.hamcrest.MatcherAssert.assertThat; 12 | import static org.hamcrest.core.Is.is; 13 | 14 | public class RulesTest { 15 | 16 | @Test 17 | public void shouldFindMatch() { 18 | Rules rules = new Rules(); 19 | 20 | rules.setPipelineRules(Arrays.asList( 21 | pipelineRule("pipeline1", "stage1", "ch1", statuses(PipelineStatus.BUILDING, PipelineStatus.FAILED)), 22 | pipelineRule("pipeline1", "stage2", "ch2", statuses(PipelineStatus.FIXED, PipelineStatus.PASSED)), 23 | pipelineRule("pipeline2", "stage2", "ch3", statuses(PipelineStatus.CANCELLED, PipelineStatus.BROKEN)) 24 | )); 25 | 26 | List foundRules1 = rules.find("pipeline1", "stage1", "ci", ".*", Status.Building.getStatus()); 27 | assertThat(foundRules1.size(), is(1)); 28 | assertThat(foundRules1.get(0).getNameRegex(), is("pipeline1")); 29 | assertThat(foundRules1.get(0).getStageRegex(), is("stage1")); 30 | 31 | List foundRules2 = rules.find("pipeline2", "stage2", "ci", ".*", Status.Cancelled.getStatus()); 32 | assertThat(foundRules2.size(), is(1)); 33 | assertThat(foundRules2.get(0).getNameRegex(), is("pipeline2")); 34 | assertThat(foundRules2.get(0).getStageRegex(), is("stage2")); 35 | 36 | List foundRules3 = rules.find("pipeline2", "stage2", "ci", ".*", Status.Passed.getStatus()); 37 | assertThat(foundRules3.size(), is(0)); 38 | } 39 | 40 | @Test 41 | public void shouldFindMatchWithRegexp() { 42 | Rules rules = new Rules(); 43 | 44 | rules.setPipelineRules(Arrays.asList( 45 | pipelineRule("[a-z]*", "[a-z]*", "ch1", statuses(PipelineStatus.BUILDING)), 46 | pipelineRule("\\d*", "\\d*", "ch2", statuses(PipelineStatus.BUILDING)), 47 | pipelineRule("\\d*", "\\d*", "ch3", statuses(PipelineStatus.PASSED)), 48 | pipelineRule("\\d*", "[a-z]*", "ch4", statuses(PipelineStatus.BUILDING)) 49 | )); 50 | 51 | List foundRules1 = rules.find("abc", "efg", "ci", ".*", Status.Building.getStatus()); 52 | assertThat(foundRules1.size(), is(1)); 53 | assertThat(foundRules1.get(0).getNameRegex(), is("[a-z]*")); 54 | assertThat(foundRules1.get(0).getStageRegex(), is("[a-z]*")); 55 | assertThat(foundRules1.get(0).getChannel(), is("ch1")); 56 | 57 | List foundRules2 = rules.find("123", "456", "ci", ".*", Status.Building.getStatus()); 58 | assertThat(foundRules2.size(), is(1)); 59 | assertThat(foundRules2.get(0).getNameRegex(), is("\\d*")); 60 | assertThat(foundRules2.get(0).getStageRegex(), is("\\d*")); 61 | assertThat(foundRules2.get(0).getChannel(), is("ch2")); 62 | 63 | List foundRules3 = rules.find("123", "456", "ci", ".*", Status.Passed.getStatus()); 64 | assertThat(foundRules3.size(), is(1)); 65 | assertThat(foundRules3.get(0).getNameRegex(), is("\\d*")); 66 | assertThat(foundRules3.get(0).getStageRegex(), is("\\d*")); 67 | assertThat(foundRules3.get(0).getChannel(), is("ch3")); 68 | 69 | List foundRules4 = rules.find("pipeline1", "stage1", "ci", ".*", Status.Passed.getStatus()); 70 | assertThat(foundRules4.size(), is(0)); 71 | } 72 | 73 | @Test 74 | public void shouldFindAllMatchesIfProcessAllRules() { 75 | Rules rules = new Rules(); 76 | rules.setProcessAllRules(true); 77 | 78 | rules.setPipelineRules(Arrays.asList( 79 | pipelineRule("[a-z]*", "stage\\d+", "ch1", statuses(PipelineStatus.BUILDING)), 80 | pipelineRule("[a-z]*", "stage2", "ch2", statuses(PipelineStatus.BUILDING)) 81 | )); 82 | 83 | List foundRules1 = rules.find("abc", "stage1", "ci", ".*", Status.Building.getStatus()); 84 | assertThat(foundRules1.size(), is(1)); 85 | assertThat(foundRules1.get(0).getChannel(), is("ch1")); 86 | 87 | List foundRules2 = rules.find("abc", "stage2", "ci", ".*", Status.Building.getStatus()); 88 | assertThat(foundRules2.size(), is(2)); 89 | assertThat(foundRules2.get(0).getChannel(), is("ch1")); 90 | assertThat(foundRules2.get(1).getChannel(), is("ch2")); 91 | 92 | List foundRules3 = rules.find("abc1", "stage2", "ci", ".*", Status.Building.getStatus()); 93 | assertThat(foundRules3.size(), is(0)); 94 | } 95 | 96 | @Test 97 | public void shouldFindMatchAll() { 98 | Rules rules = new Rules(); 99 | 100 | rules.setPipelineRules(Arrays.asList( 101 | pipelineRule("p1", "s1", "ch1", statuses(PipelineStatus.ALL)) 102 | )); 103 | 104 | assertThat(rules.find("p1", "s1", "ci", ".*", Status.Building.getStatus()).size(), is(1)); 105 | assertThat(rules.find("p1", "s1", "ci", ".*", Status.Broken.getStatus()).size(), is(1)); 106 | assertThat(rules.find("p1", "s1", "ci", ".*", Status.Cancelled.getStatus()).size(), is(1)); 107 | assertThat(rules.find("p1", "s1", "ci", ".*", Status.Failed.getStatus()).size(), is(1)); 108 | assertThat(rules.find("p1", "s1", "ci", ".*", Status.Failing.getStatus()).size(), is(1)); 109 | assertThat(rules.find("p1", "s1", "ci", ".*", Status.Fixed.getStatus()).size(), is(1)); 110 | assertThat(rules.find("p1", "s1", "ci", ".*", Status.Passed.getStatus()).size(), is(1)); 111 | assertThat(rules.find("p1", "s1", "ci", ".*", Status.Unknown.getStatus()).size(), is(1)); 112 | } 113 | 114 | @Test 115 | public void shouldGetAPIServerHost() { 116 | Rules rules = new Rules(); 117 | 118 | rules.setGoServerHost("https://gocd.com"); 119 | assertThat(rules.getGoAPIServerHost(), is("https://gocd.com")); 120 | 121 | rules.setGoAPIServerHost("http://localhost"); 122 | assertThat(rules.getGoAPIServerHost(), is("http://localhost")); 123 | } 124 | 125 | @Test 126 | public void shouldGetAPIToken() { 127 | Rules rules = new Rules(); 128 | 129 | rules.setGoAPIToken("a-valid-token-from-gocd-server"); 130 | assertThat(rules.getGoAPIToken(), is("a-valid-token-from-gocd-server")); 131 | } 132 | 133 | private static PipelineRule pipelineRule(String pipeline, String stage, String channel, Set statuses) { 134 | PipelineRule pipelineRule = new PipelineRule(pipeline, stage); 135 | pipelineRule.setStatus(statuses); 136 | pipelineRule.setChannel(channel); 137 | pipelineRule.setLabelRegex(".*"); 138 | return pipelineRule; 139 | } 140 | 141 | private static Set statuses(PipelineStatus... statuses) { 142 | return new HashSet(Arrays.asList(statuses)); 143 | } 144 | 145 | } 146 | -------------------------------------------------------------------------------- /src/test/java/in/ashwanthkumar/gocd/slack/util/TestUtils.java: -------------------------------------------------------------------------------- 1 | package in.ashwanthkumar.gocd.slack.util; 2 | 3 | import in.ashwanthkumar.gocd.slack.jsonapi.Server; 4 | import in.ashwanthkumar.gocd.slack.jsonapi.ServerFactory; 5 | import in.ashwanthkumar.gocd.slack.ruleset.Rules; 6 | 7 | import static org.mockito.Matchers.any; 8 | import static org.mockito.Mockito.mock; 9 | import static org.mockito.Mockito.when; 10 | 11 | public class TestUtils { 12 | 13 | public static ServerFactory createMockServerFactory(Server server) { 14 | ServerFactory factory = mock(ServerFactory.class); 15 | when(factory.getServer(any(Rules.class))).thenReturn(server); 16 | return factory; 17 | } 18 | 19 | public static String getResourceDirectory(String resource) { 20 | ClassLoader ldr = Thread.currentThread().getContextClassLoader(); 21 | String url = ldr.getResource(resource).toString(); 22 | return url.substring("file:".length(), url.lastIndexOf('/')); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/test/java/in/ashwanthkumar/gocd/teams/TeamsTest.java: -------------------------------------------------------------------------------- 1 | package in.ashwanthkumar.gocd.teams; 2 | 3 | import com.typesafe.config.Config; 4 | import com.typesafe.config.ConfigFactory; 5 | import in.ashwanthkumar.gocd.slack.ruleset.Rules; 6 | import org.junit.Assert; 7 | import org.junit.Test; 8 | 9 | import java.io.ByteArrayOutputStream; 10 | import java.io.IOException; 11 | 12 | public class TeamsTest { 13 | 14 | private TeamsCard buildCard() { 15 | TeamsCard card = new TeamsCard(); 16 | card.setTitle("title"); 17 | card.setColor(MessageCardSchema.Color.GREEN); 18 | card.addFact("k", "v"); 19 | card.addLinkAction("name", "uri"); 20 | return card; 21 | } 22 | 23 | @Test 24 | public void testCardHttpContent() throws IOException { 25 | TeamsCard card = buildCard(); 26 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 27 | new CardHttpContent(card).writeTo(baos); 28 | final String result = baos.toString(); 29 | 30 | Assert.assertEquals(card.toString(), result); 31 | } 32 | 33 | @Test 34 | public void testCardToString() { 35 | TeamsCard card = buildCard(); 36 | String result = card.toString(); 37 | 38 | String expected = ("{'@type':'MessageCard','themeColor':'009900','title':'title'," + 39 | "'summary':'GoCD build update','sections':[{'facts':[{'name':'k'," + 40 | "'value':'v'}]}],'potentialAction':[{'@type':'OpenUri','name':'name'," + 41 | "'targets':[{'os':'default','uri':'uri'}]}]}") 42 | .replace('\'', '"'); 43 | 44 | Assert.assertEquals(expected, result); 45 | } 46 | 47 | @Test 48 | public void testTeamsListener() { 49 | Config config = ConfigFactory.parseResources("configs/test-config-teams.conf") 50 | .withFallback(ConfigFactory.load(getClass().getClassLoader())) 51 | .getConfig("gocd.slack"); 52 | Rules rules = Rules.fromConfig(config); 53 | 54 | Assert.assertEquals(TeamsPipelineListener.class, rules.getPipelineListener().getClass()); 55 | Assert.assertEquals("https://example.com/default", rules.getWebHookUrl()); 56 | Assert.assertEquals("https://example.com/pipeline-override", 57 | rules.getPipelineRules().get(0).getWebhookUrl()); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/test/resources/configs/default-pipeline-rule.conf: -------------------------------------------------------------------------------- 1 | pipeline { 2 | name = "defaultRule" 3 | group = "ci" 4 | stage = "build" 5 | # you can provide multiple values by separating them with | (pipe) symbol - failed|broken 6 | state = "failed" # accepted values - failed / broken / fixed / passed / all 7 | channel = "#gocd" # optional field 8 | owners = ["ashwanthkumar", "gobot"] # optional field 9 | } -------------------------------------------------------------------------------- /src/test/resources/configs/go_notify.conf: -------------------------------------------------------------------------------- 1 | gocd.slack { 2 | # feature flag for notification plugin, turning this false will not post anything to Slack 3 | # quite useful while testing / debugging 4 | enabled = true 5 | # Global default channel for all pipelines, these can be overriden at a pipeline level as well 6 | channel = "#gocd" 7 | webhookUrl: "https://hooks.slack.com/services/abcd/efgh/lmnopqrst12345" # Mandatory field 8 | # Enter full FQDN of your GoCD instance. We'll be sending links on your slack channel using this as the base uri. 9 | server-host = "http://localhost:8080/" 10 | 11 | # Default settings for pipelines 12 | default { 13 | name = ".*" 14 | stage = ".*" 15 | group = ".*" 16 | # you can provide multiple values by separating them with | (pipe) symbol - failed|broken 17 | state = "failed" # accepted values - failed / broken / fixed / passed / all 18 | #channel = "gocd" # Mandatory field 19 | } 20 | 21 | # Example settings would be like 22 | # pipelines = [{ 23 | # nameRegex = "upc14" 24 | # channel = "#" 25 | # state = "failed|broken" 26 | # }] 27 | pipelines = [{ 28 | name = "gocd-slack-build-notifier" 29 | }, { 30 | name = "my-java-utils" 31 | stage = "build" 32 | # you can provide multiple values by separating them with | (pipe) symbol - failed|broken 33 | state = "failed" # accepted values - failed / broken / fixed / passed / all 34 | channel = "#gocd-build" # Mandatory field 35 | }] 36 | } 37 | -------------------------------------------------------------------------------- /src/test/resources/configs/pipeline-rule-1.conf: -------------------------------------------------------------------------------- 1 | pipeline { 2 | name = ".*" 3 | stage = ".*" 4 | group = ".*" 5 | # you can provide multiple values by separating them with | (pipe) symbol - failed|broken 6 | state = "failed" # accepted values - failed / broken / fixed / passed / all 7 | channel = "#gocd" # optional field 8 | owners = ["ashwanthkumar", "gobot"] # optional field 9 | webhookUrl = "https://hooks.slack.com/services/" 10 | } -------------------------------------------------------------------------------- /src/test/resources/configs/pipeline-rule-2.conf: -------------------------------------------------------------------------------- 1 | pipeline { 2 | name = "gocd-slack-build-notifier" 3 | } -------------------------------------------------------------------------------- /src/test/resources/configs/test-config-1.conf: -------------------------------------------------------------------------------- 1 | gocd.slack { 2 | # feature flag for notification plugin, turning this false will not post anything to Slack 3 | # quite useful while testing / debugging 4 | enabled = true 5 | # Global default channel for all pipelines, these can be overriden at a pipeline level as well 6 | channel = "#gocd" 7 | webhookUrl: "https://hooks.slack.com/services/abcd/efgh/lmnopqrst12345" # Mandatory field 8 | # Enter full FQDN of your GoCD instance. We'll be sending links on your slack channel using this as the base uri. 9 | server-host = "http://localhost:8080/" 10 | 11 | display-console-log-links = false 12 | 13 | # If you don't want to see the revision changes in the notification (for size or confidentiality concerns) 14 | # defaults to true 15 | displayMaterialChanges = false 16 | 17 | # Default settings for pipelines 18 | default { 19 | name = ".*" 20 | stage = ".*" 21 | group = ".*" 22 | # you can provide multiple values by separating them with | (pipe) symbol - failed|broken 23 | state = "failed" # accepted values - failed / broken / fixed / passed / all 24 | #channel = "gocd" # Mandatory field 25 | } 26 | 27 | # Example settings would be like 28 | # pipelines = [{ 29 | # nameRegex = "upc14" 30 | # channel = "#" 31 | # state = "failed|broken" 32 | # }] 33 | pipelines = [{ 34 | name = "gocd-slack-build-notifier" 35 | }, { 36 | name = "my-java-utils" 37 | stage = "build" 38 | group = "ci" 39 | # you can provide multiple values by separating them with | (pipe) symbol - failed|broken 40 | state = "failed" # accepted values - failed / broken / fixed / passed / all 41 | channel = "#gocd-build" # Mandatory field 42 | }] 43 | } 44 | -------------------------------------------------------------------------------- /src/test/resources/configs/test-config-invalid.conf: -------------------------------------------------------------------------------- 1 | gocd.slack { 2 | login = "foo 3 | password = "foo-bar" 4 | server-host = "https://go-instance:8153/" 5 | webhookUrl = "http://slack.com/" 6 | 7 | pipelines = [{ 8 | name = ".*" 9 | stage = ".*" 10 | state = "failed" 11 | channel = "#foo" 12 | }] 13 | } 14 | -------------------------------------------------------------------------------- /src/test/resources/configs/test-config-minimal-with-env-variables.conf: -------------------------------------------------------------------------------- 1 | gocd.slack { 2 | login = ${HOME} # HOME is the only environment variable that I could find which would be always present 3 | password = "password" 4 | server-host = "https://go-instance:8153/" 5 | webhookUrl = "https://hooks.slack.com/services/" 6 | 7 | pipelines = [{ 8 | name = ".*" 9 | stage = ".*" 10 | state = "failed" 11 | channel = "#foo" 12 | }] 13 | } 14 | -------------------------------------------------------------------------------- /src/test/resources/configs/test-config-minimal-with-pipeline.conf: -------------------------------------------------------------------------------- 1 | gocd.slack { 2 | login = "foo" 3 | password = "foo-bar" 4 | server-host = "https://go-instance:8153/" 5 | webhookUrl = "https://hooks.slack.com/services/" 6 | 7 | pipelines = [{ 8 | name = ".*" 9 | stage = ".*" 10 | state = "failed" 11 | channel = "#foo" 12 | webhookUrl = "https://hooks.slack.com/services/for-pipeline" 13 | }] 14 | } 15 | -------------------------------------------------------------------------------- /src/test/resources/configs/test-config-minimal.conf: -------------------------------------------------------------------------------- 1 | gocd.slack { 2 | login = "someuser" 3 | password = "somepassword" 4 | api-token = "a-valid-token-from-gocd-server" 5 | server-host = "http://localhost:8153/" 6 | webhookUrl = "https://hooks.slack.com/services/" 7 | 8 | # optional fields 9 | channel = "#build" 10 | slackDisplayName = "gocd-slack-bot" 11 | slackUserIconURL = "http://example.com/slack-bot.png" 12 | } 13 | -------------------------------------------------------------------------------- /src/test/resources/configs/test-config-teams.conf: -------------------------------------------------------------------------------- 1 | gocd.slack { 2 | login = "foo" 3 | password = "foo-bar" 4 | server-host = "https://go-instance:8153/" 5 | 6 | # Teams specific configuration 7 | listener = "in.ashwanthkumar.gocd.teams.TeamsPipelineListener" 8 | webhookUrl = "https://example.com/default" 9 | 10 | pipelines = [{ 11 | name = ".*" 12 | stage = ".*" 13 | state = "failed" 14 | # Optionally send these messages to another channel using a different webhook. 15 | webhookUrl = "https://example.com/pipeline-override" 16 | }] 17 | } 18 | -------------------------------------------------------------------------------- /src/test/resources/configs/test-config-with-proxy.conf: -------------------------------------------------------------------------------- 1 | gocd.slack { 2 | login = "someuser" 3 | password = "somepassword" 4 | server-host = "http://localhost:8153/" 5 | webhookUrl = "https://hooks.slack.com/services/" 6 | 7 | # optional fields 8 | channel = "#build" 9 | slackDisplayName = "gocd-slack-bot" 10 | slackUserIconURL = "http://example.com/slack-bot.png" 11 | proxy { 12 | hostname = "localhost" 13 | port = "5555" 14 | type = "socks" # acceptable values are http / socks 15 | } 16 | } 17 | --------------------------------------------------------------------------------