DEV Community

Cover image for Creating our first workflow
Kostas Kalafatis
Kostas Kalafatis

Posted on

Creating our first workflow

What are GitHub Actions?

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.

What are GitHub Workflows?

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.

Image description

Let's go through the workflow step by step:

  1. An Event has to occur for the workflow to run. In our case a user has opened a pull request.
  2. The Workflow, is now triggered and it will run each job. In our case, the workflow will run a single job.
  3. 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
  4. 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.
  5. 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 restore shell command.
    • Build: Runs the dotnet build shell command.
    • Test: Runs the dotnet test shell command.
  6. Finally, our Setup .NET Core SDK step will run the setup-dotnet action. 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.

Before We Begin

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.

Setting up the repository

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 first-workflow.yaml file.

Anatomy of a Workflow

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

The 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

The 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:

on: push
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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.

jobs

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 runs-on key.

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
Enter fullscreen mode Exit fullscreen mode

runs-on

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.

runs-on: windows-latest
Enter fullscreen mode Exit fullscreen mode

steps

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!"
Enter fullscreen mode Exit fullscreen mode

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: actions/aws@v2.0.1
Enter fullscreen mode Exit fullscreen mode

Creating our first workflow

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.

Under the .github/workflows directory, create a new yaml file. This will have the configuration of our workflow.

Image description

We will start by making a workflow that works onubuntu-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)"
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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.

Image description

By clicking on a workflow run, you can see the progress and the steps of the workflow

Image description

And with that we have created our first workflow.

Parallel and Dependent Jobs

There are two fundamental approaches for structuring our workflows: parallel jobs and dependent jobs.

Parallel 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 windows.

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
Enter fullscreen mode Exit fullscreen mode

This workflow will run these two jobs in parallel, one in an Ubuntu machine, and one in a Windows machine.

Dependent Jobs

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
Enter fullscreen mode Exit fullscreen mode

Expanding our workflow

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

If we update and commit our workflow, we will get yet another workflow run:

Image description

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.

Image description

Conclusion

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.

Top comments (0)