Easily test your application against a containerized, configurable Zeebe instance.
Please refer to testcontainers.org for general documentation on how to use containers for your tests, as well as general prerequisites.
NOTE: version 1.0 is incompatible with Zeebe versions pre 0.23.x
Version 1.x and 2.x is compatible with the following Zeebe versions:
- 0.23.x
- 0.24.x
- 0.25.x
- 0.26.x
Version 3.x is compatible with the following Zeebe versions:
- 1.x
Add the project to your dependencies:
<dependency>
<groupId>io.zeebe</groupId>
<artifactId>zeebe-test-container</artifactId>
<version>3.1.0</version>
</dependency>
testImplementation 'io.zeebe:zeebe-test-container:3.1.0'
Zeebe Test Container is built for Java 8+, and will not work on lower Java versions.
Additionally, you will need to comply with all the Testcontainers requirements, as defined here.
As there is currently only a single maintainer, only the latest major version will be maintained and supported.
zeebe-test-container
uses API guardian to
declare the stability and guarantees of its API.
Every class/interface/etc. is annotated with an @API
annotation describing its status.
Additionally, at times, inner members of a class/interface/etc. may be annotated with an
overriding @API
annotation. In that case, it would take precedence over its parent annotation. For example, if you
have the following:
import org.apiguardian.api.API;
import org.apiguardian.api.API.Status;
@API(status = Status.STABLE)
public class MyClass {
@API(status = Status.EXPERIMENTAL)
public void myExperimentalMethod() {
}
public void myStableMethod() {
}
}
Then we can assume the contract for MyClass
is stable and will not be changed until the next major
version, except for its method myExperimentalMethod
, which is an experimental addition which
may change at any time, at least until it is marked as stable or dropped.
NOTE: for contributors, please remember to annotate new additions, and to maintain the compatibility guarantees of pre-annotated entities.
If you're using junit4, you can add the container as a rule: it will be started and closed around each test execution. You can read more about Testcontainers and junit4 here.
package com.acme.zeebe;
import io.camunda.zeebe.client.ZeebeClient;
import io.camunda.zeebe.client.api.response.DeploymentEvent;
import io.camunda.zeebe.client.api.response.ProcessInstanceResult;
import io.zeebe.containers.ZeebeContainer;
import io.camunda.zeebe.model.bpmn.Bpmn;
import io.camunda.zeebe.model.bpmn.BpmnModelInstance;
import org.assertj.core.api.Assertions;
import org.junit.Rule;
import org.junit.Test;
public class MyFeatureTest {
@Rule
public ZeebeContainer zeebeContainer = new ZeebeContainer();
@Test
public void shouldConnectToZeebe() {
// given
final ZeebeClient client =
ZeebeClient.newClientBuilder()
.gatewayAddress(zeebeContainer.getExternalGatewayAddress())
.usePlaintext()
.build();
final BpmnModelInstance process =
Bpmn.createExecutableProcess("process").startEvent().endEvent().done();
// when
// do something (e.g. deploy a process)
final DeploymentEvent deploymentEvent =
client.newDeployCommand().addProcessModel(process, "process.bpmn").send().join();
// then
// verify (e.g. we can create an instance of the deployed process)
final ProcessInstanceResult processInstanceResult =
client
.newCreateInstanceCommand()
.bpmnProcessId("process")
.latestVersion()
.withResult()
.send()
.join();
Assertions.assertThat(processInstanceResult.getProcessDefinitionKey())
.isEqualTo(deploymentEvent.getProcesses().get(0).getProcessDefinitionKey());
}
}
If you're using junit5, you can use the Testcontainers
extension. It will manage the container
lifecycle for you. You can read more about the
extension here.
package com.acme.zeebe;
import io.camunda.zeebe.client.ZeebeClient;
import io.camunda.zeebe.client.api.response.DeploymentEvent;
import io.camunda.zeebe.client.api.response.ProcessInstanceResult;
import io.zeebe.containers.ZeebeContainer;
import io.camunda.zeebe.model.bpmn.Bpmn;
import io.camunda.zeebe.model.bpmn.BpmnModelInstance;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@Testcontainers
public class MyFeatureTest {
@Container
private final ZeebeContainer zeebeContainer = new ZeebeContainer();
@Test
void shouldConnectToZeebe() {
// given
final ZeebeClient client =
ZeebeClient.newClientBuilder()
.gatewayAddress(zeebeContainer.getExternalGatewayAddress())
.usePlaintext()
.build();
final BpmnModelInstance process =
Bpmn.createExecutableProcess("process").startEvent().endEvent().done();
// when
// do something (e.g. deploy a process)
final DeploymentEvent deploymentEvent =
client.newDeployCommand().addProcessModel(process, "process.bpmn").send().join();
// then
// verify (e.g. we can create an instance of the deployed process)
final ProcessInstanceResult processInstanceResult =
client
.newCreateInstanceCommand()
.bpmnProcessId("process")
.latestVersion()
.withResult()
.send()
.join();
Assertions.assertThat(processInstanceResult.getProcessDefinitionKey())
.isEqualTo(deploymentEvent.getProcesses().get(0).getProcessDefinitionKey());
}
}
zeebe-test-container
provides three different containers:
ZeebeContainer
: a Zeebe broker with an embedded gatewayZeebeBrokerContainer
: a Zeebe broker without an embedded gatewayZeebeGatewayContainer
: a standalone Zeebe gatewayZeebeCluster
: an experimental cluster builder API which lets you manage a Zeebe cluster made of various containers
If you're unsure which one you should use, then you probably want to use
ZeebeContainer
, as it is the quickest way to test your application against Zeebe.
ZeebeContainer
will start a new Zeebe broker with embedded gateway. For most tests, this is what
you will want to use. It provides all the functionality of a Zeebe single node deployment, which for
testing purposes should be enough.
The container is considered started if and only if:
- The monitoring, command, cluster, and gateway ports are open and accepting connections (read more about the ports here) .
- The broker ready check returns a 204 (see more about this check here) .
- The gateway topology is considered complete.
A topology is considered complete if there is a leader for all partitions.
Once started, the container is ready to accept commands, and a client can connect to it by setting
its gatewayAddress
to ZeebeContainer#getExternalGatewayAddress()
.
ZeebeBrokerContainer
will start a new Zeebe broker with no embedded gateway. As it contains no
gateway, the use case for this container is to test Zeebe in clustered mode. As such, it will
typically be combined with a ZeebeGatewayContainer
or a ZeebeContainer
.
The container is considered started if and only if:
- The monitoring, command, and cluster ports are open and accepting connections (read more about the ports here) .
- The broker ready check returns a 204 (see more about this check here) .
Once started, the container is ready to accept commands via the command port; you should therefore link a gateway to it if you wish to use it.
ZeebeGatewayContainer
will start a new Zeebe standalone gateway. As it is only a gateway, it
should be linked to at least one broker - a ZeebeContainer
or ZeebeBrokerContainer
. By default,
it will not expose the monitoring port, as monitoring is not enabled in the gateway by default. If
you enable monitoring, remember to expose the port as well via
GenericContainer#addExposedPort(int)
.
The container is considered started if and only if:
- The cluster and gateway ports are open and accepting connections (read more about the ports here) .
- The gateway topology is considered complete.
A topology is considered complete if there is a leader for all partitions.
Once started, the container is ready to accept commands, and a client can connect to it by setting
its gatewayAddress
to ZeebeContainer#getExternalGatewayAddress()
.
Configuring your Zeebe container of choice is done exactly as you normally would - via environment variables or via configuration file. You can find out more about it on the Zeebe documentation website .
Zeebe 0.23.x and upwards use Spring Boot for configuration - refer to their documentation on how environment variables are mapped to configuration settings. You can read more about this here
Testcontainers provide mechanisms through which environment variables can be injected, or configuration files mounted. Refer to their documentation for more.
A series of examples are included as part of the tests, see test/java/io/zeebe/containers/examples.
Note that these are written for junit5.
If you wish to use this with your continous integration pipeline (e.g. Jenkins, CircleCI), the Testcontainers has a section explaining how to use it, how volumes can be shared, etc.
There are currently several experimental features across the project. As described in the
compatibility guarantees, we only guarantee backwards compatibility for stable APIs (marked by the
annotation @API(status = Status.STABLE)
). Typically, you shouldn't be using anything else.
However, there are some features which already provide value, but for which the correct API is
unclear; these are marked with @API(status = Status.EXPERIMENTAL)
. These may be changed, or
dropped depending on their usefulness.
NOTE: you should never use anything marked as
@API(status = Status.INTERNAL)
. These are there purely for internal purposes, and cannot be relied on at all.
NOTE: the cluster API is currently an experimental API. You're encouraged to use it and give feedback, as this is how we can validate it. Keep in mind however that it is subject to change in the future.
A typical production Zeebe deployment will be a cluster of nodes, some brokers, and possibly some standalone gateways. It can be useful to test against such deployments for acceptance or E2E tests.
While it's not too hard to manually link several containers, it can become tedious and error prone if you want to test many different configurations. The cluster API provides you with an easy way to programmatically build Zeebe deployments while minimizing the surface of configuration errors.
NOTE: if you have a static deployment that you don't need to change programmatically per test, then you might want to consider setting up a static Zeebe cluster in your CI pipeline (either via Helm or docker-compose), or even using Testcontainer's docker-compose feature.
The main entry point is the ZeebeClusterBuilder
, which can be instantiated via
ZeebeCluster#builder()
. The builder can be used to configure the topology of your cluster. You can
configure the following:
- the number of brokers in the cluster (by default 1)
- whether brokers should be using the embedded gateway (by default true)
- the number of standalone gateways in the cluster (by default 0)
- the number of partitions in the cluster (by default 1)
- the replication factor of each partition (by default 1)
- the network the containers should use (by default
Network#SHARED
)
Container instances (e.g. ZeebeBrokerContainer
or ZeebeContainer
) are only instantiated and
configured once you call #build()
. At this point, your cluster is configured in a valid way, but
isn't started yet - that is, no real Docker containers have been created/started.
Once your cluster is built, you can access its brokers and gateways via ZeebeCluster#getBrokers
and ZeebeCluster#getGateways
. This allows you to further configure each container as you wish
before actually starting the cluster.
Here is a short example on how to set up a cluster for testing with junit5.
package com.acme.zeebe;
import io.camunda.zeebe.client.ZeebeClient;
import io.camunda.zeebe.client.api.response.BrokerInfo;
import io.camunda.zeebe.client.api.response.Topology;
import io.zeebe.containers.cluster.ZeebeCluster;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
/**
* Showcases how you can create a test with a cluster of two brokers and one standalone gateway.
* Configuration is kept to minimum, as the goal here is only to showcase how to connect the
* different nodes together.
*/
class ZeebeClusterWithGatewayExampleTest {
private final ZeebeCluster cluster =
ZeebeCluster.builder()
.withEmbeddedGateway(false)
.withGatewaysCount(1)
.withBrokersCount(2)
.withPartitionsCount(2)
.withReplicationFactor(1)
.build();
@AfterEach
void tearDown() {
cluster.stop();
}
@Test
@Timeout(value = 15, unit = TimeUnit.MINUTES)
void shouldStartCluster() {
// given
cluster.start();
// when
final Topology topology;
try (final ZeebeClient client = cluster.newClientBuilder().build()) {
topology = client.newTopologyRequest().send().join(5, TimeUnit.SECONDS);
}
// then
final List<BrokerInfo> brokers = topology.getBrokers();
Assertions.assertThat(topology.getClusterSize()).isEqualTo(3);
Assertions.assertThat(brokers)
.hasSize(2)
.extracting(BrokerInfo::getAddress)
.containsExactlyInAnyOrder(
cluster.getBrokers().get(0).getInternalCommandAddress(),
cluster.getBrokers().get(1).getInternalCommandAddress());
}
}
You can find more examples by looking at the test/java/io/zeebe/containers/examples/cluster package.
There are some caveat as well. For example, if you want to create a large cluster with many brokers and need to increase the startup time:
package com.acme.zeebe;
import io.zeebe.containers.cluster.ZeebeCluster;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
class ZeebeHugeClusterTest {
private final ZeebeCluster cluster =
ZeebeCluster.builder()
.withEmbeddedGateway(false)
.withGatewaysCount(3)
.withBrokersCount(6)
.withPartitionsCount(6)
.withReplicationFactor(3)
.build();
@AfterEach
void tearDown() {
cluster.stop();
}
@Test
@Timeout(value = 30, unit = TimeUnit.MINUTES)
void shouldStartCluster() {
// given
// configure each container to have a high start up time as they get started in parallel
cluster.getBrokers().values()
.forEach(broker -> broker.self().withStartupTimeout(Duration.ofMinutes(5)));
cluster.getGateways().values()
.forEach(gateway -> gateway.self().withStartupTimeout(Duration.ofMinutes(5)));
cluster.start();
// test more things
}
}
There might be cases where you want to debug a container you just started in one of your tests. You can use the RemoteDebugger utility for this. By default, it will start your container and attach a debugging agent to it on port 5005. The container startup is then suspended until a debugger attaches to it.
NOTE: since the startup is suspended until a debugger connects to it, it's possible for a the startup strategy to time out if no debugger connects to it.
You can use it with any container as:
package com.acme.zeebe;
import io.zeebe.containers.ZeebeContainer;
import io.zeebe.containers.util.RemoteDebugger;
import org.junit.jupiter.api.Test;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@Testcontainers
public class MyFeatureTest {
@Container
private final ZeebeContainer zeebeContainer = RemoteDebugger.configure(new ZeebeContainer());
@Test
void shouldTestProperty() {
// test...
}
}
Note that RemoteDebugger#configure(GenericContainer<?>)
returns the same container, so you can use
the return value to chain more configuration around your container.
package com.acme.zeebe;
import io.zeebe.containers.ZeebeContainer;
import io.zeebe.containers.util.RemoteDebugger;
import org.junit.jupiter.api.Test;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@Testcontainers
public class MyFeatureTest {
@Container
private final ZeebeContainer zeebeContainer = RemoteDebugger.configure(new ZeebeContainer())
.withEnv("ZEEBE_BROKER_NETWORK_HOST", "0.0.0.0");
@Test
void shouldTestProperty() {
// test...
}
}
You can also configure the port of the debug server, or even configure the startup to not wait for a
debugger to connect by using RemoteDebugger#configure(GenericContainer<?>, int, boolean)
. See the
Javadoc for more.
Zeebe brokers store all their data under a configurable data directory (default
to /usr/local/zeebe/data
). By default, when you start a container, this data is ephemeral and will
be deleted when the container is removed.
If you want to keep the data around, there's a few ways you can do so. One option is to use a folder on the host machine, and mount it as the data directory. Here's an example:
package com.acme.zeebe;
import static org.assertj.core.api.Assertions.assertThat;
import io.zeebe.containers.ZeebeBrokerContainer;
import io.zeebe.containers.ZeebeHostData;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
final class HostDataExampleTest {
@Test
@Timeout(value = 5, unit = TimeUnit.MINUTES)
void shouldSaveDataOnTheHost(@TempDir final Path tempDir) {
// given
final ZeebeHostData data = new ZeebeHostData(tempDir.toString());
try (final ZeebeBrokerContainer container = new ZeebeBrokerContainer().withZeebeData(data)) {
// when
container.start();
}
// then
assertThat(tempDir).isNotEmptyDirectory();
}
}
This will have Zeebe write its data directly to a directory on your host machine. Note that the
permissions of the written files will be assigned to the user the container runs in. This means if
the broker process in the container runs as root, then the files written will belong to root, and
your user may not be able to run it. To circumvent this, you will need to run your container as your
user as well. You can do so by modifying the create command passing the correct user to the host
config, e.g. container.withCreateContainerCmdModifier(cmd -> cmd.withUser("1000:0"))
.
NOTE: this may or may not work on Windows. I have no access to a Windows machine, so I can't really say how permissions should look like for a Windows machine. It's recommended instead to use volumes, and extract the data from it if you need.
On the other hand, you can also use a Docker volume for this. The volume is reusable across multiple runs, and can also be mounted on different containers. Here's an example:
package com.acme.zeebe;
import static org.assertj.core.api.Assertions.assertThatCode;
import io.zeebe.containers.ZeebeBrokerContainer;
import io.zeebe.containers.ZeebeVolume;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
final class VolumeExampleTest {
@Test
@Timeout(value = 5, unit = TimeUnit.MINUTES)
void shouldUseVolume() {
// given
final ZeebeVolume volume = ZeebeVolume.newVolume();
try (final ZeebeContainer container = new ZeebeContainer().withZeebeData(volume)) {
// when
container.start();
container.stop();
// then
assertThatCode(() -> container.start()).doesNotThrowExceptions();
}
}
}
You can see a more complete example here.
At times, you may want to extract data from a running container, or from a volume. This can happen if you want to run a broker locally based on test data for debugging. It can also be used to generate Zeebe data which can be reused in further test as a starting point to avoid always regenerating that data.
There are two main interfaces for this. If you want to extract the data from a running container, you can directly use ContainerArchive. This represents a reference to a zipped, TAR file on a given container, which can be extracted to a local path.
Here's an example which will extract Zeebe's default data directory to /tmp/zeebe
. This will
result in a copy of the Zeebe data directory at /tmp/zeebe/usr/local/zeebe/data
.
package com.acme.zeebe;
import static org.assertj.core.api.Assertions.assertThat;
import io.zeebe.containers.ZeebeBrokerContainer;
import io.zeebe.containers.archive.ContainerArchive;
import java.io.IOException;
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@Testcontainers
final class ExtractDataLiveExampleTest {
@Container
private final ZeebeBrokerContainer container = new ZeebeBrokerContainer();
@Test
void shouldExtractData() {
// given
final Path destination = Paths.get("/tmp/zeebe");
final ContainerArchive archive = ContainerArchive.builder().withContainer(container).build();
// when
archive.extract(destination);
// then
assertThat(destination).isNotEmptyDirectory();
assertThat(destination.resolve("usr/local/zeebe/data")).isNotEmptyDirectory();
}
}
NOTE: if all you wanted was to download the data, you could simply use
ContainerArchive#transferTo(Path)
. This will download the zipped archive to the given path as is.
You can find more examples for this feature under examples/archive.
As containers are somewhat opaque by nature (though Testcontainers already does a great job of
making this more seamless), it's very useful to add a LogConsumer
to a container. This will
consume your container logs and pipe them out to your consumer. If you're using SLF4J, you can use
the Slf4jLogConsumer
and give it a logger, which makes it much easier to debug if anything goes
wrong. Here's an example of how this would look like:
package com.acme.zeebe;
import io.zeebe.containers.ZeebeContainer;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testcontainers.containers.output.Slf4jLogConsumer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@Testcontainers
public class MyFeatureTest {
private static final Logger LOGGER =
LoggerFactory.getLogger("com.acme.zeebe.MyFeatureTest.zeebeContainer");
@Container
private final ZeebeContainer zeebeContainer =
new ZeebeContainer().withLogConsumer(new Slf4jLogConsumer(LOGGER));
@Test
void shouldTestProperty() {
// test...
}
}
There are three container types in this module: ZeebeContainer
, ZeebeBrokerContainer
,
ZeebeGatewayContainer
. ZeebeContainer
is a special case which can be both a gateway and a
broker. To support a more generic handling of broker/gateway concept, there are two types introduced
which are ZeebeBrokerNode
and ZeebeGatewayNode
. These types, however, are not directly extending
GenericContainer
.
At times, when you have a ZeebeBrokerNode
or ZeebeGatewayNode
, you may want to additionally
configure it in ways that are only available to GenericContainer
instances. You can use
Container#self()
to get the node's GenericContainer
representation.
For example:
package com.acme.zeebe;
import io.zeebe.containers.cluster.ZeebeCluster;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
class ZeebeHugeClusterTest {
private final ZeebeCluster cluster =
ZeebeCluster.builder()
.withBrokersCount(1)
.build();
@AfterEach
void tearDown() {
cluster.stop();
}
@Test
@Timeout(value = 5, unit = TimeUnit.MINUTES)
void shouldStartCluster() {
// given
final ZeebeBrokerNode<?> broker = cluster.getBrokers().get(0);
broker.self().withStartupTimeout(Duration.ofMinutes(2));
cluster.start();
// test more things
}
}
Remember that you have access to the raw docker client via GenericContainer#getDockerClient()
.
This lets you do all kinds of things at runtime, such as fiddling with volumes, networks, etc.,
which is a great way to test failure injection.
NOTE: this is an advanced feature which is experimental and may still cause issues, so use at your own risk.
Testcontainers
itself does not provide a convenience method to stop or pause a container. Using
GenericContainer#stop()
will actually kill and remove the container. To do these things, you can
use the DockerClient
directly, and send standard docker commands (e.g. stop, pause, etc.).
Alternatively, we provide a convenience method as ZeebeNode#shutdownGracefully()
(e.g. ZeebeContainer#shutdownGracefully()
, ZeebeBrokerContainer#shutdownGracefully()
). This will
only stop the container with a grace period, after which it will kill the container if not stopped.
However, the container is not removed, and can be restarted later.
Keep in mind that to restart it you need to use a DockerClient#startContainerCmd(String)
, as just
calling GenericContainer#start()
will not start your container again.
When starting many containers, you can use GenericContainer#withCreateContainerCmdModifier()
on
creation to limit the resources available to them. This can be useful when testing locally on a
development machine and having to start multiple containers.
Contributions are more than welcome! Please make sure to read and adhere to the Code of Conduct. Additionally, in order to have your contributions accepted, you will need to sign the Contributor License Agreement.
In order to build from source, you will need to install maven 3.6+. You can find more about it on the maven homepage.
While the project targets Java 8 for compatibility purposes, for development you will need at least Java 11 - ideally Java 17, as that's what we use for continuous integration. We recommend installing any flavour of OpenJDK such as Eclipse Temurin.
Finally, you will need to install Docker on your local machine.
With all requirements ready, you can now simply clone the repository , and from its root, run the following command:
mvn clean install
This will build the project and run all tests locally.
Should you wish to only build without running the tests, you can run:
mvn clean package
The project uses Spotless to apply consistent formatting and licensing to all project files. By default, the build only performs the required checks. If you wish to auto format/license your code, run:
mvn spotless:apply
Zeebe Test Container uses a Semantic Versioning scheme for its versions, and revapi to enforce backwards compatibility according to its specification.
Additionally, we also use apiguardian to specify
backwards compatibility guarantees on a more granular level. As such, only APIs marked as STABLE
are considered when enforcing backwards compatibility.
If you wish to incubate a new feature, or if you're unsure about a new API type/method, please use
the EXPERIMENTAL
status for it. This will give us flexibility to test out new features and change
them easily if we realize they need to be adapted.
Work on Zeebe Test Container is done entirely through the Github repository. If you want to report a bug or request a new feature feel free to open a new issue on [GitHub][issues].
To work on an issue, follow the following steps:
- Check that a [GitHub issue][issues] exists for the task you want to work on. If one does not, create one.
- Checkout the
master
branch and pull the latest changes.git checkout develop git pull
- Create a new branch with the naming scheme
issueId-description
.git checkout -b 123-my-new-feature
- Follow the Google Java Format and Zeebe Code Style while coding.
- Implement the required changes on your branch, and make sure to build and test your changes locally before opening a pull requests for review.
- If you want to make use of the CI facilities before your feature is ready for review, feel free to open a draft PR.
- If you think you finished the issue please prepare the branch for reviewing. In general the commits should be squashed into meaningful commits with a helpful message. This means cleanup/fix etc commits should be squashed into the related commit.
- Finally, be sure to check on the CI results and fix any reported errors.
Commit messages use Conventional Commits format, with a slight twist. See the Zeebe commit guidelines for more .
You will be asked to sign our Contributor License Agreement when you open a Pull Request. We are not asking you to assign copyright to us, but to give us the right to distribute your code without restriction. We ask this of all contributors in order to assure our users of the origin and continuing existence of the code. You only need to sign the CLA once.
Note that this is a general requirement of any Camunda Community Hub project.