Skip to content

feat(groovy): add script and commands triggers - #459

Merged
jymaire merged 6 commits into
kestra-io:mainfrom
sushanttiwari-14:feat/groovy-triggers
Oct 9, 2026
Merged

jymaire merged 6 commits into
kestra-io:mainfrom
sushanttiwari-14:feat/groovy-triggers

Conversation

@sushanttiwari-14

@sushanttiwari-14 sushanttiwari-14 commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

What changes are being made and why?

Add Groovy ScriptTrigger and CommandsTrigger polling triggers. They start a flow when a fresh process matches an exit-code or structured-variable condition, following the maintainer-confirmed model. Closes #425.

Script and commands properties reach their embedded tasks unchanged, so rendering can use runner variables such as {{ workingDir }}. Edge mode stores boolean state only when it changes and accepts Boolean/string values from KV. With edge: false, polling bypasses KV entirely; re-enabling resumes the last persisted edge-mode baseline. Documentation describes supported options, dependency setup, and unavailable beforeCommands, env, and taskRunner properties. Both trigger classes are listed in AGENTS.md.

How the changes have been QAed?

Built with Java 21, Kestra dependencies 1.3.39, and plugin version 1.13.1-SNAPSHOT.

  • 99/99 Groovy tests and 17/17 Node tests pass. Groovy formatting, plugin documentation lint, the shadow JAR build, and formatting of the changed Node test pass. Five unchanged Node files have baseline formatting violations; they were left unchanged.
  • Three real Docker regressions fail on the reviewed CI merge with unavailable workingDir, then pass after the fix: script expressions, expressions in serialized command-list elements, and an expression returning the whole command list. Coverage also checks KV outages with edge disabled, persisted Boolean/string/null/missing/unexpected values, mode changes and rearming, enabled KV failure propagation, invalid inputs, and literal/expression Property parity with the underlying tasks.
  • Rendering QA at commit 78c3c1c0 loaded its exact built JAR into Kestra v1.3.39-no-plugins, with one Groovy JAR. Both flows below verified an absolute usable runner directory, wrote/read a probe file, emitted structured values, and completed their downstream Return tasks. Both flows were disabled after QA. JAR SHA-256: 46591e32017920283f444364a73d029b0e13aadc6fe3b2343fc50b24ec66870a.
  • The original Ruby investigation at 78c3c1c0 passed on the reviewed merge, its unmodified base and the feature tree. Its stdout-log-count failure remains unconfirmed. The latest failed remote run did not execute Ruby tests, so no Ruby pass is claimed for that run.
  • Remote run 37351640897 failed the unchanged legacy NodeTest.requirements after Axios requested http://google.com: IPv4 timed out and IPv6 was unreachable. The dependency test now uses a real HTTP server inside the Node process, bound to loopback on an ephemeral port, with readiness, response assertions, proxy disabled, bounded timeouts and cleanup. Regression tests cover HTTP rejection and preventing script execution after dependency installation failure. Axios, Node production code and CI configuration are unchanged; npm registry access is still required.
  • Full local repository validation is not green: check shadowJar --parallel --max-workers=2 --console=plain executed 244 tests, with 243 passing and one existing JBang Kotlin test failing because kotlinc 2.3.10 could not be downloaded or installed. Groovy passed 99/99 in this run; Node passed 17/17 separately afterward. The precise compiler installation failure is unconfirmed.
  • Latest Groovy JAR SHA-256: 270288548f89b1abf747ed5c378c173dbae8756dd1caebc8a3b381a5beccfad5. The UI captures below verify the rendering fix at 78c3c1c0; the latest edge-state changes are covered by the updated tests.
  • CI for the published head is awaiting maintainer approval; no full green pipeline is claimed.
./gradlew :plugin-script-groovy:test --tests '*Trigger*' --console=plain
./gradlew :plugin-script-groovy:spotlessCheck :plugin-script-groovy:lintPluginDocs --console=plain
./gradlew :plugin-script-groovy:check :plugin-script-groovy:shadowJar --console=plain
./gradlew :plugin-script-node:check --max-workers=2 --console=plain
./gradlew check shadowJar --parallel --max-workers=2 --console=plain
git diff --check

