Skip to content

Plugins

Saturn allows users to implement their own filter and processing logic. It is also possible to react to actions of Saturn, like creating, merging or closing a pull request.

Plugins can be written in Go and Python.

Lifecycle of a plugin

Saturn starts each plugin of a task in a new sub-process, with the current working directory set to the directory that contains the plugin file. That allows a plugin to load additional files stored next to it. It communicates with plugins over gRPC.

sequenceDiagram
    Saturn->>Plugin: start plugin in sub-process
    Saturn->>Plugin: send configuration
    Saturn->>Saturn: list repositories
    loop for each repository
        Saturn->>Plugin: call filter()
        Plugin-->>Saturn: return result
        Saturn->>Plugin: call apply()
        Saturn->>Plugin: call onPrCreated()
        Saturn->>Plugin: call onPrMerged()
        Saturn->>Plugin: call onPrClosed()
    end
    Saturn->>Plugin: stop

Logs of a plugin

A plugin captures its stdout and stderr streams and sends any output to the main process of Saturn. The main process then writes the output to its logs:

// Other code omitted for brevity
type LogExample struct{
    saturnbot.BasePlugin
}

func (p *LogExample) Apply(ctx saturnbot.Context) error {
    fmt.Println("Hello plugin")
    // ...
}
// ...
# Other code omitted for brevity
class LogExample(Plugin):
    def apply(self, ctx: Context) -> None:
        print("Hello plugin")
        # ...
# ...

When Saturn calls the plugin as part of a task, it logs the following message:

PLUGIN [log-example stdout] Hello plugin

The pattern of the log message is:

PLUGIN [<name of plugin> <stderr|stdout> <message>]

Info

By default, Saturn uses the debug level to write log messages. Either set the global log level to debug to see log messages or change the log level of plugins.

Skip initialization during CI runs

saturn ci starts and initializes plugins. In case this behavior isn't desired, for example because a plugin contacts outside APIs, the plugin can detect that it is part of a CI run:

// Other code omitted for brevity
type Example struct{
    saturnbot.BasePlugin
}

func (p *Example) Init(config map[string]string) error {
    if config["saturn.ci"] == "true" {
        // Return early, don't execute init code.
        return nil
    }

    // Initialization code
}
// ...
# Other code omitted for brevity
class Example(Plugin):
    name = "example"

    def init(self, config: Mapping[str, str]) -> None:
        if config.get("saturn.ci", "false") == "true":
            # Return early, don't execute init code.
            return None
        # Initialization code
# ...

Debug a plugin

If logs aren't enough to understand what a plugin is doing, it is possible to start the plugin process separately and attach a debugger:

  1. Install Saturn
  2. Start the plugin in a debug process. How to do this depends on the IDE.

    The plugin prints a connection string to the standard output.

    Example:

    1|1|tcp|127.0.0.1:11049|grpc
    

    Copy the connection string.

  3. In a new terminal window, make Saturn connect to the plugin and call the function to debug:

    saturn plugin apply --address '1|1|tcp|127.0.0.1:11049|grpc'
    

plugin describes all available options of the command saturn plugin.