Automating your workflow with GitHub Actions

Automating your workflow with GitHub Actions

From basics to advanced deployments with real-world examples

Introduction

With GitHub Actions, developers can automate activities, workflows, and processes within their repositories. GitHub Actions is a potent CI/CD (Continuous Integration and Continuous Deployment) solution that is incorporated directly into GitHub. You can build, test, and publish your code directly from GitHub using GitHub Actions, which boosts productivity and expedites development cycles. We'll go over the fundamentals of GitHub Actions, examine its basic ideas, and offer some examples in this blog post so you can get going right away.

What are GitHub Actions?

GitHub Actions allows you to automate tasks and workflows based on events within your GitHub repository. These events can include pushes to the repository, creating pull requests, and more. There is a wide range of possibilities for actions which can be used to trigger these workflows which are defined using YAML syntax and stored in the .github/workflows directory of your repository.

Key Concepts

  1. Workflow: A workflow is an automated process defined in a YAML file that runs one or more jobs. Workflows are triggered by specific events (e.g., push, pull_request).

  2. Job: A job is a set of steps that execute on the same runner. Jobs can run sequentially or in parallel.

  3. Step: A step is an individual task that can run commands, use actions, or run scripts.

  4. Action: An action is a custom application that performs a complex but frequently repeated task and can be reused in different workflows.

Why Use GitHub Actions?

GitHub Actions offers several benefits:

  • Integration: Seamlessly integrates with your GitHub repository.

  • Customization: Define custom workflows tailored to your project's needs.

  • Scalability: Handle simple automation tasks or complex CI/CD pipelines.

  • Community Support: Leverage a vast library of pre-built actions created by the GitHub community.

Creating Your First GitHub Action

Let's start by creating a simple GitHub Action that runs whenever code is pushed to the repository. This action will install dependencies, run tests, and build the project.

  1. Create a New Repository: Start by creating a new repository on GitHub.

  2. Create a Workflow File: In your repository, create a new directory called .github/workflows. Inside this directory, create a file named ci.yml.

  3. Define the Workflow: Open ci.yml and add the following YAML configuration:

name: CI

on: [push]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Set up Node.js
        uses: actions/setup-node@v2
        with:
          node-version: '20'

      - name: Install dependencies
        run: npm install

      - name: Run tests
        run: npm test

      - name: Build project
        run: npm run build

Understanding the Workflow

  • name: Specifies the name of the workflow.

  • on: Defines the events that trigger the workflow. Here, the workflow is triggered by a push event.

  • jobs: Contains one or more jobs that execute in the workflow.

    • runs-on: Specifies the type of runner to execute the job (e.g., ubuntu-latest).

    • steps: Lists the individual steps in the job.

      • name: Describes the step.

      • uses: Specifies an action to run (e.g., actions/checkout@v2 to check out the repository).

      • run: Runs a command directly on the runner.

Advanced Example: Deploying to GitHub Pages

Next, let's create a workflow to deploy a static site to GitHub Pages whenever changes are pushed to the main branch.

  1. Create the Workflow File: Create another workflow file in the .github/workflows directory named deploy.yml.

  2. Define the Deployment Workflow: Add the following YAML configuration:

name: Deploy to GitHub Pages

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Set up Node.js
        uses: actions/setup-node@v2
        with:
          node-version: '20'

      - name: Install dependencies
        run: npm install

      - name: Build project
        run: npm run build

      - name: Deploy to GitHub Pages
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./dist

Breaking Down the Deployment Workflow

  • on: Specifies that the workflow runs on a push to the main branch.

  • deploy job: Includes steps to check out the code, set up Node.js, install dependencies, build the project, and deploy to GitHub Pages using peaceiris/actions-gh-pages.

Complex Example: Building and Deploying a Dockerized Application

For a more complex example, let's create a workflow that builds a Docker image and deploys it to a container registry whenever a pull request is merged into the main branch.

  1. Create the Workflow File: Create a new workflow file in the .github/workflows directory named docker-deploy.yml.

  2. Define the Docker Deployment Workflow: Add the following YAML configuration:

