| ← Creating Custom Actions | Next: Required Workflows, Protection & Wrap-Up → |
|---|
Reusable workflows let you define an entire workflow that other workflows can call, like a function. This is different from custom actions — actions encapsulate individual steps, while reusable workflows encapsulate entire jobs. They're triggered with the workflow_call event and can accept inputs, secrets, and produce outputs.
In this exercise you'll extract the deployment pattern into a reusable workflow, then call it from both your CD pipeline and a new manual deployment workflow for rollbacks and hotfixes.
The shelter's deploy workflow is working — code that passes CI on main gets deployed automatically. But what happens when something goes wrong in production and you need to quickly roll back to a known-good version? Or deploy a hotfix from a specific commit? Right now, the only option is to push to main and wait for CI. Let's create a manual deployment workflow that lets the team deploy any git ref on demand, and extract the shared deploy logic into a reusable workflow so both pipelines stay in sync.
In the previous exercise you created a composite action to bundle steps together. Reusable workflows solve a similar problem — avoiding duplication — but at a different level. It's important to understand when to reach for each one.
A composite action combines multiple steps into a single step that runs inside a job. A reusable workflow packages one or more entire jobs that a caller workflow references at the job level. Here's a side-by-side comparison:
| Composite Action | Reusable Workflow | |
|---|---|---|
| What it encapsulates | Multiple steps, run as a single step | One or more complete jobs |
| Where it lives | action.yml in any directory (e.g. .github/actions/) |
.github/workflows/ directory only |
| How it's called | uses: inside a job's steps |
uses: directly on a job, not inside steps |
| Runner control | Runs on the caller job's runner | Each job specifies its own runner |
| Secrets | Cannot access secrets directly | Can receive secrets via secrets: or secrets: inherit |
| Logging | Appears as one collapsed step in the log | Every job and step is logged individually |
| Nesting depth | Up to 10 composite actions per workflow | Up to 10 levels of workflow nesting |
| Marketplace | Can be published to the Actions Marketplace | Cannot be published to the Marketplace |
When to use which:
- Choose a composite action when you want to bundle a handful of related steps that run within a single job — like the
setup-python-envaction you just built. - Choose a reusable workflow when you want to share entire job definitions — including runner selection, environment targeting, and concurrency controls — across multiple workflows. Deployment pipelines are a classic use case, which is exactly what we'll build next.
Reusable workflows often need access to secrets and variables — for example, deployment credentials. There are two approaches:
Using secrets: inherit to forward every secret available in the calling workflow to the reusable workflow.
```yaml
deploy:
uses: ./.github/workflows/reusable-deploy.yml
with:
deploy-ref: main
secrets: inherit
```
For a more controlled approach, you can identify which specific secrets to pass in the reusable workflow's on.workflow_call.secrets section:
on:
workflow_call:
inputs:
deploy-ref:
required: false
type: string
secrets:
AZURE_CLIENT_ID:
required: true
AZURE_TENANT_ID:
required: true
AZURE_SUBSCRIPTION_ID:
required: trueThen caller then passes each secret explicitly:
deploy:
uses: ./.github/workflows/reusable-deploy.yml
with:
deploy-ref: main
secrets:
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}Important
For deployment workflows that need Azure credentials, secrets: inherit is the simplest approach. However, defining specific secrets provides better documentation and prevents accidentally exposing secrets the reusable workflow doesn't need. We'll use secrets: inherit in this exercise for simplicity.
Let's extract the shared deploy steps into a reusable workflow. The workflow will accept an optional deploy-ref input so callers can deploy any git ref — the current commit, a previous release tag, or a specific commit SHA.
-
In your codespace, create a new file at
.github/workflows/reusable-deploy.yml. -
Define the
workflow_calltrigger with an input for the git ref to deploy:name: Reusable Deploy Workflow on: workflow_call: inputs: deploy-ref: description: 'Git ref to deploy (commit SHA, tag, or branch). Defaults to the caller workflow ref.' required: false type: string default: ''
-
Add a single job that checks out the code, authenticates with Azure, and deploys:
jobs: deploy: runs-on: ubuntu-latest concurrency: group: deploy-production cancel-in-progress: false steps: - name: Checkout code uses: actions/checkout@v4 with: ref: ${{ inputs.deploy-ref || github.sha }} - name: Install azd uses: Azure/setup-azd@v2 - name: Log in with Azure (Federated Credentials) run: | azd auth login \ --client-id "${{ vars.AZURE_CLIENT_ID }}" \ --federated-credential-provider "github" \ --tenant-id "${{ vars.AZURE_TENANT_ID }}" - name: Deploy application run: azd up --no-prompt env: AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }} AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }} AZURE_LOCATION: ${{ vars.AZURE_LOCATION }}
Note
Reusable workflows have a few important limitations: they can be nested up to 4 levels deep, and the workflow file must be located in the .github/workflows directory. You also cannot call a reusable workflow from within a reusable workflow's steps — they are called at the job level.
Now update your azure-dev.yml to call the reusable workflow instead of defining the deploy steps inline.
-
Replace the contents of
.github/workflows/azure-dev.ymlwith:name: Deploy App on: workflow_dispatch: workflow_run: workflows: ["Run Tests"] branches: [main] types: [completed] permissions: id-token: write contents: read jobs: deploy: if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' uses: ./.github/workflows/reusable-deploy.yml secrets: inherit
Notice how the entire job definition is replaced by a single
uses:reference. The reusable workflow handles checkout, authentication, and deployment — the caller just decides when to deploy.
Now let's add the second caller — a manual deploy workflow for rollbacks and hotfixes. This is where the reusable workflow really earns its keep: same deploy logic, different trigger.
-
Create a new file at
.github/workflows/manual-deploy.yml. -
Add the following content:
name: Manual Deploy on: workflow_dispatch: inputs: deploy-ref: description: 'Git ref to deploy (commit SHA, tag, or branch)' required: true default: 'main' permissions: id-token: write contents: read jobs: deploy: uses: ./.github/workflows/reusable-deploy.yml with: deploy-ref: ${{ inputs.deploy-ref }} secrets: inherit
This workflow is only triggered manually via
workflow_dispatch— it appears as a "Run workflow" button in the Actions tab. It prompts for a git ref (a commit SHA, tag, or branch name to deploy), passes that ref to the reusable workflow'sdeploy-refinput, and uses the same deploy logic as the automated pipeline. -
In the terminal (Ctl+` to toggle), commit and push your changes:
git add .github/workflows/reusable-deploy.yml .github/workflows/azure-dev.yml .github/workflows/manual-deploy.yml git commit -m "Extract reusable deploy workflow and add manual deploy" git push -
Navigate to the Actions tab on GitHub and verify that the deploy workflow runs successfully. You should also see Manual Deploy in the workflow list — try clicking Run workflow to test deploying a specific ref.
Tip
When viewing a workflow run that calls reusable workflows, GitHub shows each caller job separately. Select a job to see the steps from the reusable workflow running inside it.
This pattern keeps your deployment logic in one place. When you need to update the deployment process — like adding health checks or notifications — you change it once in the reusable workflow and every caller benefits.
Reusable workflows reduce duplication at the workflow level. You've extracted the shared deployment pattern into a template that both the automated CD pipeline and the manual deploy workflow call with a single uses reference. This keeps your deployment process maintainable as it grows — any change happens in one place.
Next, we'll ensure quality gates are enforced with branch protection, required workflows, and more.
- Reusing workflows
- The
workflow_callevent - Sharing workflows with your organization
- GitHub Skills: Reusable workflows
| ← Creating Custom Actions | Next: Required Workflows, Protection & Wrap-Up → |
|---|