Creating our first workflow
Tags: beginners, tutorial, devops, github
Published: 2023-09-11 14:08:56

## 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ynrletvju7jixkupqrdf.png)

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:
```yaml
on: push
```

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:
```yaml
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]([Events that trigger workflows - GitHub Docs](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows)).

### 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.
```yaml
jobs:
  the-first-job:
    name: The first job to run
  the-second-job:
    name: The second job to run
```

### 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](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#choosing-github-hosted-runners), a [larger runner](https://docs.github.com/en/actions/using-github-hosted-runners/about-larger-runners), or a [self-hosted runner](https://docs.github.com/en/actions/hosting-your-own-runners).

For example, we can specify that our job will run on a Windows machine with the latest sable image that GitHub provides. 
```yaml
runs-on: windows-latest
```

## 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.
```yaml
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
```yaml
steps:
  - name: Heroku step
    uses: actions/heroku@main
  - name: AWS step
    uses: actions/aws@v2.0.1
```

## 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wxhh2pwfdw67k7dvf2m8.png)

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.
```yaml
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.
```shell
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.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8py2gekeef6buex95duw.png)

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t29e1kuiaiv7n8h20lbu.gif)

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`.

```yaml
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
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.

```yaml
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
```

### 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.
```yaml
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.
```yaml
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:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m8xqkhcms5i5rtpqmjdb.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0x3fc6h3pbjdzrfh8ikv.png)

## 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.