Repository: kestra-io/kestra (java)

Issue:
Beta plugins
### Feature description

When a plugin is annotated with `com.google.common.annotations.Beta`, add a `$beta` property in the JSON Schema and document it as beta.

The following snippet should be added with an alert of type info:

> This plugin is currently in beta. While it is considered safe for use, please be aware that its API could change in ways that are not compatible with earlier versions in future releases, or it might become unsupported.

~@anna-geller please advise for the snippet documentation, I'll update the description accordingly~ => updated

Pull request:
feat(core,ui): allow marking plugin and propertes as beta
Fixes #3344

A beta plugin must be annotated with `@Plugin(beta=true)`.
It will be displayed as following:
![image](https://github.com/kestra-io/kestra/assets/1819009/b1aac01b-1c94-44ca-a6f3-8735277a27b9)

Note, the same apply for beta properties which must be annotated with `@PluginProperty(beta=true)`.

Patch:
diff --git a/core/src/main/java/io/kestra/core/docs/AbstractClassDocumentation.java b/core/src/main/java/io/kestra/core/docs/AbstractClassDocumentation.java
index e9c60c71f..aa4d9d2dc 100644
--- a/core/src/main/java/io/kestra/core/docs/AbstractClassDocumentation.java
+++ b/core/src/main/java/io/kestra/core/docs/AbstractClassDocumentation.java
@@ -18,6 +18,7 @@ import java.util.stream.Collectors;
 @Slf4j
 public abstract class AbstractClassDocumentation<T> {
     protected Boolean deprecated;
+    protected Boolean beta;
     protected String cls;
     protected String shortName;
     protected String docDescription;
@@ -65,6 +66,7 @@ public abstract class AbstractClassDocumentation<T> {
         this.docDescription = this.propertiesSchema.containsKey("title") ? (String) this.propertiesSchema.get("title") : null;
         this.docBody = this.propertiesSchema.containsKey("description") ? (String) this.propertiesSchema.get("description") : null;
         this.deprecated = this.propertiesSchema.containsKey("$deprecated");
+        this.beta = this.propertiesSchema.containsKey("$beta");
 
         if (this.propertiesSchema.containsKey("$examples")) {
             List<Map<String, Object>> examples = (List<Map<String, Object>>) this.propertiesSchema.get("$examples");
diff --git a/core/src/main/java/io/kestra/core/docs/JsonSchemaGenerator.java b/core/src/main/java/io/kestra/core/docs/JsonSchemaGenerator.java
index 1473fb4f2..78e5c22ec 100644
--- a/core/src/main/java/io/kestra/core/docs/JsonSchemaGenerator.java
+++ b/core/src/main/java/io/kestra/core/docs/JsonSchemaGenerator.java
@@ -247,6 +247,9 @@ public class JsonSchemaGenerator {
             PluginProperty pluginPropertyAnnotation = member.getAnnotationConsideringFieldAndGetter(PluginProperty.class);
             if (pluginPropertyAnnotation != null) {
                 memberAttributes.put("$dynamic", pluginPropertyAnnotation.dynamic());
+                if (pluginPropertyAnnotation.beta()) {
+                    memberAttributes.put("$beta", true);
+                }
             }
 
             Schema schema = member.getAnnotationConsideringFieldAndGetter(Schema.class);
@@ -292,6 +295,10 @@ public class JsonSchemaGenerator {
                     if (!metrics.isEmpty()) {
                         collectedTypeAttributes.set("$metrics", context.getGeneratorConfig().createArrayNode().addAll(metrics));
                     }
+
+                    if (pluginAnnotation.beta()) {
+                        collectedTypeAttributes.put("$beta", true);
+                    }
                 }
 
                 // handle deprecated tasks
@@ -538,6 +545,9 @@ public class JsonSchemaGenerator {
         if (mainClassDef.has("$deprecated")) {
             objectNode.set("$deprecated", mainClassDef.get("$deprecated"));
         }
+        if (mainClassDef.has("$beta")) {
+            objectNode.set("$beta", mainClassDef.get("$beta"));
+        }
     }
 
     private Object buildDefaultInstance(Class<?> cls) {
diff --git a/core/src/main/java/io/kestra/core/models/annotations/Plugin.java b/core/src/main/java/io/kestra/core/models/annotations/Plugin.java
index d1da645ad..9d58739d1 100644
--- a/core/src/main/java/io/kestra/core/models/annotations/Plugin.java
+++ b/core/src/main/java/io/kestra/core/models/annotations/Plugin.java
@@ -12,4 +12,9 @@ public @interface Plugin {
     Example[] examples();
 
     Metric[] metrics() default {};
+
+    /**
+     * @return whether the plugin is in beta
+     */
+    boolean beta() default false;
 }
diff --git a/core/src/main/java/io/kestra/core/models/annotations/PluginProperty.java b/core/src/main/java/io/kestra/core/models/annotations/PluginProperty.java
index 4a8749999..1b51e5f80 100644
--- a/core/src/main/java/io/kestra/core/models/annotations/PluginProperty.java
+++ b/core/src/main/java/io/kestra/core/models/annotations/PluginProperty.java
@@ -10,7 +10,7 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
 @Target({ElementType.FIELD, ElementType.METHOD})
 public @interface PluginProperty {
     /**
-     * @return If the properties is renderer
+     * @return whether the property is renderer
      */
     boolean dynamic() default false;
 
@@ -18,4 +18,9 @@ public @interface PluginProperty {
      * @return the Class for a map
      */
     Class<?> additionalProperties() default Object.class;
+
+    /**
+     * @return whether the property is in beta
+     */
+    boolean beta() default false;
 }
diff --git a/core/src/main/java/io/kestra/core/models/script/ScriptRunner.java b/core/src/main/java/io/kestra/core/models/script/ScriptRunner.java
index 68efa6bde..ba9af24dc 100644
--- a/core/src/main/java/io/kestra/core/models/script/ScriptRunner.java
+++ b/core/src/main/java/io/kestra/core/models/script/ScriptRunner.java
@@ -1,7 +1,6 @@
 package io.kestra.core.models.script;
 
 import com.fasterxml.jackson.annotation.JsonTypeInfo;
-import com.google.common.annotations.Beta;
 import io.kestra.core.runners.RunContext;
 import io.micronaut.core.annotation.Introspected;
 import jakarta.validation.constraints.NotBlank;
@@ -20,7 +19,6 @@ import java.util.List;
 @Getter
 @NoArgsConstructor
 @Introspected
-@Beta
 public abstract class ScriptRunner {
     @NotBlank
     @Pattern(regexp="\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*(\\.\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)*")
diff --git a/core/src/main/java/io/kestra/core/models/script/types/ProcessScriptRunner.java b/core/src/main/java/io/kestra/core/models/script/types/ProcessScriptRunner.java
index 7510be0f0..7e0ba2276 100644
--- a/core/src/main/java/io/kestra/core/models/script/types/ProcessScriptRunner.java
+++ b/core/src/main/java/io/kestra/core/models/script/types/ProcessScriptRunner.java
@@ -1,6 +1,6 @@
 package io.kestra.core.models.script.types;
 
-import com.google.common.annotations.Beta;
+import io.kestra.core.models.annotations.Plugin;
 import io.kestra.core.models.script.*;
 import io.kestra.core.runners.RunContext;
 import io.micronaut.core.annotation.Introspected;
@@ -26,7 +26,8 @@ import java.util.Map;
 @EqualsAndHashCode
 @Getter
 @NoArgsConstructor
-@Beta // all script runners are beta for now, but this one is stable as it was the one used before
+// all script runners are beta for now, but this one is stable as it was the one used before
+@Plugin(beta = true, examples = {})
 @Schema(
     title = "A script runner that runs script as a process on the Kestra host",
     description = "When the Kestra Worker that runs this process is terminated, the process will be terminated and the task fail."
diff --git a/core/src/main/java/io/kestra/core/repositories/LocalFlowRepositoryLoader.java b/core/src/main/java/io/kestra/core/repositories/LocalFlowRepositoryLoader.java
index 74ba3de42..d53422d7d 100644
--- a/core/src/main/java/io/kestra/core/repositories/LocalFlowRepositoryLoader.java
+++ b/core/src/main/java/io/kestra/core/repositories/LocalFlowRepositoryLoader.java
@@ -93,7 +93,7 @@ public class LocalFlowRepositoryLoader {
                     }
                 }
             } catch (ConstraintViolationException e) {
-                log.debug("Unable to create flow {}", file, e);
+                log.warn("Unable to create flow {}", file, e);
             }
         }
     }
diff --git a/core/src/main/resources/docs/macro.peb b/core/src/main/resources/docs/macro.peb
index 5129d5f21..d18ae35d0 100644
--- a/core/src/main/resources/docs/macro.peb
+++ b/core/src/main/resources/docs/macro.peb
@@ -14,6 +14,12 @@
 ::
 {% endif %}
 
+{%- if data['$beta'] != null %}
+::alert{type="info"}
+🛈 This property is currently in beta. While it is considered safe for use, please be aware that its API could change in ways that are not compatible with earlier versions in future releases, or it might become unsupported.
+::
+{%- endif %}
+
 {%- if data.type != null -%}
 * **Type:** {{ type(data) -}}
 {%- elseif data['$ref'] -%}
diff --git a/core/src/main/resources/docs/task.peb b/core/src/main/resources/docs/task.peb
index 4e286fed1..6076d7b60 100644
--- a/core/src/main/resources/docs/task.peb
+++ b/core/src/main/resources/docs/task.peb
@@ -23,6 +23,12 @@ icon: {{ icon }}
 ::
 {%- endif %}
 
+{% if beta %}
+::alert{type="info"}
+🛈 This plugin is currently in beta. While it is considered safe for use, please be aware that its API could change in ways that are not compatible with earlier versions in future releases, or it might become unsupported.
+::
+{%- endif %}
+
 ```yaml
 type: "{{ cls }}"
 ```
diff --git a/ui/src/components/layout/Markdown.vue b/ui/src/components/layout/Markdown.vue
index d3c71be3b..ae3e7323c 100644
--- a/ui/src/components/layout/Markdown.vue
+++ b/ui/src/components/layout/Markdown.vue
@@ -98,6 +98,19 @@
                 margin-bottom: 0;
             }
         }
+
+        .info {
+            background-color: var(--el-color-info-light-9);
+            border: 1px solid var(--el-color-info-light-5);
+            padding: 8px 16px;
+            color: var(--el-color-info);
+            border-radius: var(--el-border-radius-base);
+            margin-bottom: var(--spacer);
+
+            p:last-child {
+                margin-bottom: 0;
+            }
+        }
     }
 
     .markdown-tooltip {
diff --git a/ui/src/utils/markdown.js b/ui/src/utils/markdown.js
index b45c52bed..aec7eb0a9 100644
--- a/ui/src/utils/markdown.js
+++ b/ui/src/utils/markdown.js
@@ -19,6 +19,7 @@ export default class Markdown {
             })
             // if more alert types are used inside the task documentation, they need to be configured here also
             .use(container, "warning")
+            .use(container, "info")
 
         md.set({
             html: true,


Test patch:
diff --git a/core/src/test/java/io/kestra/core/docs/DocumentationGeneratorTest.java b/core/src/test/java/io/kestra/core/docs/DocumentationGeneratorTest.java
index 16f993dc6..4d71955a0 100644
--- a/core/src/test/java/io/kestra/core/docs/DocumentationGeneratorTest.java
+++ b/core/src/test/java/io/kestra/core/docs/DocumentationGeneratorTest.java
@@ -172,5 +172,6 @@ class DocumentationGeneratorTest {
 
         assertThat(render, containsString("title: ProcessScriptRunner"));
         assertThat(render, containsString("A script runner that runs script as a process on the Kestra host"));
+        assertThat(render, containsString("\uD83D\uDEC8 This plugin is currently in beta"));
     }
 }
diff --git a/core/src/test/java/io/kestra/core/docs/JsonSchemaGeneratorTest.java b/core/src/test/java/io/kestra/core/docs/JsonSchemaGeneratorTest.java
index dc9ea2378..b3fb4b48c 100644
--- a/core/src/test/java/io/kestra/core/docs/JsonSchemaGeneratorTest.java
+++ b/core/src/test/java/io/kestra/core/docs/JsonSchemaGeneratorTest.java
@@ -2,6 +2,7 @@ package io.kestra.core.docs;
 
 import io.kestra.core.Helpers;
 import io.kestra.core.models.annotations.Example;
+import io.kestra.core.models.annotations.Plugin;
 import io.kestra.core.models.annotations.PluginProperty;
 import io.kestra.core.models.flows.Flow;
 import io.kestra.core.models.tasks.RunnableTask;
@@ -203,6 +204,15 @@ class JsonSchemaGeneratorTest {
         assertThat(((Map<String, Map<String, Object>>) generate.get("properties")).get("stringWithDefault").get("default"), is("default"));
     }
 
+    @Test
+    void betaTask() {
+        Map<String, Object> generate = jsonSchemaGenerator.properties(Task.class, BetaTask.class);
+        assertThat(generate, is(not(nullValue())));
+        assertThat(generate.get("$beta"), is(true));
+        assertThat(((Map<String, Map<String, Object>>) generate.get("properties")).size(), is(1));
+        assertThat(((Map<String, Map<String, Object>>) generate.get("properties")).get("beta").get("$beta"), is(true));
+    }
+
     @SuppressWarnings("unchecked")
     private Map<String, Map<String, Object>> properties(Map<String, Object> generate) {
         return (Map<String, Map<String, Object>>) generate.get("properties");
@@ -261,4 +271,18 @@ class JsonSchemaGeneratorTest {
         @Builder.Default
         private String stringWithDefault = "default";
     }
+
+    @SuperBuilder
+    @ToString
+    @EqualsAndHashCode
+    @Getter
+    @NoArgsConstructor
+    @Plugin(
+        beta = true,
+        examples = {}
+    )
+    private static class BetaTask extends Task {
+        @PluginProperty(beta = true)
+        private String beta;
+    }
 }