Skip to main content

βš™οΈ Introduction to GitHub Actions

Imagine a tireless robot living inside your repository that watches for events and springs into action β€” running your tests on every push, deploying on every merge, or backing up your database each night. That robot is GitHub Actions, and in this lesson you'll teach it its first tricks.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Define the five core building blocks β€” workflow, event, job, step, runner β€” and how they fit together
  • Read and write a workflow YAML file stored in .github/workflows/
  • Build a continuous integration workflow that installs, lints, and tests a project
  • Store and use secrets securely instead of hard-coding credentials
  • Apply best practices β€” pinned versions, caching, focused triggers, timeouts

Estimated Time: 35–45 minutes  β€’  Difficulty: Beginner–Intermediate

Hands-on: Add a working CI workflow to a repository and watch it run in the Actions tab.

In This Lesson

What Are GitHub Actions?

GitHub Actions is an automation platform built directly into every GitHub repository. You describe when something should happen and what should happen, and GitHub runs it for you on its own servers β€” no separate CI service to configure.

πŸ’‘ A useful analogy: Think of GitHub Actions like a smart-home system for your codebase. "When I arrive home (push code), turn on the lights (run the tests) and start the coffee (deploy the app)." You wire up the triggers once, and the automation runs itself from then on.
flowchart LR A[Event occurs
push Β· PR Β· schedule] --> B[Workflow triggers] B --> C[Job runs on a runner] C --> D1[Build] C --> D2[Test] C --> D3[Deploy] C --> D4[Notify]

Because the automation lives in your repository as code, it's version-controlled alongside everything else β€” you can review changes to your CI/CD in a pull request, just like any other file.

The Five Building Blocks

Everything in GitHub Actions is made of five nested concepts. Learn these names and the YAML stops looking like a wall of text:

How workflows, jobs, and steps nest An event triggers a workflow. A workflow contains one or more jobs. Each job runs on a runner and contains a sequence of steps. Event push Β· PR Β· cron Workflow Job β€” runs on a Runner (VM) Step 1 β€” Checkout code (an Action) Step 2 β€” Set up Node (an Action) Step 3 β€” Run tests (a command)
Figure 1 β€” An event triggers a workflow, which contains one or more jobs. Each job runs on a runner and executes a sequence of steps. A step is either a reusable action or a shell command.

πŸ“– The vocabulary

Workflow: an automated procedure defined in a YAML file; the whole recipe.

Event: the trigger that starts a workflow (a push, a pull request, a schedule).

Job: a set of steps that run together on one runner; jobs can run in parallel.

Step: a single task β€” either a shell command (run) or a reusable action (uses).

Runner: the virtual machine (Ubuntu, Windows, or macOS) that executes a job.

Anatomy of a Workflow File

Workflows are YAML files that live in .github/workflows/. GitHub automatically discovers and runs any workflow it finds there. Here is a minimal one, annotated:

name: CI Workflow            # Shown in the Actions tab

# WHEN: run on pushes and PRs targeting main
on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

# WHAT: the work to do
jobs:
  build:
    runs-on: ubuntu-latest   # The runner (VM) for this job
    steps:
      - uses: actions/checkout@v4      # Action: get your code onto the runner
      - name: Set up Node.js
        uses: actions/setup-node@v4    # Action: install Node
        with:
          node-version: '20'
      - name: Install dependencies
        run: npm ci                    # Command: a shell step
      - name: Run tests
        run: npm test

⚠️ YAML is whitespace-sensitive

Indentation defines structure in YAML β€” always use spaces, never tabs, and keep levels consistent. A single misplaced space is the most common reason a workflow fails to run. Most editors can show whitespace and warn you.

Note the difference between the two kinds of steps: uses: pulls in a prebuilt action (someone else's reusable code, like actions/checkout), while run: executes a shell command directly on the runner.

A Real CI Workflow

The most common use of Actions is continuous integration: on every push and pull request, prove the code still builds and passes its tests. A matrix lets you run the same job across several versions at once β€” cheap insurance that your code works everywhere it needs to.

name: Node.js CI

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18.x, 20.x, 22.x]   # Test on three Node versions
    steps:
      - uses: actions/checkout@v4
      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'                       # Cache deps between runs
      - run: npm ci
      - run: npm run build --if-present
      - run: npm test

What you see in the Actions tab

βœ“ test (18.x)   passed in 1m 04s
βœ“ test (20.x)   passed in 58s
βœ“ test (22.x)   passed in 1m 01s

Each matrix entry becomes its own parallel job with its own green check (or red X). If Node 22 breaks something that 20 didn't, you'll know immediately and exactly where.

πŸ’‘ Why npm ci and not npm install?

npm ci installs the exact versions locked in package-lock.json and is built for automation β€” it's faster and reproducible, so a passing CI run today means the same install tomorrow.

Secrets & Environment Variables

