GitHub Actions Advanced Workflows: Matrix Builds, Reusable Workflows, OIDC Auth, and Caching

Published 16 min read
github-actionsci-cdmatrix-buildsoidccachingreusable-workflows

GitHub Actions handles 70% of CI/CD workloads for open-source projects and an increasing share of enterprise pipelines. Moving beyond basic build-test-deploy workflows requires matrix strategies for multi-platform testing, reusable workflows for DRY pipelines, OIDC for secure cloud authentication, and caching to cut build times by 60-80%. This guide covers each pattern with production-tested configurations.

---

Matrix Builds

Matrix strategies run the same job across multiple configurations simultaneously. This is essential for testing across Node.js versions, operating systems, or database versions.

<h1>.github/workflows/test.yml</h1>

name: Test Matrix

on: [push, pull_request]

jobs:

test:

runs-on: ${{ matrix.os }}

strategy:

fail-fast: false # Continue other jobs if one fails

matrix:

os: [ubuntu-latest, windows-latest, macos-latest]

node: [18, 20, 22]

exclude:

- os: windows-latest

node: 18 # Skip older Node on Windows

include:

- os: ubuntu-latest

node: 22

coverage: true # Add coverage only for one combo

steps:

- uses: actions/checkout@v4

- uses: actions/setup-node@v4

with:

node-version: ${{ matrix.node }}

cache: npm

- run: npm ci

- run: npm test

- if: matrix.coverage

run: npm run coverage

Matrix FeaturePurposeExample

excludeSkip specific combinationsSkip Node 18 on Windows
includeAdd extra propertiesAdd coverage flag to one combo
fail-fast: falseRun all even if one failsUseful for compatibility testing
max-parallelLimit concurrent jobsReduce runner costs

---

Reusable Workflows

Reusable workflows define a workflow once and call it from multiple repositories or workflows. This eliminates duplication across projects.

<h1>.github/workflows/reusable-deploy.yml (in shared repo)</h1>

name: Reusable Deploy

on:

workflow_call:

inputs:

environment:

required: true

type: string

image_tag:

required: true

type: string

secrets:

DEPLOY_TOKEN:

required: true

jobs:

deploy:

runs-on: ubuntu-latest

environment: ${{ inputs.environment }}

steps:

- uses: actions/checkout@v4

- name: Deploy to ${{ inputs.environment }}

run: |

echo "Deploying ${{ inputs.image_tag }} to ${{ inputs.environment }}"

kubectl set image deployment/app \

app=${{ inputs.image_tag }}

env:

DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}

<h1>.github/workflows/production.yml (in consuming repo)</h1>

name: Production Deploy

on:

push:

branches: [main]

jobs:

build:

runs-on: ubuntu-latest

outputs:

image_tag: ${{ steps.build.outputs.tag }}

steps:

- uses: actions/checkout@v4

- id: build

run: echo "tag=myapp:${{ github.sha }}" >> $GITHUB_OUTPUT

deploy-staging:

needs: build

uses: org/shared-workflows/.github/workflows/reusable-deploy.yml@main

with:

environment: staging

image_tag: ${{ needs.build.outputs.image_tag }}

secrets:

DEPLOY_TOKEN: ${{ secrets.STAGING_DEPLOY_TOKEN }}

deploy-production:

needs: [build, deploy-staging]

uses: org/shared-workflows/.github/workflows/reusable-deploy.yml@main

with:

environment: production

image_tag: ${{ needs.build.outputs.image_tag }}

secrets:

DEPLOY_TOKEN: ${{ secrets.PROD_DEPLOY_TOKEN }}

---

Composite Actions

Composite actions bundle multiple steps into a single reusable action. Unlike reusable workflows, composite actions run within the calling job.

<h1>.github/actions/setup-project/action.yml</h1>

name: Setup Project

description: Install dependencies and build

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 build

shell: bash

---

OIDC Authentication

OIDC eliminates long-lived credentials for cloud provider access. GitHub Actions generates a short-lived JWT token that your cloud provider exchanges for temporary credentials.

