Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

### Architecture

This is a **multi-module** plugin with 19 submodules:
This is a **multi-module** plugin with 20 submodules:

- `plugin-script`
- `plugin-script-bun`
Expand All @@ -36,6 +36,7 @@ This is a **multi-module** plugin with 19 submodules:
- `plugin-script-r`
- `plugin-script-ruby`
- `plugin-script-shell`
- `plugin-script-zx`

### Key Plugin Classes

Expand Down Expand Up @@ -137,6 +138,11 @@ This is a **multi-module** plugin with 19 submodules:
- `io.kestra.plugin.scripts.shell.Script`
- `io.kestra.plugin.scripts.shell.ScriptTrigger`

**plugin-script-zx:**

- `io.kestra.plugin.scripts.zx.Commands`
- `io.kestra.plugin.scripts.zx.Script`

### Project Structure

```
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
* [for R](https://kestra.io/plugins/plugin-script-r/)
* [for Ruby](https://kestra.io/plugins/plugin-script-ruby/)
* [for Shell](https://kestra.io/plugins/plugin-script-shell/).
* [for zx](https://kestra.io/plugins/plugin-script-zx/).


## License
Expand Down
22 changes: 22 additions & 0 deletions plugin-script-zx/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
project.description = 'Execute zx scripts as part of Kestra data workflows.'

jar {
manifest {
attributes(
"X-Kestra-Name": project.name,
"X-Kestra-Title": "zx",
"X-Kestra-Group": project.group + ".scripts.zx",
"X-Kestra-Description": project.description,
"X-Kestra-Version": project.version
)
}
}

dependencies {
implementation project(':plugin-script')

testImplementation project(path: ':plugin-script', configuration: 'testOutput')

testImplementation group: "io.kestra", name: "scheduler", version: kestraVersion
testImplementation group: "io.kestra", name: "worker", version: kestraVersion
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package io.kestra.plugin.scripts.zx;

import java.util.List;

import io.kestra.core.exceptions.IllegalVariableEvaluationException;
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.property.Property;
import io.kestra.core.models.tasks.RunnableTask;
import io.kestra.core.models.tasks.runners.TargetOS;
import io.kestra.core.runners.RunContext;
import io.kestra.plugin.scripts.exec.AbstractExecScript;
import io.kestra.plugin.scripts.exec.scripts.models.DockerOptions;
import io.kestra.plugin.scripts.exec.scripts.models.ScriptOutput;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.*;
import lombok.experimental.SuperBuilder;

@SuperBuilder
@ToString
@EqualsAndHashCode
@Getter
@NoArgsConstructor
@Schema(
title = "Execute zx files and commands",
description = "Runs JavaScript shell automation with zx. The default image includes zx at /zx/build/cli.js; custom images and Process runners require Node.js, zx and a supported shell."
)
@Plugin(
examples = {
@Example(
full = true,
title = "Run shell commands with zx",
code = """
id: zx_commands
namespace: company.team

tasks:
- id: zx
type: io.kestra.plugin.scripts.zx.Commands
inputFiles:
main.mjs: |
await $`echo Hello Kestra`;
commands:
- node /zx/build/cli.js main.mjs
"""
)
}
)
public class Commands extends AbstractExecScript implements RunnableTask<ScriptOutput> {
private static final String DEFAULT_IMAGE = "ghcr.io/google/zx:8.5.0";

@Schema(
title = "Container image for zx runtime",
description = "Docker image used to run the commands; defaults to 'ghcr.io/google/zx:8.5.0'. Custom images must provide Node.js, zx and a supported shell."
)
@Builder.Default
@PluginProperty(group = "execution")
protected Property<String> containerImage = Property.ofValue(DEFAULT_IMAGE);

@Schema(
title = "Commands to execute",
description = "List of shell commands, such as `node /zx/build/cli.js main.mjs` in the default image, executed in order inside the container; combine with beforeCommands for npm install and inputFiles/namespaceFiles to stage code."
)
@NotNull
@PluginProperty(group = "main")
protected Property<List<String>> commands;

@Override
protected DockerOptions injectDefaults(RunContext runContext, DockerOptions original) throws IllegalVariableEvaluationException {
var builder = original.toBuilder();
if (original.getImage() == null) {
builder.image(runContext.render(this.getContainerImage()).as(String.class).orElse(null));
}

return builder.build();
}

@Override
public ScriptOutput run(RunContext runContext) throws Exception {
var rTargetOS = runContext.render(this.targetOS).as(TargetOS.class).orElse(null);

return this.commands(runContext)
.withInterpreter(this.interpreter)
.withBeforeCommands(beforeCommands)
.withBeforeCommandsWithOptions(true)
.withCommands(commands)
.withTargetOS(rTargetOS)
.run();
}
}
122 changes: 122 additions & 0 deletions plugin-script-zx/src/main/java/io/kestra/plugin/scripts/zx/Script.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package io.kestra.plugin.scripts.zx;

import java.util.List;

import io.kestra.core.exceptions.IllegalVariableEvaluationException;
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.enums.MonacoLanguages;
import io.kestra.core.models.property.Property;
import io.kestra.core.models.tasks.RunnableTask;
import io.kestra.core.models.tasks.runners.TargetOS;
import io.kestra.core.runners.FilesService;
import io.kestra.core.runners.RunContext;
import io.kestra.plugin.scripts.exec.AbstractExecScript;
import io.kestra.plugin.scripts.exec.scripts.models.DockerOptions;
import io.kestra.plugin.scripts.exec.scripts.models.ScriptOutput;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.*;
import lombok.experimental.SuperBuilder;

@SuperBuilder
@ToString
@EqualsAndHashCode
@Getter
@NoArgsConstructor
@Schema(
title = "Run inline zx script",
description = "Runs JavaScript shell automation with zx. The default image includes zx; custom images and Process runners require Node.js, zx and a supported shell."
)
@Plugin(
examples = {
@Example(
full = true,
title = "Run shell commands with zx",
code = """
id: zx_script
namespace: company.team

tasks:
- id: zx
type: io.kestra.plugin.scripts.zx.Script
script: |
const name = 'Kestra';
await $`echo Hello ${name}`;
"""
)
}
)
public class Script extends AbstractExecScript implements RunnableTask<ScriptOutput> {
private static final String DEFAULT_IMAGE = "ghcr.io/google/zx:8.5.0";
private static final String DEFAULT_EXECUTABLE = "node /zx/build/cli.js";

@Schema(
title = "Container image for zx runtime",
description = "Docker image used to run the script; defaults to 'ghcr.io/google/zx:8.5.0'. Custom images must provide Node.js, zx and a supported shell."
)
@Builder.Default
@PluginProperty(group = "execution")
protected Property<String> containerImage = Property.ofValue(DEFAULT_IMAGE);

@Schema(
title = "Inline zx script",
description = "JavaScript source as a multi-line string; saved to a temporary .mjs file and executed with the configured zx command. For existing files, use the Commands task."
)
@NotNull
@PluginProperty(language = MonacoLanguages.JAVASCRIPT, group = "main")
protected Property<String> script;

@Schema(
title = "Command used to launch zx",
description = "Defaults to `node /zx/build/cli.js` for the bundled CLI in the default image. Set to `zx` when using a custom image or Process runner with zx installed on PATH."
)
@Builder.Default
@NotNull
@PluginProperty(group = "execution")
protected Property<String> executable = Property.ofValue(DEFAULT_EXECUTABLE);

@Override
protected DockerOptions injectDefaults(RunContext runContext, DockerOptions original) throws IllegalVariableEvaluationException {
var builder = original.toBuilder();
if (original.getImage() == null) {
builder.image(runContext.render(this.getContainerImage()).as(String.class).orElse(null));
}

return builder.build();
}

@Override
public ScriptOutput run(RunContext runContext) throws Exception {
var commands = this.commands(runContext);

var inputFiles = FilesService.inputFiles(runContext, commands.getTaskRunner().additionalVars(runContext, commands), this.getInputFiles());
var relativeScriptPath = runContext.workingDir().path().relativize(runContext.workingDir().createTempFile(".mjs"));
inputFiles.put(
relativeScriptPath.toString(),
commands.render(runContext, this.script)
);
commands = commands.withInputFiles(inputFiles);

var rTargetOS = runContext.render(this.targetOS).as(TargetOS.class).orElse(null);
var rExecutable = runContext.render(this.executable).as(String.class).orElse(DEFAULT_EXECUTABLE);
return commands
.withInterpreter(this.interpreter)
.withBeforeCommands(beforeCommands)
.withBeforeCommandsWithOptions(true)
.withCommands(
Property.ofValue(
List.of(
String.join(
" ", rExecutable,
commands.getTaskRunner().toAbsolutePath(runContext, commands, relativeScriptPath.toString(), rTargetOS)
)
)
)
)
.withTargetOS(rTargetOS)
.run();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
@PluginSubGroup(
title = "zx",
description = "Tasks for running JavaScript shell automation with zx.",
categories = { PluginSubGroup.PluginCategory.CORE }
)
package io.kestra.plugin.scripts.zx;

import io.kestra.core.models.annotations.PluginSubGroup;
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# How to use the zx plugin

Run JavaScript shell automation with zx, combining JavaScript logic with shell commands through the `$` helper.

## Common properties

`containerImage` defaults to `ghcr.io/google/zx:8.5.0`, which includes Node.js, zx, and Bash. `taskRunner` defaults to Docker.

The default image is AMD64. ARM64 workers need emulation or an ARM64-compatible custom image.

For a custom image or Process runner with zx installed on PATH, set `executable: zx` on `Script`. Process execution requires Node.js, zx, and a supported shell on the worker.

## Tasks

`Script` runs inline JavaScript from the `script` property and supports top-level `await`. `Commands` runs existing files staged through `inputFiles` or [namespace files](https://kestra.io/docs/concepts/namespace-files); in the default image, use `node /zx/build/cli.js main.mjs`.

Use `beforeCommands` to install additional packages, `env` for environment variables, and `outputFiles` to persist generated files. See the individual task pages for complete flow examples.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions plugin-script-zx/src/main/resources/icons/plugin-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions plugin-script-zx/src/main/resources/metadata/index.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
group: io.kestra.plugin.scripts.zx
name: "plugin-zx"
title: "zx"
description: "Tasks that run zx scripts."
body: "Run JavaScript shell automation inline or from existing files using zx, capturing logs and output files for downstream tasks. The default container includes Node.js, zx, and Bash."
createdBy: "Kestra Core Team"
managedBy: "Kestra Core Team"
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package io.kestra.plugin.scripts.zx;

import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;

import org.junit.jupiter.api.Test;

import com.google.common.collect.ImmutableMap;

import io.kestra.core.junit.annotations.KestraTest;
import io.kestra.core.models.executions.LogEntry;
import io.kestra.core.models.property.Property;
import io.kestra.core.queues.QueueFactoryInterface;
import io.kestra.core.queues.QueueInterface;
import io.kestra.core.runners.RunContext;
import io.kestra.core.runners.RunContextFactory;
import io.kestra.core.utils.TestsUtils;
import io.kestra.plugin.scripts.exec.scripts.models.ScriptOutput;

import jakarta.inject.Inject;
import jakarta.inject.Named;
import reactor.core.publisher.Flux;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;

@KestraTest
class CommandsTest {
@Inject
RunContextFactory runContextFactory;

@Inject
@Named(QueueFactoryInterface.WORKERTASKLOG_NAMED)
private QueueInterface<LogEntry> logQueue;

@Test
void executesStagedScript() throws Exception {
List<LogEntry> logs = new CopyOnWriteArrayList<>();
Flux<LogEntry> receive = TestsUtils.receive(logQueue, l -> logs.add(l.getLeft()));

Commands zxCommands = Commands.builder()
.id("zx-commands-" + UUID.randomUUID())
.type(Commands.class.getName())
.inputFiles(Map.of("main.mjs", "$.verbose = false; console.log((await $`echo hello there!`).stdout.trim());"))
.commands(Property.ofValue(List.of("node /zx/build/cli.js main.mjs")))
.build();

RunContext runContext = TestsUtils.mockRunContext(runContextFactory, zxCommands, ImmutableMap.of());
ScriptOutput run = zxCommands.run(runContext);

assertThat(run.getExitCode(), is(0));
assertThat(run.getStdOutLineCount(), is(1));
assertThat(run.getStdErrLineCount(), is(0));

TestsUtils.awaitLog(logs, log -> log.getMessage() != null && log.getMessage().contains("hello there!"));
receive.blockLast();
assertThat(List.copyOf(logs).stream().filter(logEntry -> logEntry.getMessage() != null && logEntry.getMessage().contains("hello there!")).count(), is(1L));
}
}
Loading