Open three of your team’s repositories side by side and diff their CI configuration. Go on.
You will find the same jobs - lint, test, build, audit - in three slightly different dialects. One pins an old runtime version. One lost its security audit step during a merge conflict. One has a comment from 2024 explaining a workaround for a bug that was fixed long ago. Nobody did anything wrong. Each file was copied from the previous project on the day it started, and then all three drifted quietly for two years.
I have written about DRY where the stakes are highest - login, auth, security layers. Pipelines are the same argument one level up, and they are the most copy-pasted code most organizations own.
Why CI drifts by default
Pipeline configuration has a peculiar property: it is copied at birth and never converges again.
- A new repo starts by copying the last one - the fastest thing to do, and the reason every copy inherits that day’s snapshot of your standards.
- Improvements stay local. Someone adds a great caching step or a secret scan, and it lives in exactly one repo forever because nobody advertises CI changes.
- Nobody owns it. CI belongs to whoever last got annoyed by it.
- Fixes do not propagate. You patch a broken step in repo A; repos B through E keep the bug, and nobody remembers they have it.
The result is that your “organizational standards” exist as archaeology - readable only by comparing files across repos and guessing which one is current.
The pattern: one workflow, many callers
The fix is built into GitHub Actions and takes about an hour to adopt. A workflow declared with on: workflow_call becomes callable by any repo in your organization, with typed inputs and secrets:
# org/.github-workflows/.github/workflows/node-ci.yml
on:
workflow_call:
inputs:
node-version:
type: string
default: "24"
jobs:
checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@<sha> # v4
- uses: actions/setup-node@<sha> # v4
with:
node-version: ${{ inputs.node-version }}
- run: npm ci
- run: npm run lint
- run: npm run format:check
- run: npm audit --audit-level=high
- run: npm run build
And every product repo shrinks to the part that is actually project-specific:
# any product repo — the whole CI file
name: CI
on: [pull_request]
jobs:
ci:
uses: org/.github-workflows/.github/workflows/node-ci.yml@v1
with:
node-version: "24"
Now the logic lives once. Add a new check - a license scan, an accessibility gate - and every repo inherits it on its next version bump. Fix a bug and it is fixed everywhere. As a rule of thumb, this starts paying off at about three repos with similar pipelines, and the payoff grows with every new one.
What belongs central - and what does not
Resist the urge to centralize everything. The split I use:
Central (the same everywhere):
- Lint, format, and type checks
- Dependency and secret scanning
- Build verification and container build checks
- Test execution scaffolding (the how, not the what)
- Dependency-update triage
Local (genuinely project-specific):
- Which tests run and how long they take
- Deployment steps tied to one product’s infrastructure
- Environment-specific configuration
- Anything experimental - prove it in one repo first, promote it later
The test: would a change here be correct for every repo that calls it? If not, it takes an input - or it stays local.
Versioning: the part people get wrong
Once many repos depend on one workflow, how you reference it becomes a real decision with real consequences:
@main— never for production repos. Every caller picks up every change instantly, with no review and no approval step. One bad commit breaks every pipeline in the company simultaneously.- Floating major tag (
@v1) — consumers get patches and fixes automatically; you move the tag deliberately after testing. Convenient, and the common middle ground - but a bad move breaks everyone at once. - Exact version (
@v1.4.2) — upgrades become deliberate opt-in. Slower to roll out fixes, maximum control. - SHA pin (
@<commit-sha>) — maximum safety and reproducibility, because tags can be moved and commit SHAs cannot.
My recommendation for most teams: callers pin a floating major tag you advance intentionally, while the shared workflow itself SHA-pins every action it uses. Which brings us to the reason this is not merely tidiness.
The security payoff (this is the real argument)
Here is what most articles about reusable workflows leave out: your callers inherit your pinning choices invisibly. Every repo that calls your shared workflow runs whatever third-party actions that workflow references, whether or not those repos ever reviewed them.
This is not hypothetical. When a widely used action was compromised (CVE-2025-30066), the pattern was brutally clear: every consumer pinned to a tag received the malicious payload; every consumer pinned to a commit SHA did not. Tags can be moved by an attacker who gains write access. A SHA cannot.
So the shared workflow becomes your single choke point for supply-chain safety:
- SHA-pin every third-party action inside it, with the human-readable version in a trailing comment.
- Let Dependabot or Renovate keep those pins updated - reviewed, deliberately.
- One review, one place, and every repo in the organization inherits the safety.
That is the same compounding-scrutiny argument I made for auth components: concentrate your paranoia where it can be reviewed once and benefit everyone.
The honest risk: one broken workflow, all repos
Centralizing has a failure mode you must plan for: when the shared workflow breaks, nothing in the organization can merge. That is a genuinely bad afternoon.
Treat the shared workflow like the product it now is:
- Test it before tagging. Have a sandbox repo that consumes
@maindeliberately, so problems surface there instead of in production repos. - Roll forward with tags, never silently. A moved major tag is a release: announce it, and be ready to move it back.
- Keep it boring. Clever conditional logic across ten repos is how you build something nobody can debug at 9 a.m. on release day.
- Allow escape hatches. A repo with a genuine special case should be able to define its own job rather than contort the shared one with a fifteenth input.
One cookbook, not ten notebooks
Picture a family where every relative keeps their own handwritten notebook of the same recipes. Grandma’s cake appears in all of them - but one copy dropped the vanilla years ago, another doubled the baking time after a bad oven, and a third has a stain covering the sugar quantity. Nobody is wrong, exactly. But nobody bakes the same cake anymore, and when someone finally works out the perfect version, the improvement stays trapped in one notebook.
Now imagine the family publishes one proper cookbook instead. Everyone owns the same edition. When someone improves the recipe, a new edition goes out - and everyone can choose when to pick it up. The stains stay in the kitchen; the recipe stays true.
Your CI configuration is Grandma’s cake, and most organizations are still passing around notebooks. Publish the cookbook. Version the editions. And check very carefully which ingredients you are telling everyone to buy - because when you write the recipe for the whole family, one bad ingredient goes into every kitchen at once.