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:

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

Next Steps: Implement matrix builds for cross-platform testing, add caching to reduce build times, and configure deployment environments with approval gates.