name: Build and Deploy Docker Image

on:
  push:
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v1

      - name: Log in to Docker Hub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Build and push Docker image
        uses: docker/build-push-action@v2
        with:
          push: true
          tags: ${{ secrets.DOCKER_USERNAME }}/my-app:latest

      - name: Deploy to Kubernetes
        run: |
          kubectl config set-context --current --namespace=my-app
          kubectl apply -f k8s/deployment.yaml

Breaking Down the Docker Deployment Workflow

  • on: Triggers the workflow on a push to the main branch.

  • build job: Includes steps to check out the code, set up Docker Buildx, log in to Docker Hub, build and push the Docker image, and deploy to Kubernetes.

    • docker/setup-buildx-action@v1: Sets up Docker Buildx, a Docker CLI plugin for extended build capabilities with BuildKit.

    • docker/login-action@v2: Logs in to Docker Hub using credentials stored in GitHub Secrets.

    • docker/build-push-action@v2: Builds and pushes the Docker image to Docker Hub.

    • kubectl commands: Deploy the new Docker image to a Kubernetes cluster.

More Complex Example: Deploying a Multi-Service Application with Docker Compose

In this example, we'll create a workflow that deploys a multi-service application composed of multiple microservices using Docker Compose. Each microservice will have its own Docker container and the entire application will be orchestrated and deployed using Docker Compose.

  1. Create the Workflow File: Create a new workflow file in the .github/workflows directory named multi-service-deploy.yml.

  2. Define the Deployment Workflow: Add the following YAML configuration:

     name: Deploy Multi-Service App
    
     on:
       push:
         branches:
           - main
    
     jobs:
       deploy:
         runs-on: ubuntu-latest
    
         steps:
           - name: Checkout code
             uses: actions/checkout@v2
    
           - name: Set up Docker Buildx
             uses: docker/setup-buildx-action@v1
    
           - name: Log in to Docker Hub
             uses: docker/login-action@v2
             with:
               username: ${{ secrets.DOCKER_USERNAME }}
               password: ${{ secrets.DOCKER_PASSWORD }}
    
           - name: Build and push Docker images
             run: |
               docker-compose -f docker-compose.yml build
               docker-compose -f docker-compose.yml push
    
           - name: Deploy using Docker Compose
             run: |
               docker-compose -f docker-compose.yml up -d
    
  3. Compose File: Create a docker-compose.yml file in the root of your repository. Define the services and their configurations within this file. For example:

     version: '3'
     services:
       service1:
         image: my-service1:${{ github.sha }}
         ports:
           - "3000:3000"
         environment:
           - NODE_ENV=production
         # Add any other service configurations as needed
    
       service2:
         image: my-service2:${{ github.sha }}
         ports:
           - "4000:4000"
         environment:
           - NODE_ENV=production
         # Add any other service configurations as needed
    
       # Add more services as required
    
  4. Dockerfiles: Create Dockerfiles for each microservice in your repository. These Dockerfiles will define how to build the Docker images for each service.

    Example Dockerfile for service1:

     FROM node:20
    
     WORKDIR /app
    
     COPY package*.json ./
     RUN npm install
    
     COPY . .
    
     EXPOSE 3000
    
     CMD ["npm", "start"]
    

    Example Dockerfile for service2:

     FROM python:3.9
    
     WORKDIR /app
    
     COPY requirements.txt ./
     RUN pip install -r requirements.txt
    
     COPY . .
    
     EXPOSE 4000
    
     CMD ["python", "app.py"]
    

Breaking Down the Workflow

  • on: Triggers the workflow on a push to the main branch.

  • deploy job: Includes steps to check out the code, set up Docker Buildx, log in to Docker Hub, build and push Docker images using Docker Compose, and deploy the multi-service application using Docker Compose.

Additional features for this action

Environment Variables

You can pass environment variables to your services using the environment key in the docker-compose.yml file. This allows you to configure your services dynamically based on the environment.

