Why I Didn't Use Reusable Workflows: GitHub Actions Readability, Maintainability, and Operator-Facing Tradeoffs
I originally thought that extracting shared logic into reusable workflows would be cleaner.
After actually implementing it, I ended up pulling them back into entry workflows, keeping only composite actions. Not because reusable workflows don’t work, but because in this case, they didn’t deliver the “cleanliness” I actually wanted.
This post documents the tradeoffs I ended up with.
Initial Motivation for Extracting Shared Pipelines
The reasoning was sound.
If deploy-dev and deploy-prod both have these steps:
- test
- build
- migrate
- terraform deploy
- health check
Then the natural instinct is to extract two reusable workflows:
|
|
And the entry workflows become a thin orchestration layer.
This design appears to have several clear advantages:
- less YAML duplication
- one shared implementation path
- easier to update one common pipeline
Looks great on paper.
Friction Shows Up in the Actions UI, Not in the YAML
What actually made me change my mind wasn’t functional failure, but the operational and readability aspects.
GitHub Actions UI displays reusable workflows as separate runs too. This creates two practical issues:
- Operators see too many workflows
- Responsibility attribution becomes less clear
The UI I actually wanted:
|
|
But after using reusable workflows, the Actions page gets extra internal workflow entries. Technically they’re not broken, but for people who operate them daily, it gets very cluttered.
Tracing Through Multiple Files Slows Debugging
Beyond the UI, source code tracing cost also increases.
When a deploy fails, you often have to jump between:
- entry workflow
- reusable workflow
- composite action
- Terraform root
This tracing path is walkable, but slower than reading the complete pipeline directly in the entry workflow.
Especially when the workflow also involves:
- branch guard
- environment-scoped vars
- job-level permissions
- OIDC auth
- concurrency
If these things are split across different workflow files, the cognitive load of review increases.
Decision Rule for What to Factor Out
I ended up using a very simple criterion.
Cases Where Bundled Steps Remain Preferable
If it’s just a step group, it’s suitable as a composite action.
For example:
- install uv and sync dependencies
- install Cloud SQL Proxy
These are reasonable to extract because:
- no job graph
- no permissions model
- no environment decision
- no workflow identity ambiguity
Cases Where Steps Stay in the Trigger File
If it is itself the body of the deploy pipeline, I now prefer keeping it in the entry workflow.
For example:
- build and push image
- run migration
- terraform plan and apply
- health checks
These steps are strongly tied to branch, environment, permissions, and identity. Keeping them in the entry workflow makes them easier to audit and debug.
Deduplication Versus Comprehensibility
Fundamentally this isn’t about technical right or wrong, it’s a tradeoff.
| Option | Advantage | Cost |
|---|---|---|
| Reusable workflows | less duplication | worse UI and more indirection |
| Flat entry workflows | more explicit | some duplication |
| Composite actions | reuse step groups cleanly | cannot model full workflow graphs |
In my case, what truly needed to be prioritized was:
- readable
- maintainable
- clean operator-facing surface
So I ended up choosing:
- flat entry workflows
- composite actions for setup steps
- accept some duplication
Repeating Code That Justifies Itself
Not all duplication should be eliminated.
If deploy-dev and deploy-prod each have their own copy of:
- build step
- migrate step
- terraform apply step
In some cases this is actually acceptable.
Because what it buys is:
- each workflow can stand on its own
- each environment path is clear
- permission auditing is more intuitive
- operators don’t need to understand the internal wiring of workflows
For small to medium repos, this kind of duplication is often more maintainable than high abstraction.
Scenarios Where Externalized Pipeline Definitions Add Value
I wouldn’t say reusable workflows should never be used.
They still have value in these situations:
- many repositories share the same workflow
- the workflow graph itself is the reusable asset
- UI clutter is acceptable
- operators never directly interact with the Actions page
But if your requirements are:
- Actions page must stay clean
- operator-facing workflows must be understandable at a glance
- permissions must be easy to audit
Then reusable workflows are not necessarily the best sharing approach.
Final Repository Layout
What I ended up being happy with:
|
|
The benefit of this shape is that operator-facing workflows are clear, and shared setup steps haven’t turned into massive copy-paste.
Main Takeaway
My biggest takeaway this time:
less YAML is not always less complexity.
Reusable workflows can reduce duplication, but they don’t necessarily make the entire system easier to understand.
If what you truly care about is:
- operator clarity
- reviewability
- permission visibility
- GitHub Actions UI cleanliness
Then flat entry workflows + composite actions may be a better fit than reusable workflows.