GitHub Actions Workflow Basics
GitHub Actions Workflow Basics
Workflow Structure
# .github/workflows/ci.yml
name: CI Pipeline
# Trigger on events
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
# Environment variables
env:
NODE_VERSION: '20'
REGISTRY: ghcr.io
# Jobs run in parallel by default
jobs:
build:
runs-on: ubuntu-latest
steps:
# Checkout code
- uses: actions/checkout@v4
# Setup Node.js
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
# Install dependencies
- run: npm ci
# Run tests
- run: npm test
# Build
- run: npm run build
# Upload artifacts
- uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/
retention-days: 7
Triggers
on:
# Push to branches
push:
branches: [main]
tags: ['v*']
paths: ['src/**', 'package.json'] # Only trigger on changes
paths-ignore: ['docs/**', '*.md'] # Ignore certain paths
# Pull requests
pull_request:
branches: [main]
types: [opened, synchronize, reopened]
# Scheduled (cron)
schedule:
- cron: '0 2 * * 1' # Every Monday at 2 AM UTC
# Manual dispatch
workflow_dispatch:
inputs:
environment:
description: 'Deploy target'
required: true
type: choice
options: [staging, production]
# Reusable from another workflow
workflow_call:
inputs:
image-tag:
required: true
type: string
Conditional Execution
jobs:
deploy-staging:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
deploy-production:
if: startsWith(github.ref, 'refs/tags/v')
needs: [deploy-staging] # Wait for staging to complete
test:
runs-on: ubuntu-latest
steps:
- name: Skip on docs-only PR
if: ${{ contains(github.event.head_commit.message, '[skip ci]') }}
run: echo "Skipping"
Matrix Builds
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20, 22]
os: [ubuntu-latest, windows-latest]
fail-fast: false # Don't cancel other jobs on failure
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm test
Actions and Reusable Workflows
Actions and Reusable Workflows
Using Marketplace Actions
steps:
# Docker build and push
- uses: docker/build-push-action@v5
with:
context: .
push: true
tags: user/app:latest
# Deploy to AWS
- uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
# Deploy to Kubernetes
- uses: azure/k8s-deploy@v4
with:
manifests: k8s/
images: user/app:${{ github.sha }}
# Notify Slack
- uses: slackapi/slack-github-action@v1
with:
payload: |
{"text": "Deploy complete: ${{ github.sha }}"}
Custom Actions
# action.yml — Define a composite action
name: 'Setup and Test'
description: 'Install deps, lint, and test'
inputs:
node-version:
description: 'Node.js version'
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: bash
- run: npm test
shell: bash
# Usage in workflow
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-and-test
with:
node-version: '22'
Reusable Workflows
# .github/workflows/reusable-deploy.yml (called by other workflows)
name: Reusable Deploy
on:
workflow_call:
inputs:
environment:
required: true
type: string
image-tag:
required: true
type: string
secrets:
deploy-key:
required: true
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- uses: actions/checkout@v4
- run: echo "Deploying ${{ inputs.image-tag }} to ${{ inputs.environment }}"
# Calling workflow
jobs:
deploy:
uses: ./.github/workflows/reusable-deploy.yml
with:
environment: staging
image-tag: ${{ github.sha }}
secrets:
deploy-key: ${{ secrets.DEPLOY_KEY }}
Artifacts and Caching
steps:
# Build and upload artifact
- run: npm run build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
retention-days: 7
# Download artifact from another job
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/
# Cache npm dependencies
- uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
restore-keys: ${{ runner.os }}-npm-
``
Secrets, Environments, and Protection
Secrets, Environments, and Protection
Managing Secrets
steps:
# Repository secrets (Settings → Secrets and variables → Actions)
- run: echo "Password is ${{ secrets.DB_PASSWORD }}"
# Environment secrets
- run: echo "Key is ${{ secrets.API_KEY }}"
environment: production
# Never log secrets!
- run: echo "This is safe: ${{ secrets.MY_SECRET }}"
- run: echo "WARNING: $MY_SECRET" # DANGEROUS — logs the value
env:
MY_SECRET: ${{ secrets.MY_SECRET }} # Use env, not inline
Environments with Protection Rules
# .github/workflows/deploy.yml
jobs:
deploy-staging:
runs-on: ubuntu-latest
environment: staging # No approval needed
steps:
- run: echo "Deploying to staging"
deploy-production:
runs-on: ubuntu-latest
environment: production # Requires approval + wait timer
steps:
- run: echo "Deploying to production"
# Set up in GitHub: Settings → Environments → production
# - Required reviewers
# - Wait timer (e.g., 15 minutes)
# - Branch restrictions
OIDC for Cloud Authentication (No Static Keys)
jobs:
deploy-aws:
runs-on: ubuntu-latest
permissions:
id-token: write # Required for OIDC
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/github-actions
aws-region: us-east-1
- run: aws s3 sync dist/ s3://my-bucket/
Complete CI/CD Pipeline Example
name: Full CI/CD Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
lint-and-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/
build:
needs: lint-and-test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
docker:
needs: build
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
deploy-staging:
needs: docker
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: staging
steps:
- uses: actions/checkout@v4
- run: kubectl set image deployment/myapp myapp=ghcr.io/${{ github.repository }}:${{ github.sha }}
env:
KUBECONFIG: ${{ secrets.KUBE_CONFIG }}
deploy-production:
needs: deploy-staging
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- run: kubectl set image deployment/myapp myapp=ghcr.io/${{ github.repository }}:${{ github.sha }}
env:
KUBECONFIG: ${{ secrets.KUBE_CONFIG }}