Skip to content

Typesafe GitHub Expressions

GitHub expressions

GitHub supports pretty advanced expressions via the ${{ ... }} syntax.

They include:

  • functions
  • environment variables
  • secrets
  • different contexts like the runner or the github context
  • events payloads
  • and more (read here).

Here is an example

run(
    name = "Environment variable and functions",
    command = "echo \$GITHUB_ACTORS",
    condition = "\${{invariably()}}",
)
run(
    name = "GitHubContext echo sha",
    command = "echo commit: \${{ github.sha256 }}  event: \${{ github.event.release.zip_url }}",
)

Unfortunately, it is easy to get those expressions wrong.

In fact this snippet contains four different errors.

Can you spot them all?

To make life easier, let us introduce type-safe GitHub expressions.

The expr("") helper function

First, because \${{ ... }} is awkward in Kotlin, it can be replaced by the expr("") helper function

- "\${{invariably()}}"
+ expr("invariably()")

But this is still not type-safe.

Type-safe functions with the expr { } DSL

We went one step further towards type-safety by introducing the expr { } DSL.

Goals:

  • an invalid expression should not even compile.
  • increase discoverability of what is available.

For example, you can use auto-completion to find out which functions are available:

Here we immediately see how to fix a first bug in our original snippet:

- "\${{invariably()}}"
- expr("invariably()")
+ expr { always() }

Reference: https://docs.github.com/en/actions/learn-github-actions/expressions#functions

The runner context

The runner context contains information about the runner that is executing the current job.

The possible properties are available via expr { runner.xxx }

https://docs.github.com/en/actions/learn-github-actions/contexts#example-contents-of-the-runner-context

The github context

The github context contains information about the workflow run and the event that triggered the run.

The possible properties are available via expr { github.xxx }

Here we detect immediatly another bug in our original snippet

-command = "echo commit: ${'$'}{{ github.sha256 }}
+command = "echo commit: " + expr { github.sha }

Reference: https://docs.github.com/en/actions/learn-github-actions/contexts#github-context

The github.eventXXX payload

The github.event field is special because it depends on what kind of events triggered the workflow:

  • Push
  • PullRequest
  • WorkflowDispatch
  • Release
  • ...

Since they have a different type, there is a diferent property expr { github.eventXXX } per type:

By leveraging this feature, we quickly fix another bug in our original snippet:

Default environment variables

GitHub supports a number of default environment variables.

They are available directly in the IDE via the library's Contexts.env By using this feature in our snippet we would have avoided escaping the dollar and the typo:

-command = "echo \$GITHUB_ACTORS",
+command = "echo " + Contexts.env.GITHUB_ACTOR,

Reference: https://docs.github.com/en/actions/learn-github-actions/environment-variables#default-environment-variables

Custom environment variables

You are not limited to the default environment variables.

You can create your own type-safe property by using the syntax

val MY_VARIABLE_NAME by Contexts.env

For example:

val GREETING by Contexts.env
val FIRST_NAME by Contexts.env

job(
    env =
        linkedMapOf(
            GREETING to "World",
        ),
) {
    run(
        name = "Custom environment variable",
        env =
            linkedMapOf(
                FIRST_NAME to "Patrick",
            ),
        command = "echo $GREETING $FIRST_NAME",
    )
}

Reference: https://docs.github.com/en/actions/learn-github-actions/environment-variables#about-environment-variables

GitHub Secrets

If you have sensitive information, you should store it as a GitHub secret:

Actions_secrets

You use them the same way as environment variables, but using Contexts.secrets instead of Contexts.env:

val SUPER_SECRET by Contexts.secrets

For example:

val SUPER_SECRET by Contexts.secrets

val SECRET by Contexts.env
val TOKEN by Contexts.env

job(id = "job1", runsOn = RunnerType.UbuntuLatest) {
    run(
        name = "Encrypted secret",
        env =
            linkedMapOf(
                SECRET to expr { SUPER_SECRET },
                TOKEN to expr { secrets.GITHUB_TOKEN },
            ),
        command = "echo secret=$SECRET token=$TOKEN",
    )
}

Missing a feature?

GitHub has more contexts that we don't support yet: https://docs.github.com/en/actions/learn-github-actions/contexts

There are more github.event payloads that we currently do not support: https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads

We feel what we have is a pretty good start, but if you need an additional feature, you can create an issue

Or maybe have a look how this type-safe feature is implemented in io.github.typesafegithub.workflows.dsl.expressions and submit a pull request 🙏🏻