GitHub Actions: Complete Step-by-Step Guide

github-actions

1. Introduction to GitHub Actions

What is GitHub Actions?

GitHub Actions is a CI/CD (Continuous Integration and Continuous Deployment) tool that allows developers to automate workflows directly in their GitHub repository. It helps automate software development processes like testing, building, and deploying applications.

Why Use GitHub Actions?

  • Automate repetitive tasks in software development.
  • Run tests automatically after every push.
  • Deploy applications seamlessly.
  • Manage workflows with GitHub-hosted or self-hosted runners.
  • Integrate with third-party services (Docker, Kubernetes, AWS, etc.).

Use Cases

  • CI/CD Pipelines: Automate testing, building, and deployment.
  • Code Quality Checks: Run linters and security scans on every push.
  • Automated Releases: Publish artifacts to package managers.
  • Infrastructure as Code (IaC): Deploy infrastructure using Terraform, Ansible, or Kubernetes.

Hosted Runners vs. Self-Hosted Runners

Hosted Runners

GitHub provides virtual machines (VMs) that run jobs in a workflow. Examples:

  • ubuntu-latest
  • windows-latest
  • macos-latest

Self-Hosted Runners

A self-hosted runner allows you to run workflows on your own infrastructure, giving you more control over the environment and resource allocation.

A GitHub Actions workflow consists of:

  • name – Workflow display name.
  • on – Defines when the workflow should run (events like push, pull_request, or schedule).
  • env – Sets environment variables.
  • jobs – Defines a list of jobs (each job runs independently).
  • steps – Tasks executed in a job (e.g., checking out code, running scripts).
  • needs – Specifies job dependencies; ensures a job runs only after another completes.
  • runs-on – Defines the virtual machine environment for the job (e.g., ubuntu-latest, macos-latest).
  • container – Runs jobs inside a specified container instead of the default virtual environment.
  • services – Defines additional containers for services like databases (e.g., Redis, MySQL).
  • timeout-minutes – Maximum execution time before GitHub cancels the job.
  • strategy – Defines a build matrix to run jobs with different configurations (e.g., multiple OS or Node.js versions).
  • fail-fast – Stops all in-progress matrix jobs if one fails.
  • uses – Runs an action from a public/private repository or Docker image.
  • with – Provides input parameters for actions.
  • if – Conditional execution of a step or job based on an expression.
  • run – Executes shell commands directly in a step.
  • artifact storage – Stores and shares files between jobs using upload-artifact and download-artifact.
  • caching – Speeds up workflows by caching dependencies with actions/cache.
  • context and expressions – Uses expressions (${{ }}) to reference variables, secrets, and runtime information.

Here’s the structured, parent-child syntax sequence for GitHub Actions workflow components, with each component explained in terms of its relationship to others. This will give you a tree structure to help understand how these parts fit together:

Workflow
  ├── name         # (Parent: Workflow) – Display name of the workflow.
  ├── on           # (Parent: Workflow) – Defines when the workflow runs (e.g., push, pull_request).
  │     └── push#(Child:on)-Examp of an event that triggers the workflow when a push is made to a branch.
  │     └── pull_request#(Child:on)-Example of an event that triggers the workflow on a pull request.
  ├── env # (Parent: Workflow) – Sets environment variables accessible throughout the workflow.
  ├── jobs # (Parent: Workflow) – Defines the jobs in the workflow.
  │     └── job_name # (Child: jobs) – Each job that runs in parallel or sequentially.
  │          ├── runs-on #(Parent:job_name)-Defines the VM environment for the job (e.g., ubuntu-latest).
  │          ├── container # (Parent: job_name) – Specifies if the job should run inside a container.
  │          ├── services # (Parent:job_name)-Defines services (Redis, MySQL) required for the job.
  │          ├── timeout-minutes # (Parent: job_name) – Defines the max execution time for the job.
  │          ├── strategy #(Parent:job_name)-Defines a build matrix to run jobs with multiple config.
  │          │     └── matrix # (Child: strategy)-Matrix configurations (e.g., multiple OS or versions).
  │          ├── steps # (Parent: job_name) – The list of steps to execute in the job.
  │          │     ├── name # (Parent: steps) – Descriptive name for each step.
  │          │     ├── uses # (Parent:steps)-Runs an action from a pub/private repo or Docker image.
  │          │     ├── with # (Parent: steps) – Input parameters for the action.
  │          │     ├── run  # (Parent: steps) – Run shell commands directly in the step.
  │          │     ├── if   # (Parent: steps) – Conditional execution of a step based on expressions.
  │          │     ├── artifact storage#(Parent:steps)-Defines storage,upload-artf, download-artifact.
  │          │     ├── caching  # (Parent: steps) – Caches dependencies with actions/cache.
  │          │     └── context and expressions #(Parent:steps)Uses expressions for dynamic variables, secrets, etc.
  └── needs # (Parent: Workflow, job_name) – Specifies job dependencies, ensuring jobs run in order.
        └── job_name # (Child: needs) – The job that must finish before the current job can run.

