Mastering GitHub Actions: From Basics to Advanced Workflows
Development

Mastering GitHub Actions: From Basics to Advanced Workflows

SI

Shamal Iroshan

2025-05-19 | 12 min read

With GitHub Actions, developers can automate their processes directly within the GitHub environment. It is GitHub's native CI/CD platform. GitHub Actions facilitates development and increases efficiency by automating testing, deployments, and repetitive processes.

We'll go over everything you need to know in this blog article so you can start using GitHub Actions and take use of all of its great features.

What is GitHub Actions?

GitHub Actions allows you to automate your software workflows directly from your GitHub repository. You can trigger actions on events like code pushes, pull requests, issue comments, and even on a schedule. It integrates seamlessly with GitHub, making automation an integral part of your development life-cycle.


Key Concepts of GitHub Actions

To effectively use GitHub Actions, it’s essential to understand its core components. Each piece plays a role in building flexible and automated workflows.

1. Workflow

A workflow is the top-level configuration for your automation. It defines what should happen and when. Workflows are written in YAML and stored in the .github/workflows/ directory of your repository.

Example

name: CI Pipeline

on: [push, pull_request]

jobs:
  ...

You can have multiple workflows in a single repository, each triggered by different events.

2. Events

An event is what triggers a workflow. Common events include

  • push: When code is pushed to a branch.
  • pull_request: When a PR is opened or updated.
  • schedule: A cron-like scheduled job.
  • workflow_dispatch: A manual trigger from the GitHub UI.
  • release, issue_comment, etc.

Example

on:
  push:
    branches: [main]

3. Jobs

A job is a collection of steps that run on the same runner (a virtual environment). Jobs can run sequentially or in parallel, depending on how they’re configured.

Example

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      ...
  test:
    needs: build
    runs-on: ubuntu-latest

Here, the test job waits for the build job to complete.

4. Steps

Steps are individual tasks inside a job. They run sequentially and can either

  • Execute shell commands.
  • Use an existing action.

Example

steps:
  - run: npm install
  - run: npm test

5. Actions

An action is a reusable unit of code that performs a task. GitHub provides many official actions, and you can also create or use third-party ones from the GitHub Marketplace.

Example using a prebuilt action

- uses: actions/checkout@v4

6. Runners

A runner is the server that executes the jobs. GitHub offers

  • GitHub-hosted runners: Comes pre-configured with common tools (Ubuntu, Windows, macOS).
  • Self-hosted runners: Run on your own machines and give more control/customization.

Each job specifies a runner

runs-on: ubuntu-latest

7. Artifacts and Caching

  • Artifacts: Used to share files between jobs or store output (e.g., logs, test reports).
  • Caching: Used to speed up workflows by saving and reusing dependencies.

Example (caching npm modules)

- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}

8. Secrets and Environment Variables

  • Secrets: Securely store sensitive data (API keys, tokens) in your repo's settings.
  • Environment variables: Used to configure workflows or pass values between steps.

Example

env:
  NODE_ENV: production

Secrets are accessed like this

- run: echo ${{ secrets.API_KEY }}

These building blocks—workflow, event, job, step, action, runner, artifact/cache, and secrets/env—work together to automate virtually any part of your development process within GitHub.


GitHub Actions Workflow Structure

┌──────────────────────────────┐
│         Repository           │
└────────────┬─────────────────┘
             │
             ▼