Rendering UI evidence at 78c3c1c0 shows successful trigger values and downstream outputs. Screenshots 01–02 are historical and predate the rendering fix; screenshots 03–06 were captured with that commit’s JAR. No new UI capture is claimed for the later edge-state changes.

Evidence and QA flows.

ScriptTrigger values

Script downstream output

CommandsTrigger values

Commands downstream output

ScriptTrigger flow

id: groovy_script_workingdir
namespace: qa.groovy459final
tasks:
  - id: values
    type: io.kestra.plugin.core.debug.Return
    format: "path={{ trigger.vars.path }} usable={{ trigger.vars.usable }} flow={{ trigger.vars.flow }} exitCode={{ trigger.exitCode }}"
triggers:
  - id: poll
    type: io.kestra.plugin.scripts.groovy.ScriptTrigger
    interval: PT5S
    containerImage: groovy:jdk21
    exitCondition: exit 0
    script: |
      def dir = new File('{{ workingDir }}')
      assert dir.isAbsolute() && dir.isDirectory()
      def probe = new File(dir, 'probe.txt')
      probe.text = 'usable'
      assert probe.text == 'usable'
      println '::{"outputs":{"path":"{{ workingDir }}","usable":true,"flow":"{{ flow.id }}"}}::'

CommandsTrigger flow

id: groovy_commands_workingdir
namespace: qa.groovy459final
tasks:
  - id: values
    type: io.kestra.plugin.core.debug.Return
    format: "path={{ trigger.vars.path }} usable={{ trigger.vars.usable }} flow={{ trigger.vars.flow }} exitCode={{ trigger.exitCode }}"
triggers:
  - id: poll
    type: io.kestra.plugin.scripts.groovy.CommandsTrigger
    interval: PT5S
    containerImage: groovy:jdk21
    exitCondition: exit 0
    commands:
      - |
        groovy -e 'def dir = new File('"'"'{{ workingDir }}'"'"')
        assert dir.isAbsolute() && dir.isDirectory()
        def probe = new File(dir, '"'"'probe.txt'"'"')
        probe.text = '"'"'usable'"'"'
        assert probe.text == '"'"'usable'"'"'
        println '"'"'::{"outputs":{"path":"{{ workingDir }}","usable":true,"flow":"{{ flow.id }}"}}::'"'"'
        '

Setup Instructions

Build with Java 21 and the checked-in wrapper. Docker is required. Load the new Groovy shadow JAR into a compatible Kestra instance without another Groovy JAR, then create the two flows above. Disable the polling flows after QA. No API keys are required.

Dependencies/configuration belong in script/commands or the image; @Grab may require network access. Edge persistence and execution delivery are not transactional and do not guarantee exactly-once delivery across crashes.

Contributor Checklist ✅

@kestrabot kestrabot Bot added this to Pull Requests Oct 2, 2026
@github-project-automation github-project-automation Bot moved this to To review in Pull Requests Oct 2, 2026
@sushanttiwari-14
sushanttiwari-14 marked this pull request as ready for review October 2, 2026 22:19
@MilosPaunovic MilosPaunovic added the area/plugin Plugin-related issue or feature request label Oct 5, 2026
@MilosPaunovic
MilosPaunovic requested review from a team and jymaire October 5, 2026 08:01
@MilosPaunovic MilosPaunovic added the kind/external Pull requests raised by community contributors label Oct 5, 2026

@jymaire jymaire left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for this contribution, @sushanttiwari-14, and for the detailed QA write-up! The trigger model lines up well with the other language triggers: it uses the shared ExitConditionRegex helper, length-prefixed KV edge state, vars are kept on nonzero exits, and the condition tests cover case, whitespace, null, blank and invalid regex.

I found one functional issue, plus a few minor points:

  • [MEDIUM] ScriptTrigger and CommandsTrigger render script/commands before handing them to the task. That drops the task runner variables, so expressions like {{ workingDir }} that work in the Groovy tasks make the trigger fail silently. See the inline comments for a fix.
  • [LOW] The edge state is rewritten in KV on every poll. Writing only when it changes would avoid that.
  • [LOW] The docs don't say that beforeCommands/env are not available on the trigger.
  • [LOW] AGENTS.md lists each module's plugin classes, and the other trigger PRs (#447, #453, #454) added their triggers there. Could you add io.kestra.plugin.scripts.groovy.CommandsTrigger and io.kestra.plugin.scripts.groovy.ScriptTrigger under plugin-script-groovy?

I ran GroovyTriggerTest and GroovyTriggerConditionTest locally at 945e8df, and all tests passed.

@sushanttiwari-14

Copy link
Copy Markdown
Contributor Author

Pushed the review fixes in 70c47e2 and the additional invalid-input/literal-Property coverage in 78c3c1c. AGENTS.md now lists both Groovy trigger classes. The PR description includes complete workingDir reproducers and fresh, unedited UI evidence from the exact built JAR. All 70 trigger tests and all 85 Groovy tests pass, along with formatting, documentation lint, and the JAR build.

I investigated the original Ruby failure in run 37071337432, attempt 2: ScriptTest.script expected one matching stdout queue entry and got zero, although the process exit/stream-count assertions passed and both date strings appeared in the captured process logs. The unchanged test passed twice on the exact reviewed merge and twice on its unmodified base, including modest parallel Groovy load, and passed on the final feature tree. Kestra 1.3.39's bounded log wait and asynchronous queue delivery make timing/cancellation plausible, but the root cause remains unconfirmed. No Ruby source change or CI/dependency adjustment is included.

The full local repository check remains incomplete: its latest attempt encountered missing host python, then disk exhaustion and Docker read-only errors during other-language tests. The Groovy check had already passed separately. The new CI run is awaiting maintainer approval before jobs can start; please approve it when ready. No reviewer threads have been resolved.

@jymaire jymaire left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kestra Plugin Code Review

Business Requirements — met

Issue #425 asks for Groovy script and commands polling triggers with an exit-code or vars condition and edge mode. Both are delivered, consistent with the Lua trigger model.

Previous Review Follow-up — 4 of 4 addressed

Baseline: 945e8df (jymaire review 5412275190), compared with head 78c3c1c.

  • ✅ [MEDIUM] Pre-rendering of script/commands (ScriptTrigger.java, CommandsTrigger.java): this.script / this.commands are now passed unchanged to the embedded Script / Commands task, so {{ workingDir }} renders in the runner context. workingDirRendersInRunnerContext passes with real Docker (3/3).
  • ✅ [LOW] KV write on every poll (AbstractGroovyTrigger.java:86-91): store.put only runs when matched != lastMatched. writesOnlyTransitionsEvenWithEdgeDisabled verifies the write counts.
  • ✅ [LOW] Docs missing the unavailable beforeCommands/env/taskRunner: stated in both property @Schema descriptions and in io.kestra.plugin.scripts.groovy.md.
  • ✅ AGENTS.md: both trigger classes are listed under the groovy module.

The four earlier threads are resolved.

Kestra Guidelines — 1 finding

  • 🟡 AbstractGroovyTrigger.java:86 [Consistency] edge: false still reads and writes the KV store

Security (OWASP Top 10:2025 + KPS) — 0 findings

✅ No issues found. No secrets, shell construction or template injection beyond the standard task rendering; the regex condition goes through ExitConditionRegex (timeout plus substring fallback).

Performance — 1 finding

  • 🟡 AbstractGroovyTrigger.java:88 [Performance] Extra KV read per poll when edge is disabled (same point as above)

Compatibility with Kestra 1.3.x and 2.0.x: no core API changed in 2.0 is used (TriggerRunContext.forEmbeddedTask, namespaceKv, KVValueAndMetadata are the same as in the Lua trigger). GroovyTrigger* tests: 70/70 pass locally (1 worker, no daemon). CI (check / main) was still pending at review time.

Verdict: APPROVE (recommendation only; no remaining blocking finding, waiting for CI)

Skip KV access when edge mode is disabled and resume the stored baseline when enabled. Accept Boolean string representations and cover state transitions, outages, and mode changes.
Keep real Axios installation and HTTP execution without relying on Google connectivity. Bound fixture execution and cover HTTP rejection and dependency installation failures.
@sushanttiwari-14

Copy link
Copy Markdown
Contributor Author

