Monday, 12 August 2024

Introduction to GitHub Actions





GitHub Actions (GHA): 
  • Workflow Automation Service offered by the GitHub
  • Allows you to automate all kinds of repository-related processes and actions
  • Service that offers various automations around the code that is stored on GitHub, around these repositories that hold that code.
  • Free for public repositories
Two main areas of processes that GitHub Actions can automate:
  • CI/CD processes (Continuous Integration/Continuous Delivery/Continuous Deployment) - methods for automating app development, testing, building and deployment
    • Continuous Integration is all about automatic handling code changes - integrating new code or code changes into an existing code base by building that code automatically. So that changed code by testing it automatically and by then merging it into existing code.
    • Continuous Delivery or deployment is about publishing new versions of your app or package or website automatically after the code has been tested and integrated
    • Example: After we make a change to the website code, we want to automatically upload and publish a new version of our website
    • GitHub Actions helps setting up, configuring and running such CI/CD workflows
    • It makes it very easy for us to set up processes that do automatically build, test, and publish new versions of our app, website, or package whenever we make a code change.
  • Code and repository management - automating:
    • code reviews
    • issue management

Key Elements


  • Workflows
  • Jobs
  • Steps


Workflows:
  • Attached to GitHub repositories
  • We can add as many workflows to GitHub repository as we wish
  • The first thing we build/create when setting up an automation process with GHA
  • Include one or more jobs
  • Built to set up some automated process that should be executed
  • Not executed all the time but on assigned triggers or events which define when a given workflow will be executed. Here are some examples of events that can be added:
    • an event that requires manual activation of a workflow
    • an event that executes a workflow whenever a new commit is pushed to a certain branch
  • Defined in a YAML file at this path: <repo_root>/.github/workflows/<workflow_name>.y[a]ml

Workflow can have the following elements:
  • name - name of the workflow
  • on - defines a workflow trigger
    • can have some of these values:
      • workflow_call - can have the following attributes:
        • inputs - an object containing one or more key-value pairs. inputs are only supported for workflow_dispatch (manual trigger). In each such pair a key is the names of the input variable which is used for referencing this input later in yaml document as ${{ inputs.<INPUT_VAR_NAME> }}. A value is an object containing one or more key-value pairs where keys can be:
          • required: boolean
          • type: string |
          • default - string, number or boolean - a default value of the input variable, if one is not specified/set 

      • pull_request
        • inputs are NOT allowed for workflows triggered by pull_request. The pull_request event does not accept user-defined inputs because it is triggered automatically when a PR is opened, synchronized, or updated.
        • If you need dynamic behavior in a pull_request workflow, you can:
          • Use environment variables (env)
          • Use a GitHub secret or variable (secrets / vars)
  • permissions - workflow permissions
  • jobs - list of jobs

name: ci

on:
  workflow_call:
    inputs:
      AWS_REGION:
        type: string
        default: eu-east-2
permissions:
  id-token: write # aws-actions/configure-aws-credentials (OIDC)
  contents: read
  pull-requests: write # actions/github-script to create comment in PR




Jobs:
  • Contain one or more steps that will be executed in the order in which they're specified
  • Define a runner
    • Execution environment, the machine and operating system that will be used for executing these steps
    • Can either be predefined by GitHub (runners for Linux, Mac OS, and Windows) or custom,  configured by ourselves
  • Steps will be executed in the specified runner environment/machine
  • If we have multiple jobs they run in parallel by default, but we can also configure them to run in sequential order, one job after another
  • We can also set up conditional jobs which will not always run, but which instead need a certain condition to be met.
Each job can have the following attributes:
  • name - Job name which will be shown in GitHub Actions tab
  • needs
  • runs-on - runner type: ubuntu-latest or custom
  • env - its value is an object containing one or more key-value pairs where key is the environment variable name and value is its value. These environment values have the scope of the current job. 
  • steps; Steps are listed within steps key
  • outputs: 


Example:

# create plan
plan:
  needs: [conflict, format, validate, lint, security]
  name: plan
  runs-on: ${{ inputs.RUNNER }}
  defaults:
    run: 
      working-directory: ${{ inputs.TF_ROOT }}
  outputs:
    tfplan_identifier: ${{ steps.plan.outputs.tfplan_identifier }}
  steps:
    - name: Step1
      run: echo "This is a Step 1" 
    - name: Step2
      run: echo "This is a Step 2" 



Steps:
  • Define the actual things that will be done
  • Example:
    • download the code in the first step
    • install the dependencies in the second step
    • run automated tests in the third step
  • Belong to jobs, and a job can have one or more steps
  • And a step is either:
    • a shell script
    • a command in the command line that should be executed (e.g. for simple tasks), or 
    • an action, which is another important building block
      • predefined scripts that performs a certain task
      • We can build our own actions or use third party actions
  • We must have at least have one step,
  • Steps are then executed in order, they don't run in parallel, but instead, step after step
  • Steps can also be conditional

