Skip to content

Using actions

As a reminder, to be able to use an action, you have to know its owner, name and version, e.g. actions/checkout@v3. You can use any action you want. Read on to learn about your options.

Maven-compatible action bindings repository

When to use this approach

This is the recommended, default approach. Start with this.

To add a dependency on an action: 1. If you haven't already, add a dependency on a Maven repository that generates the action bindings on the fly: @file:Repository("https://bindings.krzeminski.it"). 2. Add a dependency on a Maven artifact, e.g. for actions/checkout@v3 the right way to add the dependency in the script is: @file:DependsOn("actions:checkout:v3"). As you can see, the group ID was adopted to model the action's owner, the artifact ID models the action name, and the version is just action's version (a tag or a branch corresponding to a released version). If an action's manifest is defined in a subdirectory, like the popular gradle/actions/setup-gradle@v3, replace the slashes in the action name with __, so in this case it would be @file:DependsOn("gradle:actions__setup-gradle:v3"). 3. Use the action by importing a class like io.github.typesafegithub.workflows.actions.actions.Checkout.

For every action, a binding will be generated. However, some less popular actions don't have typings configured for their inputs, so by default all inputs are of type String, have the suffix _Untyped, and additionally the class name will have an _Untyped suffix. The nullability of the inputs will be according to their required status.

There are two ways of configuring typings: 1. Recommended: a typing manifest (action-typing.yml) in the action's repo, see github-actions-typing. Thanks to this, the action's owner is responsible for providing and maintaining the typings defined in a technology-agnostic way, to be used not only with this Kotlin library. There are also no synchronization issues between the action itself and its typings. When trying to use a new action that has no typings, always discuss this approach with the action owner first. 2. Fallback: if it's not possible to host the typings with the action, use github-actions-typing-catalog, a community-maintained place to host the typings. You can contribute or fix typings for your favorite action by sending a PR.

Once there are any typings in place for the action, the _Untyped suffixed class is marked @Deprecated, and a class without that suffix is created additionally. In that class for each input that does not have type information available there will still be only the property with _Untyped suffix and nullability according to required status. For each input that does have type information available, there will still be the _Untyped property and additionally a properly typed property. Both of these properties will be nullable. It is a runtime error to set both of these properties as well as setting none if the input is required. The _Untyped properties are not marked @Deprecated, as it could still make sense to use them, for example if you want to set the value from a GitHub Actions expression.

This approach supports dependency updating bots that support Kotlin Script's .main.kts files. E.g. Renovate is known to support it.

User-defined actions

If you are in a hurry and adding typings is not possible right now, browse these options.

Typed binding

When to use this approach

It lets you create an action binding in a similar manner that is provided by the action bindings server i.e. a class that takes some constructor arguments with types of your choice, and maps them to strings inside toYamlArguments. Use it to have better type-safety when using the binding.

Repository based actions

In case of a repository based action which most GitHub actions are, inherit from RegularAction and in case of actions without explicit outputs, use the Actions.Outputs class as type argument:

class MyCoolActionV3(
    private val someArgument: String,
) : RegularAction<Action.Outputs>("acmecorp", "cool-action", "v3") {
    override fun toYamlArguments() =
        linkedMapOf(
            "some-argument" to someArgument,
        )

    override fun buildOutputObject(stepId: String) = Outputs(stepId)
}

or, in case of actions with explicit outputs, create a subclass of Action.Outputs for the type argument:

class MyCoolActionV3(
    private val someArgument: String,
) : RegularAction<MyCoolActionV3.Outputs>("acmecorp", "cool-action", "v3") {
    override fun toYamlArguments() =
        linkedMapOf(
            "some-argument" to someArgument,
        )

    override fun buildOutputObject(stepId: String) = Outputs(stepId)

    class Outputs(
        stepId: String,
    ) : Action.Outputs(stepId) {
        public val coolOutput: String = "steps.$stepId.outputs.coolOutput"
    }
}

Once you've got your action, it's now as simple as using it like this:

uses(
    name = "FooBar",
    action = MyCoolActionV3(someArgument = "foobar"),
)

Local actions

In case of a local action you have available in your repository or cloned from a private repository, inherit from LocalAction instead:

class MyCoolLocalActionV3(
    private val someArgument: String,
) : LocalAction<Action.Outputs>("./.github/actions/cool-action") {
    override fun toYamlArguments() =
        linkedMapOf(
            "some-argument" to someArgument,
        )

    override fun buildOutputObject(stepId: String) = Outputs(stepId)
}

Published Docker actions

In case of a published Docker action, inherit from DockerAction instead:

class MyCoolDockerActionV3(
    private val someArgument: String,
) : DockerAction<Action.Outputs>("alpine", "latest") {
    override fun toYamlArguments() =
        linkedMapOf(
            "some-argument" to someArgument,
        )

    override fun buildOutputObject(stepId: String) = Outputs(stepId)
}

Untyped binding

When to use this approach

It omits typing entirely, and both inputs and outputs are referenced using strings. Use it if you don't care about types because you're in the middle of experimenting. It's also more convenient to produce such code by a code generator.

Repository based actions

In case of a repository based action which most GitHub actions are, use a CustomAction:

val customAction =
    CustomAction(
        actionOwner = "xu-cheng",
        actionName = "latex-action",
        actionVersion = "v2",
        inputs =
            mapOf(
                "root_file" to "report.tex",
                "compiler" to "latexmk",
            ),
    )

If your custom action has outputs, you can access them, albeit in a type-unsafe manner:

job(id = "test_job", runsOn = RunnerType.UbuntuLatest) {
    val customActionStep =
        uses(
            name = "Some step with output",
            action = customAction,
        )

    // use your outputs:
    println(expr(customActionStep.outputs["custom-output"]))
}

Local actions

In case of a local action you have available in your repository or cloned from a private repository, use a CustomLocalAction instead:

val customAction =
    CustomLocalAction(
        actionPath = "./.github/actions/setup-build-env",
    )

Published Docker actions

In case of a published Docker action, use a CustomDockerAction instead:

val customAction =
    CustomDockerAction(
        actionImage = "alpine",
        actionTag = "latest",
    )