GitHub Actions: Workflows from Scratch
Master GitHub Actions workflow syntax β events, jobs, runners, steps, and reusable workflows. Build CI/CD pipelines that run on every push, pull request, or schedule.
What is GitHub Actions?
Think of a smart home automation system.
You set rules: βWhen the front door opens (event), turn on the hallway lights (job 1) AND start the security camera (job 2).β Each rule has a trigger (the event) and one or more actions that happen in response. Some actions happen in parallel (lights + camera), others happen in sequence (unlock door, then disarm alarm).
GitHub Actions works the same way for your code repository. You define rules: βWhen someone pushes code (event), run the build (job 1) AND run the tests (job 2).β Workflows live in YAML files inside your repository, and GitHubβs cloud runners execute them automatically.
The workflow hierarchy
Workflow (.github/workflows/*.yml)
βββ Jobs (run in parallel by default)
βββ Steps (run sequentially)
| Level | What It Represents | Key Property |
|---|---|---|
| Workflow | The entire automation definition | on: β which events trigger it |
| Job | A unit of work on one runner | runs-on: β which runner executes it |
| Step | A single action or command | uses: (action) or run: (script) |
Key difference from Azure Pipelines: GitHub Actions has no βstageβ level. Jobs ARE the top-level unit under a workflow. You create stage-like behaviour by grouping jobs with needs: dependencies.
Events β when workflows run
GitHub Actions supports 50+ event types. The exam focuses on the most common:
Code events
| Event | Fires When | Common Use |
|---|---|---|
| push | Code is pushed to a branch | CI builds, deployments on merge to main |
| pull_request | PR is opened, updated (synchronize), reopened, or closed | PR validation, code review automation |
| pull_request_target | Same as pull_request but runs in the BASE branch context | Safe automation on PRs from forks (avoids secret leakage) |
Scheduled events
on:
schedule:
- cron: '30 5 * * 1-5'
Runs on a UTC cron schedule. Note: GitHub Actions cron can be delayed during high-demand periods β donβt rely on precise timing for time-critical workflows.
Manual and API events
| Event | Fires When | Use Case |
|---|---|---|
| workflow_dispatch | Manually triggered from the UI or REST API | Ad-hoc deployments, manual test runs |
| repository_dispatch | External system sends a webhook | Cross-repo triggers, external CI integration |
Event filtering
Events support filtering by branch, path, tag, and activity type:
on:
push:
branches:
- main
- 'release/**'
paths:
- 'src/**'
tags:
- 'v*'
pull_request:
branches:
- main
types:
- opened
- synchronize
The types: filter is unique to GitHub Actions β you can react to specific PR activities (opened, closed, labeled, review_requested) rather than all PR events.
Exam tip: pull_request vs pull_request_target
This is a high-value exam topic:
- pull_request runs the workflow from the HEAD branch (the PRβs source code). Secrets from the base repo are NOT available to PRs from forks β this prevents secret theft.
- pull_request_target runs the workflow from the BASE branch (the target branch). Secrets ARE available, but the code being tested is from the BASE, not the PR. Use this for safe label-based automation on fork PRs.
Security rule: Never use pull_request_target with actions/checkout pointing at the PR head AND passing secrets β this allows a fork PR to inject malicious code that runs with your secrets.
Jobs β parallel by default
Jobs within a workflow run in parallel unless you add needs: to create dependencies:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm test
deploy:
runs-on: ubuntu-latest
needs: [build, test]
steps:
- run: echo "Deploying after build and test succeed"
In this example, build and test run in parallel. deploy waits for both to finish. This is βfan-out / fan-inβ execution.
Matrix strategy
A matrix lets you run the same job across multiple configurations (OS, language version, Node version) in parallel:
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node: [18, 20, 22]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm ci && npm test
This creates 9 parallel jobs (3 OS times 3 Node versions). Matrix is powerful for testing cross-platform compatibility.
Key matrix options:
fail-fast: falseβ continue all matrix jobs even if one fails (default is true β cancel all on first failure)max-parallel: 2β limit how many matrix jobs run simultaneouslyinclude:/exclude:β add or remove specific combinations
Steps: actions vs run
Steps execute sequentially within a job:
Actions (uses:)
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
Actions are reusable units from the GitHub Marketplace or your own repositories. The @v4 pins the major version.
Scripts (run:)
- run: npm ci && npm test
env:
NODE_ENV: test
Shell commands executed on the runner. Use shell: bash or shell: pwsh to specify the shell explicitly.
Key built-in actions
| Action | Purpose |
|---|---|
| actions/checkout@v4 | Check out repository code onto the runner |
| actions/setup-node@v4 | Install a specific Node.js version |
| actions/setup-dotnet@v4 | Install a specific .NET SDK version |
| actions/setup-java@v4 | Install a specific JDK version |
| actions/setup-python@v5 | Install a specific Python version |
| actions/cache@v4 | Cache dependencies (node_modules, .nuget) between runs |
| actions/upload-artifact@v4 | Upload build artefacts for later jobs or downloads |
| actions/download-artifact@v4 | Download artefacts from previous jobs |
Environments with protection rules
GitHub Actions environments provide deployment targets with governance:
jobs:
deploy-staging:
runs-on: ubuntu-latest
environment: staging
steps:
- run: echo "Deploy to staging"
deploy-production:
runs-on: ubuntu-latest
needs: deploy-staging
environment: production
steps:
- run: echo "Deploy to production"
Environment protection rules:
- Required reviewers β pause the workflow until specified people approve
- Wait timer β delay deployment by N minutes (cool-down period)
- Deployment branches β restrict which branches can deploy (e.g., only
mainto production) - Custom protection rules β call external APIs for approval decisions
Environments also store secrets and variables scoped to that environment β production secrets are only available to jobs targeting the production environment.
Azure Pipelines vs GitHub Actions β side by side
| Concept | Azure Pipelines | GitHub Actions |
|---|---|---|
| Definition file | azure-pipelines.yml (root of repo) | .github/workflows/*.yml (multiple files supported) |
| Hierarchy | Pipeline > Stages > Jobs > Steps | Workflow > Jobs > Steps (no stage concept) |
| CI trigger | trigger: keyword with branch/path filters | on: push with branch/path/tag filters |
| PR trigger | pr: keyword (Azure Repos) or branch policies (GitHub) | on: pull_request with branch/type filters |
| Scheduled trigger | schedules: with cron and branch filters | on: schedule with cron expression |
| Manual trigger | Pipeline can always be queued manually, or use trigger: none | on: workflow_dispatch with optional input parameters |
| Job parallelism | Jobs within a stage run in parallel by default | Jobs within a workflow run in parallel by default |
| Dependencies | dependsOn: keyword at stage or job level | needs: keyword at job level |
| Matrix | strategy: matrix on jobs | strategy: matrix on jobs (similar syntax) |
| Reusable templates | YAML templates (extends, include) for stages, jobs, steps | Reusable workflows (workflow_call), composite actions |
| Environments | environment: keyword on deployment jobs, with checks and approvals | environment: keyword on jobs, with protection rules (reviewers, timers, branch restrictions) |
| Marketplace | Azure DevOps Marketplace (tasks and extensions) | GitHub Marketplace (20,000+ actions) |
Scenario: Kai builds the Launchpad Labs CI workflow
π Kai Tanaka at Launchpad Labs sets up CI for their Next.js SaaS app. The team is GitHub-first, so Actions is the natural choice.
The workflow:
- Trigger β
on: pushto main andon: pull_requestto main - Matrix β test on Node 20 and 22, on Ubuntu and Windows (4 combinations)
- Steps β checkout, setup-node with caching, npm ci, lint, test with coverage, build
- Artefact upload β the build output is uploaded with
actions/upload-artifact - Deploy job β
needs: [test], targets thestagingenvironment with required reviewer (Sam, CTO), deploys to Kubernetes
Riku (frontend) asks: βWhy does the matrix run 4 jobs? We only deploy on Ubuntu.β Kai explains: βThe matrix validates cross-platform compatibility. The deploy job runs once, after all matrix jobs pass β needs: creates the dependency.β
Zoe (QA) adds a comment: βCan I run E2E tests only on PRs to main?β Kai adds a separate job with if: github.event_name == 'pull_request' to conditionally run Playwright tests.
Knowledge check
Kai defines a workflow with three jobs: lint, test, and deploy. Lint and test have no 'needs:' keyword. Deploy has 'needs: [lint, test]'. What is the execution order?
A workflow uses on: pull_request to run CI checks. An external contributor forks the repo and opens a PR. The workflow needs a secret (API_KEY) to run integration tests. What happens?
Nadia wants to migrate a three-stage Azure Pipeline (Build > Test > Deploy) to GitHub Actions. Azure Pipelines has stages β GitHub Actions does not. How should she model the three stages?
π¬ Video coming soon
Next up: Pipeline Agents β choose between hosted and self-hosted agents, design agent infrastructure, and handle hybrid and air-gapped environments.