<h1>OIDC with AWS</h1>

jobs:

deploy:

runs-on: ubuntu-latest

permissions:

id-token: write

contents: read

steps:

- uses: aws-actions/configure-aws-credentials@v4

with:

role-to-assume: arn:aws:iam::123456789012:role/github-actions

aws-region: us-east-1

- run: aws s3 ls # Uses temporary credentials

Auth MethodSecuritySetup ComplexityCredential Rotation

Repository secretsMediumLowManual
Environment secretsHigherMediumManual
OIDCHighestMediumAutomatic (per-run)
Self-hosted runner IAMHighestHighAutomatic

---

Dependency Caching

<h1>Advanced caching strategies</h1>

steps:

- uses: actions/cache@v4

id: npm-cache

with:

path: |

~/.npm

node_modules

key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}

restore-keys: |

${{ runner.os }}-node-

- if: steps.npm-cache.outputs.cache-hit != 'true'

run: npm ci

# Docker layer caching

- uses: docker/build-push-action@v5

with:

context: .

push: true

tags: myapp:latest

cache-from: type=gha

cache-to: type=gha,mode=max

Cache StrategyTime SavedCache SizeBest For

npm/yarn cache30-60s50-200 MBNode.js projects
pip cache20-40s20-100 MBPython projects
Docker layer cache2-10 min500 MB-5 GBContainer builds
Gradle/Maven cache1-5 min100-500 MBJava projects
Turbo/nx cache1-10 min50-500 MBMonorepos

---

Self-Hosted Runners

<h1>Set up self-hosted runner on VPS</h1>

mkdir actions-runner && cd actions-runner

curl -o actions-runner-linux-x64.tar.gz -L \

https://github.com/actions/runner/releases/latest/download/actions-runner-linux-x64-2.321.0.tar.gz

tar xzf actions-runner-linux-x64.tar.gz

<h1>Configure with repository token</h1>

./config.sh --url https://github.com/org/repo \

--token YOUR_TOKEN \

--labels self-hosted,linux,x64,gpu

<h1>Install as service</h1>

sudo ./svc.sh install

sudo ./svc.sh start

For CI/CD pipeline fundamentals, see our CI/CD pipeline guide for startups. Teams implementing feature flags alongside CI/CD should read our feature flags deployment strategy. For infrastructure testing within your CI pipeline, check our infrastructure testing strategies guide.

---

Frequently Asked Questions

How do I reduce GitHub Actions costs?

Three strategies: (1) Use caching aggressively to reduce build times. (2) Use self-hosted runners for heavy workloads (Docker builds, GPU tasks). (3) Cancel redundant workflow runs with concurrency groups. The concurrency key cancels in-progress runs when a new commit is pushed to the same branch, saving minutes on superseded builds.

When should I use reusable workflows vs composite actions?

Use reusable workflows when you need a complete job with its own runner, environment protections, and secret access. Use composite actions when you want to bundle steps that run within an existing job. Reusable workflows are better for deployment pipelines. Composite actions are better for setup and build steps.

How secure is OIDC compared to stored secrets?

OIDC is significantly more secure. Stored secrets can be exfiltrated by anyone with write access to the repository (via a malicious workflow). OIDC tokens are scoped to a specific repository, branch, and environment, expire after the job completes, and cannot be reused. The cloud provider IAM role can restrict access based on the repository, branch, and workflow.

Can I run GitHub Actions locally for testing?

Use the act tool (github.com/nektos/act) to run workflows locally in Docker containers. It supports most workflow features but has limitations with services, OIDC, and some GitHub context variables. For simple build-test workflows, act provides 90% accuracy compared to running on GitHub.

What is the maximum workflow run time?

Each job can run for up to 6 hours on GitHub-hosted runners. Each workflow run can last up to 35 days (including wait times for approvals). Self-hosted runners have no time limit. If your builds take longer than 6 hours, split them into multiple jobs or use self-hosted runners.