Workflows often need sensitive values β€” an API token, a deploy key, a database URL. Never hard-code these into the YAML, which is committed to your repository for all to see. Instead, store them as encrypted secrets.

πŸ”’ The analogy: A secret is a bank safe-deposit box. You lock the valuable away once; the workflow can reach in and use it when needed, but no one reading your code ever sees the contents.

Add one under Settings β†’ Secrets and variables β†’ Actions β†’ New repository secret, then reference it with the secrets context:

steps:
  - name: Deploy to production
    run: ./deploy.sh
    env:
      API_TOKEN: ${{ secrets.API_TOKEN }}

GitHub automatically masks secret values in logs β€” even if a script accidentally prints one, it shows up as ***. GitHub also provides a built-in GITHUB_TOKEN secret automatically for authenticating with your own repository, so you rarely need to create a token for that.

Common Patterns

Scheduled jobs (cron)

The schedule event runs a workflow on a timer using cron syntax β€” perfect for nightly backups, dependency checks, or report generation:

name: Daily Database Backup

on:
  schedule:
    - cron: '0 2 * * *'      # Every day at 02:00 UTC

jobs:
  backup:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Back up the database
        run: ./backup.sh
        env:
          DB_CONNECTION: ${{ secrets.DB_CONNECTION }}

Cache dependencies to run faster

Reinstalling packages from scratch on every run is slow. Caching restores them from a previous run, keyed on your lockfile so it invalidates automatically when dependencies change:

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

βœ… Reuse the community's work

The GitHub Marketplace hosts thousands of prebuilt actions β€” deploy to cloud providers, publish packages, post to Slack, and much more. Before writing a custom step, check whether a well-maintained action already does the job.

Hands-on Exercise

πŸ‹οΈ Add CI to a real repository

Objective: Create a workflow, push it, and watch GitHub run it automatically.

Instructions:

  1. In a small Node project (or a fresh one with npm init -y and a trivial test), create the folder and file .github/workflows/ci.yml.
  2. Paste a starter workflow:
    name: CI
    
    on:
      push:
        branches: [ main ]
      pull_request:
        branches: [ main ]
    
    jobs:
      build:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - uses: actions/setup-node@v4
            with:
              node-version: '20'
              cache: 'npm'
          - run: npm ci
          - run: npm test
  3. Commit and push it to main:
    git add .github/workflows/ci.yml
    git commit -m "Add CI workflow"
    git push
  4. Open the repository's Actions tab. You should see your workflow running; click into it to watch each step's live logs.
  5. Break it on purpose: push a commit with a failing test, watch the run turn red, then fix it and confirm it goes green again.
πŸ’‘ Hint

If the run fails at npm ci with a lockfile error, your project has no package-lock.json β€” run npm install locally once and commit the lockfile. If it fails at npm test because there's no test script, add a placeholder to package.json: "test": "node -e \"console.log('ok')\"".

βœ… What success looks like

The Actions tab shows a workflow run with a green checkmark. Expanding the job reveals each step β€” checkout, setup-node, install, test β€” each with a tick. When you push the deliberately failing test, the run turns red and points at the failing step; fixing it returns the run to green.

🎯 Quick Quiz

Question 1: Where must a GitHub Actions workflow file live to be run automatically?

Question 2: Which sequence correctly nests the core building blocks from largest to smallest?

Question 3: How should you provide an API token to a workflow?

Best Practices

βœ… Do

  • Pin actions to a major version (actions/checkout@v4) so an upstream change can't break you overnight.
  • Keep workflows focused β€” separate files for CI, deployment, and scheduled jobs.
  • Cache dependencies to keep runs fast, and scope triggers to the branches that need them.
  • Set a timeout-minutes on jobs so a hung step can't run forever and burn minutes.

⚠️ Don't

  • Don't reference @master or @main on third-party actions β€” you inherit whatever they push, including breakage.
  • Don't hard-code secrets; store them encrypted and reference the secrets context.
  • Don't trigger heavy workflows on every event β€” narrow the on: filters to avoid wasting runner minutes.

Summary & Quiz

πŸŽ‰ Key Takeaways

  • GitHub Actions automates work inside your repo: an event triggers a workflow of jobs, each a sequence of steps on a runner.
  • Workflows are YAML files in .github/workflows/ β€” and YAML is whitespace-sensitive (spaces, not tabs).
  • A CI workflow installs, builds, and tests on every push and PR; a matrix runs it across versions in parallel.
  • Store credentials as encrypted secrets and reference them with ${{ secrets.NAME }} β€” never hard-code them.
  • Follow best practices: pin versions, cache dependencies, focus triggers, set timeouts.

πŸ“š Further Reading

πŸš€ What's Next?

You've automated the software side. Next we shift from code to environments and ask a bigger question: how do we make an app run identically on every machine? That's the promise of containerization.

πŸŽ‰ Nicely automated!

Your repository now has a robot doing the repetitive work. Let's give it more to do.