GitHub Actions offer a powerful way to automate your software development workflows, including continuous integration (CI) and continuous deployment (CD). While GitHub Actions are a popular choice, it’s worth noting that similar automation tools exist across different platforms.

For instance, I host some of my repositories on codeberg.org. While Codeberg itself requires an email inquiry to use their CI/CD solution, Forgejo (the self-hostable software that powers Codeberg) includes a built-in runner and tooling similar to GitHub Actions.

I might look into Forgejo’s capabilities more as I continue tinkering with my homelab platform.


What Exactly Are GitHub Actions?

At its core, a GitHub Actions workflow is an automated process that you define in a YAML file. You can configure these workflows to run when specific events occur in your GitHub repository, such as pushing code, creating a pull request, or on a set schedule. These automated tasks can range from running tests and linters to building your application and deploying it to various environments.

A key feature of GitHub Actions is the ability to securely manage environment variables and secrets (like API keys or passwords). These are encrypted and made available only to the scripts within your workflow, ensuring sensitive information remains protected.


Core Concepts: Workflows, Jobs, Steps, and Runners

To understand GitHub Actions, let’s break down its main components:

  • Workflow: The entire automated process, defined in a .yaml file located in the .github/workflows directory of your repository. A repository can have multiple workflows.
  • Event: A specific activity that triggers a workflow. Common events include push, pull_request, schedule, or manual triggers (workflow_dispatch).
  • Job: A set of steps that execute on the same runner. Workflows can have one or more jobs, which can run sequentially or in parallel. By default, jobs run in parallel. You can define dependencies between jobs (e.g., a deploy job only runs after a build job succeeds).
  • Step: An individual task within a job. A step can either be a shell script that your workflow executes or an action.
  • Action: A reusable unit of code that can perform complex tasks. You can use actions created by GitHub, the community, or write your own. Actions simplify common operations like checking out code (actions/checkout), setting up a specific version of a tool (like Node.js or Python), or logging into a cloud provider.
  • Runner: A server that executes your workflows when they’re triggered. GitHub provides hosted runners (Linux, Windows, and macOS virtual machines), or you can host your own self-hosted runners for more control over the environment and hardware.

Demystifying CI & CD

You’ll often hear GitHub Actions mentioned in the context of CI/CD. Let’s clarify these terms:

  • CI (Continuous Integration): This is a software development practice where developers frequently merge their code changes into a central repository. After each merge, automated builds and tests are run. The primary goals of CI are to find and address bugs quicker, improve software quality, and reduce the time it takes to validate and release new software updates.
  • CD (Continuous Deployment or Continuous Delivery):
    • Continuous Delivery means that your code is always in a deployable state after passing automated tests. The final decision to deploy to a live production environment is often a manual step, allowing for business considerations.
    • Continuous Deployment takes this one step further by automatically deploying every change that passes all stages of your production pipeline to your users.

GitHub Actions provides the automation capabilities to implement both CI and CD practices effectively.

A GitHub Action could look like this:

# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.

# GitHub recommends pinning actions to a commit SHA.
# To get a newer version, you will need to update the SHA.
# You can also reference a tag or branch, but the action may change without warning.

name: Build and deploy a container to an Azure Web App

# Environment variables available to all jobs and steps in the workflow
env:
  AZURE_WEBAPP_NAME: MY_WEBAPP_NAME   # set this to your application's name


# Defines the trigger for this workflow. In this case, a push to the 'main' branch.
on:
  push:
    branches:
      - main

# Permissions granted to the GITHUB_TOKEN for this workflow
permissions:
  contents: 'read'
  packages: 'write'

# Defines the jobs that will run as part of this workflow
jobs:
  build:
    runs-on: ubuntu-latest

    # Steps define the sequence of tasks within the 'build' job
    steps:
      # Checks out your repository's code so the workflow can access it
      - uses: actions/checkout@v4

      - name: Set up Docker Buildx
      # Uses a pre-defined action from the Docker organization
        uses: docker/setup-buildx-action@7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b

      - name: Log in to GitHub container registry
        uses: docker/login-action@8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Lowercase the repo name
        # The `run` keyword can run scripts.
        run: echo "REPO=${GITHUB_REPOSITORY,,}" >>${GITHUB_ENV}

      - name: Build and push container image to registry
        uses: docker/build-push-action@9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f
        with:
          push: true
          tags: ghcr.io/${{ env.REPO }}:${{ github.sha }}
          file: ./Dockerfile
  
  # The 'deploy' job
  deploy:
    runs-on: ubuntu-latest

    # The `deploy` job 'needs' the `build` job to have been finished successfully.
    needs: build

    # The environment is what variables this job has access to. This can be setup in github.
    environment:
      name: 'production'
      url: ${{ steps.deploy-to-webapp.outputs.webapp-url }}

    steps:
      - name: Lowercase the repo name
        run: echo "REPO=${GITHUB_REPOSITORY,,}" >>${GITHUB_ENV}

      - name: Deploy to Azure Web App
        id: deploy-to-webapp
        uses: azure/webapps-deploy@85270a1854658d167ab239bce43949edb336fa7c
        with:
          app-name: ${{ env.AZURE_WEBAPP_NAME }}
          publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }}
          images: 'ghcr.io/${{ env.REPO }}:${{ github.sha }}'

Note: I added some of my own comments, the source and original example file can be found here.