Advanced GitHub Actions
What Makes GitHub Actions Advanced?
Advanced GitHub Actions techniques go beyond basic workflows to optimize performance, manage complex dependencies, handle artifacts, implement sophisticated testing strategies, and integrate with deployment environments. These patterns enable production-grade CI/CD pipelines that scale with project complexity.
Mastering these techniques reduces build times, improves reliability, and creates maintainable automation systems.
Matrix Builds
Matrix builds run jobs across multiple configurations simultaneously, testing code against different versions, operating systems, or environments.
Basic Matrix Strategy
name: Matrix Build
on: push
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [16, 18, 20]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm test
This creates 9 jobs (3 operating systems × 3 Node versions).
Matrix with Include and Exclude
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
node-version: [16, 18, 20]
include:
# Add specific configuration
- os: ubuntu-latest
node-version: 20
experimental: true
exclude:
# Skip Windows + Node 16
- os: windows-latest
node-version: 16
Dynamic Matrix from JSON
jobs:
setup:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- id: set-matrix
run: |
echo "matrix={\"include\":[{\"version\":\"16\"},{\"version\":\"18\"}]}" >> $GITHUB_OUTPUT
test:
needs: setup
strategy:
matrix: ${{ fromJSON(needs.setup.outputs.matrix) }}
runs-on: ubuntu-latest
steps:
- run: echo "Testing version ${{ matrix.version }}"
Matrix Build Visualization
graph TD
A["Matrix Strategy"] -->B["Ubuntu + Node 16"]
A -->C["Ubuntu + Node 18"]
A -->D["Ubuntu + Node 20"]
A -->E["Windows + Node 16"]
A -->F["Windows + Node 18"]
A -->G["Windows + Node 20"]
A -->H["macOS + Node 16"]
A -->I["macOS + Node 18"]
A -->J["macOS + Node 20"]
style A fill:#e1f5ff
B -->K["Results"]
C -->K
D -->K
E -->K
F -->K
G -->K
H -->K
I -->K
J -->K
Caching Dependencies
Caching speeds up workflows by reusing dependencies from previous runs.
NPM Caching
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- run: npm ci
Custom Cache Paths
steps:
- uses: actions/cache@v4
with:
path: |
~/.npm
~/.cache
node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
Pip Caching (Python)
steps:
- uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: 'pip'
- run: pip install -r requirements.txt
Maven/Gradle Caching
steps:
- uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: 'maven'
- run: mvn install
Artifacts
Artifacts preserve files between jobs and store build outputs.
Upload Artifacts
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm run build
- uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/
retention-days: 7
Download Artifacts in Another Job
jobs:
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
name: build-output
path: ./dist
- run: ls -la ./dist
Multiple Artifacts
steps:
- uses: actions/upload-artifact@v4
with:
name: test-results
path: |
test-results/**
coverage/**
Artifact Flow
sequenceDiagram
participant Build as Build Job
participant Storage as GitHub Storage
participant Deploy as Deploy Job
Build->>Build: Compile & Build
Build->>Storage: Upload artifact
Storage->>Deploy: Download artifact
Deploy->>Deploy: Deploy to server
Job Dependencies
Control job execution order with dependencies.
Sequential Jobs
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: npm run build
test:
needs: build
runs-on: ubuntu-latest
steps:
- run: npm test
deploy:
needs: [build, test]
runs-on: ubuntu-latest
steps:
- run: echo "Deploying..."
Conditional Dependencies
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: npm test
deploy:
needs: test
if: success() && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- run: echo "Deploying to production"
Job Dependency Graph
graph TD
A["Lint"] -->D["Deploy"]
B["Test"] -->D
C["Build"] -->D
D -->E["Smoke Test"]
style A fill:#e1f5ff
style B fill:#e1f5ff
style C fill:#e1f5ff
style D fill:#fff3e0
style E fill:#e8f5e9
Environments
Environments provide deployment protection rules, secrets, and approval gates.
Defining Environments
jobs:
deploy-staging:
runs-on: ubuntu-latest
environment:
name: staging
url: https://staging.example.com
steps:
- run: echo "Deploying to staging"
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
environment:
name: production
url: https://example.com
steps:
- run: echo "Deploying to production"
Environment Protection Rules
Configure in GitHub Repository Settings > Environments:
- Required reviewers (manual approval)
- Wait timer (deployment delay)
- Deployment branches (limit to specific branches)
jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: production
steps:
- run: |
echo "This requires approval in production environment"
echo "Deployment URL: https://example.com"
Concurrency Control
Prevent multiple workflow runs from executing simultaneously.
Job-Level Concurrency
jobs:
deploy:
runs-on: ubuntu-latest
concurrency:
group: production-deployment
cancel-in-progress: false
steps:
- run: echo "Only one deployment at a time"
Workflow-Level Concurrency
name: Deploy
on:
push:
branches:
- main
concurrency:
group: production-${{ github.ref }}
cancel-in-progress: true
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- run: echo "Deploying..."
Composite Actions
Create custom actions combining multiple steps.
Composite Action Definition
# .github/actions/setup-node-app/action.yml
name: 'Setup Node.js Application'
description: 'Install Node.js and dependencies'
inputs:
node-version:
description: 'Node.js version'
required: false
default: '18'
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 build
shell: bash
Using Composite Action
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-node-app
with:
node-version: '20'
Docker Container Actions
Run actions inside Docker containers for isolated environments.
Container Action
# .github/actions/custom-action/action.yml
name: 'Custom Docker Action'
description: 'Runs in Docker container'
runs:
using: 'docker'
image: 'Dockerfile'
# Dockerfile
FROM python:3.11-slim
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
Using Container in Job
jobs:
build:
runs-on: ubuntu-latest
container:
image: node:18
env:
NODE_ENV: production
steps:
- uses: actions/checkout@v4
- run: npm install
- run: npm test
Service Containers
Run databases or services alongside jobs.
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: testdb
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
redis:
image: redis:7
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4
- run: npm install
- run: npm test
env:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/testdb
REDIS_URL: redis://localhost:6379
Service Container Architecture
graph TD
A["GitHub Runner"] -->B["Job Container"]
A -->C["Service: PostgreSQL"]
A -->D["Service: Redis"]
A -->E["Service: MongoDB"]
B -->|connects| C
B -->|connects| D
B -->|connects| E
style A fill:#e1f5ff
style B fill:#fff3e0
style C fill:#e8f5e9
style D fill:#e8f5e9
style E fill:#e8f5e9
Output Parameters
Pass data between steps and jobs.
Step Outputs
jobs:
build:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.get-version.outputs.version }}
commit: ${{ steps.get-commit.outputs.sha }}
steps:
- uses: actions/checkout@v4
- id: get-version
run: |
VERSION=$(cat package.json | jq -r .version)
echo "version=$VERSION" >> $GITHUB_OUTPUT
- id: get-commit
run: |
echo "sha=${{ github.sha }}" >> $GITHUB_OUTPUT
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- run: |
echo "Deploying version ${{ needs.build.outputs.version }}"
echo "Commit: ${{ needs.build.outputs.commit }}"
Strategy Options
Control matrix behavior and failure handling.
strategy:
matrix:
node-version: [16, 18, 20]
# Don't cancel other jobs if one fails
fail-fast: false
# Run maximum 3 jobs in parallel
max-parallel: 3
Timeout and Retry
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Install dependencies
timeout-minutes: 5
run: npm ci
- name: Retry flaky test
uses: nick-invision/retry@v2
with:
timeout_minutes: 10
max_attempts: 3
command: npm run test:integration
Real-World Advanced Pipeline
name: Advanced CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
node-version: [18, 20]
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: postgres
options: --health-cmd pg_isready --health-interval 10s
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm test
env:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/test
- uses: actions/upload-artifact@v4
if: failure()
with:
name: test-results-${{ matrix.os }}-${{ matrix.node-version }}
path: test-results/
build:
needs: test
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run build
- id: version
run: echo "version=$(cat package.json | jq -r .version)" >> $GITHUB_OUTPUT
- uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/
deploy-staging:
needs: build
if: github.ref == 'refs/heads/develop'
runs-on: ubuntu-latest
environment:
name: staging
url: https://staging.example.com
steps:
- uses: actions/download-artifact@v4
with:
name: build-output
- run: echo "Deploy version ${{ needs.build.outputs.version }} to staging"
deploy-production:
needs: build
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment:
name: production
url: https://example.com
steps:
- uses: actions/download-artifact@v4
with:
name: build-output
- run: echo "Deploy version ${{ needs.build.outputs.version }} to production"
Key Takeaways
- Matrix builds test code across multiple configurations simultaneously
- Caching dependencies dramatically reduces workflow execution time
- Artifacts preserve build outputs and pass files between jobs
- Job dependencies control execution order with
needs:keyword - Environments provide deployment gates, approvals, and environment-specific secrets
- Concurrency control prevents conflicting simultaneous deployments
- Composite actions encapsulate reusable multi-step logic
- Service containers run databases and services for integration testing
- Output parameters pass data between steps and jobs
- Strategy options control matrix behavior, parallelism, and failure handling
- Timeouts prevent workflows from running indefinitely
Next Steps: Implement matrix builds for cross-platform testing, add caching to reduce build times, and configure deployment environments with approval gates.