Addressed both new KV findings in dad6fef: edge-disabled polling bypasses KV, and persisted Boolean/string values are handled consistently. Regression tests cover KV outages, state representations, mode changes and rearming; the docs describe the retained baseline when edge mode is re-enabled.

Fixed the latest CI failure in 1ee9aa7: the Node dependency test now makes a real Axios request to a bounded loopback HTTP fixture instead of Google. It still verifies npm installation, HTTP execution and structured output, with HTTP and installation failure coverage. The earlier Ruby log-count failure and this Node connection timeout were different failures; the Ruby cause remains unconfirmed.

Validation: 99/99 Groovy and 17/17 Node tests pass, along with documentation, JAR and relevant formatting checks. The full local check executed 244 tests: 243 passed, and one existing JBang test failed because its Kotlin compiler could not be downloaded or installed. The PR description records this limit and the evidence provenance.

The new CI run awaits maintainer approval. Please approve it when ready. No new reviewer threads have been resolved.

@jymaire jymaire left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kestra Plugin Code Review

Business Requirements — met

Groovy script and commands polling triggers with exit-code or vars condition and edge mode are delivered; the latest commits only refine edge-mode KV handling and tests.

Previous Review Follow-up — 2 of 2 addressed

Baseline: 78c3c1c, new head: 1ee9aa7.

  • ✅ [Consistency] edge: false still reads/writes KV (AbstractGroovyTrigger.java:87-91): evaluate now returns the fresh match before namespaceKv is touched; edgeDisabledDoesNotAccessKv verifies it with a throwing KV and never().
  • ✅ [Performance] strict Boolean check on the stored value (AbstractGroovyTrigger.java:96): now Boolean.parseBoolean(String.valueOf(value.value())); persistedBooleanAndStringStateControlsEdges covers Boolean and string states.

Both threads resolved after verifying the code on the current head.

Kestra Guidelines — 1 finding

  • 🟢 GroovyTriggerTest.java:305 [Guidelines] Fully qualified names in tests

Security (OWASP Top 10:2025 + KPS) — 0 findings

✅ No issues found. The delta to plugin-script-node is a test-only change (loopback HTTP fixture instead of google.com); no build, CI or .github files are touched.

Performance — 0 findings

✅ No issues found. Edge-disabled polls now skip the KV round trip entirely.

Compatibility with Kestra 1.3.x and 2.0.x (kestraVersion 1.3.39): no core API changed in 2.0 is used in the delta. CI (Main) was waiting for fork approval and has been approved; results are pending.

Verdict: APPROVE (recommendation only; no remaining blocking finding, waiting for CI)

@jymaire

jymaire commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

QA report: Groovy ScriptTrigger / CommandsTrigger (head 1ee9aa7)

Edition: OSS | Docker tags: v1.3.42 and v2.0.5 (plugin kestraVersion=1.3.39, so the same JAR ran on both lines, one instance at a time)
Instance: http://localhost:28459/ on container qa-scripts459-kestra-v205 (Kestra 2.0.5). It is left running for manual review; stop it with docker rm -f qa-scripts459-kestra-v205.
Screenshots: https://claude.ai/artifact/GpwC1KhjmD4tepN4mbbJ4k
Build: ./gradlew :plugin-script-groovy:shadowJar at head 1ee9aa7. Only plugin-script-groovy-1.13.1-SNAPSHOT.jar was mounted at /app/plugins. Task runner: default Docker runner (socket mounted, shared tmpDir), image groovy:latest (Groovy 5.1.3).

Verdict: ✅ QA PASS (no HIGH/MEDIUM). Two LOW notes are listed below.

# Scenario v1.3.42 v2.0.5
1 ScriptTrigger @Example, unmodified (defaults: PT60S, no image, no runner settings) ✅ fired, vars={status: ready} ✅ fired, vars={status: ready}
2 CommandsTrigger @Example, unmodified (exit 1) ✅ fired, exitCode=1 ✅ fired, exitCode=1
3 No fire when the condition does not match (regex miss + exit 3 vs exit 2), several minutes of polling ✅ 0 executions ✅ 0 executions
4 edge: true: KV toggle off→on→(stay on)→restart→off→on ✅ exactly 2 executions, one per false→true edge ✅ exactly 2 executions
5 edge: false: fires on every matching poll, no KV access ✅ 19 executions at PT5S, no KV key ✅ 27 executions, no KV key (GET → 404)
6 {{ workingDir }} rendering in a ScriptTrigger script (Docker runner) ✅ absolute runner dir, probe file written and read back ✅
7 {{ workingDir }} rendering in CommandsTrigger commands ✅ ✅
8 vars emitted before a non-zero exit (exit 5) still match a regex condition ✅ exitCode=5, vars={progress: partial} ✅
9 Invalid inputs: missing required props, bad interval, unsupported taskRunner ✅ rejected at save ✅ rejected at save
10 Invalid inputs found at runtime: edge: notabool, exitCondition: '', exit 99999999999, commands as a string, missing image ✅ never fires, clear error (see LOW-1) ✅ same
11 Non-regression: Groovy Eval, Script, Commands tasks ✅ SUCCESS ✅ SUCCESS
12 Docs for both triggers in the UI (properties, examples) ✅ ✅

The #460 trap (null targetOS forwarded from the trigger to the task) does not occur here. Both examples fire with all defaults on both versions.

Note on 2.0.5: the PT60S examples first fired about 61 s after the flow was created. On 1.3.42 the first poll ran about 4 s after creation. This comes from the core scheduler and does not depend on the plugin.


Flow 1: groovy_script_trigger (plugin example, ✅ SUCCESS)

Flow YAML
id: groovy_script_trigger
namespace: company.team

tasks:
  - id: log
    type: io.kestra.plugin.core.log.Log
    message: "Triggered with exitCode={{ trigger.exitCode }} (vars={{ trigger.vars }})"

triggers:
  - id: poll
    type: io.kestra.plugin.scripts.groovy.ScriptTrigger
    interval: PT60S
    exitCondition: "ready"
    script: |
      println '::{"outputs":{"status":"ready"}}::'

Gantt

Task Status Duration
log SUCCESS 0.02s
Total SUCCESS 0.08s

Trigger variables: condition: ready, exitCode: 0, vars: {status: ready}, timestamp set. There was one execution on each version. Edge mode (the default) did not re-fire on later polls.

Flow 2: groovy_commands_trigger (plugin example, ✅ SUCCESS)

Flow YAML
id: groovy_commands_trigger
namespace: company.team

tasks:
  - id: log
    type: io.kestra.plugin.core.log.Log
    message: "Triggered with exitCode={{ trigger.exitCode }}"

triggers:
  - id: poll
    type: io.kestra.plugin.scripts.groovy.CommandsTrigger
    interval: PT60S
    exitCondition: "exit 1"
    commands:
      - groovy -e "System.exit(1)"

Gantt (docs/example screenshot)

Task Status Duration
log SUCCESS 0.02s
Total SUCCESS 0.07s

Trigger variables: condition: exit 1, exitCode: 1, vars: {}.

Flow 3: groovy_nofire (✅ never fired, as expected)

Flow YAML
id: groovy_nofire
namespace: qa.groovy
tasks:
  - id: log
    type: io.kestra.plugin.core.log.Log
    message: "SHOULD NOT FIRE {{ trigger.vars }}"
triggers:
  - id: poll
    type: io.kestra.plugin.scripts.groovy.ScriptTrigger
    interval: PT5S
    exitCondition: "ready"
    script: |
      println '::{"outputs":{"status":"waiting"}}::'
  - id: poll_cmd
    type: io.kestra.plugin.scripts.groovy.CommandsTrigger
    interval: PT5S
    exitCondition: "exit 3"
    commands:
      - groovy -e "System.exit(2)"

Result: 0 executions on either version. No KV key was written, because state is only stored when it changes.

Flow 4: groovy_edge_true (✅ SUCCESS, edge semantics)

Flow YAML
id: groovy_edge_true
namespace: qa.groovy
tasks:
  - id: out
    type: io.kestra.plugin.core.debug.Return
    format: "state={{ trigger.vars.state }} exitCode={{ trigger.exitCode }} condition={{ trigger.condition }} ts={{ trigger.timestamp }}"
