Chapter 16Lesson 05~250 minutes

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.

Checkpoint labContract repairTwo callersNested outputEvidence packet

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 only contents: read to 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/workflows and 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?

What proves least-privilege secret propagation in the checkpoint?

Why is no permission change part of the repair?

What identity should a cross-repository production reusable workflow add to this evidence packet?

What conceptual boundary changes in Chapter 17?

Next chapter concept

Package repeatable step logic safely

Chapter 17 compares composite, JavaScript and Docker actions and their execution/trust boundaries.

Official references and version notes

Version and compatibility note

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.