GitHub Actions, a feature announced in October 2018 during GitHub Universe, generated immense hype under the apt positioning as the “swiss army knife” of git workflow automation.
Github Actions allows developers to perform tasks automatically in a Github workflow, such as pushing commits to a repository, deploying a release to staging, running tests, removing feature flags, and so on, by way of a simple text file.
Chewy.com, for example, demoed an action that checks if a Jira ticket number is included in every pull request name among other things, to ensure the code being deployed to production is compliant with their policies and best practices.
“With GitHub Actions, you can automate your workflow from idea to production.”
– GitHub actions page
I started tinkering with the feature as soon as I could get access to the private beta. I noticed that most Actions are written in shell script. GitHub itself promoted writing actions in shell script for simple actions. While I understand the motivation (so you can quickly and easily start writing Actions), I feel that shell scripts are limited in terms of writing full-fledged software.
The basic setup is straightforward – start by following GitHub’s tutorial all the way to the entrypoint.sh section. The main difference up to this point is in the chosen docker image. I suggest using the alpine image, it’s very lightweight compared to the regular node image. In any case, I suggest using an LTS variant, currently being node 10.
A very important tool, one that helped reduce the development cycle from 5 minutes per iteration to mere seconds is Act, a zero-config, easy to use tool to run actions locally. It doesn’t fully replicate the environment (for obvious reasons, it doesn’t provide a GitHub token, more on that later) but it’s close enough to speed up the development process and test your action locally.
At the end of this step, you should have a Dockerfile that looks like this:
This is the basic entrypoint.sh file that will work for a Node.js action:
I chose npm ci because it’s the easiest way to make sure you always get the same versions of the packages you want to install. It requires you to have package.json and package-lock.json in your project – but that’s a best practice anyway.
The installation script is in the entry point file and NOT in the Dockerfile (as is usual in classic container use cases) because it makes it much easier to use an npm token and install private packages. All you need to do is add an NPM_TOKEN secret and use it in the entry point file (above npm ci) by adding:
npm config set //registry.npmjs.org/:_authToken=$NPM_TOKEN
node script.js $* runs the script and passes the action args as arguments to the script.
Over the years, I’ve developed a preferred structure for a node executable (CLI) script. I will share it here but for the purpose of this tutorial, this part is completely optional and at this stage, you’re more than ready to develop your own action in Node.js.
The script looks like this:
The important section is at the bottom: if (require.main === module)
It checks if the file was imported/required or if it’s the entrypoint into the program. This allows reusing the same module both programmatically and as a CLI tool.
If this is the entrypoint, I would then parse the command line arguments (using commander) passed in from entrypoint.sh. The arguments were injected into entrypoint.sh by GitHub from the workflow file through the container (more on that later).
I then invoke the main function. Since it’s an async function, I handle its return value with a then clause and handle failure with a catch clause.
It’s also useful to read the event, provided by GitHub, and use it in the script:
const event = JSON.parse(fs.readFileSync('/github/workflow/event.json', 'utf8'))
The Actions environment takes some getting used to. Although GitHub provides great tutorials on all things workflow related, I wanted to mention:
Secrets are pretty straightforward, you define them in the repo settings tab and then they’re exposed as environment variables inside the container.
Defining the secret in the repo settings tab:
Using the secret in your workflow file:
The only exception is the GitHub token, which you don’t need to define in the settings. The token is only exposed in the workflow file and GitHub will provide the token itself with these permissions.
Another important item to note is the mounted folder GitHub provides. It’s mounted under /github and provides a couple of useful things:
-the event under /github/workflow/event.json and
-the repo where the action runs under /github/workspace/REPO_NAME
More information on the mechanics of the mount can be found here.
If you’re interested in learning more about how you can use Github Actions to automate git workflows, check out this webinar to watch how Shimon Tolts, Datree.io Co-founder, built a CI/CD dev pipeline with Github Actions, Node.js, Docker, and AWS Fargate.
If you have any questions, corrections, or suggestions please comment below or contact me directly.
Also published on StackShare 😎
Check out: Best practices on managing secrets using AWS or Hashicorp Vault. Also see: sample code we use at Datree.io to inject secrets into our application.
Best practices for writing git commit messages - cheatsheet included. Read how to optimize git commit messages to improve your development productivity.