Reusable Workflows
Eliminate duplication with composite actions, reusable workflows (workflow_call), and shared templates across repositories.
π§ Simple Explanation (ELI5)
Think of building with LEGO bricks.
- Composite actions are like custom bricks you make yourself. You glue a few small pieces together into one handy brick β now whenever you need that shape, you grab it off the shelf instead of assembling it from scratch every time.
- Reusable workflows are like instruction booklets for complete sub-builds. Instead of writing step-by-step instructions for the same wing or turret in every LEGO set, you create one booklet called "Build the Standard Wing" and just reference it: "See Booklet #7." You can even pass in custom colors (inputs) and get back the finished piece (outputs).
- The
.githubrepository is your org's shared parts bin. Every team in the building can grab bricks and booklets from it β no need to duplicate them in every box.
Instead of building every creation from scratch, you make reusable LEGO bricks (actions) and instruction booklets (reusable workflows) that anyone can use. This is the DRY (Don't Repeat Yourself) principle in action.
π§± Composite Actions
A composite action bundles multiple steps into a single reusable unit defined in an action.yml file. Think of it as creating your own custom action β like the ones from the marketplace β but tailored to your project's needs.
action.yml Structure
Every composite action requires these top-level keys:
nameβ display name of the actiondescriptionβ what it doesinputsβ parameters the caller can pass in (each withdescription, optionaldefaultandrequired)outputsβ values the action exposes back to the callerrunsβ must setusing: compositeand liststeps
Example: Setup and Lint
# .github/actions/setup-and-lint/action.yml
name: 'Setup and Lint'
description: 'Install dependencies and run linting'
inputs:
node-version:
description: 'Node.js version'
default: '20'
runs:
using: composite
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- run: npm ci
shell: bash
- run: npm run lint
shell: bash
Every run: step inside a composite action must specify a shell: explicitly. Unlike regular workflow steps, composite actions don't inherit a default shell β this is deliberate so actions are portable across operating systems.
Using the Composite Action
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-and-lint
with:
node-version: '20'
The path ./.github/actions/setup-and-lint points to a directory in your repository containing the action.yml. For actions published to the marketplace or a separate repo, use the full reference: uses: org/action-repo@v1.
Publishing to the Marketplace
- Create a dedicated repository for the action
- Ensure
action.ymlis at the root of the repo - Tag a release (e.g.,
v1.0.0), then create a major version tag (v1) pointing to the same commit - Go to the repo's Releases page and check "Publish this Action to the GitHub Marketplace"
π Reusable Workflows (workflow_call)
Reusable workflows let you define an entire workflow (with jobs, steps, environment references) and call it from other workflows. This operates at a higher level than composite actions β you're reusing whole pipelines, not just step bundles.
Trigger: on: workflow_call
A reusable workflow declares itself callable by other workflows using the workflow_call trigger. It defines inputs, secrets, and outputs at the workflow level.
Complete Example β Called Workflow
# .github/workflows/deploy-template.yml
name: Reusable Deploy Workflow
on:
workflow_call:
inputs:
environment:
required: true
type: string
image-tag:
required: true
type: string
secrets:
AZURE_CREDENTIALS:
required: true
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- uses: azure/login@v2
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- run: |
az aks get-credentials --resource-group myRG --name myCluster
helm upgrade --install myapp ./charts \
--set image.tag=${{ inputs.image-tag }}
Complete Example β Caller Workflow
# .github/workflows/deploy.yml
name: Deploy Pipeline
on:
push:
branches: [main]
jobs:
deploy-staging:
uses: ./.github/workflows/deploy-template.yml
with:
environment: staging
image-tag: ${{ github.sha }}
secrets:
AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }}
deploy-production:
needs: deploy-staging
uses: ./.github/workflows/deploy-template.yml
with:
environment: production
image-tag: ${{ github.sha }}
secrets:
AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }}
Instead of passing each secret explicitly, you can use secrets: inherit to forward all secrets from the caller to the reusable workflow automatically. This is convenient but less explicit β prefer explicit passing in production for auditability.
Outputs from Reusable Workflows
Reusable workflows can expose outputs back to the caller. Define outputs at the workflow level, map them from job outputs, and access them in the caller via needs.<job>.outputs.<name>.
# In the reusable workflow
on:
workflow_call:
outputs:
deploy-url:
description: 'URL of the deployed app'
value: ${{ jobs.deploy.outputs.url }}
jobs:
deploy:
runs-on: ubuntu-latest
outputs:
url: ${{ steps.get-url.outputs.url }}
steps:
- id: get-url
run: echo "url=https://myapp-staging.azurewebsites.net" >> $GITHUB_OUTPUT
Cross-Repo Calls
To call a reusable workflow from another repository, use the full reference:
jobs:
ci:
uses: my-org/shared-workflows/.github/workflows/ci.yml@main
with:
node-version: '20'
secrets: inherit
Limitations
- Max 4 levels of nesting β a reusable workflow can call another reusable workflow, up to 4 deep
- No
envcontext from the caller β workflow-levelenv:variables from the caller are not passed to the reusable workflow. Useinputsinstead - Max 20 reusable workflows per workflow file
- Reusable workflows must be in a
.ymlor.yamlfile in the.github/workflows/directory - Private repo workflows are only callable by workflows in the same repository unless you enable sharing in repository settings
π Comparison: Composite Actions vs Reusable Workflows vs JavaScript/Docker Actions
| Feature | Composite Action | Reusable Workflow | JavaScript Action | Docker Action |
|---|---|---|---|---|
| Granularity | Steps (step-level) | Entire jobs/workflow | Single step | Single step |
| Defined in | action.yml | .github/workflows/*.yml | action.yml + JS | action.yml + Dockerfile |
| Reuse scope | Same repo or cross-repo | Same repo or cross-repo | Cross-repo (marketplace) | Cross-repo (marketplace) |
| Inputs | Yes (inputs:) | Yes (inputs:) | Yes (inputs:) | Yes (inputs:) |
| Outputs | Yes | Yes (via job outputs) | Yes | Yes |
| Secrets | Via ${{ secrets.* }} | Explicit or secrets: inherit | Via ${{ secrets.* }} | Via ${{ secrets.* }} |
| Nesting | Can use other actions | Up to 4 levels | N/A | N/A |
| Best for | Bundling setup steps | Standardizing entire pipelines | Complex logic, API calls | Custom environments |
π Sharing Across Repositories
The .github Repository
Every GitHub organization can have a special repository named .github. Workflow templates and reusable workflows placed here are available org-wide. This is the recommended pattern for platform teams maintaining standardized CI/CD across many repos.
Pinning Versions
Always pin the version of reusable workflows and actions to prevent unexpected changes from breaking your pipelines:
- Major tag:
@v1β gets patch/minor updates (convenient, slightly less safe) - Exact tag:
@v1.2.0β deterministic, you control when to upgrade - Commit SHA:
@a1b2c3d4...β most secure, immutable, immune to tag tampering
# Recommended: pin to full SHA for security-critical workflows uses: my-org/shared-workflows/.github/workflows/ci.yml@8f3e1b2a... # Acceptable: pin to exact version tag uses: my-org/shared-workflows/.github/workflows/ci.yml@v1.2.0 # Convenient but less safe: pin to major tag uses: my-org/shared-workflows/.github/workflows/ci.yml@v1
Internal Visibility for Private Orgs
To share actions or reusable workflows from a private repository, go to the repo's Settings β Actions β General and set "Access" to allow other repositories in the organization. Without this, only workflows in the same repo can reference them.
πΊοΈ How It All Connects
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β CALLER WORKFLOW β β deploy.yml β β β β jobs: β β deploy-staging: β β uses: ./.github/workflows/deploy-template.yml ββββββββββ β β with: β β β environment: staging (inputs) β β β image-tag: abc123f (inputs) β β β secrets: β β β AZURE_CREDENTIALS: *** (secrets) β β β βΌ β β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β β REUSABLE WORKFLOW β β β β deploy-template.yml β β β β β β β β on: workflow_call β β β β inputs: environment, image-tag βββ receives inputs β β β β secrets: AZURE_CREDENTIALS βββ receives secrets β β β β outputs: deploy-url βββΆ returns outputs β β β β β β β β jobs: β β β β deploy: β β β β steps: β β β β - azure/login@v2 β β β β - helm upgrade --install ... β β β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β β β β deploy-production: βββ receives outputs ββββ β β needs: deploy-staging β β uses: ./.github/workflows/deploy-template.yml β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
π οΈ Hands-on Lab
Lab 1: Create a Composite Action
- Create
.github/actions/setup-node-project/action.yml - Define inputs:
node-version(default:20),install-command(default:npm ci) - Add steps: checkout (if needed), setup-node, install deps, run lint
- Reference this action from a workflow and verify it runs successfully
Lab 2: Create a Reusable Workflow
- Create
.github/workflows/build-and-test.ymlwithon: workflow_call - Define inputs:
node-version(string),run-e2e(boolean, defaultfalse) - Add a job that checks out code, installs deps, runs unit tests, and conditionally runs e2e tests
- Expose an output:
test-result(pass/fail)
Lab 3: Call and Chain Reusable Workflows
- Create a caller workflow that invokes
build-and-test.ymlwithrun-e2e: true - Add a second job that depends on the first and reads the
test-resultoutput - If
test-resultis "pass", proceed to a deploy job; otherwise, skip deployment - Pass secrets using both explicit passing and
secrets: inheritβ observe the difference
Use act to test composite actions locally before pushing. Note that act has limited support for workflow_call β for reusable workflows, push to a branch and trigger manually with workflow_dispatch during development.
π Debugging Common Issues
"Reusable workflow not found"
- Wrong path: Ensure the path is relative to the repo root β
./.github/workflows/ci.yml, notci.yml - Wrong ref: For cross-repo calls, the
@refmust be a branch, tag, or SHA that exists.@mainis common but verify the default branch name - Visibility: Private repo workflows are not callable from other repos unless explicitly enabled in repository settings under Actions β General β Access
- File location: Reusable workflows must be in
.github/workflows/β not in a subdirectory of it
"Input not available" or Empty Input
- Type mismatch: If the input is declared as
type: booleanbut you pass a string like'true', it may not behave as expected. Ensure types match - Not declared: The input must be declared in the reusable workflow's
workflow_call.inputs. Undeclared inputs are silently ignored - Wrong context: In the called workflow, use
${{ inputs.my-input }}, not${{ github.event.inputs.my-input }}β that's forworkflow_dispatch
"Secret not passed"
- Must explicitly pass: Secrets are not automatically forwarded to reusable workflows. You must either pass each secret explicitly or use
secrets: inherit - Typo in name: Secret names in the caller must exactly match the names declared in the reusable workflow's
secrets:definition - Missing at org/repo level: The secret must exist in the caller's repository or organization β the reusable workflow doesn't have its own secret storage
π― Interview Questions
Basic (5)
1. What is a composite action in GitHub Actions?
A composite action bundles multiple run and uses steps into a single reusable action defined in an action.yml file. It uses using: composite in the runs: block and requires an explicit shell: for every run step.
2. How do you trigger a reusable workflow?
A reusable workflow uses on: workflow_call as its trigger. The caller workflow references it with uses: at the job level (not step level), passing inputs via with: and secrets via secrets:.
3. What is the difference between workflow_call and workflow_dispatch?
workflow_dispatch triggers a workflow manually from the GitHub UI or API. workflow_call is invoked by another workflow β it's a machine-to-machine call. They're often combined so a workflow can be both manually triggered and reusable.
4. Why must composite actions specify shell: in every run step?
Unlike regular workflows that inherit a default shell from the runner, composite actions must be explicit to ensure portability. The same action may run on Linux (bash), Windows (pwsh), or macOS, so forcing an explicit shell prevents ambiguity.
5. How do you pass secrets to a reusable workflow?
Either explicitly in the caller: secrets: MY_SECRET: ${{ secrets.MY_SECRET }}, or use secrets: inherit to forward all caller secrets automatically.
Intermediate (5)
6. What's the DRY advantage of reusable workflows over copy-pasting YAML?
With reusable workflows, you define the pipeline once and reference it from many repos. When you need to update the pipeline (e.g., add a security scan), you change one file and all callers get the update automatically. Copy-pasting means updating every repo individually β a recipe for drift and missed updates.
7. Can a reusable workflow call another reusable workflow? What are the limits?
Yes, reusable workflows can be nested β a called workflow can call another. The limit is 4 levels of nesting and a maximum of 20 reusable workflow references per workflow file.
8. Why can't a reusable workflow access the caller's env context?
The env context is scoped to the workflow file that defines it. Reusable workflows are separate files with their own scope. To pass configuration, use inputs β this makes the dependency explicit and avoids hidden coupling between caller and callee.
9. How do you share reusable workflows across an entire GitHub organization?
Create a repository named .github in the organization. Place reusable workflows in .github/workflows/ within that repo. For private orgs, ensure the repo's Actions access settings allow other repos to call these workflows.
10. What's the difference between secrets: inherit and explicit secret passing?
secrets: inherit forwards all secrets from the caller, which is convenient but less explicit. Explicit passing (secrets: KEY: ${{ secrets.KEY }}) documents exactly which secrets the reusable workflow needs, improving auditability and following the principle of least privilege.
Senior (5)
11. You're a platform engineer maintaining 50+ microservices. Design a reusable CI/CD strategy.
Create a central .github repo with reusable workflows: ci.yml (lint, test, build image), deploy.yml (deploy to AKS with Helm), security-scan.yml (SAST/DAST). Each microservice's workflow calls these with service-specific inputs (image name, chart path, environment). Pin to version tags (@v2) so teams can adopt updates at their own pace. Use secrets: inherit with organization secrets for common credentials. Add a workflow_dispatch trigger alongside workflow_call so workflows can be tested standalone.
12. How would you version reusable workflows to avoid breaking downstream consumers?
Use semantic versioning with Git tags. Create release branches (release/v1, release/v2) for major versions. The major tag (v1) floats to the latest compatible release. Breaking changes get a new major version (v2). Consumers pin to the major tag for convenience or exact tag/SHA for stability. Document changes in release notes and provide a migration guide for major bumps.
13. A reusable workflow runs fine in repo A but fails in repo B with "secret not available." Both repos pass secrets. What do you investigate?
Check: (1) Does repo B actually have the secret configured? (2) Is the secret at the right scope β repo-level vs environment-level? (3) Is repo B a fork? Fork PRs don't get secrets by default. (4) Are branch protection rules or environment protection rules blocking access? (5) If using org secrets with "selected repositories" visibility, is repo B in the selected list?
14. When should you prefer a composite action over a reusable workflow, and vice versa?
Composite action: when you need to bundle a few setup/utility steps that run within a single job β e.g., "setup Node, install deps, run lint." The caller controls the job configuration (runner, environment, permissions). Reusable workflow: when you need to standardize entire jobs or multi-job pipelines β e.g., "build, test, scan, deploy." The called workflow controls runners, environments, and service containers. Rule of thumb: composite for step-level reuse, reusable workflows for job/pipeline-level reuse.
15. How do you ensure security when consuming third-party actions or cross-org reusable workflows?
Pin to the full commit SHA, not a mutable tag. Review the action's source code before adoption. Use permissions: at the job level to restrict what the action can do. Enable GitHub's dependency review for actions. For high-security environments, fork the action into your org, audit it, and reference your fork. Use Dependabot to get alerts when pinned SHAs have newer versions.
π Real-World Scenario
A platform engineering team at a mid-size company maintains 50+ microservices, each with its own repository. Before reusable workflows, every repo had a copy-pasted CI/CD pipeline. When the team needed to add a container security scan, they had to open 50+ pull requests β and inevitably some repos got missed or had slightly different implementations.
Their solution β a central .github repository with three reusable workflows:
ci.ymlβ Lint, test, build Docker image, push to ACR (inputs:node-version,image-name,run-e2e)deploy.ymlβ Deploy to AKS with Helm (inputs:environment,image-tag,chart-path)security-scan.ymlβ Trivy image scan + CodeQL analysis (inputs:image-ref,severity-cutoff)
Each microservice's workflow shrank to ~20 lines β just calling the three shared workflows with service-specific inputs. Adding the security scan was a single PR to the central repo. Teams pin to @v2 and upgrade on their own schedule. The platform team publishes changelogs and maintains backward compatibility within major versions.
Result: 95% reduction in pipeline YAML duplication, consistent security posture across all services, and new microservices get production-ready CI/CD by copying a 20-line template instead of a 200-line workflow.
π Summary
- Composite actions bundle multiple steps into one reusable action (
action.ymlwithusing: composite) β ideal for step-level reuse like setup and linting - Reusable workflows use
on: workflow_callto define sharable pipelines with inputs, secrets, and outputs β ideal for job/pipeline-level standardization - Caller workflows invoke reusable workflows with
uses:at the job level, passing inputs viawith:and secrets explicitly or viasecrets: inherit - Reusable workflows support up to 4 levels of nesting and don't inherit the caller's
envcontext - The
.githubrepository is the recommended place for org-wide shared workflows and actions - Pin versions with tags (
@v1,@v1.2.0) or commit SHAs for security and reproducibility - Composite actions require explicit
shell:in everyrunstep for cross-platform portability - Choose composite actions for step-level reuse, reusable workflows for pipeline-level standardization, and JavaScript/Docker actions for complex custom logic