Stage 5 · Platform
Building Pipelines
GitHub Actions Workflows
Events, workflow_dispatch, reusable workflows, composite actions, and permissions in YAML.
Workflow File Structure
GitHub Actions workflows are YAML files stored in .github/workflows/. Each file defines a workflow with a name, trigger events, and one or more jobs. Workflows are independent — they do not share state or depend on each other by default.
name: CI
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: echo "Hello from GitHub Actions"The name field appears in the GitHub UI. The on field defines triggers. The jobs section contains one or more jobs, each with steps that run sequentially on a fresh runner.
Events and Triggers
Workflows trigger on GitHub events like push, pull_request, schedule, and workflow_dispatch. You can filter triggers by branch, path, or activity type. Understanding event filtering is critical to avoid running unnecessary pipelines.
name: Deploy
on:
push:
branches: [main]
paths:
- "src/**"
- "package.json"
- "Dockerfile"
paths-ignore:
- "**.md"
- ".github/workflows/docs.yml"
pull_request:
branches: [main]
types: [opened, synchronize, reopened]
schedule:
- cron: "0 2 * * 1" # Monday at 2 AM UTC
workflow_dispatch:
inputs:
environment:
description: "Target environment"
required: true
default: "staging"
type: choice
options:
- staging
- production
dry_run:
description: "Dry run"
type: boolean
default: falsepaths filters avoid running deploy workflows when only documentation changes. workflow_dispatch lets you trigger manually with inputs. schedule runs the workflow on a cron expression.
Reusable Workflows
Reusable workflows let you define a workflow once and call it from other workflows. They accept inputs and secrets as parameters, reducing duplication across repositories. This is the GitHub Actions equivalent of a function call.
name: Reusable CI
on:
workflow_call:
inputs:
node-version:
required: false
type: string
default: "20"
run-integration-tests:
required: false
type: boolean
default: false
secrets:
NPM_TOKEN:
required: true
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: npm
- run: npm ci
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- run: npm test
- run: npm run build
integration:
if: ${{ inputs.run-integration-tests }}
runs-on: ubuntu-latest
steps:
- run: npm run test:integrationThis reusable workflow accepts a node-version input and a boolean for integration tests. It also receives the NPM_TOKEN secret from the calling workflow.
name: App CI
on: push
jobs:
ci:
uses: ./.github/workflows/reusable-ci.yml
with:
node-version: "20"
run-integration-tests: true
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}The calling workflow uses the uses keyword to reference the reusable workflow. Inputs and secrets are passed explicitly. The reusable workflow runs as if its steps were defined directly in the caller.
Composite Actions
Composite actions combine multiple steps into a single reusable action. They are stored in the same repository or referenced from external repositories. Composite actions are useful for common setup sequences that appear across multiple workflows.
name: "Setup Node Project"
description: "Setup Node.js with caching and dependencies"
inputs:
node-version:
description: "Node.js version"
required: false
default: "20"
runs:
using: "composite"
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: npm
- run: npm ci
shell: bash
- run: npm run lint
shell: bashComposite actions run as a single step in the caller workflow. They must specify shell: bash for each run step. The inputs are defined in the action.yml and passed from the calling workflow.
Permissions Model
GitHub Actions uses a permissions model that controls what the GITHUB_TOKEN can do. By default, workflows get broad permissions. Explicitly setting permissions to the minimum required is a security best practice.
name: CI
on: push
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm test
publish:
needs: build
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build
- uses: actions/setup-node@v4
with:
registry-url: https://npm.pkg.github.com
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}The top-level permissions restrict the default GITHUB_TOKEN to read-only. The publish job explicitly requests packages: write to push to the package registry. This follows the principle of least privilege.
Concurrency Controls
Concurrency groups prevent multiple workflow runs from competing for the same resources. This is critical for deployment workflows where two simultaneous deploys could cause conflicts.
name: PR Validation
on: pull_request
concurrency:
group: pr-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm test
name: Deploy
on:
push:
branches: [main]
concurrency:
group: deploy-production
cancel-in-progress: false
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- run: ./deploy.shFor PR validation, cancel-in-progress: true cancels the previous run when a new commit is pushed — only the latest commit matters. For deployments, cancel-in-progress: false queues the deployment instead of cancelling it.
cancel-in-progress: true is great for CI validation but dangerous for deployment workflows. If you cancel a deployment mid-way, you might leave the system in a partially deployed state. For deployments, use cancel-in-progress: false and let them queue.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.