Intermediate Lesson 7 of 14

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.

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:

Example: Setup and Lint

yaml
# .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
⚠️
shell is Required

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

yaml
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

  1. Create a dedicated repository for the action
  2. Ensure action.yml is at the root of the repo
  3. Tag a release (e.g., v1.0.0), then create a major version tag (v1) pointing to the same commit
  4. 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

yaml
# .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

yaml
# .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 }}
πŸ’‘
secrets: inherit

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>.

yaml
# 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:

yaml
jobs:
  ci:
    uses: my-org/shared-workflows/.github/workflows/ci.yml@main
    with:
      node-version: '20'
    secrets: inherit

Limitations

πŸ“Š Comparison: Composite Actions vs Reusable Workflows vs JavaScript/Docker Actions

Feature Composite Action Reusable Workflow JavaScript Action Docker Action
GranularitySteps (step-level)Entire jobs/workflowSingle stepSingle step
Defined inaction.yml.github/workflows/*.ymlaction.yml + JSaction.yml + Dockerfile
Reuse scopeSame repo or cross-repoSame repo or cross-repoCross-repo (marketplace)Cross-repo (marketplace)
InputsYes (inputs:)Yes (inputs:)Yes (inputs:)Yes (inputs:)
OutputsYesYes (via job outputs)YesYes
SecretsVia ${{ secrets.* }}Explicit or secrets: inheritVia ${{ secrets.* }}Via ${{ secrets.* }}
NestingCan use other actionsUp to 4 levelsN/AN/A
Best forBundling setup stepsStandardizing entire pipelinesComplex logic, API callsCustom 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:

yaml
# 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

text
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                       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

  1. Create .github/actions/setup-node-project/action.yml
  2. Define inputs: node-version (default: 20), install-command (default: npm ci)
  3. Add steps: checkout (if needed), setup-node, install deps, run lint
  4. Reference this action from a workflow and verify it runs successfully

Lab 2: Create a Reusable Workflow

  1. Create .github/workflows/build-and-test.yml with on: workflow_call
  2. Define inputs: node-version (string), run-e2e (boolean, default false)
  3. Add a job that checks out code, installs deps, runs unit tests, and conditionally runs e2e tests
  4. Expose an output: test-result (pass/fail)

Lab 3: Call and Chain Reusable Workflows

  1. Create a caller workflow that invokes build-and-test.yml with run-e2e: true
  2. Add a second job that depends on the first and reads the test-result output
  3. If test-result is "pass", proceed to a deploy job; otherwise, skip deployment
  4. Pass secrets using both explicit passing and secrets: inherit β€” observe the difference
πŸ’‘
Testing Locally

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"

"Input not available" or Empty Input

"Secret not passed"

🎯 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:

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

← Back to GitHub Actions Course