GitHub: GitHub Actions overview and ArgoCD deployment example

GitHub Actions overview: main capabilities, components, creating a workflow, working with Events, Secrets, and ArgoCD deployment example

GitHub: GitHub Actions overview and ArgoCD deployment example

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

So, let’s take a closer look at its abilities, and 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:

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:

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:

Also, GitHub Actions can work 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 downloading 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, the 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 consists of one or more actions

  3. actions are running on a runner, and multiple 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 jobs, 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 triggered with different events:

$ mkdir -p .github/workflows

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

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

Go to the GitHub Web UI, switch to the Actions tab, and 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 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

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

$ git checkout -b a-new-branch
Switched to a new branch ‘a-new-branch’
$ git push -u origin a-new-branch

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 job, or per 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 except the run, for example in the if conditions (will be discussed below)

  • environment variable  —  $VARNAME: a value will be set during a task execution from the run on a runner

  • to create your 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 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 the 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 allow 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 has no permissions to use ArgoCD tokens (do not do this on Production!):

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

Log in:

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

Create a token:

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

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.

Originally published at RTFM: Linux, DevOps, and system administration.