Skip to content

GitHub Actions & CI/CD

12 min read

GitHub Actions runs code in response to things that happen in a repository.

That is the whole primitive. Someone pushes a commit, opens a pull request, publishes a release or waits for a scheduled time; GitHub starts a workflow, which runs jobs on runners, and each job executes steps that are either shell commands or reusable actions.

Everything in this pillar — continuous integration, container builds, cloud deployments, supply-chain attestation — is that primitive applied with increasing care.

Start with GitHub Actions Fundamentals

The previous three pillars built up to a repository where changes arrive as pull requests and merge under policy. This pillar is what happens around those events automatically.

From repository to deployment

A vertical chain: a Git repository produces a GitHub event, which starts a workflow, which contains jobs, which run on a runner, executing steps and actions that build, test and scan, producing an artifact, which is deployed to an environment.

Git repositoryCommits, branches, tagsGitHub eventpush, pull_request, release, scheduleWorkflowYAML in .github/workflows/JobsRun in parallel unless they declare needsRunnerGitHub-hosted or self-hosted machineSteps and actionsShell commands, or reusable componentsBuild / test / scanThe work itselfArtifactOptional — not every workflow produces oneEnvironmentOptional — protection rules and approvalsDeploymentOptional — many workflows never deploy

The dashed stages matter. Most workflows do not deploy anything. A repository whose Actions usage is entirely “run the tests on every pull request” is using the platform correctly and completely. CI is the common case; deployment is a specialisation.

Six words carry most of the weight, and they are used loosely everywhere else on the internet.

TermWhat it is
WorkflowA YAML file in .github/workflows/ describing automation
EventThe thing that happened — a push, a pull request, a schedule firing
JobA unit of work that runs on one runner; jobs run in parallel by default
RunnerThe machine executing a job
StepOne command or one action inside a job
ActionA reusable component a step can call with uses:

The distinction that trips people up most:

GitHub Actions is the platform. An action is a reusable component you call from a step.

A workflow with no uses: lines contains no actions at all, and it is still GitHub Actions. That ambiguity is baked into the product’s name and it is worth being deliberate about — this pillar always says “an action” or “the platform” rather than relying on context.

Concretely, this is the whole shape:

name: CI
on:
pull_request:
branches: [main]
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- run: ./run-tests.sh

Seven meaningful lines. An event, a permission grant, one job, one runner, two steps — the first an action that clones the repository, the second a shell command.

Everything else in this pillar is that structure with more of it: more jobs, conditions between them, values passed along, credentials obtained safely, and outputs preserved.

Three terms used interchangeably in conversation and distinctly here.

Continuous integration is validating changes as they are integrated: build, lint, test, scan. Its output is a verdict — this change is or is not safe to merge.

Continuous delivery is ensuring every validated change could be released. It produces a deployable artefact and proves it deploys, without necessarily releasing it.

Continuous deployment is releasing every validated change automatically, with no human gate.

A successful build should not automatically imply an uncontrolled production deployment.

Which of these you want is an organisational decision, not a technical one. A team deploying twenty times a day and a team deploying quarterly can both be doing continuous integration correctly. The Continuous Delivery cluster treats the gate between them as the design decision it is.

A change from commit to production

A sequence: a commit becomes a pull request, which triggers CI comprising build, lint, test and scan; review and policy follow; the change merges; a package is built once as an artifact; it deploys to staging; validation runs; and finally it is promoted to production.

CommitLocal work, pushed to a branchPull requestThe collaboration recordCIBuild, lint, test, scanReview and policyHumans and rulesetsMergeThe change landsBuild artifactBuilt once, immutably identifiedStagingDeploy the same artifactValidationProve it works thereProductionPromote the same artifact

One property of that diagram is worth extracting because it is a genuine engineering principle rather than a convention:

Build once, promote the same artifact.

Rebuilding for each environment means production runs something that was never tested — a different binary, produced from the same source at a different moment, with different dependency resolution. Promoting one immutable artifact through environments is what makes staging validation mean anything.

  • Read and write workflow YAML without guessing, including the parts that are genuinely confusing.
  • Choose triggers deliberately, and understand why some are security-sensitive.
  • Build and test real projects in Python, Node, Go, Java, .NET, containers and infrastructure code.
  • Decide between GitHub-hosted and self-hosted runners on evidence rather than on cost alone.
  • Deploy to AWS, Azure and Google Cloud without storing long-lived cloud credentials.
  • Design reusable workflows and composite actions, and know which one a problem calls for.
  • Set permissions to least privilege, pin third-party actions, and reason about supply-chain risk.
  • Produce build provenance attestations, and verify them.
  1. Actions Fundamentals

    Workflows, events, jobs, steps, actions, variables, secrets and contexts.

    11 lessons125+ min

  2. Continuous Integration

    Building and testing Python, Node, Go, Java, .NET, containers and infrastructure code.

    8 lessons100+ min

  3. Continuous Delivery

    Deploying to clouds, containers, Kubernetes, infrastructure and static hosting.

    7 lessons35+ min

  4. Advanced Actions

    Matrices, reusable workflows, custom actions, runners, caching, artifacts and environments.

    11 lessons50+ min

  5. Actions Security

    OIDC, least privilege, action pinning, runner hardening and supply-chain integrity.

    10 lessons45+ min

