This plugin was born out of the need to easily send alert messages from the gradle build. While there are several great options available none of them really fit my purpose or had the flexibility to be easily configured AND be able to send more than static messages. This plugin supports the complete message structure for the current version of slack plus has additional pre-configured tasks that when declared within the DSL allow you to easily send many common alerts.
This plugin has been tested with the now deprecated Slack WebHook api as well as the currently suggested way of using a WebHook through a custom Slack Bot. Both configurations and file upload are fully supported.
- Including the plugin
- Configuration
- Global
- Dynamically Generated Alerts
- Known Issues
- Global Config
- Messages
- Complete DSL
- Resources
I prefer any project I build to do the heavy lifting for me in the future, so I try to make the configuration as easy and simple as possible for day to day use. By default, just include the plugin, and you're ready to go.
plugins {
id 'com.benrhine.slack-alerts-groovy' version '0.0.1'
}
Granted just including the plugin doesn't really do anything for you, but I've had ones I have tested previously that as soon as you declare them they break your config as they require additional elements to be defined that were not specified in the documentation and are exceptionally challenging to figure out. This plugin should not cause that problem.
If you are using an ancient version of gradle or wish to pull this plugin down and extend it yourself
buildscript {
repositories {
mavenLocal()
mavenCentral()
}
dependencies {
classpath 'com.benrhine:slack-alerts-groovy:0.0.1'
}
}
apply plugin: 'com.benrhine.slack-alerts-groovy'
This plugin has multiple configuration options available to you and even supports a multi DSL naming scheme. In some instances it is possible to provide a global configuration option for some values to reduce duplicate declarations (this will be covered in detail below).
By default, it is assumed you are not configuring the plugin with the global config blocks. In this case defining alert
messages to send is super easy. Start by adding the following to the build.gradle
file.
slackMessages {
yourMessage1 {
...
}
yourMessage2 {
...
}
}
Note: The above DSL will generate two tasks sendYourMessage1Alert
and sendYourMessage2Alert
Every DSL block declared within the slackMessages
block is considered a message and a unique task will be generated for
each declaration. To see all the tasks that are generated run the ./gradlew tasks
command and search for the alerts
group. Messages created at this point are static only as they are generated on the initial plugin run with the data
available at that time. To be perfectly clear if you are attempting to send an alert with the result from another task or
process that assigns data into a build variable and this happens at any time after the declaration of these alerts the
data will not be included.
This seemed to be how most of the slack plugins work and I found unacceptable so see my custom dynamic message sends below.
It is important to note that due to the nature of generative task creation when / if updating an alert
name in the slackMessages
block may sometimes cause task not found errors. This occurs because those task names are generated
when the plugin is applied and since one of those names has been updated, the previously generated name is no longer available.
This can be particularly annoying when chaining tasks later on in the build.
The easiest way to fix this is to comment out any places you are programmatically calling any of the generated task names,
comment out the slackMessages
DSL block followed by cleaning the project then refreshing gradle. After which, uncomment
the DSL and any programmatic task calls and refresh gradle once again. This will re-trigger the plugin to regenerate the
task names.
Essentially every DSL field is optional EXCEPT webHook
and while text
is optional if you do not provide a value then
the alert will be empty, and you won't know if it worked or not. Rather than use text
in most cases I prefer attachment
as it gives greater control over the message layout. In order to configure a simple static message you can declare
something similar to the following ...
slackMessages {
applicationBuildStart {
webHook = "https://hooks.slack.com/services/XXXXXXXXXXXXX" // or if uploading something https://slack.com/api/files.upload
attachment {
fallback = "$project.name build starting ..."
pretext = "$project.name build starting ..."
color = "warning"
field {
title = "Build Info"
value = "*Version*: $project.version"
shortValue = true // Means value displays in half width
}
field {
title = "Git Info"
value = "*Branch*: $branch\n*Commit*: $commit"
shortValue = true // Means value displays in half width
}
}
}
}
Reminder again, messages declared this way are static only so if $branch
is not already available when plugin scans
this block the value will be empty. If you were to configure every message
value available it would look similar to the
following ...
slackMessages {
applicationBuildStart {
webHook = "https://hooks.slack.com/services/XXXXXXXXXXXXX" // or if uploading something https://slack.com/api/files.upload
verificationUrl = "https://..."
displayLogging = true // Defaulted to false for clean build display
uploadFilePath = "path to file"
uploadFileName = "filename"
uploadFileType = "zip"
uploadTitle = "a better title"
authType = "Bearer"
environment = "dev"
token = "oauth"
payload = "additional field"
retries = 37
sleepAmountOne = 1000
sleepAmountTwo = 500
channels = "C0XXXXXXXXX"
iconUrl = ""
iconEmoji = ""
text = ""
username = ""
threadTs = ""
mrkdwn = ""
attachment {
fallback = "$project.name build starting ..."
pretext = "$project.name build starting ..."
color = "warning"
field {
title = "Build Info"
value = "*Version*: $project.version"
shortValue = true
}
field {
title = "Git Info"
value = "*Branch*: $branch\n*Commit*: $commit"
shortValue = true
}
}
blocks {
}
}
}
As mentioned above there are two ways to configure this plugin, by message or by using the global configuration blocks.
slackConfig {
environment = "" // (Optional) Define what environment an alert is coming from
webHook = "" // (Optional) Define the webHook url
uploadUrl = "" // (Optional) Define the upload url
token = "" // (Optional) Define the auth token for a slack bot
channels = "" // (Optional) Define channel or channels
payload = "" // (Optional) Additional payload
displayLogging = "" // (Optional) Define if logging is enabled
}
Use this to define what environment the alert is coming from, since the environment is normally available at the very start of the build it should be easy to programmatically define this value and have it available to any of the alerts.
By defining a webHook
in the config block you no longer have to define it for each individual message as it will
automatically set for each message. If you define a message with its own webHook
that value will be preferred and will
NOT be overridden.
It is suggested you set this in the config block as Slack has a universal upload url and there is no reason to define it on a per-message basis.
This would be your bot token, if this is not defined then it is not possible to upload files into slack
This allows you to set a default channel that everything will be sent to if you want it to be different that what was
defined when the webHook
and or bot was configured
Just an extra field for right now
Also suggested you set this in the config block. While this can be configured on a per-message basis it is easier to do globally.
Having the ability to send static messages can be great, but it is much more likely you want the ability to programmatically
build your alert messages. Unfortunately, in order to accomplish this you will likely need to fork this plugin if the
following does not cover your use cases. While these names are dynamically generated they work by checking if a task name
contains
a value to allow some flexibility when naming your dsl blocks. Thus, you can add values before or
after the following parts but the following parts must be part of the dsl message name in order for the pre-defined
generative tasks to be created.
- unitTest
- intTest
- loadTest
- authenticatedSmokeTest
- unauthenticatedSmokeTest
- validationSmokeTest
- applicationHealthCheck
- applicationInfo
In addition, in order to get message color and test results sent along with the dynamic messages your build should have
the following properties declared in the main build.gradle
ext.buildColor = ""
ext.testResults = []
And you will want a block similar to the following added to any test blocks you have to populate the data. For a full example of what is necessary to send test results see here
afterSuite { desc, result ->
project.buildColor = result.failedTestCount == 0 ? "good" : "danger"
if (desc.parent) {
return
} // Only summarize results for whole modules
final String summary = "${result.resultType} " +
"(" +
"${result.testCount} tests, " +
"${result.successfulTestCount} successes, " +
"${result.failedTestCount} failures, " +
"${result.skippedTestCount} skipped" +
") " +
"in ${TimeCategory.minus(new Date(result.endTime), new Date(result.startTime))}" +
"\n"
project.testResults = []
// Add reports in `testsUnitResults`, keep failed suites at the end
if (result.resultType == TestResult.ResultType.SUCCESS) {
project.testResults.add(0, summary)
} else {
project.testResults += summary
}
}
Used to send an alert that unit tests have passed
unitTestSuccess {
channels = "C04RVG40RTP" // Currently only used for upload
uploadFileName = "unitJacoco.zip" // Only used for upload
attachment {
fallback = "Unit tests successfully completed."
pretext = "Unit tests successfully completed."
color = unitTestBuildColor
field {
title = "Application"
value = project.name
shortValue = true
}
field {
title = "Version"
value = project.version
shortValue = true
}
field {
title = "Branch"
value = "$branch"
shortValue = true
}
field {
title = "Commit"
value = "$commit"
shortValue = true
}
}
attachment {
color = unitTestBuildColor
field {
title = "Results"
value = "N / A"
shortValue = false
}
}
}
Upload the results of the unit test run. This requires the above dsl for sendUnitTestAlert
to be defined AND to include the channels
and uploadFileName
values. If no channels
value is set nothing
will be sent, and you will receive the following message "No channel(s) set, nothing will be sent". If no uploadFileName
is provided an exception will be thrown with the message "Provided file path does not exist or is a directory".
Used to send an alert that integration tests have passed
intTestSuccess {
channels = "C04RVG40RTP"
uploadFileName = "intJacoco.zip"
attachment {
fallback = "Integration tests successfully completed."
pretext = "Integration tests successfully completed."
color = "$intTestBuildColor"
field {
title = "Application"
value = project.name
shortValue = true
}
field {
title = "Version"
value = project.version
shortValue = true
}
field {
title = "Branch"
value = "$branch"
shortValue = true
}
field {
title = "Commit"
value = "$commit"
shortValue = true
}
}
attachment {
color = "$intTestBuildColor"
field {
title = "Results"
value = "Something text"
}
}
}
Upload the results of the int test run. This requires the above dsl for sendIntTestAlert
to be defined AND to include the channels
and uploadFileName
values. If no channels
value is set nothing
will be sent, and you will receive the following message "No channel(s) set, nothing will be sent". If no uploadFileName
is provided an exception will be thrown with the message "Provided file path does not exist or is a directory".
Used to send an alert that load tests have passed
Used to send an alert that authenticated smoke tests have passed
Used to send an alert that validation smoke tests have passed
Used to send an alert that unauthenticated smoke tests have passed
Used to send an alert on if the application started up correctly or not, checks both the http status and if the result contains the word "UP".
applicationHealthCheck {
verificationUrl = "http://localhost:7001/actuator/health"
attachment {
color = "danger"
field {
title = "Application Status: N / A"
value = "N / A"
shortValue = false
}
}
attachment {
color = "danger"
field {
title = "QA Status: N / A"
value = "N / A"
shortValue = false
}
}
}
Used to send an alert on the application information. I normally use this in conjunction with a call to sendApplicationHealthCheckAlert and call this immediately after.
applicationInfo {
verificationUrl = "http://localhost:7001/actuator/info"
attachment {
color = "danger"
field {
title = "Application Info: N / A"
value = "N / A"
shortValue = false
}
field {
title = "Build Info"
value = "N / A"
shortValue = true
}
field {
title = "Git Info"
value = "N / A"
shortValue = true
}
}
}
I don't know for certain that this is an issue but when attempting to chain the alerts for integration testing
integrationTest.finalizedBy sendIntTestAlert
sendIntTestAlert.finalizedBy sendIntTestResults
The first alert sends fine and I can see on the console that the results are posted and return a 200 but never are seen
in the Slack channel. This is set up exactly like the unit tests which work flawlessly, so I don't understand. To add
to the confusion if sendIntTestResults
is called independently then it uploads the file exactly as expected. I thought
this may be a timing issue and tried inducing some waits but that did not change the behavior.
In short, call sendIntTestResults
independently, and you will be fine - if you can get around the chaining issue or see
a mistake I made please let me know.
slackConfig {
webHook = System.env.SLACK_WEBHOOK
uploadUrl = System.env.SLACK_UPLOAD
displayLogging = true
token = System.env.SLACK_TOKEN
}
slackMessages {
applicationBuildStart {
attachment {
fallback = "$project.name build starting ..."
pretext = "$project.name build starting ..."
color = "good" // Default to 'good' as build is just starting
field {
title = "Build Info"
value = "*Version*: $project.version"
shortValue = true
}
field {
title = "Git Info"
value = "*Branch*: $branch\n*Commit*: $commit"
shortValue = true
}
}
}
unitTest {
channels = "C04RVG40RTP" // (Required) Currently only used for upload
uploadFileName = "unitJacoco.zip" // (Required) Only used for upload
uploadFileType = "zip" // (Optional) Only used for upload
uploadTitle = "JaCoCo Coverage Report" // (Optional) Only used for upload
attachment {
fallback = "Unit tests successfully completed."
pretext = "Unit tests successfully completed."
color = buildColor
field {
title = "Application"
value = project.name
shortValue = true
}
field {
title = "Version"
value = project.version
shortValue = true
}
field {
title = "Branch"
value = "$branch"
shortValue = true
}
field {
title = "Commit"
value = "$commit"
shortValue = true
}
}
attachment {
color = buildColor
field {
title = "Results"
value = "N / A"
shortValue = false
}
}
}
intTest {
channels = "C04RVG40RTP" // (Required) Currently only used for upload
uploadFileName = "intJacoco.zip" // (Required) Only used for upload
uploadFileType = "zip" // (Optional) Only used for upload
uploadTitle = "JaCoCo Coverage Report" // (Optional) Only used for upload
attachment {
fallback = "Integration tests successfully completed."
pretext = "Integration tests successfully completed."
color = buildColor
field {
title = "Application"
value = project.name
shortValue = true
}
field {
title = "Version"
value = project.version
shortValue = true
}
field {
title = "Branch"
value = "$branch"
shortValue = true
}
field {
title = "Commit"
value = "$commit"
shortValue = true
}
}
attachment {
color = buildColor
field {
title = "Results"
value = "Something text"
}
}
}
loadTest {
attachment {
fallback = "Load tests successfully completed."
pretext = "Load tests successfully completed."
color = buildColor
field {
title = "Application"
value = project.name
shortValue = true
}
field {
title = "Version"
value = project.version
shortValue = true
}
field {
title = "Branch"
value = "$branch"
shortValue = true
}
field {
title = "Commit"
value = "$commit"
shortValue = true
}
}
attachment {
color = buildColor
field {
title = "Results"
value = "N / A"
shortValue = false
}
}
}
authenticatedSmokeTest {
attachment {
fallback = "Authenticated smoke tests successfully completed."
pretext = "Authenticated smoke tests successfully completed."
color = buildColor
field {
title = "Application"
value = project.name
shortValue = true
}
field {
title = "Version"
value = project.version
shortValue = true
}
field {
title = "Branch"
value = "$branch"
shortValue = true
}
field {
title = "Commit"
value = "$commit"
shortValue = true
}
}
attachment {
color = buildColor
field {
title = "Results"
value = "N / A"
shortValue = false
}
}
}
unauthenticatedSmokeTest {
attachment {
fallback = "Unauthenticated smoke tests successfully completed."
pretext = "Unauthenticated smoke tests successfully completed."
color = buildColor
field {
title = "Application"
value = project.name
shortValue = true
}
field {
title = "Version"
value = project.version
shortValue = true
}
field {
title = "Branch"
value = "$branch"
shortValue = true
}
field {
title = "Commit"
value = "$commit"
shortValue = true
}
}
attachment {
color = buildColor
field {
title = "Results"
value = "N / A"
shortValue = false
}
}
}
validationSmokeTest {
attachment {
fallback = "Validation smoke tests successfully completed."
pretext = "Validation smoke tests successfully completed."
color = buildColor
field {
title = "Application"
value = project.name
shortValue = true
}
field {
title = "Version"
value = project.version
shortValue = true
}
field {
title = "Branch"
value = "$branch"
shortValue = true
}
field {
title = "Commit"
value = "$commit"
shortValue = true
}
}
attachment {
color = buildColor
field {
title = "Results"
value = "N / A"
shortValue = false
}
}
}
applicationBuildComplete {
attachment {
fallback = "$project.name build complete ..."
pretext = "$project.name build complete ..."
color = "warning"
field {
title = "Build Info"
value = "*Version*: $project.version"
shortValue = true
}
field {
title = "Git Info"
value = "*Branch*: $branch\n*Commit*: $commit"
shortValue = true
}
}
}
applicationHealthCheck {
verificationUrl = "http://localhost:7001/actuator/health"
attachment {
color = "danger"
field {
title = "Application Status: N / A"
value = "N / A"
shortValue = false
}
}
attachment {
color = "danger"
field {
title = "QA Status: N / A"
value = "N / A"
shortValue = false
}
}
}
applicationInfo {
verificationUrl = "http://localhost:7001/actuator/info"
attachment {
color = "danger"
field {
title = "Application Info: N / A"
value = "N / A"
shortValue = false
}
field {
title = "Build Info"
value = "N / A"
shortValue = true
}
field {
title = "Git Info"
value = "N / A"
shortValue = true
}
}
}
}
This is an example test block showing what needs to be included to populate the variables for the dynamic message as well as how the dynamic message send can be triggered after test complete.
test { testTask ->
maxParallelForks ((env == "LOCAL") ? 5 : parseInt("$intParallelism")) // Set max threads to speed up tests
reports.html.required = htmlReportsEnabled // Configured in gradle.properties
// ignoreFailures = true
testLogging { logging ->
events TestLogEvent.FAILED,
TestLogEvent.SKIPPED,
TestLogEvent.STANDARD_OUT,
TestLogEvent.STANDARD_ERROR
exceptionFormat TestExceptionFormat.FULL
showExceptions true
showCauses true
showStackTraces true
showStandardStreams(parseBoolean("$testShowStandardStreams")) // Show standard out and standard error of the test JVM(s) on the console
}
useJUnitPlatform {
if (!parseBoolean("$testAllEnabled")) {
// To run all available tests with ./gradlew clean test set `testAllEnabled` in gradle.properties to true
excludeTags "int", "slow", "thin", "load"
}
}
jacoco {
enabled = jacocoEnabled // Configured in gradle.properties
}
finalizedBy unitTestReports // instructs the tests to finish by generating a coverage report
afterSuite { desc, result ->
project.buildColor = result.failedTestCount == 0 ? "good" : "danger"
if (desc.parent) {
return
} // Only summarize results for whole modules
final String summary = "${result.resultType} " +
"(" +
"${result.testCount} tests, " +
"${result.successfulTestCount} successes, " +
"${result.failedTestCount} failures, " +
"${result.skippedTestCount} skipped" +
") " +
"in ${TimeCategory.minus(new Date(result.endTime), new Date(result.startTime))}" +
"\n"
project.testResults = []
// Add reports in `testsUnitResults`, keep failed suites at the end
if (result.resultType == TestResult.ResultType.SUCCESS) {
project.testResults.add(0, summary)
} else {
project.testResults += summary
}
}
}
test.finalizedBy combineJaCoCoReports // Combine all present JaCoCo reports (exec files) into one
combineJaCoCoReports.finalizedBy sendUnitTestAlert
sendUnitTestAlert.finalizedBy sendUnitTestResults