In this document you will find a lot of information on how you can contribute to DukeOps.
- How to contribute
- Quickstart for Local Development
- Communication
- Architecture
- Internationalization
- Database
- Build
- Running and debugging
- Running using Docker
To get an overview of ongoing and upcoming work, you can check our project board, which organizes issues in a Kanban-style workflow:
| Status | Description |
|---|---|
| Backlog | Ideas and requests that are not yet analyzed. |
| In Analysis | Issues currently under discussion or refinement. |
| Ready | Issues that are fully specified and ready to be implemented. |
| In Progress | Issues actively being worked on. |
| Done | Recently completed issues; older ones are archived over time. |
Tip
If you’re looking for something to work on, focus on issues in the Ready column. These contain all necessary information to start contributing right away.
To find possible tasks for your first contribution to DukeOps, we tagged some of the hopefully easier to solve issues as good first issue.
If you prefer to meet people in real life to contribute to DukeOps together, we recommend to visit a Hackergarten event. DukeOps is often selected as a contribution target in Lucerne, Zurich, and at the CyberLand Open Source Camp.
Please join our developer community using our Matrix chat to get support and help for contributing to DukeOps.
Please follow the Conventional Commits specification for all commit messages and pull request titles. This structured format helps automate changelogs, release processes, and code reviews.
For DukeOps, we love to add the issue number to the end of the commit message. This helps to track the changes in the code and the issue tracker. When the commit closes an issue, the closes, fixes, or resolves keyword can be used. This will automatically close the issue when the commit is merged into the main branch.
A commit message consists of the following parts:
<type>[optional scope]: <short description> [optional keyword] #<issue number>
[optional body]
[optional footer]
feat: add support for passkey authentication closes #123fix(event): correct date formatting in export fixes #456chore: add missing license headers to source files resolves #789
| Type | Purpose |
|---|---|
feat |
Introduce a new feature |
fix |
Fix a bug |
docs |
Documentation-only changes |
style |
Code style changes (formatting, whitespace, missing semicolons, etc.) |
refactor |
Code refactoring without functional change |
perf |
Improve performance without changing features or behavior |
test |
Add or update tests |
build |
Changes to the build system, packaging or dependencies (e.g. Maven) |
ci |
Changes to CI/CD configuration (e.g. GitHub Actions, Woodpecker) |
chore |
Maintenance tasks (configs, license, release meta, etc.) |
revert |
Revert a previous commit |
deps |
Add, update, or remove dependencies |
security |
Address security issues or vulnerabilities |
Tip
For more information, see conventionalcommits.org
It is important to sign-off every commit. That is a de facto standard way to ensure that you have the right to submit your content and that you agree to the DCO (Developer Certificate of Origin).
You can find more information about why this is important and how to do it easily in a very good blog post by Josef Andersson.
AI generated source code is based on real existing source code, which is copied in whole or in part into the generated code. The license of the original source code with which the AI was trained is not taken into account. It is not clear which license conditions apply and how these can be complied with. For legal reasons, we therefore do not allow AI-generated source code at all.
DukeOps uses AssertJ for writing assertions in unit tests. To ensure consistency, readability, and rich failure messages, only AssertJ should be used for all assertions. The use of JUnit's built-in assert* methods and Hamcrest matchers is explicitly disallowed and enforced through architecture tests. This includes, but is not limited to:
org.junit.Assertorg.junit.jupiter.api.Assertionsorg.hamcrest.MatcherAssert
Instead, use AssertJ's fluent assertion style, for example:
assertThat(actual).isEqualTo(expected);
assertThat(collection).containsExactly("one", "two");
assertThatThrownBy(() -> service.doSomethingBad())
.isInstanceOf(IllegalArgumentException.class);All integration tests must extend the abstract KaribuTest or BrowserTest base class. This ensures consistent configuration of the Spring Boot context, dependency injection, and database setup across all integration tests. Extend from the KaribuTest class for tests that do not require a browser using Karibu Testing, or from the BrowserTest class for tests that need to interact with the UI in a browser environment using Playwright.
Do not annotate integration tests manually with @SpringBootTest, @Transactional, or other Spring annotations. This is already handled by the base class. The base classes also provide shared utilities and guarantees a controlled test environment. To distinguish integration tests from unit tests, all Karibu test classes should be named with the KT suffix and all Playwright browser tests should be named with the BT suffix.
Example:
class UserServiceIT extends KaribuTest {
@Autowired
private UserService userService;
@Test
void shouldReturnUserByEmail() {
final var user = userService.findByEmail("demo@example.eu");
assertThat(user).isPresent();
}
}Warning
Integration tests that do not extend KaribuTest or BrowserTest may not run correctly or may produce unforeseen errors.
Please make sure that each pull request is focused on a single issue or change. Avoid bundling unrelated modifications together in the same PR, as this makes the review process harder and slows down merging. If you want to contribute multiple independent changes, submit them as separate pull requests so they can be reviewed and merged individually.
Note
GitHub only allows one pull request per branch. To open multiple pull requests, you need to create a separate branch for each change.
Please make sure your contribution meets all the following requirements:
- Checkstyle and PMD report no violations.
- The Modernizer Maven Plugin passes without issues.
- All automated tests are green.
- Test coverage is 100% (we practice Test-Driven Development).
- License headers are present in all source files (missing headers can be added using:
./mvnw license:format). - The branch is up to date with the main branch (please rebase if necessary before opening a Pull Request).
- The Pull Request title follows the Conventional Commit style and references the related issue number (e.g.
feat: add event export closes #123).
Tip
All checks above are executed by:
./mvnw verifyRun this locally and ensure it completes successfully before opening a Pull Request.
The CI pipeline will run the same checks and block PRs that fail.
Commit & signature policy
- Every commit must include a
Signed-off-byline (Developer Certificate of Origin). - Every commit must be cryptographically signed with GPG, SSH, or S/MIME.
- GitHub verifies these signatures during Pull Requests; unverified commits may be rejected.
- For detailed setup and usage instructions, see Signing commits – GitHub Docs.
This guide helps you get DukeOps up and running on your local machine for development purposes.
Make sure the following tools are installed:
- Git
- Java LTS (currently version 25) - check with
java -version - Podman or Docker
If you are using Podman on Linux, you need to enable the podman.socket systemd service.
This allows non-root users to run Podman commands without sudo.
You can enable it with the following command:
systemctl --user enable --now podman.socketThis command starts the Podman socket for your user and ensures it automatically starts on login. Once enabled, you can use Podman just like Docker, without elevated privileges.
To make sure that Testcontainers can connect to your Podman socket, set the following environment variables in your shell:
export DOCKER_HOST=unix://$(podman machine inspect --format '{{.ConnectionInfo.PodmanSocket.Path}}')
export TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE=$(podman machine inspect --format '{{.ConnectionInfo.PodmanSocket.Path}}')
export TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED=trueIf you use Podman Desktop for macOS, activate the full Docker compatibility layer instead:
- Start the "Podman Desktop" app.
- Navigate to "Settings", "Preferences", "Docker Compatibility" and enable it.
- Now a new entry "Docker Compatibility" appears directly under "Settings".
- Click on the new entry "Docker Compatibility".
- Make sure, "Third-Party Docker Tool Compatibility" is activated.
-
Fork the DukeOps repository on GitHub.
-
Clone your fork locally:
git clone https://github.com/YOUR_USERNAME/dukeops.git cd dukeops -
Run all tests to verify that everything works:
./mvnw verify
DukeOps needs a database and a mail server to run. For local development, you can start both using the provided docker-compose.yml file in the dev-tools directory:
Docker
cd ./dev-tools
docker compose up -d && docker compose logs -f || docker compose downPodman
cd ./dev-tools
podman compose up -d && podman compose logs -f || podman compose downThe commands shown above will start the services in detached mode and then show the logs. If you stop the log view (e.g. using CTRL+C), all services will be stopped as well.
The following services will be available:
- MariaDB on Port 3306 with persistent data in
mariadb-data.
The database user will bedukeopswith the passworddukeops. The database name is alsodukeops. - phpMyAdmin (database web UI) on port 4000
- Mailpit (SMTP server) on port 1025, web UI on port 8025, persistent data in
mailpit-data
Important
This setup is for development only and not suitable for production use.
MariaDB Login Information
| Field | Value |
|---|---|
| System | MySQL / MariaDB |
| Server | mariadb |
| User | dukeops |
| Password | dukeops |
| Database | dukeops |
After starting the script, logs will be shown. To stop the services, press Ctrl+C. This stops all containers as well.
DukeOps is configured using environment variables. At minimum, the following must be set:
DUKEOPS_INSTANCE_ADMIN=admin@example.com
DUKEOPS_DB_URL=jdbc:mariadb://localhost:3306/dukeops?serverTimezone=Europe/Zurich&allowMultiQueries=true
DUKEOPS_DB_USER=dukeops
DUKEOPS_DB_PASS=dukeops
DUKEOPS_MAIL_HOST=localhost
DUKEOPS_MAIL_PORT=1025If you're in a different timezone, adjust the serverTimezone parameter accordingly.
You can define these variables via your IDE, or directly in your shell.
You can run DukeOps either via your IDE or the command line. On the command line, use the Maven Wrapper:
./mvnwThis runs the eu.ijug.dukeops.Application main class.
Once the application is running, open your browser and navigate to:
Make sure JavaScript is enabled.
- In your IDE: use the stop button
- In the terminal: press
Ctrl+C
There is a channel at Matrix for quick and easy communication. This is publicly accessible for everyone. For developers as well as users. The communication in this chat is to be regarded as short-lived and has no documentary character.
You can find our Matrix channel here: @dukeops:ijug.eu
We use the corresponding GitHub function for discussions. The discussions held here are long-lived and divided into categories for the sake of clarity. One important category, for example, is that for questions and answers.
Discussions on GitHub: https://github.com/ijug-ev/dukeops/discussions
Questions and Answers: https://github.com/ijug-ev/dukeops/discussions/categories/q-a
The server of DukeOps is written using the Java programming language. The main framework is Spring. For the user interface, we use Vaadin Flow. To access the database, we rely on jOOQ. To coordinate the build process, Maven is used.
Vaadin web applications are full-stack and include both client-side and server-side code in the same project.
| Directory | Description |
|---|---|
src/main/frontend/themes/dukeops/ |
Base directory for default DukeOps theme |
components/ |
Directory with CSS files for UI components |
fonts/ |
Directory containing font files |
views/ |
Directory with CSS files for UI views |
styles.css |
Base CSS file with includes only |
theme.json |
Theme configuration for Vaadin |
src/main/java/eu/ijug/dukeops/ |
Server-side source directory |
domain/ |
Code for all business domains (BCE architecture) |
jooq/ |
jOOQ DB layer code (converters, generators, etc.) |
util/ |
Universal utility classes (not bound to a business domain) |
vaadin/components/ |
Universal UI components (not bound to a business domain) |
Application.java |
Server entrypoint with main class |
DukeOps follows the Boundary Control Entity (BCE) architecture pattern. This pattern divides the application into three main layers:
- Boundary Layer: This layer contains all user interface components, including Vaadin views and UI components. It handles user interactions and displays data to the user.
- Control Layer: This layer contains the service classes that implement the business logic of the application. It acts as an intermediary between the boundary and entity layers, coordinating data flow and enforcing business rules.
- Entity Layer: This layer contains the data model, like DTOs.
Inside the domain package, each business domain has its own subpackage containing its boundary, control, and entity classes. This modular structure promotes separation of concerns, making the codebase easier to maintain and extend. Each layer has a well-defined responsibility, ensuring that changes in one layer have minimal impact on others. Example:
| Directory | Description |
|---|---|
src/main/java/eu/ijug/dukeops/domain/ |
Code for all business domains (BCE architecture) |
community/ |
Subpackage for the "Community" domain |
event/ |
Subpackage for the "Event" domain |
core/ |
Subpackage for core features not specific to a domain |
core/config |
Subpackage for the "Configuration" core feature |
Inside each domain subpackage, you will find the following structure:
| Directory | Description |
|---|---|
src/main/java/eu/ijug/dukeops/domain/community/ |
Subpackage for the "Community" domain |
boundary/ |
Everything regarding the Boundary Layer |
control/ |
Everything regarding the Control Layer |
entity |
Everything regarding the Entity Layer |
Tip
More information about the BCE architecture can be found on the BDE Design website.
- Read the documentation at vaadin.com/docs.
- Follow the tutorials at vaadin.com/tutorials.
- Watch training videos and get certified at vaadin.com/learn/training.
- Create new projects at start.vaadin.com.
- Search UI components and their usage examples at vaadin.com/components.
- View use case applications that demonstrate Vaadin capabilities at vaadin.com/examples-and-demos.
- Discover Vaadin's set of CSS utility classes that enable building any UI without custom CSS in the docs.
- Find a collection of solutions to common use cases in Vaadin Cookbook.
- Find Add-ons at vaadin.com/directory.
- Ask questions on Stack Overflow or join our Discord channel.
- Report issues, create pull requests in GitHub.
Important
DukeOps uses the ICU message format for all translation files. This allows for advanced formatting options, including pluralization and named parameters. ICU stands for "International Components for Unicode".
All user-facing text is externalized into properties files located in the src/main/resources/vaadin-i18n/ directory. The english language is the default and is stored in translations.properties. Other languages follow the naming convention translations_xx.properties, where xx is the ISO 639-1 language code (e.g., de for German, fr for French).
As the key for the translations, we use a dot-separated hierarchical structure that reflects the package structure without the leading eu.ijug.dukeops and include the class name. This makes it easier to locate and manage translations.
Please try keep the translation files sorted alphabetically by key for better maintainability.
Important
The message needs to be added to the properties files on a single line without line breaks. Line breaks in the examples below are for better readability only!
Example (English):
community.boundary.CommunityDetailView.memberCount = {0, plural,
=0 {no members}
one {one member}
other {# members}
}
Example (German):
community.boundary.CommunityDetailView.memberCount = {0, plural,
=0 {keine Mitglieder}
one {ein Mitglied}
other {# Mitglieder}
}
Example (Russian):
community.boundary.CommunityDetailView.memberCount = {0, plural,
=0 {нет участников}
one {один участник}
few {# участника}
many {# участников}
other {# участников}
}
The ICU message format also supports named parameters for better readability.
Example (English):
community.boundary.CommunityDetailView.memberCount = {count, plural,
=0 {no members}
one {one member}
other {{count} members}
}
- Database independence: All schema definitions must be compatible with common SQL databases. Avoid vendor-specific features unless absolutely necessary.
- Schema migrations: Use Flyway for schema versioning. Each migration must have a clearly written description and follow our naming convention.
- Identifier generation: Always use application-generated IDs (e.g., UUIDs) instead of
AUTO_INCREMENT,SERIAL, or database sequences to ensure cross-database compatibility. - Primary key constraints: Primary key constraints should not have explicit names; let the database assign a default name to avoid conflicts across different environments.
- Timestamps: Use
TIMESTAMPinstead ofDATETIMEto ensure consistent handling of time zones and better cross-database support. - Enum usage: Use Java
enumtypes mapped via jOOQ converters toVARCHARcolumns in the database. Do not reference enum values from lookup tables. - DTO mapping: All database access must go through the service layer. Records returned by jOOQ must be converted to DTOs in a dedicated mapping layer.
- Referential Integrity: Use foreign key constraints to ensure referential integrity where appropriate.
- Optional Fields: If a field is optional, make sure the database column is nullable and check for
nullexplicitly in the application logic.
List of prefixes for index names:
| Prefix | Used For | Example |
|---|---|---|
uk_ |
Unique Key | uk_group_profile |
fk_ |
Foreign Key | fk_event_group |
idx_ |
Normal Index | idx_event_begin |
Primary keys don't have a name and therefore no prefix!
To start a local MariaDB instance using Docker or Podman, you can use the provided docker-compose.yml file in the dev-tools directory. Please see section Start Required Services for detailed instructions.
DukeOps uses Maven to build the project. Please use standard Maven commands to build what you need:
| Command | What it does |
|---|---|
./mvnw |
compile and run the app |
./mvnw clean |
cleanup generated files and build artefacts |
./mvnw compile |
compile the code without running the tests |
./mvnw test |
compile and run all tests |
./mvnw package |
compile, test, and create a JAR file to run it with Java directly |
./mvnw verify |
compile, test, package, and run analysis tools |
There is no need to run the install or deploy tasks. They will just run longer, produce unnecessary output, burn energy, and occupy your disk space. Don't just blindly run mvn clean install...
There are two ways to run the application: From the command line or directly from your IDE.
To run from the command line, use ./mvnw and open http://localhost:8080 in your browser.
- On the right side of the window, select "Maven" --> "Plugins" -->
spring-boot-->spring-boot:run - Optionally, you can disable tests by clicking on a
Skip Tests modeblue button.
After the server has started, you can view it at http://localhost:8080/ in your browser. You can now also attach breakpoints in code for debugging purposes, by clicking next to a line number in any source file.
- Locate the
Application.javaclass in the Project view. It is in thesrcfolder, under the main package's root. - Right-click on the Application class
- Select "Debug 'Application.main()'" from the list
After the server has started, you can view it at http://localhost:8080/ in your browser. You can now also attach breakpoints in code for debugging purposes, by clicking next to a line number in any source file.
- Right click on a project folder and select
Run As-->Maven build.... After that a configuration window is opened. - In the window set the value of the Goals field to
spring-boot:run - You can optionally select the
Skip testscheckbox - All the other settings can be left to default
Once configurations are set clicking Run will start the application.
Do not worry if the debugger breaks at a SilentExitException. This is a Spring Boot feature and happens on every startup.
After the server has started, you can view it at http://localhost:8080/ in your browser. You can now also attach breakpoints in code for debugging purposes, by clicking next to a line number in any source file.
- Locate the
Application.javaclass in the Package Explorer. It is insrc/main/java, under the main package. - Right-click on the file and select
Debug As-->Java Application.
Do not worry if the debugger breaks at a SilentExitException. This is a Spring Boot feature and happens on every startup.
After the server has started, you can view it at http://localhost:8080/ in your browser. You can now also attach breakpoints in code for debugging purposes, by clicking next to a line number in any source file.
Tip
There is an official Docker image you can use to run DukeOps.
To build the dockerized version of DukeOps yourself, run:
docker build . -t dukeops:latest
Once the Docker image is correctly built, you can test it locally using:
docker run -p 8080:8080 dukeops:latest