Creating a GitHub Action using Kotlin/JS

Mohitesh
OkCredit
5 min readAug 19, 2021

--

We at OkCredit have been using GitHub Actions to automate our Android CI and CD workflows like running unit tests, danger checks, releasing the app on Play Store, etc. Even though many CI/CD tools are available in the market, GitHub Action is probably the simplest one to integrate. And the best part about GitHub Action is we can build our own action and customize it to our specific requirements.

While exploring Kotlin Multiplatform, we recently realized that we can use Kotlin (Kotlin/JS to be specific) to develop a GitHub Action. GitHub supports creating and running the JS actions on any runner machine. So we decided to try our hand at it and wrote a GitHub action for creating a changelog from Pull Requests. We also published it on the GitHub marketplace for free.

Changelog Creator parses all the Pull Requests merged in a milestone and creates a changelog using the configuration file supplied to the Action. The configuration supports creating categories using the labels added to a PR, overall format to be used to create release notes. You can find the full code of changelog-creator-action in this open-source repository.

If you have made it till here and are curious about how to build an Action using Kotlin, keep reading.

Creating GitHub Action

Let’s start with quick prerequisites for creating the GitHub Action

Getting Started With Kotlin/JS

We used IntelliJ IDEA to develop the Kotlin/JS project because of the familiarity with the IDE and first citizen support from Jetbrains.

Creating a Kotlin/JS project is fairly simple in IDEA because of the built-in templates when creating a New Project. It gives you a basic structure of the project and you can directly start the development. For our GitHub Action, select the NodeJS Application under the Kotlin section in the New Project window.

Using JS libraries in Kotlin/JS

To call any JS module from Kotlin we need external declarations of the JS module. You can read more about it here.

Creating the externals manually can be a tiring task and also error-prone. So the Kotlin team came up with a handy tool Dukat to automatically create external declarations for any TypeScript definition files(.d.ts). Unfortunately, it is still early days for Dukat and its usage is still experimental. So it might not generate correct externals for all NPM libraries. In our case, it was not able to generate the correct external file, so we had to create them manually.

Action Metadata File (action.yml)

Every GitHub Action requires a metadata file that defines the basic info of Action like name, author, inputs, outputs, and the entry point triggered when we run the action.
This file should be placed at the root of the project and its name should be either action.yml or action.yaml.

GitHub REST APIs

Github provides an extensive set of REST APIs which can be used to perform a variety of actions.

Github also provides various pre-built wrappers to integrate the Github APIs like octokit/core.js. But using the vast JS library directly in the Kotlin project is still error-prone and integrating individual APIs required for the Action made more sense.

Some of the basic requirements for integrating Github REST APIs are following

  • token - A token is required for authentication which needs to be passed in the header of the request. We can ask the users to pass the token as an explicit input or use the token from default environment variables.
  • org and repo is required as path variables in Github requests. They can be fetched from the GITHUB_REPOSITORY environment variable.

Github provides very good documentation of all the APIs with their request and response structure. You can find it here.

NOTE: Please take care of rate limitations and pagination while integrating any APIs.

Building the project

Getting the executable JS is probably the most tricky part of the development. We need a single JS file with all the Kotlin and NPM dependencies bundled in it. We cannot upload the node modules folder or the Kotlin dependencies with our JS.

We used the new IR compiler as it adds a Gradle task called compileProductionExecutableKotlinJS which takes care of bundling all Kotlin dependencies in the JS.
For bundling the NPM dependencies, we used NCC packer which is very commonly used in JS projects. The tricky part is making it work with the Kotlin/JS project.

First, we added the Gradle task to copy generated JS from the build folder to the folder at the root of the project. Next, we did the same for node_modules the folder present in build/js/node_modules. Now we run these tasks sequentially after we run assemble in the project. These files serve as inputs for the NCC packer.

Thanks to Björn Kautler (@Vampire), I found the Kotlin/JS version of NCC packer which can be called after the compilation. So I added a new module, passing the copied JS path from the previous step as an argument and the same path as an output so that NCC just replaces the JS file. Finally, to call the ncc_packer every time I assemble, I finalized the copy task with the NCC packer run.

tasks.named("CopyGeneratedJSToDistribution") {
finalizedBy(project(":ncc-packer").tasks.named("run"))
}

That’s it, you will get the final JS which can be used to run your GitHub Action.

NOTE: Please make sure to use the same path in your action.yml.

Libraries that make things easier

  • @actions/core: Utility library from GitHub for setting results, logging, registering secrets, and exporting variables across actions.
  • Ktor: Perform all the network calls
  • kotlinx.serialization: For serialization and deserialization of JSON
  • kotlinx-datetime: A multiplatform Kotlin library for working with date and time.
  • @vercel/ncc: Simple CLI for compiling a Node.js module into a single file, together with all its dependencies, gcc-style.

References

--

--