Checkpoint Lab — Reusable Workflows, workflow_call, Inputs, Secrets, Outputs, and Nesting
The checkpoint creates one central reusable CI contract and two callers in a disposable repository. Caller A succeeds; Caller B first supplies a syntactically valid but unsupported runtime value so the called workflow fails with preserved evidence. The repair changes only the caller input, proving typed inputs, outputs, explicit secret forwarding, permission down-scoping and nested-call behavior.
Learning objectives
- Build one central reusable CI contract and two caller workflows in a disposable repository.
- Predict and verify source, call-chain, permission, secret and output state before each run.
- Preserve a real first-failure run caused by a syntactically valid but unsupported interface value.
- Repair only the caller input and prove the central and nested outputs without exposing the fake secret.
- Deliver a compact evidence packet and cleanly remove all disposable secret/repository state.
1. Checkpoint scenario and invariant
Your repository has two entry points that should share one CI
policy. The invariant is: callers choose only supported typed
inputs, explicitly delegate one synthetic secret and at most
contents: read, the central contract validates its
interface, the nested leaf receives the secret only through explicit
forwarding, and outputs contain no secret material.
Caller A represents the supported path. Caller B first violates the
semantic runtime contract with 3.12. The failure must
remain in Actions history. The repaired Caller B changes only that
input to 3.13.
2. Preflight and assumptions
-
GitHub.com disposable repository:
gha-reuse-checkpoint. -
One synthetic repository secret:
CH16_FAKE_SECRET; never use a real token. -
GitHub-hosted
ubuntu-24.04; no self-hosted runner, cloud, registry or Kubernetes. - No Marketplace or third-party actions; all work is shell plus same-repository reusable workflows.
- All four workflow files are committed together so relative reusable calls resolve from the same source SHA.
-
Top-level callers default to
permissions: {}and grant onlycontents: readto the central call job.
3. Contract files
Use the reusable-leaf.yml and
reusable-ci.yml definitions from Lesson 2 unchanged.
Their critical contract is reproduced below in compact form so the
checkpoint is independently runnable.
# .github/workflows/reusable-leaf.yml
name: chapter16-reusable-leaf
on:
workflow_call:
inputs:
channel: {required: true, type: string}
secrets:
probe_secret: {required: true}
outputs:
leaf_summary:
value: ${{ jobs.probe.outputs.summary }}
permissions: {}
jobs:
probe:
runs-on: ubuntu-24.04
outputs:
summary: ${{ steps.result.outputs.summary }}
steps:
- id: result
shell: bash
env:
CHANNEL: ${{ inputs.channel }}
PROBE_SECRET: ${{ secrets.probe_secret }}
run: |
set -euo pipefail
test -n "$PROBE_SECRET"
case "$CHANNEL" in strict|relaxed) ;; *) exit 21 ;; esac
printf 'summary=leaf-%s-secret-present
' "$CHANNEL" >> "$GITHUB_OUTPUT"
# .github/workflows/reusable-ci.yml
name: chapter16-reusable-ci
on:
workflow_call:
inputs:
runtime: {required: true, type: string}
strict: {required: false, default: true, type: boolean}
secrets:
demo_secret: {required: true}
outputs:
contract_result:
value: ${{ jobs.contract.outputs.result }}
leaf_summary:
value: ${{ jobs.leaf.outputs.leaf_summary }}
permissions:
contents: read
jobs:
contract:
runs-on: ubuntu-24.04
outputs:
result: ${{ steps.check.outputs.result }}
steps:
- id: check
shell: bash
env:
RUNTIME: ${{ inputs.runtime }}
STRICT: ${{ inputs.strict }}
DEMO_SECRET: ${{ secrets.demo_secret }}
run: |
set -euo pipefail
echo "repository=$GITHUB_REPOSITORY sha=$GITHUB_SHA"
echo "run=$GITHUB_RUN_ID attempt=$GITHUB_RUN_ATTEMPT"
echo "called_path=.github/workflows/reusable-ci.yml"
test -n "$DEMO_SECRET"
if [[ "$RUNTIME" != "3.13" ]]; then
echo "unsupported runtime: $RUNTIME; contract supports 3.13" >&2
exit 23
fi
printf 'result=runtime-%s-strict-%s
' "$RUNTIME" "$STRICT" >> "$GITHUB_OUTPUT"
leaf:
needs: contract
permissions: {}
uses: ./.github/workflows/reusable-leaf.yml
with:
channel: ${{ inputs.strict && 'strict' || 'relaxed' }}
secrets:
probe_secret: ${{ secrets.demo_secret }}
4. Caller A — supported baseline
# .github/workflows/caller-a.yml
name: chapter16-checkpoint-a
on: workflow_dispatch
permissions: {}
jobs:
ci:
permissions:
contents: read
uses: ./.github/workflows/reusable-ci.yml
with:
runtime: '3.13'
strict: true
secrets:
demo_secret: ${{ secrets.CH16_FAKE_SECRET }}
report:
needs: ci
permissions: {}
runs-on: ubuntu-24.04
steps:
- shell: bash
env:
CONTRACT_RESULT: ${{ needs.ci.outputs.contract_result }}
LEAF_SUMMARY: ${{ needs.ci.outputs.leaf_summary }}
run: |
echo "contract_result=$CONTRACT_RESULT"
echo "leaf_summary=$LEAF_SUMMARY"
Run Caller A first. Record its run ID, attempt and source SHA.
Expected safe outputs are runtime-3.13-strict-true and
leaf-strict-secret-present.
5. Caller B Revision A — deliberate interface failure
# .github/workflows/caller-b.yml — Revision A
name: chapter16-checkpoint-b
on: workflow_dispatch
permissions: {}
jobs:
ci:
permissions:
contents: read
uses: ./.github/workflows/reusable-ci.yml
with:
runtime: '3.12' # deliberate semantic incompatibility
strict: false
secrets:
demo_secret: ${{ secrets.CH16_FAKE_SECRET }}
report:
needs: ci
permissions: {}
runs-on: ubuntu-24.04
steps:
- run: echo "This should not run because ci failed."
Commit Revision A and run Caller B. Predict that GitHub can create
the run because 3.12 is a valid string, the central
contract job starts, verifies that the fake secret is
present, then exits 23 with
unsupported runtime: 3.12; contract supports 3.13. The
nested leaf and report jobs should not execute because their
dependencies did not succeed.
Preserve this run. Do not re-run it blindly and do not change permissions or the secret. Record the failure before repairing the caller.
6. Caller B Revision B — least destructive repair
# Change exactly one contract value
with:
runtime: '3.13'
strict: false
Commit only that input correction and run Caller B again as a new
workflow run. Expected safe outputs are
runtime-3.13-strict-false and
leaf-relaxed-secret-present. The central and leaf
workflow implementation did not need modification.
7. Required state predictions and independent verification
| Prediction | How to verify independently |
|---|---|
| Caller A and repaired B run at their recorded source SHAs | run summary plus logged GITHUB_SHA |
| Same-repository called workflows come from the same caller commit |
relative ./.github/workflows/… reference plus
committed file at that SHA
|
| Revision A reaches central contract but not leaf | job graph and central first-failure log |
| Secret crosses caller → central → leaf only through two mappings |
review both secrets: mappings; no secret value
in logs
|
| Leaf cannot elevate token permission | central leaf call has permissions: {} |
| Outputs are non-sensitive and caller-visible only after success | report log and needs.ci.outputs values |
| No external target changes | no packages, releases, environments, cloud or registry operations exist in workflow |
8. Evidence packet
| Evidence field | Record |
|---|---|
| Caller identity | workflow name/path for A and B |
| Run identity | Run A ID/attempt, broken B ID/attempt, repaired B ID/attempt |
| Source revision | exact GITHUB_SHA for every run |
| Called identity |
.github/workflows/reusable-ci.yml and
reusable-leaf.yml, same commit as caller
|
| Input contract | runtime string; strict boolean; observed values for each caller |
| Secret interface |
names only: CH16_FAKE_SECRET →
demo_secret → probe_secret
|
| Permission chain | caller default none → central call contents:read → leaf call none |
| Runner source |
GitHub-hosted ubuntu-24.04 in called jobs,
evaluated from caller context
|
| First failure | unsupported runtime 3.12, exit 23, leaf/report not run |
| Repair | only runtime input changed to 3.13 |
| Outputs | contract result plus leaf summary; no secret content |
| External state | explicitly none |
| Limitations | same-repository lab; central cross-repo production use should pin reviewed full commit SHA |
9. Verification checklist
-
All workflow files are under
.github/workflowsand callers invoke reusable workflows at the job level. - Caller A succeeds before the broken Caller B experiment.
- Broken Caller B preserves a real run, exact source SHA and first-failure message.
- The broken run does not execute the nested leaf or report job.
-
Repaired Caller B changes only
runtime: '3.12'to'3.13'. - Search all three run logs for the literal fake secret value; it is absent.
- Central and leaf outputs contain fixed non-sensitive labels only.
-
No workflow uses
secrets: inherit, write-all permissions, mutable external reusable refs or external deployment resources.
10. Cleanup and rollback
Delete the repository secret CH16_FAKE_SECRET, then
delete the throwaway repository when you have recorded the
non-sensitive evidence you need. There is no package, release,
artifact, cache, cloud resource, environment or runner registration
to delete.
If you accidentally used a real credential despite the lab instructions, deleting the workflow/repository is not sufficient remediation. Revoke or rotate the external credential at its authority source and review logs/audit evidence.
11. What Chapter 16 adds to the operating model
You can now treat reusable workflows as explicit, versioned automation contracts: caller identity and trust remain visible; inputs and outputs are typed interfaces; secrets cross only named boundaries; permissions do not escalate; runner access follows caller context; and nested call chains are evidence that can be reviewed and diagnosed. Chapter 17 moves one layer down to Composite Actions, JavaScript Actions, Docker Actions, and Custom Automation, where reuse happens inside individual job steps rather than by delegating whole job graphs.
Knowledge check
Why does the broken Caller B use 3.12 instead of an undeclared input name?
A valid string lets GitHub create a real run and enter the called workflow, producing first-failure runtime evidence. A schema/structural validation error may prevent a run from existing.
What proves least-privilege secret propagation in the checkpoint?
The explicit name mapping at both hops and the absence of the secret value from logs/outputs. The leaf receives only the secret deliberately forwarded by the middle workflow.
Why is no permission change part of the repair?
The demonstrated failure is an unsupported input value. Permission widening would not address the causal layer and would weaken the contract.
What identity should a cross-repository production reusable workflow add to this evidence packet?
The exact reviewed commit SHA of the central reusable workflow repository, ideally with its release/tag mapping.
What conceptual boundary changes in Chapter 17?
Reuse moves from whole workflow/job graphs to custom actions executed as steps inside a caller-owned job.
Official references and version notes
-
GitHub Docs — reuse workflows
—
workflow_call, typed inputs, secrets, outputs, nesting and matrix callers. - GitHub Docs — reusing workflow configurations — access rules, current limits, supported caller-job keywords, runner behavior, permissions and rerun semantics.
-
GitHub Docs — workflow syntax:
on.workflow_call— input, secret and workflow-output contract syntax. -
GitHub Docs —
jobs.<job_id>.uses— same-repository and cross-repository reusable-workflow references. - GitHub Docs — re-run workflows and jobs — run-attempt identity and rerun behavior.
- GitHub Docs — use secrets — secret availability and reusable-workflow propagation boundaries.
Version-sensitive behavior was rechecked on
2026-09-09 for GitHub.com. A reusable workflow is
called at the job level, not from a step. Current
GitHub.com limits allow up to
10 connected workflow levels and
50 unique reusable workflows in one top-level
workflow tree. Supported caller-job surfaces are
name, uses, with,
secrets, strategy, needs,
if, concurrency and
permissions. Nested
GITHUB_TOKEN permissions can stay the same or become
more restrictive, never more permissive. Secrets are passed only
to the directly called workflow unless forwarded again.
Workflow-level caller env values do not cross the
boundary automatically. Same-repository
./.github/workflows/file.yml calls use the same
commit as the caller; cross-repository production calls should use
a reviewed full commit SHA instead of a mutable branch or tag.
Re-running all jobs against a non-SHA ref resolves that ref again,
while re-running failed/specific jobs uses the called workflow
commit from the first attempt. The checkpoint intentionally uses
no external reusable workflow, so its exact called-workflow SHA is
the caller source SHA. This keeps mandatory learning free and
removes an external dependency while still teaching how a central
cross-repository contract should be pinned in production.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.