┌──────────────────────────────┐
│          Workflow            │
│  (.github/workflows/*.yml)   │
└────────────┬─────────────────┘
             │
             ▼
┌──────────────────────────────┐
│            Event             │
│  (push, pull_request, etc.)  │
└────────────┬─────────────────┘
             │
             ▼
┌──────────────────────────────┐
│             Job              │
│  (runs-on: ubuntu-latest)    │
└────────────┬─────────────────┘
             │
             ▼
┌──────────────────────────────┐
│             Step             │
│  (run: npm install)          │
│  (uses: actions/checkout@v4) │
└────────────┬─────────────────┘
             │
             ▼
┌──────────────────────────────┐
│            Action            │
│  (Prebuilt or Custom)        │
└──────────────────────────────┘

Explanation of Components

  • Repository: The GitHub repository containing your code and workflow files.
  • Workflow: Defined in YAML files within the .github/workflows/ directory; specifies the automation process.
  • Event: Triggers that initiate the workflow, such as push, pull_request, or schedule.
  • Job: A set of steps executed on the same runner; jobs can run sequentially or in parallel.
  • Step: Individual tasks within a job; can be shell commands or actions.
  • Action: Reusable units of code that perform specific tasks; can be prebuilt (from the GitHub Marketplace) or custom-defined.

Using Pre-built Actions

One of the most powerful features of GitHub Actions is the ability to use pre-built actions—reusable components created by the GitHub community and maintained in public repositories. These save time and reduce the need to write custom scripts for common tasks.

What Is a Pre-built Action?

A pre-built action is a modular, shareable task packaged as a GitHub Action. You can plug it directly into your workflows with just a few lines of YAML. Pre-built actions can

  • Set up programming environments
  • Deploy apps
  • Lint code
  • Send notifications
  • Integrate with external services

Where to Find Them

You can find thousands of pre-built actions in the GitHub Actions Marketplace. Each action comes with

  • Usage instructions
  • Inputs and outputs
  • Supported platforms

Example: Using Pre-built Actions

Here’s a real example of a CI workflow using several pre-built actions

name: Node.js CI

on: [push]

jobs:
  build:
    runs-on: ubuntu-latest

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

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

      - name: Install dependencies
        run: npm install

      - name: Run tests
        run: npm test

Explanation

  • actions/checkout@v4: Pulls the latest code from the repo.
  • actions/setup-node@v4: Installs Node.js version 18.
  • The rest are shell steps to install packages and run tests.

Customizing Pre-built Actions

You can pass inputs to actions to change their behavior

- uses: actions/setup-node@v4
  with:
    node-version: '20'
    cache: 'npm'

And access outputs like this

- id: example
  uses: some/action@v1

- run: echo "The result is ${{ steps.example.outputs.result }}"

Security Considerations

  • Use actions from trusted sources.
  • Avoid using unpinned @main or @latest tags in critical workflows.
  • Review the source code of unfamiliar actions before using them.

Workflow Triggers

Workflow triggers define when your GitHub Actions workflow should run. They are specified under the on: key in your workflow file.

Common Triggers

push: Runs when code is pushed to a branch.

on: push

pull_request: Runs when a pull request is opened, updated, or reopened.

on: pull_request

schedule: Runs on a cron schedule (e.g., nightly builds)

on:
  schedule:
    - cron: '0 0 * * *'

workflow_dispatch: Allows manual triggering via the GitHub UI.

on: workflow_dispatch

release, issues, etc.: Event-based triggers for GitHub activities.


Matrix Build in GitHub Actions

A matrix build allows you to automatically run the same job multiple times with different combinations of inputs—like Node versions, OS types, or environment variables—in parallel. This is useful for testing your code across multiple platforms or configurations without duplicating your workflow code.

Why Use Matrix Builds?

  • Ensure compatibility across multiple environments (e.g., different Node.js versions).
  • Test different dependency versions, flags, or operating systems.
  • Reduce manual effort and speed up CI by running jobs in parallel.

How It Works

You define a strategy.matrix block in your job

jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node: [16, 18, 20]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
      - run: npm install
      - run: npm test

GitHub will create three parallel jobs, each with a different node-version: 16, 18, and 20.


Secrets and Environment Variables in GitHub Actions

GitHub Actions allows you to manage configuration data and sensitive values through environment variables and secrets. These help you keep your workflows clean, flexible, and secure.

Environment Variables

Environment variables are key-value pairs used to store non-sensitive configuration data.

Ways to Set Environment Variables

  1. Globally in a job
jobs:
  build:
    runs-on: ubuntu-latest
    env:
      NODE_ENV: production
    steps:
      - run: echo $NODE_ENV
  1. In a single step
- name: Print version
  run: echo $VERSION
  env:
    VERSION: '1.0.0'

Secrets

Secrets are encrypted environment variables used to store sensitive information, such as API keys, tokens, and credentials.

How to Use Secrets

  1. Add them in your repository or organization settings under Settings → Secrets and variables → Actions.
  2. Access them in workflows using ${{ secrets.SECRET_NAME }}
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Use secret
        run: echo "The token is ${{ secrets.MY_TOKEN }}"

GitHub masks secrets in logs and restricts their usage to workflows with proper permissions.


Advanced Workflows in GitHub Actions

As your project grows, you may need more complex automation. GitHub Actions supports powerful features to create advanced workflows—enabling you to orchestrate builds, tests, and deployments across multiple conditions and environments.

1. Job Dependencies (needs)

Control the order of job execution using needs

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Building..."

  test:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - run: echo "Testing..."

test will only run after build completes successfully.

2. Matrix Strategies with Exclusions and Includes

Use complex combinations of inputs and fine-tune with include and exclude

strategy:
  matrix:
    os: [ubuntu-latest, windows-latest]
    node: [16, 18]
    exclude:
      - os: windows-latest
        node: 16

3. Reusable Workflows

You can call one workflow from another—great for sharing logic (e.g., linting, testing)

jobs:
  call-shared:
    uses: your-org/.github/.github/workflows/test.yml@main
    with:
      node-version: '18'
    secrets: inherit

Define workflow_call in the reusable workflow to expose inputs and secrets.

4. Manual Approvals with environments

Add manual approval steps before deploying to production

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://yourapp.com

Set up deployment protection rules via GitHub's Environment settings.

5. Conditionals and Expressions

Run steps or jobs conditionally using if

- name: Deploy
  if: github.ref == 'refs/heads/main' && success()
  run: ./deploy.sh

You can use

  • github.* context (e.g. event, branch)
  • env.* variables
  • success(), failure(), etc.

6. Workflow Reuse and Dispatch Chaining

Trigger one workflow from another using workflow_dispatch + repository_dispatch. This allows advanced chaining across repos or teams.


Debugging GitHub Actions

Debugging GitHub Actions is essential when workflows don't behave as expected. Fortunately, GitHub provides tools and techniques to help you identify and fix issues quickly.

1. View Logs in the Actions Tab

  • Go to your repository’s "Actions" tab.
  • Click on the failed workflow run.
  • Expand each job and step to see real-time output logs, including environment variables, errors, and command outputs.

2. Add Debug Output

You can print custom logs using echo

- run: echo "Step completed"

Use environment variables for more insight

- run: echo "Branch: $GITHUB_REF"

3. Enable Step Debug Logging

You can enable extra debugging by adding secrets to your repository

  • Go to Settings → Secrets and variables → Actions → Repository secrets.
  • Add
    • ACTIONS_RUNNER_DEBUGtrue (enables runner-level debug logs)
    • ACTIONS_STEP_DEBUGtrue (enables step-level logs)

These reveal internal details like step execution, caching behavior, and environment setup.

4. Use continue-on-error for Isolation

Use this to allow steps to fail without stopping the entire job—useful for checking multiple things at once

- name: Run optional test
  run: ./noncritical.sh
  continue-on-error: true

5. Save Artifacts for Postmortem

You can upload logs, test reports, or config files as artifacts to inspect later

- uses: actions/upload-artifact@v4
  with:
    name: debug-logs
    path: ./logs/

Best Practices for Using GitHub Actions

Following best practices ensures your workflows are secure, efficient, and maintainable over time.

1. Secure Secrets and Credentials

  • Store sensitive data using GitHub Secrets (never hard-code).
  • Use least privilege permissions via permissions: block.
  • Avoid logging secrets—even by mistake.
permissions:
  contents: read
  deployments: write

2. Use Specific Versions of Actions

Always pin actions to a version (@v3) instead of using @latest or @main to avoid unexpected changes

uses: actions/checkout@v4

3. Re-use and Modularize Workflows

  • Use composite actions or reusable workflows to reduce duplication.
  • Keep logic DRY and easy to manage across projects or teams.

4. Use Matrix Builds for Compatibility Testing

Run tests across multiple environments (Node versions, OS, etc.) to ensure wider support.

5. Cache Dependencies Smartly

Use actions/cache to speed up builds but ensure cache keys are precise to avoid stale dependencies

- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: npm-${{ hashFiles('**/package-lock.json') }}

6. Keep Workflows in Version Control

Store and version all workflows inside .github/workflows/ so changes are tracked, reviewed, and tested.

7. Limit Workflow Scope

Avoid running workflows on every event unless needed. Use filters like

on:
  push:
    branches: [main]
    paths: ['src/**', 'package.json']

8. Use Context and Expressions Thoughtfully

Use ${{ github.* }} and ${{ secrets.* }} to make workflows dynamic, but keep logic readable and clear.

9. Protect Production with Environments

Use environments with manual approvals and secret scoping to protect production deployments.

10. Enable Debugging When Needed

Set ACTIONS_STEP_DEBUG and ACTIONS_RUNNER_DEBUG to true for deeper insights during failures.


Conclusion

GitHub Actions is a powerful and flexible CI/CD tool that integrates deeply with your development workflow. Whether you're automating simple build and test pipelines or managing complex multi-environment deployments, it offers the features and extensibility you need—right within your GitHub repository.

By understanding key concepts like workflows, jobs, matrix builds, secrets, and advanced features like reusable workflows and environments, you can create reliable and maintainable automation pipelines. Following best practices around security, performance, and modularity will help you scale with confidence.

Start simple, iterate, and let GitHub Actions handle the heavy lifting so you can focus on building great software.