GitHub Actions: Building Modern CI/CD Pipelines
Creating efficient CI/CD workflows with GitHub Actions. Reusable workflows, matrix builds, security scanning, and deployment strategies.
GitHub Actions: Building Modern CI/CD Pipelines
GitHub Actions provides powerful CI/CD capabilities directly integrated with your repository. Understanding workflow syntax, reusable components, and best practices enables building efficient, secure automation pipelines.
Workflow Fundamentals
Workflow Structure
# .github/workflows/ci.yml
name: CI Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
workflow_dispatch:
inputs:
environment:
description: 'Environment to deploy'
required: true
default: 'staging'
type: choice
options:
- staging
- production
env:
NODE_VERSION: '20'
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: build-artifacts
path: dist/
retention-days: 7Trigger Events
| Event | Description | Use Case |
|---|---|---|
| push | Branch commits | CI on feature branches |
| pull_request | PR events | PR validation |
| workflow_dispatch | Manual trigger | On-demand deployments |
| schedule | Cron schedule | Nightly builds |
| release | Release events | Production deployments |
Matrix Builds
Testing Across Versions
jobs:
test:
name: Test (${{ matrix.os }}, Node ${{ matrix.node }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node: [18, 20, 22]
exclude:
- os: windows-latest
node: 18
include:
- os: ubuntu-latest
node: 20
coverage: true
steps:
- uses: actions/checkout@v4
- name: Setup Node.js ${{ matrix.node }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Upload coverage
if: matrix.coverage
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage/lcov.infoReusable Workflows
Defining a Reusable Workflow
# .github/workflows/reusable-build.yml
name: Reusable Build Workflow
on:
workflow_call:
inputs:
node-version:
description: 'Node.js version'
required: false
type: string
default: '20'
environment:
description: 'Target environment'
required: true
type: string
secrets:
npm-token:
description: 'NPM authentication token'
required: false
outputs:
artifact-name:
description: 'Name of uploaded artifact'
value: ${{ jobs.build.outputs.artifact-name }}
jobs:
build:
name: Build
runs-on: ubuntu-latest
outputs:
artifact-name: ${{ steps.artifact.outputs.name }}
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: 'npm'
registry-url: 'https://npm.pkg.github.com'
- name: Install dependencies
run: npm ci
env:
NODE_AUTH_TOKEN: ${{ secrets.npm-token }}
- name: Build
run: npm run build
env:
ENVIRONMENT: ${{ inputs.environment }}
- name: Set artifact name
id: artifact
run: echo "name=build-${{ inputs.environment }}-${{ github.sha }}" >> $GITHUB_OUTPUT
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ steps.artifact.outputs.name }}
path: dist/Calling a Reusable Workflow
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
build:
uses: ./.github/workflows/reusable-build.yml
with:
node-version: '20'
environment: production
secrets:
npm-token: ${{ secrets.NPM_TOKEN }}
deploy:
needs: build
runs-on: ubuntu-latest
environment: production
steps:
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: ${{ needs.build.outputs.artifact-name }}
path: dist/
- name: Deploy to production
run: |
echo "Deploying ${{ needs.build.outputs.artifact-name }}"Composite Actions
Creating a Composite Action
# .github/actions/setup-project/action.yml
name: Setup Project
description: Setup Node.js project with dependencies
inputs:
node-version:
description: 'Node.js version'
required: false
default: '20'
install-command:
description: 'Install command'
required: false
default: 'npm ci'
cache-key-prefix:
description: 'Cache key prefix'
required: false
default: 'deps'
outputs:
cache-hit:
description: 'Whether cache was hit'
value: ${{ steps.cache.outputs.cache-hit }}
runs:
using: composite
steps:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- name: Get npm cache directory
id: npm-cache-dir
shell: bash
run: echo "dir=$(npm config get cache)" >> $GITHUB_OUTPUT
- name: Cache dependencies
id: cache
uses: actions/cache@v4
with:
path: |
${{ steps.npm-cache-dir.outputs.dir }}
node_modules
key: ${{ inputs.cache-key-prefix }}-${{ runner.os }}-node${{ inputs.node-version }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ inputs.cache-key-prefix }}-${{ runner.os }}-node${{ inputs.node-version }}-
- name: Install dependencies
if: steps.cache.outputs.cache-hit != 'true'
shell: bash
run: ${{ inputs.install-command }}Using Composite Actions
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup project
uses: ./.github/actions/setup-project
with:
node-version: '20'
- name: Build
run: npm run buildSecurity Scanning
Complete Security Pipeline
name: Security
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: '0 0 * * 1' # Weekly on Monday
jobs:
secrets-scan:
name: Secret Scanning
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Gitleaks scan
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
dependency-scan:
name: Dependency Scanning
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: NPM Audit
run: npm audit --audit-level=high
- name: Snyk scan
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --severity-threshold=high
sast-scan:
name: SAST Scanning
runs-on: ubuntu-latest
permissions:
security-events: write
steps:
- uses: actions/checkout@v4
- name: Semgrep scan
uses: semgrep/semgrep-action@v1
with:
config: >-
p/security-audit
p/secrets
p/owasp-top-ten
p/typescript
- name: CodeQL analysis
uses: github/codeql-action/init@v3
with:
languages: javascript,typescript
- name: Perform CodeQL analysis
uses: github/codeql-action/analyze@v3
container-scan:
name: Container Scanning
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t app:${{ github.sha }} .
- name: Trivy scan
uses: aquasecurity/trivy-action@master
with:
image-ref: app:${{ github.sha }}
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
- name: Upload results
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: 'trivy-results.sarif'Deployment Strategies
Blue-Green Deployment
name: Blue-Green Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: eu-west-1
- name: Get current target group
id: current
run: |
CURRENT=$(aws elbv2 describe-listeners \
--listener-arn ${{ vars.LISTENER_ARN }} \
--query 'Listeners[0].DefaultActions[0].TargetGroupArn' \
--output text)
echo "target-group=$CURRENT" >> $GITHUB_OUTPUT
- name: Determine new target group
id: new
run: |
if [[ "${{ steps.current.outputs.target-group }}" == "${{ vars.BLUE_TG_ARN }}" ]]; then
echo "target-group=${{ vars.GREEN_TG_ARN }}" >> $GITHUB_OUTPUT
echo "color=green" >> $GITHUB_OUTPUT
else
echo "target-group=${{ vars.BLUE_TG_ARN }}" >> $GITHUB_OUTPUT
echo "color=blue" >> $GITHUB_OUTPUT
fi
- name: Deploy to ${{ steps.new.outputs.color }}
run: |
# Deploy to new target group
aws ecs update-service \
--cluster ${{ vars.ECS_CLUSTER }} \
--service ${{ vars.ECS_SERVICE }}-${{ steps.new.outputs.color }} \
--force-new-deployment
- name: Wait for deployment
run: |
aws ecs wait services-stable \
--cluster ${{ vars.ECS_CLUSTER }} \
--services ${{ vars.ECS_SERVICE }}-${{ steps.new.outputs.color }}
- name: Health check
run: |
# Run health checks against new deployment
./scripts/health-check.sh ${{ steps.new.outputs.color }}
- name: Switch traffic
run: |
aws elbv2 modify-listener \
--listener-arn ${{ vars.LISTENER_ARN }} \
--default-actions Type=forward,TargetGroupArn=${{ steps.new.outputs.target-group }}
- name: Notify on failure
if: failure()
uses: slackapi/slack-github-action@v1
with:
payload: |
{
"text": "Deployment failed for ${{ github.repository }}",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":x: Deployment failed\n*Repository:* ${{ github.repository }}\n*Branch:* ${{ github.ref_name }}\n*Workflow:* ${{ github.workflow }}"
}
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}OIDC Authentication
AWS OIDC Integration
name: Deploy with OIDC
on:
push:
branches: [main]
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/GitHubActionsRole
aws-region: eu-west-1
role-session-name: GitHubActions-${{ github.run_id }}
- name: Deploy
run: |
aws s3 sync ./dist s3://my-bucket/Key Takeaways
-
Reusable workflows: Extract common patterns into reusable workflows
-
Composite actions: Package related steps into composite actions
-
Matrix builds: Test across multiple configurations efficiently
-
OIDC authentication: Use OIDC instead of long-lived credentials
-
Security scanning: Integrate SAST, SCA, and secret scanning
-
Caching: Cache dependencies and build outputs to speed up workflows
-
Environments: Use environments for deployment protection rules
-
Artifacts: Share data between jobs with artifacts
GitHub Actions provides flexible, powerful CI/CD capabilities. Invest in reusable components and security scanning for maintainable, secure pipelines.