- Part 1: Repositories, Branches & Pull Requests →
- ▸ Part 2: GitHub Actions, Workflows & OIDC YOU ARE HERE
- Part 3: Packages, Container Registry & Security →
Note: This is Part 2 of a three-part deep dive into GitHub. Part 1 covered the core collaboration model. Part 3 covers GitHub Packages and Security.
GitHub Actions is the automation engine baked into every GitHub repository. It runs CI/CD pipelines, but also schedules, code maintenance bots, infrastructure provisioning, release publishing, security scans, and almost anything else triggered by an event on GitHub. With more than 20,000 prebuilt actions in the Marketplace and tight integration with the rest of the platform, it has become one of the most widely used CI/CD systems in the industry. This article digs into how it actually works under the hood and how to use it well.
1. The Core Abstractions
GitHub Actions is built on a small set of nested abstractions: an event triggers one or more workflows, each workflow contains one or more jobs, each job runs on a runner, and each job contains a sequence of steps that execute either shell commands or reusable actions.
2. Anatomy of a Workflow File
Workflows live in .github/workflows/*.yml. Here is a representative file that runs on every push and pull request, sets up the toolchain, runs tests, and uploads a coverage artifact.
name: CI
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
permissions:
contents: read
pull-requests: write
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm test -- --coverage
- uses: actions/upload-artifact@v4
with:
name: coverage
path: coverage/
3. Events: 35+ Triggers
Workflows can react to many events: push, pull_request, pull_request_target, schedule (cron), workflow_dispatch (manual), repository_dispatch (API), release, issues, discussion, deployment_status, and dozens more.
| Event | Use Case | Notes |
|---|---|---|
push | CI on every commit | Filter with branches / paths |
pull_request | PR validation | Forks have read-only secrets |
schedule | Nightly builds, sweeps | UTC cron, min interval 5min |
workflow_dispatch | Manual deploy with inputs | Type-safe parameter forms |
release | Publish on tag/release | Pairs with Releases API |
4. Runners: Hosted, Self-hosted, Larger
A runner is the machine that executes a job. GitHub offers GitHub-hosted runners (free for public repos, metered for private), larger runners (more CPU/RAM, GPU options, optional static IPs), and self-hosted runners (your own machines, on-prem or in your cloud). Each runner type has distinct trade-offs around security, performance, and cost.
| Type | Specs | Pros | Cons |
|---|---|---|---|
| Hosted (standard) | 2-4 vCPU, 7-16 GB | Zero setup, free for OSS | No persistent state |
| Larger runners | Up to 96 vCPU, GPU | Fast builds, IP allow-list | Higher per-minute price |
| Self-hosted | Your hardware | Custom tools, cached state, in-VPC access | You maintain and secure them |
5. Matrix Builds: Parallel by Design
The matrix strategy fans out a single job into many parallel jobs across combinations of dimensions: OS, language version, database version, etc. It is the most cost-effective way to validate code across the platform spectrum.
jobs:
test:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node: [18, 20, 22]
include:
- os: ubuntu-latest
node: 20
coverage: true
exclude:
- os: macos-latest
node: 18
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '${{ matrix.node }}' }
- run: npm ci && npm test
6. Caching and Artifacts
Two distinct primitives. Caches persist build inputs (npm, pip, cargo, Maven, Docker layers) across runs to speed up subsequent jobs, keyed by file hash. Artifacts persist build outputs (binaries, reports, coverage) for download or downstream jobs. Both have size and retention limits.
Reused inputs (deps, build cache). Auto-evicted after 7 days unused. 10 GB per repo.
Workflow output. Configurable retention (1-90 days). Downloadable from UI or API.
7. Reusable Workflows and Composite Actions
Three mechanisms for reuse, each suited to different scopes. Composite actions bundle a series of steps; reusable workflows bundle whole jobs and can be called by other workflows with typed inputs and secrets; JavaScript / Docker actions are full custom actions you publish to the Marketplace.
# .github/workflows/reusable-deploy.yml
name: Reusable Deploy
on:
workflow_call:
inputs:
environment: { required: true, type: string }
secrets:
DEPLOY_TOKEN: { required: true }
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- run: ./deploy.sh ${{ inputs.environment }}
env:
TOKEN: ${{ secrets.DEPLOY_TOKEN }}
# Caller:
jobs:
staging:
uses: ./.github/workflows/reusable-deploy.yml
with: { environment: staging }
secrets: inherit
8. Secrets, Variables and Environments
Sensitive config lives in repository, environment, or organization secrets. Non-sensitive config uses variables. Environments are named deployment targets (e.g., staging, production) that can require approvers, wait timers, branch restrictions, and have their own scoped secrets - the cleanest way to gate production deployments.
Shared across selected repos. Used for vendor API keys.
Scoped to one repo. Default location for most secrets.
Tied to deployment targets. Supports manual approval gates.
9. OIDC: Keyless Cloud Authentication
The single most important security upgrade you can make to GitHub Actions is replacing long-lived cloud credentials with OpenID Connect (OIDC). Each workflow run gets a short-lived JWT signed by the GitHub identity provider, which AWS / Azure / GCP / Vault can verify and trust to issue temporary credentials. No more AWS_ACCESS_KEY_ID secrets sitting in the repo.
permissions:
id-token: write # required for OIDC
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubDeployRole
aws-region: us-east-1
- run: aws s3 sync ./dist s3://my-bucket/
10. The Runner Sandbox Model
Each job runs on a fresh runner with no persistent state by default. The workflow checks out the repo, installs tools, runs steps, then the runner is destroyed (for hosted runners) or returned to the pool (for self-hosted). The job has access to a built-in GITHUB_TOKEN with permissions scoped via the permissions: key.
11. Observability and Debugging
Each run gets a real-time log view with collapsible sections, downloadable logs, and a re-run option (full or failed-only). For deeper debugging, enable step debug logging with ACTIONS_STEP_DEBUG=true, set summary output with $GITHUB_STEP_SUMMARY, and use actions/upload-artifact to ship logs out for analysis.
ACTIONS_STEP_DEBUG=trueverbose step logsACTIONS_RUNNER_DEBUG=trueverbose runner logs$GITHUB_STEP_SUMMARYrender markdown into the run UIactions/upload-artifactexfiltrate logs and reportsgh run watch+gh run view --logCLI tailing
12. Performance and Cost Optimization
Action minutes add up fast on private repos. Practical optimizations: cache aggressively, use path filters to skip unchanged areas, run quick checks first with fail-fast, parallelize with matrices, split monolithic workflows, and consider larger runners - a 16-core runner that finishes in 2 minutes can be cheaper than a 2-core runner taking 20 minutes.
13. Marketplace and Pinning
The Marketplace has more than 20,000 actions, but third-party actions are arbitrary code running with your secrets. Best practices: prefer actions from verified creators (especially actions/* and github/*), pin by full commit SHA rather than tag (tags are mutable), enable Dependabot for action updates, and consider mirroring critical actions into your org.
# Good - pinned to immutable SHA
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
# Bad - mutable tag, can be overwritten
- uses: random-author/some-action@v1
Conclusion
GitHub Actions packs a remarkable amount of capability into a YAML format, but its power comes from understanding the layered model: events trigger workflows, workflows orchestrate jobs, jobs run on runners, and steps invoke actions. Combine that with the modern toolkit (OIDC for keyless cloud auth, environments for production gates, matrix builds for coverage, reusable workflows for DRY pipelines, and SHA-pinning for supply chain safety) and you have one of the most flexible automation platforms available. Part 3 finishes the series with GitHub Packages and the GitHub Advanced Security suite: Dependabot, code scanning, secret scanning, push protection, and SBOM generation.
- Part 1: Repositories, Branches & Pull Requests →
- ▸ Part 2: GitHub Actions, Workflows & OIDC YOU ARE HERE
- Part 3: Packages, Container Registry & Security →