Repository: kestra-io/kestra (java)

Issue:
Migrate to Java 21
### Issue description

Migrate to Java 21.
The decision has been taken to upgrade to Java 21 early in the 0.1 release process.

See https://github.com/kestra-io/kestra-ee/issues/883

Pull request:
chore: upgrade to Java 21
Fixes #3234

Patch:
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 207ff75d3..8d069b723 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -53,7 +53,7 @@ jobs:
       uses: actions/setup-java@v4
       with:
         distribution: 'temurin'
-        java-version: 17
+        java-version: 21
 
     # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
     # If this step fails, then you should remove it and run the build manually (see below)
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 235983c82..2f60cc865 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -93,7 +93,7 @@ jobs:
         uses: actions/setup-java@v4
         with:
           distribution: 'temurin'
-          java-version: 17
+          java-version: 21
 
       - name: Validate Gradle wrapper
         uses: gradle/wrapper-validation-action@v2
@@ -364,7 +364,7 @@ jobs:
         uses: actions/setup-java@v4
         with:
           distribution: 'temurin'
-          java-version: 17
+          java-version: 21
 
       # Publish
       - name: Publish package to Sonatype
diff --git a/build.gradle b/build.gradle
index 152a6f73a..6f8607b6a 100644
--- a/build.gradle
+++ b/build.gradle
@@ -37,8 +37,8 @@ idea {
  * Main
  **********************************************************************************************************************/
 mainClassName = "io.kestra.cli.App"
-sourceCompatibility = 17
-targetCompatibility = 17
+sourceCompatibility = 21
+targetCompatibility = 21
 
 dependencies {
     implementation project(":cli")
diff --git a/core/src/main/java/io/kestra/core/endpoints/SchedulerEndpoint.java b/core/src/main/java/io/kestra/core/endpoints/SchedulerEndpoint.java
index 2e09d52c6..c5544d9b3 100644
--- a/core/src/main/java/io/kestra/core/endpoints/SchedulerEndpoint.java
+++ b/core/src/main/java/io/kestra/core/endpoints/SchedulerEndpoint.java
@@ -26,7 +26,7 @@ public class SchedulerEndpoint {
     public SchedulerEndpointResult running() {
         Map<String, AbstractScheduler.FlowWithPollingTriggerNextDate> schedulableNextDate = scheduler.getSchedulableNextDate();
 
-        List<SchedulerEndpointSchedule> result = scheduler.getSchedulable()
+        List<SchedulerEndpointSchedule> result = scheduler.schedulerTriggers()
             .stream()
             .map(flowWithTrigger -> {
                 String uid = Trigger.uid(flowWithTrigger.getFlow(), flowWithTrigger.getAbstractTrigger());
diff --git a/core/src/main/java/io/kestra/core/models/triggers/Trigger.java b/core/src/main/java/io/kestra/core/models/triggers/Trigger.java
index 190e69104..30cc39cc1 100644
--- a/core/src/main/java/io/kestra/core/models/triggers/Trigger.java
+++ b/core/src/main/java/io/kestra/core/models/triggers/Trigger.java
@@ -78,6 +78,10 @@ public class Trigger extends TriggerContext {
         );
     }
 
+    public String flowUid() {
+        return Flow.uid(this.getTenantId(), this.getNamespace(), this.getFlowId(), Optional.of(this.getFlowRevision()));
+    }
+
     /**
      * Create a new Trigger with no execution information and no evaluation lock.
      */
diff --git a/core/src/main/java/io/kestra/core/schedulers/AbstractScheduler.java b/core/src/main/java/io/kestra/core/schedulers/AbstractScheduler.java
index 2e3dec03a..f3d4a745d 100644
--- a/core/src/main/java/io/kestra/core/schedulers/AbstractScheduler.java
+++ b/core/src/main/java/io/kestra/core/schedulers/AbstractScheduler.java
@@ -50,6 +50,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.BiConsumer;
 import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 @Slf4j
@@ -203,7 +205,8 @@ public abstract class AbstractScheduler implements Scheduler, Service {
     // and if some flows were created outside the box, for example from the CLI,
     // then we may have some triggers that are not created yet.
     private void initializedTriggers(List<Flow> flows) {
-        record FlowAndTrigger(Flow flow, AbstractTrigger trigger) {}
+        record FlowAndTrigger(Flow flow, AbstractTrigger trigger) {
+        }
         List<Trigger> triggers = triggerState.findAllForAllTenants();
 
         flows
@@ -217,7 +220,7 @@ public abstract class AbstractScheduler implements Scheduler, Service {
                     ConditionContext conditionContext = conditionService.conditionContext(runContext, flowAndTrigger.flow(), null);
                     try {
                         // new polling triggers will be evaluated immediately except schedule that will be evaluated at the next cron schedule
-                        ZonedDateTime nextExecutionDate = flowAndTrigger.trigger() instanceof Schedule  schedule ? schedule.nextEvaluationDate(conditionContext, Optional.empty()): now();
+                        ZonedDateTime nextExecutionDate = flowAndTrigger.trigger() instanceof Schedule schedule ? schedule.nextEvaluationDate(conditionContext, Optional.empty()) : now();
                         Trigger newTrigger = Trigger.builder()
                             .tenantId(flowAndTrigger.flow().getTenantId())
                             .namespace(flowAndTrigger.flow().getNamespace())
@@ -243,7 +246,7 @@ public abstract class AbstractScheduler implements Scheduler, Service {
                             Trigger updated = trigger.get().toBuilder().nextExecutionDate(previousDate).build();
                             this.triggerState.update(updated);
                         }
-                    } else if (recoverMissedSchedules == Schedule.RecoverMissedSchedules.NONE ) {
+                    } else if (recoverMissedSchedules == Schedule.RecoverMissedSchedules.NONE) {
                         Trigger updated = trigger.get().toBuilder().nextExecutionDate(schedule.nextEvaluationDate()).build();
                         this.triggerState.update(updated);
                     }
@@ -308,6 +311,24 @@ public abstract class AbstractScheduler implements Scheduler, Service {
 
     abstract public void handleNext(List<Flow> flows, ZonedDateTime now, BiConsumer<List<Trigger>, ScheduleContextInterface> consumer);
 
+    public List<FlowWithTriggers> schedulerTriggers() {
+        Map<String, Flow> flows = this.flowListeners.flows()
+            .stream()
+            .collect(Collectors.toMap(Flow::uid, Function.identity()));
+
+        return this.triggerState.findAllForAllTenants().stream()
+            .filter(trigger -> flows.containsKey(trigger.flowUid()))
+            .map(trigger ->
+                new FlowWithTriggers(
+                    flows.get(trigger.flowUid()),
+                    flows.get(trigger.flowUid()).getTriggers().stream().filter(t -> t.getId().equals(trigger.getTriggerId())).findFirst().orElse(null),
+                    trigger,
+                    null,
+                    null
+                )
+            ).toList();
+    }
+
     private void handle() {
         if (!isReady()) {
             log.warn("Scheduler is not ready, waiting");
@@ -399,8 +420,7 @@ public abstract class AbstractScheduler implements Scheduler, Service {
                                         e
                                     );
                                 }
-                            }
-                            else if (f.getPollingTrigger() instanceof Schedule schedule) {
+                            } else if (f.getPollingTrigger() instanceof Schedule schedule) {
                                 // This is the Schedule, all other triggers should have an interval.
                                 // So we evaluate it now as there is no need to send it to the worker.
                                 // Schedule didn't use the triggerState to allow backfill.
@@ -690,7 +710,9 @@ public abstract class AbstractScheduler implements Scheduler, Service {
         this.workerTaskQueue.emit(workerGroupService.resolveGroupFromJob(workerTrigger), workerTrigger);
     }
 
-    /** {@inheritDoc} **/
+    /**
+     * {@inheritDoc}
+     **/
     @Override
     @PreDestroy
     public void close() {
@@ -788,19 +810,25 @@ public abstract class AbstractScheduler implements Scheduler, Service {
         eventPublisher.publishEvent(new ServiceStateChangeEvent(this));
     }
 
-    /** {@inheritDoc} **/
+    /**
+     * {@inheritDoc}
+     **/
     @Override
     public String getId() {
         return id;
     }
 
-    /** {@inheritDoc} **/
+    /**
+     * {@inheritDoc}
+     **/
     @Override
     public ServiceType getType() {
         return ServiceType.SCHEDULER;
     }
 
-    /** {@inheritDoc} **/
+    /**
+     * {@inheritDoc}
+     **/
     @Override
     public ServiceState getState() {
         return state.get();
diff --git a/core/src/main/java/io/kestra/core/services/FlowService.java b/core/src/main/java/io/kestra/core/services/FlowService.java
index 30cf1c355..09f49b41e 100644
--- a/core/src/main/java/io/kestra/core/services/FlowService.java
+++ b/core/src/main/java/io/kestra/core/services/FlowService.java
@@ -36,6 +36,8 @@ import java.util.stream.StreamSupport;
 @Singleton
 @Slf4j
 public class FlowService {
+    private final IllegalStateException NO_REPOSITORY_EXCEPTION = new IllegalStateException("No flow repository found. Make sure the `kestra.repository.type` property is set.");
+
     @Inject
     RunContextFactory runContextFactory;
 
@@ -43,7 +45,7 @@ public class FlowService {
     ConditionService conditionService;
 
     @Inject
-    FlowRepositoryInterface flowRepository;
+    Optional<FlowRepositoryInterface> flowRepository;
 
     @Inject
     YamlFlowParser yamlFlowParser;
@@ -62,6 +64,11 @@ public class FlowService {
             .tenantId(tenantId)
             .build();
 
+        if (flowRepository.isEmpty()) {
+            throw NO_REPOSITORY_EXCEPTION;
+        }
+
+        FlowRepositoryInterface flowRepository = this.flowRepository.get();
         return flowRepository
             .findById(withTenant.getTenantId(), withTenant.getNamespace(), withTenant.getId())
             .map(previous -> flowRepository.update(withTenant, previous, source, taskDefaultService.injectDefaults(withTenant)))
@@ -69,7 +76,11 @@ public class FlowService {
     }
 
     public List<FlowWithSource> findByNamespaceWithSource(String tenantId, String namespace) {
-        return flowRepository.findByNamespaceWithSource(tenantId, namespace);
+        if (flowRepository.isEmpty()) {
+            throw NO_REPOSITORY_EXCEPTION;
+        }
+
+        return flowRepository.get().findByNamespaceWithSource(tenantId, namespace);
     }
 
     public Stream<Flow> keepLastVersion(Stream<Flow> stream) {
diff --git a/core/src/main/java/io/kestra/core/services/TaskDefaultService.java b/core/src/main/java/io/kestra/core/services/TaskDefaultService.java
index 80509fc40..292f4517a 100644
--- a/core/src/main/java/io/kestra/core/services/TaskDefaultService.java
+++ b/core/src/main/java/io/kestra/core/services/TaskDefaultService.java
@@ -34,6 +34,7 @@ public class TaskDefaultService {
 
     @Inject
     @Named(QueueFactoryInterface.WORKERTASKLOG_NAMED)
+    @Nullable
     protected QueueInterface<LogEntry> logQueue;
 
     /**
diff --git a/core/src/main/java/io/kestra/core/tasks/debugs/Return.java b/core/src/main/java/io/kestra/core/tasks/debugs/Return.java
index fb02025e2..65846ca0d 100644
--- a/core/src/main/java/io/kestra/core/tasks/debugs/Return.java
+++ b/core/src/main/java/io/kestra/core/tasks/debugs/Return.java
@@ -24,7 +24,8 @@ import java.util.Optional;
 @NoArgsConstructor
 @Schema(
     title = "Debugging task that returns a rendered value.",
-    description = "This task is mostly useful for debugging purpose.\n\n" +
+    description = "Deprecated task, please use io.kestra.core.tasks.outputs.OutputValues\n" +
+        "This task is mostly useful for debugging purpose.\n\n" +
         "It allows you to see inputs or output variables or to debug some templated functions."
 )
 @Plugin(
@@ -38,6 +39,7 @@ import java.util.Optional;
         @Metric(name = "duration", type = Timer.TYPE)
     }
 )
+@Deprecated
 public class Return extends Task implements RunnableTask<Return.Output> {
     @Schema(
         title = "The templated string to render."
diff --git a/core/src/main/java/io/kestra/core/tasks/log/Log.java b/core/src/main/java/io/kestra/core/tasks/log/Log.java
index aaa17228b..272f8d95c 100644
--- a/core/src/main/java/io/kestra/core/tasks/log/Log.java
+++ b/core/src/main/java/io/kestra/core/tasks/log/Log.java
@@ -7,8 +7,8 @@ import io.kestra.core.models.tasks.RunnableTask;
 import io.kestra.core.models.tasks.Task;
 import io.kestra.core.models.tasks.VoidOutput;
 import io.kestra.core.runners.RunContext;
-import io.micronaut.core.annotation.NonNull;
 import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
 import lombok.*;
 import lombok.experimental.SuperBuilder;
 import org.slf4j.Logger;
@@ -59,7 +59,7 @@ public class Log extends Task implements RunnableTask<VoidOutput> {
             String[].class
         }
     )
-    @NonNull
+    @NotNull
     @PluginProperty(dynamic = true)
     private Object message;
 
diff --git a/core/src/main/java/io/kestra/core/tasks/outputs/OutputValues.java b/core/src/main/java/io/kestra/core/tasks/outputs/OutputValues.java
new file mode 100644
index 000000000..1b149d169
--- /dev/null
+++ b/core/src/main/java/io/kestra/core/tasks/outputs/OutputValues.java
@@ -0,0 +1,57 @@
+package io.kestra.core.tasks.outputs;
+
+import io.kestra.core.models.annotations.Example;
+import io.kestra.core.models.annotations.Plugin;
+import io.kestra.core.models.tasks.RunnableTask;
+import io.kestra.core.models.tasks.Task;
+import io.kestra.core.runners.RunContext;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import lombok.experimental.SuperBuilder;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@SuperBuilder
+@ToString
+@EqualsAndHashCode
+@Getter
+@NoArgsConstructor
+@Schema(
+    title = "Task that allows output of multiples values.",
+    description = "This task is mostly useful for debugging purpose.\n\n" +
+        "It allows you to see inputs or output variables or to debug some templated functions."
+)
+@Plugin(
+    examples = {
+        @Example(
+            code = """
+                values:
+                    taskInfo: \"{{ task.id }} > {{ taskrun.startDate }}\"
+                """
+        )
+    }
+)
+public class OutputValues extends Task implements RunnableTask<OutputValues.Output> {
+    @Schema(
+        title = "The templated strings to render."
+    )
+    private HashMap<String, String> values;
+
+
+    @Override
+    public OutputValues.Output run(RunContext runContext) throws Exception {
+        return OutputValues.Output.builder()
+            .values(runContext.renderMap(values))
+            .build();
+    }
+
+    @Builder
+    @Getter
+    public static class Output implements io.kestra.core.models.tasks.Output {
+        @Schema(
+            title = "The generated values."
+        )
+        private Map<String, String> values;
+    }
+}
diff --git a/gradle/jar/selfrun.bat b/gradle/jar/selfrun.bat
index 913d5feaa..22c01d6d2 100644
--- a/gradle/jar/selfrun.bat
+++ b/gradle/jar/selfrun.bat
@@ -34,10 +34,40 @@ IF NOT ERRORLEVEL 1 (set java_version=9)
 ECHO %java_fullversion% | find " full version ""10" > NUL
 IF NOT ERRORLEVEL 1 (set java_version=10)
 
+ECHO %java_fullversion% | find " full version ""11" > NUL
+IF NOT ERRORLEVEL 1 (set java_version=11)
+
+ECHO %java_fullversion% | find " full version ""12" > NUL
+IF NOT ERRORLEVEL 1 (set java_version=12)
+
+ECHO %java_fullversion% | find " full version ""13" > NUL
+IF NOT ERRORLEVEL 1 (set java_version=13)
+
+ECHO %java_fullversion% | find " full version ""14" > NUL
+IF NOT ERRORLEVEL 1 (set java_version=14)
+
+ECHO %java_fullversion% | find " full version ""15" > NUL
+IF NOT ERRORLEVEL 1 (set java_version=15)
+
+ECHO %java_fullversion% | find " full version ""16" > NUL
+IF NOT ERRORLEVEL 1 (set java_version=16)
+
+ECHO %java_fullversion% | find " full version ""17" > NUL
+IF NOT ERRORLEVEL 1 (set java_version=17)
+
+ECHO %java_fullversion% | find " full version ""18" > NUL
+IF NOT ERRORLEVEL 1 (set java_version=18)
+
+ECHO %java_fullversion% | find " full version ""19" > NUL
+IF NOT ERRORLEVEL 1 (set java_version=19)
+
+ECHO %java_fullversion% | find " full version ""20" > NUL
+IF NOT ERRORLEVEL 1 (set java_version=20)
+
 IF NOT DEFINED java_version (set java_version=0)
 
 IF %java_version% NEQ 0 (
-    ECHO [ERROR] Kestra require at least Java 11.. 1>&2
+    ECHO [ERROR] Kestra require at least Java 21.. 1>&2
     EXIT 1
 )
 
@@ -46,7 +76,7 @@ REM Opens java.util due to https://github.com/Azure/azure-sdk-for-java/issues/27
 REM Opens java.lang due to https://github.com/kestra-io/kestra/issues/1755, see https://github.com/micronaut-projects/micronaut-core/issues/9573
 SET "JAVA_ADD_OPENS=--add-opens java.base/java.nio=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED"
 
-java %JAVA_OPTS% %JAVA_ADD_OPENS% -jar "%this%" %*
+java %JAVA_OPTS% %JAVA_ADD_OPENS% -Djava.security.manager=allow -jar "%this%" %*
 
 ENDLOCAL
 
diff --git a/gradle/jar/selfrun.sh b/gradle/jar/selfrun.sh
index 4b6b80e00..89179710f 100644
--- a/gradle/jar/selfrun.sh
+++ b/gradle/jar/selfrun.sh
@@ -12,8 +12,8 @@ fi
 # Check java version
 JAVA_FULLVERSION=$(java -fullversion 2>&1)
 case "$JAVA_FULLVERSION" in
-    [a-z]*\ full\ version\ \"\(1|9|10\)\..*\")
-        echo "[ERROR] Kestra require at least Java 11." 1>&2
+    [a-z]*\ full\ version\ \"\(1|9|10|11|12|13|14|15|16|17|18|19|20\)\..*\")
+        echo "[ERROR] Kestra require at least Java 21." 1>&2
         exit 1
         ;;
 esac
@@ -24,5 +24,5 @@ esac
 JAVA_ADD_OPENS="--add-opens java.base/java.nio=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED"
 
 # Exec
-exec java ${JAVA_OPTS} ${JAVA_ADD_OPENS} -jar "$0" "$@"
+exec java ${JAVA_OPTS} ${JAVA_ADD_OPENS} -Djava.security.manager=allow -jar "$0" "$@"
 exit 127
\ No newline at end of file
diff --git a/ui/src/components/executions/Gantt.vue b/ui/src/components/executions/Gantt.vue
index b8d1269df..f1b33b711 100644
--- a/ui/src/components/executions/Gantt.vue
+++ b/ui/src/components/executions/Gantt.vue
@@ -25,7 +25,7 @@
                             </span>
                         </el-tooltip>
                     </th>
-                    <td :colspan="dates.length">
+                    <td :colspan="dates.length" @click="onTaskSelect(serie.task)" class="cursor-pointer">
                         <el-tooltip placement="top" :persistent="false" transition="" :hide-after="0">
                             <template #content>
                                 <span style="white-space: pre-wrap;">
@@ -323,6 +323,10 @@
 
     }
 
+    .cursor-pointer {
+        cursor: pointer;
+    }
+
     table {
         table-layout: fixed;
         width: 100%;
diff --git a/ui/src/components/home/Home.vue b/ui/src/components/home/Home.vue
index 2cfb8d374..92915e786 100644
--- a/ui/src/components/home/Home.vue
+++ b/ui/src/components/home/Home.vue
@@ -105,7 +105,7 @@
             </el-row>
         </div>
         <div v-else-if="dailyReady">
-            <el-card class="pb-3">
+            <el-card class="pb-3 mb-4">
                 <el-row justify="center">
                     <span class="new-execution-img" />
                 </el-row>
diff --git a/ui/src/components/inputs/Editor.vue b/ui/src/components/inputs/Editor.vue
index d2a4f9483..50fd9172c 100644
--- a/ui/src/components/inputs/Editor.vue
+++ b/ui/src/components/inputs/Editor.vue
@@ -102,7 +102,6 @@
                     BookMultipleOutline: shallowRef(BookMultipleOutline),
                     Close: shallowRef(Close)
                 },
-                oldDecorations: [],
                 editorDocumentation: undefined,
                 plugin: undefined,
                 taskType: undefined,
@@ -209,6 +208,8 @@
 
                 this.editor = editor;
 
+                this.decorations = this.editor.createDecorationsCollection();
+
                 if (!this.original) {
                     this.editor.onDidBlurEditorWidget(() => {
                         this.$emit("focusout", editor.getValue());
@@ -308,27 +309,27 @@
                     this.editor.onDidContentSizeChange(_ => {
                         if (this.guidedProperties.monacoRange) {
                             editor.revealLine(this.guidedProperties.monacoRange.endLineNumber);
-                            let decorations = [
-                                {
-                                    range: this.guidedProperties.monacoRange,
-                                    options: {
-                                        isWholeLine: true,
-                                        inlineClassName: "highlight-text"
-                                    },
-                                    className: "highlight-text",
-                                }
-                            ];
-                            decorations = this.guidedProperties.monacoDisableRange ? decorations.concat([
-                                {
+                            const decorationsToAdd = [];
+                            decorationsToAdd.push({
+                                range: this.guidedProperties.monacoRange,
+                                options: {
+                                    isWholeLine: true,
+                                    inlineClassName: "highlight-text"
+                                },
+                                className: "highlight-text",
+                            });
+                            if (this.guidedProperties.monacoDisableRange) {
+                                decorationsToAdd.push({
                                     range: this.guidedProperties.monacoDisableRange,
                                     options: {
                                         isWholeLine: true,
                                         inlineClassName: "disable-text"
                                     },
                                     className: "disable-text",
-                                },
-                            ]) : decorations;
-                            this.oldDecorations = this.editor.deltaDecorations(this.oldDecorations, decorations)
+                                });
+                            }
+
+                            this.decorations.set(decorationsToAdd);
                         } else {
                             this.highlightPebble();
                         }
@@ -363,14 +364,14 @@
             highlightPebble() {
                 // Highlight code that match pebble content
                 let model = this.editor.getModel();
-                let decorations = [];
                 let text = model.getValue();
                 let regex = new RegExp("\\{\\{(.+?)}}", "g");
                 let match;
+                const decorationsToAdd = [];
                 while ((match = regex.exec(text)) !== null) {
                     let startPos = model.getPositionAt(match.index);
                     let endPos = model.getPositionAt(match.index + match[0].length);
-                    decorations.push({
+                    decorationsToAdd.push({
                         range: {
                             startLineNumber: startPos.lineNumber,
                             startColumn: startPos.column,
@@ -382,7 +383,7 @@
                         }
                     });
                 }
-                this.oldDecorations = this.editor.deltaDecorations(this.oldDecorations, decorations);
+                this.decorations.set(decorationsToAdd);
             }
         },
     };
diff --git a/ui/src/components/onboarding/Welcome.vue b/ui/src/components/onboarding/Welcome.vue
index a83ace01a..dc5b25d8e 100644
--- a/ui/src/components/onboarding/Welcome.vue
+++ b/ui/src/components/onboarding/Welcome.vue
@@ -62,7 +62,6 @@
             ...mapGetters("core", ["guidedProperties"]),
             logo() {
                 // get theme
-                console.log((localStorage.getItem("theme") || "light") === "light")
                 return (localStorage.getItem("theme") || "light") === "light" ? onboardingImageLight : onboardingImage;
             }
         }
diff --git a/ui/src/override/components/LeftMenu.vue b/ui/src/override/components/LeftMenu.vue
index f1231662f..2242700e0 100644
--- a/ui/src/override/components/LeftMenu.vue
+++ b/ui/src/override/components/LeftMenu.vue
@@ -45,6 +45,7 @@
     import TimerCogOutline from "vue-material-design-icons/TimerCogOutline.vue";
     import {mapState} from "vuex";
     import ChartBoxOutline from "vue-material-design-icons/ChartBoxOutline.vue";
+    import Connection from "vue-material-design-icons/Connection.vue";
     import {shallowRef} from "vue";
 
     export default {
@@ -169,6 +170,15 @@
                             class: "menu-icon"
                         },
                     },
+                    {
+                        href: {name: "plugins/list"},
+                        routes: this.routeStartWith("plugins"),
+                        title: this.$t("plugins.names"),
+                        icon: {
+                            element: shallowRef(Connection),
+                            class: "menu-icon"
+                        },
+                    },
                     {
                         title: this.$t("administration"),
                         routes: this.routeStartWith("admin"),


Test patch:
diff --git a/cli/src/test/java/io/kestra/cli/commands/flows/FlowExpandCommandTest.java b/cli/src/test/java/io/kestra/cli/commands/flows/FlowExpandCommandTest.java
index adfa2b606..2ad049568 100644
--- a/cli/src/test/java/io/kestra/cli/commands/flows/FlowExpandCommandTest.java
+++ b/cli/src/test/java/io/kestra/cli/commands/flows/FlowExpandCommandTest.java
@@ -2,8 +2,6 @@ package io.kestra.cli.commands.flows;
 
 import io.micronaut.configuration.picocli.PicocliRunner;
 import io.micronaut.context.ApplicationContext;
-import io.micronaut.context.env.Environment;
-import io.micronaut.runtime.server.EmbeddedServer;
 import org.junit.jupiter.api.Test;
 
 import java.io.ByteArrayOutputStream;
@@ -11,7 +9,6 @@ import java.io.PrintStream;
 
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.core.Is.is;
-import static org.hamcrest.core.StringContains.containsString;
 
 class FlowExpandCommandTest {
     @Test
@@ -19,10 +16,7 @@ class FlowExpandCommandTest {
         ByteArrayOutputStream out = new ByteArrayOutputStream();
         System.setOut(new PrintStream(out));
 
-        try (ApplicationContext ctx = ApplicationContext.run(Environment.CLI, Environment.TEST)) {
-            EmbeddedServer embeddedServer = ctx.getBean(EmbeddedServer.class);
-            embeddedServer.start();
-
+        try (ApplicationContext ctx = ApplicationContext.builder().deduceEnvironment(false).start()) {
             String[] args = {
                 "src/test/resources/helper/flow.yaml"
             };
diff --git a/cli/src/test/java/io/kestra/cli/commands/flows/FlowValidateCommandTest.java b/cli/src/test/java/io/kestra/cli/commands/flows/FlowValidateCommandTest.java
index 40a947564..f36349db4 100644
--- a/cli/src/test/java/io/kestra/cli/commands/flows/FlowValidateCommandTest.java
+++ b/cli/src/test/java/io/kestra/cli/commands/flows/FlowValidateCommandTest.java
@@ -2,8 +2,6 @@ package io.kestra.cli.commands.flows;
 
 import io.micronaut.configuration.picocli.PicocliRunner;
 import io.micronaut.context.ApplicationContext;
-import io.micronaut.context.env.Environment;
-import io.micronaut.runtime.server.EmbeddedServer;
 import org.junit.jupiter.api.Test;
 
 import java.io.ByteArrayOutputStream;
@@ -19,10 +17,7 @@ class FlowValidateCommandTest {
         ByteArrayOutputStream out = new ByteArrayOutputStream();
         System.setOut(new PrintStream(out));
 
-        try (ApplicationContext ctx = ApplicationContext.run(Environment.CLI, Environment.TEST)) {
-            EmbeddedServer embeddedServer = ctx.getBean(EmbeddedServer.class);
-            embeddedServer.start();
-
+        try (ApplicationContext ctx = ApplicationContext.builder().deduceEnvironment(false).start()) {
             String[] args = {
                 "--local",
                 "src/test/resources/helper/flow.yaml"
@@ -39,10 +34,7 @@ class FlowValidateCommandTest {
         ByteArrayOutputStream out = new ByteArrayOutputStream();
         System.setOut(new PrintStream(out));
 
-        try (ApplicationContext ctx = ApplicationContext.run(Environment.CLI, Environment.TEST)) {
-            EmbeddedServer embeddedServer = ctx.getBean(EmbeddedServer.class);
-            embeddedServer.start();
-
+        try (ApplicationContext ctx = ApplicationContext.builder().deduceEnvironment(false).start()) {
             String[] args = {
                 "--local",
                 "src/test/resources/warning/flow-with-warning.yaml"
diff --git a/cli/src/test/java/io/kestra/cli/commands/flows/namespaces/FlowNamespaceCommandTest.java b/cli/src/test/java/io/kestra/cli/commands/flows/namespaces/FlowNamespaceCommandTest.java
index d4f60f49b..7f4e4cea2 100644
--- a/cli/src/test/java/io/kestra/cli/commands/flows/namespaces/FlowNamespaceCommandTest.java
+++ b/cli/src/test/java/io/kestra/cli/commands/flows/namespaces/FlowNamespaceCommandTest.java
@@ -3,7 +3,6 @@ package io.kestra.cli.commands.flows.namespaces;
 import io.micronaut.configuration.picocli.PicocliRunner;
 import io.micronaut.context.ApplicationContext;
 import io.micronaut.context.env.Environment;
-import io.micronaut.runtime.server.EmbeddedServer;
 import org.junit.jupiter.api.Test;
 
 import java.io.ByteArrayOutputStream;
@@ -19,10 +18,7 @@ class FlowNamespaceCommandTest {
         ByteArrayOutputStream out = new ByteArrayOutputStream();
         System.setOut(new PrintStream(out));
 
-        try (ApplicationContext ctx = ApplicationContext.run(Environment.CLI, Environment.TEST)) {
-            EmbeddedServer embeddedServer = ctx.getBean(EmbeddedServer.class);
-            embeddedServer.start();
-
+        try (ApplicationContext ctx = ApplicationContext.builder().deduceEnvironment(false).start()) {
             String[] args = {};
             Integer call = PicocliRunner.call(FlowNamespaceCommand.class, ctx, args);
 
diff --git a/cli/src/test/java/io/kestra/cli/commands/namespaces/NamespaceCommandTest.java b/cli/src/test/java/io/kestra/cli/commands/namespaces/NamespaceCommandTest.java
index 2e860a930..9aad84ba4 100644
--- a/cli/src/test/java/io/kestra/cli/commands/namespaces/NamespaceCommandTest.java
+++ b/cli/src/test/java/io/kestra/cli/commands/namespaces/NamespaceCommandTest.java
@@ -2,8 +2,6 @@ package io.kestra.cli.commands.namespaces;
 
 import io.micronaut.configuration.picocli.PicocliRunner;
 import io.micronaut.context.ApplicationContext;
-import io.micronaut.context.env.Environment;
-import io.micronaut.runtime.server.EmbeddedServer;
 import org.junit.jupiter.api.Test;
 
 import java.io.ByteArrayOutputStream;
@@ -19,10 +17,7 @@ class NamespaceCommandTest {
         ByteArrayOutputStream out = new ByteArrayOutputStream();
         System.setOut(new PrintStream(out));
 
-        try (ApplicationContext ctx = ApplicationContext.run(Environment.CLI, Environment.TEST)) {
-            EmbeddedServer embeddedServer = ctx.getBean(EmbeddedServer.class);
-            embeddedServer.start();
-
+        try (ApplicationContext ctx = ApplicationContext.builder().deduceEnvironment(false).start()) {
             String[] args = {};
             Integer call = PicocliRunner.call(NamespaceCommand.class, ctx, args);
 
diff --git a/cli/src/test/java/io/kestra/cli/commands/namespaces/files/NamespaceFilesCommandTest.java b/cli/src/test/java/io/kestra/cli/commands/namespaces/files/NamespaceFilesCommandTest.java
index 4ba979da5..659f30aae 100644
--- a/cli/src/test/java/io/kestra/cli/commands/namespaces/files/NamespaceFilesCommandTest.java
+++ b/cli/src/test/java/io/kestra/cli/commands/namespaces/files/NamespaceFilesCommandTest.java
@@ -3,7 +3,6 @@ package io.kestra.cli.commands.namespaces.files;
 import io.micronaut.configuration.picocli.PicocliRunner;
 import io.micronaut.context.ApplicationContext;
 import io.micronaut.context.env.Environment;
-import io.micronaut.runtime.server.EmbeddedServer;
 import org.junit.jupiter.api.Test;
 
 import java.io.ByteArrayOutputStream;
@@ -19,10 +18,7 @@ class NamespaceFilesCommandTest {
         ByteArrayOutputStream out = new ByteArrayOutputStream();
         System.setOut(new PrintStream(out));
 
-        try (ApplicationContext ctx = ApplicationContext.run(Environment.CLI, Environment.TEST)) {
-            EmbeddedServer embeddedServer = ctx.getBean(EmbeddedServer.class);
-            embeddedServer.start();
-
+        try (ApplicationContext ctx = ApplicationContext.builder().deduceEnvironment(false).start()) {
             String[] args = {};
             Integer call = PicocliRunner.call(NamespaceFilesCommand.class, ctx, args);
 
diff --git a/cli/src/test/java/io/kestra/cli/commands/sys/database/DatabaseCommandTest.java b/cli/src/test/java/io/kestra/cli/commands/sys/database/DatabaseCommandTest.java
index d1f2d46ec..cb524b147 100644
--- a/cli/src/test/java/io/kestra/cli/commands/sys/database/DatabaseCommandTest.java
+++ b/cli/src/test/java/io/kestra/cli/commands/sys/database/DatabaseCommandTest.java
@@ -2,8 +2,6 @@ package io.kestra.cli.commands.sys.database;
 
 import io.micronaut.configuration.picocli.PicocliRunner;
 import io.micronaut.context.ApplicationContext;
-import io.micronaut.context.env.Environment;
-import io.micronaut.runtime.server.EmbeddedServer;
 import org.junit.jupiter.api.Test;
 
 import java.io.ByteArrayOutputStream;
@@ -19,10 +17,7 @@ class DatabaseCommandTest {
         ByteArrayOutputStream out = new ByteArrayOutputStream();
         System.setOut(new PrintStream(out));
 
-        try (ApplicationContext ctx = ApplicationContext.run(Environment.CLI, Environment.TEST)) {
-            EmbeddedServer embeddedServer = ctx.getBean(EmbeddedServer.class);
-            embeddedServer.start();
-
+        try (ApplicationContext ctx = ApplicationContext.builder().deduceEnvironment(false).start()) {
             String[] args = {};
             Integer call = PicocliRunner.call(DatabaseCommand.class, ctx, args);
 
diff --git a/cli/src/test/java/io/kestra/cli/commands/templates/namespaces/TemplateNamespaceCommandTest.java b/cli/src/test/java/io/kestra/cli/commands/templates/namespaces/TemplateNamespaceCommandTest.java
index ea99fb90f..325bbe7e6 100644
--- a/cli/src/test/java/io/kestra/cli/commands/templates/namespaces/TemplateNamespaceCommandTest.java
+++ b/cli/src/test/java/io/kestra/cli/commands/templates/namespaces/TemplateNamespaceCommandTest.java
@@ -2,8 +2,6 @@ package io.kestra.cli.commands.templates.namespaces;
 
 import io.micronaut.configuration.picocli.PicocliRunner;
 import io.micronaut.context.ApplicationContext;
-import io.micronaut.context.env.Environment;
-import io.micronaut.runtime.server.EmbeddedServer;
 import org.junit.jupiter.api.Test;
 
 import java.io.ByteArrayOutputStream;
@@ -19,10 +17,7 @@ class TemplateNamespaceCommandTest {
         ByteArrayOutputStream out = new ByteArrayOutputStream();
         System.setOut(new PrintStream(out));
 
-        try (ApplicationContext ctx = ApplicationContext.run(Environment.CLI, Environment.TEST)) {
-            EmbeddedServer embeddedServer = ctx.getBean(EmbeddedServer.class);
-            embeddedServer.start();
-
+        try (ApplicationContext ctx = ApplicationContext.builder().deduceEnvironment(false).start()) {
             String[] args = {};
             Integer call = PicocliRunner.call(TemplateNamespaceCommand.class, ctx, args);
 
diff --git a/core/src/test/java/io/kestra/core/services/FlowServiceTest.java b/core/src/test/java/io/kestra/core/services/FlowServiceTest.java
index 5b0692926..7d5e1b772 100644
--- a/core/src/test/java/io/kestra/core/services/FlowServiceTest.java
+++ b/core/src/test/java/io/kestra/core/services/FlowServiceTest.java
@@ -1,17 +1,17 @@
 package io.kestra.core.services;
 
+import io.kestra.core.models.flows.Flow;
 import io.kestra.core.models.flows.Type;
 import io.kestra.core.models.flows.input.StringInput;
+import io.kestra.core.tasks.debugs.Return;
 import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import jakarta.inject.Inject;
 import org.junit.jupiter.api.Test;
-import io.kestra.core.models.flows.Flow;
-import io.kestra.core.tasks.debugs.Return;
 
 import java.util.Collections;
 import java.util.List;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
-import jakarta.inject.Inject;
 
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.*;
@@ -140,6 +140,6 @@ class FlowServiceTest {
                 .build()))
             .build();
 
-        assertThat(flowService.deprecationPaths(flow), is(List.of("inputs[1].name")));
+        assertThat(flowService.deprecationPaths(flow), containsInAnyOrder("inputs[1].name", "tasks[0]"));
     }
 }
\ No newline at end of file
diff --git a/core/src/test/java/io/kestra/core/tasks/OutputValuesTest.java b/core/src/test/java/io/kestra/core/tasks/OutputValuesTest.java
new file mode 100644
index 000000000..59dabab58
--- /dev/null
+++ b/core/src/test/java/io/kestra/core/tasks/OutputValuesTest.java
@@ -0,0 +1,32 @@
+package io.kestra.core.tasks;
+
+import io.kestra.core.models.executions.Execution;
+import io.kestra.core.models.executions.TaskRun;
+import io.kestra.core.models.flows.State;
+import io.kestra.core.repositories.FlowRepositoryInterface;
+import io.kestra.core.runners.AbstractMemoryRunnerTest;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.Test;
+
+import java.util.HashMap;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.is;
+
+public class OutputValuesTest extends AbstractMemoryRunnerTest {
+    @Inject
+    FlowRepositoryInterface flowRepository;
+
+    @Test
+    void output() throws Exception {
+        Execution execution = runnerUtils.runOne(null, "io.kestra.tests", "output-values");
+
+        assertThat(execution.getState().getCurrent(), is(State.Type.SUCCESS));
+        assertThat(execution.getTaskRunList(), hasSize(1));
+        TaskRun outputValues = execution.getTaskRunList().get(0);
+        HashMap<String, String> values = (HashMap<String, String>) outputValues.getOutputs().get("values");
+        assertThat(values.get("output1"), is("xyz"));
+        assertThat(values.get("output2"), is("abc"));
+    }
+}
diff --git a/core/src/test/resources/flows/valids/output-values.yml b/core/src/test/resources/flows/valids/output-values.yml
new file mode 100644
index 000000000..be27359b4
--- /dev/null
+++ b/core/src/test/resources/flows/valids/output-values.yml
@@ -0,0 +1,10 @@
+id: output-values
+namespace: io.kestra.tests
+
+tasks:
+  - id: return
+    type: io.kestra.core.tasks.outputs.OutputValues
+    values:
+      output1: xyz
+      output2: abc
+      taskInfo: "{{ task.id }} > {{ taskrun.startDate }}"
\ No newline at end of file
diff --git a/webserver/src/test/java/io/kestra/webserver/controllers/api/FlowControllerTest.java b/webserver/src/test/java/io/kestra/webserver/controllers/api/FlowControllerTest.java
index da2f7e6f0..cf00d1548 100644
--- a/webserver/src/test/java/io/kestra/webserver/controllers/api/FlowControllerTest.java
+++ b/webserver/src/test/java/io/kestra/webserver/controllers/api/FlowControllerTest.java
@@ -718,7 +718,7 @@ class FlowControllerTest extends JdbcH2ControllerTest {
         assertThat(body.get(0).getWarnings().size(), is(1));
         assertThat(body.get(0).getWarnings().get(0), containsString("The system namespace is reserved for background workflows"));
         assertThat(body.get(1).isOutdated(), is(false));
-        assertThat(body.get(1).getDeprecationPaths(), empty());
+        assertThat(body.get(1).getDeprecationPaths(), containsInAnyOrder("tasks[0]", "tasks[1]"));
         assertThat(body.get(1).getWarnings(), empty());
         assertThat(body, everyItem(
             Matchers.hasProperty("constraints", is(nullValue()))