The mental model, then every part of it in detail: YAML, events, jobs, steps, actions, variables, secrets, outputs, and the contexts and expressions that connect them. Eleven lessons, mostly beginner, and the foundation everything else assumes.

Eight worked pipelines — Python, Node.js, Go, Java, .NET, Docker, Terraform and Ansible. Each is a real workflow rather than a snippet, covering dependency caching, matrices, artifacts and the pull-request feedback loop.

Deploying to AWS, Azure, Google Cloud, container platforms, Kubernetes, infrastructure and GitHub Pages. Every cloud lesson uses OIDC rather than stored credentials, because that is the current recommended architecture and because the alternative ages badly.

Matrix builds, reusable workflows, composite actions, custom actions, self-hosted runners, GPU and ARM runners, caching, artifacts, environments and deployment approvals. This is where CI stops being a script and becomes platform engineering.

OIDC in depth and per cloud, eliminating long-lived credentials, least-privilege permissions, action pinning, runner hardening, workflow injection, and software supply-chain integrity with artifact attestations.

A job needs a machine. GitHub offers two kinds, and the choice has more consequences than it first appears.

GitHub-hosted runners are fresh virtual machines that GitHub provisions per job and destroys afterwards. You choose one with a label:

runs-on: ubuntu-latest # Linux x64
runs-on: ubuntu-24.04-arm # Linux ARM64
runs-on: windows-latest # Windows x64
runs-on: macos-latest # macOS on Apple silicon

The important properties are ephemerality and isolation. Every job starts from a clean image with no memory of previous runs, and nothing a job does can affect another. That is what makes it safe to run a pull request from a stranger.

Self-hosted runners are machines you provide and operate. You get control over hardware, installed software and network position — and you inherit every security and maintenance responsibility that GitHub was handling.

GitHub-hostedSelf-hosted
ProvisioningPer job, automaticYours
IsolationFresh VM every timeWhatever you build
Internal network accessNoneYes — often the reason to use them
Hardware controlLabel selection onlyTotal
Patching and updatesGitHubYou
Cost modelIncluded minutes, then per minuteYour infrastructure
Safe for untrusted pull requestsYesOnly with deliberate isolation

That last row is the one that matters most and is most often overlooked. A self-hosted runner executing a fork’s pull request is running a stranger’s code on your infrastructure, with whatever network access that machine has. Secure Self-Hosted Runners covers the threat model properly.

The honest guidance: do not self-host to reduce your bill. Self-host when you need hardware, software or network access that hosted runners cannot provide, and budget for the operational work that comes with it.

There is a security cluster, and it goes deeper. But the decisions that determine whether an Actions setup is safe are made in ordinary workflows, so they appear throughout this pillar rather than being deferred.

Five questions recur in almost every lesson:

What can this token do? GITHUB_TOKEN is issued per job with permissions you can — and should — declare explicitly.

Where do credentials come from? Stored secrets are the obvious answer and usually the wrong one for cloud access. OIDC exchanges a short-lived, signed identity token for temporary cloud credentials, so nothing long-lived exists to leak.

Is any of this input untrusted? A pull request title is attacker-controlled text. Interpolating it directly into a shell command is a script injection.

Whose code is this action? uses: some-org/some-action@v1 runs a third party’s code in your job with your token. Version references are mutable; commit SHAs are not.

Who can change the workflow? A workflow file change is a privilege change. Owning .github/workflows/ in CODEOWNERS is a small habit with a large effect.

None of these require the security cluster to act on. All five are one line each.

Worth understanding early, because it shapes design decisions throughout.

Every job consumes runner minutes. For public repositories, standard GitHub-hosted runners are free. For private repositories, minutes come from the account’s included allowance and are billed beyond it, with multipliers — Windows and macOS runners cost considerably more per minute than Linux.

Three design consequences follow:

Matrix size multiplies everything. A matrix of four language versions across three operating systems is twelve jobs, and each pays its own setup cost. That is often worth it and it should be a decision rather than an accident — see Matrix Builds.

Caching pays for itself quickly. Restoring dependencies rather than downloading them removes a fixed cost from every run, on every branch, forever. Caching covers it.

Concurrency control avoids wasted work. Cancelling superseded runs when someone pushes again stops you paying for results nobody will read.

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

That is right for CI and wrong for deployments, where cancelling a half-finished production run is worse than queueing. The deployment lessons return to this repeatedly.

This pillar assumes the previous three and links rather than repeating them.

Two connections are worth stating explicitly because they cause real confusion.

A required status check is a workflow job’s result, matched by name. Renaming a job renames the check, and a required check that no longer reports blocks every pull request permanently.

Merge queues need merge_group. A repository with a queue whose workflows only trigger on pull_request will stall silently — the required check never reports against the merge group.

The failure modes that recur across real repositories, each covered where it belongs:

  • Granting broad token permissions because the default was never narrowed.
  • Storing cloud access keys as repository secrets where OIDC would eliminate them entirely.
  • Running deployments on every branch because the trigger was never filtered.
  • Interpolating untrusted input into shell commands, turning a pull request title into a command.
  • Using a mutable action reference for third-party code, so an upstream compromise reaches you automatically.
  • Confusing cache and artifacts — one is a performance optimisation that may vanish, the other is output you intended to keep.
  • Rebuilding for production instead of promoting the artifact that was tested.
  • Running untrusted pull requests on self-hosted runners with network access.
  • Copying the same workflow into thirty repositories instead of calling a reusable one.
  • Omitting concurrency control on deployments, allowing two production runs simultaneously.

Every one of these is cheap to avoid at the point of writing and expensive to discover later.

A practical skill, since you will read far more workflows than you write. Answer these four and you know what a workflow does and what it risks, usually without reading a single step.

What triggers it? The on: block. This tells you when it runs and — critically — whether it runs on pull requests from strangers.

What can it do? The permissions: block, or its absence. No block means the repository default, which may be broad.

What is the job graph? Job names plus needs: gives the shape: what runs in parallel, what waits, what gates what.

Whose code does it run? Every uses: line that is not actions/* is a third party executing inside the job with your token in reach.

That last question is the one most often skipped and the one with the largest consequences. A workflow with eight third-party actions has eight suppliers, and none of them is reviewed by anyone in your organisation unless someone decided to.

The clusters are ordered deliberately, and Fundamentals genuinely is a prerequisite — the rest of the pillar uses contexts, expressions and job dependencies without re-explaining them.

After that, the path depends on what you are doing:

  • Setting up CI for a project — Fundamentals, then the language lesson in Continuous Integration that matches your stack.
  • Deploying to a cloud — read OIDC before the deployment lesson. The cloud lessons assume it, and understanding the trust model first makes them much shorter.
  • Standardising CI across many repositoriesReusable Workflows is the article to start from.
  • Auditing an existing setup — the Security cluster stands alone reasonably well, and Workflow Security is the broadest single page.
You should be able toCovered in
Commit, branch and pushGit Fundamentals
Explain what a pull request storesPull Requests Explained
Describe required checks and branch policyRequired Reviews
Use gh for run and workflow inspectiongh run · gh workflow
Choose a credential type deliberatelyGitHub API Authentication

You do not need prior CI/CD experience. You do need a repository you can safely break — every exercise in this pillar uses a disposable one.

GitHub Actions changes faster than anything else this site covers. Action major versions advance, runner labels are added and retired, and features arrive in preview.

Two consequences shape how this pillar is written.

Action versions are centralised. Every action reference in this site is registered in src/lib/action-versions.ts and checked against its current release by npm run check:actions. That turns “are these examples current?” from a manual audit into one command — which matters, because during this pillar’s research several actions were found to be four or more major versions ahead of what contemporary tutorials show.

Availability is stated, not assumed. Larger runners, GPU runners, some environment protections and some attestation features depend on plan, repository visibility or organisation policy. Where that is true, the page says so and links GitHub’s own documentation rather than implying universal availability.

Pages documenting vendor-controlled behaviour are marked platform-sensitive in their frontmatter, which is the list to re-check when something on your screen disagrees with something here.

Begin: GitHub Actions Fundamentals

You’ve learned how to automate builds, tests, releases and deployments. Next, learn how to protect repositories, credentials, source code, dependencies, build systems and release artifacts.

The two pillars overlap deliberately and divide cleanly: this one secures the automation, and Pillar 5 secures the lifecycle the automation sits inside — the repository it reads from, the credentials it must never encounter, the dependencies it builds against, and whether anyone downstream can verify what it produced.

Continue to Git Security & DevSecOps