Health Checks and Dependencies

Docker Compose supports defining health checks for your services, ensuring that they are healthy and ready to receive traffic before being added to the pool. You can also define dependencies between services to ensure they start up in the correct order.

Deploying multi-service applications with Docker Compose and GitHub Actions provides a scalable and efficient solution for managing complex architectures. By automating the deployment process using GitHub Actions, you can ensure consistency and reliability in your deployments while reducing manual effort. Experiment with different configurations and scaling options to find the setup that best fits your application's requirements.

With this example, you can take your automation efforts to the next level and efficiently manage even the most complex deployments with ease.

Advanced Features of GitHub Actions

Matrix Builds

Matrix builds allow you to run multiple jobs in parallel with different configurations. This is particularly useful for testing your application across different environments.

Example: Testing a Node.js application on multiple versions of Node.js:

name: Node.js CI

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [12, 14, 16]
    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Set up Node.js
        uses: actions/setup-node@v2
        with:
          node-version: ${{ matrix.node-version }}

      - name: Install dependencies
        run: npm install

      - name: Run tests
        run: npm test

Caching Dependencies

To speed up your workflows, you can cache dependencies so that they don't need to be downloaded on every run. This is particularly useful for large projects with many dependencies.

Example: Caching npm dependencies:

name: Node.js CI

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Use Node.js
        uses: actions/setup-node@v2
        with:
          node-version: '20'

      - name: Cache npm dependencies
        uses: actions/cache@v2
        with:
          path: ~/.npm
          key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-node-

      - name: Install dependencies
        run: npm install

      - name: Run tests
        run: npm test

Self-hosted Runners

GitHub Actions supports self-hosted runners, which allow you to run workflows on machines that you control, such as your own servers or virtual machines. Self-hosted runners provide more control over the execution environment and can be customized to meet specific requirements, such as hardware configurations, software dependencies, and network access. This feature is especially useful for running workflows that require access to private resources or specialized hardware.

Example: Using a self-hosted runner for CI/CD:

name: CI

on: [push]

jobs:
  build:
    runs-on: self-hosted

    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      # Add build steps here

Environment Variables and Secrets

GitHub Actions allows you to define environment variables and store sensitive information securely as secrets. Environment variables can customize your workflows dynamically, while secrets provide a secure way to store sensitive data such as API keys, tokens, and passwords. You can reference these variables and secrets within your workflow YAML files, enabling greater flexibility and security in your automation pipelines.

Example: Using environment variables and secrets:

name: CI

on: [push]

jobs:
  build:
    runs-on: ubuntu-latest

    environment:
      NODE_ENV: production

    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Set up Node.js
        uses: actions/setup-node@v2
        with:
          node-version: '20'

      - name: Install dependencies
        run: npm install

      # Use environment variables in your workflow steps
      - name: Run tests
        run: npm test
        env:
          API_KEY: ${{ secrets.API_KEY }}

Security Best Practices

  1. Use Secrets for Sensitive Data: Store sensitive information such as API keys and passwords in GitHub Secrets, and reference these secrets in your workflows using ${{ secrets.SECRET_NAME }}.

  2. Limit Permissions: Minimize the permissions granted to the GitHub token in your workflows to reduce the risk of unauthorized access.

  3. Review Third-Party Actions: Carefully review the code and permissions of third-party actions before using them in your workflows to ensure they do not introduce security vulnerabilities.

Conclusion

GitHub Actions provides a versatile and efficient way to automate your CI/CD pipelines directly within GitHub. From simple workflows that run tests and build projects to more complex workflows that deploy Dockerized applications to Kubernetes, GitHub Actions can handle it all. By following these examples, you can quickly set up workflows to automate testing, building, and deploying your projects. Start experimenting with GitHub Actions today to streamline your development process and increase productivity. Happy coding!

Additional Resources

To further enhance your understanding and capabilities with GitHub Actions, consider exploring the following resources:

By leveraging these resources, you'll be well on your way to becoming proficient with GitHub Actions and reaping the benefits of automated workflows.