Guide
GitHub Actions fundamentals explained
Push a commit, and within two minutes a remote machine installs your dependencies,
runs your test suite, builds a container image, and opens a deploy preview —
all without you SSH-ing anywhere. That is GitHub Actions: a
built-in CI/CD engine tied to your
Git repository.
Workflows live as YAML files in .github/workflows/, triggered by
pushes, pull requests, schedules, or manual buttons. GitHub provisions ephemeral
runners (Ubuntu, Windows, or macOS VMs) for each job, executes
steps from the Actions marketplace or your own scripts, and tears the machine down
when finished. For teams already on GitHub, Actions removes the Jenkins server tax
while integrating branch protection, environments, and OIDC cloud deploys. This
guide covers workflow anatomy, jobs and dependency graphs, secrets and
environments, matrix testing, caching and artifacts, reusable workflows, OIDC
patterns, a Harbor Supply pipeline worked example, a tooling decision table,
common pitfalls, and a practitioner checklist — alongside our
CI/CD pipelines explainer,
Docker fundamentals guide,
and
Playwright E2E testing overview.
Core concepts: workflows, jobs, steps, and runners
GitHub Actions organizes automation into four nested layers:
- Workflow — one YAML file triggered by an event (e.g.
pushtomain). A repo can have many workflows. - Job — a set of steps that run on the same runner. Jobs in one workflow run in parallel by default unless connected with
needs:. - Step — a single task: run a shell command or invoke a packaged action from the marketplace.
- Runner — the VM executing the job. GitHub-hosted runners are fresh each run; self-hosted runners persist on your infrastructure.
A minimal workflow looks like this:
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm test
actions/checkout clones the commit that triggered the workflow.
Subsequent steps see the repo at that exact SHA — reproducible builds
are the point of
continuous integration.
Triggers you will use daily
push/pull_request— standard CI on every change; filter by branch or path withpaths:to skip docs-only commits.workflow_dispatch— manual “Run workflow” button in the UI; useful for one-off migrations.schedule— cron syntax for nightly security scans or dependency updates.release/workflow_call— publish on GitHub Release tags; invoke reusable workflows from other repos.
Jobs, dependencies, and concurrency
Split pipelines into focused jobs: lint, unit-test,
build-image, deploy-staging. Parallel jobs finish
faster than one monolithic script. Chain jobs with needs: so deploy
waits for tests:
jobs:
test:
runs-on: ubuntu-latest
steps: [ ... ]
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps: [ ... ]
Use concurrency: groups to cancel superseded runs when a developer
pushes three fixes in a row — only the latest commit should consume runner
minutes. Set cancel-in-progress: true on feature-branch CI; use
false on production deploys so two releases never fight.
Matrix builds
Test across Node 18 and 20, or Ubuntu and macOS, with a single job definition:
strategy:
matrix:
node: [18, 20]
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
Matrix jobs multiply runner minutes — a 2×2 grid is four VMs. Use
fail-fast: false when you want every combination to finish even if
one fails (helpful for flaky browser tests). Pair matrix Node versions with
Playwright
sharding for large E2E suites.
Actions marketplace and custom actions
An action is a reusable unit — JavaScript, Docker, or
composite shell steps — published at
owner/repo@version. Pin to a major version tag (@v4)
or an immutable SHA for supply-chain safety. Common actions:
actions/checkout— clone repo with optional submodules and LFS.actions/setup-node,setup-python,setup-go— install language runtimes with caching hooks.actions/cache— persist~/.npm, Gradle, or pip directories between runs.actions/upload-artifact/download-artifact— pass build outputs between jobs.docker/build-push-action— build and push images to GHCR or ECR.
Composite actions bundle shell steps your org reuses across repos
(e.g. “run lint + test with our standard Node version”). Store them
in .github/actions/my-action/action.yml and reference with
uses: ./.github/actions/my-action.
Reusable workflows
When multiple repositories need identical deploy logic, extract a workflow to a
central repo and call it with workflow_call. Inputs and secrets pass
explicitly — no copy-paste YAML drift across forty microservices.
Secrets, variables, and environments
Never commit API keys. Store them in repository secrets
(Settings → Secrets) and reference as
${{ secrets.MY_TOKEN }}. GitHub masks secret values in logs, but
secrets can still leak through careless echo commands or error
messages — treat CI logs as semi-public.
- Repository secrets — available to all workflows in the repo.
- Environment secrets — scoped to
productionorstaging; pair with protection rules requiring reviewer approval before deploy jobs run. - Organization secrets — shared across repos with selective access.
- Variables — non-sensitive config (region names, feature flags) exposed as
${{ vars.AWS_REGION }}.
Reference an environment in a job:
deploy:
environment: production
steps:
- run: ./deploy.sh
env:
API_KEY: ${{ secrets.PROD_API_KEY }}
OIDC instead of long-lived cloud keys
Modern deploy workflows exchange a short-lived OIDC token from GitHub for AWS,
GCP, or Azure credentials — no static AWS_ACCESS_KEY_ID in
secrets. Configure an IAM role trust policy on token.actions.githubusercontent.com
and use aws-actions/configure-aws-credentials. Tokens expire in
minutes; a leaked log line is far less catastrophic than a permanent key.
Caching, artifacts, and permissions
Cold npm ci on every push wastes time. actions/cache
keys on lockfile hashes restore node_modules when dependencies are
unchanged. Language setup actions often include cache: npm as a
one-liner. Cache misses still complete — they just reinstall.
Artifacts pass built binaries, coverage reports, or
Docker image
tarballs from a build job to a deploy job in the same
workflow run. Set retention days to avoid unbounded storage costs on busy repos.
Default GITHUB_TOKEN permissions are read-only since 2023. Explicitly
set permissions: at workflow or job level — contents: read
for CI, contents: write plus packages: write only on
release jobs. Principle of least privilege limits damage if a third-party action
is compromised.
Worked example: Harbor Supply CI pipeline
Harbor Supply runs a Node.js inventory API and a React admin dashboard in one
monorepo. Their ci.yml workflow triggers on every pull request and
on pushes to main:
- Job
lint— checkout, setup Node 20 with npm cache, run ESLint and TypeScripttsc --noEmit. Fails in ~40 seconds. - Job
unit-test— parallel with lint;npm test -- --coverage; uploads coverage artifact. - Job
e2e—needs: [lint, unit-test]; installs Playwright browsers, runs three spec files against a local dev server started in background; uploads trace on failure. - Job
build-image— onmainonly after e2e passes; builds Docker image, tags with git SHA, pushes to GitHub Container Registry usingGITHUB_TOKEN. - Job
deploy-staging—environment: staging; pulls image by SHA, rolls Kubernetes Deployment; smoke test hits/healthz. - Job
deploy-production—environment: productionwith required reviewer; manual gate after staging green for 30 minutes on weekday releases.
Path filters skip the workflow when only docs/ changes — saving
~800 runner minutes per month. Concurrency group ci-${{ github.ref }}
cancels stale PR runs. Production deploy uses OIDC to AWS EKS; no static kubeconfig
in secrets.
Tooling decision table
| Question | Best choice | Why |
|---|---|---|
| Code already on GitHub, team < 50 engineers? | GitHub Actions | Zero infra, native PR checks, free tier for public repos. |
| Self-hosted GitLab with compliance air-gap? | GitLab CI | Runs entirely inside your VPC; same YAML mental model. |
| Hundreds of plugins, legacy Jenkinsfiles? | Jenkins | Mature ecosystem; you operate the controller and agents. |
| Complex monorepo with custom remote cache? | Buildkite / CircleCI | Elastic agent pools, advanced caching, per-minute billing clarity. |
| Container-native GitOps deploy only? | Argo CD + minimal CI | CI builds image; cluster pulls from Git; see GitOps guide. |
| Open-source project wanting free macOS builds? | GitHub Actions | macOS runners included; other vendors charge premium. |
| Need deterministic hermetic builds (Bazel)? | Self-hosted or BuildBuddy | GitHub cache helps but remote execution may need dedicated infra. |
| Regulated industry requiring signed attestations? | GHA + artifact attestations | Native SLSA provenance generation on supported ecosystems. |
Common pitfalls
- Floating action tags —
@maincan change overnight; pin SHAs for security-critical steps. - Secrets in fork PRs — workflows from forks do not receive write secrets; use
pull_request_targetonly with extreme care (RCE risk). - Unbounded matrix — 10 OS × 8 Node versions = 80 jobs; costs explode on private repos.
- Missing
permissions:— overly broad default write access before you tighten it. - Cache poisoning — cache keys must include lockfile hash; never cache
node_moduleskeyed only on branch name. - Deploy on green without smoke tests — CI green does not mean production healthy; add post-deploy checks.
- Ignoring runner minute budgets — private repos bill per minute; optimize with path filters and concurrency cancel.
- Storing production DB URLs in repo variables — variables are visible to anyone with repo read access; use environment secrets.
- One giant workflow file — 800-line YAML becomes unmaintainable; split by concern or use reusable workflows.
Practitioner checklist
- Add a minimal
ci.ymlthat runs lint and unit tests on every PR. - Pin third-party actions to version tags or commit SHAs.
- Enable branch protection requiring the CI check before merge.
- Configure
concurrencyto cancel outdated PR runs. - Cache dependencies keyed on lockfile content hash.
- Split build and deploy into separate jobs with
needs:. - Create
stagingandproductionenvironments with approval gates on production. - Replace long-lived cloud keys with OIDC federation where supported.
- Set explicit least-privilege
permissions:per workflow. - Upload test artifacts and Playwright traces on failure for debugging.
Key takeaways
- GitHub Actions runs CI/CD as YAML workflows triggered by Git events, with ephemeral hosted runners.
- Jobs parallelize work;
needs:chains dependencies;matrixmultiplies test coverage across versions and OSes. - Secrets and environments gate production deploys; prefer OIDC over static cloud credentials.
- Caching and artifacts speed pipelines and pass build outputs between jobs.
- Pin actions, scope permissions, and treat workflow design as production code — it ships what users see.
Related reading
- CI/CD pipelines explained — continuous integration, delivery, and deployment concepts
- Git fundamentals explained — branches, merges, and pull request workflows
- Docker fundamentals explained — container images Actions builds and pushes
- Playwright E2E testing explained — browser tests in CI pipelines