Skip to content

Commit e5a9fec

Browse files
committed
Add WorkflowCheck Gradle plugin module
1 parent 3da1f1d commit e5a9fec

7 files changed

Lines changed: 718 additions & 0 deletions

File tree

settings.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@ include 'temporal-spring-boot-starter'
1414
include 'temporal-remote-data-encoder'
1515
include 'temporal-shaded'
1616
include 'temporal-workflowcheck'
17+
include 'temporal-workflowcheck-gradle-plugin'
1718
include 'temporal-envconfig'
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Temporal WorkflowCheck Gradle Plugin
2+
3+
A Gradle plugin that runs the [`temporal-workflowcheck`](../temporal-workflowcheck) bytecode analyzer to detect non-deterministic calls in Temporal Workflow code.
4+
5+
## Usage
6+
7+
Apply the plugin to a Java project that depends on the Temporal Java SDK:
8+
9+
```groovy
10+
plugins {
11+
id 'java'
12+
id 'io.temporal.workflowcheck'
13+
}
14+
15+
dependencies {
16+
implementation 'io.temporal:temporal-sdk:<version>'
17+
}
18+
```
19+
20+
The plugin automatically:
21+
22+
- Detects the Temporal SDK version from `runtimeClasspath`.
23+
- Downloads the matching `io.temporal:temporal-workflowcheck` analyzer.
24+
- Registers a `workflowCheck` verification task.
25+
- Wires `workflowCheck` into the `check` lifecycle.
26+
- Forks a dedicated JVM to run the analysis, isolated from the Gradle daemon.
27+
28+
## Configuration
29+
30+
All options are configured through the `workflowCheck` extension:
31+
32+
```groovy
33+
workflowCheck {
34+
// Source sets to analyze. Default: ['main']
35+
sourceSets = ['main']
36+
37+
// Fail the build on violations. Default: true
38+
failOnViolation = true
39+
40+
// Show valid workflow methods alongside invalid ones. Default: false
41+
showValid = false
42+
43+
// Skip the default workflowcheck configuration. Default: false
44+
noDefaultConfig = false
45+
46+
// Additional .properties config files merged on top of defaults
47+
configFiles.from(file('workflowcheck-overrides.properties'))
48+
49+
// Override the ASM version used by temporal-workflowcheck, for example for newer JDK support
50+
asmVersion = '9.9.1'
51+
52+
// Max heap size for the forked JVM. Default: unset (JVM ergonomics)
53+
maxHeapSize = '1g'
54+
}
55+
```
56+
57+
## Command-line options
58+
59+
```bash
60+
# Show valid methods in the output
61+
./gradlew workflowCheck --show-valid
62+
63+
# Skip the default workflowcheck config
64+
./gradlew workflowCheck --no-default-config
65+
```
66+
67+
## Report
68+
69+
The check report is written to `build/reports/workflowcheck/report.txt`.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
plugins {
2+
id 'java-gradle-plugin'
3+
}
4+
5+
description = 'Temporal Java WorkflowCheck Gradle Plugin'
6+
7+
gradlePlugin {
8+
plugins {
9+
workflowCheck {
10+
id = 'io.temporal.workflowcheck'
11+
implementationClass = 'io.temporal.workflowcheck.gradle.WorkflowCheckPlugin'
12+
displayName = 'Temporal WorkflowCheck Plugin'
13+
description = 'Runs temporal-workflowcheck bytecode analyzer to detect non-deterministic calls in Temporal workflow code'
14+
}
15+
}
16+
}
17+
18+
dependencies {
19+
testImplementation gradleTestKit()
20+
testImplementation "junit:junit:${junitVersion}"
21+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package io.temporal.workflowcheck.gradle;
2+
3+
import org.gradle.api.file.ConfigurableFileCollection;
4+
import org.gradle.api.provider.ListProperty;
5+
import org.gradle.api.provider.Property;
6+
7+
/** Configuration for the {@code workflowCheck} task. */
8+
public abstract class WorkflowCheckExtension {
9+
10+
public static final String NAME = "workflowCheck";
11+
12+
/** Source set names to check. Default: {@code ["main"]}. */
13+
public abstract ListProperty<String> getSourceSets();
14+
15+
/** Additional {@code .properties} config files merged on top of defaults. */
16+
public abstract ConfigurableFileCollection getConfigFiles();
17+
18+
/** Fail the build on violations. Default: {@code true}. */
19+
public abstract Property<Boolean> getFailOnViolation();
20+
21+
/** Show valid workflow methods alongside invalid ones. Default: {@code false}. */
22+
public abstract Property<Boolean> getShowValid();
23+
24+
/** Skip the default bundled workflowcheck config. Default: {@code false}. */
25+
public abstract Property<Boolean> getNoDefaultConfig();
26+
27+
/** Override the ASM version used by temporal-workflowcheck. */
28+
public abstract Property<String> getAsmVersion();
29+
30+
/** Max heap size for the forked JVM. Default: unset, which uses JVM ergonomics. */
31+
public abstract Property<String> getMaxHeapSize();
32+
}
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
package io.temporal.workflowcheck.gradle;
2+
3+
import java.util.Collections;
4+
import java.util.stream.Collectors;
5+
import org.gradle.api.Plugin;
6+
import org.gradle.api.Project;
7+
import org.gradle.api.artifacts.Configuration;
8+
import org.gradle.api.artifacts.ModuleVersionIdentifier;
9+
import org.gradle.api.artifacts.ResolvedDependency;
10+
import org.gradle.api.tasks.SourceSet;
11+
import org.gradle.api.tasks.SourceSetContainer;
12+
import org.gradle.api.tasks.TaskProvider;
13+
14+
/** Gradle plugin that runs the temporal-workflowcheck bytecode analyzer. */
15+
public class WorkflowCheckPlugin implements Plugin<Project> {
16+
17+
public static final String CONFIGURATION_NAME = "workflowcheck";
18+
19+
@Override
20+
public void apply(Project project) {
21+
project
22+
.getPluginManager()
23+
.withPlugin(
24+
"java",
25+
appliedPlugin -> {
26+
WorkflowCheckExtension extension =
27+
project
28+
.getExtensions()
29+
.create(WorkflowCheckExtension.NAME, WorkflowCheckExtension.class);
30+
31+
extension.getSourceSets().convention(Collections.singletonList("main"));
32+
extension.getFailOnViolation().convention(true);
33+
extension.getShowValid().convention(false);
34+
extension.getNoDefaultConfig().convention(false);
35+
36+
Configuration toolClasspath =
37+
project
38+
.getConfigurations()
39+
.create(
40+
CONFIGURATION_NAME,
41+
configuration -> {
42+
configuration.setCanBeConsumed(false);
43+
configuration.setCanBeResolved(true);
44+
configuration.setVisible(false);
45+
configuration.setDescription(
46+
"Classpath for the temporal-workflowcheck bytecode analyzer");
47+
});
48+
49+
SourceSetContainer sourceSets =
50+
project.getExtensions().getByType(SourceSetContainer.class);
51+
52+
toolClasspath.defaultDependencies(
53+
dependencies -> {
54+
String sdkVersion =
55+
findTemporalSdkVersion(
56+
project, sourceSets, extension.getSourceSets().get());
57+
if (sdkVersion != null) {
58+
dependencies.add(
59+
project
60+
.getDependencies()
61+
.create("io.temporal:temporal-workflowcheck:" + sdkVersion));
62+
}
63+
});
64+
65+
toolClasspath
66+
.getResolutionStrategy()
67+
.eachDependency(
68+
details -> {
69+
if ("org.ow2.asm".equals(details.getRequested().getGroup())
70+
&& extension.getAsmVersion().isPresent()) {
71+
details.useVersion(extension.getAsmVersion().get());
72+
details.because(
73+
"Overridden by workflowCheck.asmVersion for JDK compatibility");
74+
}
75+
});
76+
77+
TaskProvider<WorkflowCheckTask> workflowCheckTask =
78+
project
79+
.getTasks()
80+
.register(
81+
WorkflowCheckTask.TASK_NAME,
82+
WorkflowCheckTask.class,
83+
task -> {
84+
task.setDescription(
85+
"Checks Temporal workflow code for determinism violations");
86+
task.setGroup("verification");
87+
88+
task.getToolClasspath().from(toolClasspath);
89+
task.getClasspath()
90+
.from(
91+
extension
92+
.getSourceSets()
93+
.map(
94+
names ->
95+
names.stream()
96+
.map(sourceSets::getByName)
97+
.map(SourceSet::getRuntimeClasspath)
98+
.collect(Collectors.toList())));
99+
task.dependsOn(
100+
extension
101+
.getSourceSets()
102+
.map(
103+
names ->
104+
names.stream()
105+
.map(sourceSets::getByName)
106+
.map(SourceSet::getClassesTaskName)
107+
.collect(Collectors.toList())));
108+
109+
task.getConfigFiles().from(extension.getConfigFiles());
110+
task.getFailOnViolation().set(extension.getFailOnViolation());
111+
task.getShowValid().set(extension.getShowValid());
112+
task.getNoDefaultConfig().set(extension.getNoDefaultConfig());
113+
task.getMaxHeapSize().set(extension.getMaxHeapSize());
114+
task.getReportFile()
115+
.set(
116+
project
117+
.getLayout()
118+
.getBuildDirectory()
119+
.file("reports/workflowcheck/report.txt"));
120+
});
121+
122+
project
123+
.getTasks()
124+
.named("check")
125+
.configure(check -> check.dependsOn(workflowCheckTask));
126+
});
127+
}
128+
129+
/**
130+
* Walks resolved runtime dependencies for the configured source sets to find {@code
131+
* io.temporal:temporal-sdk} and returns its version.
132+
*/
133+
static String findTemporalSdkVersion(
134+
Project project, SourceSetContainer sourceSets, Iterable<String> sourceSetNames) {
135+
for (String sourceSetName : sourceSetNames) {
136+
try {
137+
SourceSet sourceSet = sourceSets.getByName(sourceSetName);
138+
Configuration runtimeClasspath =
139+
project.getConfigurations().getByName(sourceSet.getRuntimeClasspathConfigurationName());
140+
for (ResolvedDependency dependency :
141+
runtimeClasspath.getResolvedConfiguration().getFirstLevelModuleDependencies()) {
142+
String version = findSdkVersionRecursive(dependency);
143+
if (version != null) {
144+
return version;
145+
}
146+
}
147+
} catch (Exception e) {
148+
project
149+
.getLogger()
150+
.warn(
151+
"WorkflowCheck: Failed to resolve Temporal SDK version from source set {}: {}",
152+
sourceSetName,
153+
e.getMessage());
154+
}
155+
}
156+
project
157+
.getLogger()
158+
.warn(
159+
"WorkflowCheck: No io.temporal:temporal-sdk found on configured source set runtime classpaths. Task will be skipped.");
160+
return null;
161+
}
162+
163+
private static String findSdkVersionRecursive(ResolvedDependency dependency) {
164+
ModuleVersionIdentifier id = dependency.getModule().getId();
165+
if ("io.temporal".equals(id.getGroup()) && "temporal-sdk".equals(id.getName())) {
166+
return id.getVersion();
167+
}
168+
for (ResolvedDependency child : dependency.getChildren()) {
169+
String found = findSdkVersionRecursive(child);
170+
if (found != null) {
171+
return found;
172+
}
173+
}
174+
return null;
175+
}
176+
}

0 commit comments

Comments
 (0)