Don't Repeat Yourself (DRY) at Scale
As your organization grows, you will find yourself copy-pasting the same deployment logic into 50 repositories. When a security team mandates a new scanning step, you now have 50 files to update. GitHub provides two complementary mechanisms to share and standardize logic: Composite Actions and Reusable Workflows.
1. Composite Actions (action.yml)
A Composite Action is a reusable bundle of steps stored in its own repository (or a subdirectory of a repository). Think of it as a function you can call from any workflow.
- What it abstracts: A sequence of
steps— shell commands and other action calls. - Use case: "Set up our internal Python environment, authenticate to our private PyPI feed, and install dependencies." This is a 5-step sequence that every Python project needs.
- How it's called: a single
uses:line referencing the action's repository and version. - Inputs and outputs: Composite Actions accept
inputs:and can setoutputs:for the calling workflow to consume.
A composite action's action.yml declares a name, description, its typed inputs (like a Python version with a default), and a runs block of type composite listing the steps it bundles — for example, invoking the standard Python setup action with the given version, then running a shell step that points pip at an internal private package feed.
2. Reusable Workflows (workflow_call)
A Reusable Workflow is an entire workflow file that can be invoked by other workflows. It is not just steps — it includes full jobs with their own runner environments, environment protection rules, and secret handling.
- What it abstracts: An entire deployment pipeline, including security gates, approval environments, and post-deployment verification.
- Use case: "Every team that deploys to production must use the company-approved pipeline that includes SAST scanning, image signing, and a manual approval gate."
- How it's called: a job in the calling repository's workflow sets
uses:to the reusable workflow's path and version instead ofruns-on:, passes typedwith:inputs, and can inherit all of the caller's secrets in one line. The reusable workflow itself, on the other end, declares aworkflow_calltrigger with its own typed inputs and required secrets — the contract that callers must satisfy.
Choosing between them:
| Concern | Composite Action | Reusable Workflow |
|---|---|---|
| Abstraction level | Steps | Jobs |
| Has its own runner | No (shares caller's runner) | Yes (fresh runner per job) |
| Can enforce environment gates | No | Yes |
| Versioned independently | Yes | Yes |
| Best for | Setup scripts, utility tasks | Full deployment pipelines |
The organizational recommendation: Use Composite Actions for shared tooling (setup, lint, test utilities). Use Reusable Workflows to enforce company-wide deployment policies that teams cannot bypass.