Skip to content
April 7, 2026·10 min read

GitHub Actions: Reusable Workflows and Composite Actions

Stop duplicating CI/CD config across repos — build reusable workflows, composite actions, and shared secrets strategies for multi-repo organizations.

Back

When you have five repos all running similar CI pipelines, you have five places to update every time you change a Node version or add a security scan. Reusable workflows and composite actions fix this.


Prerequisites

  • GitHub organization with multiple repos
  • Basic YAML and GitHub Actions knowledge
  • Organization admin or repo admin access

Reusable Workflows: Call One Workflow from Another

A reusable workflow is a workflow file that other workflows can call like a function. It lives in any repo (typically a shared platform or .github repo).

Defining a reusable workflow:

# .github/workflows/deploy-node.yml (in your platform repo)
name: Deploy Node App
 
on:
  workflow_call:
    inputs:
      environment:
        required: true
        type: string
      node-version:
        required: false
        type: string
        default: '20'
    secrets:
      DEPLOY_TOKEN:
        required: true
      AWS_ROLE_ARN:
        required: true
 
jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: ${{ inputs.environment }}
    steps:
      - uses: actions/checkout@v4
 
      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
          cache: 'npm'
 
      - run: npm ci
      - run: npm run build
      - run: npm test
 
      - 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: Deploy
        run: |
          aws s3 sync ./dist s3://my-app-${{ inputs.environment }} --delete

Calling it from another repo:

# Any repo's .github/workflows/ci.yml
name: CI/CD
 
on:
  push:
    branches: [main]
 
jobs:
  deploy-staging:
    uses: my-org/platform/.github/workflows/deploy-node.yml@main
    with:
      environment: staging
      node-version: '20'
    secrets:
      DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
      AWS_ROLE_ARN: ${{ secrets.STAGING_AWS_ROLE }}
 
  deploy-prod:
    needs: deploy-staging
    uses: my-org/platform/.github/workflows/deploy-node.yml@main
    with:
      environment: production
    secrets: inherit   # Pass all secrets from calling workflow

Composite Actions: Reusable Steps

Composite actions bundle multiple steps into a single uses: reference. Unlike reusable workflows, they run in the same job context.

# my-org/actions/setup-node-app/action.yml
name: 'Setup Node App'
description: 'Install deps, cache, and lint'
 
inputs:
  node-version:
    description: 'Node version'
    default: '20'
  run-lint:
    description: 'Run ESLint'
    default: 'true'
 
runs:
  using: composite
  steps:
    - uses: actions/setup-node@v4
      with:
        node-version: ${{ inputs.node-version }}
        cache: 'npm'
 
    - name: Install dependencies
      run: npm ci
      shell: bash
 
    - name: Lint
      if: inputs.run-lint == 'true'
      run: npm run lint
      shell: bash
# Any workflow using the composite action
steps:
  - uses: actions/checkout@v4
  - uses: my-org/actions/setup-node-app@v1
    with:
      node-version: '22'
      run-lint: 'false'
  - run: npm test

Sharing Secrets Across Repos

Option 1: Organization secrets (simplest)

GitHub Org → Settings → Secrets and variables → Actions → New organization secret
Access: Select repositories → pick which repos can use it

Option 2: OIDC with cloud providers (no long-lived secrets)

permissions:
  id-token: write
  contents: read
 
steps:
  - name: Configure AWS credentials
    uses: aws-actions/configure-aws-credentials@v4
    with:
      role-to-assume: arn:aws:iam::123456789:role/github-actions-role
      aws-region: eu-west-1

OIDC is the gold standard: no secret rotation, no credential exposure — GitHub exchanges a signed JWT for a short-lived cloud token at runtime.


Matrix Strategy for Multi-Environment Testing

jobs:
  test:
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest]
        node: [18, 20, 22]
        exclude:
          - os: windows-latest
            node: 18  # Skip Node 18 on Windows
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
      - run: npm test

Caching for Speed

- name: Cache node_modules
  uses: actions/cache@v4
  id: cache-npm
  with:
    path: node_modules
    key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
    restore-keys: |
      npm-${{ runner.os }}-
 
- name: Install (only if cache miss)
  if: steps.cache-npm.outputs.cache-hit != 'true'
  run: npm ci

Key insight: hash the lockfile, not package.json. The lockfile changes when exact versions change; package.json changes when ranges change (which doesn't always mean new installs).


Common Pitfalls

  • No needs: on production job: without needs: [staging], both staging and production deploy simultaneously on every push
  • Long-lived secrets in environment variables: use OIDC — secrets that never expire are a security liability
  • Reusable workflow in the same repo only: reusable workflows called with uses: must be in a public repo or the same org; use Actions for cross-org
  • Missing shell: bash in composite actions: each step in a composite action needs an explicit shell: declaration

Resources

Aïcha Imène DAHOUMANE
About the author
Aïcha Imène DAHOUMANE
Salesforce Administrator & Developer · Consultant

Salesforce developer with a background in IT infrastructure — building reliable, maintainable solutions with Apex, Flows, LWC, and CI/CD.

ShareShare on LinkedIn