GitHub Actions is a CI/CD platform that allows us to automate our software development workflows. GitHub Actions can react to some internal or external event on our repository and run some workflow based on that event.
Events are things that happen in our repository. They can be either internal or external events. Internal events are events that originate from GitHub, such as pushes, pull requests, etc. External events are events that originate outside of GitHub, such as webhooks, or even a manual push of the Run button.
These events will trigger a workflow associated with them.
Workflows are configurable, automated processes that we can setup in our repository. These processes will be triggered by events, and will perform one or many tasks. Each workflow can contain multiple jobs, and each job can contain multiple steps.
Let's go through the workflow step by step:
- An Event has to occur for the workflow to run. In our case a user has opened a pull request.
- The Workflow, is now triggered and it will run each job. In our case, the workflow will run a single job.
- The Job is the building block of our workflow. It contains a set of steps that run on a specific runner environment. By default, our jobs will run in parallel, but we can configure our workflow to specify dependencies. Our job will now assign a Runner Machine
- The Runner Machine is the machine that will execute our job. These machines can be either GitHub-hosted or self-hosted. GitHub-hosted runners are VMs provided by GitHub and can have various operating systems and software tools installed. Self-hosted runners are machines that we manage and connect to GitHub. Once the Runner Machine is assigned by the Job it will start performing the Steps.
- A Step is an individual task that runs sequentially in a job. They can be either actions or shell commands. Our job has 4 steps:
- Setup .NET Core SDK: Installs .NET SDK, adds binaries to PATH, and caches NuGet packages.
Install Dependencies: Runs the
dotnet restoreshell command.
Build: Runs the
dotnet buildshell command.
Test: Runs the
dotnet testshell command.
- Finally, our
Setup .NET Core SDKstep will run the
setup-dotnetaction. An Action is a reusable unit of code that runs as part of a job. They can be either predefined or custom. Predefined actions are provided by the GitHub Marketplace. Custom actions are created by others in their own repositories.
In order to create our first workflow, we only need a GitHub repository. But in order to avoid repository pollution it is better to create a new repository and work from there. In this part we are going to create a very basic workflow and work from there.
And with this out of the way lets begin.
Before we can begin working on our workflow, we must first build some folders in our repository that GitHub requires. The first folder to create is the
.github folder. This is a separate folder where we can store GitHub-related files. The
.github folder may include GitHub-related files like workflows, issue templates, pull request templates, funding information, and other project-specific files.
Next, we need to create the
.workflow folder. This will contain all of our workflow configurations. Finally, we need a configuration for our workflow. Let's create a
Our workflow will be defined in the
first-workflow.yaml file we created. But workflows have a specific syntax that we need to follow.
name key specifies the name of the workflow. GitHub will display the names of our workflows under the repository's
Actions tab. If we decide not to use the
name key, GitHub will display the path relative to the root repository. So it is better to have a
name key to keep things neat and organized.
on key defines which events can trigger the workflow to run. We can define multiple events that can trigger the workflow. We can even set a time schedule and also restrict the execution of the workflow to only specific files or branch changes.
For example, we can define that our workflow will work when a push is made to any branch in the workflow's repository:
We can also specify multiple triggering events. For example, we can specify that our workflow will be triggered when a push is made to any branch in the repository, or when someone opens a pull request:
on: [push, pull_request]
If we specify multiple events, only one of the triggers need to occur for our workflow to run. If multiple triggering events occur at the same time, multiple workflow runs will be triggered.
There are literally dozens of triggering events, and we are not going to cover all of them here, but if you are interested, you can read this GitHub Doc.
A workflow run is made up of one or more jobs that, by default, operate in parallel. We can define dependencies on other jobs to make them run in sequence, but we'll get to that later. Each job is executed in a runner environment specified by the
We can also give our
job a unique identifier. The unique identifier is a string and its value is a map of the job's configuration data. Finally we can set a name for the job, which is displayed in the GitHub UI.
jobs: the-first-job: name: The first job to run the-second-job: name: The second job to run
A job needs to be executed in a runner environment. To do that, we can define the type of machine to run the job on, using the
runs-on key. The destination machine can be a GitHub-hosted runner, a larger runner, or a self-hosted runner.
For example, we can specify that our job will run on a Windows machine with the latest sable image that GitHub provides.
A job consists of a series of duties known as steps. Steps can execute commands, establish tasks, or execute an action in our repository, a public repository, or an action published in a Docker registry.
Not all steps run actions, but all actions run as a step. Each step runs in its own process in the runner environment and has access to the workspace and disk. Because steps operate in their own process, changes to environment variables are not saved between steps. GitHub also includes steps for setting up and finishing a task.
GitHub only shows the first 1,000 checks, but you can perform an unlimited amount of steps as long as you stay within the workflow use restrictions.
steps: - name: Print a greeting run: echo "Hello World!"
We can also use actions published in a public repository. For that, we need a branch, ref, or SHA of the public GitHub repository. For example, we can use actions that specified in Heroku and AWS repositories
steps: - name: Heroku step uses: actions/heroku@main - name: AWS step uses: email@example.com
Now that we have discussed the basics of a workflow, let's create our first workflow. First open an editor that you are comfortable with, and navigate to the folder containing your repository. I am going to use VSCode, but you are free to use any editor you like.
.github/workflows directory, create a new
yaml file. This will have the configuration of our workflow.
We will start by making a workflow that works on
ubuntu-latest. It will have one job with two steps. The first step will execute one command, and the second step will execute multiple commands represented by a YAML multiline tool.
name: Hello World Workflow on: [push] jobs: run-shell: runs-on: ubuntu-latest steps: - name: echo a string run: echo "Hello World" - name: Get environment versions run: | echo "Checking environment versions" echo "Node Version: $(node --version)" echo "NPM Version: $(npm --version)" echo "NuGet Version: $(nuget help | grep Version)"
After you've created this workflow, save it and push it upstream. I usually utilize the console to perform these tasks, so here's a brief refresher on how to accomplish it.
git add . git commit -m "My first workflow" git push origin/<your-branch-name>
Once you push the changes, open your repository and click on the
Actions button. You will notice that the workflow is running on the repository.
By clicking on a workflow run, you can see the progress and the steps of the workflow
And with that we have created our first workflow.
There are two fundamental approaches for structuring our workflows: parallel jobs and dependent jobs.
As we discussed earlier, the default running mode of GitHub actions is parallel jobs. Parallel jobs are a way to run multiple tasks in a workflow at the same time, instead of waiting for each task to finish before starting the next one. This can save time and resources, especially when the tasks are independent of each other. For example we can run tests on different operating systems in parallel.
To run parallel jobs, we just need to define them in the
jobs section of our workflow file. Each job has a unique identifier and a
runs-on keyword that specifies the runner environment. By default, all jobs run in parallel, unless we specify dependencies. For example, this workflow will run two parallel jobs, one on
ubuntu and one on
name: Parallel Jobs Example on: [push] jobs: build-ubuntu: name: build and test on ubuntu runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup .NET Core uses: actions/setup=dotnet@v3 - name: Install dependencies run: dotnet restore - name: Build run: dotnet build --configuration Release --no-restore - name: Test run: dotnet test --no-restore --verbosity normal build-windows: name: build and test on windows runs-on: windows-latest steps: - uses: actions/checkout@v3 - name: Setup .NET Core uses: actions/setup=dotnet@v3 - name: Install dependencies run: dotnet restore - name: Build run: dotnet build --configuration Release --no-restore - name: Test run: dotnet test --no-restore --verbosity normal
This workflow will run these two jobs in parallel, one in an Ubuntu machine, and one in a Windows machine.
Dependent jobs are jobs that require one or more jobs to complete successfully before they can start. This can be useful when we have tasks that depend on the output or status of previous tasks, such as building the code before testing it. We can also use dependent jobs to control the order of execution, such as running a cleanup job after all other jobs are completed.
To create dependent jobs, we need to use the
needs keyword in the
job section of our workflow file. The
needs keyword takes either a single job identifier or an array of identifiers. The job that has the
needs keyword will wait for all the dependencies to finish successfully before running.
For example we can create a job that deploys the code to a server and another job that sends a notification.
name: Dependent Jobs Example on: [push] jobs: deploy: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v2 - name: Deploy code run: make deploy notify: needs: deploy runs-on: ubuntu-latest steps: - name: Send notification run: make notify
Let's now expand our workflow to add one parallel and one dependent job.
First we are going to add a parallel job that runs on a
MacOS machine. To do it just add the following lines in the workflow.
macos-parallel-job: runs-on: macos-latest steps: - name: View SW Version run: sw_vers
The above YAML snippet will add another job in our existing workflow and run some
MacOS specific code.
Next up, lets add a dependent job. For the dependent job we are going to do something similar but on a
Windows machine. To do this, lets add another job in our workflow configuration.
windows-dependent-job: runs-on: windows-latest needs: run-shell steps: - name: echo a string run: Write-Output "Windows String"
If we update and commit our workflow, we will get yet another workflow run:
Opening the workflow run, we can see that the
run-shell and the
macos-parallel-job jobs ran in parallel, while the
windows-dependent-job was waiting for
run-shell job to successfully complete.
This guide is all about GitHub Actions and Workflows. We discussed the basics and made our first workflow. We also discussed how to use GitHub Actions to do things automatically, like testing, deploying, and responding to events. This can help you save time and work better.
GitHub Actions let you create your own automation platform that works with your development needs. No matter what kind of developer you are, you can use GitHub Actions to make your life easier.
We discussed how GitHub Actions and Workflows work, how to run jobs in parallel or in order, and how to set up your repo for automation. With this knowledge, you can improve your development by automating boring tasks and making sure your code works well.
And don’t forget, if you liked this post, please show some love with a heart. ❤️ Have fun, and see you on the next iteration where we are going to dive deeper in the inner workings of GitHub Actions.