Post

Github Actions

Github Actions

GitHub Actions

Motivation

Before GitHub Actions, setting up automation meant:

  • Signing up for a separate service (CircleCI, Jenkins, Travis CI)
  • Connecting it to GitHub via webhooks
  • Managing credentials across two platforms
  • Debugging integrations whenever GitHub changed something

The core problem: your code lived in one place, your automation lived somewhere else.

GitHub Actions (launched 2019) solved this by putting CI/CD inside GitHub. Workflow files live in the same repo as your code, triggered by the same events, with direct repo access — no plumbing required.


Comparison with Alternatives

Tool Native GitHub? Self-hosted? Best for
GitHub Actions ✅ Yes Optional GitHub repos, zero-friction setup
Jenkins ❌ No Required On-prem, maximum control
CircleCI ❌ No Optional Faster runners, speed-sensitive pipelines
GitLab CI ❌ No (GitLab only) Optional GitLab repos

Key Vocabulary

Term What it means
Workflow A YAML file in .github/workflows/ — one automated process
Event What triggers the workflow (push, PR, schedule, etc.)
Job A chunk of work running on its own VM. Parallel by default
Step A single command inside a job. Run sequentially
Runner The VM that executes your job (ubuntu-latest is most common)
Action A reusable plugin dropped in with uses:
Secret An encrypted variable stored in GitHub Settings, never logged

Workflow File Structure

1
2
3
4
5
6
7
8
9
10
11
12
13
14
name: My Pipeline          # Name shown in the GitHub UI

on:                        # TRIGGER — when does this run?
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:                      # JOBS — what does it do?
  my-job-id:               # Just a label you make up
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4   # Download your code
      - run: echo "Hello!"          # Run a shell command

Common Triggers (on:)

1
2
3
4
5
6
7
8
on:
  push:                        # On every push
  pull_request:                # On every PR
  schedule:
    - cron: '0 9 * * 1'       # Every Monday at 9am UTC
  workflow_dispatch:           # Manual trigger from GitHub UI
  release:
    types: [published]         # When you publish a release

A Real CI Pipeline (Node.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
name: CI Pipeline

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

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'         # Caches node_modules between runs

      - run: npm ci            # Clean install — safer than npm install in CI
      - run: npm run lint
      - run: npm test

Job Dependencies (needs:)

Without needs:, all jobs run in parallel. Use needs: to chain them:

1
2
3
4
5
6
7
8
9
10
11
12
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - run: npm test

  deploy:
    needs: test                            # Only runs if test passes
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'   # Only deploy from main
    steps:
      - run: ./deploy.sh

Secrets

Never hardcode passwords. Store them in GitHub → Settings → Secrets and variables → Actions.

1
2
3
4
5
6
7
steps:
  - name: Deploy
    run: ./deploy.sh
    env:
      API_KEY: $
      DB_URL: $
      TOKEN: $   # Auto-provided by GitHub, no setup needed

Matrix Builds

Test across multiple versions in one workflow:

1
2
3
4
5
6
7
8
9
10
11
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18, 20, 22]   # Runs the job 3 times
    steps:
      - uses: actions/setup-node@v4
        with:
          node-version: $
      - run: npm test

Docker Build & Push

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
steps:
  - uses: actions/checkout@v4

  - name: Log in to GitHub Container Registry
    uses: docker/login-action@v3
    with:
      registry: ghcr.io
      username: $
      password: $   # No setup needed

  - name: Build and push
    uses: docker/build-push-action@v5
    with:
      context: .
      push: true
      tags: ghcr.io/$:$
      cache-from: type=gha
      cache-to: type=gha,mode=max

Useful Context Variables

Variable Value
github.sha The commit SHA that triggered the run
github.ref The branch/tag ref (e.g. refs/heads/main)
github.actor The username who triggered the run
github.repository owner/repo-name
github.event_name The event type (push, pull_request, etc.)

Common Pitfalls

  • No needs: → deploy job starts before tests finish
  • Hardcoded secrets → always use $
  • YAML indentation errors → use a YAML linter or the GitHub browser editor
  • Tagging Docker images as latest → always tag with github.sha for traceability
  • Secrets in forked PRs → GitHub blocks secrets from fork PRs by design
  • Multiline run commands → use the | operator:
1
2
3
- run: |
    echo "line one"
    echo "line two"

Quick Reference

1
2
3
4
5
6
7
8
9
10
11
12
.github/
  workflows/
    ci.yml          ← your workflow file

Trigger → Job(s) → Steps → Done
         (parallel)  (sequential)

needs:            → chain jobs in order
if:               → conditional execution
secrets:          → $
matrix:           → run job N times with different values
GITHUB_TOKEN      → auto-provided, no setup needed
This post is licensed under CC BY 4.0 by the author.