contentintech
Learn/devops/GitHub Actions
Beginner~20 min read

GitHub Actions

Workflows, events and triggers, jobs and steps, runners, using marketplace actions with uses and run, matrix strategies, secrets and environments, artifacts, caching, reusable workflows, permissions, and the GITHUB_TOKEN.

CI/CDWorkflowsRunnersSecrets

What GitHub Actions Is

GitHub Actions is GitHub's built-in CI/CD platform. You describe automation as workflows — YAML files stored in your repo — that run in response to repository events like a push, a pull request, or a schedule. GitHub runs them on hosted virtual machines called runners.

Where Workflows Live

Workflow files go in .github/workflows/ and end in .yml. Each file is one workflow. A minimal example:

yaml
# .github/workflows/ci.yml
name: CI

on: push

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: echo "Hello from GitHub Actions"

Mental model

A workflow contains jobs. Each job runs on its own fresh runner and contains ordered steps. A step either runs a shell command (run) or calls a reusable action (uses).

Events and Triggers

The on key declares what starts a workflow. Common triggers:

EventFires when
pushCommits are pushed to a branch or tag
pull_requestA PR is opened, updated, or reopened
scheduleOn a cron timetable (UTC)
workflow_dispatchManually, from the Actions tab (can take inputs)
yaml
on:
  push:
    branches: [main]
    paths: ["src/**"]          # only when src changes
  pull_request:
    branches: [main]
  schedule:
    - cron: "0 6 * * *"        # daily at 06:00 UTC
  workflow_dispatch:
    inputs:
      environment:
        description: "Deploy target"
        default: staging

Jobs, Steps, and Runners

Jobs run in parallel by default. Use needs to make one job wait for another. Each job specifies its runner via runs-on.

yaml
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "22"
          cache: npm
      - run: npm ci
      - run: npm test

  deploy:
    needs: test              # waits for test to succeed
    runs-on: ubuntu-latest
    steps:
      - run: echo "Deploying..."

GitHub-hosted runner labels include ubuntu-latest, windows-latest, and macos-latest. For custom hardware or private networks you register your own self-hosted runners.

uses vs run

KeywordPurpose
runExecutes shell commands on the runner.
usesRuns a prebuilt action from the Marketplace or a repo, passing inputs via with.

Pin your actions

Reference actions by a full version tag such as actions/checkout@v4, or for maximum supply-chain safety pin to a full commit SHA. Never rely on a mutable branch like @main for third-party actions.

Matrix Strategy

A matrix runs the same job across many combinations of values — great for testing multiple language versions or operating systems.

yaml
jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, windows-latest]
        node: ["20", "22"]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
      - run: npm ci && npm test

Secrets and Environments

Never hard-code credentials. Store them as repository or organization secrets and read them via the secrets context. Environments add protection rules — required reviewers, wait timers, and environment-scoped secrets.

yaml
jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production        # requires approval if configured
    steps:
      - run: ./deploy.sh
        env:
          API_TOKEN: ${{ secrets.API_TOKEN }}

GITHUB_TOKEN and Permissions

Every run gets an automatic GITHUB_TOKEN secret for authenticating to the GitHub API. Follow least privilege by declaring permissions explicitly — the modern default is read-only.

yaml
permissions:
  contents: read
  pull-requests: write        # e.g. to comment on a PR

steps:
  - run: gh pr comment $PR --body "Build passed"
    env:
      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      PR: ${{ github.event.number }}

Caching Dependencies

Caching speeds up builds by restoring dependency directories between runs. Many setup actions cache automatically, but actions/cache gives full control.

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

Artifacts

Artifacts persist files (build output, test reports, coverage) after a run so you can download them or pass them between jobs.

yaml
- uses: actions/upload-artifact@v4
  with:
    name: dist
    path: dist/

# in a later job:
- uses: actions/download-artifact@v4
  with:
    name: dist

Reusable Workflows

Avoid copy-pasting pipelines across repos by defining a workflow that others call with workflow_call.

yaml
# .github/workflows/reusable-deploy.yml
on:
  workflow_call:
    inputs:
      env: { required: true, type: string }
    secrets:
      token: { required: true }

# caller
jobs:
  call:
    uses: my-org/repo/.github/workflows/reusable-deploy.yml@v1
    with:
      env: staging
    secrets:
      token: ${{ secrets.DEPLOY_TOKEN }}

Practice Exercises

  1. Create a workflow that runs on every push and pull_request to main, checks out the code, and runs your test suite.
  2. Add a matrix that tests against Node 20 and 22 on both Ubuntu and Windows.
  3. Cache your package manager's download directory with actions/cache and confirm a cache hit on the second run.
  4. Build the project and upload the dist/ folder as an artifact, then download it in a dependent job that needs the build.
  5. Add a manual workflow_dispatch deploy job that targets a production environment protected by a required reviewer, reading a secret token.
  6. Set top-level permissions to read-only and grant only the scopes a job actually needs.

Section navigation