Skip to main content
Juliano Alves
Back to blog

GitHub Actions

4 min read
By Juliano Alves

GitHub Actions runs arbitrary automation on GitHub-hosted or self-hosted runners when events occur in your repository. It is YAML-first, integrates with the GitHub permission model, and scales from “run tests on PR” to multi-environment continuous delivery.

This post goes beyond a hello-world workflow: matrices, caching, reusable workflows, and security basics.

Anatomy of a workflow#

name: CI

on:
  push:
    branches: [main]
  pull_request:

concurrency:
  group: ci-${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 15

    steps:
      - uses: actions/checkout@v4

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

      - name: Install
        run: npm ci

      - name: Unit tests
        run: npm test -- --coverage

      - name: Upload coverage
        uses: actions/upload-artifact@v4
        with:
          name: coverage
          path: coverage/

Highlights#

  • concurrency: cancels outdated runs on the same branch—saves minutes when people push rapidly.
  • timeout-minutes: prevents hung jobs from burning quota indefinitely.
  • npm ci: deterministic installs from lockfile—prefer over npm install in CI.

Matrix builds#

Test across Node versions or OS targets:

strategy:
  matrix:
    node: [18, 20, 22]
steps:
  - uses: actions/setup-node@v4
    with:
      node-version: ${{ matrix.node }}

Use fail-fast: false when you want every cell to finish even after one fails (useful for compatibility matrices).

Reusable workflows#

Extract common pipelines to .github/workflows/reusable-ci.yml with workflow_call triggers, then call from app repos with uses: org/repo/.github/workflows/reusable-ci.yml@main. Centralizes Node version, lint commands, and artifact retention policies.

Caching beyond setup-node#

For monorepos with Turborepo or Nx, cache remote artifacts:

- uses: actions/cache@v4
  with:
    path: .turbo
    key: turbo-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}-${{ github.sha }}
    restore-keys: |
      turbo-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}-

Tune keys when cache poisoning is a concern (rare for build artifacts, more relevant for sensitive outputs).

Secrets and environments#

  • Store long-lived tokens as repository or organization secrets.
  • Use environments with required reviewers for production deploy jobs.
  • Prefer OIDC federation to AWS/GCP/Azure over copying static cloud keys into GitHub when possible—short-lived tokens per job.

Never echo secrets to logs; GitHub masks known patterns but custom formats can leak.

Self-hosted runners#

Use when you need GPUs, private network access, or unusual dependencies. Harden machines: auto-update, single-purpose runners, network segmentation—runners execute untrusted PR code.

Debugging#

  • Re-run jobs with debug logging enabled.
  • SSH-like debugging: tmate action for stuck steps (use sparingly, security implications).

Summary#

Actions shines when workflows are small, fast, and idempotent: concurrency control, deterministic installs, matrices for coverage, reusable workflows for consistency, and modern secret patterns (OIDC). Treat YAML as code—review changes like application logic.

© 2026 Juliano Alves. All rights reserved.