Writing Custom Github Actions with Javascript

Lately I’ve been involved with an Open Source project called AskQL. I really like the project, try to contribute as much as I can and learn a lot in the process.

One of the issues there was to enforce conventional commit names.

This looked pretty easy – there must be tons of github actions in the marketplace for it, right?

If you don’t know what github actions are here’s a one liner that should explain it:

Github actions are a group of instructions set in yaml files that are being ran on certain conditions like commit, push, pull request etc.

It might come out as a two liner – depending on your screen resolution 😉

I googled and found several such actions. They all looked promising and I immediately implemented the first one I found.

It seemed relatively easy: just add a yaml file with the given example and it should work out of the box!

name: "Lint PR"
on:
  pull_request:
    types:
      - opened
      - edited
      - synchronize

jobs:
  main:
    runs-on: ubuntu-latest
    steps:
      - uses: amannn/action-semantic-pull-request@v1.2.0
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

The yaml file above is pretty simple. The procedure name is “Lint PR”. It fires on pull request open, edit and synchronize.

It eventually just needs to run on ubuntu and uses the github action from the marketplace with GITHUB_TOKEN as an environment variable.

Easy, right?

Aw, Snap!

Well… seems like it works – if you are not working Open Source style. That is – if you are not working with forks.

Let me explain – the GITHUB_TOKEN is available only if you create a Pull Request directly from the main repository. If you are creating a Pull Request from a fork (as in most Open Source projects), GITHUB_TOKEN is not available.

Figure 1: The error I got when trying to use the existing PR linters in the marketplace

It’s a well known issue, summarized nicely in this post: https://github.community/t/github-actions-are-severely-limited-on-prs/18179/8

So how does one lint the Pull Requests?

DIY script

It’s time to do the wrong thing – write my own script.

Because we wanted to use conventional commits, I turned to npm.

A quick search found this really nice library called commitlint.

This library can lint a PR according to conventional commit standards. I created a simple script that gets a title and makes sure it is a valid conventional commit:

const load = require('@commitlint/load').default;
const lint = require('@commitlint/lint').default;
const CONFIG = {
extends: ['@commitlint/config-conventional'],
};
function buildLintError(lintErrors) {
return lintErrors.map((error) => error.message);
}
export async function testTitle(title) {
const lintOptions = await load(CONFIG);
const lintResult = await lint(
title,
lintOptions.rules,
lintOptions.parserPreset ? lintOptions.parserPreset : {}
);
if (!lintResult.valid) {
throw new Error(buildLintError(lintResult.errors));
}
}
view raw lintpr.js hosted with ❤ by GitHub

This script works pretty well locally. To make it work in a github action we’re going to need 3 more things.

#1 Get the PR Title

Github actions has a really nice SDK one can use to get all the data and processes one needs in order to build a fully-fledged CI/CD.

Using @actions/github gives us access to the data we need. Let’s use it in our code:

const { testTitle } = require('./lintPR');
const github = require('@actions/github');
function getTitle() {
return github.context.payload.pull_request.title;
}
async function run() {
await testTitle(getTitle());
}
run();
view raw useTestTitle.js hosted with ❤ by GitHub

Our code gets the title of the PR and verifies it is a valid conventional commit. Hooray!

Now we are missing 2 more things.

#2 Throwing an error during the CI

Github actions has us covered here as well. Using the @actions/core library we can easily set the CI status to failed:

const { testTitle } = require('./lintPR');
const github = require('@actions/github');
const core = require('@actions/core');
function getTitle() {
return github.context.payload.pull_request.title;
}
async function run() {
await testTitle(getTitle());
}
run().catch(e => core.setFailed(e));
view raw useTestTitle.js hosted with ❤ by GitHub

Adding core.setFailed on line 13 to notify the CI process it should fail

This small addition will set this job’s status to failed so our CI will fail if our title is invalid.

And now for the final piece of the puzzle.

#3 Setting up the yaml file

This part can be a bit tricky. Eventually, we need to clone our repository (because we put our script in it) and install the needed dependnecies (@actions/core, @actions/github, @commitlint/config-conventional, @commitlint/load and @commitlint/lint).

After these two steps are done, we can move on to actually running our script.

Here’s the yaml file for this task:

name: "Lint PR"
on:
pull_request:
types:
- opened
- edited
- synchronize
jobs:
main:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install dependencies
run: npm install @actions/core @actions/github @commitlint/config-conventional @commitlint/lint @commitlint/load
- name: Checks the PR title
run: node ./scripts/useTestTitle
view raw pr-lint.yaml hosted with ❤ by GitHub

Again – very simple. Checkout, install npm dependencies and run our script.

And it appears to be working! Figure 2 shows what happens with an invalid PR title, while Figure 3 shows the results of a valid one.

Figure 2: The results of trying to change the title to “foo(ci): enforcing conventional commits”
Figure 3: The results of changing the title to “feat(ci): enforcing conventional commits”

I guess that means mission accomplished right? Hooray!

You can view the PR here: https://github.com/xFAANG/askql/pull/230/files

Summary and Future plans

This task was really nice. I learned something new about github actions in addition to using new libraries I also didn’t know about before. All in all – a great day!

The issue that got me a-searching was that the marketplace actions did not work out of the box. The cause was GITHUB_TOKEN not being available for PR’s that are coming from forks.

I eventually wrote a script and ran it inside my own action – and just created the solution we needed for the project.

A future improvement would be to create the useTestTitle as a package. This way, the github action using it would not need to install the dependencies (which take most of its run time). From there – why not just turn this code to an official action in the marketplace?

Hope you enjoyed the article 🙂

Thanks a lot for this article reviewer MichalKutz

Sign up to my newsletter to enjoy more content:

Leave a Reply

Your email address will not be published. Required fields are marked *