Continuous Integration and Continuous Deployment (CI/CD) with GitHub Actions

Overview

CI/CD is a set of practices in software development that aims to automate the process of building, testing, and releasing software.

Here's a breakdown of the two parts:

  • Continuous integration (CI)

    Involves frequently merging code changes from multiple developers into a central repository. This is typically done with every small change or a few times a day. Automated tests are then run to ensure the merged code doesn't break anything. This helps catch bugs early and prevents them from accumulating.

  • Continuous delivery (CD)

    Automates the delivery of code changes to different environments, like testing or staging. This allows for frequent releases and makes it easier to deploy new features or fixes. In some cases, CD can also refer to continuous deployment, which means automatically deploying changes directly to production.

By automating these tasks, CI/CD helps development teams release software faster and more reliably. It also improves the quality of software by catching bugs early in the development process.

In the next sections, we will review the CI/CD Pipeline setup in GitHub Actions for jfs-web.

GitHub Actions Setup

The primary workflow for jfs-web is defined in .github/workflows/cicd.yml.

Here's an overview of the steps in that workflow:

CI Workflow

The CI workflow is triggered on every push and pull request to the main branch. It consists of the following jobs:

  1. Setup: Prepares the necessary environment for the build and test processes.
  2. Build: Compiles the source code and packages it into a Docker image.
  3. Test: Runs the automated tests.
  4. Security Scans: Performs security scanning on the Docker image.

CD Workflow

The CD workflow is triggered after the CI workflow completes successfully. It consists of the following jobs:

  1. Publish Image: Publishes the Docker image to a container registry in Google Cloud Platform.
  2. Deploy: Deploys the Docker image to a production environment.

GitHub Actions CICD Pipeline

Demo

Here's a demo of the CI/CD pipeline in action:

Workflow Examples

Here are some snippets from my GitHub Actions setup:

Setup

This job generates a unique image tag for this workflow run that gets used in the subsequent jobs. Here's an example tag 2024-07-01--04-58-9fc1eb90.

setup:
  runs-on: ubuntu-latest
  outputs:
    image_tag: ${{ steps.image_tag.outputs.image_tag }}
  steps:
    - name: Determine Image Tag
      id: image_tag
      run: |
          export SHA=${{ github.sha }}
          export SHORT_SHA=${SHA:0:8}
          export DATE=$(date -u "+%Y-%m-%d--%H-%M")
          export FULL_TAG=${DATE}-${SHORT_SHA}
          echo "image_tag=${FULL_TAG}"
          echo "image_tag=${FULL_TAG}" >> "$GITHUB_OUTPUT"
          echo "image_tag=${FULL_TAG}" >> "$GITHUB_STEP_SUMMARY"

Build

This job uses the setup-env, build, and package actions. This downloads dependencies, compiles css and templ files, and packages the application using a dockerfile.

build:
  needs: [setup]
  runs-on: ubuntu-latest
  steps:
    - name: Checkout
      uses: actions/checkout@v4

    - name: Setup Build Environment
      uses: ./.github/actions/setup-env

    - name: Build Application
      uses: ./.github/actions/build

    - name: Package Application
      uses: ./.github/actions/package
      with:
        IMAGE_TAG: ${{ needs.setup.outputs.image_tag }}
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Note: Build and Test jobs reuse similar actions because each job is a seperate GitHub Runner VM. This can be improved by building and caching the built files.

Test

This job uses the setup-env, build, and test actions. This downloads dependencies, compiles css and templ files, and runs a go test.

test:
  needs: [setup]
  runs-on: ubuntu-latest
  steps:
    - name: Checkout
      uses: actions/checkout@v4

    - name: Setup Build Environment
      uses: ./.github/actions/setup-env

    - name: Build Application
      uses: ./.github/actions/build

    - name: Test Application
      uses: ./.github/actions/test
Note: Build and Test jobs reuse similar actions because each job is a seperate GitHub Runner VM. This can be improved by building and caching the built files.

Security Scans

This job uses the security-scan action to scan the docker image for os and library vulnerabilities using aquasecurity/[email protected].

security_scans:
  needs: [setup, build]
  runs-on: ubuntu-latest
  steps:
    - name: Checkout
      uses: actions/checkout@v4

    - name: Security Scan
      uses: ./.github/actions/security-scan
      with:
        IMAGE_TAG: ${{ needs.setup.outputs.image_tag }}
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Publish Image

This job uses the publish action to publish the docker image to Google Cloud Platform Artifact Registry. Under the hood, I use google-github-actions/auth@v2 to authenticate with my GCP account, login to the artifact registry with docker/login-action@v3 and push the docker image.

publish_image:
  needs: [setup, build, test, security_scans]
  runs-on: ubuntu-latest
  permissions:
    packages: "read"
    contents: "read"
    id-token: "write"
  steps:
    - name: Checkout
      uses: actions/checkout@v4

    - name: Publish Application
      uses: ./.github/actions/publish
      with:
        IMAGE_TAG: ${{ needs.setup.outputs.image_tag }}
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        GCP_REGION: ${{ vars.GCP_REGION }}
        GCP_PROJECT_ID: ${{ vars.GCP_PROJECT_ID }}
        GCP_WIP: ${{ vars.GCP_WIP }}
        GCP_SERVICE_ACCOUNT: ${{ vars.GCP_SERVICE_ACCOUNT }}
        GCP_CONTAINER_REPO: ${{ vars.GCP_CONTAINER_REPO }}

Deploy

This job uses the deploy action to deploy the application to Google Cloud Platform. Under the hood, I use google-github-actions/auth@v2 to authenticate with my GCP account, and deploy a new release to Cloud Run with the google-github-actions/deploy-cloudrun@v2 action.

deploy:
  needs: [setup, publish_image]
  runs-on: ubuntu-latest
  permissions:
    packages: "read"
    contents: "read"
    id-token: "write"
  steps:
    - name: Checkout
      uses: actions/checkout@v4

    - name: Deploy Application
      uses: ./.github/actions/deploy
      with:
        IMAGE_TAG: ${{ needs.setup.outputs.image_tag }}
        GCP_REGION: ${{ vars.GCP_REGION }}
        GCP_PROJECT_ID: ${{ vars.GCP_PROJECT_ID }}
        GCP_WIP: ${{ vars.GCP_WIP }}
        GCP_SERVICE_ACCOUNT: ${{ vars.GCP_SERVICE_ACCOUNT }}
        GCP_CONTAINER_REPO: ${{ vars.GCP_CONTAINER_REPO }}