Repository navigation
feat(php): add ScriptTrigger and CommandsTrigger - #457
Conversation
Mirror the dotnet/perl polling trigger pattern for the PHP plugin: KV-backed edge mode, exitCondition validation, and integration tests.
fdelbrayelle
left a comment
There was a problem hiding this comment.
Review: request changes
No blockers, no security findings. Builds and tests were not run in this review.
Must fix
- Errors swallowed (
ScriptTrigger.java:484-487,CommandsTrigger.java:170-173).catch (Exception e)also wraps theexitConditionrender check. A bad config, Docker outage or render error is only logged at WARN, and the trigger never fires. Edge state is not advanced on a failed poll. Fix: render and validateexitConditionbefore the try, and narrow the catch to execution failures. - Runner options missing (
ScriptTrigger.java:530-536,CommandsTrigger.java:217-222). The inner task is built with onlycontainerImageand the script or commands. NotaskRunner,env,beforeCommands,inputFilesornamespaceFiles. Fix: expose them, or document the limit in@Schema. - Comment violations. Multi-line blocks in both triggers and in
EdgeStateTest,ScriptTriggerEvaluateTest,ScriptTriggerTestandCommandsTriggerTest. Limit is one short line, only when the why is non-obvious. - Test gaps. No tests for blank
exitCondition, the swallow path,edge=falseviaevaluate, orCommandsTrigger.matchesCondition. Docker-backed tests were not run by the author. Tests use explicit types instead ofvar.
Suggestions
ScriptTriggerandCommandsTriggerare near-copies (same in dotnet and perl). Consider an abstract base, maybe as a repo-wide follow-up.safeExitCodeandsafeVarstry/catch is unneeded. Call the getters directly.Integer.parseInton a huge exit code throwsNumberFormatExceptionoutside the try. UseLongor catch it.- Regex runs on
vars.toString(), which is fragile. Compile once. Output.varsmay hold sensitive data, so note it in the schema.conditionis a redundant echo.@Schemadescriptions are inconsistent (single-line strings, no""").
Nits
AGENTS.md: missing blank line before the powershell block.- Unsorted imports in
ScriptTrigger. - PR says "23/23 passed". Note the Docker tests are unverified.
Validate exitCondition before execution, narrow swallowed errors, document runner limits, trim comments, and extend trigger unit tests.
|
Pushed 8befe84 addressing the review:
Non-Docker PHP trigger tests: 31/31 passed locally. Docker-backed tests were not run on this machine. |
fdelbrayelle
left a comment
There was a problem hiding this comment.
Review of the PHP ScriptTrigger and CommandsTrigger at head 8befe84f. No security issue found: regex matching goes through ExitConditionRegex.find (1s deadline, substring fallback) and the exit N pattern is linear. 11 of 21 findings from the previous review are addressed. Remaining items:
Should fix before merge
- Broad catch still swallows infrastructure errors (
ScriptTrigger.java:157-159,CommandsTrigger.java:155-158). This was previous finding M1b and is only partly fixed. TheexitConditionrender now happens before the try, but Docker or runner failures are still only logged at WARN, so the trigger never fires and edge state is never reset. Narrow the catch to task execution failures and rethrow infrastructure errors, or at least log that the state was kept. - Vacuous failure test (
ScriptTriggerEvaluateTest.java:62,evaluate_executionFailure_doesNotAdvanceEdgeState). It asserts only insideif (execution.isEmpty()), so it cannot fail. The generic-exception swallow path has no test. Assert unconditionally, for example that the call returns empty and the KV key is absent. - Docker-gated tests pass silently (
ScriptTriggerEvaluateTest.java:77,99,114,130). They useif (!dockerAvailable()) return;, so they report green without running.CommandsTriggerTesthas no guard and fails without Docker. UseAssumptions.assumeTrue(dockerAvailable())so skips are visible, or a runner CI can execute. - Package doc not updated (
plugin-script-php/src/main/resources/doc/io.kestra.plugin.scripts.php.md). Add a## Triggerssection (dotnet has one at line 48) coveringexitCondition(exit N, otherwise regex with substring fallback matched against vars), edge mode and its KV state, the 60s default interval, and that a failed run has no vars. Also fix the "Common properties" text that saystaskRunneris configurable, since the triggers do not expose it.
Worth considering
- Edge state written before the execution exists (
ScriptTrigger.java:173-190,CommandsTrigger.java:171-190). IfgenerateExecutionthrows aftershouldEmitwrote the KV state, the next poll is suppressed and the event is lost. Write the state aftergenerateExecutionsucceeds, or document the trade-off. - Issue #430 is only partly covered. There is no
commandPattern,argumentsPatternorexitCode != 0, and the output is onlytimestamp,condition,exitCodeandvars. This matches dotnet and perl, so please state the deviation in the PR description. - Duplicated logic (
shouldEmit,edgeStateKey,matchesCondition,extractFailure,Output).CommandsTrigger.java:195calls the staticScriptTrigger.renderExitCondition, which couples the two classes. An abstract base, or a repo-wide follow-up, would fix this. Output.conditionis a redundant echo (ScriptTrigger.java:276).- Missing cases in
CommandsTriggerConditionTest:EXIT 1, null condition and no-match, whichScriptTriggerTesthas.
Nits
- Use
varfor locals in main code and in tests (for exampleCommandsTriggerTest.java:23,ScriptTriggerEvaluateTest.java:42). - Single-line
@Schemastrings remain:CommandsTrigger.java:102(commands) and theOutput.exitCodedescriptions in both triggers. - The comment at
EdgeStateTest.java:173restates the assertion. Remove it or shorten it.
Rethrow infrastructure errors from triggers, harden evaluate tests, document triggers in package doc, and use visible Docker skips.
|
Pushed 464737f for the four should-fix items from the second review:
PR description updated for the #430 scope note vs dotnet/perl. |
fdelbrayelle
left a comment
There was a problem hiding this comment.
Kestra Plugin Code Review
Business Requirements — partial
Issue #430 asks for PHP polling triggers. ScriptTrigger and CommandsTrigger are delivered and mirror dotnet/perl. commandPattern, argumentsPattern and exitCode != 0 are not implemented; the PR description now states this deviation.
Previous Review Follow-up — 5 of 9 addressed
Baseline: 8befe84f (review by a kestra-io member). Head: 464737fd. The earlier review had no inline threads, so none were resolved.
- ✅ Should-fix 1, broad catch: removed in both triggers; only
RunnableTaskExceptionis caught (ScriptTrigger.java:209,CommandsTrigger.java:201), infrastructure errors propagate. - ✅ Should-fix 2, vacuous failure test:
evaluate_executionFailure_doesNotAdvanceEdgeStatenow asserts unconditionally (ScriptTriggerEvaluateTest.java:78-79); infra path covered at line 83. - ✅ Should-fix 3, silent Docker skips:
assumeTrueused (ScriptTriggerEvaluateTest.java:61,97,118,135,147). - ✅ Should-fix 4, package doc:
## Triggersadded andtaskRunnerwording fixed. - ✅ #6, #430 deviation: stated in the PR description.
- ❌ #5, edge state written before the execution exists:
ScriptTrigger.java:155-161,CommandsTrigger.java:153-159. - ❌ #7, duplicated logic and static cross-class call:
CommandsTrigger.java:186. - ❌ #8, redundant
Output.condition:ScriptTrigger.java:271. - ❌ #9, missing
CommandsTriggerConditionTestcases:CommandsTriggerConditionTest.java:24-33. - ❌ Nits,
varand single-line@Schema: still present (see below).
Kestra Guidelines — 5 findings
- 🟡
ScriptTrigger.java:161[Guidelines] Edge state written before execution is generated - 🟡
CommandsTrigger.java:186[Guidelines] Duplicated trigger logic and cross-class static call - 🟡
ScriptTrigger.java:271[Guidelines] Redundantconditionoutput - 🟡
ScriptTrigger.java:149[Guidelines] Explicit local types instead ofvar - 🟢
CommandsTrigger.java:102[Guidelines] Single-line@Schemadescriptions
Test gap: CommandsTriggerConditionTest.java:31 [Tests] is covered as an inline 🟡 comment.
Security (OWASP Top 10:2025 + KPS) — 0 findings
✅ No issues found. Regex matching goes through ExitConditionRegex.find (bounded), the exit N pattern is linear, and Output.vars carries a sensitivity note in the schema.
Performance — 0 findings
✅ No issues found. EXIT_CONDITION_PATTERN is a static final constant; one container run per poll is inherent to the design.
Verdict: APPROVE
| return Optional.empty(); | ||
| } | ||
|
|
||
| return Optional.of(TriggerService.generateExecution(this, conditionContext, context, out)); |
There was a problem hiding this comment.
🟡 [Guidelines] Edge state written before execution is generated
Problem: shouldEmit persists the new KV state at line 155, then generateExecution runs here; if it throws, the next poll sees previouslyMatched=true and the event is lost (same in CommandsTrigger.java:153-159).
Fix: Compute emit without writing, call generateExecution, then write the KV state; or document the trade-off in the class description.
| @Override | ||
| public Optional<Execution> evaluate(ConditionContext conditionContext, TriggerContext context) throws Exception { | ||
| RunContext runContext = conditionContext.getRunContext(); | ||
| boolean renderedEdge = runContext.render(this.edge).as(Boolean.class).orElse(true); |
There was a problem hiding this comment.
🟡 [Guidelines] Explicit local types instead of var
Problem: The guidelines require var for all locals; main code still uses explicit types (RunContext, boolean, Output, String, KVStore, Script, Matcher at lines 148-155, 169-170, 172, 198, 216, 218), and the same in CommandsTrigger and the tests (CommandsTriggerTest.java:32, ScriptTriggerEvaluateTest.java:46,63,73).
Fix: Replace the explicit local types with var in both triggers and the new tests.
| title = "Rendered condition", | ||
| description = "Rendered value of the exitCondition property for this poll." | ||
| ) | ||
| private String condition; |
There was a problem hiding this comment.
🟡 [Guidelines] Redundant condition output
Problem: Output.condition only echoes the exitCondition property the user already wrote, which the Outputs Contract says to avoid unless needed downstream (same in CommandsTrigger.java:263).
Fix: Drop condition from both Output classes and from the example message, or keep it and justify it in the PR.
| } | ||
|
|
||
| private String renderExitCondition(RunContext runContext) throws IllegalVariableEvaluationException { | ||
| return ScriptTrigger.renderExitCondition(runContext, this.exitCondition); |
There was a problem hiding this comment.
🟡 [Guidelines] Duplicated trigger logic and cross-class static call
Problem: shouldEmit, edgeStateKey, matchesCondition, extractFailure and Output are copied between both triggers, and CommandsTrigger depends on the static ScriptTrigger.renderExitCondition, so changing one class silently changes the other. This follows dotnet/perl, so it is not blocking.
Fix: Extract an AbstractPhpTrigger (or a shared helper) holding the edge, condition and render logic, or open a repo-wide follow-up for all language triggers.
|
|
||
| @Schema( | ||
| title = "PHP commands to execute", | ||
| description = "Commands executed on each poll (same semantics as the PHP Commands task)." |
There was a problem hiding this comment.
🟢 [Guidelines] Single-line @Schema descriptions
Problem: The commands, interval (line 126) and both Output.exitCode descriptions (CommandsTrigger.java:267, ScriptTrigger.java:275) use single-line strings instead of """ blocks.
Fix: Convert them to text blocks like the other descriptions in the class.
| "exit 1, 1, true", | ||
| "exit 1, 0, false", | ||
| }) | ||
| void exitCodeCondition(String condition, int exitCode, boolean expected) { |
There was a problem hiding this comment.
🟡 [Tests] Missing condition cases for CommandsTrigger.matchesCondition
Problem: Only exit 0 and exit 1 are covered; case-insensitive EXIT 1, null condition, null exit code, and a no-match regex are not, while ScriptTriggerTest has them.
Fix: Add those cases to exitCodeCondition and add null-condition and no-match tests.
|
The four should-fix items from the 5 Oct review are in 464737f. CI on that commit is green. Nothing new has been pushed since. |
fdelbrayelle
left a comment
There was a problem hiding this comment.
Automated review by the kestra-plugins:code-reviewer agent, run on behalf of @fdelbrayelle. Static read of the diff, the build and tests were not run.
VERDICT: APPROVE
No new commits since your last review. The head is still 464737fd, so I only re-checked the unresolved findings against it.
Blocking: none. All four earlier should-fix items are addressed, and I confirmed the narrowed catch in ScriptTrigger.java:209. I did not re-check the other three. Your last review's own follow-up says they are fixed, and I did not run the code or tests.
Non-blocking notes (all still open):
ScriptTrigger.java:155-161andCommandsTrigger.java:153-159:shouldEmitwrites the edge KV state beforegenerateExecution. IfgenerateExecutionthrows, the event is lost. Write the state after it succeeds, or document the trade-off.CommandsTrigger.java:186andScriptTrigger.java:271: the two triggers duplicate the edge, condition and render logic, andCommandsTriggercalls the staticScriptTrigger.renderExitCondition.Output.conditionis also a redundant echo ofexitCondition. This matches the dotnet and perl triggers.- Style and tests:
CommandsTriggerConditionTestlacks theEXIT 1, null-condition and no-match cases. Main code and tests still use explicit local types instead ofvar. Several@Schemadescriptions are still single-line strings instead of"""blocks.
fdelbrayelle
left a comment
There was a problem hiding this comment.
Approved. See my earlier review comment for non-blocking suggestions.
…nds-triggers # Conflicts: # AGENTS.md
|
Edition: OSS | Docker tag: develop (next: 2.1.0-SNAPSHOT) Tested merged state Summary
Flow 1: php_script_trigger (✅ SUCCESS)Flow YAMLid: php_script_trigger
namespace: qa.php
triggers:
- id: script_failure
type: io.kestra.plugin.scripts.php.ScriptTrigger
interval: PT10S
exitCondition: "exit 1"
edge: true
containerImage: php
script: |
<?php
exit(1);
tasks:
- id: log
type: io.kestra.plugin.core.log.Log
message: "Triggered with exitCode={{ trigger.exitCode }} (condition={{ trigger.condition }})"Gantt (screenshot)
Logs synthesis (screenshot): fired on the first poll. Outputs synthesis: trigger vars Flow 2: php_commands_trigger (✅ SUCCESS)Flow YAMLid: php_commands_trigger
namespace: qa.php
triggers:
- id: commands_failure
type: io.kestra.plugin.scripts.php.CommandsTrigger
interval: PT10S
exitCondition: "exit 1"
edge: true
containerImage: php
commands:
- php -r 'exit(1);'
tasks:
- id: log
type: io.kestra.plugin.core.log.Log
message: "Triggered with exitCode={{ trigger.exitCode }} (condition={{ trigger.condition }})"Gantt (screenshot)
Logs synthesis (screenshot): same result as flow 1, Flow 3 and 4: edge and negativeSame YAML as flow 1 (edge) and a variant with Flow 5: php_trigger_blank_condition (
|
| Task | Status | Duration |
|---|---|---|
script |
SUCCESS | 16.04s (image pull) |
commands |
SUCCESS | 0.85s |
| Total | SUCCESS | 17.00s |
Logs synthesis (screenshot): hello script and hello commands, both exit code 0.
Outputs synthesis: no outputs.
Timeouts: none. Topology: skipped (develop is not a 2.0 tag, no UI or artifact change in this PR).
What
Closes #430.
Adds polling triggers for the PHP plugin:
io.kestra.plugin.scripts.php.ScriptTrigger— runs the inlineScripttask on each poll and emits whenexitConditionmatches.io.kestra.plugin.scripts.php.CommandsTrigger— same for theCommandstask.Both follow the dotnet/perl polling trigger pattern (namespace KV edge mode,
ExitConditionRegex, blankexitConditionvalidation).Issue #430 also mentions
commandPattern,argumentsPattern, and PebbleexitCode != 0; those are not implemented because no sibling trigger (dotnet, perl) has them. Trigger output istimestamp,condition,exitCode, andvars.Also updates
AGENTS.mdandplugin-script-phppackage docs.How tested
Locally (no Docker socket):
ScriptTriggerTest,CommandsTriggerConditionTest,EdgeStateTest, plus non-DockerScriptTriggerEvaluateTestcases) — 31/31 passed.ScriptTriggerEvaluateTestandCommandsTriggerTest— 9 skipped (visible via JUnit assumptions), not run as passes.Docker-backed
ScriptTest,CommandsTest, andRunnerTestwere not run locally.