Repository: kestra-io/kestra
Issue: Beta plugins
Labels: enhancement

### 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

Issue comments:
how about this?

> 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.

Pull request review comments:

Patch that resolved the issue:
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 e9c60c71f5..aa4d9d2dc5 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 @@
 @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 @@ protected AbstractClassDocumentation(JsonSchemaGenerator jsonSchemaGenerator, Cl
         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 1473fb4f21..78e5c22ec4 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 CustomDefinition provideCustomSchemaDefinition(ResolvedType javaType, Sch
             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 CustomDefinition provideCustomSchemaDefinition(ResolvedType javaType, Sch
                     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 @@ private void addMainRefProperties(JsonNode mainClassDef, ObjectNode objectNode)
         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 d1da645ad8..9d58739d11 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 @@
     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 4a8749999e..1b51e5f80b 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 @@
 @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 @@
      * @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 68efa6bde1..ba9af24dc9 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 @@
 @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 7510be0f05..7e0ba2276a 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 @@
 @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/resources/docs/macro.peb b/core/src/main/resources/docs/macro.peb
index 5129d5f21e..d18ae35d04 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 4e286fed16..6076d7b60c 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 d3c71be3bd..ae3e7323cd 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 b45c52beda..aec7eb0a97 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 16f993dc6a..4d71955a0f 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 @@ void scriptRunner() throws IOException {
 
         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 dc9ea23785..b3fb4b48cd 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 @@
 
 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 @@ void testEnum() {
         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 @@ private static abstract class ParentClass extends Task {
         @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;
+    }
 }