Opsio - Cloud and AI Solutions
DevOpsCI/CD7 min read· 1,315 words

Reusable Workflows and Composite Actions: Patterns for Enterprise GitHub Actions at Scale

Published: ·Updated: ·Reviewed by Opsio Engineering Team
Oscar Bergenbrink

CTO

Technology leadership, cloud architecture, and digital transformation strategy

Reusable Workflows and Composite Actions: Patterns for Enterprise GitHub Actions at Scale

By the time an engineering organisation has 50+ repositories on GitHub Actions, copy-pasted workflow YAML becomes the dominant maintenance burden. A change to the build standard (a new lint rule, a tightened image-scan threshold, a corrected secret name) means opening 50 pull requests instead of one. Reusable workflows and composite actions are GitHub's two answers to this problem. They look superficially similar — both let you encapsulate steps and call them from multiple places — but they have different runtime semantics and different failure modes. Pick the wrong one and you end up with a re-use system that re-introduces the duplication you tried to remove.

This article documents the patterns we deploy in customer engagements with 100-2000 repositories on GitHub Actions. It assumes you have already read the basics on the github actions implementation services and you are at the stage of designing org-wide standards rather than per-repo workflows.

Reusable Workflow vs. Composite Action: The Decision Rule

The two mechanisms differ in three concrete ways that determine which one fits a given use case.

PropertyReusable workflowComposite action
Invoked asA whole job: uses: org/repo/.github/workflows/x.yml@v1A single step: uses: org/repo/path@v1
RunnerIts own runner — separate machine, separate filesystemThe caller's runner — shares filesystem, env, working directory
PermissionsInherits or restricts via permissions: block; can declare own permissionsInherits caller's permissions exactly; cannot raise or restrict
SecretsExplicit pass-through: secrets: inherit or named secretsInherited from caller's environment
InputsStrongly typed via on.workflow_call.inputsStrongly typed via inputs in action.yml
OutputsFrom any job in the called workflowFrom any step in the action
Nesting limit4 levels of reusable workflow callsComposite actions can nest other composite actions
Concurrency controlCan declare its own concurrency: blockInherits caller's concurrency

The decision rule we apply: if the unit of re-use is a job (different runner, different permissions, different concurrency), use a reusable workflow. If the unit of re-use is a sequence of steps inside an existing job (same runner, same filesystem, same permissions), use a composite action. Mixing them up — defining a reusable workflow for what should be a composite action — is the most common architectural mistake at scale.

Composite Action: A Real Example

Composite actions shine for "bundle of related steps." Here is a real composite action we deploy that runs Trivy scans, parses results, and posts a PR comment with vulnerability counts:

# .github/actions/security-scan/action.yml
name: 'Container security scan'
description: 'Trivy scan with PR comment and severity gate'
inputs:
  image:
    description: 'Container image to scan'
    required: true
  severity-fail:
    description: 'Severities that fail the build'
    default: 'CRITICAL,HIGH'
outputs:
  critical-count:
    description: 'Number of CRITICAL vulnerabilities'
    value: ${{ steps.parse.outputs.critical }}
runs:
  using: composite
  steps:
    - shell: bash
      run: |
        trivy image --format json --output trivy.json ${{ inputs.image }}
    - id: parse
      shell: bash
      run: |
        CRITICAL=$(jq '[.Results[].Vulnerabilities[]? | select(.Severity=="CRITICAL")] | length' trivy.json)
        echo "critical=$CRITICAL" >> $GITHUB_OUTPUT
    - shell: bash
      run: |
        trivy image --severity ${{ inputs.severity-fail }} --exit-code 1 ${{ inputs.image }}

This action is called from a single step in a build job. It shares the runner's filesystem, so the trivy.json file written by step 1 is read by step 2. That filesystem-sharing is the whole reason it is a composite action and not a reusable workflow.

Free Expert Consultation

Need expert help with reusable workflows and composite actions?

Our cloud architects can help you with reusable workflows and composite actions — from strategy to implementation. Book a free 30-minute advisory call with no obligation.

Solution ArchitectAI ExpertSecurity SpecialistDevOps Engineer
50+ certified engineersAWS Advanced Partner24/7 support
Completely free — no obligationResponse within 24h

Reusable Workflow: A Real Example

Reusable workflows shine for "an entire job we want every repo to run." Here is a reusable workflow we deploy as the standard release job:

# .github/workflows/release.yml in opsio/.github (org-default repo)
name: standard-release
on:
  workflow_call:
    inputs:
      service-name:
        type: string
        required: true
      registry:
        type: string
        default: ghcr.io
    secrets:
      DEPLOY_TOKEN:
        required: true
    outputs:
      image-tag:
        value: ${{ jobs.build.outputs.tag }}

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
      id-token: write
    outputs:
      tag: ${{ steps.tag.outputs.value }}
    steps:
      - uses: actions/checkout@v4
      - id: tag
        run: echo "value=v$(date +%Y.%m.%d)-${GITHUB_SHA::7}" >> $GITHUB_OUTPUT
      - uses: docker/login-action@v3
        with:
          registry: ${{ inputs.registry }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - run: docker build -t ${{ inputs.registry }}/${{ inputs.service-name }}:${{ steps.tag.outputs.value }} .
      - run: docker push ${{ inputs.registry }}/${{ inputs.service-name }}:${{ steps.tag.outputs.value }}

Calling repos invoke this workflow with a one-line job:

jobs:
  release:
    uses: opsio/.github/.github/workflows/release.yml@v1
    with:
      service-name: api
    secrets:
      DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}

The reusable workflow runs as a separate job on a separate runner. Its permissions are scoped explicitly. Secrets are passed in by name (not secrets: inherit, which is the convenience option that defeats the security boundary). Outputs flow back to the calling workflow, which can chain them into downstream jobs.

Versioning: SHA Pinning, Tags, or Branches?

Three options exist for versioning a reusable workflow or composite action. They have meaningfully different stability and supply-chain trade-offs:

  • Branch reference (e.g., @main) — every call gets the latest main. Easy to roll out changes; impossible to roll back individual repos; supply-chain risk if the producing repo's main is compromised
  • Tag reference (e.g., @v1) — calls get the latest commit pointed at by the v1 tag. Tags are mutable on GitHub; a compromised producer can repoint a tag to a malicious commit
  • SHA reference (e.g., @a81bbbf6...) — pins to an immutable commit. Strongest supply-chain stance; harder to keep up to date without Dependabot

The right answer for cross-org reuse is SHA pinning with Dependabot configured for github-actions. The right answer for in-org reuse where you control the producing repo is a major-version tag (@v1) with a strict release process: any change to v1 goes through PR review on the producing repo. Branch references should be used only for in-development workflows.

The Org-Default Pattern: opsio/.github Repository

GitHub respects a special repository per organisation called .github. Workflow files placed at OWNER/.github/.github/workflows/ are addressable from any repo in the org. This is the canonical home for org-wide reusable workflows. We typically structure it as:

opsio/.github/
├── .github/
│   ├── workflows/
│   │   ├── release.yml         # standard release pipeline
│   │   ├── security-gate.yml   # standard SAST/SCA/container scan
│   │   ├── go-ci.yml           # standard Go CI
│   │   └── node-ci.yml         # standard Node CI
│   └── actions/
│       ├── security-scan/      # composite action
│       └── notify-slack/       # composite action
└── README.md

This shape gives 100-line product workflows in every repo that are mostly uses: calls into the org-default repo. Standards changes happen once, reviewed once, and propagate by version-tag bump in calling repos (or by Dependabot PR if you are SHA-pinning).

The Failure Modes We See in Customer Audits

Three failure patterns surface in customer audits of mature reusable-workflow estates:

  1. Reusable workflow used as composite action — a 5-step "lint and test" sequence implemented as a reusable workflow. Each call spins up a fresh runner, re-clones the repo, re-installs dependencies. Pipeline minutes inflate by 3-5x
  2. secrets: inherit everywhere — every reusable workflow inherits all caller secrets. The blast radius of a compromised reusable workflow is the union of every caller's secrets. Pass secrets explicitly
  3. Branch references in production — workflows pinned to @main on a producing repo where any contributor can merge. A compromised commit on main propagates to every caller within minutes

How Opsio Helps

Opsio's managed github actions typically include reusable-workflow library design as a core deliverable. Engagements run 8-12 weeks for organisations with 100-500 repositories: discovery and standard-extraction, library design, security-and-versioning policy, rollout to a pilot cohort, and migration of the long tail. Customers running broader DevOps platforms pair this work with our devops consulting delivery, and customers consolidating CI estates from multiple tools onto GitHub Actions use our pipeline services delivery service for the cross-tool migration.

About the Author

Oscar Bergenbrink
Oscar Bergenbrink

CTO at Opsio

Technology leadership, cloud architecture, and digital transformation strategy

Editorial standards: This article was written by a certified practitioner and peer-reviewed by our engineering team. We update content quarterly to ensure technical accuracy. Opsio maintains editorial independence — we recommend solutions based on technical merit, not commercial relationships.