Github actions: writing your own
Hooray, we know how to use ready-made actions and build pipelines from them (if you don’t yet, here’s an article about it)!
We run tests, add labels to pull requests, build and deploy branches — awesome.
But often, the marketplace doesn’t have a solution that meets our needs. Or it almost does, but in true open-source fashion, the author isn’t keen on making small tweaks to make it usable for us.
Then it’s the perfect time to write your own! And we, as JavaScript developers, are in luck because we can just write the code in JavaScript!
With other languages, you’d have to wrap everything in Docker, but here, you can just start coding right away.
We’ll create a separate repository, initialize a Node.js application, and add a file called action.yml
in the root with information about our future action. Of course, you can always modify this file later—it's only required on GitHub's side.
name: 'Fancy name of the action'
description: 'Short description'
author: 'name I want to become famous with'
inputs: # if you need external params
github-token:
description: 'This is a token to access github'
required: true
flag:
description: 'This is flag that you can set to true or false'
default: false
required: false
runs:
using: 'node12' # required node version - imprtant
main: 'lib/index.js' # final file with all the logic
branding: # How icon would look like in the marketplace
icon: 'terminal'
color: 'blue'
Important: If you don’t specify certain parameters in the inputs
field, you won’t be able to access them in the JavaScript code when running on GitHub, even if they’re listed under with
.
However, marking an input as required
here but not passing it won’t cause anything to happen—no warning or error will show up. You can safeguard against this in your JavaScript code, which I’ll explain below.
The branding option is really nice — it lets you create an action logo from a set of predefined icons and colors. Later, you can go all out and add a custom logo, but we’ll deal with that later.
Next, we’ll create a file called lib/index.js
and start writing our code there.
console.log('Hello, I am an action!')
You’re all set! You now have an action that simply logs to the console, but it works! Let’s test it out.
Add a test-itself.yaml
file in the .github/workflows
directory.
name: "test itself"
on:
push:
jobs:
test:
runs-on: ubuntu-16.04
steps:
# first step is required to be able to use action from our own repo
- name: Check out repository
uses: actions/checkout@v2
- uses: ./
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
Lets push it to github to see everything in action!
In the config, you can specify not only a relative path but also an absolute one:
uses: ./<username>/repo-name@<branch-name>
(here, use the name of the repository where you're pushing your action). This way, you can test it in any other repository without needing the first step withcheckout
. How you choose to test it is entirely up to you.
You can specify not only a relative path in the config, but if you’re working in a private repository, there’s only one way — testing locally
Awesome, we’ve created the simplest GitHub Action in the world, and honestly, you can already start sharing it in open source. You laugh, but there’s already something like this in the marketplace 😄. The niche is taken, so we’ll have to come up with something a bit more complex!
Let’s add some logic!
Your logic is only limited by your imagination, since it’s essentially just a Node.js application.
Regardless, you’ll need some packages from actions/toolkit
. Two of the most essential ones, in my opinion, are:
@actions/core
: Use this if your action will take inputs or needs to fail with a verbose error message.@actions/github
: Use this if your action needs to retrieve information about the repository, pull requests, or other GitHub-related data.
There are also others like @actions/exec
, @actions/io
, @actions/glob
, and @actions/artifact
. You can read more about them in the actions/toolkit
documentation.
How to get the passed inputs?
import * as core from '@actions/core';// {required: true} would throw an error if input not provided
const TOKEN = core.getInput('github-token', {required: true});
It’s important to remember that all inputs are received as strings, so if you’re expecting a number
or bool
, you need to cast the types.
How to bundle everything into a single file?
GitHub will eventually run one JS file in a Node.js environment, and the path to this file will be specified in action.yml
. No one wants to run npm install
or npm build
before every execution, so you need to provide a single entry point that ensures the action works without additional steps. It's similar to bundling for HTML.
You can either:
- Specify your main file (e.g.,
src/main.js
) as the entry point and push the entirenode_modules
directory so that your code has access to its dependencies. - Or, instead of this somewhat crude approach, bundle your code. For example, using
ncc
, a package recommended by GitHub itself. It even supports TypeScript ❤️
ncc build src/main.js -o lib -m
This script bundles everything into the file lib/index.js
, using src/main.js
as the entry point.
How to bundle automatically?
It’s easy to forget to bundle if it’s not done automatically. How can this be automated? You could do it every time on pre-push and pre-commit, for example. But that would be terribly slow and very annoying, and eventually, it would get disabled.
This is where GitHub Actions come into play! Here’s how I implemented the build for my action:
It’s important to ensure that at the moment of tag creation, the repository contains the current build, but this happens much less frequently than commits, so you can rely on your attention here.
And an important point: committing and pushing from within an action can be dangerous due to the potential creation of an infinite loop — push a commit from the action, trigger the action again, push another commit from the action, and so on infinitely. Therefore, you need to be cautious.
How to get information about a repository/pull request?
import { context, GitHub } from '@actions/github/lib/github';
import * as core from '@actions/core';const TOKEN = core.getInput('github-token', {required: true});// client can use the whole REST api by octokit https://octokit.github.io/rest.js/v18
const client = new GitHub(TOKEN);// context contains all the information
const {
payload: {
repository,
organization: { login: owner },
pull_request: pullRequest,
},
} = context;const isPRMerged = await octokit.pulls.checkIfMerged({
owner,
repo: repository.name,
pull_number: pullRequest.number
});
How to stop the execution of an action with success/error?
// success
process.exit(0);
// error
core.setFailed('Hi, log failed, I failed and this is why:');
If you are looking for an example of working code, I can suggest taking a look at the action I wrote for work purposes.
In the same way, you can find other working code examples and borrow some practices from there.
How to publish to the marketplace?
Congratulations, you’ve already created a useful action! It can reside in the same repository where it’s being used (even in a private one), but that won’t help you gain recognition. So, let’s go over how to publish it as open-source after you’ve written it.
Now is the perfect time to publish it in the marketplace to get noticed. If you feel like it’s too early for the marketplace, or you’re dealing with imposter syndrome and feel awkward, forget about that! Now, while actions are still new, is the perfect time to jump on board. Plus, if you’ve encountered a certain need, chances are high that someone else has too.
To begin with, you need to write a good, detailed README
, otherwise, no one will understand how to use your action. Moreover, you won’t be able to publish to the marketplace without a README.md
Go to the “Releases” section (available on the right panel) and click “Draft new release”. If you have a valid action.yaml
, you’ll see a panel like this at the top:
That blue checkbox will do all the magic. Create a new release tag, specify the correct version according to semver (a hint will be on the right), and click “Publish release.” That’s it, you’re amazing! Go ahead and find yourself in the marketplace!
If the tag has already been created and pushed, you can release it from the “Tags” tab.
In the future, if you add changes, create new releases in the same way. Don’t forget that you can’t change an old release, as people rely on it!
Now you’re ready to take your first step in writing GitHub Actions.
Of course, there are many more possibilities — you just need to understand the requirements and formulate a request to the universe (crossed out) or to GitHub’s excellent documentation, or to Google.
Or, you can simply ask a question here or send me a message.
Hooray!