2. Understanding GitHub Actions Workflow Syntax

A GitHub Actions workflow is defined using YAML files stored in .github/workflows/.

Here are practical examples for each component of the GitHub Actions workflow, with explanations for each parent-child relationship:

1. name (Parent: Workflow) – Display name of the workflow

This is the name of your workflow.

name: CI/CD Pipeline

2. on (Parent: Workflow) – Defines when the workflow runs (e.g., push, pull_request)

on:
  push:          # (Child: on) – Trigger when a push is made to a branch
    branches:
      – main     # Trigger only when changes are pushed to the ‘main’ branch
  pull_request:  # (Child: on) – Trigger when a pull request is created or updated
    branches:
      – main     # Trigger for pull requests targeting the ‘main’ branch

3. env (Parent: Workflow) – Sets environment variables accessible throughout the workflow

env:
  GLOBAL_ENV_VAR: value   # Global environment variable available in all jobs

4. jobs (Parent: Workflow) – Defines the jobs in the workflow

Each job can be executed independently.

jobs:
  build:  # (Child: jobs) – The ‘build’ job defined here
    runs-on: ubuntu-latest  # (Parent: job_name) – Specifies the virtual environment for the job

5. runs-on (Parent: job_name) – Defines the virtual machine environment for the job

jobs:
  build:
    runs-on: ubuntu-latest  # (Parent: job_name) – Runs the build job on the latest Ubuntu environment

6. container (Parent: job_name) – Specifies if the job should run inside a container

jobs:
  build:
    container:
      image: node:16  # (Parent: job_name) – Use Node.js version 16 inside a Docker container
    steps:
      – name: Checkout code
        uses: actions/checkout@v2

7. services (Parent: job_name) – Defines services like databases required for the job

jobs:
  build:
    services:
      redis:  # (Parent: job_name) – A Redis service required for the job
        image: redis:alpine
        ports:
          – 6379:6379

8. timeout-minutes (Parent: job_name) – Defines the max execution time for the job

jobs:
  build:
    timeout-minutes: 30  # (Parent: job_name) – Set the timeout to 30 minutes for the build job

9. strategy (Parent: job_name) – Defines a build matrix to run jobs with multiple configurations

jobs:
  build:
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest]  # (Child: strategy) – Run the job on both Ubuntu and macOS
        node_version: [14, 16]             # (Child: strategy) – Test with Node.js versions 14 and 16
    runs-on: ${{ matrix.os }}  # Use the OS defined in the matrix

10. matrix (Child: strategy) – Matrix configurations (e.g., multiple OS or versions)

strategy:
  matrix:
    os: [ubuntu-latest, macos-latest]  # (Child: strategy) – Define the OS configurations
    node_version: [14, 16]             # (Child: strategy) – Define the Node.js versions to test

11. steps (Parent: job_name) – The list of steps to execute in the job

jobs:
  build:
    steps:
      – name: Checkout code  # (Parent: steps) – Check out the code repository
        uses: actions/checkout@v2

12. name (Parent: steps) – Descriptive name for each step

steps:
  – name: Checkout repository  # (Parent: steps) – This is a descriptive name for the step
    uses: actions/checkout@v2

13. uses (Parent: steps) – Runs an action from a public/private repository or Docker image

steps:
  – name: Set up Node.js  # (Parent: steps) – Set up the Node.js environment
    uses: actions/setup-node@v2  # (Parent: steps) – Uses the setup-node action
    with:
      node-version: ’16’  # (Parent: steps) – Specify Node.js version