triggers:
  - id: poll
    type: io.kestra.plugin.scripts.groovy.ScriptTrigger
    interval: PT5S
    exitCondition: "state=on"
    script: |
      println '::{"outputs":{"state":"{{ kv('qa_toggle', errorOnMissing=false) ?? 'off' }}"}}::'

Sequence (same on both versions): the namespace KV qa_toggle was missing, so no fire. Setting it to on produced 1 execution. It stayed on for about 30 s with no new execution. docker restart of Kestra followed, and polls resumed with no re-fire (the edge key groovy_edge_16_groovy_edge_true_4_poll = BOOLEAN true survived). Setting off stored the edge key as false (revision 2). Setting on again produced 1 more execution. Total: 2 executions.

Gantt (executions, gantt)

Task Status Duration
out SUCCESS 0.03s
Total SUCCESS 0.06s

Outputs: out.value = state=on exitCode=0 condition=state=on ts=2026-10-09T12:30:45.128188Z. Trigger variables are shown in the execution overview (screenshot).

Flow 5: groovy_edge_false (✅ SUCCESS, fires on every matching poll)

Flow YAML
id: groovy_edge_false
namespace: qa.groovy
tasks:
  - id: out
    type: io.kestra.plugin.core.debug.Return
    format: "exitCode={{ trigger.exitCode }} vars={{ trigger.vars }}"
triggers:
  - id: poll
    type: io.kestra.plugin.scripts.groovy.CommandsTrigger
    interval: PT5S
    edge: false
    exitCondition: "exit 0"
    commands:
      - groovy -e "println 'tick'"

Result: one SUCCESS execution per poll. That gave 19 before disabling on 1.3.42 and 27 on 2.0.5, and the trigger kept firing across the restart. The KV store has no groovy_edge_17_groovy_edge_false_4_poll key on either version (KV screenshot, executions).

Flow 6: groovy_workdir (✅ SUCCESS, runner rendering in the script)

Flow YAML
id: groovy_workdir
namespace: qa.groovy
tasks:
  - id: out
    type: io.kestra.plugin.core.debug.Return
    format: "script path={{ trigger.vars.path }} usable={{ trigger.vars.usable }}"
triggers:
  - id: poll
    type: io.kestra.plugin.scripts.groovy.ScriptTrigger
    interval: PT10S
    containerImage: groovy:latest
    exitCondition: "usable"
    script: |
      def dir = new File('{{ workingDir }}')
      assert dir.isAbsolute() && dir.isDirectory()
      def probe = new File(dir, 'probe.txt')
      probe.text = 'usable'
      println '::{"outputs":{"path":"' + dir.absolutePath + '","usable":"' + probe.text + '","flow":"{{ flow.id }}"}}::'

Outputs (screenshot): script path=<tmpDir>/5Ik6fOVmExSqSwLJmsu6nj usable=usable. {{ workingDir }} and {{ flow.id }} rendered inside the Docker container.

Flow 7: groovy_workdir_cmd (✅ SUCCESS, runner rendering in commands)

Flow YAML
id: groovy_workdir_cmd
namespace: qa.groovy
tasks:
  - id: out
    type: io.kestra.plugin.core.debug.Return
    format: "commands vars={{ trigger.vars }} exit={{ trigger.exitCode }}"
triggers:
  - id: poll
    type: io.kestra.plugin.scripts.groovy.CommandsTrigger
    interval: PT10S
    exitCondition: "exit 0"
    commands:
      - echo 'println "::{\"outputs\":{\"wd\":\"" + new File(".").canonicalPath + "\"}}::"' > {{ workingDir }}/probe.groovy
      - groovy {{ workingDir }}/probe.groovy

Outputs (screenshot): commands vars={"wd":"<tmpDir>/6y2Y3cNERcvZ8xkA1adNBM"} exit=0.

Flow 8: groovy_vars_on_failure (✅ SUCCESS)

Flow YAML
id: groovy_vars_on_failure
namespace: qa.groovy
tasks:
  - id: out
    type: io.kestra.plugin.core.debug.Return
    format: "exit={{ trigger.exitCode }} vars={{ trigger.vars }}"
triggers:
  - id: poll
    type: io.kestra.plugin.scripts.groovy.ScriptTrigger
    interval: PT10S
    exitCondition: "partial"
    script: |
      println '::{"outputs":{"progress":"partial"}}::'
      System.exit(5)

Outputs (screenshot): exit=5 vars={"progress":"partial"}. The vars emitted before the process failure are kept and matched.

Flow 9: invalid inputs (✅ rejected)

Each check used a one-trigger flow (groovy_invalid / inv_*, namespace qa.groovy, a single Log task, interval: PT5S).

Input Result (both versions)
no exitCondition save rejected: triggers[0].exitCondition: must not be null
ScriptTrigger without script save rejected: triggers[0].script: must not be null
CommandsTrigger without commands save rejected: triggers[0].commands: must not be null
interval: abc save rejected: Text cannot be parsed to a Duration
taskRunner: on the trigger save rejected: Unrecognized field "taskRunner" (matches the docs: not exposed)
edge: notabool saves, as with any Property<Boolean>. Every poll fails with ERROR Unrecognized token 'notabool', never fires
exitCondition: '' ERROR exitCondition must not be empty; set 'exit N' or a regex against vars, never fires
exitCondition: exit 99999999999 ERROR exitCondition exit code must be between 0 and 2147483647, never fires (screenshot)
exitCondition: 'a[b' (invalid regex) WARN not a valid regex; falling back to a substring match, fires on substring (documented)
commands: groovy -v (string, not list) WARN only, never fires. See LOW-1
containerImage: <missing image> WARN only, never fires. See LOW-1

Flow 10: groovy_tasks_nonreg (✅ SUCCESS, non-regression)

Flow YAML
id: groovy_tasks_nonreg
namespace: qa.groovy
tasks:
  - id: eval
    type: io.kestra.plugin.scripts.groovy.Eval
    outputs:
      - total
    script: |
      total = (1..10).sum()
      logger.info("Eval total {}", total)
  - id: script
    type: io.kestra.plugin.scripts.groovy.Script
    script: |
      println "hello from {{ flow.id }} in " + new File('{{ workingDir }}').absolutePath
      println '::{"outputs":{"answer":42}}::'
  - id: commands
    type: io.kestra.plugin.scripts.groovy.Commands
    commands:
      - groovy -e 'println "::{\"outputs\":{\"v\":\"" + GroovySystem.version + "\"}}::"'
  - id: show
    type: io.kestra.plugin.core.debug.Return
    format: "eval={{ outputs.eval.outputs.total }} script={{ outputs.script.vars.answer }} commands={{ outputs.commands.vars.v }}"

Gantt (screenshot, v2.0.5)

Task Status Duration
eval SUCCESS 0.67s
script SUCCESS 1.84s
commands SUCCESS 1.69s
show SUCCESS 0.02s
Total SUCCESS 4.39s

Outputs: show.value = eval=55 script=42 commands=5.1.3 on both versions.


LOW notes (non-blocking)

  • LOW-1: the cause of a swallowed task failure is hidden at the default log level. AbstractGroovyTrigger.java:82 catches every task exception and logs warn("Groovy trigger could not execute its task; check the container image and task runner", e). Kestra writes the attached throwable as a separate TRACE (1.3) or DEBUG (2.0) entry. At the default INFO filter, users only see the generic line, and it points to a task runner the trigger does not expose. Example: commands: groovy -v (string) hides JsonParseException: Unrecognized token 'groovy'. Including e.getMessage() in the WARN text would make it actionable (screenshot).
  • LOW-2 (maintainer decision): because that catch returns Optional.empty(), failOnTriggerError has no effect on task-level failures (image pull errors, bad commands). Errors in condition validation and in rendering edge do propagate.
  • Triggers always use the default Docker runner. taskRunner is intentionally not exposed, so the Process runner cannot be tested for the triggers.

Timeout notes

No scenario hit the 30 s timeout. On 2.0.5 the unmodified PT60S examples fired about 61 s after creation because the core scheduler waits one interval before the first poll; 1.3.42 polls immediately.

@jymaire jymaire left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All good for me, thanks for the contribution!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/plugin Plugin-related issue or feature request kind/external Pull requests raised by community contributors

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Introduce ScriptTrigger & CommandsTrigger for the Groovy plugin

3 participants