Building GitHub Actions in Node.js

Hey! You there! Heard all about these new fan-dangled GitHub Actions, and want to go out and build one? Well then let's do it!

This post will serve as a guide to building a run-of-the-mill GitHub Action in Node.js. You can build Actions in whatever language/runtime you want - I'm choosing Node.js because JavaScript is the largest language on GitHub and because Node.js is bae 😍

Before we dive in

I'll assume that you've built some things in Node before - this post won't cover Node.js fundamentals, just the things specific to Actions.

If you aren't at all familiar with Actions, check out @jessfraz's excellent post on the life of an Action - it should help you wrap your head around the concept, because this post is really about building the thing.

What we're building

As a thought experiment, let's build @jessfraz's branch-cleanup-action Action. It deletes branches whose pull requests have been merged. If you just want to use the thing, you should use hers - we're porting it to Node for fun!

Let's do it

We'll start by preparing our Dockerfile. Every Action runs in a Docker container, so we need to describe that container. The below code should suffice for most (but not all) Node.js GitHub Actions. Docker is an amazing technology, but if your goal is just to build your Action you shouldn't need to learn it all.

FROM node:slim

# A bunch of `LABEL` fields for GitHub to index
LABEL "com.github.actions.name"="Delete merged branches"
LABEL "com.github.actions.description"="Cleans up merged branches."
LABEL "com.github.actions.icon"="gear"
LABEL "com.github.actions.color"="red"
LABEL "repository"="https://github.com/JasonEtco/node-branch-cleanup-action"
LABEL "homepage"="https://github.com/JasonEtco/node-branch-cleanup-action"
LABEL "maintainer"="Jason Etcovitch <jasonetco@github.com>"

# Copy over project files
COPY . .

# Install dependencies
RUN npm install

# This is what GitHub will run
ENTRYPOINT ["node", "/index.js"]

We're using node:slim to have a small, but still functional Docker image. You can use whatever image you want, but a smaller image will make for a faster Action.

Are there ways to make that Dockerfile better? Sure! But we're going for clarity over performance for now.

Let's go ahead and create our index.js file:

// index.js
console.log('Hi GitHub!')

Now, when GitHub runs this Action, it will build the Docker container from the node:slim tag and run node /index.js - which will print Hi GitHub! to the Action logs:

Step 11/11 : ENTRYPOINT ["node", "/index.js"]
 ---> Using cache
 ---> 539689e6c5b6
Successfully built 539689e6c5b6
Successfully tagged gcr.io/gct-12-...
Already have image (with digest): gcr.io/github-actions-images/action-runner:latest
Hi GitHub!

Honestly, now you can build whatever you want. You've "set up your environment," and all that's left is writing your code. There are some special things in Actions that I'd like to talk about though; hang tight, because we'll finish our Action before this post is over.

Environment variables

The container in which your index.js file is being run has a few things injected as environment variables - I'll cover the main ones below, but you can see the full list in the GitHub Action documentation.

  • GITHUB_TOKEN: An authentication token used to make API requests to GitHub.
  • GITHUB_EVENT_PATH: A path to a .json file containing the event that triggered the action. This is where you'll find things like the resource that triggered it (like an opened PR, or a commit).

Back at it

Ok, back to our Action. Using actions-toolkit (you'll need to npm install it), here's the whole .js file:

// index.js
const { Toolkit } = require('actions-toolkit')
const tools = new Toolkit({ event: 'pull_request.closed' })

if (tools.context.payload.pull_request.merged) {
// An authenticated instance of `@octokit/rest`, a GitHub API SDK
tools.github.git
.deleteRef(
tools.context.repo({
ref: `heads/${payload.pull_request.head.ref}`
})
)
.then(() => {
tools.log.success(`Branch ${payload.pull_request.head.ref} deleted!`)
})
}
image

But I depend on dependencies!

Cool, me too! I'll share some libraries that are particularly useful for GitHub Actions, as well as actions-toolkit, a library I'm working on designed specifically for Actions.

@octokit/rest & @octokit/graphql

These are part of the official family of GitHub API SDKs called Octokit. If you're making requests to the GitHub API (REST or GraphQL), these are great libraries to look at.

minimist

A really interesting part of GitHub Actions is the ability to pass arguments by defining them in your workflow. minimist is a handy tool for parsing a string of arguments:

args = "some-arg --flag true"
minimist(process.argsv)
// -> { _: ['some-arg'], flag: true }

Probot

I'd be remiss if I didn't mention Probot, a framework for building GitHub Apps. We've taken a lot of care into building testing patterns and helpful extensions. You can deploy your Probot Apps to GitHub Actions, but it comes with some tradeoffs.

That said, if all you care about is the syntactic sugar that Probot provides, go right ahead!

actions-toolkit is a wrapper around some fantastic open source libraries, and provides some helper methods for dealing with GitHub Actions. It uses some of the above libraries, and bases its paradigms on our experience building and using Probot. Check out the repo for the full API documentation.

I'm looking for feedback on its use, as well as potential new features - let me know if you have any thoughts!

I don't want this to be a "use this library, its the definitive way to build Actions" kind of post - that's why I'm leaving it to the end. You can absolutely build all this functionality yourself but... now you don't have to 🙌


And that's it! Hopefully this will give you an idea on how to build your own GitHub Actions in Node.js. Tweet me with whatever you build ✨