GitHub Actions is a powerful and flexible automation tool built into GitHub. It allows developers to automate build, test, and deploy tasks. If you already use GitHub as your code repository, you should at least consider what GitHub Actions can offer you to automate repetitive tasks and replace the use of additional tools.
With GitHub Actions, you can create custom workflows that respond to a wide range of events, including code pushes, pull requests, at a scheduled time, and even when an event outside GitHub occurs.
In this article, we will cover all of the main concepts of GitHub Actions and see them in action in a practical example.
Workflows
A workflow is an automated process that can be configured to run one or more jobs. Workflows are defined as YAML files stored in the .github/workflows
directory of your code repository. It is possible for a repository to contain many workflows, each one defined as a separate YAML file with a unique set of tasks to perform. For instance, a workflow may be designed to build and test pull requests, while another may deploy the application every time a new release is made.
Events
An event is a specific trigger that can initiate a workflow. For instance, pushing code to a repository or creating a new pull request can trigger a workflow. Although GitHub Actions supports a broad range of events, this article will focus on the two most common ones, ‘push’ and ‘pull_request’. Nonetheless, many more events are available that can be used to initiate workflows and fulfill specific automation requirements including events related to issues, releases, and external events. It is even possible to schedule the trigger of a workflow at specific times.
The events that initiate the run of a workflow are defined under the on:
key. It is possible to have multiple events triggering a workflow, for example, the following code will run the workflow when a push is made to the branches main or develop and whenever a pull_request targeting the branches main and develop is created, synchronize, or reopened.
on:
push:
branches:
- main
- develop
pull_request:
branches:
- main
- develop
types: [opened, synchronize, reopened]
Jobs
A job is a set of steps that are executed sequentially. A workflow run consists of one or more jobs, which are executed in parallel by default but can be set to run sequentially by defining dependencies between jobs. Two or more jobs on the same workflow can share data with each other using cache or artifacts.
Each job needs a unique identifier in the form of a string that only contains alphanumeric characters, slashes, and underscores. This identifier is very important as it is used to refer to the job on the rest of the workflow. You can also give your jobs a human-readable name which is displayed in the GitHub UI.
jobs:
first_job:
name: This will be run first
second_job:
name: This will be run second
Runners
Each job in a workflow needs somewhere to run, so the first thing we have to do in a Job definition is to specify the type of machine to run the job on, in GitHub Actions that machine is called a runner.
A runner is a machine or virtual machine that executes the steps of a job in a workflow. When a workflow is triggered, GitHub selects an available runner that matches the operating system and architecture specified in the workflow file.
You can choose to use one of the different types of GitHub-hosted runners, or self-host them on a local machine or in a cloud-based environment. GitHub provides hosted runners that can be used for free on various platforms such as Windows, Linux, and macOS.
jobs:
first_job:
name: This will be run first
runs-on: ubuntu-latest
second_job:
name: This will be run second
runs-on: macos-11
Actions
An action is a custom application for the GitHub Actions platform that performs a complex but frequently repeated task. We use actions to help reduce the amount of repetitive code that you write in your workflow files. For example, an action can pull your git repository from GitHub, set up the tools needed for your build environment, or deploy the build application to a cloud environment.
You can find actions to use in your workflows for the most common tasks in the GitHub Marketplace, or you can write your own actions and store them as part of the code repository of your workflow, or you can keep the action in its own repository to better reuse or share with other people.
Actions can run in a Docker container, or directly on a runner machine using pure JavaScript code and no other binary dependency. Docker container actions are slower than JavaScript actions and can only execute on Linux runners.
The development of custom actions is outside the scope of this introductory article.
Steps
A step is a unit of work that is executed by a runner in a job. Each step in a job represents a single task, such as running commands or invoking an action stored in a repository, a public repository, or a Docker registry.
To define a step that invokes an action you have to use the keyword uses:
followed by a string that identifies the action we want to use.
Example of a Step that invokes an action:
steps:
- name: Checks out your repository
uses: actions/checkout@v3
This is one of the most common actions, by default it checkouts the GitHub repository so you can access the code in your workflow.
If you are using a repository or Docker image it is strongly recommended to include the specific version of the action you want to use. You can do this by specifying a Git commit, a version release, or a branch.
If you have an action that you want to use only on the workflow of this repository, you can store the action in a directory and point to that directory to invoke it. It is not mandatory but recommended to store the custom action under the .github/actions directory
jobs:
my_first_job:
steps:
- name: Checks out your repository
uses: actions/checkout@v3
- name: Use a custom action stored within the repository
uses: ./.github/actions/my-custom-action
Sometimes you want to execute something on the runner without creating a customs action or invoking a public one. For this scenario you have the option to run a shell command with the keyword run: followed by the command you want to execute.
Example of a Step that runs a command on the host runner:
steps:
- name: My first step
run: echo This step runs a bash command on Mac and Linux.
On runners with macOS and Linux operating systems, the default shell is set to bash, and on Windows is set to PowerShell Core. You can change this by specifying the shell on the step.
steps:
- name: Bash run step
run: echo This step runs a bash command on all host runners.
shell: bash
If you have more than a command to run you can do it by using a pipe or vertical bar ( | ) at the run line of your YAML file and having the commands you want to execute as separate lines below it.
steps:
- name: Multiple Shell Commands
run: |
echo "First command"
echo "Second command"
Practical Example
Let’s put all these concepts together to create a basic GitHub Actions workflow. For this example, we will have a simple Next.js application that we will build and export as static HTML and finally deploy to an AWS S3 bucket.
We will assume that we already have a GitHub repository with our Next.js application and an AWS S3 bucket configured for static website hosting, but until now we were doing manual builds and deploys, and want to set up automation for every time we push to the main branch.
First, we will create a file for the Workflow. We can call this file whatever we want. For example, I named my workflow file nextjs.yml and stored it under the .github/workflows directory.
We gave the workflow a name that will be displayed on GitHub UI.
name: Build a Next.js site, export it as HTML, and deploy it to AWS S3
To define to what events we want this workflow to be triggered we use on:
. It is possible to specify more than one branch or to react to other events in the same workflow definition, but for this example, we will keep it simple and will only run on pushes targeting the main branch
on:
push:
branches:
- 'main'
Next, we define a job to build and deploy our website. We do this with jobs:
and give our only job an ID and a human-readable name. We also choose a runner for our job using runs-on:
to define the type of machine to run the job on. In this example, it will run on a Github-hosted runner with the latest version of Ubuntu Linux.
“`yaml
jobs:
Build_and_deploy:
name: Build and Deploy
runs-on: ubuntu-latest
Now is the time to define what we want our job in this workflow to do. We do this by creating a series of steps, each one either invoking an action or executing a command. To define our steps we use `steps:` and put each one as a separate item on a list starting with a dash and a space.
Checkout the commit that triggered the workflow.
```yaml
steps:
- name: Checkout
uses: actions/checkout@v3
Set up the Node.js environment, configure it to use Node version 18, and cache Yarn dependencies.
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'yarn'
Run yarn install
command to install the app dependencies.
- name: Install dependencies
run: yarn install
Run a command to build the Next.js app.
- name: Build with Next.js
run: yarn build
Export the Next.js app as static HTML.
- name: Static HTML export with Next.js
run: yarn export
Configure AWS credentials for our AWS account using an action maintained by the AWS team and using GitHub Actions secrets to store our AWS credentials.
- name: Configures AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-2
Run the command to deploy the website to an AWS S3 bucket. We use the AWS CLI which is available by default on all GitHub-hosted runners.
- name: Deploy to S3 bucket
run: aws s3 sync . s3://ghactionsnext
For the final workflow, we will split the build and deploy tasks into two separate jobs, and use artifacts to share the result site files between the build and deploy jobs.
Artifacts are files that are produced by a job in a workflow. They are uploaded and stored on GitHub servers to be preserved for use in subsequent jobs inside the same workflow or download via the GitHub UI or API. In our example, the artifact will contain the all files generated by the Next.js export and include all the HTML, CSS, and images files that compose our website.
name: Build a Next.js site, export it as HTML, and deploy it to AWS S3
on:
push:
branches:
- 'main'
jobs:
# Build job
build:
name: Build and Export
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: "18"
cache: 'yarn'
- name: Install dependencies
run: yarn install
- name: Build with Next.js
run: yarn build
- name: Static HTML export with Next.js
run: yarn export
- name: Upload artifact
uses: actions/upload-artifact@v3
with:
name: export
path: out
# Deploy job
deploy:
name: Deploy to AWS S3
runs-on: ubuntu-latest
needs: build
steps:
- name: Download artifacts
uses: actions/download-artifact@v3
with:
name: export
- name: Configures AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-2
- run: ls
- name: Deploy to S3 bucket
run: aws s3 sync . s3://ghactionsnext
Overall, GitHub Actions is a powerful and flexible automation tool that can help you streamline your workflow and increase productivity. I hope this can help you understand its basic principles and encourage you to do your first automation workflow.