Skip to content
5 min read·Lesson 4 of 10

Reusable Workflows and Composite Actions

Stop copy-pasting pipeline YAML. Learn the three ways to share CI logic across repos: composite actions, reusable workflows, and JavaScript/Docker actions.

Once you have more than a couple of repos, the same 30 lines of YAML start showing up in each ci.yml. GitHub Actions provides three reusability primitives.

1. Composite Actions

A composite action bundles several steps into a single uses: step. Perfect for things like "set up Node, install deps, cache".

File layout:

my-org/setup-app/
├── action.yml
└── README.md
# action.yml
name: Setup App
description: Checkout, install Node, install deps with cache
inputs:
  node-version:
    description: Node.js version
    required: false
    default: '20'
runs:
  using: composite
  steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-node@v4
      with:
        node-version: ${{ inputs.node-version }}
        cache: 'npm'
    - run: npm ci
      shell: bash

Use it from any workflow:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: my-org/setup-app@v1
        with:
          node-version: '22'
      - run: npm test

Trade-offs:

  • ✅ Simple, lightweight, no separate runner
  • ✅ Can be in the same repo (uses: ./.github/actions/setup-app)
  • ❌ Can't have its own jobs — only steps
  • ❌ Secrets aren't passed automatically; you must forward them as inputs

2. Reusable Workflows

A reusable workflow is a complete workflow that can be called by other workflows. It runs in its own job and can have its own concurrency, environments, and matrix.

# .github/workflows/build-and-test.yml in shared repo
name: Build & Test (reusable)

on:
  workflow_call:
    inputs:
      node-version:
        type: string
        default: '20'
    secrets:
      NPM_TOKEN:
        required: true

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
      - run: npm ci
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
      - run: npm test

Call it from another workflow:

jobs:
  ci:
    uses: my-org/shared-workflows/.github/workflows/build-and-test.yml@v1
    with:
      node-version: '20'
    secrets:
      NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

Trade-offs:

  • ✅ Encapsulates whole jobs, including matrix and parallelism
  • ✅ Single source of truth for organisational pipelines
  • ✅ Can require approvals and use environments
  • ❌ Slightly heavier; runs as a child workflow with its own setup

3. JavaScript / Docker Actions

For logic that's awkward in YAML — parsing JSON, calling APIs, complex string handling — write the action in code.

my-org/release-notes/
├── action.yml
├── package.json
├── src/
│   └── index.js
# action.yml
name: Generate Release Notes
inputs:
  from-tag:
    required: true
runs:
  using: 'node20'
  main: 'dist/index.js'
// src/index.js
import * as core from '@actions/core';
import * as github from '@actions/github';

const fromTag = core.getInput('from-tag');
const token = process.env.GITHUB_TOKEN;
const octokit = github.getOctokit(token);

const { data } = await octokit.rest.repos.compareCommits({
  owner: github.context.repo.owner,
  repo: github.context.repo.repo,
  base: fromTag,
  head: 'HEAD',
});

core.setOutput('notes', data.commits.map(c => '- ' + c.commit.message).join('\n'));

Docker actions work similarly but run in a container — useful for non-Node tooling. They're slower (image pull) but trivially portable.

Choosing

NeedBest fit
"Run these 5 setup steps everywhere"Composite action
"Whole CI pipeline shared across repos"Reusable workflow
"Call an API and produce structured output"JavaScript action
"Run a shell tool that needs a specific environment"Docker action

Versioning Shared Actions

Treat shared actions as products: tag releases, write release notes, and publish.

  • Major tags (@v1) auto-update — convenient but trust-based
  • Pinned SHAs (@a1b2c3d) — immutable, secure, reproducible — preferred for production critical workflows
  • Use Dependabot to keep SHAs current safely

Centralised Pipelines: An Org Pattern

Many large orgs run a single shared-workflows repo:

shared-workflows/
└── .github/
    └── workflows/
        ├── nodejs-build-test.yml
        ├── docker-build-push.yml
        ├── terraform-plan-apply.yml
        └── compliance-scan.yml

Application repos call into them with three lines of YAML. Adding a new security scan? Update one file. Hundreds of repos pick it up automatically.

Permissions and Secrets

  • Reusable workflows must be in a public repo or in the same org with permission granted
  • Inputs are public; sensitive data goes through secrets:
  • Use secrets: inherit to forward all caller secrets — convenient but be deliberate
  • Restrict who can edit shared workflow repos with branch protection and CODEOWNERS

Anti-Patterns

  • Copy-pasting the same 50 lines of YAML across 30 repos. Make it a reusable workflow.
  • Reusable workflow that takes 20 inputs — usually a sign you should split it
  • Hardcoded secrets in actions — always pass via secrets:
  • One mega-workflow that does everything — split by concern; small composable pieces beat monoliths in CI too

Reusability is what turns CI from a per-repo chore into a platform.

Key Takeaways

  • Composite actions package multiple steps as a single uses: directive.
  • Reusable workflows let you call a whole workflow from another with workflow_call.
  • JavaScript and Docker actions are full programs — useful when YAML steps are not enough.
  • Centralise pipeline logic in a shared org-level repo for consistency and security.
  • Pin shared actions/workflows to a SHA in production-critical pipelines.

Test your knowledge

Try exam-style practice questions to reinforce what you've learned.

Practice Questions →