14. with (Parent: steps) – Input parameters for the action

steps:
  – name: Set up Node.js
    uses: actions/setup-node@v2
    with:
      node-version: ’16’  # (Parent: steps) – Specify Node.js version

15. run (Parent: steps) – Executes shell commands directly in the step

steps:
  – name: Install dependencies  # (Parent: steps) – Install dependencies for the project
    run: npm install  # (Parent: steps) – Run the shell command to install dependencies

16. if (Parent: steps) – Conditional execution of a step based on expressions

steps:
  – name: Run tests  # (Parent: steps) – Run tests conditionally
    if: ${{ success() }}  # (Parent: steps) – Only run this step if the previous step was successful
    run: npm test

17. artifact storage (Parent: steps) – Defines artifact storage, e.g., upload-artifact, download-artifact

steps:
  – name: Upload build artifact  # (Parent: steps) – Upload build artifacts for later use
    uses: actions/upload-artifact@v2
    with:
      name: build-artifact
      path: ./build/output  # (Parent: steps) – Upload the files from the ‘output’ directory

18. caching (Parent: steps) – Caches dependencies with actions/cache

steps:
  – name: Cache Node.js dependencies  # (Parent: steps) – Cache dependencies for faster builds
    uses: actions/cache@v2
    with:
      path: node_modules  # (Parent: steps) – Cache the ‘node_modules’ folder
      key: ${{ runner.os }}-node-modules-${{ hashFiles(‘**/package-lock.json’) }}  # (Parent: steps) – Cache key based on package-lock.json

19. context and expressions (Parent: steps) – Uses expressions for dynamic variables, secrets, etc.

steps:
  – name: Deploy to production  # (Parent: steps) – Deploy to production only if it’s the main branch
    if: github.ref == ‘refs/heads/main’  # (Parent: steps) – Run this step only if the branch is ‘main’
    run: |
      scp -i ${{ secrets.SSH_KEY }} ./build user@server:/path/to/deploy  # (Parent: steps) – Use SSH secret to deploy

20. needs (Parent: Workflow, job_name) – Specifies job dependencies, ensuring jobs run in order

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      – name: Checkout code
        uses: actions/checkout@v2

  deploy:
    needs: build  # (Parent: needs) – This job will only run after ‘build’ job completes
    runs-on: ubuntu-latest
    steps:
      – name: Deploy application
        run: echo “Deploying to server”

21. job_name (Child: needs) – The job that must finish before the current job can run

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      – name: Build project
        run: npm run build

  deploy:
    needs: build  # (Child: needs) – The ‘deploy’ job depends on the ‘build’ job
    runs-on: ubuntu-latest
    steps:
      – name: Deploy to production
        run: npm run deploy

These individual examples should give you a clear understanding of each component and how it fits together in a GitHub Actions workflow. You can copy these into a .yml file and experiment with them to build your practical understanding.


3. Step-by-Step Practical Guide

Step 1: Creating a Simple GitHub Actions Workflow

  1. Navigate to your GitHub repository.
  2. Create a .github/workflows/ directory.
  3. Create a new workflow file, e.g., ci.yml.
  4. Add the following basic workflow:
name: Simple CI
on: push
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      – name: Checkout Code
        uses: actions/checkout@v3
      – name: Print Message
        run: echo “Build Successful!”

Step 2: Running Jobs in a Self-Hosted Runner

  1. Install a GitHub self-hosted runner on your machine.
  2. Register the runner in GitHub.
  3. Use it in a workflow:
jobs:
  self_hosted_build:
    runs-on: self-hosted
    steps:
      – name: Print Machine Name
        run: hostname

Step 3: Running Tests in Docker

jobs:
  test:
    runs-on: ubuntu-latest
    container:
      image: python:3.9
    steps:
      – name: Run Python Test
        run: python –version

Step 4: Deploying to Kubernetes

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      – name: Setup Kubectl
        uses: azure/setup-kubectl@v1
      – name: Apply Deployment
        run: kubectl apply -f deployment.yaml

      Complete pipeline example for a web application. This pipeline covers the following stages:

  1. Checkout the code.
  2. Set up dependencies (Node.js, Python, etc., based on the web app’s tech stack).
  3. Run tests.
  4. Build the web app.
  5. Deploy the web app.
  6. Notify on success or failure.
