Github actions: basic concepts
What is this and how to remotely run tests on every push using a tiny config file?
We’d talk about:
* what is github actions
* how to run them in your repo
* more complex questions about configuraiton
* Next chapter: How to write your own aciton in Javascript
What is GitHub Actions?
It’s a way to instruct GitHub to run certain code whenever an event happens: a push, PR creation, timer, external event, and many more.
Where does this code run? On GitHub’s virtual machines (but you can run it on your own, if needed). Convenient — no setup required.
What does it do? Anything you can imagine! It automates actions like running tests, building and deploying products, gathering stats, and notifying people.
Thus, GitHub offers not just a free CI/CD (like GitLab, which I wrote about too), but a highly flexible system to support your development process.
AWESOME, I WANT TO USE GITHUB ACTIONS!
It’s beautifully simple.
First, think about what you want to do. Then, find a ready-made action (or a few) to help you achieve that. The easiest place to search is GitHub Marketplace — it’s essentially just a list of repositories that have applied to be there. You’ll be referencing a specific user’s repository.
If you can’t find a suitable one, you can always write your own — it’s super easy. For example, I wrote an action to integrate Jira into pull requests:
Let’s start with the simplest example — let’s set up tests to run on every push to any branch (this example will be for JavaScript development).
First, you need to create a .github
folder in your project, then inside it, create a workflows
folder. In that folder, create a file named name-matters-only-for-you.yaml
.
The content of this file will be something like this:
name: 'test my project'
on:
push:
pull-request:jobs:
test-job:
runs-on: ubuntu-16.04
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
name: 'setup node'
with:
node-version: '13.x'
- name: 'install'
run: npm i
- name: 'test'
run: npm run test
Then, in the GitHub interface, the test runs will look like this:
Let’s break down what each line does. This example doesn’t cover all the possible options, but GitHub has wonderful documentation that explains absolutely everything.
# name to be dispalyed in the interface
name: 'test my project'# list of events which should trigger an event
on:
push: # let it run on any push
pull-request: # and for any pull request # what do we do
jobs:
test-job: # uniqe id
runs-on: ubuntu-16.04 # which machine to use
steps: # which steps to run
- uses: actions/checkout@v2
....
Steps
The entire configuration is written in YAML, so it’s important to follow the syntax and watch the indentation. A new entity in the list starts with a -
.
- Steps are essentially an ordered list. They are executed strictly one after the other in sequence.
- Let’s take another look at the config. You can see we have four steps, but they seem to have different sets of parameters. How does that work?
Would you like to proceed with breaking down each step or discuss the parameters further?
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
name: 'setup node'
with:
node-version: '13.x'
- name: 'install'
run: npm i
- name: 'test'
run: npm run test
name
is just for display in the interface; you can omit it if desired.uses
specifies the name of an action to be used, which can be from a specific branch of any repository, a relative folder in the ssame repo, or even a Docker image. Here is the full list
In the example,actions/checkout@v2
andactions/setup-node@v1
are used. You can easily find them in the marketplace, where you can check the source code of each version (the version is specified after the@
symbol).checkout
pulls the repository and branch where the action is running, giving access to the code. Without this action, there wouldn’t be anything to runnpm i
on.setup-node
installs Node.js so it can be used in the following steps.- The
with
section is used to pass parameters to an action when necessary, and these parameters are defined by the action itself. In the example, you can specify the version of Node.js required for the project. - The
run
command executes a shell command. You cannot mix shell commands with actions in the same step—they need to be in separate steps.
Once this file is added and pushed to the master
branch, the script with tests will run on every future push.
If it’s a simple push, you can view the result in the commit list. If it’s a pull request, you’ll find the result summary at the bottom of the PR page.
That’s it! We’ve covered a small example, hooray! Now you’re ready to dive in!
To make it easier, I’ve put together a list of things I learned when I was diving in myself.
More Complex Questions:
How to pass a token without exposing it publicly? What are secrets in configs?
Tokens that are publicly accessible pose a significant security risk. How can they be passed securely? GitHub has already taken care of this by creating secrets. You can find them in any repository under Settings -> Secrets. There, you can create a secret with almost any name, such as MY_TOKEN
, and assign it a value. In any action, you can then reference secrets.MY_TOKEN
, and that value will be used.
Interesting Points:
- Once a secret is created and saved, you can no longer view its value — only update it.
- If you try to log a secret’s value in the logs or inside the action’s code, it will appear as
***
. - However, it’s best not to log secrets at all, as any security measure like this can still potentially be bypassed
How to add secrets.GITHUB_TOKEN
, which is needed in the config?
All secrets starting with GITHUB_
are system-provided and automatically substituted by GitHub. No need to worry—just write {{secrets.GITHUB_TOKEN }}
, and everything will work.
How to prevent merging a pull request if tests fail?
Go to the repository settings and set up branch protection rules for merging into master
. Check the box for the workflow containing the critical tests. This will ensure that if tests fail, the pull request cannot be merged.
How to pass the result of one step to another step?
This is a really cool feature! With the code below, I check whether there are modified files after the build in the lib
folder, and if there are, I commit the updated build. Variables added in one step are available in any subsequent step within the same job.
- name: "check if build has changed"
run: echo ::set-env name=DIFF::$(git diff --stat -- 'lib')
- name: "Commit files"
if: ${{ env.DIFF }}
run: git commit -m "build action" -a
How to pass the result between jobs?
This is where it gets a bit more interesting!
jobs:
first-job:
- name: "check if build has changed"
id: has-diff
run: echo ::set-output name=DIFF::$(git diff --stat -- 'lib')
outputs:
diff_output: ${{ steps.has-diff.outputs.DIFF }}
second-job:
needs: build-test
steps:
- name: "print diff"
run: echo "${{ needs.build-test.outputs.diff_output }}"
It’s important to remember that both outputs and env
parameters are strings, which can include special characters. So, when displaying them in the console, this must be considered. Otherwise, you might encounter an error like this if you write run: echo ${{ needs.build-test.outputs.diff_output }}
without quotes:
If the information in this article isn’t enough to help you implement what you need, it might be time to write your own custom action: