GitHub Actions Reusable Workflows: We Standardized CI Across 20 Open Source Repos and Lived to Tell the Tale
Let’s be real. Maintaining CI for twenty open source repos is a special kind of hell.
You start with one project. Clean setup. Fast builds. You feel good.
Claude Code Guide: A Practical AI Coding Tool for Developers
Summary: Claude Code is a powerful AI coding tool that helps developers accelerate software development. This article provides… ...
Then you create a second repo. Copy the workflow. Tweak it. Fine.
By the time you hit ten repos, you’ve got nine different YAML configurations that all do roughly the same thing. One uses `ubuntu-20.04`. Another is on `22.04`. Someone hardcoded a Node version that’s two majors behind. Your Python linting step uses flake8 in one repo, pylint in another, and black just for fun in a third.
Outsourcing Software Development: The Real Playbook for CTOs in 2025
TL;DR: Outsourcing software isn’t about cheap labor — it’s about execution speed. Vietnam is the new sweet spot… ...
It’s chaos. And it’s costing you time and trust.
I manage a small remote team here in Can Tho, Vietnam, and we oversee a suite of open source libraries for a US-based SaaS company. When the repo count hit twenty, I knew we needed a better way. Copy-paste wasn’t scaling.
We didn’t need better CI for each project. We needed one CI that worked for all of them.
Here’s how we used GitHub Actions reusable workflows to fix this mess.
The Problem: Pipeline Drift
Pipeline drift is real. It happens slowly. A developer needs a specific cache key for one repo. Someone else updates a linter config only in their project. Before you know it, your “standard” CI is just a myth.
Honestly, I spent more time debugging CI failures than reviewing code some weeks. The fun ones were the silent failures—builds that passed but used outdated tools, or tests that skipped because of a renamed workflow step.
The metrics were brutal:
- 14 hours per week spent on CI maintenance across all repos
- 8 standard workflows that shared zero common components
- ~40% of CI failures were caused by configuration drift, not actual code bugs
That’s insane. And completely fixable.
The Solution: Reusable Workflows
GitHub Actions has supported reusable workflows since 2021. It’s not new tech. But most teams don’t use it well.
The idea is simple: define a workflow once in a central repository, then call it from any other repo. Changes propagate automatically.
But there’s a catch—you have to design the abstraction correctly. Get it wrong, and you’ve just created another layer of complexity.
Here’s how we got it right.
Step 1: The Central Workflow Repository
We created a single private repo called `oss-ci-workflows`. This is the single source of truth for every CI pipeline across our 20 open source projects.
Structure:
oss-ci-workflows/
├── .github/
│ └── workflows/
│ ├── ci-python.yml
│ ├── ci-node.yml
│ ├── lint-and-test.yml
│ └── release.yml
├── actions/
│ ├── setup-python/
│ │ ├── action.yml
│ │ └── ...
│ └── cache-deps/
│ ├── action.yml
│ └── ...
└── README.md
Each workflow in the `workflows` folder is a reusable workflow. Each `action` is a composable action that the workflows call internally.
Step 2: The Reusable Workflow Template
Here’s the Python CI workflow we use. It’s not magic. It’s just deliberate.
yaml
# oss-ci-workflows/.github/workflows/ci-python.yml
name: CI - Python
on:
workflow_call:
inputs:
python-version:
description: 'Python version to use'
required: false
type: string
default: '3.11'
os:
description: 'Operating system'
required: false
type: string
default: 'ubuntu-22.04'
run-lint:
description: 'Run linter'
required: false
type: boolean
default: true
run-type-check:
description: 'Run mypy type check'
required: false
type: boolean
default: true
jobs:
test:
runs-on: ${{ inputs.os }}
steps:
- uses: actions/checkout@v4
- uses: oss-ci-workflows/actions/setup-python@v1
with:
python-version: ${{ inputs.python-version }}
- uses: oss-ci-workflows/actions/cache-deps@v1
- name: Run tests
run: |
uv run pytest --cov --cov-report=xml -x -q
- name: Upload coverage
uses: codecov/codecov-action@v5
if: success()
lint:
if: ${{ inputs.run-lint }}
runs-on: ${{ inputs.os }}
steps:
- uses: actions/checkout@v4
- uses: oss-ci-workflows/actions/setup-python@v1
with:
python-version: ${{ inputs.python-version }}
- name: Lint with ruff
run: |
uv run ruff check .
type-check:
if: ${{ inputs.run-type-check }}
runs-on: ${{ inputs.os }}
steps:
- uses: actions/checkout@v4
- uses: oss-ci-workflows/actions/setup-python@v1
with:
python-version: ${{ inputs.python-version }}
- name: mypy check
run: |
uv run mypy src/
Step 3: Calling the Workflow from Consumer Repos
Now here’s the part that makes you smile. Each consumer repo’s CI file is tiny.
yaml
# consumer-repo/.github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
ci:
uses: oss-ci-workflows/.github/workflows/ci-python.yml@v1
with:
python-version: '3.12'
run-lint: true
run-type-check: true
That’s it. Twelve lines. No duplication. No drift.
If I want to update the Python CI across all 20 repos, I change one file in `oss-ci-workflows`. Done.
Step 4: Versions Matter
We tag every release of our central workflows repo with semantic versions: `v1.0.0`, `v1.1.0`, `v2.0.0`.
Consumer repos pin to a major version:
uses: oss-ci-workflows/.github/workflows/ci-python.yml@v1
This gives us confidence. We can push minor updates without breaking anyone. Major changes require a version bump. It’s the same discipline you’d use for libraries.
The Results After 6 Months
We rolled this out six months ago. Here’s what changed:
| Metric | Before | After |
|---|---|---|
| CI maintenance time/week | 14 hours | 2.5 hours |
| Unique workflow configs | 20 | 1 |
| CI drift incidents/month | ~18 | 0 |
| Average build time (Python) | 11 min | 6 min |
| Failed builds from misconfig | ~40% | 2% |
The build time improvement came from removing cruft. One repo was using `pip install` without caching. Another had a bloated test matrix. When you centralize, you optimize once for everyone.
We also saw fewer “works on my machine” excuses. When CI is identical across projects, developers can’t hide behind environment differences. That’s uncomfortable at first. But it makes better engineers.
Three Hard Lessons We Learned
It wasn’t all smooth sailing. Here are the things I’d do differently if I started over.
1. Don’t Over-Abstract Too Early
We started with three reusable workflows. That was fine. Then someone suggested we make one “mega workflow” that handled everything with conditional steps.
Don’t do this.
Reusable workflows should map to natural boundaries: language-specific CI, security scanning, deployment. One workflow per concern. Mixing them creates code that’s harder to debug than the original duplication.
2. Pin Your Dependencies Explicitly
We learned this the hard way. The central repo’s `setup-python` action used `actions/setup-python@v4`. When v5 came out, we updated it, thinking it was a minor change. One of our older consumer repos broke because it relied on a deprecated Node version that v5 stopped supporting.
Now we pin internal action references to specific commits:
yaml
uses: oss-ci-workflows/actions/setup-python@a1b2c3d4e5f6
We only update pinned hashes after testing on a representative set of consumer repos.
3. Communicate Breaking Changes Early
Reusable workflows can silently break downstream projects if you’re not careful.
Our policy now: any change to the central workflow repo gets a PR. We tag at least 3 active consumer repos as reviewers. If they all pass, we merge. Then we cut a release.
It sounds bureaucratic. But it beats waking up to 12 failed CI runs at 2 AM.
The ECOAAI Connection: Why This Matters for Remote Teams
I’m telling you this because it directly relates to how we work here at ECOAAI.
Our team in Can Tho manages open source projects for clients in the US and Europe. The timezone difference means we can’t afford chaotic CI. If a pipeline breaks at 3 PM Vietnam time, our client in San Francisco wakes up to a red build.
Reusable workflows eliminated that problem.
When we introduce a new developer to a project—which happens frequently as we scale—they don’t need to learn a bespoke CI setup. They learn one pattern. They apply it everywhere.
That’s the multiplier effect. Consistent tooling eliminates context switching. And context switching is the silent killer of developer productivity.
Should You Adopt It?
If you maintain more than 3 repos that should have similar CI, the answer is yes.
Here’s a quick litmus test:
- Are you copying YAML files between repos? Yes → You need reusable workflows.
- Are your CI failures inconsistent across projects? Yes → You need reusable workflows.
- Are you spending more time on CI than on actual feature work? Yes → You need reusable workflows.
But don’t overcomplicate it. Start with one workflow. Migrate one repo. Prove it works. Then expand.
Actually, that’s how we started. We migrated one Python repo first. Then three. Then all of them.
The first migration took 2 hours. The last one took 10 minutes.
Frequently Asked Questions
Can I use GitHub Actions reusable workflows across different organizations?
Yes, but there are limitations. You can reference workflows from public repos in any organization. For private workflows, all consuming repos must be in the same organization. You can also use environment secrets to handle cross-org authentication if needed.
How do I test changes to my reusable workflows without breaking consumer repos?
Create a test consumer repo that uses the `@main` branch of your workflow. Run CI on that repo before cutting a release. We also use a GitHub Actions matrix to test with different Python/Node versions in a dedicated testing workflow within the central repo itself.
What’s the difference between reusable workflows and composite actions?
Reusable workflows are entire CI jobs that run in their own context. Composite actions are just a series of steps bundled together, running in the same context as the caller. Use reusable workflows when you need multiple jobs (e.g., test + lint + deploy). Use composite actions when you just need to share a few steps.
Does ECOAAI use this approach for client projects?
Yes. We standardize CI across all client projects using reusable workflows hosted in our private infrastructure. This ensures every project we ship has the same quality gates, regardless of which developer or team is working on it. It’s one reason our defect rate is below 2% across the board.
Related reading: Outsourcing Software in 2025: Why Vietnam Is Winning the Offshore Engineering Game
Related reading: Why Smart CTOs Hire Vietnamese Developers: A Strategic Talent Playbook for 2025