Part 5: Add a DataFlow Action
Learn how to use Gradle’s modern Dataflow API to hook into the build lifecycle and add an action to your plugin.
Step 1: Understanding the Dataflow API
Dataflow Actions are Gradle’s modern, recommended way to react to the build lifecycle and handle build-wide events.
They are superior to older methods like BuildListener
because they are fully compatible with the Configuration Cache, leading to faster and more reliable builds.
A FlowAction
is a declarative, isolated, and lazy unit of work that runs in response to specific events, such as the build finishing.
The key components of the Dataflow API are:
-
FlowScope
: A service that allows you to register `FlowAction`s to be triggered under certain conditions. -
FlowAction<T>
: An interface you implement to define the actual logic that runs. -
FlowParameters
: A container for all the inputs to theFlowAction
.
Step 2: Creating a Dataflow Action
Let’s create the FlowAction
that will send a Slack message at the end of the build.
Create a new file named `SlackBuildFlowAction.kt` in `plugin/src/main/kotlin/org/example/` and add the following code:
Create a new file named `SlackBuildFlowAction.groovy` in `plugin/src/main/groovy/org/example/` and add the following code:
package org.example
import com.slack.api.Slack
import com.slack.api.methods.request.chat.ChatPostMessageRequest
import org.gradle.api.flow.FlowAction
import org.gradle.api.flow.FlowParameters
import org.gradle.api.logging.Logging
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
/**
* A Gradle FlowAction that sends a Slack message at the end of a build.
* This leverages Gradle's build lifecycle via the dataflow API, which allows actions
* to run automatically when the build completes—without needing to attach listeners manually.
*/
abstract class SlackBuildFlowAction : FlowAction<SlackBuildFlowAction.Params> {
private val logger = Logging.getLogger(SlackBuildFlowAction::class.java)
/**
* Parameters that are passed to this action when it executes.
* These are injected by Gradle and used to control what the action does.
*/
interface Params : FlowParameters {
/** Slack bot token used to authenticate API requests */
@get:Input
val token: Property<String>
/** Slack channel ID or name to send the message to */
@get:Input
val channel: Property<String>
/** Flag indicating whether the build failed */
@get:Input
val buildFailed: Property<Boolean>
}
/**
* Executes the action when the build finishes.
* Constructs and sends a Slack message indicating whether the build succeeded or failed.
*/
override fun execute(parameters: Params) {
// Initialize the Slack client and get the API methods interface
val slack = Slack.getInstance()
val methods = slack.methods(parameters.token.get())
// Compose the message text based on the build result
val status = if (parameters.buildFailed.get()) "Build failed" else "Build succeeded"
// Create a Slack message request
val request = ChatPostMessageRequest.builder()
.channel(parameters.channel.get())
.text(status)
.build()
// Send the message via the Slack API and check for success
val response = methods.chatPostMessage(request)
if (response.isOk) {
logger.lifecycle("Slack message sent successfully to channel ${response.channel}")
} else {
logger.error("Failed to send Slack message: ${response.error}")
throw RuntimeException("Slack message failed: ${response.error}")
}
}
}
package org.example
import com.slack.api.Slack
import com.slack.api.methods.request.chat.ChatPostMessageRequest
import org.gradle.api.flow.FlowAction
import org.gradle.api.flow.FlowParameters
import org.gradle.api.logging.Logging
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
/**
* A Gradle FlowAction that sends a Slack message at the end of a build.
* This leverages Gradle's build lifecycle via the dataflow API, which allows actions
* to run automatically when the build completes—without needing to attach listeners manually.
*/
abstract class SlackBuildFlowAction implements FlowAction<SlackBuildFlowAction.Params> {
private final logger = Logging.getLogger(SlackBuildFlowAction)
/**
* Parameters that are passed to this action when it executes.
* These are injected by Gradle and used to control what the action does.
*/
interface Params extends FlowParameters {
/** Slack bot token used to authenticate API requests */
@Input
Property<String> getToken()
/** Slack channel ID or name to send the message to */
@Input
Property<String> getChannel()
/** Flag indicating whether the build failed */
@Input
Property<Boolean> getBuildFailed()
}
/**
* Executes the action when the build finishes.
* Constructs and sends a Slack message indicating whether the build succeeded or failed.
*/
@Override
void execute(Params parameters) {
// Initialize the Slack client and get the API methods interface
def slack = Slack.getInstance()
def methods = slack.methods(parameters.token.get())
// Compose the message text based on the build result
def status = parameters.buildFailed.get() ? 'Build failed' : 'Build succeeded'
// Create a Slack message request
def request = ChatPostMessageRequest.builder()
.channel(parameters.channel.get())
.text(status)
.build()
try {
// Send the message via the Slack API and check for success
def response = methods.chatPostMessage(request)
if (response.isOk()) {
logger.lifecycle("Slack message sent successfully to channel ${parameters.channel.get()}")
} else {
logger.error("Failed to send Slack message: ${response.error}")
throw new RuntimeException("Slack message failed: ${response.error}")
}
} catch (Exception e) {
logger.error("Exception while sending Slack message", e)
throw new RuntimeException("Slack message failed", e)
}
}
}
This code completes two of the three main components of our flow action:
-
The
SlackBuildFlowAction
class is aFlowAction
that uses the Slack API client to send a message. -
The
SlackBuildFlowAction.Params
interface defines the input parameters for the action, including thetoken
,channel
, and abuildFailed
flag.
Step 3: Registering the Dataflow Action in the Plugin
Now, we’ll register the flow action in our plugin’s apply()
method to connect it to the build lifecycle.
Update your SlackPlugin
class to look like this:
package org.example
import org.gradle.api.Project
import org.gradle.api.Plugin
import org.gradle.api.tasks.TaskProvider
import org.gradle.api.flow.*
import javax.inject.Inject
abstract class SlackPlugin : Plugin<Project> {
@Inject
abstract fun getFlowScope(): FlowScope
@Inject
abstract fun getFlowProviders(): FlowProviders
override fun apply(project: Project) {
// Create the 'slack' extension so users can configure token, channel, and message
val extension = project.extensions.create("slack", SlackExtension::class.java)
// Register a task named 'sendTestSlackMessage' of type SlackTask
val taskProvider: TaskProvider<SlackTask> = project.tasks.register("sendTestSlackMessage", SlackTask::class.java)
// Configure the task using values from the extension
taskProvider.configure {
it.group = "notification" // Logical task grouping for help output
it.description = "Sends a test message to Slack using the configured token and channel."
// Bind extension values to the task's input properties
it.token.set(extension.token)
it.channel.set(extension.channel)
it.message.set(extension.message)
}
// Hook into the build lifecycle using the dataflow API
getFlowScope().always(SlackBuildFlowAction::class.java) { spec ->
spec.parameters.token.set(extension.token)
spec.parameters.channel.set(extension.channel)
spec.parameters.buildFailed.set(getFlowProviders().buildWorkResult.map { it.failure.isPresent })
}
}
}
package org.example
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.tasks.TaskProvider
import org.gradle.api.flow.*
import javax.inject.Inject
/**
* The SlackPlugin class is a Gradle plugin that registers the 'slack' extension
* and a task, and also hooks into the build lifecycle using a FlowAction.
*/
abstract class SlackPlugin implements Plugin<Project> {
@Inject
abstract FlowScope getFlowScope()
@Inject
abstract FlowProviders getFlowProviders()
@Override
void apply(Project project) {
// Create the 'slack' extension so users can configure token, channel, and message
def extension = project.extensions.create('slack', SlackExtension)
// Register a task named 'sendTestSlackMessage' of type SlackTask
TaskProvider<SlackTask> taskProvider = project.tasks.register('sendTestSlackMessage', SlackTask)
// Configure the task using values from the extension
taskProvider.configure {
it.group = 'notification' // Logical task grouping for help output
it.description = 'Sends a test message to Slack using the configured token and channel.'
// Bind extension values to the task's input properties
it.token.set(extension.token)
it.channel.set(extension.channel)
it.message.set(extension.message)
}
// Hook into the build lifecycle using the dataflow API
getFlowScope().always(SlackBuildFlowAction) { spec ->
spec.parameters.token.set(extension.token)
spec.parameters.channel.set(extension.channel)
spec.parameters.buildFailed.set(getFlowProviders().buildWorkResult.map { it.failure.isPresent() })
}
}
}
In this code, we ask Gradle to inject two services that are essential for the Dataflow API:
-
@Inject abstract fun getFlowScope(): FlowScope
: This is used to register our action with thealways
scope, which means it will run after every build, regardless of the outcome. -
@Inject abstract fun getFlowProviders(): FlowProviders
: This service provides access to data about the build, such as whether it succeeded or failed, via thebuildWorkResult
provider.
By using these services, we can configure our FlowAction
to run automatically at the end of the build, providing a robust and cache-compatible way to send build notifications.
Next Step: Write a Functional Test >>