Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions gradle/publishing.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ subprojects {

publishing {
publications {
mavenJava(MavenPublication) {
afterEvaluate {
plugins.withId('java-platform') {
from components.javaPlatform
}
plugins.withId('java') {
from components.java
if (name != 'temporal-workflowcheck-gradle-plugin') {
mavenJava(MavenPublication) {
afterEvaluate {
plugins.withId('java-platform') {
from components.javaPlatform
}
plugins.withId('java') {
from components.java
}
}
}
}
Expand Down
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ include 'temporal-spring-boot-starter'
include 'temporal-remote-data-encoder'
include 'temporal-shaded'
include 'temporal-workflowcheck'
include 'temporal-workflowcheck-gradle-plugin'
include 'temporal-envconfig'
69 changes: 69 additions & 0 deletions temporal-workflowcheck-gradle-plugin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Temporal WorkflowCheck Gradle Plugin

A Gradle plugin that runs the [`temporal-workflowcheck`](../temporal-workflowcheck) bytecode analyzer to detect non-deterministic calls in Temporal Workflow code.

## Usage

Apply the plugin to a Java project that depends on the Temporal Java SDK:

```groovy
plugins {
id 'java'
id 'io.temporal.workflowcheck'
}

dependencies {
implementation 'io.temporal:temporal-sdk:<version>'
}
```

The plugin automatically:

- Detects the Temporal SDK version from `runtimeClasspath`.
- Downloads the matching `io.temporal:temporal-workflowcheck` analyzer.
- Registers a `workflowCheck` verification task.
- Wires `workflowCheck` into the `check` lifecycle.
- Forks a dedicated JVM to run the analysis, isolated from the Gradle daemon.

## Configuration

All options are configured through the `workflowCheck` extension:

```groovy
workflowCheck {
// Source sets to analyze. Default: ['main']
sourceSets = ['main']

// Fail the build on violations. Default: true
failOnViolation = true

// Show valid workflow methods alongside invalid ones. Default: false
showValid = false

// Skip the default workflowcheck configuration. Default: false
noDefaultConfig = false

// Additional .properties config files merged on top of defaults
configFiles.from(file('workflowcheck-overrides.properties'))

// Override the ASM version used by temporal-workflowcheck, for example for newer JDK support
asmVersion = '9.9.1'

// Max heap size for the forked JVM. Default: unset (JVM ergonomics)
maxHeapSize = '1g'
}
```

## Command-line options

```bash
# Show valid methods in the output
./gradlew workflowCheck --show-valid

# Skip the default workflowcheck config
./gradlew workflowCheck --no-default-config
```

## Report

The check report is written to `build/reports/workflowcheck/report.txt`.
21 changes: 21 additions & 0 deletions temporal-workflowcheck-gradle-plugin/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
plugins {
id 'java-gradle-plugin'
}

description = 'Temporal Java WorkflowCheck Gradle Plugin'

gradlePlugin {
plugins {
workflowCheck {
id = 'io.temporal.workflowcheck'
implementationClass = 'io.temporal.workflowcheck.gradle.WorkflowCheckPlugin'
displayName = 'Temporal WorkflowCheck Plugin'
description = 'Runs temporal-workflowcheck bytecode analyzer to detect non-deterministic calls in Temporal workflow code'
}
}
}

dependencies {
testImplementation gradleTestKit()
testImplementation "junit:junit:${junitVersion}"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package io.temporal.workflowcheck.gradle;

import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.provider.ListProperty;
import org.gradle.api.provider.Property;

/** Configuration for the {@code workflowCheck} task. */
public abstract class WorkflowCheckExtension {

public static final String NAME = "workflowCheck";

/** Source set names to check. Default: {@code ["main"]}. */
public abstract ListProperty<String> getSourceSets();

/** Additional {@code .properties} config files merged on top of defaults. */
public abstract ConfigurableFileCollection getConfigFiles();

/** Fail the build on violations. Default: {@code true}. */
public abstract Property<Boolean> getFailOnViolation();

/** Show valid workflow methods alongside invalid ones. Default: {@code false}. */
public abstract Property<Boolean> getShowValid();

/** Skip the default bundled workflowcheck config. Default: {@code false}. */
public abstract Property<Boolean> getNoDefaultConfig();

/** Override the ASM version used by temporal-workflowcheck. */
public abstract Property<String> getAsmVersion();

/** Max heap size for the forked JVM. Default: unset, which uses JVM ergonomics. */
public abstract Property<String> getMaxHeapSize();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package io.temporal.workflowcheck.gradle;

import java.util.Collections;
import java.util.stream.Collectors;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.ModuleVersionIdentifier;
import org.gradle.api.artifacts.ResolvedDependency;
import org.gradle.api.tasks.SourceSet;
import org.gradle.api.tasks.SourceSetContainer;
import org.gradle.api.tasks.TaskProvider;

/** Gradle plugin that runs the temporal-workflowcheck bytecode analyzer. */
public class WorkflowCheckPlugin implements Plugin<Project> {

public static final String CONFIGURATION_NAME = "workflowcheck";

@Override
public void apply(Project project) {
project
.getPluginManager()
.withPlugin(
"java",
appliedPlugin -> {
WorkflowCheckExtension extension =
project
.getExtensions()
.create(WorkflowCheckExtension.NAME, WorkflowCheckExtension.class);

extension.getSourceSets().convention(Collections.singletonList("main"));
extension.getFailOnViolation().convention(true);
extension.getShowValid().convention(false);
extension.getNoDefaultConfig().convention(false);

Configuration toolClasspath =
project
.getConfigurations()
.create(
CONFIGURATION_NAME,
configuration -> {
configuration.setCanBeConsumed(false);
configuration.setCanBeResolved(true);
configuration.setVisible(false);
configuration.setDescription(
"Classpath for the temporal-workflowcheck bytecode analyzer");
});

SourceSetContainer sourceSets =
project.getExtensions().getByType(SourceSetContainer.class);

toolClasspath.defaultDependencies(
dependencies -> {
String sdkVersion =
findTemporalSdkVersion(
project, sourceSets, extension.getSourceSets().get());
if (sdkVersion != null) {
dependencies.add(
project
.getDependencies()
.create("io.temporal:temporal-workflowcheck:" + sdkVersion));
}
});

toolClasspath
.getResolutionStrategy()
.eachDependency(
details -> {
if ("org.ow2.asm".equals(details.getRequested().getGroup())
&& extension.getAsmVersion().isPresent()) {
details.useVersion(extension.getAsmVersion().get());
details.because(
"Overridden by workflowCheck.asmVersion for JDK compatibility");
}
});

TaskProvider<WorkflowCheckTask> workflowCheckTask =
project
.getTasks()
.register(
WorkflowCheckTask.TASK_NAME,
WorkflowCheckTask.class,
task -> {
task.setDescription(
"Checks Temporal workflow code for determinism violations");
task.setGroup("verification");

task.getToolClasspath().from(toolClasspath);
task.getClasspath()
.from(
extension
.getSourceSets()
.map(
names ->
names.stream()
.map(sourceSets::getByName)
.map(SourceSet::getRuntimeClasspath)
.collect(Collectors.toList())));
task.dependsOn(
extension
.getSourceSets()
.map(
names ->
names.stream()
.map(sourceSets::getByName)
.map(SourceSet::getClassesTaskName)
.collect(Collectors.toList())));

task.getConfigFiles().from(extension.getConfigFiles());
task.getFailOnViolation().set(extension.getFailOnViolation());
task.getShowValid().set(extension.getShowValid());
task.getNoDefaultConfig().set(extension.getNoDefaultConfig());
task.getMaxHeapSize().set(extension.getMaxHeapSize());
task.getReportFile()
.set(
project
.getLayout()
.getBuildDirectory()
.file("reports/workflowcheck/report.txt"));
});

project
.getTasks()
.named("check")
.configure(check -> check.dependsOn(workflowCheckTask));
});
}

/**
* Walks resolved runtime dependencies for the configured source sets to find {@code
* io.temporal:temporal-sdk} and returns its version.
*/
static String findTemporalSdkVersion(
Project project, SourceSetContainer sourceSets, Iterable<String> sourceSetNames) {
for (String sourceSetName : sourceSetNames) {
try {
SourceSet sourceSet = sourceSets.getByName(sourceSetName);
Configuration runtimeClasspath =
project.getConfigurations().getByName(sourceSet.getRuntimeClasspathConfigurationName());
for (ResolvedDependency dependency :
runtimeClasspath.getResolvedConfiguration().getFirstLevelModuleDependencies()) {
String version = findSdkVersionRecursive(dependency);
if (version != null) {
return version;
}
}
} catch (Exception e) {
project
.getLogger()
.warn(
"WorkflowCheck: Failed to resolve Temporal SDK version from source set {}: {}",
sourceSetName,
e.getMessage());
}
}
project
.getLogger()
.warn(
"WorkflowCheck: No io.temporal:temporal-sdk found on configured source set runtime classpaths. Task will be skipped.");
return null;
}

private static String findSdkVersionRecursive(ResolvedDependency dependency) {
ModuleVersionIdentifier id = dependency.getModule().getId();
if ("io.temporal".equals(id.getGroup()) && "temporal-sdk".equals(id.getName())) {
return id.getVersion();
}
for (ResolvedDependency child : dependency.getChildren()) {
String found = findSdkVersionRecursive(child);
if (found != null) {
return found;
}
}
return null;
}
}
Loading