GitHub Actions · lesson 4 of 7
Beginner
2Intermediate
4Also part of GitHub Actions
5- + Bash Error Handling from Bash
- + CI/CD Pipeline Patterns: Sequential, Parallel, and Staged from DevOps
- + Terraform CI/CD with GitHub Actions (plan/apply) from Terraform
- + A CI Security Gate: Gitleaks, Bandit and pip-audit in One Workflow from DevSecOps (AppSec in the pipeline)
- + Migrating a Jenkins Pipeline to GitHub Actions from Jenkins
Reusable Workflows: Real Input Passing, and a required Validation Gap in act
A workflow_call input flows through correctly when supplied — but marking it required: true doesn't stop act from running the job anyway with a silently empty value when it's omitted.
By SkillByExample · Updated
A reusable workflow takes real inputs from its caller — but one of its safety guarantees
doesn’t survive local testing with act the way it should.
A reusable workflow, called with a real input
# .github/workflows/reusable.yml
name: Reusable
on:
workflow_call:
inputs:
target:
required: true
type: string
jobs:
greet:
runs-on: ubuntu-latest
steps:
- run: echo "deploying to ${{ inputs.target }}"
# .github/workflows/caller.yml
name: Caller
on: [push]
jobs:
call-it:
uses: ./.github/workflows/reusable.yml
with:
target: staging
act push -W .github/workflows/caller.yml -P ubuntu-latest=catthehacker/ubuntu:act-latest
[call-it/Reusable/greet] ⭐ Run Set up job
[call-it/Reusable/greet] ✅ Success - Set up job
[call-it/Reusable/greet] ⭐ Run Main echo "deploying to staging"
[call-it/Reusable/greet] | deploying to staging
[call-it/Reusable/greet] ✅ Success - Main echo "deploying to staging" [126.928656ms]
[call-it/Reusable/greet] 🏁 Job succeeded
The job name call-it/Reusable/greet shows the full chain: the caller’s job (call-it), the
called workflow’s name (Reusable), and its job (greet) — target: staging flowed straight
through into inputs.target inside the called workflow.
The gap: required: true isn’t actually enforced by act
# .github/workflows/caller_missing.yml
name: CallerMissing
on: [push]
jobs:
call-it:
uses: ./.github/workflows/reusable.yml
No with: block at all — the required target input is simply never supplied.
act push -W .github/workflows/caller_missing.yml -P ubuntu-latest=catthehacker/ubuntu:act-latest
[call-it/Reusable/greet] ⭐ Run Set up job
[call-it/Reusable/greet] ✅ Success - Set up job
[call-it/Reusable/greet] ⭐ Run Main echo "deploying to "
[call-it/Reusable/greet] | deploying to
[call-it/Reusable/greet] ✅ Success - Main echo "deploying to " [149.864147ms]
[call-it/Reusable/greet] 🏁 Job succeeded
required: true should have rejected this call outright — on real GitHub Actions,
workflow_call inputs are validated before any job is even scheduled, and a missing required
input fails the run immediately with a clear error. Here, act let it through, silently
resolved inputs.target to an empty string, and the job reported succeeded while actually
deploying to nowhere. This is a real, observed gap in act’s local emulation of
workflow_call — not something GitHub’s real service is documented to do.
Practice
1. Add an explicit runtime check that fails loudly instead of trusting required: true alone.
jobs:
greet:
runs-on: ubuntu-latest
steps:
- name: Check target is set
run: |
if [ -z "${{ inputs.target }}" ]; then
echo "::error::target input is required but was empty"
exit 1
fi
- run: echo "deploying to ${{ inputs.target }}"
[call-it/Reusable/greet] ⭐ Run Main Check target is set
[call-it/Reusable/greet] ❗ ::error::target input is required but was empty
[call-it/Reusable/greet] ❌ Failure - Main Check target is set [263.652124ms]
[call-it/Reusable/greet] 🏁 Job failed
Error: Job 'greet' failed
Now the missing-input case actually fails, under act and on real GitHub Actions alike —
::error:: is GitHub’s own log-annotation syntax for surfacing a message as a workflow
error, visible even without opening the raw log.
2. Pass an unexpected extra input the reusable workflow never declared.
jobs:
call-it:
uses: ./.github/workflows/reusable.yml
with:
target: staging
region: us-east-1
[call-it/Reusable/greet] ⭐ Run Main echo "deploying to staging"
[call-it/Reusable/greet] | deploying to staging
[call-it/Reusable/greet] ✅ Success - Main echo "deploying to staging" [105.432526ms]
[call-it/Reusable/greet] 🏁 Job succeeded
region was never declared in the called workflow’s on.workflow_call.inputs block, and
act didn’t reject it — it just silently dropped the unrecognized input and ran normally with
target still working correctly. This is the same pattern as the missing-required-input gap:
act’s workflow_call input validation is looser than GitHub’s real service, so a typo’d
input name here would not be caught locally either.
3. Add a second declared input with a default value and omit it from the call.
# reusable.yml, updated
inputs:
target:
required: true
type: string
dry_run:
required: false
type: boolean
default: false
[call-it/Reusable/greet] | deploying to staging, dry_run=false
Omitting an optional input with a declared default correctly falls back to that default —
this is the one case where “input not supplied” behaves exactly as documented, unlike the
required: true case above.
Next: deployment environments, and a real limitation this substrate has for approval gates.