Automating your workflow with GitHub Actions
From basics to advanced deployments with real-world examples
Table of contents
- Introduction
- What are GitHub Actions?
- Key Concepts
- Why Use GitHub Actions?
- Creating Your First GitHub Action
- Advanced Example: Deploying to GitHub Pages
- Complex Example: Building and Deploying a Dockerized Application
- More Complex Example: Deploying a Multi-Service Application with Docker Compose
- Advanced Features of GitHub Actions
- Security Best Practices
- Conclusion
- Additional Resources
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
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).
Job: A job is a set of steps that execute on the same runner. Jobs can run sequentially or in parallel.
Step: A step is an individual task that can run commands, use actions, or run scripts.
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.
Create a New Repository: Start by creating a new repository on GitHub.
Create a Workflow File: In your repository, create a new directory called
.github/workflows
. Inside this directory, create a file namedci.yml
.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.
Create the Workflow File: Create another workflow file in the
.github/workflows
directory nameddeploy.yml
.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.
Create the Workflow File: Create a new workflow file in the
.github/workflows
directory nameddocker-deploy.yml
.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.
Create the Workflow File: Create a new workflow file in the
.github/workflows
directory namedmulti-service-deploy.yml
.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
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
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
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 }}
.Limit Permissions: Minimize the permissions granted to the GitHub token in your workflows to reduce the risk of unauthorized access.
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.