Steps are elements of yaml list named steps

A step element can contain the following keys:
  • name - step name (step title)
  • id - a string uniquely identifying the step within enclosing job. Used in references to this step e.g. ${{ steps.<id>.outputs.<output_variable_name> }}
  • run - 
  • uses - name of the reusable GitHub Action 
  • with
  • env - its value is an object containing one or more key-value pairs where key is the environment variable name and value is its value. These environment values have the scope of the current step. To access its value, use ${process.env.<ENV_VAR_NAME>}.
    • environment variable can be used as workflow's internal variable 
    • some actions require certain environment variables to be defined and set e.g. https://github.com/terraform-linters/tflint/blob/master/docs/user-guide/plugins.md#avoiding-rate-limiting
  • continue-on-error - a boolean (true|false) value denoting whether execution of the job can resume even if this step errors out
  • run - an arbitrary bash script

Example step:

- name: Step 1
  uses: actions/github-script@v7
  env:
     PLAN_OUTPUT: "${{ steps.plan.outputs.stdout }}"
  with:
     script: |
        let plan = "${process.env. PLAN_OUTPUT}"

- name: TFLint Init
  run: tflint --init
  env:
     # https://github.com/terraform-linters/tflint/blob/master/docs/user-guide/plugins.md#avoiding-rate-limiting
     GITHUB_TOKEN: ${{ github.token }}



- name: Terraform Plan
  id: plan
  continue-on-error: true
  env:
    TF_ROOT: "${{ inputs.TF_ROOT }}" 
  run: |
    tfplan_identifier="${{ inputs.TF_APP_NAME }}-tfplan-expected"
    echo "tfplan_identifier=$tfplan_identifier" >> $GITHUB_OUTPUT
    terraform plan -var-file="prod.tfvars" -input=false -no-color -out=${tfplan_identifier}
    terraform-bin show -no-color $tfplan_identifier > "$tfplan_identifier.log"



How to create a Workflow?


Workflow can be created in two ways:
  • directly on the remote, via browser
  • in the local repo, and then pushed to remote
If we use browser, we need to go to our repo's web page and then click on Actions tab. There we can select a default Workflow or choose some other template. Default workflow creates the following file:

my-repo/.github/workflows/blank.yml:

# This is a basic workflow to help you get started with Actions

name: ci-prod-my-app

# Controls when the workflow will run
on:
  # Triggers the workflow on push or pull request events but only for the "main" branch
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

  # Allows you to run this workflow manually from the Actions tab
  workflow_dispatch:

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
  # This workflow contains a single job called "build"
  build:
    # The type of runner that the job will run on
    runs-on: ubuntu-latest

    # Steps represent a sequence of tasks that will be executed as part of the job
    steps:
      # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
      - uses: actions/checkout@v4

      # Runs a single command using the runners shell
      - name: Run a one-line script
        run: echo Hello, world!

      # Runs a set of commands using the runners shell
      - name: Run a multi-line script
        run: |
          echo Add other actions to build,
          echo test, and deploy your project.


How to trigger a workflow?



Workflows can be triggered on various events:
  • pushing changes on an arbitrary branch
  • creating a pull request (including a draft PR)
  • pushing a tag to remote
  • manually
  • ...
Certain triggers (like workflow_dispatch) requires the new workflow to be merged to main branch first before showing up in GitHub Actions tab.

CI workflows are usually triggered on pushing changes to remote or on creating a pull request.

Trigger on push to any branch:

on:
  push:
    branches:
      - '**'

Trigger on pull request on master branch:

on:
  pull_request:
    branches:
      - master

Trigger on pull request on master and prod branch:

on:
  pull_request:
    branches: [main, prod]


Trigger on pull request that include files at any of these paths:

on:
  pull_request:
    paths: ["path/to/my-app/prod/app/**", "terraform/modules/**", ".github/workflows/my-app-prod-tf-ci.yaml"]


CD workflows are usually triggered on pushing a tag to remote:

on:
  push:
    tags:
      - "my-app/prod/v*"


Workflows which un-deploy the apps or destroy resources are usually triggered manually:

on:
  workflow_dispatch:
   
workflow_dispatch-triggered workflow files need to be on the default branch (e.g. main). See Events that trigger workflows - GitHub Docs.

Once files are there, we can select against which branch we want to execute this workflow:



If we define input variables, they will be listed and we can enter their values:




CD (Deployment) workflows deploy applications and/or provision resources on the remote infrastructure. We want to be 100% sure that CD workflow won't deploy e.g. changes from test branch into production. To make sure there will be no mismatch in the commit hash and deployment environment, we can specify deployment environment in the workflow and also in GitHub specify protection rules for it e.g. which tag (defined by tag format) can deploy in that environment. 


Useful Reusable Actions



Examples



---

No comments: