Github: Github Actions overview and ArgoCD deployment example

By | 05/07/2021

Github Actions actually is very similar to the TravisCI, but have much more closer integration with Github, and even its interface is included in the Github WebUI:

So, let’s take a closer look at its abilities, how to use it, and in the following posts will deploy its self-hosted runners to a Kubernetes cluster and will build a CI/CD pipeline to deploy applications using Github Actions and ArgoCD.

Github Actions pricing

Documentation is here>>>.

GitHub Actions is free for all account types but with some limitations:

Product Storage Minutes (per month)
GitHub Free 500 MB 2,000
GitHub Pro 1 GB 3,000
GitHub Free for organizations 500 MB 2,000
GitHub Team 2 GB 3,000
GitHub Enterprise Cloud 50 GB 50,000

For example, our project uses GitHub Team, thus we can have 2 gigabytes and 3000 minutes per month.

With this, minutes are different for Linux, macOS, and Windows:

Operating system Minute multiplier
Linux 1
macOS 10
Windows 2

I.e. from our 3000 total, we can use only 300 minutes if we’re using macOS agents and every additional minute will cost additional money:

OS Resources Price per extra minute
Linux 2 cores, 7 GB $ 0,008
Windows 2 cores, 7 GB $0,016
MacOS 2 cores, 7 GB $ 0,08
Self-hosted free

Also, Github Actions can be working in Github Cloud, and as self-hosted runners, which can solve an issue with access to your secured resources because Github haven’t static IP ranges, so you’re not able to configure your SecurityGroup/firewalls.

Github suggests periodically download a json-file with updated networks (btw, Github Actions is working on Microsoft Azure), but I’m too lazy to create some additional automation to update the security configuration.

Github Actions: an overview 

In the Actions, build flow is the following (see Introduction to GitHub Actions):

  1. an event (for example, a pull-request or a commit to a repository, see the full list here>>>) triggers a workflow, which contains jobs
  2. a job contains a list of steps, and every step consist of one or more actions
  3. actions are running on a runner, and multiply actions of a  workflow can be running simultaneously

The main components are:

  • runner: a server running on Github Cloud or self-hosted, which will execute a job
  • workflow: a procedure described in YAML, that includes one or more job, and is triggered by an event
  • jobs: a set of steps that are running on the same runner. If a workflow has multiple jobs, by default they will be started in parallel, but also can be configured with dependencies from each other
  • steps: зa task to execute a common command or actions. As steps of the same job are running on the same runner, they can share data with each other.
  • actions: main “execution blocks” – can be a set of already prepared tasks, or run simple commands

A workflow file structure

In short, let’s see how a workflow file is built:

  • name: a workflow name
  • on: an event(s), that will trigger this workflow
  • jobs: a list of tasks of this workflow
    • <JOB_NAME>
      • runs-on: a runner, which will execute job(s)
      • steps: tasks in this job to be executed with uses or run
        • uses: an action to execute
        • run: a command to execute

Getting started: Creating workflow file

Let’s start with a simple file to see how it works.

In your repository root create a directory called .github/workflows/ – here we will store all workflows, that can be trigger with different events:

[simterm]

$ mkdir -p .github/workflows

[/simterm]

In this directory, create a file for your flow, for example, named as .github/workflows/actions-test.yaml:

name: actions-test
on: [push]
jobs:
  print-hello:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Hello, world"

Save it and push to the Github:

[simterm]

$ git add .github/workflows/actions-test.yaml && git commit -m "Test flow" && git push

[/simterm]

Go to the Github WebUI, switch to the Actions tab, you’ll see this workflow execution:

Events

In Events, you can describe conditions to run that flow.

Such a condition can be a pull request or commit to a repository, a schedule, or some event outside of Github that will run a webhook to your repository.

Also, you can configure those conditions for different branches of the repository:

name: actions-test
on: 
  push:
    branches:
      - master
  pull_request:
    branches:
      - test-branch
...

Or use a cronjob, see the Scheduled events:

name: actions-test
on: 
  schedule:
    - cron: '* * * *'

Manual trigger – workflow_dispatch

Also, you can configure an ability to execute a workflow manually by using the workflow_dispatch in the on:

name: actions-test
on: 
   workflow_dispatch
jobs:
  print-hello:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Hello, world"

And after this, in the Actions you’ll get a button to run that flow:

Workflow inputs

In your workflow, you also can add some inputs that will be available as variables in steps via the github.event context:

name: actions-test
on: 
   workflow_dispatch:
     inputs:
       userName:
         description: "Username"
         required: true
         default: "Diablo"
jobs:
  print-hello:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Username: ${{ github.event.inputs.username }}"
      - run: echo "Actor's username: ${{ github.actor }}"

Here, in the ${{ github.event.inputs.username }} we are getting a value of the workflow_dispatch.inputs.userName, and in the github.actor receiving the Github Actions metadata :

A use case can be, for example, to pass a Docker image tag to deploy with ArgoCD.

Webhooks: create

Beside of the push which we’ve used above we can configure our workflow on any other event in a repository.

See the full list in the Webhook events.

As another example let’s configure our flow to be running when a new branch or tag is created by using the create:

name: actions-test
on: 
  create
jobs:
  print-hello:
    runs-on: ubuntu-latest
    steps:
      - run: |
          echo "Event name: ${{ github.event_name }}"
          echo "Actor's username: ${{ github.actor }}"

Here the ${{ github.event_name }} is used to display the trigger name.

Create a new branch and push it:

[simterm]

$ git checkout -b a-new-branch
Switched to a new branch 'a-new-branch'

$ git push -u origin a-new-branch

[/simterm]

Check:

Environment variables

Also, Github Actions supports environment variables in workflows.

There is a list of the default variables, see the Default environment variables, and you can create your own on a workflow level, jobs level, per a job, or per a step.

During this, pay attention that you access variables in different ways, see the About environment variables:

  • context variable${{ env.VARNAME }}: a value will be set during a workflow file preprocessing before it will be sent to a runner, use it everywhere excepting the run, for example in the if conditions (will be discussed below)
  • environment variable$VARNAME: a value will set during a task execution from the run on a runner
  • to create an own variable during a job’s execution, use a specific file that is set in the default $GITHUB_ENV variable

Variables example:

name: vars-test

on:
  push

env:
  VAR_NAME: "Global value"

jobs:
  print-vars:
    runs-on: ubuntu-latest
    steps:

      # using own varibales
      - name: "Test global var as $VAR_NAME"
        run: echo "Test value $VAR_NAME"

      - name: "Test global var as ${{ env.VAR_NAME }}"
        run: echo "Test value ${{ env.VAR_NAME }}"

      # using default variables
      - name: "Test job var as $GITHUB_REPOSITORY"
        run: echo "Test value $GITHUB_REPOSITORY"

      # this will be empty, as default variables are not in the context
      - name: "Test job var as ${{ env.GITHUB_REPOSITORY }}"
        run: echo "Test value ${{ env.GITHUB_REPOSITORY }}"

      # using 'dynamic' variables
      - name: "Set local var"
        run: echo "local_var=local value" >> $GITHUB_ENV

      - name: "Print local var as $local_var"
        run: echo "$local_var"

      - name: "Print local var as ${{ env.local_var }}"
        run: echo "${{ env.local_var }}"

And result:

Secrets

Documentation is here>>>.

A secret can be added in a repository Settings > Secrets:

Now, add its use in a workflow. A secret can be cases directly via the ${{ secret.SECRETNAME }} or can be set to a variable:

name: actions-test

on: 
  push

env:
  TEST_ENV: ${{ secrets.TEST_SECRET }}

jobs:
  print-hello:
    runs-on: ubuntu-latest
    steps:
      - run: |
          echo "Test secret: ${{ secrets.TEST_SECRET }}"
          echo "Test secret: ${{ env.TEST_ENV }}"

Run the flow:

Conditions and if

Github Actions supports a conditions check for jobs by using the if operator followed by an expression, see the About contexts and expressions.

An example:

name: actions-test

on:
  push
  
jobs:
  print-hello:
    runs-on: ubuntu-latest
    steps:

      - id: 'zero'
        run: echo "${{ github.actor }}"

      - id: 'one'
        run: echo "Running because of 'github.actor' contains a 'setevoy' string"
        if: "contains(github.actor, 'setevoy')"

      # this will not run
      - id: 'two'
        run: echo "Skipping because of 'github.actor' contains a 'setevoy' string"
        if: "!contains(github.actor, 'setevoy')"

      - id: 'three'
        run: echo "Running because of Step Two was skipped"
        if: steps.two.conclusion == 'skipped'

      - id: 'four'
        run: echo "Running because of commit message was '${{ github.event.commits[0].message }}'"
        if: contains(github.event.commits[0].message, 'if set')

      - id: 'five'
        run: echo "Running because of previous Step was successful and the trigger event was 'push'"
        if: success() && github.event_name == 'push'

Here, we are using the github context, the contains() function, != and && operators, and steps context to check the condition.

The result will be:

needs – jobs dependency

Beside of the  if: success() in steps, you can add jobs dependency on each other by using the needs:

name: actions-test

on: 
  push

jobs:

  init:
    runs-on: ubuntu-latest
    steps:
      - run: echo "An init job"

  build:
    runs-on: ubuntu-latest
    steps:
      - run: echo "A build job" && exit 1
    needs: 'init'

  deploy:
    runs-on: ubuntu-latest
    steps:
      - run: echo "A deploy job"
    if: always()
    needs: ['init', 'build']

Here, in the build job we are waiting for the init job to finish, and in the deploy job waiting for both init and build, and by using the if: always() we’ve set to run the deploy job regardless of the result of execution of the dependency jobs:

Actions

And the last thing to take a look at is the main component of the Github Actions – the Actions.

Actions allows us to use already existing scripts and utilities from the Github Actions Marketplace, or Docker images from the Docker Hub.

See the Finding and customizing actions.

In the example below, we will use the actions/checkout@v2 to clone a repository roo a runner-agent, and omegion/argocd-app-actions to synchronize an ArgoCD application (see the ArgoCD: an overview, SSL configuration, and an application deploy post for details).

An ArgoCD application

Let’s create a testing application:

Update the argocd-cm ConfigMap, as by default the admin user have no permissions to use ArgoCD tokens (do not do this on Production!:

...
data:
  accounts.admin: apiKey,login
...

Log in:

[simterm]

$ argocd login dev-1-18.argocd.example.com
Username: admin
Password: 
'admin' logged in successfully
Context 'dev-1-18.argocd.example.com' updated

[/simterm]

Create a token:

[simterm]

$ argocd account generate-token
eyJ***3Pc

[/simterm]

Github Actions workflow for ArgoCD

Add a Secret with this token:

Create a new workflow:

name: "ArgoCD sync"
on: "push"
jobs:
  build:
    runs-on: ubuntu-latest
    steps:

      - name: "Clone reposiory"
        uses: actions/checkout@v2
        with:
          repository: "argoproj/argocd-example-apps.git"
          ref: "master"

      - name: "Sync ArgoCD Application"
        uses: omegion/argocd-app-actions@master
        with:
          address: "dev-1-18.argocd.example.com"
          token: ${{ secrets.ARGOCD_TOKEN }}
          appName: "guestbook"

Push it to a repository, and check its execution:

And the application in ArgoCD now is synchronized:

Done.