name: Web App CI/CD Pipeline

# This pipeline is triggered by a push to the main branch or any feature branches
on:
  push:
    branches:
      – main         # Trigger on push to the main branch
      – ‘feature/*’  # Trigger on push to any feature branch
  pull_request:
    branches:
      – main         # Trigger for pull requests to the main branch

jobs:
  # Job 1: CI – Continuous Integration (Build & Test)
  ci:
    runs-on: ubuntu-latest

    steps:
      # Step 1: Checkout the repository code
      – name: Checkout repository
        uses: actions/checkout@v2
        # This checks out the code so that the next steps can use it

      # Step 2: Set up Node.js environment
      – name: Set up Node.js (for Node.js-based web apps)
        uses: actions/setup-node@v2
        with:
          node-version: ’16’  # Set the desired Node.js version

      # Step 3: Install dependencies
      – name: Install dependencies
        run: |
          npm install  # Install dependencies from package.json (or use yarn)

      # Step 4: Run tests
      – name: Run tests
        run: |
          npm test  # Run your test suite (adjust if you’re using a different testing framework)

  # Job 2: Build – Build the web app
  build:
    runs-on: ubuntu-latest
    needs: ci  # This job depends on the ci job; it will run after successful tests

    steps:
      # Step 1: Checkout the repository (same as in the previous job)
      – name: Checkout repository
        uses: actions/checkout@v2

      # Step 2: Set up Node.js again (required for build)
      – name: Set up Node.js
        uses: actions/setup-node@v2
        with:
          node-version: ’16’  # Make sure to use the same Node.js version

      # Step 3: Install dependencies (again, in case some are needed for the build)
      – name: Install dependencies
        run: |
          npm install

      # Step 4: Build the application
      – name: Build the web app
        run: |
          npm run build  # Adjust this to your build command (e.g., webpack, create-react-app, etc.)

  # Job 3: Deploy – Deploy the web app to a server (e.g., AWS, DigitalOcean, etc.)
  deploy:
    runs-on: ubuntu-latest
    needs: build  # This job depends on the build job; it will run after a successful build

    steps:
      # Step 1: Checkout the repository
      – name: Checkout repository
        uses: actions/checkout@v2

      # Step 2: Set up environment variables (optional)
      – name: Set up deployment secrets
        run: echo “DEPLOY_KEY=${{ secrets.DEPLOY_KEY }}” >> $GITHUB_ENV  # Example for a deploy key (add your secret to GitHub Actions secrets)

      # Step 3: Deploy to server (adjust to your deployment method)
      – name: Deploy to server
        run: |
          # Example: Using SSH to deploy via SCP or rsync
          ssh -i ${{ secrets.DEPLOY_KEY }} [email protected] ‘cd /path/to/your/web/app && git pull && npm install && pm2 restart app’
          # Or use any other deployment tool (e.g., FTP, SFTP, AWS CLI, etc.)

  # Job 4: Notify – Send notifications after the pipeline runs (success or failure)
  notify:
    runs-on: ubuntu-latest
    needs: deploy  # This job depends on the deploy job

    steps:
      # Step 1: Send success or failure notification
      – name: Send notification on success/failure
        run: |
          # Check if the workflow was successful or failed
          if [[ $GITHUB_RUN_STATUS == “success” ]]; then
            # Send success notification (e.g., to Slack)
            curl -X POST -H ‘Content-type: application/json’ \
            –data ‘{“text”:”Build and deployment successful!”}’ \
            https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK
          else
            # Send failure notification (e.g., to Slack)
            curl -X POST -H ‘Content-type: application/json’ \
            –data ‘{“text”:”Build or deployment failed. Please check the logs!”}’ \
            https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK

4. Conclusion

This guide provides a structured approach to learning GitHub Actions from basic syntax to advanced use cases. By following these steps, you can create robust CI/CD pipelines using GitHub-hosted and self-hosted runners.

For more details, refer to the GitHub Actions Documentation.

Leave a Reply

Your email address will not be published. Required fields are marked *