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:

1
2
service-build-test
service-deploy

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:

  1. Operators see too many workflows
  2. Responsibility attribution becomes less clear

The UI I actually wanted:

1
2
3
4
CI
Deploy Dev
Deploy Prod
Infrastructure

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:

1
2
3
4
5
6
7
8
9
.github/
  workflows/
    ci.yml
    deploy-dev.yml
    deploy-prod.yml
    infra.yml
  actions/
    setup-api-env/
    install-cloud-sql-proxy/

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.

References