Skip to content

Latest commit

 

History

History
440 lines (328 loc) · 20.2 KB

File metadata and controls

440 lines (328 loc) · 20.2 KB

Coding Agent Guidelines for Kestra Open Source Edition

This document provides essential information for AI coding agents working on the Kestra codebase.

IMPORTANT — READ FIRST

  • Act as a Senior Software Engineer and Software Architect. Approach software development with:
    • Pragmatism: Favor simple solutions over clever ones
    • Skepticism: Question decisions that could cause technical debt or scalability issues
    • Efficiency: Only challenge when it genuinely matters
  • Think before coding: explicitly state assumptions, compare alternatives, and justify choices.
  • Simplicity first (KISS): overengineering and "gas factories" are strictly forbidden.
  • Surgical changes only: touch only what is strictly necessary to achieve the goal.
  • Goal-driven execution: define what success looks like before writing the first line of code.
  • Preserve existing comments: never delete any existing comment unless you are improving its clarity or usefulness.
  • Keep comments short and only where they earn their place: a comment you write should be one sentence, or two at most when the why genuinely needs it (a non-obvious constraint, a workaround, a subtle ordering or concurrency requirement). Do not comment obvious code — no restating what the next line plainly says (// increment the counter), no narrating a self-explanatory getter, loop, or well-named call. If the code is readable, the comment is noise; if it isn't, prefer making the code clearer over explaining it.
  • Write clear, maintainable, and well-documented code
  • Build & test are mandatory

Project

Monorepo built with Java (backend) and Vue (frontend), using Gradle as the build system.

Tech Stack

  • Backend: Java 25, Micronaut Framework, Lombok
  • Frontend: Vue 3, TypeScript, Vite, Element Plus, Pinia
  • Build: Gradle 8.x with multi-project structure (77 submodules)
  • Testing: JUnit 5, Mockito, AssertJ, Vitest, Playwright

Critical Code Patterns

Dependency Injection

DO: Use constructor injection with final fields.

@Singleton
public class MyService {
    private final SomeDependency dependency;

    @Inject
    public MyService(SomeDependency dependency) {
        this.dependency = Objects.requireNonNull(dependency);
    }
}

DON'T: Use field injection (@Inject on fields directly). Always prefer constructor injection.

Class Structure

// 1. Package declaration and imports
// 2. Class-level annotations (@Slf4j, @Singleton, etc.)
// 3. Class declaration with Javadoc
// 4. Static constants (UPPER_SNAKE_CASE)
// 5. Injected fields (@Inject)
// 6. Constructors
// 7. Public methods
// 8. Protected methods
// 9. Private methods
// 10. Inner classes/records

Annotations

  • Micronaut: @Singleton, @Inject, @Controller, @Replaces, @Requires
  • Validation: @Valid, @NotNull, @Nullable
  • Lombok: @Slf4j, @Getter, @NoArgsConstructor, @AllArgsConstructor
  • Use @Builder for complex object creation

Error Handling

DO:

  • Use specific exception types — extend KestraException or KestraRuntimeException
  • Use Optional<T> for potentially absent returned values
  • Return empty collections (e.g., List.of(), Collections.emptyList()) for absent values
  • Use try-with-resources for resource management
  • Log errors before re-throwing: log.error("message", exception)
  • Write exception messages as plain, complete sentences that state the fact and the actionable detail — build them with String.formatted()/String.format(), not string concatenation or em dashes, e.g. "Cannot acquire lock on asset '%s': already locked by '%s' until %s.".formatted(id, owner, until)

DON'T: Use generic Exception. Don't return null for collections. Don't write terse or telegraphic exception messages (e.g. dropping articles/verbs) or string-concatenate message parts.

Java Language Features

  • Use java records for simple data carriers

Naming Conventions

  • Follow Java naming-convention best practices for Classes, Methods, Variables, Constants.
  • Boolean methods: Start with is, has, should, can (e.g., isReadOnly()).

File Organization

  • Use 4-space indentation (configured in .editorconfig)
  • UTF-8 encoding with LF line endings
  • No trailing whitespace

Utility Classes

  • Mark utility classes as final with a private constructor
  • Use static methods only
  • Use existing utility classes (e.g., ListUtils, MapUtils) instead of creating new ones (io.kestra.core.utils.*)

MANDATORY — never hand-roll Pebble delimiter detection. Pebble has two block delimiter pairs — print blocks ({{ ... }}) and execute/statement blocks ({% ... %}) — and code that only checks for {{/}} silently misses {%/%} blocks. Use io.kestra.core.utils.PebbleUtil (containsOpeningBlockDelimiter, startsWithOpeningBlockDelimiter, endsWithClosingBlockDelimiter, openingBlockDelimiters()/closingBlockDelimiters()) instead of writing a new delimiter regex or literal — it derives the delimiter pairs from Pebble's own Syntax.Builder defaults, so it never drifts from what Pebble actually parses.

Enums

  • Use enums for fixed sets of constants, including internal fields not exposed over the API — prefer a typed enum over a raw String/int whenever the value is drawn from a closed set of known cases, even if the set may only ever have a couple of members
  • Use @JsonValue for custom serialization if needed
  • Use UNKNOWN enum value for unknown cases in deserialization
  • Compare Constants From The Left (a.k.a., Yoda conditions)
  • Use a static fromString method for case-insensitive lookups using Enums class.

e.g.:

public enum MyEnum {
    VALUE_ONE,
    VALUE_TWO,
    UNKNOWN;

    @JsonCreator
    public static ResourceType fromString(final String value) {
        return Enums.getForNameIgnoreCase(value, MyEnum.class, UNKNOWN);
    }
}

Documentation

  • Javadoc for all public classes and methods - be concise
  • Use @param, @return, @throws appropriately
  • Use {@inheritDoc} for inherited methods
  • Include usage examples for complex methods

Webserver Constraints

  • Put classes used by only controllers in the webserver module (not core)
  • No business code/rule inside controllers - instead use a Service class
  • All APIs must return a valid JSON object
  • APIs should not return a response being a JSON array which cannot be evolved in a backwards-compatible way
  • Unit tests must assert that a user can only access a given API if authorized to do so, and that access is denied otherwise
  • APIs must be documented with OpenAPI annotations
  • Use DTOs for requests/responses
  • Always validate input parameters with @Valid
  • Use @ExecuteOn(TaskExecutors.IO) for blocking operations
  • Return meaningful error responses in controllers

Worker Constraints

  • Never depend on repositories for code called by the workers - instead use MetaStore/StateStore facades

Executor Constraints

  • Run the H2RunnerTest whenever you update part of the executor

Testing Guidelines

Java Tests

DO:

  • Place tests in same package structure as source code
  • Simple unit test with mocks over complex integration tests when possible
  • Add // Given-When-Then comments for clarity
  • Test method naming: should<ExpectedBehavior>When<ConditionOrAction> (also ...Given<Input>, ...For<Condition>, ...If<Condition>), e.g. shouldThrowExceptionWhenDividingByZero()
  • Use @MicronautTest for tests that require Micronaut beans
  • Use @KestraTest for tests that require running Kestra services (e.g., Executor, Scheduler)
@KestraTest
class ServiceTest {
    @Inject
    private ServiceClass service;

    @Test
    void shouldPerformActionWhenCondition() {
        // Given (setup)

        // When (action)

        // Then (assertions)
        assertThat(result).isNotNull();
    }
}

DON'T: Use Nested classes for test organization. Avoid complex test hierarchies.

Assertions:

  • Use AssertJ: assertThat().isEqualTo(), assertThat().isNotNull(), assertThatThrownBy(), assertThatObject()
  • Prefer descriptive assertion methods
  • Use @MockBean for mocking dependencies

Test Categories:

  • Unit tests: Fast, isolated, no external dependencies
  • Integration tests: Test component interaction, use @Tag("integration")
  • Flaky tests: Use @Tag("flaky") for unreliable tests

Frontend Tests

  • Unit tests with Vitest and @vue/test-utils
  • E2E tests with Playwright
  • Storybook component tests
  • Use JSdom environment for DOM testing
  • Prefer Storybook component tests over Vitest unit tests whenever possible — components render through their real story setup (props, slots, design-system deps) instead of being stubbed out, catching regressions unit mocks miss. Fall back to a Vitest unit test only when the logic under test isn't component-rendering behavior (e.g. a pure helper/composable) or no story exists and adding one isn't practical.

UI Design System

The full UI design-system rules, component catalogue, token reference, and frontend best practices live in ui/AGENTS.md. That file is auto-loaded by AI coding agents whenever work happens under ui/ in OSS or ui-ee/ in Enterprise edition, and should be consulted (and kept up to date) for any frontend change.

@ui/AGENTS.md

Frontend Code Style (Vue 3)

File Organization:

  • Use 2-space indentation for Vue, JSON, YAML, CSS
  • Use 4-space indentation for JavaScript/TypeScript
  • Follow Vue 3 Composition API patterns
  • Organize imports: Vue/framework → third-party → local modules

Naming Conventions:

  • Components: PascalCase files (e.g., MyComponent.vue)
  • Variables/functions: camelCase
  • Constants: UPPER_SNAKE_CASE
  • CSS classes: Follow Element Plus conventions

TypeScript:

  • Use strict TypeScript configuration
  • Prefer type definitions over any
  • Use interfaces for object shapes
  • Use enums for fixed sets of values

Build Commands

Java Backend

# Clean build
./gradlew clean

# Full build (includes tests)
./gradlew build

# Build without tests (faster)
./gradlew build -x test -x integrationTest -x testCodeCoverageReport --refresh-dependencies --no-daemon --parallel

Test Commands

# Run all tests (excludes flaky tests)
./gradlew test

# Run only unit tests (fastest)
./gradlew unitTest

# Run integration tests
./gradlew integrationTest

# Run flaky tests (separate from build)
./gradlew flakyTest

# Run tests for specific module
./gradlew :core:test

# Run single test class
./gradlew :module-name:test --tests "ClassName"

# Run single test method
./gradlew :module-name:test --tests "ClassName.methodName"

# After running tests: generate a markdown summary of failures only
npx --yes @kestra-io/kestra-devtools generateTestReportSummary --only-errors $(pwd)

Frontend (UI)

cd ui

# Install dependencies
npm install

# Development server
npm run dev

# Type checking
npm run check:types

# Build for production
npm run build

# Run tests
npm run test:all        # All tests with coverage
npm run test:unit       # Unit tests only
npm run test:storybook  # Storybook tests
npm run test:e2e        # End-to-end tests

# Linting
npm run lint            # Fix linting issues
npm run test:lint       # Check linting only

# Storybook
npm run storybook       # Development
npm run build-storybook # Build

Development Workflow

Running Locally

  1. Start/stop backends:
# Start databases with Docker Compose
docker compose -f docker-compose-ci.yml up

# Stop databases with Docker Compose
docker compose -f docker-compose-ci.yml down
  1. Access application: http://localhost:8080

Worktree setup

When working in an EE worktree (detected by: the working directory is under a worktrees/ directory):

dev-tools/setup-worktree.sh ../worktrees/foo

This copies the gitignored cli/src/main/resources/application-*.yml files from the main checkout into the worktree. Without this step Kestra cannot boot in the worktree. The script is idempotent — safe to re-run.

Security Considerations

  • Use tenant isolation for multi-tenant features
  • Implement proper authorization with @HasAnyPermission
  • Handle secrets securely (never log sensitive data)

Performance Best Practices

  • Implement pagination for large datasets
  • Use streaming for large file operations
  • Cache frequently accessed data appropriately
  • Initialize collections with the expected size to avoid resizing overhead

Troubleshooting

Common Issues:

  • Build failures: Run ./gradlew clean and retry
  • Test failures: Check for service dependencies (Docker containers)
  • Frontend issues: Ensure Node.js version matches package.json requirements

Debugging:

  • Use IDE debugging with remote JVM debugging
  • Use Micronaut's built-in health endpoints
  • Enable debug logging: --logging.level.io.kestra=DEBUG
  • Use JUnit and Vitest reports for test failures

Module Structure

Core Modules:

  • cli - Command Line Interface
  • core - Core functionality
  • webserver - Web server
  • ui - Vue 3 frontend application
  • executor - The component responsible for managing execution state
  • scheduler - The component responsible for scheduling polling and schedule triggers
  • worker - The component that executes tasks and manages worker instances
  • worker-controller - The component that manages worker instances and job distribution
  • indexer - The component responsible for indexing executions
  • plateform - provides the Platform Bill of Materials (BOM) for dependency management

Queuing Layer:

  • queue - Core API for queue implementations
  • queue-jdbc - JDBC-based queue implementation

Data Layer:

  • jdbc-* - Database implementations (H2, Postgres, MySQL)

Testing Modules:

  • tests - Common test utilities and base classes
  • jmh-benchmark - JMH benchmarks for performance testing

Key Patterns:

  • Repository pattern for data access
  • Service layer for business logic
  • Controller layer for HTTP endpoints
  • Builder pattern for object construction (often with Lombok @Builder)

Pull request guidelines

  • Always add tests, keep your branch rebased instead of merged, and adhere to the commit message recommendations from https://www.conventionalcommits.org/en/v1.0.0.
  • Use types: chore, feat, fix, refactor, test, docs, build
  • Use scopes: apps, assets, core, dashboards, deps, design-system, executions, flows, iam, namespaces, plugins, secrets, storage, scheduler, system, tasks, tenants, tests, topology, triggers, variables, version, worker

Issue guidelines

  • Classify an issue with its GitHub issue type, not a kind/* label. The kind/bug label is retired — do not add it. Set the type instead: gh issue create --title … followed by gh issue edit <number> --type Bug, or gh issue edit <number> --type Task|Feature|Epic. Available types are Task, Bug, Feature and Epic (list them with gh api /orgs/kestra-io/issue-types).
  • Do add the area/* labelsarea/frontend, area/backend, area/devops, area/docs, area/plugin, area/qa, area/analytics — since those drive routing and are still in use.
  • Leave triage labels such as kind/cooldown to kestrabot; it applies them automatically on new issues.

This document should be updated as the codebase evolves. When in doubt, follow existing patterns in the codebase and maintain consistency with established conventions.

UI Translations

MANDATORY — never hardcode user-facing strings. Every label, button, tooltip, placeholder, dialog/section title, table-column header, and toast/confirm message rendered to the user MUST go through vue-i18n: t("key") (or :label/:tooltip bindings) in components, and <i18n-t keypath="..."> with named slots when the string embeds markup or a component (e.g. a <code> fragment). Never write a literal user-facing string in a template, a :tooltip/:label attribute, or a toast.* call. Reuse existing generic keys (cancel, delete, edit, save, add, id, description, namespace, revision, …) instead of duplicating them; put feature-specific strings under one namespaced object (e.g. "reusableInputs": { … }). After adding keys to en.json, propagate them to every language (translation generation script) so the missing-keys check stays clean — a key present only in en.json fails the check.

Translation files live in ui/src/translations/. There is one JSON file per language code (e.g. de.json, fr.json) plus the source en.json.

Checking for missing translations

Run the check script from the ui/ directory:

cd ui && npm run translations:check

A clean run reports No missing keys., No extra keys. and No stale keys. for every language. Anything listed must be fixed before merging — the same check runs as a PR gate.

Enterprise Edition: EE-only keys live in ui-ee/src/translations/ee_translations/en.json and are checked separately — run npm run translations:check in ui-ee as well (see kestra-ee/AGENTS.md → "Frontend i18n").

Editing English strings

Changing an existing English value is a translation change. Every key carries a fingerprint of the English text its translations were generated from, so editing en.json — even just the capitalisation — marks that key stale in all twelve languages and fails translations:check until it is regenerated. Run npm run translations:generate and commit the result alongside your change.

This is deliberate: before it existed, edited values were never propagated, and a rename of "SuperAdmin" to "Superadmin" sat un-translated in eleven locales for a year (#10656).

Adding or regenerating translations

Prefer npm run translations:generate (needs GEMINI_API_KEY); it fills missing keys and re-translates stale ones on its own, with no flag to remember. Pass true to force a full re-translation of everything.

If you must write a translation by hand:

  1. Identify gaps by running npm run translations:check.
  2. Follow these translation rules (mirroring ui/scripts/translations/generateTranslations.ts, the generator shared by OSS and EE):
    • Reserved English terms — never translate: kv store, namespace, tenant, flow, subflow, task, log, blueprint, id, trigger, label, key, value, input, output, port, worker, backfill, healthcheck, min, max.
    • ALL-CAPS status labels stay in English: WARNING, FAILED, SUCCESS, PAUSED, RUNNING, etc.
    • Preserve {placeholder} variables exactly — vue-i18n uses a single pair of braces. Do not translate the name inside the braces, do not rename it, and never write {{placeholder}}: double braces are a compile error (Not allowed nest placeholder) and make t() throw at render time. Each translation must carry exactly the same placeholders as the English source — no invented ones, none dropped.
    • Use natural UI terminology — avoid false friends or overly literal translations (e.g. German: Execution → Ausführung, Theme → Modus, State → Zustand).
  3. Insert the translated keys into the correct position in the target language JSON, mirroring the key order of en.json.
  4. Re-run npm run translations:check to confirm everything is clean before committing.

The tooling itself lives in ui/scripts/translations/ and is shared with EE, which keeps only thin entry points. Rules live in .mjs so the dependency-free PR gate can apply them; file IO and orchestration stay in .ts.

Conflicts in fingerprints.json

Two branches that both touch en.json will both regenerate ui/scripts/translations/fingerprints.json, so it conflicts often. Never hand-merge the hashes and never pick a side — a hash says "this English text is what the twelve translations were generated from", so choosing the wrong one silently marks a drifted key as current and the drift becomes invisible again.

Resolve it the same way as a kestra-sdk conflict — regenerate:

git checkout --ours ui/src/translations/*.json ui/scripts/translations/fingerprints*.json
cd ui && npm run translations:generate   # fills whatever the other branch added
npm run translations:check               # must report no missing / extra / stale keys

en.json itself normally merges cleanly, since branches usually add different keys; it is the generated files that collide.